我们最近使用 OpenResty XRay 帮助网络安全行业的一个企业客户优化了他们的 OpenResty/Nginx 应用程序请求吞吐量低的问题。

OpenResty XRay 在客户的生产环境中自动进行了所有分析并生成了诊断报告,在报告中我们立即看到了瓶颈问题。Lua IPC1 管道操作,io.popen 等,严重阻塞了 OpenResty/Nginx 的事件循环。

使用 OpenResty 自身的非阻塞 lua-resty-shell 库替换标准的 Lua API 函数调用,吞吐量几乎有七倍的改进。

改进结果

用 OpenResty 的非阻塞 cosocket API 替换系统管道,可以将吞吐量进一步提高 150 倍。

更多改进

OpenResty XRay 是一个动态追踪产品,它可以自动分析正在运行中的应用程序,以排除性能问题、行为问题和安全漏洞,并提供可行的建议。在底层实现上,OpenResty XRay 由我们的 Y 语言驱动,可以在不同环境下支持多种不同的运行时,如 Stap+, eBPF+, GDB 和 ODB。

问题

客户在他们的在线 OpenResty 应用程序中遭遇了严重的性能问题。在他们的服务器上,每秒最大请求数非常低,仅有 130 左右。这糟糕到让服务几乎不可用。此外,尽管他们的服务器上配有高端 CPU,他们的 Nginx worker 进程只能利用不到一半的 CPU 资源。

分析

OpenResty XRay 对客户的在线进程进行了深入分析。它不需要客户的应用程序进行任何协作。

  • 没有额外的插件、模块或库。
  • 没有代码注入或补丁。
  • 没有特殊的编译或启动选项。
  • 甚至不需要重新启动应用程序进程。

分析完全是以“事后”的方式进行的。这得益于 Openresty XRay 采用的动态追踪技术。

这类性能问题对于 OpenResty XRay 来说很容易分析。它发现 CPU 和 off-CPU 操作一起阻塞了 OpenResty/Nginx 的事件循环。

CPU 操作

OpenResty XRay 的自动分析报告中,我们可以看到 io.popen 及其相关的 file:close() 操作让 CPU 使用率非常高。

io.popen

在 CPU 类别下,我们可以找到 io.popen 问题。它占据了目标进程所消耗的总 CPU 时间的 93.8%

io.popen 的 CPU 问题

注意高亮显示的问题中的 [buildin#io.popen] Lua 函数帧。

问题文本显示了完整的 Lua 代码路径,包括虚拟机内的 LuaJIT 原语,因此用户可以在对应 Lua 源代码中快速定位。如果我们将鼠标光标悬停在 Lua 函数 run() 的绿框上,就会弹出一个工具提示,其中有 Lua 源文件名和行号等细节。

io.popen 的 CPU 问题的源文件名和行号

我们可以看到,io.popen 调用的位置在 Lua 源文件 /usr/local/openresty/site/lualib/cfg-utils.lua 的第 8 行。

file:read()

在 CPU 类别下,我们发现了 file:read() 问题。它占据了目标进程所消耗的总 CPU 时间的 26.3%

file:read 的 CPU 问题

注意高亮显示的问题中的 [buildin#io.method.read] 函数帧。

问题文本显示了完整的 Lua 代码路径,包括虚拟机内的 LuaJIT 原语,因此用户可以在对应 Lua 源代码中快速定位。如果我们将鼠标光标悬停在 Lua 函数 run() 的绿框上,就会弹出一个工具提示,其中有 Lua 源文件名和行号等细节。

file:close 的 cpu 问题的源文件名和行号

我们可以看到,file:read() 的调用位置在 Lua 源文件 /usr/local/openresty/site/lualib/cfg-utils.lua 的第 14 行。客户检查了该行,并确认它是在先前的 io.popen 调用所打开的文件柄上。

off-CPU 操作

这里的 “off-CPU” 指的是操作系统线程被阻塞并处于等待状态,不能执行后面的代码。

off-CPU 图解

file:read()

在诊断报告中,我们看到 file:read() 的调用在 off-CPU 时间方面很热。它占用了目标进程所消耗的总 off-CPU 时间的 99.8%,而唯一会阻塞的应该是 Nginx 事件循环的事件等待操作(如 epoll_wait 系统调用)。

file:read 的 off-CPU 问题

注意高亮显示的问题中的 [buildin#io.method.read] Lua 函数帧。

问题文本显示了完整的 Lua 代码路径,包括虚拟机内的 LuaJIT 原语,因此用户可以在其 Lua 源代码中快速定位。如果我们将鼠标光标悬停在 Lua 函数 run() 的绿框上,就会弹出一个工具提示,其中有 Lua 源文件名和行号等细节。

file:read 的 off-CPU 问题的源文件名和行号

我们可以看到,file:read() 的调用位置在 Lua 源文件 /usr/local/openresty/site/lualib/cfg-utils.lua 的第 14 行。客户检查了该行,确认它也是在先前的 io.popen 调用所打开的文件句柄上。

解决方案

根据上述分析,罪魁祸首是管道的 Lua API,包括 io.popen 和对管道文件句柄的读取和关闭操作。它在 CPU 和 off-CPU 时间方面都严重阻塞了 Nginx 的事件循环。因此,解决方案也是直截了当的。

  1. 在 OpenResty 应用程序中避免使用 io.popen Lua API。使用 OpenResty 的 lua-resty-shell 库或低级别的 Lua API ngx.pipe 来代替。
  2. 完全避免系统命令和 IPC 管道。使用 OpenResty 提供的更有效的 cosocket API 或建立在它之上的更高级别的库。

结果

客户听从了我们的建议,在他们的网关应用中从标准的 Lua API io.popen迁移到 OpenResty 的 lua-resty-shell 库。然后他们立即看到了大约七倍的改进。

改进结果

我们进一步建议他们应该完全避免昂贵的系统命令调用。然后他们修整了业务逻辑,并利用 OpenResty 的非阻塞 cosocket API 来获取元数据。这一变化带来了 150 倍的改进,让1个 CPU 核心达到每秒数以万计的请求。

更多改进

客户对现在的性能表现很满意。

关于作者

章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。

章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴, 是 “边缘计算“、”动态追踪 “和 “机器编程 “的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,并编写过 60 多个开源软件库。

关注我们

如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站

我们也在 B 站上也有 OpenResty 官方的视频分享空间,欢迎订阅。

同时欢迎扫码关注我们的微信公众号:

我们的微信公众号


  1. IPC 是指进程间通信。 ↩︎