单进程函数失败时,调用者往往能看到异常;跨网络调用超时后,调用者却不知道对端是否完成。分布式设计的起点是部分失败:一部分节点仍在工作,另一些通信中断,而不同节点看到的状态可能不同。

一次重试怎样变成流量放大

入口、服务 A、服务 B 各做最多 3 次尝试,一次用户请求最坏会在最下游产生 3 × 3 × 3 = 27 次调用。如果错误来自过载,这些尝试会进一步延长排队。

给请求建立总 deadline,由调用链向下分配预算。只在明确可重试且操作幂等时重试,并限制总尝试次数。指数退避加随机抖动可以减少同时醒来;还要尊重服务端的 Retry-After。验证错误、权限错误和参数不变的业务冲突,通常不应原样重试。

总预算 800 ms
  连接、读取与本地处理都占预算
  第一次下游尝试 200 ms
  退避 50~100 ms
  第二次只使用剩余预算允许的时间
  预算不足就停止,不再开启“最后一次”长调用

上述时间是设计示例,不是通用默认值。真实配置要根据延迟分布、用户目标和下游 SLO 决定。

限流、熔断、隔离不是同一种开关

限流控制单位时间进入的请求量;并发上限控制同时占用的资源;有界队列控制等待数量。熔断器在持续失败时暂时停止调用,避免资源反复耗在已知故障上;恢复时用有限探测,而不是突然放开全部流量。按依赖隔离线程池或连接预算,可以让一个慢服务不拖死所有功能。

降级需要说明语义:商品推荐可以暂时隐藏,账户余额不能随意返回过期缓存冒充当前值。返回 200 并不意味着服务仍满足业务目标。

复制与一致性必须连同读写路径解释

主从复制可以提高读取能力和容灾能力,但异步副本可能落后。写后马上从副本读取,可能看不到刚保存的数据;可以把此会话暂时读主库,或使用明确的位点/版本条件。故障切换还要考虑未复制写入、旧主隔离以及客户端连接更新。

CAP 讨论的是发生网络分区时,线性一致性与所有非故障节点对每次请求作出可用响应之间的约束,不能简化成日常“任选两个”。多数系统还需在延迟、吞吐、成本和一致性等级之间选择。共识协议的作用也不等于让整个业务流程天然原子化。

分片让数据分散到多个节点,但引入分片键、热点、跨片查询和再平衡成本。按用户 ID 分片有利于用户内局部事务,跨用户转账则需另外协调。数据库仍很空闲时,过早分片只会增加故障面。

先模块化,再决定是否拆服务

模块化单体可以清楚划分预约、商品和通知边界,同时保留本地调用与事务。只有独立扩缩、发布节奏、资源隔离或团队边界带来实际收益时,再评估拆分。拆分以后必须补齐契约、身份传播、超时、追踪、幂等和一致性策略,不能只把类换成 HTTP 请求。

租约与分布式锁还会遇到进程长暂停:租期过了,旧持有者恢复后仍可能继续写。要保护下游资源,通常需要资源端验证递增的 fencing token。单纯缩短锁 TTL 或加续期线程,都不能消除这个问题。

故障演练

给一次预约请求画出数据库、缓存和通知三条路径。依次设定“数据库变慢”“缓存不可用”“通知超时”。为每个故障写出用户可见结果、资源上限、重试位置、恢复后需要补做的工作。

在 Go 任务工程里把处理器设为可取消的阻塞操作,验证 shutdown 的等待上界。工程使用内存队列,进程重启会丢任务;把它改成可靠队列之前,不应声称已经具备持久恢复。

面试自查

  • 读副本看不到刚写入的数据,是否说明数据库违反了自己的承诺?先问什么配置?
  • 加锁后仍重复执行,可能有哪些租期与进程暂停原因?
  • 为什么一次“可安全重试”的 GET 仍可能因重试风暴造成事故?

参考资料