现场回顾

故事发生于某个下午,采用 salt 更新某集群的 neutron.conf (log 相关配置项) 并批量重启 neutron-openvswitch-agent(以下简称 neutron-ovs-agent),不久便有人反馈云主机宕机。

立即排查发现云主机并没有宕机,只是网络不通,大部分计算节点的 ovs 流表空空如也。Nova 和 Neutron 打出 ERROR 级别的日志。

$ ovs-ofctl dump-flows br-bond   
NXST_FLOW repy (xid=0x4)
 cookies=0x0, duration=433.691s, table=0, n_packages=568733, n_bytes=113547542, idle_age=0, priority=1 actions=NORMAL
 cookies=0x0, duration=432.358s, table=0, n_packages=8418, n_bytes=356703, idle_age=0, priority=2, in_port=3 actions=drop

neutron-ovs-agent Log:

DeviceListRetrievalError: Unable to retrieve port details for devices because of error: Remote error: TimeoutError QueuePool limit of size 10 overflow 20 reached, connection timed out, timeout 10

neutron-server Log:

File "/usr/lib64/python2.6/site-packages/sqlalchemy/pool.py", ... 'TimeoutError: QueuePool limit of size 10 overflow 20 reached, connection timed out, timeout 10\n'

nova Log:

NeutronClientException: Request Failed: internal server error while processing your request.

上述信息可得出以下结论:

  • Neutron 日志表明 neutron-ovs-agent 通过 rpc 向 neutron-server 请求虚拟机 port 相关信息失败,失败的原因是 neutron-server 和数据库的连接数超出连接池的上限。
  • Nova 日志表明 neutron-server 无法响应 HTTP 请求。
  • 被清空的 ovs 流表导致虚拟机的网络瘫痪。

导火索

深入剖析之前,先了解该 Icehouse 集群的基本信息:该集群有 102 个计算节点,运行着 nova, neutron, glance,ceilometer 等服务,为了避免单点故障,我们去除了 neutron l3 等相关服务,采用大二层的网络,虚拟机通过物理路由与外界通信。理论上说,无论哪个服务异常,甚至任意节点宕机,最差的结果是 openstack 服务不可用或者少量虚拟机故障,但绝大部分虚拟机依旧能正常运行。

经验告知,采用以上网络模型的多个集群一年多以来从未发生如此规模的故障。由于 log 模块配置项不会影响流表,直觉上推测批量重启可能是触发 ovs 流表被清空的导火索。


Neutron 的一个坑

由于流表被清空前仅仅重启了 neutron-openvs-agent,而计算节点,仅仅只有 neutron-ovs-agent 与 ovs 有交互,故按图索骥的浏览 neutron-ovs-agent 重启流程,梳理其逻辑如下:

  1. 清除所有流表
  2. 通过 rpc 向 neutron-server 获取流表相关信息
  3. 创建新流变

不难发现,如果步骤 2 异常,比如 neutron-server 繁忙,消息中间件异常和数据库异常等种种因素,都会影响流表的重建,重则导致虚拟机网络瘫痪。事实上,社区也意识到了类似的问题:重启 neutron-ovs-agent 会导致网络暂时中断。

Restarting neutron ovs agent causes network hiccup by throwing away all flows

社区的处理方式是增加配置项 drop_flows_on_start,默认其值为 False,从而避免上述问题。该 Patch 已合入 Liberty,梳理其重启的逻辑流程如下:

  1. 用一个 cookie 标志当前流表
  2. 获取新流变并更新至 ovs
  3. 根据 cookie 清除旧流表

neutron-ovs-agent 在重启的过程中就流表的处理留下了一个隐患,直接的后果就是导致计算节点的流表被清理的干干净净,虚拟机成一个个孤立的点,而多种因素可触发该隐患。


最大连接数

现在解释为什么批量重启 neutron-ovs-agent 会触发上述隐患,重启过程中,neutron 日志报出如下错误:

TimeoutError: QueuePool limit of size 10 overflow 20 reached, connection timed out, timeout。

