题干与适用场景
一个 API 请求依次调用三个 gRPC 服务:聚合层调用用户服务、目录服务和推荐服务。用户可能在等待时离开页面, 推荐服务也可能短暂超时。系统需要控制端到端延迟、释放服务端资源、避免重试放大,并在写操作被取消后保持状态 可查询。
gRPC 官方文档把 deadline 定义为客户端愿意等待的上限,并说明超时会触发 DEADLINE_EXCEEDED;取消会立即终止 不再需要的 RPC。强回答要把预算传播、重试边界、流式清理和未知结果分开,不能只给每层配置一个固定 5 秒超时。
面试官考察点
关注四点:用绝对 deadline 或剩余预算而非层层重新计时;当父调用取消时把取消传给子调用和数据库;重试消耗同一 总预算且只针对可重试错误;写请求取消后不假设“没有响应”等于没有执行。还要说明观测字段和压测验证。
回答前需要澄清的问题
- 顶层请求的 SLO、客户端 deadline 和是否允许部分结果是什么?
- 三个子服务是只读还是包含扣款、写订单等副作用?
- 哪些状态码和方法被服务端标记为可重试,是否有幂等键?
- 流式 RPC 的消息是否可重复、是否支持断点或重连?
- 数据库和外部 API 是否接受取消信号?
- 取消后是否需要查询状态、补偿或对账?
30 秒回答框架
“入口创建一个绝对 deadline,子调用只继承剩余预算,绝不重新获得完整超时。父取消立即传播到所有 RPC、数据库和 可取消工作;重试共享总 deadline、尝试上限和退避预算。只读请求可在安全错误上重试,写请求用幂等键或状态查询。流 结束时释放迭代器和连接,并记录每层剩余预算、取消原因和最终状态。”
分步骤深入解答
第一步:定义端到端时间预算
入口把客户端提供的 deadline 视为不可突破的上限。服务收到后计算 remaining = deadline - now,为子调用设置不晚于 该时间的 deadline,并扣除自身排队、序列化和响应余量。
parent_deadline = 2.0s from request start
child_deadline = min(parent_deadline, now + remaining_budget)不要在每层使用“再等 2 秒”,否则三层串联最坏可能超过 6 秒。时钟偏差由 gRPC 的 deadline 传播机制处理;应用仍需 在本地用单调时间监控排队和工作阶段。
第二步:传播取消并让工作真正停止
客户端离开页面、显式取消或 deadline 到期都应触发取消。服务端 handler 监听调用上下文的 cancellation token,并把它 传给数据库、HTTP 客户端、队列等待和流迭代器。仅让 handler 返回错误而继续执行后台任务,会把取消变成泄漏。
取消处理要可重复:释放连接、临时文件和锁,停止产生新消息,记录已完成阶段。已经提交的事务不能凭取消撤销;后续由 状态查询或补偿流程处理。
第三步:把重试放在剩余预算内
只对明确的瞬时错误重试,例如 UNAVAILABLE,并限制最大尝试次数、指数退避和随机抖动。每次尝试使用同一个总 deadline, 而不是重置计时器;deadline 到期立即停止剩余重试。
for attempt in 1..maxAttempts:
if remaining(deadline) <= backoff: stop
result = call(child, deadline, cancellation)
if result is success or permanent_error: return result
sleep(jittered_backoff, cancellation)
return DEADLINE_EXCEEDED客户端和服务端不要同时对同一错误无限重试。跨层重试预算应由调用方统一,监控 retry amplification 和每层实际尝试数。
第四步:区分读操作与写操作
读操作在服务契约允许时可以重试;写操作必须证明幂等,例如携带业务操作 ID、服务端唯一约束和可查询状态。取消发生在 外部写入之后时,客户端看到的是 UNKNOWN,不能立即换一个新 ID 再写一次。
PENDING -> CONFIRMED
\-> FAILED
\-> UNKNOWN (reconcile before retry)状态查询应返回权威结果;超出供应商保存窗口后仍无证据,进入人工或对账队列。deadline 与取消控制等待,不提供跨系统原子 回滚。
第五步:处理流式 RPC 生命周期
为流设置总 deadline,必要时另设空闲超时;每次读取都检查取消。客户端导航离开后取消流,服务端停止生成和发送消息,关闭 数据库游标或订阅。若支持重连,使用明确的游标或消息序号,不能从头重放产生重复副作用。
长流应区分“没有新消息”和“调用已失效”,并在指标中记录最后一条消息时间、取消原因、重连次数和积压。服务端优雅停止时, 让活跃流在剩余 deadline 内完成或返回可识别的取消状态。
第六步:验证传播和资源边界
测试矩阵至少包含:父请求取消、每个子服务超时、数据库慢查询、瞬时 UNAVAILABLE、永久错误、流中断、写入后响应丢失、 多层重试和服务实例优雅停止。断言子调用不会超过父 deadline,取消后无后台工作泄漏,重试次数不超预算。
日志和 tracing 记录 RPC 方法、trace ID、deadline 剩余时间、attempt、状态码、取消来源和操作 ID;禁止记录敏感载荷。 压测要观察线程、连接、队列、CPU、重试放大和尾延迟,而不是只看平均成功率。
高品质示范回答
“我会在入口建立绝对 deadline,聚合层向三个子服务传播不晚于它的剩余预算。每个 handler 都把取消传到数据库、HTTP 和流迭代器;回调返回前释放资源。只读调用在 UNAVAILABLE 等瞬时错误上使用有限重试,所有尝试共享总 deadline 和抖动退避。”
“写调用携带操作 ID,取消后标记 UNKNOWN 并查询状态,不能换新 ID 盲重试。流使用总 deadline、游标和重连策略。验证时 注入父取消、子超时、响应丢失和优雅停止,检查子调用停止、无资源泄漏、重试放大受控,并保存每层剩余时间和取消原因。”
常见错误
- 每层重新给完整超时 → 串联延迟突破入口 SLO → 传播绝对 deadline。
- handler 返回就算取消完成 → 数据库或后台任务继续运行 → 把取消传给所有可取消工作并清理资源。
- 所有错误都重试 → 负载放大且挤占健康请求 → 按错误语义、总预算和次数上限重试。
- 写请求超时后换新操作 ID → 第一次可能已经成功 → 查询权威状态或执行补偿。
- 流重连从头开始 → 重复消息和副作用 → 使用游标、序号和幂等消费。
- 只记录最终错误 → 无法解释预算在哪一层耗尽 → 记录每层剩余 deadline 与 attempt。
追问及应对
追问一:子服务能否延长父 deadline?
不能。子服务只能使用剩余预算;若业务确实需要更长操作,应改成异步任务并返回可查询操作 ID,而不是偷偷延长同步请求。
追问二:取消是否能撤销已经提交的数据库事务?
不能保证。取消可以阻止尚未执行的工作;已提交事务需要状态查询、补偿或对账。接口必须暴露可观察状态,而不是把取消当作回滚。
追问三:如何避免双重重试?
定义单一重试责任方,其他层只传播错误;为每个逻辑请求注入 attempt 和预算,统一限制最大尝试。监控每层重试比率和放大倍数。
追问四:流式 RPC 没有消息时要不要取消?
按产品语义区分长连接和闲置。使用总 deadline、空闲超时或心跳;空闲超时触发可重连状态,不应在服务器仍健康时无限占用资源。
追问五:deadline 已过但服务端还在计算怎么办?
服务端必须监听取消并让下游支持取消;无法取消的计算移入受控后台队列,记录操作 ID 和资源上限,避免继续占用请求线程。