如果对Ngx_Lua开发有兴趣的朋友们,可以购买书籍 << Nginx实战:基于Lua语言的配置、开发与架构详解 >>https://item.jd.com/12487157.html

16章 性能分析和优化
Nginx如果只是做一个简单的反向代理,它的优化方式简单且有效,比如增加worker进程,增加长连接,减少硬盘存储临时文件,优化内核配置等。但随着Nginx被当作开发工具后,代码复杂度也在逐步加深,无论Nginx是作为反向代理还是Web应用,任何语言在不合理的使用中都会出现性能问题。本章中我们会介绍多个开源工具,利用它们帮助开发者在Nginx中查找性能问题。
注意:本章内容包含Ngx_Lua有关的分析但不仅限于此。这些指令可以在Nginx和OpenResty两个平台测试。部分工具对LuaJIT 2.0和LuaJIT 2.1有区别,会在讲解时说明。
16.1 性能分析场景搭建
性能分析一般会在测试环境中进行分析(当然线上服务有性能问题并且测试环境不易复现的情况下,那就另当别论了),这样可以确保上线前做好性能优化,而且性能分析需要开启debug模式,这对线上的服务来说不是很友好,所以先搭建一个debug环境,性能分析的安装环境依赖很多包,请大家按照步骤一步一步地来。
16.1.1 SystemTap安装
SystemTap是用来分析Linux系统性能问题的工具,通过它提供的接口就可以开发出用来调试和分析性能的代码,本章中的分析工具都是依赖此SystemTap的。
1.首先确认你的系统内核版本,推荐Linux内核版本大于3.5+的,如果低于此版本的官方也会提供utrace补丁在其内核中,本测试安装在CentOS 6.4上。

uname -r

2.6.32
2.安装对应版本的内核包:
先确认是否安装了kernel-devel。

rpm -qa |grep kernel-devel

kernel-devel-2.6.32-696.30.1.el6.x86_64
如果没有安装就yum,安装时显示了update到新的版本,下面对应的其他rpm包也需要找到对应的版本。

yum install kernel-devel

3.提供debug支持

wget -S http://debuginfo.centos.org/6/x86_64/kernel-debuginfo-common-x86_64-2.6.32-696.30.1.el6.x86_64.rpm

wget -S http://debuginfo.centos.org/6/x86_64/kernel-debuginfo-2.6.32-696.30.1.el6.x86_64.rpm

rpm -ivh kernel-debuginfo-2.6.32-696.30.1.el6.x86_64.rpm kernel-debuginfo-common-x86_64-2.6.32-696.30.1.el6.x86_64.rpm

4.安装systemtap

yum install systemtap

验证安装是否成功:

stap -ve 'probe begin { log("Test Nginx Systemtap!")exit() }'

Pass 1: parsed user script and 117 library script(s) using 213788virt/41172res/3232shr/38628data kb, in 330usr/20sys/348real ms.
Pass 2: analyzed script: 1 probe(s), 2 function(s), 0 embed(s), 0 global(s) using 214580virt/42280res/3536shr/39420data kb, in 10usr/0sys/8real ms.
Pass 3: translated to C into "/tmp/stapldNGqo/stap_193cfbe6fbce06a86a9581c649f20084_960_src.c" using 214580virt/42668res/3892shr/39420data kb, in 0usr/0sys/0real ms.
Pass 4: compiled C into "stap_193cfbe6fbce06a86a9581c649f20084_960.ko" in 1040usr/210sys/1281real ms.
Pass 5: starting run.
Test Nginx Systemtap!
Pass 5: run completed in 0usr/10sys/345real ms.
16.1.2 LuaJIT的debug模式
需要在安装LuaJIT时启动debug模式,即make CCDEBUG=-g。下面是安装步骤顺序:

wget http://luajit.org/download/LuaJIT-2.1.0-beta3.tar.gz

tar -zxvf LuaJIT-2.1.0-beta3.tar.gz

cd LuaJIT-2.1.0-beta3

make CCDEBUG=-g

make install

export LUAJIT_LIB=/usr/local/lib

