基于spring cloud+zuul+sentinel开发微服务网关

基于spring cloud+zuul+sentinel开发微服务网关

前言

本文代码于GitHub - felixlyd/zuul-sentinel-sample: 该demo:1. zuul、ribbon、hystrix实现网关路由转发、限流熔断;2. 引入feign实现微服务调用;3. 考虑接口安全设计

由于项目历史遗留问题,需要使用较老的spring cloud组件集成,并且需要契合老应用做局部改造 因此,有以下考虑和要求:

  • zuul、ribbon、hystrix实现网关路由转发、限流熔断;
  • 引入feign实现微服务调用;
  • 引入Sentinel实现限流和熔断;
  • 考虑接口安全设计;
  • 能够契合老spring cloud应用做局部改造;
  • 能够对限流进行监控;
  • 考虑两台网关的限流;

整合feign、ribbon?

Ribbon:Spring Cloud负载均衡与服务调用组件(非常详细) 引入feign后,feign默认集成了ribbon做负载均衡. feign的默认负载均衡策略为轮询

如何修改feign的默认负载均衡策略?

OpenFeign修改负载均衡策略_wx62e0b69890c77的技术博客_51CTO博客 Spring Cloud Feign 负载均衡策略配置_guoqiusheng的博客-CSDN博客 以上为两种方式,尚未实践

zuul和feign的区别和联系

  1. zuul作为整个应用的流量入口,接收所有的请求,如app、网页等,并且将不同的请求转发至不同的处理微服务模块,其作用可视为nginx;feign则是将当前微服务的部分服务接口暴露出来,并且主要用于各个微服务之间的服务调用。
  2. zuul默认集成hystrix和ribbon,并基于http通讯的,用于代理服务;feign默认集成ribbon,可以通过配置集成hystrix,是在服务互相调用时使用,仿rpc通讯。

引入hystrix?

zuul默认集成hystrix,hystrix只能做微服务应用层级的限流,sentinel可以做到接口级的限流

hystrix原理

Spring Cloud - Hystrix 原理解析 - 掘金

引入zuul

hystrix的限流,是基于设置最大的线程池/信号量,来进行限流,因此,是有限的限流

对hystrix进行监控?

需引入hystrix-dashboardturbine

引入zuul

spring cloud zuul 原理简介及使用 Zuul 网关转发的五种方式 - qiuxuhui - 博客园

引入Sentinel

版本说明 · alibaba/spring-cloud-alibaba Wiki 网关限流 · alibaba/spring-cloud-alibaba Wiki introduction | Sentinel Sentinel/sentinel-demo/sentinel-demo-zuul-gateway at master · alibaba/Sentinel 一篇文章彻底学会使用Spring Cloud Alibaba Sentinel GitHub - sentinel-group/sentinel-awesome: A curated list of awesome things (resource, sample, extensions) for Sentinel 阿里巴巴开源限流降级神器Sentinel大规模生产级应用实践

Sentinel整合到zuul上

sentinel对接口级进行流控

sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和**自定义 API **的实体和管理逻辑:

  • GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
  • ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。

Q:sentinel有分组api限流的功能,如果我的两个接口都放在同一个api下面,设置10s内最多访问5次。那么我实际的访问中,是两个接口一共每10s内最多访问5次,还是两个接口分别10s内最多访问5次?

A:如果你的两个接口都放在同一个 API 下面,设置 10s 内最多访问 5 次,那么你实际的访问中,两个接口一共每 10s 内最多访问 5 次。这意味着如果一个接口被访问了 5 次,另一个接口在 10s 内不能再被访问。

应用被sentinel识别为网关

方式1:启动参数

java -Dcsp.sentinel.app.type=1 -jar zuul-gateway.jar

方式2:配置文件 在资源目录下新建sentinel.properties文件,使用以下配置

csp.sentinel.app.type=1

方式3:代码 在main主函数中加入以下代码

