fbprophet屏蔽pystan输出时触发Too many open files报错的解决咨询
自定义输出屏蔽逻辑的问题根因
你遇到的OSError: [Errno 24] Too many open files不是__exit__里没关闭null设备文件导致的,核心是原实现存在文件描述符(fd)泄漏:
- 原类在
__init__里通过os.dup(1)、os.dup(2)备份标准输出、标准错误的流副本时,会生成两个独立的fd表项,但整个__exit__流程里,从来没有对这两个备份出来的fd执行关闭操作。 - 每执行一次
with suppress_stdout_stderr():的代码块,就会泄漏2个fd。按照单进程累计15万次模型拟合的规模,累积泄漏的fd数会达到30万,远超Linux默认单进程1024个fd的软上限,哪怕null设备的fd每次都正常关闭,最终也会把进程的fd配额占满,触发报错。 - 额外说明代码里的笔误:
Model类的__init__方法缺少self作为第一个参数,实例化时会直接抛出语法相关的错误,和本次fd泄漏问题无关但需要修正。
最小改动的修复方案
如果想保留原来自定义类的实现,只需要在__exit__的收尾部分,把两个备份用的fd也补上关闭操作即可:
def __exit__(self, *_): # 还原真实输出流 os.dup2(self.save_fds[0], 1) os.dup2(self.save_fds[1], 2) # 关闭之前打开的所有fd,补上之前漏写的备份fd关闭逻辑 os.close(self.null_fds[0]) os.close(self.null_fds[1]) os.close(self.save_fds[0]) os.close(self.save_fds[1])
更符合Python规范的优雅替代方案
不建议在每次模型拟合时都反复开关fd做流重定向,一方面容易出现资源泄漏,另一方面高频的fd系统调用会带来不必要的性能开销。针对大规模批量跑模型的场景,有两个更稳妥的实现思路:
方案1:全量fd托管的上下文实现
把所有打开的fd统一存放到列表中,通过finally块做强制资源回收,避免漏关任意一个fd:
import os from contextlib import contextmanager @contextmanager def suppress_c_ext_output(): opened_fds = [] try: # 打开devnull设备 devnull_fd = os.open(os.devnull, os.O_RDWR) opened_fds.append(devnull_fd) # 备份原始stdout/stderr saved_stdout = os.dup(1) saved_stderr = os.dup(2) opened_fds.extend([saved_stdout, saved_stderr]) # 重定向系统级输出到devnull,拦截pystan这类C扩展的直接fd写入 os.dup2(devnull_fd, 1) os.dup2(devnull_fd, 2) yield finally: # 先还原真实输出流 os.dup2(saved_stdout, 1) os.dup2(saved_stderr, 2) # 从后往前关闭所有打开的fd,不管流程中有没有异常都能完成回收 for fd in reversed(opened_fds): os.close(fd)
方案2:全局一次性重定向
既然所有Prophet拟合产生的冗余输出都需要屏蔽,完全不需要在每次拟合时临时操作流,程序启动时做一次全局重定向即可。整个进程生命周期只需要固定占用3个fd,根本不会出现累积泄漏:
import os from contextlib import contextmanager # 程序启动时一次性初始化全局流 devnull_fd = os.open(os.devnull, os.O_RDWR) original_stdout = os.dup(1) original_stderr = os.dup(2) # 全局把系统级stdout/stderr切到devnull,所有pystan的输出会直接被吞掉 os.dup2(devnull_fd, 1) os.dup2(devnull_fd, 2) # 临时切回真实控制台的上下文,用来打印自己的程序日志 @contextmanager def console_log(): try: os.dup2(original_stdout, 1) os.dup2(original_stderr, 2) yield finally: os.dup2(devnull_fd, 1) os.dup2(devnull_fd, 2) # 之后所有Prophet拟合代码不需要再套任何屏蔽输出的上下文,直接运行即可 # 需要打印自己的进度日志时临时切回控制台: for ticker in tickers: with console_log(): print(f"开始处理标的 {ticker}") # 下面的拟合操作输出会被全局屏蔽 m = Prophet() m.fit(history)
关于调高系统fd上限的说明
直接修改Ubuntu系统的最大可打开文件数,本质上是延后问题爆发的时间,并没有解决fd泄漏的根源。哪怕把上限调到65535,按照原实现2个fd/次的泄漏速度,跑3万次还是会把配额占满,而且泄漏的fd会持续占用进程内存,拖慢批量计算的性能,不建议作为长期解决方案。
内容的提问来源于stack exchange,提问作者Lorenz Weidinger
相关产品推荐
相关产品推荐

