想象一下:你写了一个向网站发送请求的客户端,一切运行正常。然后突然错误蜂拥而至,工作线程挂起,服务器返回神秘的状态码429。熟悉吗?那么这份指南就是为你准备的。我们将介绍如何构建一个不会在第一次遇到问题时惊慌失措,而是表现得礼貌且稳定的HTTP客户端。

引言:为什么429不是错误,而是信号

许多开发者看到429状态码时会想:出错了。实际上,服务器告诉你一件非常具体的事情:你发送了太多请求,请减速。这不是永久拒绝或封锁。这是要求你放慢节奏。如果你正确理解了这个信号,你的客户端会变得可靠。

读者最终会得到什么

在本指南结束时,你将拥有一个能够完成几项重要任务的现成工作HTTP客户端。它能正确处理429状态码并尊重Retry-After头部。它使用带抖动的指数退避,避免引发重试风暴。它限制并发性,以免淹没目标服务器。并且由于正确的超时设置,它不会挂起。

你将获得三种语言的现成代码片段:Python(通过httpx库和urllib3 Retry)、Node.js和Go。每个片段都可以插入你的项目并根据需要进行调整。

本指南适合谁

本指南面向已经可以执行简单HTTP请求但尚未处理过生产负载的初级开发者。同时,这里也包含一些高级内容:断路器、指标、优雅降级。如果你正在编写解析器、与外部API集成或构建访问外部资源的服务,这些材料将为你节省无数个不眠之夜。

你需要提前了解什么

你只需要了解什么是HTTP请求和HTTP响应。最好知道状态码是什么(例如200表示成功,404表示页面未找到)。至少熟悉一种语言的基础知识:Python、JavaScript或Go会很有帮助。不需要深入的网络知识——我们会用简单的语言解释一切。

这需要多长时间

阅读并理解理论部分——大约40分钟。按步骤构建基本客户端——大约一小时。完整实现所有保护、指标和测试——大约三小时。不要着急:最好慢慢理解每一步,而不是快速复制你不理解的代码。

建议: 阅读本指南时打开编辑器。立即在测试端点上尝试示例,而不是在生产服务上。

准备工作

在编写代码之前,我们先准备好工作环境。这会花一点时间,但能避免后续的混乱。

所需工具

  • 以下语言之一及其环境:Python 3.11或更新版本,或Node.js 20或更新版本,或Go 1.22或更新版本。
  • 代码编辑器——任何编辑器都可以,例如VS Code。
  • 用于运行脚本的终端。
  • 能够访问返回不同状态码的测试HTTP服务。

Python需要安装什么

  1. 在终端中输入python --version并按回车检查Python版本。
  2. 使用命令python -m venv venv创建虚拟环境。
  3. 激活它:Windows上运行venv\Scripts\activate,macOS/Linux上运行source venv/bin/activate。
  4. 使用命令pip install httpx urllib3 requests安装库。

Node.js需要安装什么

  1. 使用命令node --version检查版本。
  2. 创建项目文件夹并进入。
  3. 使用命令npm init -y初始化项目。
  4. 从Node.js 20开始,内置的fetch无需安装,基本客户端不需要额外包。

Go需要安装什么

  1. 使用命令go version检查版本。
  2. 创建文件夹并使用go mod init myclient初始化模块。
  3. 标准库net/http就足够了,不需要外部包。

备份与安全

⚠️ 注意: 永远不要直接在重要的生产服务上测试新的客户端。首先使用你自己控制的测试端点或本地模拟服务器。否则,激进的重试可能会损害他人服务并导致你被封锁。

如果你在修改现有项目,请复制文件或创建版本控制分支。这样你随时可以回滚。

✅ 检查: 你已经安装好所选语言,创建了项目,并确认测试脚本可以无错误运行。现在可以进入理论部分了。

基本概念简单讲

要自信地构建客户端,需要理解几个关键术语。我们来用简单的语言解释它们。

状态码403、407、429和503意味着什么

