|
|
|
@ -6,74 +6,104 @@ import org.springframework.cloud.client.ServiceInstance; |
|
|
|
import org.springframework.cloud.client.loadbalancer.DefaultResponse; |
|
|
|
import org.springframework.cloud.client.loadbalancer.EmptyResponse; |
|
|
|
import org.springframework.cloud.client.loadbalancer.Request; |
|
|
|
import org.springframework.cloud.client.loadbalancer.RequestDataContext; |
|
|
|
import org.springframework.cloud.client.loadbalancer.Response; |
|
|
|
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier; |
|
|
|
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; |
|
|
|
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; |
|
|
|
import org.springframework.http.HttpHeaders; |
|
|
|
import reactor.core.publisher.Mono; |
|
|
|
|
|
|
|
import java.util.Arrays; |
|
|
|
import java.util.Collections; |
|
|
|
import java.util.List; |
|
|
|
|
|
|
|
/** |
|
|
|
* 优先级负载均衡器 - 首选优先策略 |
|
|
|
* 优先级负载均衡器 - 按角色顺序选择实例 |
|
|
|
* <p> |
|
|
|
* 根据配置文件中实例的声明顺序确定优先级, |
|
|
|
* 始终选择优先级最高的健康实例。 |
|
|
|
* 从请求 header {@value #HEADER_ROLE_ORDER} 读取角色优先级列表(由 PreferredRoleFilter 从路由 metadata 注入), |
|
|
|
* 依次在 Nacos 拉到的健康实例中匹配 metadata {@value #METADATA_ROLE_KEY},命中即返回; |
|
|
|
* 若列表中所有角色均未命中,则回退取第一个健康实例作为兜底。 |
|
|
|
* <p> |
|
|
|
* 健康检查并发执行后返回结果顺序不确定, |
|
|
|
* 因此通过 preferredOrder 显式匹配配置中的优先级顺序。 |
|
|
|
* 完全替代原有静态 SimpleDiscoveryProperties 方案,同时保留“首选优先 + 兜底容错”语义。 |
|
|
|
* |
|
|
|
* @author zdxt |
|
|
|
*/ |
|
|
|
@Slf4j |
|
|
|
public class PreferredFirstLoadBalancer implements ReactorServiceInstanceLoadBalancer { |
|
|
|
|
|
|
|
/** 路由 metadata 中优先级顺序注入到请求头的名称 */ |
|
|
|
public static final String HEADER_ROLE_ORDER = "X-Preferred-Role-Order"; |
|
|
|
|
|
|
|
/** Nacos 实例 metadata 中标识角色的 key(与 admin 服务 application.yml 保持一致) |
|
|
|
* 注意:不使用点号(Spring Boot 会把点号视为嵌套路径拆分 Map key) */ |
|
|
|
public static final String METADATA_ROLE_KEY = "zdxt-role"; |
|
|
|
|
|
|
|
private final String serviceId; |
|
|
|
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; |
|
|
|
/** 配置中声明的实例顺序(host:port),索引越小优先级越高 */ |
|
|
|
private final List<String> preferredOrder; |
|
|
|
|
|
|
|
public PreferredFirstLoadBalancer( |
|
|
|
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, |
|
|
|
String serviceId, |
|
|
|
List<String> preferredOrder) { |
|
|
|
String serviceId) { |
|
|
|
this.serviceId = serviceId; |
|
|
|
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; |
|
|
|
this.preferredOrder = preferredOrder; |
|
|
|
log.info("[{}] 优先级顺序: {}", serviceId, preferredOrder); |
|
|
|
log.info("[{}] PreferredFirstLoadBalancer 已启用,将按请求头 {} 指定的角色顺序优先选择实例", |
|
|
|
serviceId, HEADER_ROLE_ORDER); |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public Mono<Response<ServiceInstance>> choose(Request request) { |
|
|
|
List<String> roleOrder = extractRoleOrder(request); |
|
|
|
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider |
|
|
|
.getIfAvailable(NoopServiceInstanceListSupplier::new); |
|
|
|
return supplier.get(request) |
|
|
|
.next() |
|
|
|
.map(this::selectInstance); |
|
|
|
.map(instances -> selectByRoleOrder(instances, roleOrder)); |
|
|
|
} |
|
|
|
|
|
|
|
private Response<ServiceInstance> selectInstance(List<ServiceInstance> instances) { |
|
|
|
private Response<ServiceInstance> selectByRoleOrder(List<ServiceInstance> instances, List<String> roleOrder) { |
|
|
|
if (instances.isEmpty()) { |
|
|
|
log.warn("[{}] 无可用服务实例", serviceId); |
|
|
|
return new EmptyResponse(); |
|
|
|
} |
|
|
|
|
|
|
|
// 按配置优先级顺序遍历,找到第一个在健康列表中的实例
|
|
|
|
for (String preferred : preferredOrder) { |
|
|
|
// 按 roleOrder 顺序遍历,找到第一个匹配角色且健康的实例
|
|
|
|
for (String role : roleOrder) { |
|
|
|
for (ServiceInstance instance : instances) { |
|
|
|
String key = instance.getHost() + ":" + instance.getPort(); |
|
|
|
if (preferred.equals(key)) { |
|
|
|
log.debug("[{}] 选择优先实例: {}, 健康实例数: {}", |
|
|
|
serviceId, key, instances.size()); |
|
|
|
String instanceRole = instance.getMetadata().get(METADATA_ROLE_KEY); |
|
|
|
if (role.equals(instanceRole)) { |
|
|
|
log.debug("[{}] 命中优先角色 [{}] -> {}:{} (metadata={})", |
|
|
|
serviceId, role, instance.getHost(), instance.getPort(), instance.getMetadata()); |
|
|
|
return new DefaultResponse(instance); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 如果配置顺序中的实例都不在健康列表中(不应发生),取健康列表第一个
|
|
|
|
// 兜底:所有优先角色都不健康,取列表首个健康实例
|
|
|
|
ServiceInstance fallback = instances.get(0); |
|
|
|
log.warn("[{}] 未匹配到配置优先级实例,回退使用: {}:{}", |
|
|
|
serviceId, fallback.getHost(), fallback.getPort()); |
|
|
|
log.warn("[{}] roleOrder={} 未匹配到任何健康实例,回退到首个健康实例 {}:{} (role={})", |
|
|
|
serviceId, roleOrder, fallback.getHost(), fallback.getPort(), |
|
|
|
fallback.getMetadata().get(METADATA_ROLE_KEY)); |
|
|
|
return new DefaultResponse(fallback); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 从请求头 X-Preferred-Role-Order 提取角色优先级列表(逗号分隔) |
|
|
|
*/ |
|
|
|
private List<String> extractRoleOrder(Request<?> request) { |
|
|
|
Object context = request.getContext(); |
|
|
|
if (context instanceof RequestDataContext rdc) { |
|
|
|
HttpHeaders headers = rdc.getClientRequest().getHeaders(); |
|
|
|
if (headers != null) { |
|
|
|
String raw = headers.getFirst(HEADER_ROLE_ORDER); |
|
|
|
if (raw != null && !raw.isBlank()) { |
|
|
|
return Arrays.stream(raw.split(",")) |
|
|
|
.map(String::trim) |
|
|
|
.filter(s -> !s.isEmpty()) |
|
|
|
.toList(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return Collections.emptyList(); |
|
|
|
} |
|
|
|
} |
|
|
|
|