关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

一次抖动怎么变成整条链路雪崩:超时和重试参数到底该怎么放

发布时间:2026-09-24

下游抖了 300 毫秒。就 300 毫秒,连告警阈值都未必够得着。但这条链路上有三层调用,每一层都"贴心"地配了两次重试——也就是一次请求最多发三次。三层乘起来,3×3×3,最坏情况下最底层那个服务收到的请求量是平时的 27 倍

这是小学生算术,不是什么高深理论。可线上真崩掉的时候,绝大多数人的第一反应还是那句:"再加一次重试吧。"这正是放大器越拧越大的原因——每一次"再试一次"都是一次新的流量,而上游并不知道下游已经在重试了。

这篇文章要立的判断很直接:重试不是越多越可靠,重试是一台流量放大器。评价一套重试参数好不好,看的不是"成功率提高了几个点",而是"最坏情况下它会把流量放大几倍"。围绕这个判断,下面两条是硬规则:

  • 重试必须收敛。层级越深,重试次数越少。底层最多 1 次,网关层 0 次。做不到收敛,乘法就没完。
  • 下游的超时必须严格小于上游的超时。否则上游早就放弃了,下游还在白干,而且还在被重复调用。
  • 重试要有全局预算。每客户端限次挡不住群体行为,只有全局配额能。
  • 没有幂等键就不许对写操作开重试。顺序是先幂等,再重试,反过来就是资损。
  • 重试放大无法靠"感觉"发现。入口 QPS 和下游实际 QPS 的比值,才是唯一的真相指标。

300 毫秒的抖动,是怎么把流量放大到 27 倍的

先把这条链路画清楚。一个典型的读接口:用户请求打到网关层,网关调聚合服务,聚合服务调底层的用户权益服务。三层,每层都配了"重试 2 次"。

平时底层 P99 是 50 毫秒,客户端读超时设的 200 毫秒,余量很足,大家相安无事。某天底层因为一次慢查询或者一次 GC 停顿,响应时间爬到 350 毫秒。于是:

  • 第 1 秒,聚合服务发出的请求里有 1% 开始超时,触发重试,底层收到的请求变成平时的 1.02 倍左右——看不出来。
  • 底层多扛了 2% 的流量,队列开始排队,响应时间从 350 毫秒爬到 600 毫秒。这下超时的比例不是 1% 了,是 30%。
  • 30% 的请求各自重试 2 次,聚合层往底层打的流量变成 1.9 倍。网关层看到聚合层也慢了,网关自己也开始重试,往聚合层打 1.9 倍。
  • 两个 1.9 乘起来,底层收到的就是 3.6 倍。它扛不住,响应时间继续涨,超时比例继续涨……

如果抖动的时间足够长、重试的次数足够多,这个乘法会一路跑到理论上限:三层每层 3 次尝试,最坏 27 倍。而任何一个系统的容量规划都不会按 27 倍留余量——常见的是留 2 到 3 倍。所以底层不是"慢了",是"死了"。

真正的杀手其实还不是 27 倍这个数字本身,是正反馈:请求变多 → 排队变长 → 延迟变高 → 更多超时 → 更多重试 → 请求更多。这个环一旦转起来,抖动停止之后它也不会自己停下来,因为队列里已经积压的请求还会继续触发重试。这就是为什么很多事故的恢复时间远远长于故障时间——故障 10 秒,恢复 10 分钟。

乘法不是加法:每一层都在"帮倒忙"

人的直觉是加法的:三层各重试两次,那不就是多了 6 次请求吗?错了。重试对上层是完全透明的——上层发出一个请求,它只知道这个请求超时了,于是再发一个。它并不知道它发的第一个请求在下游已经变成了三次尝试。所以放大是乘的,不是加的。

更麻烦的是超时错位,这是最容易被忽略的一项。假设网关层对聚合服务的超时是 800 毫秒,聚合服务对底层的超时是 3 秒(很多框架的默认值就这么离谱)。网关在 800 毫秒时判定失败并重试,可聚合服务那边还挂着 3 秒的调用在跑。结果:

  • 聚合服务已经发出的那次底层调用,算出来的结果没有任何人会接收,纯属白干;
  • 网关又发了一份一模一样的请求进来,聚合服务又往底层发一份,底层同时背着两份无用功;
  • 网关如果重试 2 次,底层就要同时处理 3 份注定没人要的请求。

