数据由语言缓冲进入页缓存和存储;独立读取者从页缓存分支读取,早于显式 fsync 边界。可见性不能证明持久完成。

写回把缓冲可见性与存储完成分开

缓冲文件 I/O 可以在不同时间经过语言缓冲区、内核页缓存和存储栈。被修改的缓存页需要写回,使数据进入后端文件系统与设备。分开这些阶段可以合并工作、提高吞吐,但也意味着另一读取者看到新字节,本身不能证明已经完成可经历崩溃的存储边界。理解性能和持久性时,需要先确定观察的是哪一层完成。 坏掉的样子 缓冲写入不保证瞬间返回。分配、内存压力、脏页限制或文件系统活动,都可能让写者等待。如果生产速度持续超过存储处理能力,脏数据不能无限增长,压力最终会反映到工作负载。把每次 write 都描述为单纯内存复制、所有 I/O 都在之后发生,会错误解释持续负载下的延迟尖峰。 语言层 flush 与文件系统同步含义不同。flush 把应用缓冲数据提交到下一层,fsync 则请求文件同步边界。普通关闭文件,或者从另一个描述符读到相同字节,不能替代这一显式契约。因此只测成功写入的基准,可能测到的是缓存接受速度,而不是稳定、持久的存储吞吐,需要与实际工作要求对应。 写回设置也不是逐文件持久性截止时间。后台阈值促使刷新线程工作,更高的脏页阈值影响生成写入的进程,周期参数则控制唤醒或较老脏数据何时符合处理条件。设备进展、文件系统行为和竞争负载仍会影响结果。不能仅设一个时间间隔,就证明每次已确认更新在这么多秒后必定安全。 原文读取者在显式 fsync 前看到字节;可见性与系统 Dirty 计数不能建立逐文件持久性。 快速验证 把脚本保存为 writeback.py,在装有 Python 和 strace 的 Linux 环境中运行 python3 writeback.py,并让临时目录位于 ext4。实测使用 Linux 6.12.76、Python 3.11.2 和 strace 6.1。样本写入约四 MiB、读取记账信息,不改变全局脏内存参数或缓存控制: ...

十一月 26, 2026 · 2 分钟 · XB Field Notes
交换占用用保存的冷页表示,间隔流量用双向页面箭头表示,内存压力用任务停顿标记表示;三种观察回答不同问题。

Swap 用回收成本换取驻留空间

