第一个版本上线时,我们最关心功能是否正确。随着流量和团队增长,问题会悄悄改变:一次请求经过了哪些服务?重试会不会制造重复数据?某个依赖变慢时,会拖垮多大范围?
这时,“能跑”的架构就需要进化为“可演进”的系统。后者不一定采用更多中间件,但一定拥有更明确的边界。
一、先定义故障边界
设计服务时,不要只画正常路径。为每个外部依赖写下三个问题:它超时了怎么办?返回错误数据怎么办?彻底不可用时核心流程还能保留多少?
超时、有限重试、熔断和降级不是四件孤立的工具。它们共同定义了故障是否会沿调用链扩散。尤其要避免无上限重试:它往往在下游最脆弱的时候,送去更多流量。
# 每次重试都必须服从请求的总时间预算
timeout_budget = 1200ms
max_attempts = 2
backoff = exponential_with_jitter
fallback = "return cached summary"
timeout_budget = 1200ms
max_attempts = 2
backoff = exponential_with_jitter
fallback = "return cached summary"
二、把一致性写成业务语言
“我们需要强一致”通常不是一个足够准确的需求。真正需要明确的是:什么数据允许旧五秒?什么操作绝不能重复?用户在两个页面看到短暂差异是否可接受?
一旦这些语义被说清,就能选择更简单的实现。订单创建需要幂等键;统计数字可以最终一致;通知发送则需要至少一次投递配合消费端去重。
三、让系统能够解释自己
可观测性不是上线后再补的仪表盘。日志回答“发生了什么”,指标回答“影响有多大”,Trace 回答“时间花在哪里”。它们必须通过统一的请求标识连接起来。
最有价值的告警通常对应用户体验,而不是机器状态。相比 CPU 达到 80%,结算成功率低于目标或 P99 延迟持续恶化,更接近真正需要处理的问题。
四、为变化留下接缝
可演进不等于提前抽象所有未来。更有效的方法是保持模块边界清晰,让关键决策可替换:把领域逻辑与数据库访问分开,把供应商 API 包在适配层后,让异步消息带版本号。
最后,稳定性来自日常的小选择:一次明确的超时、一个可检索的错误码、一场基于真实事故的复盘。系统不会突然变得可靠,它只会逐步减少未知。