生产者 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
源站 /manual.txt 已改为 B。缓存 0 的精确条目被移除,下一次请求获取 B;独立的缓存 1 仍保留新鲜的 A 并直接复用。一次定点移除不会让两个缓存同步。

缓存失效是主动推进,不是祈祷

源站文件改了,并不意味着每个已经保存响应的缓存都会收到通知。失效操作控制的是哪些已存副本还可以继续复用,而不只是更新磁盘上的内容。可靠的发布要区分到期、重新验证、定点移除以及使用新 URL:它们解决不同范围的问题,也没有哪一种能远程擦掉读者已经下载的所有字节。 两个独立私有缓存让移除范围变得具体:改变一个条目,另一个仍可复用。发布并请求新 URL 会建立独立条目,不会擦除旧 URL,也不会自动更新所有读者。 到期不是广播通知 源站发生变化后,一份仍然新鲜的响应可能继续被合法使用。新鲜度由缓存寿命和当前年龄共同决定,年龄计算涉及缓存元数据与经过的时间,并非只从源站的 Date 字段开始倒计时。到期改变的是直接复用时必须满足的条件,既不保证存储介质立即删除条目,也不会让无人访问的缓存主动联系源站。 验证器同样不是内容变化的推送通道。请求需要重新验证时,正确的 ETag 才能帮助源站确认表示未变,或者返回替代正文。浏览器手上的响应还新鲜、没有理由回源时,新增验证器并不能强迫它询问。把源站策略改成 no-cache 或 no-store,也无法追溯修改浏览器之前已经收到的缓存指令。 先确定失效覆盖范围 RFC 9111 要求:缓存收到不安全方法对应的成功或重定向响应时,必须使目标 URI 的已存响应失效。这个义务只涉及该请求实际经过的缓存,不是向整个网络广播。通过另一条接口修改数据库,不会自动覆盖所有派生页面、查询参数变体、不同语言表示和搜索列表,应用需要识别这些真实依赖。 ...

八月 5, 2027 · 2 分钟 · XB Field Notes
建连与两次读取共用整体预算。预算耗尽后不再开始下一次等待;单次读取成功不代表完整响应及时完成。

连接超时阻止无声的停摆

超时首先规定某个操作最多等待多久。它不会自动成为整个请求的总期限,也不能证明远端工作已经停止。选择数值或解释错误之前,要把连接建立、单次读取和调用方的整体预算分开;否则配置看起来都有限,整条调用链仍然可能等得远比预期久。 时间示意,不按比例。数字回环 TCP 实验未约束 DNS 或 TLS。调用方截止点不取消远端工作;仍须验证数据完整性。 连上了,不代表能拿到有用的响应 TCP 建连可以成功,而对端应用始终不产生响应。此时问题属于读取阶段,不是 TCP 握手尚未完成。诊断信息应写出发生超时的阶段;含糊的“数据库连接超时”,可能指连接池获取借用失败,也可能指新建套接字失败,或已经连上后等不到查询结果。它们占用的资源、下一步检查和恢复语义并不相同。 Python 套接字上的超时作用于阻塞操作。循环执行多次成功的 recv(),每次都可能获得一份新的等待额度,因此对端只要持续发送小片段,就可能把整个读取循环拖到远超那个超时值。下面特意每次只读一个字节,便于观察单次操作成功与完整响应及时到达之间的差别,而不是把“一次没有超时”当成整个消息已经满足时限。 如果需要整个接收过程共用预算,应先用 time.monotonic() 建立一个截止点。每次开始下一项操作前,计算剩余时间,再取剩余时间与该操作上限中的较小值;预算耗尽后,就不能继续开始新的等待。单调时钟不会因校正系统日期而突然倒退或跳到另一段墙钟时间,但它也不能保证线程恰好在截止的那一刻获得调度,因此不能把它理解成硬实时执行器。 把这个模式扩展到真实客户端之前,仍要核对所用库的契约。域名解析、多个地址尝试、TLS 协商、发送请求以及从连接池排队获取连接,可能有各自的计时方式。本例只访问一个数字形式的回环地址,并使用普通 TCP;它没有证明 DNS 解析已经受限,也没有实现一个覆盖完整 HTTPS 请求流程的通用总超时。 坏掉的样子 已接受连接但保持沉默的对端,会让调用方读到超时;不断滴送少量数据的对端,则能让每次读取都成功,却使完整响应过慢。只有建连超时,并不能处理连接已经成功之后的这两种情况。反过来,设置得过短,也可能把正常的网络波动或线程调度延迟误判成故障,数值需要结合真实调用预算和工作负载来确定。 失败路径需要释放自己拥有的资源,并保留完整数据与部分数据的区别。套接字读取返回空字节串,表示 EOF,并不等于应用消息必然收齐。实验知道目标帧应该有六个字节,所以提前关闭会被当成失败;不能因为连接已经结束,就把收到的半份结果当作正常响应交给业务。 放弃等待也不等于取消远端工作。调用方超时时,对端可能已经执行了某个副作用。重试需要匹配的重放契约,以及原操作还剩下的预算,而不是每次重新获得一整套等待时间。这里的总期限是在操作边界协作检查的约束,仍有调度与清理开销,并不是对任意线程、系统调用或远端执行的瞬时中断保证。 快速验证 将代码保存为 connection_probe.py,执行 python3 connection_probe.py。脚本只依赖标准库,本次在 macOS 的 Python 3.14.4 上验证。每种情况都创建自己的回环监听端口和对端线程,结束时关闭套接字并等待线程退出;不会连接外部主机,也不修改防火墙或生产服务。 ...