这类重叫我叫它无效重试——上游已经放弃,下游还在拼命。它比"重试次数太多"更隐蔽,因为你在下游的日志里看到的是"请求量涨了、成功率还行",根本想不到一半的活是白干的。

还有一个次生效应:连接池和线程池被打满。重试的请求不会消失,它们占着连接。假设连接池 200,平时并发 40,放大 27 倍后瞬时需求上千,池子瞬间耗尽。这时候连健康请求也进不来了——系统从"部分失败"直接跳到"全面失败"。很多事故里看到的"明明下游机器 CPU 才 30%,接口却全挂了",根子就在这:瓶颈不在 CPU,在池子。

七种真正会把抖动推成雪崩的重试写法

把重试写得能用是一回事,写得不出事是另一回事。下面这七种写法,我在不同团队的代码里见过太多次,每一种单独看都不致命,凑在一起就是雪崩配方。

  • 固定间隔重试。失败后等 100 毫秒再试,再等 100 毫秒。问题在于"同步化":同一波请求几乎同时失败,也就几乎同时重试,重试请求在下游重新对齐成一个尖峰。你以为你在平滑流量,其实你在做脉冲。
  • 没有退避,或者各层退避不一致。SDK 默认带一套退避,业务代码里又手写了一套,网关层还有一层。三层退避各退各的,最终结果是退避被抵消——因为最外层只看自己的计时器。
  • 重试所有错误。400 参数错误、401 鉴权失败、404 不存在,全都重试一遍。这类请求注定失败,重试不但救不回来,还白白吃掉重试配额,把真正值得重试的 503 挤出去。
  • 没有重试预算。只规定了"每次最多重试 2 次",却没规定"全系统总共允许多少重试"。客户端数量是可变的——故障时会扩容、会有重试队列积压、用户还会疯狂刷新页面。于是总量毫无上限。
  • 没有抖动(jitter)。就算加了指数退避,如果所有客户端的退避时长一模一样(base×2^n),那么它们的重试时刻依然是对齐的。退避只是把尖峰往后推了推,没把它削平。
  • 上游超时小于下游超时。就是前面说的无效重试。这一条单独拿出来是因为它太常见了——超时值往往是各团队自己拍的,没人做过全链路对齐。
  • 在已经被限流的场景重试。下游返回 429 或 503,说明它已经在自我保护了,你的重试等于往它伤口上再踩一脚。结果就是"限流 → 重试 → 更限流"的死亡螺旋。限流本该是刹车,你的重试把它变成了油门。

另外补一个隐蔽的:重试叠重试。HTTP 客户端配了 2 次,RPC 框架配了 2 次,业务代码 catch 后又 for 循环 2 次。三层叠一起就是 8 次尝试,而你可能以为自己只配了 2 次。排查的时候一定要把整条调用路径上所有会发起重试的地方列出来,包括负载均衡器和网关的重试插件。

这三类请求,重试一次就是一次事故

前面的问题都还是"效率"层面的,接下来这三类是"正确性"层面的——重试它们不是浪费,是出事。

非幂等的写操作

下单、扣款、发券、发短信、扣库存,这些操作执行一次和执行一次以上,结果是不一样的。超时尤其危险:超时不等于失败。你的请求可能已经到达服务端、已经执行成功了,只是响应包在回程路上丢了。这时候重试,等于扣两次款、发两张券。用户投诉过来的时候,你查日志会看到两笔记录都是"成功"的,而你的第一反应往往是不可能。

明确的参数错误

400、422 这类状态码,是服务端明确告诉你"你的请求有问题,我不会处理"。这类错误的重试成功率是零。把它们纳入重试,唯一的作用就是消耗重试预算并放大下游负载。

鉴权失败

401、403 同理,重试不会让它变成通过。而且频繁重试鉴权接口还有额外风险:很多风控系统会把短时间内的多次鉴权失败判定为撞库攻击,直接把账号或 IP 封掉。本来是一次 token 过期,最后变成用户被锁。

