Spring Cloud 系列之 Netflix Zuul 服务网关(三)
本篇文章为系列文章,未读前几集的同学请猛戳这里:
本篇文章讲解 Zuul 和 Hystrix 的无缝结合,实现网关监控、网关熔断、网关限流、网关调优。
1|0Zuul 和 Hystrix 无缝结合
在 Spring Cloud 中,Zuul 启动器中包含了 Hystrix 相关依赖,在 Zuul 网关工程中,默认是提供了 Hystrix Dashboard 服务监控数据的(hystrix.stream),但是不会提供监控面板的界面展示。在 Spring Cloud 中,Zuul 和 Hystrix 是无缝结合的,我们可以非常方便的实现网关容错处理。
关于 Hystrix 服务监控更多内容请猛戳:Spring Cloud 系列之 Netflix Hystrix 服务监控
1|1网关监控
Zuul 的依赖中包含了 Hystrix 的相关 jar 包,所以我们不需要在项目中额外添加 Hystrix 的依赖。
但是需要开启数据监控的项目中要添加 dashboard
依赖。
配置文件
在配置文件中开启 hystrix.stream
端点。
启动类
在需要开启数据监控的项目启动类中添加 @EnableHystrixDashboard
注解。
访问并查看数据
访问:http://localhost:9000/hystrix 监控中心界面如下:
请求多次:http://localhost:9000/product-service/product/1?token=abc123 结果如下:
1|2网关熔断
在 Edgware 版本之前,Zuul 提供了接口 ZuulFallbackProvider
用于实现 fallback 处理。从 Edgware 版本开始,Zuul 提供了接口 FallbackProvider
来提供 fallback 处理。
Zuul 的 fallback 容错处理逻辑,只针对 timeout 异常处理,当请求被 Zuul 路由后,只要服务有返回(包括异常),都不会触发 Zuul 的 fallback 容错逻辑。
因为对于Zuul网关来说,做请求路由分发的时候,结果由远程服务运算。远程服务反馈了异常信息,Zuul 网关不会处理异常,因为无法确定这个错误是否是应用程序真实想要反馈给客户端的。
代码示例
ProductProviderFallback.java
访问
关闭商品服务,访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下:
1|3网关限流
顾名思义,限流就是限制流量,就像你宽带包有 1 个 G 的流量,用完了就没了。通过限流,我们可以很好地控制系统的 QPS,从而达到保护系统的目的。Zuul 网关组件也提供了限流保护。当请求并发达到阀值,自动触发限流保护,返回错误结果。只要提供 error 错误处理机制即可。
为什么需要限流
比如 Web 服务、对外 API,这种类型的服务有以下几种可能导致机器被拖垮:
- 用户增长过快(好事)
- 因为某个热点事件(微博热搜)
- 竞争对象爬虫
- 恶意的请求
这些情况都是无法预知的,不知道什么时候会有 10 倍甚至 20 倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的。
从上图可以看出,对内而言:上游的 A、B 服务直接依赖了下游的基础服务 C,对于 A,B 服务都依赖的基础服务 C 这种场景,服务 A 和 B 其实处于某种竞争关系,如果服务 A 的并发阈值设置过大,当流量高峰期来临,有可能直接拖垮基础服务 C 并影响服务 B,即雪崩效应。
限流算法
常见的限流算法有:
- 计数器算法
- 漏桶(Leaky Bucket)算法
- 令牌桶(Token Bucket)算法
计数器算法
点击链接观看:计数器算法视频(获取更多请关注公众号「哈喽沃德先生」)
计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于 A 接口来说,我们 1 分钟的访问次数不能超过 100 个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器 counter,每当一个请求过来的时候,counter 就加 1,如果 counter 的值大于 100 并且该请求与第一个请求的间隔时间还在 1 分钟之内,触发限流;如果该请求与第一个请求的间隔时间大于 1 分钟,重置 counter 重新计数,具体算法的示意图如下:
这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题,我们看下图:
从上图中我们可以看到,假设有一个恶意用户,他在 0:59 时,瞬间发送了 100 个请求,并且 1:00 又瞬间发送了 100 个请求,那么其实这个用户在 1 秒里面,瞬间发送了 200 个请求。我们刚才规定的是 1 分钟最多 100 个请求,也就是每秒钟最多 1.7 个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
还有资料浪费的问题存在,我们的预期想法是希望 100 个请求可以均匀分散在这一分钟内,假设 30s 以内我们就请求上限了,那么剩余的半分钟服务器就会处于闲置状态,比如下图:
漏桶算法
点击链接观看:漏桶算法视频(获取更多请关注公众号「哈喽沃德先生」)
漏桶算法其实也很简单,可以粗略的认为就是注水漏水的过程,往桶中以任意速率流入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。
漏桶算法是使用队列机制实现的。
漏桶算法主要用途在于保护它人(服务),假设入水量很大,而出水量较慢,则会造成网关的资源堆积可能导致网关瘫痪。而目标服务可能是可以处理大量请求的,但是漏桶算法出水量缓慢反而造成服务那边的资源浪费。
漏桶算法无法应对突发调用。不管上面流量多大,下面流出的速度始终保持不变。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就会丢弃。
令牌桶算法
点击链接观看:令牌桶算法视频(获取更多请关注公众号「哈喽沃德先生」)
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。
场景大概是这样的:桶中一直有大量的可用令牌,这时进来的请求可以直接拿到令牌执行,比如设置 QPS 为 100/s,那么限流器初始化完成一秒后,桶中就已经有 100 个令牌了,等服务启动完成对外提供服务时,该限流器可以抵挡瞬时的 100 个请求。当桶中没有令牌时,请求会进行等待,最后相当于以一定的速率执行。
Zuul 内部使用 Ratelimit 组件实现限流,使用的就是该算法,大概描述如下:
- 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
- 根据限流大小,设置按照一定的速率往桶里添加令牌;
- 桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
- 请求到达后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
- 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。
漏桶算法主要用途在于保护它人,而令牌桶算法主要目的在于保护自己,将请求压力交由目标服务处理。假设突然进来很多请求,只要拿到令牌这些请求会瞬时被处理调用目标服务。
添加依赖
Zuul 的限流保护需要额外依赖 spring-cloud-zuul-ratelimit 组件,限流数据采用 Redis 存储所以还要添加 Redis 组件。
RateLimit 官网文档:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit
全局限流配置
使用全局限流配置,Zuul 会对代理的所有服务提供限流保护。
Zuul-RateLimiter 基本配置项:
配置项 | 可选值 | 说明 |
---|---|---|
enabled | true/false | 是否启用限流 |
repository | REDIS:基于 Redis,使用时必须引入 Redis 相关依赖 CONSUL:基于 Consul JPA:基于 SpringDataJPA,需要用到数据库 使用 Java 编写的基于令牌桶算法的限流库: BUCKET4J_JCACHE BUCKET4J_HAZELCAST BUCKET4J_IGNITE BUCKET4J_INFINISPAN |
限流数据的存储方式,无默认值必填项 |
key-prefix | String | 限流 key 前缀 |
default-policy-list | List of Policy | 默认策略 |
policy-list | Map of Lists of Policy | 自定义策略 |
post-filter-order | - | postFilter 过滤顺序 |
pre-filter-order | - | preFilter 过滤顺序 |
Bucket4j 实现需要相关的 bean @Qualifier("RateLimit"):
- JCache - javax.cache.Cache
- Hazelcast - com.hazelcast.core.IMap
- Ignite - org.apache.ignite.IgniteCache
- Infinispan - org.infinispan.functional.ReadWriteMap
Policy 限流策略配置项说明:
项 | 说明 |
---|---|
limit | 单位时间内请求次数限制 |
quota | 单位时间内累计请求时间限制(秒),非必要参数 |
refresh-interval | 单位时间(秒),默认 60 秒 |
type | 限流方式: ORIGIN:访问 IP 限流 URL:访问 URL 限流 USER:特定用户或用户组限流(比如:非会员用户限制每分钟只允许下载一个文件) URL_PATTERN ROLE HTTP_METHOD |
访问
访问:http://localhost:9000/product-service/product/1?token=abc123 控制台结果如下:
查看 Redis
局部限流配置
使用局部限流配置,Zuul 仅针对配置的服务提供限流保护。
访问:http://localhost:9000/order-service/order/1?token=abc123 控制台结果如下:
查看 Redis
自定义限流策略
如果希望自己控制限流策略,可以通过自定义 RateLimitKeyGenerator
的实现来增加自己的策略逻辑。
修改商品服务控制层代码如下,添加 /product/single
:
自定义限流策略类。
多次访问:http://localhost:9000/api/product-service/product/single?token=abc123&id=1 被限流后,马上更换 id=2
重新访问发现服务任然可用,再继续多次访问,发现更换过的 id=2
也被限流了。Redis 信息如下:
错误处理
配置 error
类型的网关过滤器进行处理即可。修改之前的 ErrorFilter
让其变的通用。
还有一种方法是实现 org.springframework.boot.web.servlet.error.ErrorController
重写 getErrorPath()
本文中不做重点讲解。
多次访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下:
1|4网关调优
使用 Zuul 的 Spring Cloud 微服务结构图:
从上图中可以看出。整体请求逻辑还是比较复杂的,在没有 Zuul 网关的情况下,client 请求 service 的时候,也有请求超时的可能。那么当增加了 Zuul 网关的时候,请求超时的可能就更明显了。
当请求通过 Zuul 网关路由到服务,并等待服务返回响应,这个过程中 Zuul 也有超时控制。Zuul 的底层使用的是 Hystrix + Ribbon 来实现请求路由。
Zuul 中的 Hystrix 内部使用线程池隔离机制提供请求路由实现,其默认的超时时长为 1000 毫秒。Ribbon 底层默认超时时长为 5000 毫秒。如果 Hystrix 超时,直接返回超时异常。如果 Ribbon 超时,同时 Hystrix 未超时,Ribbon 会自动进行服务集群轮询重试,直到 Hystrix 超时为止。如果 Hystrix 超时时长小于 Ribbon 超时时长,Ribbon 不会进行服务集群轮询重试。
配置文件
Zuul 中可配置的超时时长有两个位置:Hystrix 和 Ribbon。具体配置如下:
添加依赖
Spring Cloud Netflix Zuul 网关重试机制需要使用 spring-retry 组件。
启动类
启动类需要开启 @EnableRetry
重试注解。
模拟超时
商品服务模拟超时。
访问
配置前访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下(触发了网关服务降级):
配置后访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下:
思考:根据目前的配置如果访问订单服务会怎么样?
下一篇我们讲解 Zuul 和 Sentinel 整合,实现网关限流和容错以及高可用网关环境搭建,记得关注噢~