在实际失败的进程中诊断描述符耗尽
进程使用文件描述符访问的不只是普通文件。网络套接字、管道、事件接口和复制出来的引用,都要占用描述符表中的入口。RLIMIT_NOFILE 约束新分配可以使用的编号,其软值比允许的最大编号大一。因此,即使机器仍有充足磁盘空间和内存,服务也可能因为自己的表项额度用尽而无法继续接受工作。排查时必须先确认耗尽的是哪一种资源。 坏掉的样子 EMFILE 表示调用进程触及适用的描述符限制,ENFILE 则表示系统打开文件资源耗尽。这两种错误需要沿不同方向调查。提高机器级文件设置,不会自动改变失败进程的软值;扩大某个应用的限制,也不能修复系统级资源短缺。把日志里的错误名和实际限制对应起来,比看到“文件太多”就随意调整参数更有用。 耗尽还会破坏诊断。打开日志文件、枚举目录本身,也可能需要新的描述符。如果应用直到额度用光后才尝试读取 /proc/self/fd,检查动作就可能一起失败。已经存在的描述符通常仍可使用,因此预先建立的控制管道有价值:它让进程无需再分配资源,也能够报告当前错误,而不会把诊断失败误认为路径消失。 描述符数量持续增长,需要结合上下文解释。它可能反映正常并发增加、连接池逐渐填满,也可能是某些所有者忘记关闭引用。曲线本身不能证明泄漏。应同时比较描述符类型、工作负载,以及工作排空后的数量,再沿超出预期寿命的引用追踪所有权。只有把数量变化与资源生命周期关联起来,才能选择正确修复。 快速验证 把下面脚本保存为 exhaustion.py,在一次性 Linux 环境中运行 python3 exhaustion.py。此前记录的实测使用 Linux 6.12.76 和 Python 3.11.2。这里的描述符版本改用管道代替套接字,并为等待就绪设置截止时间。控制进程启动独立子进程,后者只把自身软上限降到三十二;继承的软硬值原本都必须允许这个设置: import os,resource,select,subprocess,sys before=resource.getrlimit(resource.RLIMIT_NOFILE) child=r''' import errno,os,resource,sys soft,hard=resource.getrlimit(resource.RLIMIT_NOFILE) assert soft>=32 and hard>=32 resource.setrlimit(resource.RLIMIT_NOFILE,(32,hard)) opened=[] def emfile(operation): try:operation() except OSError as error:assert error.errno==errno.EMFILE,error else:raise AssertionError('expected per-process descriptor exhaustion') try: while True: try:opened.append(os.open('/dev/null',os.O_RDONLY)) except OSError as error: assert error.errno==errno.EMFILE,error;break assert opened and max(opened)==31 emfile(os.pipe) emfile(lambda:os.dup(0)) emfile(lambda:os.listdir('/proc/self/fd')) print('exhausted',flush=True) assert sys.stdin.readline()=='continue\n' released=opened.pop();os.close(released) replacement=os.open('/dev/null',os.O_RDONLY);opened.append(replacement) assert replacement==released print('EMFILE affects open, pipe, dup, and self-enumeration; releasing one slot permits one open') finally: for fd in opened:os.close(fd) assert os.listdir('/proc/self/fd') print('after cleanup, descriptor inspection succeeds again') ''' process=subprocess.Popen([sys.executable,'-c',child],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True) try: assert select.select([process.stdout],[],[],10)[0], 'child readiness timeout' assert process.stdout.readline().strip()=='exhausted' descriptor=os.open('/dev/null',os.O_RDONLY);os.close(descriptor) print('controller can still open files while child is exhausted') process.stdin.write('continue\n');process.stdin.flush() output,error=process.communicate(timeout=10) assert process.returncode==0,error;print(output,end='') finally: if process.poll() is None:process.kill();process.wait() assert resource.getrlimit(resource.RLIMIT_NOFILE)==before 子进程重复打开 /dev/null,直到分配返回 EMFILE。它得到的最大编号为三十一,与软值三十二一致。新增打开次数少于三十二,因为标准流以及其他继承描述符已经占用表项。应观察整张描述符表,而不能把限制值理解为当前还能额外执行多少次 open,否则预算从开始就会漏掉既有资源。 ...