关于写回、可见性与刷写的标题卡,配有 OBSERVE、DECIDE、ACT(观察、判断、行动)面板。

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

缓冲文件 I/O 可以在不同时间经过语言缓冲区、内核页缓存和存储栈。被修改的缓存页需要写回,使数据进入后端文件系统与设备。分开这些阶段可以合并工作、提高吞吐,但也意味着另一读取者看到新字节,本身不能证明已经完成可经历崩溃的存储边界。理解性能和持久性时,需要先确定观察的是哪一层完成。 坏掉的样子 缓冲写入不保证瞬间返回。分配、内存压力、脏页限制或文件系统活动,都可能让写者等待。如果生产速度持续超过存储处理能力,脏数据不能无限增长,压力最终会反映到工作负载。把每次 write 都描述为单纯内存复制、所有 I/O 都在之后发生,会错误解释持续负载下的延迟尖峰。 语言层 flush 与文件系统同步含义不同。flush 把应用缓冲数据提交到下一层,fsync 则请求文件同步边界。普通关闭文件,或者从另一个描述符读到相同字节,不能替代这一显式契约。因此只测成功写入的基准,可能测到的是缓存接受速度,而不是稳定、持久的存储吞吐,需要与实际工作要求对应。 写回设置也不是逐文件持久性截止时间。后台阈值促使刷新线程工作,更高的脏页阈值影响生成写入的进程,周期参数则控制唤醒或较老脏数据何时符合处理条件。设备进展、文件系统行为和竞争负载仍会影响结果。不能仅设一个时间间隔,就证明每次已确认更新在这么多秒后必定安全。 快速验证 把脚本保存为 writeback.py,在装有 Python 和 strace 的 Linux 环境中运行 python3 writeback.py,并让临时目录位于 ext4。实测使用 Linux 6.12.76、Python 3.11.2 和 strace 6.1。样本写入约四 MiB、读取记账信息,不改变全局脏内存参数或缓存控制: from pathlib import Path from tempfile import TemporaryDirectory import subprocess,sys program=r''' from pathlib import Path import json,os,subprocess,sys root=Path(sys.argv[1]);path=root/'data' def io():return {key:int(value) for key,value in (line.split(':',1) for line in Path('/proc/self/io').read_text().splitlines())} def dirty():return int(next(line.split()[1] for line in Path('/proc/meminfo').read_text().splitlines() if line.startswith('Dirty:'))) with path.open('wb',buffering=65536) as handle: handle.write(b'buffered') assert path.stat().st_size==0 and path.read_bytes()==b'' before=io();handle.flush();after=io() assert after['wchar']-before['wchar']>=8 reader='from pathlib import Path;import sys;assert Path(sys.argv[1]).read_bytes()==b"buffered"' subprocess.run([sys.executable,'-c',reader,str(path)],check=True) print('visible before explicit fsync',flush=True) before=io();chunk=b'x'*(4*1024*1024);handle.write(chunk);handle.flush();after=io() assert after['wchar']-before['wchar']>=len(chunk) assert after['write_bytes']-before['write_bytes']>=len(chunk) dirty_before=dirty();os.fsync(handle.fileno());dirty_after=dirty() assert path.stat().st_size==len(chunk)+8 print(json.dumps({'wchar_delta':after['wchar']-before['wchar'],'write_bytes_delta':after['write_bytes']-before['write_bytes'],'system_dirty_kib_before':dirty_before,'system_dirty_kib_after':dirty_after})) ''' with TemporaryDirectory(prefix='xbcatm-writeback-') as directory: root=Path(directory);trace=root/'trace' result=subprocess.run(['strace','-f','-yy','-s','200','-e','trace=write,fsync,fdatasync','-o',str(trace),'--',sys.executable,'-c',program,str(root)],capture_output=True,text=True,timeout=10) assert result.returncode==0,result.stderr lines=trace.read_text().splitlines() visible=next(i for i,line in enumerate(lines) if 'visible before explicit fsync' in line) syncs=[i for i,line in enumerate(lines) if 'fsync(' in line and str(root/'data') in line] assert len(syncs)==1 and visible<syncs[0] and '= 0' in lines[syncs[0]] print(result.stdout,end='');print(lines[syncs[0]]) print('another process read flushed bytes before the only explicit file synchronization; global Dirty is observational, not an isolated-device counter') 子进程用六万五千五百三十六字节的 Python 缓冲打开文件,并写入八字节。flush 前,路径报告长度零,独立打开也读不到内容,因为数据仍在应用缓冲里。这个对照直接建立最早的边界,没有假定每次语言级 write 都已经发出内核写入。应用接口返回与系统调用发生,需要分别确认。 ...

十一月 26, 2026 · 2 分钟 · XB Field Notes
三个相连方块,依次标有 Observe、Decide 和 Act。

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

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

十一月 25, 2026 · 1 分钟 · XB Field Notes
内存内的一台压缩块设备,保存本来要写进磁盘的页。

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

