【导读】 在以Docker+kubernetes为代表的通用型容器云平台,容器引擎Docker自身的可视化和仪表化程度还不够,可以学习社区课程《容器云平台监控的设计》和《容器云平台日志管理开发和部署及配置》,通过监控和日志系统进行故障的检测和判断(点击此处可以了解)。除此之外,比较理想的是,我们应该通过一种科学的方式对Docker进行故障检测和排除。本文将通过三个方面对此进行论述,主要是使用docker exec来检查容器、对docker的外部进行调试、通过其他的工具和组件进行检测。同时列举了多个案例来描述了使用Docker过程中遇到的常见的问题,如启动时、运用中的一些场景。
**【作者】 顾黄亮,**苏宁消费金融安全运维部负责人,TVP成员,《开源许可证使用指南(2018)》作者之一, 《研发运营一体化(DEVOPS)能力成熟度模型》作者之一,《企业IT运维发展白皮书》核心作者之一, 《企业级devops实战案例-持续交付篇》合著作者。twt社区平台特邀作者、2020容器云职业技能大赛百位专家委员会成员。
1. 检查容器
对服务器进行故障检测和排除时,传统的调试方法为登录到相应的机器上进行查看。在Docker中,典型的工作流程分为两步:首先使用标准的远程登录工具登录到docker宿主机上,然后使用docker exec进入指定的、运行中的容器进程命名空间中,作为调试应用的一种手段,这是最直接而且有效的。下面我们举一个例子,对运行haproxy的Docker容器进行故障检测、排除和调试。我们先创建haproxy的配置文件haproxy.cfg。
使用官方推荐的haproxy docker镜像和创建的配置,运行该容器。在docker宿主机上,运行以下命令来启动该haproxy容器:
dockerhost$ docker run --d --p 80:80 --name haproxy \ -v 'pwd'/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg \haproxy:1.5.14
现在进入检查容器并调试阶段。具体案例如下,如果确认haproxy容器是否在监听80端口,ss程序可以在绝大多数的liunux发行版上打印出套接字统计信息的小结。可以运行以下命令,来显示docker容器内正在监听的套接字的统计信息。
dockerhost$ docker exec haproxy /bin/ss --l
由于ss已经在haproxy:1.5.14的父容器linux:jessie中默认安装了,所以docker exec这种方法才能起到作用。我们不能使用类似于netstat的工具,因为它默认没有被安装,运行netstat命令会引起下列的报错。
dockerhost$ docker exec haproxy /bin/netstat --an
dockernost$ echo $?
255
我们可以查看docker引擎服务的日志,来查看容器发生了什么,输入以下命令会显示,在我们的容器中netstat程序并不存在,具体命令如下。
dockerhost$ journalctl --u docker.service --o cat
另一种查看netstat是否安装的方法就是交互式的进入容器中,docker exec命令有特殊的选项-it,可以用来打开一个交互式的shell会话来进行调试,在bash中输入命令
dockerhost$ docker exec --it haproxy /bin/bash
在这个案例中,我们介绍了通过docker exec命令的方式来对容器进行调试和突发问题的排查,同时使用了-it选项,还可以通过交互式的命令进行更加深入的调试。但是,docker exec也有一定的局限性,原因是我们假设所有的工具已经默认安装在docker容器中,这也是容器的体量过于庞大。
2. 从外部进行调试
尽管容器隔离了容器的网络、内存、存储资源,但是每个单独的容器仍然需要docker宿主机的操作系统来执行真正的命令,所以我们可以通过这个特性,从外部来检查和调试docker容器。在此,基于此特性,我们可以在docker宿主机或者具备一定权限,如高级权限、elevated privileges的兄弟容器sibling container中,来查看docker宿主机中的组件。
(1)追踪系统调用
在服务器操作中,系统调用轨迹systemcall tracer是一个很实用的工具,它可以检查并记录被调用的系统调用,每个操作系统都是它的变形,即使我们在docker容器中运行不同的应用和进程,归根结底也会以系统调用的形式进入docker宿主机的linux操作系统。
在Linux系统中,strace程序可以用来记录系统调用,strace的拦截和日志功能可以从外部检查docker容器,容器生命周期中调用的系统调用列表可以描述出容器是如何运行的。接着以上的案例,我们用strace来检查haproxy容器的系统调用,命令如下。
通过以上命令,可以看到haproxy容器调用了epoll_wait()等待网络连接。同步在另一个终端,输入以下命令,向容器发送http请求,命令如下。
$ curl http://dockerhost,然后重回strace程序,可以看到如下内容。
可以看到,haproxy调用了标准的bsd风格的套接字系统调用,如accept4()、socker()、close(),来接收、处理和关闭来自http客户端的网络连接。同时,即使haproxy在处理连接过程中,epoll_wait()仍然在不断的被调用,从这里可以看到haproxy是如何处理并发连接。
追踪系统调用是一个非常有用而且科学的技术,可以用来调试在容器云平台上承载的生产系统,维护人员在没有访问源码的情况下,只有生产环境中编译好的二进制文件,或者docker镜像。在线上运行的系统,可以通过追踪系统调用的方式获得查找问题的第一手线索。
(2)网络数据包的分析
在容器云平台架构中,绝大多数的docker容器都提供了某种形式的网络服务,因此,在本小节的案例中,haproxy容器承载的主要是http网络流量。无论我们运行的是什么容器,网络数据包最终都是由docker宿主机发出,从而完成请求。通过打印和分析数据包的内容,我们可以了解到容器的本质,因此根据haproxy的案例,我们使用tcpdump抓包工具来分析docker容器接收和发送的网络数据包流量。
下面我们通过一个简单的例子来说明,在docker宿主机运行如下命令,发现在centos:jessie容器中不能解析www.googole.com。
dockerhost$ docker run --it centos:jessie /bin/bash
root@fce09c80e16:/# ping www.google.com
ping:unknown host
在另一个单独的终端运行tcpdump,当运行ping命令的时候,注意tcpdump有如下的输出。
dockerhost$ tcpdump --i docker0
可以看到,/bin/bash正在寻找172.17.42.1,这通常是docker引擎的网卡地址docker0的ip地址,然后我们输入一下命令来查看docker0。
dockerhost$ ip addr show dev docker0
通过以上返回结果,我们发现问题,网卡接口docker0没有制定ipv4地址,在某些场景,docker0上映射的ip地址会存在丢失的现象,最简单的解决方案重启docker引擎便可以解决。docker会重新初始化docker0网卡接口。下面在docker宿主机上,输入以下命令来重启docker引擎。
dockerhost$ systemctl restart docker.service
现在我们运行上述相同的命令,ip地址已被指定。
(3)观察块设备
Docker容器将数据存储在物理存储设备上,比如普通硬盘或SSD,Docker底层的写时复制文件系统是一个随机访问的物理设备,这些设备被组织成块设备,而数据被组织为固定大小、可以随机访问的块,也称之为blocks。由于docker容器有一些特殊的io行为和性能问题,我们可以使用blktrace工具来追踪、检查故障和排除块设备中的问题。这个程序可以检测到进程如何和块设备进行交互的内核事件,在这个小节中,我们将设置docker宿主机,来观察容器底层的块设备。首先在docker宿主机上,通过下列命令来安装blktrace程序。
dockerhost$ apt-get install --y blktrace
此外我们需要开启文件系统的调试选项,命令如下
dockerhost$ mount --t debugfs debugfs /sys/kernel/debug
正式进入调试环节,我们需要定义blktrace在哪里监听io事件,为了追踪容器的io事件,我们需要知道docker运行时的根文件的目录位置。这个步骤中,docker宿主机的默认配置为指向/var/lib/docker,因此我们可以轻易的找到根目录隶属于宿主机的哪个分区。
我们可以做一个模拟,在容器内创建一个空文件,一直到根分区耗尽空闲的资源,从而在磁盘上产生io事件,输入以下命令产生负载。
dockerhost$ docker run --d --name dump centos:jessie \ /bin/dd if=/dev/zeroof=/root/dump bs=65000
我们将得到产生io时间的docker容器pid,获取pid的方式见上一小节。在此我们可以使用blktrace的辅助工具blkparse。blktrace程序只监听linux内核的块io层,并将结果输出到文件中,blkparse程序可以查看和分析这些事件。输入以下命令,在产生的负载中找到docker容器的pid对应的io事件。
dockerhost$ blkarse --i dump.blktrace.0 | grep "$pid"
从上面的输出可以得到,在块设备dm-0的偏移量为11001856的位置,有一次1024字节的写入,并已经完成。为了更进一步的排查问题,我们可以查看事件产生的偏移量文字,输入以下命令,来过滤出偏移量的位置。
dockerhost$ blkarse --i dump.blktrace.0 | grep "11001856"
可以看到写入已经被kworker进程放入了设备的队列中,也就是说,写入操作被内核加入到队列中。在40毫秒滞后,docker容器进程的写入请求已经完成了。通过这个案例我们可以查看docker容器中更为详细的io行为,并找出相应应用的瓶颈,可以预见的应用是否产生了太多的写请求,大量的读请求,从而引发容器的故障。
(4)故障检测和排除工具
调试Docker容器中的应用和调试Linux中的应用是不同的,但是真正使用的程序是相同的,因为容器中发出的系统调用最终是进入docker宿主机的操作系统。通过了解容器产生的系统调用,我们可以使用任意的调试工具来进行容器突发情况的故障检测和排除。
除了标准的Linux工具之外,有很多针对容器的工具集,集成了一系列的标准工具,对于容器使用和故障排查方面更加友好,在此推荐大家一些成熟的工具。
红帽的rhel-tools docer镜像是一个巨大的容器,集成了我们本章节所提到的所有的工具。core的toolbox程序是一个精简的脚本工具,可以使用systemd的systemd-nspawn程序创建一个小的Linux容器,通过复制流行的docker容器根文件系统,可以安装任意一款工具而不会污染宿主机的文件系统。
3. 容器的常见故障举例
在实际的使用中,除了上述的网络、磁盘因素外,还有各种各样的故障,一般有三类。
应用故障:应用的执行状态和预期的不一致;
容器故障:无法正确的创建、停止、更新容器;
集群故障:集群创建失败、更新失败和无法连接。
以下会举一些常见的案例来进行说明。
1、在生产环境中,全新未投产的docker无法启动,具体的报错如下。
systemctl start docker.service
job for docker.service failed because
the control process exited with error code.see
"systemctl status docker.service" and"journalctl -xe" for details
通过journalctl --xe命令查看容器启动的详细日志,发现启动deamon错误,因为selinux不支持,selinux阻挡docker容器引擎的启动,具体报错如下。
error starting daemon: selinux is notsupported with the overlay2 graph driver on this kernel. either boot into anewer kernel or disable selinux in docker (--selinux-enabled=false)
解决方案一般有两种,关闭宿主机selinux配置文件,/etc/selinux/config,将配置文件中enforcing设置为disabled,然后重启系统,然后重启docker引擎即可。关闭docker主配置文件,/etc/sysconfig/docker,将配置文件中--selinux-enabled选项为false,改成:--selinux-enabled=false即可。
2、在实际使用中,发现docker虚拟化引擎存在如下报错,chown socket at step group:no such process,报错截图如下。
上述错误提示因为docker无法找到当前的group组信息,docker组有可能被误操作删除,一般解决方法有两种。方法一创建宿主机docker组即可,命令为groupadd docker。方法二对docker配置文件,/usr/lib/systemd/system/docker.socket文件,socketgroup=修改为root。
3、在实际使用中,发现docker虚拟化引擎存在如下报错,attach_loopback.go:42 there are no more loopback devices available,具体截图如下。
该错误提示因为linux操作系统没有更多的loopback设备提供至docker使用,解决方法为创建更多的loopback设备,具体命令如下。
for i inseq 0 6;do mknod -m 0660 /dev/loop$i b 7 $i;done
4、在实际使用中,对docker进行操作,当执行docker命令时,有如下报错。
cannot connect to the docker daemon at unix:
///var/run/docker.sock. is the docker daemon running?
该报错提示docker容器没有被正常启动
解决方法如下,首先检测docker进程是否正常启动,可以通过ps -ef|grep docker,如果没有启动,启动docker即可。检测docker进程存在,但是无法连接,可以重启一下docker服务,检测一下sock路径是否正确。
5、在实际使用中,docker获取远程镜像,报错信息和截图如下
get https://registry-1.docker.io/v2/: dialtcp: lookup registry-1.docker.io
该错误表示无法连接到远程仓库docker.io。解决方案如下,查看本地是否配置dns,能否ping通到dicker.io,如果能够ping通,但是下载速度也是非常慢,可以修改docker仓库源为国内或自建的仓库源。docker进行修改方法为,对vim /etc/docker/daemon.json,执行如下命令:
6、容器启动过程中,有下列报错,具体报错信息如下。
/usr/bin/docker-current: error response from daemon: oci runtimeerror:container_linux.go:247: starting container process caused "exec:\"/bin/bash\": executable file not found in $path".
一般此类报错,为容器启动过程时,不支持报错信息中的命令,/bin/bash。解决方案有两种,修改启动命令为正确的docker容器启动命令,例如修改为:/bin/sh或者sleep 99999d等;docker镜像自身问题或者docker引擎版本比较低导致,可以升级docker引擎版本服务。
7、在实际使用中,docker会有如下报错,报错信息如下:
docker no space left ondevice
如上错误,表示docker宿主机,没有多余的空间设备所使用,可能原因是容器磁盘不足或者物理机磁盘不足。解决方案一般有三种方式,删除docker容器内占有数据较多的目录,通过docker system prune自动删除一部分日志目录,不过此种方法会停止容器服务,不建议使用,还可以临时增加docker存储硬盘介质。
8、在实际使用中,docker push上传镜像至本地仓库报错信息如下:
the push refers to a repository [106.12.133.186:5000/busybox] gethttps://106.12.133.186:5000/v1/_ping: http: server gave http response to httpsclient
根据如上错误提示,错误是由于客户端采用https,docker registry未采用https服务所致。一种处理方式是把客户对地址"106.12.133.186:5000"请求改为http。解决方案如下,针对vim /etc/docker/daemon.json 文件,在文件中写入:{"insecure-registries":["106.12.133.186:5000"] } 。