赵占旭的博客

QEMU vhostuser重连

我们这次想要解决的问题是ovs-dpdk的vhost-user和qemu虚拟机重连的问题。

问题描述


qemu version: 2.7.1
ovs-dpdk重启后,qemu虚拟机网络不通。

ovs为什么不存在这个问题

我们都知道ovs的vhost其实真正起作用的是vhost_worker线程,都是发包方去触发该线程,该线程再做报文处理,去调用ovs的kernel端的datapath,所以这个时候ovs如果挂掉,再恢复是不影响ovs处理报文的。

ovs-dpdk问题原因

当使用ovs-dpdk的时候,和qemu虚拟机连接使用的是vhost-user,采用的是unix socket domain的连接收发报文,默认是ovs-dpdk作为server端,而qemu作为client端,这个时候ovs-dpdk如果崩溃掉,那么连接就会出问题,ovs-dpdk即使恢复也不能和虚拟机正常通信了。

解决方案


下面先说考虑的几个方案:

  • 让qemu做server端,ovs-dpdk做client端,重连由ovs-dpdk来做,这个已经验证可以实施,只是需要注意socket文件需要指定,默认是空的。经过验证,qemu2.7和2.3都支持。
  • server和client保持不变,但是需要qemu默认支持reconnect,目前的情况是qemu默认是关闭重连的,需要修改代码,修改量比较小。
  • server和client保持不变,但是需要qemu支持设置reconnect,而libvirt也需要xml支持设置reconnect,这个方案不进需要qemu修改部分代码,而且libvirt源码也要进行修改。

方案的优劣


Thinking …

qemu相关源码


main函数位于vl.c文件中,因为我们主要关注点在qemu连接ovs的vhost-user,所以main函数我们只看相关的。

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char **argv, char **envp)
{
if (qemu_opts_foreach(qemu_find_opts("chardev"),
chardev_init_func, NULL, NULL)) {
exit(1);
}
if (net_init_clients() < 0) {
exit(1);
}
main_loop();
}

