我们这次想要解决的问题是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函数我们只看相关的。

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

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;
    }
}
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;
        }
    }
}
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;
}
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

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

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;
}
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

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

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部分。

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相关的参数。

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);
}
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。

最后修改:2021 年 08 月 18 日
如果觉得我的文章对你有用,请随意赞赏