三个依赖框显示 alias、middle、target,分别标注同一回答中独立的 9、12、20 秒 TTL。独立 HTTP 身份栏保留 Host 为 alias.lab.test 加临时端口。箭头表示 DNS 依赖,不是 URL 重定向。

CNAME 改变 DNS 解析路径,请求主机名仍保持原样

CNAME 记录把一个别名与另一个 DNS 名称关联起来。通过别名解析地址时,解析器沿目标继续查找,直到获得请求的数据或遇到失败。这可以让公开主机名保持稳定,同时改变它背后的 DNS 依赖。它不会让浏览器跳转到另一个 URL,也不会替换应用正在请求的主机名;是否改变客户端可见地址,应由相应应用协议行为决定。 坏掉的样子 别名解析完全正常,目标服务器仍可能返回错误网站。HTTP 继续标识原始主机名,HTTPS 证书验证也关联所请求的服务身份。如果只配置目标名称对应的虚拟主机或证书,原别名请求未必能够工作。DNS 提供的是获得地址的路径,选中的服务器还必须接受用户和应用真正请求的名称。仅检查最终 IP 相同,无法验证这项应用契约。 CNAME 不能与普通 A、AAAA、TXT 等数据共存于同一个所有者名称。DNSSEC 元数据存在特定例外,但这不会让 CNAME 与 A 共存变得有效。普通区域顶点本来就需要 SOA 和 NS,因此在顶点放传统 CNAME 会产生冲突。服务商提供的别名展平等功能,应依据其实际文档理解,不能把界面里类似的名称直接当成标准 CNAME 规则发生变化。 别名链可能终止于不存在的数据,也可能形成循环。每个依赖还有自己的缓存寿命与管理边界;缩短别名 TTL,不会缩短以另一个记录集缓存的目标地址。同时,并非每增加一个别名都必然增加一次网络往返:服务器可能在同一回答中附带多段链,解析器也可能已经缓存相关数据。延迟分析应结合实际问答,而不是机械按链长计数。 解析别名,保留请求身份。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 将脚本保存为 check-cname.py,准备 Docker 后执行 python3 check-cname.py。固定环境使用 BIND 9.18.49、dnsmasq 2.90 和小型 HTTP 服务。所有名称均属于隔离测试区域;解析配置修改和候选区域文件留在可丢弃容器内部,不改公共域名或宿主解析器。运行结束后,脚本清理自己创建的实例和临时镜像标签: ...

七月 9, 2027 · 3 分钟 · XB Field Notes
DNS 回答在 A、B 与 B、A 顺序之间轮换,但文章示例的应用保留一次选择,将十二次请求发给选中端点,另一端点收到零次。

多个 DNS 地址不保证流量均匀或端点健康

同一名称下的多条 A 或 AAAA 记录提供一个地址集合。DNS 服务器可能轮换返回顺序,但收到集合后如何使用,由解析器和应用决定。普通地址记录不携带请求负载测量或应用健康结果;仅仅存在多个地址,不能保证均匀流量、自动故障转移,甚至不能保证不同实现看到相同的第一个地址。应将地址发现与后续选择分别理解。 轮换地址集合不会按应用请求调度,也不会自动检测端点健康。文章中的一次保存选择造成十二比零分布,恢复依赖客户端明确重试。此图未新增运行验证。 坏掉的样子 DNS 回答分布与应用请求分布的分母不同。一个递归解析器可以用缓存回答许多客户端,一个应用也可能只查询一次,随后发出数千次请求。连接池会进一步强化这种差异。即便权威回答中的首个地址完美交替,实际服务器请求量仍可能严重不均。只统计 DNS 查询数量,不能证明用户业务已经按同样比例分配。 普通静态记录集中的端点失败后,其地址仍会保留,直到 DNS 数据被修改或另一个系统主动修改它。DNS 服务器不会仅因返回地址就知道应用健康状态。有的客户端连接失败后会尝试其他地址,有的会耗尽截止时间或者直接向调用方报错。是否重试、何时重试及哪些操作可重试,都应针对实际客户端和业务验证,不能默认由 DNS 完成。 把这种机制描述成保证公平的抛硬币,也不准确。顺序可能由配置决定、被随机化、被缓存保留,或者被地址选择策略重排。客户端可能选择首项、并发尝试,或复用先前结果。要解释一次分布观察,除了后端请求计数,还需要知道 DNS 配置、解析器行为、连接复用和工作负载;少了这些条件,无法判断不均衡出现在哪一层。 快速验证 将脚本保存为 check-dns-addresses.py,准备 Docker 后执行 python3 check-dns-addresses.py。固定环境使用 BIND 9.18.49,并明确把 rrset-order 设置为循环顺序。两个真实 HTTP 监听在隔离容器中绑定不同回环地址、使用相同临时端口,响应正文标识真正处理请求的端点。脚本不依赖公网域名,也不会访问现有业务服务器: ...