八月 1, 2027 · 2 分钟 · XB Field Notes
五列主值班/备援表让五个虚构名字轮转,每周两角色不同。重复四次覆盖 20 周;仅首周期第 2、5 周有虚构特殊标记。

排班需要连续覆盖,也需要复核实际负担

排班可以分配责任,但相同班次数量并不保证负担公平。应同时检查夜间告警、事故处理时间、恢复休息和可靠备援,而不只看每个人在表里出现了几次。 一份被共同承担的职责 值班是团队的事,不是某一位英雄的。轮班把「谁上次没睡好就谁来」变成一张明确的排班表、一套流程,让被打断分担到每个人头上,而不是让一个人独自背完一整条告警队列。而具体排班好不好,就在于「换人不换上下文」这六个字——交接若只换名字不换现状,同一个故障会在每次轮换时被重新发现一遍。所以轮班的灵魂不是「谁那天晚上在」,而是「责任与上下文如何平稳移交」。 交接是工作的一半 一次值班最容易断的,恰是交接这个环节。当接手的人从零开始、一片黑地接过来,本来该是交接的事就变成了重新破案。堵住这个缝只需要两样:一份书面交接(当前坏在哪、试过哪些、下一棒该试什么),以及一段重叠时间——让接班的人跟着旧的人一起盯岗,旧的人在正式放下前把现场讲完。一张只用来排名字的表,会一次次把同一个事故变成新事故。做出这套的难点,不在排满班,而在排完后还留得住上下文。 在铃声响起前演练 演练应安排在告警响起之前,而不是等故障发生。低峰期把 runbook 平铺地走一遍,练的是「恢复路径」而不是症状集;每一条 pager 都应映射到一份写好的计划,让雪夜里的铃声到来时,它是 checklist 而不是谜。提前练习可以增加团队在压力下的可靠选择;平静时候一遍遍过的常规动作,正是把「到时候再想」变成「按部就班照做」的地方。 坏掉的样子 单人长期值班看似省事,实则先压垮的是人。一个人连续几个月拿着寻呼机,即使没有告警也睡不踏实,看到通知就心慌,最后要么倦怠请辞,要么在一次深夜故障中彻底失手。谷歌站点可靠性工程指导强调人员配置与可持续工作负担。解法是设定单次值班上限,并保证下线周真正下线,上周的主值班不再兼任隐形备援。 升级链断裂是第二种常见死法。备援从不确认、联系方式过期,或者所谓升级到经理只是发进一个无人查看的邮箱,深夜三点主值班只能独自拆弹。升级链要像恢复流程一样演练:在工作时间实际寻呼一次备援,确认有人能在几分钟内应答,而不是只在通讯录里写个名字。 告警疲劳是第三种慢性失效。几百条噪声大、无法行动的告警教会团队寻呼机在说谎,于是唯一真实的那条也被顺手划掉。每条告警都应有负责人、有可执行的 runbook 链接、有经过上次误报调优后的阈值,无法行动的告警宁可降级为工单,也不要继续消耗信任。 交接缺失则让轮换只换名字不换上下文。周一换人时没有书面状态,周二就把周一的排查从零再做一遍。必须要求简短交接记录:当前症状、已尝试步骤、下一步动作,并在旧主值班真正下线前保留十五分钟重叠时间,把现场讲完再走。 每周都保留主值班与备援。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 在正式发布排班前,可以先运行这个纸面模型。它为二十个连续星期安排虚构的主值班与备援,并保留两周虚构的特殊覆盖标记。标记不代表任何国家真实的公共假日,遇到标记也不会让整个服务无人负责。持续运行的服务,在某位工程师休假时仍需要明确接替,不能以节日为由直接删除那一周的覆盖。 把代码保存为空目录下的 rota_probe.py。 用 Python 3.9 或更新版本运行 python3 rota_probe.py;程序不访问网络或文件。运行时不要使用 -O、-OO 或非零的 PYTHONOPTIMIZE,以免断言被跳过。 核对逐周人员、各角色次数和最后的 PASS 行。 from collections import Counter from datetime import date, timedelta TEAM = ["ana", "ben", "cara", "dev", "elin"] # fictional names START = date(2027, 8, 2) WEEKS = 20 SPECIAL_WEEKS = {START + timedelta(weeks=1), START + timedelta(weeks=4)} rota = [(START + timedelta(weeks=i), TEAM[i % 5], TEAM[(i + 1) % 5]) for i in range(WEEKS)] assert len(rota) == WEEKS assert len({week for week, _, _ in rota}) == WEEKS assert all(primary != secondary for _, primary, secondary in rota) assert all(primary in TEAM and secondary in TEAM for _, primary, secondary in rota) assert all(b[0] - a[0] == timedelta(days=7) for a, b in zip(rota, rota[1:])) assert all(a[1] != b[1] for a, b in zip(rota, rota[1:])) primary_counts = Counter(primary for _, primary, _ in rota) secondary_counts = Counter(secondary for _, _, secondary in rota) assert set(primary_counts.values()) == {4} assert set(secondary_counts.values()) == {4} for week, primary, secondary in rota: marker = " special-coverage" if week in SPECIAL_WEEKS else "" print(f"{week}: primary={primary} secondary={secondary}{marker}") assert sum(week in SPECIAL_WEEKS for week, _, _ in rota) == 2 print("primary:", dict(sorted(primary_counts.items()))) print("secondary:", dict(sorted(secondary_counts.items()))) print("PASS: 20 covered weeks, distinct backup, four turns per role; special weeks remain staffed") 每一周都有不同的主值班与备援,包括两周特殊覆盖。每人分别获得四次主值班和四次备援,相邻两周的主值班也不同。本轮在 Python 3.12.14 中执行通过了这些断言。它们只证明表里明确检查的排班性质:星期没有缺口、两个角色不由同一人兼任,以及各角色计数相等。 ...