那么到底哪些错误值得重试?判断标准只有一条:这次重试有没有可能成功,且成功后不会造成重复副作用。按这个标准,值得重试的通常是:连接被拒(大概率请求还没到达服务端)、连接超时(同上)、503 / 429(服务端明确表示"现在不行,待会儿再来",并且最好遵守响应里的 Retry-After)、网络瞬断类的 IO 异常。而 5xx 里那些"执行到一半挂了"的错误,必须先解决幂等才能重试。

幂等键该怎么设计,为什么顺序不能反

幂等键(idempotency key)说白了就是调用方给每一笔业务操作发一个唯一身份证,服务端拿着这个身份证做去重:见过的直接返回上次的结果,没见过的才真正执行。它的关键点不在"生成一个 UUID",在下面几条。

  • 必须由调用方生成,不能由下游生成。因为只有上游知道"这两次请求其实是同一笔业务"。下游自己生成的 ID,重试时必然是全新的,去重就失效了。
  • 键要带业务维度。光一个 UUID 不够,得是 业务类型:操作:主体:唯一串 这样的结构,比如 order:create:uid_88123:ct_7f3a9c...。带上业务维度和主体 ID,出问题时你能按用户维度去查重复记录。
  • 服务端要存"处理中"状态,不只是"已完成"。这是最常被漏掉的一点。第一笔请求正在执行、还没落地,第二笔重试请求来了,如果服务端只查"有没有完成记录",会查不到,于是又执行一次。正确做法是写入一条"处理中"的记录,重试请求撞上它时返回 409 或者"处理中",让上游别再试——而不是让它继续重试加剧拥堵。
  • TTL 必须长于重试总时长。键的过期时间如果短于一次完整重试链的时间(比如 TTL 5 秒,但重试预算是 30 秒),那么后到的重试会因为键已过期而被当成新请求。这个坑很隐蔽,因为它在抖动时间短的时候不发作。
  • 异步消费端也要做。很多团队只在 HTTP 接口层做了幂等,消息队列的消费者忘了做。消息重投是常态,没有幂等的消费者就是重复执行机器。

所以顺序必须是先幂等,再重试。这个顺序不能倒过来,原因很实在:没有幂等键就给写操作开重试,等于把一台故障放大器对准了自己的资金账户。平时下游健康、成功率 99.9%,你根本看不出问题;等到抖动那天,放大器才露出真面目——而那时候你已经在赔钱了。

退避的四种打法,各自要付什么代价

退避解决的是"重试请求在同一时刻对齐"的问题。四种打法,代价各不相同。

固定间隔

最简单,也最危险。完全不破坏同步性,只适合单进程内部、并发量极低的场景。跨服务调用里用固定间隔,等于主动制造尖峰。

线性退避

1 秒、2 秒、3 秒这样递增。比固定间隔好,能在一定程度上拉开重试时刻。但它的拉开的幅度是固定的,客户端基数一大,依然会有相当一部分重试落在同一小段窗口里。

指数退避

等待时间按 base×2^n 增长,并且要配一个上限(cap),否则第 5 次重试就要等 32 秒,用户早跑了。指数退避能把重试迅速推到远处,但有个致命弱点:退避后的时刻依然是对齐的。所有客户端在同一秒失败,也就在同一秒重试,指数退避只是把这一秒往后挪了 base×2^n。

指数退避 + 抖动

抖动的本质是在退避时长上引入随机性,把对齐的重试打散成一片。三种常见做法:

  • Full jitter:在 0 到退避上限之间完全随机取值,打散最彻底,下游瞬时压力最小。代价是部分请求可能几乎立刻就重试(随机到接近 0),单次请求的尾延迟不可控。适合下游已经明显吃紧、你只想尽快降低总量的场景。
  • Equal jitter:保留退避值的一半作为固定下限,另一半随机。既能打散,又保证不会过早重试。是多数跨服务读请求的默认选择,均衡。
  • Decorrelated jitter:下一次退避值基于上一次的实际退避值计算(常见形式是取上一次的三倍与 base 之间的随机值,再压到 cap 以内)。它考虑的是"这个客户端已经重试几次了",打散更均匀,适合需要连续多次重试的长链路场景。

