分布式调度面试
分布式调度面试
服务注册和发现
【简单】什么是服务注册和发现?⭐⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 服务注册与发现
💎 关键结论
服务注册与发现就是 Provider 和 Consumer 借助注册中心完成"地址对接"的协调机制:Provider 注册地址,Consumer 订阅并缓存地址列表,然后直接发起调用。因为微服务实例数量庞大且动态变化,静态配置靠人工根本维护不过来,必须依赖注册中心自动同步。
⚡记忆卡片
- 口诀:三角色、四步走——注册、订阅、通知、调用
- 关键词:服务提供者/服务消费者/注册中心/心跳/订阅通知/本地缓存
- 链路:Provider 注册地址 → 注册中心存储并监测健康 → Consumer 订阅并缓存地址 → 推拉结合保持地址最新 → Consumer 直连 Provider 调用
📖 核心知识
服务定义是服务提供者和服务消费者之间的约定,但是在微服务架构中,如何达成这个约定呢?这就依赖于服务注册和发现机制。
三种角色:在微服务架构下,服务注册和发现机制中主要有三种角色:
- 服务提供者(RPC Server / Provider)
- 服务消费者(RPC Client / Consumer)
- 服务注册中心(Registry)
注册与发现流程:服务发现通常依赖于注册中心来协调服务发现的过程,其步骤如下:
- 服务提供者将接口信息注册到注册中心。
- 服务消费者从注册中心读取和订阅服务提供者的地址信息。
- 如果有可用的服务,注册中心会主动通知服务消费者。
- 服务消费者根据可用服务的地址列表,调用服务提供者的接口。
这个过程很像是生活中的房屋租赁,房东将租房信息挂到中介公司,房客从中介公司查找租房信息。房客如果想要租房东的房子,通过中介公司牵线搭桥,联系上房东,双方谈妥签订协议,就可以正式建立起租赁关系。

发现模式:客户端发现 vs 服务端发现:
方案 原理 优点 缺点 适用边界 客户端发现 Consumer 自行拉取地址列表,本地做负载均衡 少一跳、性能高、策略灵活 每种语言/框架都要实现 SDK,升级成本高 技术栈统一的 RPC 框架(Dubbo、Spring Cloud) 服务端发现 Consumer 请求网关/LB 中间层,由其查询地址后转发 Consumer 无感知、语言无关 多一跳转发,LB 自身要高可用 跨语言、云原生 Sidecar 架构 数据同步模型:推 vs 拉:
- 推模型 - 注册中心在数据变更时主动通知 Consumer(ZooKeeper 的 Watch 机制、Nacos 的 UDP 推送),实时性好(毫秒级),但推送通道不可靠,必须配合拉取兜底。
- 拉模型 - Consumer 定时轮询注册中心(Eureka 默认 30s 增量拉取),实现简单可靠,但存在一个拉取周期内的延迟,且轮询对注册中心产生持续负载。
- 实践中多用"推为主 + 拉兜底"的混合模型,例如 Nacos 优先 UDP 推送,客户端再以 10s 周期轮询兜底。
一致性取舍:CP vs AP:
- CP(ZooKeeper、Etcd、Consul) - 保证数据强一致,但 Leader 选举期间对外不可读写(ZK 约 200ms~数秒),适合对正确性极度敏感的场景。
- AP(Eureka、Nacos 默认) - 分区期间优先可用,可能读到过期地址列表,靠客户端容错(重试、熔断)兜底,适合互联网服务发现场景。
- 选型经验:服务发现天然容忍旧数据(地址错了调用失败会重试其他节点),因此多数公司服务发现选 AP;CP 更适合分布式锁、选主等协调场景。
案例:ZK 选举空窗期推送空地址列表(真实生产事故)
一次真实生产事故:凌晨流量高峰期,网关大量报 No provider available,而下游服务自身监控一切正常。排查发现,ZooKeeper 集群恰好在做 Leader 选举(某节点 Full GC 超过 20s),所有服务的临时节点 session 过期被删除;选举结束后各 Provider 陆续重新注册,但在这约 40s 的空窗期内,Consumer 收到了"空地址列表"推送,直接清空了本地缓存,导致流量全部断掉。根因:Consumer 把推送数据当作唯一事实来源,缺少对空列表的本地容错。修复:① 开启 Dubbo 本地缓存文件容错,收到空推送时降级使用最近一次的地址文件;② ZK 的 sessionTimeout 从 30s 调大到 60s,避免 GC 停顿触发 session 过期;③ 增加"地址列表骤降 50% 以上"的监控告警。此后经历过两次 ZK 节点故障,业务均无感知。
🔬 扩展知识
详情
【L3】
- 失效场景:① 注册中心全挂——已建立的服务调用不受影响(Consumer 本地缓存了地址列表),但新实例注册、新 Consumer 订阅、地址变更同步全部失效。因此注册中心的关键不是"永远在线",而是配合客户端本地缓存做数据面与控制面解耦。② 网络分区——AP 型注册中心两边分区各自提供服务,两侧 Consumer 看到的地址列表不一致,可能出现单侧流量打满。③ 误摘除——网络抖动导致心跳丢失,注册中心把健康的 Provider 大批摘除,Consumer 收到空列表引发全站"无可用服务"。防护手段:Nacos 的摘除保护阈值(默认 0.5,健康实例低于 50% 时停止摘除)、Consumer 本地缓存兜底。
- 关键量化参数:Nacos 临时实例客户端每 5s 上报一次心跳,15s 未收到标记为不健康,30s 未收到从服务列表摘除;永久实例由服务端主动发起 TCP/HTTP 健康检查。ZooKeeper 临时节点 + session 保活,客户端 SDK 每 sessionTimeout/3 发送一次 ping,Dubbo 默认 sessionTimeout 为 60s。Eureka 客户端心跳 30s,服务端默认每 60s 剔除一次不达标实例,加上多级缓存约 30s 的同步延迟,新实例最坏约 90s 才对全部 Consumer 可见。推送时延:ZK Watch / Nacos UDP 推送在正常网络下通常 100ms 以内。
【L4】
- 注册中心容量规划方法(推演:支撑 10 万实例、5 千个服务的集群):核心算三个数字——① 写 QPS ≈ 实例数 ÷ 心跳间隔(10 万 ÷ 5s = 2 万次/秒心跳写),需要考虑分片或分组;② 推送风暴 = 单服务实例数 × 订阅方数量(一个 1000 实例的服务被 500 个 Consumer 订阅,一次变更就是 50 万次推送),必须做推送合并与限速;③ 内存 ≈ 实例数 × 元数据大小(10 万 × 1KB ≈ 100MB,反而不是瓶颈)。结论:瓶颈在推送保护和心跳分片,不在存储。
⚠️ 常见误区
详情
常见误区:
- ❌ "注册中心挂了,所有服务调用都会中断" → 不对。Consumer 在订阅时会把地址列表缓存到本地,注册中心挂掉后已有调用依靠本地缓存直连 Provider 仍可继续,只有地址变更同步会受影响。
- ❌ "推送模型实时性好,就不需要拉取兜底了" → 不对。推送通道(ZK Watch、Nacos UDP)并不保证可靠送达,生产上普遍采用"推为主 + 拉兜底"的混合模型。
- ❌ "注册中心数据越一致越好,应该选 CP" → 服务发现天然容忍旧数据,拿到过期地址最多一次调用失败后重试;而 CP 型注册中心在选举期间拒绝读写,可用性代价过高,多数公司服务发现选 AP + 客户端容错。
🔀 发散问题
1. 注册中心全部宕机后,为什么已有的服务调用仍然能正常工作?数据从哪里来?
Consumer 在启动订阅时会把地址列表缓存到本地:Dubbo 落盘到 ~/.dubbo/dubbo-registry-{appName}.cache 文件,Spring Cloud 缓存在内存中。后续调用只依赖本地缓存直连 Provider,注册中心仅负责"地址变更的同步"。这正是注册中心可用性要求可以放宽的原因——数据面与控制面天然解耦。
2. 服务发现场景为什么大多选 AP 而不是 CP?CP 的代价是什么?
服务地址列表属于"容忍旧数据"的场景:拿到过期地址最多一次调用失败后重试其他节点,代价是一次 RPC 失败;而 CP 型注册中心(如 ZK)在 Leader 选举期间拒绝读写,会导致所有新订阅失败。因此业界普遍用 AP + 客户端容错(重试、熔断、本地缓存) 组合来覆盖一致性缺口。
3. 大规模滚动重启时如何防止注册中心被注册洪峰打垮?
大促压测推演:2000 台实例 5 分钟内滚动重启、集中重新注册,注册中心 CPU 可能飙到 90%。应对思路:应急时调大摘除保护阈值、调低 Consumer 拉取频率、缩小重启批次;长期靠注册中心推送合并与限速、发布系统联动优雅注销、Consumer 本地缓存兜底。
【中等】注册中心有哪些基本功能?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 服务注册与发现
💎 关键结论
注册中心本质上是一个保存服务元数据的分布式存储,其基本功能都围绕这个目标展开:存储元数据、提供读写 API、检测服务健康、通知状态变更,并通过集群部署保证高可用与一致性。抓住"存储"这一个词,所有设计要点都能顺理成章地推导出来。
⚡记忆卡片
- 口诀:存、读写、查、通知、集群
- 关键词:元数据/注册 API/心跳检测/变更通知/CP 与 AP 集群
- 链路:Provider 注册元数据 → 注册中心按"服务-分组-节点"层次存储 → 心跳/长连接探测存活 → 变更经订阅通知 Consumer → 集群 + 一致性协议保障高可用
📖 核心知识
从服务注册和发现的流程,可以看出,注册中心是服务发现的核心组件。常见的注册中心组件有:Nacos、Consul、Zookeeper 等。注册中心的实现主要涉及几个问题:注册中心需要提供哪些接口,该如何部署;如何存储服务信息;如何监控服务提供者节点的存活;如果服务提供者节点有变化如何通知服务消费者,以及如何控制注册中心的访问权限。
元数据定义:构建微服务的首要问题是:服务提供者和服务消费者通信时,如何达成共识。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。常见的定义服务元数据的方式有:
- XML 文件 - 如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。
- IDL 文件 - 如果企业内部存在多个跨语言服务,建议使用 IDL 文件方式进行描述服务。
- REST API - 如果存在对外开放服务调用的情形的话,使用 REST API 方式则更加通用。
元数据存储:注册中心本质上是一个用于保存元数据的分布式存储。你如果明白了这一点,就会了解实现一个注册中心的所有要点都是围绕这个目标去构建的。服务的元数据信息通常有以下信息:
- 服务节点信息,如 IP、端口等。
- 接口定义,如接口名、请求参数、响应参数等。
- 请求失败的重试次数
- 序列化方式
- 压缩方式
- 通信协议
- 等等
在具体存储时,注册中心一般会按照“服务 - 分组 - 节点信息”的层次化的结构来存储。
注册中心 API:既然是分布式存储,势必要提供支持读写数据的接口,也就是 API,一般来说,需要支持以下功能:
- 服务注册接口:服务提供者通过调用服务注册接口来完成服务注册。
- 服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销。
- 心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存活状态上报。
- 服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表。
- 服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表。
除此之外,为了便于管理,注册中心还必须提供一些后台管理的 API,例如:
- 服务查询接口:查询注册中心当前注册了哪些服务信息。
- 服务修改接口:修改注册中心中某一服务的信息。
服务健康检测:注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。注册中心通常使用长连接或心跳探测方式检查服务健康状态。还是以 ZooKeeper 为例,它是基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。在 ZooKeeper 中,客户端和服务端建立连接后,会话也随之建立,并生成一个全局唯一的 Session ID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT 周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT 后服务端都没有收到客户端的心跳消息,则服务端认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。
服务状态变更通知:一旦注册中心探测到有服务提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。注册中心通常基于服务状态订阅来实现服务状态变更通知。继续以 ZooKeeper 为例,基于 ZooKeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 ZooKeeper 的 getData 方法订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData 方法来获取变更后的数据,刷新本地缓存的服务节点信息。
集群部署:注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。根据 CAP 理论,三种特性无法同时达成,必须在可用性和一致性之间做取舍。于是,根据不同侧重点,注册中心可以分为 CP 和 AP 两个阵营:
- CP 型注册中心 - 牺牲可用性来换取数据强一致性,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 Raft 协议来保证强一致性,如果出现脑裂的话,注册中心也不可用。
- AP 型注册中心 - 牺牲一致性(只保证最终一致性)来换取可用性,最典型的例子就是 Eureka、Nacos 了。对比下 Zookeeper,Eureka 不用选举一个 Leader,每个 Eureka 服务器单独保存服务注册地址,因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务。
🔬 扩展知识
详情
【L3】
- 健康检测的两种模式:客户端主动上报心跳(Eureka、Nacos 临时实例、ZK session)实现简单、开销小,但依赖客户端自觉;服务端主动探测(Nacos 永久实例、Consul 的 TCP/HTTP/Script 检查)能发现"进程假死但心跳照发"的僵尸节点,代价是探测带来的额外网络与调度开销。成熟注册中心往往两者兼备,按实例类型区分。
【L4】
- 从"分布式存储"视角理解注册中心:注册中心的一致性、可用性、容量、监听性能等问题,本质与分布式 KV 存储是同一类问题。因此评估注册中心的 CAP 取舍、扩容方式和失效模式时,可以复用分布式存储的成熟分析方法。
🔀 发散问题
1. 为什么 ZooKeeper 这类 CP 注册中心的健康检测天然比 AP 型更"及时"?
ZK 的临时节点与 session 绑定,session 过期节点立即被删除并触发 Watch 通知;而 Eureka 这类 AP 注册中心依赖心跳周期加多级缓存,摘除与同步延迟最坏可达数十秒。
2. CP 和 AP 注册中心到底怎么选?
核心看场景容忍度:服务发现容忍旧数据,多选 AP;分布式锁、选主等协调场景必须 CP。具体产品对比见本文档『主流注册中心对比有什么区别?』。
3. 除了基本功能,生产级注册中心还要解决哪些问题?
见本文档『注册中心有哪些扩展功能?』,如并行订阅、批量注销、摘除保护、白名单等。
【困难】注册中心有哪些扩展功能?⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 服务注册与发现
💎 关键结论
基本功能解决"能不能用",扩展功能解决"大规模下好不好用、稳不稳"。核心思想就一句话:用并行化和增量化提效率,用阈值保护和白名单防抖动防误操作,把注册中心从"能用"打磨到"抗造"。
⚡记忆卡片
- 口诀:提效三板斧(并行订阅、批量注销、增量更新),保命三件套(心跳开关、摘除阈值、白名单)
- 关键词:并行订阅/批量注销/增量更新/心跳开关/摘除阈值/白名单/静态注册
- 链路:规模变大 → 订阅慢、注销残留、拉取风暴 → 并行化 + 增量更新提效 → 网络抖动引发误摘除 → 阈值保护 + 开关兜底 → 环境误注册 → 白名单拦截
📖 核心知识
效率类扩展——多注册中心、并行订阅、批量注销、增量更新:
- 多注册中心:对于服务消费者来说,要能够同时从多个注册中心订阅服务;对于服务提供者来说,要能够同时向多个注册中心注册服务。
- 并行订阅服务:如果只支持串行订阅,订阅的服务较多且某些服务节点的初始化连接出现超时,后续所有初始化连接都要等待它完成,导致消费者启动非常慢。可以每订阅一个服务就单独用一个线程来处理,这样即使遇到个别服务节点连接超时,其他服务节点的初始化连接也不受影响,最慢也就是这个服务节点的初始化连接耗费的时间,最终所有服务节点的初始化连接耗时可控制在 30 秒以内。
- 批量注销服务:在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。偶发的注册调用失败对服务调用基本没有影响,顶多是某一个服务少了一个可用节点;但偶发的反注册调用失败会导致不可用的节点残留在注册中心中,变成"僵尸节点"。需要定时清理"僵尸节点",如果支持批量注销服务,就可以一次调用把该节点上提供的所有服务同时注销掉。
- 服务变更信息增量更新:为了减少服务消费者从注册中心拉取的数据量,注册中心只返回变化的那部分节点信息。尤其在只有少数节点信息变更时,此举可以大大减少拉取的数据量,从而最大程度避免产生网络风暴。
稳定类扩展——心跳开关保护与节点摘除保护:
- 心跳开关保护机制:网络频繁抖动时,可用节点不断变化,服务消费者会频繁收到节点变更信息,不断请求注册中心拉取最新节点列表。当成百上千个消费者同时请求时,可能把注册中心的带宽占满(尤其是百兆网卡的情况)。需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息。可行方案是给注册中心设置一个开关,开关打开时即使网络频繁抖动,注册中心也不通知所有消费者,比如只给 10% 的消费者返回变更,将请求量减少到原来的 1/10。代价是消费者感知节点变更的延迟从 10s 内拉长到几分钟,所以网络正常时不适合打开,只在网络频繁抖动时作为紧急措施使用。
- 服务节点摘除保护机制:服务提供者每隔一段时间汇报心跳,超时未汇报则被注册中心判定为"dead"并从可用节点中移除。遇到网络问题时,大批心跳传达失败,注册中心会把这些节点全部移除,造成剩余节点难以承受所有调用,引起"雪崩"——而实际上大部分节点可能是可用的。这时需要根据实际业务设定一个阈值比例,即使遇到上述情况,注册中心也不能摘除超过这个阈值比例的节点。该比例可根据业务冗余度确定,通常设定在 20% 左右。业务明确要下线大批量节点的情况是可预知的,此时可关闭阈值保护;正常情况下应打开保护,防止网络抖动时大批可用节点被摘除。
安全类扩展——白名单机制:实际微服务测试和部署时通常包含多套环境(生产一套、测试一套)。开发自测、测试回归时一般用测试环境,节点注册到测试注册中心集群。但经常出现部署时错误地把测试环境节点注册到线上注册中心集群,线上流量就会调用到测试环境节点,可能造成意想不到的后果。注册中心可以提供白名单机制——把注册中心想象成带门禁的房间,只有拥有门禁卡的 RPC Server 才能进入——只有添加到白名单内的 RPC Server 才能调用注册接口,避免测试环境节点意外跑到线上。
容灾类扩展——静态注册中心:因为服务提供者是向服务消费者提供服务的,服务是否可用,服务消费者应该比注册中心更清楚。因此可以直接在服务消费者端,根据调用是否成功来判定服务提供者是否可用:如果调用某一节点连续失败超过一定次数,就在本地内存中将其标记为不可用;并且每隔一段固定时间,向被标记为不可用的节点发起保活探测,探测成功则恢复为可用状态,重新发起调用。
🔀 发散问题
1. 摘除保护阈值设多少合适?设置过高或过低各有什么风险?
一般参考业务冗余度,常见取值在 20%~50% 之间(如 Nacos 默认保护阈值 0.5)。过低防不住网络抖动引发的大面积误摘除,过高则可能让真正故障的节点滞留过久,把错误地址持续推给消费者。
2. "僵尸节点"为什么必须主动清理,等心跳超时自动摘除不行吗?
反注册失败的节点已经不再上报心跳,靠超时机制确实能兜底,但超时窗口内的调用会持续打到死节点上产生错误;批量注销加定时清理可以把这个窗口压缩到最短。
【中等】主流注册中心对比有什么区别?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式调度 / 服务注册与发现
💎 关键结论
注册中心选型本质上就看三件事:CAP 模型、功能集成度、生态与维护状态。服务发现场景多数选 AP(可用性优先):Spring Cloud 生态 + 配置中心一体化选 Nacos,多数据中心选 Consul,K8s 生态用 etcd,大数据生态离不开 ZooKeeper,Eureka 已停止维护应迁离。
⚡记忆卡片
- 口诀:AP 保可用,CP 保正确;Eureka 已停,一体化选 Nacos
- 关键词:Eureka/Nacos/Consul/ZooKeeper/etcd/CAP/Raft/ZAB/Distro
- 链路:明确 CAP 取向 → 看一致性协议(Raft/ZAB/Distro) → 看功能集成(服务发现 + 配置中心) → 看生态与维护状态 → 输出选型结论
📖 核心知识
核心对比:
维度 Eureka Nacos Consul ZooKeeper etcd 开发语言 Java Java Go Java Go CAP 模型 AP AP + CP(可切换) CP(支持 stale 模式近似 AP) CP CP 一致性协议 无(去中心化复制) Distro(AP)+ Raft(CP) Raft ZAB Raft 健康检查 客户端心跳 客户端心跳 + 服务端主动探测 多种(TCP/HTTP/Script/DNS) Session 心跳 无内置,需应用层实现 服务发现 原生支持 原生支持 原生支持 需自行实现(临时节点) 需自行实现 配置中心 不支持 支持(集成) 支持(KV 存储) 支持(但弱) 支持 多数据中心 不支持 支持(通过 namespace) 原生支持 不支持 不支持 Watch 机制 客户端轮询(默认 30s) 长轮询 + UDP 推送 长轮询/blocking query 一次性触发 持续 Watch 管理控制台 有 有(功能丰富) 有 第三方(如 ZKUI) 无官方 运维复杂度 低 中 中 高(JVM) 低 社区活跃度 停止维护(Eureka 2.x 取消) 活跃 活跃 活跃 活跃(K8s 背书) 选型建议:
- Spring Cloud 生态 + 配置中心一体化 → Nacos:同时提供服务发现和配置中心,AP/CP 可切换,国内生态成熟。
- 多数据中心 + 服务网格 → Consul:原生多数据中心,健康检查丰富,与 Service Mesh 集成好。
- Kubernetes 生态 → etcd:K8s 原生依赖,但服务发现需自行封装。
- 大数据生态 → ZooKeeper:Kafka/HBase 等深度依赖。
- 历史项目 → 迁移离开 Eureka(已停止维护)。
**为什么 Eureka 适合做注册中心而 ZooKeeper 不适合?**核心区别在于 CAP 模型的选择:
- Eureka(AP):优先保证可用性。节点间数据最终一致,但任一节点都能独立提供注册和发现服务。即使网络分区,各节点仍可用,只是数据可能短暂不一致。注册中心最怕的是"不可用"(导致服务调用全盘失败),因此 AP 更合适。
- ZooKeeper(CP):优先保证一致性。Leader 选举期间集群不可用,网络分区时少数派分区停止服务。注册中心偶尔的数据不一致(如短暂有下线节点)影响有限(调用失败可重试),但不可用影响致命。
结论:服务注册中心应选择 AP 模型。Nacos 默认 AP 模式正是基于此考虑。
🔬 扩展知识
详情
【L3】
- Nacos 的 AP/CP 双模型:Nacos 对临时实例使用 Distro 协议(AP,去中心化,各节点对等负责一部分数据的同步),对持久化实例使用 Raft 协议(CP),按实例类型自动切换,这也是它"AP + CP 可切换"的实现基础。
- Consul 的 stale 读:Consul 默认只允许 Leader 读以保证一致性,但支持 stale 模式从任意节点读,用少量一致性换取读可用性与横向扩展,是"CP 主体 + 近似 AP"的折中设计。
【L4】
- 选型要放到业务容忍度里看:注册中心选型不是纯技术问题——服务发现容忍旧数据但极怕不可用,所以 AP 占优;而分布式锁、Leader 选举等协调场景容忍不可用但极怕数据错,所以 ZK/etcd 这类 CP 系统在这些场景仍是主流。同一个公司常常"服务发现用 Nacos、协调服务用 ZooKeeper"并存。
🔀 发散问题
1. 注册中心挂了服务调用会断吗?
不会立即断,Consumer 本地缓存了地址列表可以继续直连 Provider,详见本文档『什么是服务注册和发现?』中的失效场景分析。
2. Eureka 停止维护后还在用它的历史项目该怎么办?
优先迁移到 Nacos(同为 Java 生态,Spring Cloud Alibaba 提供适配);迁移期间可以利用双注册(同时向新旧注册中心注册)过渡,验证无误后再摘除旧注册中心,避免一次性切换的风险。
3. 为什么 K8s 环境下 etcd 反而不直接当注册中心用?
etcd 是 K8s 的元数据存储,服务发现由 K8s Service + DNS(CoreDNS)在更上层完成,应用无需自己对接 etcd;这也是"云原生下注册中心被平台内置能力替代"趋势的一个例证。
负载均衡
【简单】什么是负载均衡?为什么需要负载均衡?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:分布式调度 / 负载均衡
💎 关键结论
负载均衡就是把请求合理地分摊到多台机器上,目标是"优化资源利用率、最大化吞吐、最小化响应时间、避免过载"。它一个组件同时给系统带来高并发、伸缩性、高可用三重能力,是分布式系统的流量入口标配。
⚡记忆卡片
- 口诀:一分摊、三收益——高并发、伸缩性、高可用
- 关键词:负载分摊/吞吐量/故障剔除/伸缩性/安全防护
- 链路:请求到达 → 算法选出最优机器 → 流量均匀分摊 → 单机不过载、故障自动跳过 → 系统高可用高吞吐
📖 核心知识
“负载均衡(Load Balance,简称 LB)”是一种技术,用来在多个计算机、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到优化资源利用率、最大化吞吐率、最小化响应时间、同时避免过载的目的。
负载均衡的主要作用如下:
- 高并发:负载均衡可以优化资源使用率,通过算法调整负载,尽力均匀的分配资源,以此提高资源利用率、从而提升整体吞吐量。
- 伸缩性:发生增减资源时,负载均衡可以自动调整分发,使得应用集群具备伸缩性。
- 高可用:负载均衡器可以监控候选机器,当某机器不可用时,自动跳过,将请求分发给可用的机器。这使得应用集群具备高可用的特性。
- 安全防护:有些负载均衡软件或硬件提供了安全性功能,如:黑白名单、防火墙,防 DDos 攻击等。
🔀 发散问题
1. 负载均衡和服务路由看起来很相似,区别是什么?
负载均衡的目标是提供服务分发而不是解决路由问题,常见的静态、动态负载均衡算法无法实现精细化的路由管理,但负载均衡也可以简单看做是路由方案的一种。详见本文档『什么是服务路由?路由有什么用?』。
2. 负载均衡有哪些常见的实现形态?
从载体看分硬件和软件负载均衡,从网络层次看分四层和七层负载均衡,详见本文档『负载均衡技术有哪些分类?』。
【中等】负载均衡技术有哪些分类?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式调度 / 负载均衡
💎 关键结论
负载均衡按载体分硬件(F5/A10)和软件(Nginx/HAProxy/LVS),按网络层次分四层(改 IP/MAC)和七层(DNS/HTTP 重定向/反向代理)。记住一条主线:层次越低性能越高但越"看不懂"请求内容,层次越高越灵活但开销越大。
⚡记忆卡片
- 口诀:硬件贵而强,软件广而活;四层改地址,七层看内容
- 关键词:F5/Nginx/HAProxy/LVS/四层/七层/DNS/反向代理
- 链路:请求到达入口 → 七层按 URL/请求头选后端(DNS/HTTP 重定向/反向代理) → 四层按 IP+端口转发(改 IP/改 MAC) → 后端集群分摊负载
📖 核心知识
支持负载均衡的技术很多,我们可以通过不同维度去进行分类。
载体维度:硬件 vs 软件:
- 硬件负载均衡:一般是在定制处理器上运行的独立负载均衡服务器,价格昂贵,土豪专属。主流产品有:F5 和 A10。优点:功能强大(支持全局负载均衡并提供较全面、复杂的负载均衡算法);性能强悍(在专用处理器上运行,吞吐量大,可支持单机百万以上的并发);安全性高(往往具备防火墙,防 DDos 攻击等安全功能)。缺点:成本昂贵(购买和维护成本都很高);扩展性差(当访问量突增时,超过限度不能动态扩容)。
- 软件负载均衡:应用最广泛,无论大公司还是小公司都会使用,从软件层面实现负载均衡,一般可以在任何标准物理设备上运行。主流产品有:Nginx、HAProxy、LVS。其中 LVS 可以作为四层负载均衡器,其负载均衡的性能要优于 Nginx;HAProxy 可以作为 HTTP 和 TCP 负载均衡器;Nginx、HAProxy 可以作为四层或七层负载均衡器。优点:扩展性好(适应动态变化,可以通过添加实例动态扩展);成本低廉(可在任何标准物理设备上运行)。缺点:性能略差(相比硬件负载均衡要略低一些)。
网络通信维度:七层负载均衡,可以根据访问用户的 HTTP 请求头、URL 信息将请求转发到特定的主机,手段包括 DNS 重定向、HTTP 重定向、反向代理:
DNS 负载均衡:一般用于互联网公司,复杂的业务系统不适合使用。大型网站一般使用 DNS 负载均衡作为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。DNS 即域名解析服务,是 OSI 第七层网络协议,被设计为一个树形结构的分布式应用,自上而下依次为:根域名服务器,一级域名服务器,二级域名服务器,...,本地域名服务器。如果所有数据都存储在根域名服务器,DNS 查询的负载和开销会非常庞大。因此 DNS 查询相对于 DNS 层级结构是一个逆向的递归流程,DNS 客户端依次请求本地 DNS 服务器、上一级 DNS 服务器、...、根 DNS 服务器(又叫权威 DNS 服务器),一旦命中立即返回;为了减少查询次数,每一级 DNS 服务器都会设置 DNS 查询缓存。DNS 负载均衡的工作原理就是:基于 DNS 查询缓存,按照负载情况返回不同服务器的 IP 地址。