七月 16, 2027 · 2 分钟 · XB Field Notes
启动门闩把就绪流量资格与存活重启处理作为两个独立分支放行。

探针告诉平台何时出手

探针结果是控制循环使用的证据,不是平台对应用健康的直接理解。Kubernetes 不会从 healthz 这个路径名称推断你希望怎样恢复服务;真正决定下一步的是探针类型、结果、阈值与容器生命周期。把每次失败都描述成“重启 Pod”,会抹掉直接影响可用性的关键区别。 三个问题,对应不同动作 启动探针为初始化提供独立的检测政策。配置后,在它成功之前,存活和就绪探针都不会执行。启动探针成功后,在当前容器生命周期内便不再持续检查;如果容器重新启动,则需要再次通过启动阶段,不能把上一次成功沿用到新生命周期。 存活检查用于识别适合通过重启当前容器恢复的状态。达到失败阈值后,kubelet 终止受到影响的容器,并应用对应的重启政策。这不是保证重建整个 Pod、改变其身份或修复外部数据库,也不意味着容器之间的依赖与在途工作都能自动安全恢复。 就绪失败则让容器继续运行,并持续检查是否恢复。它影响通常按就绪状态选择目标的 Service 流量,但不是防火墙,也不会取消已经通过连接到达的请求。直接访问实例的客户端和现有连接,仍需要自己的生命周期处理,不能把就绪状态误读成网络阻断保证。 阈值统计连续观察结果 failureThreshold 为三,不是从启动以来累计失败三次就动作,而是连续失败达到三次;一次成功会打断这段失败序列。就绪恢复可以要求多次连续成功,而启动和存活探针的 successThreshold 必须为一。阅读结果轨迹之前,应先明确进入失败和恢复成功两个方向的阈值。 超时、初始延迟、执行周期和终止宽限期同样影响实际表现。阈值乘周期可以辅助估算,但不能当成端到端故障切换的精确秒表;调度、探针执行和路由传播都参与其中。容器尚未就绪时,就绪探针还可能比配置周期更频繁地执行,以便更快恢复就绪。 实验选择四次启动失败、三次存活失败、两次就绪失败,并要求连续两次就绪成功才能恢复。这组数字只是为了看清不同计数器的边界,不是生产推荐。真实门槛应结合启动时间、检查成本、错误恢复方式以及误判代价确定,不能照搬实验参数。 检查机制决定你拿到了什么证据 HTTP 检查使用文档定义的 200 至 399 成功范围。即使响应正文写着“内部错误”,返回 200 也不会因为人类读出了坏消息,就自动变成失败探针。应沿实际探测路径检查认证和重定向,而不是默认平台与交互式浏览器具有完全相同的访问结果。 TCP 探针证明的是连接可以打开,不是查询或事务能够完成。exec 检查则需要镜像里确实存在相应命令,并且退出状态有明确含义;频繁创建进程还会消耗节点资源。应该选择能够回答当前问题的低成本机制,而不是认为检查越复杂、调用越多就越可靠。 就绪成功并不是存活执行的前提。没有启动探针隔离时,初始化较长的应用可能还没来得及就绪,就因为存活失败反复被终止。相反,启动容忍窗口过大,也会推迟发现真正无法完成的启动。两边都需要依据实际启动分布和恢复目标权衡。 探针失败与 Unknown 结果不是同一种状态。Kubernetes 文档规定 Unknown 不改变容器状态,之后继续检查。本例只接受成功与失败两个值,故意没有第三种结果,所以无法预测传输错误会如何分类。实际排查应检查 kubelet 事件和部署中的探针处理机制,先确认错误属于哪类,再解释是否应计入连续失败。 坏掉的样子 把外部依赖可用性直接作为存活条件,可能在共享故障期间不断重启健康进程;另一个独立线程上始终绿色的处理器,又可能遗漏业务工作线程的死锁。重启并不天然便宜,它会中断在途工作、重复初始化,也可能进一步增加故障依赖的压力。 ...