七月 6, 2027 · 3 分钟 · XB Field Notes
客户端需经过可用路由与防火墙策略才能到达节点侧 NodePort 入口,再转发到合适后端。分配端口不等于已经可达。

NodePort 通过符合配置的节点地址开放 Service

NodePort 类型的 Service 分配一个集群范围的端口,并通过符合配置的节点地址转发流量。它适用于外部负载均衡、开发环境及有明确用途的服务暴露,但不保证每台机器的每个地址都能访问,也不保证每台节点都有一个普通应用监听套接字。路由、防火墙、代理实现与流量策略仍然决定真实访问路径,不能从 Service 类型推断全部网络条件。 NodePort 通过符合配置的节点地址转发到后端端口。Cluster 与 Local 改变端点候选范围;端口分配不能证明应用可达。本次未重跑诊断。 一次分配,多种转发路径 默认分配范围为 30000–32767,管理员可以配置其他范围。请求指定 nodePort 时,只有分配与校验允许才会成功;端口冲突会拒绝 API 事务,通常不是先创建一个对象,再悄悄留下不可用端口。应分别核对命令退出状态与实际保存的对象,不能把创建失败解释成运行期网络故障。 Service port、targetPort 与 nodePort 各有职责:前者是虚拟服务入口,中间值指定后端端口,最后一个是节点侧入口。多端口 Service 可以占用多个号码。LoadBalancer 类型也可能分配 NodePort,除非配置与实现允许省略。因此,仅统计类型为 NodePort 的对象,会漏掉地址池的部分实际使用者,第一端口也不代表整份清单。 ...

五月 11, 2027 · 1 分钟 · XB Field Notes
原有 Go HTTP/1.1 实验在 IdleTimeout 200 ms 下,让两次 /ping 和执行 350 ms 的 /slow 复用同一连接。响应完成后等待 StateClosed,再发 GET 才使用新编号。另一个不完整请求头探测由 ReadHeaderTimeout 500 ms 约束,读到 EOF 且未调用处理器。客户端 2 s 等待上限另算;未测试 Nginx,本轮也未重跑原文实验。

空闲超时回收边缘连接

空闲超时会在一段时间没有符合定义的活动后关闭连接。它是一项资源保留策略,不是客户端已经消失的证明。健康客户端也可能暂时安静,关键是说清哪种活动重置哪只计时器,以及连接关闭之后,协议允许调用方如何恢复;不能仅凭“连接还在,但没有字节”就判断它应当被立即回收。 原有 Go HTTP/1.1 实验在 IdleTimeout 200 ms 下,让两次 /ping 和执行 350 ms 的 /slow 复用同一连接。响应完成后等待 StateClosed,再发 GET 才使用新编号。另一个不完整请求头探测由 ReadHeaderTimeout 500 ms 约束,读到 EOF 且未调用处理器。客户端 2 s 等待上限另算;未测试 Nginx,本轮也未重跑原文实验。 仅为原文实验示意,本轮未重跑。 空闲属于某个明确的边界 TCP 连接会占用文件描述符、内核缓冲,以及应用或代理记录的连接状态,但通常不需要为每条连接建立独立的路由表项。回收暂时不用的连接,可以限制保留的资源;复用健康连接则能减少重新建连的成本。策略应服务于工作负载,而不是把所有沉默连接都理解成已经被遗弃的泄漏。 ...

