欢迎您光临本站,如有问题请及时联系我们。

一个Docker附加参数引起的网络服务异常

1. 事件

背景

事发在线上环境,OpenStack由容器化部署方案Kolla提供,各主要软件版本如下:

OpenStack

时间线

2017/3/17: 测试完成修复方案,上线更新;

2017/3/15: 找到root cause,确定触发场景,确定修复方案;

2017/3/14: 找到neutron产生无限多tap的场景2,能够在模拟条件下复现问题,与环境日志记录匹配;

2017/3/13: 找到neutron产生无限多tap的场景1,能够在模拟条件下复现问题,但对比环境日志并不完全匹配,而且模拟条件的出现概率非常低,排除;

……

2017/3/11

09:00:00: 在控制节点恢复网络服务(neutron-metadata-agent、neutron-dhcp-agent);

08:00:00: 自动化脚本清理完成所有无效tap设备,控制节点成功启动neutron-*-agent,虚拟网络服务能力恢复;

……

2017/3/10

22:00:00: 运行脚本自动化清理控制节点的无效tap设备

21:30:00: 原控制节点所属(6台)虚拟机恢复访问在其他节点(nc05、nc03)恢复原控制节点的虚拟机

21:00:00: 环境更新包功能还原在其他节点(nc05)成功启动neutron-metadata-agent

20:30:00: 整体环境新建虚机分配网络还原在其他节点(nc05)成功启动 neutron-dhcp-agent

18:30:00: 放弃在控制节点还原网络服务,尝试在其他节点还原网络服务;

18:00:00: 尝试删除tap设备,但进度较慢;尝试在代码去掉neutron-openvsiwtch-agent中关于tap设备的预读过程,因涉及点太多放弃在线修改;

17:00:00: 发现控制节点存在10000+tap设备,link状态为DOWN(导致neutorn-openvswitch-agent启动失败) ;

16:40:00: 整体环境新建虚拟机分配网络失效,处理人忙乱之中再次重启neutron-dhcp-agent,发现循环错误,neutron-dhcp-agent重启失败;

16:30:00: 控制节点所属(6台)虚拟机网络失效怀疑是控制节点流表未刷新原因,开发同事尝试重启控制节点的neutron-openvsiwtch-agent刷新,但重启失败;

16:25:00: 开发开始定位处理开发同事检查虚拟机创建成功,但新建虚拟机无法获取neutron-metadata服务接口,导致业务集群配置失败;

16:10:00: 线上环境更新包功能失效项目维护同事反映培训环境更新业务应用集群失效;

2. 分析

2.1.不正常的循环

经过内部环境反复测试与现场日志对比,发现实际产生10000多个tap设备的循环位于neutron-dhcp-agent服务的定时同步功能函数中

下面这段整体逻辑由于一个设置namespace的异常而不断重试循环:

namespace

1) Neutron-dhcp-agent循环监听是否存在更新需求(need_resunc_reasons);

2) 每次循环延迟间隔conf.resync_interval秒;

3) setup_dhcp_port()方法中申请一个新的Port(新的tapid产生);

4) add_veth()方法创建veth设备(新的tap设备产生);

5) ensure_namespace()方法中确认namespace,如不存在则创建;

6) set_netns()方法设置tap设备的network namespace,设置失败;

7) 跳转到第一步循环;

可以看到,这段逻辑本身有一定缺陷

1) 设置namespace失败后,没有正确的try…catch流程删除之前创建的设备,导致失败的tap设备积累越来越多

2) ensure_namespace()方法只检查namespace是否存在,没有深入检查namespace权限等可能导致后续设置失败的属性

接下来的问题是(可能也是neutron在最后一步不设防的原因):namespace是neutron-dhcp-agent进程自身创建的,tap设备也是neutron-dhcp-agent进程自身创建的,为什么设置时会失败呢?

2.2.为什么没有权限

容器服务的隔离与共享

容器

其中各容器共享主机的Network namespace,但每个容器具备非共享的Mount namespace;在各自独立的Mount namespace中,共享主机/run/netns目录,用于共享虚拟网络的network namespace操作入口。

Mount

而在Docker实现中,(共享)使用外部存储空间、数据卷功能都最终会依赖mount系统调用,代码片段:

Docker对附加传入的private、shared等不同属性的处理,实际对应执行mount系统调用时传入不同flags,不同的flags对应到不同的Mount Propagation Type:

MS_SHARED

This mount point shares mount and unmount events with other mount points that are members of its “peer group”. When a mount point is added or removed under this mount point, this change will propagate to the peer group, so that the mount or unmount will also take place under each of the peer mount points. Propagation also occurs in the reverse direction, so that mount and unmount events on a peer mount will also propagate to this mount point.

MS_PRIVATE

This is the converse of a shared mount point. The mount point does not propagate events to any peers, and does not receive propagation events from any peers.

MS_SLAVE

This propagation type sits midway between shared and private. A slave mount has a master—a shared peer group whose members propagate mount and unmount events to the slave mount. However, the slave mount does not propagate events to the master peer group.

MS_UNBINDABLE

This mount point is unbindable. Like a private mount point, this mount point does not propagate events to or from peers. In addition, this mount point can’t be the source for a bind mount operation.

找到原因

由于启动容器时对/run目录没有使用MS_SHARED传播类型,容器重启后,之前创建的namespace文件会因/run/netns产生的`peer group`内其他peer的引用计数不能正常删除(Device or resource busy)

umount(“/run/netns/ns1”, MNT_DETACH) = -1 EINVAL (Invalid argument)