六月 18, 2027 · 3 分钟 · XB Field Notes
两条独立箭头分别将资源请求连到放置,将资源限制连到运行执行。限制不是调度预留。

资源限制约束运行,请求参与放置

Kubernetes 的资源 request 与 limit 属于不同决策。请求参与调度和资源共享,限制则配置运行时及操作系统支持的执行约束。把 limit 当成调度器预留的容量,会同时混淆这两个机制。有效的资源策略需要说明放置需求、可以接受的争用,以及运行中的工作负载到达边界后应该发生什么,不能只在清单上填写一组看起来合理的数字。 图示区分放置、记账、终止、重启策略与 Pod 驱逐。空闲邻居仅在历史最终观察时仍为 Ready,不能因此推导邻居延迟或完整隔离保证。本次未重跑示例。 CPU 与内存具有不同失败方式 在典型 Linux 环境中,CPU 限制会形成带宽约束,繁忙控制组可能被节流,而进程仍然存活。内存限制依赖内存记账和回收;受约束的分配无法满足时,可能触发控制组 OOM 处理。内核不会像推迟 CPU 使用那样,把所有超限内存申请都温和地放慢。两种资源因此需要不同的容量指标和故障判断方式。 内存执行机制具有反应性,不能描述成恰好达到某个字节就调用应用回调,也不能保证使用量绝对不会短暂超过配置值。应用可能收到分配失败,OOM 处理也可能终止进程。应先观察真实终止原因、容器状态和可用的内存事件,再给故障命名,避免仅因为配置了内存限制,就把所有异常退出都解释成越界。 限制覆盖的不只是应用打算保存的业务数据。运行时开销、其他进程,以及被记账的缓存或其他内存,都可能占用控制组额度。容器限制为六十四 MiB,并不意味着它能在已有开销之外,再安全创建同样大小的数据结构。内存支持的卷也可能贡献内存消耗,表现为文件存储并不会让这部分成本消失。 ...