这四个状态码很容易混淆,但它们的行为不同,处理方法也不同。

  • 状态码429 Too Many Requests——服务器告诉你超出了请求限制。这是暂时的。你需要减速并稍后重试。
  • 状态码403 Forbidden——访问被拒绝。这通常不是速度问题,而是权限问题:错误的密钥、缺少授权、地区限制。不加更改地重试通常是徒劳的。
  • 状态码503 Service Unavailable——服务器暂时超载或正在维护。和429一样,这是暂时的,稍后重试可能有效。
  • 状态码407 Proxy Authentication Required——这里有一个重要的区别。这个状态码不是来自目标网站,而是来自代理服务器。它表示代理需要认证,而你没有提供或提供了错误的认证。

⚠️ 注意: 状态码407不能通过IP轮换或退避来解决。这是客户端配置错误,具体是代理凭据不正确。请检查登录名、密码和连接字符串格式。在修复授权之前,任何重试都无济于事。

429和403的区别

记住一个简单的规则。429是数量问题:你操作得太频繁。403是权限问题:你根本不被允许。遇到429时,暂停后重试可以解决问题。遇到403时,不加改变的重试无法解决问题——你需要更换密钥、头部或方法。

Retry-After和X-RateLimit头部

有礼貌的服务器会提示你何时可以回来。Retry-After头部告诉你应该等待多少秒再重试。有时是秒数,有时是具体日期。你的客户端必须尊重这个头部:如果服务器说等待10秒,1秒后重试只会让情况更糟。

X-RateLimit系列头部告知你的限制:允许的请求数、剩余数以及计数器何时重置。例如,X-RateLimit-Remaining显示剩余数量。如果它接近零,你应该提前减速,而不是等到429出现。

限制是如何工作的:令牌桶和滑动窗口

服务器通过两种流行方式计算你的请求。

令牌桶(Token bucket)的工作原理如下:想象一个桶,令牌以固定速率不断滴入。每个请求消耗一个令牌。如果没有令牌,请求被拒绝并返回429。这种方案允许短时突发:如果你长时间保持沉默,桶会被填满,你可以立即发出一批请求。

滑动窗口(Sliding window)计算最近一段时间内的请求数,例如一分钟。一旦你在这个窗口中超过限制,就会收到429。这种方案对突发行为的惩罚更严厉。

为什么并发也是一种限制

很多人忘记:限制不仅体现在频率上,还体现在同时连接的数量上。如果你打开500个并行请求,服务器可能会认为这是攻击,即使每分钟的总请求数并不高。并发必须像频率一样严格限制。

建议: 在构建客户端之前,从目标服务的文档中了解其限制。知道确切数字可以避免猜测和不必要的429。

✅ 检查: 你理解了429、403、407和503之间的区别,了解了Retry-After,并知道服务器如何计算你的请求。很好,现在进入实践部分。

第1步:正确设置超时

阶段目标: 确保没有任何请求会永远挂起并阻塞工作线程。

为什么没有超时的客户端是危险的

没有超时的客户端是一颗定时炸弹。如果服务器停止响应,你的请求将无限期等待。一个挂起的请求占用一个工作线程。十个挂起的请求——你的整个线程池都被占用,新任务无法处理,服务实际上停摆。超时是你的第一道防线。

四种超时

正确的客户端会区分多种超时,而不是设置一个笼统的超时。

  1. 连接超时(Connect timeout)——等待与服务器建立连接的时间。如果服务器不可达,你将很快知道。
  2. 读取超时(Read timeout)——发送请求后等待数据的时间。防止服务器接受请求后保持沉默。
  3. 写入超时(Write timeout)——等待发送请求体的时间。对于大型上传很重要。
  4. 总超时(Total timeout)——整个请求(包括所有阶段)的最大时间。

起始值如何选择

没有通用数字,但有合理的起始值。连接超时设为3-5秒:建立连接通常很快。读取超时设为10-30秒,取决于服务返回数据的速度。总超时应覆盖最长的合理请求,例如30-60秒。