优点:使用简单(负载均衡工作交给 DNS 服务器处理,省掉了负载均衡服务器维护的麻烦);提高性能(可以支持基于地址的域名解析,解析成距离用户最近的服务器地址,类似 CDN 的原理,可以加快访问速度,改善性能)。缺点:可用性差(DNS 解析是多级解析,新增/修改 DNS 后解析时间较长,解析过程中用户访问网站将失败);扩展性差(控制权在域名商那里,无法对其做更多的改善和扩展);维护性差(不能反映服务器的当前运行状态,支持的算法少,不能区分服务器的差异)。
HTTP 负载均衡:基于 HTTP 重定向实现。原理是:根据用户的 HTTP 请求计算出一个真实的服务器地址,将该服务器地址写入 HTTP 重定向响应中,返回给浏览器,由浏览器重新进行访问。

优点:方案简单。缺点:额外的转发开销(每次访问需要两次请求服务器,增加访问延迟);降低搜索排名(使用重定向后,搜索引擎会视为 SEO 作弊);如果负载均衡器宕机,就无法访问该站点。由于其缺点比较明显,这种负载均衡策略实际应用较少。
反向代理负载均衡:反向代理(Reverse Proxy)方式是指以代理服务器来接受网络请求,然后将请求转发给内网中的服务器,并将从内网服务器上得到的结果返回给网络请求的客户端。反向代理服务的主流产品:Nginx、Apache。正向代理与反向代理的区别:正向代理发生在客户端,是由用户主动发起的,翻墙软件就是典型的正向代理;反向代理发生在服务端,用户不知道代理的存在。

反向代理是如何实现负载均衡的呢?以 Nginx 为例,如下所示:

首先,在代理服务器上设定好负载均衡规则。然后,当收到客户端请求,反向代理服务器拦截指定的域名或 IP 请求,根据负载均衡算法,将请求分发到候选服务器上。其次,如果某台候选服务器宕机,反向代理服务器会有容错处理,比如分发请求失败 3 次以上,将请求分发到其他候选服务器上。优点:支持多种负载均衡算法,以应对不同的场景需求;基于 HTTP 协议可以监控转发服务器的状态(如系统负载、响应时间、是否可用、连接数、流量等),从而根据这些数据调整负载均衡策略。缺点:额外的转发开销(转发操作本身有性能开销,可能包括创建连接、等待连接响应、分析响应结果等操作);增加系统复杂度(反向代理自身宕机则无法访问站点,需要高可用方案,如主备模式、双主模式;反向代理自身也存在性能瓶颈,需要可扩展方案)。
网络通信维度:四层负载均衡,基于 IP 地址和端口进行请求的转发,手段包括修改 IP 地址、修改 MAC 地址:
IP 负载均衡:在网络层通过修改请求目的地址进行负载均衡。

IP 均衡处理流程大致为:① 客户端请求 192.168.137.10,由负载均衡服务器接收到报文;② 负载均衡服务器根据算法选出一个服务节点 192.168.0.1,然后将报文请求地址改为该节点的 IP;③ 真实服务节点收到请求报文,处理后,返回响应数据到负载均衡服务器;④ 负载均衡服务器将响应数据的源地址改为负载均衡服务器地址,返回给客户端。IP 负载均衡在内核进程完成数据分发,较反向代理负载均衡有更好的处理性能。但是,由于所有请求响应都要经过负载均衡服务器,集群的吞吐量受制于负载均衡服务器的带宽。
数据链路层负载均衡:指在通信协议的数据链路层修改 mac 地址进行负载均衡。

