今天更新RPC的最后一篇,网关,由于公司用到是Zuul,这篇着重对Zuul的介绍。
服务网关是统一管理API的一个网络关口、通道,是整个微服务平台所有请求的唯一入口,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。主要功能:
1、路由转发:接收一切外界请求,转发到后端的微服务上去;
2、过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等;
目前zuul有两个版本:zuul1和zuul2,目前spring cloud只集成了zuul1。zuul2是Netflix在2018年5月推出,它最大的特点就是支持异步调用 (zuul1仅支持同步) ,可惜springcloud暂时没有计划集成zuul2,而且还推出spring cloud gateway来替代zuul1。
来看下zuul1的编程模型(同步阻塞)
本质上就是一个同步 Servlet,每来一个请求,zuul会专门分配一个线程去处理,然后转发到后端服务,后端再启线程处理请求,后端处理时网关的线程会阻塞,当请求数量比较大时,很容易造成线程池被沾满而无法接受新的请求,Netflix 为此还专门研发了Hystrix熔断组件来解决慢服务耗尽资源问题。
zuul2的编程模型(异步非阻塞)
zuul2是基于Netty实现的异步非阻塞编程模型,一般异步模式的本质都是使用队列 Queue(或称总线 Bus)。网关中会有一个队列专门处理用户请求,一个队列专门负责后端服务调用,中间有个事件环线程 (Event Loop Thread)同时监听两个队列,它的主要作用是将请求转发给后端,并将后端服务的处理结果返回给客户端,用队列的形式减轻了前端请求数量的压力。
zuul1与spring-cloud-gateway的区别
1、gateway对比zuul多依赖了spring-webflux(地产netty),内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件。
zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等。
2、zuul仅支持同步,gateway支持异步。
3、gateway线程开销少,支持各种长连接、websocket,spring官方支持,但运维复杂,
zuul编程模型简单,开发调试运维简单,有线程数限制,延迟堵塞会耗尽线程连接资源。
来源:服务网关与Gateway,Zuul详解
Zuul负载均衡
Zull默认结合Ribbon,默认使用使用的轮询算法。
可以使用其他策略
@Configuration
public class RibbonClientConfig {
@Bean
public IRule getRule() {
return new RandomRule();
}
}
Zuul降级限流
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-zuul-adapter</artifactId>
</dependency>
网关流控实现原理
当通过 GatewayRuleManager 加载网关流控规则(GatewayFlowRule)时,无论是否针对请求属性进行限流,Sentinel 底层都会将网关流控规则转化为热点参数规则(ParamFlowRule),存储在 GatewayRuleManager 中,与正常的热点参数规则相隔离。转换时 Sentinel 会根据请求属性配置,为网关流控规则设置参数索引(idx),并同步到生成的热点参数规则中。
外部请求进入 API Gateway 时会经过 Sentinel 实现的 filter,其中会依次进行 路由/API 分组匹配、请求属性解析和参数组装。Sentinel 会根据配置的网关流控规则来解析请求属性,并依照参数索引顺序组装参数数组,最终传入 SphU.entry(res, args) 中。Sentinel API Gateway Adapter Common 模块向 Slot Chain 中添加了一个 GatewayFlowSlot,专门用来做网关规则的检查。GatewayFlowSlot 会从 GatewayRuleManager 中提取生成的热点参数规则,根据传入的参数依次进行规则检查。若某条规则不针对请求属性,则会在参数最后一个位置置入预设的常量,达到普通流控的效果。
在 API Gateway 端,用户只需要在原有启动参数的基础上添加如下启动参数即可标记应用为 API Gateway 类型:
# 注:通过 Spring Cloud Alibaba Sentinel 自动接入的 API Gateway 整合则无需此参数
-Dcsp.sentinel.app.type=1
添加正确的启动参数并有访问量后,我们就可以在 Sentinel 上面看到对应的 API Gateway 了。我们可以查看实时的 route 和自定义 API 分组的监控和调用信息:
其中Route ID需要在代码中配置
server:
port: 8090
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
enabled: true
discovery:
locator:
lower-case-service-id: true
routes:
# Add your routes here.
- id: product_route
uri: lb://product
predicates:
- Path=/product/**
- id: httpbin_route
uri: https://httpbin.org
predicates:
- Path=/httpbin/**
filters:
- RewritePath=/httpbin/(?<segment>.*), /$\{segment}
自定义API在API管理添加即可
来源:https://sentinelguard.io/zh-cn/docs/api-gateway-flow-control.html
Zuul常用配置
同一个系统中各个服务之间通过Headers来共享信息是没啥问题的,但是如果不想Headers中的一些敏感信息随着HTTP转发泄露出去话,需要在路由配置中指定一个忽略Header的清单。
如果客户端在发请求是带了X-ABC,那么X-ABC不会传递给下游服务
# 对指定路由开启自定义敏感头
zuul.routes.[route].customSensitiveHeaders=true
zuul.routes.[route].sensitiveHeaders=[这里设置要过滤的敏感头]
如果客户端在发请求是带了X-ABC,那么X-ABC依然会传递给下游服务。但是如果下游服务再转发就会被过滤
zuul.ignoredHeaders=[这里设置要忽略的Header]
spring-cloud-starter-zuul本身已经集成了hystrix和ribbon,所以Zuul天生就拥有线程隔离和断路器的自我保护能力,以及对服务调用的客户端负载均衡功能。但是,我们需要注意,当使用path与url的映射关系来配置路由规则时,对于路由转发的请求则不会采用HystrixCommand来包装,所以这类路由请求就没有线程隔离和断路器保护功能,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合进行配置,这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。
可以通过zuul.prefix可为所有的映射增加统一的前缀。如: /api。默认情况下,代理会在转发前自动剥离这个前缀。如果需要转发时带上前缀,可以配置: zuul.stripPrefix=false来关闭这个默认行为。例如:
zuul.routes.networking-serverforward.path = /networking/open-api/v1/**
zuul.routes.networking-serverforward.serviceId = networking-server
zuul.routes.networking-serverforward.stripPrefix = false
Zuul代码过滤
1,filterType代表过滤类型:PRE,ROUTING,POST,ERROR
pre:在请求被路由之前调用, 利用这种路由器进行鉴权等服务, 记录日志、 限流
route:在路由请求时候被调用,作用是将请求路由到指定服务中去, 用于构建发送给微服务的请求, 并且用Http Client(或者Ribbon)请求微服务
post:在route和error过滤器之后被调用,可以用来添加Header , 记录日志, 将响应返回给客户端。
error:处理请求时发生错误时调用
2,filterOrder代表过滤器顺序
3,shouldFilter代表这个过滤器是否生效(一般结合注册中心使用)
比如:
验证签名,不需要token,对requestbody不需要加解密,对response body也不加密
signed.without.token.plaintext.path=
不需要验证签名(request和response不进行aes加密),token存在即验证转化userId,nickname等字段,不存在则不验证
no.necessary.token.path=
不需要签名(reqest和response不进行AES加密),只需要验证token,对requestbody不需要加解密
inner.interface.url.prefix = /api/device/open-api=
4,run:这是我们的过滤器的方法主题在这里写我们主要的方法(解密,token验证,权限验证,重构请求或者响应,解密)
run中执行return还是会进行路由转发的,如果过滤的校验没有通过,我们就不希望zuul转发到后端服务器上了,这样的话我们就可以在run的逻辑中写上
RequestContext.getCurrentContext().setSendZuulResponse(false)
一般用于try-catch,catch中或者pre校验失败返回
下图是filter的执行顺序:
zuul修改请求响应body
一般用于pre的try-catch,catch中或者post/error的返回
public class ModifyResponseBodyFilter extends ZuulFilter {
public String filterType() {
return "post";
}
public int filterOrder() {
return 999;
}
public boolean shouldFilter() {
return true;
}
public Object run() {
try {
RequestContext context = getCurrentContext();
InputStream stream = context.getResponseDataStream();
String body = StreamUtils.copyToString(stream, Charset.forName("UTF-8"));
context.setResponseBody("new body: "+body);
}
catch (IOException e) {
rethrowRuntimeException(e);
}
return null;
}
}
参考:zuul网关Filter处理流程及异常处理