微服务中,客户端调用服务时,将服务分发到对应的微服务
zuul 使用阻塞式模型,一个请求一个线程,高并发压力大,已经停止维护。
gateway 使用非阻塞式响应模型,基于Netty使用webFlux,用于替代 Tomcat
统一接入:
为各种无线应用提供统一接入入口
高性能、高并发、高可靠
负载均衡、容灾切换(异地多活)
流量监管
服务降级
熔断
路由(异地多活中的应用)
安全防护
和安全部合作,IP 黑名单,URL 黑名单
风控防刷,恶意攻击等
协议适配
前端系统(http、https)后端(RPC)
长连接、短链接支持
根据请求路由分配至相应的 SOA 服务并执行
spring:
cloud:
# 路由规则
gateway:
routes:
# 将 http://localhost:9000/product/** 路由至 http://localhost:7070/product/**
- id: provider # 路由 ID,唯一
uri: http://localhost:7070/ # 目标 URI,路由到微服务的地址
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应的 URL 请求
spring:
cloud:
# 路由规则
gateway:
# 将 http://localhost:9000/product/** 路由至 http://localhost:7070/product/**
- id: order # 路由 ID,唯一
uri: http://localhost:6060/ # 目标 URI,路由到微服务的地址
predicates: # 断言(判断条件)
- Query=name # 匹配请求参数中包含 name 的请求
# - Query=name, abc. # 匹配请求参数中包含 name 且满足正则表达式 abc. 的请求
spring:
cloud:
# 路由规则
gateway:
routes:
# Method
- id: provider # 路由 ID,唯一
uri: http://localhost:7070/ # 目标 URI,路由到微服务的地址
predicates: # 断言(判断条件)
- Method=GET # 匹配 GET 请求
spring:
cloud:
# 路由规则
gateway:
routes:
# Datetime
- id: provider # 路由 ID,唯一
uri: http://localhost:7070/ # 目标 URI,路由到微服务的地址
predicates: # 断言(判断条件)
# 匹配中国上海 2022-06-27 16:00:00 后的时间
- After=2022-06-27T16:00:00.000+08:00[Asia/Shanghai]
spring:
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: http://localhost:7070/ # 目标 URI,路由到微服务的地址
predicates: # 断言(判断条件)
# RemoteAddr
- RemoteAddr=172.16.10.82/0 # 匹配请求中地址是 172.16.10.82 的请求,/0为子网掩码
API: http://172.16.10.82:9000/product/list
spring:
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: http://localhost:7070/ # 目标 URI,路由到微服务的地址
predicates: # 断言(判断条件)
# Header
- Header=X-Request-Id, \d+ # 匹配请求头中包含 X-Request-Id 并且其值匹配正则表达式 \d+ 的请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KS9fnrzD-1670555453107)(img/Gateway 路由规则 Header test.png)]
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
spring:
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: lb://provider # 根据注册中心动态路由
# uri: http://localhost:7070/ # 目标 URI,路由到微服务的地址
predicates: # 断言(判断条件)
# Path
- Path=/product/** # 匹配对应的 URL 请求
API:http://localhost:9000/product/list
spring:
cloud:
# 路由规则
gateway:
discovery:
locator:
# 是否与服务发现组件进行结合,通过 serviceId 转发到具体服务
enabled: true # 是否开启基于服务发现的路由规则,开启后会自动生成一套基于注册中心服务名的路由,自定义路由时建议关闭
lower-case-service-id: true # 是否将服务名称转小写
API前要加服务名称服务名称: http://localhost:9000/provider/product/list
通过 spring.cloud.routes.filters 配置在具体路由下,只作用于当前路由或通过 spring.cloud.default-filters 配置在全局,作用在所有路由上
spring:
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: lb://provider # 根据注册中心动态路由
# uri: http://localhost:7070/ # 目标 URI,路由到微服务的地址
predicates: # 断言(判断条件)
# Path
- Path=/product/**,/gateway/** # 匹配对应的 URL 请求
filters:
# 将 /gateway/product/list 重写为 /product/list
- RewritePath=/gateway(?>/?.*),$\{segment}
# 请求增加前缀 /list 变为 /product/list
- PrefixPath=/product
# 分割前缀 /api/gateway/product/list 变为 /product/list
- StripPrefix=2
- id: provider2 # 路由 ID,唯一
uri: lb://provider # 根据注册中心动态路由
predicates: # 断言(判断条件)
# Path 设置参数 segment
- Path=/gateway/product/{segment}
filters:
# 将 /gateway/product/list 重写为 /product/list
- SetPath=/product/{segment}
spring:
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: lb://provider # 根据注册中心动态路由
predicates: # 断言(判断条件)
# Path
- Path=/** # 匹配对应的 URL 请求
filters:
# 在下游请求中添加 id = 1
- AddRequestParameter=id,1
spring:
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: lb://provider # 根据注册中心动态路由
predicates: # 断言(判断条件)
# Path
- Path=/product/**,/gateway/** # 匹配对应的 URL 请求
filters:
# 在任何情况下,响应状态码设置为888
- SetStatus=888
设置全局默认 filters
spring:
application:
name: gateway # 应用名称
cloud:
# 路由规则
gateway:
discovery:
locator:
# 是否与服务发现组件进行结合,通过 serviceId 转发到具体服务
enabled: true # 是否开启基于服务发现的路由规则
lower-case-service-id: true # 是否将服务名称转小写
default-filters:
- PrefixPath=/product
无需在配置文件中配置,作用在所有的路由上,最终通过 GatewayFilterAdapter 包装成 GatewayFilterChain 可识别的过滤器,它为请求业务以及路由的 URI 转换为真实业务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上
CustomGatewayFilter:实现接口: GatewayFilter,Ordered
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* Date: 2022-06-28 星期二
* Time: 13:56
* Author: Dily_Su
* Remark: 自定义网关协议
*/
public class CustomGatewayFilter implements GatewayFilter, Ordered {
/**
* 过滤器业务逻辑
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("自定义网关过滤器被执行");
return chain.filter(exchange);
}
/**
* 过滤器执行顺序,数值越小,优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
Configuration
import com.study.gateway.filter.CustomGatewayFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Date: 2022-06-28 星期二
* Time: 14:00
* Author: Dily_Su
* Remark:
*/
@Configuration
public class GatewayRoutesConfiguration {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes().route("test1", r -> r
// 断言(判断条件)
.path("/product/**")
// 自定义网关过滤器
.filters(f -> f.filter(new CustomGatewayFilter()))
// 目标 URI 路由到服务器的地址
.uri("lb://provider"))
.build();
}
}
CustomGlobalFilter:实现接口: GatewayFilter,Ordered
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* Date: 2022-06-28 星期二
* Time: 14:55
* Author: Dily_Su
* Remark: 自定义全局过滤器
* 统一鉴权
*/
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
/**
* 过滤器业务逻辑
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("自定义全局过滤器被执行");
// 获取请求参数
String token = exchange.getRequest().getQueryParams().getFirst("token");
// 业务处理
if (token == null){
ServerHttpResponse response = exchange.getResponse();
// 响应类型
response.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);
// 响应状态, 401 代表没权限
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 响应内容
String message = "{\"message\":" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}";
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes(StandardCharsets.UTF_8));
// 请求结束
return response.writeWith(Mono.just(buffer));
}
// 使用 token 进行身份验证
System.out.println("验证通过");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
顾名思义,限制流量,通过限流控制系统的QPS,从而保护系统
设置规定时间内请求数量
时间节点前后一起发送请求上限的请求,数量可能会超出,拖垮服务
资源浪费
使用队列机制,请求进入网关不做限制,但是从网关访问下行服务时按规定速率请求
请求堆积,网关压力大,网关容易宕机
超出队列上限,可能导致请求丢失
微服务资源浪费
改进的漏桶算法,可以应对突发请求
1、网关恒定速率生成令牌放入令牌桶,桶满后丢弃令牌
2、请求到达后先获取令牌再做业务处理,没拿到令牌的请求丢弃
通过 RequestRateLimiterGatewayFilterFactory 过滤工厂,使用Redis 和 lua 实现令牌桶
KeyResolverConfiguration
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* Date: 2022-06-28 星期二
* Time: 16:45
* Author: Dily_Su
* Remark:
*/
@Configuration
public class KeyResolverConfiguration {
/**
* 限流规则
* URI 限流
* @return
*/
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
}
application.yml
spring:
application:
name: gateway # 应用名称
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: lb://provider # 根据注册中心动态路由
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应的 URL 请求,并追加到 URI 后
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌每秒填充数量
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表达式按名称引用 bean
redis:
host: localhost # redis 地址
port: 6379 # 端口
database: 1 # database
redis 中的 令牌会自动清理
KeyResolverConfiguration
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* Date: 2022-06-28 星期二
* Time: 16:45
* Author: Dily_Su
* Remark: 限流规则
* 下面 Bean 只能有一个
*/
@Configuration
public class KeyResolverConfiguration {
/**
* 参数 限流
*
* @return
*/
@Bean
public KeyResolver parameterKeyResolver() {
return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("id")));
}
}
application.yml
spring:
application:
name: gateway # 应用名称
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: lb://provider # 根据注册中心动态路由
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应的 URL 请求,并追加到 URI 后
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌每秒填充数量
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@parameterKeyResolver}" # 使用 SpEL 表达式按名称引用 bean
redis:
host: localhost
port: 6379
database: 1
KeyResolverConfiguration
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* Date: 2022-06-28 星期二
* Time: 16:45
* Author: Dily_Su
* Remark: 限流规则
* 下面 Bean 只能有一个
*/
@Configuration
public class KeyResolverConfiguration {
/**
* IP 限流
*
* @return
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress().getHostName()));
}
}
application.yml
spring:
application:
name: gateway # 应用名称
cloud:
# 路由规则
gateway:
routes:
- id: provider # 路由 ID,唯一
uri: lb://provider # 根据注册中心动态路由
predicates: # 断言(判断条件)
- Path=/product/** # 匹配对应的 URL 请求,并追加到 URI 后
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌每秒填充数量
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@ipKeyResolver}" # 使用 SpEL 表达式按名称引用 bean
redis:
host: localhost
port: 6379
database: 1
nginx + 网关集群 + docker部署
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-isdc5lu2-1670555453111)(img/高可用网关项目结构.png)]
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.8version>
<relativePath/>
parent>
<groupId>com.studygroupId>
<artifactId>parentartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>pompackaging>
<properties>
<java.version>11java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-cloud.version>2021.0.3spring-cloud.version>
<packaging.type>jarpackaging.type>
<lombok.version>1.18.24lombok.version>
<feign-httpClienr.version>10.7.4feign-httpClienr.version>
<ribbon.version>2.7.18ribbon.version>
properties>
<repositories>
<repository>
<id>centralid>
<name>Nexus aliyunname>
<url>https://maven.aliyun.com/repository/publicurl>
repository>
repositories>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
<version>${feign-httpClienr.version}version>
dependency>
<dependency>
<groupId>com.netflix.ribbongroupId>
<artifactId>ribbon-loadbalancerartifactId>
<version>${ribbon.version}version>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
<profiles>
<profile>
<id>dockerid>
<properties>
<activated.profile>dockeractivated.profile>
<redis.host>cloud-redisredis.host>
<redis.port>6379redis.port>
properties>
<build>
<resources>
<resource>
<filtering>truefiltering>
<directory>src/main/resourcesdirectory>
resource>
resources>
build>
profile>
<profile>
<id>devid>
<properties>
<activated.profile>devactivated.profile>
<redis.host>localhostredis.host>
<redis.port>6379redis.port>
properties>
<build>
<resources>
<resource>
<filtering>truefiltering>
<directory>src/main/resourcesdirectory>
<excludes>
<exclude>application-docker.*exclude>
excludes>
resource>
resources>
build>
profile>
profiles>
project>
# nginx 配置文件:负载均衡默认为轮询
worker_processes 1;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
sendfile on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
client_max_body_size 100m;
# 网关集群
upstream gateway {
# docker 中 localhost 为 镜像容器名
server gateway-01:9000;
server gateway-02:9001;
}
server {
listen 8080;
location / {
# 将所有请求指向 网关集群
proxy_pass http://gateway;
}
}
}
# 每个微服务根目录下都要放置一个,用于将打包好的 jar 整理为 docker image
# jdk 版本不能低于自己编译时的版本,否则 docker 启动会报错
FROM openjdk:11.0.15-jre
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
WORKDIR /
ENTRYPOINT ["java","-jar","/app.jar"]
version: "3.7"
services:
redis:
image: redis:latest # 从 docker hub 中 pull 最新的 redis image
container_name: cloud-redis # docker 中的容器名称
ports: # 对外映射的端口,占用容器内的 6379,本机的26379,不想对外暴漏时,可以省略
- "26379:6379"
networks: # 容器内网关,用于容器内镜像互相调用
- backend
environment: # 设置时区
- TZ=Asia/Shanghai
erurka-01:
build: ./eureka01 # build 存在时,表示从该目录下获取镜像名称
image: cloud/eureka01:latest # 镜像名称
container_name: eureka-01 # 容器名称
ports:
- "28761:8761"
networks:
- backend
environment:
- TZ=Asia/Shanghai
erurka-02:
build: ./eureka02
image: cloud/eureka02:latest
container_name: eureka-02
ports:
- "28762:8762"
networks:
- backend
environment:
- TZ=Asia/Shanghai
gateway-01:
build: ./gateway01
image: cloud/gateway01:latest
container_name: gateway-01
networks:
- backend
environment:
- TZ=Asia/Shanghai
gateway-02:
build: ./gateway02
image: cloud/gateway02:latest
container_name: gateway-02
networks:
- backend
environment:
- TZ=Asia/Shanghai
provider-01:
build: ./provider01
image: cloud/provider01:latest
container_name: provider-01
networks:
- backend
environment:
- TZ=Asia/Shanghai
provider-02:
build: ./provider02
image: cloud/provider02:latest
container_name: provider-02
networks:
- backend
environment:
- TZ=Asia/Shanghai
consumer-eureka:
build: ./consumer-eureka
image: cloud/consumer-eureka:latest
container_name: consumer-eureka
networks:
- backend
environment:
- TZ=Asia/Shanghai
consumer-eureka-feign:
build: ./consumer-eureka-feign
image: cloud/consumer-eureka-feign:latest
container_name: consumer-eureka-feign
networks:
- backend
environment:
- TZ=Asia/Shanghai
nginx:
image: nginx
container_name: cloud-demo-nginx
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- 28080:8080
restart: unless-stopped
networks:
- backend
networks: # 该微服务项目在容器中的网关
backend:
name: cloud-demo
# 清理 targer 文件,并按照 按照 docker 配置 打包
mvn clean package -P docker -DskipTests
# build image 并 生成容器,启动 image
docker compose up -d