面对性能调优问题,很多人往往只是单纯的套用既往的经验:先试试一个,不行再试试另一个。面对简单的问题,如此通常能事半功倍;但是当面对复杂问题的时候,单凭经验往往并不能达到立竿见影的效果,此时我们需要更精准的判断性能短板在哪里。
一个 openresty 项目,不了解 openresty 的可以参考我以前的文章,从 top 运行结果看,软中断 si 分配不均,绝大部分压在了 CPU5 上,导致 CPU5 的空闲 id 接近于零,最终的结果是其它 CPU 虽然还有空闲 id,但是却碍于 CPU5 的限制而使不上劲儿:
top 显示 si 不均衡
既然知道了软中断是系统的性能短板,那么让我们看看软中断都消耗在哪:
代码语言:javascript复制shell> watch -d -n 1 'cat /proc/softirqs'
watch 显示软中断集中在 NET_RX
通过 watch 命令,我们可以确认 CPU5 的软中断接种在 NET_RX 上,也就是网卡上,除了 CPU5,其它 CPU 的 NET_RX 普遍低了一个数量级,由此可以判断,此网卡工作在单队列模式上,我们不妨通过 ethtool 命令来确认一下:
代码语言:javascript复制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 也能使上劲儿了:
代码语言:javascript复制shell> ethtool -L eth0 combined 8
top 显示 si 均衡了
至此我们搞定了网卡多队列功能,其实说白了这是一个资源分配不均衡的问题,那么除了网卡多队列以外,还有其它资源分配不均衡的问题么,让我们继续看 top 运行结果:
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 的行为,结果导致请求在不同进程间的分配变得十分不均衡:
使用 reuseport 前
为了解决此类问题,nginx 实现了 reuseport 指令,每个进程都有对应自己的 socket:
使用 reuseport 后
激活了 reuseport 指令后,我们通过 top 命令观察会发现 time 分配变得均衡了:
代码语言:javascript复制http {
server {
listen 80 reuseport;
...
}
}
top 显示 nginx 的 time 均衡了
虽然我们没有改动一行代码,但是仅仅通过激活网卡多队列和 nginx reuseport,就提升了性能,但是如果想更进一步提升性能,必须深入代码层面才行,下面让我们看看如何发现代码层面的短板在哪里,是时候请出火焰图了,关于火焰图的概念可以参考我以前的文章,如下是一个 on-CPU (sample-bt)的火焰图,同时采样用户态和内核态数据:
火焰图显示 cjson 吃掉了大量 cpu
如图所示,cjson 吃掉了大量 cpu,同时发现宽大的火苗基本都是用户态行为,于是我们去掉采样内核态数据,从而降低噪音,重新绘制用户态 on-CPU 火焰图:
火焰图显示 cjson 吃掉了大量 cpu
说明:不了解火焰图用法的话,可以参考 iresty 示例,另外,本例中因为服务器缺少 luajit debug symbol,采样的是 C 语言数据,而不是 Lua 语言数据,结果可能有失精准。
如图所示,确实 cjson 吃掉了大量 CPU。对照代码,发现存在若干次解码 json 数据的操作,于是我们可以判断 CPU 是被 cjson.decode 吃掉的,这也正常,不管是什么语言,高效解码 json 数据都是一个让人头疼的问题,群里问问别人有什么银弹可用,结果有人推荐了 lua-resty-json,从官网说明看,相对于 cjson,它把解码 json 数据的性能提升了 10%~50%,实际具体大小取决于数据的复杂程度,使用上也很简单:
代码语言:javascript复制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 来实现:
mlcache 的多级缓存结构
至此,本次性能调优告一段落,实际上这并不是一次严谨的性能调优,我只是利用一些项目的间歇期偶尔搞一下,不过最终我们把服务器数量降低了一半以上。