TT Lab
开始
学习 学习路径 课程

调试实战

一到下午就挂 — 用数据抓住耗尽的资源

在 TT Lab 中继续学习

目标

用斜率证明文件描述符泄漏,调低限制让底线提前出现来复现,并记录耗尽之后出现在无关位置的错误。内存方面也用地址空间限制安全地复现,写下以异常形式收到与被杀死之间的差别,并用同一个工具证明修复版的斜率为 0。

为什么重要

资源耗尽是症状不会指向原因的典型事件。泄漏描述符的是会话处理器,而错误却出现在数据库连接处。造成泄漏的代码早已拿走了自己的那份,而在见底的那一刻,其后来申请资源的代码就失败了。 所以这项调查不是读消息,而是测量。改变请求数测量打开的描述符数量,会得到几个数据点,它们的斜率就是每个请求泄漏的个数。斜率为 0,就没有泄漏。这就是把“感觉”变成数据的方法。 限制同时也是调查工具。软上限可以由进程自行调低,所以十个小时之后才会遇到的底线,现在几秒钟就能造出来。反过来,靠调高限制来掩盖,只会把崩溃的时间向后推。 评分器不会相信你的结论。评分器会另外做出一个确切知道每个请求泄漏多少个的处理器,实际接上你的测量工具,并直接核对斜率、限制和错误名称。这个个数每次运行都会变。

步骤

  1. 创建并运行 /root/exhaust/gen_exhaust.py,生成 /root/exhaust/leaky.py。
  2. 用 /root/exhaust/fdcount.py 统计存活进程的打开描述符数量。
  3. 用 /root/exhaust/measure_leak.py 改变请求数进行测量,把斜率保存到 /root/exhaust/trend.json。
  4. 用 /root/exhaust/run_under_limit.py 调低限制、让底线提前出现,并写入 /root/exhaust/nofile.json。
  5. 用 /root/exhaust/symptoms.py 收集耗尽之后的各种失败,写入 /root/exhaust/symptoms.json。
  6. 用 /root/exhaust/mem.py 复现地址空间限制,写入 /root/exhaust/mem.json。
  7. 用同样的工具测量修复版,在 /root/exhaust/fixed.json 中留下斜率 0 和通过的结果。
  8. 用 /root/exhaust/summary.json 和 /root/exhaust/exhaust_report.md 分四节进行报告。

参考

拿到会话处理器

创建并运行 /root/exhaust/gen_exhaust.py,生成 /root/exhaust/leaky.py。可以用 --requests 指定请求数,用 --fixed 运行修复版。

把这个脚本原样保存并运行即可。用 --requests 100 运行一次 leaky.py 试试。看起来什么事也没有——因为描述符只在进程内部累积,进程结束时内核会全部回收。

统计当前持有多少个

创建 /root/exhaust/fdcount.py,让它用 --pid <번호>(占位符为进程号)统计该进程的打开描述符数量,并输出包含 pid、open_fds 的 JSON。

Linux 在 proc(5) 中为每个进程设有 /proc//fd 目录,每个打开的描述符对应一个条目。统计工作就是数这个目录的条目数。也要事先规定,询问一个不存在的进程时如何应答。

用斜率求出每个请求泄漏多少个

用 /root/exhaust/measure_leak.py 分别在请求数为 10、40、80 时测量描述符数量,并在 /root/exhaust/trend.json 中保存 target、points、per_request、baseline。per_request 必须大于等于 1。

处理器在创建 ready 文件之后,会一直存活到 pause 文件出现。在这期间用 /proc 来统计即可。有两个点就能得出斜率,但打三个点还可以看它是不是直线。测量完之后,别忘了创建 pause 文件,让处理器退出。

调低限制,让底线提前出现

用 /root/exhaust/run_under_limit.py 在较低的 --nofile 之下运行 leaky.py,并在 /root/exhaust/nofile.json 中保存 nofile、cmd、exit_code、errno_name、stderr_tail、stdout_tail。errno_name 必须是 EMFILE。

resource.setrlimit 作用于自身进程以及之后产生的子进程。在 subprocess 的 preexec_fn 中调低,就能只把那条命令关进一间窄屋子。请求数要比限制大得多——必须碰到限制,才能看到底线。

症状并不指向原因

用 /root/exhaust/symptoms.py 耗尽描述符后,尝试 open、socket、subprocess、sqlite3 四种操作,并在 /root/exhaust/symptoms.json 中保存 nofile、held、observations 和 misleading_ops。misleading_ops 是错误消息中没有提到描述符的那些项。

这四种操作都需要描述符,但消息的面孔各不相同。尤其是 sqlite3,只说“无法打开数据库文件”。看到这条消息就去翻磁盘和权限,几个小时就这样消失了。请把掩盖了原因的那些项列成清单保存下来。

内存是怎样耗尽的

用 /root/exhaust/mem.py 分别复现超过地址空间限制的分配和没有超过的分配,并在 /root/exhaust/mem.json 中保存 over 和 under 两个结果以及 note。over 的 outcome 必须是 MemoryError,under 必须是 ok。

RLIMIT_AS 是地址空间的上限,所以超过限制的分配会以 MemoryError 抛出。会留下 traceback 这一点很重要——内核的 OOM killer 是 SIGKILL,不会留下任何痕迹。请在 note 中写明这一差别,以及地址空间与实际使用量并不相同。

证明修复版的斜率为 0

用同样的工具测量修复版(--fixed),并在 /root/exhaust/fixed.json 中保存 per_request_before、per_request_after、nofile、requests、exit_code_after。per_request_after 必须是 0,exit_code_after 必须是 0。

声称修好了,也必须用同样的测量来证明。斜率为 0,意味着请求增加而描述符不增加;如果在较低的限制下处理了大量请求仍然通过,就意味着没有碰到底线。请把两项证据一并留下。

把症状和原因分开写并报告

在 /root/exhaust/summary.json 中写入 per_request_before、per_request_after、nofile、errno_name、misleading_ops、mem_outcome,并在 /root/exhaust/exhaust_report.md 中分四节进行报告:## 무엇이 바닥났나、## 증상은 어디서 났나、## 어떻게 증명했나、## 남은 위험(韩文,依次意为“什么耗尽了”“症状出现在哪里”“如何证明的”“剩余风险”)。

报告的价值不在于“描述符泄漏了”,而在于“每个请求泄漏一个,修复之后是 0”。把症状列表和原因分开写,别让下一个人看到 sqlite 的消息就去翻磁盘。