本文主要介绍了Core Dump实现容器进程的方法和相关内容。
上篇文章回顾: IPv6入门教程
在咱们调试程序时常常会使用到core dump功能,使用调试器(如gdb)分析其产生的Core Dump文件(如下称"core文件"),对于排查问题、定位bug堪称无往不利的利器。当前容器技术使用越发广泛,线上大量业务使用容器技术部署,那咱们的业务进程在容器环境下core文件是如何产生、与在宿主机中有什么不一样呢?本文针对这个问题简略说明,抛砖引玉。html
Core文件是当一个进程在收到某些信号后终止时产生的文件,其中包含进程终止时刻进程内存的镜像。咱们可使用gdb从该镜像中观察进程终止时处于什么状态,用于追踪排查定位问题。
linux
以下示例,其中/usr/share/core_pipe/test是crash程序,core.29873就是core文件,其中29873是crash进程的PID。git
# gdb /usr/share/core_pipe/test core.29873GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-gitCopyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /usr/share/core_pipe/test...done.
[New LWP 34]
warning: .dynamic section for "/lib64/ld-linux-x86-64.so.2" is not at the expected address (wrong library or version mismatch?)warning: Could not load shared library symbols for /lib64/libc.so.6.
Do you need "set solib-search-path" or "set sysroot"?
Core was generated by `./test'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x0000556039e756aa in main () at main.c:7 7 printf("this is null p %d \n", *intp); (gdb)复制代码
宿主机进程Core Dumpgithub
在讲容器进程Core Dump以前,咱们先来简单回顾下宿主机进程Core Dump在什么条件下产生。这里的宿主机泛指物理机和虚拟主机,宿主机进程特指运行在系统主机Initial Namespace的进程,redis
容器进程特指运行在容器Namespace中的进程。shell
前文说到当进程收到某种信号才会产生,那么什么才是"某种信号"呢?这些信号大约有十多个,这里只列出咱们常见比较熟悉的几个:ubuntu
- SIGQUIT 数值2 从键盘输入 Ctrl+'\'能够产生此信号
- SIGILL 数值4 非法指令
- SIGABRT 数值6 abort调用
- SIGSEGV 数值11 非法内存访问
- SIGTRAP 数值5 调试程序时使用的断点复制代码
其中SIGSEGV应该是咱们最常接触的,尤为是使用C/C++程序的同窗更是常见。bash
main.c
#include <stdio.h>int main(){ int *p = NULL;
printf("hello world! \n");
printf("this will cause core dump p %d", *p);
}复制代码
使用下面命名编译便可产生运行时触发非法内存访问SIGSEGV信号,其中-g选项是编译添加调试信息,对于gdb调试很是有用。运维
# gcc -g main.c -o test复制代码
除了上述产生信号的方法外,使用咱们常用的kill命令能够很是方便的产生这些信号,另外还有gcore命令能够在不终止进程的前提下产生core文件。socket
那么只要进程收到这些信号就必定会有core文件吗?显然不是,linux在这方面提供了相关配置。那么这些配置都有哪些呢?
ulimit
# ulimit -c 0 // 0表示不产生core文件# ulimit -c 100 // 100表示设置core文件最大为100k,固然能够根据本身须要设置,注意设置单位是KB# ulimit -c unlimited // 不限制core文件大小复制代码
使用上述命令设置core文件大小只对当前shell环境有效,系统重启或者退出当前shell设置就会失效,恢复默认设置。
若想永久生效能够把该shell放到/etc/profile文件中,系统从新启动初始化环境时会执行其中的命令,该设置就会在全局范围内生效,达到永久生效的效果。也可使用 source /etc/profile命令当即全局生效。
# echo "unlimit -c unlimited" >> /etc/profile // 配置添加到/etc/profile中# source /etc/profile // 当即生效复制代码
# echo "/var/core-dir/core-%e-%p-%t" > /proc/sys/kernel/core_pattern复制代码
该命令能够控制core文件保存位置和文件名格式。注意须要使用root权限执行,而且存储路径必须是绝对路径,不能是相对路径
其中%e表示添加用户程序名称,%p表示添加程序进程PID,%t表示添加产生core文件时的时间戳,还有其余一些很是有用的格式,能够参阅CORE(5) 文档。
这样修改后系统重启后也会消失,一样也有永久生效的办法
修改/etc/sysctl.conf文件在其中修改或者添加
/etc/sysctl.conf
kernel.core_pattern = /var/core-dir/core-%e-%p-%t复制代码
而后执行下面命令配置当即生效,这样系统重启后配置依然有效
# sysctl –p /etc/sysctl.conf复制代码
宿主机产生core文件可使用以下步骤
1. ulimit -c 命令设置core文件大小2. 修改core_pattern选项配置core文件存储目录复制代码
具有上述两个条件后,当进程在必定条件core后,就会存储core文件到咱们配置的目录中。
大概说一下从进程出现异常到生成core文件的流程,不会涉及太多Linux系统实现细节,具体细节能够参考相关文档和linux内核源码。
进程运行时发生一个异常,好比非法内存地址访问(即段错误),相应硬件会上报该异常,CPU检测到该异常时会进入异常处理流程,包括保存当前上下文,跳转到对应的中断向量执行入口等
在异常处理流程中若是判断该异常发生时是处于用户态,则该异常只会影响当前进程,此时向用户态进程发送相应的信号,如段错误就会发送SIGSEGV信号
当用户态进程被调度时会检查pending的信号,若是发现pending的信号是SIG_KERNEL_COREDUMP_MASK中的一个,就会进入core文件内容收集存储流程,而后根据配置(core_pattern等)生成core文件
那么若是咱们要收集在容器里运行的进程的core文件应该如何设置呢?
答案是上述宿主机针对core文件的设置对容器中的进程依然有效。
众所周知,宿主机上全部的容器都是共享系统内核的,/proc/sys文件下只有一小部分支持namespace隔离,而core_pattern恰巧不支持隔离的,因此不管是从宿主机仍是容器里修改core_pattern,最终修改的是同一个设置,而且全局生效,不论是对宿主机仍是对容器都是有效的。
通常状况下每一个容器都有本身的mount namespace,其中的文件系统与宿主机和其余容器相隔离,那么在core_pattern指定的core文件存储目录是容器中的文件目录仍是宿主机中呢?不妨推测一二,刚才咱们已经说过这个core_pattern是全局生效,若是该目录是针对某个容器的文件目录,那么确定是不合理的,由于若是宿主机上进程Core Dump时就会找不到对应的目录,没法保存。
实际上有效的core_pattern中的目录必须是宿主机中的绝对目录,更准确的描述是宿主机Initial Namespace中的绝对路径。
另一个问题是,每一个容器都有本身pid namespace,咱们再core_pattern中设置的获取crash进程的各类信息好比PID,可执行文件名,是容器namespace中的仍是宿主机namespace中的呢?从相关文档和实验得知,能够同时获取crash进程在容器中的PID(经过%p格式指定)和在宿主机Initial Namespace中的PID(经过%P格式指定),可执行文件名称(经过%e或%E格式指定)是容器的namespace中的。
之因此形成上述状况,根本缘由是Core Dump流程中内核进程最后负责处理core文件存储的,而内核进程运行在宿主机Initial Namespace中,实际上全部的容器进程在宿主机Initial Namespace都有映射,对内核来说,宿主机进程和容器进程能够统一处理,并无本质区别。
上文中咱们得知了容器进程core文件产生的方法,可是有一个问题就是上述方法的设置是对宿主机和容器内全部的进程都生效的。没法针对特定容器进程特定设置。好比说咱们但愿宿主机进程core文件保存到/var/crash目录,而对容器的core文件保存在/var/container/crash目录,或者我要限制某个容器产生core文件的总存储大小,而不是单个core文件的大小;若是咱们作一个服务平台对其余用户开放Core Dump功能的话,咱们确定还但愿获取一下crash进程的其余额外信息好比进程当前环境变量、当前用户、当前进程有效UID和GID、任务名称属性;若是咱们但愿针对core事件进行统计分析的话,可能还须要各类回调通知等等操做。
显然上述简单的设置core文件存储目录的方法没法知足咱们的需求的,那么咱们还有另一个选择,就是使用linux的piping技术转储core文件。
从linux内核版本2.6.19以后,内核就开支支持在/proc/sys/kernel/core_pattern文件中指定一个管道程序来实际处理core文件存储。core文件内容会做为该管道程序的标准输入传输给管道程序,管道程序就接管了接下来的core文件内容的全部处理。以下设置可使用piping技术转储core文件
# echo "|/usr/share/core_pipe/core_pipe core -H=%h -p=%p -i=%i -s=%s -c=%c > /proc/sys/kernel/core_pattern
# cat /proc/sys/kernel/core_pattern
|/usr/share/core_pipe/core_pipe core -H=%h -p=%p -i=%i -s=%s -c=%c复制代码
其中/usr/share/core_pipe/core_pipe是咱们的管道程序,须要注意的是必须以|开发, |以后必须紧接管道程序的路径,没有空格。当有进程core时,就会调用该管道程序进行处理。
咱们能够开发本身的管道处理程序,从管道程序启动的参数获取crash的进程信息,从管道程序的标准输入获取core文件的内容。
咱们如今知晓该管道程序何时被调用(进程Core Dump时),那么管道程序是由谁来调用呢?
既然管道程序是咱们本身开发的,咱们就能够获取管道程序的父进程是谁,也就是被谁调用的,经过实验咱们可一知道父进程的PID是2,当咱们再看该进程的父进程是谁:
# ps -ef -q 2UID PID PPID C STIME TTY TIME CMD
root 2 0 0 Jan20 ? 00:00:00 [kthreadd]复制代码
进程PID2的父进程是PID 0,而PID 0表明的是linux系统内核idle进程,Linux系统中共有三个特殊进程,分别是idle(PID 0), init(PID 1), kthreadd(PID 2),而kthreadd是全部内核进程的父进程,也就是说咱们的管道程序是做为内核线程在运行的,运行在内核态,而且在宿主机Initial Namespace中以root用户身份运行,不在任何容器内。
上文说了管道程序运行在内核态,并且是在宿主机的Initial Namespace中运行,容器的各类限制对其不起做用,好比core文件大小有可能超过容器的硬盘空间限制。固然咱们管道程序能够经过crash进程的PID拿到crash进程的容器namespace以及各类cgroup限制,而后针对性处理。这样显然对容器极有侵入性,代码写起来也不够优雅。若是处理core文件存储程序在容器中运行,就能较优雅的解决好这个问题。管道程序已经做为内核线程运行在宿主机的Initial Namespace了,虽然有办法能够动态的加入和退出某个namespace和cgroup,可是考虑的边界条件多,易出错,并不优雅。
若是管道程序可以和容器内某个程序进行交互,能够解决上述问题,同一个宿主机进程通讯的方式有不少,好比共享内存,管道,消息队列等。可是这里的两个程序是分布在不一样的namespace中,并且彼此并不知道何时能够交互,咱们为了低几率的core文件长时间让容器内某个进程空跑占用资源吗?那么socket activation技术能够用来解决这个问题。
socket activation并非一种新技术,其技术理念和原理早就被应用到Linux和MacOS中,关于socket activation技术原理细节又是须要另外一篇的长篇大论,这里暂且再也不详述,简单来讲,就是由系统init进程(对于目前大多数linux系统来讲是systemd)来为普通应用进程监听特定socket,此时应用进程并未启动,当有链接到达该socket后,由init进程接管该链接并跟进配置文件启动相应的应用进程,而后把链接传递给应用进程来处理,主要好处是当没有链接到达时,应用进程无需常驻后台空跑耗费系统资源。很是适合像Core Dump这种低频服务。
咱们能够设置一个unix socket来把管道程序的文件描述符传递到容器内进程,完成传递后, 管道程序就能够退出,由容器内进程处理core文件的存储。
下面是一个socket activation示例,其中/usr/share/core_pipe/core_pipe是咱们的core 文件处理程序, /run/core_pipe.socket是咱们unix socket文件,存在容器中,该文件咱们在Initial Namespace中的管道程序能够经过/proc/${crash pid}/root/run/core_pipe.socket拿到,而后与之交互。
core_pipe-forward.socket
# 此为Unit文件,保存内容为文件到 /etc/systemd/system/core_pipe-forward.socket
[Unit]
Description=Unix socket for core_pipe crash forwarding
ConditionVirtualization=container
[Socket]
ListenStream=/run/core_pipe.socket
SocketMode=0600Accept=yes
MaxConnections=10Backlog=5PassCredentials=true[Install]
WantedBy=sockets.target复制代码
# 此为service文件,保存内容到 /etc/systemd/system/core_pipe-forward.service
[Unit]
Description=Core Pipe crash forwarding receiver
Requires=core_pipe-forward.socket
[Service]
Type=oneshot
ExecStart=/usr/share/core_pipe/core_pipe复制代码
core_pipe-forward.socket
执行下面命令使得socket生效
# systemctl enable core_pipe-forward.socket
# systemctl start core_pipe-forward.socket
# systemctl status core_pipe-forward.socket复制代码
上述命令若是是在容器内的init进程不是systemd状况下会出错,大多数状况下容器内的init进程并非systemd,此时能够退一步使用容器内常驻进程的方式来实现core文件的处理。
本文简单说明了实现容器进程Core Dump的方法,概况一下主要有三点:
使用ulimit -c和/proc/sys/kernel/core_pattern设置Core Dump文件大小限制和存储位置
使用管道程序加强Core Dump文件处理能力
使用管道程序和容器内进程结合的方式完成内核态转到用户态,在容器内处理Core文件存储
参考文献:
CORE(5) man7.org/linux/man-p…
GETRLIMIT(2) man7.org/linux/man-p…
GDB(1) man7.org/linux/man-p…
KILL(2) man7.org/linux/man-p…
SIGNAL(7) man7.org/linux/man-p…
NAMESPACE(7) man7.org/linux/man-p…
BASH(1) man7.org/linux/man-p…
go-systemd github.com/coreos/go-s…
systemd-socket-activation-in-go www.darkcoding.net/software/sy…
Core Dump 流程分析 blog.csdn.net/omnispace/a…
Socket activation in systemd hustcat.github.io/socket-acti…
socket activation 0pointer.de/blog/projec…
文章首发于公众号“小米运维”,点击查看原文。