内存吃紧时,冷页面常被送去慢速磁盘;zram 则用压缩的内存替代这一步,用小部分 CPU 换取可观的内存空间。 内存里的压缩块设备 zram 是一块驻留在内存里的块设备,它不跟磁盘对话,而是先在内存里把换出的页压缩、保存起来。内核把一页换给它时,页先被压缩、留在内存中,于是内存方面的减压方式并不是「把页挪去磁盘」,而是「让这一页变小」。因为设备本身挂在内存上,读写它的速度远比磁盘后备 swap 快;可它的容量同样受制约——既取决于当前这页有多可压缩,也取决于你愿意拿多少真实内存去换这块 swap。 压缩就是这笔交易 「压缩」就是这笔交易赚头所在,也是「一台小机型显得能装下比物理内存更多内容」的根源。一页值不值得压要看内容:全零页、文本和有结构的数据都缩得不错,而图片、压缩包这类已经被压缩过的对象几乎缩不动。站在工程视角看,zram 等于把暂时闲置的 CPU 换算成「看似更多」的内存;当初工作负责多半是可压缩的、又特别想避开真实磁盘换页的延迟时,这笔交易才划算。 适合服务器与低端设备 它最合身的地方是那些内存不多、换页压力又不过度的机器:小 VPS、容器宿主机,以及手机、路由器这类如今要把袖珍机器安排得明明白白的角色。反过来,内存本来就充裕、或者本来就要换到冷冷盘上的服务器,同一笔钱买一张真实磁盘往往能装下更多,因此未必轮到 zram。实用的判断方法是盯住压缩率和 swap 使用量:当驻留数据始终很小、又够可压缩、且 CPU 相对空闲,才是让 zram 出场的时刻。 坏掉的样子 最常见的失望是压缩比根本没有到来。照片、视频、加密缓冲和已经压过的归档几乎压不动,mm_stat 里 orig_data_size 只比 compr_data_size 大一点,交换空间却照样被占满。内存池和元数据照样吃掉真实内存,腾出来的余量几乎为零,杀手来得和没开压缩交换一样快。压缩比是数据的属性,不是设备的属性,容量只能按实测值去估。 选错算法是死在处理器上,不是死在容量上。重型算法放在小内存虚拟机或旧手机上,每次换出都要烧大量周期,回收线程转个不停,系统态占用随压力一起涨,交换还没满,长尾延迟先垮了。交互型机器先选 lzo-rle、lz4 或低档 zstd 这类快速算法,换算法前一定要在真实负载下看占用和延迟。 同时打开 zswap 和压缩交换等于把同一页压两遍,白白浪费处理器。zswap 自己先留一块压缩池,再把已经压过的页交给压缩设备,后者几乎找不到可省的空间。两条路径一起烧处理器,交换看似用了不少,有效容量却没涨多少。一条交换路径只留一层压缩:要么把压缩设备当交换,要么让 zswap 走在磁盘交换前面,不要双开。 ...

十一月 24, 2026 · 2 分钟 · XB Field Notes
文章标题旁是标有 OBSERVE、DECIDE、ACT 的三个通用面板,以及箭头和圆形仪表。

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

信号依据内核规定的投递规则,把事件通知给进程或线程。信号处理方式可以选择默认动作、安装处理函数,或者在允许时忽略;线程掩码则能阻塞符合条件的信号,让它们暂时保持待处理。当程序似乎漏掉重复通知,或者没有按预期停止时,需要分别检查处理方式、阻塞状态和待处理状态,不能把这些不同层面统称为“有没有收到信号”。 坏掉的样子 普通信号不是可靠事件计数器。同一种标准信号已经处于待处理状态时,随后多次发生可能合并。如果应用把每次 SIGUSR1 都理解为一个排队任务,就可能丢失这个计数关系。实时信号具有排队语义,但资源仍然有限,也需要明确的应用协议;不能仅因为名字里有实时,就假定它适合无限量承载业务消息。 信号名称不保证应用功能。某些守护进程约定 SIGHUP 表示重载,但它的默认动作是终止。SIGTERM 默认也会终止,只有应用实现了相应行为,它才成为优雅关停请求。发送成功不能证明配置已经重载,也不能证明在途工作已经排空。控制方必须观察目标实际响应,而不是把投递结果当作业务操作结果。 SIGKILL 与 SIGSTOP 不能捕获、忽略或阻塞。KILL 不给应用执行清理的机会,但也不应被描述为在每种内核状态下都保证瞬时释放资源。监督者仍需观察实际完成。使用符号名称也更清晰,不能假定所有平台上的信号编号都完全一致;信号数字与进程退出状态还可能由工具采用不同方式表示。 快速验证 把下面脚本保存为 signals.py,在一次性 Linux 环境中运行 python3 signals.py。原始记录使用 Linux 6.12.76 和 Python 3.11.2;本轮在 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
三个相连方块,依次标有 Observe、Decide 和 Act。

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
标题卡旁有三个方框,分别标注 Observe、Decide 和 Act。

Logrotate 在执行时应用轮转策略

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

十一月 21, 2026 · 2 分钟 · 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 v2 资源预算的标题。

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

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

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