使用火焰图分析并优化 OpenResty

最近在向网关添加一个新的功能(Signature Verify)并测试时,发现性能损耗十分严重,单条选择器来执行流管时的 TPS 只有 3K 多,通过火焰图来分析定位性能损耗的点并进行优化将 TPS 提升到 12K+ ,本文主要记录使用火焰图定位问题并取实际环境中修改的一些点来提升性能的过程。

系统性能的评估维度可能性很多,像系统工作时的吞吐量、响应时间、任务完成时间和资源利用率等,但这些指标仅仅只是表象,一旦发生异常(如前面提到的功能中我使用了 gmatch 去匹配并且使用了可 jit 的一些代码技巧,但性能损耗仍然非常严重时无法通过预判来定位问题),则需要通过火焰图来观察问题根本原因。

问题代码如下:

以下代码片段中主要性能损耗在 gmatch 对请求体的匹配过程中,而后两次 while 中需要对匹配到的字符进行操作,新建的 table 有多次 rehash 的操作, 但它们是可被 jit 的,故此当时我认为这是可以被理解的实现方式, 那么也就只有 table.sort 是严重影响性能的了,结果也符合预期,见后面的火焰图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ngx.req.read_body()
local body_data = ngx.req.get_body_data()

local iterator, err = ngx.re.gmatch(body_data, "([a-z0-9A-Z\\x{4e00}-\\x{9fa5}]+)", "uj")
if err then
...
end

while true do
...
end

local iterator, err = ngx.re.gmatch(ngx_b64(stringy.join(raw_strings, "")), ".", "uj")
if err then
...
end

table_sort(raw_numbers)

如何观察火焰图:

燃烧在火苗尖部的就是 CPU 正在执行的操作,不过需要说明的是颜色是随机的,本身并没有特殊的含义,纵向表示调用栈的深度,横向表示消耗的时间。因为调用栈在横向会按照字母排序,并且同样的调用栈会做合并,所以一个格子的宽度越大越说明其可能是瓶颈。
综上所述,lj_cf_table_sort 就是罪魁祸首,我压测的机器时 16core 12GB 的配置,当时压测 25 万次请求 500 并发,可以把 CPU 打满哦。

smoothsort

既已发现是排序( table.sort 是不可被 jit 支持的)导致的性能问题,那么我就想到了使用其他排序算法,第一次尝试了 smoothsort 的 ffi 实现,结果符合预期,TPS 上升了 1000 多,但还是很不理想,因为我们对网关的期望是能达到 10K 以上的, 如图:

可以看得到性能仍然受排序算法影响,值得一提的是在烧这个图的时候,我已经优化掉了 gmatch ,使用 gsub 来取反替换以及 re.split 来替代昂贵的循环操作。

shellsort

经过一番思考,我选择了 shellsortshellsort 通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快),看图:

可以看到排序带来的影响已经非常小了,至于目前 get_bodysplit 是我能想到的最优方式了,故此优化算是告一段落,随后我请测试组帮忙压测时用到了 15 条选择器 3 条规则,将 TPS 提升到了 12K+。

代码实现如下:

至于 smoothsort ffi 的实现比较一大段,就不贴了,有兴趣的同学自行了解一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local function shellsort(tb)
local tb_len = #tb
local incr = math_ceil( tb_len / 2 )
while incr > 0 do
for i = incr, tb_len do
local temp = tb[i]
local j = i
while j > incr and tb[j-incr] > temp do
tb[j] = tb[j-incr]
j = j - incr
end
tb[j] = temp
end
incr = math_floor( 0.5 + incr / 2.2 )
end
return tb
end

总结

解决问题的能力非常重要,问题可以说是千变万化,但是发现问题的手段却不多,在此感谢 春哥 提供的工具包和 OpenResty 生态。谈及解决问题的思路无非是优化代码本身、多尝试(例如排序算法在实际环境中的性能差异),改变思路(当前认为够好是否还有其他方式来变向的实现)。