⚠️ 注意: 永远不要设置像300秒这样的巨大超时用于所有请求。这会掩盖问题并创建挂起操作的队列。更快地失败并重试,总比长时间空等要好。

逐步设置超时

  1. 确定成功请求到你服务的通常时长。测量几次。
  2. 将读取超时设为平均响应时间的大约两倍。
  3. 将连接超时设为3-5秒。
  4. 将总超时设为各阶段合理时间之和加上少量余量。
  5. 发送测试请求,确保它完成而不是挂起。

建议: 如果你的服务有时返回大文件,有时返回小响应,请为不同类型的请求创建不同的超时配置文件。一种尺寸并不适合所有情况。

预期结果: 当访问已知缓慢或不可达的地址时,客户端会在指定时间内返回清晰的超时错误,而不是永远挂起。

✅ 检查: 向一个不响应的地址(例如不存在的端口)发送请求。客户端应在大约设定时间内返回超时错误。如果挂起时间更长,则超时设置不正确。

第2步:使用带抖动的指数退避构建重试

阶段目标: 教会客户端聪明地重试请求,而不伤害自己和服务器。

什么可以重试:幂等性

在重试请求之前,请问自己:执行两次是否安全? 这个属性称为幂等性。如果一个请求重复执行得到相同结果且没有副作用,那么它是幂等的。

  • GET、HEAD、PUT、DELETE 通常是幂等的。重试它们安全。
  • POST 通常不是幂等的。重试可能产生重复订单、第二笔付款、重复记录。

⚠️ 注意: 永远不要盲目重试POST请求。重复发送非幂等请求可能导致重复扣款或数据重复。如果需要重试POST,请使用幂等性密钥(Idempotency-Key),服务器会识别它并避免执行两次操作。

重试多少次

无限重试是邪恶的。合理的限制是3到5次尝试。如果五次尝试后请求仍未成功,问题比暂时故障更严重,需要记录并单独处理。

什么是指数退避

退避是重试之间的暂停。指数退避意味着暂停时间随每次尝试成倍增长。例如:第一次暂停1秒,第二次2秒,第三次4秒,第四次8秒。公式简单:基本延迟乘以2的尝试次数次方。

为什么这样做?如果服务器过载,频繁的短间隔重试只会让它更糟。增长的暂停给服务器恢复的时间。

为什么没有抖动会导致重试风暴

想象一千个客户端同时收到429。它们都等待恰好1秒,然后恰好2秒,然后恰好4秒。并且都在同一时刻重试。这会导致同步风暴:服务器再次同时收到一千个请求,再次返回429。问题没有解决,而是陷入了循环。

解决方案是抖动,即向暂停中添加随机变化。不是恰好2秒,一个客户端等待1.7秒,另一个等待2.3秒,第三个等待1.9秒。重试在时间上分散开来,服务器平稳地减轻负载。

如何尊重Retry-After

如果服务器发送了Retry-After头部,它比你的退避公式更重要。规则很简单:取你计算的暂停值和Retry-After中的较大值。永远不要在服务器要求的时间之前重试。这是严重的不礼貌行为,会导致更多的429。

重试逻辑的逐步实现

  1. 检查请求是否幂等。如果不是且没有幂等密钥,则不重试。
  2. 检查响应码。只对429、503和网络错误(超时、连接断开)进行重试。
  3. 增加尝试计数器。如果超过限制,停止并返回错误。
  4. 根据指数增长公式计算基本暂停。
  5. 向暂停中添加随机抖动。
  6. 如果收到Retry-After,取两个值中的较大者。
  7. 等待计算的时间,然后重试请求。

建议: 设置暂停的上限,例如30或60秒。否则,第五次尝试时退避可能变得不合理地长,用户将等待过长时间。

预期结果: 当收到429时,客户端暂停、重试,并且暂停时间随重试次数增长并略有不同。

✅ 检查: 设置一个测试服务器,连续返回几次429,然后返回200。你的客户端应成功获得最终响应,并且在日志中你会看到增长且有随机波动的暂停时间。

