什么是服务网关
传统的单体架构中只需要开放一个服务给客户端调用,但是微服务架构中是将一个系统拆分成多个微服务,如果没有网关,客户端只能在本地记录每个微服务的调用地址,当需要调用的微服务数量很多时,它需要了解每个服务的接口,这个工作量很大。那有了网关之后,能够起到怎样的改善呢?
网关作为系统的唯一流量入口,封装内部系统的架构,所有请求都先经过网关,由网关将请求路由到合适的微服务,所以,使用网关的好处在于:
- 简化客户端的工作。 网关将微服务封装起来后,客户端只需同网关交互,而不必调用各个不同服务;
- 降低函数间的耦合度。 一旦服务接口修改,只需修改网关的路由策略,不必修改每个调用该函数的客户端,从而减少了程序间的耦合性
- 解放开发人员把精力专注于业务逻辑的实现。
由网关统一实现服务路由(灰度与ABTest)、负载均衡、访问控制、流控熔断降级等非业务相关功能,而不需要每个服务 API 实现时都去考虑
但是 API 网关也存在不足之处,在微服务这种去中心化的架构中,网关又成了一个中心点或瓶颈点,它增加了一个我们必须开发、部署和维护的高可用组件。正是由于这个原因,在网关设计时必须考虑即使 API 网关宕机也不要影响到服务的调用和运行,所以需要对网关的响应结果有数据缓存能力,通过返回缓存数据或默认数据屏蔽后端服务的失败。
服务网关的基本功能:
流量网关与服务网关的区别
流量网关和服务网关在系统整体架构中所处的位置如上图所示,流量网关(如Nignx)是指提供全局性的、与后端业务应用无关的策略,例如 HTTPS证书卸载、Web防火墙、全局流量监控等。
而微服务网关(如Spring Cloud Gateway)是指与业务紧耦合的、提供单个业务域级别的策略,如服务治理、身份认证等。也就是说,流量网关负责南北向流量调度及安全防护,微服务网关负责东西向流量调度及服务治理。
服务网关的介绍
- **Kong 网关:**Kong 的性能非常好,非常适合做流量网关,但是对于复杂系统不建议业务网关用 Kong,主要是工程性方面的考虑
- Zuul1.x 网关:Zuul 1.0
的落地经验丰富,但是性能差、基于同步阻塞IO,适合中小架构,不适合并发流量高的场景,因为容易产生线程耗尽,导致请求被拒绝的情况 - gateway 网关:功能强大丰富,性能好,官方基准测试 RPS (每秒请求数)是Zuul的1.6倍,能与 SpringCloud
生态很好兼容,单从流式编程+支持异步上也足以让开发者选择它了。 - Zuul 2.x:性能与 gateway 差不多,基于非阻塞的,支持长连接,但 SpringCloud 没有集成 zuul2 的计划,并且
Netflix 相关组件都宣布进入维护期,前景未知。
综上,gateway 网关更加适合 SpringCloud 项目,而从发展趋势上看,gateway 替代 zuul 也是必然的。
Spring Cloud Gateway 网关的
- pom.xml 的 引入
<properties>
<spring-boot.version>2.3.2.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
</properties>
<!-- 只声明依赖,不引入依赖 -->
<dependencyManagement>
<dependencies>
<!-- 声明springBoot版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 声明springCloud版本 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 声明 springCloud Alibaba 版本 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 引入gateway网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
- 配置项目名与端口:
server:
port: 8100
spring:
application:
name: gateway-service
cloud:
nacos:
server-addr: 33.183.134.49:8848
gateway:
routes:
- id: mqtt-service-route
uri: lb://mqtt-service
predicates:
- Path=/mqtt/**
- id: usermgr-service-route
uri: lb://usermgr-service
predicates:
- Path=/usermgr/**
- Spring Cloud Gateway 配置项的说明:
- 断言(Predicate):参照 Java8 的新特性Predicate,允许开发人员匹配 HTTP请求中的任何内容,比如请求头或请求参数,最后根据匹配结果返回一个布尔值。
- 路由(route):由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则转发到该路由。
- 过滤器(filter):可以在返回请求之前或之后修改请求和响应的内容。
- 路由 Route:
Route 主要由 路由id、目标uri、断言集合和过滤器集合组成,那我们简单看看这些属性到底有什么作用。 - id:路由标识,要求唯一,名称任意(默认值 uuid,一般不用,需要自定义)
- uri:请求最终被转发到的目标地址
- order:路由优先级,数字越小,优先级越高
- predicates:断言数组,即判断条件,如果返回值是boolean,则转发请求到 uri 属性指定的服务中
- filters:过滤器数组,在请求传递过程中,对请求做一些修改
- 断言 Predicate
Predicate 来自于 Java8 的接口。Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
Predicate 可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。Spring Cloud Gateway 内置了许多 Predict,这些 Predict 的源码在 org.springframework.cloud.gateway.handler.predicate 包中。
实例如下:
spring:
cloud:
gateway:
# 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
routes:
# 路由标识,要求唯一,名称任意
- id: gateway-provider_1
# 请求最终被转发到的目标地址
uri: http://mqtt-service
# 设置断言
predicates:
# Path Route Predicate Factory 断言,满足 /gateway/provider/** 路径的请求都会被路由到 http://localhost:9024 这个uri中
- Path=/mqtt/**
# Weight Route Predicate Factory 断言,同一分组按照权重进行分配流量,这里分配了80%
# 第一个group1是分组名,第二个参数是权重
- Weight=group1, 8
# 配置过滤器(局部)
filters:
# StripPrefix:去除原始请求路径中的前1级路径,即/gateway
- StripPrefix=1
- 过滤器 filter:
Gateway 过滤器的生命周期: - PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP
Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 过滤器从作用范围可分为两种: - GatewayFilter:应用到单个路由或者一个分组的路由上(需要在配置文件中配置)
- GlobalFilter:应用到所有的路由上(无需配置,全局生效)
- 自定义局部过滤器
虽说内置的过滤器能够解决很多场景,但是难免还是有些特殊需求需要定制一个过滤器,下面就来介绍一下如何自定义局部过滤器。
/**
* 名称必须是xxxGatewayFilterFactory形式
* todo:模拟授权的验证,具体逻辑根据业务完善
*/
@Component
@Slf4j
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config> {
private static final String AUTHORIZE_TOKEN = "token";
//构造函数,加载Config
public AuthorizeGatewayFilterFactory() {
//固定写法
super(AuthorizeGatewayFilterFactory.Config.class);
log.info("Loaded GatewayFilterFactory [Authorize]");
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
//Config.enabled
return Arrays.asList("enabled");
}
@Override
public GatewayFilter apply(AuthorizeGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
//判断是否开启授权验证
if (!config.isEnabled()) {
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
//从请求头中获取token
String token = headers.getFirst(AUTHORIZE_TOKEN);
if (token == null) {
//从请求头参数中获取token
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}
ServerHttpResponse response = exchange.getResponse();
//如果token为空,直接返回401,未授权
if (StringUtils.isEmpty(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//处理完成,直接拦截,不再进行下去
return response.setComplete();
}
/**
* todo chain.filter(exchange) 之前的都是过滤器的前置处理
*
* chain.filter().then(
* 过滤器的后置处理...........
* )
*/
//授权正常,继续下一个过滤器链的调用
return chain.filter(exchange);
};
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
// 控制是否开启认证
private boolean enabled;
}
}
局部过滤器需要在路由中配置才能生效,配置如下:
spring:
cloud:
gateway:
routes:
- id: gateway-provider_1
uri: http://localhost:9024
predicates:
- Path=/gateway/provider/**
## 配置过滤器(局部)
filters:
- AddResponseHeader=X-Response-Foo, Bar
## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
- Authorize=true
- GlobalFilter 全局过滤器:
全局过滤器应用全部路由上,无需开发者配置
@Slf4j
@Component
@Order(value = Integer.MIN_VALUE)
public class AccessLogGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//filter的前置处理
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().pathWithinApplication().value();
InetSocketAddress remoteAddress = request.getRemoteAddress();
return chain
//继续调用filter
.filter(exchange)
//filter的后置处理
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
HttpStatus statusCode = response.getStatusCode();
log.info("请求路径:{},远程IP地址:{},响应码:{}", path, remoteAddress, statusCode);
}));
}
}
全局过滤器不必在路由上配置,注入到IOC容器中即可全局生效。
网关自定义全局异常处理器:
/**
* 用于网关的全局异常处理
* @Order(-1):优先级一定要比ResponseStatusExceptionHandler低
*/
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@SuppressWarnings({"rawtypes", "unchecked", "NullableProblems"})
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
// JOSN格式返回
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatus());
}
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
//todo 返回响应结果,根据业务需求,自己定制
CommonResponse resultMsg = new CommonResponse("500",ex.getMessage(),null);
return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMsg));
}
catch (JsonProcessingException e) {
log.error("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}