Swap 为 Linux 中适合换出的页面提供后备存储,让暂时不需要驻留的数据离开物理内存,把 RAM 留给活跃工作。但它不会让超出容量的工作集继续拥有内存访问速度。真正要问的是:回收是否腾出了空间,同时没有反复阻塞重要任务。交换占用、交换流量与应用进展,分别回答这个问题的一部分,不能拿其中一个数字概括全部情况。 分清哪些页面可以移动 匿名内存经常使用交换,但并非唯一情况。配置允许时,tmpfs 和共享内存页面也能换出。普通文件支撑的干净页面通常可以丢弃,以后从原文件重读;脏文件页则需要适当写回。并非所有分配都可以回收,被锁定而禁止换页的内存还有另外约束。因此,“所有 malloc 分配的东西”不足以描述内核实际上能够移走哪些内容。 vm.swappiness 在零到二百的文档范围内,表达交换与文件系统换页之间的相对成本;它不是达到某个 RAM 使用百分比后才开启交换的阈值。设为零也不等于在所有情形下禁用交换。如果压缩交换或更快的交换设备比文件系统换页便宜,大于一百的值可能合理,但适当取值取决于工作负载与存储。解释图表之前,应先读取实际配置,而不是默认每台机器都采用相同策略。 zram 提供压缩内存块设备,可以配置为交换;zswap 则在交换页与后备交换设备之间提供压缩缓存。压缩本身消耗 RAM 和 CPU,效果也依赖数据内容。两者都没有通用的延迟、压缩比或安全超配比例。zswap 不是可以直接挂到 /tmp 的文件系统;tmpfs 挂载具有自己的存储语义和容量约束,不能把名称相近的内存机制当成同一项功能。 坏掉的样子 反复回收马上又要使用的页面,会造成严重停顿。原因可能涉及存储延迟与吞吐,也可能涉及回收工作、压缩开销、控制组限制和竞争 I/O。颠簸并不保证最终触发 OOM 杀进程,固定的交换速率也不是普遍适用的诊断线。应把内存压力与有效吞吐、响应时间放在同一个观察窗口比较,再决定主要瓶颈在哪里。 交换已用量很大,可能只是早先事件留下的冷页面,不证明当前正在发生故障。相反,交换流量很小也不证明内存足够:交换可能未启用,工作负载可能碰到控制组上限,或者回收主要发生在文件页。主缺页计数上升也包括文件支撑的缺页,不能直接重新命名成换入次数。把相关指标当成同一个计数器,会让排查走向相反结论。 计划中的交换文件可能因为布局或文件系统限制而无法启用。带空洞的文件不适用,但复制是否产生空洞,取决于具体工具、选项和源文件,不能一概而论。应检查已经激活的交换清单,不要假定配置文件里写过一行就已经生效。也不要仅为重现一篇诊断文章,就在活动机器上启停交换或调整容量。 仅凭交换占用不能判断当前压力;间隔流量与 PSI 分别量化活动与停顿的不同方面。 快速验证 在安装了 util-linux 和 procps 的 Linux 环境运行这些只读命令。检查只观察当前环境的三个采样间隔,不制造内存压力,也不更改内核设置。机器没有启用交换也是有效观察结果,并不是实验失败;它只是告诉你不能依靠这些交换设备缓解未来的页面回收需求: ...

十一月 25, 2026 · 1 分钟 · XB Field Notes
三块原始页符号变为一块压缩载荷与独立开销符号,全部位于 RAM 边界内;这是示意比例,不是实测容量收益。

zram 用压缩的方式在内存里挤出交换空间

内存吃紧时,符合条件的冷页可能被回收到交换空间。作为交换设备的 zram 把页面压缩后放在 RAM 中,以处理器工作和内存开销换取潜在余量。收益依赖工作负载,不是一份固定增加的内存,也不保证每台机器都只有很小的处理器成本。 内存里的压缩块设备 zram 是驻留在 RAM 中的压缩块设备。配置为交换时,它可以保留符合条件的页面,避免普通磁盘交换输入输出。但可选写回功能能够使用另行配置的后备设备,因此“永不接触磁盘”不是通用性质。压缩可能降低延迟,具体仍取决于算法、处理器负载和数据。逻辑设备容量与真实 RAM 占用必须分开观察,二者也都不能直接替代系统剩余内存。 压缩就是这笔交易 「压缩」就是这笔交易赚头所在,也是「一台小机型显得能装下比物理内存更多内容」的根源。一页值不值得压要看内容:全零页、文本和有结构的数据都缩得不错,而图片、压缩包这类已经被压缩过的对象几乎缩不动。站在工程视角看,zram 等于把暂时闲置的 CPU 换算成「看似更多」的内存;当工作负载多半是可压缩的、又特别想避开真实磁盘换页的延迟时,这笔交易才划算。 适合服务器与低端设备 它最合身的地方是那些内存不多、换页压力又不过度的机器:小 VPS、容器宿主机,以及手机、路由器这类如今要把袖珍机器安排得明明白白的角色。反过来,内存本来就充裕、或者本来就要换到冷冷盘上的服务器,同一笔钱买一张真实磁盘往往能装下更多,因此未必轮到 zram。实用的判断方法是盯住压缩率和 swap 使用量:当驻留数据始终很小、又够可压缩、且 CPU 相对空闲,才是让 zram 出场的时刻。 坏掉的样子 压缩效果差可能消除预期收益。照片、视频、加密缓冲和已压缩归档可能几乎缩不动。应比较 orig_data_size 与 compr_data_size,同时查看包含分配器碎片和元数据的 mem_used_total。池占用、可用 RAM、负载需求与压力仍然重要。接近一的压缩比不能决定是否发生 OOM,也不能预测其时间;冷页驻留与回收行为同样影响结果。不能把一组字节比率当成故障倒计时。 昂贵的压缩算法可能增加处理器占用与交换延迟。可用算法及参数取决于内核构建,应在代表性压力下比较 lzo-rle、lz4 或 zstd 等候选,而不是断言某一个总适合交互机器。系统态占用高或 kswapd 忙碌,也不能单独证明压缩器就是原因。修改策略前,应把应用延迟、有效进展、压缩统计和回收活动放在同一观察窗口,保留原配置并验证实际变化。 ...

