系列文章第6篇 | 50天手搓Spring Cloud Gateway:44项功能+561测试用例的完整实践
系列导航
- 第一篇:控制平面/数据平面架构设计与动态路由实现
- 第二篇:安全防护体系与性能优化
- 第三篇:弹性设计与限流降级
- 第四篇:全链路可观测性与AI Copilot智能运维
- 第五篇:Kubernetes部署与测试保障
- 第六篇:高级路由与负载均衡实战 ← 本篇
前言
在前5篇文章中,我们介绍了网关的架构设计、安全防护、弹性设计、可观测性和Kubernetes部署。但一个企业级API网关的路由能力远不止这些。
在实际的生产环境中,你会遇到这些场景:
- 灰度发布:新版本上线,需要10%流量先验证,没问题再逐步放量
- 多版本共存:user-service有v1、v2两个版本,需要按Header/cookie分配流量
- 遗留系统集成:有些后端服务没有注册到Nacos,只有固定IP:Port
- 节点动态调整:在Nacos控制台修改节点上下线、权重,网关需要实时生效
- 健康检查:如何判断节点是否健康?不健康节点如何处理?
这些需求,我们的网关全部支持。
本文将深度解析以下核心功能:
- 灰度发布系统设计(4种灰度规则 + 百分比流量分配)
- 两层负载均衡(服务级 + 节点级)
lb://vsstatic://双协议设计- Nacos控制台节点管理(上下线、权重、健康状态)
- 混合健康检查机制(主动 + 被动 + 即时)
- 节点故障降级策略(可用性优先设计哲学)
- 4种负载均衡算法(平滑加权轮询、随机、一致性Hash、简单轮询)
一、灰度发布系统设计
1.1 为什么需要灰度发布?
在生产环境发布新版本服务时,直接全量上线风险极大:
全量发布(高风险):
user-v2 直接替换 user-v1
→ 如果有Bug,100%用户受影响
→ 回滚需要时间,损失已经造成
灰度发布(低风险):
user-v1 (90%) + user-v2 (10%)
→ 只有10%用户受影响
→ 发现问题立即回滚,损失可控
1.2 灰度规则配置
灰度规则存储在路由的 metadata 中,通过 JSON 配置:
{
"routeId": "user-service-route",
"uri": "lb://user-v1",
"metadata": {
"multiServiceConfig": {
"mode": "MULTI",
"services": [
{"serviceId": "user-v1", "version": "v1", "weight": 90, "enabled": true},
{"serviceId": "user-v2", "version": "v2", "weight": 10, "enabled": true}
],
"grayRules": {
"enabled": true,
"rules": [
{
"type": "HEADER",
"name": "X-Version",
"value": "v2",
"targetVersion": "v2"
},
{
"type": "COOKIE",
"name": "version",
"value": "beta",
"targetVersion": "v2"
},
{
"type": "QUERY",
"name": "preview",
"value": "true",
"targetVersion": "v2"
},
{
"type": "WEIGHT",
"value": "10",
"targetVersion": "v2"
}
]
}
}
}
}
1.3 4种灰度规则类型
| 规则类型 | 匹配方式 | 使用场景 |
|---|---|---|
| HEADER | 匹配 HTTP Header 值 | 内部测试:设置 X-Version: v2 路由到新版本 |
| COOKIE | 匹配 Cookie 值 | Beta用户:设置 version=beta 体验新版本 |
| QUERY | 匹配 URL Query 参数 | 临时验证:访问 ?preview=true 查看新版本 |
| WEIGHT | 百分比流量分配 | 灰度放量:10%流量自动路由到新版本 |
1.4 灰度规则匹配逻辑
private String matchGrayRules(ServerWebExchange exchange, MultiServiceConfig config) {
GrayRuleConfig grayRules = config.getGrayRules();
if (grayRules == null || !grayRules.isEnabled()) {
return null;
}
// 按配置顺序匹配,First-Match-Wins
for (GrayRule rule : grayRules.getRules()) {
String matchedVersion = matchSingleRule(exchange, rule, config);
if (matchedVersion != null) {
return matchedVersion; // 匹配成功,立即返回目标版本
}
}
return null; // 所有规则都未匹配,走权重分配
}
匹配流程:
请求到达
│
▼
按顺序检查灰度规则
│
├─ HEADER: X-Version=v2? ──是──> 路由到 v2
│
├─ COOKIE: version=beta? ──是──> 路由到 v2
│
├─ QUERY: preview=true? ──是──> 路由到 v2
│
└─ WEIGHT: 10%概率? ──是──> 路由到 v2
│
▼
都未匹配 → 走权重分配(平滑加权轮询)
1.5 百分比流量分配实现
private boolean shouldRouteByPercentage(int percentage) {
return ThreadLocalRandom.current().nextInt(100) < percentage;
}
原理:
- 使用
ThreadLocalRandom生成 0-99 的随机数 - 如果随机数 < 百分比值(如10),则路由到目标版本
- 每次请求独立判断,保证长期流量分布接近设定比例
效果:
1000个请求:
~100个 → v2 (WEIGHT规则匹配)
~900个 → 走权重分配
长期统计:
v1: 90% ± 2%
v2: 10% ± 2%
1.6 灰度规则实战示例
场景1:内部测试新版本
# 设置Header,强制路由到v2
curl -H "X-Version: v2" http://gateway/api/user/123
场景2:Beta用户体验
# 设置Cookie,Beta用户访问新版本
curl -b "version=beta" http://gateway/api/user/123
场景3:临时验证
# URL参数,快速验证新功能
curl "http://gateway/api/user/123?preview=true"
场景4:自动灰度放量
// 配置10%流量自动走v2,无需任何标记
{"type": "WEIGHT", "value": "10", "targetVersion": "v2"}
二、两层负载均衡架构
2.1 为什么需要两层负载均衡?
大多数网关教程只讲一层负载均衡(服务实例级别),但我们的网关实现了两层负载均衡:
第一层:服务级负载均衡
└─ 从多个服务版本中选择(user-v1, user-v2)
└─ 基于灰度规则 + 权重分配
第二层:节点级负载均衡
└─ 从服务实例中选择具体节点(192.168.1.10:8080, 192.168.1.11:8080)
└─ 基于4种算法(加权轮询、随机、一致性Hash、简单轮询)
2.2 完整请求链路
graph TB
A[请求到达]
B[MultiServiceLoadBalancerFilter]
C{灰度规则匹配?}
D[选择服务版本]
E[设置 lb:// 或 static://]
F[NacosDiscoveryLoadBalancerFilter]
G[DiscoveryLoadBalancerFilter]
H[SCG ReactiveLoadBalancer]
I[后端节点]
A --> B
B --> C
C -->|是| D
C -->|否| D
D --> E
E --> F
F --> G
G --> H
H --> I
style B fill:#e1f5ff
style C fill:#fff4e1
style D fill:#e1ffe1
style F fill:#f0e1ff
style G fill:#ffe1e1
2.2.1 Filter 执行顺序的源码级设计
这是理解 Spring Cloud Gateway 源码的关键!
很多读者会问:为什么 lb:// 和 static:// 能互不影响?它们都用了 lb:// 协议头,不会冲突吗?
答案就在 Filter Order 的精心设计 和 SCG 的 Filter 链执行机制 中。
Spring Cloud Gateway 原生 Filter 执行顺序:
RouteToRequestUrlFilter (Order = 10000)
└─ 作用:拼接最终请求URL(路径+Query)
└─ 设置 GATEWAY_REQUEST_URL_ATTR
ReactiveLoadBalancerClientFilter (Order = 10150)
└─ 作用:SCG 原生的负载均衡过滤器
└─ 只处理 lb:// 协议的URI
└─ 调用 Spring Cloud LoadBalancer 选择实例
我们自定义的 Filter 如何嵌入?
RouteToRequestUrlFilter (10000)
↓
MultiServiceLoadBalancerFilter (10001) ← 我们的灰度+服务选择器
├─ 读取路由metadata中的multiServiceConfig
├─ 执行灰度规则匹配(Header/Cookie/Query/Weight)
├─ 选择目标服务版本(user-v1 vs user-v2)
├─ 设置 TARGET_SERVICE_ID_ATTR 和 SERVICE_BINDING_TYPE_ATTR
└─ 转换URI:lb://serviceId 或 static://serviceId
↓
NacosDiscoveryLoadBalancerFilter (10100) ← 我们的Nacos namespace/group覆盖
├─ 只对 lb:// 协议生效
├─ 读取 SERVICE_NAMESPACE_ATTR 和 SERVICE_GROUP_ATTR
├─ 覆盖默认的Nacos Namespace和Group
└─ 不修改URI,仅设置上下文信息
↓
ReactiveLoadBalancerClientFilter (10150) ← SCG 原生负载均衡过滤器
├─ 检查URI协议是否为 lb://
├─ 如果是 lb:// → 调用 LoadBalancer 选择实例
└─ 如果是 static:// → 跳过,不处理
↓
DiscoveryLoadBalancerFilter (10150) ← 我们的静态节点负载均衡器
├─ 只对 static:// 协议生效
├─ 读取静态服务配置(IP:Port列表)
├─ 执行健康检查过滤(排除不健康节点)
├─ 执行负载均衡(4种算法选择节点)
└─ 转换 static:// 为 http://ip:port
↓
NettyRoutingFilter (最终发送请求)
关键设计原则:
| Filter | Order | 处理协议 | 跳过条件 |
|---|---|---|---|
| MultiServiceLoadBalancerFilter | 10001 | 全部 | 无multiServiceConfig |
| NacosDiscoveryLoadBalancerFilter | 10100 | lb:// | 无namespace/group覆盖 |
| ReactiveLoadBalancerClientFilter | 10150 | lb:// | URI不是lb:// |
| DiscoveryLoadBalancerFilter | 10150 | static:// | URI不是static:// |
为什么不会冲突?
// ReactiveLoadBalancerClientFilter (SCG原生)
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI requestUrl = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
// 关键判断:只处理 lb:// 协议
if (scheme.equals("lb")) {
return loadBalancer.choose(serviceId)
.flatMap(instance -> {
// 转换 lb://serviceId → http://ip:port
URI uri = UriComponentsBuilder.fromUri(requestUrl)
.scheme("http")
.host(instance.getHost())
.port(instance.getPort())
.build()
.toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
return chain.filter(exchange);
});
}
// static:// 协议直接跳过,交给 DiscoveryLoadBalancerFilter 处理
return chain.filter(exchange);
}
// DiscoveryLoadBalancerFilter (我们的静态节点负载均衡)
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI requestUrl = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
// 关键判断:只处理 static:// 协议
if (!scheme.equals("static")) {
return chain.filter(exchange); // lb:// 直接跳过
}
// 执行静态节点的负载均衡
List<ServiceInstance> instances = staticDiscoveryService.getInstances(serviceId);
List<ServiceInstance> filtered = instanceFilter.filter(instances); // 健康检查过滤
ServiceInstance selected = instanceSelector.select(filtered, strategy, exchange);
// 转换 static://serviceId → http://ip:port
URI uri = UriComponentsBuilder.fromUri(requestUrl)
.scheme("http")
.host(selected.getHost())
.port(selected.getPort())
.build()
.toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
return chain.filter(exchange);
}
设计精髓:
- 协议隔离:通过
scheme.equals("lb")和scheme.equals("static")精确判断 - Order 协同:10001 → 10100 → 10150,层层递进,各司其职
- 属性传递:
TARGET_SERVICE_ID_ATTR、SERVICE_BINDING_TYPE_ATTR在Filter间传递上下文 - 零侵入:不修改SCG原生Filter,仅通过Order优先级嵌入
这就是对 SCG 源码级的深度理解!
2.3 第一层:服务级负载均衡
核心代码 (MultiServiceLoadBalancerFilter.java):
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
MultiServiceConfig config = extractMultiServiceConfig(route);
if (config.getMode() == RoutingMode.SINGLE) {
// 单服务模式:直接路由
return handleSingleServiceMode(exchange, chain, config);
} else {
// 多服务模式:灰度 + 权重
return handleMultiServiceMode(exchange, chain, config);
}
}
private Mono<Void> handleMultiServiceMode(...) {
// Step 1: 检查灰度规则
String targetVersion = matchGrayRules(exchange, config);
// Step 2: 未匹配灰度规则 → 使用权重分配
if (targetVersion == null) {
targetVersion = selectByWeight(routeId, config);
}
// Step 3: 获取服务绑定信息
ServiceBinding selectedBinding = getServiceBindingByVersion(config, targetVersion);
// Step 4: 设置目标服务ID和类型
exchange.getAttributes().put(TARGET_SERVICE_ID_ATTR, selectedBinding.getServiceId());
exchange.getAttributes().put(SERVICE_BINDING_TYPE_ATTR, selectedBinding.getType());
// Step 5: 转换URI(lb:// 或 static://)
transformUriForServiceType(exchange, selectedBinding.getServiceId(), selectedBinding.getType());
return chain.filter(exchange);
}
2.4 第二层:节点级负载均衡
第一层选定服务版本后,第二层负责从该版本的具体实例中选择一个节点:
选定了 user-v1 (lb://user-v1)
↓
Nacos返回3个实例:
- 192.168.1.10:8080 (weight=3)
- 192.168.1.11:8080 (weight=2)
- 192.168.1.12:8080 (weight=1)
↓
InstanceSelector 选择具体节点
↓
192.168.1.10:8080 (加权轮询选中)
关键设计:两层负载均衡互不影响
- 第一层:选择服务版本(user-v1 vs user-v2)
- 第二层:选择具体节点(192.168.1.10 vs 192.168.1.11)
- 两层的配置独立,可以灵活组合
三、负载均衡算法深度解析
3.1 4种负载均衡算法
| 算法 | 策略名 | 适用场景 | 实现位置 |
|---|---|---|---|
| 平滑加权轮询 | weighted (默认) | 节点性能不均匀,需要按权重分配 | selectByWeightedRoundRobin() |
| 简单轮询 | round-robin | 节点性能均匀,轮流分配 | selectByRoundRobin() |
| 加权随机 | random | 节点性能不均匀,随机选择 | selectByRandom() |
| 一致性Hash | consistent-hash | 需要会话保持,相同Hash到相同节点 | selectByConsistentHash() |
3.2 平滑加权轮询算法(Nginx风格)
这是默认算法,也是最有技术含量的一个。
核心思想:
每个实例维护一个 currentWeight(当前权重)
每次选择时:
1. currentWeight += 配置权重
2. 选择 currentWeight 最大的实例
3. 选中实例的 currentWeight -= 总权重
代码实现 (InstanceSelector.java):
private ServiceInstance selectByWeightedRoundRobin(List<ServiceInstance> instances) {
// 1. 初始化权重
for (ServiceInstance instance : instances) {
String key = instance.getHost() + ":" + instance.getPort();
currentWeights.putIfAbsent(key, 0.0);
}
// 2. 累加当前权重
double totalWeight = 0;
for (ServiceInstance instance : instances) {
String key = instance.getHost() + ":" + instance.getPort();
double weight = getWeight(instance);
currentWeights.put(key, currentWeights.get(key) + weight);
totalWeight += weight;
}
// 3. 选择最大当前权重的实例
ServiceInstance selected = null;
double maxWeight = -1;
for (ServiceInstance instance : instances) {
String key = instance.getHost() + ":" + instance.getPort();
double currentWeight = currentWeights.get(key);
if (currentWeight > maxWeight) {
maxWeight = currentWeight;
selected = instance;
}
}
// 4. 减去总权重
if (selected != null) {
String key = selected.getHost() + ":" + selected.getPort();
currentWeights.put(key, currentWeights.get(key) - totalWeight);
}
return selected;
}
实际效果演示:
假设有3个节点,权重分别为 3:2:1
| 请求序号 | 累加后当前权重 | 选中节点 | 减去总权重后 |
|---|---|---|---|
| 1 | [3, 2, 1] | 节点A (3) | [-3, 2, 1] |
| 2 | [0, 4, 2] | 节点B (4) | [0, -2, 2] |
| 3 | [3, 0, 3] | 节点A (3) | [-3, 0, 3] |
| 4 | [0, 2, 4] | 节点C (4) | [0, 2, -2] |
| 5 | [3, 4, -1] | 节点B (4) | [3, -2, -1] |
| 6 | [6, 0, 0] | 节点A (6) | [0, 0, 0] |
6次请求分布:
- 节点A:3次 (50%)
- 节点B:2次 (33.3%)
- 节点C:1次 (16.7%)
完美符合 3:2:1 的权重比例!
优势:
- 避免"批量倾斜":不会出现连续多次都选同一节点
- 平滑分配:流量均匀分散
- 支持动态权重:权重调整后自动适应
3.3 一致性Hash算法
适用场景: 需要会话保持,相同用户始终路由到相同节点。
private ServiceInstance selectByConsistentHash(List<ServiceInstance> instances, ServerWebExchange exchange) {
// 1. 获取Hash Key(优先X-Hash-Key Header,否则用IP)
String hashKey = getHashKey(exchange);
// 2. 构建一致性Hash环
String serviceId = instances.get(0).getServiceId();
ConsistentHashRing hashRing = hashRingCache.computeIfAbsent(serviceId, k -> buildHashRing(instances));
// 3. 从Hash环上找到节点
return hashRing.getNode(hashKey);
}
private String getHashKey(ServerWebExchange exchange) {
// 优先使用自定义Header
String hashKey = exchange.getRequest().getHeaders().getFirst("X-Hash-Key");
if (hashKey != null) return hashKey;
// 其次使用客户端真实IP
String forwardedFor = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
if (forwardedFor != null) return forwardedFor.split(",")[0].trim();
// 最后使用直连IP
return exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
}
Hash环构建:
private ConsistentHashRing buildHashRing(List<ServiceInstance> instances) {
ConsistentHashRing ring = new ConsistentHashRing();
int virtualNodes = 150; // 每个真实节点对应150个虚拟节点
for (ServiceInstance instance : instances) {
// 权重越高,虚拟节点越多
int nodes = virtualNodes * (int) getWeight(instance);
for (int i = 0; i < nodes; i++) {
ring.addNode(instance.getHost() + ":" + instance.getPort() + "#" + i, instance);
}
}
return ring;
}
原理:
Hash环(0 ~ 2^64-1):
0 ──────────────────────────────────────────────── 2^64-1
│ │ │ │ │ │
A#1 B#1 A#2 C#1 B#2 A#3
请求Hash值:
- 用户A (IP: 1.2.3.4) → Hash = 12345 → 顺时针找到 A#1
- 用户B (IP: 5.6.7.8) → Hash = 67890 → 顺时针找到 B#1
优势:
- 相同用户始终到相同节点(会话保持)
- 节点增减时,影响范围最小化
3.4 算法选择建议
| 场景 | 推荐算法 | 配置值 |
|---|---|---|
| 节点性能均匀,无需会话保持 | 平滑加权轮询 | weighted |
| 节点性能均匀,简单轮询即可 | 简单轮询 | round-robin |
| 节点性能不均匀 | 加权随机/加权轮询 | random / weighted |
| 需要会话保持(如WebSocket) | 一致性Hash | consistent-hash |
四、lb:// vs static:// 双协议设计
4.1 为什么需要 static:// 协议?
很多教程只讲 lb://(服务发现协议),但实际生产环境中,你会遇到:
- 遗留系统:有些老服务没有注册到Nacos,只有固定IP:Port
- 外部API:调用第三方API,IP固定,不需要服务发现
- 混合架构:部分服务上云,部分还在物理机
如果只支持 lb://,这些场景就无法处理。所以我们需要自定义 static:// 协议。
4.2 两种协议对比
| 特性 | lb:// (DISCOVERY) | static:// (STATIC) |
|---|---|---|
| URI示例 | lb://user-service | static://legacy-backend |
| 服务发现 | Nacos/Consul 注册中心动态发现 | 静态配置(JSON中定义IP:Port列表) |
| 负载均衡 | SCG 原生 ReactiveLoadBalancer | 自定义 DiscoveryLoadBalancerFilter |
| 健康检查 | 委托给Nacos,网关信任Nacos的健康状态 | 网关自主执行(混合健康检查) |
| 节点上下线 | Nacos控制台操作 | Nacos配置中心更新服务JSON |
| 使用场景 | 云原生微服务 | 遗留系统、外部API、固定IP后端 |
4.3 协议推断逻辑
private ServiceBindingType inferServiceTypeFromUri(URI uri) {
if (uri == null) {
return ServiceBindingType.STATIC; // 默认为STATIC
}
String scheme = uri.getScheme();
if ("lb".equalsIgnoreCase(scheme)) {
return ServiceBindingType.DISCOVERY;
}
return ServiceBindingType.STATIC; // 其他情况都是STATIC
}
规则:
lb://→ DISCOVERY(服务发现模式)static://→ STATIC(静态配置模式)- 无协议头 → 默认 STATIC
4.4 static:// 配置示例
{
"name": "legacy-backend",
"type": "STATIC",
"instances": [
{"ip": "192.168.1.100", "port": 8080, "weight": 1, "enabled": true},
{"ip": "192.168.1.101", "port": 8080, "weight": 2, "enabled": true},
{"ip": "192.168.1.102", "port": 8080, "weight": 1, "enabled": false}
],
"loadBalancer": "weighted"
}
4.5 两种协议如何互不影响?
Filter执行链:
RouteToRequestUrlFilter (MIN_VALUE)
↓
MultiServiceLoadBalancerFilter (10001)
├─ 选择服务版本
├─ 设置 lb:// 或 static://
└─ 设置 SERVICE_BINDING_TYPE_ATTR
↓
NacosDiscoveryLoadBalancerFilter (10100)
└─ 处理 lb:// 带 namespace/group 覆盖的情况
↓
SCG ReactiveLoadBalancer (10150)
└─ 处理原生 lb://(Nacos服务发现)
↓
DiscoveryLoadBalancerFilter (10150)
└─ 处理 static://(静态配置 + 健康检查 + 负载均衡)
关键设计:
MultiServiceLoadBalancerFilter设置SERVICE_BINDING_TYPE_ATTR标记SCG ReactiveLoadBalancer只处理lb://,不处理static://DiscoveryLoadBalancerFilter只处理static://,不处理lb://- 两者通过 Filter Order 和类型标记隔离,互不干扰
4.6 lb:// 服务配置示例
{
"routeId": "user-service-route",
"uri": "lb://user-service",
"metadata": {
"multiServiceConfig": {
"mode": "SINGLE",
"serviceId": "user-service",
"serviceType": "DISCOVERY",
"serviceNamespace": "production", // 可选:覆盖默认Namespace
"serviceGroup": "DEFAULT_GROUP" // 可选:覆盖默认Group
}
}
}
五、Nacos控制台节点管理
5.1 节点上下线机制
场景1:lb:// 服务(Nacos注册中心)
在Nacos控制台操作 → Gateway 实时感知:
| Nacos操作 | Gateway行为 | 生效时间 |
|---|---|---|
| 实例下线(enabled=false) | 强制排除,不再路由到该节点 | 实时 |
| 实例上线(enabled=true) | 加入负载均衡列表 | 实时 |
| 修改权重(0-100) | 更新加权轮询参数 | 实时 |
| 健康检查失败 | Nacos自动排除,网关只收到健康实例列表 | 实时 |
核心代码 (NacosDiscoveryLoadBalancerFilter.java):
// 过滤掉用户在Nacos下线的实例
List<ServiceInstance> enabledInstances = instances.stream()
.filter(ServiceInstance::isEnabled) // enabled=false的强制排除
.collect(Collectors.toList());
if (enabledInstances.isEmpty()) {
return Mono.empty(); // 503 Service Unavailable
}
场景2:static:// 服务(静态配置)
通过Nacos配置中心更新 → ServiceRefresher 监听变更:
@Service
public class ServiceRefresher {
@NacosConfigListener(dataId = "services-index")
public void onIndexChange(String content) {
// 解析服务索引变更
}
@NacosConfigListener(dataId = "{serviceId}")
public void onSingleServiceChange(String serviceId, String content) {
if (content == null || content.isBlank()) {
// 服务下线
serviceManager.clearServiceCache(serviceId);
hybridHealthChecker.clearServiceInstances(serviceId);
} else {
// 服务创建或更新
serviceManager.parseAndCacheService(serviceId, serviceNode);
triggerHealthCheckForService(serviceId); // 触发健康检查
}
}
}
5.2 权重调整
lb:// 服务:
- 在Nacos控制台直接设置实例权重(0-100)
- Gateway 通过
instance.getWeight()读取 - 用于加权轮询负载均衡
static:// 服务:
- 在Nacos配置中心修改服务JSON
- 修改
instances数组中的weight字段 ServiceRefresher监听变更,实时更新
效果:
调整前:
节点A: weight=1 → 25%流量
节点B: weight=1 → 25%流量
节点C: weight=1 → 25%流量
节点D: weight=1 → 25%流量
节点B配置升级为高配机器,调整权重:
节点A: weight=1 → 16.7%流量
节点B: weight=2 → 33.3%流量 ← 流量翻倍
节点C: weight=1 → 16.7%流量
节点D: weight=1 → 16.7%流量
节点E: weight=1 → 16.7%流量(新增)
5.3 节点禁用实战
场景: 节点B需要维护,在Nacos控制台设置为 enabled=false
Nacos控制台操作:
实例列表:
- 192.168.1.10:8080 enabled=true ✓
- 192.168.1.11:8080 enabled=false ✗ ← 维护中
- 192.168.1.12:8080 enabled=true ✓
Gateway行为:
1. NacosDiscoveryLoadBalancerFilter 强制排除 192.168.1.11
2. 负载均衡只在 192.168.1.10 和 192.168.1.12 之间分配
3. 维护完成后,在Nacos控制台恢复 enabled=true
4. Gateway 自动将该节点加入负载均衡列表
无需重启网关,配置实时生效!
六、混合健康检查机制
6.1 设计哲学
为什么只检查 static:// 节点?
| 服务类型 | 健康检查责任方 | 原因 |
|---|---|---|
lb:// (DISCOVERY) | Nacos | Nacos已经做了完善的主动健康检查,网关无需重复 |
static:// (STATIC) | Gateway | 静态配置节点没有注册中心管理,需要网关自主检查 |
Gateway信任Nacos的健康状态:
Nacos健康检查 → 只返回健康实例给Gateway → Gateway直接使用
Gateway自主检查静态节点:
静态节点配置 → Gateway主动发起TCP/HTTP检查 → 记录健康状态 → 路由时过滤
6.2 三种检查方式
| 检查类型 | 触发时机 | 检查内容 | 性能开销 |
|---|---|---|---|
| 被动检查 (PASSIVE) | 每次业务请求 | 请求成功 → recordSuccess() → 标记健康 | 无额外开销 |
| 主动检查 (ACTIVE) | 定时调度 | TCP端口探测 + 可选HTTP /actuator/health | 定期开销 |
| 即时检查 (IMMEDIATE) | 路由选择前 | 新节点/无记录节点立即探测 | 按需开销 |
6.3 主动检查逻辑
public void probe(String serviceId, String ip, int port) {
// Step 1: TCP端口检查(基本连通性)
boolean tcpReachable = checkPortOpen(ip, port, timeoutMs);
if (!tcpReachable) {
handleUnhealthy(..., "TCP_UNREACHABLE");
return;
}
// Step 2: 可选HTTP健康检查(应用级健康)
if (httpCheckEnabled) {
boolean httpHealthy = checkHttpHealth(ip, port);
if (!httpHealthy) {
handleUnhealthy(..., "HTTP_UNHEALTHY");
return;
}
}
// 全部通过 → 标记健康
hybridHealthChecker.markHealthy(serviceId, ip, port, "ACTIVE");
}
6.4 三级检查频率
| 模式 | 间隔 | 适用节点 | 配置项 |
|---|---|---|---|
| Regular (常规) | 30秒 | 健康节点、新不健康节点 | gateway.health.regular-check-interval |
| Stable (稳定) | 2分钟 | 连续健康检查≥10次的节点 | gateway.health.stable-check-interval |
| Degraded (降级) | 3分钟 | 连续不健康检查≥5次的节点 | gateway.health.degraded-check-interval |
设计亮点:
- 健康节点降低检查频率(从30s → 2min),减少无效探测
- 不健康节点也降低频率(避免频繁报错日志)
- 网络抖动检测:≥10个节点同时变化 → 判定为网络抖动 → 跳过上报
6.5 健康状态数据结构
public class InstanceHealth {
// 基本信息
private String serviceId;
private String ip;
private int port;
// 健康状态
private volatile boolean healthy;
private int consecutiveFailures; // 连续失败次数
private String checkType; // PASSIVE/ACTIVE/INIT
private String unhealthyReason; // TCP_UNREACHABLE/HTTP_UNHEALTHY
// 降级模式
private int totalUnhealthyChecks; // 连续不健康检查次数
private boolean degradedCheckMode; // 是否进入降级检查模式
private Long degradedModeEnteredTime; // 降级模式进入时间
// 稳定模式
private int totalHealthyChecks; // 连续健康检查次数
private boolean stableCheckMode; // 是否进入稳定检查模式
// 时间戳
private long lastCheckTime;
private long lastRequestTime;
}
Key设计:
// Key = ip:port(跨服务共享)
// 同一IP:Port在不同服务中健康状态一致
String key = ip + ":" + port;
6.6 被动检查机制
请求成功时:
public void recordSuccess(String serviceId, String ip, int port) {
InstanceHealth health = healthCache.getIfPresent(key);
boolean wasHealthy = health.isHealthy();
// 重置失败计数,标记健康
health.setConsecutiveFailures(0);
health.setHealthy(true);
health.setUnhealthyReason(null);
health.setCheckType("PASSIVE");
// 如果状态改变(不健康→健康),立即推送给Admin
if (!wasHealthy) {
log.info("Instance {}:{} recovered to HEALTHY via successful request", ip, port);
queueForBatchPush(serviceId, ip, port, true);
}
}
请求失败时:
public void recordFailure(String serviceId, String ip, int port, String reason) {
InstanceHealth health = healthCache.getIfPresent(key);
// 累计失败次数
health.setConsecutiveFailures(health.getConsecutiveFailures() + 1);
// 超过阈值 → 标记不健康
if (health.getConsecutiveFailures() >= failureThreshold) {
health.setHealthy(false);
health.setUnhealthyReason(reason);
}
// 检测网络抖动
int stateChangeCount = countRecentStateChanges();
if (stateChangeCount >= networkFlapThreshold) {
log.warn("Network flap detected: {} instances changed state", stateChangeCount);
skipStatePush(); // 跳过上报,避免误判
}
}
七、节点故障降级策略
7.1 降级优先级设计
设计哲学:可用性优先
所有实例
│
▼
排除 enabled=false 的节点(用户明确选择,强制排除)
│
▼
分类:healthy / pending(无记录) / unhealthy
│
▼
pending节点处理:
- 如果没有健康节点 → 立即执行健康检查 → 通过则加入healthy
- 如果有健康节点 → pending暂不处理,后台检查
│
▼
返回策略:
if (!healthy.isEmpty()) return healthy; // 优先返回健康节点
return unhealthy; // 没有健康节点时,返回不健康节点尝试
7.2 核心过滤代码
public List<ServiceInstance> filter(List<ServiceInstance> instances) {
// Step 1: 排除禁用实例(用户显式选择)
List<ServiceInstance> enabled = instances.stream()
.filter(this::isEnabled)
.collect(Collectors.toList());
if (enabled.isEmpty()) {
return enabled; // 503
}
// Step 2: 按健康状态分类
List<ServiceInstance> healthy = new ArrayList<>();
List<ServiceInstance> pendingCheck = new ArrayList<>();
List<ServiceInstance> unhealthy = new ArrayList<>();
for (ServiceInstance inst : enabled) {
InstanceHealth health = healthChecker.getHealth(serviceId, inst.getHost(), inst.getPort());
if (health == null || "INIT".equals(health.getCheckType())) {
pendingCheck.add(inst); // 无健康记录
} else if (health.isHealthy()) {
healthy.add(inst);
} else {
unhealthy.add(inst);
}
}
// Step 3: 处理pending节点
if (!pendingCheck.isEmpty() && healthy.isEmpty()) {
// 没有健康节点,必须立即检查pending节点
for (ServiceInstance inst : pendingCheck) {
activeHealthChecker.probe(serviceId, inst.getHost(), inst.getPort());
// 根据检查结果重新分类...
}
}
// Step 4: 返回策略
if (!healthy.isEmpty()) {
return healthy; // 优先返回健康节点
}
// 没有健康节点 → 返回不健康节点(可用性优先)
log.warn("No healthy instances for {}. Returning unhealthy for LB to choose", serviceId);
return unhealthy;
}
7.3 为什么返回不健康节点?
这是一个反直觉但正确的设计决策:
场景:
服务只有一个节点:192.168.1.10:8080
健康检查标记为不健康(可能是网络抖动、GC停顿等)
如果直接返回503:
→ 100%请求失败
→ 但实际上节点可能已经恢复
如果返回不健康节点让LB尝试:
→ 可能成功(节点已恢复)
→ 可能失败(LB重试机制会找替代节点)
→ 最差情况:返回真实错误(比直接503更有价值)
核心考量:
- 健康检查的状态更新有延迟(30秒检查间隔)
- 节点此时不健康,但可能已经恢复
- "宁可错杀,不可漏过":尝试比直接拒绝更有价值
7.4 重试降级策略
当请求失败时,InstanceRetryExecutor 执行降级:
public Mono<Void> executeWithRetry(ServerWebExchange exchange, List<ServiceInstance> instances) {
Set<String> triedInstances = new HashSet<>();
// 第一次尝试
ServiceInstance selected = instanceSelector.select(instances, strategy, exchange);
triedInstances.add(selected.getHost() + ":" + selected.getPort());
try {
return routeToInstance(exchange, selected);
} catch (Exception e) {
// 记录失败
healthChecker.recordFailure(serviceId, selected.getHost(), selected.getPort(), e.getMessage());
// 寻找替代节点
ServiceInstance alternative = instanceFilter.findAlternative(serviceId, instances, triedInstances);
if (alternative == null) {
// 无替代节点 → 返回真实503
return Mono.error(new GatewayException("No available instance", 503));
}
// 重试替代节点
return routeToInstance(exchange, alternative);
}
}
替代节点查找优先级:
1. 健康节点(优先)
2. pending节点(即时检查后尝试)
3. 不健康节点(最后手段,可能已恢复)
八、完整实战演示
8.1 灰度发布全流程
场景: user-service 从 v1 升级到 v2
Step 1:部署v2版本
# Nacos注册user-v2
curl -X POST http://nacos:8848/nacos/v1/ns/instance \
-d "serviceName=user-v2&ip=192.168.2.10&port=8080"
Step 2:配置灰度路由
{
"routeId": "user-service-route",
"uri": "lb://user-v1",
"metadata": {
"multiServiceConfig": {
"mode": "MULTI",
"services": [
{"serviceId": "user-v1", "version": "v1", "weight": 90, "enabled": true},
{"serviceId": "user-v2", "version": "v2", "weight": 10, "enabled": true}
],
"grayRules": {
"enabled": true,
"rules": [
{"type": "HEADER", "name": "X-Version", "value": "v2", "targetVersion": "v2"},
{"type": "WEIGHT", "value": "10", "targetVersion": "v2"}
]
}
}
}
}
Step 3:验证灰度
# 内部测试(强制走v2)
curl -H "X-Version: v2" http://gateway/api/user/123
# 普通用户(10%概率走v2)
for i in {1..100}; do
curl http://gateway/api/user/123
done
# 查看日志,v1:v2 ≈ 90:10
Step 4:逐步放量
// 第一阶段:10%灰度
{"type": "WEIGHT", "value": "10", "targetVersion": "v2"}
// 第二阶段:50%灰度(验证通过)
{"type": "WEIGHT", "value": "50", "targetVersion": "v2"}
// 第三阶段:100%全量(v2稳定)
{"type": "WEIGHT", "value": "100", "targetVersion": "v2"}
Step 5:完成升级
// 切换默认版本
{
"services": [
{"serviceId": "user-v2", "version": "v2", "weight": 100, "enabled": true}
],
"grayRules": {"enabled": false}
}
8.2 静态节点健康检查实战
场景: 遗留系统 legacy-backend 有3个静态节点
配置:
{
"name": "legacy-backend",
"type": "STATIC",
"instances": [
{"ip": "192.168.1.100", "port": 8080, "weight": 1, "enabled": true},
{"ip": "192.168.1.101", "port": 8080, "weight": 1, "enabled": true},
{"ip": "192.168.1.102", "port": 8080, "weight": 1, "enabled": true}
],
"loadBalancer": "weighted"
}
健康检查过程:
Gateway启动
↓
InstanceDiscoveryService发现需要检查的节点
↓
ActiveHealthChecker发起TCP端口检查
├─ 192.168.1.100:8080 → TCP OK → HTTP /actuator/health OK → 标记健康
├─ 192.168.1.101:8080 → TCP OK → HTTP /actuator/health OK → 标记健康
└─ 192.168.1.102:8080 → TCP FAIL → 标记不健康(TCP_UNREACHABLE)
↓
每30秒定期检查
↓
192.168.1.102恢复
├─ TCP OK → HTTP OK → 标记健康
└─ 推送给Admin UI(状态改变通知)
请求路由:
请求到达 → InstanceFilter.filter()
↓
排除 enabled=false 的节点(无)
↓
分类:
- healthy: [192.168.1.100, 192.168.1.101]
- unhealthy: [192.168.1.102]
↓
返回 healthy 节点
↓
InstanceSelector 选择具体节点(加权轮询)
↓
192.168.1.100:8080(选中)
九、核心代码文件索引
| 功能 | 文件路径 | 说明 |
|---|---|---|
| 多服务负载均衡器 | my-gateway/src/main/java/com/leoli/gateway/filter/loadbalancer/MultiServiceLoadBalancerFilter.java | 灰度规则 + 服务级负载均衡 |
| 实例选择器 | my-gateway/src/main/java/com/leoli/gateway/filter/loadbalancer/InstanceSelector.java | 4种负载均衡算法 |
| 实例过滤器 | my-gateway/src/main/java/com/leoli/gateway/filter/loadbalancer/InstanceFilter.java | 健康/禁用节点过滤 |
| Nacos发现负载均衡 | my-gateway/src/main/java/com/leoli/gateway/filter/loadbalancer/NacosDiscoveryLoadBalancerFilter.java | lb:// namespace/group覆盖 |
| 静态发现负载均衡 | my-gateway/src/main/java/com/leoli/gateway/filter/loadbalancer/DiscoveryLoadBalancerFilter.java | static:// 负载均衡 |
| 混合健康检查器 | my-gateway/src/main/java/com/leoli/gateway/health/HybridHealthChecker.java | 被动+主动健康检查 |
| 主动健康检查器 | my-gateway/src/main/java/com/leoli/gateway/health/ActiveHealthChecker.java | TCP+HTTP探测 |
| 服务刷新器 | my-gateway/src/main/java/com/leoli/gateway/refresher/ServiceRefresher.java | Nacos配置监听 |
| 服务管理器 | my-gateway/src/main/java/com/leoli/gateway/manager/ServiceManager.java | 服务缓存 + 权重解析 |
十、总结
本文深入介绍了企业级API网关的高级路由与负载均衡实战:
核心亮点回顾
- 灰度发布系统:4种灰度规则(Header/Cookie/Query/Weight)+ 百分比流量分配
- 两层负载均衡:服务级(版本选择)+ 节点级(实例选择),互不影响
- 4种负载均衡算法:平滑加权轮询(Nginx风格)、简单轮询、加权随机、一致性Hash
- lb:// vs static:// 双协议:云原生+遗留系统双支持
- Nacos控制台实时管理:节点上下线、权重调整、健康状态,无需重启网关
- 混合健康检查:PASSIVE(被动)+ ACTIVE(主动)+ IMMEDIATE(即时),三级检查频率
- 可用性优先降级策略:没有健康节点时,尝试不健康节点(考虑健康检查延迟)
设计哲学
可用性优先:宁可尝试不健康节点,也不要直接返回503
- 健康检查有延迟,节点可能已恢复
- 尝试比直接拒绝更有价值
- 重试机制兜底,最差情况返回真实错误
职责分离:
lb://健康检查委托给Nacosstatic://健康检查由Gateway自主执行- 两种协议互不干扰,各司其职
平滑过渡:
- 灰度发布支持渐进式放量
- 平滑加权轮询避免流量倾斜
- 节点权重调整自动适应
参考资料
关于作者
李朝,网关开发,7年+分布式系统经验,专注于API网关、微服务架构、云原生技术领域。
50天独立开发企业级API网关平台,涵盖44项核心功能、561个测试用例,从架构设计到生产环境部署全流程实践。
专业服务
如果你需要构建类似的API网关或微服务平台,我可以提供以下服务:
- API网关定制开发:根据业务需求定制开发网关功能
- 架构设计与咨询:微服务架构设计、技术选型、性能优化
- 性能调优:JVM调优、连接池优化、限流降级方案
- AI集成:AI Copilot开发、智能运维、自动化诊断
联系方式:
- Email: lizhao5695@gmail.com
- Upwork: www.upwork.com/freelancers…
- GitHub: github.com/leoli5695
- BiliBili(项目演示):www.bilibili.com/video/BV1S2…
需要API网关或微服务架构方面的帮助? 欢迎通过邮件或Upwork联系我,提供技术咨询和定制开发服务。