第3步:限制并发

阶段目标: 防止客户端用大量同时请求淹没服务器。

什么是信号量(简单解释)

信号量是一个许可计数器。想象一个衣帽间,只有有限数量的挂钩。只要有空闲挂钩,你就可以挂衣服。如果都满了,就等待直到有人释放。信号量允许有限数量的任务同时执行,其余任务排队等候。

任务队列

所有需要执行的请求都放入队列。工作者线程在空闲时从队列中取出任务。这让你完全控制速率:工作者线程的数量就是最大并发请求数。

按主机的限制

一个重要细节:应该为每个主机单独设置限制。如果你与多个服务交互,全局限制是不优化的。一个缓慢的主机不应阻塞对其他主机的请求。为每个域设置单独的限制。

连接池和keep-alive

每个新的TCP连接都有成本:握手、建立安全通道。Keep-alive允许为多个连续请求重用连接。这节省时间和服务器资源。连接池保留打开的连接以备使用。将池的大小与你的并发限制协调设置。

⚠️ 注意: 不要混淆连接池大小和并发限制。池可以略大于限制作为缓冲,但如果池很大而限制很小,你会浪费打开的连接。保持合理的平衡。

逐步设置限制

  1. 确定每个主机的安全并发请求数。从小开始,例如5-10。
  2. 使用该数字创建信号量。
  3. 每次请求前从信号量请求许可。
  4. 请求完成后,无论成功与否,务必释放许可。
  5. 将连接池与keep-alive设置为相近的值。
  6. 逐渐增加限制,同时观察429的比例。一旦比例上升,就停止。

建议: 在finally块或其等效结构中释放信号量许可。否则,如果发生错误,许可不会返回,计数泄露,客户端最终会完全停止。

预期结果: 无论你向队列中放入多少任务,对主机的并发请求数都不会超过设定的限制。

✅ 检查: 将100个任务放入队列,限制为5。在日志或连接监视器中,你应看到任何时候最多只有5个活动请求。

第4步:正确响应状态码429

阶段目标: 建立对过载信号的正确响应,并了解何时适合更换IP。

收到429时的三个动作

当收到429时,你手头有三个工具,需要结合使用。

  1. 减速——降低整体请求速率,而不仅仅是对单个请求暂停。这是关键:429是一个信号,表明你的整体速率太高。
  2. 更换IP——如果你使用IP轮换,更换地址可能有助于当限制与特定IP关联时。但这并非万能。
  3. 推迟任务——将请求放回队列并延迟执行,待限制恢复后再处理。

⚠️ 注意: 更换IP不能取代礼貌。如果限制不是基于IP而是基于账户或密钥,那么任何轮换都无济于事——你仍然会收到429。不要将轮换视为绕过规则的方法:无论如何都要尊重服务限制和Retry-After。

按响应码的行动矩阵

随身携带一个简单的决策表。以下是每个状态码的处理方法。

  • 200-299 成功——处理响应,释放资源,获取下一个任务。
  • 429 Too Many Requests——降低速率,尊重Retry-After,带退避重试,必要时推迟任务或更换IP。
  • 503 Service Unavailable——带退避重试,尊重Retry-After,但不更换IP:问题在服务器端。
  • 403 Forbidden——不要盲目重试。检查授权、头部、权限。记录日志用于分析。
  • 407 Proxy Authentication Required——修复代理凭据。在修复配置之前不要重试或轮换。
  • 400, 404, 422 客户端错误——不要重试。这是你请求的错误,重试不会改变什么。
  • 500, 502, 504 服务器错误——谨慎重试少量次数,带退避。
  • 网络错误和超时——如果请求幂等,带退避重试。

逐步实现429响应

  1. 收到429后,立即停止增加速率。
  2. 读取Retry-After头部(如果存在)。
  3. 计算暂停为退避和Retry-After中的最大值。
  4. 如果限制可能与IP相关并且你有轮换机制,则在重试前更换地址。
  5. 如果重试次数用尽,将任务放回队列并带大延迟。
  6. 暂时降低整体并发限制,给服务器喘息时间。