十一月 24, 2026 · 3 分钟 · XB Field Notes
实验先阻塞信号:五次 SIGUSR1 合并为一次待处理,三次 SIGRTMIN 保持三个排队实例。实时信号的队列资源仍然有限。

信号具有处理方式、掩码和投递规则

信号依据内核规定的投递规则,把事件通知给进程或线程。信号处理方式可以选择默认动作、安装处理函数,或者在允许时忽略;线程掩码则能阻塞符合条件的信号,让它们暂时保持待处理。当程序似乎漏掉重复通知,或者没有按预期停止时,需要分别检查处理方式、阻塞状态和待处理状态,不能把这些不同层面统称为“有没有收到信号”。 线程掩码、待处理状态和处理方式分别回答不同问题。应观察目标实际响应与清理;发送成功、熟悉的信号名称或某种返回码表示,都不能单独证明工作完成。 坏掉的样子 普通信号不是可靠事件计数器。同一种标准信号已经处于待处理状态时,随后多次发生可能合并。如果应用把每次 SIGUSR1 都理解为一个排队任务,就可能丢失这个计数关系。实时信号具有排队语义,但资源仍然有限,也需要明确的应用协议;不能仅因为名字里有实时,就假定它适合无限量承载业务消息。 信号名称不保证应用功能。某些守护进程约定 SIGHUP 表示重载,但它的默认动作是终止。SIGTERM 默认也会终止,只有应用实现了相应行为,它才成为优雅关停请求。发送成功不能证明配置已经重载,也不能证明在途工作已经排空。控制方必须观察目标实际响应,而不是把投递结果当作业务操作结果。 SIGKILL 与 SIGSTOP 不能捕获、忽略或阻塞。KILL 不给应用执行清理的机会,但也不应被描述为在每种内核状态下都保证瞬时释放资源。监督者仍需观察实际完成。使用符号名称也更清晰,不能假定所有平台上的信号编号都完全一致;信号数字与进程退出状态还可能由工具采用不同方式表示。 快速验证 把下面脚本保存为 signals.py,在一次性 Linux 环境中运行 python3 signals.py。原始记录使用 Linux 6.12.76 和 Python 3.11.2;2026-10-15 的复核记录了增加就绪超时的实验在 Linux、Python 3.12.14 上通过;此次未执行信号实验。脚本只向自身和自己创建的子进程发送信号,恢复原始掩码,并把清理标记限制在临时目录中: from pathlib import Path from tempfile import TemporaryDirectory import errno,os,select,signal,subprocess,sys watched={signal.SIGUSR1,signal.SIGRTMIN} old=signal.pthread_sigmask(signal.SIG_BLOCK,watched) try: for _ in range(5):os.kill(os.getpid(),signal.SIGUSR1) assert signal.SIGUSR1 in signal.sigpending() assert signal.sigwait({signal.SIGUSR1})==signal.SIGUSR1 assert signal.sigtimedwait({signal.SIGUSR1},0) is None for _ in range(3):os.kill(os.getpid(),signal.SIGRTMIN) for _ in range(3):assert signal.sigtimedwait({signal.SIGRTMIN},0).si_signo==signal.SIGRTMIN assert signal.sigtimedwait({signal.SIGRTMIN},0) is None print('five blocked standard signals coalesce into one; three blocked real-time signals remain queued') finally:signal.pthread_sigmask(signal.SIG_SETMASK,old) for sig in [signal.SIGKILL,signal.SIGSTOP]: try:signal.signal(sig,signal.SIG_IGN) except OSError as error:assert error.errno==errno.EINVAL else:raise AssertionError('uncatchable signal accepted a disposition change') old=signal.pthread_sigmask(signal.SIG_BLOCK,{signal.SIGKILL,signal.SIGSTOP}) assert not {signal.SIGKILL,signal.SIGSTOP}&signal.pthread_sigmask(signal.SIG_BLOCK,set()) signal.pthread_sigmask(signal.SIG_SETMASK,old) print('SIGKILL and SIGSTOP cannot be ignored or added to the blocked mask') worker=r''' from pathlib import Path import signal,sys if int(sys.argv[1]):signal.signal(signal.SIGTERM,lambda *_:sys.exit(0)) try: print('ready',flush=True) while True:signal.pause() finally:Path(sys.argv[2]).write_text('cleanup') ''' with TemporaryDirectory(prefix='xbcatm-signals-') as directory: for name,sig,handled,expected,cleaned in [ ('hup-default',signal.SIGHUP,False,-signal.SIGHUP,False), ('term-default',signal.SIGTERM,False,-signal.SIGTERM,False), ('term-handled',signal.SIGTERM,True,0,True), ('kill',signal.SIGKILL,True,-signal.SIGKILL,False)]: marker=Path(directory)/name process=subprocess.Popen([sys.executable,'-c',worker,str(int(handled)),str(marker)],stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True) try: assert select.select([process.stdout], [], [], 5)[0], 'child readiness timed out' assert process.stdout.readline().strip()=='ready' os.kill(process.pid,sig);output,error=process.communicate(timeout=5) assert process.returncode==expected,(name,process.returncode,error) assert marker.exists()==cleaned,name print(name,'returncode',process.returncode,'cleanup',cleaned) finally: if process.poll() is None:process.kill();process.wait() 脚本先阻塞 SIGUSR1,向自己发送五次,再确认它待处理。一次同步等待取走信号后,第二次零超时等待没有结果。先阻塞再发送,让这个对照具有确定性。它证明的是待处理普通信号的合并规则,而不是根据异步回调碰巧打印几次,去猜测信号是否丢失或程序调度是否足够快。 ...

