火焰图显示lj_str_new调用过多,请大家帮我找一下原因

391 views
Skip to first unread message

晏旭

unread,
Nov 8, 2017, 9:12:48 AM11/8/17
to openresty
nginx 1.13.6
lua_nginx 0.10.11
lua_jit 2.1.0-beta2

get_messages函数的代码如下:
function _M.get_messages(queue, receiver, start, max, retry_num)
    assert(start>=0)
    assert(max>=1)

    local result = {}
    local index = string.format('%s:%d,%d', queue, start, max)
    log(INFO, index)

    local db = database.connect()
    if not db then
        return 500, 'failed to connect to mysql'
    end

    for id = start,start+max-1 do
        local message = cache_message:get(queue..':'..id)
        if message then
            table.insert(result, message)
        else
            -- 1. select from mysql if cache miss
            log(INFO, 'message #', id, ' miss, query message ', index)
            local sql = string.format('select * from %s_msg where id >= %d limit %d', queue, start, max)
            log(INFO, 'sql: ', sql)
            local err, errcode, sqlstate
            result, err, errcode, sqlstate = db:query(sql)
            assert(next(result))
            -- print('res: ', inspect(result))
            if not result then
                log(ERR, "bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
                return 500, 'mysql error'
            end
            set_messages(queue, result)
            break
        end
    end

    -- 2. insert result into mysql
    local values = {}
    local sql
    if retry_num > 0 then
        for i, message in ipairs(result) do
            table.insert(values, string.format("(%d, '%s')", message['id'], receiver))
        end
        sql = string.format([[insert into %s_rst(m_id, receiver)
values%s]], queue, table.concat(values, ','))
    else
        -- handle the special case when retry_num is 0
        for i, message in ipairs(result) do
            table.insert(values, string.format("(%d, '%s', 'failed')", message['id'], receiver))
        end
        sql = string.format([[insert into %s_rst(m_id, receiver, status)
values%s]], queue, table.concat(values, ','))
    end
    log(INFO, 'sql: ', sql)

    local res, err, errcode, sqlstate = db:query(sql)
    -- print('res: ', inspect(res))
    if not res then
        log(ERR, "bad result: ", err, ": ", errcode, ": ", sqlstate, ".")
        return 500, 'mysql error'
    end

    -- 3. update queue.recv.last and queue.recv.processing
    set_last_id(queue, receiver, result[#result]['id'])
    if retry_num > 0 then
        update_processing_num(queue, receiver, #result)
    end

    database.keepalive(db)

    return 200, result
end

目前我还不清楚到底是这个函数中的哪几行导致的性能瓶颈,希望各位大牛帮忙找一下原因,谢谢了!

我有几个疑问:
  1. message.lua:_M.get_messages上面的T:message.lua:_M.get_messages表示什么意思?或者说前缀带T:的是什么意思?
  2. lj_str_new调用过多是因为使用了string.format吗?我看string.format的源码并未调用lj_str_new,所以觉得很奇怪
  3. 如果是string.format的原因,那么lua里面有哪些高效拼接字符串的方法?


a.svg

晏旭

unread,
Nov 8, 2017, 9:35:12 AM11/8/17
to openresty
纠正一下,string.format调用了lj_str_new,但也只是调用了一次。

晏旭

unread,
Nov 9, 2017, 1:15:10 AM11/9/17
to openresty
使用luajit2.0.5也存在同样的问题,似乎lj_str_new真的有性能问题。

附件是使用sample-bt采集的火焰图
a.svg

tokers

unread,
Nov 9, 2017, 4:14:30 AM11/9/17
to openresty
1. 你的这个火焰图并没有显示 Lua 层面的函数调用,可以尝试画出 Lua 层面的火焰图,这样更加直观。

2. get_messages 这个函数,里面的循环,我看内部有

local message = cache_message:get(queue..':'..id)

local sql = string.format('select * from %s_msg where id >= %d limit %d', queue, start, max)

这些调用,有字符串拼接,还有 string.format,如果循环次数比较多,可能 lj_str_new 的占用会比较高。

3. Lua 里面最经典的拼接字符串的方法是 table.concat。

Alex Zhang




Zexuan Luo

unread,
Nov 9, 2017, 6:51:07 AM11/9/17
to open...@googlegroups.com
我仔细看了下你的代码,

> local index = string.format('%s:%d,%d', queue, start, max)

这一行似乎可以去掉了?
index 只在日志的时候用到,可以交由 ngx.log 拼接(实际上线上环境下应该是不会处理的),不需要事先创建出来。
当然这个不会是瓶颈所在。

我觉得影响性能的应该是 insert into 那一块,每个 id 都会创建一个新的字符串,最后还会啪的一下拼出个较长的 SQL 语句出来。LuaJIT 在执行 lj_str_new 的时候,会先去查找是否有同样的字符串存在,没有才会创建新的。
但是这里的字符串个个不同,是没法复用的,走的代码路径会更长。不过这点似乎没什么好办法解决,除非 mysql 库支持 prepare statement……

另外我看你的 LuaJIT 版本比较旧,是否能试下 openresty/luajit2 的最新版呢?他们在新版本上修改了 luajit 的字符串哈希逻辑。虽然这么做是出于提高安全性的目的,不过说不定也能改善你的应用的 lj_str_new 性能呢。


--
--
邮件来自列表“openresty”,专用于技术讨论!
订阅: 请发空白邮件到 openresty+subscribe@googlegroups.com
发言: 请发邮件到 open...@googlegroups.com
退订: 请发邮件至 openresty+unsubscribe@googlegroups.com
归档: http://groups.google.com/group/openresty
官网: http://openresty.org/
仓库: https://github.com/agentzh/ngx_openresty
教程: http://openresty.org/download/agentzh-nginx-tutorials-zhcn.html

晏旭

unread,
Nov 9, 2017, 7:30:48 AM11/9/17
to openresty
 非常感谢你的回复🙏

1.
一楼的附件是是luajit2.1.0-beta2下的lua层面的火焰图
三楼的附件是luajit2.0.5下的c层面的火焰图
两者都可以看出lj_str_new这个函数是瓶颈

2.
循环
for id = start,start+max-1 do
    local message = cache_message:get(queue..':'..id)
    ...
end
在我压测的时候只会执行一遍,这个我可以保证

此外
local sql = string.format('select * from %s_msg where id >= %d limit %d', queue, start, max)

只会执行一遍,因为最后有break

3.
table.concat确实比多次..操作效率高,但是format的效率也不应该这么差。。。
请问大神,拼接mysql语句有什么好的建议吗?感觉用format是最直白的了

晏旭

unread,
Nov 9, 2017, 7:36:38 AM11/9/17
to openresty
非常感谢你!
你说的我都赞同。
我试了luajit2.1.0-beta2和beta3,还是一样的问题,最新稳定版的openresty用的是beta3。
订阅: 请发空白邮件到 openresty...@googlegroups.com
发言: 请发邮件到 open...@googlegroups.com
退订: 请发邮件至 openresty+...@googlegroups.com

晏旭

unread,
Nov 9, 2017, 7:44:40 AM11/9/17
to openresty
类似的问题在这里有讨论

我看了一下luajit-2.1.0-beta3中lj_str_new函数的代码,应该是查找字符串是否已经存在的代码块开销比较大,这个是根本的原因。


Zexuan Luo

unread,
Nov 9, 2017, 11:44:06 AM11/9/17
to open...@googlegroups.com
我看了下 lua-resty-mysql 的实现,似乎有个不需要拼接字符串的方法。关键是在 _send_packet 这个函数里面的 sock:end(packet) 方法。sock:send 除了接受 string 之外,还可以接收 array table,这时候会在 C land 里面实现 concat。如果能把 concat 操作挪到 C land 里面,就可以避免大量临时字符串的产生了。
代码大概要改成这样:
sock:send({_set_byte3(size), strchar(band(self.packet_no, 255)), req_part1, req_part2, req_part3, ...}

你试下这样改能不能减少 CPU 损耗?

订阅: 请发空白邮件到 openresty+subscribe@googlegroups.com
发言: 请发邮件到 open...@googlegroups.com
退订: 请发邮件至 openresty+unsubscribe@googlegroups.com

Zexuan Luo

unread,
Nov 9, 2017, 12:01:17 PM11/9/17
to open...@googlegroups.com
更正一下,直接
sock:send({_set_byte3(size), strchar(band(self.packet_no, 255)), {"insert into ...", {row1, row2...})
即可。
这里面的 array 是可以递归嵌套的。所以完全不需要 table.concat,直接把 table 传进去。
你还可以试下把
(%d, '%s', 'failed') 这一块拆成 array 传过去,看看能不能直接把 string.format 都省了。

晏旭

unread,
Nov 10, 2017, 12:02:18 PM11/10/17
to openresty
太感谢你了,你的方法可行。在我的应用了把format改成直接传table性能提高了50%。
已经提pull request了:feature: support pass table to send_query,希望能帮到更多的人。

个人认为luajit在创建字符串这个地方设计的不好,为什么相同的字符串只存一份?目前来看这种方式是弊大于利了。

最后再次感谢社区的热心帮助🙏

Zexuan Luo

unread,
Nov 11, 2017, 7:32:57 AM11/11/17
to open...@googlegroups.com
luajit 这么做,个人看法是为了减少内存占用。毕竟作为一个嵌入语言,以减低内存占用优先也算是合理的。

--

Yichun Zhang (agentzh)

unread,
Nov 11, 2017, 4:22:13 PM11/11/17
to openresty
Hello!

2017-11-09 4:36 GMT-08:00 晏旭:
> 非常感谢你!
> 你说的我都赞同。
> 我试了luajit2.1.0-beta2和beta3,还是一样的问题,最新稳定版的openresty用的是beta3。
>

OpenResty 自带的 LuaJIT 不同于官方的 LuaJIT,有一些特别的优化,特别是在 lj_str_new 里面。使用最新的
OpenResty 1.13.6.1 RC1,是没有这个性能问题的。

Regards,
Yichun

unread,
Jun 12, 2019, 11:05:39 PM6/12/19
to openresty
我在1.15.8.1版本中stream模块开发mqtt服务器, 这个问题还是出现了, 也是lj_str_new cpu占用很高, hash冲突非常严重, 也和测试的数据变化但是变化字节少有关,
我增加collectgarbage("step")调用可以缓解, 但是有消息队列堆积消息时, cpu就会很高.
分段接收为table不拼接也可以缓解, 是否有更好的解决方案?

在 2017年11月12日星期日 UTC+8上午5:22:13,agentzh写道:

DeJiang Zhu

unread,
Jun 17, 2019, 4:47:48 AM6/17/19
to open...@googlegroups.com
最好的办法是减少创建字符串,可以自己捋一遍代码。
另外,可以考虑把部分高频的字符串处理挪到 c 里面来实现。

峰 <9445...@qq.com> 于2019年6月13日周四 上午11:05写道:
--
--
邮件来自列表“openresty”,专用于技术讨论!
订阅: 请发空白邮件到 openresty...@googlegroups.com
发言: 请发邮件到 open...@googlegroups.com
退订: 请发邮件至 openresty+...@googlegroups.com
归档: http://groups.google.com/group/openresty
官网: http://openresty.org/
仓库: https://github.com/agentzh/ngx_openresty
教程: http://openresty.org/download/agentzh-nginx-tutorials-zhcn.html
---
您收到此邮件是因为您订阅了Google网上论坛上的“openresty”群组。
要退订此群组并停止接收此群组的电子邮件,请发送电子邮件到openresty+...@googlegroups.com
要在网络上查看此讨论,请访问https://groups.google.com/d/msgid/openresty/f8ee56e4-0866-40b2-8832-7d72d9a532d7%40googlegroups.com
Reply all
Reply to author
Forward
0 new messages