四月 26, 2027 · 3 分钟 · XB Field Notes
TCP 443 一栏在同一个明文监听上显示 HTTP 收到 200、HTTPS 的 TLS 失败;独立 UDP 443 一栏收到数据报回复。相同数字不合并传输端口空间。

默认端口是约定,不负责协议识别或安全认证

默认端口让客户端在协议或配置已经给出数字时省略它。HTTP 常用 TCP 八十,HTTPS 常用 TCP 四百四十三,SSH 常用 TCP 二十二。这些分配减少了配置工作,却不会检查监听发送的字节,也不会建立对端身份。一个端点还包含地址和传输协议;客户端最终使用什么应用协议,必须与监听真正提供的能力一致。 坏掉的样子 把服务迁移到其他端口,不会让现有客户端自动发现变化。URL、数据库连接配置、健康探测、代理和访问规则,可能各自保存目的地。只改服务器,可能让探测仍检查旧监听,或让客户端连接一个已经无人使用的端口。需要记录预期映射,并检查所有受影响的真实调用方,不能仅凭新服务启动成功就宣布迁移完成。 熟悉的数字也容易造成错误信任。进程可以在 TCP 四百四十三提供明文 HTTP,也可以在 TCP 八十提供其他协议。选择 HTTPS 意味着实际执行 TLS 和预期证书检查,单独选择四百四十三不会增加加密。同样,TCP 建连成功既不证明下一步对话符合应用协议,也不证明对端获得了服务该身份的授权。 服务名称数据库描述映射,不管理服务生命周期。Linux 的 /etc/services 将名称映射到端口与传输协议,添加记录不会启动守护进程,也不会打开防火墙。该文件可能不同于另一台主机的数据库或当前 IANA 登记内容。把服务名称转换成端口,与把主机名称解析成地址,是连接建立之前两项不同输入,不能混为同一种发现机制。 端口数字不会选择协议。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 将脚本保存为 check-default-ports.py,准备 Docker 后执行 python3 check-default-ports.py。固定 Linux 镜像中的 Python 3.11.16 不发布宿主端口。实验有意在 TCP 四百四十三启动明文 HTTP,随后让 UDP 套接字绑定同一个数字。所有监听都可丢弃,不会替换宿主已有的生产 HTTPS 服务,也不会要求修改生产端口分配: ...

四月 14, 2027 · 2 分钟 · XB Field Notes
退避上限先增长后封顶,完全抖动的实际等待在区间内变化,也可能变短。次数上限、总期限和更广的重试预算是独立约束。

带抖动与退避的重试