十一月 23, 2026 · 2 分钟 · XB Field Notes
文件经加载器漏斗进入内核;另一条后续写入者箭头可以改变运行值。

sysctl 在运行中调整内核

在 Linux 上,sysctl 通过 /proc/sys 暴露一部分内核参数。它可以观察当前运行状态;有权限的写入则能在不重启的情况下改变状态。配置文件是另一层东西:它要求某个加载器在特定时刻执行赋值。混淆这两层,就会遇到常见故障:磁盘上的配置看起来正确,内核实际使用的却是另一个值。 本文限定 Linux、procps-ng 和 systemd。BSD、macOS 也提供名为 sysctl 的命令,但接口和持久化方式不同。在 macOS 上数出多少个键,不能作为 Linux procfs 接口的验证证据。 先确认正在读取哪个接口 对普通名称来说,kernel.ostype 对应 /proc/sys/kernel/ostype;procps 也接受斜杠分隔符。若名称本身含有字面意义的点号,应遵循工具的分隔规则,不能盲目把所有点替换成斜杠。可用键取决于内核配置、已加载模块和执行环境。不同机器的键集合不保证相同,同一台机器运行期间也可能变化。 procps 的普通输出是 key = value,-n 则只打印值。一次读取成功,说明调用者在当前环境看到了这个值,并不证明持久策略已经生效。有些值还会在观察期间变化。排查时只读取所需键,不能把所有读取都当成无副作用操作:手册明确说明,读取 vm.stat_refresh 会刷新每个 CPU 的统计,因此普通枚举会跳过它。 持久化由具体加载器完成 运行时赋值不会替你生成持久配置文件;反过来,保存配置片段也不会立即改变内核。讨论优先级之前,先确定系统在启动或重载时由哪个组件应用设置。 procps 的 --system 会先根据目录优先级选出文件:高优先级目录中的同名文件遮蔽低优先级副本。选中的片段随后按文件名的字典序应用,不按目录逐层决定最终值;最后再读取 /etc/sysctl.conf。systemd 的 sysctl.d 加载器也结合目录优先级和文件名排序,但不直接读取 /etc/sysctl.conf。发行版仍可能通过配置目录中的链接把这个文件纳入加载范围,因此要检查实际安装,不能只凭路径名称下结论。 ...

