春节特别策划 | 高并发下如何发现和排查问题?-高并发系统设计40问-极客时间

如何及时发现问题

这一点我们在课程中已经有过介绍了,在我看来,主要有两个手段:监控和压测。在这期加餐中,我再用几个实际的案例强调一些你容易忽视的点。

首先,你需要格外重视客户端的监控(也就是我在 31讲中提到的监控),因为这一级的监控是最靠近用户的,也最能真实反映用户的使用体验,有时候你发现后端的监控一切正常,但其实在用户这一侧已经存在比较严重的问题了。我分享一下这几天在项目中发生的事情。

我的项目最近在做上云的迁移,在迁移到云上之后,我们会使用某公有云的外网负载均衡服务。在这个负载均衡服务上,我们购买了一定量的外网带宽包,这样就可以让内网应用和外网通信了。但是,当流量超过了这个带宽包中提供的带宽总量,就会产生丢包的现象。而在元旦节日的高峰期时,这个带宽包就达到了瓶颈,但是从服务端监控来看,所有的性能指标都显示正常,但是在客户端这边已经有用户感觉到接口响应时间缓慢了。

从客户端监控来看,在带宽被打满的那段时间里,客户端请求服务接口会有大量 504 的响应码,如果我们可以针对客户端监控做一些及时的报警,就会很容易发现这个问题了。

另一方面,压测也是一种常规的发现系统问题和隐患的手段(在课程中我也介绍了应该如何实现全链路压测系统,以及在实现中需要注意的点)。而在最近的迁移上云项目中,我也着重对云上部署的服务做了一次完善的全链路压测,在压测的过程中确实发现了很多云上服务和组件隐藏的问题,下面我就分享一个真实的案例。

在我现在维护的项目中,会重度依赖 Redis 缓存作为提升数据读取速率的手段,而在我们做全链路压测过程中,当我们的压测流量到达一定的量级,会出现访问某一个或者几个 Redis 组件时,平均响应时间有比较大波动的情况,有比较多的慢请求,影响了请求的响应时间。

发现这个问题之后,我们首先看了一下 Redis 的监控,发现在波动期间,Redis 的 CPU 使用率会有大幅度的上升,接近 100%,同时观察到 Redis 会逐出大量的 Key,所以推断逐出 Key 时会消耗大量的 CPU 时间,从而导致 CPU 负载升高。进一步通过观察监控发现,在逐出大量的 Key 之前,Redis 的连接数会有比较大的上涨。

我们和公有云维护同学讨论后确认了原因:由于我们的 Redis 内存使用率接近 100%,那么当连接数大量上涨的时候,Redis 需要逐出 Key,释放出内存资源,从而保存连接信息,那么为什么 Redis 的连接数会大涨呢? 进一步观察业务错误日志,同时排查 Redis 客户端代码之后我们发现,在连接数上涨之前,业务服务在访问 Redis 的时候会有一些慢请求,这些慢请求会导致业务认为与 Redis 的连接出现问题,会重新建立新的连接,并且异步关闭现有连接,从而导致连接会在短时间之内有大幅度的上升。

而我们通过使用 tcpdump 抓取网络包发现,在这一段时间,Redis 的响应时间确实有比较大幅度的升高。通过进一步排查 Redis 的实现逻辑我们发现,在 Redis3.0 版本中使用的 jemalloc 在释放内存时,会存在偶发的卡顿情况,会导致短时间内,访问 Redis 的所有请求全部阻塞,从而导致响应时间升高,这样我们就找到了这个问题的根本原因。

而在云厂商解决了这个问题之后,我们再次压测发现问题不再复现。你看,在这个案例中,我们正是通过全链路的压力测试发现了问题,并且压测也能够帮助我们验证优化方案是否可行。

排查问题的方法是怎样的

那么,发现了问题之后,有哪些排查问题的方法呢?

其实问题(尤其是性能问题),比较难排查的原因在于:我们通常看到的是问题的外在表象,比如,接口响应时间长了、系统的 SLA 下降了、消息队列堆积了等等,而我们想要从表象推理出根本原因就需要分析能力、归纳总结能力以及一些经验的积累了。这就好比你可以从表情和语气推断出女朋友生气了,但要花费很多的精力再加上之前的一些经验总结,才能够推断出女朋友为什么生气。

当然,监控和日志依然是我们排查问题的主要手段,大部分的问题我们都可以通过监控和日志来找到根本原因。比如我在刚刚维护现在的项目时,发现每天凌晨 2 点的时候,系统的 SLA 会有一个抖动,于是我追查系统的错误日志,发现那段时间访问 Redis 会有少量的慢请求,进一步与 DBA 确认那段时间 Redis 在做 BGSAVE,Redis Server 会有短暂时间的阻塞,这就解释了 Redis 的慢请求以及 SLA 的下降。

而有些问题需要我们做一些归纳总结,针对性地分析问题发生的一些共性特点。比如,是不是只有某几台服务器存在这个问题,或者出现问题的间隔时间是不是固定的等等。

我在之前维护一套注册中心的时候,遇到过这么一个问题: 注册中心总是在每天晚上的时候,出现大量节点被标记为不可用,并且很快又被标记可用的情况,直到过了凌晨 0 点才会恢复。

拿到这个问题之后,我首先考虑的就是,如何找到问题每次发生的共性特点,于是我查看了注册中心服务标记节点的时间,发现只有一台服务器是在标记节点不可用,并且节点被标记之后,其他的服务器又很快地将它们恢复。我们在 24讲,讲注册中心时曾经提到,注册中心是通过心跳机制来检测节点是否可用的,注册中心服务会比较上次心跳的时间,以及服务器本地时间,如果两者相差超过一定阈值,就标记服务节点不可用。

于是,我在确认了心跳时间正确的前提下,判断是服务器本地时间的问题。经过进一步排查,我们发现,标记节点不可用的注册中心服务器的系统时间是错误的,而它的系统时钟对时间隔是一天,而其它服务器是一个小时,这也解释了为什么过了凌晨之后就恢复了(时钟重新对时后系统时间就正确了)。于是我们修改了时钟对时的间隔,问题果然就不再出现了。

除了监控和日志以外,一些常见的工具也是问题排查的重要手段,当我们通过监控找不到思路的时候,我们不妨看一看系统的 CPU、内存、磁盘和网络等等是否存在错误,饱和度如何,也许可以给我们的问题排查提供一些线索。这就需要你在实际工作中不断地积累,熟悉常见工具的使用方法和场景了。

比如,我们想要查看 CPU 的负载情况,我们都知道可以使用 top 命令;而如果你是 Java 应用,你还可以结合 jstack 命令来查看 CPU 使用率比较高的线程正在执行什么操作。但这些并不够,你还可以使用 pidstat、vmstat、mpstat 来查看 CPU 的运行队列、阻塞进程数、上下文切换的数量,这些都会给你的问题排查提供线索。同时,Perf 也是一个常见的工具,可以帮助你排查哪些系统调用或者操作消耗了更多的 CPU 时间,这样你就可以有针对性地做调整和优化了。

再比如,我在面试的时候经常会问面试者如何来排查内存泄漏的问题,大部分的 Java 面试者可以回答使用 jmap 命令 dump 出内存信息,然后使用类似 MAT 的工具来分析。

这种分析方法只对 java 堆有效,如果是堆外内存的泄漏我们要如何排查呢?也许你可以使用 pmap 和 GDB 来查看堆外内存都有哪些数据,这样也可以给我们的排查提供思路。