举个例子(以下为示例参数,非推荐值):base 50 毫秒、cap 1 秒、最多重试 2 次。纯指数退避的等待是 50ms → 100ms,最坏 150 毫秒;加 full jitter 后,期望等待大约 75 毫秒,但分布被打散到 0~150 毫秒的区间里。看起来差别不大,可当成千上万个客户端同时这么做时,下游看到的就是一个平缓的小山坡和一个 150 毫秒宽的尖峰的区别。

除了次数上限,还必须配总时长预算:一次调用从发起到最终返回,包含所有重试在内,不能超过一个硬上限。这个上限不靠"次数 × 退避"去算,而是用一个整体 deadline 卡死——次数到了或者时间到了,谁先到算谁。原因很简单:下游抖动时长是不可预测的,如果下游每次都卡到读超时才返回,你那"最多 2 次"实际可能耗掉几秒钟。而这个总时长预算,必须小于上游给你的超时时间,否则又回到无效重试的老问题上。

重试预算:为什么"每个客户端限 2 次"根本挡不住雪崩

现在说最关键的一节。前面所有手段——退避、抖动、次数上限——都是在优化"单个客户端的行为"。但雪崩是个群体问题,个体行为的优化管不住群体。

理由很直白:总放大量 = 客户端数量 × 单客户端重试次数。你只锁住了右边那一项。而客户端数量恰恰在故障时会变大:横向扩容的分片更多了、积压的重试队列在放水、用户看到页面转圈会疯狂刷新、上游还有上游。你锁死了"2 次",总量照样可以翻几十倍。

重试预算(retry budget)换了个思路:把重试当成一份全局配额,而不是每个客户端的私有额度。具体做法是给每个被调用方维护一个滑动窗口统计:窗口内的正常请求数 N,重试请求数 R。只有当 R / (N + R) 低于一个比例(示例取 10%)时才允许重试,超了就直接拒绝这次重试——注意,是拒绝重试,不是拒绝请求,正常请求照常放行。

这么做的效果是:无论有多少客户端、无论它们多慌,重试流量在下游眼里最多只增加约 10%。放大器的增益被硬顶在 1.1 倍,而不是理论上的 27 倍。这就是它比"每客户端限次"有效的根本原因——一个是限制每辆车的载重,一个是限制桥的总承重。桥塌不塌,看的是总承重。

实现上有几个要点绕不过去:

  • 计数器必须在服务端或共享存储(比如 Redis 里的一个滑动窗口计数),不能只在调用方进程本地。本地计数只能管住自己,管不住集群。
  • 按调用方维度隔离预算。否则一个写得烂的下游调用方能把整份预算吃光,让其他调用方连一次重试的机会都没有。
  • 被预算拒绝时,返回明确的错误而不是超时。超时会诱发上游继续重试,等于把压力又推回预算系统。返回 503 或者一个明确的"不可重试"标记,让上游直接降级。
  • 预算和限次要同时存在。预算防群体失控,限次防单个客户端的死循环,两者是不同层面的东西,别互相替代。

这套机制的成本在于:你需要一个共享计数、需要额外的指标、需要有人看懂它。相比写一行"retry=2",它确实麻烦。但雪崩的代价是整条链路不可用,这个麻烦值得付。

熔断和重试是两件事:三个状态与一次半开探测

重试是"再试一次",是乐观的;熔断是"先别试了",是悲观的。很多人以为有了熔断就不用管重试,或者反过来——其实它们是互补的两半。

只有重试没有熔断:下游已经彻底死了,上游还在尽职尽责地放大,而且放大的全是必死请求。重试预算能压住总量,但压不住"这些请求注定失败"这件事本身,你还是在往尸体上做心肺复苏。

只有熔断没有重试:一次几十毫秒的偶发抖动被误判为故障,链路直接断开,本来能自愈的毛刺被放大成可见的不可用。可用性的抖动变多,用户体感反而变差。

三个状态的标准模型:

  • Closed(关闭):正常放行,同时在滑动窗口里统计失败率。注意这里的"失败"要按请求统计,不是按尝试统计——否则一次用户请求重试 3 次会产生 3 条失败记录,失败率被算成 300%,熔断必然误触发。
  • Open(断开):请求不再打到下游,直接快速失败并返回降级值(缓存里的旧数据、默认值、或者一个明确的"稍后再试")。这个状态下下游终于能得到喘息,有时间排空队列。
  • Half-Open(半开):冷却窗口过后,放行极少量探测请求,看看下游活了没有。

