[SRE] 排查 Scala 文件描述符泄漏

[SRE] 排查 Scala 文件描述符泄漏

💡 原文英文,约700词,阅读约需3分钟。
📝

内容提要

某Scala系统因文件描述符耗尽导致DNS解析失败、访问AWS端点出错并挂起,触发EC2自动终止。排查发现进程fd限制为4096,且存在目录fd泄漏:删除目录后进程仍持有读取句柄。原因是代码调用Files.list返回惰性流却未关闭,正确做法是用try-with-resources或手动close释放fd。

🔎

延伸解读

文件描述符泄漏的连锁反应

文章显示,文件描述符耗尽会引发一系列连锁故障:首先导致DNS解析失败,进而无法访问AWS端点,最终服务挂起并触发ELB健康检查失败,EC2实例被自动终止。这种从资源泄漏到服务不可用的路径在分布式系统中具有典型性,提醒我们监控文件描述符使用率的重要性。

进程级限制与系统级限制的差异

排查中发现,系统最大文件描述符限制为65534,但进程的软硬限制均为4096。这种差异意味着即使系统资源充足,单个进程也可能因限制过低而提前耗尽fd。在容器化或高并发场景下,需要关注进程级别的ulimit设置,避免因默认值过小导致故障。

Files.list的惰性流与资源管理

文章指出,Files.list返回一个惰性流,底层通过opendir系统调用打开目录并持有文件描述符,直到流被关闭。如果代码未使用try-with-resources或手动调用close(),即使目录被删除,进程仍会保持对已删除目录的读取句柄,造成fd泄漏。这提醒开发者在使用返回Stream的API时,必须确保及时关闭资源。

❓

Q&A

为什么Scala系统会出现文件描述符耗尽导致DNS解析失败和AWS端点访问错误?

因为进程的文件描述符限制为4096,且存在目录文件描述符泄漏,导致打开的文件描述符数量超过限制,从而无法进行DNS解析和访问AWS端点,最终服务器挂起。

如何检查Linux进程的文件描述符限制?

可以使用命令 `ulimit -n` 查看系统的最大文件描述符限制,使用 `cat /proc/<pid>/limits` 查看特定进程的软硬限制。

Scala中Files.list方法为什么会导致文件描述符泄漏?

Files.list返回一个惰性流,该流会持有目录的文件描述符,如果不关闭流,文件描述符就不会释放,即使目录被删除,进程仍保持读取访问,导致泄漏。

如何避免Scala中Files.list引起的文件描述符泄漏?

必须关闭Files.list返回的流,可以使用try-with-resources结构或手动调用stream.close()来释放文件描述符。

文件描述符泄漏时,如何确认是哪个文件或目录导致的?

可以使用lsof命令,例如 `lsof -p <pid> -a -d <fd>`,查看进程打开的文件描述符,如果显示DIR类型且路径后带有(deleted),则表明是已删除目录的泄漏。

Files.list底层是如何打开目录的?

Files.list最终调用UnixFileSystemProvider的newDirectoryStream,进而调用native方法opendir0,最终触发C标准库的opendir函数,执行系统调用openat打开目录。

🏷️

标签

➡️

继续阅读