建议: 单独跟踪最近一分钟内429的比例。如果它上升,即使还没有出现严重情况,也要自动降低速率。这称为自适应限制。

预期结果: 遇到一系列429时,客户端平稳降低速率,尊重Retry-After,并最终成功完成请求,而不会引发风暴。

✅ 检查: 在测试服务器上模拟429爆发。客户端应减少活动,而不是增加重试。暂停后成功响应的比例应恢复。

第5步:添加断路器和优雅降级

阶段目标: 为客户端提供一个保护器,在持续问题期间保护你和服务器。

什么是断路器

断路器就像电闸中的保险丝。如果错误源源不断,它就会断开电路:在一段时间内停止向有问题的服务发送请求。这保护了服务器不被压垮,也保护了你的客户端不浪费资源。

断路器的三种状态

  • Closed(闭合)——正常运行,请求通过。客户端统计错误。
  • Open(断开)——太多错误,请求被立即阻止,不发送到服务器。保持设定时间。
  • Half-open(半开)——测试模式。客户端允许少量请求通过以检查服务是否已恢复。如果是,则回到Closed;否则回到Open。

优雅降级而非完全停止

当服务不可用时,不必完全崩溃。优雅降级是指能够以较差但仍在运行的方式工作。示例:从缓存中返回数据而不是新鲜数据,显示简化结果,推迟非必要任务,返回有意义的占位符而不是错误。

建议: 始终考虑当外部服务宕机时向用户或系统显示什么。有意义的占位符比挂起或堆栈跟踪更好。

逐步设置断路器

  1. 设定错误阈值,当触发器断开,例如在20个请求的窗口中失败率达50%。
  2. 设定断路器保持断开的时间,例如30秒。
  3. 在滑动窗口中计数成功和失败。
  4. 超过阈值时,将断路器切换到Open状态。
  5. 时间到后,切换到Half-open并发送几个测试请求。
  6. 根据测试结果,回到Closed或再次回到Open。

⚠️ 注意: 不要将断路器与重试混淆。重试重复单个请求,而断路器管理整个服务流量。它们一起使用时很强大,但需要协调配置,以免断路器因正常的单点故障而过早触发。

预期结果: 当服务持续不可用时,客户端停止向它发送请求,快速返回占位符,并定期检查恢复情况。

✅ 检查: 使测试服务器不可用。客户端应在一系列失败后停止发送请求(Open),而在服务器恢复后通过Half-open自动回到正常运行。

检查结果:需要统计哪些指标

稳定性不能凭感觉评估。需要数据。以下关键指标将显示客户端是否更可靠。

主要指标

  • 成功率(Success rate)——以2xx状态码完成请求的百分比。越高越好。即使在负载下也应努力保持稳定高位。
  • p95延迟——95%的请求所花费的时间。这个指标比平均值更诚实,因为它反映了大多数用户的体验,而不仅仅是幸运的请求。
  • 429比例——返回429状态码的响应百分比。如果很高,说明你发送得太激进了。目标是将其降至最低。
  • 每个请求的重试次数——显示成功有多么困难。上升表明有问题。
  • 断路器打开次数——频繁触发表明服务不稳定或配置过于激进。

准备就绪检查清单

  1. 所有阶段的超时都已设置,没有请求会永远挂起。
  2. 重试仅适用于幂等请求和安全的状态码。
  3. 退避呈指数增长并包含抖动。
  4. 始终尊重Retry-After。
  5. 每个主机的并发由信号量限制。
  6. 连接池与keep-alive的设置与限制协调一致。
  7. 对429的响应会降低速率,而不是增加重试。
  8. 实现了按状态码的行动矩阵。
  9. 断路器保护免受持续故障的影响。
  10. 指标已收集并可访问以进行分析。

如何知道客户端变得更稳定