重试意味着再增加一份工作,不是自动提高可靠性的操作。它可能帮助恢复暂时失败,也可能重复一个不安全的业务行动,或把额外需求送到已经过载的依赖。选择延迟公式之前,应先确定哪些操作允许重复、总共能投入多少次尝试与多长时间,以及这些额外工作由哪一层负责。 圆点为示意取样,不是实际到达时刻。抖动不保证容量或逐次延长等待。检查重放资格、次数、总期限、更广预算和 Retry-After;必需等待放不进期限时应放弃。 先约束尝试次数,再安排时间 需要区分尝试和重试:总共三次尝试,是第一次调用加最多两次重试。还要确定恢复责任属于哪一层。如果嵌套的三层各自允许三次尝试,一次外层行动就可能对持续失败的最底层发起二十七次调用。抖动只能改变这些调用的时间,不能消除次数相乘的结果。 Google SRE 同时讨论单请求预算与更广范围的重试预算。即使每个请求都有限次尝试,大量失败的客户端仍可能制造相当可观的额外工作。应把重试流量与原始需求分别统计,并核对 SDK、代理和应用层已经存在的政策,避免每一层都独立认为自己只增加了少量调用。 指数退避增长的是上限 封顶指数退避让等待上限逐步增大,直到达到最大值。没有抖动时,同时失败的客户端可能算出相同的下次时间,甚至在到达上限后继续保持同样间隔。等待更久,并不必然打破同步,不能把周期变长当成请求已经分散的证据。 完全抖动会在零与当前上限之间采样。因为取值随机,即使上限正在增长,下一次实际等待仍可能比上一次短。因此,“每次失败后一定比之前多等一会儿”不是这种算法的准确描述;需要区分退避上限的变化与本次真正取到的延迟。 AWS 的文章比较了完全抖动、等量抖动和去相关抖动,并报告特定竞争模拟的结果。这些结果能帮助理解取舍,却不保证某种政策在所有工作负载下都具有最低延迟,也不保证完全没有突发。相同随机种子、定时器粒度和大量客户端,仍可能形成聚集。 整条操作共用一个截止期限 预算必须同时包括执行、等待和后续尝试。每次调用都重新拿到完整超时时长,会悄悄扩大原调用方允许的生命周期。应把剩余预算传给下游,并在准备进行更多工作之前再次检查,而不是只限制两次调用之间睡多久。 进程内计算经过时间,应使用单调时钟。客户端超时,也不证明服务器已经停止或从未提交;取消与远端截止期限需要真实的传输层支持,业务仍然需要重试或幂等契约。不能因为本地函数返回错误,就把未知结果当成未执行。 Retry-After 等服务端等待提示会增加另一项约束。不能因为本地抖动公式允许较小值,就采样出早于该边界的请求时间。如果必须等待的时间已经无法放进剩余期限,应在当前操作中放弃重试,而不是为了凑预算缩短服务端要求。 坏掉的样子 永久性的参数验证错误,不会因为重复发送就变得合法。非幂等操作在结果不明时重放,可能再次产生同一业务效果。应明确选择可重试的失败类别;捕获所有异常后循环重试,既会掩盖编程错误,也可能把一次局部故障推成持续过载。 只有退避上限,没有次数或总耗时限制,仍然允许无限工作;但期限过短,又可能让所有昂贵却合法的操作一直失败。需要一起观察完成工作量、重试次数、总延迟和放弃原因。仅看到错误率下降,可能忽略为了获得成功而越来越高的重试成本。 快速验证 保存为 retry_check.py,用 Python 3.10 以上版本执行 python3 retry_check.py。2026-10-19 以 Python 3.12.14 完整运行原样程序并通过,只使用标准库。时钟和下游调用都是确定性模型,不真实等待,不启动服务,也不访问依赖。示例政策只允许 GET、HEAD 在超时或 503 后重试,不是所有 HTTP 客户端行为的实现。 from collections import Counter import random class Clock: def __init__(self): self.now = 0.0 def advance(self, seconds): self.now += seconds class ScriptedCall: def __init__(self, clock, steps): self.clock, self.steps, self.timeouts = clock, iter(steps), [] def __call__(self, timeout): self.timeouts.append(timeout) status, duration = next(self.steps) self.clock.advance(min(duration, timeout)) if duration >= timeout: raise TimeoutError("the cooperative fixture used the remaining budget") return status def retry(call, clock, budget, sample, method="GET", max_attempts=3): deadline = clock.now + budget for attempt in range(1, max_attempts + 1): remaining = deadline - clock.now if remaining <= 0: return "deadline", attempt - 1 try: status = call(remaining) except TimeoutError: status = None if clock.now >= deadline: return "deadline", attempt if status == 200: return "success", attempt if status not in (None, 503) or method not in ("GET", "HEAD"): return "not-retryable", attempt if attempt == max_attempts: return "attempt-limit", attempt ceiling = 2.0 ** min(attempt - 1, 3) delay = ceiling * sample() assert 0 <= delay <= ceiling if delay >= deadline - clock.now: return "wait-exceeds-budget", attempt clock.advance(delay) raise AssertionError("positive attempt limit required") clock = Clock() call = ScriptedCall(clock, [(503, 0.25), (503, 0.25), (200, 0.25)]) assert retry(call, clock, 5, lambda: 0.5) == ("success", 3) assert clock.now == 2.25 and call.timeouts == [5, 4.25, 3] print("three attempts finish at 2.25; propagated timeouts shrink from 5 to 4.25 to 3") class EarlyTimeout: def __init__(self, clock): self.clock, self.timeouts = clock, [] def __call__(self, timeout): self.timeouts.append(timeout) self.clock.advance(0.25) if len(self.timeouts) == 1: raise TimeoutError("a transient failure before the deadline") return 200 clock = Clock() call = EarlyTimeout(clock) assert retry(call, clock, 5, lambda: 0.5) == ("success", 2) assert clock.now == 1.0 and call.timeouts == [5, 4.25] print("an early timeout can recover without resetting the shared deadline") for method, status in (("POST", 503), ("GET", 400)): clock = Clock() call = ScriptedCall(clock, [(status, 0.25)]) assert retry(call, clock, 5, lambda: 0.5, method) == ("not-retryable", 1) clock = Clock() call = ScriptedCall(clock, [(503, 0.25)] * 4) assert retry(call, clock, 5, lambda: 0.5) == ("attempt-limit", 3) assert len(call.timeouts) == 3 print("POST, permanent failure, and the total-attempt cap stop further work") clock = Clock() call = ScriptedCall(clock, [(503, 0.25)] * 7) assert retry(call, clock, 100, lambda: 1.0, max_attempts=7) == ("attempt-limit", 7) assert clock.now == 32.75 # Seven 0.25 calls plus waits 1, 2, 4, 8, 8, 8. print("the exponential ceiling stops at 8 rather than growing without bound") clock = Clock() call = ScriptedCall(clock, [(503, 0.1)]) assert retry(call, clock, 0.5, lambda: 1.0) == ("wait-exceeds-budget", 1) assert clock.now == 0.1 and len(call.timeouts) == 1 clock = Clock() call = ScriptedCall(clock, [(503, 99)]) assert retry(call, clock, 0.5, lambda: 0.5) == ("deadline", 1) assert clock.now == 0.5 clock = Clock() call = ScriptedCall(clock, []) assert retry(call, clock, 0, lambda: 0.5) == ("deadline", 0) and call.timeouts == [] print("a wait that cannot fit is abandoned; cooperative execution cannot consume a fresh budget per attempt") rng = random.Random(7) # Repeatable samples for this experiment, not a production seed. delays = [rng.random() for _ in range(100)] bins = Counter(int(delay * 10) for delay in delays) fixed_bins = Counter(int(1.0 * 10) for _ in range(100)) assert all(0 <= delay < 1 for delay in delays) and sum(bins.values()) == 100 assert max(bins.values()) < 100 assert max(fixed_bins.values()) == 100 print("first-retry 0.1-second-bin peak: fixed delay", max(fixed_bins.values()), "; full-jitter sample", max(bins.values())) leaf_calls = 0 class Unavailable(Exception): pass def nested(depth): global leaf_calls if depth == 0: leaf_calls += 1 raise Unavailable for _ in range(3): try: return nested(depth - 1) except Unavailable: pass raise Unavailable try: nested(3) except Unavailable: pass assert leaf_calls == 27 print("three retrying layers, each allowing three total attempts, invoke the failing leaf 27 times") 两次失败后成功,最终模型时间为 2.25。传给各次调用的超时依次缩短为五秒、4.25 秒和三秒,没有每次重新领取一份完整预算。额外用例确认提前超时仍能在同一预算内恢复;指数先封顶再计算,避免总尝试上限很大时先生成过大的幂。其他断言覆盖总尝试次数、八秒退避上限、永久错误、POST 不自动重放、放不进预算的等待,以及开始时就已经耗尽的期限。 ...

