转自https://www.yanning.wang/archives/717.html
一般需要对外提供服务的Docker容器,我们在启动时后使用-p命令将对外访问端口暴露给外部,例如启动Docker Registry,我们将5000端口映射出来供外部访问:
docker run -d -p 5000:5000 registry
但最近碰到一个非常奇怪的情况:研发组里一个CentOS 7测试环境里部署有Docker Registry,并对外暴露了端口。启动容器后一段时间内都是可以正常工作的,但在不定时间间隔后,外部主机就会出现无法从仓库中拉取镜像的情况,提示TimeOut:
然而在Docker宿主机上访问仓库则可以正常访问:
至于这个问题,只有手动重启出问题的Docker daemon服务后,外部才可以重新访问,但只要再过一段时间又会出现这样的问题。
碰到这个问题我第一反应就是问组里的人,是不是有人重启过CentOS 7 自己的firewallD了。
因为这台服务器是我配置的,防火墙虽然开着但我已经开启端口访问了,所以肯定不是因为防火墙阻断连接的缘故。但由于这篇文章是篇踩坑排查文档,所以还是把这种情况写出来了
情况一:开着防火墙但没有开放端口
CentOS 7自带并启用了防火墙FirewallD,我们可以通过下面的命令检查FirewallD的状态:
firewall-cmd --state
如果输出的是“not running”则FirewallD没有在运行,且所有的防护策略都没有启动,那么可以排除防火墙阻断连接的情况了。
如果输出的是“running”,表示当前FirewallD正在运行,需要再输入下面的命令查看现在开放了哪些端口和服务:
firewall-cmd --list-ports
firewall-cmd --list-services
可以看到当前防火墙只开放了80/tcp端口、ssh服务(22/tcp)和dhcpv6-client服务,并没有打开Docker容器映射的5000/tcp端口。
解决方案有两种:
1.关闭FirewallD服务:
如果您不需要防火墙,那直接关掉FirewallD服务就好了
systemctl stop firewalld.service
2.添加策略对外打开指定的端口:
比如我们现在要打开对外5000/tcp端口,可以使用下面的命令:
firewall-cmd --add-port=5000/tcp --permanent
firewall-cmd --reload
如果只是临时打开端口,去掉第一行命令中的“–permanent”参数,那么当再次重启FirewallD服务时,本策略将失效。
情况二:人为重启CentOS 7的FirewallD服务
FirewallD是CentOS系统在7版本引入的新组件,简单的说就是iptables的包装,用于简化防火墙相关的设置。
然而FirewallD和Docker相处的并不是特别好,当FirewallD启动(或重新启动)时,会从iptables中删除DOCKER链,造成Docker不能正常工作:
FirewallD
CentOS-7 introduced firewalld, which is a wrapper around iptables and can conflict with Docker.
When firewalld is started or restarted it will remove the DOCKER chain from iptables, preventing Docker from working properly.
When using Systemd, firewalld is started before Docker, but if you start or restart firewalld after Docker, you will have to restart the Docker daemon.
摘自Docker官方文档《CentOS - Docker Documentation》
在CentOS 7中,如果设置使用systemd开机自启动Docker服务是不会有问题的,因为Docker在systemd配置文件中明确注明了“After= firewalld.service”,以保证Docker daemon 在FirewallD启动后再启动。
(Docker:惹不起我还躲不起吗)
但每当用户手动重启过FirewallD服务之后,FirewallD服务会将Docker daemon写入iptables的DOCKER链删除,所以需要手动重新启动一次Docker daemon服务,让Docker daemon服务重建DOCKER链。
不过问了组里另外两个研发,都说没有动过。查看了shell的history也没找到对应的记录。
这就很奇怪了。不过经过一段时间的蹲点排查之后,我终于发现了一个新的原因:
情况三:没有启用IP_FORWARD
因为一直没法定位出问题的所在,所以我们研发组都是发现不能正常访问仓库时,手动登陆宿主机重启Docker daemon服务。
在有一次登录到宿主服务器上准备重启Docker daemon服务前,我突然想起之前在用Docker的时候还碰到过另一个问题:如果宿主机没有启用IP_FORWARD功能,那Docker容器在启动时会输出一条警告消息:
WARNING: IPv4 forwarding is disabled. Networking will not work.
并且将不能在启动的容器中访问外部网络,容器对外暴露的端口外部也不能正常访问:
会不会是因为宿主机的IP_FORWARD功能没有启用所以才引起的这个故障呢?
sysctl net.ipv4.ip_forward
果然,输出表示当前系统的IP_FORWARD功能处于停用状态!
可是问题来了,当时启动容器的时候都是好的啊,什么都没有输出,怎么用着用着IP_FORWARD功能就被禁用了呢?
等等,Docker daemon服务在启动的时候会自动设置iptables设置,难不成它还会检查IP_FORWARD设置,并帮我临时启用吗?
带着这个假设,我手动重启了一下Docker daemon服务:
果然,Docker daemon服务在启动过程中会检查系统的IP_FORWARD配置项,如果当前系统的IP_FORWARD功能处于停用状态,会帮我们临时启用IP_FORWARD功能,然而临时启用的IP_FORWARD功能会因为其他各种各样的原因失效…
虽然具体造成本次故障的原因现在还没有确凿的证据定位出,但我现在严重怀疑是因为重启网络服务造成的。因为出问题的服务器宿主机上运行着我们研发组正在开发的Web项目,其中有一个功能是修改网卡IP地址,这个功能在修改完网卡IP后,会自动调用下面的命令重启网络服务:
systemctl restart network.service
而重启网络服务正会使Docker daemon服务自动设置的临时启用IP_FORWARD配置失效:
另外因为是程序直接调用命令,所以不会在history命令中留下痕迹。
至于修复方案倒非常简单,只要一行命令就可以了:
echo 'net.ipv4.ip_forward = 1' >> /usr/lib/sysctl.d/50-default.conf
执行完成后,重启服务器或使用下面的命令从文件中加载配置:
sysctl -p /usr/lib/sysctl.d/50-default.conf
就可以了。
Docker daemon服务在启动的时候会帮帮我们调整很多的配置项,比如这次出事儿的IP_FORWARD配置。
Docker daemon启用IP_FORWARD功能是因为Docker容器默认的网络模式(bridge/网桥模式)会给每个容器分配一个私有IP,如果容器需要和外部通信,就需要使用到NAT。NAT需要IP_FORWARD功能支持,否则无法使用。这也解释了为什么会出现在IP_FORWARD功能停用的情况下,使用bridge模式的容器内外均无法访问的情况。
只是在Linux下,出于安全考虑,默认是停用IP_FORWARD功能的,Docker daemon服务在启动时会检查IP_FORWARD功能是否已经启用,如果没有启用的话,Docker daemon会悄无声息的临时启用此功能,然而临时启用的IP_FORWARD功能并不能持久化,会因为其他命令的干扰导致失效。
不过这次的事情告诉了我一个小道理:当出现问题的时候,不要慌,要结合经验大胆的做出假设并验证,治标治本。