System.setProperty("csp.sentinel.app.type", "1")

Sentinel-Dashboard对流量的监控

sentinel-dashboard示例图

相比hystrix的dashboard

sentinel监控数据持久化?

dashboard实时监控仅能查看5分钟内的metric数据,持久化到数据库中?需要自行实现相关接口代码。

Sentinel规则持久化

  1. 代码
  2. 文件
  3. 关系数据库

默认情况下:sentinel-dashboard能够读取代码中的规则配置到内存中,并且可以热修改

Sentinel规则语法

流控规则

流量控制 · alibaba/spring-cloud-alibaba Wiki

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource:资源名,即限流规则的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型(QPS 或并发线程数)
  • limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
  • strategy: 调用关系限流策略
  • controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

熔断降级

熔断降级 · alibaba/spring-cloud-alibaba Wiki

熔断降级规则(DegradeRule)包含下面几个重要的属性:

Field 说明 默认值
resource 资源名,即规则的作用对象
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

系统规则

system-adaptive-protection | Sentinel

Sentinel/SystemGuardDemo.java at master · alibaba/Sentinel

Sentinel 做系统自适应保护的目的:

  • 保证系统不被拖垮
  • 在系统稳定的前提下,保持系统的吞吐量

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量。 系统自适应限流:Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5 .

热点参数

parameter-flow-control | Sentinel

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

在网关限流中,GatewayFlowRule 下的paramItem 会配置该规则是热点规则还是普通流控规则

黑白名单?

网关限流中没有 黑白名单控制 · alibaba/spring-cloud-alibaba Wiki

网关api限流规则

其中网关限流规则 GatewayFlowRule 的字段解释如下:

  • resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
  • resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。
  • grade:限流指标维度,同限流规则的 grade 字段。
  • count:限流阈值
  • intervalSec:统计时间窗口,单位是秒,默认是 1 秒。
  • controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
  • burst:应对突发请求时额外允许的请求数目。
  • maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
  • paramItem:**参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。**其中的字段:
    • parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。
    • fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。
    • pattern:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)
    • matchStrategy:参数值的匹配策略,目前支持精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持)

用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或通过GatewayRuleManager.register2Property(property) 注册动态规则源动态推送(推荐方式)。

集群流控?

网关限流中没有 为什么要使用集群流控呢?假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控的方式。 另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。**不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。**因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。

Sentinel多台机器

后端

后端机器限流是单机限流,如果有2台机器,希望QPS限制到100,那么单机设为50.

网关

例如某个单机app的QPS限制100,启动两台网关,两个网关加起来限制该app的QPS为100.

方案讨论

网关+限流

  1. zuul+sentinel 所有请求都会经过监听
  2. sentinel 手动加api接口进行监听,就不会影响之前的代码
  3. adapter往一个adapter2上转发(双网关思路),adapter2为一个新的用于做数据安全、限流、分发的网关

数据安全处理、xml和json转换

数据安全处理内容:加解密、解签名、防重放验证

zuul流程示意图

zuul 1.x流程

zuul转发:http请求->prefilter->routingfilter->后端应用->postfilter->http响应

zuul 2.x流程

spring cloud gateway 流程

方案1:http请求和esb请求由zuul网关转发

http请求能通过controller层手动转发,esb请求(netty请求)转发需要发起新的http请求访问后端应用。

http请求通过controller层手动转发请求

方式1:

@PostMapping("/{path1}")
    public String test(@PathVariable String path1){
        log.info("/rule/" + path1);
        return "/rule/" + path1;
    }

@PostMapping("/{path1}/{path2}")
public String test2(@PathVariable String path1, @PathVariable String path2){
    log.info("/rule/" + path1 + "/" + path2);
    return "/rule/" + path1 + "/" + path2;
}