四月 10, 2027 · 4 分钟 · XB Field Notes
预期 IPv4 地址匹配模型:矩阵行是发往 127.0.0.1、127.0.0.2 的请求,列为指定 127.0.0.1 与通配 0.0.0.0 绑定。指定绑定只接受前者,通配接受两者。这不是复现输出,也不证明 IPv6 就绪。

绑定地址选择本地目的地,可达性还取决于完整路径

服务器的绑定地址决定套接字可以接受哪些本地目的地址。IPv4 监听绑定到 127.0.0.1,与绑定到通配地址 0.0.0.0,具有不同匹配范围。但任何一个设置都不能独立描述完整访问策略。路由、网络命名空间、包过滤、端口发布、代理和应用认证,共同决定请求怎样到达监听,以及连接之后能够执行什么操作。 坏掉的样子 通配监听不等于互联网已经能够访问服务。进程可能位于没有外部接口的隔离命名空间,也可能受到防火墙限制。反过来,回环监听也可以被接受远程请求的反向代理或隧道间接访问。判断谁能使用服务前,需要检查这些转发路径。不能看到监听地址就把网络路径、调用者身份和业务权限合并成一个结论。 IPv6 通配地址 :: 还涉及地址族选择。在 Linux 中,IPV6_V6ONLY 控制 IPv6 套接字是否也接受映射形式的 IPv4 连接。默认值取决于环境与应用,所以监听列表里只有一条 IPv6 项,并不能证明 IPv4 不可用。服务契约要求明确地址族边界时,应显式设置这个选项,并分别建立连接验证,而不是从显示格式推测行为。 指定地址也可能让服务在启动阶段失败。地址与端口被冲突套接字占用,和地址根本没有在本地配置,是不同错误。重用选项和非本地绑定机制会改变部分场景,本例没有启用这些机制。如果把所有绑定失败都解释成端口被占用,就可能去停止无关进程,却没有修复真正缺失的地址配置。错误码应与启动参数一起保留。 绑定范围选择本地地址。脚本仅验证 IPv4,IPv6 面板是独立条件模型。不声称已有复现输出或通过技术验收。 快速验证 2026-10-19 源码审阅说明:修订后的实验仅使用 IPv4。Docker 的 none 驱动不配置 ::1;IPv6 需要独立配置并核验。技术与执行验收仍保持暂缓,等待获准环境中的完整运行。下文均为预期结果,并非本次复现输出,静态修订不等于运行验证。 保存为 check-bind-address.py,在获准的 Linux Docker 主机上运行 python3 check-bind-address.py,所固定镜像须事先已存在。本次没有重新确认其宣称的 Python 版本。脚本禁止自动拉取镜像,不发布宿主端口,也不修改宿主防火墙;所有监听和客户端都位于同一个可丢弃网络命名空间: ...

