一到下午就挂 — 用数据抓住耗尽的资源
目标
用斜率证明文件描述符泄漏,调低限制让底线提前出现来复现,并记录耗尽之后出现在无关位置的错误。内存方面也用地址空间限制安全地复现,写下以异常形式收到与被杀死之间的差别,并用同一个工具证明修复版的斜率为 0。
为什么重要
资源耗尽是症状不会指向原因的典型事件。泄漏描述符的是会话处理器,而错误却出现在数据库连接处。造成泄漏的代码早已拿走了自己的那份,而在见底的那一刻,其后来申请资源的代码就失败了。 所以这项调查不是读消息,而是测量。改变请求数测量打开的描述符数量,会得到几个数据点,它们的斜率就是每个请求泄漏的个数。斜率为 0,就没有泄漏。这就是把“感觉”变成数据的方法。 限制同时也是调查工具。软上限可以由进程自行调低,所以十个小时之后才会遇到的底线,现在几秒钟就能造出来。反过来,靠调高限制来掩盖,只会把崩溃的时间向后推。 评分器不会相信你的结论。评分器会另外做出一个确切知道每个请求泄漏多少个的处理器,实际接上你的测量工具,并直接核对斜率、限制和错误名称。这个个数每次运行都会变。
步骤
- 创建并运行 /root/exhaust/gen_exhaust.py,生成 /root/exhaust/leaky.py。
- 用 /root/exhaust/fdcount.py 统计存活进程的打开描述符数量。
- 用 /root/exhaust/measure_leak.py 改变请求数进行测量,把斜率保存到 /root/exhaust/trend.json。
- 用 /root/exhaust/run_under_limit.py 调低限制、让底线提前出现,并写入 /root/exhaust/nofile.json。
- 用 /root/exhaust/symptoms.py 收集耗尽之后的各种失败,写入 /root/exhaust/symptoms.json。
- 用 /root/exhaust/mem.py 复现地址空间限制,写入 /root/exhaust/mem.json。
- 用同样的工具测量修复版,在 /root/exhaust/fixed.json 中留下斜率 0 和通过的结果。
- 用 /root/exhaust/summary.json 和 /root/exhaust/exhaust_report.md 分四节进行报告。
参考
- 处理器约定:
python3 /root/exhaust/leaky.py --requests N [--fixed] [--dir D] [--ready-file F] [--pause-file P](占位符为各个参数)处理完请求后会创建 ready 文件,在 pause 文件出现之前保持存活,然后输出一行 JSON。存活期间可以测量描述符数量。 - 统计工具:
python3 /root/exhaust/fdcount.py --pid <번호>(占位符为进程号)输出一个包含 pid、open_fds 的 JSON。Linux 在/proc/<pid>/fd中为每个打开的描述符放置一个条目。 - 测量工具:
python3 /root/exhaust/measure_leak.py --target <처리기> --points 10,40,80 [--target-arg=--fixed] --out <json>(占位符依次为处理器、JSON 文件)输出 target、target_args、points(请求数与描述符数的配对)、per_request(斜率)、baseline。用--target-arg给出的值会原样传给处理器(用等号连接书写)。斜率为(最后的描述符数 - 最初的描述符数)/(最后的请求数 - 最初的请求数)。 - 限制工具:
python3 /root/exhaust/run_under_limit.py --nofile <상한> --cmd "<명령>" --out <json>(占位符依次为上限、命令、JSON 文件)只对该命令施加较低的上限并运行,输出 nofile、cmd、exit_code、errno_name、stderr_tail、stdout_tail。如果描述符已耗尽,errno_name 为EMFILE,否则为 null。 - 症状工具:
python3 /root/exhaust/symptoms.py --nofile <상한> --out <json>(占位符依次为上限、JSON 文件)在耗尽描述符之后,依次尝试 open、socket、subprocess、sqlite3 四种操作,输出 nofile、held、observations(op、error、errno)。 - 内存工具:
python3 /root/exhaust/mem.py --as-mb <상한> --alloc-mb <할당> --out <json>(占位符依次为上限、分配量、JSON 文件)输出 as_mb、alloc_mb、outcome(MemoryError 或 ok)、exit_code。 resource.setrlimit作用于自身进程以及之后产生的子进程。在即将启动子进程之前(preexec_fn)调低它,就可以不动父进程,只把那条命令关进一间窄屋子。- 常见错误:只测一次就说是泄漏,靠调高限制来掩盖,只按文件来统计描述符,试图读取已经结束的进程的 /proc。
- 本实验的假设:把斜率看作直线,是因为这个处理器结构简单,每个请求打开的数量相同。在实际服务中,必须多打一些点,先确认是不是直线。
- 不要编写负载测试。Pod 为 2 核、2Gi 内存,每次评分的预算是 60 秒。
拿到会话处理器
创建并运行 /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 的消息就去翻磁盘。