export LUAJIT_INC=/usr/local/include/luajit-2.1

16.1.3 开启pcre的debug模式
有些时候需要对Nginx正则表达式的使用情况进行分析,确保服务在正则方面的性能。
如果Nginx是在静态编译中使用了--with-pcre=的方式,请重新编译开启debug模式:

cd nginx-1.12.2

./configure --with-pcre=/path/to/my/pcre-8.39 \

--with-pcre-jit  --with-pcre-opt=-g \

如果是动态链接的,可直接安装rpm即可

wget http://debuginfo.centos.org/6/x86_64/pcre-debuginfo-7.8-7.el6.x86_64.rpm

rpm -ivh pcre-debuginfo-7.8-7.el6.x86_64.rpm

确保这3个包都在:

rpm -qa |grep pcre

pcre-debuginfo-7.8-7.el6.x86_64
pcre-7.8-7.el6.x86_64
pcre-devel-7.8-7.el6.x86_64
16.1.4 分析工具包下载
安装基础环境后,就可以使用下面的工具来进行性能分析了。
openresty-systemtap-toolkit和stapxx,其中stapxx是对openresty-systemtap-toolkit的一个简单的扩展,我们会在分析中混用两个工具,它们都有各自使用的场景,其中stapxx更侧重于Ngx_Lua有关的性能分析。
下载:

git clone https://github.com/openresty/openresty-systemtap-toolkit

git clone https://github.com/openresty/stapxx.git

然后下载FlameGraph,它是将上面两个工具采集的性能数据生成火焰图的工具。

git clone https://github.com/brendangregg/FlameGraph.git

最后编译Nginx,它需要加上--with-debug即可。
注意:Linux系统需要Perl至少5.6.1以上的版本,默认情况下已经安装。
16.1.5 找出debug不支持的包
通过下面命令可以找出不支持debug模式的lib,如果lib不支持,你又需要测试功能和这个lib有关,可能会出现报错,所以在性能分析中按需安装。
check-debug-info -p pid, pid可以是Nginx的worker进程pid,它也可以用来检测非Nginx的进程,此命令就是刚才下载的openresty-systemtap-toolkit目录中的命令。

cd openresty-systemtap-toolkit

./check-debug-info -p 30396

File /lib64/ld-2.12.so has no debug info embedded.
File /lib64/libc-2.12.so has no debug info embedded.
File /lib64/libcrypt-2.12.so has no debug info embedded.
File /lib64/libdl-2.12.so has no debug info embedded.
File /lib64/libfreebl3.so has no debug info embedded.
File /lib64/libm-2.12.so has no debug info embedded.
File /lib64/libnss_files-2.12.so has no debug info embedded.
File /lib64/libpthread-2.12.so has no debug info embedded.
File /lib64/libresolv-2.12.so has no debug info embedded.
16.2 流量复制
性能分析的环境已经搭建完成,现在需要有流量进入才可以验证这些代码的使用情况。我们可以用下面2种工具模拟出真实的请求和并发。它们在13.5.2和13.5.3节有介绍,就不重复说明了。
如果是新的服务,URL也是新的,可以理解为线上还没有流量可以复制,因为没有上线此功能,就可以利用压测工具,如AB、Webbench等
16.3 各项指标分析和优化建议
性能测试环境全部就位,可以进行性能分析了。
注意:所有的分析操作都是在Nginx的worker进程pid上执行的。
16.3.1 连接池使用状态分析
我们在前面介绍过lua-resty-redis和lua-resty-mysql,它们都有使用过连接池的配置。如下:
local ok, err = db:set_keepalive(10000, 100)
if not ok then
ngx.say("failed to set keepalive: ", err)
return
end
连接池的作用可以极大地减少timewait和建联的开销,但如何才知道连接池配置是否满足需求呢?可以使用如下方式:

cd openresty-systemtap-toolkit

./ngx-lua-conn-pools -p 30396 --luajit20

Tracing 30396 (/usr/local/nginx/sbin/nginx) for LuaJIT 2.0...