六月 14, 2027 · 3 分钟 · XB Field Notes
十二个到达任务经过准入门槛;三个获准,分别是两个运行和一个排队,另外九个在提交执行器之前被拒绝。

负载削减保护系统

负载削减是在全部接收工作会威胁服务时,主动拒绝其中一部分。目标是保留容量,让仍有机会完成的工作继续进行,而不是保证每个获准请求都足够快、都足够重要。拒绝本身也会消耗资源,因此在哪个阶段做准入判断,以及拒绝路径有多贵,与阈值大小同样重要。 保护还能产生价值的容量 过载队列既占内存,也增加等待时间。客户端可能在任务被工作线程取走之前就已经放弃,服务器却继续为无人等待的答案花费资源。更长的队列可以吸收某些突发,但在需求长期超过处理能力时,它不会创造额外容量,只会推迟暴露问题。 Google SRE 讨论了队列限制、资源压力信号、选择性拒绝和降低响应质量。削减与降级相关,但并不相同:拒绝一个请求,不等于完成一个较便宜的版本。后者必须有清楚的产品契约,说明剩下的结果仍有什么价值,不能只是悄悄少做一部分工作。 不同保护机制也可能针对不同组件。Envoy 过载管理器保护代理自身资源,而它的熔断机制主要保护上游服务。依赖熔断器不会自动解决本地内存、连接或文件描述符耗尽,所以不能只看到请求被快速拒绝,就认定整个入口已经免于过载。 选择有依据的政策 按优先级拒绝,可以保护关键业务路径,但 HTTP 方法或是否容易重试,本身并不能决定重要性。一次读取可能非常关键,一次写入也可能只是允许丢弃的遥测。应使用可信的应用分类,而不是任由调用方把自己的所有请求都声明成最高优先级。 统一准入上限或按比例拒绝,也可以是合理政策,不能因为没有业务优先级就一概认为它们错误。需要明确公平性与容量之间的取舍,包括低优先级工作是否可能长期得不到执行,以及不同租户是否共用同一个限制。拒绝顺序应该服务于真实契约,而不是凭方法名称猜测。 请求数量、字节、执行时间和 CPU 成本是不同预算。只限制三个任务,如果每个任务都可以保留任意大的正文,仍然不能约束内存。应选择与受保护资源对应的信号,并通过真实请求组合验证行为,不能把单一计数上限解释成对所有资源的统一保证。 限制准入,而不只是线程数量 线程池的工作线程上限约束同时执行的任务数,不一定限制已经接受、正在排队的工作。只在工作线程内部获取信号量,会让它前面的队列继续增长。应在昂贵工作提交之前做有界准入,并在任务生命周期真正结束时归还容量,而不是等到执行阶段才开始计数。 名称类似的代理限制,也需要精确理解。NGINX limit_conn 按文档规定的连接规则计数;在 HTTP/2、HTTP/3 中,每个并发请求都被单独计入。不能默认某个“连接数上限”始终只是在统计 TCP socket,否则复用连接下的真实请求并发可能与预想完全不同。 下面的实验同时约束排队和运行中的任务。被拒提交不会创建 future;完成回调会在成功、失败或执行前取消后归还名额,而向已经关闭的执行器提交失败,也会归还尚未使用的名额。这样准入预算对应完整生命周期,而不是只覆盖正常结束的路径。 坏掉的样子 如果已经解析巨大正文、打开数据库连接或执行了业务写入,才开始拒绝,可能已经来不及保护目标资源。昂贵的日志和错误响应生成,也可能让拒绝路径本身过载。应一起观察拒绝成本、获准吞吐量、排队时长、活动工作量与资源压力,而不是仅统计拒绝了多少次。 HTTP 503 可以表达服务暂时不可用,429 则表示调用方在某种政策下请求过多。Retry-After 只是等待信号,不证明重放写操作安全,也不保证之后一定存在容量。立即重试可能把刚削减的负载原样加回来,因此拒绝与客户端行为必须放在同一条链路中理解。 不要仅因为调用方停止等待,就提前回收仍在运行任务的名额。等待 future 结果超时,不会终止工作线程。提前归还会让新任务与实际上尚未结束的旧任务重叠,破坏原先承诺的容量上限;资源记账应以工作真实结束为依据。 提交前的三个名额同时限制排队和运行任务;十二个任务中九个被拒。名额跟随真实生命周期。这个统一门槛没有实现优先级或期限调度。 快速验证 保存为 shedding_check.py,用 Python 3.10 以上版本执行 python3 shedding_check.py。实验已在 Python 3.14.4 上运行,只使用标准库。真实线程等待测试协调事件,不访问外部服务,也不制造生产负载。前后两种配置接收的是同样十二个任务的突发,用来比较获准与等待数量。 ...

