Container及其内部进程监控剖析
目前市场上的虚拟化技术种类很多,例如moby(docker)、LXC、RKT等等。在带来方便应用部署和资源充分利用的好处的同时,如何监控相应Container及其内部应用进程成为运维人员不可避免遇到的新情况。UAV.Container从虚拟化技术的基础原理和Linux操作系统的内核特性出发,得到Container容器和内部进程的各维度监控数据,使无论是虚拟机或物理机运维人员,还是业务运维人员角度,都能得到合适的监控维度。
虚拟化技术从基础原理上主要是cgroups、namespace和file system的应用,而操作系统作为cgroup和namespace根节点,无论在container里启动何种应用,从内核角度上来说,肯定在操作系统有其一定的特征和表现形式。我们需要做的就是对这些特征做加工处理,以得到相应的监控数据。
下面我们以docker技术举例,其他虚拟化技术类似。
一、Container ID
Container ID是一个Container的唯一标识。从容器监控的角度我们需要能得到该进程在哪个Container里运行。在操作系统层面,进程的cgroup的挂载情况就能有所体现。如图所示,我们在一个ID为3411554ff684的Container内部跑一个Tomcat进程。
由于Container的pid namespace是操作系统的pid namespace的子namespace,那么该进程在操作系统级也应该有相应的pid,用docker top命令验证一下:
该容器内进程在宿主机上的进程号为1848。接下来进入/proc/1848/cgroup下看看该进程的cgroup挂载情况
从cgroup文件里清楚的显示了实现了该容器的虚拟化技术、Container ID和此container的资源挂载路径,对比一下这里面显示的Container ID,和创建Container时的ID完全相同。这也验证了通过扫描宿主机进程的cgroup信息可以获得Container ID。这样就将pid和Container ID做了关联。
二、CPU
虽然cgroup管控了该cgroup下所有进程的CPU使用情况,但从操作系统的角度上,不论进程是否隶属于某个子cgroup下,仍然是共用宿主机的CPU。所以监控宿主机上该进程的CPU就能得到进程的CPU监控指标。
Linux上常用的CPU监控命令是top。top对CPU监控的原理是在time1时刻获取CPU从启动时的累计总时间countAll1和busy总时间countBusy1,再到time2时刻获取CPU总时间countAll2和busy总时间countBusy2,最后用busy的时间差值减去总时间的差值得到了在time1到time2这个时间段内机器CPU的占用情况。也就是:
CPU占用率(%) = (countBusy2 - countBusy1)/(countAll2 - countAll1) * 100
进程同理,在两个时刻分别得到每个进程的busy总时间countProcBusy1和countProcBusy2,则得到进程CPU占用率:
进程CPU占用率(%) = (countProcBusy2 - countProcBusy1)/(countProcAll2 - countProcAll1)*100
宿主机从启动开始的CPU总时间片可以从/proc/stat下获取:
第一行是总的CPU使用情况,具体参数的意思:
所以,选择当前为time1,3秒后为time2,countAll = user + nice + system + idle + iowait + irq + softirq + stealstolean + guest + guest_nice。countBusy为countAll减去idle的值,这样上面第一个公式的所有需要的值就齐了,可以直接计算。
第二行、第三行是每个逻辑CPU的使用情况,这里记下有两个逻辑CPU,CPU的逻辑核数与CPU显示模式irix和solaris有关。
接下来是countProcBusy的计算,进程的CPU时间片位于/proc/$pid/stat下,如图所示:
这个文件里面体现了很多进程的相关信息。其中第14、15、16、17个参数与CPU有关。
所以,countProcBusy = utime + stime + cutime + cstime,该值包括其所有线程的cpu时间。而countProcAll2-countProcAll1=3s,通过两个时刻的countProcBusy和countProcAll,进程CPU的占用率就能得到了。
其中需要注意的问题有两点:
1) jiffies实际上指的是内核时钟使用的节拍总数,所以这里的jiffies需要换算成秒才能应用上面的除法公式。
2) 刚刚我们谈到CPU显示模式irix和solaris,简单来说irix模式就是机器有N个逻辑CPU,CPU显示上限就是N*100%,solaris模式就是不管机器有多少逻辑CPU,CPU显示上限就是100%,而/proc/$pid/stat显示的是计算了所有逻辑CPU时间的,所以两种显示方式意味着计算方法稍有差异,solaris模式的结果需要在上面进程CPU占用率公式基础之上除以逻辑核数。
三、内存
进程内存的监控有两个维度的数据:一是物理占用内存大小,二是进程内存占用百分比的大小。
进程内存占用率(%) = 进程物理内存占用大小 / 宿主机总内存大小 * 100
与CPU类似,/proc/$pid/status文件记录了进程物理内存使用情况,其中VmRSS即为该进程目前所占实际物理内存的大小。
/proc/meminfo文件下记录了机器内存占用情况,这个文件很长,截取其中的一部分展示一下,MemTotal就是宿主机总内存大小:
这样,这个进程的物理内存占用和机器总内存就都得到了,相应的进程内存的占用率也就得到了。
四、磁盘IO
磁盘IO获取也很简单,/proc/$pid/io已经帮我们把这个进程的io情况记录下来了,但是与CPU类似,io文件里存的也是该进程从启动到现在的io总量,那么:
磁盘I/O(bytes/秒) = (time2时刻I/O – time1时刻I/O) / (time2 – time1)
其中的read_bytes和write_bytes分别为该进程从启动到目前为止的读取字节数和写入字节数,分别取2个时刻的值,根据上面的公式,就得到了该进程的磁盘IO。
五、端口号和连接数
由于Network Namespace对网络做了隔离,所以如果进程在Container内部运行,该进程的端口信息也应该是进程本身监听的端口号,而不是真正实际对外的端口,而Container内外端口的映射机制是由应用的虚拟化技术本身控制的,这就避免不了与实现容器的虚拟化技术打交道了,那么问题就转化成获取容器内进程本身监听的端口了。
/proc/$pid/net/tcp(tcp6,udp,udp6)就对端口号和连接数做了相应的历史记录。这些文件格式都类似,以tcp6举例
解释几个关键的key:
因为st = 0A代表listen,所以从其中挑选出st = 0A的数据,取出对应的inode号,这里这个inode号是socket号,则问题转换为了这个进程是否还存在这个socket号代表的socket。在/proc/$pid/fd下有该进程所有的fd(file descriptor),截取一段举个例子。
每个文件描述符后面的软链实际上就是打开的文件,以socket开头的就是这个进程打开的socket,在中括号中间的部分就是socket号。拿着这个socket号和上面tcp6里获得的inode号做一个匹配,如果对应上,那么tcp6里的st = 0A的端口就是这个进程监听的。至于容器内外端口的映射,这就需要根据应用的虚拟化技术的映射方法来获取了。连接数计算与端口扫描是同理的,区别只在于需要对st = 01(establish)进行扫描计数累加。
六、总结
- 上面的方法将容器内所有进程的CPU、内存、磁盘IO、端口号和连接数都拿到了。根据Container ID就可以对不同的Container的对应数据做加和,就获得了Container级的监控数据。
- 在容器内的进程是通过在操作系统级别反映出的pid和Container ID的对应关系来关联的。这样就可以通过读取/proc下的文件来获取监控数据。
参考文档:
http://elixir.free-electrons.com/linux/v3.10/ident
作者:周新宇