生产者 c 等在已被 a、b 填满的两槽队列外;出队路径表示取出 a 后释放槽位,c 才能进入。

背压只有改变生产者行为,才真正控制了有界接入

背压把消费者的有限容量与生产者行为联系起来。通道已满时,生产者可能等待、拒绝新任务,或执行经过明确选择的丢弃策略。关键是容量限制真正改变上游怎么做。队列不断增长,或仪表盘显示积压,只能说明供需失衡,本身不自动形成控制接入的机制。需要从观察到过载,继续追到生产者是否确实减速或停止提交。 坏掉的样子 无界队列可以吸收短暂突发,却也会隐藏持续过载。如果到达速度长期超过完成速度,未完成任务和等待时间就持续增长。把积压挪到待处理线程、重试任务或另一个无界缓冲,并没有消除压力。容量边界必须覆盖任务真正被保存的位置,不能只限制一个容易观察的队列对象,却允许周围继续无限保留工作。 有界队列本身也没有定义完整过载策略。无限阻塞可能耗尽生产者线程或让关闭流程卡住;立即拒绝时,调用方若误认为成功,就可能丢失工作;每次拒绝后立即重试,又可能加重负载。应依据应用契约明确选择:在截止时间内等待、拒绝并稍后重试,还是有意丢弃。不同选择改变交付语义,不能在实现中含糊处理。 队列容量与处理完成是两件事。消费者取出项目可以释放槽位,但此时可能仍在处理这个项目。等待容量为二,也可能同时还有额外执行中的工作。生产者成功入队,仅说明队列接受了项目,不证明已经执行、持久保存或完成下游副作用。若把所有这些状态都叫作“成功”,过载和失败时就无法判断责任归属。 取出 a 先释放准入容量,处理确认发生在后。put 成功、队列为空与 join 完成是不同边界;队列记账不代表持久交付。 快速验证 将脚本保存为 check-backpressure.py,运行 python3 check-backpressure.py。它只使用 Python 标准库,在 Python 3.14.4 上执行。线程通过明确事件和有界等待协调,不依赖远端消息代理、数据库或刻意拖慢的外部服务。所有队列和任务都只存在当前进程中,结果用于观察接入与完成的语义,不代表分布式队列的持久化保证: from queue import Queue,Full from threading import Event,Thread import json unbounded=Queue() for item in range(1000):unbounded.put_nowait(item) assert unbounded.qsize()==1000 channel=Queue(maxsize=2) channel.put('a');channel.put('b') try: channel.put('deadline-rejected',timeout=.02) raise AssertionError('a full bounded queue accepted another item') except Full:rejected=True assert channel.qsize()==2 entered,delivered=Event(),Event() def produce(): entered.set();channel.put('c',timeout=2);delivered.set() producer=Thread(target=produce,daemon=True);producer.start();assert entered.wait(1) assert not delivered.wait(.05) and channel.qsize()==2 first=channel.get(timeout=1);assert first=='a' assert delivered.wait(1);producer.join(timeout=1);assert not producer.is_alive() assert channel.qsize()==2 rest=[channel.get(timeout=1),channel.get(timeout=1)];assert rest==['b','c'] assert channel.empty() completed=Event() def join():channel.join();completed.set() waiter=Thread(target=join,daemon=True);waiter.start();assert not completed.wait(.05) for item in [first,*rest]:channel.task_done() assert completed.wait(1);waiter.join(timeout=1);assert not waiter.is_alive() print(json.dumps({'unbounded_backlog':unbounded.qsize(),'bounded_capacity':2,'deadline_rejected':rejected, 'blocked_producer_resumed_after_get':delivered.is_set(),'delivered_items':[first,*rest], 'empty_queue_did_not_mean_processing_complete':True,'join_finished_after_task_done':completed.is_set()})) 第一个队列使用默认无界容量。没有消费者运行时,一千次 put_nowait 全部成功,留下同样数量的积压。这是小规模行为演示,不是耗尽内存测试。它说明接入成功本身无法证明下游已有进展,也不能证明到达速度可持续。监控若只统计提交成功率,就可能在处理能力不足时仍显示表面正常。 ...

八月 12, 2027 · 1 分钟 · XB Field Notes
受限 cgroup 包围组内候选进程,无关宿主进程留在边界之外;宿主空闲内存不能单独判断控制组 OOM。

排查 OOM 应先确认分配范围

当一次分配在相关限制范围内无法满足,适当的回收尝试也不能解决问题时,Linux 可能调用内存不足终止机制。它是最后阶段的恢复手段,不是理解业务重要性的调度器。有些分配会直接失败而不杀进程,配置也可能使系统进入恐慌处理。因此,排查应先识别分配范围与实际事件,不能一开始就认定整台机器的每一字节内存都已经用完。 先看范围,再看评分 全局内存短缺与内存控制组短缺是两回事。某个控制组可以在主机仍有可用内存时达到自身上限;它的 OOM 处理不能任意挑选组外进程来解决组内问题。允许使用的内存节点等其他限制也可能影响分配。因此,主机剩余内存图表不足以证明或否定某次失败,更不能在没有检查事件范围时,把所有容器异常都归类为全局容量不足。 在适用的选择规则内,内存占用与调整值会影响牺牲者选择。内核并不知道哪个进程商业价值高、哪个空闲服务可以丢弃,也不知道哪一个业务必须持续可用。一个体积很大的数据库可能被选中,即使压力也来自许多小工作者。procfs 中的 oom_score 是可观察数值,不是承诺把所有可见进程排序以后,就能准确预测任何内存域下一次会杀谁。 理解调整值,不许诺必然存活 oom_score_adj 的范围为负一千到正一千。正值增加被选中的倾向,负值降低它;负一千会把该任务排除在这项内核 OOM 选择之外。一般负值并不保证存活,这个设置也不能阻止显式信号、服务管理器或独立用户态内存压力策略终止任务。调整分值不会增加容量,也不会修复泄漏。把它解释成“重要进程已经免疫所有内存故障”,会造成危险的运维误判。 保护所有任务不是可行的容量方案。如果没有合适的牺牲者,不能假定内核会越过控制组边界,去杀一个无关的监管进程。整组终止与恐慌设置需要分别分析。调整值可以被子进程继承,而持久服务设置应归入监管器配置;修改之前需要明确授权并验证适用范围。下面实验不会改动这些值,也不制造内存压力,避免把接口学习变成真实故障。 坏掉的样子 反复重启可能掩盖持续泄漏。如果同一个工作负载不断增长直至失败,增加内存也许只是延长两次失败之间的间隔。应关联分配速度、常驻和记账内存、负载大小、回收及交换活动。单个进程的常驻大小不能完整表示控制组全部费用或共享内存关系。降低上限可以约束增长范围,却也可能让应用更早失败;不能只观察邻居未被终止,就认为业务恢复问题已经解决。 退出状态一百三十七与 SIGKILL 相符,但并不是 OOM 专属证据。应查看运行时的终止原因、控制组内存事件计数和操作员可见的内核记录。分配失败、进程被杀、内核 OOM 和用户态压力管理器终止,是需要分开的情况。看不到内核日志,也可能只是当前观察范围受限,不能直接证明没有事件发生。诊断必须说明证据来自哪里、有哪些缺口。 评分会随内存和范围变化。读取 procfs 的过程中进程可能退出,容器内的进程视图也可能隐藏主机的大部分任务。一次成功读取并不是原子的整机快照。分析真实失败时,应保留事件时间与受影响的控制组,然后比较相同范围内的观察,而不是拿事后另一个进程的分数去解释之前的牺牲者选择。 OOM 范围及事件上下文先于评分排名;此处 PID 排序是虚构展示夹具。 快速验证 保存为 oom_observe.py,使用 Python 3.9 或更新版本执行 python3 oom_observe.py。程序只读取自身进程的两个评分文件,不创建压力负载,不修改任何限制或策略。两条确定性断言仅对明确虚构的记录测试展示顺序,实时读取和模型检查会分别报告结果,避免把一种成功误当成另一种验证。 ...

十月 26, 2026 · 1 分钟 · XB Field Notes
客户端与服务端轨道之间有请求和返回箭头;单向延迟不对称会使偏移估计有误差。

时钟同步约束机器之间的时间偏差

时钟同步用于减少机器之间的民用时间读数差异,支撑证书有效期检查、事件时间解释以及许多过期策略,但不会建立完美的全局时钟。一台主机能够输出格式正确的 UTC,同时仍然测量了错误瞬间。应用需要区分经过时长、绝对截止时间和因果顺序,不能让所有语义都依赖未经解释的钟表数值比较。 四个时间戳估算两个量 一次 NTP 交换包含客户端发送时间 T1、服务端接收时间 T2、服务端发送时间 T3 和客户端接收时间 T4。常用偏移估计为 ((T2-T1)+(T3-T4))/2,正值表示服务端看起来比客户端更快。往返延迟估计为 (T4-T1)-(T3-T2),其中减去了服务端处理所花的时间。它们是根据观测推算的量,并没有直接测出每一段单向网络路径的时延。 当去程和回程延迟不同,偏移估计就会包含误差。较小的往返延迟有参考价值,却不能证明两个方向完全对称。同步守护进程会使用多次采样、来源选择、过滤和时钟模型,而不是把单个报文当作无条件可信的真相。来源可达性、估计不确定性和来源可信度都需要考虑,较低的 stratum 数字本身并不是精度证书。 时间戳的表示分辨率也不同于测量准确度。小数字段即使能编码非常细的步长,实际时钟或网络观测仍可能带有明显更大的不确定性。多打印几位小数不会改善证据质量,解释结果时应同时说明测量方法和它能够支持的结论。 时钟控制是后续的另一阶段。守护进程可以逐渐调整,也可以在配置允许的条件下直接跳变。因此不能保证 NTP 永远不会造成实时时钟的不连续,更不能承诺所有环境都达到亚毫秒精度。网络、振荡器、平台和配置共同决定实际效果。告警阈值应来自应用需求和测量条件,而不是把某个固定偏差值无条件推广到所有系统。 坏掉的样子 实时时钟可能向前或向后调整,直接相减得到的时长就可能失真甚至变成负数。本机持续时间和超时应选择适当的单调时钟,但其参考起点不能直接当成其他主机或下次启动仍可使用的时间戳。证书日期和业务截止时间仍需要民用时间语义,因此单调时钟并不是替代所有日期处理的通用答案。 格式不完整、与请求不对应、来源未同步或被策略拒绝的 NTP 响应,都不能当成普通样本使用。核对返回的 origin 时间戳是否匹配本次请求,有助于拒绝无关报文,但并不构成密码学认证。生产客户端还需要协议规定的更多校验和来源选择行为。时钟同步也不能自动让基于时间戳的租约成为安全分布式锁,更不能保证非常接近的事件按时间排序就等于因果顺序。 四个时间戳估计偏移;两组合成路径往返相同,偏移误差不同。 快速验证 使用全新的临时 Debian 12 Docker 容器,先执行 apt-get update,再执行 apt-get install -y python3 chrony 准备依赖。将代码保存为 ntp-exchange.py,以 root 运行 python3 ntp-exchange.py。原先报告的版本是 chrony 4.3 和 Python 3.11.2;上述安装命令并未固定这些版本。运行时必须启用断言,不使用 -O 或 PYTHONOPTIMIZE。创建容器时应去掉 SYS_TIME 能力,并保持独立网络命名空间,不使用宿主网络;实验只需要容器内的回环通信。 ...

九月 9, 2026 · 3 分钟 · XB Field Notes