五月 16, 2027 · 3 分钟 · XB Field Notes
收到三秒等待要求后形成最早重试边界,非负抖动把计划放在边界之后;截止期限可以终止策略。

Retry-After 告诉你何时再试

Retry-After 告诉客户端,在发起后续请求之前应当等待多久。它是调度信号,不证明下一次请求一定成功,也不证明重放原操作是安全的。客户端仍然需要截止期限、尝试次数政策,以及哪些操作可以重试的判断,不能把这些责任都交给一个响应头。 结合响应语境解释字段值 RFC 9110 允许非负十进制整数秒数,或者 HTTP 日期。秒数从收到响应之后开始计算,不是从发出原请求时算起;日期则表示一个绝对 UTC 时刻,因此正确解释它依赖客户端墙上时钟足够准确,不能把两个形式都当成“从请求开始算多久”。 发送方必须使用首选的 IMF-fixdate 格式。解析 HTTP 日期的接收方还必须接受两种旧格式,其中旧 RFC 850 格式的两位年份需要遵守五十年解释规则。负数、小数秒和任意文字,都不是合法的 delay-seconds;零和已经过去的日期,则不要求增加服务器指定的等待时间。 配合 503 时,该字段估计服务还会对客户端不可用多久;配合 3xx 响应时,它要求在发送重定向请求之前至少等待相应时间。RFC 6585 另行允许 429 携带 Retry-After,但这里是可选字段,不是每一条限流响应都必须包含的内容。 尊重等待时间,但不无限等待 一个合法等待值可能长于调用方剩余期限。客户端可以停止重试并报告失败,或者在另有授权的流程中安排后续处理;不能只是为了符合本地上限,就缩短服务器要求的等待,然后提前发起请求。很长的值并不自动等于格式错误,也可能是真实的维护安排。 把绝对日期换算成等待时长后,执行调度和经过时间记账应使用单调时钟。真正开始下一次尝试之前,还应重新检查期限,并为这次请求本身保留完成时间。时钟偏差、响应传输和调度延迟,都说明计算出的计划时间与实际请求时间不是同一份证据。 如果字段缺失或无法使用,有界客户端政策可以采用自己的退避规则。但那是客户端的选择,不是服务器给出的恢复估计。必须明确最多尝试几次、总共允许耗费多久,避免一个可选字段让暂时失败变成没有终点的重试循环。 相同等待值仍可能让调用者同时回来 如果成千上万的客户端几乎同时收到相同等待值,它们仍可能在同一个时刻返回。Retry-After 本身不会把这些请求自动摊开到恢复窗口。可以在尊重要求的等待后增加非负抖动,或者用其他不早于该边界的调度方式,但不能向前减掉等待来制造错峰。 组合本地退避和服务器提示时,需要同时保留两者约束。示例取本地退避与解析出的等待值中较大者,再添加正向抖动。注入的偏移量展示的是调度关系,不是实际分布式流量的测量结果,也不是对某个生产环境抖动范围的推荐。 坏掉的样子 当原请求结果未知时,盲目重放写操作可能重复产生副作用。429、503 和 Retry-After 都不会赋予操作幂等保证;重复非幂等工作之前,需要依据应用自己的重试契约,包括操作标识或去重规则,不能只检查响应头是否存在。 ...

五月 4, 2027 · 4 分钟 · XB Field Notes