这条日志意味着 neutron-server 和数据库的连接数超出客户端连接池的上限,当 neutron-ovs-agent 批量重启时,上百并发的通过 rpc 向 neutron-server 请求构建流表的相关信息,neutron-server 和数据库的连接数也就远远超过 30,造成大量的请求失败,计算节点获取不到流表相关信息无法重建流表,故计算节点的流表为空。

要解决因 QueuePool 的限制而引起的 TimeoutError 问题也很简单,sqlalchemy 提供了两个配置项[1],把下面两个参数适当调大即可。

[database]   
max_overflow =   
max_pool_size =

Note: MySQL server 默认最大连接数为 100,当集群的规模上升时,需适当调整 MySQL server 最大连接数。


进程的性能问题

然而问题并没有完全解决,请注意 nova 的错误日志:

nova.compute.manager  NeutronClientException: Request Failed: internal server error while processing your request.

这是nova 发给 neutron-server 的一条 HTTP 请求,neutron 返回了 internal server error。Internal server error 对应的 HTTP status code 为 500,意味着 neutron-server 无法响应 nova 请求。为什么会无法响应呢,批量重启的过程中,发现 neutron-server 进程的 CPU 使用率为 100 %,意味着 neutron-server 正忙于处理 neutron-ovs-agent 大量的请求,因而无暇处理 nova 的 HTTP 请求。

解决该问题的方法同样很简答,增加更多的 neutron-server 进程数即可。事实上,因 nova-conductor 要处理 nova-compute 大量的 rpc 请求(nova-compute 通过 nova-conductor 访问数据库),自 Icehouse 起,nova-conductor 就默认启动了多个进程,进程的数目等同服务器 CPU 的逻辑核数。

$ workers=\`cat /proc/cpuinfo |grep processor |wc | awk '{print $1}'\`   

[DEFAULT]   
api_workers = $workers   
rpc_workers = $workers   

Python 的并发 & IO

值得思考的是,对双路 12 核共 24 线程的服务器来说,数百并发请求真的算高么?neutron-server 进程的 CPU 最大使用率只能到达 100 %,意味着 neutron-server 仅仅利用了服务器的一个线程。这里不得不提协程(crontine)[2],这种伪并发的“用户态线程” 意味着任意时刻只有一个协程在执行,即任意时刻只能利用服务器 CPU 的一个线程,而 openstack 所有组件的进程里满满都是协程(只有一个主线程),因而单个进程下,neutron-server 不能充分利用多核的优势以提高处理并发的能力。

传统的 web 服务器 apache 和 nginx 利用多进程多线程提高并发数(上下文切换的开销:进程 > 线程 > 协程)。那么问题来了,单进程条件下,是不是可以用线程替换协程从而提高 neutron 的并发能力呢?真实的答案是 No, 根本原因在于 python GIL[3],俗称 python 全局锁。对于 python 的程序来说,单进程只能在任何时刻只能占用一个物理线程,所有只有多进程才能充分利用服务器的多核多线程。

让我们深入一点,假设的 CPU 足够强大,是不是可以解决 neutron-server 单进程的并发问题呢?我觉得未必,这又回到了 IO 问题。Monkey patch[4] 虽然把系统的 socket 库替换成自己提供的非阻塞 socket 库,从而避免某些阻塞 IO 阻塞了整个进程(主线程)。但是 OpenStack 访问 MySQL 使用的是 libmysqlclient 库,eventlet 并不能对这个使用了系统 socket 的 C 库使用 monky_patch,所以对 MySQL CRUD 的时候会阻塞主线程,意味着 neutron-server 在访问数据库也容易出现性能瓶颈。

解决上述并发问题的方法之一就是启动更多的 api worker 和 rpc worker。在较新的版本,OpenStack 默认的 wokers 就是服务器的逻辑核数。另外一种办法是用 apache 替换 python http server,最近各个组件逐步提供了对 apache 的支持。


Reference

  1. http://docs.sqlalchemy.org/en/rel_0_9/core/pooling.html
  2. http://www.dabeaz.com/coroutines/Coroutines.pdf
  3. http://stackoverflow.com/questions/1294382/what-is-a-global-interpreter-lock-gil
  4. http://stackoverflow.com/questions/11977270/monkey-patching-in-python-when-we-need-it