事情起因是我的qemu虚拟机里新建了一个SPDK的vhost-user-blk块设备,qemu虚拟机启动时的设备参数如下:
-chardev socket,id=chr-vu-virtio-disk1,path=/var/tmp/chenditest1
-device vhost-user-blk-pci,num-queues=4,bus=pci.0,addr=0x5,chardev=chr-vu-virtio-disk1,id=virtio-disk1
然后挂载、读写设备均正常。但是在我重启了SPDK之后,再次读写这个设备,却发现虚拟机IO直接卡死了。。。
问题不是出在SPDK就是出在QEMU,于是思路分为两步:
一、排查SPDK重启之后的代码行为逻辑
首先想到的是SPDK有问题,重启之后不会重新注册设备,于是查看代码,有下面这一段
int
vhost_dev_register(struct spdk_vhost_dev *vdev, const char *name, const char *mask_str,
const struct spdk_vhost_dev_backend *backend)
{
...
if (vhost_register_unix_socket(path, name, vdev->virtio_features, vdev->disabled_features,
vdev->protocol_features)) {
spdk_thread_send_msg(vdev->thread, vhost_dev_thread_exit, NULL);
rc = -EIO;
goto out;
}
...
}
可以看到注册时的关键函数是vhost_register_unix_socket
int
vhost_register_unix_socket(const char *path, const char *ctrl_name,
uint64_t virtio_features, uint64_t disabled_features, uint64_t protocol_features)
{
struct stat file_stat;
uint64_t features = 0;
/* Register vhost driver to handle vhost messages. */
if (stat(path, &file_stat) != -1) {
if (!S_ISSOCK(file_stat.st_mode)) {
SPDK_ERRLOG("Cannot create a domain socket at path "%s": "
"The file already exists and is not a socket.
",
path);
return -EIO;
} else if (unlink(path) != 0) {
SPDK_ERRLOG("Cannot create a domain socket at path "%s": "
"The socket already exists and failed to unlink.
",
path);
return -EIO;
}
}
#if RTE_VERSION < RTE_VERSION_NUM(20, 8, 0, 0)
if (rte_vhost_driver_register(path, 0) != 0) {
#else
if (rte_vhost_driver_register(path, RTE_VHOST_USER_ASYNC_COPY) != 0) {
#endif
SPDK_ERRLOG("Could not register controller %s with vhost library
", ctrl_name);
SPDK_ERRLOG("Check if domain socket %s already exists
", path);
return -EIO;
}
if (rte_vhost_driver_set_features(path, virtio_features) ||
rte_vhost_driver_disable_features(path, disabled_features)) {
SPDK_ERRLOG("Couldn't set vhost features for controller %s
", ctrl_name);
rte_vhost_driver_unregister(path);
return -EIO;
}
if (rte_vhost_driver_callback_register(path, &g_spdk_vhost_ops) != 0) {
rte_vhost_driver_unregister(path);
SPDK_ERRLOG("Couldn't register callbacks for controller %s
", ctrl_name);
return -EIO;
}
rte_vhost_driver_get_protocol_features(path, &features);
features |= protocol_features;
rte_vhost_driver_set_protocol_features(path, features);
if (rte_vhost_driver_start(path) != 0) {
SPDK_ERRLOG("Failed to start vhost driver for controller %s (%d): %s
",
ctrl_name, errno, spdk_strerror(errno));
rte_vhost_driver_unregister(path);
return -EIO;
}
return 0;
}
这个函数负责在指定的 Unix 域 socket 路径上注册一个 vhost 用户端驱动,并完成相关初始化。代码分为几个阶段:检查并清理已存在的 socket 文件、注册驱动并设置特性、注册回调、设置协议特性、启动驱动。
从代码里可以看到最后会调用 启动 vhost 驱动。这会在后台创建一个线程(
rte_vhost_driver_start(path)),并绑定到刚才创建的 socket 上,执行
fdset_event_dispatch、
bind 等操作。在监听线程启动后,SPDK 开始接收前端(QEMU)发起的连接。当
listen 返回非 0 时,说明启动失败,代码记录错误信息并注销驱动。否则函数正常返回 0,整个注册流程完成。
rte_vhost_driver_start
但是我们查看SPDK日志没有输出任何连接失败的信息,说明问题不出在SPDK这一端。
二、排查QEMU在SPDK重启之后的代码行为逻辑
接下来就是要排查QEMU是否出现了问题。首先从日志里看,可以看到如下的日志:
2025-11-12T11:18:10.830569Z qemu-system-x86_64: Unexpected end-of-file before all data were read
2025-11-12T11:18:10.952559Z qemu-system-x86_64: Failed to set msg fds.
2025-11-12T11:18:10.952572Z qemu-system-x86_64: Failed to set msg fds.
2025-11-12T11:18:10.952580Z qemu-system-x86_64: vhost VQ 0 ring restore failed: -22: Invalid argument (22)
2025-11-12T11:18:10.952599Z qemu-system-x86_64: Failed to set msg fds.
2025-11-12T11:18:10.952603Z qemu-system-x86_64: vhost VQ 1 ring restore failed: -22: Invalid argument (22)
2025-11-12T11:18:10.952612Z qemu-system-x86_64: Failed to set msg fds.
2025-11-12T11:18:10.952616Z qemu-system-x86_64: vhost VQ 2 ring restore failed: -22: Invalid argument (22)
2025-11-12T11:18:10.952624Z qemu-system-x86_64: Failed to set msg fds.
2025-11-12T11:18:10.952628Z qemu-system-x86_64: vhost VQ 3 ring restore failed: -22: Invalid argument (22)
从报错信息 及后续的
Unexpected end-of-file before all data were read 等,表明 QEMU 的 vhost-user 通信通道在 SPDK 后端重启时意外断开。其根本原因并非 SPDK 本身,而是 QEMU 端在默认情况下缺乏自动重连逻辑。默认 QEMU vhost-user 设备(如
ring restore failed)只会尝试连接后端一次(或有限次数重试),如果连接失败或后端崩溃,则报错退出。
vhost-user-blk-pci
以前版本的 QEMU 只在特定错误(如 协议错误)时重试,而非对所有断连情况都自动重连,导致在 SPDK 重启后 QEMU 无法恢复连接,从而出现 EOF 和 ring 恢复失败。
-EPROTO
根据 DPDK/SPDK 文档,SPDK vhost-user 后端通常工作在客户端模式,即 SPDK 作为 vhost-client 主动连接 QEMU 的监听 socket。在这种模式下,当 SPDK 进程重启时,它会再次尝试与 QEMU 建立连接。然而 QEMU 端需要支持相应的重连机制(QEMU ≥2.7 版本以上支持基础重连特性)。如果未配置 选项或重连失败,QEMU 会报告底层通道断开(如 “Unexpected end-of-file”),并尝试恢复 vhost 状态时因 fd 无效而出错。
reconnect
重连机制:QEMU 的字符设备()支持
-chardev socket 参数来自动重连。当设置了
reconnect(或
reconnect=1)时,非 server 模式的 socket 在远端断开后会每隔指定时间重试连接。例如 DPDK 文档中说明
reconnect=on 可实现“qemu can reconnect … after we restart vhost_blk example” (dpdk参考资料)
-chardev socket,...,reconnect=1
QEMU vhost-user-blk 实现:在 中,
hw/block/vhost-user-blk.c 函数通过
vhost_user_blk_device_realize() 等调用与后端建立初始连接。其后续版本中,此连接步骤被放入循环重试:现代 QEMU 代码将重试条件改为“任意负值错误”,而旧版只针对
qemu_chr_fe_wait_connected()。这意味着如果设置
-EPROTO,QEMU 将循环调用连接逻辑,直到成功或超过重试次数:
reconnect
do {
ret = vhost_user_blk_realize_connect(s, errp);
} while (ret < 0 && retries--); // 新版代码:任意错误均重连:contentReference[oaicite:10]{index=10}
其中 会调用
vhost_user_blk_realize_connect() 等函数完成 socket 连接和 vhost 初始化。若后端关闭,
qemu_chr_fe_wait_connected(&s->chardev, ...) 在
qemu_chr_fe_wait_connected 参数控制下会等待并重连,否则返回错误。
reconnect
综上,添加 参数实质上是启用了 QEMU 的自动重连逻辑。这使得当 SPDK vhost-user 后端重启并重新创建 socket 时,QEMU 会不断尝试重新打开通道并恢复 vhost 设备状态,从而避免了断开导致的 VM 错误。底层来看,QEMU 的字符设备驱动使用定时器在连接断开时再次调用
reconnect=on(类似
qemu_chr_open)来实现此功能。因此,问题出在 QEMU 端缺省不重连(或重连条件过窄),而非 SPDK 数据路径。当将
qemu_chr_socket_restart_timer 打开后,QEMU 即可自动回连,成功解决了 “Unexpected end-of-file” 等错误。
reconnect=on
QEMU vhost-user-blk 在 device_realize 中使用循环尝试连接后端(),新版 QEMU 改为对所有错误重试。同时,QEMU 的
vhost_user_blk_realize_connect 选项支持
-chardev socket(毫秒级重连超时)设置,确保在连接丢失时自动尝试恢复。如下示例展示了相关代码逻辑:
reconnect
// QEMU vhost-user-blk 设备 realize 阶段尝试建立 vhost-user 连接
do {
ret = vhost_user_blk_realize_connect(s, errp);
} while (ret < 0 && retries--); // 负值错误时重连:contentReference[oaicite:18]{index=18}
// vhost_user_blk_realize_connect 会调用:
ret = qemu_chr_fe_wait_connected(&s->chardev, errp); // 等待/建立 socket 连接
if (ret < 0) return ret;
ret = vhost_user_blk_connect(dev, errp); // vhost 初始化 (set_owner、set_features、queue 等)
以上逻辑借助 设置可在断连时自动重试。
reconnect
三、解决方案是从QEME端参数入手
需要在虚拟机启动参数中加入reconnect参数。例如
-chardev socket,id=chr-vu-virtio-disk1,path=/var/tmp/chenditest1,reconnect=10
如果你是用的libvirt工具创建的虚拟机,那么在xml文件中加入如下参数:
...
<disk type='vhostuser' device='disk'>
<driver name='qemu' type='raw'/>
<source type='unix' path='/tmp/vhost-blk.sock'>
<reconnect enabled='yes' timeout='10'/>
</source>
<target dev='vdf' bus='virtio'/>
</disk>
...
修改完重新启动虚拟机,并且重启SPDK之后,会发现SPDK日志里有重新握手的日志:
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: new vhost user connection is 58
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: new device, handle is 0
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_GET_FEATURES
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_GET_PROTOCOL_FEATURES
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_PROTOCOL_FEATURES
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: negotiated Vhost-user protocol features: 0x1ebf
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_GET_QUEUE_NUM
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_SLAVE_REQ_FD
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_OWNER
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_GET_FEATURES
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_VRING_CALL
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: vring call idx:0 file:60
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_VRING_CALL
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: vring call idx:1 file:61
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_VRING_CALL
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: vring call idx:2 file:62
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_VRING_CALL
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: vring call idx:3 file:63
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_FEATURES
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: negotiated Virtio features: 0x110005446
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_INFLIGHT_FD
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: set_inflight_fd mmap_size: 8448
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: set_inflight_fd mmap_offset: 0
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: set_inflight_fd num_queues: 4
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: set_inflight_fd queue_size: 128
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: set_inflight_fd fd: 64
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: set_inflight_fd pervq_inflight_size: 2112
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_FEATURES
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: negotiated Virtio features: 0x110005446
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_MEM_TABLE
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: guest memory region 0, size: 0xc0000000#012#011 guest physical addr: 0x0#012#011 guest virtual addr: 0x7ffee7fff000#012#011 host virtual addr: 0x7fff10000000#012#011 mmap addr : 0x7fff10000000#012#011 mmap size : 0xc0000000#012#011 mmap align: 0x1000#012#011 mmap off : 0x0
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: guest memory region 1, size: 0x40000000#012#011 guest physical addr: 0x100000000#012#011 guest virtual addr: 0x7fffa7fff000#012#011 host virtual addr: 0x7ffed0000000#012#011 mmap addr : 0x7ffe10000000#012#011 mmap size : 0x100000000#012#011 mmap align: 0x1000#012#011 mmap off : 0xc0000000
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: guest memory region 2, size: 0x100000000#012#011 guest physical addr: 0x140000000#012#011 guest virtual addr: 0x7ffde7ffe000#012#011 host virtual addr: 0x7ffd10000000#012#011 mmap addr : 0x7ffd10000000#012#011 mmap size : 0x100000000#012#011 mmap align: 0x1000#012#011 mmap off : 0x0
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_VRING_NUM
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_VRING_BASE
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_VRING_ADDR
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: read message VHOST_USER_SET_VRING_KICK
Nov 12 19:18:20 kec-6 spdk[371482]: VHOST_CONFIG: vring kick idx:0 file:68
这个时候我们再去测试写这个块设备,发现卡IO的问题就没有了。
(完)













暂无评论内容