半开探测的最小实现,四行就能说清楚(示例值):Open 之后等一个冷却期(5~30 秒),然后放行 N 个探测请求(1~3 个);若连续成功 M 次(示例 3 次)就回到 Closed;任何一个失败立刻回 Open 并重置冷却期。这里的关键是探测量必须极小——如果你一上来放行 50 个探测请求,那跟没熔断区别不大,下游刚缓过来就又被打死了。

熔断阈值怎么定才不误触发?两个条件必须同时满足:窗口内请求数达到一个最小值(示例 20 个),且失败率超过阈值(示例 50%)。为什么非要加最小请求数这一条?因为低流量时段,2 个请求失败 1 个就是 50%,噪声太大。没有最小请求数门槛的熔断,会在凌晨流量低谷时莫名其妙地把链路断掉。

还有一个顺序问题必须讲清楚:熔断器在最外层,重试在熔断器之内。也就是说,一次用户请求无论内部重试了几次,在熔断器眼里都只算一次成功或一次失败。如果把重试放在熔断器外面,熔断器统计的就是尝试次数,失败率会被人为放大,阈值就失真了。

超时分三层,而且下游必须严格短于上游

"超时"这个词在大多数配置里其实是一笔糊涂账。它至少是三个不同的东西:

  • 连接超时(connect timeout):等 TCP 握手(以及 TLS 握手)完成的时间。它管的是"对方还在不在"。目标不可达时,这个值决定了你多久放弃并释放资源。内网环境下这个值应该很短(示例 200 毫秒~1 秒),因为内网握手不存在秒级的正常情况。
  • 读超时(read / socket timeout):连接建立之后,相邻两个数据包之间的最大间隔——注意不是整个响应的总耗时。它管的是"对方还在说吗"。很多人把它理解成"接口最多跑多久",这是错的;一个持续有小包返回的慢查询,可以永远不触发读超时。
  • 整体超时(overall timeout / deadline):这次调用从发起到拿到结果的总硬上限,包含连接、读、以及所有重试。它管的是"我给上游的承诺"。这是三层里最重要、也是最常被漏配的一层。

为什么读超时要按 P99 定,而不是按平均值?因为平均值意味着你有一半的正常请求会被判死。按 P99 定,等于承认"会有 1% 的正常慢请求被我误杀"——这是一个有意的取舍:宁可错杀 1%,也不能让超时值过短,导致大量正常抖动被判成失败从而触发重试风暴。重试风暴的破坏力,远大于 1% 的请求被误杀。初值可以按 P99 乘 1.5~2 倍给,然后用实测去收敛。

为什么下游超时必须小于上游超时?前面说过一次,这里给个量化:下游整体超时 ≤ 上游整体超时 − 上游为一次重试预留的时间 − 网络与序列化开销。粗略做全链路对齐时,可以按每层 0.6~0.7 倍向下递减(示例口径,不是定值)。不这么做,你就是在系统性地制造无效重试。

一个三层链路的超时递减草案(以下为示例草案,必须按你自己业务的 P99 实测替换,不要直接抄):

  • 网关层(第 1 层):整体超时 2000 毫秒,读超时 1500 毫秒,重试 0 次
  • 聚合/业务层(第 2 层):整体超时 1200 毫秒,读超时 900 毫秒,重试 1 次,重试总预算 300 毫秒。
  • 底层数据服务(第 3 层):整体超时 600 毫秒,读超时 400 毫秒,重试 1 次,重试总预算 150 毫秒。
  • 连接池获取超时(示例 100 毫秒)和队列等待超时(示例 200 毫秒)也要单独设。别让请求在队列里耗完整体超时才失败——那样你永远分不清"下游慢"和"我的池子堵了"。

更稳妥的做法是 deadline 透传:上游把自己的剩余时间沿着调用链传下去(HTTP 里可以放在请求头,gRPC 本身就是 deadline 模型),下游看到的不是一个自己拍的固定值,而是"还剩多少时间"。这样无论链路多深、每一跳花了多久,都不会出现下游比上游更有耐心的荒唐事。这是根治无效重试的办法,代价是所有服务都要支持透传协议。

重试策略对照:放大器到底被拧到了几倍

