Cloud发现与负载均衡:实例、缓存与摘流
调用 http://catalog/items 时,catalog 是逻辑服务名。发现客户端先提供实例目录,supplier 链再生成本次请求的候选列表,LoadBalancer 从中选择一个实例,HTTP 客户端最后执行网络请求。“目录中存在”“被选中”“网络成功”“业务成功”是四个不同事实。本页沿这条链解释 Spring Cloud Commons / LoadBalancer,并把下线后的陈旧目录、探活与重试放到同一条时间线上。
本页是 U32 / #69 的 L01 权威正文。范围为 R0 的 D01–D06、B01–B14;Gateway 的路由、过滤器、身份与限流属于本票 L02。真实注册中心的安装、provider/registry/consumer 传播、集群分区与适配差异由 U35 / U40 承接。本页的静态目录、可控目录和 HTTP 上游均不能证明真实注册中心协议。
版本、模块与最小完整入口
机制源码固定 Commons / LoadBalancer 5.0.4,commit db9b65a5a8553e4ec04faa4d100eeb45817a2a92。后文“原码”均是该提交的连续摘录,注释里的 SHA-256 对应整文件;教学代码和算例另行标明。查询日期为 2026-10-26。官方兼容矩阵 将 Cloud 2025.1 对应到 Boot 4.0.x;本轮候选精确组合为 Boot 4.0.9 + Cloud 2025.1.4,由 train BOM 选择 Commons、LoadBalancer 和 Gateway 5.0.4。源码核对、BOM 匹配、编译、HTTP 实跑是分别报告的证据,不能相互替代。
Commons 根 pom 的完整 Maven 模块承担不同职责:spring-cloud-commons 提供发现、注册与客户端公共抽象;spring-cloud-loadbalancer 实现算法、supplier、缓存、生命周期统计;spring-cloud-starter-loadbalancer 是应用引入入口;spring-cloud-starter 聚合公共依赖;spring-cloud-commons-dependencies 管理版本。spring-cloud-context 提供上下文、环境与刷新基础,spring-cloud-starter-bootstrap 是显式启用旧 bootstrap 路径的入口,不能为负载均衡随手加入。spring-cloud-test-support、spring-cloud-context-integration-tests、spring-cloud-context-webflux-integration-tests 和 docs 分别承担测试与文档,不是生产注册中心实现。Context 刷新与配置传播主讲归 U34,测试模块存在不代表本页已经运行其测试。
下面是一套教学复现工程,并非实验 PASS 声明。目录为 pom.xml、src/main/java/demo/Consumer.java、src/main/resources/application.yml。JDK 21、Maven 3.9.9;候选基线 JDK 为 BellSoft Liberica 21.0.11+11。实际运行时仍须保存 java -version、mvn -version、OS/架构与依赖树。WebFlux Gateway 的 server starter 不保证提供 WebClient.Builder:需要 WebClient 的健康检查示例显式引入 Boot 4 的 spring-boot-starter-webclient,不能仅凭服务器能启动判断客户端已装配。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.9</version>
<relativePath/>
</parent>
<groupId>study.u32</groupId><artifactId>consumer</artifactId><version>1.0.0</version>
<properties><java.version>21</java.version></properties>
<dependencyManagement><dependencies><dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId><version>2025.1.4</version>
<type>pom</type><scope>import</scope>
</dependency></dependencies></dependencyManagement>
<dependencies>
<dependency><groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId></dependency>
<dependency><groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webclient</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Consumer {
public static void main(String[] args) {
SpringApplication.run(Consumer.class, args);
}
}server:
address: 127.0.0.1
port: 18080
spring:
application:
name: u32-consumer
cloud:
service-registry:
auto-registration:
enabled: false # 此静态目录例子不向任何远端注册
discovery:
client:
simple:
instances:
catalog:
- instance-id: catalog-a
uri: http://127.0.0.1:18081
metadata: {zone: east, hint: stable, weight: '1', API_VERSION: '1.0'}
- instance-id: catalog-b
uri: http://127.0.0.1:18082
metadata: {zone: west, hint: canary, weight: '3', API_VERSION: '2.0'}
loadbalancer:
retry:
enabled: false
gateway:
server:
webflux:
routes:
- id: u32-catalog
uri: lb://catalog
predicates:
- Path=/catalog/**
filters:
- StripPrefix=1
management:
server:
address: 127.0.0.1
port: 18090
endpoints:
web:
exposure:
include: health,features上游需自己返回实例身份,不能只统计网关 200。可在隔离临时目录保存下列 upstream.py,分别运行 python3 upstream.py 18081 A 和 python3 upstream.py 18082 B;它是合成 HTTP 上游,无数据库、无注册协议。/health/200 故意返回 HTTP 200 与 DOWN JSON,/health/204 和 /health/503 用于区分状态码判定。只结束自己启动的两个 PID。
import json
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
port, identity = int(sys.argv[1]), sys.argv[2]
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
status = 200
if self.path.startswith('/health/'):
status = int(self.path.split('/')[2].split('?')[0])
body = b'' if status == 204 else json.dumps({
'instance': identity, 'path': self.path, 'status': 'DOWN'
}).encode()
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
ThreadingHTTPServer(('127.0.0.1', port), Handler).serve_forever()启动调用入口为 mvn package、java -jar target/consumer-1.0.0.jar、curl -i http://127.0.0.1:18080/catalog/items。mvn dependency:tree 应分别确认三个 Cloud 组件都是 5.0.4;未运行时这些命令只有复现意义。静态目录的 YAML 不会因为杀掉上游就自动删除实例,也不能把修改 YAML 文件等同于进程内即时刷新。动态增删、缓存命中/过期、全空/恢复使用主任务的可控目录实验;其公开入口为 labs/cloud-gateway/README.md,以该文件最终保存的输入、退出状态和产物为准。
服务、实例与发现和注册的职责
D01。 serviceId 标识一组可互相替代的逻辑服务,instanceId 标识其中一个实例生命周期;地址由 host、port、secure / scheme 表达,metadata 是附加路由信息。一个服务可以有两个相同版本实例,也可以同时有两个 API 版本。必须另行定义替代条件:实例都叫 catalog,不代表 1.0 调用能任意送给 2.0。metadata、namespace、zone 和 hint 都是控制面数据,不自动建立用户身份和权限。
DiscoveryClient.getInstances(serviceId) 返回阻塞列表,getServices() 列出服务名;ReactiveDiscoveryClient 返回相应 Flux。ServiceRegistry<Registration> 负责 register、deregister、setStatus、getStatus、close;Registration 继承实例描述,用于本进程的注册信息。接口本身不规定心跳、租约、复制协议、缓存新鲜度和状态字符串,须由具体注册中心实现说明。官方公共抽象 是本节的 API 来源。
没有更具体发现客户端时,SimpleDiscoveryClient / SimpleReactiveDiscoveryClient 从前节 simple.instances 生成本地目录。其初始化把 map key 写成 serviceId,连续原码如下:
原码连续摘录:SimpleDiscoveryProperties.java,5.0.4,L77–L84;整文件 SHA-256 f2325294eb17d80a509ac9f21e9b4176962b5c2cfc6e3e5357cb0c45dfcf40ce。
@Override
public void afterPropertiesSet() {
for (String key : this.instances.keySet()) {
for (InstanceProperties instance : this.instances.get(key)) {
instance.setServiceId(key);
}
}
}输入 catalog → [A,B],输出是两个 ServiceInstance;不会对 A/B 发注册、续租或探活。B 停机后列表仍可含 B,此时“发现成功、执行失败”完全可能。恢复应分别确认目录、候选与上游响应,不把 DNS 失败归结成“注册失败”。这也是静态目录实验与 U35/U40 真实服务集成的边界。
常规自动配置不要求再写 @EnableDiscoveryClient。spring.cloud.discovery.enabled=false 关闭发现相关条件;spring.cloud.discovery.blocking.enabled=false 和 spring.cloud.discovery.reactive.enabled=false 分别控制两条发现 API 的自动配置;spring.cloud.service-registry.auto-registration.enabled=false 只关闭自动注册。作为消费者禁止本机注册,仍可以发现别人。显式自定义 Bean 的行为还要看自己的配置,不能把条件开关当成全 JVM 的 API 禁令。
复合发现源与控制面陈旧窗口
D02。 同时引入多个 discovery 实现时,阻塞 Composite 对 client 按 order 排序,然后为同一 serviceId 返回首个非空列表。A 中有旧实例、B 中有新实例,得到的不是两者并集;A 返回空才继续 B。A 抛异常也不等于返回空,方法没有把任意异常吞掉后换中心。
[[source:S01]]
getServices() 则逐源添加到 LinkedHashSet,保序去重后返回服务名并集。这解释了“能列出 catalog,却查不到预期 B 实例”的一种原因:列服务名和取实例用的聚合规则不同。
reactive Composite 同样不合并实例,但不能照搬阻塞版“依次等待每个源”的解释:它把各源 Flux 交给 CloudFlux.firstNonEmpty。该运算符竞争首个 onNext / onError,空完成不是获胜信号;实际时序会影响哪个源赢。先错误可能终止选择,全部空完成才空完成;若其余皆空而一个源永不发信号,就可能一直等到调用方超时。ReactiveCompositeDiscoveryClient 的 getServices() 使用 flatMapSequential 再 distinct,服务名仍为去重聚合。
[[source:commons|spring-cloud-commons/src/main/java/org/springframework/cloud/commons/publisher/CloudFlux.java|46|60]]
需要可测试的反例:源甲 order 更前但 100 ms 才返回 A,源乙 10 ms 返回 B;阻塞链先等甲,reactive 竞争可能选乙。再把乙改成 10 ms 报错,不能期待自然切到甲。定位时记录 client 实现名、order、每次订阅/查询时刻、结果与错误,不能只抓最后一个空列表。
目录新鲜度至少经过“注册中心状态 → 具体 discovery client 缓存 → LB 缓存 / health replay → 本次候选”。若上游 SDK 30 s 刷新、LB 写入后 TTL 35 s,在最不利相位,删除可能要接近 65 s 才在下一次取值时显现,另加传播/调度。控制面失联而 SDK 无限保留旧目录时,这个窗口没有可证明的有限上界,缩短 LB TTL 也不能创造新事实。是否允许继续用旧目录、超过多旧拒绝请求,应由业务风险与目录最大年龄策略决定。
目录健康、实例探活与业务就绪
D03。 DiscoveryClientHealthIndicator 的 UP 意味着 discovery API 的查询或 probe 没抛错;不是“每个实例都能接单”。源码先检查是否收到 InstanceRegisteredEvent,初始化前返回 UNKNOWN;收到事件后默认调用 getServices,异常为 DOWN。静态消费者禁用注册后可能一直 UNKNOWN,不能为消除 UNKNOWN 而伪造注册成功。
[[source:S03]]
配置位于 spring.cloud.discovery.client.health-indicator:enabled=true、include-description=false、use-services-query=true 是字段默认;设 use-services-query=false 改调 probe()。默认 DiscoveryClient.probe() 可以是空操作,具体实现也可能从本地缓存回答,因此该 UP 甚至不保证这次真的连过中心。大目录每次 getServices 列全量有开销,应该衡量服务数量、探测频率、响应体和权限后选轻量 probe。
spring.cloud.discovery.client.composite-indicator.enabled=false 关闭聚合发现健康贡献者;这与禁用单个 indicator、禁用整个 discovery 不是一回事。Actuator、发现 Bean 与装配条件同时满足才有对应贡献者。CommonsClientAutoConfiguration 将这些条件分开。
故障诊断顺序应是:查看 UNKNOWN / DOWN 的原始详情和初始化事件,再查目录 API 成功与否,再查业务端口探活,最后查业务依赖与授权。readiness、liveness、管理端口的通用语义保留在既有 Boot / Actuator 教材;这里新增的是发现贡献者的实际含义。不能为 readiness 方便而把“能列服务”当业务接流许可。
自动注册、状态端点与摘流状态链
D04。 具体注册实现继承 AbstractAutoServiceRegistration 时,业务 WebServerInitializedEvent 会填入监听端口并 start,management context 被排除。这个事件不保证业务预热或依赖就绪,也不等于 ApplicationReadyEvent。
[[source:S04]]
start 在 enabled 且尚未 running 时发布 InstancePreRegisteredEvent,运行注册 lifecycle 前置处理,调用 register,运行后置处理;需要时注册管理实例,之后发布 InstanceRegisteredEvent 并置 running。事件是进程内生命周期通知,不是“所有消费者均已更新”的确认。
[[source:S05]]
自动注册可以用前节属性关闭;@EnableDiscoveryClient(autoRegister=false) 也表达不要自动注册。本机端口初始化与远端注册状态应分别观测。显式实现 ServiceRegistry 只是拿到协议接入点,仍须决定何时注册、未就绪时状态是什么、失败是否重试和谁负责回滚。
Actuator 的 /actuator/serviceregistry GET 读取本机 Registration 的状态,POST 例如 {"status":"OUT_OF_SERVICE"} 把字符串交给具体 registry。这个字符串是否被接受、消费者是否过滤,完全依赖适配器;没有 Registration 返回 404。
[[source:commons|spring-cloud-commons/src/main/java/org/springframework/cloud/client/serviceregistry/endpoint/ServiceRegistryEndpoint.java|49|68]]
仅在真实适配器支持该状态、管理访问已有授权时,才显式配置 management.endpoints.web.exposure.include=health,serviceregistry 及 management.endpoint.serviceregistry.access=unrestricted。curl -X POST -H 'Content-Type: application/json' --data '{"status":"OUT_OF_SERVICE"}' http://127.0.0.1:18090/actuator/serviceregistry 是状态操作示例;前节 Simple 工程不能据此验证注销。暴露端点与安全授权独立,管理 POST 的 200 只表示本机调用链返回,不表示全局摘流完毕。
stop 的原码依次注销业务实例、必要时注销管理实例,最后 close registry:
[[source:S06]]
它没有等所有消费者确认,也不等待应用的在途事务和连接耗尽。可执行的摘流状态链应由部署控制器与应用共同记录:接到维护意图 → readiness/接单策略变化 → registry 禁用或注销 → consumer 目录更新 → LB 最后一次选择该实例 → 在途结束 → 连接与资源关闭。如果先拒新,旧目录会得到明确拒绝,需要重试预算兜底;如果先注销但继续服务陈旧请求,就必须保留业务端口直到已知传播预算结束。两种策略的错误率、资源占用和在途完成保证不同,不能把顺序画成不存在的原子切换。
正常注销、进程崩溃、中心失联、consumer 持有旧目录要分别验证。崩溃靠具体注册中心失效检测;失联可能长期保留旧实例;已建立连接与正在执行的调用不因目录删除而回滚。恢复也需要状态闭环:实例依赖恢复 → 业务 probe 200 → 注册/启用 → 新目录被观察 → 候选重新出现 → 小流量业务成功 → 全量恢复。会话过期、租约与资源撤权的原理继续回查旧 ZooKeeper 和可靠性页;实际中心传播归 U35/U40。
地址选择、features 与兼容诊断
D05、D06。 注册地址必须从消费者所在网络可达。host=localhost 只对当前网络命名空间有意义;管理端口通而业务端口不通、secure 标志误设导致 TLS 握手失败、多网卡选择容器桥地址,都会出现“注册成功而不能服务”。metadata 不能修复路由、证书或端口错误。选择实例后应记录脱敏的 instanceId / host / port / scheme,并从消费者网络直接验证地址。
InetUtils 的配置示例应放进业务进程的 YAML,且具体 registry 适配器须实际采用 InetUtils 的结果;显式注册 host 可能覆盖它:
spring:
cloud:
inetutils:
ignored-interfaces: ['docker.*', 'veth.*']
preferred-networks: ['10\\.20\\..*']
use-only-site-local-interfaces: false
timeout-seconds: 1
default-hostname: localhost
default-ip-address: 127.0.0.1默认忽略/偏好列表为空,use-only-site-local-interfaces=false;hostname 解析超时为 1 秒,默认回退主机名 / IP 是 localhost / 127.0.0.1。ignored-interfaces 使用 Java 正则;preferred-networks 对字符串进行 matches 或 startsWith,并非 CIDR。启用 site-local 分支后会直接依据 isSiteLocalAddress 返回,不再继续 preferred-networks 条件,不能以为两个条件必定相交。
[[source:commons|spring-cloud-commons/src/main/java/org/springframework/cloud/commons/util/InetUtils.java|128|149]]
本机 loopback 实验只验证本机路由;多网卡、跨区、容器/集群网络尚须各自验证。地址不可达时修正实际注册值并重启/重新注册,再从 consumer 网络重测,而不是先关 TLS 校验。
/actuator/features 描述 classpath 中启用的抽象能力和命名能力,含实现/版本等信息;spring.cloud.features.enabled=false 关闭其自动配置。它依赖 Actuator 与端点暴露,不是业务健康证明;HasFeatures 可声明自定义命名能力。兼容 verifier 的装配默认启用来自条件,而不是 boolean 字段的 Java 零值:
[[source:S54]]
spring.cloud.compatibility-verifier.enabled=false 会跳过这项启动守卫,compatible-boot-versions 可覆盖允许列表;它们都不能修复二进制兼容问题。5.0.4 的 verifier 默认允许 4.0.x / 4.1.x,不代表 Cloud 2025.1 官方支持矩阵已经认可 4.1。排障应先核父 POM、train BOM、实际 jar 和报告,再修版本;不把关闭校验当正常解法。
每服务上下文、supplier 链与选择算法
B01、B02。 每个 serviceId 有自己的 LoadBalancer 子 ApplicationContext,LoadBalancerClientFactory 在环境注入 loadbalancer.client.name,按服务创建算法与 supplier。默认懒初始化,第一次请求可能承担 Bean 创建成本;spring.cloud.loadbalancer.eager-load.clients: [catalog] 显式预热所列服务。预热 context 不保证上游网络、认证或业务缓存都已预热。
默认配置在 reactive 发现可用时构造 withDiscoveryClient().withCaching(),阻塞路径用 withBlockingDiscoveryClient().withCaching();默认算法为 RoundRobinLoadBalancer。classpath 有某个类不代表所有策略一起启用:spring.cloud.loadbalancer.configurations 的 default、zone-preference、health-check、request-based-sticky-session、same-instance-preference、weighted、subset、api-version 是预定义替代链;组合策略应提供一个明确的 supplier Bean,而非期待多个字符串自动按业务顺序叠加。
下面是按服务组合策略的完整配置类。把它保存为 src/main/java/lbconfig/CatalogLoadBalancerConfiguration.java,位于 demo 主扫描包外;在 Consumer 上增加 @org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient(name="catalog", configuration=lbconfig.CatalogLoadBalancerConfiguration.class)。后续所有“替换链”均替换这个方法的 return,不同时声明多个同类型 Bean。
package lbconfig;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
public class CatalogLoadBalancerConfiguration {
@Bean
ServiceInstanceListSupplier instances(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withCaching()
.withZonePreference()
.withHints()
.build(context);
}
}builder 从 base 开始逐个套 decorator:上例数据方向为 Discovery → Cache → Zone → Hint,调用时从最外层向内委派再返回。缓存/health 应贴近发现源,request-aware 过滤在其外;call-get-with-request-on-delegates=true 默认转发 Request,Caching / HealthCheck 是不依赖请求的例外。自定义 decorator 如果只调用 delegate.get(),可能丢失内层 hint、cookie 或 version 的请求信息;应根据是否请求相关明确实现 get(Request)。selectedServiceInstance 回调还要穿过 delegate,让同实例偏好等状态更新;资源销毁也要沿链传递。
[[source:commons|spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java|384|394]]
DiscoveryClientServiceInstanceListSupplier 查询超时默认 30 s,可用 spring.cloud.loadbalancer.service-discovery.timeout=2s 显式调整;异常或超时转空列表,后续选择可能表现为“无实例”,需保留发现日志区分真空目录和控制面故障。这个值不限制 HTTP connect / response。
[[source:S10]]
.timeout(..., Schedulers.boundedElastic()) 指定超时计时调度器,并未给 fromCallable 加 subscribeOn。不能因此宣称阻塞 discovery 已卸载到弹性线程。自定义 blocking discovery 应确认订阅线程、阻塞时长和线程池隔离;事件循环被阻塞是另一类故障,需线程名/栈和 HTTP 延迟实测。
RoundRobin 初始 position 随机,不能要求第一个响应一定是 A。候选列表稳定、串行、无重试时,两个实例会交替;单实例直接返回且不推进 position,空列表返回 EmptyResponse:
[[source:S08]]
RandomLoadBalancer 每次用 ThreadLocalRandom 选索引;十次请求不必五五分配。替代算法可在同一服务配置类提供 ReactorLoadBalancer<ServiceInstance> Bean,使用 environment 中的 LoadBalancerClientFactory.PROPERTY_NAME 和 factory.getLazyProvider(name, ServiceInstanceListSupplier.class) 创建 RandomLoadBalancer;算法改变不会自动增加健康感知或请求耗时反馈。
@Bean
org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer<org.springframework.cloud.client.ServiceInstance>
random(org.springframework.core.env.Environment environment,
org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory factory) {
String name = environment.getRequiredProperty(
org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory.PROPERTY_NAME);
return new org.springframework.cloud.loadbalancer.core.RandomLoadBalancer(
factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}验证分别统计候选、被选实例、出站尝试、响应和业务成功,固定请求数与时间窗。并发完成顺序可能不同于选择顺序;即使轮询完全均分,慢实例也可能积压更多在途请求。
实例缓存的默认值与双缓存成本
B03。 默认链请求先读 CachingServiceInstanceListSupplier,以 serviceId 为键缓存实例列表;miss 才调用 delegate 取首个列表并写入。空列表在查找阶段当 miss,因此不能假定失败后会负缓存 35 秒来保护注册中心。
[[source:S11]]
spring.cloud.loadbalancer.cache.enabled 装配默认开启;配置对象的 TTL 默认 35 s,capacity 256 是初始容量,不是最多 256 个实例,也不是每实例占一项。默认缓存实现与 Caffeine 由 classpath 条件选择。没有 Caffeine 时用 DefaultLoadBalancerCache;有 Caffeine 则用 CaffeineBasedLoadBalancerCacheManager。
[[source:S12]]
spring:
cloud:
loadbalancer:
cache:
enabled: true
ttl: 800ms # 仅本地演示,生产先算控制面QPS与陈旧预算
capacity: 256若显式引入 com.github.ben-manes.caffeine:caffeine,可改设 cache.caffeine.spec: 'initialCapacity=256,maximumSize=1024,expireAfterWrite=800ms'。spec 非空时覆盖通用 ttl/capacity 等构建配置;不能同时填写 ttl=35s 又声称真正用了 35 秒。这里 maximumSize 限的是缓存条目,通常是服务列表键,并非列表中的实例数量。Caffeine 构造实现 在 spec 与通用参数之间明确二选一。
若 SDK 自带新鲜目录缓存,可设 cache.enabled=false,或为该服务构造不含 withCaching 的链。关闭 LB 缓存只消掉这一层,未让 SDK 变成直连中心。缓存应放在 zone/hint/sticky/version 前:若把“hint=canary 得到的 [B]”缓存成 catalog 的公共结果,下一次 stable 请求也可能读到 [B],造成请求之间的选择污染。
失败与恢复推演:t0 缓存 [A],t1 源目录换 [B],直接链已经 B,缓存链仍 A,t0+TTL 后下一次请求回源才可变 B。按默认 TTL,在 4 秒内期待刷新会失败——这是从 35 s 默认值出发的推导,不是本次运行结果;不要把这个推导“修复”为延迟断言或偷偷把默认解释成 800 ms。本次实验显式设 800 ms TTL,只证明“直连 B、缓存仍 A、过期后 B”;默认 35 s 下 4 秒不刷新的反例未在本次运行,属待验证。
健康 supplier 的调度、HTTP 判定与全空分支
B04。 替换前面的服务配置方法为 return ServiceInstanceListSupplier.builder().withDiscoveryClient().withHealthChecks().build(context);,依赖前节明确列出的 WebClient starter;没有 WebClient.Builder 时,手工链第一次懒创建可能直接失败,而不是得到探活后的空列表。预定义 health-check 链还有 Bean 条件,需检查究竟装配了哪条链。blocking 场景有 withBlockingHealthChecks / withBlockingRestClientHealthChecks,需对应 RestTemplate / RestClient;不要在 reactive event loop 上假定这些阻塞调用自动卸载。
本地短周期、按轮输出的显式配置如下;它与默认配置是两组实验:
spring:
cloud:
loadbalancer:
health-check:
initial-delay: 0s
interval: 100ms
path:
default: /health/200
catalog: /health/200
refetch-instances: true
refetch-instances-interval: 100ms
repeat-health-check: false
update-results-list: false默认 initial-delay=0、interval=25s、refetch-instances=false、refetch-instances-interval=25s、repeat-health-check=true、update-results-list=true。默认 path 为 /actuator/health,端口取实例业务端口;可设 health-check.port。指定单独管理端口探活成功,不能证明业务端口可用。服务专属 path 存在且为空/null 时源码直接视为健康;禁用探活不是摘流。
[[source:S15]]
真实判定在 builder 的 aliveFunction:GET 后转成 bodiless entity,只有 HTTP 200 为 true,不解析 JSON 的 status。
[[source:commons|spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java|396|406]]
因此 200 {"status":"DOWN"} 仍可入选;204 不入选,503 / 连接异常也不入选。如果需要业务依赖就绪,应让约定路径返回相应 HTTP 状态并限制探测成本,或提供经过验证的自定义健康函数,不能只改变 body。
health flux 对初始发现列表探测;refetch=false、repeat=true 可以长时间只反复检查最初那批实例,新实例不会因为“有定时健康检查”自然进入。refetch=true 才重复订阅发现源;若每次重新发现就完成一轮探活,可设 repeat=false,避免两套重复节奏。它使用 replay(1)/refCount(1) 共享最近结果,因此通常不再套 LB cache;delaySubscription 影响首次结果,关闭时通过 DisposableBean 结束自己的订阅,不能留下独立轮询器。
[[source:S13]]
update-results-list=true 时,同轮每个成功探针都会增量发布列表,先完成的 A 可以先形成 [A],随后才 [A,B];false 才聚齐本轮。探针可能等到 interval 所表示的超时才结束,因此 interval 既影响轮间 backoff,也影响单探针超时,不能当作严格固定频率时钟。
文档冲突必须保留: 固定版本官方文档/Javadoc 描述无健康实例会回退全部,但 5.0.4 实际 healthCheckFlux 从空 result 开始,只加入 alive 实例。全部 false、异常或超时都会发空列表,源码如下:
[[source:S14]]
同版本官方测试 的方法名仍叫 shouldEmitAllInstancesIfAllIsAliveChecksFailed,实际断言是 expectNext(Lists.list())。阅读测试不能写成已运行。本组已在本机真实运行:一台 UP 选 B;两实例都失败后 Gateway 返回 503;200 且 body 为 DOWN 仍算 UP 并选 A;204 不算 UP;A 恢复 200。health 层返回的是候选列表,503 是 Gateway 无实例的默认结果,不是 health 自己返回 HTTP 503。
权重的实际展开与非法值
B05。 替换 supplier 链为 withDiscoveryClient().withCaching().withWeighted().build(context),或显式配置 spring.cloud.loadbalancer.configurations=weighted。metadata 中的 weight 是正整数相对权重;A=1、B=3 时,展开后的候选相当于一份 A、三份 B,之后由原算法选择。它不测延迟、不自动降权,也不表示 B 必须处理四分之三的“业务成功数”。
[[source:S22]]
缺 metadata / 缺 key 默认 1,解析异常、非正整数、WeightFunction 抛异常也都回 1。0、-1、abc 不能作为下线标记,尤其 weight=0 仍会接流。自定义权重可用 withWeighted(instance -> ...),但函数应有边界、可解释且不可阻塞取远端负载;生产先验证归一化和配置拒绝规则。
内部 LazyWeightedServiceInstanceList 用权重和/GCD 分配 expanded 数组,名称含 Lazy 不表示没有数组成本。例如权重 1000/3000 约为 4 个展开槽,而 1/1000000 约为 1000001 槽;一千个服务、多份 consumer context 会放大内存。极端和还需防溢出,框架的 fallback 不替代入口校验。
[[source:S55]]
验证以固定候选及窗口统计选择比例;先测合法 1:3,再把 B 设为 0 确认仍可入选,最后恢复正整数并确认分布。真正摘流应移除候选或使用被验证的健康/注册状态,不以相对权重冒充排除策略。
zone 和 hint 的优先与宽松回退
B06、B07。 前面组合链已经启用 zone 和 hint。设 spring.cloud.loadbalancer.zone=east,从 metadata 的 zone 选同区实例;无本地 zone 或没有同区候选时回退 delegate 全部列表。Eureka 有专门的 zone 自动集成,其他客户端不要假设能自动推断本区。
[[source:S18]]
hint 可设 spring.cloud.loadbalancer.hint.default=stable 和服务专属 spring.cloud.loadbalancer.hint.catalog=canary,默认 hint 字符串为 default。实际请求在 RequestDataContext 中携带 X-SC-LB-Hint 时,header 优先于属性;hint-header-name 可改名。它精确匹配 metadata.hint,无 hint 或无匹配则回退原候选。
[[source:S19]]
输入只有 east/stable 的 A 与 west/canary 的 B,先 Zone(east) 再 Hint(canary),hint 在 [A] 中没有匹配,会回退 [A];不是跨过 zone 去全局找 B。反过来先 hint 得 [B],zone 无同区又回退 [B],结果不同。这是链的组合语义,不能把两个策略当互不影响的独立开关。
两者适合软偏好。要求“流量只能留 east”或“特定租户只能去授权版本”时,宽松回退违反不变量,需单独设计失败关闭的过滤策略并测试空集。入口应删除或验证外部可控 hint,日志只记低基数的策略结果;hint 不得成为授权凭据。修复先核对原目录 metadata,再核对当前链及顺序,最后对无匹配和恢复匹配重放同一输入。
同实例偏好、请求粘性与 Cookie 写入边界
B08。 withSameInstancePreference() 记住该 supplier 先前通过 selectedServiceInstance 回调选中的实例;仍在候选里就只返回它,不在则清空记忆并回退。状态属于该 consumer 的该服务 supplier,不是按用户分开;两个不同用户可能一起偏向 A,不能据此提供会话隔离。
[[source:S20]]
withRequestBasedStickySession() 则读取 RequestDataContext 中的实例 cookie,匹配 instanceId 返回单元素列表。默认 cookie 名 sc-lb-instance-id;缺 RequestDataContext、缺 cookie、实例已消失均回退当前候选。只要上游过滤已经排除 A,即便 cookie 仍指 A,也不会复活 A。cookie 可伪造,不能承载授权或数据所有权。
[[source:S21]]
使用时替换链为 withDiscoveryClient().withCaching().withRequestBasedStickySession().build(context),配置为:
spring:
cloud:
loadbalancer:
sticky-session:
instance-id-cookie-name: sc-lb-instance-id
add-service-instance-cookie: trueadd-service-instance-cookie 默认 false,真实路径含 sticky-session。这项 true 必须沿实际执行链解释:Commons WebClient 的 LB filter 会给发往上游的 ClientRequest 添加实例 cookie;它不直接操作浏览器响应。Gateway 5.0.4 有专用 LoadBalancerServiceInstanceCookieFilter,在 LB 选择之后读取已选实例,同样追加出站请求的 Cookie header。
[[source:gateway|spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/LoadBalancerServiceInstanceCookieFilter.java|79|91]]
这里没有向浏览器写 Set-Cookie。若需要浏览器保存粘性信息,必须由明确的上游/网关响应机制签发并设置作用域、期限和安全属性,且要验证是否跨 serviceId 冲突;本开关不提供整套浏览器会话协议。本次实验只检查 echo 上游收到的 Cookie:手动带 sc-lb-instance-id=B 选 B,带不存在实例的 cookie 回退到可用候选;客户端响应是否出现 Set-Cookie、以及移除/恢复实例后的粘性行为未在本次运行,属待验证。不要只看某次响应来自 A 就宣布粘性成立。
确定子集与 API 版本的严格空集
B09。 withSubset() 或 configurations=subset 对大实例目录选有限子集,再交算法选择。默认 spring.cloud.loadbalancer.subset.size=100,可显式设置正整数如 20;subset.instance-id 决定 client 身份,未配置时从客户端实例身份信息解析,最终可生成 UUID。重启、不同 client 身份或目录顺序变化都可能改变子集;稳定身份与一致输入才可谈可重复选择。
[[source:S23]]
当总实例数 ≤ size 时原样返回;否则用 client identity 的 hash 选 bucket,对复制列表做确定性 shuffle 后切片。它不是“同一个用户永远落同一实例”的一致性哈希会话,也不是健康过滤。若所有消费者误用相同固定 client-id,它们可能共同集中在同一小子集;反之每次启动换身份可能引发连接预热。应记录每客户端子集、全集覆盖和故障后的剩余容量,size=0 等非法值在绑定/计算前拒绝。
5.0 增加 ReactiveApiVersionServiceInstanceListSupplier 和 BlockingApiVersionServiceInstanceListSupplier,分别面向 WebClient / RestClient。启用 configurations=api-version,或替换链尾为 .withReactiveApiVersioning() / .withBlockingApiVersioning();metadata 键是大写 API_VERSION,不是 hint,也不是 Gateway 路由的 Version 谓词。示例使用前节两个版本实例:
spring:
cloud:
loadbalancer:
configurations: api-version
clients:
catalog:
api-version:
header: X-API-Version
required: false
default-version: '1.0'
fallback-to-available-instances: false真实前缀为 loadbalancer,固定文档中 load-balancer 拼写不能照抄。还可以配置 query-parameter、path-segment、media-type-parameters;应选定一致的请求取值策略,发生冲突时按解析策略实际顺序验证。默认 SemanticApiVersionParser 解析后以 Comparable 的 equals 匹配实例版本;metadata 缺版本不匹配一个指定版本。默认 required=false,default-version 未设,fallback-to-available-instances=false。
[[source:commons|spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ReactiveApiVersionServiceInstanceListSupplier.java|112|141]]
请求 1.0 得 A,2.0 得 B,3.0 无匹配得空列表;没有请求版本且没有 default-version 时,也可能走到空列表,而不是“required=false 就回所有”。required=true 的缺版本会在解析/校验路径报错;非法版本也可能在解析阶段失败,与无匹配的空列表不同。显式 fallback=true 才回退所有,代价是可能调用不兼容版本。子 context 可以提供自定义 ApiVersionParser / ApiVersionStrategy;不要误用服务端父 context 的版本策略当客户端路由策略。
对照无匹配行为:zone / hint 回退 delegate,cookie 失效回退当前候选,version 默认空,health 全 DOWN 也空。恢复时修复 metadata 或请求版本后确认特定版本重新入选;不能为“恢复成功率”直接把版本 fallback 打开而破坏兼容性不变量。
重试装配、尝试计数与总预算
B10。 “默认重试”先回答哪条执行链。LoadBalancerProperties.Retry 的 enabled 字段为 true,但 reactive WebClient 的普通 filter 条件 matchIfMissing=true,可重试 filter 要求显式 spring.cloud.loadbalancer.retry.enabled=true。字段默认与自动配置默认不等价:
[[source:S26]]
阻塞 RestTemplate / RestClient 的重试还依赖 Spring Retry 的 RetryTemplate 在 classpath;没有 org.springframework.retry:spring-retry 时不能靠该字段生成重试。Gateway 的 ReactiveLoadBalancerClientFilter 负责选实例,不会因为打开 WebClient 的 retry.enabled 就给 Netty 出站自动重试;Gateway 要单独配置 Retry filter。Feign 还有自己的客户端与重试约定,归 U33。
启用适用的 LB 重试后,默认同实例重试 0、下一个实例重试 1;通常首次加一次下实例最多两次尝试。默认只 GET,retry-on-all-operations=false,retry-on-all-exceptions=false,retryable-status-codes 为空;默认异常集合含 IOException、TimeoutException 和两种 RetryableStatusCodeException,不能把任意 500 或任意异常都说成自动重试。
spring:
cloud:
loadbalancer:
retry:
enabled: true
max-retries-on-same-service-instance: 0
max-retries-on-next-service-instance: 1
retry-on-all-operations: false
retry-on-all-exceptions: false
retryable-status-codes: [503]
backoff:
enabled: true
min-backoff: 20ms
max-backoff: 100ms
jitter: 0.5backoff 原默认 enabled=false、min=5ms、max=Long.MAX_VALUE 毫秒、jitter=0.5;固定文档里一处把“启用”写成 false,应以装配与实现为准。上述是 reactive 退避例子;阻塞策略与全局总 deadline 必须独立验证,不把 Duration 参数当端到端 deadline。
默认 avoid-previous-instance=true 的适用自动装配会加 RetryAwareServiceInstanceListSupplier,尽量排除上一实例;但过滤后无别的实例会回退全列表,所以“下一个实例”不保证真换机器。
[[source:S27]]
请求链是 选择 A → 执行 → 判断异常/状态/方法/预算 → 可选退避 → 带上一次实例上下文重新选择 → 再执行。状态型重试还需要正确处理和释放前一响应;外层调用的 get-body、retrieve 错误映射并不都发生在 LB filter 同一个环节。验证应让 A/B 记录每次真实尝试,并分别测试 GET503、POST503、连接拒绝、单实例与恢复。
假设网关 Retry 配置 retries=3,最多四次总尝试;被调用服务内部的 LB 客户端每次再尝试两次,下游客户端每次又两次,且各层均触发满额,则一次入口最多 4×2×2=16 次末端尝试。若网关只是直接代理,没有内部 LB WebClient 重试层,则不能凭空乘这个 2。旧可靠性教材的“三层各三次总尝试=27”仍是另一个明确假设,不能把 retries=3 错当总尝试=3。
统一预算例子:总 800 ms、入口与返回预留 100 ms,每次完整尝试 150 ms、两次间退避 80 ms,三次耗 100+450+160=710 ms,剩 90 ms;第四次不够就不再发送。UNKNOWN 写入、幂等和恢复责任的权威讲解仍在超时重试、熔断限流降级。本页只补 Cloud 的装配和计数,不能把 HTTP 超时说成下游事务已回滚。
生命周期、指标与故障归因
B11、B12。 LoadBalancerLifecycle 的 onStart 在选择前,onStartRequest 能拿到选择 Response,onComplete 带 CompletionContext:状态 SUCCESS / FAILED / DISCARD、LB 请求、已选实例、可用响应与 Throwable。RequestDataContext 保存请求 URI、方法、头/cookie、hint 等;ResponseData 保存响应状态/头。这些是观测上下文,不应把原始 cookie、令牌或用户 ID 放进日志和标签。
以实际 Gateway 执行链为例,空选择先触发 DISCARD,再抛无实例异常,外层错误回调可能再次 FAILED;有 HTTP 响应包括 5xx 也可能回调 SUCCESS,因为该状态描述“执行得到响应”,不等于业务成功。取消路径没有天然保证恰一个终态回调。自定义 lifecycle 必须按实际调用点做幂等汇总,并区分每次尝试与入口请求,不能用回调条数充当业务请求数。
[[source:gateway|spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java|113|133]]
选实例后的请求变换扩展点,阻塞链为 LoadBalancerRequestTransformer,WebClient 为 LoadBalancerClientRequestTransformer,默认 order=0。可追加低敏实例诊断头,但不能凭该头替代下游鉴权;多个 transformer 需要声明顺序,避免重复追加或覆盖用户输入。教学 WebClient transformer 如下,可作为主应用中的 Bean:
@org.springframework.context.annotation.Bean
org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerClientRequestTransformer
selectedInstanceHeader() {
return (request, instance) -> org.springframework.web.reactive.function.client.ClientRequest
.from(request)
.headers(headers -> headers.set("X-Study-Selected-Instance", instance.getInstanceId()))
.build();
}统计需同时有 MeterRegistry 并显式设 spring.cloud.loadbalancer.stats.micrometer.enabled=true;并非带 Actuator 就默认开启。spring.cloud.loadbalancer.stats.include-path=true 是路径标签字段默认,含 ID 的路径会造成高基数,教学/生产示例应显式 false。RestTemplate 拿不到某些 uriTemplate,更应注意原始路径。装配条件如下:
[[source:commons|spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerStatsAutoConfiguration.java|36|46]]
loadbalancer.requests.active 是每实例 Gauge,onStartRequest 加一;success / failed 是 Timer,discard 是 Counter。DISCARD 提前返回,不走正常 active 减一;Timer 还要求上下文具备计时起点,某条回调发生不代表一定生成一次 Timer。Gateway 自己的 requests 指标是入口/路由层,不能同 LB Timer count 相加当请求总数。
[[source:commons|spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycle.java|117|150]]
排障最小观测记录应关联一个合成请求 ID、serviceId、目录版本/年龄、过滤阶段候选数、选择实例、尝试序号、实际 HTTP 状态、错误类型与耗时。依次比较正常 200、HTTP503、连接失败、无实例、取消,再看每个指标的单次增量与 active 是否归零;不要把自动配置成功当观测后端已收到数据。取消/多回调等高风险边界目前以源码审读说明,须在对应执行链实跑后才能写运行通过。
RestTemplate、RestClient、WebClient 与 HTTP Service groups
B13。 客户端 LB 使用虚拟 host,例如 http://catalog/items,而非让 DNS 真解析 catalog。阻塞 LoadBalancerInterceptor 从原 URI host 取 serviceName,交 BlockingLoadBalancerClient 选择并重建 scheme/host/port,然后才委托真实 HTTP 执行。无 LB 的普通 builder 则可能直接去 DNS,表现为 UnknownHost。
[[source:commons|spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerInterceptor.java|49|56]]
WebClient.Builder 必须作为带 @LoadBalanced 的 Bean 被后处理,创建出来的 WebClient 才带相应 filter。完整 Bean 例子显式应用 Boot 4 的 customizer,依赖前面列出的 webclient starter:
package demo;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.webclient.WebClientCustomizer;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration(proxyBeanMethods = false)
public class Clients {
@Bean
@LoadBalanced
WebClient.Builder catalogWebClient(ObjectProvider<WebClientCustomizer> customizers) {
WebClient.Builder builder = WebClient.builder();
customizers.orderedStream().forEach(c -> c.customize(builder));
return builder;
}
}注入时用 Clients 之外的业务 Bean 构造参数 @LoadBalanced WebClient.Builder builder,调用 builder.build().get().uri("http://catalog/items").retrieve().bodyToMono(String.class);在 WebFlux 返回 Mono,不在事件循环 block。多个普通/LB builder 通过 qualifier、@Primary 区分。Boot customizer 提供观测等能力;直接 new builder 虽可能能发 HTTP,却可能丢掉连接器、TLS 和观察配置。
RestClient 使用 Boot 4 spring-boot-starter-restclient,配置 Bean 为 @Bean @LoadBalanced RestClient.Builder balanced(RestClientBuilderConfigurer c) { return c.configure(RestClient.builder()); };类型来自 org.springframework.web.client.RestClient 与 org.springframework.boot.restclient.autoconfigure.RestClientBuilderConfigurer。调用为 builder.build().get().uri("http://catalog/items").retrieve().body(String.class),属于阻塞 API,应在适合的线程使用。RestTemplate 可声明 @Bean @LoadBalanced RestTemplate template(RestTemplateBuilder b) { return b.build(); },类型来自 org.springframework.web.client.RestTemplate 与 Boot 4 org.springframework.boot.restclient.RestTemplateBuilder;如自己构造 builder,应经 Boot 的 RestTemplateBuilderConfigurer 应用配置。对应调用 getForObject("http://catalog/items", String.class)。这些方法放在带 @Configuration 的类中;不需要为了调用 RestClient 加 MVC server starter。
RestTemplate / RestClient / WebClient 用的是不同 interceptor/filter 执行链;自动注入的 builder 是否有 LB,要看注解后处理或下面的 group configurer,不能只看变量名。固定版本里的旧构造器有弃用迁移提示,应使用 ObjectProvider / factory 等当前入口;老教程的 Commons Apache/OkHttp HttpClientFactory 在 5.0.4 固定源码树中已不构成本页可用入口。LB 不拥有 HTTP 连接池,Netty / Apache / JDK 的连接池、DNS、TLS、connect / response timeout 应由所选实际客户端配置,Feign 的独立客户端栈归 U33。
5.0 HTTP Service Client 自动集成不能漏掉。 用 Framework 的 @ImportHttpServices 注册 group,Boot 为该 group 创建 RestClient / WebClient builder;Cloud 的 group configurer 再按 base-url 决定是否装 LB。RestClient 方式示例依赖 restclient starter 与 LoadBalancer starter:
package demo;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.registry.ImportHttpServices;
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(group = "catalog", types = CatalogApi.class)
class HttpServices {}
interface CatalogApi {
@GetExchange("/items")
String items();
}调用者注入 CatalogApi 后执行 items。spring.http.serviceclient.catalog.base-url 未配置/null,Cloud 以 groupName 推导 http://catalog 并加入 LB interceptor;显式 lb://catalog/api 则改成初始 http 并加入 LB;显式 http://catalog/api 或 https://fixed.example/api 则不添加 LB,按普通 HTTP 执行。选择到 secure 的 ServiceInstance 后,LB 可以将初始 http 改为 https。WebClient group 使用对应 reactive configurer,取相同属性并加 filter,不能从 blocking group 的配置推导两种客户端都同时生效。
[[source:S28]]
将 group base-url 从未设改为普通 http URL,可能使同一接口从服务发现切成 DNS/固定地址访问。这是架构行为变更,必须以最终出站地址和实际 jar / builder 装配验收。configurer 找不到必要的 deferring interceptor/filter 会报错,不能把接口代理能生成当作请求已验证。适用实验覆盖 null、lb、普通 http 三种入口和 secure 实例,编译与 HTTP 状态分别记录。
按客户端配置、扩展隔离与 AOT 输入
B14。 全局 spring.cloud.loadbalancer 是默认,clients.<serviceId> 设置具体客户端属性;例如全局 health initial-delay=1s、catalog health interval=30s,合并后 catalog 同时得到 1s 与 30s。hint.<serviceId> 与 health-check.path.<serviceId> 是已经存在的 map 入口,不要求再套一层 clients;不能把这些键机械改名。
spring:
cloud:
loadbalancer:
health-check:
initial-delay: 1s
clients:
catalog:
health-check:
interval: 30s
retry:
enabled: false
eager-load:
clients: [catalog]有些属性决定全局自动装配:enabled、retry.enabled、cache.enabled、stats.micrometer.enabled。全局 retry 没装出来,不能只对某服务写 enabled=true 就期待它出现;全局已经开,才可对具体服务收紧策略。全局禁缓存也不允许凭 client 字段变出 CacheManager;只对一服务禁缓存可为它构造不含 withCaching 的 supplier。
@LoadBalancerClient(name="catalog", configuration=...) 只应给 catalog 子 context 提供策略。若配置类被主 @ComponentScan 扫到,算法或 supplier 可能进入父 context 干扰其他服务,出现“只改 catalog,inventory 也改了”。前面的 lbconfig 包隔离是故意的;使用 defaultConfiguration 则明确表示全体默认配置。扩展点包括算法、supplier / WeightFunction、Lifecycle、transformer、ApiVersionParser/Strategy,分别对应候选、选择、执行后处理、观测,不能把重试副作用塞进选实例函数。
固定 AOT 说明 要求提前给出服务 ID:@LoadBalancerClient 的 name/value,或 eager-load.clients。AOT 要在构建时知道需生成的子 context,运行时任意新 serviceId 不能照 JVM 懒加载方式承诺支持。Native 工具链、镜像和通用限制归 U28;本页没有 Native 编译/运行证据。演进到 Native 前,应固定服务 ID 清单、生成输入和扩展 Bean,再用真实请求验证各服务,不能以 JVM 启动代替。
架构推演:撤权窗口、摘流与恢复成本
本节是 D01–D06、B01–B14 共用的 K08 案例:目录来源决定年龄上界,context/缓存/health/subset 决定负载和候选,zone/hint/sticky/version 决定允许回退条件,算法/重试/HTTP client 决定实际尝试与在途成本,生命周期/指标负责证明。所有数字为给定假设的计算推演,不是压测结果。
设两个区各 30 个 catalog 实例,每实例可持续 400 request/s;全站 12000 request/s,100 个消费者,均匀时每消费者 120 request/s、每实例 200 request/s。单次业务处理最长 2s,入口总 deadline=3s,最多两次总尝试。控制面更新在健康网络下 2s 内可见;一个实例的目录响应按 200B 估算,全目录约 12KB,不含 HTTP/SDK/对象开销。业务不变量是:已撤权实例不得在撤权截止时间后提交新效果;维护摘流要在已知窗口后不再选择它,并让既有请求在预算内完成;API1 请求不可静默切到不兼容 API2。发现与缓存本身不能执行资源侧提交授权。
方案甲:SDK 缓存目录 + LB TTL 缓存,以可用性优先保留旧列表。 假设 SDK 最慢 30s 更新,LB TTL=35s,最不利健康网络窗口为 2+30+35=67s;最后被选择的请求还能处理 2s,维护关闭前保守预算约 69s,再加明确的调度余量。一个实例在 200 request/s 下,67s 内可能承接约 13400 次陈旧选择;这不是承诺的故障数,而是潜在暴露量。只等 stop 返回就关业务端口,会把其中一部分变成连接错误/UNKNOWN。
SDK 按 30s 向控制面刷新时,100 个 consumer 的量级为 100/30≈3.33 次/s;平均目录载荷约 40KB/s。LB 默认缓存再减少 SDK getter 调用,但不一定进一步降低中心 QPS。每 consumer 一份 12KB 的 LB 目录,100 份约 1.2MB 原始描述量,还不含 SDK 的另一份、Java 对象与 expanded weights。控制面失联若无目录最大年龄限制,上述 67s 上界失效;运维/应用负责人必须决定是否继续服务旧列表、何时拒绝并如何补偿。
此方案适合短时控制面失联但数据面仍通、允许陈旧路由的读取。恢复时先恢复目录链,确认 consumer revision/年龄趋同,再逐步恢复实例,而非一次重启全部 consumer。要求 5s 内摘流时,默认 35s 缓存已直接不合格;演进触发是摘流 SLO、陈旧选择率或中心故障策略发生变化,而非单凭觉得缓存多余。
方案乙:新鲜度有界的目录 + 无 LB cache + 业务健康探测,以明确排除优先。 假设替换为能在 2s 内更新的 SDK/可控目录,关闭 LB cache;health refetch=1s、repeat=false、每轮 probe 最长1s、update-results-list=false。删除实例在顺利条件下可按 2+1+1=4s 预算观察到;实例还在目录但服务故障可按下轮探测间隔加 probe 耗时约 2s 检出。数值都要求真实网络、调度和上游 timeout 满足假设,不是 interval 一项给出的硬 SLA。100ms 的本地实验参数不能直接充当生产方案。
不做 subset 时,100 consumers 对60实例每秒一轮约 6000 probes/s;若每探针来回1KB,约6MB/s,不含连接和 TLS 成本。每实例约100 probes/s;若把探针当普通业务等价成本,400 request/s 容量里要先留100,余300。任一区失效后,剩30×300=9000 request/s 小于12000,必须限流至少3000 request/s、扩容或使用更便宜健康端点,不能只配置跨区回退就宣称可用。
可设每 consumer 子集20,把探针量降至2000/s,代价是每个子集须有足够冗余与跨客户端分散,20×400=8000 request/s 是单子集理论业务上限而非全站自动均摊保证;如果所有100 consumers 恰落同一子集,其12000 request/s 会超载。权重、同实例偏好和 cookie 又可能进一步集中流量,必须验证每子集的选择分布、故障剩余容量与重建时连接峰值。跨区 fallback 可以救容量,也会增加延迟/流量费;有地域强约束时必须 fail-closed 并接受容量下降。
方案乙由客户端平台负责人维护探测与目录年龄预算,服务负责人维护轻量 readiness、拒新和在途上限;注册中心团队只保证其协议传播部分。全部 DOWN 返回空集时,由入口做明确失败或受限降级,不能回放已知故障列表。恢复应先恢复一个实例的业务端口/依赖,确认 probe200、候选重新出现、特定 API 版本正确,再逐步增流;对20实例子集和另一区分别做恢复测试。频繁探活抖动时需要设计经过验证的迟滞策略,5.0.4 这条链不自带完整“连续N次才恢复”的业务状态机。
撤权与摘流要分开给窗口。 方案甲正常传播窗口67s、方案乙假设窗口4s,只能限制“仍被选择”,不能限制“何时还可提交效果”。若资源侧授权快照最多缓存5s,授权传播2s,且在提交效果时原子核对版本,允许的撤权窗口仍可能到7s,不能满足5s;若只在请求进入检查,还可能再加2s在途处理。要满足严格撤权,可以在资源提交点做在线授权/epoch校验并失败关闭,代价是新依赖、每12000 request/s 的校验负载及可用性损失;或把授权租约缩短到有证据的上界并在提交点核验,代价是频繁续租和时钟/网络假设。目录删除、weight0、cookie 更新和 health200 都不能替代这个资源守卫。
deadline 也限制恢复尝试:3s 内若第一次等待2s,再退避100ms,剩900ms,不足第二次完整2s尝试,应提前失败;“最多两次”是上限不是义务。业务写入 UNKNOWN 应按幂等键查状态/恢复,责任归业务调用方;网关不能通过换实例证明没发生副作用。评审两方案时至少记录:中心 QPS、probe QPS/字节、每实例余量、目录年龄最大值、最后一次陈旧选择、在途归零时刻、撤权后提交数、恢复期间错误率与重试放大;任一上界没有故障证据,就标待验证。
实验读法与旧内容唯一归属
本页配置是使用入口,连续原码是机制证据,数字是可计算推演。主任务的真实 WebFlux Gateway / 可控目录 / 合成上游实验入口为 labs/cloud-gateway/README.md,运行证据为 evidence/u32-2026-10-26.json;它拥有最终工具版本、输入、HTTP 输出、退出状态、产物及定向清理。可控目录不是注册中心,echo 上游不是生产业务,不能把本地健康/缓存实验外推成真实 Nacos/ZK 集群传播通过。
2026-10-26/27 的一次入口运行在本机完成(跨零点,最终于 27 日 00:46 结束;证据归档为 u32-2026-10-26.json),exit 0,15 个真实 socket HTTP 测试:webflux 12、mvc 1、proxy-mvc 1、proxy-webflux 1。与本页直接相关的观察是:轮询两实例交替、空目录 503、单 A 恢复 200;显式 800ms TTL 下得到“直连 B、缓存仍 A、过期后 B”(默认 35s 的 4 秒反例未在本次运行,按默认值推导);health 在显式加入 spring-boot-starter-webclient 后,一台 UP 选 B、全部探活失败 503、200 且 body 为 DOWN 仍算 UP 选 A、204 不算 UP、A 恢复后 200;Redis 令牌桶 replenishRate=1/burst=60/requested=60 下 alice 首次 200、紧接 429,bob 独立 200,空 key 403,Redis 链路不可用期间放行 200 且 X-RateLimit-Remaining=-1,恢复后仍 429。运行环境为 Oracle JDK 21.0.5、Maven 3.9.9、macOS 26.7.1 x86_64、Redis 8.2.1;候选基线 Liberica 21.0.11 与 aarch64 未复现。未运行项(真实注册中心、集群摘流、跨副本限流、压测、TLS、WebSocket/gRPC)在 README 的“已知未运行”单列,不以源码或官方测试审读冒充。
旧页正确内容不搬走、不删除、不重复建立第二权威解释:Nacos注册发现与配置一致性 保留2.4.3临时/持久注册原码、配置快照与多dataId混版、原题答和模型,配置主归U34、Nacos实现主归U40;本页只接它的目录/可服务边界。ZooKeeper协调与会话恢复 保留3.9.2 quorum、session、watch、UNKNOWN create、fencing及原码题答,主归U39、适配归U35。Dubbo调用链与服务治理 保留3.2.15代理/Invoker、Failover目录、deadline和优雅下线原码,Cloud客户端对照归U33。超时重试、熔断限流降级 保留端到端deadline、UNKNOWN、27次放大、隔离与业务恢复的权威讲解和题答,本页仅新增Cloud装配差异。
labs/middleware-senior 的 config/session/budget 是有限模型,旧固定源码核查仍有其价值;它们不证明本次Gateway网络集成。旧工程基线仅导入Cloud BOM的启动记录,也不能替代本次LB实际classpath。U32拥有Commons/发现/LB的解释,不抢占U34配置原子性、U35平台适配、U39协调协议、U40 Alibaba实现、U28通用Native或U33远程容错主归属。
闭卷验收
本页 32 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。