你好呀,我的老朋友!我是老寇,跟我一起学习动态路由和负载均衡
Spring Cloud Gateway
介绍
提供了一个建立在 Spring 生态系统之上的 API 网关,包括:Spring 6、Spring Boot 3 和 Project Reactor。 Spring Cloud Gateway旨在提供一种简单而有效的方法来路由到API,并为它们提供跨领域关注点,例如:安全性,监控/指标、限流、路由等等。
注意
- 不启用网关,请设置
spring.cloud.gateway.enabled=false
- Spring Cloud Gateway需要运行在由Spring Webflux(响应式)提供的Netty容器,不适用于传统的Servlet容器或作为WAR构建
核心概念
- Route:网关的基本构成单元,它由ID,目标URI,Predicate集合和Filer集合组成,如果满足Predicate,则匹配路由
- Predicate:断言,这是jdk8 断言函数,输入类型是
Spring Framework ServerWebExchange
,可以匹配HTTP请求中的任何内容,例如请求头或参数 - Filter:是使用特定工厂构造的
GatewayFilter
实例,分为两种类型,分别是Gateway Filter(某个路由过滤器)和Global Filter(全局过滤器),您可以对下游服务请求之前或之后修改请求或响应
流程图

动态路由
路由规则不是写在配置文件中,而是存储在外部系统(如 Nacos、Consul、Apollo、Redis 或数据库)中。Spring Cloud Gateway 能够监听这些外部系统的变化,在不重启网关的情况下,实时地获取最新的路由配置并使其生效
注意
静态路由 是路由规则以配置文件(如 application.yml
)的方式硬编码在项目中
引入依赖
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
</dependencies>
yaml配置
yaml
spring:
config:
import:
# 临时配置文件【解决拉取nacos配置文件时,group默认为DEFAULT_GROUP问题】
- optional:nacos:router.json?refreshEnabled=true&group=DEFAULT
cloud:
gateway:
server:
webflux:
enabled: true
discovery:
locator:
# 关闭动态生成路由 => DiscoveryClientRouteDefinitionLocator
# 查看DiscoveryLocatorProperties
enabled: false
# 开启服务ID强制小写
lower-case-service-id: true
nacos:
discovery:
# 开启服务注册&发现
enabled: true
# 服务注册&发现-地址
server-addr: nacos:8848
# 服务注册&发现-命名空间
namespace: public
# 服务注册&发现-用户名
username: nacos
# 服务注册&发现-密码
password: nacos
# 服务注册&发现-分组
group: DEFAULT
# true支持https,false不支持https
secure: false
# true 临时 false 持久
ephemeral: true
# 服务注册&发现-集群名称
cluster-name: nacos-cluster
heart-beat:
# 开启心跳检测
enabled: true
# 每10秒发送一次心跳【单位毫秒】
heart-beat-interval: 10000
# 超过30秒,则标记为不健康
heart-beat-timeout: 30000
config:
# 开启配置中心
enabled: true
# 配置中心-地址
server-addr: nacos:8848
# 配置中心-命名空间
namespace: public
# 配置中心-用户名
username: nacos
# 配置中心-密码
password: nacos
# 配置中心-分组
group: DEFAULT
# 配置中心-集群名称
cluster-name: nacos-cluster
# 配置中心-开启自动刷新
refresh-enabled: true
# 配置中心-配置文件格式
file-extension: yaml
Nacos路由配置
java
// @formatter:off
/**
* nacos动态路由缓存库.
* <a href="https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-examples/nacos-example/nacos-config-example/src/main/java/com/alibaba/cloud/examples/example/ConfigListenerExample.java">nacos拉取配置</a>
*
* @author laokou
*/
// @formatter:on
@Slf4j
@NonNullApi
@Repository
public class NacosRouteDefinitionRepository implements RouteDefinitionRepository {
static {
ForyFactory.INSTANCE.register(org.springframework.cloud.gateway.route.RouteDefinition.class);
ForyFactory.INSTANCE.register(org.springframework.cloud.gateway.filter.FilterDefinition.class);
ForyFactory.INSTANCE.register(org.springframework.cloud.gateway.handler.predicate.PredicateDefinition.class);
}
private final String dataId = "router.json";
private final ConfigUtils configUtils;
private final ReactiveHashOperations<String, String, RouteDefinition> reactiveHashOperations;
private final ExecutorService virtualThreadExecutor;
public NacosRouteDefinitionRepository(ConfigUtils configUtils,
ReactiveRedisTemplate<String, Object> reactiveRedisTemplate,
ExecutorService virtualThreadExecutor) {
this.configUtils = configUtils;
this.reactiveHashOperations = reactiveRedisTemplate.opsForHash();
this.virtualThreadExecutor = virtualThreadExecutor;
}
@PostConstruct
public void listenRouter() throws NacosException {
log.info("开始监听路由配置信息");
configUtils.addListener(dataId, configUtils.getGroup(), new Listener() {
@Override
public Executor getExecutor() {
return Executors.newSingleThreadExecutor();
}
@Override
public void receiveConfigInfo(String routes) {
log.info("监听路由配置信息,开始同步路由配置:{}", routes);
virtualThreadExecutor.execute(() -> syncRouter(getRoutes(routes))
.subscribeOn(Schedulers.boundedElastic())
.subscribe());
}
});
}
// @formatter:off
/**
* 路由基本原理总结:
* 1.从NacosRouteDefinitionRepository、DiscoveryClientRouteDefinitionLocator和PropertiesRouteDefinitionLocator加载定义的路由规则.
* 2.通过CompositeRouteDefinitionLocator合并定义的路由规则.
* 3.加载所有的定义的路由规则,使用配置的断言工厂和过滤器工厂来创建路由.
* 4.将路由缓存,提高路由查找性能.
* <p>
* 获取动态路由(避免集群中网关频繁调用Redis,需要本地缓存).
* {@link org.springframework.cloud.gateway.config.GatewayAutoConfiguration
* @return 定义的路由规则
*/
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return reactiveHashOperations.entries(RedisKeyUtils.getRouteDefinitionHashKey())
.mapNotNull(Map.Entry::getValue)
.onErrorContinue((throwable, routeDefinition) -> {
if (log.isErrorEnabled()) {
log.error("从Redis获取路由失败,错误信息:{}", throwable.getMessage(), throwable);
}
});
}
// @formatter:on
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return Mono.empty();
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return Mono.empty();
}
/**
* 同步路由【同步Nacos动态路由配置到Redis,并且刷新本地缓存】.
* @return 同步结果
*/
public Mono<Void> syncRouter() {
return syncRouter(getRoutes());
}
/**
* 同步路由【同步Nacos动态路由配置到Redis,并且刷新本地缓存】.
* @param routes 路由
* @return 同步结果
*/
private Mono<Void> syncRouter(Collection<RouteDefinition> routes) {
return reactiveHashOperations.delete(RedisKeyUtils.getRouteDefinitionHashKey())
.doOnError(throwable -> log.error("删除路由失败,错误信息:{}", throwable.getMessage(), throwable))
.doOnSuccess(removeFlag -> publishRefreshRoutesEvent())
.thenMany(Flux.fromIterable(routes))
.flatMap(router -> reactiveHashOperations.putIfAbsent(RedisKeyUtils.getRouteDefinitionHashKey(), router.getId(), router)
.doOnError(throwable -> log.error("保存路由失败,错误信息:{}", throwable.getMessage(), throwable)))
.then()
.doOnSuccess(saveFlag -> publishRefreshRoutesEvent());
}
// @formatter:off
/**
* 获取nacos动态路由配置.
* @return 拉取结果
*/
private Collection<RouteDefinition> getRoutes() {
return getRoutes(EMPTY);
}
/**
* 获取nacos动态路由配置.
* @param str 路由配置
* @return 拉取结果
*/
private Collection<RouteDefinition> getRoutes(String str) {
try {
String routes = StringUtils.isEmpty(str) ? configUtils.getConfig(dataId, configUtils.getGroup(), 5000) : str;
return JacksonUtils.toList(routes, RouteDefinition.class);
}
catch (Exception e) {
log.error("动态路由【API网关】不存在,错误信息:{}", e.getMessage(), e);
throw new SystemException(ROUTER_NOT_EXIST);
}
}
/**
* 刷新事件.
*/
private void publishRefreshRoutesEvent() {
// 刷新事件
SpringContextUtils.publishEvent(new RefreshRoutesEvent(this));
}
// @formatter:on
}
路由配置【router.json】
json
[
{
"id": "laokou-auth",
"uri": "lb://laokou-auth",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/auth/**"
}
},
{
"name": "Weight",
"args": {
"_genkey_0": "auth",
"_genkey_1": "100"
}
}
],
"filters": [
{
"name": "StripPrefix",
"args": {
"parts": "1"
}
},
{
"name": "RewritePath",
"args": {
"_genkey_0": "/auth/(?<path>.*)",
"_genkey_1": "/$\{path}"
}
}
],
"metadata": {
"version": "v3"
},
"order": 1
}
]
启动任务
java
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApp implements CommandLineRunner {
private final NacosRouteDefinitionRepository nacosRouteDefinitionRepository;
private final ExecutorService virtualThreadExecutor;
// @formatter:off
public static void main(String[] args) throws UnknownHostException, NoSuchAlgorithmException, KeyManagementException {
// 配置关闭nacos日志,因为nacos的log4j2导致本项目的日志不输出的问题
System.setProperty("nacos.logging.default.config.enabled", "false");
// 启用虚拟线程支持
System.setProperty("reactor.schedulers.defaultBoundedElasticOnVirtualThreads", "true");
new SpringApplicationBuilder(GatewayApp.class).web(WebApplicationType.REACTIVE).run(args);
}
@Override
public void run(String... args) {
// 执行同步路由任务
virtualThreadExecutor.execute(() -> nacosRouteDefinitionRepository.syncRouter()
.subscribeOn(Schedulers.boundedElastic())
.subscribe());
}
// @formatter:on
}
原理
text
路由基本原理总结:
1.从NacosRouteDefinitionRepository、DiscoveryClientRouteDefinitionLocator和PropertiesRouteDefinitionLocator加载定义的路由规则.
2.通过CompositeRouteDefinitionLocator合并定义的路由规则.
3.加载所有的定义的路由规则,使用配置的断言工厂和过滤器工厂来创建路由.
4.将路由缓存,提高路由查找性能.
负载均衡
引入依赖
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
ymal配置
yaml
spring:
cloud:
# loadbalancer
loadbalancer:
cache:
caffeine:
# 初始容量 => 30
# 最大容量 => 4096
# 淘汰规则 => 最后一次写操作后经过30s过期
spec: initialCapacity=30,expireAfterWrite=30s,maximumSize=4096
# 开启缓存
enabled: true
nacos:
# 开启Nacos路由负载均衡
enabled: true
代码配置【全路径覆盖spring cloud alibaba】
java
@ConditionalOnDiscoveryEnabled
@ConditionalOnLoadBalancerNacos
@Configuration(proxyBeanMethods = false)
public class NacosLoadBalancerClientConfiguration {
private static final int REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER = 183827465;
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean({ LoadBalancerClientFactory.class, NacosDiscoveryProperties.class, InetIPv6Utils.class })
public ReactorLoadBalancer<ServiceInstance> nacosLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory, NacosDiscoveryProperties nacosDiscoveryProperties,
InetIPv6Utils inetIPv6Utils, List<ServiceInstanceFilter> serviceInstanceFilters,
List<LoadBalancerAlgorithm> loadBalancerAlgorithms) {
String serviceId = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
Map<String, LoadBalancerAlgorithm> loadBalancerAlgorithmMap = new HashMap<>();
loadBalancerAlgorithms.forEach(loadBalancerAlgorithm -> {
if (!loadBalancerAlgorithmMap.containsKey(loadBalancerAlgorithm.getServiceId())) {
loadBalancerAlgorithmMap.put(loadBalancerAlgorithm.getServiceId(), loadBalancerAlgorithm);
}
});
return new NacosLoadBalancer(
loadBalancerClientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId,
nacosDiscoveryProperties, inetIPv6Utils, serviceInstanceFilters, loadBalancerAlgorithmMap);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnReactiveDiscoveryEnabled
@Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER)
public static class ReactiveSupportConfiguration {
@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "default",
matchIfMissing = true)
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withDiscoveryClient().build(context);
}
@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "zone-preference")
public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withDiscoveryClient().withZonePreference().build(context);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBlockingDiscoveryEnabled
@Order(REACTIVE_SERVICE_INSTANCE_SUPPLIER_ORDER + 1)
public static class BlockingSupportConfiguration {
@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "default",
matchIfMissing = true)
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().build(context);
}
@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "zone-preference")
public ServiceInstanceListSupplier zonePreferenceDiscoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withBlockingDiscoveryClient()
.withZonePreference()
.build(context);
}
}
}
java
@Slf4j
public class NacosLoadBalancer implements ReactorServiceInstanceLoadBalancer {
/**
* Storage local valid IPv6 address, it's a flag whether local machine support IPv6
* address stack.
*/
public static String ipv6;
private final String serviceId;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final NacosDiscoveryProperties nacosDiscoveryProperties;
private final InetIPv6Utils inetIPv6Utils;
private final List<ServiceInstanceFilter> serviceInstanceFilters;
private final Map<String, LoadBalancerAlgorithm> loadBalancerAlgorithmMap;
public NacosLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties, InetIPv6Utils inetIPv6Utils,
List<ServiceInstanceFilter> serviceInstanceFilters,
Map<String, LoadBalancerAlgorithm> loadBalancerAlgorithmMap) {
this.serviceId = serviceId;
this.inetIPv6Utils = inetIPv6Utils;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.serviceInstanceFilters = serviceInstanceFilters;
this.loadBalancerAlgorithmMap = loadBalancerAlgorithmMap;
}
/**
* 初始化.
*/
@PostConstruct
public void init() {
String ip = nacosDiscoveryProperties.getIp();
if (com.alibaba.cloud.commons.lang.StringUtils.isNotEmpty(ip)) {
ipv6 = RegexUtils.ipv4Regex(ip) ? nacosDiscoveryProperties.getMetadata().get("IPv6") : ip;
}
else {
ipv6 = inetIPv6Utils.findIPv6Address();
}
}
/**
* 根据IP类型过滤服务实例.
* @param instances 服务实例
* @return 服务实例列表
*/
private List<ServiceInstance> filterInstanceByIpType(List<ServiceInstance> instances) {
if (com.alibaba.cloud.commons.lang.StringUtils.isNotEmpty(ipv6)) {
List<ServiceInstance> ipv6InstanceList = new ArrayList<>();
for (ServiceInstance instance : instances) {
if (RegexUtils.ipv4Regex(instance.getHost())) {
if (com.alibaba.cloud.commons.lang.StringUtils.isNotEmpty(instance.getMetadata().get("IPv6"))) {
ipv6InstanceList.add(instance);
}
}
else {
ipv6InstanceList.add(instance);
}
}
// Provider has no IPv6, should use IPv4.
if (ipv6InstanceList.isEmpty()) {
return instances.stream().filter(instance -> RegexUtils.ipv4Regex(instance.getHost())).toList();
}
else {
return ipv6InstanceList;
}
}
return instances.stream().filter(instance -> RegexUtils.ipv4Regex(instance.getHost())).toList();
}
/**
* 路由负载均衡.
* @param request 请求
* @return 服务实例(响应式)
*/
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new)
.get(request)
.next()
.map(instances -> getInstanceResponse(instances, request));
}
/**
* 路由负载均衡.
* @param serviceInstances 服务实例列表
* @param request 请求
* @return 服务实例响应体
*/
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances, Request<?> request) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: {}", this.serviceId);
return new EmptyResponse();
}
if (request.getContext() instanceof RequestDataContext context) {
String path = context.getClientRequest().getUrl().getPath();
HttpHeaders headers = context.getClientRequest().getHeaders();
// 服务灰度路由
if (isGrayRouter(headers)) {
String version = RegexUtils.getRegexValue(path, "/(v\d+)/");
if (StringUtils.isNotEmpty(version)) {
serviceInstances = serviceInstances.stream()
.filter(item -> item.getMetadata().getOrDefault("version", "v3").equals(version))
.toList();
}
}
}
return getInstanceResponse(request, serviceInstances);
}
/**
* 服务实例响应.
* @param serviceInstances 服务实例
* @return 响应结果
*/
private Response<ServiceInstance> getInstanceResponse(Request<?> request, List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.error("No servers available for service: {}", this.serviceId);
return new EmptyResponse();
}
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
List<ServiceInstance> instancesToChoose = serviceInstances;
if (com.alibaba.cloud.commons.lang.StringUtils.isNotBlank(clusterName)) {
List<ServiceInstance> sameClusterInstances = serviceInstances.stream().filter(serviceInstance -> {
String cluster = serviceInstance.getMetadata().get("nacos.cluster");
return com.alibaba.cloud.commons.lang.StringUtils.equals(cluster, clusterName);
}).toList();
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
}
}
else {
log.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", serviceId,
clusterName, serviceInstances);
}
instancesToChoose = this.filterInstanceByIpType(instancesToChoose);
// Filter the service list sequentially based on the order number
for (ServiceInstanceFilter filter : serviceInstanceFilters) {
instancesToChoose = filter.filterInstance(request, instancesToChoose);
}
ServiceInstance instance;
// Find the corresponding load balancing algorithm through the service ID and
// select the final service instance
if (loadBalancerAlgorithmMap.containsKey(serviceId)) {
instance = loadBalancerAlgorithmMap.get(serviceId).getInstance(request, instancesToChoose);
}
else {
instance = loadBalancerAlgorithmMap.get(LoadBalancerAlgorithm.DEFAULT_SERVICE_ID)
.getInstance(request, instancesToChoose);
}
return new DefaultResponse(instance);
}
catch (Exception e) {
log.error("NacosLoadBalancer error", e);
return null;
}
}
/**
* 判断服务灰度路由.
* @param headers 请求头
* @return 判断结果
*/
private boolean isGrayRouter(HttpHeaders headers) {
String gray = headers.getFirst("service-gray");
return ObjectUtils.equals(TRUE, gray);
}
}
我是老寇,我们下次再见啦!