bigywater.tech
微服务 · 46

服务实例的下线:主动注销、先摘流量还是先停进程,崩溃后的摘除延迟

服务实例下线,client 还会打多少请求给它?TTL 和客户端缓存的刷新间隔怎么影响?先注销再停、优雅退出各自解决什么?client 重试能补多少?这篇用 3 个实例和一个小注册中心量出来。

42 对 82
实例崩溃(不注销)后的错误请求数:TTL 2 s 与 TTL 4 s(每秒 60 个请求,共 480 个)
22 对 0
先注销再 kill 时,等待 0 s 与等待 1.2 s(超过 client 的刷新间隔 1 s)的错误请求数
42 → 1
崩溃之后 client 重试一次,错误请求数的变化;代价是平均每个请求尝试 1.13 次

崩溃

实测 · 本机执行输出
  配置                | 错误请求 / 总数 | 错误发生的时间范围   | 平均每个请求尝试几次
  ------------------+-----------+-------------+-----------
  TTL 1 s · R 0.5 s | 23 / 480  | 2.8 ~ 3.9 s | 1.00      
  TTL 1 s · R 1 s   | 42 / 480  | 2.8 ~ 4.9 s | 1.00      
  TTL 2 s · R 0.5 s | 42 / 480  | 2.9 ~ 4.9 s | 1.00      
  TTL 2 s · R 1 s   | 42 / 480  | 2.8 ~ 4.9 s | 1.00      
  TTL 4 s · R 0.5 s | 82 / 480  | 2.9 ~ 6.9 s | 1.00      
  TTL 4 s · R 1 s   | 82 / 480  | 2.8 ~ 6.9 s | 1.00      
  死实例在 client 缓存里存在的时间 ≈ 注册中心发现它(TTL 内)加上下一次拉取(≤ R):TTL 和 R 越大,错误越多;每秒 60 个请求、轮询 3 个实例,发给死实例的约 20 个/秒,错误数 ≈ 20 × (从崩溃到缓存更新的秒数)

错误数取决于注册中心发现它所需的时间加上 client 缓存的刷新间隔。

先注销再停

实测 · 本机执行输出
  做法                  | 错误请求 / 总数 | 错误发生的时间范围   | 平均每个请求尝试几次
  --------------------+-----------+-------------+-----------
  注销后 kill · 等待 0 s   | 22 / 480  | 2.8 ~ 3.9 s | 1.00      
  注销后 kill · 等待 1.2 s | 0 / 480   | -           | 1.00      
  注销后 kill · 等待 2.5 s | 0 / 480   | -           | 1.00      
  注销后优雅退出 · 等待 0 s    | 18 / 480  | 3.0 ~ 3.9 s | 1.00      
  注销后优雅退出 · 等待 1.2 s  | 0 / 480   | -           | 1.00      
  注销后优雅退出 · 等待 2.5 s  | 0 / 480   | -           | 1.00      
  注销之后 client 的缓存里还有这个实例,最长 R 秒:drain 小于 R 时,这段时间里发给它的请求落在已经停掉的实例上;drain 大于 R 之后(client 的缓存已更新)再停,就不会有请求发给它。优雅退出还解决另一件事:在途请求(0.2 s)是被处理完还是被掐断——kill 会让注销瞬间在途的请求失败

注销后要等 client 缓存更新(≥ R)再停;优雅退出让在途请求被处理完。

重试

实测 · 本机执行输出
  做法                        | 错误请求 / 总数 | 错误发生的时间范围   | 平均每个请求尝试几次
  --------------------------+-----------+-------------+-----------
  崩溃 · 不重试                  | 42 / 480  | 2.8 ~ 4.9 s | 1.00      
  崩溃 · 重试一次                 | 1 / 480   | 2.9 ~ 2.9 s | 1.13      
  注销后立刻 kill · 不重试          | 22 / 480  | 2.8 ~ 3.9 s | 1.00      
  注销后立刻 kill · 重试一次         | 1 / 480   | 2.9 ~ 2.9 s | 1.06      
  注销 + 等 2.5 s + 优雅退出 · 不重试 | 0 / 480   | -           | 1.00      
  重试把用户可见的错误降到很低,代价是对死实例多发的那些尝试(右列);最彻底的是不让请求去到死实例:先注销、等 client 缓存更新、再优雅退出,错误为 0 而且不需要重试

重试把错误降到很低,代价是多打请求;最彻底的是不让请求去到死实例。

分步演示

实例下线
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。注册中心是我写的 Python 类(带 TTL 的登记、惰性过期、实例按 TTL/3 发心跳,没有网络);实例是本进程里的 Python 服务,崩溃 = 关闭监听并断开所有已建立的连接(Svc.stop);优雅退出 = 关监听但让在途请求处理完;client 每 R 秒拉一次列表并缓存;时间点用开环请求的发出时刻;每个配置只跑一次
    • 没有验证:真实的服务发现客户端(Nacos / Eureka / Consul 客户端)各自的缓存与订阅机制;推送式更新(watch)把 R 降到接近 0 的效果;连接池里的长连接(本实验每个请求新建连接);Kubernetes 里 readiness 与 preStop 的配合(见 k8s 系列)
    京ICP备2026067502号-1京公网安备11010502063487号