四月 2, 2027 · 2 分钟 · XB Field Notes
健康的优先级 10、权重 1 目标先于优先级 20、权重 65535 备机;客户端在连接失败后回退。

SRV 提供目标和端口,客户端必须理解其选择规则

SRV 通过优先级、权重、端口和目标主机名描述服务位置。它的所有者通常组合服务标签、传输标签与域名,例如 _echo._tcp.lab.test。理解该服务的客户端可以先查 SRV,再通过 A 或 AAAA 取得目标地址,连接记录给出的端口。普通主机名查询不会自动执行这套应用相关的发现流程,服务名与查询方式必须由调用方契约明确。 优先级选择可达的组,权重只在同组内起作用。文章每组只有一个目标;故障转移展示客户端处理,不是同组加权选择或 DNS 健康摘除。 坏掉的样子 发布 SRV 不会让所有现有客户端自动采用它。应用需要规定服务标签,并实际支持相应发现行为。只接受普通主机名和配置端口的浏览器或库,可能根本不询问 SRV。因此,手工查询 SRV 成功,不能证明应用已经改变端点选择,也不能据此宣布今后不再需要更新配置。验证应沿真实调用方进行,不能止于区域里已有记录。 优先级和权重不是可互换字段。在可达目标中,较低数值优先级更优先;权重只影响相同优先级记录之间的选择。较高数值备机即使具有很大权重,也不能越过可用的低数值目标。把所有记录按权重统一排序,或直接采用报文中的首项,都可能反转服务所有者设计的主备关系。解释结果前,应确认客户端是否正确使用这些字段。 发现也不证明健康。监听停止后,目标仍可继续被发布,SRV 与目标地址记录集还具有各自缓存寿命。客户端需要连接截止时间与适当重试策略。连接被拒绝后重试读取,与重复可能已产生副作用的操作不同;端点故障转移必须符合应用协议,不能因为还有候选项,就无条件把每个业务动作再执行一遍。 快速验证 将脚本保存为 check-srv-discovery.py,准备 Docker 后执行 python3 check-srv-discovery.py。固定 BIND 9.18.49 环境发布测试 _echo._tcp 服务,两个真实 HTTP 监听在不同临时端口提供可识别的只读响应。HTTP 只是实验中便于观察的载荷;示例是自定义 SRV 客户端,不代表通用 HTTP 客户端本来就实现了这个服务标签: ...

三月 13, 2027 · 3 分钟 · XB Field Notes