chardev_init_func-->qemu_chr_new_from_opts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CharDriverState *qemu_chr_new_from_opts(QemuOpts *opts,
void (*init)(struct CharDriverState *s),
Error **errp)
{
if (cd->parse) {
//调用qemu_chr_parse_socket
//主要是解析vhost-user的一些参数
cd->parse(opts, backend, &local_err);
if (local_err) {
error_propagate(errp, local_err);
goto qapi_out;
}
}

//创建vhost-user连接
ret = qmp_chardev_add(bid ? bid : id, backend, errp);
if (!ret) {
goto qapi_out;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ChardevReturn *qmp_chardev_add(const char *id, ChardevBackend *backend,
Error **errp)
{
for (i = backends; i; i = i->next) {
cd = i->data;

if (cd->kind == backend->type) {
//调用qmp_chardev_open_socket创建连接
chr = cd->create(id, backend, ret, &local_err);
if (local_err) {
error_propagate(errp, local_err);
goto out_error;
}
break;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void qemu_chr_parse_socket(QemuOpts *opts, ChardevBackend *backend,
Error **errp)
{
//检测是否将qemu作为server端,默认是不支持
bool is_listen = qemu_opt_get_bool(opts, "server", false);
//默认是wait
bool is_waitconnect = is_listen && qemu_opt_get_bool(opts, "wait", true);
//判断是否支持reconnect,默认不支持,简单修改的话此处修改0即可
int64_t reconnect = qemu_opt_get_number(opts, "reconnect", 0);
//unix socket domain path
const char *path = qemu_opt_get(opts, "path");

//下面的函数会用到
sock->has_server = true;
sock->server = is_listen;
sock->has_wait = true;
sock->wait = is_waitconnect;
sock->has_reconnect = true;
sock->reconnect = reconnect;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static CharDriverState *qmp_chardev_open_socket(const char *id,
ChardevBackend *backend,
ChardevReturn *ret,
Error **errp)
{
//鉴于之前函数写死了这几项为true,所以这几块都是看上面的解析
bool is_listen = sock->has_server ? sock->server : true;
bool is_waitconnect = sock->has_wait ? sock->wait : false;
int64_t reconnect = sock->has_reconnect ? sock->reconnect : 0;

s->is_listen = is_listen;

//将一些函数注册
chr->opaque = s;
chr->chr_wait_connected = tcp_chr_wait_connected;
chr->chr_write = tcp_chr_write;
chr->chr_sync_read = tcp_chr_sync_read;
chr->chr_close = tcp_chr_close;
chr->chr_disconnect = tcp_chr_disconnect;
chr->get_msgfds = tcp_get_msgfds;
chr->set_msgfds = tcp_set_msgfds;
chr->chr_add_client = tcp_chr_add_client;
chr->chr_add_watch = tcp_chr_add_watch;
chr->chr_update_read_handler = tcp_chr_update_read_handler;
/* be isn't opened until we get a connection */
chr->explicit_be_open = true;


//如果是server模式即不操作,client模式判定是否设置了reconnect,
//如果设置了则赋值
if (is_listen) {
} else if (reconnect > 0) {
s->reconnect_time = reconnect;
}

//支持reconnect,需要做一些事情
if (s->reconnect_time) {
sioc = qio_channel_socket_new();
//创建一个线程,运行一个routine,运行connect连接server的工作,
qio_channel_socket_connect_async(sioc, s->addr,
qemu_chr_socket_connected,
chr, NULL);
} else {
//不支持reconnect的时候,如果是server端,则进行如下操作
if (s->is_listen) {
sioc = qio_channel_socket_new();
//调用listen
if (qio_channel_socket_listen_sync(sioc, s->addr, errp) < 0) {
goto error;
}
s->listen_ioc = sioc;
//从上面可以看到调用的是tcp_chr_wait_connected,
//此处是server端,调用accept,如果是client,调用connect
if (is_waitconnect &&
qemu_chr_wait_connected(chr, errp) < 0) {
goto error;
}
if (!s->ioc) {
s->listen_tag = qio_channel_add_watch(
QIO_CHANNEL(s->listen_ioc), G_IO_IN,
tcp_chr_accept, chr, NULL);
}
}
//此处和上面相反,是client,所以调用connect
else if (qemu_chr_wait_connected(chr, errp) < 0) {
goto error;
}
}

return chr;
}

net_init_clients-->net_init_netdev-->net_client_init-->net_client_init1-->net_client_init_fun[]-->net_init_vhost_user

1
2
3
4
5
6
7
8
9
10
11
int net_init_vhost_user(const Netdev *netdev, const char *name,
NetClientState *peer, Error **errp)
{
//解析vhost-user的信息
chr = net_vhost_parse_chardev(vhost_user_opts, errp);
if (!chr) {
return -1;
}

return net_vhost_user_init(peer, "vhost_user", name, chr, queues);
}

net_vhost_parse_chardev-->net_vhost_chardev_opts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int net_vhost_chardev_opts(void *opaque,
const char *name, const char *value,
Error **errp)
{
//此处没有什么要说的,主要是不支持reconnect,如果要支持需要添加一行代码
//只要类似server的行,并不需要做什么操作,前面已经做了,
//这里只是不写的话会进else报错
if (strcmp(name, "backend") == 0 && strcmp(value, "socket") == 0) {
props->is_socket = true;
} else if (strcmp(name, "path") == 0) {
props->is_unix = true;
} else if (strcmp(name, "server") == 0) {
} else {
error_setg(errp,
"vhost-user does not support a chardev with option %s=%s",
name, value);
return -1;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static int net_vhost_user_init(NetClientState *peer, const char *device,
const char *name, CharDriverState *chr,
int queues)
{
NetClientState *nc, *nc0 = NULL;
VhostUserState *s;
int i;

for (i = 0; i < queues; i++) {
//初始化队列
nc = qemu_new_net_client(&net_vhost_user_info, peer, device, name);
if (!nc0) {
nc0 = nc;
}

snprintf(nc->info_str, sizeof(nc->info_str), "vhost-user%d to %s",
i, chr->label);

nc->queue_index = i;

s = DO_UPCAST(VhostUserState, nc, nc);
s->chr = chr;
}

s = DO_UPCAST(VhostUserState, nc, nc0);
do {
Error *err = NULL;
//当开启reconnect的时候这里才会继续调用,这块需要再看看
if (qemu_chr_wait_connected(chr, &err) < 0) {
error_report_err(err);
return -1;
}
//添加event事件,这块最终会添加一个端口hung up时的调用qio_channel_fd_source_dispatch
//如果前面设置了reonnect的时候,后面最终会调用到qemu_chr_socket_restart_timer最终重连
qemu_chr_add_handlers(chr, NULL, NULL,
net_vhost_user_event, nc0->name);
} while (!s->started);

assert(s->vhost_net);

return 0;
}

以下就是端口hung up的时候的调用流程:

main_loop-->main_loop_wait-->os_host_main_loop_wait-->glib_pollfds_poll-->g_main_context_dispatch-->qio_channel_fd_source_dispatch-->net_vhost_user_watch-->qemu_chr_disconnect-->tcp_chr_disconnect

1
2
3
4
5
6
7
8
9
static void tcp_chr_disconnect(CharDriverState *chr)
{
//首先调用closed关闭、清除一些信息
qemu_chr_be_event(chr, CHR_EVENT_CLOSED);
if (s->reconnect_time) {
//当设置了重连之后,会调用该接口进行重连
qemu_chr_socket_restart_timer(chr);
}
}

qemu_chr_be_event-->net_vhost_user_event

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void net_vhost_user_event(void *opaque, int event)
{
switch (event) {
//创建设备信息,一开始和后面重连之后会调用
case CHR_EVENT_OPENED:
s->watch = qemu_chr_fe_add_watch(s->chr, G_IO_HUP,
net_vhost_user_watch, s);
if (vhost_user_start(queues, ncs) < 0) {
qemu_chr_disconnect(s->chr);
return;
}
qmp_set_link(name, true, &err);
s->started = true;
break;
//设备hung up之后会调用
case CHR_EVENT_CLOSED:
qmp_set_link(name, false, &err);
vhost_user_stop(queues, ncs);
g_source_remove(s->watch);
s->watch = 0;
break;
}
}

下面的流程是当上面重连了之后,会在空闲的时候调用net_vhost_user_eventCHR_EVENT_OPENED,最终可以实现通信了。

main_loop-->main_loop_wait-->os_host_main_loop_wait-->glib_pollfds_poll-->g_main_context_dispatch-->gio_task_thread_result-->qio_task_complete-->qemu_chr_socket_connected-->tcp_chr_new_client-->tcp_chr_connect-->qemu_chr_be_generic_open-->qemu_chr_be_event-->net_vhost_user_event

libvirt相关源码


我们目前只关心virsh define xxx.xml命令相关的一些代码。因为这里会解析xml文件,目前只支持server和client两种模式,并不支持reconnect。对应的接口为cmdDefine

cmdDefine-->virDomainDefineXML-->conn->driver->domainDefineXML-->qemuDomainDefineXML-->qemuDomainDefineXMLFlags-->virDomainDefParseString-->virDomainDefParse-->virDomainDefParseNode-->virDomainDefParseXML-->virDomainNetDefParseXML

virDomainNetDefParseXML主要负责网络模块的解析,我们只关注的是vhostuser部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static virDomainNetDefPtr
virDomainNetDefParseXML(virDomainXMLOptionPtr xmlopt,
xmlNodePtr node,
xmlXPathContextPtr ctxt,
virHashTablePtr bootHash,
char *prefix,
unsigned int flags)
{
while (cur != NULL) {
if (cur->type == XML_ELEMENT_NODE) {
if (!vhostuser_path && !vhostuser_mode && !vhostuser_type
&& def->type == VIR_DOMAIN_NET_TYPE_VHOSTUSER &&
xmlStrEqual(cur->name, BAD_CAST "source")) {
//获取type path mode等信息
vhostuser_type = virXMLPropString(cur, "type");
vhostuser_path = virXMLPropString(cur, "path");
vhostuser_mode = virXMLPropString(cur, "mode");
}
}
}
switch (def->type) {
case VIR_DOMAIN_NET_TYPE_VHOSTUSER:
def->data.vhostuser->type = VIR_DOMAIN_CHR_TYPE_UNIX;
def->data.vhostuser->data.nix.path = vhostuser_path;
vhostuser_path = NULL;

//判断是server还是client模式,后面会用到
//这里只支持server和client模式,其他的会报错
//所以我们想要支持reconnect的话需要在这里进行修改
if (STREQ(vhostuser_mode, "server")) {
def->data.vhostuser->data.nix.listen = true;
} else if (STREQ(vhostuser_mode, "client")) {
def->data.vhostuser->data.nix.listen = false;
} else {
virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s",
_("Wrong <source> 'mode' attribute "
"specified with <interface "
"type='vhostuser'/>"));
goto error;
}
break;
}
}

记下来看一下virsh start xxx是怎么将xml数据转换成qemu的命令行的,如果上面添加支持reconnect的话,这块的参数也要相应的添加。对应的接口为cmdStart

cmdStart-->virDomainCreate-->conn->driver->domainCreate-->qemuDomainCreate-->qemuDomainCreateWithFlags-->qemuDomainCreateWithFlags-->qemuDomainObjStart-->qemuProcessStart-->qemuProcessLaunch-->qemuBuildCommandLine-->qemuBuildNetCommandLine-->qemuBuildInterfaceCommandLine-->qemuBuildVhostuserCommandLine

qemuBuildVhostuserCommandLine主要是生成vhostuser相关的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int
qemuBuildVhostuserCommandLine(virQEMUDriverPtr driver,
virLogManagerPtr logManager,
virCommandPtr cmd,
virDomainDefPtr def,
virDomainNetDefPtr net,
virQEMUCapsPtr qemuCaps,
unsigned int bootindex)
{
switch ((virDomainChrType) net->data.vhostuser->type) {
case VIR_DOMAIN_CHR_TYPE_UNIX:
//解析chardev部分
if (!(chardev = qemuBuildChrChardevStr(logManager, cmd, cfg, def,
net->data.vhostuser,
net->info.alias, qemuCaps, false)))
goto error;
break;
}

//解析netdev部分
if (!(netdev = qemuBuildHostNetStr(net, driver,
',', -1,
NULL, 0, NULL, 0)))
goto error;

virCommandAddArg(cmd, "-chardev");
//添加chardev参数
virCommandAddArg(cmd, chardev);
VIR_FREE(chardev);

virCommandAddArg(cmd, "-netdev");
//添加netdev部分参数
virCommandAddArg(cmd, netdev);
VIR_FREE(netdev);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static char *
qemuBuildChrChardevStr(virLogManagerPtr logManager,
virCommandPtr cmd,
virQEMUDriverConfigPtr cfg,
const virDomainDef *def,
const virDomainChrSourceDef *dev,
const char *alias,
virQEMUCapsPtr qemuCaps,
bool nowait)
{
switch (dev->type) {
case VIR_DOMAIN_CHR_TYPE_UNIX:
virBufferAsprintf(&buf, "socket,id=%s,path=", charAlias);
virQEMUBuildBufferEscapeComma(&buf, dev->data.nix.path);
//此处是server的时候的添加
//我们可以在这里进行reconnect的判定并且添加
if (dev->data.nix.listen)
virBufferAdd(&buf, nowait ? ",server,nowait" : ",server", -1);
break;
}
}

之前方案


  • qemu做client端,xml中为<source type='unix' path='xxx' mode='client'/>。最终libvirt会将xml翻译成qemu的参数,作为命令行启动qemu。
  • 如果不看xml,直接看qemu参数,-chardev socket,id=charnet0,path=xxx
  • ovs做client端,创建的vhost-user端口方式为ovs-vsctl add-port br-int vhostuser0 -- set Interface vhostuser0 type=dpdkvhostuser

方案1实施


  • 让qemu做server端,此时xml中的mode需要改成server,另外就是path不能配置到/var/run/openvswitch路径中,因为ovs挂掉的时候该路径也会删除,所以会出问题。
  • 如果不改xml,就是qemu参数,最后需要添加,server来告知qemu这个需要建立server端的socket连接。路径问题同上。
  • ovs做client端,需要将创建端口的命令改成ovs-vsctl add-port br-int vhostuser0 -- set Interface vhostuser0 type=dpdkvhostuserclient options:vhost-server-path="/tmp/vhostuser0",主要区别在于type和添加一个path,因为ovs做client默认是空的路径,所以必须指定,还要和上面qemu的路径一致。

方案2实施


相对于之前的方案,只需要修改qemu的代码,让vhostuser默认支持reconnect即可,修改代码如下:
文件qemu-char.c的函数qemu_chr_parse_socket,将int64_t reconnect = qemu_opt_get_number(opts, "reconnect", 0);修改为int64_t reconnect = qemu_opt_get_number(opts, "reconnect", 1);即可。

注意:此方案可能会对server模式有影响,只要我们确定不用qemu作为server,那就不会产生影响。

方案3实施


相对于之前的方案,我们需要分别修改libvirt和qemu,让他们都支持reconnect。
这里需要改动的地方有一些多,晚些时候再以patch更新或者直接采用方案1或者2。

注意:所有文章非特别说明皆为原创。为保证信息与源同步,转载时请务必注明文章出处!谢谢合作 :-)

原始链接:http://zhaozhanxu.com/2017/02/16/QEMU/2017-02-16-qemu-reconnect/

许可协议: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。