21

记一次性能调优

 3 years ago
source link: https://blog.huoding.com/2020/09/12/850
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

面对性能调优问题,很多人往往只是单纯的套用既往的经验:先试试一个,不行再试试另一个。面对简单的问题,如此通常能事半功倍;但是当面对复杂问题的时候,单凭经验往往并不能达到立竿见影的效果,此时我们需要更精准的判断性能短板在哪里。

一个 openresty 项目,不了解 openresty 的可以参考我以前的文章,从 top 运行结果看,软中断 si 分配不均,绝大部分压在了 CPU5 上,导致 CPU5 的空闲 id 接近于零,最终的结果是其它 CPU 虽然还有空闲 id,但是却碍于 CPU5 的限制而使不上劲儿:

bEruqm7.png!mobile

top 显示 si 不均衡

既然知道了软中断是系统的性能短板,那么让我们看看软中断都消耗在哪:

shell> watch -d -n 1 'cat /proc/softirqs'
rYN3mq6.png!mobile

watch 显示软中断集中在 NET_RX

通过 watch 命令,我们可以确认 CPU5 的软中断接种在 NET_RX 上,也就是网卡上,除了 CPU5,其它 CPU 的 NET_RX 普遍低了一个数量级,由此可以判断,此网卡工作在单队列模式上,我们不妨通过 ethtool 命令来确认一下:

shell> ethtool -l eth0

Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 0
Combined: 8
Current hardware settings:
RX: 0
TX: 0
Other: 0
Combined: 1

主要留意结果中的 Combined 即可,其中 Channel parameters 里的 Combined 表示硬件支持的最大队列数,而 Current hardware settings 里的 Combined 表示当前值。如果硬件只支持单队列,那么可以通过 RPS 之类的方式来模拟多队列;如果硬件支持多队列,那么激活它就行了。结果显示:本例中的网卡支持 8 个队列,当前开启了 1 个队列,激活网卡的多队列功能后再观察 top 运行结果会发现 si 均衡了,其它 CPU 也能使上劲儿了:

shell> ethtool -L eth0 combined 8
jQVnUfF.png!mobile

top 显示 si 均衡了

至此我们搞定了网卡多队列功能,其实说白了这是一个资源分配不均衡的问题,那么除了网卡多队列以外,还有其它资源分配不均衡的问题么,让我们继续看 top 运行结果:

yeyu2iI.png!mobile

top 显示 nginx 的 time 不均衡

如上所示,会发现 nginx 多进程间的 time 分配并不均衡(此 time 是 cpu time),有的干活多,有的干活少,相关问题在「 Why does one NGINX worker take all the load? 」一文中有相关描述:在原本的nginx 模型中,一个 socket 接收所有的请求,不同的 worker 按照accet_mutext 的设置来争抢请求,不过因为 Linux 的 epoll-and-accept 负载均衡算法采取了类似 LIFO 的行为,结果导致请求在不同进程间的分配变得十分不均衡:

3yuYvib.png!mobile

使用 reuseport 前

为了解决此类问题,nginx 实现了 reuseport 指令,每个进程都有对应自己的 socket:

AJV7b2U.png!mobile

使用 reuseport 后

激活了 reuseport 指令后,我们通过 top 命令观察会发现 time 分配变得均衡了:

http {
    server {
        listen 80 reuseport;
        ...
    }
}
M3qumua.png!mobile

top 显示 nginx 的 time 均衡了

虽然我们没有改动一行代码,但是仅仅通过激活网卡多队列和 nginx reuseport,就提升了性能,但是如果想更进一步提升性能,必须深入代码层面才行,下面让我们看看如何发现代码层面的短板在哪里,是时候请出火焰图了,关于火焰图的概念可以参考我以前的文章,如下是一个 on-CPU ( sample-bt )的火焰图,同时采样用户态和内核态数据:

YniMBj6.png!mobile

火焰图显示 cjson 吃掉了大量 cpu

如图所示,cjson 吃掉了大量 cpu,同时发现宽大的火苗基本都是用户态行为,于是我们去掉采样内核态数据,从而降低噪音,重新绘制用户态 on-CPU 火焰图:

iU3iuqq.png!mobile

火焰图显示 cjson 吃掉了大量 cpu

说明:不了解火焰图用法的话,可以参考 iresty 示例,另外,本例中因为服务器缺少 luajit debug symbol,采样的是 C 语言数据,而不是 Lua 语言数据,结果可能有失精准。

如图所示,确实 cjson 吃掉了大量 CPU。对照代码,发现存在若干次解码 json 数据的操作,于是我们可以判断 CPU 是被 cjson.decode 吃掉的,这也正常,不管是什么语言,高效解码 json 数据都是一个让人头疼的问题,群里问问别人有什么银弹可用,结果有人推荐了 lua-resty-json ,从官网说明看,相对于 cjson,它把解码 json 数据的性能提升了 10%~50%,实际具体大小取决于数据的复杂程度,使用上也很简单:

shell> cd /path/to/lua-resty-json/
shell> make
shell> cp json_decoder.lua /usr/local/openresty/lualib/
shell> cp libljson.so /usr/local/openresty/lualib/

剩下的具体用法参考 测试用例 就可以了,需要说明的是 lua-resty-json 只实现了解码。

不过我并没有采用把 cjson 替换为 lua-resty-json 的方法来提升性能,这是因为通过数据分析,我发现在本例中,存在明显的热数据,如果把这些热数据直接缓存在进程中,那么对热数据而言,就完全不需要解码 json 数据了,可以利用 lua-resty-mlcache 来实现:

A7z6JfJ.png!mobile

mlcache 的多级缓存结构

至此,本次性能调优告一段落,实际上这并不是一次严谨的性能调优,我只是利用一些项目的间歇期偶尔搞一下,不过最终我们把服务器数量降低了一半以上。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK