队列腾出位置会让生产者继续,但处理完成需要另一项明确确认。

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

背压把消费者的有限容量与生产者行为联系起来。通道已满时,生产者可能等待、拒绝新任务,或执行经过明确选择的丢弃策略。关键是容量限制真正改变上游怎么做。队列不断增长,或仪表盘显示积压,只能说明供需失衡,本身不自动形成控制接入的机制。需要从观察到过载,继续追到生产者是否确实减速或停止提交。 坏掉的样子 无界队列可以吸收短暂突发,却也会隐藏持续过载。如果到达速度长期超过完成速度,未完成任务和等待时间就持续增长。把积压挪到待处理线程、重试任务或另一个无界缓冲,并没有消除压力。容量边界必须覆盖任务真正被保存的位置,不能只限制一个容易观察的队列对象,却允许周围继续无限保留工作。 有界队列本身也没有定义完整过载策略。无限阻塞可能耗尽生产者线程或让关闭流程卡住;立即拒绝时,调用方若误认为成功,就可能丢失工作;每次拒绝后立即重试,又可能加重负载。应依据应用契约明确选择:在截止时间内等待、拒绝并稍后重试,还是有意丢弃。不同选择改变交付语义,不能在实现中含糊处理。 队列容量与处理完成是两件事。消费者取出项目可以释放槽位,但此时可能仍在处理这个项目。等待容量为二,也可能同时还有额外执行中的工作。生产者成功入队,仅说明队列接受了项目,不证明已经执行、持久保存或完成下游副作用。若把所有这些状态都叫作“成功”,过载和失败时就无法判断责任归属。 快速验证 将脚本保存为 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
在线程与进程之间,当内存耗尽时由内核挑选一个牺牲者。

OOM killer 替内核做最后一搏

青黄不接时,内核需要一个倒霉蛋替大家承担OOM。这个倒霉蛋不是随机的;它遵循可读,也可被我们善用以保全核心服务。 耗尽之后才有最后通牒 内存被耗光,分配器把常规的回收路径都走完了,仍然无法满足一段内存申请——这时候才轮到 OOM Killer 出手。它绝不是随机的:它用「评分」为每个进程算账,选出血大、优先级又低的那一个杀掉。于是常驻内存大、却对系统意义不大的后台服务,往往比小而关键的进程更早被牺牲。理解它依据的是分值,而不是「谁先来的」,是预测这个杀手会动手时最要紧的一步。 评分决定牺牲哪个 评分是可以被改写的。oom_score 反映进程的内存代价有多高;oom_score_adj 允许你给某个关键进程一个暗地里更低(甚至为负)的分数,把它从受害者的名单里踢出去。在需要扛下来的容器入口、agent 这类进程上,给一个 -500 的偏移,同时让其余默认值不动,就是在把杀手的「最不重要」往你的迫切上对齐。这套设置要放进容器/管理器的配置而不是运行时现场救,因为内存耗尽的那一刻,已经来不及谈谁该活了。区分一下分工:overcommit 管申请放不放行,这里的评分管不够分时谁先死。 把策略推到这个事件的尘埃落定之前 要让 OOM 变成不成一回事,办法是别让它走到那个阈值:给工作负载设一个明确的上限(比如容器级的 CPU/内存限制),盯住内存与 swap 的趋势以提前预警,并预留你付得起的富余。真撞上它,就把这次记录进告警、当成一次有原因的噪声——而不是回头去猜到底谁背了锅。杀手值得被当成一张安全网:有显式的配置、有清晰的记录(杀了谁、什么时候),而不是让它成为你默认的「方案」。 坏掉的样子 最经典的翻车,是最重要的进程先死。数据库抱着巨大的缓冲池,常驻内存全场最大,评分自然先选中它,而真正制造压力的那群小 worker 反而活了下来。故障看起来是反的——你最想保住的服务恰恰是内核牺牲掉的那一个——但日志里 Out of memory: Killed process 那一行说明,评分只是按既有规则办事。 调参调反了,死得更冤。给每个服务都贴上 oom_score_adj 为 -1000 的护身符,等于告诉杀手谁都不能动,它只好越过你的保护名单,去杀容器监管进程、SSH 会话或者监控 agent。正负号是另一个坑:正值意味着更容易被杀,把示例里的 +500 当成「优先级」抄到关键守护进程身上,等于亲手画了个靶子。还有人把参数写在交互式 shell 上,而不是写进被管理的 unit 里,会话一断、机器一重启,配置就悄悄回到默认值。 ...

十月 26, 2026 · 2 分钟 · 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。创建容器时应去掉 SYS_TIME 能力,并保持独立网络命名空间,不使用宿主网络;实验只需要容器内的回环通信。 ...

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