实战Alibaba Sentinel:深度解析微服务高并发流量治理
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

引言 笔者经历的一次服务雪崩

什么是服务雪崩?

雪崩一词的本意是指山地积雪由于底部溶解等原因而突然大块塌落,具有很强的破坏力;而在微服务项目中,雪崩是指因突发流量导致某个服务不可用,从而导致上游服务不可用,并产生级联效应,最终导致整个系统不可用的现象。

服务雪崩,听到这个词就能想到问题的严重性……

那一次服务雪崩,公司整条业务线的服务都中断了,从该业务线延伸出来的下游业务线也被波及。笔者连续三天两夜忙着处理问题,睡眠时间加起来不足5小时,也正因如此,对其印象非常深刻。

事发那天,我们正在进行每周的技术分享会,一位运营工作人员推开会议室的大门传来“噩耗”,画面瞬间转变,技术分享会变成了问题排查讨论会。

通过查看当时服务的负载均衡统计记录,我们发现并发请求量增长了一倍,从每分钟3万~4万个请求数,增长到每分钟约8.6万个请求数。而在事发之前,服务一直稳定运行,很显然,这次事故与并发请求量翻倍有直接关系。

这是由笔者负责技术选型与架构设计的一个分布式广告系统,也是笔者入门分布式微服务实战的第一个项目。从设计到实现该项目,遇到过很多的难题,熬了很多个夜晚,但也颇有收获。

服务的部署如下。

• 广告点击服务(RPC远程调用服务消费者):两台2核8GB的实例,每台都运行一个服务进程,下文将其统称为服务A。

• 渠道广告过滤服务(RPC远程调用服务提供者):两台2核4GB的实例,每台都运行一个服务进程,下文将其统称为服务B。

• 其他服务。

广告点击服务(服务A)通过Dubbo RPC调用渠道广告过滤服务(服务B)来验证某广告是否被批单给某渠道,如图1所示。

图1 服务A与服务B的调用关系

查看当时服务打印的日记可以发现以下3个问题。

1. 服务A的RPC远程调用大量超时

我们将服务B每个接口的超时时间都配置为3秒。服务B提供的接口的实现都是缓存级别的操作,理论上除了发生网络问题,否则耗时不可能超过这个值(3秒)。

2. 服务B的Jedis读操作超时

服务B的每个节点都配置了200个最小连接数的Jedis连接池,这是根据Netty工作线程数配置的,即就算有200个请求线程并发执行,也能为每个线程分配一个Jedis连接。但服务B的几个节点的日记全是Jedis读操作超时(Read time out)。

3. 服务A文件句柄数达到上限

SocketChannel套接字会占用一个文件句柄,并且有多少个客户端连接就占用多少个文件句柄。我们在服务的启动脚本上为每个进程配置最大文件打开数为102 400,理论上当时的并发请求量并不可能达到这个数值。服务A底层用的是自研的基于Netty实现的HTTP服务框架,没有限制最大连接数。

所以,这3个问题就是排查此次服务雪崩真正原因的突破口。

首先,怀疑Redis服务承载不了这么大的并发请求量。根据业务代码估算,处理广告的一次点击需要执行30次GET请求才能从Redis获取数据,如果每分钟有8万个并发请求数,则需要执行240万次GET请求。而Redis除了服务A和服务B在使用,还有其他两个并发量高的服务在使用,保守估计,Redis每分钟需要承受300万次的读/写请求,即每秒要承受5万次的读/写请求,与理论值10万次(Redis每秒可以处理超过10万次读/写请求)相比已经过半。

由于历史原因,我们使用的还是Redis 2.x版本,采用“一主一从”模式。Jedis配置的连接池是读/写分离的连接池,也就是一个主节点处理写请求,一个从节点处理读请求。由于写请求非常少,一般为每15分钟写一次,因此可先忽略写请求对Redis性能的影响,将每秒接近5万次的读请求交由一个Redis从节点处理。我们将Redis升级到4.x版本,并由原来的主从集群改为分布式集群,采用“两主无从”模式(使用AWS的Redis服务可以被配置为无从节点,从而节约成本)。

在Redis升级后,理论上当两个主节点分槽位后,请求会被平摊到两个节点上,性能应该会好很多。但是,服务重新上线后不到一个小时,每分钟的并发请求数又突增到了6万~7万个,这次的问题是大量的RPC远程调用超时,已经没有了Jedis读超时,相比之前好了一些,至少不用再给Redis添加节点。

虽然Redis升级后没有出现超时的情况,但某个Jedis的GET读操作仍然很耗时。Redis的命令耗时与Jedis的读操作超时不同,Jedis的读操作还受网络传输的影响,Redis响应的数据包越大,Jedis接收数据包就越耗时。

Redis执行一条命令的过程如下。