unlink(“/run/netns/ns1”)            = -1 EBUSY (Device or resource busy)

删除失败带来的后续结果是,由于文件系统层的namespace文件没有完全删除,而实际的networknamespace已经释放,所以这个“半删除”的namespace从用户态程序的角度就呈现出这样的状态:能够查看到namespace,对应2.1第5)步中ensure_namespace()操作正常,但set操作时会失败,对应2.1第6)步set_ns()操作失败

setns(4, 1073741824)                    = -1 EINVAL (Invalid argument)

write(2, “seting the network namespace /”ns”…, 60seting the network namespace “ns1” failed: Invalid argument

解决方法

修改neutron-*-agent容器的启动参数:

-v /run/netns:/run/netns:shared  -v /run:/run:rw

+ -v /run/netns:/run/netns:shared  -v /run:/run:rw:shared

2.3.确定触发场景

模拟再现故障

1) 创建网络和子网

neutron net-create –shared –provider:network_type vlan –provider:physical_network physnet1 –provider:segmentation_id 108 vlan108

neutron subnet-create –name subnet108 vlan108 192.168.0.0/24

2) 重启neutron_dhcp_agent容器

docker restart neutron_dhcp_agent

这时neutron_dhcp_agent所创建的namespace已被docker daemon和其他容器引用,权限已转移不允许neutron_dhcp_agent删除

3) 删除该网络的子网

neutron subnet-delete subnet108

neutron删除子网成功,但后台实际删除namespace失败,而且namespace命名空间已释放,但文件系统接口任然存在,处于“半删除”状态,直接删除网络不会触发后续异常。

4) 为该网络重新创建子网

neutron subnet-create –name subnet108 vlan108 172.16.0.0/24

neutron创建子网成功,这时后台应当重新创建namespace,但由于上一步删除namespace动作失败导致namespace非正常残留,所以这里跳过创建namespace动作,接下来为tap设备设置namespace的动作失败,开始进入2.1描述的循环状态。

用Docker模拟局部故障

1) 启动两个容器共享可操作network namespace文件系统

docker run -d –name testa -it –v /run:/run:rw -v /run/netns:/run/netns:shared –privileged –net=host nova-compute:latest bash

docker run -d –name testb -it –v /run:/run:rw -v /run/netns:/run/netns:shared –privileged –net=host nova-compute:latest bash

2) 在容器A中创建namespace ns1

docker exec -u root testa ip netns add ns1

3) 重启容器A

docker restart testa

4) 使用容器A删除之前创建的namespace

docker exec -u root testa ip netns del ns1

Cannot remove namespace file “/var/run/netns/ns1”: Device or resource busy

5) 使用容器A设置namespace

docker exec -u root testa ip netns exec ns1 ip a

seting the network namespace “ns1” failed: Invalid argument

3. 小结

3.1.经验总结

Mount陷阱

每个Mount namespace有自己独立的文件系统视图,但是这种隔离性同时也带来一些问题:比如,当系统加载一块新的磁盘时,在最初的实现中每个namespace必须单独挂载磁盘。为此内核在2.6.15引入了shared subtrees feature:“The key benefit of shared subtrees is to allow automatic, controlled propagation of mount and unmount events between namespaces. This means, for example, that mounting an optical disk in one mount namespace can trigger a mount of that disk in all other namespaces.” 每个挂载点都会标记Propagation type,用于决定在当前挂载点下创建/删除(子)挂载点时,是否传播到别的挂载点。功能同样带来潜在的复杂,如2.2描述的权限传播转移出乎使用者的预料。

目前容器技术的存储管理和使用最终都依赖Mount系统调用,后续的使用场景需注意。

应该温柔的重试

如果neutron的重试机制“聪明”一点,就不会累计产生越来越多的tap设备,也就不会造成实际用户可见的网络异常。更好的方式是采用随机化、指数型递增的重试周期,有时候系统出现的一个小故障可能会导致重试请求同时出现,这些请求可能会逐渐放大故障。在不同场景应该考虑限制某个请求的重试次数或者进程整体在单位时间内的重试配额。

谨慎对待重启

我们常使用重启服务“快刀斩乱麻”,解决一般性问题,但从这件事的处理经过来看,两次重启服务使问题影响范围不断扩大,而且在其他很多场景下,重启服务时程序会重新读取外部资源,也常常会暴露出很多已经潜在、但尚未产生影响的问题。所以,在生产环境应该避免草率重启,而且还应在服务可中断时间有计划的演练重启,以便提前发现、解决未来被动重启时才会暴露的问题。

容器化的利与弊

这次事故由容器启动参数的不正确使用引起,但同样因为容器利于部署应用的特性,现场较快的在其他节点部署恢复了网络服务,减少了业务中断时间。

3.2.事件还原

容器化

附录参考:

Docker基础技术——Linux Namespace http://coolshell.cn/articles/17010.html

Mount namespace and mount propagation http://hustcat.github.io/mount-namespace-and-mount-propagation/

Shared Subtrees https://lwn.net/Articles/159077/

Mount namespaces and shared trees https://lwn.net/Articles/689856/

Mount namespaces, mount propagation, and unbindable mountshttps://lwn.net/Articles/690679/

作者:崔昊之,云技术开发者和爱好者,Openstack化石玩家,Docker社区committer,就职于中电科华云,希望和大家分享云技术实践过程中有趣的事儿。

文章来自微信公众号:云计术实践

来源:本文由E8运维原创撰写,欢迎分享本文,转载请保留出处和链接!