十一月 22, 2026 · 1 分钟 · XB Field Notes
每次调用 logrotate 可把符合条件的活动日志移入第一代,再把前一代变为压缩的第二代。保留两代与大小阈值不会持续限制磁盘占用。

Logrotate 在执行时应用轮转策略

Logrotate 在被调用时读取配置,轮转符合条件的日志,并执行选定的保留与压缩动作。何时检查由定时器或其他调度器决定,它不会持续限制文件大小,改名也不会自动重定向应用的打开描述符。完整设置需要把检查频率、文件策略和写者实际重开行为结合起来,才能判断日志确实按预期流转。 文件框表示代数与描述符目标,不表示时间或字节比例。HUP 在此有效是因为样例显式实现了重开,并非所有应用的共同约定。CLI 结果均为历史记录,本次未重跑。当前文章审核仍待完成;技术与执行检查仍受阻。 坏掉的样子 两次执行之间可能超过 size 阈值,突发写入或调度停止时尤其明显。rotate 限制的是保留代数,不是历史时间,也不覆盖全部临时磁盘使用。因此,阈值乘代数不是硬容量保证;活动文件、检查间隔内增长,以及压缩或复制工作,都需要空间。只看到配置里有两个有限数字,不能证明磁盘不会耗尽。 create 指定替代文件的模式和所有权,不会向应用发送信号。postrotate 可以调用受支持的重开机制,但 HUP 并不普遍表示重载。delaycompress 只把压缩推迟一个轮转周期,无法修复从不放弃旧 inode 的写者。每项指令负责不同动作,需要核对它们是否共同完成实际交接。 脚本可能在文件操作已经成功后失败。Logrotate 返回非零,不会回滚全部改名和创建;未检查文件及写者状态就反复强制轮转,可能再次切分或删除需要的代数。应诊断已经发生的部分结果,而不是把失败理解成没有变化。错误恢复也必须依据当前事实,而不能只看最后退出码。 快速验证 2026-10-19 修复草案:修订后的代码只进行了静态检查与离线命令/文件状态替身测试,没有执行 logrotate 或发送信号。技术与实际运行验证仍然受阻。原始示例的历史运行使用 Linux 6.12.76、Python 3.11.2、logrotate 3.21.0 与 ext4;这些观察都不能验证本次修订。 未来执行时,应把代码保存为 logrotate-cli.py,置于另行授权的一次性 Linux 实验环境,预装 Python 3.11+、logrotate 3.21.0 和 gzip,不挂载宿主目录,也不承载其他工作。独立核实边界后,才可用 root 执行 python3 logrotate-cli.py --isolated-disposable-lab。参数与 root 身份是前提,不是隔离证明;结束后应丢弃整个环境。 ...

十一月 21, 2026 · 4 分钟 · XB Field Notes
持久化定时器恢复活动后,错过的三个日历时刻合并为一次补触发。

为 systemd 定时器选对时钟和补跑规则