(1)接收客户端请求。

(2)进入队列等待执行。

(3)执行命令。

(4)响应结果给客户端。

在排查业务代码后发现,有一个业务处理逻辑也很耗时:在拿到缓存的value后将其分割成数组,判断请求参数是否在数组中。

复现代码如下。

这段代码是用来模拟高并发请求的,观察在200个业务线程全部耗尽的情况下,执行一个简单的判断元素是否存在的业务逻辑需要多长时间。把这段代码运行一遍,发现很多业务逻辑的执行耗时超过1500毫秒,程序输出如下。

缓存的value字符串越长,这段代码就越耗时,同时越消耗内存,再加上Jedis从发送GET请求到接收完成Redis响应的数据包的耗时,接口的执行总耗时就会超过3秒。所以,导致服务雪崩的根本原因就是这个隐藏的性能问题。

服务B接口执行超时还会导致Dubbo请求量翻倍。

当服务A向服务B发起RPC远程调用时,虽然Dubbo消费端因超时放弃了请求,但是请求已经发出,即使Dubbo消费端取消,提供端也无法感知到服务A的RPC远程调用因超时放弃了,也就无法中断当前正在执行的线程,所以服务B还是要执行完一次调用的业务逻辑。

Dubbo集群容错机制默认使用Failover,即当调用失败时,重试调用其他服务节点。由于默认最大重试次数为两次,且不算第一次调用,因此在最坏情况下,一共会发起3次RPC远程调用,如图2所示。

图2 超时重试机制

当服务A的RPC远程调用因超时放弃时,Dubbo的集群容错机制会重新选择服务B的一个节点发起RPC远程调用,所以并发8万个请求对服务B而言,在最糟糕的情况下就变成了并发24万个请求,最后导致服务B的每个节点业务线程池的线程一直被占用。此时,RPC远程调用就会收到远程服务抛出的线程池已满的异常。

对造成这次服务雪崩事故原因的分析如下。

(1)Jedis抛出Read time out的原因:由于缓存的value字符串太长,网络传输的数据包太大,因此Jedis执行GET请求的耗时较长。

(2)服务A出现RPC远程调用超时的原因:由于业务代码的缺陷、并发请求量的突增及缓存设计的缺陷,因此Jedis读操作耗时长,服务B接口执行耗时超过3秒,服务A的RPC远程调用超时。

(3)服务A出现服务B拒绝请求异常的原因:服务A的RPC远程调用超时触发Dubbo超时重试,由于原本并发量就已经很高,再加上耗时的接口调用,因此服务B业务线程池的线程全部处于工作状态,使得服务B高负荷运行,而Dubbo超时重试又导致服务B并发请求量翻倍,最后导致服务B处理不过来只能拒绝执行服务A的请求。

(4)服务A崩溃的原因:服务B的不可用导致服务A来不及处理客户端发送的请求,而服务A又没有拒绝客户端的请求,客户端便会一直发送请求,最后服务A请求堆积,导致SocketChannel占用的文件句柄达到上限,服务A就崩溃了。

服务B崩溃导致服务A崩溃,正是这种级联效应导致服务雪崩。

另外,定时任务服务会调用服务B的接口,在每次任务执行时,都会导致服务B不可用。由于该服务是内部服务,我们可以通过修改定时任务发送请求的线程数和频率来降低接口的QPS,但如果有其他第三方的定时任务服务调用这个接口,接口的QPS就不好控制了。

为了避免流量再次突增导致服务雪崩,在优化完业务代码和缓存设计后,我们也为项目引入了熔断器(也称断路器):Sentinel。Sentinel可以为接口配置熔断降级规则和系统负载保护规则,当服务器负载过高或请求失败率过高时,可自动切断请求,以确保服务能够稳定运行。

由于Sentinel支持按来源限流,因此我们也为定时任务发起的请求配置限流规则,限制服务B只能有5个线程可以同时处理定时任务发起的请求。

Sentinel是阿里巴巴集团于2018年开源的微服务熔断器组件,于同年被列入云原生全景图谱,位于编排和管理模块象限中,其商业化产品AHAS(应用高可用服务)也被列入该图谱,如图3所示。

图3 云原生全景图谱

Sentinel承接了阿里巴巴集团近10年的“双十一”促销活动流量的核心场景,截至笔者写作本书时已有15.2k的Star(Sentinel在GitHub上的Star数)。Sentinel以流量为切入点,通过流量控制、熔断降级、系统负载保护等多种服务降级方式保护服务的稳定性,并已提供对多种主流框架的适配,如Spring Cloud、Dubbo。

之所以在学习Sentinel之前跟大家分享这个服务雪崩的故事,是因为笔者想通过这次事故帮助大家更好地理解什么是服务雪崩,了解熔断器组件在分布式项目中的重要性。