pool "172.16.1.51:6301"
out-of-pool reused connections: 1
in-pool connections: 2
reused times (max/avg/min): 29/18/7
pool capacity: 100

pool "172.16.13.171:6398"
out-of-pool reused connections: 0
in-pool connections: 1
reused times (max/avg/min): 0/0/0
pool capacity: 100

pool "172.16.1.55:8186"
out-of-pool reused connections: 0
in-pool connections: 1
reused times (max/avg/min): 377/377/377
pool capacity: 30

pool "172.16.1.7:5689"
out-of-pool reused connections: 0
in-pool connections: 1
reused times (max/avg/min): 1/1/1
pool capacity: 100

For total 4 connection pool(s) found.
122 microseconds elapsed in the probe handler.
指令:ngx-lua-conn-pools
语法:ngx-lua-conn-pools -p $pid (--luajit20 or--lua51)
它的作用是获取指定worker进程中的Ngx_Lua的连接池状态。如果是Nginx编译时采取的是Lua 5.1就用--lua51。如果是LuaJIT 2.0就用--luajit20,目前测试在Luajit2.1也可以用。
分析结果说明如表16-1所示。
结果 说明
pool "172.16.1.51:6301" 连接池所连接的服务,比如Redis、MySQL
out-of-pool reused connections: 1 外部连接池重数量
in-pool connections: 2 池内连接数量
reused times (max/avg/min): 29/18/7 重用次数、最大值、平均值、最小值
pool capacity: 100 连接池容量,配置文件内的配置连接池的数量
表16-1 连接池信息说明
通过此命令我们可以获取连接池的使用数量和配置数量,在流量测试中观察连接池使用情况,来确认是否需要调整连接池的配置。
16.3.2 找出硬盘读写频繁的文件
找出读文件次数最多的文件名,默认前10:

cd openresty-systemtap-toolkit

./accessed-files -p 48070 -r

Tracing 48070 (/usr/local/nginx/sbin/nginx)...
Hit Ctrl-C to end.
^C
=== Top 10 file reads ===
#1: 1 times, 2033 bytes reads in file middle_page.lua.
#2: 1 times, 2374 bytes reads in file rand_str.lua.
找出被写入次数最多的文件名,默认前10:

./accessed-files -p 48070 -w

Tracing 48070 (/usr/local/nginx/sbin/nginx)...
Hit Ctrl-C to end.
^C
=== Top 10 file writes ===
#1: 1976 times, 3353103 bytes writes in file access.log-2018-06-28-20-14-01.log.
#2: 1975 times, 841837 bytes writes in file wireless.access.log.
#3: 1360 times, 2206474 bytes writes in file access.log.
#5: 17 times, 5598 bytes writes in file error.log.
16.3.3 执行阶段耗时分析
请求会在多个执行阶段进行处理,获取到每个阶段的执行效率,就可以指定优化范围,以此提升响应速度:

cd stapxx

export PATH=$PWD:$PATH

./samples/ngx-single-req-latency.sxx -x 18993

Start tracing process 18993 (/usr/local/nginx/sbin/nginx)...

[1530153801023919] pid:18993 GET /zhe800_n_api/search/hot_words?new_user=1&user_id=0&user_type=0&callback=hot_words
total: 1235us, accept() ~ header-read: 65us, rewrite: 23us, pre-access: 23us, access: 26us, content: 985us
upstream: connect=270us, time-to-first-byte=496us, read=0us
./samples/ngx-single-req-latency.sxx -x $pid,获取worker进程的单个请求在各个阶段的消耗时间(us是微妙)。默认它只获取它看到的第一条请求,所以要测试某个请求的阶段耗时时,在goreplay进行过滤,只发送特定的URL就可以完成验证,也可以ab测试。
16.3.4 连接数和文件打开数分析
分析worker进程的连接数量和文件打开数量,可以得知系统的配置资源是否足够,避免出现文件数或者连接数不够用的情况。

cd stapxx

export PATH=$PWD:$PATH

./samples/ngx-count-conns.sxx -x 18993

Start tracing 18993 (/usr/local/nginx/sbin/nginx)...

