文章标题旁是三个相连面板,英文标签依次为 Observe、Decide 和 Act。

在实际失败的进程中诊断描述符耗尽

进程使用文件描述符访问的不只是普通文件。网络套接字、管道、事件接口和复制出来的引用,都要占用描述符表中的入口。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,否则预算从开始就会漏掉既有资源。 ...

十月 27, 2026 · 1 分钟 · XB Field Notes
标题卡配有三个相连面板,标注 OBSERVE、DECIDE 和 ACT。

排查 OOM 应先确认分配范围

当一次分配在相关限制范围内无法满足,适当的回收尝试也不能解决问题时,Linux 可能调用内存不足终止机制。它是最后阶段的恢复手段,不是理解业务重要性的调度器。有些分配会直接失败而不杀进程,配置也可能使系统进入恐慌处理。因此,排查应先识别分配范围与实际事件,不能一开始就认定整台机器的每一字节内存都已经用完。 先看范围,再看评分 全局内存短缺与内存控制组短缺是两回事。某个控制组可以在主机仍有可用内存时达到自身上限;它的 OOM 处理不能任意挑选组外进程来解决组内问题。允许使用的内存节点等其他限制也可能影响分配。因此,主机剩余内存图表不足以证明或否定某次失败,更不能在没有检查事件范围时,把所有容器异常都归类为全局容量不足。 在适用的选择规则内,内存占用与调整值会影响牺牲者选择。内核并不知道哪个进程商业价值高、哪个空闲服务可以丢弃,也不知道哪一个业务必须持续可用。一个体积很大的数据库可能被选中,即使压力也来自许多小工作者。procfs 中的 oom_score 是可观察数值,不是承诺把所有可见进程排序以后,就能准确预测任何内存域下一次会杀谁。 理解调整值,不许诺必然存活 oom_score_adj 的范围为负一千到正一千。正值增加被选中的倾向,负值降低它;负一千会把该任务排除在这项内核 OOM 选择之外。一般负值并不保证存活,这个设置也不能阻止显式信号、服务管理器或独立用户态内存压力策略终止任务。调整分值不会增加容量,也不会修复泄漏。把它解释成“重要进程已经免疫所有内存故障”,会造成危险的运维误判。 保护所有任务不是可行的容量方案。如果没有合适的牺牲者,不能假定内核会越过控制组边界,去杀一个无关的监管进程。整组终止与恐慌设置需要分别分析。调整值可以被子进程继承,而持久服务设置应归入监管器配置;修改之前需要明确授权并验证适用范围。下面实验不会改动这些值,也不制造内存压力,避免把接口学习变成真实故障。 坏掉的样子 反复重启可能掩盖持续泄漏。如果同一个工作负载不断增长直至失败,增加内存也许只是延长两次失败之间的间隔。应关联分配速度、常驻和记账内存、负载大小、回收及交换活动。单个进程的常驻大小不能完整表示控制组全部费用或共享内存关系。降低上限可以约束增长范围,却也可能让应用更早失败;不能只观察邻居未被终止,就认为业务恢复问题已经解决。 退出状态一百三十七与 SIGKILL 相符,但并不是 OOM 专属证据。应查看运行时的终止原因、控制组内存事件计数和操作员可见的内核记录。分配失败、进程被杀、内核 OOM 和用户态压力管理器终止,是需要分开的情况。看不到内核日志,也可能只是当前观察范围受限,不能直接证明没有事件发生。诊断必须说明证据来自哪里、有哪些缺口。 评分会随内存和范围变化。读取 procfs 的过程中进程可能退出,容器内的进程视图也可能隐藏主机的大部分任务。一次成功读取并不是原子的整机快照。分析真实失败时,应保留事件时间与受影响的控制组,然后比较相同范围内的观察,而不是拿事后另一个进程的分数去解释之前的牺牲者选择。 快速验证 保存为 oom_observe.py,使用 Python 3.9 或更新版本执行 python3 oom_observe.py。程序只读取自身进程的两个评分文件,不创建压力负载,不修改任何限制或策略。两条确定性断言仅对明确虚构的记录测试展示顺序,实时读取和模型检查会分别报告结果,避免把一种成功误当成另一种验证。 """Read-only observations and a sorting test, not an OOM predictor.""" from pathlib import Path import os def snapshot(pid): base = Path('/proc') / str(pid) try: return (int(pid), int((base/'oom_score').read_text()), int((base/'oom_score_adj').read_text())) except (OSError, ValueError): return None def ranked(rows): return sorted(rows, key=lambda row: (-row[1], row[0])) fixtures = [(42, 700, 0), (17, 100, -500), (99, 700, 200)] assert [row[0] for row in ranked(fixtures)] == [42, 99, 17] assert ranked([]) == [] row = snapshot(os.getpid()) if row is None: print('SKIP: this process has no readable Linux OOM score files') else: pid, score, adjustment = row assert -1000 <= adjustment <= 1000 print(f'pid={pid} oom_score={score} oom_score_adj={adjustment}') print('Observed one process; no victim selection or memory pressure tested') print('PASS: deterministic sorting checks; live observation is reported separately') 虚构记录按分数降序排列,同分时按数字进程号排序,低分记录排在最后。代码没有根据虚构调整值重新计算内核评分。Linux 分支单独打印当前进程的分数和调整值;文件不可读时报告 SKIP,但仍执行排序断言。因此,末尾 PASS 不意味着实际的 OOM 处理已经发生,更不是一次牺牲者选择实验。 ...

