你好,我是唐扬,新年快乐!

过年嘛,都要吃好玩好,给自己一年的辛苦付出“加餐”,那咱们的课程也不例外,在新的一年里,我为你策划了两期加餐,今天先来聊聊在高并发下,我们如何发现和排查问题。

为什么要讲这个问题呢?是因为我在课程结束之后,发现有同学反馈说:

虽然课程里几乎涵盖了高并发系统设计的全部方面(比如数据库、缓存和队列的使用、分布式系统主要组件的原理,以及系统运维方面需要关注的重点),但自己按照课程中提供的方式正确使用了组件,在实际工作中仍然会发现系统中各种各样的问题,比如服务性能衰减、依赖资源的抖动甚至是服务整体故障。

尤其在高并发环境下,由于并发请求更多,对于资源和服务的压力更大,所以原本隐藏在冰山下的问题又都会在某一时间突然浮出水面。

这其实就像墨菲定律说的那样: 如果事情有变坏的可能,不管这种可能性有多小,它总会发生。这不是一个数据概率问题,也不是一个心理学效应,而是一种必然的法则。

在高并发场景下,一些细微的问题可能会迅速恶化,并且对系统中多个模块的SLA带来巨大的影响。比如,业务仅仅缓存的平均响应时间增加1ms或者缓存命中率下降1个百分点,都会带来灾难性的影响。这不仅增加了问题排查的难度,也对问题排查的及时性提出了更高的要求。

那么作为团队核心开发成员的你,在系统存在隐患的时候,如何快速发现问题?在出现问题的时候又要如何排查呢?接下来,我就结合课程中讲到的一些知识,通过一些实际案例,再带你深入了解一下。

如何及时发现问题

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

首先,你需要格外重视客户端的监控(也就是我在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来查看堆外内存都有哪些数据,这样也可以给我们的排查提供思路。

课程小结

以上就是本节课的全部内容了。本节课我带你了解了发现和排查问题的方式和手段,这里你需要了解的几个重点是:

问题的排查过程虽然痛苦,但是你每一次的排查经历都是在为你的下一次排查积累经验,同时也能让你更加熟悉工具的使用,慢慢地你就会发现,问题的排查关键在于你是否熟练,“无他,唯手熟尔”。

一课一思

在你开发和维护项目的过程中,你都遇到过哪些诡异的问题呢?你又是通过什么样的方法来发现和排查的呢?欢迎在留言区和我一起讨论,或者将你的实战经验分享给更多的人。

最后,感谢你的阅读,我们下期见。

评论