在 Linux 平台上最好的链路层负载均衡开源产品是 LVS(Linux Virtual Server)。LVS 是基于 Linux 内核中 netfilter 框架实现的负载均衡系统。netfilter 是内核态的 Linux 防火墙机制,可以在数据包流经过程中,根据规则设置若干个关卡(hook 函数)来执行相关的操作。LVS 的工作流程大致如下:当用户访问 www.sina.com.cn 时,用户数据通过层层网络,最后通过交换机进入 LVS 服务器网卡并进入内核网络层;进入 PREROUTING 后经过路由查找,确定访问的目的 VIP 是本机 IP 地址,数据包进入 INPUT 链;IPVS 工作在 INPUT 链上,根据访问的
vip+port判断请求是否是 IPVS 服务,如果是则调用注册的 IPVS HOOK 函数,进行 IPVS 相关主流程,强行修改数据包的相关数据,并将数据包发往 POSTROUTING 链;POSTROUTING 收到数据包后,根据目标 IP 地址(后端服务器)通过路由选路,将数据包最终发往后端服务器。开源 LVS 版本有 3 种工作模式,每种模式工作原理截然不同,各有优缺点,分别适合不同的应用场景,不过最终本质的功能都是能实现均衡的流量调度和良好的扩展性,主要包括三种模式:DR 模式、NAT 模式、Tunnel 模式。
🔬 扩展知识
详情
【L3】
- LVS 三种模式的差异:NAT 模式下请求和响应都经过 LVS,部署简单但 LVS 是带宽瓶颈;DR(直接路由)模式下响应由 Real Server 直接返回客户端,LVS 只改 MAC 地址,性能最好但要求 LVS 与 Real Server 同网段;Tunnel(隧道)模式用 IP 封装实现跨网段直连返回,适合异地部署。
- 四层与七层的性能差在哪:四层负载均衡(如 LVS/IPVS)工作在内核态,只改地址不解析报文内容;七层负载均衡(如 Nginx)要完整代理连接、解析 HTTP 报文,单核吞吐通常低一个数量级,但能按 URL、Header 做精细转发。
【L4】
- 大型网站的典型分层流量架构:DNS 负载均衡做第一级(地域就近),四层 LB(LVS)做第二级扛吞吐,七层 LB(Nginx)做第三级按业务路由,最后客户端 SDK 做服务内负载均衡。每层解决上一层解决不了的问题,这是"层次越低性能越高、层次越高越灵活"原则的工程落地。
🔀 发散问题
1. 四层和七层负载均衡应该怎么选?
追求极致吞吐、转发规则简单(只按 IP+端口)选四层;需要按 URL/Header 路由、做健康检查与灰度的选七层。大规模场景常四层七层叠加使用。
2. 负载均衡器自身的高可用怎么保证?
常见方案是主备模式(一主一备)或双主模式(互为主备),配合 Keepalived/VRRP 之类的虚拟 IP 漂移机制;七层代理还要考虑自身的水平扩展方案。
3. 选出了候选机器之后,具体用哪个算法分发请求?
见本文档『负载均衡有哪些算法?』,从轮询、随机到一致性哈希逐层递进。
【中等】负载均衡有哪些算法?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:20 min | 🏷 标签:分布式调度 / 负载均衡
💎 关键结论
负载均衡算法的演进主线是:从静态到动态、从无状态到有状态。机器同质用轮询/随机,能力不同加权重,连接时长不均看最少连接数,有状态服务用哈希,节点要伸缩就上一致性哈希 + 虚拟节点。选算法前先问三个问题:机器是否同质、请求是否等重、服务是否有状态。
⚡记忆卡片
- 口诀:同质轮询随机,异质加权;连接不均看最少,有状态哈希,伸缩一致性 + 虚拟节点
- 关键词:轮询/随机/加权/最少连接数/最少响应时间/哈希/一致性哈希/虚拟节点
- 链路:机器同质 → 轮询/随机均匀分发 → 能力差异 → 权重倾斜 → 连接时长差异 → 最少连接数动态调度 → 有状态定位 → 哈希绑定 → 节点增减 → 一致性哈希 + 虚拟节点减小迁移
📖 核心知识
负载均衡器的实现可以分为两个部分:根据负载均衡算法在候选机器列表选出一个机器;将请求数据发送到该机器上。负载均衡算法是负载均衡服务核心中的核心。负载均衡产品多种多样,但是各种负载均衡算法原理是共性的。负载均衡算法有很多种,分别适用于不同的应用场景。
静态均匀分发:轮询与随机算法:
“轮询算法(Round Robin)”的策略是:将请求“依次”分发到候选机器。如下图所示,轮询负载均衡器收到来自客户端的 6 个请求,编号为 1、4 的请求会被发送到服务端 0;编号为 2、5 的请求会被发送到服务端 1;编号为 3、6 的请求会被发送到服务端 2。

轮询算法适合的场景需要满足:各机器处理能力相近,且每个请求工作量差异不大。
“随机算法(Random)” 将请求“随机”分发到候选机器。如下图所示,随机负载均衡器收到来自客户端的 6 个请求,会随机分发请求,可能会出现:编号为 1、5 的请求会被发送到服务端 0;编号为 2、4 的请求会被发送到服务端 1;编号为 3、6 的请求会被发送到服务端 2。

随机算法适合的场景需要满足:各机器处理能力相近,且每个请求工作量差异不大。学习过概率论的都知道,调用量较小的时候,可能负载并不均匀,调用量越大,负载越均衡。
引入权重:加权轮询/随机算法:
轮询/随机算法适合的场景都需要满足:各机器处理能力相近,且每个请求工作量差异不大。在理想状况下,假设每个机器的硬件条件相同(CPU、内存、网络 IO 等配置都相同),并且每个请求的耗时一样(请求传输时间、请求访问数据时间、计算时间等),这时轮询算法才能真正做到负载均衡。显然,要满足以上条件都相同是几乎不可能的。如果有一点不能满足,都无法做到真正的负载均衡。个体存在较大差异,当请求量较大时,处理较慢的机器可能会逐渐积压请求,从而导致过载甚至宕机。
如下图所示,假设存在这样的场景:服务端 1 的处理能力远低于服务端 0 和服务端 2;轮询/随机算法可以保证将请求尽量均匀的分发给两个机器;编号为 1、4 的请求被发送到服务端 0;编号为 3、6 的请求被发送到服务端 2;二者处理能力强,应对游刃有余;编号为 2、5 的请求被发送到服务端 1,服务端 1 处理能力弱,应对捉襟见肘,导致过载。

《蜘蛛侠》电影中有一句经典台词:能力越大,责任越大。显然,以上情况不符合这句话,处理能力强的机器并没有被分发到更多的请求,它的处理能力被闲置了。那么,如何解决这个问题呢?
一种比较容易想到的思路是:引入权重属性,可以根据机器的硬件条件为其设置合理的权重值,负载均衡时,优先将请求分发到权重较高的机器。“加权轮询算法(Weighted Round Robbin)” 和“加权随机算法(Weighted Random)” 都采用了加权的思路,在轮询/随机算法的基础上,引入了权重属性,优先将请求分发到权重较高的机器。这样,就可以针对性能高、处理速度快的机器设置较高的权重,让其处理更多的请求;而针对性能低、处理速度慢的机器则与之相反。一言以蔽之,加权策略强调了——能力越大,责任越大。
如下图所示,服务端 0 设置权重为 3,服务端 1 设置权重为 1,服务端 2 设置权重为 2。负载均衡器收到来自客户端的 6 个请求,那么编号为 1、2、5 的请求会被发送到服务端 0,编号为 4 的请求会被发送到服务端 1,编号为 3、6 的请求会被发送到机器 2。

动态感知负载:最少连接数与最少响应时间算法:
加权轮询/随机算法虽然一定程度上解决了机器处理能力不同时的负载均衡场景,但它最大的问题在于不能动态应对网络中负载不均的场景。加权的思路是在负载均衡处理的事前,预设好不同机器的权重,然后分发。然而,每个请求的连接时长不同,负载均衡器也不可能准确预估出请求的连接时长。因此,采用加权轮询/随机算法,都无法动态应对连接时长不均的网络场景,可能会出现某些机器当前连接数过多,而另一些机器的连接过少的情况,即并非真正的流量负载均衡。
如下图所示,假设存在这样的场景:3 个服务端的处理能力相同;编号为 1、4 的请求被发送到服务端 0,但是 1 很快就断开连接,此时只有 4 请求连接服务端 0;编号为 2、5 的请求被发送到服务端 1,但是 2 始终保持长连接;该系统继续运行时,服务端 1 发生过载;编号为 3、6 的请求被发送到服务端 2,但是 3 很快就断开连接,此时只有 6 请求连接服务端 2。

既然请求的连接时长不同,会导致有的服务端积压大量连接数,而有的服务端保持的连接数少。那么,如果负载均衡器监控一下服务端当前所持有的连接数,优先将请求分发给连接数少的服务端,不就能有效提高分发效率了吗?最少连接数算法正是采用这个思路去设计的。
“最少连接数算法(Least Connections)” 将请求分发到连接数/请求数最少的候选机器。要根据机器连接数分发,显然要先维护机器的连接数。因此,最少连接数算法需要实时追踪每个候选机器的活跃连接数;然后,动态选出连接数最少的机器,优先分发请求。最少连接数算法会记录当前时刻每个候选节点正在处理的连接数,然后选择连接数最小的节点。该策略能够动态、实时地反应机器的当前状况,较为合理地将负载分配均匀,适用于对当前系统负载较为敏感的场景。由此可见,最少连接数算法适用于对系统负载较为敏感且请求连接时长相差较大的场景。
如下图所示,假设存在这样的场景:服务端 0 和服务端 1 的处理能力相同;编号为 1、3 的请求被发送到服务端 0,但是 1、3 很快就断开连接;编号为 2、4 的请求被发送到服务端 1,但是 2、4 保持长连接;由于服务端 0 当前连接数最少,编号为 5、6 的请求被分发到服务端 0。

“加权最少连接数算法(Weighted Least Connection)”在最少连接数算法的基础上,根据机器的性能为每台机器分配权重,再根据权重计算出每台机器能处理的连接数。
“最少响应时间算法(Least Time)” 将请求分发到响应时间最短的候选机器。最少响应时间算法和最少连接数算法二者的目标其实是殊途同归,都是动态调整,将请求尽量分发到处理能力强的机器上。不同点在于,最少连接数关注的维度是机器持有的连接数,而最少响应时间关注的维度是机器上一次响应时间哪个最短。理论上来说,持有的连接数少、响应时间短,都可以表明机器潜在的处理能力比较强。最少响应时间算法具有高度的敏感性、自适应性。但是,由于它需要持续监控候选机器的响应时延,相比于监控候选机器的连接数,会显著增加监控的开销。此外,请求的响应时延并不一定能完全反应机器的处理能力,有可能某机器上一次处理的请求恰好是一个开销非常小的请求。

有状态定位:哈希算法:
前面提到的负载均衡算法,都只适用于无状态应用。所谓无状态应用,意味着:请求无论分发到集群中的任意机器上,得到的响应都是相同的;然而,有状态服务则不然:请求分发到不同的机器上,得到的结果是不一样的。典型的无状态应用是普通的 Web 服务器;典型的有状态应用是各种分布式数据库(如:Redis、ElasticSearch 等),这些数据库存储了大量,乃至海量的数据,无法全部存储在一台机器上,为了提高整体容量以及吞吐量,采用了分区(分片)的设计,将数据化整为零的存储在不同机器上。对于有状态应用,不仅仅需要保证负载的均衡,更为重要的是,需要保证针对相同数据的请求始终访问的是相同的机器,否则,就无法获取到正确的数据。
“哈希算法(Hash)” 根据一个 key(可以是唯一 ID、IP、URL 等),通过哈希函数计算得到一个数值,用该数值在候选机器列表的进行取模运算,得到的结果便是选中的机器。

这种算法可以保证,同一关键字(IP 或 URL 等)的请求,始终会被转发到同一台机器上。哈希负载均衡算法常被用于实现会话粘滞(Sticky Session)。但是,哈希算法的问题是:当增减节点时,由于哈希取模函数的基数发生变化,会影响大部分的映射关系,从而导致之前的数据不可访问。要解决这个问题,就必须根据新的计算公式迁移数据。显然,如果数据量很大的情况下,迁移成本很高;并且,在迁移过程中,要保证业务平滑过渡,需要使用数据双写等较为复杂的技术手段。

伸缩性改良:一致性哈希与虚拟节点:
哈希算法的缺点是:当集群中出现增减节点时,由于哈希取模函数的基数发生变化,会导致大量集群中的机器不可用;需要通过代价高昂的数据迁移来解决问题。一致性哈希算法就是为了尽量减少影响的机器数而应运而生。
一致性哈希算法对哈希算法进行了改良。“一致性哈希算法(Consistent Hash)”,根据哈希算法将对应的 key 哈希到一个具有 2^32 个桶的空间,并且头尾相连(0 到 2^32-1),即一个闭合的环形,这个圆环被称为“哈希环”。哈希算法是对节点的数量进行取模运算;而一致性哈希算法则是对 2^32 进行取模运算。哈希环的空间是按顺时针方向组织的,需要对指定 key 的数据进行读写时,会执行两步:① 先对节点进行哈希计算,计算的关键字通常是 IP 或其他唯一标识(例:hash(ip)),然后对 2^32 取模,以确定节点在哈希环上的位置;② 对 key 进行哈希计算(hash(key)),然后对 2^32 取模,以确定 key 在哈希环上的位置;③ 根据 key 的位置,顺时针找到的第一个节点,就是 key 对应的节点。所以,一致性哈希是将“存储节点”和“数据”都映射到一个顺时针排序的哈希环上。

一致性哈希算法会尽可能保证,相同的请求被分发到相同的机器上。当出现增减节点时,只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响,不会引起剧烈变动。其中,相同的请求是指:一般在使用一致性哈希时,需要指定一个 key 用于 hash 计算,可能是:用户 ID、请求方 IP、请求服务名称,参数列表构成的串;尽可能是指:哈希环上出现增减节点时,少数机器的变化不应该影响大多数的请求。
(1)增加节点:如下图所示,假设哈希环中新增了一个节点 S4,新增节点经过哈希计算映射到图中位置:

此时,只有 K1 收到影响;而 K0、K2 均不受影响。
(2)减少节点:如下图所示,假设哈希环中减少了一个节点 S0:

此时,只有 K0 收到影响;而 K1、K2 均不受影响。
一致性哈希算法并不保证节点能够在哈希环上分布均匀,由此而产生一个问题,哈希环上可能有大量的请求集中在一个节点上。从概率角度来看,哈希环上的节点越多,分布就越均匀。正因为如此,一致性哈希算法不适用于节点数过少的场景。如下图所示:极端情况下,可能由于节点在哈希环上分布不均,有大量请求计算得到的 key 会被集中映射到少数节点,甚至某一个节点上。此外,节点分布不均匀的情况下,进行容灾与扩容时,哈希环上的相邻节点容易受到过大影响,从而引发雪崩式的连锁反应。

虚拟哈希算法进一步对一致性哈希算法进行了改良。解决思路是:虽然实际的集群可能节点数较少,但是在哈希环上引入大量的虚拟哈希节点。具体来说,“虚拟哈希算法”有二次映射:先将虚拟节点映射到哈希环上,再将虚拟节点映射到实际节点上。
如下图所示,假设存在这样的场景:分布式集群中有 4 个真实节点,分别是:S0、S1、S2、S3;我们不妨先假定分配给哈希环 12 个虚拟节点,并将虚拟节点映射到真实节点上,映射关系如下:S0 - S0_0、S0_1、S0_2、S0_3;S1 - S1_0、S1_1、S1_2、S1_3;S2 - S2_0、S2_1、S2_2、S2_3;S3 - S3_0、S3_1、S3_2、S3_3。

