耗尽的是这里,报错却出在那里
一句话总结
资源耗尽时,崩溃的不是造成泄漏的代码,而是其后需要资源的代码。所以 traceback 会指向无辜的模块,调查只能靠测量来收尾。
为什么需要它
“下午三点左右会挂”是资源耗尽类报告的典型说法。早上一切正常,与负载也不严格成正比,重启之后有一阵子没问题。最后这个条件是决定性的线索——如果重启能修好,就说明有东西在不断累积。
但打开日志,看到的却不是元凶,而是受害者。泄漏文件描述符的是会话处理器,而错误却出现在数据库连接处,或者启动子进程处,或者打开日志文件处。造成描述符泄漏的代码早已拿走了自己的那份,而在见底的那一刻,其后来申请资源的代码就失败了。
这种错位会把调查带向错误的地方。看到“sqlite 无法打开数据库文件”的消息,就去翻了几个小时的磁盘、权限和路径。文件完好,权限也对。只是那个进程再也打不开任何文件了而已。
工作原理
资源耗尽调查的骨架有三点。
1. 了解限制。Linux 为每个进程设置资源上限,在 Python 中用 resource 模块读取和设置。RLIMIT_NOFILE 是可同时打开的描述符数量,RLIMIT_AS 是地址空间的大小。正如 getrlimit(2) 所规定的,上限有软限制和硬限制两个值,软限制可以由进程在不超过硬限制的范围内自行调低。可以调低这一点在调查中派上了用场——十个小时之后才会遇到的底线,现在就能造出来。
2. 统计累积的东西。Linux 在 proc(5) 中为每个进程设有 /proc/<pid>/fd 目录,每个打开的描述符对应一个条目。统计条目数,就能知道当前持有多少个。改变请求数多次测量这个值,会得到几个数据点,如果这些点位于一条直线上,斜率就是每个请求泄漏的个数。斜率为 0,就没有泄漏。这就是把泄漏从“感觉”变成数据的方法。
3. 把症状和原因分开写。耗尽之后的错误消息,只会告诉你资源的种类,不会告诉你是谁用掉的。所以报告中要把两者分开写——观察到的症状列表,以及通过测量查明的原因。
한도 무엇을 막나 바닥났을 때의 얼굴
RLIMIT_NOFILE 열린 파일·소켓 수 OSError errno 24 (EMFILE),
그리고 자원을 쓰는 남의 코드의 오류
RLIMIT_AS 주소 공간 크기 MemoryError (트레이스백이 남는다)
커널 OOM 킬러 머신 전체의 메모리 SIGKILL (트레이스백이 남지 않는다)
该代码块中的韩文说明依次为:RLIMIT_NOFILE 限制的是打开的文件和套接字数量,耗尽时表现为 OSError errno 24(EMFILE),以及其他使用该资源的代码的错误;RLIMIT_AS 限制的是地址空间大小,耗尽时表现为 MemoryError(会留下 traceback);内核 OOM killer 针对的是整台机器的内存,表现为 SIGKILL(不会留下 traceback)。
在内存方面,尤其重要的区分就是这张表的最后两行。撞上限制的分配会以异常的形式抛出并留下 traceback,而被内核杀死时,进程会因 SIGKILL 消失,连最后一条日志都留不下。“日志中途中断”的报告,本身就是线索。不过也要同时写明,RLIMIT_AS 是地址空间的上限,与实际使用量并不相同——只做了映射却没有触碰的区域,同样会占用地址空间。
在现场相遇的样子
第一,搜索错误消息中的词。用“unable to open database file”搜索,会出现大量关于权限和路径的内容。这些都对,但与这起事件无关。怀疑资源耗尽的信号不是消息,而是模式——随着时间推移越来越糟,重启能修好,不同的模块轮流失败。
第二,凭一次观察就下结论。“现在有 900 个描述符”本身什么也说明不了。必须同时测量原来是多少个,以及请求增加时如何变化。只要有两个点,就能得出斜率。
第三,靠调高限制来掩盖。调高限制,只是把崩溃的时间从下午三点挪到晚上十点。不修复泄漏的一方,迟早会再次遇到。不过,调低限制作为调查工具非常有用——能把需要十个小时的复现缩短到几秒钟。
第四,只数文件。描述符不只是文件。套接字、管道、事件通知,以及启动子进程时短暂使用的东西,都共用同一个限制。所以耗尽的进程不是打不开文件,而是什么都做不了。
实际工作中真正重要的事
- 如果重启能修好,就有东西在累积。把这句话作为调查的出发点。
- 泄漏要用斜率来证明。一次观察不是数据。
- 调低限制,让底线提前到来。把复现时间从小时级缩短到秒级。
- 症状和原因分开写。traceback 所指向的模块通常是无辜的。
下一项实验要做什么
拿到会话处理器后,做出统计打开描述符数量的工具,改变请求数进行测量,用斜率求出每个请求泄漏的个数。调低限制,让底线提前出现来复现,并记录耗尽之后四种不同的操作各自以什么面孔失败。内存方面也用地址空间限制安全地复现,写下以异常形式收到与被杀死之间的差别,最后用同一个工具证明修复版的斜率为 0。