====== CONNECTIONS ======
Max connections: 102400 #Nginx配置文件设置最大连接数
Free connections: 101854 #还可以接受的连接数
Used connections: 546 #已经使用的连接数

====== FILES ======
Max files: 102400 #Nginx配置文件设置最大文件打开数
Open normal files: 20 #当前打开的数量
16.3.5 找出CPU偷窃者
通过下面的命令可以获取到其他进程抢占指定pid的CPU的频率,此命令也支持分析其他非Nginx的进程。

cd stapxx

export PATH=$PWD:$PATH

./samples/cpu-robbers.sxx -x 30396

Start tracing process 30396 (/usr/local/nginx/sbin/nginx)...
Hit Ctrl-C to end.
^C
#1 consul: 38% (5 samples)
#2 events/0: 30% (4 samples)
#3 kblockd/0: 15% (2 samples)
#4 watchdog/0: 7% (1 samples)
#5 zabbix_agentd: 7% (1 samples)
16.3.6 正则表达式耗时分析
Nginx中会大量使用正则表达式,特别是有些动态路由,如果正则的耗时过长对整体的请求还有CPU消耗都会造成不良影响,通过下面的命令,我们可以捕获Nginx中耗时最长的正则操作:

cd stapxx

export PATH=$PWD:$PATH

./samples/ngx-pcre-top.sxx --skip-badvars -x 18999