在相同负载下,将改进前后的指标进行对比。稳定的客户端显示高成功率、低429比例、稳定的p95,并且没有挂起的工作线程。即使服务器偶尔出现小问题,你的服务也能继续运行,而不会发生级联故障。

✅ 检查: 在测试端点上进行负载测试。如果负载下成功率保持高位且没有挂起,恭喜你,客户端是稳定的。

常见错误及其解决方案

我们来分析几乎每个人都会遇到的一些常见陷阱。

错误1:重试加剧了负载

问题: 服务器过载,而你的激进重试最终压垮了它。原因: 没有退避和没有降低速率的重试。解决方案: 添加带抖动的指数退避,限制重试次数,并且在错误增加时降低整体并发。

错误2:重试非幂等请求

问题: 重复订单、重复扣款、重复记录。原因: 盲目重试POST请求。解决方案: 只重试幂等方法。对于POST,使用服务器可以识别的幂等密钥,避免重复执行操作。

错误3:通过无限更换IP来应对429

问题: 你不断更换IP,但429并未消失。原因: 限制绑定到密钥或账户,而不是IP,或者你只是总体上发送太多。解决方案: 降低速率并尊重Retry-After。IP轮换只是工具之一,不能替代礼貌。

错误4:同步重试风暴

问题: 所有客户端在同一时刻重试,服务器再次崩溃。原因: 没有抖动的退避。解决方案: 为每次暂停添加随机成分。

错误5:工作线程挂起

问题: 服务逐渐停止处理任务。原因: 缺少超时,请求永远挂起。解决方案: 为所有请求设置连接、读取和总超时。

错误6:信号量许可泄露

问题: 一段时间后客户端停止发送请求。原因: 错误发生时信号量许可没有释放。解决方案: 在finally块中释放许可,确保总是执行。

错误7:对407的错误反应

问题: 客户端无限重试并轮换IP,但一直收到407。原因: 407状态码来自代理,表示代理认证错误,而不是服务问题。解决方案: 检查并修复代理凭据。重试在此无用。

现成代码片段

以下是三种技术栈的实现方法描述。请根据你的项目进行调整。

Python (httpx)

使用httpx创建客户端,通过Timeout对象设置显式超时,其中单独指定连接和读取超时。通过httpx.Limits设置池限制,指定每个主机的最大连接数。将调用包装在重试循环中:对于429和503,读取Retry-After,将暂停计算为带抖动的指数退避和Retry-After值的最大值,然后通过asyncio.sleep暂停。通过asyncio.Semaphore限制并发,在finally块中释放许可。只重试幂等方法,限制最多五次尝试。

Python (urllib3 Retry)

urllib3库提供了现成的机制。创建Retry对象,参数:total设置重试次数,backoff_factor启用指数暂停,status_forcelist列出需要重试的状态码,例如429、500、502、503、504。参数respect_retry_after_header启用对Retry-After的尊重。将此Retry对象传递给PoolManager或通过HTTPAdapter传递给requests。这是最快获得基本稳定性而无需手动编写循环的方法。

Node.js

使用内置的fetch和AbortController实现超时:创建控制器,设置setTimeout触发abort,将signal传递给fetch。将调用包装在带有重试循环的函数中。检查response.status:对于429和503,通过response.headers.get读取Retry-After头部,计算带抖动的暂停,通过setTimeout的Promise等待。为了限制并发,使用简单的基于Promise的信号量或流行的限制库。通过队列控制同时进行的Promise数量。

Go

在Go中,使用http.Client设置Timeout字段作为总超时,并配置Transport,设置MaxIdleConnsPerHost和IdleConnTimeout用于连接池和保持连接。对于连接超时,使用带有net.Dialer的DialContext。实现重试循环:对于429和503,读取Retry-After头部,通过time.Duration计算带指数增长和随机抖动的暂停,通过time.Sleep或带有context的select等待。通过缓冲通道作为信号量限制并发:在请求前向通道写入,在defer中从通道读取。

