vSphere Client 里点开 vSAN → 虚拟对象(Virtual Objects),页面不给列表,只甩一句:

无法提取请求的数据。有关详细信息,请查看 vSphere Client 日志

按经验这通常意味着 vSAN 对象健康度出问题。但这次查下去发现集群健康全绿、没有对象降级、20 多台 VM 全部正常运行 —— 问题完全不在存储层。

真正的元凶是一个孤儿 iSCSI 目标,以及 vSAN UI 插件里一处没做判空的代码。

一、先排除”老套路”

这个报错在老环境里最常见的成因是:某台主机磁盘拥塞 → 触发组件重建 → 重建风暴 → Web UI 查询超时。

所以先按常规流程查一遍:

  • 集群健康状态(cluster status)
  • 各主机磁盘 / 对象健康度
  • 是否有降级对象、重建中对象

结果:全绿。 三台主机都是 green,没有任何对象降级、没有重建、没有磁盘故障。

那就说明:数据面是好的,界面是坏的。 这是纯 UI 渲染问题。

二、去 vSphere Client 日志里捞 JS 堆栈

既然前端渲染失败,就直接去看前端日志:

1
2
ls -lat /var/log/vmware/vsphere-ui/logs/
grep -iE 'vsan' /var/log/vmware/vsphere-ui/logs/*.log | grep -iE 'error|exception' | tail

一下就捞到了关键堆栈:

1
2
3
4
TypeError: Cannot read properties of undefined (reading 'vsanObjectUuid')
at Proxy.buildIscsiTargetModel (main.js)
at Proxy.buildIscsiTargets
→ Error: 无法提取请求的数据。有关详细信息,请查看 vSphere Client 日志

注意这条日志的价值:它直接把”vSAN 虚拟对象列表”和”iSCSI Target”这两件看起来无关的事情绑在了一起。

调用链是:

1
listVirtualObjects → listObjects → getVirtualObjects → buildIscsiTargets

三、扒插件源码,确认是判空缺失

vSphere Client 的 vSAN 插件是一个前端 bundle,可以直接从 vCenter 本机拉下来:

1
2
3
FQDN=$(hostname)
BASE="https://localhost/plugins/com.vmware.vsan.client~8.0.202.10000~843514146/$FQDN-443/vsan/plugins/vsan-ui-repa/main.js"
curl -sk -H "Host: $FQDN" -o /tmp/_main.js "$BASE"

再从报错信息给的精确字符偏移main.js:1:815462)附近截取代码:

1
2
3
d = open('/tmp/_main.js', encoding='utf-8', errors='replace').read()
off = 815462
print(d[max(0, off - 700): off + 700])

拿到崩溃点原文:

1
2
3
4
buildIscsiTargetModel(v, d, O, _) {
E = O.has(v.objectInformation.vsanObjectUuid) // ← v 是 iSCSI 目标
? O.get(...) : buildIscsiModel(v.objectInformation, _)
}

根因清楚了:插件假定每一个 iSCSI 目标都有 objectInformation。只要存在一个目标该字段为 undefined,整个”虚拟对象”列表的渲染就全线崩溃,前端只能显示那句含糊的错误。

而 vSAN 的 API 返回里,确实有一个目标不满足这个假设。

四、定位具体是哪个目标

用 vSAN API 列出所有 iSCSI 目标,逐个检查 objectInformation

1
2
3
4
5
6
7
8
9
vc_mos = vsanapiutils.GetVsanVcMos(vsanapiutils.GetVsanVcStub(si._stub, context=ctx), context=ctx)
iscsi = vc_mos['vsan-cluster-iscsi-target-system']

for t in iscsi.GetIscsiTargets(cluster):
oi = getattr(t, 'objectInformation', None)
print("alias=%-16s lunCount=%s objectInfo=%s ioOwner=%s" % (
t.alias, t.lunCount,
'None' if oi is None else oi.vsanObjectUuid,
t.ioOwnerHost))

结果一目了然:

目标 alias ioOwnerHost objectInformation
target-a host-1006 ✓ healthy
target-b host-1006 ✓ healthy
某个残留目标 None
target-c host-1006 ✓ healthy

就是它。 一个 I/O owner 为空、对象信息丢失的目标。

继续验证它是不是”孤儿”:

1
2
GetIscsiTarget(cluster, '<alias>')
→ vim.fault.VsanFault: Failed to get iSCSI target. Cannot find target '<alias>'

列表里能列出来,按名字去取却取不到 —— 连它归属的主机也不认这个目标。典型的状态残留孤儿

五、尝试删除:撞上死结

第一反应是把这个残留目标删掉。两条路都试了:

方式 结果
vCenter API RemoveIscsiTarget(cluster, alias) Util: Cannot find target namespace dir for target <uuid>
主机侧 esxcli vsan iscsi target remove -a <alias> 完全相同的错误

(补充一个前置细节:如果目标下还挂着 LUN,会先报 Target <alias> is not empty,需要先 RemoveIscsiLUN(cluster, alias, lunId) 把 LUN 删掉。)

再去主机侧看这个目标的真实状态:

1
esxcli vsan iscsi target list
1
2
3
4
5
Alias        ...  LUNs  Is Compliant  UUID                                  I/O Owner UUID
target-a ... 1 true 09389065-... 6569b0ac-...
target-b ... 0 true 55049465-... 6569b0ac-...
<问题目标> ... 0 false 08a3e768-... (空)
target-c ... 0 true 7c331369-... 6569b0ac-...

Is Compliant = false + I/O Owner UUID 为空 —— 问题目标连合规性都不满足了。

1
2
esxcli vsan iscsi target get -a <alias>
# → Failed to fetch target owner entry from CMMDS

CMMDS 里的 owner 记录丢了,目标的”命名空间目录”也不存在了。

于是形成死结:想删,删除流程第一步就要去找这个命名空间目录,找不到直接报错退出;想修,普通管理接口没有任何入口。

六、找到官方工具:vitRecoveryTool

翻主机上的 vSAN 工具目录时发现了这个:

1
2
3
4
5
ls /usr/lib/vmware/vsan/bin/
# ...
# vitRecoveryTool.pyc
# vitd
# vitsafehd

VIT = vSAN iSCSI Target。 vitRecoveryTool 正是 VMware 为”VIT 状态损坏”提供的官方恢复工具:

1
python /usr/lib/vmware/vsan/bin/vitRecoveryTool.pyc --help
1
2
3
4
5
6
7
8
9
10
11
usage: vitRecoveryTool.pyc [-h] [--force] [--dry-run] [--type TYPE] [--target TARGET]

--force USE WITH CAUTION!!! (for homeobject) even without the vit.conf backup,
the recovery still proceed(for target)
force recovery even if target namespace is accessible
--dry-run (for homeobject) Do not perform the recovery, but just generate
the vit.conf that will be used for recovery.
--type TYPE choose what you need to recover, currently we support
'homeobject' and 'target', default is 'homeobject'
--target TARGET the alias of target namespace that needs to be recover,
valid only when --type is set to 'target'

七、执行恢复(有关键前置条件)

跑起来后工具先给出一段警告,这是整个操作里最需要注意的地方

1
2
3
4
5
6
7
8
9
10
NOTE:
YOU SHOULD EXPLICITLY *STOP* VIT SERVICE ON ALL HOSTS IN THIS CLUSTER BY RUN
"/etc/init.d/vitd io_stop" ON EVERY SINGLE HOST IN THIS CLUSTER.
THIS WILL STOP VIT DATA SERVICE.

A successful execution of this script will:
1) Create new target namespace objects for corrupted targets
2) Modify vit.conf to link the related targets to new target namespace objects
3) Recover all healthy LUNs under these target namespaces.
After a successful run, make sure *START* vitd on all hosts in the cluster.

影响评估(动手前必须做)

停 VIT 服务会不会影响业务?逐项查证:

  • vSAN 数据存储不受影响。VIT(vSAN iSCSI Target)是独立服务,vsanDatastore 上的所有 VM 完全不受波及。
  • 受影响的只有 iSCSI LUN 的使用方(会短暂断连几分钟)。
  • 检查 iSCSI datastore 挂在哪台主机、上面跑着什么 VM —— 确认这些和 VIT 无关。

结论:可以安全执行,但要让所有相关方知晓那几分钟的 iSCSI 中断。

步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1) 集群内每台主机:停 VIT 数据服务
/etc/init.d/vitd io_stop
/etc/init.d/vitd status
# 期望:vitd is not running / vitsafehd is not running
# 输出里的 "vit unload successfully" 表示卸载成功

# 2) 在任意一台集群主机上执行恢复
printf 'y\ny\ny\n' | python /usr/lib/vmware/vsan/bin/vitRecoveryTool.pyc \
--type target --target <alias>

# 3) 集群内每台主机:立刻启动 VIT
/etc/init.d/vitd start
/etc/init.d/vitd status
# 期望:vitd is running / vitsafehd is running

⚠️ 必须喂 3 个 y —— 工具有两次 Continue(Y/y/N/n) 确认,第二次出现在全部检查通过之后。只喂一个会看到:

1
Continue(Y/y/N/n)[n]: Failed with: EOF when reading a line

这属于”检查已全过、就差确认”,重新跑一遍喂足就行,不会留下半成品状态

工具的先置检查清单

正常时应全部为 Y:

1
2
3
4
5
6
7
8
9
10
11
Check hostd is alive ................................. Y
Target has related config entry ...................... Y
vSAN is enabled ...................................... Y
Home Object is alive ................................. Y
Home Object is healthy ............................... Y
vit.conf is accessible ............................... Y
vit.conf has valid content ........................... Y
Check if specified target namespace is inaccessible .. Y
vitd is stopped ...................................... Y
vitsafehd is stopped ................................. Y
Checks OK, will proceed with target namespaces recovery

中间出现的 clomdb-Cdb_FindNonPreferredFaultDomainId: Failed to find Non-Preferred FD非拉伸集群的正常噪音,不是错误,忽略即可。

成功输出

1
2
3
4
5
6
7
8
Backup vit.conf ..........................................................Success
Remove old target from vit.conf ..........................................Success
Create a new target namespace ............................................Success
Restore vit.conf .........................................................Success
Create all missing vmdk descriptor files under the target ................Success
Replace old target uuid with new target uuid in vit.conf .................Success
Remove backup config file created by this script .........................Success
Target namespace recovery is done. Please run "/etc/init.d/vitd start" on all hosts.

八、验证

修复前后对比:

指标 修复前 修复后
Is Compliant ❌ false true
I/O Owner UUID ❌ 空 6569b0ac-...(与正常目标一致)
目标 UUID 旧(已丢失) 新的 uuid
objectInformation None ✅ 有值
GetIscsiTarget(cluster, alias) ❌ 报错找不到 ✅ 正常返回

objectInformation 不再是 None → UI 插件的 buildIscsiTargetModel 不再崩溃 → “虚拟对象”页面恢复正常。

其他完整性检查:

  • 所有 VM 照常运行,数量与操作前一致
  • 两台主机 vitd + vitsafehd 均已恢复运行
  • vSAN 集群全绿
  • 其余 iSCSI 目标未受影响,原有 LUN 保持 Online

小提示:修完后浏览器里的 vSphere Client 建议 Ctrl+F5 硬刷新(插件 JS 有缓存)。

九、经验总结

1. 报错信息虽然含糊,但日志里的 JS 堆栈是金子

那句”无法提取请求的数据”本身毫无信息量,但 vSphere Client 日志里的 TypeError 把问题精确定位到了具体函数遇到前端报错,第一件事是去看前端日志,而不是从底层往上猜。

2. 反直觉的信号要当线索用

“vSAN 虚拟对象页面坏了” → 直觉是存储层问题 → 但集群健康是全绿的。这个矛盾点恰恰是最有价值的线索:它告诉你问题在渲染层,不在数据层。

3. 扒源码定位判空 bug,比读一百篇文档快

从报错给的字符偏移直接定位到那一行 O.has(v.objectInformation.vsanObjectUuid),几分钟就确认了根因。前端 bundle 就在 vCenter 本机,可以直接 curl 下来分析。

4. 官方工具常常藏在 /usr/lib/vmware/<组件>/bin/

vitRecoveryTool.pyc 不是常规管理命令,esxcli 里也没有。它就在工具目录里躺着。遇到”官方 API 走不通”的死结,先去翻各组件自带的工具目录。

5. “删不掉”不等于”修不好”

原目标是想删掉这个残留记录,但删除路径依赖的命名空间目录已不存在,形成死结。官方工具的解法不是”强行删除”,而是把命名空间重建出来 —— 目标恢复合规后,UI 恢复正常;此时若还想删,也能正常删掉了。

修复优先于删除。

6. 破坏性操作前把影响面查清楚

停 VIT 服务前,逐项确认了:vSAN 数据存储不受影响(独立服务)、受影响的只有 iSCSI LUN 使用方、iSCSI datastore 与服务无关。把这些查清楚了,才敢动手停服务。

7. 顺手记下几个 API 参数坑

  • GetIscsiLUNs(cluster, targetAliases) 第二个参数是字符串列表 ['alias'],传单个对象会报 not iterable,传列表里的对象会报 expected type str
  • 删除目标前必须先删 LUN,否则报 Target <alias> is not empty
  • RemoveIscsiTarget / RemoveIscsiLUN 返回的是 Task,需要等待任务完成

十、一句话复盘

存储全绿、VM 全好、界面报错”无法提取数据” —— 这种矛盾组合大概率是前端插件被某条异常数据噎住了。这次是一条损坏的 vSAN iSCSI 目标记录,让一个没做判空的 JS 函数掀翻了整个页面。定位靠日志堆栈,修复靠官方工具,前提是把影响面查清楚再动手。

⬆︎TOP