Liangshan

Inner peace.

记一次性能调优

继上次做性能优化之后,再次针对我们刚刚全新升级的 app 做了一轮性能调优,而这个过程又引起了我一些思考,这里做一个记录。

根据多年的经验,性能问题一般都是由后端服务引起,API 服务器忙都是受后端服务的拖累所表现出来的现象。所以一入手就是监控各个后端服务的运行情况,初步定位瓶颈在数据库上。第一步就是趁着业务上将来要做全文检索,使用搜索引擎代替数据库作为列表的数据源。这个优化上线之后,高峰时数据库压力有了明显改善。然而这个改善并没有表现在前端页面的速度监控上,几乎所有页面都非常统一的在某些时间点会有长至几秒钟的响应时间,发生的时间间隔没有明显规律,大概几分钟一次,每次持续十几秒钟,并且这些异常并不跟我们的业务高峰有重合。

当时的猜测有 2 个,一个是仍然有什么慢查询影响了整个数据库的性能,从而影响了整站的性能。另外一个是某台机器有问题,所以所有落在那台机器上的请求都会变慢。要验证第二点是非常简单的,我单独拿了一台机器去独立运行最简单的一个业务,结果显示并没有什么变化。

再次确认 API 机器没有资源瓶颈的前提下,决定在代码里埋点统计执行时间,发现了一些执行很慢的代码块,慢的时间和那些异常时间也吻合,只是执行时长对不上。刚才提到异常时刻的响应时间以秒计算,这些慢查询最慢也就是几百毫秒,不在一个数量级。第一感觉就是也许这些慢查询累加起来就会将危害放大。总之优化这些地方最起码不是错误的,于是着手把这些找到的点都修正掉了。

然而那些监控图表上的毛刺像一根根的针一样依然存在,并且由于做了几轮优化,高峰和低谷的对比更加明显,这些图看起来就更诡异。

针对每个页面都有同样表现这个特点,我想到了会不会是代码的中间层有问题。框架总会提供一些方案来给开发人员在执行业务代码之前统一做一些事情,比如权限检查、登录检查等等。如果是这里有问题,可以解释为什么所有页面都受影响。同样做埋点和统计,发现了某个中间层代码存在的慢查询,这次异常的时间和时长都对应的上。于是很高兴的做了优化,然后上线。

结果仍然没有什么改变,最不可思议的是异常代码块似乎会「转移」,现在转移到一个最简单的主键查询上。

当时刚好是午饭时间,带着郁闷的心情去吃了寿司,边吃饭边思考,这些所有看到的现象是不是都仅仅是现象,而不是根本原因?为什么出现在中间层?为什么会转移?突然我意识到或许是建立连接很慢,哪里第一次建立连接哪里就慢,之前优化中增加了缓存所以慢的代码发生了「转移」。

有些假设你第一次想到的时候就知道是对的,就像你上学时候做出的物理题结果一看就知道对错,正确的东西带有一种独特的气息。

下午回去就做了一个实验,在应用程序刚开始运行的时候就执行一段数据库查询。结果印证了我的假设,异常代码块「转移」到了新加的实验代码上。那么在建立连接的时候到底发生了什么呢?于是在生产环境抓包,在本地来做分析。通过比对正常时间和异常时间 TCP 包的内容,发现每次异常期间,就会有大量的 use db 语句卡住。说实话刚刚确认这一点的时候我觉得非常惊讶,因为从来没有想过这个会有问题。为了避免是某种语言或者特定框架的问题,我单独写了一个 shell 脚本做测试,结果显示和线上代码反应的结果一致。

我们使用的是阿里云服务,联系了他们的技术支持,虽然仍然不知道什么导致了 use db 的问题,但最终通过切换实例的可用区解决了现象,应用程序终于可以平稳运行。

在这个曲折的过程中,我想说的是,解决不了问题的时候,吃顿好的还是非常有用的。

好吧,我认为在特定的时刻允许工程师直接到线上调试是非常重要的,设想如果每次想验证想法都要等上线,整个调试过程的连续性和有效性都无法得到保证。但是我依然坚持线上调试需要至少两个人以结对编程的方式进行。

后来,我把这个事情和之前的同事分享。他提到一个词,也是这篇文章的英文标题:抽象泄漏。有兴趣可以去看他 翻译的原文,这里只做摘录,其中的观点非常有趣。

这就是我所说的“抽象泄漏”。TCP协议试图提供一个完整的抽象,将底层不可靠的数据传输包装起来,但是,底层的传输有时也会发生问题,即便是TCP协议也无法解决,这时你会发现,它也不是万能的。TCP协议就是“抽象泄漏定律”的示例之一,其实,几乎所有的抽象都是泄漏的。这种泄漏有时很小,有时会很严重。

由于抽象定律的存在,每当有人说自己发现了一款新的代码生成工具,能够大大提高我们的编程效率时,你会听很多人说“先学习手工编写,再去用工具生成”。代码生成工具是一种抽象,同样也会泄漏,唯一的解决方法是学习它的实现原理,即它抽象了什么。所以说抽象只是用于提高我们的工作效率的,而不会节省我们的学习时间。

这就形成了一个悖论:当我们拥有越来越高级的开发工具,越来越好的“抽象”,要成为一个高水平的程序员反而越来越困难了。

具体到我们这个案例里面,至少有 2 处抽象泄漏。首先,数据库连接层将如何连接数据库抽象出来,将登录数据库和切换数据库的行为隐藏起来,对于程序员来说,只需要初始化一个数据库连接对象就会做掉这 2 件事情,所以一开始我会误以为是数据库连接问题,而其实是切换数据库时才会有问题。其次,更要命的抽象泄漏就是云服务,云服务带来很多便利,然而一旦服务出了问题,就非常难以定位和解决,且不说定位就花了一周时间,单单从确认是阿里云的问题开始到最终解决就花了 4 天时间,而这个就是「抽象泄漏」一文中很重要的观点:

十年前,我们会想象未来能够出现各种新式的编程范型,简化我们的工作。的确,这些年我们创造的各类抽象使得开发复杂的大型软件变得比十五年前要简单得多,就像GUI和网络编程。现代的面向对象编程语言让我们的工作变得高效快速。但突然有一天,这种抽象泄漏出一个问题,解决它需要耗费两星期。