建议: 在任何语言中,将设置(超时、重试次数、并发限制)提取到配置中,而不是硬编码。这样你可以根据每个服务调整行为,而无需重写代码。

额外功能与优化

当基本客户端运行正常后,你可以让它更智能。

自适应限速

不要使用固定限制,而是使其动态化。读取X-RateLimit-Remaining头部,并在剩余量较小时提前减速。这样你可以在429出现之前就避免它们。

任务优先级

并非所有请求都是平等的。创建一个优先级队列:重要任务先执行,非必要任务在降级时首先被推迟。

缓存

对于幂等的GET请求,添加一个短生存期的缓存。这减少了服务器负载和你的429比例,无需任何技巧。

可观测性

接入结构化日志和指标。记录每次重试、每次断路器打开、每次长时间暂停。这样你就能在故障排查时快速找到瓶颈。

建议: 从简单的客户端开始,根据实际需求逐步添加高级功能。过早的复杂化与缺乏复杂化一样有害。

常见问题解答

是否总是需要尊重Retry-After,即使它很大?

是的。如果Retry-After对你的场景来说太长,最好推迟任务或返回降级响应,而不是提前重试。忽略Retry-After几乎总是导致新的429。

可以重试POST请求吗?

谨慎处理。如果操作不是幂等的,重试可能产生重复。使用幂等密钥,让服务器保护你免于重复执行。

初始并发请求数设置为多少?

从小开始,例如每个主机5-10个,然后逐步增加,同时观察429比例和p95。一旦429开始上升,你就找到了上限。

429和503在实践中有什么区别?

429是关于你的速率:你发送得太频繁。503是关于服务器:它自身过载或正在维护。对于429,降低速率并可能更换IP是有效的。对于503,更换IP没有意义,只需稍后重试。

为什么我的客户端有时收到407?

407状态码来自代理,表示代理认证失败。请检查代理登录名和密码。IP轮换和退避在这里无用——这是配置错误。

重试几次算是正常的?

通常3到5次。更多很少有意义:如果五次尝试都不成功,问题比暂时故障更严重。

既然退避已经增长,为什么还需要抖动?

没有抖动,多个客户端会在相同的时间点重试,造成同步风暴。随机波动分散了重试的时间,使服务器平稳地恢复。

何时打开断路器?

当滑动窗口中的错误比例超过设定阈值时,例如一半请求失败时。这保护了服务器和你自己免受资源浪费。

更换IP能帮助应对429吗?

有时可以,如果限制与IP绑定。但如果限制基于密钥或账户,更换IP没有用。更换IP不能取代降低速率和尊重Retry-After。

当服务宕机时,向用户显示什么?

一个有意义的占位符、缓存数据或简化结果。这比挂起或屏幕上显示技术错误要好。

结论

你已经走过了一段很长的旅程。让我们回顾一下你构建了什么。你设置了所有阶段的超时,以确保没有请求永远挂起。你添加了带抖动指数退避的智能重试,只重试安全请求并尊重Retry-After。你使用信号量限制了并发,并配置了带有keep-alive的连接池。你构建了对429的正确响应,并创建了按状态码的行动矩阵。最后,你添加了断路器和优雅降级。

整个指南的核心思想很简单。429不是错误,而是一次对话。 服务器告诉你放慢速度,有礼貌的客户端会倾听。稳定性并非来自攻击性,而是来自在适当时候减速的能力。

下一步做什么

收集真实负载下的指标,查看成功率和429比例。逐步为每个服务调整限制。根据X-RateLimit头部添加自适应限速。为幂等请求实现缓存。

进一步发展

单独学习IP池及其健康管理的主题——这是一个我们有意没有涉及的庞大领域。深入可观测性:追踪、仪表盘、告警。并且务必阅读你所用服务的文档:精确的限制总是比猜测更好。

你做得很好。现在你有一个不会惊慌失措、而是行为稳健且有礼貌的客户端。这是构建可靠集成的基础。祝你的项目顺利。