上一篇讲述了casrserver分离计算内核的方案,介绍了实现该方案需要进行的改造内容。在实现完这套新的方案后,提供给识别团队改造内核的同事做压测,压测下来新方案的性能有不少的提升,同时我这边也对重构完的casrserver做了一些性能的优化。
casrserver是基于openresty的一个长连接服务,对外提供基于websocket协议的接口, 支持实时的语音流上行,casrserver和识别内核服务通过自定义的tcp协议进行通信。
开了4个worker, 在 Intel(R) Core(TM) i5-4570 CPU @ 3.20GHz 的cpu上400个并发连接做识别的测试(之所以是400个并发是因为单个asr-kernel-server支持的识别并发上限就是400左右)。
问题1
一开始测试的时候,发现4个worker的负载不是很均衡,有一个worker的cpu使用率会到40%多。
通过排查,发现之前的单worker方案,不需要开启reuseport, 我们修改了一下nginx配置,开启了reuseport,这个配置可以减少worker之间获取新连接时的锁竞争,具体介绍可以看这篇
继续压测,casrserver每个worker的cpu的负载基本均衡了,每个占用大约在30%, 与此同时,从同事的测试结果来看,这个参数的优化给识别带来了10ms左右的延时提升。
问题2
同事将我修改完打包好的csarserver镜像部署到自己的服务器上进行压测的时候,发现同样400个并发, cpu占用会高达50%多, 看了一下机器配置,这台服务器有48个核,cpu是Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50GHz, 看上去主频比我自测的机器低了一些。于是我准备抓个火焰图看看cpu的占用都在哪里,看看有没有优化的空间。
在抓取前需要做一些准备工作,具体参照《openresty最佳实践》中关于如何安装火焰图生成工具的步骤。
值得一提的是安装systemtap完后,执行测试的时候出现了如下的报错
fatal error: runtime_defines.h: No such file or directory
需要安装一下systemtap-devel
抓了30s的压测数据,最终得到了如下的火焰图:
从火焰图可以看到,49%的cpu消耗在lua-resty-websocket库的recv_frame函数中,还有36.44%的cpu消耗在我们自己的recv协程中,其中32%在封装的unpack函数中,进一步的看到一个简单的unpack_prefix函数竟然占用了11.46%的cpu, 预感是这里有优化空间。 贴一下这个函数的实现
local prefix = {byte(data, 1, 8)}
for i = 1, 4 do
if band(0xA5, prefix[i]) ~= 0 then
return nil, "bad prefix"
end
end
for i = 5, 8 do
if band(0x5A, prefix[i]) ~= 0 then
return nil, "bad prefix"
end
end
return true
其实prefix就是一个固定的8字节的头,但是为了接收者八个字节,每次都创建了一个临时的table,然后每个字节遍历判断是否符合要求,优化也很简单定义好常量,直接比较字符串。因为lua里面的字符串是不可变的,比较字符串时可以直接比较它的内存地址。
修改后直接简化为:
local function unpack_prefix(data)
local received_prefix = string.sub(data, 1, 8)
return received_prefix == protocol_prefix
end
同时排查protocol.lua中类似的代码,然后写了一个benchmark的脚本进行比较
做了100w次的测试, 得到了protocol.lua中6个函数的耗时比较
$ /usr/local/openresty/bin/resty benchmark.lua
======== BENCHMARK 1 ===============
========benchnark unpack_prefix: 12930.000066757 ms
========benchnark unpack_reserved: 12336.999893188 ms
========benchnark unpack_payload_length: 0.99992752075195 ms
========benchnark pack_reserved: 0 ms
========benchnark pack_prefix: 61.000108718872 ms
========benchnark pack_payload_length: 59.999942779541 ms
======== BENCHMARK 2 ===============
========benchnark unpack_prefix: 0.99992752075195 ms
========benchnark unpack_reserved: 0 ms
========benchnark unpack_payload_length: 1.0001659393311 ms
========benchnark pack_reserved: 0 ms
========benchnark pack_prefix: 0.99992752075195 ms
========benchnark pack_payload_length: 0 ms
可以看到unpack_prefix和unpack_reserved两个函数性能相差巨大,确认这块代码没问题后,重新进行了压测,得到的火焰图如下:
可以看到websocket库cpu占用的比例进一步提升,recv协程cpu的占用下降了约15%。打包服务后并发压测每个worker的cpu下降了约5%。
问题3
经过上面的优化,火焰图中cpu的消耗主要集中到了recv_frame这个函数上,我们项目里使用的是openresty 官方提供的lua-resty-websocket库,于是我们准备找找这个库有没有可以优化的地方。
我们先来讲一下这个websocket库,源码分为3个文件分别是protocol.lua, server.lua和client.lua。其中protocol.lua实现了websocket的协议,client.lua封装了作为客户端的api,server.lua 封装了作为服务端的api。
从火焰图里可以看到性能主要消耗在protocol.lua 封装的recv_frame里面,在代码里找到了两个for循环,都是在对mask报文做计算,解析出原始报文。 熟悉websocket协议的朋友都知道,websocket协议里规定了client -> server的payload报文需要进行mask处理,websocket协议的介绍可以看这篇websocket协议解读
同时代码里也写了一个TODO, 用luajit的string.buffer进行优化:
-- TODO string.buffer optimizations
local bytes = new_tab(payload_len, 0)
for i = 1, payload_len do
bytes[i] = str_char(bxor(byte(data, 4 + i),
byte(data, (i - 1) % 4 + 1)))
end
msg = concat(bytes)
于是我们开始寻找luajit的string.buffer api的实现,不幸的是找了一遍openresty维护的luajit库,也没有找到string.buffer的API。于是我们准备自己动手,尝试用ffi string 替换掉当前这段代码的实现,同时写了benchmark的代码在这里
$ /usr/local/openresty/bin/resty test.lua
=========benchmark1 cost:6322.9999542236 ms.
=========benchmark2 cost:720.00002861023 ms.
从结果看,执行了10w次的masked payload计算,我们发现ffi string版本的实现相较于原来的版本,性能上有8-10倍的提升。于是我们替换了官方的库再次进行压测,400并发,每个worker的cpu又下降了5%左右。同时从抓到的火焰图看recv_frame的比例下降到了22.4%。
我修改的ffi string版本的lua-resty-websocket库, 已经给官方仓库提交了PR。到此carserver性能优化工作也告一段落,不得不感叹,systemtap结合火焰图真的是排查openresty写的服务性能问题的神兵利器。