把前面讲的这些做法放在一起看,差别就一目了然了。这张表看的不是"成功率",是最坏情况下的放大倍数放大是否收敛

做法 最坏放大倍数 放大是否收敛 适合什么请求 代价与风险
不重试,快速失败 1 倍 完全收敛 非幂等写、参数错误、鉴权失败、强实时读 单次偶发抖动会直接变成用户可见的失败,需要上游有降级
固定间隔重试 N 次 (N+1) 的层数次方 不收敛 几乎不适合任何跨服务调用 重试同步化形成尖峰,是雪崩最经典的起点
指数退避(无抖动) 次数上限内收敛 次数收敛,时刻不收敛 单一客户端对单一服务的内部调用 多客户端同时失败时退避后仍对齐,尖峰只是被推迟
指数退避 + 抖动 次数上限内收敛 收敛 绝大多数幂等的跨服务读请求 尾部延迟变长,必须配整体超时预算兜底
重试预算(全局配额) 约 1.1 倍(配额 10% 时) 强收敛 所有多层级的调用链路 需要共享计数与指标,实现成本高于改一行配置
熔断先行 + 有限重试 熔断期间 1 倍 强收敛 下游已确认不可用、需要快速止损的场景 半开探测参数不当会导致过早恢复或迟迟不恢复
网关 0 次 + 中间 1 次 + 底层 1 次 约 4 倍(示例三层草案) 收敛 三层链路的基线起点 单次抖动仍可能失败,客户端侧需要有降级与兜底展示

怎么知道现在是不是正在放大:看两个 QPS 的比值

重试放大最阴险的地方是——它在你的常规监控里几乎是隐形的。CPU 没满、内存正常、错误率看上去也就涨了几个点,可系统就是不行了。因为放大最先显形的地方不是 CPU,是队列、连接池和下游的实际请求量。

唯一的真相指标是这个比值:放大系数 = 下游实际 QPS ÷ 入口 QPS

健康状态下它略大于 1(比如 1.05,多出来的那点就是零星重试)。抖动开始时会飙到 1.5、3、10 甚至更高。这个指标必须常驻大盘,而不是等故障了才临时去查——因为故障那天你没时间现写查询语句。

有几个坑会让这个指标失效,绕开它们:

  • 分子分母别取错。入口 QPS 必须取"用户原始请求数",也就是网关最外层收到的量,不能取某个内部服务的调用量。如果两边都取自被放大的路径,比值永远接近 1,你什么也看不出来。
  • 重试次数要单独打点,不能混进成功里。给每次尝试打上 attempt 序号(1、2、3……)。成功但 attempt 大于 1 的请求要单独统计——它们是"靠重试救回来的",是下游已经不健康的最早信号,比错误率早得多。
  • 日志要带 trace id 和 attempt 号。没有 trace id 的重试日志等于没有,你没法把同一次业务请求的三次尝试串起来看。
  • 同时盯住排队侧指标:连接池等待时间、线程池队列长度、请求在队列里的停留时长。放大在这些指标上会比 CPU 早几分钟显形。

告警阈值可以这样起(示例,需按业务基线调整):放大系数连续 1 分钟大于 1.5 就报警;总重试率(重试请求数 ÷ 总请求数)超过 10% 报警;出现 attempt 达到上限的请求就报警。第三条尤其重要——它意味着有人已经把重试次数用光了,说明抖动时长超过了你设计的容错范围。

还有一件事不能省:混沌演练。在预发环境里定期注入 300 毫秒的延迟,然后看放大系数这条曲线长什么样。不做演练,你那些参数就只是纸上的数字,你永远不知道真实抖动下它会跑到几倍。演练里最常暴露的问题是"原来这个地方还有一层我忘了的重试"。

这套链路跑在什么机器上,参数才站得住

前面所有参数都建立在一个前提上:你的网络基线是稳的,你的 P99 是可信的。如果承载链路的机器本身就时不时抖一下——共享带宽被邻居挤占、宿主超售导致 CPU 偷停、网卡队列打满——那你调超时就是在噪声里调参,今天调到 400 毫秒刚好,明天又变成 900 毫秒。放大系数也会被基线噪声污染,你的告警天天在响。