@PostMapping("/{path1}/{path2}/{path3}")
public String test2(@PathVariable String path1, @PathVariable String path2, @PathVariable String path3){
    log.info("/rule/" + path1 + "/" + path2 + "/" + path3);
    return "/rule/" + path1 + "/" + path2 + "/" + path3;
}

spring boot 2.6.0之前对多级路径支持不友好.

方式2:

@PostMapping("/**")
public String test2(HttpServletRequest request){
    String path = request.getServletPath();
    List<String> paths = Arrays.stream(path.split("/")).filter(i -> !i.contains("rule-controller")).collect(Collectors.toList());
    String zuulPath = "/rule/" + String.join("/", paths);
    log.info(zuulPath);
    return zuulPath;
}

esb请求发起新的http请求

@PostMapping("/**")
public String test2(HttpServletRequest request, @RequestBody Map<String, String> reqMap){
    log.info("解密--");
    String path = request.getServletPath();
    List<String> paths = Arrays.stream(path.split("/")).filter(i -> !i.contains("rule-controller")).collect(Collectors.toList());
    String zuulPath = "http://localhost:8081/rule" + String.join("/", paths);
    RestTemplate restTemplate = new RestTemplate();
    String responseBody = restTemplate.postForObject(zuulPath, reqMap, String.class);
    log.info("加密--");
    return responseBody;
}

esb请求发起http请求如果走feign,则不会过zuul的转发;如果走restTemplate,需要告诉ip:port发送给网关自己(可以走localhost:port,也可以走域名(F5)去负载均衡),然后到zuul,再由zuul转发。

PRE-网关转发请求前

前端请求报文需要数据安全设计(解密、防重放、签名),以及esb请求报文协议转换需要xml转换为json

  1. 不自定义zuul的prefilter。对于http请求,先进行数据安全处理、报文协议转换,然后通过controller转发到zuul,再由zuul做转发;对于esb请求,先进行数据安全处理、报文协议转换,然后发起新的http请求到zuul,再由zuul做转发。
  2. 自定义zuul的prefilter。对于http请求,在zuul的prefilter中实现数据安全处理、报文协议转换;对于esb请求,发起新的http请求到zuul,再由zuul的prefilter进行数据安全处理、报文协议转换。

zuul 1.x不支持netty转发

POST-网关转发请求后

前端请求的响应报文需要加密,以及esb请求的响应报文需要json转xml

  1. 不自定义zuul的postfilter。对于http请求通过controller转发到zuul,不太可能,因为zuul转发会通过postfilter直接返回调用方。必须在postfilter中处理响应报文,或者通过研究源码写切面从postfilter捞取响应报文;对于esb请求,发起了新的http请求,可以实现。但是,发起http请求如果走feign,则不会过zuul的转发;如果走restTemplate,需要告诉ip:port发送给网关自己(可以走localhost:port,也可以走域名(F5)去负载均衡),然后到zuul,再由zuul转发。
  2. 自定义zuul的postfilter。对于http请求,在zuul的postfilter中实现数据安全处理、报文协议转换;对于esb请求,发起新的http请求到zuul,再由zuul的postfilter进行数据安全处理、报文协议转换。

综上所述,如果是http请求,直接在zuul网关中的prefilter和postfilter中处理请求响应报文最简便。如果是esb请求,发起新的http请求给网关自己,也直接在zuul网关中prefilter和postfilter处理请求响应报文。

如果均走prefilter和postfilter,就需要区分二者。因为http请求只需要数据安全处理和限流而不需要报文协议转换,esb请求只需要报文协议转换不涉及数据安全处理,可能涉及限流。

方案2:http请求走zuul网关转发,esb请求沿用feign客户端转发

  1. http请求:分别在zuul网关中的prefilter和postfilter中实现对应数据安全处理。
  2. esb请求:沿用之前的流程

因为http请求和esb请求处理请求响应报文规则不同,因此http请求走zuul,改prefilter和postfilter;esb请求在feign客户端前后另加一套逻辑。