通过引入虚拟哈希节点,使得哈希环上的节点分布相对均匀了。举例来说,假如此时某请求的 key 哈希取模后,先映射到哈希环的 [S3_2, S0_0]、[S3_0, S0_1]、[S3_1, S0_2] 这三个区间的任意一点;接下来的二次映射都会匹配到真实节点 S0。在实际应用中,虚拟哈希节点数一般都比较大(例如:Redis 的虚拟哈希槽有 16384 个),较大的数量保证了虚拟哈希环上的节点分布足够均匀。
虚拟节点除了会提高节点的均衡度,还会提高系统的稳定性。当节点变化时,会有不同的节点共同分担系统的变化,因此稳定性更高。例如,当某个节点被移除时,分配给该节点的多个虚拟节点会被一并移除,而这些虚拟节点按顺时针方向的下一个虚拟节点,可能会对应不同的真实节点,即这些不同的真实节点共同分担了节点变化导致的压力。此外,有了虚拟节点后,可以通过调整分配给真实节点的虚拟节点数,来达到设置权重一样的效果,使得负载均衡更加灵活。综上所述,虚拟一致性哈希算法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景。
🔬 扩展知识
详情
【L3】
- 加权算法的工程实现差异:简单加权轮询按权重展开序列(权重 3:1:2 展开为 6 个位置的序列)会导致同权重请求连续集中;平滑加权轮询(Dubbo、Nginx 采用的思路)通过每轮给各节点当前权重累加并扣减最大者,让分发序列尽量打散,避免瞬时集中压垮单机。
- 最少响应时间的监控成本:需要持续采集各候选机器的 RT(通常用滑动窗口求平均),统计开销明显高于连接数计数,且单次 RT 受请求本身开销影响大,实践中常以 P95/P99 而非平均值作为选择依据。
【L4】
- 一致性哈希的工业级应用:Redis Cluster 用 16384 个哈希槽(slot)做数据分片,节点增减时按槽迁移;Amazon Dynamo 早期论文采用一致性哈希 + 虚拟节点做对等存储节点管理。虚拟节点数越多分布越均匀,但元数据与再均衡管理开销也越大,二者需要权衡。
📚 延伸阅读:Dubbo 官方负载均衡算法说明(源码讲解非常详细,非常值得借鉴);各算法的可执行示例已归档在 Github 仓库:java-load-balance,可以通过执行
io.github.dunwu.javatech.LoadBalanceDemo查看各算法执行效果。
⚠️ 常见误区
详情
常见误区:
- ❌ “轮询算法在任何场景下都能做到负载均衡” → 不对。轮询只有当各机器处理能力相近且每个请求工作量差异不大时才近似均衡,否则会拖垮弱机器,需要加权算法补偿。
- ❌ “随机算法调用量小时也很均匀” → 不对。随机算法服从大数定律,调用量越小偏差越大,调用量越大负载越均衡。
- ❌ “一致性哈希能完全避免节点增减的影响” → 不对。一致性哈希只能把影响范围缩小到顺时针相邻节点,且节点过少时分布不均可能引发倾斜,需要虚拟节点来改善均匀度。
🔀 发散问题
1. 会话粘滞(Sticky Session)有哪些实现方式,各有什么问题?
常用哈希算法按用户 IP 或 Cookie 哈希到固定机器,实现简单但破坏均衡性,且机器下线会话丢失;更稳妥的做法是把会话状态外置到 Redis 等共享存储,让任意机器都能处理请求,代价是多一次存储访问。
2. 为什么加权算法应对不了连接时长不均的场景?
权重是事前静态设定的,而每个请求的连接时长无法预估,长连接会持续占用机器处理能力,静态权重反映不出这种实时差异,必须用最少连接数这类动态感知算法。
3. 微服务 RPC 框架默认用哪种负载均衡?
见本文档『同机房优先调用有哪些常见策略?』,实际框架常在算法之上再叠加机房、区域过滤等路由层。
【中等】同机房优先调用有哪些常见策略?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 负载均衡
💎 关键结论
同机房优先调用(Zone-Aware Routing)就是消费者优先调用与自己在同一机房/可用区的提供者实例,以降低跨机房网络延迟和带宽成本;同机房无可用实例时自动跨机房兜底,保证可用性。它是"延迟成本"与"可用性"的平衡艺术——同机房为主,跨机房兜底。
⚡记忆卡片
- 口诀:同机房为主,跨机房兜底;严格、优先、加权三级可选
- 关键词:Zone-Aware/区域元数据/路由过滤/故障兜底/ZoneAwareLoadBalance
- 链路:实例注册时携带机房元数据 → 消费者读取自身区域 → 负载均衡前按区域过滤地址 → 同机房不足自动跨机房 → 延迟与可用性兼顾
📖 核心知识
策略分级:
策略 说明 适用场景 同机房严格 只调用同机房实例,无可用实例则报错 强数据 locality(单元化封闭调用) 同机房优先 优先同机房,无可用实例则自动跨机房 通用多机房场景(推荐默认策略) 同机房加权 同机房流量权重高,容量不足按比例溢出 各机房容量不均 跨机房容错 同机房调用失败后重试跨机房 读接口/幂等接口 实现要点:
- 区域标识:注册实例元数据中记录机房/可用区属性(如 Nacos/Dubbo 的 region 属性),消费者从环境变量获取自身区域。
- 路由过滤:负载均衡前先按区域过滤地址列表。Dubbo 的 ZoneAwareLoadBalance 即实现了严格/优先/加权三级策略。
- 故障兜底:同机房健康实例数低于阈值时自动切跨机房,避免同机房过载引发雪崩。
- 配合流量调度:容灾演练时通过路由规则强制跨机房调用,实现机房级流量调度。
🔬 扩展知识
详情
【L3】
- Dubbo ZoneAwareLoadBalance 的行为细节:它会读取 Invoker URL 中的 registry 参数(对应 zone 标签),将实例分为"同区域、无区域标记、其他区域"三档,优先选同区域且可用的实例集合;配合
zone参数的availableonly(严格模式)等开关控制是否允许溢出到其他区域。
【L4】
- 同机房优先与单元化架构的关系:同机房优先解决的是"调用就近",单元化(Set 化)进一步要求"数据就近"——业务按用户维度封闭在单元内完成读写,二者结合才能实现机房级容灾时的流量整体切换。仅做同机房优先而数据仍跨机房访问,切流时延迟收益会大打折扣。
🔀 发散问题
1. 为什么默认推荐"同机房优先"而不是"同机房严格"?
严格模式在同机房实例全部不可用时直接报错,可用性差;优先模式自动跨机房兜底,只牺牲少量延迟换取可用性,适合绝大多数通用场景。严格模式只适合单元化封闭调用这类强 locality 场景。
2. 同机房优先和负载均衡算法是什么关系?
同机房优先是负载均衡之前的区域过滤层:先按机房把地址列表缩小,再在小列表上执行轮询/加权等算法。见本文档『负载均衡有哪些算法?』。
3. 容灾演练时如何验证同机房优先真的生效?
通过路由规则强制将部分流量跨机房调用,对比跨机房调用的 RT 增量与错误率,确认兜底链路可用,避免真故障时才发现跨机房链路不通。
流量控制
【简单】什么是流量控制?为什么需要流量控制?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
流量控制就是根据流量、并发、响应时间等指标,把随机到来的流量"整形"成系统能承受的形状,避免应用被瞬时高峰冲垮。分布式系统下依赖众多,任何一个依赖失败都可能级联放大成雪崩,不做流量控制等于把系统稳定性交给运气。
⚡记忆卡片
- 口诀:流量整形防冲垮,隔离依赖防雪崩
- 关键词:流量塑形/并发线程数/响应时间/依赖隔离/雪崩效应
- 链路:随机流量涌入 → 超过系统承载能力 → 按指标整形(限流/隔离) → 依赖故障被隔离 → 避免级联雪崩 → 系统高可用
📖 核心知识
什么是流量控制:流量控制(Flow Control),根据流量、并发线程数、响应时间等指标,把随机到来的流量调整成合适的形状,即流量塑形。避免应用被瞬时的流量高峰冲垮,从而保障应用的高可用性。
为什么需要:依赖失败的数学必然性:复杂的分布式系统架构中的应用程序往往具有数十个依赖项,每个依赖项都会不可避免地在某个时刻失败。如果主机应用程序未与这些外部故障隔离开来,则可能会被波及。例如,对于依赖于 30 个服务的应用程序,假设每个服务的正常运行时间为 99.99%,则可以期望:
99.9930 = 99.7% 的正常运行时间
10 亿个请求中的 0.3%= 3,000,000 个失败
即使所有依赖项都具有出色的正常运行时间,每月也会有 2 个小时以上的停机时间。
然而,现实情况一般比这种估量情况更糟糕。
雪崩效应的形成过程:当一切正常时,整体系统如下所示:

图片来自 Hystrix Wiki
在分布式系统架构下,这些强依赖的子服务稳定与否对系统的影响非常大。但是,依赖的子服务可能有很多不可控问题:如网络连接、资源繁忙、服务宕机等。例如:下图中有一个 QPS 为 50 的依赖服务 I 出现不可用,但是其他依赖服务是可用的。

图片来自 Hystrix Wiki
当流量很大的情况下,某个依赖的阻塞,会导致上游服务请求被阻塞。当这种级联故障愈演愈烈,就可能造成整个线上服务不可用的雪崩效应,如下图。这种情况若持续恶化,如果上游服务本身还被其他服务所依赖,就可能出现多米诺骨牌效应,导致多个服务都无法正常工作。

图片来自 Hystrix Wiki
🔀 发散问题
1. 流量控制具体有哪些手段?
常见手段是限流、熔断、降级三大保护机制,详见本文档『流量控制有哪些保护机制?』;此外还有隔离模式,详见本文档『流量控制有哪些隔离模式?』。
2. 流量控制的衡量指标有哪些?
包括流量指标(QPS、并发线程数)、资源调用关系(调用链路、调用来源)和控制效果(排队等待、直接拒绝、预热)三个角度,详见本文档『流量控制有哪些衡量指标?』。
【简单】流量控制有哪些衡量指标?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
设计流量控制要回答三个问题:"拿什么指标判断"(QPS、并发线程数)、"对谁控制"(资源调用链路和来源)、"控制成什么效果"(拒绝、排队、预热)。三个角度缺一不可,只定阈值不定控制效果的规则是不完整的。
⚡记忆卡片
- 口诀:指标定水位,关系定对象,效果定动作
- 关键词:QPS/并发线程数/调用链路/调用来源/排队等待/直接拒绝/Warm Up 预热
- 链路:流量/并发指标超水位 → 按资源调用关系定位控制对象 → 选择控制效果(拒绝/排队/预热) → 流量被塑形成系统可承受的形状
📖 核心知识
流量控制有以下几个角度:
- 流量指标:例如 QPS、并发线程数等。这是判断"当前是否过载"的直接水位线:QPS 反映入口流量大小,并发线程数反映系统正在同时处理的压力。
- 资源的调用关系:例如资源的调用链路,资源和资源之间的关系,调用来源等。这决定了"对谁限流"——可以按接口、按调用来源(哪个上游应用)、按关联资源(保护热点资源)等维度设置规则。
- 控制效果:例如排队等待、直接拒绝、Warm Up(预热)等。这决定了"超限后怎么办"——直接拒绝快速失败、排队等待削峰填谷、预热模式让冷系统缓慢爬升到满负载。
🔀 发散问题
1. 为什么并发线程数和 QPS 要分开控制?
QPS 高但处理快的接口未必危险,QPS 不高但处理慢的接口会积压线程把线程池耗尽。两者结合才能既防"流量型"过载又防"慢调用型"过载。
2. Warm Up 预热适合什么场景?
适合冷启动后性能需要逐步爬升的系统(如 JIT 未编译、缓存未预热),用预热模式避免冷系统被瞬时满负载流量击穿,详见本文档『有哪些限流算法?』中的算法选型。
【中等】流量控制有哪些保护机制?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
流量控制的三大保护机制是限流、熔断、降级,方向各不相同:限流保护自己不被上游压垮(入口控制),熔断保护自己不被下游拖垮(出口保护),降级是牺牲非核心功能换系统稳定。三者配合才能构成完整的容错防线。
⚡记忆卡片
- 口诀:限流防上游压垮,熔断防下游拖垮,降级弃车保帅
- 关键词:限流/熔断/降级/异常比例/慢调用/快速失败
- 链路:上游流量突增 → 限流拦截超量请求 → 下游不稳定 → 熔断快速失败隔离故障 → 整体压力仍大 → 降级关闭非核心功能 → 系统稳定存活
📖 核心知识
流量控制常见的手段就是限流、熔断、降级。
什么是降级?:降级是保障服务能够稳定运行的一种保护方式:面对突增的流量,牺牲一些吞吐量以换取系统的稳定。常见的降级实现方式有:开关降级、限流降级、熔断降级。
什么是限流?:限流一般针对下游服务,当上游流量较大时,避免被上游服务的请求撑爆。限流就是限制系统的输入和输出流量,以达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。限流规则包含三个部分:时间粒度,接口粒度,最大限流值。限流规则设置是否合理直接影响到限流是否合理有效。
什么是熔断?:熔断一般针对上游服务,当下游服务超时/异常较多时,避免被下游服务拖垮。当调用链路中某个资源出现不稳定,例如,超时异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。熔断尽最大的可能去完成所有的请求,容忍一些失败,熔断也能自动恢复。熔断的常见策略有:
- 在每秒请求异常数超过多少时触发熔断降级
- 在每秒请求异常错误率超过多少时触发熔断降级
- 在每秒请求平均耗时超过多少时触发熔断降级
🔬 扩展知识
详情
【L3】
- 三者的触发方向与组合使用:限流是"入口控制",基于 QPS/并发数限制请求进入;熔断是"出口保护",基于下游健康状态快速失败;降级是"结果动作",往往由前两者触发后执行。生产上典型组合:入口限流 → 依赖熔断 → 熔断后走降级分支(返回缓存/默认值)。
【L4】
- 系统自适应保护:除规则式限流外,Sentinel 还支持基于系统负载(Load1、CPU、RT、线程数、入口 QPS)的自适应保护,类似"系统的自动免疫系统",在规则配置不全时作为最后防线,详见本文档『Sentinel vs Hystrix vs Resilience4j 有什么区别?』。
🔀 发散问题
1. 限流和熔断的区别一句话怎么概括?
限流防止自身被压垮,看的是进入的流量;熔断防止被下游拖垮,看的是下游的健康状态。
2. 熔断具体怎么实现自动恢复?
通过 CLOSED → OPEN → HALF_OPEN 三状态机,半开状态探测成功后自动恢复,详见本文档『熔断器的工作原理是什么?有哪些状态?』。
3. 限流的阈值怎么定才合理?
限流规则包含时间粒度、接口粒度、最大限流值三部分,阈值应由压测水位与真实流量分布数据支撑,而不是拍脑袋,具体算法见本文档『有哪些限流算法?』。
【中等】流量控制有哪些隔离模式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
隔离的本质是把资源切分成互不干扰的小池子,让一个慢服务/坏业务拖不垮全局。三种主流模式:线程池隔离(隔离最彻底但开销大)、信号量隔离(轻量但不支持超时)、集群/资源组隔离(多租户防跨业务影响)。选型关键看两点:是否需要超时控制、调用频率多高。
⚡记忆卡片
- 口诀:线程池彻底但重,信号量轻但无超时,集群隔离防跨业务
- 关键词:线程池隔离/信号量隔离/资源隔离/Hystrix/Sentinel/超时控制
- 链路:慢服务占满公共资源 → 拖垮其他服务 → 按服务/接口划分独立资源池 → 故障被限制在池内 → 全局稳定
📖 核心知识
线程池隔离:
- 原理:为每个服务或接口分配独立的线程池,线程资源互不干扰。
- 优点:完全隔离——慢服务只会耗尽自己的线程池,不影响其他服务;支持超时控制——线程池可配置队列和超时策略。
- 缺点:上下文切换开销——线程数多时性能下降;资源浪费——线程池需按峰值预留,利用率低。
- 代表:Hystrix 的线程池隔离模式。
信号量隔离:
- 原理:使用计数器(信号量) 控制并发线程数(或并发请求数)。
- 实现:请求进入时申请信号量,离开时释放。
- 优点:轻量高效——无额外线程开销,适合高频内部调用;资源可控——精确控制并发数。
- 缺点:不支持超时——请求持有信号量后无法主动中断(可能长时间阻塞);不隔离执行线程——慢调用仍可能占用公共线程(如 Tomcat 线程)。
- 代表:Hystrix 的信号量模式、Sentinel 的并发控制。
资源隔离:
- 原理:在集群维度或逻辑资源组之间进行隔离。
- 常见形式:① 物理集群隔离——不同业务使用独立集群(如订单集群、用户集群);② 逻辑资源组——在同一个集群内,通过命名空间或标签划分资源组,并设置组级配额。
- 优点:防止单一业务流量打满整个集群。
- 代表:Kubernetes 命名空间配额、Sentinel 集群流控。
隔离模式对比:
模式 隔离粒度 性能开销 超时支持 适用场景 线程池隔离 服务/接口级 高(线程上下文切换) 支持 外部依赖调用(如 HTTP/RPC),需严格超时控制 信号量隔离 服务/接口级 极低(仅计数器操作) 不支持 高频内部调用(如本地缓存查询),性能敏感 集群隔离 集群/资源组级 低(管控层面) 依赖具体实现 多租户/多业务线资源共享,防跨业务影响
🔬 扩展知识
详情
【L3】
- Hystrix 两种模式的切换标准:Hystrix 官方建议对第三方网络调用(延迟不可控)用线程池隔离以获得超时能力;对高频、低延迟的本地调用(如内存缓存)用信号量隔离,否则线程切换开销会超过调用本身的耗时。
【L4】
- 隔离与虚拟化的对应关系:信号量隔离类似进程内的"软配额",线程池隔离类似独立进程的"硬隔离",集群隔离类似容器/虚拟机的"物理隔离"。隔离强度越高,资源开销和管理成本越高,选型本质是在故障爆炸半径与资源效率之间做权衡。
🔀 发散问题
1. 为什么信号量隔离不支持超时?
信号量只是计数器,请求仍在调用方线程中同步执行,框架无法中断它;线程池隔离因为任务在独立线程执行,调用方可以通过 Future 超时机制主动放弃等待。
2. Sentinel 默认用信号量(并发线程数)控制,够用吗?
对大多数 HTTP/RPC 入口场景够用,因为 Tomcat/Netty 自身已有线程池与连接超时兜底;只有当需要把慢依赖的等待时间严格切断时,才需要引入线程池式隔离(如 Resilience4j 的 Bulkhead 固定线程池模式)。
【中等】有哪些限流算法?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:20 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
四大限流算法各有取舍:固定窗口最简单但有临界问题,滑动窗口解决临界但占内存,漏桶恒定出流保护最彻底但不能突发,令牌桶允许突发、资源利用率最高。选型一句话:下游能力绝对固定选漏桶,自身有弹性余量选令牌桶,粗粒度统计用窗口计数。
⚡记忆卡片
- 口诀:窗口数数,漏桶匀速,令牌可突发
- 关键词:固定窗口/滑动窗口/临界问题/漏桶/令牌桶/突发容量
- 链路:请求到达 → 窗口计数判是否超阈 → 或漏桶恒定出流削峰 → 或令牌桶取令牌(桶容量内允许突发) → 超限拒绝/排队 → 下游得到保护
📖 核心知识
常见的限流算法有:固定窗口计数算法、滑动窗口计数算法、漏桶限流算法、令牌桶限流算法。
固定窗口计数算法:
基本策略:① 设置一个固定时间窗口,以及这个固定时间窗口内的最大请求数;② 为每个固定时间窗口设置一个计数器,用于统计请求数;③ 一旦请求数超过最大请求数,则请求会被拦截。

