编写collectd插件调用libguestfs采集虚拟机disk信息遇到的问题
问题描述:
在collectd中编写新插件vm_disk,并在插件初始化时通过 plugin_register_read() 函数注册数据读取函数vm_disk_read。vm_disk_read在调用libguestfs相关函数读取虚拟机disk使用量信息时报错。比较奇怪的是,即便将调用libguestfs的代码抽取出来作为一个单独的程序。在vm_disk_read通过popen()调用这个demo程序,还是会发生错误。或是直接执行popen("virt-df domain")等类似的操作,依旧报错。
解决方案版本1:
在vm_disk插件注册时,创建一个子进程(记为C)来调用libguestfs进行数据采集,父进程P(记为P)中执行collectd插件vm_disk注册的的数据读取函数vm_disk_read。同时通过共享内存实现进行C与进程P之间的通信。流程如下:
module_register
↓
start_collect_work
↓
fork → collectd轮询回调数据采集进程 ← 1.锁请求 2.从共享存储区域拉取数据 3.锁释放
↓ ↑(拉取)
虚拟机device使用量信息采集进程 → 1.锁请求 2.数据提交至共享存储区域 3.锁释放 → share memory
该方案暂时达到了采集数据的目的,但是有很多问题:需要大量的并发控制、涉及到共享内存,且使用ps -ef | grep "collectd"查询时,可以看到有两个collect进程,十分丑陋。
分析错误日志:
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: trace: get_tmpdir
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: trace: get_tmpdir = "/tmp"
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: trace: disk_create "/tmp/libguestfsxGyYh3/overlay1" "qcow2" -1 "backingfile:/home/zstack/local_root/rootVolumes/acct-106896eff88049089626022948449d2f/vol-27126a121a414499b23faf432eadb2e1/27126a121a414499b23faf432eadb2e1.qcow2" "backingformat:qcow2"
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: command: run: qemu-img
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: command: run: \ create
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: command: run: \ -f qcow2
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: command: run: \ -o backing_file=/home/zstack/local_root/rootVolumes/acct-106896eff88049089626022948449d2f/vol-27126a121a414499b23faf432eadb2e1/27126a121a414499b23faf432eadb2e1.qcow2,backing_fmt=qcow2
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: command: run: \ /tmp/libguestfsxGyYh3/overlay1
Jun 19 11:40:21 zstack-system-test-12 collectd: Formatting '/tmp/libguestfsxGyYh3/overlay1', fmt=qcow2 size=8589934592 backing_file=/home/zstack/local_root/rootVolumes/acct-106896eff88049089626022948449d2f/vol-27126a121a414499b23faf432eadb2e1/27126a121a414499b23faf432eadb2e1.qcow2 backing_fmt=qcow2 encryption=off cluster_size=65536 lazy_refcounts=off refcount_bits=16
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: error: command: waitpid: No child processes
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: error: qemu-img: /tmp/libguestfsxGyYh3/overlay1: qemu-img exited for an unknown reason (status -1), see debug messages above
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: trace: disk_create = -1 (error)
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: trace: add_drive = -1 (error)
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: trace: add_libvirt_dom = -1 (error)
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: trace: add_domain = -1 (error)
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: trace: close
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: closing guestfs handle 0x7f1efe9cc8f0 (state 0)
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: command: run: rm
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: command: run: \ -rf /tmp/libguestfsxGyYh3
Jun 19 11:40:21 zstack-system-test-12 collectd: libguestfs: error: command: waitpid: No child processes
根据日志,直到日志中标红部分 (error: command: waitpid: No child processes)之前,都是正常的,那么可以定位问题产生的位置,有两个可能的地方:
- disk_create执行失败,导致了后面的进程问题,这也是我最开始认为一直产生问题的地方;
- 进程相关的问题;
最开始一直花时间在排查第一条可能性,但是一直没有结果,最后从头读了一下collectd插件加载、调用的代码,分析libguestfs的函数调用流程,终于发现了问题的根源,首先直接给结论:
libguestfs会fork一个子进程(记为A),在进程A中使用某个guest(根据用户的调用参数确定)的备份文件来创建一个临时虚拟机,完成相关操作。collectd进程(记为B)通过 plugin_register_read() 函数注册到链表中,调用回调函数采集指标。fork涉及到一个对SIGCHLD设置的操作。问题出在这里:collectd的进程(进程B)和libguestfs的进程(进程A)针对SIGCHLD信号的设置不一样。一方面,collectd设置SIGCHLD为SIG_IGN[1],但libguestfs设置SIGCHLD的时候并未做这个假设,而且最终调用了waitpid()函数[2]。另一方面,我们是在collectd中调用libguestfs,libguestfs中的fork出的子进程的父进程实际上就是collectd进程,而fork默认继承了父进程的SIGCHLD设置[3]。这样就等于libguestfs设置SIGCHLD为SIG_IGN的同时还调用waitpid函数。因此,waitpid 这时候会返回-1,同时errno被置为ECHILD,报 error: command: waitpid: No child processes错误,libguestfs也就会因为错误而返回[4]。
[1] 关于collectd的调用流程分析,参见https://jin-yang.github.io/post/collectd-source-code.html,我后面也会有专门详细的分析。这里只需要注意在collectd的main函数中,关于SIGCHLD的设置,源码见https://github.com/collectd/collectd/blob/master/src/daemon/collectd.c,从源代码557行开始,可以看到如下代码
struct sigaction sig_chld_action = {.sa_handler = SIG_IGN}; sigaction(SIGCHLD, &sig_chld_action, NULL);
随后572行开始,通过fork创建了进程,后续流程参见上面的流程分析。
[2] collectd各个流程中比较核心的就是guestfs_launch,而上述错误也是发生在该函数所调用的函数中,其调用路径如下(以下流程基本也说明了libguestfs的工作流程,当前仅罗列调用关系,想了解更多参见《libguestfs源码浅析》):
guestfs_launch(guestfs_h *g) | guestfs_impl_launch(guestfs_h *g) | g->backend_ops->launch (g, g->backend_data, g->backend_arg)
这里需要说明一下g->backend_ops->launch到底是哪个函数,看下面的调用路径:
guestfs_create(void) | guestfs_create_flags(0) | guestfs_int_set_backend (guestfs_h *g, const char *method) :
1.该函数执行了 g->backend_ops = b->ops进行赋值操作
2.libguestfs启动这个小型的特殊虚拟机方式:a.直接使用qume启动 b.使用libvirt启动和管理;在这里进行设定选择a方式还是b方式
再来看 b->ops,通过guestfs_int_init_libvirt_backend ->guestfs_int_register_backend ("libvirt", &backend_libvirt_ops),b->ops保存的是以下结构体中的launch,换言之,上面的调用路径接下来调用的是launch_libvirt(注意这里跟上面guestfs_create调用中guestfs_int_set_backend设置的方式有关,也可以是launch_direct,两者调用路径异曲同工)
static struct backend_ops backend_libvirt_ops = { .data_size = sizeof (struct backend_libvirt_data), .create_cow_overlay = create_cow_overlay_libvirt, .launch = launch_libvirt, .shutdown = shutdown_libvirt, .max_disks = max_disks_libvirt, .hot_add_drive = hot_add_drive_libvirt, .hot_remove_drive = hot_remove_drive_libvirt, }; static struct backend_ops backend_direct_ops = { .data_size = sizeof (struct backend_direct_data), .create_cow_overlay = create_cow_overlay_direct, .launch = launch_direct, .shutdown = shutdown_direct, .get_pid = get_pid_direct, .max_disks = max_disks_direct, };
现在追踪,launch_libvirt的相关函数调用:
launch_libvirt
| make_qcow2_overlay | guestfs_disk_create_argv | guestfs_impl_disk_create | disk_create_qcow2 |guestfs_int_cmd_run (struct command *cmd)
到了guestfs_int_cmd_run,查看其源码
int guestfs_int_cmd_run (struct command *cmd) { finish_command (cmd); if (cmd->g->verbose) debug_command (cmd); if (run_command (cmd) == -1)//run_command会fork出新的进程执行相关的命令,如qemu-img create等 return -1; if (loop (cmd) == -1) return -1; return wait_command (cmd); }
wait_command最终调用了waitpid,从而引发了错误。
同时,知道了以上流程,也就明白了调用libguestfs相关的方法时,为啥要有一下的几步:
g = guestfs_create();//设定:qemu直接创建虚拟机或者libvirt连接已有 if (g == NULL){/*......*/} /* dom为目标虚拟机对象(或者虚拟机对象的名称) * 这里使用guestfs_add_domain,参数dom是虚拟机的名称,也可以使用guestfs_add_libvirt_domain,参数为virDomainPtr dom * 主要是添加dom虚拟机的disk到guestfs_h对象g中,原理:通过libvirt获取dom的xml,解析出disk相关信息,再调用guestfs_add_drive_opts实现 */ guestfs_add_domain(g, (const char *)dom, GUESTFS_ADD_DOMAIN_READONLY, 1, GUESTFS_ADD_DOMAIN_ALLOWUUID, 1, -1); guestfs_set_identifier(g, (const char *)dom); guestfs_set_trace(g, 0);//函数跟踪信息相关,配合ltrace guestfs_set_verbose(g, 0);//详细信息与否 if (guestfs_launch(g) == -1){/*......*/} /*开始对虚拟机的操作,例如查看文件系统信息......*/
这里也简单的看一下launch_direct函数的内容:
static int launch_direct (guestfs_h *g, void *datav, const char *arg) | guestfs_int_build_appliance (g, &kernel, &initrd, &appliance) | socket:create accept bind listen | guestfs_int_qemu_supports | get_backend_setting_bool | get_cpu_model (has_kvm && !force_tcg) | fork | 子进程中通过 execv (g->hv, cmdline.argv);运行qemu | 父进程:再次fork | 父进程 /* Loop around waiting for one or both of the other processes to * disappear. It's fair to say this is very hairy. The PIDs that * we are looking at might be reused by another process. We are * effectively polling. Is the cure worse than the disease? */ for (;;) { if (kill (qemu_pid, 0) == -1) /* qemu's gone away, we aren't needed */ _exit (EXIT_SUCCESS); if (kill (parent_pid, 0) == -1) { /* Parent's gone away, qemu still around, so kill qemu. */ kill (qemu_pid, 9); _exit (EXIT_SUCCESS); } sleep (2); } |子进程 /* Wait for qemu to start and to connect back to us via * virtio-serial and send the GUESTFS_LAUNCH_FLAG message. */ g->conn = guestfs_int_new_conn_socket_listening (g, daemon_accept_sock, console_sock);
[3] fork完整源码见linux内核源码目录kernel/fork.c,这里关注fork系统调用定义中的SIGCHLD继承即可。
#ifdef __ARCH_WANT_SYS_FORK SYSCALL_DEFINE0(fork) { #ifdef CONFIG_MMU return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0); #else /* can not support in nommu mode */ return -EINVAL; #endif } #endif
[4] 为什么设置SIGCHLD行为是SIG_IGN但同时调用waitpid函数,会导致waitpid返回-1,同时errno被置为ECHILD?
Linux man page 给出了事实,但没有解释原因,参见https://linux.die.net/man/2/waitpid,相关描述:
ECHILD
(for waitpid() or waitid()) The process specified by pid (waitpid()) or idtype and id (waitid()) does not exist or is not a child of the calling process. (This can happen for one's own child if the action for SIGCHLD is set to SIG_IGN. See also the Linux Notes section about threads.)
我认为主要原因是:通过signal(SIGCHLD, SIG_IGN)通知内核当前进程对子进程的结束不关心,由内核回收,所以子进程成为了init的子进程,这时候父进程如果还调用wait或则waitpid就会因为没有子进程而发生错误。
现在的解决方案版本2:
module_register
↙ ↘
plugin_register_init(PLUGIN_NAME, vm_disk_init) plugin_register_read(PLUGIN_NAME, vm_disk_read);
↓ ↓
start_vm_disk_read_thread() collectd定期调用vm_disk_read,轮询回调数据采集
↓ ↓
新线程:调用libguestfs采集虚拟机device使用量信息 1.锁请求 2.从共享存储区域拉取数据 3.锁释放
| ↑(拉取)
↪ → 提交至临时数据区域 →1.锁请求 2.数据提交至共享存储区域share memory 3.锁释放
|← libguestfs数据采集阶段:比较耗时 →|← 共享存储器数据存取阶段,有锁竞争,须满足:collectd数据采集线程不被长时间阻塞→|
这里由于是在同一个进程下的不同线程间协同工作,因此不用再专门通过内核开辟共享存储空间,仅仅需要处理一下并发访问即可。
而在调用libguestfs相关的函数时,作一下处理,vm_disk便可以正常工作:
sighandler_t old_handler = signal(SIGCHLD, SIG_DFL); //调用libguestfs相关函数获取信息 signal(SIGCHLD, old_handler);
下一步解决方案版本3:
guestfs_create(void) | guestfs_create_flags(0) | guestfs_int_set_backend (guestfs_h *g, const char *method) //从这里入手,优化vm_disk的性能,提高效率
撰写过程中......

浙公网安备 33010602011771号