learn-tech/专栏/深入理解Sentinel(完)/01开篇词:一次服务雪崩问题排查经历.md
2024-10-16 09:22:22 +08:00

182 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
01 开篇词:一次服务雪崩问题排查经历
笔者想跟大家分享笔者经历的一次服务雪崩事故,分析导致此次服务雪崩事故的原因。或许大多数读者都有过这样的经历,这是项目给我们上的一次非常宝贵的实战课程。
什么是服务雪崩?
雪崩一词指的是山地积雪由于底部溶解等原因而突然大块塌落的现象,具有很强的破坏力,在微服务项目中指由于突发流量导致某个服务不可用,从而导致上游服务不可用,并产生级联效应,最终导致整个系统不可用,使用雪崩这个词来形容这一现象最合适不过。
服务雪崩,听到这个词就能想到问题的严重性。是的,当时公司整条业务线的服务都挂了,从该业务线延伸出来的下游业务线也被波及。笔者当时是连续三天两夜的忙着处理问题,加起来睡眠时间不足 5 小时,正是如此,印象非常深刻。
其实这一天的到来我是有预感的,但我以为会是数据量上升导致,实际却是并发量先上升,而严重程度超出我的预料。问题出现那天,我们还在进行每周的技术分享会,结果一运营小姐姐推开会议室的大门传来噩耗,画面瞬间转变,技术分享会变成了问题排查讨论会。
当时看了服务的负载均衡统计,发现并发请求量增长了一倍,从每分钟 3 到 4 万的请求数,增长到 8.6 万。在事发之前,服务一直稳定运行,很显然,这次事故与并发量翻倍有直接的关系。
这是由笔者负责技术选型与架构设计的一个分布式广告系统,也是笔者入门分布式微服务实战的第一个项目,从设计到实现,期间遇到过很多的难题,被项目推着走,熬了很多个夜,但也颇有收获。
关于服务的部署:
处理广告点击的服务2 台 2 核 8g 的实例,每台都运行一个服务进程,下文统称服务 A
渠道广告过滤服务RPC 远程调用服务提供者2 台 2 核 4g 实例,每台都运行一个服务进程,下文统称服务 B
还有其它的服务提供者,但不是影响本次服务雪崩的凶手,因此这里就不列举了。
从当时查看服务打印的日记可以看出三个问题。
1. 服务 ARPC 远程调用大量超时
我们配置服务 B 每个接口的超时时间都是 3 秒。服务 B 提供的接口的实现都是缓存级别的操作3 秒的超时时间,理论上除了网络问题,调用不可能会超过这个值。
2. 服务 BJedis 读操作超时,服务 B 几个节点的日记全是 Jedis 读超时Read time out
服务 B 每个节点配置了 200 个最小连接数的 Jedis 连接池,这是根据 Netty 工作线程数配置的,即读写操作就算 200 个线程并发执行,也能为每个线程分配一个 Jedis 连接。
3. 服务 A文件句柄数达到上限
SocketChannel 套接字会占用一个文件句柄,有多少个客户端连接就占用多少个文件句柄。我们在服务的启动脚本上为每个进程配置 102400 的最大文件打开数,理论上当时的并发量并不可能会达到这个数值。服务 A 底层用的是自研的基于 Netty 实现的 HTTP 服务框架,没有限制最大连接数。
所以,这三个问题就是排查此次服务雪崩真正原因的突破口。
首先是怀疑 Redis 服务扛不住这么大的并发请求。根据业务代码估算,处理广告的一次点击需要执行 30 次 get 操作从 Redis 获取数据,那么每分钟 8w 并发,就需要执行 240w 次 get 请求,而 Redis 除了本文提到的服务 A 和服务 B 用到外还有其它两个并发量高的服务在用保守估计Redis 每分钟需要承受 300w 的读写请求。转为每秒就是 5w 的读写请求,与理论值 Redis 每秒可以处理超过 10 万次读写操作已经过半。
由于历史原因Redis 使用的还是 2.x 的版本用的一主一从Jedis 配置连接池是读写分离的连接池,也就是写请求打到主节点,读请求打到从节点。由于写请求非常的少,大多都是定时 15 分钟写一次,因此可先忽略写请求对 Redis 性能的影响,那么就是每秒接近 5w 读请求只有一个 Redis 从节点处理。所以我们将 Redis 升级到 4.x 版本,并由主从集群改为分布式集群,两主无从(使用 AWS 的 Redis 服务可以配置无从节点,还是节约成本的问题)。
Redis 升级后,理论上,两个主节点分槽位后请求会平摊到两个节点上,性能应该会好很多。但好景不长,服务重新上线一个小时不到,并发又突增到了六七万每分钟,这次是大量的 RPC 远程调用超时,已经没有 Jedis 的读超时Read time out相比之前好了点至少不用再给 Redis 加节点,排除掉 Redis 性能瓶颈。
虽然升级后没有“Read time out!”,但某个 Jedis 的 Get 读操作还是很耗时这才是罪魁祸首。Redis 的命令耗时与 Jedis 的读操作 Read time out 不同Jedis 的读操作还受网络传输的影响Redis 响应的数据包越大Jedis 接收数据包就越耗时。Redis 执行一条命令的过程分为:
接收客户端请求
进入队列等待执行
执行命令
响应结果给客户端
Jedis 的 get 耗时长导致服务 B 接口执行耗时超过设置的 3s。服务 A 向服务 B 发起 RPC 调用,虽然 dubbo 消费端超时放弃请求,但是请求已经发出,就算消费端取消,提供者无法感知服务 A 超时放弃了,没有中断当前正在执行的线程,所以服务 B 还是要执行完一次调用的业务逻辑,这与说出去的话收不回来一样的道理。
Dubbo 集群容错机制默认使用 Failover即当调用出现失败时重试其它服务节点。默认会重试两次不算第一个调用所以最坏情况下一共会发起三次 RPC 调用,如下图所示。
当服务 A 超时放弃时Dubbo 的集群容错处理会重新选择服务 B 的一个节点发起调用,所以并发 8w 对于服务 B 而言,最糟糕的情况下就变成了并发 24w。最后导致服务 B 的每个节点业务线程池的线程一直被占用RPC 远程调用又多出了一个异常,就是远程服务线程池已满,服务 B 直接响应失败。
问题最终还是要回到 Jedis 的 Read time out 上,就是 key 对应的 value 太大导致传输耗时,业务代码拿到 value 后将 value 分割成数组,判断请求参数是否在数组中也非常耗时,就会导致服务 B 处理接口调用耗时超过 3s从而导致服务 B 不可用,服务 B 不可用直接拖垮服务 A。
模拟服务 B 接口的业务代码如下:
public class Match {
static class Task implements Runnable {
private String value;
public Task(String value) {
this.value = value;
}
@Override
public void run() {
for (; ; ) {
// 模拟 jedis get 耗时
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// =====> 实际业务代码
long start = System.currentTimeMillis();
List<String> ids = Arrays.stream(value.split(",")).collect(Collectors.toList());
// 判断字符串是否存在数组中
boolean exist = ids.contains("4029000");
// ====> 输出业务代码耗时.
System.out.println("exist:" + exist + ",time:" + (System.currentTimeMillis() - start));
}
}
};
public static void main(String[] args) {
// 模拟业务场景,从缓存中获取到的字符串
StringBuilder value = new StringBuilder();
for (int i = 4000000; i <= 4029000; i++) {
value.append(String.valueOf(i)).append(",");
}
String strValue = value.toString();
System.out.println(strValue.length());
// 开启 200 个线程执行 Task 的 run 方法
for (int i = 0; i < 200; i++) {
new Thread(new Task(strValue)).start();
}
}
}
这段代码很简单就是模拟高并发观察在 200 个业务线程全部耗尽的情况下一个简单的判断元素是否存在的业务逻辑执行需要多长时间把这段代码跑一遍发现很多执行耗时超过 1500ms如下图所示
缓存的 value 字符串越长这段代码就越耗时同时也越消耗内存如果再加上 Jedis 从发送 get 请求到接收完成 Redis 响应的数据包的耗时接口的执行总耗时就会超过 3000ms所以导致服务雪崩的根本原因就是这个隐藏的性能问题
代码层面的优化就是将 id 拼接成字符串的存储方式改为使用 hash 结构存储直接 hget 方式判断一个元素是否存在不需要将这么大的数据读取到本地即避免了网络传输消耗也优化了接口的执行速度当然最好使用 bitmap 存储但由于该缓存还有其它用途因此才选用 hash
造成这次服务雪崩事故的原因分析总结
Jedis 抛出 Read time out 的原因由于缓存的 value 字符串太长网络传输数据包大导致 Jedis 执行 get 命令耗时长
服务 A 出现 RPC 调用超时的原因业务代码的缺陷并发量的突增以及缓存设计缺陷导致 Jedis 读操作耗时长导致服务 B 接口执行耗时超过 3 从而导致服务 A 远程 RPC 调用超时
服务 A 出现服务 B 拒绝请求异常的原因服务 A 调用超时触发 dubbo 超时重试原本并发量就已经很高加上耗时的接口调用服务 B 业务线程池的线程全部处于工作状态服务 B 已经高负荷运行 Dubbo 的超时重试又导致服务 B 并发量翻倍简直雪上加霜服务 B 处理不过来只能拒绝执行服务 A 的请求
服务 A 奔溃的原因服务 B 的不可用导致服务 A 处理不过来客户端发来的请求而服务 A 又没有拒绝客户端的请求客户端请求源源不断最后服务 A 请求堆积导致 SocketChannel 占用的文件句柄达到上限服务 A 就奔溃了
服务 B 的奔溃导致服务 A 奔溃正是这种级联效应导致服务雪崩
另外由定时任务服务调用服务 B 的接口在每次任务执行时都会导致服务 B 变得不可用由于是内部服务我们可以通过修改定时任务发送请求的线程数和频率来降低接口的 QPS一开始我们也是这么做的但如果有其它第三方的定时任务服务调用这个接口就不好控制了
为避免流量再次突增导致服务雪崩在优化完业务代码和缓存设计后我们也为项目引入了断路器Sentinel为接口配置熔断降级规则系统负载保护规则当服务器负载过高或者请求失败率过高时自动熔断上游服务的请求以确保服务能够稳定运行由于 Sentinel 支持按来源限流我们也为定时任务发起的请求配置限流规则限制服务 B 同时只能有五个线程处理定时任务发起的请求
Sentinel 是阿里于 2018 年开源的微服务断路器组件意义为流量防卫兵承接了阿里巴巴近 10 年的双十一大促流量的核心场景目前已有 13.3k StarSentinel 以流量为切入点实现流量控制熔断降级系统负载保护等多种服务降级方式保护服务的稳定性并已提供对多种主流框架的适配例如 Spring CloudDubbo
之所以在学习 Sentinel 之前跟大家分享这个服务雪崩故事是想通过这次事故帮助读者更好的理解什么是服务雪崩这次服务雪崩事故让笔者明白了服务降级在分布式系统中的重要性可以这么说微服务项目不能缺少服务降级每个服务都需要有自我保护的能力
专栏大纲
了解 Sentinel 首先要攻克其基于滑动窗口实现的指标数据统计以及基于责任链模式实现的服务降级过滤器链在掌握这两点之后整个 Sentinel 的框架源码将不难理解Sentinel 实现的冷启动限流效果算法与匀速限流效果的算法算是限流模块中最难理解的一部份在介绍这部分内容时我们会结合 Guava 的限流算法分析降低理解难度
本专栏内容安排如下
第一部分01-03服务雪崩与服务降级介绍从一个服务雪崩故事开始了解服务雪崩进而理解为什么需要服务降级服务降级的实现方式有哪些以及为什么选择 Sentinel
第二部分04-07理解 Sentinel 的核心实现原理我们将了解指标数据的统计与框架的整个骨架深入理解 Sentinel 中的重要概念和核心类介绍 Java SPI Sentinel 中的使用
第三部分08-15分析 Sentinel 的核心功能实现原理内容包括限流的实现与流量效果控制熔断降级与系统自适应限流黑白名单与热点参数限流最后通过自定义 ProcessorSlot 实现开关降级
第四部分16-20Sentinel 对主流框架的适配和扩展功能我们将了解动态数据源以及分析 Sentinel 集群限流的实现原理最后使用 JMH 压测 Sentinel 对应用性能的影响
关于源码分析笔者选择的是 Sentinel 1.7.1 版本
为什么写这个专栏
我第一次看 Sentinel 源码也感觉无从下手特别是关于节点树这些概念的理解也是硬着头片去啃源码结合官方文档去揣摩代码背后的设计思想我想要研究 Sentinel 的源码一开始只是好奇 Sentinel 是怎么统计每个接口的 QPS 并且也模仿 Sentinel 实现了一个基于滑动窗口的 QPS 统计工具但后来又不满足于这搁浅的认识于是深入探索 Sentinel 整个框架的核心实现原理在对 Sentinel 有一定的了解后也基于 Sentinel 做过一些扩展例如笔者在最近的一个新项目中在网关层实现请求的熔断项目从单体迁移的一些原因)、抛弃 AOP+Redis 实现开关降级的方式基于 Sentinel 实现开关降级提高了开关降级的灵活度
基于 Sentinel 自学难度高分析 Sentinel 原理细节的资料零零散散且不全官方文档介绍得不够深入笔者下定决心完成此专栏希望能够帮助到想要深入学习了解 Sentinel 的读者由于笔者的表达能力有限如果有表达不够清晰或者表达错误的地方还恳请大家帮忙指出
作者简介
吴就业洋葱集团后端架构师Java 开发工程师主要负责新项目的技术选型与架构设计旧项目的重构以及订单服务支付中心的需求开发与维护在微服务领域有丰富的实战经验广告系统重构的分布式架构设计支付中心的技术选型与架构设计基于 xxl-job 二次开发的分布式定时任务调度平台自研微服务监控系统喜欢研究优秀的开源框架源码擅长 Spring CloudDubboNettyJava 虚拟机字节码等技术
适宜人群
想深入了解 Sentinel 的使用者
正在实践微服务的开发者/组织
Spring CloudDubbo 微服务初学者
想要了解如何统计接口 QPS 的开发者
想要了解匀速限流冷启动限流算法的开发者