DS / LAB 分布式系统
章节 05 / INTERACTIVE EDITION · ETCD 3.6.6

RPC、未知结果与幂等

“没有收到成功”和“操作没有发生”是两件事。

可操作的原理模型

一个意图,两次效果?

改变响应延迟,比较相同和新重试 ID。

正在加载互动… 交互需要 JavaScript;正文仍可直接阅读。

合成状态在浏览器内运行,不是真实 etcd 集群。 阅读原理 · 查看真实证据

提交效果回执丢失同 ID 重试
副作用与操作结果必须处于同一原子边界。

学习目标

识别未知结果,设计重试身份,让去重记录与副作用原子提交。

前置:状态机的有序决策、请求响应与原子比较更新。

不确定性窗口

假设递增在服务端 20 ms 时提交,调用者在 150 ms 停止等待,而响应被故意延迟至 800 ms。直接重试普通递增,就可能把同一个意图执行两遍。共识并不阻止它:两次递增完全可以是日志中的两条合法命令。

调用者                应用服务                etcd
  increment ───────► transaction ────────────► 提交
  150 ms 截止             │
  重试 ────────────►      响应延迟 800 ms
无身份:再次执行。有身份:返回已保存结果。

幂等指重复执行同一操作,与执行一次具有相同效果。把值设置成 7 可以幂等;加 7 通常不幂等。重试键让服务把重复的非幂等意图转换为查询已提交结果。键的作用域应包含调用主体与操作,并绑定请求内容,明确保留期限与过期后的行为。

实现:效果与证据共用一次提交

示例 HTTP 应用先读计数器,再提交一笔 etcd Txn,比较计数器 revision,并在去重模式比较操作键不存在。成功分支同时写入新计数和操作的 amount/result。并发变化让比较失败时,应用重新读后再尝试,有次数上限。

若 operation_key 已存在:
    要求保存的 amount == 本次 amount
    返回保存的 result
Txn:
    比较 counter.mod_revision == 之前读取的 revision
    比较 operation_key.version == 0
    写 counter = old + amount
    写 operation_key = {amount, result}

持久化操作键填上“副作用已发生,但去重记录还不存在”的缝隙。若分两笔事务写,就重新引入这个窗口。本例所有效果都在 etcd 内部;发邮件、远程扣款或写另一个数据库,需要额外协议。这里不声称外部副作用恰好一次。

真实实验 B:提交、延迟、重试

应用先完成提交,再把真实 HTTP 响应故意延迟 800 ms;调用者的截止时间为 150 ms。另一个正常请求重试同一操作。普通模式的最终计数应为 2;去重模式应为 1。去重模式再并发重试四次,均应返回已保存结果;同一 ID 换成 amount=9 应报告冲突。

查看客户端结果、服务端提交记录与最终计数。服务端提交是实验观察者获得的证据,不是超时客户端当时拥有的信息。故障来自真实 HTTP 服务中的延迟响应,不是生成的示意图。

动手:在下载副本中修改 lab/services.py,让去重标记在计数器之后单独写入。在两次写之间插入崩溃,预测重试如何产生重复效果。仅在自己的学习运行时操作,保留基线通过结果以供对照。基线去重记录不设过期;生产清理必须定义重试有效期与过期重放约定。

自测

Raft 能让递增安全重试吗?

它为两条命令排序,不知道它们代表相同业务意图。应用仍需要身份和原子的效果/结果记录。

同一个 ID 可以换一个 amount 使用吗?

不可以。基线会识别冲突;静默返回无关的旧结果会隐藏调用者错误。

原始资料

对照 MIT 6.5840 学习

Lecture 2: Threads and RPC · Lab 2: Key/Value Server

观察先提交后超时,以及原子保存的去重结果。

python3 scripts/run_all.py --repeats 1 --out build/my-retry-study

本 Lab 可运行练习为独立原创,不是所链接 MIT 作业的解答。安装方法见复现页;不代表注册、评分或认证。

检索已核验课程地图 →