十月 26, 2026 · 1 分钟 · XB Field Notes
文章标题旁是三个相连面板,英文标签依次为 Observe、Decide 和 Act。

防火墙策略必须匹配报文路径、地址族和链顺序

防火墙在网络栈的特定位置执行报文策略。一条有用规则因此不只包含端口,还要明确地址族、挂钩、方向、连接状态和相对其他规则的位置。nftables 基础链按指定优先级接入 Netfilter 挂钩。报文实际经过的路径,决定它会遇到哪些裁决,而不是运维文档中描述的先后顺序。审阅策略必须保留这些执行上下文。 坏掉的样子 规则位于错误挂钩时,预期流量可能完全不受影响。INPUT 处理交付给当前命名空间本地的流量,FORWARD 处理经过它转发的流量,OUTPUT 面向本地产生的流量。宿主的容器转发路径,不一定就是本地输入路径。假定宿主级规则保护所有附近服务之前,应先查清实际流量经过哪个命名空间和挂钩。 地址族和方向错误也容易被掩盖。允许服务 TCP 目的端口,不同于匹配通常临时分配的客户端来源端口。只覆盖 IPv4 的策略不会自动覆盖 IPv6;IPv6 通信还依赖邻居发现等 ICMPv6 功能,丢弃所有非 TCP 报文,可能让 TCP 端口明明被允许的连接仍然失败。端口规则不能脱离链路和协议所需控制流量解释。 nftables 的 accept 不一定是所有基础链中的最终裁决。同一挂钩上更晚优先级的另一条链,仍能丢弃报文;drop 则会结束其遍历。只审阅一个表或一条链,可能看到允许计数器增长,却无法解释客户端为何失败。应查看全部适用规则集及其挂钩优先级,不能把第一条可见 accept 当成系统已经最终放行的证明。 快速验证 将脚本保存为 check-firewall.py,只在获得授权、装有 Docker 的一次性虚拟机中执行 python3 check-firewall.py。此前实验使用 nftables 1.0.6 和 Python 3.11.16,本轮审阅没有重新执行。两个命名空间通过一次性 veth 使用 IPv4 与 IPv6 通信。NET_ADMIN 和 SYS_ADMIN 是重要权限;规则限定在命名空间内,并不让容器变成可靠安全边界。设置失败时不要削弱宿主保护。镜像摘要也不会冻结 apt 仓库,指定版本的软件包以后可能无法取得: ...

七月 1, 2026 · 3 分钟 · XB Field Notes