Found exact match for libluajit: /usr/local/lib/libluajit-5.1.so.2.1.0
Found exact match for libpcre: /lib64/libpcre.so.0.0.1
Start tracing 18999 (/usr/local/nginx/sbin/nginx)
Hit Ctrl-C to end.
^C
Top N regexes with longest total running time:

  1. pattern //*/: 7921us (total data size: 17947)
  2. pattern //address/[a-zA-Z]+[0-9]+/view/edit$/: 6801us (total data size: 14684)
  3. pattern //address/[a-zA-Z]+[0-9]+/view/default$/: 6555us (total data size: 14088)
  4. pattern //address/[a-zA-Z]+[0-9]+/queryAddressById$/: 6253us (total data size: 14684)
  5. pattern //address/[a-zA-Z]+[0-9]+/view/add$/: 6161us (total data size: 14684)
  6. pattern //address/[a-zA-Z]+[0-9]+/view/query$/: 5834us (total data size: 14684)
  7. pattern //address/[a-zA-Z]+[0-9]+/view/delete$/: 5634us (total data size: 14088)
  8. pattern //address/[a-zA-Z]+[0-9]+/get_default$/: 5475us (total data size: 14088)
  9. pattern /0/: 3521us (total data size: 7258)
  10. pattern //orders/*/: 3088us (total data size: 7530)
    使用--arg utime=1选项可以提高时间的准确性,但某些平台上可能不支持。它既支持Ngx_Lua的API,也支持 lua-resty-core API。

    cd stapxx

    export PATH=$PWD:$PATH

    ./samples/ngx-pcre-top.sxx --skip-badvars -x 18999 --arg utime=1

    ^[[AFound exact match for libluajit: /usr/local/lib/libluajit-5.1.so.2.1.0
    Found exact match for libpcre: /lib64/libpcre.so.0.0.1
    Start tracing 18999 (/usr/local/nginx/sbin/nginx)
    Hit Ctrl-C to end.
    ^C
    Top N regexes with longest total running time:

  11. pattern //*/: 3000us (total data size: 20418)
  12. pattern //address/[a-zA-Z]+[0-9]+/view/add$/: 2000us (total data size: 11302)
  13. pattern //address/[a-zA-Z]+[0-9]+/view/query$/: 2000us (total data size: 11302)
  14. pattern //address/[a-zA-Z]+[0-9]+/view/manage$/: 2000us (total data size: 5428)
  15. pattern //mz/catelist/[a-zA-Z]+[0-9]+$/: 1000us (total data size: 1188)
  16. pattern //mz/baoyou/[a-zA-Z]+[0-9]+$/: 1000us (total data size: 594)
  17. pattern //m/list/baoyou/[a-zA-Z]+[0-9]+$/: 1000us (total data size: 594)
  18. pattern //m/detail/*/: 1000us (total data size: 718)
  19. pattern //cn/z_key/*/: 1000us (total data size: 997)
  20. pattern //nnc/orders/[a-zA-Z]{14}.[a-zA-Z]{4}$/: 1000us (total data size: 1445)
    你可以使用下面的命令获取所有的正则消耗的时间分布。如下面的执行结果,主要的耗时集中在16us,请求的数量是113128。

    cd stapxx

    export PATH=$PWD:$PATH

    ./samples/ngx-pcre-dist.sxx -x 18999

    Found exact match for libpcre: /lib64/libpcre.so.0.0.1
    Start tracing 18999 (/usr/local/nginx/sbin/nginx)
    Hit Ctrl-C to end.
    ^C
    Logarithmic histogram for data length distribution (byte) for 196882 samples:
    (min/avg/max: 0/22/4749)
    value |-------------------------------------------------- count
    0 | 1255
    1 | 1272
    2 |@ 3336
    4 | 1481
    8 |@@@@@@@@@@@@@ 50690
    16 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 113128
    32 |@@@@@@ 23912
    64 | 1341
    128 | 419
    256 | 30
    512 | 12
    1024 | 4
    2048 | 0
    4096 | 2
    8192 | 0
    16384 | 0
    它也支持使用--arg utime=1,可以提高时间的准确性,但某些平台上可能不支持。它既支持Ngx_Lua的API,也支持lua-resty-core API

    cd stapxx

    export PATH=$PWD:$PATH

    ./samples/ngx-pcre-dist.sxx -x 18999 --arg utime=1

    Found exact match for libpcre: /lib64/libpcre.so.0.0.1
    Start tracing 18999 (/usr/local/nginx/sbin/nginx)
    Hit Ctrl-C to end.
    ^C
    Logarithmic histogram for data length distribution (byte) for 46247 samples:
    (min/avg/max: 0/23/805)
    value |-------------------------------------------------- count
    0 | 215
    1 | 214
    2 |@ 715
    4 | 349
    8|@@@@@@@@@@@@@@@@@ 10117
    16 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 28346
    32 |@@@@@@@@@ 5973
    64 | 247
    128 | 66
    256 | 3
    512 | 2
    1024 | 0
    2048 | 0
    16.3.7找出CPU消耗高的指令
    如果发现CPU使用上异常或是消耗过高,可以利用lua-stacks.sxx,它可以获取到Nginx worker进程中占用CPU的各项指令的比例。
    如果你是LuaJIT 2.1就使用下面的指令,你得到的数据将会如下:

    cd stapxx

    export PATH=$PWD:$PATH

    ./samples/lj-lua-stacks.sxx --arg time=10 --skip-badvars -x 36984

    ./samples/lj-lua-stacks.sxx --arg time=10 --skip-badvars -x 48070
    Found exact match for libluajit: /usr/local/lib/libluajit-5.1.so.2.1.0
    WARNING: Start tracing 48070 (/usr/local/nginx/sbin/nginx)
    WARNING: Please wait for 10 seconds...
    WARNING: Time's up. Quitting now...br/>match
    builtin#88
    @/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:4
    @/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:9br/>@/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:29
    @/usr/local/nginx/conf/lua/log_jk/log_to_influxdb.lua:1br/>131
    match
    builtin#84
    @/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:9
    @/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:29br/>@/usr/local/nginx/conf/lua/log_jk/log_to_influxdb.lua:1
    114br/>compile_regex
    C:ngx_http_lua_ngx_re_find
    @/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:29
    @/usr/local/nginx/conf/lua/log_jk/log_to_influxdb.lua:1br/>75
    match
    C:ngx_http_lua_ngx_exit
    26
    lj_str_new
    C:ngx_http_lua_var_get
    @/usr/local/nginx/conf/lua/log_jk/log_to_influxdb.lua:1
    21br/>max_expand
    builtin#88
    @/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:4
    @/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:9br/>@/usr/local/nginx/conf/lua/log_jk/utils/utils.lua:29
    @/usr/local/nginx/conf/lua/log_jk/log_to_influxdb.lua:1
    如果你是LuaJIT2.0或者Lua5.1就使用下面的指令,得到和上图一样的输出

    cd openresty-systemtap-toolkit

    ./ngx-sample-lua-bt -p 9768 --luajit20 -t 10 # 如果是Lua5.1, 将--luajit20换成--lua51

    很显然这种输出方式是不便于我们观察CPU的消耗情况的,就需要用到下面章节的FlameGraph。
    16.3.8利于火焰图展示数据和分析
    FlameGraph是将采集的性能数据生成火焰图的工具,让数据更直观,使用方式如下:
    1.将16.3.7指令输出的内容存放到文件中:

    cd stapxx

    export PATH=$PWD:$PATH

    ./samples/lj-lua-stacks.sxx --arg time=10 --skip-badvars -x 36984 >/tmp/a.bt

    2.使用openresty-systemtap-toolkit中的fix-lua-bt获取具体的Lua代码:

    cd openresty-systemtap-toolkit

    ./fix-lua-bt /tmp/a.bt > /tmp/a_new.bt

    3.使用FlameGraph生成图:

    cd FlameGraph

    ./stackcollapse-stap.pl /tmp/a_new.bt > /tmp/a_new.cbt

    ./flamegraph.pl /tmp/a_new.cbt > /tmp/a.svg

    4.将a.svg拷贝出来,放到浏览器中打开,会看到如图16-1所示的数据。
    图16-1 worker进程火焰图
    对这个a.svg分析如下。
    1.解读火焰图:
    图中垂直的数轴即y轴,它表示调用栈的深度。最下面是父函数,依次向顶部。高度越高调用的层次越深。
    图中水平的数轴即x轴,它表示请求占用CPU的情况,越下面的请求代表次数越多,耗时占用也就越长。
    一般最上面如果是平顶的情况,就可能存在瓶颈,正常的火焰图呈现的效果应该是会有很多尖刺,而不应该是平顶。
    2.点击图中的一个CPU占比高的,就会看到是哪个函数执行的。
    如图16-2中match采样1144次,CPU占比8.75%,在火焰图的顶端,它表示是这个函数最后执行的一个命令,match是正则匹配操作。
    图16-2 CPU占用高的火焰图
    3.可以看出此火焰图平顶过多,有优化的空间,根据里面定位的函数进行优化即可。
    4.图16-3是OpenResty官网提供的一个正常的火焰图。

图16-3 正常火焰图模型
16.4 检查全局变量
在前面的章节中曾多次提到过要避免使用全局变量,它会带来很多不利的影响,通过下面的方式可以检查出变量是全局还是局部的。
1.安装检查工具luacheck

yum install luarocks

luarocks install luacheck --deps-mode=none

--deps-mode=none 是可选参数,如果你在安装中出现报错,就加上此参数再试试。
2.如果需要检查目录下所有的Lua脚本的变量,请安装LuaFileSystem,luacheck依赖它。

luarocks install luafilesystem

3.执行检查,一般情况下代码都是基于Ngx_Lua或者LuaJIT 2.0/2.1开发,所以检查时加入参数--std Ngx_Lua。如果是Lua5.1开发的,请使用--std lua51。--std的作用是设置使用指定格式的全局变量的环境。
如下发现version和xx可能存在问题的,请打开文件检查吧。

luacheck --std ngx_lua /tmp/ab_version.lua

Checking /tmp/ab_version.lua 2 warnings

/tmp/ab_version.lua:1:7: value assigned to variable version is unused
/tmp/ab_version.lua:2:1: setting non-standard global variable xx

Total: 2 warnings / 0 errors in 1 file, couldn't check 1 file
16.5 小结
我们讲解了很多常见的性能分析命令,这对服务的优化是有很多帮助的,并且openresty-systemtap-toolkit和stapxx提供了非常丰富的指令来分析性能问题。比如分析off-cpu、内存泄漏、请求的队列等,有兴趣的读者可以访问它们各自的GitHub的wiki来按需使用。