利弊:优点是实现简单。缺点是存在临界问题。所谓临界问题,是指:流量分别集中在一个固定时间窗口的尾部和一个固定时间窗口的头部。举例来说,假设限流规则为每分钟不超过 100 次请求。在第一个时间窗口中,起初没有任何请求,在最后 1s,收到 100 次请求,由于没有达到阈值,所有请求都通过;在第二个时间窗口中,第 1 秒就收到 100 次请求,而后续没有任何请求。虽然,这两个时间窗口内的流量都符合限流要求,但是在两个时间窗口临界的这 2s 内,实际上有 200 次请求,显然是超过预期吞吐量的,存在压垮系统的可能。

滑动窗口计数算法:
滑动窗口计数算法是对固定窗口计数算法的改进,解决了临界问题。基本策略:将固定时间窗口分片为多个子窗口,每个子窗口的访问次数独立统计;当请求时间大于当前子窗口的最大时间时,则将当前子窗口废弃,并将计时窗口向前滑动,并将下一个子窗口置为当前窗口;要保证所有子窗口的统计数之和不能超过阈值。滑动窗口计数算法就是针对固定窗口计数算法的更细粒度的控制,分片越多,则限流越精准。

利弊:优点是在滑动窗口计数算法中,临界位置的突发请求都会被算到时间窗口内,因此可以解决计数器算法的临界问题。缺点是:① 额外的内存开销——滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,所以存在额外的内存开销;② 限流的控制粒度受限于窗口分片粒度——滑动窗口计数算法只能在选定的时间粒度上限流,对选定时间粒度内的更加细粒度的访问频率不做限制。但是,由于每个分片窗口都有额外的内存开销,所以也并不是分片数越多越好的。
漏桶算法:
基本策略:水(请求)以任意速率由入口进入到漏桶中;水以固定的速率由出口出水(请求通过);漏桶的容量是固定的,如果水的流入速率大于流出速率,最终会导致漏桶中的水溢出(这意味着请求拒绝)。

利弊:优点是流量速率固定——即无论流量多大,即便是突发的大流量,处理请求的速度始终是固定的。缺点是不能灵活的调整流量。例如:一个集群通过增减节点的方式,弹性伸缩了其吞吐能力,漏桶限流算法无法随之调整。漏桶策略适用于间隔性突发流量且流量不用即时处理的场景。
令牌桶算法:

原理:① 接口限制 T 秒内最大访问次数为 N,则每隔 T/N 秒会放一个 token 到桶中;② 桶内最多存放 M 个 token,如果 token 到达时令牌桶已经满了,那么这个 token 就会被丢弃;③ 接口请求会先从令牌桶中取 token,拿到 token 则处理接口请求,拿不到 token 则进行限流处理。利弊:因为令牌桶存放了很多令牌,那么大量的突发请求会被执行,但是它不会出现临界问题,在令牌用完之后,令牌是以一个恒定的速率添加到令牌桶中的,因此不能再次发送大量突发请求。规定固定容量的桶,token 以固定速度往桶内填充,当桶满时 token 不会被继续放入,每过来一个请求把 token 从桶中移除,如果桶中没有 token 不能请求。令牌桶算法适用于有突发特性的流量,且流量需要即时处理的场景。
算法对比与选型:
算法 允许突发 平滑输出 实现复杂度 典型实现 适用场景 固定窗口 临界处允许 2 倍突发 否 最低 简单计数器 粗粒度统计、告警计数 滑动窗口 否 否 中 Sentinel 默认统计 接口调用量限流 漏桶 否 是(恒定速率) 中 Nginx limit_req 下游处理能力绝对固定的场景 令牌桶 是(桶容量内) 基本平滑 中 Guava RateLimiter、Sentinel 预热模式 有突发特性且需即时处理的流量 漏桶 vs 令牌桶:漏桶强制恒定出水速率,对下游保护最彻底,但无法利用系统空闲能力消化突发;令牌桶允许用积攒的令牌应对突发,资源利用率更高,但突发上限由桶容量决定。当下游处理能力绝对固定(如磁盘写入、第三方协议 QPS)选漏桶;当系统自身有弹性余量选令牌桶。
案例:用户维度限流导致正常用户"无法下单"(真实生产事故)
一次真实生产事故:为防爬虫,我们在网关加了"每用户每分钟 100 次请求"的令牌桶限流,突发容量设为 100。上线后客诉激增"无法下单",但爬虫流量并未减少。排查发现,正常用户的下单链路一次页面加载就会触发 5~8 个后端接口调用(购物车、库存、优惠券、下单),加上 App 每 30s 自动刷新,高峰期正常用户每分钟请求量可达 60~80 次;突发容量 100 导致正常用户首页一打开就把令牌瞬间耗尽,后续请求全部被拦。根因:限流维度选了"用户",但没人统计过真实用户的请求分布,阈值纯靠拍脑袋。修复:① 拉取 APM 监控的 P99 数据,阈值调到 300 次/分钟、突发降到 30;② 限流下沉到"下单"这一个关键接口,只读接口放宽;③ 为每条限流规则增加命中率监控,命中率超过 5% 自动告警。教训:限流阈值必须由流量分布数据支撑,而不是直觉。
案例:第三方硬性限速接口的限流设计(推演)
场景推演:你的服务对接第三方支付接口,对方协议只允许 1000 QPS,超限会触发风控,封禁出口 IP 10 分钟(期间所有请求全部失败)。限流方案如何设计?
- 应急处理:若已被封禁,立即切换到备用出口 IP 池或备用通道,请求在本地队列暂存,解封后限速补发,避免积压流量二次触发风控。
- 根因分析:该场景的特点是"超限代价极大且不连续"(一旦触发就是 10 分钟整体不可用),因此限流必须保守——不能用允许突发的令牌桶,突发流量可能瞬间击穿 1000 QPS。
- 长期方案:① 采用漏桶恒定出流,速率设为 900 QPS(协议值的 90%,预留时钟误差与并发竞态的余量);② 多实例共同调用时必须做集群限流(集中式 Redis 发令牌或统一出口代理),单机分摊在弹性伸缩下不可靠;③ 增加对端反馈熔断:第三方返回 429 或超时率超过 1% 时,主动将速率再降 50%。
- 权衡:漏桶恒定出流意味着流量突增时请求排队等待,必须设置队列上限与等待超时(如等待超过 3s 直接返回"系统繁忙请重试"),避免无界排队拖垮自身内存。
🔬 扩展知识
详情
【L3】
- 主流实现的量化参数:Sentinel 默认滑动窗口统计,1s 切分为 2 个窗口(LeapArray);单机限流判断开销为微秒级,对接口 RT 可忽略。Guava RateLimiter 的
SmoothWarmingUp默认预热时间 10s,冷启动因子为 3,即预热期初放行速率约为阈值的 1/3,逐步爬升到满速。Nginx limit_req 的burst默认是 0,必须显式配置;常见配置形如rate=100r/s burst=200 nodelay,burst 内请求立即处理,超出部分排队或拒绝。令牌桶突发容量经验值:突发容量一般设为平均 QPS 阈值的 1~2 倍,例如阈值 500 QPS,突发 500~1000,既能吸收秒级突刺,又不至于击穿下游。 - Guava RateLimiter vs Sentinel:Guava RateLimiter 纯单机、内存实现,无动态配置,阈值只能硬编码或启动时读配置;支持平滑预热(
SmoothWarmingUp)和预消费(tryAcquire带超时),适合单机工具类、后台任务的简单防护。Sentinel 支持单机 + 集群限流,规则可由配置中心动态推送,带监控大盘,还能联动熔断与系统自适应保护(基于 Load/RT 自动降流量),适合生产级微服务,代价是 SDK 依赖与大盘运维成本。
【L4】
- 新接口没有历史流量数据时如何定阈值(推演方法):三步走——① 压测定水位:测出单实例 RT 拐点处的 QPS,阈值取拐点的 80%;② 观察期宽松限流:先放大 2~3 倍上线,观察一周的限流命中率与下游水位;③ 按数据收敛:以峰值 P99 流量 × 1.5 安全系数作为最终值。核心监控指标:限流命中率正常应接近 0,持续大于 1% 说明阈值偏低或容量不足。
📚 延伸阅读:Guava 的 RateLimiter 工具类就是基于令牌桶算法实现,其源码分析可以参考:RateLimiter 基于漏桶算法,但它参考了令牌桶算法
⚠️ 常见误区
详情
常见误区:
- ❌ "固定窗口计数器统计是严格精确的" → 不对。存在临界问题:跨窗口的 2s 内可放行 2 倍阈值流量,攻击者可以刻意把请求压到窗口边界,需要滑动窗口解决。
- ❌ "令牌桶的突发容量设得越大越保险" → 不对。突发容量设为阈值的 10 倍时,瞬时 10 倍流量全部放行,限流形同虚设,下游直接被击穿;经验值是平均阈值的 1~2 倍。
- ❌ "限流判断走 Redis 很安全,不用做兜底" → 不对。限流逻辑走 Redis 时,Redis 延迟抖动会让所有请求同步阻塞在限流判断上,限流组件反而成为故障源,必须设置超时与降级。
- ❌ "集群总阈值除以实例数写死在配置里就行" → 不对。缩容时总阈值不变而实例数减少,单实例实际流量 = 总阈值 ÷ 实例数,可能直接压垮剩余实例,必须订阅实例数变化动态重算。
🔀 发散问题
1. Sentinel 为什么用滑动窗口而不是令牌桶作为默认统计方式?
滑动窗口是"统计手段",能同时算出窗口内的 QPS、RT、异常比例,天然支撑限流、熔断、系统保护多种规则;令牌桶只服务于限速这一个场景。Sentinel 的架构是统计层(LeapArray 滑动窗口)与控制策略层(直接拒绝/匀速排队/预热)分离,令牌桶语义对应的是"匀速排队"效果。
2. 令牌桶放到分布式环境下直接实现,会有哪些坑?
多实例各自维护本地令牌桶时,总限流量 = 单机阈值 × 实例数,且随弹性伸缩漂移,无法控制全局总量;若改为集中式 Redis 发令牌,每次判断多一次网络往返,Redis 故障则限流整体失效。所以分布式令牌桶通常做成"本地桶 + 周期性中心配额分配"(Sentinel 集群模式的思路),用精度换可用性。
3. 集群级别的限流到底怎么落地?
见本文档『如何实现集群限流?限流方案实战如何选型?』,核心是用"精度"换"性能与可用性",小规模用集中式 Redis,大规模用嵌入式配额分配。
【困难】如何实现集群限流?限流方案实战如何选型?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:20 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
集群限流的本质是用"精度"换"性能与可用性":单机限流控不住全局总量(总 QPS = 实例数 × 单实例阈值,实例数还随弹性伸缩漂移),小规模用集中式 Redis + Lua 精确控制,大规模用嵌入式配额分配换性能,且无论哪种方案都必须备好降级兜底。
⚡记忆卡片
- 口诀:小规模集中式 Redis,大规模嵌入式配额,故障降级单机限流
- 关键词:Token Server/Redis + Lua/配额分配/Sentinel Cluster/网关限流/降级兜底
- 链路:实例数动态变化 → 单机阈值控不住全局总量 → 集中式统一计数或嵌入式批量分配配额 → 本地/远程判断放行与否 → 限流组件故障 → 降级单机限流而非放行
📖 核心知识
单机限流(如 Guava RateLimiter)只能限制单个实例的流量。集群场景下,总 QPS = 实例数 × 单实例阈值,而实例数随弹性伸缩动态变化,要控制全局总量,必须实现集群限流。
集中式架构(独立令牌服务):所有实例向统一的令牌服务(Token Server / Redis)申请令牌,由服务端维护全局计数。
- 优点:全局精确控制,实现简单(如 Redis + Lua 实现滑动窗口)。
- 缺点:令牌服务成为单点和性能瓶颈,每次限流判断都有网络开销。需高可用部署,故障时必须降级为单机限流,而非放行全部流量。
嵌入式架构(分配协调):令牌服务周期性地将令牌配额批量分配给各实例,实例本地消费配额,用尽后再申请(Sentinel Cluster 模式的 Token Server / Token Client 即此类设计)。
- 优点:限流判断本地完成,性能好,无中心瓶颈。
- 缺点:精度略差(配额分配有滞后),实现复杂。
Redis + Lua 限流要点:用 Lua 脚本保证"读取-判断-修改"的原子性(INCR + EXPIRE 或 ZSet 滑动窗口);Redis 成为热点时可按业务维度分片;Redis 故障时降级为本地限流,绝不能因限流组件故障导致全站不可用。
实战选型:
场景 推荐方案 简单接口限流 Redis + Lua 滑动窗口 复杂规则 + 动态配置 Sentinel 集群模式 / 网关限流(Kong、APISIX) 超高 QPS 本地限流 + 全局配额分配(嵌入式) 精度要求低 单机限流静态分摊(总阈值 ÷ 实例数,配合弹性调整) 方案 精度 性能开销 动态规则 可用性风险 适用场景 Guava RateLimiter(单机分摊) 低(随弹性伸缩漂移) 无 否 无 实例少、规模稳定 Redis + Lua 高(全局精确) 每次判断 1 次 Redis 调用(约 0.3~0.5ms) 阈值可热更 强依赖 Redis 高可用 中小 QPS(< 5 万)、简单规则 Sentinel 集群模式 中(配额分配有滞后) 本地判断,微秒级 是(控制台 + 配置中心) Token Server 故障期间精度降级 大 QPS、复杂规则、需监控大盘 网关限流(APISIX/Kong) 高 网关承担,业务无感知 是 网关自身需高可用 南北向流量统一入口 选型决策路径:先看流量入口(南北向流量优先网关,东西向流量优先 SDK);再看 QPS 量级(万级以下用 Redis + Lua 简单精确,十万级以上用嵌入式配额分配);最后看运维诉求(需要大盘、动态规则、灰度联动就选 Sentinel)。
🔬 扩展知识
详情
【L3】
- 关键量化数据:Redis 开销——同机房下一次 Lua 限流脚本判断约 0.3~0.5ms,单个 Redis 实例可承载约 8~10 万次/秒简单 Lua 判断,用 pipeline 批量判断吞吐更高。Sentinel 集群模式——单 Token Server 可承载数十万 QPS 的配额判断;配额分配周期一般 1s,即精度误差上限为 1s 的配额量。单机分摊误差——总阈值 ÷ 实例数的方式,若实例数来自缓存(通常有 10~30s 延迟),弹性伸缩期间实际总限流量可漂移 ±30%。
- Redis + Lua 两种实现的坑:INCR + EXPIRE 是固定窗口,存在临界问题(2s 内可过 2 倍阈值),且若 INCR 后进程崩溃未执行 EXPIRE,会留下永不过期的脏 Key;ZSet 滑动窗口精确,但每次请求需要 ZADD + ZREMRANGEBY + ZCARD 多个操作,内存与 CPU 开销是计数器的数倍,高 QPS 下必须按 Key 分片。工程上常用少分片的滑动窗口或 Lua 版令牌桶折中。
【L4】
- 自适应限流方向:静态阈值在大促等流量形态剧变场景下必然失准,业界更高级的做法是"系统自适应保护"——基于 Load/RT/CPU 等系统水位反馈自动调节放行速率,作为阈值规则的最后防线;Sentinel 的系统规则即属此类。
📚 延伸阅读:Sentinel 官方仓库(含集群限流文档)
🏭 实战场景
详情
一次真实生产事故(大促场景):交易集群用 Redis + Lua 做集群限流保护库存服务。大促零点流量突增 8 倍,限流 Lua 脚本全部打到单个 Redis 分片,该分片 CPU 打满到 100%,限流判断开始超时;而代码里的兜底逻辑写的是"Redis 异常则默认放行"——结果限流完全失效,库存服务被打挂,交易成功率跌到 60%。根因:① 限流 Key 集中在单一 Redis 分片没有打散;② 降级策略方向错误,异常时应退化为单机限流而不是放行。修复:① 限流 Key 按资源名哈希分散到 8 个分片;② 降级策略改为单机阈值兜底,并纳入混沌演练定期验证;③ 核心链路限流迁移为嵌入式本地判断,Redis 只做配额同步。教训:限流组件的故障降级策略,比限流算法本身更重要。
另一类典型场景(大促零点推演):流量突增 10 倍时,网关限流阈值必须是动态的——按日常峰值定的阈值会把正常用户拦掉,按大促预估定的在平时又形同虚设。应对:大促预案阈值提前推送到配置中心按时生效,活动结束后自动回落;分层限流(网关按"用户 ID × 接口"防刷,业务层按全局容量);令牌桶突发容量设为日常峰值的 2 倍吸收零点跳变;系统自适应保护作最后防线;限流响应返回友好提示而非裸 5xx。权衡:动态阈值依赖配置推送通道可靠性,配置中心故障时应冻结在最后一次已知值(fail-safe),绝不能回落到默认值。
⚠️ 常见误区
详情
常见误区:
- ❌ "Redis 故障时限流直接放行,总比拒绝服务好" → 严重错误。放行等于限流完全失效,下游会被击穿;正确降级是退化为单机限流(全局阈值 ÷ 当前实例数),实例数可从注册中心或 K8s API 实时获取;也不能全部拒绝(业务整体不可用)。
- ❌ "Sentinel 的 Token Server 挂了集群限流就废了" → 不对。Token Server 挂掉后,Token Client 会自动回退为本地单机限流,期间全局精度丢失但可用性不受损;生产上建议 Token Server 主备部署并开启自动 failover。
- ❌ "限流判断同步等 Redis 返回,慢点也没关系" → 不对。Redis RT 从 0.5ms 抖到 50ms 时,所有业务请求 RT 都会被拖慢;必须给限流判断设置独立超时(如 5ms),超时即走本地兜底判断。
🔀 发散问题
1. Sentinel 嵌入式集群限流为什么用批量配额分配,而不是每次请求都找 Token Server 判断?
每次请求远程判断会让 Token Server 成为全局热点,限流自身先成为瓶颈(网络往返约 0.5ms,吞吐上限只有十万级)。批量分配(一次领 1000 个令牌,本地消费完再申请)把判断下沉到本地,只在领配额时与 Server 通信,用秒级精度误差换数量级的性能提升。
2. 集群弹性扩缩容时,如何保证全局限流总量不漂移?
两条路:① 用集中式限流(Redis + Lua),全局总量天然精确,与实例数无关;② 单机分摊模式必须订阅实例数变化(注册中心或 K8s watch),实例数变更时重算单机阈值 = 总阈值 ÷ 实例数,并乘以 0.9 的保险系数。最差的做法是把"总阈值 ÷ 预估实例数"写死在配置里。
3. 限流触发后快速失败和排队等待怎么选?
对实时接口用直接拒绝快速失败,对可等待的异步/批量任务用匀速排队(漏桶语义)削峰填谷,这正是本文档『有哪些限流算法?』中控制效果与算法的对应关系。
【中等】熔断器的工作原理是什么?有哪些状态?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
熔断器借鉴了电路中"保险丝"的思想:当下游故障率达到阈值时自动"断开"调用链路快速失败,防止故障扩散;通过 CLOSED → OPEN → HALF_OPEN 三状态机实现"熔断—探测—自愈"的闭环,既能快速止血又能自动恢复。
⚡记忆卡片
- 口诀:故障多了跳闸,隔段时间试探,探好就合闸
- 关键词:CLOSED/OPEN/HALF_OPEN/失败率阈值/慢调用比例/半开探测
- 链路:统计失败率/慢调用比例 → 超阈值且达最小请求量 → 跳闸 OPEN 快速失败 → 等待熔断时长 → HALF_OPEN 放行少量探测 → 成功回 CLOSED、失败回 OPEN
📖 核心知识
熔断器(Circuit Breaker)是服务容错的核心组件,借鉴了电路中"保险丝"的思想:当下游服务故障率达到阈值时,自动"断开"调用链路,快速失败,防止故障扩散。
熔断器的三状态机:熔断器通常包含三个状态,形成一个状态机:
故障率/慢调用率达阈值 ┌─────────────────────┐ ▼ │ [CLOSED] ──────────► [OPEN] ▲ │ │ 半开探测成功 │ 等待熔断恢复时间 │ ▼ └──────────────── [HALF_OPEN] │ │ 半开探测失败 ▼ [OPEN]- CLOSED(关闭):正常状态,所有请求放行。熔断器持续统计请求的成功率、失败率、响应时间。当故障率(或慢调用比例)超过阈值(如 50%)且达到最小请求量时,切换到 OPEN。
- OPEN(打开):熔断状态,所有请求快速失败,不再调用下游服务,直接返回降级响应。等待"熔断恢复时间"(如 5s)后,切换到 HALF_OPEN 进行探测。
- HALF_OPEN(半开):探测状态,放行少量请求(如 1 个)到下游服务:探测成功 → 切换回 CLOSED,恢复正常调用;探测失败 → 切换回 OPEN,继续等待恢复时间。
关键参数:
参数 说明 示例值 失败率阈值 触发熔断的最小失败率 50% 慢调用比例阈值 触发熔断的最小慢调用比例 50% 慢调用阈值 响应时间超过此值视为慢调用 100ms 最小请求量 达到此请求数后才统计失败率(避免误判) 5 熔断时长 OPEN 状态持续时间 5s 半开探测请求数 HALF_OPEN 状态放行的请求数 1 ~ 10 熔断策略:
- 异常比例熔断:当异常请求数 / 总请求数 > 阈值时触发。
- 异常数熔断:当异常请求数 > 阈值时触发(适合低 QPS 场景)。
- 慢调用比例熔断:当响应时间 > 阈值的请求数 / 总请求数 > 阈值时触发(防止慢调用拖垮线程池)。
与限流的区别:限流是"入口控制",基于 QPS/并发数限制请求进入;熔断是"出口保护",基于下游健康状态快速失败。限流防止自身被压垮,熔断防止被下游拖垮。
🔬 扩展知识
详情
【L3】
- 最小请求量的必要性:若请求量极少(如 1 个请求失败),失败率 100% 但纯属偶然,直接熔断是误判;因此需要"最小请求量"门槛(如 Sentinel 的 minRequestAmount),统计样本足够后才计算比例。
- 慢调用熔断的独特价值:异常比例熔断只盯报错,但下游"活着但变慢"同样致命——慢调用会长期占用线程导致线程池耗尽,慢调用比例熔断能在下游还没报错时就提前止血。
【L4】
- 熔断与超时、重试的协同:熔断器依赖超时机制才能及时拿到失败信号(无超时则慢调用挂死线程,统计失真);而熔断打开期间的请求不应重试(重试只会打到已熔断的资源),合理组合是:超时定生死、重试应对瞬时抖动、熔断隔离持续故障。
🔀 发散问题
1. 为什么半开状态只放行少量请求而不是全部?
如果下游实际还没恢复,全量放行会让大量请求再次失败,既浪费资源又拉长了故障时间;少量探测是用最小代价验证下游是否真的恢复,失败了也只是损失几个探测请求。
2. 熔断触发后返回什么?
应返回降级响应(缓存数据、默认值、友好提示)而非裸异常,与本文档『流量控制有哪些保护机制?』中的降级概念衔接。
3. 超时时间应该怎么配才能和熔断配合好?
见本文档『超时和重试策略如何设计?』,核心原则是下游超时 < 上游超时,链路超时逐层收敛。
【中等】超时和重试策略如何设计?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
超时是容错第一道防线,重试应对瞬时故障,但两者都是双刃剑:超时配不好会让慢调用挂死线程,重试配不好会把流量放大成风暴。记住两条铁律:下游超时 < 上游超时逐层收敛;只对瞬时故障重试、指数退避 + 抖动、写操作必须幂等。
⚡记忆卡片
- 口诀:超时下游小于上游,重试指数退避加抖动,写操作必幂等
- 关键词:连接超时/读取超时/重试预算/指数退避/抖动 Jitter/幂等
- 链路:下游变慢 → 超时及时释放线程 → 瞬时故障 → 重试 + 指数退避 + 抖动错开重试洪峰 → 重试预算限制放大 → 写操作靠幂等去重兜底
📖 核心知识
超时策略:超时是服务容错的第一道防线,防止因下游慢调用导致线程长时间阻塞。
超时层次:
层次 说明 典型配置 连接超时 建立 TCP 连接的超时 200ms ~ 1s 读取超时 等待响应数据的超时 500ms ~ 3s 请求超时 整个请求的总超时(含连接 + 读取) 1s ~ 5s 超时设置的黄金法则:① 下游超时 < 上游超时——上游的超时时间应略大于下游,避免上游已超时但下游仍在执行(浪费资源),例如服务 A 调用服务 B,B 的超时设为 1s,A 的超时设为 1.2s;② 链路超时收敛——整条调用链的超时应从最下游开始向上设置,每一层留出缓冲;③ 区分读写——读操作可设置较短超时(如 500ms),写操作适当放宽(如 3s)。
重试策略:重试用于应对瞬时故障(如网络抖动),但需谨慎设计,避免放大流量。重试的关键设计:
- 重试条件:网络异常(连接超时、连接重置等)可重试;服务端临时错误(503、502 等)可重试;业务错误不重试(400、401、403、404 等业务语义错误不应重试);写操作慎重重试(可能导致重复写入,需配合幂等性)。
- 重试次数:通常 1~3 次,不宜过多。
- 退避策略(Backoff):避免重试风暴,逐步增加重试间隔——固定间隔(每次重试间隔固定,如 1s);线性退避(间隔线性增长:1s、2s、3s);指数退避(间隔指数增长:1s、2s、4s、8s,最常用);指数退避 + 抖动(Jitter,在指数退避基础上加随机抖动,避免重试同步化)。
- 重试预算(Retry Budget):限制重试比例,防止重试放大效应,例如限制重试请求不超过总请求的 20%。
示例:指数退避 + 抖动的 Java 实现
// 指数退避 + 抖动示例
long baseDelay = 1000; // 1s
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
return doRequest();
} catch (RetryableException e) {
long delay = (long) (baseDelay * Math.pow(2, i));
long jitter = (long) (Math.random() * delay * 0.2); // 20% 抖动
Thread.sleep(delay + jitter);
}
}重试的副作用与防范:
- 重试放大:链路多层重试会导致流量指数放大(A→B→C 各重试 3 次,总请求达 27 倍)。需在链路层级联设置,避免每层都重试。
- 雪崩:大量重试集中冲击下游,加剧故障。需配合熔断和限流。
- 幂等性:写操作重试必须保证下游接口幂等,可通过唯一请求 ID(requestId)去重。
🔬 扩展知识
详情
【L3】
- 抖动(Jitter)为什么必要:如果所有客户端都用固定的指数退避,大批请求会在同一时刻同步重试,形成周期性重试洪峰;加入随机抖动后重试时刻被打散,平滑了下游压力。
- 重试预算的价值:按次数限制重试在低流量时没问题,但高流量时"每个失败请求重试 3 次"可能把下游流量放大数倍;重试预算把重试量约束为总流量的固定比例(如 20%),故障越严重自动重试越少,天然防雪崩。
【L4】
- 超时与熔断、降级组成的容错链:超时负责"及时拿到失败信号",熔断负责"持续故障时不再尝试",降级负责"失败后给用户什么"。三者缺一都会漏:没超时则慢调用挂死线程,没熔断则持续冲击故障下游,没降级则用户看到裸异常。
🔀 发散问题
1. 为什么"上游超时略大于下游超时"而不是相等?
相等时网络波动会让上下游几乎同时超时,上游超时后下游可能仍在执行,造成资源浪费甚至结果不一致;留出 20% 左右的缓冲,让下游先超时返回失败信号,上游才能拿到明确的错误做后续处理。
2. 读接口和写接口的重试策略为什么要区别对待?
读接口天然幂等,重试安全,可以激进些(2~3 次 + 退避);写接口重试可能重复写入,必须先确认幂等(唯一 requestId 去重)才能重试,否则宁可失败上报由业务层决策。
3. 慢调用拖垮线程池的问题还有什么防护手段?
除超时外,可以用熔断器的慢调用比例熔断提前隔离,见本文档『熔断器的工作原理是什么?有哪些状态?』。
【中等】Sentinel vs Hystrix vs Resilience4j 有什么区别?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式调度 / 流量控制
💎 关键结论
三大容错组件的核心差异在于限流能力与维护状态:Hystrix 已停止维护且无独立限流能力;Sentinel 功能最全(限流 + 熔断 + 降级 + 系统自适应保护),是国内微服务首选;Resilience4j 轻量函数式,对响应式架构最友好。新项目 Java 生态首选 Sentinel,响应式选 Resilience4j,历史项目应迁离 Hystrix。
⚡记忆卡片
- 口诀:Sentinel 全,Hystrix 停,Resilience4j 轻而函数式
- 关键词:Sentinel/Hystrix/Resilience4j/限流/系统自适应保护/线程池隔离/装饰器模式
- 链路:选型先看维护状态(Hystrix 已停) → 再看限流需求(Sentinel 最强) → 再看架构风格(响应式选 Resilience4j) → 最后看生态(Spring Cloud 推荐 Resilience4j 替代 Hystrix)
📖 核心知识
Sentinel、Hystrix、Resilience4j 是 Java 生态三大主流容错组件,它们在功能、架构和易用性上有显著差异。
核心对比:
维度 Hystrix Sentinel Resilience4j 维护状态 停止维护(Netflix 官方停止) 活跃维护(阿里巴巴) 活跃维护 核心功能 熔断、降级、隔离 限流、熔断、降级、系统自适应保护 熔断、限流、重试、Bulkhead、缓存 限流能力 无(需配合其他组件) 强大(QPS/并发数/关联/链路限流) 基础(RateLimiter 模块) 熔断策略 异常比例 异常比例、异常数、慢调用比例 异常比例、慢调用比例 隔离方式 线程池/信号量 信号量(默认) Bulkhead(信号量/固定线程池) 流量塑形 无 支持(预热、匀速排队) 不支持 系统保护 无 支持(Load1/CPU/RT/线程数自适应) 不支持 实时监控 Dashboard(已停止维护) Sentinel Dashboard(功能强大) Micrometer + Prometheus + Grafana 规则配置 代码/配置文件 代码/控制台/动态数据源(Nacos等) 代码/配置文件 编程模型 注解 + 编程式 注解 + 编程式 + SphU 函数式(装饰器模式) 响应式支持 弱 支持(Reactor/WebFlux) 原生支持(Reactor/ RxJava) 云原生 弱 Sentinel Cluster(集群限流) 与 Spring Boot 2.x 深度集成 选型建议:
- 新项目 + Java 生态 → Sentinel:功能最全,限流能力强大,控制台好用,社区活跃,阿里巴巴背书。
- 响应式/函数式架构 → Resilience4j:原生支持 Reactor,装饰器模式优雅,轻量无依赖。
- 历史项目 → 迁移离开 Hystrix(已停止维护),优先迁移到 Sentinel 或 Resilience4j。
- Spring Cloud 生态 → Spring Cloud 官方推荐 Resilience4j 作为 Hystrix 的替代。
Hystrix 为什么被淘汰?:
- 停止维护:Netflix 于 2018 年宣布 Hystrix 进入维护模式,不再添加新功能。
- 功能局限:缺少限流、系统自适应保护等关键能力。
- 线程池开销大:默认线程池隔离模式上下文切换开销高。
- 监控体系陈旧:Dashboard 基于 Servlet,不支持响应式。
- 配置不灵活:规则不支持动态数据源,需重启生效。
Sentinel 的系统自适应保护:Sentinel 独有的"系统自适应保护"能根据系统负载(CPU、Load、RT、线程数、入口 QPS)自动限流,无需预定义规则,类似"系统的自动免疫系统":
- Load1 触发:当系统 1 分钟平均负载超过阈值时触发。
- CPU 使用率触发:当 CPU 使用率超过阈值时触发。
- 平均 RT 触发:当所有入口流量的平均 RT 超过阈值时触发。
- 并发线程数触发:当所有入口流量的并发线程数超过阈值时触发。
- 入口 QPS 触发:当所有入口流量的 QPS 超过阈值时触发。
🔬 扩展知识
详情
【L3】
- 隔离方式的本质差异:Hystrix 默认线程池隔离,隔离最彻底但上下文切换开销大;Sentinel 默认信号量隔离(并发线程数控制),轻量但依赖调用方线程;Resilience4j 的 Bulkhead 同时提供信号量和固定线程池两种模式,可按依赖类型灵活选择。选型时对外部网络依赖(延迟不可控)优先线程池,对高频本地调用优先信号量。
【L4】
- 从"容错组件"到"流量治理平台"的演进:Hystrix 时代容错规则靠代码硬编码;Sentinel 把规则抽离为可由控制台/配置中心动态下发的数据源,并与监控大盘、集群限流打通,反映的是"容错从开发关注点变为运维治理关注点"的趋势。
📚 延伸阅读:Sentinel 官方文档、Resilience4j 官方文档。
🔀 发散问题
1. Sentinel 和 Hystrix 都有熔断,核心差异在哪?
Hystrix 只有异常比例熔断且默认配线程池隔离;Sentinel 支持异常比例、异常数、慢调用比例三种策略,且不强制线程池开销,还能与限流共用同一套滑动窗口统计。
2. 为什么 Spring Cloud 官方推荐 Resilience4j 而不是 Sentinel?
Spring Cloud 官方维护的是与 Netflix/Reactor 生态的兼容性,Resilience4j 对 Reactor 的原生支持和轻量特性更契合;Sentinel 则在阿里巴巴主导的 Spring Cloud Alibaba 生态中是默认选择,两者生态归属不同。
3. 系统自适应保护和普通限流规则什么关系?
普通限流是针对具体资源的规则式控制,需要预先配好阈值;系统自适应保护是全局兜底,基于 Load/CPU 等系统水位自动降速,在规则配置不全或突发未知流量时作为最后防线,见本文档『流量控制有哪些保护机制?』。
网关路由
【简单】什么是服务路由?路由有什么用?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 服务路由
💎 关键结论
服务路由就是通过一定规则从集群中选择合适的节点,本质是实现流量隔离。它和负载均衡看似相近,但负载均衡算法做不了精细化路由管理。路由的典型价值在于支撑分组调用、蓝绿/灰度发布、流量切换等发布与容灾场景。
⚡记忆卡片
- 口诀:按规则选节点,本质是流量隔离
- 关键词:服务路由/流量隔离/分组调用/蓝绿发布/灰度发布/流量切换/读写分离
- 链路:定义路由规则 → 按规则从集群筛选节点 → 流量被导向指定分组 → 实现发布隔离/容灾切换/读写分离
📖 核心知识
服务路由是指通过一定的规则从集群中选择合适的节点。
负载均衡的作用和服务路由的功能看上去很近似,二者有什么区别呢?负载均衡的目标是提供服务分发而不是解决路由问题,常见的静态、动态负载均衡算法无法实现精细化的路由管理,但是负载均衡也可以简单看做是路由方案的一种。
服务路由通常用于以下场景,目的在于实现流量隔离:
- 分组调用:一般来讲,为了保证服务的高可用性,实现异地多活的需求,一个服务往往不止部署在一个数据中心,而且出于节省成本等考虑,有些业务可能不仅在私有机房部署,还会采用公有云部署,甚至采用多家公有云部署。服务节点也会按照不同的数据中心分成不同的分组,这时对于服务消费者来说,选择哪一个分组调用,就必须有相应的路由规则。
- 蓝绿发布:蓝绿发布场景中,一共有两套服务群组:一套是提供旧版功能的服务群组,标记为绿色;另一套是提供新版功能的服务群组,标记为蓝色。两套服务群组都是功能完善的,并且正在运行的系统,只是服务版本和访问流量不同。新版群组(蓝色)通常是为了做内部测试、验收,不对外部用户暴露。如果新版群组(蓝色)运行稳定,并测试、验收通过后,则通过服务路由、负载均衡等手段逐步将外部用户流量导入新版群组(蓝色);如果运行不稳定,或测试、验收不通过,则排查、解决问题后,再继续测试、验收。
- 灰度发布:灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行 A/B 测试,即让一部分用户使用特性 A,一部分用户使用特性 B:如果用户对 B 没有什么反对意见,那么逐步扩大发布范围,直到把所有用户都迁移到 B 上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。要支持灰度发布,就要求服务能够根据一定的规则,将流量隔离。
- 流量切换:在业务线上运行过程中,经常会遇到一些不可抗力因素导致业务故障,比如某个机房的光缆被挖断,或者发生着火等事故导致整个机房的服务都不可用。这个时候就需要按照某个指令,能够把原来调用这个机房服务的流量切换到其他正常的机房。
- 线下测试联调:线下测试时,可能会缺少相应环境。可以将测试应用注册到线上,然后开启路由规则,在本地进行测试。
- 读写分离:对于大多数互联网业务来说都是读多写少,所以在进行服务部署的时候,可以把读写分开部署,所有写接口可以部署在一起,而读接口部署在另外的节点上。
🔀 发散问题
1. 服务路由和负载均衡到底怎么分工?
负载均衡负责"在一组候选机器里均匀分发",服务路由负责"先按规则缩小到哪个分组"。实践中常常是路由先筛选,再在小范围内做负载均衡,见本文档『负载均衡有哪些算法?』。
2. 灰度发布和蓝绿发布的核心区别是什么?
蓝绿是两套完整环境间的整体切换(非此即彼),灰度是按比例/规则逐步放量(平滑过渡)。灰度风险更小但实现更复杂,需要路由规则支撑按用户/百分比隔离流量。
3. 路由规则具体有哪些类型?
见本文档『服务路由有哪些常见规则?』,主要有条件路由、脚本路由、标签路由三类。
【中等】服务路由有哪些常见规则?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式调度 / 服务路由
💎 关键结论
服务路由规则主要分三类:条件路由(用表达式匹配消费者和提供者,如 Dubbo 的 => 语法)、脚本路由(用脚本语言编写 route 方法,最灵活)、标签路由(给实例分组打标签,是蓝绿/灰度发布的能力基础)。记住一条主线:表达力越强,配置越复杂,适用场景越精细。
⚡记忆卡片
- 口诀:条件写表达式,脚本写 route,标签分组隔离
- 关键词:条件路由/脚本路由/标签路由/Dubbo/灰度发布/动态规则/静态规则
- 链路:定义路由规则类型 → 匹配消费者与提供者 → 过滤出目标地址列表 → 流量只在指定分组流转 → 支撑蓝绿/灰度发布
📖 核心知识
条件路由:条件路由是基于条件表达式的路由规则。各个 RPC 框架的条件路由表达式各不相同。参考 Dubbo 的条件路由,有两种配置粒度:
应用粒度:
# app1 的消费者只能消费所有端口为 20880 的服务实例 # app2 的消费者只能消费所有端口为 20881 的服务实例 --- scope: application force: true runtime: true enabled: true key: governance-conditionrouter-consumer conditions: - application=app1 => address=*:20880 - application=app2 => address=*:20881服务粒度:
# DemoService 的 sayHello 方法只能消费所有端口为 20880 的服务实例 # DemoService 的 sayHi 方法只能消费所有端口为 20881 的服务实例 --- scope: service force: true runtime: true enabled: true key: org.apache.dubbo.samples.governance.api.DemoService conditions: - method=sayHello => address=*:20880 - method=sayHi => address=*:20881
其中,
conditions定义具体的路由规则内容,是规则的主体,由 1 到任意多条规则组成。Dubbo 的条件路由规则由两个条件组成,分别用于对服务消费者和提供者进行匹配。格式如下:
[服务消费者匹配条件] => [服务提供者匹配条件]- 服务消费者匹配条件:所有参数和消费者的 URL 进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。
- 服务提供者匹配条件:所有参数和提供者的 URL 进行对比,消费者最终只拿到过滤后的地址列表。
condition://代表这是一段用条件表达式编写的路由规则,示例:host = 10.20.153.10 => host = 10.20.153.11该条规则表示 IP 为
10.20.153.10的服务消费者只可调用 IP 为10.20.153.11机器上的服务,不可调用其他机器上的服务。Dubbo 条件路由的典型应用场景:
- 服务消费者的匹配条件为空,表示所有的服务消费者都可以访问:
=> host != 10.20.153.11 - 服务提供者的过滤条件为空,表示禁止所有的服务消费者访问:
host = 10.20.153.10 => - 排除某个服务节点:
=> host != 172.22.3.91 - 白名单:
register.ip != 10.20.153.10,10.20.153.11 => - 黑名单:
register.ip = 10.20.153.10,10.20.153.11 => - 只暴露部分机器节点:
=> host = 172.22.3.1*,172.22.3.2* - 为重要应用提供额外的机器节点:
application != kylin => host != 172.22.3.95,172.22.3.96 - 读写分离:
method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96、method != find*,list*,get*,is* => host = 172.22.3.97,172.22.3.98 - 前后台分离:
application = bops => host = 172.22.3.91,172.22.3.92,172.22.3.93、application != bops => host = 172.22.3.94,172.22.3.95,172.22.3.96 - 隔离不同机房网段:
host != 172.22.3.* => host != 172.22.3.* - 提供者与消费者部署在同集群内,本机只访问本机的服务:
=> host = $host
脚本路由:脚本路由是基于脚本语言的路由规则,常用的脚本语言比如 JavaScript、Groovy、JRuby 等。
'script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=' + URL.encode('(function route(invokers) { ... } (invokers))')script://代表这是一段脚本语言编写的路由规则,具体规则定义在脚本语言的 route 方法实现里。比如下面这段用 JavaScript 编写的 route() 方法表达的意思是,只有 IP 为10.20.153.10的服务消费者可以发起服务调用:function route(invokers){ var result = new java.util.ArrayList(invokers.size()); for(i =0; i < invokers.size(); i ++){ if("10.20.153.10".equals(invokers.get(i).getUrl().getHost())){ result.add(invokers.get(i)); } } return result; } (invokers));标签路由:标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。标签主要是指对服务提供者的分组,目前有两种方式可以完成实例分组,分别是动态规则打标和静态规则打标。一般,动态规则优先级比静态规则更高,当两种规则同时存在且出现冲突时,将以动态规则为准。
以 Dubbo 的标签路由用法为例:
(1)动态规则打标,可随时在服务治理控制台下发标签归组规则:
# governance-tagrouter-provider 应用增加了两个标签分组 tag1 和 tag2 # tag1 包含一个实例 127.0.0.1:20880 # tag2 包含一个实例 127.0.0.1:20881 --- force: false runtime: true enabled: true key: governance-tagrouter-provider tags: - name: tag1 addresses: ["127.0.0.1:20880"] - name: tag2 addresses: ["127.0.0.1:20881"] ...(2)静态规则打标:
<dubbo:provider tag="tag1"/>or
<dubbo:service tag="tag1"/>or
java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}(3)服务消费者指定标签路由:
RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1");请求标签的作用域为每一次 invocation,使用
attachment来传递请求标签,注意保存在attachment中的值将会在一次完整的远程调用中持续传递,得益于这样的特性,我们只需要在起始调用时,通过一行代码的设置,达到标签的持续传递。
🔬 扩展知识
详情
【L3】
- 三种路由的表达力排序:标签路由(分组隔离,语义简单)< 条件路由(表达式匹配,中等灵活)< 脚本路由(任意编程逻辑,最灵活)。表达力越强,配置和排查成本越高,标签路由因为语义清晰成为蓝绿/灰度发布的主流选择。
【L4】
- 动态规则优先级高于静态规则的设计意图:静态规则(启动参数/配置文件)反映部署时的意图,动态规则(控制台下发)反映运行期的治理意图;冲突时以动态为准,保证运维能在不重启的情况下实时调整流量走向,这正是"治理与部署分离"的体现。
📚 延伸阅读:Dubbo 路由规则。
🔀 发散问题
1. 为什么蓝绿/灰度发布主要用标签路由而不是条件路由?
标签路由的分组语义与"蓝/绿环境""灰度批次"天然对应,配置和回滚都更直观;条件路由适合按 IP/方法等精细匹配,但不适合表达"整组实例"的隔离语义。
2. 请求标签是怎么在整条调用链上传递的?
通过 RPC 的 attachment 隐式传参,起始调用处设置一次,后续每一跳都持续传递,保证跨多个服务时流量仍落在同一标签分组内。
3. 路由规则从哪里获取?
见本文档『路由规则有哪些获取方式?』,主要是本地静态配置、配置中心管理、注册中心动态下发三种。
【中等】路由规则有哪些获取方式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式调度 / 服务路由
💎 关键结论
路由规则有三种获取方式:本地静态配置、配置中心管理、注册中心动态下发。最佳实践是默认存配置中心统一管理,特殊需求用本地配置定制,需要实时调整时用动态下发。核心原则:集中管理降低沟通成本,动态下发应对突发场景。
⚡记忆卡片
- 口诀:默认配置中心,特殊本地定制,应急动态下发
- 关键词:本地静态配置/配置中心/注册中心动态下发/服务治理平台
- 链路:运维/开发在服务治理平台改规则 → 治理平台调用配置中心接口持久化 → 消费者订阅规则变更 → 拉取最新规则执行 → 实现流量实时调度
📖 核心知识
路由规则的获取方式主要有三种:
- 本地静态配置:路由规则存储在服务消费者本地上。服务消费者发起调用时,从本地固定位置读取路由规则,然后按照路由规则选取一个服务节点发起调用。
- 配置中心管理:所有的服务消费者都从配置中心获取路由规则,由配置中心来统一管理。
- 注册中心动态下发:一般是运维人员或者开发人员,通过服务治理平台修改路由规则,服务治理平台调用配置中心接口,把修改后的路由规则持久化到配置中心。因为服务消费者订阅了路由规则的变更,于是就会从配置中心获取最新的路由规则,按照最新的路由规则来执行。
一般来讲,服务路由最好是存储在配置中心,由配置中心来统一管理。这样的话,所有的服务消费者就不需要在本地管理服务路由,因为大部分的服务消费者并不关心服务路由的问题,或者说也不需要去了解其中的细节。通过配置中心,统一给各个服务消费者下发统一的服务路由,节省了沟通和管理成本。
但也不排除某些服务消费者有特定的需求,需要定制自己的路由规则,这个时候就适合通过本地配置来定制。
而动态下发可以理解为一种高级功能,它能够动态地修改路由规则,在某些业务场景下十分有用。比如某个数据中心存在问题,需要把调用这个数据中心的服务消费者都切换到其他数据中心,这时就可以通过动态下发的方式,向配置中心下发一条路由规则,将所有调用这个数据中心的请求都迁移到别的地方。
🔬 扩展知识
详情
【L3】
- 三种方式的优先级叠加:三种获取方式并非互斥,实际系统中常见"配置中心统一规则 + 本地覆盖 + 动态下发应急"的组合:本地规则优先级最高(特定消费者的定制需求),动态下发用于应急切流,配置中心规则作为全局默认,冲突时按"本地 > 动态 > 中心"的顺序生效。
【L4】
- 规则生效链路的可靠性设计:动态下发的完整链路是"治理平台 → 配置中心持久化 → 消费者订阅推送 → 本地生效",任何一环故障都会影响生效。工程上需要:规则变更先校验后下发(防止错误规则清空全部地址)、消费者保留最后一次有效规则做兜底、变更全程审计留痕可回滚。
🔀 发散问题
1. 为什么默认推荐配置中心而不是本地配置?
大部分服务消费者并不关心路由细节,本地配置会导致规则分散、难以维护、版本不一致;配置中心统一管理后,规则变更可审计、可回滚,沟通和管理成本大幅降低。
2. 动态下发在容灾场景下怎么发挥作用?
某机房整体故障时,通过服务治理平台动态下发一条路由规则,把原本调用该机房的流量全部切换到其他机房,无需重启应用,是机房级容灾的关键手段,见本文档『什么是服务路由?路由有什么用?』中的流量切换场景。
3. 配置中心故障时路由规则会失效吗?
不会立即失效。消费者已拉取的路由规则缓存在本地,配置中心故障期间仍按最后一次规则执行;这与注册中心挂掉后靠本地缓存兜底的思路一致。
分布式任务
【中等】在 Java 中,实现一个进程内定时任务有哪些方案?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式调度 / 定时任务
💎 关键结论
JDK 原生三种方案(Timer、ScheduledThreadPoolExecutor、DelayQueue)本质都是"优先级队列 + 轮询线程",增删任务是 O(logn),海量任务下有性能瓶颈,且 Timer 有单线程/异常致命的硬伤;性能敏感场景用时间轮,增删任务 O(1),这也是 Netty、Kafka 等框架的选择。
⚡记忆卡片
- 口诀:Timer 别用,调度用 Scheduled,延迟重试 DelayQueue,海量任务时间轮
- 关键词:Timer/ScheduledThreadPoolExecutor/DelayQueue/时间轮/小根堆/O(1)调度
- 链路:任务按 deadline 入队 → 轮询线程检查到期任务 → 到期执行(周期任务重新入队) → 任务量大时 O(logn) 成瓶颈 → 换时间轮 O(1) 增删
📖 核心知识
定时器有非常多的使用场景,例如生成年/月/周/日统计报表、财务对账、会员积分结算、邮件推送等。定时器一般有三种表现形式:按固定周期定时执行、延迟一定时间后执行、指定某个时刻执行。
定时器的本质是设计一种数据结构,能够存储和调度任务集合,而且 deadline 越近的任务拥有更高的优先级。那么定时器如何知道一个任务是否到期了呢?定时器需要通过轮询的方式来实现,每隔一个时间片去检查任务是否到期。所以定时器的内部结构一般需要一个任务队列和一个异步轮询线程,并且能够提供三种基本操作:Schedule 新增任务至任务集合;Cancel 取消某个任务;Run 执行到期的任务。
JDK 原生提供了三种常用的定时器实现方式,分别为 Timer、DelayedQueue 和 ScheduledThreadPoolExecutor。三种实现思路都非常相似,都离不开任务、任务管理、任务调度三个角色,新增和取消任务的时间复杂度都是 O(logn),面对海量任务插入和删除的场景都会遇到比较严重的性能瓶颈。对于性能要求较高的场景,一般都会采用时间轮算法来实现定时器。
- Timer:属于 JDK 比较早期版本的实现,可以实现固定周期的任务以及延迟任务。
Timer会启动一个异步线程去执行到期的任务,任务可以只被调度执行一次,也可以周期性反复执行多次。
示例:Timer 的使用与内部结构
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// do something
}
}, 10000, 1000); // 10s 后调度一个周期为 1s 的定时任务可以看出,任务是由 TimerTask 类实现,TimerTask 是实现了 Runnable 接口的抽象类,Timer 负责调度和执行 TimerTask。Timer 的内部构造:
public class Timer {
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
public Timer(String name) {
thread.setName(name);
thread.start();
}
}TaskQueue 是由数组结构实现的小根堆,deadline 最近的任务位于堆顶端,queue[1] 始终是最优先被执行的任务。所以使用小根堆的数据结构,Run 操作时间复杂度 O(1),新增(Schedule)和取消(Cancel)操作的时间复杂度都是 O(logn)。
Timer 内部启动了一个 TimerThread 异步线程,不论有多少任务被加入数组,始终都是由 TimerThread 负责处理。TimerThread 会定时轮询 TaskQueue 中的任务,如果堆顶的任务的 deadline 已到,那么执行任务;如果是周期性任务,执行完成后重新计算下一次任务的 deadline,并再次放入小根堆;如果是单次执行的任务,执行结束后会从 TaskQueue 中删除。
Timer 只使用一个线程来执行任务意味着同一时间只能有一个任务得到执行,而前一个任务的延迟或者异常会影响到之后的任务。不推荐使用 Timer,因为 Timer 存在以下设计缺陷:
- Timer 是单线程模式。如果某个 TimerTask 执行时间很久,会影响其他任务的调度。
- Timer 的任务调度是基于系统绝对时间的,如果系统时间不正确,可能会出现问题。
- TimerTask 如果执行出现异常,Timer 并不会捕获,会导致线程终止,其他任务永远不会执行。
- ScheduledExecutorService:为了解决
Timer的设计缺陷,JDK 提供了功能更加丰富的ScheduledThreadPoolExecutor,它提供了周期执行任务和延迟执行任务的特性。
示例:ScheduledThreadPoolExecutor 的使用与原理
public class ScheduledExecutorServiceTest {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
// 1s 延迟后开始执行任务,每 2s 重复执行一次
executor.scheduleAtFixedRate(() -> System.out.println("Hello World"), 1000, 2000, TimeUnit.MILLISECONDS);
}
}ScheduledThreadPoolExecutor 继承于 ThreadPoolExecutor,因此它具备线程池异步处理任务的能力。线程池主要负责管理创建和管理线程,并从自身的阻塞队列中不断获取任务执行。ScheduledThreadPoolExecutor 在 ThreadPoolExecutor 的基础上,重新设计了任务 ScheduledFutureTask 和阻塞队列 DelayedWorkQueue。ScheduledFutureTask 继承于 FutureTask,并重写了 run() 方法,使其具备周期执行任务的能力。DelayedWorkQueue 内部是优先级队列,deadline 最近的任务在队列头部。对于周期执行的任务,在执行完会重新设置时间,并再次放入队列中。
- DelayQueue:是 JDK 中一种可以延迟获取对象的阻塞队列,其内部是采用优先级队列
PriorityQueue存储对象。DelayQueue中的每个对象都必须实现Delayed接口,并重写compareTo和getDelay方法。
示例:DelayQueue 的使用
public class DelayQueueTest {
public static void main(String[] args) throws Exception {
BlockingQueue<SampleTask> delayQueue = new DelayQueue<>();
long now = System.currentTimeMillis();
delayQueue.put(new SampleTask(now + 1000));
delayQueue.put(new SampleTask(now + 2000));
delayQueue.put(new SampleTask(now + 3000));
for (int i = 0; i < 3; i++) {
System.out.println(new Date(delayQueue.take().getTime()));
}
}
static class SampleTask implements Delayed {
long time;
public SampleTask(long time) {
this.time = time;
}
public long getTime() {
return time;
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
}
}DelayQueue 提供了 put() 和 take() 的阻塞方法,可以向队列中添加对象和取出对象。对象被添加到 DelayQueue 后,会根据 compareTo() 方法进行优先级排序。getDelay() 方法用于计算消息延迟的剩余时间,只有 getDelay <=0 时,该对象才能从 DelayQueue 中取出。
DelayQueue 在日常开发中最常用的场景就是实现重试机制。例如,接口调用失败或者请求超时后,可以将当前请求对象放入 DelayQueue,通过一个异步线程 take() 取出对象然后继续进行重试。如果还是请求失败,继续放回 DelayQueue。为了限制重试的频率,可以设置重试的最大次数以及采用指数退避算法设置对象的 deadline,如 2s、4s、8s、16s ……以此类推。相比于 Timer,DelayQueue 只实现了任务管理的功能,需要与异步线程配合使用。DelayQueue 使用优先级队列实现任务的优先级排序,新增(Schedule)和取消(Cancel)操作的时间复杂度也是 O(logn)。
时间轮:时间轮(Timing Wheel)是 George Varghese 和 Tony Lauck 在 1996 年的论文 Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility 实现的,它在 Linux 内核中使用广泛,是 Linux 内核定时器的实现方法和基础之一。
时间轮可以理解为一种环形结构,像钟表一样被分为多个 slot 槽位。每个 slot 代表一个时间段,每个 slot 中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个 slot 转动,并执行 slot 中的所有到期任务。

任务是如何添加到时间轮当中的呢?可以根据任务的到期时间进行取模,然后将任务分布到不同的 slot 中。如上图所示,时间轮被划分为 8 个 slot,每个 slot 代表 1s,当前时针指向 2。假如现在需要调度一个 3s 后执行的任务,应该加入
2+3=5的 slot 中;如果需要调度一个 12s 以后的任务,需要等待时针完整走完一圈 round 零 4 个 slot,需要放入第(2+12)%8=6个 slot。那么当时针走到第 6 个 slot 时,怎么区分每个任务是否需要立即执行,还是需要等待下一圈 round,甚至更久时间之后执行呢?所以我们需要把 round 信息保存在任务中。例如图中第 6 个 slot 的链表中包含 3 个任务,第一个任务 round=0,需要立即执行;第二个任务 round=1,需要等待
1*8=8s后执行;第三个任务 round=2,需要等待2*8=16s后执行。当时针转动到对应 slot 时,只执行 round=0 的任务,slot 中其余任务的 round 应当减 1,等待下一个 round 之后执行。时间轮有点类似 HashMap,如果多个任务对应同一个 slot,处理冲突的方法采用的是拉链法。在任务数量比较多的场景下,适当增加时间轮的 slot 数量,可以减少时针转动时遍历的任务个数。时间轮定时器最大的优势就是,任务的新增和取消都是 O(1) 时间复杂度,而且只需要一个线程就可以驱动时间轮进行工作。HashedWheelTimer 是 Netty 中时间轮算法的实现类。
🔬 扩展知识
详情
【L3】
- 时间轮的层级化优化:单层时间轮要支撑很长的延迟时间,需要非常多的 slot,内存开销大;层级时间轮(Hierarchical Timing Wheel,类似手表的秒针/分针/时针)用多层小轮代替单层大轮,任务到期时从高层轮"降级"到低层轮,Netty、Kafka 的实现都采用了这一思路。
【L4】
- 堆 vs 时间轮的选型边界:任务量在百千级且增删不频繁时,ScheduledThreadPoolExecutor 的 O(logn) 完全够用且 API 更友好;当任务达到百万级(如海量延迟消息、连接超时管理)时,O(logn) 的堆调整会成为瓶颈,才值得引入时间轮这类 O(1) 结构。典型应用:Netty 用 HashedWheelTimer 管理连接超时,Kafka 用时间轮管理生产者/消费者超时与延迟操作。
🔀 发散问题
1. Timer 和 ScheduledThreadPoolExecutor 最关键的差别是什么?
Timer 是单线程 + 绝对时间 + 异常不捕获,一个任务异常或阻塞会毁掉所有任务;ScheduledThreadPoolExecutor 是多线程池 + 相对时间,任务间互不影响,是 Timer 的全面替代。
2. DelayQueue 适合做定时任务调度器吗?
它只负责任务管理(到期才能取出),不含调度线程,需要自己配合异步线程使用;更常见的用法是实现延迟重试、延迟消息这类"到期触发"场景。
3. 进程内定时器不够用时(多实例、需持久化、需失败重调)怎么办?
需要分布式定时任务方案,见本文档『分布式定时任务有哪些方案?』,如 Quartz、XXL-Job、ElasticJob。
【中等】分布式定时任务有哪些方案?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式调度 / 定时任务
💎 关键结论
分布式定时任务三大主流方案定位不同:Quartz 是经典框架(靠数据库实现分布式,重侵入)、XXL-Job 是调度平台(调度中心 + 执行器分离,开箱即用带控制台)、ElasticJob 是去中心化框架(以 jar 形式集成,依赖 ZooKeeper)。需要可视化运营选 XXL-Job,需要弹性资源治理选 ElasticJob,轻量嵌入选 Quartz。
⚡记忆卡片
- 口诀:Quartz 库锁集群,XXL-Job 平台解耦,ElasticJob 无中心靠 ZK
- 关键词:Quartz/XXL-Job/ElasticJob/调度中心/执行器/JobHandler/分片
- 链路:任务定义(Job/JobHandler) → 调度中心/框架按 cron 触发 → 路由到执行器实例 → 执行业务逻辑 → 结果回传与失败转移
📖 核心知识
分布式定时任务常见方案有:Quartz、XXL-Job、ElasticJob。
Quartz:Quartz 是一个经典的开源定时调度框架,它支持进程内调度和分布式调度。Quartz 提供两种基本作业存储类型:
- RAMJobStore - 在默认情况下 Quartz 将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序中途停止或系统崩溃时,所有运行的信息都会丢失。
- JobStoreTX - 所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务。
XXL-Job:xxl-job 是一个分布式任务调度平台。
设计思想:将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。将任务抽象成分散的 JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的 JobHandler 中业务逻辑。因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性。
系统组成:
- 调度模块(调度中心):负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE 开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器 Failover。
- 执行模块(执行器):负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。

输入图片说明 ElasticJob:两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。它通过弹性调度、资源管控、以及作业治理的功能,打造一个适用于互联网场景的分布式调度解决方案,并通过开放的架构设计,提供多元化的作业生态。它的各个产品使用统一的作业 API,开发者仅需一次开发,即可随意部署。ElasticJob 采用去中心化架构,没有作业调度中心。它以框架的形式,集成到应用中,提供调度服务。ElasticJob-Lite 定位为轻量级无中心化解决方案,使用 jar 的形式提供分布式任务的协调服务。

ElasticJob Architecture ElasticJob-Cloud 采用自研 Mesos Framework 的解决方案,额外提供资源治理、应用分发以及进程隔离等功能。

ElasticJob-Cloud Architecture ElasticJob-Lite 和 ElasticJob-Cloud 对比:
ElasticJob-Lite ElasticJob-Cloud 无中心化 是 否资源分配 不支持 支持作业模式 常驻 常驻 + 瞬时部署依赖 ZooKeeper ZooKeeper + MesosElasticJob-Cloud 的优势在于对资源细粒度治理,适用于需要削峰填谷的大数据系统。
🔬 扩展知识
详情
【L3】
- Quartz 分布式模式的实现原理:Quartz 的集群模式依赖数据库(JobStoreTX)作为协调存储,多个实例通过数据库行锁竞争任务执行权,同一时刻只有一个实例能抢到某个任务的触发权,从而避免重复执行;代价是调度吞吐受数据库性能限制,且强依赖数据库可用性。
- XXL-Job 与 ElasticJob 的架构差异:XXL-Job 是有中心架构,调度中心统一管理(控制台、告警、Failover),执行器无状态可水平扩展;ElasticJob-Lite 是无中心架构,各实例地位对等,靠 ZooKeeper 的临时节点与选举完成触发权协调,少一个需要运维的中心组件但缺少统一控制台(Lite 后续版本提供运维控制台)。
【L4】
- 分布式定时任务的核心设计命题:① 如何避免多实例重复执行(分布式锁/选主/分片);② 实例故障时任务如何转移(Failover);③ 任务量超过单机能力时如何拆分(分片广播,每个实例处理一部分数据);④ 调度与执行解耦,避免调度中心被业务拖垮。评估任何分布式任务方案都可以从这四个维度入手。
📚 延伸阅读:XXL-Job 官方仓库。
🔀 发散问题
1. 为什么 XXL-Job 要把"调度"和"任务"拆成调度中心与执行器?
调度中心只管触发不管业务,执行器只管执行不管调度,两者解耦后调度中心性能不受业务任务影响,执行器可以随业务量独立水平扩展,任一侧故障不会拖垮另一侧。
2. 多实例部署时如何防止同一个任务被重复执行?
常见手段有三类:数据库行锁竞争(Quartz 集群模式)、分布式协调选主(ElasticJob 基于 ZooKeeper)、调度中心集中派发(XXL-Job 由调度中心选择目标执行器),本质都是把"触发权"收敛为单点。
3. 任务处理量太大单机执行不完怎么办?
用分片广播路由策略:调度中心把分片总数和当前分片号下发给每个执行器实例,每个实例只处理自己分片对应的数据,实现任务的水平拆分并行处理。