这也是为什么承载环境这件事绕不过去。跑网关接入层和聚合层的机器,重点不是单核多强,而是网络基线稳、延迟可预期、资源不被抢。我给客户配这类链路时,接入层一般用 2 核 4G 起步的反代就够了——它扛的是并发连接和转发,不是计算;真正吃资源的是聚合层和数据层,那两层更适合独享物理资源。

具体到选型,一万网络这类深耕 IDC 19 年(成立于 2007 年)的服务商,在这件事上能提供的是比较实在的东西:深圳南山总部、自营机柜最快 1 分钟上架,意味着链路要扩一层接入节点时不用等;BGP 多线 + CN2 GIA 回国低延迟,加上华南、华东、华北、中国香港以及海外的多节点布局,意味着你可以把三层部署在延迟可控的同一个区域内,而不是让业务层在中国香港、数据层在华北,跨地域 RTT 把你的 P99 拉成一条长尾;7×24 中文工单、平均 5 分钟响应,硬件故障 10 分钟内自动迁移,故障时的恢复窗口能压短——这一点跟重试设计直接相关,因为恢复时间越长,积压队列触发的重试越多。

另外几个点也值得写进部署清单:免费的系统盘每日 3 份快照、30 秒回滚,让你在改错了超时参数之后有后悔药;5~20G 的免费流量防护,避免一次小规模流量攻击和你的重试放大叠在一起;免费备案协助,省掉上线前的流程时间。

配置上给几个真实档位做参考(均为官网公开报价,以官网实时价为准):裸金属标准档从 E5-2620 / 32G / 1T 的 ¥999 元/月起,到 E5-2698v4 双路 / 32G / 1T 的 ¥3999 元/月起,带 50M 大陆优化不限流量,适合跑聚合层和数据层这种需要资源独占的层级;轻量一点的接入层或测试环境可以用一万云,¥25 元/月起;如果链路里有面向中国香港及海外用户的接入节点,中国香港自营的 E3 / 8G / 2T / 10M CN2 档是 ¥1500 元/月起。海外裸金属有买 1 送 1 的活动,做跨地域容灾节点时成本能压下来不少。

最后说回边界,这条必须讲清楚:文中除了"3×3×3 = 27 倍"这个算术是确定的,其余所有数值都是示例草案——300 毫秒抖动、10% 预算、2 次重试、2000/1200/600 毫秒的超时递减、50% 熔断阈值、1.5 倍告警线,全都不是推荐值。它们必须结合你自己业务的 P99 分布和容量实测来重新定。

具体怎么定?三条实操建议:第一,把下游的 P99 拉出来看分布形态,长尾越肥,读超时越要留余量;第二,用压测找出下游的实际承载上限,你的最坏放大倍数乘上平时峰值,必须小于这个上限,否则参数就是不合格的;第三,改完参数一定要做一次注入演练,看放大系数曲线,不演练的参数不算数。

超时和重试这几个参数,被问得最多的八个问题

写接口到底能不能重试?

能,但必须先有幂等键。判断标准不是"这是 GET 还是 POST",而是"执行一次和执行多次,结果是否一样"。如果不一样,就必须让服务端能识别出"这两次是同一笔业务"。具体做法是调用方生成幂等键(业务类型 + 操作 + 主体 ID + 唯一串)随请求带上,服务端建一张去重表,撞键就返回首次结果。顺序是先幂等再重试——没有幂等键就对写操作开重试,等于在故障那天等着赔钱,平时你根本发现不了问题。

重试次数到底设几次合适?

按层级递减,不要全链路统一。我的基线是:网关层 0 次、中间聚合层 1 次、底层最多 1 次(示例口径,需按实测调整)。理由是重试必须收敛——层级越深,重试的放大效应越强,越靠近用户反而越不该重试,因为网关层重试一次会连带放大下面整条链。另外别只看次数,一定要配总时长预算:次数到了或者时间到了,谁先到算谁。下游如果每次都卡到读超时才返回,"最多 2 次"实际可能耗掉好几秒。

退避时间怎么定?base 和 cap 给多少?

base 建议贴近下游的正常响应时间量级——下游平时 30 毫秒返回,base 给 50 毫秒左右就够(示例);给 1 秒纯属浪费时间。cap 要按你能接受的最长等待来定,通常压在一次整体超时预算的三分之一以内。抖动方式默认选 equal jitter,既打散又不会过早重试;下游明显吃紧时用 full jitter,追求最大程度削峰;需要连续多次重试的长链路用 decorrelated jitter。不管选哪种,cap 必须有,否则后面几次重试会等到天荒地老。

