4.4 OpenResty性能优化
网关层由于其特定的架构位置,很容易成为整个系统的瓶颈,因此我们对网关层的性能问题应时刻保持警惕。由于Kong网关基于OpenResty打造,因此OpenResty中通用的优化技巧对于Kong网关同样适用。下面我们将从阻塞函数、table组件、缓存、火焰图这4个方面分享OpenResty的优化技巧。
4.4.1 避免使用阻塞函数
OpenResty性能优化的第一步是避免使用阻塞函数。OpenResty之所以能够保持高性能,是因为借用了Nginx事件处理和Lua协程机制。在处理流程中,如果使用阻塞函数来处理I/O,LuaJIT就不会把控制权交给Nginx的事件循环,导致其他请求要一直排队等待阻塞的事件处理完。
在很多场景下,开发者并不只是把OpenResty当作Web服务器,而是会赋予其更多业务逻辑。在这种情况下,OpenResty就有可能调用外部的命令和工具来辅助完成一些操作。例如:使用OS库中的execute方法。在OpenResty中,os.execute会阻塞当前请求。如果执行时间很长,OpenResty的性能就会急剧下降。解决的方案是使用FFI库中的lua-resty-signal方法来替换阻塞函数,还可以使用ngx.log方法来记录OpenResty日志到本地磁盘。这个方法不能频繁调用,一是因为ngx.log的使用代价大,二是因为频繁对磁盘执行写入操作会严重影响性能。解决的方案依旧是使用FFI库中的lua-resty-logger-socket方法来避免写入本地磁盘,而是将日志文件存放到远程的日志服务器中。
上述例子中,大多数解决方案是使用FFI库,避免使用Lua内置的方法。如果阻塞实在不可避免,则尽可能不要阻塞主要的工作线程,并且交给外部的其他线程或者其他服务器去处理。
4.4.2 巧用table组件
在OpenResty开发中,我们不可避免地会使用到table组件。对于table组件的优化,我们可以从以下几方面入手。
1)尽量复用,避免不必要的table组件被创建。每次使用table组件前,先创建一个空数组。如果需要初始化数据,尽量使用LuaJIT中的table.new(narray,nhash)方法。它会事先分配好指定的数组和哈希的空间,而不是在插入元素时自增长。这样多次的空间分配、resize和rehash等动作可以合并为一次完成。
2)自己维护table下标。向table中添加元素或者遍历table的操作的时间复杂度是O(n)。我们可以自己维护table下标,将时间复杂度降至O(1)。
3)循环使用单个table。循环利用table时需要将原有table中的数据清理干净。我们可以使用table.clear函数将table中的每个元素置为nil,避免给下一个使用者造成污染。一般将循环使用的table放入一个模块的最高层,这样我们在使用模块中的函数时可以根据实际情况决定是直接使用还是待数据清理干净后再使用table。
4)使用table池。将多个table存入缓存池,当需要使用table时,可以使用fetch方法从缓存池中拿出使用。如果没有空闲的数组,可调用table.new方法创建一个数组。使用完后,调用release方法将数组放回缓存池。
我们需要根据实际情况来决定是否进行table组件优化。比如不一定必须使用table池;自己维护table下标有利有弊,它虽然保证了不浪费空间,但是降低了代码的可读性,并且容易出错。我们需要做的是权衡利弊,通过实际的压测数据进行针对性的优化。
4.4.3 使用缓存
缓存在性能优化中承担着非常重要的角色。优化效果也是立竿见影的。OpenResty提供了两种缓存组件:shared dict和lru,如表4-2所示。
表4-2 OpenResty缓存组件对比
两种组件侧重点不同,使用场景也不一样。例如:当不需要在多个worker进程之间共享数据时,lru应该作为首选。其可以缓存数组、函数等复杂的数据类型,并且性能最高。当需要在多个worker进程之间共享数据时,可以在lru缓存组件的基础上加上shared dict缓存组件,构成两级缓存架构。
这两种组件本身都足够稳定、好用,但是在实际使用中还需要处理太多细节,例如缓存风暴、过期数据、多级缓存等。对于缓存风暴与过期数据,我们可以使用lua-resty-memcached-shdict来处理。它使用shared dict缓存组件为memcached做了一层封装,使用lua-resty-lock做到互斥。在缓存失效的情况下,只有一个请求到memcached获取数据,避免缓存风暴。如果没有获取最新数据,则使用过时数据先返给终端。对于多级缓存,我们可以使用lua-resty-mlcache来处理。
注意
缓存风暴,英文名dog-pile effect,表示缓存失效的瞬间。大量请求涌向数据库导致系统卡死或崩溃。发现缓存失效后,我们需要加一把锁控制数据库请求。读者可以参考lua-resty-lock文档了解细节。
4.4.4 火焰图
在开发OpenResty项目过程中,性能瓶颈问题往往隐藏得很深,不易发现。此时,我们可以使用火焰图来解决该问题,将性能指标可视化。在4.4.1节中,我们知道频繁使用ngx.log方法会严重降低OpenResty性能。下面对代码清单4-5进行改造(如代码清单4-6所示),在set_by_lua和content_by_lua块中循环调用ngx.log方法。
程序清单4-6 nginx.conf配置文件(改造后)
46 ... 47 location /mixed { 48 set_by_lua_block $variable { 49 for i=1,100,1 do 50 ngx.log(ngx.ERR, "set_by_lua*") 51 end 53 } ... 60 content_by_lua_block { 61 for i=1,300,1 do 62 ngx.log(ngx.ERR, "content_by_lua*") 63 end 64 } 65 ...
在图4-3(代码清单4-5生成的火焰图)和图4-4(代码清单4-6生成的火焰图)中,色块的颜色和深浅都没有实际意义,只是对不同的色块做简单区分。火焰图是把每次采样的数据进行叠加,所以真正有意义的是色块的宽度和长度。对于on-CPU火焰图来说,色块的宽度表示该方法占用CPU时间的百分比。色块越宽,说明性能越低。色块的长度表示该方法调用的深度,最顶端的框显示的是正在运行的方法,其之下框显示的都是这个方法的调用者。我们从图4-4中不难看出,ngx.log方法被频繁调用,CPU消耗最大。set_by_lua和content_by_lua块的CPU消耗比为1∶3。这也符合我们的预期(参见代码清单4-6,ngx.log方法调用次数分别为100和300)。
图4-3 代码清单4-5生成的火焰图
图4-4 代码清单4-6生成的火焰图
注意
常见的火焰图类型有on-CPU、of f-CPU、Memory、Hot/Cold和Differential等。关于火焰图详细的介绍,读者可以参考Blazing Performance with Flame Graphs。on-CPU火焰图或者off-CPU火焰图的使用场景取决于当前的瓶颈。如果瓶颈是CPU,则使用on-CPU火焰图;如果瓶颈是I/O或锁,则使用off-CPU火焰图。如果瓶颈无法确定,可以通过压测工具来确认。通过压测工具观察能否让CPU使用率趋于饱和,如果能,则使用on-CPU火焰图;如果不能,则使用of f-CPU火焰图。
火焰图的生成主要依赖于SystemTap工具,具体操作流程如下:
# 安装SystemTap,如果已经安装请忽略。rpm包依赖的版本来源于系统内核版本 $ wget http://ftp.riken.jp/Linux/cern/centos/7/updates/Debug/ x86_64/kernel-debuginfo-3.10.0-957.21.3.el7.x86_64.rpm $ wget http://ftp.riken.jp/Linux/cern/centos/7/updates/Debug/ x86_64/kernel-debuginfo-common-x86_64-3.10.0-957.21.3.el7.x86_64.rpm $ wget https://linuxsoft.cern.ch/cern/centos/7/updates/ x86_64/Packages/kernel-devel-3.10.0-957.21.3.el7.x86_64.rpm $ rpm -ivh kernel-debuginfo-3.10.0-957.21.3.el7.x86_64.rpm $ rpm -ivh kernel-debuginfo-common-x86_64-3.10.0-957.21.3.el7.x86_64.rpm $ rpm -ivh kernel-devel-3.10.0-957.21.3.el7.x86_64.rpm # 安装SystemTap $ yum install systemtap -y # 下载火焰图所需工具 $ git clone https://github.com/openresty/stapxx.git $ git clone https://github.com/openresty/openresty-systemtap-toolkit.git $ git clone https://github.com/brendangregg/FlameGraph.git
安装完成后,将SystemTap工具放到指定目录/opt/tool/下。目录结构如下:
. ├── FlameGraph ├── openresty-systemtap-toolkit ├── stapxx └── start.sh
其中,start.sh脚本用于获取Nginx的worker进程ID,进而捕获数据,最终将数据转换为文件形式。start.sh脚本内容如代码清单4-7所示。
程序清单4-7 start.sh脚本
1 export PATH=/opt/tool/stapxx:/opt/tool/FlameGraph:/opt/tool/ openresty-systemtap-toolkit:$PATH 2 pid=$(ps aux|grep 'nginx'|grep 'worker'|awk '{print $2}') 3 ./stapxx/samples/lj-lua-stacks.sxx --arg time=5 --skip-badvars -x $pid > a.bt 4 ./FlameGraph/stackcollapse-stap.pl a.bt > a.cbt 5 ./FlameGraph/flamegraph.pl --encoding="ISO-8859-1" \ 6 --title="Whitelist Interface ON CPU FlameGraph" a.cbt > a.svg
完成上述步骤后,使用wrk-t10-c100-d20shttp://127.0.0.1:80/mixed命令对OpenResty接口进行压测,同时在服务器中执行start.sh脚本。脚本执行完成后,我们会得到一个a.svg文件。文件内容如图4-3和图4-4所示。