一份计划必须先说清楚“时间”是什么意思。“纽约每天早上九点”指向民用日历,“任务结束十五分钟后”则指向经过时长和一个参考事件。systemd timer 同时支持这两类表达,但它们使用的时钟与起算点不同,在时区变化、系统休眠和定时器停用期间会表现出不同结果。只把它们统称为周期调度,会隐藏本来应该由需求决定的差别。 日历持久化请求一次补触发,不会逐个重放所有遗漏业务周期。日期与三个事件的例子来自原文;本图不增加定时器运行验证证据。 坏掉的样子 OnCalendar 按墙钟日历表达式触发,省略时区通常使用系统本地时区,因此同一份配置在不同机器上可能代表不同实际时刻。夏令时规则变化时,每天相同的本地时间也可能相对 UTC 移动。应使用固定参考日期预览预期表达式,而不是把今天的 UTC 偏移当成全年不变的事实。调度语法能解析,只证明表达式有效,不证明它符合业务要求的地区时间。 单调计时选项明确命名参考事件。OnActiveSec 从 timer 本身激活起算,OnStartupSec 从服务管理器启动起算,OnUnitInactiveSec 从目标单元结束活动起算。这些并不是“隔几分钟运行”的可互换写法。普通单调计时在系统休眠期间暂停,WakeSystem 会改变时钟选择,并依赖平台支持。因此是否计入休眠时间,也应该属于调度需求,而不是部署后的偶然发现。 Persistent=yes 对日历计时器生效。它记录触发时刻,在定时器恢复活动且至少错过一个事件时请求一次补触发;不会逐次重放所有错过的日历时刻,也不能证明上次业务执行成功。如果配置了随机延迟,补触发仍受其影响。触发历史不是一条持久业务队列,不能据此推定停机期间应做的所有工作都已完整保存。 快速验证 在 PID 1 为 systemd、安装了时区数据库的一次性 Linux 系统中,以管理员身份运行下面的 Python 脚本。恢复文本记载的环境为 Debian 12、systemd 252.39,但原始运行日志不可用。本轮没有执行此样例;以下结果说明其预期断言,不能视为本轮实测。脚本先预览日历表达式,再安装两个唯一命名的定时器与服务组合,双方使用同样三个即将到来的日历时刻,全程不会调整机器时钟: from pathlib import Path from tempfile import TemporaryDirectory from datetime import datetime,timezone import subprocess,time,uuid name='xbcatm-calendar-'+uuid.uuid4().hex[:8] names=[name,name+'-plain'];paths=[] def run(*args): result=subprocess.run(args,text=True,capture_output=True,timeout=10) assert result.returncode==0,(args,result.stderr) return result.stdout.strip() preview=run('systemd-analyze','calendar','--base-time=2026-03-07 00:00:00 UTC', '--iterations=3','*-*-* 09:00:00 America/New_York') for expected in ('2026-03-07 14:00:00 UTC','2026-03-08 13:00:00 UTC','2026-03-09 13:00:00 UTC'): assert expected in preview,preview print('09:00 New York: 14:00 UTC before spring DST, 13:00 UTC after it') with TemporaryDirectory(prefix=name+'-',dir='/run') as directory: root=Path(directory);worker=root/'worker.py' worker.write_text("from pathlib import Path\nimport sys\np=Path(sys.argv[1])\nwith p.open('a') as output:output.write('run\\n')\n") now=int(time.time());events=[now+3,now+4,now+5] calendar=''.join('OnCalendar='+datetime.fromtimestamp(t,timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')+'\n' for t in events) try: for index,prefix in enumerate(names): service=Path('/run/systemd/system')/(prefix+'.service');timer=service.with_suffix('.timer') assert not service.exists() and not timer.exists() service.write_text(f'[Service]\nType=oneshot\nExecStart=/usr/bin/python3 {worker} {root}/{index}.log\n');paths.append(service) timer.write_text('[Timer]\n'+calendar+'AccuracySec=1ms\nRandomizedDelaySec=0\nPersistent='+('yes' if index==0 else 'no')+'\n');paths.append(timer) run('systemd-analyze','verify',*[str(p) for p in paths]);run('systemctl','daemon-reload') run('systemctl','start',name+'.timer');run('systemctl','stop',name+'.timer') assert not (root/'0.log').exists() and time.time()<events[0] while time.time()<events[-1]+0.15:time.sleep(0.05) run('systemctl','start',name+'.timer',name+'-plain.timer') deadline=time.monotonic()+3 while not (root/'0.log').exists(): assert time.monotonic()<deadline,'catch-up did not run';time.sleep(0.02) time.sleep(0.15) assert (root/'0.log').read_text()=='run\n' and not (root/'1.log').exists() print('three elapsed calendar events: persistent timer catches up once; plain timer does not') run('systemctl','stop',name+'.timer');run('systemctl','start',name+'.timer') time.sleep(0.2);assert (root/'0.log').read_text()=='run\n' print('reactivating with no newly missed event does not replay the job') print('timer inactivity was tested without changing the system clock or rebooting') finally: if paths: run('systemctl','stop',*[p.name for p in paths]) run('systemctl','clean','--what=state',name+'.timer') for path in paths:path.unlink(missing_ok=True) run('systemctl','daemon-reload') 预览从二〇二六年三月夏令时切换前的固定日期开始,询问纽约早上九点的后续发生时刻。第一次对应 UTC 十四点,后两次对应 UTC 十三点,本地钟表时间却始终是九点。这检查的是日历解释,并不是修改主机时区,也不是把系统时间跳到那些日期。对指定地区的每日任务而言,这种偏移变化可能正是正确行为。 ...

十一月 20, 2026 · 2 分钟 · XB Field Notes
三条 cgroup 控制器路径分别表示 CPU 带宽节流、内存回收及可能的 OOM、任务创建拒绝;预算属于同一层次。

cgroup v2 的不同资源预算有不同失败方式

cgroup v2 用统一层次组织资源控制。一个工作负载的多个进程可以共享合计预算,限制还会沿层次继承,而不是只对某个特定 PID 生效。CPU 带宽、内存用量和任务数量由不同控制器管理,执行方式也不同。读取真实 cgroup 文件和失败计数器,比把所有配置上限都当作同一种“到点就停”更能解释实际行为。 坏掉的样子 cpu.max 按周期限制 CPU 带宽。控制组用完配额后,可运行工作可能被节流,但这不会预留一颗专用处理器,也不保证某个请求延迟。多个线程可以很快消耗共享配额。CPU 权重则影响竞争情况下的相对分配;混淆权重、配额和 CPU 亲和性,会让容量规划建立在错误预期上。 内存上限不会统一把大分配变成可由应用干净处理的错误。到达 memory.max 时,内核可以回收内存;无法降低用量时,可能触发控制组 OOM 机制。某些分配路径还有其他失败方式。memory.high 施加回收压力和节流,本身不会调用 OOM,因此不能把这两个设置解释成同义的硬终止阈值。 任务上限也包括线程,不只是监控面板显示的应用进程数量。达到 pids.max 后,即使 CPU 和内存看似充足,fork 或 clone 仍可能失败。层次限制意味着子组配置更大,也不能简单覆盖祖先更紧的预算。解释可见文件时,需要确认对应路径与控制器委派关系,不能只读取一个数值就断定完整有效上限。 CPU 带宽、内存压力与任务计数使用不同上限;退出码 137 不能单独证明 OOM。 快速验证 先取得固定镜像,再用 Python 和采用 cgroup v2 的 Docker 守护进程运行脚本。实测为 Docker 29.3.1、Linux 6.12.76、Python 3.11.16。所有限制只施加到可丢弃实验容器。脚本执行受限的小规模内存耗尽测试,清理自己的容器,不改变宿主全局 cgroup 设置,也不修改其他服务的预算: ...

十一月 10, 2026 · 2 分钟 · XB Field Notes