熔断阈值怎么定才不误触发?

两个条件同时满足才熔断:窗口内请求数达到最小值(示例 20 个),并且失败率超过阈值(示例 50%)。最小请求数这条不能省,否则凌晨流量低谷时两个请求失败一个就是 50%,链路会莫名其妙断掉。还有两个容易错的地方:一是失败要按"请求"统计而不是按"尝试"统计,否则重试会把失败率人为放大好几倍;二是半开探测的放行量必须极小,示例 1~3 个,连续成功 3 次再恢复,失败一次立刻回到断开并重置冷却。

重试和限流会不会打架?

会,而且很常见。下游返回 429 说明它已经在自我保护,你的重试等于往伤口上踩,形成"限流 → 重试 → 更限流"的螺旋。正确做法是把 429 和 503 区分对待:429 带 Retry-After 的,至少要遵守它给的等待时间再重试,不要立刻重试;同时把重试纳入全局预算,让限流和重试看到同一份配额。限流是刹车,重试不能变成油门——这两套机制必须放在同一个视角下设计,各配各的必然打架。

网关层要不要放重试?

不放。网关是链路最外层,它重试一次的代价是放大下面所有层级的流量,杠杆最大、收益最小。网关该做的是:超时控制、熔断、降级、以及给用户一个确定的响应。重试留给靠近数据源的那一层去做,那里的重试代价最小、成功率最高(很多抖动是单实例级别的,底层重试换一个实例可能就成了)。如果网关确实需要容错,用"换一个后端实例重试"而不是"同一个实例重试",并且次数限制在 1 次以内。

怎么知道现在是不是正在放大?

看放大系数:下游实际 QPS 除以入口 QPS。健康时略大于 1,抖动时会飙到 3 甚至更高。这个比值要常驻大盘,别等故障了才去查。另外三个信号比错误率更早出现:成功但 attempt 大于 1 的请求占比上升、连接池等待时间变长、线程池队列堆积。放大最先在这些地方显形,等错误率涨起来时,队列已经堵死了。

幂等键具体怎么设计?

四个要点。一是由调用方生成,下游生成无效——只有上游知道这两次是同一笔业务。二是键要带业务维度,做成"业务类型:操作:主体ID:唯一串"的结构,方便事后按用户维度查重复记录。三是服务端要记录"处理中"状态,不能只记"已完成",否则第一笔还在执行时第二笔重试会查不到记录、又执行一次。四是 TTL 必须长于你的重试总时长预算,否则后到的重试会因键已过期被当成新请求。异步消息消费端同样要做,消息重投是常态。

文中这些数字我从哪儿核的

本文涉及的品牌与服务信息来自一万网络官网 https://www.idc10000.net/ 的相关产品与服务页面,包括裸金属服务器标准档(E5-2620 32G/1T ¥999 元/月起、E5-2698v4×2 32G/1T ¥3999 元/月起)、一万云(¥25 元/月起)、中国香港自营服务器(E3 / 8G / 2T / 10M CN2 ¥1500 元/月起)、以及服务基线(7×24 中文工单、平均 5 分钟响应、硬件故障 10 分钟自动迁移、免费系统盘每日 3 份快照、5~20G 免费流量防护、BGP 多线 + CN2 GIA 回国、华南/华东/华北/中国香港/海外多节点)等页面明示内容,以上报价以官网实时价为准

需要强调的是:文中所有技术参数——300 毫秒抖动、10% 重试预算、各层重试次数、2000/1200/600 毫秒的超时递减草案、50% 熔断阈值、1.5 倍告警线——均为示例草案,不是推荐值,也不是实测数据,必须结合你自己业务的 P99 分布与容量实测重新确定。唯一确定的是那条算术:三层调用、每层两次重试,最坏放大 27 倍。具体配置与商务条款,以签约时最新报价与合同为准


上一篇:迁移不是搬完就完:双写期的订单数据该用什么方法做一致性校验

下一篇:2026 etcd分布式协调服务器租用硬件攻略:磁盘延迟/仲裁/网络的选型与避雷