文章标题旁是三个相连面板,英文标签依次为 Observe、Decide 和 Act。

fsync 在更新协议中完成存储同步请求

普通 write(2) 成功,可以让实际接受的字节通过内核文件状态被访问,但本身不能建立崩溃持久性。fsync 请求同步文件数据及相关元数据,并等待存储栈报告完成。它是更新协议中的一个操作,仍需要正确顺序、错误检查、目录项处理和底层真实保证。不能只在代码里出现一次 fsync,就推断整个更新已经具有完整事务与恢复语义。 坏掉的样子 用户态缓冲还形成更早一层边界。语言缓冲区尚未把字节提交给内核时,对描述符调用 fsync 无法同步这些字节。应先 flush 缓冲,并检查两个阶段的错误。普通 write 也可能只完成部分写入,延迟存储错误则可能在同步时才出现,而不是在最初写入时报告,因此每层返回结果都需要按契约处理。 同步新建文件,不一定同步命名它的目录项。在 Linux 上,持久创建或替换协议还需要适当的目录同步。原子 rename 提供运行期间的命名空间可见性保证,却不会独立证明目录变化能够经历崩溃保留。当前打开新路径能读到数据,与重启后仍能通过该名字找到预期对象,是不同问题。 fdatasync 不意味着完全不处理元数据。它同步数据,以及后续正确读取数据所需的元数据,包括变化后的文件长度;对于读取不必需的其他元数据更新,则可以省略。把它理解为与 fsync 在所有方面相同,或者完全无视元数据,都会掩盖选择两者的真正依据,应根据应用需要保持的状态理解同步范围。 快速验证 把脚本保存为 syncfile.py,在装有 Python 和 strace 的一次性 Linux 系统中运行 python3 syncfile.py。临时目录必须支持普通文件和目录同步。实测使用 Linux 6.12.76、Python 3.11.2、strace 6.1 和 ext4 卷。样本检查真实调用与运行结果,不会切断存储电源: from pathlib import Path from tempfile import TemporaryDirectory import subprocess,sys program=r''' from pathlib import Path import errno,os,sys root=Path(sys.argv[1]);current=root/'current';candidate=root/'candidate' current.write_bytes(b'old');old=current.open('rb') try: with candidate.open('wb') as handle: handle.write(b'new');handle.flush();os.fsync(handle.fileno()) os.replace(candidate,current) directory=os.open(root,os.O_RDONLY|os.O_DIRECTORY) try:os.fsync(directory) finally:os.close(directory) assert current.read_bytes()==b'new' and old.read()==b'old' with current.open('ab') as handle: handle.write(b'-extended');handle.flush();os.fdatasync(handle.fileno()) assert current.read_bytes()==b'new-extended' and current.stat().st_size==12 read_end,write_end=os.pipe() try: try:os.fsync(write_end) except OSError as error:assert error.errno==errno.EINVAL else:raise AssertionError('fsync on a pipe unexpectedly succeeded') finally:os.close(read_end);os.close(write_end) finally:old.close() print('replacement content, old open reference, appended length, and unsupported-pipe error verified') ''' with TemporaryDirectory(prefix='xbcatm-fsync-') as directory: root=Path(directory);trace=root/'trace' result=subprocess.run(['strace','-yy','-s','200','-e','trace=%file,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() file_sync=next(i for i,line in enumerate(lines) if 'fsync(' in line and str(root/'candidate') in line and '= 0' in line) rename=next(i for i,line in enumerate(lines) if line.startswith('rename') and str(root/'current') in line and '= 0' in line) directory_sync=next(i for i,line in enumerate(lines) if 'fsync(' in line and '<'+str(root)+'>' in line and '= 0' in line) assert file_sync<rename<directory_sync assert any('fdatasync(' in line and str(root/'current') in line and '= 0' in line for line in lines) print(result.stdout,end='') for line in lines: if line.startswith(('fsync(','fdatasync(','rename')):print(line) print('syscall order verified: flush data, fsync candidate, replace name, fsync parent directory') 子进程创建内容为 old 的当前文件,并保留一份打开读取引用。然后把 new 写入独立候选文件,flush Python 缓冲,再对候选描述符调用 fsync。候选文件位于同一个目录,也使此实验的后续 rename 保持在同一文件系统内,不依赖跨挂载复制等不同操作来模拟替换。 ...

十一月 27, 2026 · 2 分钟 · XB Field Notes
观察、决策与行动三块面板,旁边的标题说明限流键定义预算范围。

限流需要一个稳定的客户端标识

限流键定义了哪些请求共用一份预算。账号、API 凭据、地址、租户和已认证会话是不同身份,不是“客户端”的几种可互换写法。选错归组方式,可能让无关用户相互挤占额度,也可能让同一份工作只改一个字段,就拿到新的使用机会。 使用政策真正承诺的身份 如果规则是按账号限流,就应从认证结果中取得账号,而不是相信调用方提交的账号请求头或查询参数。仅仅看见一个凭据,并不能证明它有效;认证层必须先验证凭据、解析主体,再让账号级准入决定使用这个结果。限流键的可信程度不能高于它的来源。 按凭据限流也可以是合理的产品契约,但行为与按账号限流不同。轮换凭据或再创建一份凭据,可能得到新的凭据键,却仍属于同一个账号。应先决定这是否应当带来新额度,而不是等出现滥用或投诉后,才发现实现按的身份与产品描述并不一致。 一个账号不一定只对应一个人或一个应用,一个人也可以拥有多个账号。稳定账号键改善的是归组的一致性,并不证明公平,不阻止批量养号,也不限制整个服务的总需求。其他层级的限制应由明确需求支持,不能认为“已经按账号计数”就涵盖所有容量风险。 地址表示的是网络路径 企业 NAT、运营商网络或 VPN 后面的多个账号,可能共用一个公网地址;按 IP 的桶会把它们的流量合在一起。反过来,更换地址的调用方可能进入一个新桶。这两种结果并不说明地址解析错误,而是说明网络地址与业务主体本来就在表示不同对象。 没有已验证账号的公开接口或认证前入口,仍可以用地址级限制保护资源。但应按地址政策评估阈值,包括正常共享网络的流量,不能把它描述成精确的个人限制,也不能假设一个地址规则一定拦得住分散在许多网络上的抓取者。 地址在成为键之前需要语法规范化。同一个 IPv6 地址的不同合法写法,不应创建多份额度。是否按网段聚合,或把特定地址族形式视为同一个对象,是另外的政策选择,需要权衡误伤范围;规范化文本本身并不能替你回答这些业务问题。 转发身份必须有信任边界 随意提交的 Forwarded、X-Forwarded-For 或类似字段,不是客户端来源的证明。RFC 7239 明确指出,客户端和沿途节点都能改变转发信息。是否可信取决于直接连接的对端、配置的代理链,以及这条链如何移除或安全处理用户原先提交的字段。 NGINX 的真实 IP 模块需要明确配置可信来源地址和递归处理规则。“永远取转发字段里的第一个地址”不是适用于所有网络的正确方法。如果代理接收后原样转发了不可信声明,看似稳定的身份键就可能变成由调用者控制的新预算生成器。 下面的实验只模拟一个已经配置的可信代理,它会覆盖字段,并只发送一个 IP 字面值。其他对端提交的替换声明被忽略,来自这个代理的多地址列表则被拒绝。它不是通用代理链解析器,也不是已经部署并验收的 NGINX 配置,不能直接扩大其覆盖范围。 坏掉的样子 键里漏掉租户维度,会合并不同租户中标识相同的账号。按原始 URL 计数,又可能因为查询参数不同,把同一个操作拆成多个桶;如果政策按操作限流,应使用路由解析出的稳定操作身份。直接拼接字符串还可能产生歧义,应在编码中保留各组件边界。 不要把原始 bearer 秘密作为预算键或诊断标签。稳定的内部凭据标识或主体标识通常更合适,日志也仍需要隐私控制。无限创建身份会消耗限流器内存;评估过期或状态淘汰时,还应考虑桶被重置后产生的新额度,不能只看到内存回收这一面。 选定阈值前,应测试共享地址、漫游客户端、凭据轮换、租户隔离和伪造声明等真实身份组合。还要沿数据来源检查所谓“已验证主体”是否真的来自认证结果。如果它本身是从不可信请求直接复制出来的,后面再精巧的字典键也修复不了这个信任错误。 快速验证 保存为 identity_check.py,用 Python 3.10 以上版本执行 python3 identity_check.py。实验已在 Python 3.12.14 上运行,只使用标准库。记录代表已经完成认证和路由解析的测试请求,并不是认证实现;计数器只模拟一个测试窗口,不启动服务,不发送网络请求,也不写文件。 ...

十月 29, 2026 · 2 分钟 · XB Field Notes
以观察、判断和行动三个面板呈现请求分类与错误预算。

错误预算为允许的失败命名

“可靠性达到百分之九十九点九”还不是一项完整的工程约定。它针对哪些用户、在哪个位置测量、观察多长时间,又把怎样的结果视为成功?只有回答这些问题,错误预算才能把允许的失败变成可计算的数量。因此,首先要定义统计总体和成功规则,而不是先挑选仪表盘上的绿色阈值。两个页面展示相同的百分比,如果分母和判定不同,也不能直接比较。 统计真正承诺的用户体验 服务水平指标描述实际测得的量,服务水平目标则为它规定阈值与时间窗口。对于基于事件的指标,成功比例等于符合条件的成功事件数除以全部符合条件的事件数。成功可以要求一次业务操作正确完成,并且没有超过延迟上限。仅有正常的响应状态码,不能证明订单已经正确写入;服务器内部计时也可能没有包含用户实际经历的网络等待。 分母必须明确,例如某个接口收到的有效客户请求尝试,而文档约定的合成探针可以另行统计。还要事先决定重试、客户端取消、认证失败、超时以及异步完成如何表示。排除条件应来自服务承诺的边界,不能在事故后临时创造,用来美化结果。应保留足够的记录,分辨一条事件是按规则排除,还是因为采集失败而没有进入统计;这两者的含义完全不同。 一条失败可以同时触发多个条件 假设成功请求既要返回预期响应,也要在三百毫秒内完成。一个缓慢的服务器错误同时违反两个条件,但仍然只是一条不良请求。应计算失败条件的并集,或者直接对成功判定取反。如果把“超时延迟计数”和“错误状态计数”相加,而两者不是互斥类别,就会重复计入交叉部分,导致预算消耗被夸大。 对于目标值与符合条件的请求总数,允许失败数就是请求数乘以一减目标值,剩余预算再减去实际失败数。请求型预算统计的是事件,时间型预算统计的是服务处于指定状态的时长。三十天内百分之九十九点九的时间型目标,对应四十三点二分钟的不良时间;不能把这个换算直接套到任意流量下的请求统计。部分用户失败、高低峰切换和不同请求权重,都会破坏两种口径之间的简单对应关系。 滚动窗口还会随着旧事件离开而变化,即使没有新增失败。应在同一个窗口内重新计算额度与失败数,不能把终身累计扣款直接当成滚动余额。发布政策还必须说明负余额会触发哪些实际行动;计算本身既不授权继续制造失败,也不会自动阻止部署,这些决策需要事先约定。 坏掉的样子 只统计应用服务器日志,可能看不到到达应用之前就被拒绝的请求。只记录已经完成的请求,也可能遗漏仍然卡在队列中的请求。这样得到的成功率可以很好看,用户却已经无法使用服务。需要选择合适的观测位置,纳入超时结果,并展示覆盖程度或独立检查;不能把遥测缺失解释成服务成功,更不能靠丢掉失败样本让分母看起来干净。 如果窗口里没有符合条件的事件,成功比例没有可解释的分母,应标记为没有观察值,并另行处理监控缺失。请求量很小时,高目标也会对单次失败特别敏感,因为预算是一个数学额度,不一定恰好等于整数次事件。不要为了得到一个“至少允许失败一次”的结果而向上取整。跨实例比较时也必须保留同样的范围和请求权重,直接平均各实例的百分比会改变指标含义。 快速验证 把代码保存为 slo-population.py,用 Python 3 执行。这个模拟接口的规则是:只有状态二百且耗时不超过零点三秒才算成功。样本包含已完成的客户事件,以及明确标记为探针的独立事件,用来验证分类计算。它没有证明真实采集器的完整性,也没有把正常状态码等同于真实业务结果已经正确完成。 from fractions import Fraction # Synthetic events: (kind, status, duration_seconds). events = ([('user', 200, 0.10)] * 9970 + [('user', 200, 0.50)] * 10 + [('user', 503, 0.02)] * 10 + [('user', 503, 0.50)] * 10 + [('probe', 503, 0.50)] * 100) eligible = [(status, duration) for kind, status, duration in events if kind == 'user'] bad_status = sum(status != 200 for status, _ in eligible) too_slow = sum(duration > 0.30 for _, duration in eligible) bad = sum(status != 200 or duration > 0.30 for status, duration in eligible) good = len(eligible) - bad target = Fraction(999, 1000) allowed = len(eligible) * (1 - target) remaining = allowed - bad assert len(eligible) == 10000 and bad_status == too_slow == 20 assert bad == 30 and bad_status + too_slow == 40 assert allowed == 10 and remaining == -20 assert Fraction(good, len(eligible)) == Fraction(997, 1000) # The inclusive threshold is part of the synthetic contract. assert not (200 != 200 or 0.30 > 0.30) assert 200 != 200 or 0.300001 > 0.30 # Aggregation must weight each eligible event, not each instance equally. assert Fraction(999 + 0, 999 + 1) == target assert (Fraction(999, 999) + Fraction(0, 1)) / 2 != target assert 1 * (1 - target) == Fraction(1, 1000) print(f"eligible={len(eligible)}; good={good}; bad_union={bad}") print(f"status_failures={bad_status}; slow={too_slow}; wrong_sum={bad_status + too_slow}") print(f"SLI={good / len(eligible):.3%}; allowed={float(allowed):.0f}; remaining={float(remaining):.0f}") 样本里有一万次符合条件的客户事件,另有一百次探针被排除。客户事件中,状态不符合预期的有二十次,延迟超限的也有二十次,其中十次同时属于两类。因此失败并集实际只有三十次,成功事件为九千九百七十次;直接相加会错误地得到四十次失败。 ...

九月 17, 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