Lua 级别 CPU 火焰图简介

在 OpenResty 或 Nginx 服务器中运行 Lua 代码现在已经变得愈来愈常见,由于人们但愿他们的非阻塞的 Web 服务器可以兼具超高的性能和很大的灵活性。有些人使用 Lua 完成一些很是简单的任务,好比检查和修改某些请求头和响应体数据,而有些人则利用Lua 建立很是复杂的 Web 应用、 CDN 软件和 API 网关等等。Lua 以简单、内存占用小和运行效率高而著称,尤为是在使用 LuaJIT 这样的的即时编译器 (JIT) 的时候。但有些时候,在 OpenResty 或 Nginx 服务器上运行的 Lua 代码也会消耗过多的 CPU 资源。一般这是因为程序员的编程错误,好比调用了一些昂贵的 C/C++ 库代码,或者其余缘由。html

要想在一个在线的 OpenResty 或 Nginx 服务器中快速地定位全部的 CPU 性能瓶颈,最好的方法是使用 OpenResty XRay 产品提供的 Lua 语言级别 CPU 火焰图的采样工具。这个工具 须要对 OpenResty 或 Nginx 的目标进程作任何修改,也不会对生产环境中的进程产生任何可觉察的影响。nginx

本文将解释什么是火焰图,以及什么是 Lua 级别的 CPU 火焰图,会穿插使用多个小巧且独立的 Lua 代码实例来作演示。咱们将利用 OpenResty XRay 来生成这些示例的火焰图来进行讲解和分析。咱们选择小例子的缘由是,它们更容易预测和验证各类性能分析的结果。相同的分析方法和工具也适用于那些最复杂的 Lua 应用。过去这几年,咱们使用这种技术和可视化方式,成功地帮助了许多拥有繁忙网站或应用的企业客户。git

什么是火焰图

火焰图是由 Brendan Gregg 发明的一种可视化方法,用于展现某一种系统资源或性能指标,是如何定量分布在目标软件里全部的代码路径上的。程序员

这里的“系统资源”或指标能够是 CPU 时间、off-CPU 时间、内存使用、硬盘使用、延时,或者任何其余你能想到的资源。github

而“代码路径”能够定义为目标软件代码中的调用栈轨迹。调用栈轨迹一般是由一组函数调用帧组成的,一般出如今 GDB 命令 bt 的输出中,以及 Python 或 Java 程序的异常错误信息当中。好比下面是一个 Lua 调用栈轨迹的样例:编程

C:ngx_http_lua_ngx_timer_at
at
cache.lua:43
cache.lua:record_timing
router.lua:338
router.lua:route
v2_routing.lua:1214
v2_routing.lua:route
access_by_lua.lua:130

在这个例子中,Lua 栈是从基帧 access_by_lua.lua:130 一路生长到顶帧 C:ngx_http_lua_ngx_timer_at。它清晰地显示了不一样的 Lua 或 C 函数之间是如何相互调用的,从而构成了“代码路径”的一种近似表示。安全

而上文中的“全部代码路径”,其实是从统计学的角度来看,并非要真的要去枚举和遍历程序中的每一条代码路径。显然在现实中,后者的开销极其高昂,由于组合爆炸的问题。咱们只要确保全部那些开销不过小的代码路径,都有机会出如今咱们的图中,而且咱们能以足够小的偏差去量化他们的开销。bash

本文会聚焦在一种特定类型的火焰图上面。这种火焰图专用于展现 CPU 时间(或 CPU 资源)是如何定量分布在全部的 Lua 代码路径上的。特别地,咱们这里只关注 OpenResty 或 Nginx 目标进程里的 Lua 代码。天然地,这类火焰图被咱们命名为“Lua 级别 CPU 火焰图”(Lua-land CPU Flame Graphs)。服务器

本文标题图片是一个火焰图示例,后文将提供更多示例。微信

为何须要火焰图

火焰图仅用一张小图,就能够定量展现全部的性能瓶颈的全景图,而不论目标软件有多么复杂。

传统的性能分析工具一般会给用户展现大量的细节信息和数据,而用户很难看到全貌,反而容易去优化那些并不重要的地方,常常浪费大量时间和精力却看不到明显效果。传统分析器的另外一个缺点是,它们一般会孤立地显示每一个函数调用的延时,但很难看出各个函数调用的上下文,并且用户还须刻意区分当前函数自己运行的时间(exclusive time)和包括了其调用其余函数的时间在内的总时间(inclusive time)。

而相比之下,火焰图能够把大量信息压缩到一个大小相对固定的图片当中(一般一屏就能够显示全)。不怎么重要的代码路径会在图上天然地淡化乃至消失,而真正重要的代码路径则会天然地凸显出来。越重要的,则会显示得越明显。火焰图老是为用户提供最适当的信息量,很少,也很多。

如何解读火焰图

对于新手而言,正确地解读火焰图可能不太容易。但经过一些简单的解释,用户就会发现火焰图其实很直观,很容易理解。火焰图是一张二维图。y 轴显示是代码(或数据)上下文,好比目标编程语言的调用栈轨迹,而 x 轴则显示的是各个调用栈所占用的系统资源的比例。整个 x 轴一般表明了目标软件所消耗的 100% 的系统资源(好比 CPU 时间)。x 轴上的各个调用栈轨迹的前后顺序一般并不重要,由于这些调用栈只是根据函数帧名的字母顺序来排列。固然,也会有一些例外,例如笔者发明了一种时序火焰图,其中的 x 轴其实是时间轴,此时调用栈的前后顺序就是时间顺序。本文将专一于讨论经典的火焰图类型,即图中 x 轴上的顺序并不重要。

要学会读懂一张火焰图,最好的方法是尝试解读真实的火焰图样本。下文将提供多个火焰图实例,针对 OpenResty 和 Nginx 服务器上运行的 Lua 应用,并提供详细的解释。

简单的 Lua 样例

本节将列举几个简单的有明显性能特征的 Lua 样例程序,并将使用 OpenResty XRay 分析真实的 nginx 进程,生成 Lua 级别的 CPU 火焰图,并验证图中显示的性能状况。咱们将检查不一样的案例,例如开启了
JIT 即时编译的 Lua 代码、禁用了 JIT 编译的 Lua 代码(即被解释执行),以及调用外部 C 库代码的 Lua 代码。

JIT 编译过的 Lua 代码

首先,咱们来研究一个开启了 JIT 即时编译的 Lua 样本程序(LuaJIT 是默认开启 JIT)。

考虑下面这个独立的 OpenResty 小应用。本节将一直使用这个示例,但会针对不一样情形的讨论需求,适时对这个例子进行少量修改。

咱们首先准备这个应用的目录布局:

mkdir -p ~/work
cd ~/work
mkdir conf logs lua

而后咱们建立以下所示的 conf/nginx.conf 配置文件:

master_process on;
worker_processes 1;

events {
    worker_connections 1024;
}

http {
    lua_package_path "$prefix/lua/?.lua;;";

    server {
        listen 8080;

        location = /t {
            content_by_lua_block {
                require "test".main()
            }
        }
    }
}

在 location /t 的 Lua 处理程序中,咱们加载了名为 test 的外部 Lua 模块,并当即调用该模块的 main 函数。咱们使用了 lua_package_path 配置指令,来把 lua/ 目录添加到 Lua 模块的搜索路径列表中 ,由于咱们会把刚说起的 test 这个 Lua 模块文件放到 lua/ 目录下。

这个 test Lua 模块定义在 lua/test.lua 文件中:

local _M = {}

local N = 1e7

local function heavy()
    local sum = 0
    for i = 1, N do
        sum = sum + i
    end
    return sum
end

local function foo()
    local a = heavy()
    a = a + heavy()
    return a
end

local function bar()
    return (heavy())
end

function _M.main()
    ngx.say(foo())
    ngx.say(bar())
end

return _M

这里咱们定义了一个计算量较大的 Lua 函数 heavy(),计算从 1 到 1000 万 (1e7)的数字之和。而后咱们在函数 foo() 中调用两次 heavy() 函数,而在 bar() 函数中只调用一次 heavy() 函数。最后,模块的入口函数 _M.main() 前后调用 foobar 各 一次,并经过 ngx.say 向 HTTP 响应体输出它们的返回值。

显然,在这个 Lua 处理程序中,foo() 函数占用的 CPU 时间应当是 bar() 函数的两倍,由于 foo() 函数调用了 heavy() 函数两次,而 bar() 仅调用了一次。经过下文中由 OpenResty XRay 采样生成的 Lua 级别的 CPU 火焰图,咱们能够很容易地验证这里的观察结果。

由于在这个示例中,咱们并无触碰 LuaJIT 的 JIT 编译器选项,所以 JIT 编译便使用了默认的开启状态,而且现代的 OpenResty 平台版本则老是只使用 LuaJIT(对标准 Lua 5.1 解释器的支持早已移除)。

如今,咱们能够按下面的命令启动这个 OpenResty 应用:

cd ~/work/
/usr/local/openresty/bin/openresty -p $PWD/

假设 OpenResty 安装在当前系统的 /usr/local/openresty/ 目录下(这是默认的安装位置)。

为了使 OpenResty 应用忙碌起来,咱们可使用 abweighttp 这样的压测工具,向 URI http://localhost:8080/t 施加请求压力,或者使用 OpenResty XRay 产品自带的负载生成器。不管使用何种方式,当目标 OpenResty 应用的 nginx 工做进程保持活跃时,咱们能够在 OpenResty XRay 的 Web 控制台里获得相似下面这张 Lua 级别的 CPU 火焰图:

启用即时编译的 Lua 代码的 Lua-land CPU 火焰图

咱们从图上能够观察到下列现象:

  1. 图中的全部 Lua 调用栈都源自同一个入口点,即 content_by_lua(nginx.conf:24)。这符合预期。
  2. 图中主要显示了两个代码路径,分别是

    content_by_lua -> test.lua:main -> test.lua:bar -> test.lua:heavy -> trace#2:test.lua:8

    以及

    content_by_lua -> test.lua:main -> test.lua:foo -> test.lua:heavy -> trace#2:test.lua:8

    两个代码路径的惟一区别是中间的 foo 函数帧与 bar 函数帧。这也不出所料。

  3. 左侧涉及 bar 函数的代码路径的宽度,是右侧涉及 foo 的代码路径宽度的一半。换言之,这两个代码路径在图中 x 轴上的宽度比为 1:2,即 bar 代码路径占用的 CPU 时间,只有 foo 代码路径的50%。将鼠标移动到图中的 test.lua:bar 帧(即方框)上,咱们能够看到它占据总样本量(即总 CPU 时间)的 33.3%,而 test.lua:foo 所占的比例为66.7%. 显然,与咱们以前的预测相比较,这个火焰图提供的比例数字很是精确,尽管它所采起的是采样和统计分析的方法。
  4. 咱们在图中没有看到 ngx.say() 等其余代码路径,毕竟它们与那两个调用了 heavy() 的 Lua 代码路径相比,所占用的 CPU 时间微乎其微。在火焰图中,那些微不足道的代码路径本就是小噪音,不会引发咱们的关注。咱们能够始终专一于那些真正重要的部分,而不会为其余东西分心。
  5. 那两条热代码路径(即调用栈轨迹)的顶部帧是彻底相同的,都是 trace#2:test.lua:8. 它并非真正的 Lua 函数调用帧,而是一个“伪函数帧”,用于表示它正在运行一个被 JIT 编译了的 Lua 代码路径。按照 LuaJIT 的术语,该路径被称为”trace“(由于 LuaJIT 是一种 tracing JIT 编译器)。这个”trace“的编号为 2,而对应的被编译的 Lua 代码路径是从 test.lua 文件的第 8 行开始的。而 test.lua:8 所指向的 Lua 代码行是:

    sum = sum + i

咱们很高兴地看到,这个非侵入的采样工具,能够从一个没有任何外挂模块、没有被修改过、也没有使用特殊编译选项的标准 OpenResty 二进制程序,获得如此准确的火焰图。这个工具没有使用 LuaJIT 运行时的任何特殊特性或接口,甚至没有使用它的 LUAJIT_USE_PERFTOOLS 特性或者 LuaJIT 内建的性能分析器。相反,该工具使用的是先进的动态追踪 技术,仅读取原始目标进程中原有的信息。咱们甚至能够从 JIT 编译过的 Lua 代码中获取足够多的有用信息。

解释执行的 Lua 代码

解释执行的 Lua 代码一般可以获得最完美的的调用栈轨迹和火焰图样本。若是咱们的采样工具可以正确处理 JIT 即时编译后的 Lua 代码,那么在分析解释的 Lua 代码时,效果只会更好。LuaJIT 既有一个 JIT 编译器,又同时有一个解释器。它的解释器的有趣之处在于,几乎彻底是用手工编写的汇编代码实现的(固然,LuaJIT 引入了本身的一种汇编语言记法,叫作 DynASM)。

对于咱们一直在使用的那个 Lua 样例程序,咱们须要在此作少量修改,即在 server {} 配置块中添加下面的 nginx.conf 配置片断:

init_by_lua_block {
    jit.off()
}

而后从新加载(reload)或重启服务器进程,并保持流量负载。

这回咱们获得了下面这张 Lua 级别 CPU 火焰图:

解释性 Lua 代码的 Lua-land 火焰图

这张新图与前一张图在如下方面都极其类似:

  1. 咱们依旧只看到了两条主要的代码路径,分别是 bar 代码路径和 foo 代码路径。
  2. bar 代码路径依旧占用了总 CPU 时间的三分之一左右,而 foo 占用了余下的全部部分(即大约三分之二)。
  3. 图中显示的全部代码路径的入口都是 content_by_lua 那一帧。

然而,这张图与前图相比仍然有一个重要的区别:代码路径的顶帧再也不是 "trace" 伪帧了。这个变化也是预期的,由于这一回没有 JIT 编译过的 Lua 代码路径了,因而代码路径的顶部或顶帧变成为 lj_BC_IFORLlj_BC_ADDVV 等函数帧。而这些被 C: 前缀标记出来的 C 函数帧其实也并不是 C 语言函数,而是属于汇编代码帧,对应于实现各个 LuaJIT 字节码的汇编例程,它们被标记成了 lj_BC_IFORL 等符号。天然地,lj_BC_IFORL 用于实现 LuaJIT 字节码指令 IFORL,而 lj_BC_ADDVV 则用于字节码指令 ADDVVIFORL 用于解释执行 Lua代码中的 for 循环, 而 ADDVV 则用于算术加法。这些字节码的出现,都符合咱们的 Lua 函数 heavy() 的实现方式。另外,咱们还能够看到一些辅助的汇编例程,例如如 lj_meta_arithlj_vm_foldarith

经过观察这些函数帧的比例数值,咱们还得以一窥 CPU 时间在 LuaJIT 虚拟机和解释器内部的分布状况,为这个虚拟机和解释器自己的优化铺平道路。

调用外部 C/C++ 函数

Lua 代码调用外部 C/C++ 库函数的状况很常见。咱们也但愿经过 Lua 级别的 CPU 火焰图,了解这些外部的 C 函数所占用的 CPU 时间比例,毕竟这些 C 语言函数调用也是由 Lua 代码发起的。这也是基于动态追踪的性能分析的真正优点所在:这些外部 C 语言函数调用在性能分析中永远不会成为盲点1

咱们一直使用的 Lua 样例在这里又须要做少量修改,即须要将 heavy() 这个 Lua 函数修改为下面这个样子:

local ffi = require "ffi"
local C = ffi.C

ffi.cdef[[
    double sqrt(double x);
]]

local function heavy()
    local sum = 0
    for i = 1, N do
        -- sum = sum + i
        sum = sum + C.sqrt(i)
    end
    return sum
end

这里咱们使用 LuaJIT 的 FFI API ,先声明了一下标准 C 库函数 sqrt(),并直接在 Lua 函数 heavy()内部调用了这个 C 库函数。它应当会显示在对应的 Lua 级别 CPU 火焰图中。

这次咱们获得了下面这张火焰图:

调用 C 语言函数的 Lua 代码的 Lua-land 火焰图

有趣的是,咱们果真在那两条主要的 Lua 代码路径的顶部,看到了 C 语言函数帧 C:sqrt。另外值得注意的是,咱们在顶部附近依旧看到了 trace#N 这样的伪帧,这说明咱们经过 FFI 调用 C 函数的 Lua 代码,也是能够被 JIT 编译的(这回咱们从 init_by_lua_block 指令中删除了 jit.off() 语句)。

代码行层面的火焰图

上文展现的火焰图其实都是函数层面的火焰图,由于这些火焰图中所显示的全部调用帧都只有函数名,而没有发起函数调用的源代码行的信息。

幸运的是, OpenResty XRay 的 Lua 级别性能分析工具支持生成代码行层面的火焰图,会在图中添加 Lua 源代码行的文件名和行号,以方便用户在较大的 Lua 函数体中直接定位到某一行 Lua 源代码。下图是咱们一直使用的那个 Lua 样例程序所对应的一张 Lua 代码行层面的 CPU 火焰图:

即时编译的 Lua 代码的代码行层面 Lua-land 火焰图

咱们能够看到在每个函数帧上方都多了一个源代码行的伪帧。例如,在函数 main 所在的 test.lua 源文件的第 32 行 Lua 代码,调用了 foo() 函数。而在 foo() 函数所在的 test.lua:22 这一行,则调用了 heave() 函数。

代码行层面的火焰图对于准肯定位最热的 Lua 源代码行和 Lua 语句有很是大的帮助。当对应的 Lua 函数体很大的时候,代码行层面的火焰图能够帮助节约排查代码行位置的大量时间。

多进程

在多核 CPU 的系统上,为单个 OpenResty 或 Nginx 服务器实例配置多个 nginx 工做进程是很常见的作法。OpenResty XRay 的分析工具支持同时对一个指定进程组中的全部进程进行采样。当进来的流量不是很大,而且可能分布在任意一个或几个 nginx 工做进程上的时候,这种全进程组粒度的采样分析是很是实用的。

复杂的 Lua 应用

咱们也能够从很是复杂的 OpenResty/Lua 应用中获得 Lua 级别的 CPU 火焰图。例如,下面的 Lua 级别 CPU 火焰图源自对运行了咱们的 OpenResty Edge 产品的“迷你 CDN”服务器进行了采样。这是一款复杂的 Lua 应用,同时包含了全动态的 CDN 网关、地理敏感的 DNS 权威服务器和一个 Web 应用防火墙(WAF):

咱们的迷你-CDN 服务器的 Lua-land CPU 火焰图

从图上能够看到,Web 应用防火墙(WAF)占用的 CPU 时间最多,内置 DNS 服务器也占用了很大一部分 CPU 时间。咱们布署在全球范围的”迷你 CDN“网络为咱们本身运营的多个网站,好比 openresty.orgopenresty.com 提供了安全和加速支持。

它还能够分析那些基于 OpenResty 的 API 网关软件,例如 Kong 等等。

采样开销

咱们使用的是基于采样的方法,而不是全量埋点,所以为生成 Lua 级别 CPU 火焰图所产生的运行时开销一般能够忽略不计。不管是数据量仍是
CPU 损耗都是极小的,因此这类工具很是适合于生产环境和在线环境。

若是咱们经过固定速率的请求来访问 nginx 目标进程,而且 Lua 级别 CPU 火焰图工具同时在进行密集采样,则该目标进程的 CPU 使用率随时间的变化曲线以下所示:

采样时的 CPU 使用量曲线

该 CPU 使用率的变化曲线图也是由 OpenResty XRay 自动生成和渲染的。

在咱们中止工具采样以后,同一个 nginx 工做进程的 CPU 使用量曲线仍然很是类似:

不采样时进程的 CPU 使用量

咱们凭肉眼很难看出先后两条曲线之间有什么差别。因此,工具进行分析和采样的开销确实是很是低的。

而当工具不在采样时,对目标进程的性能影响严格为零,毕竟咱们并不须要对目标进程作任何的定制和修改。

安全性

因为使用了动态追踪技术,咱们不会改变目标进程的任何状态,甚至不会修改其中哪怕一比特的信息2。这样能够确保目标进程不管是在采样时,仍是没有采样时,其行为(几乎)是彻底相同的。这就保证了目标进程自身的可靠性(不会有意外的行为变化或进程崩溃),其行为不会由于分析工具的存在而受到任何影响。目标进程的表现彻底没有变化,就像是为一只活体动物拍摄 X 光片同样。

传统的应用性能管理(APM)产品可能要求在目标软件中加载特殊的模块或插件,甚至在目标软件的可执行文件或进程空间里强行打上补丁或注入本身的机器代码或字节码,这均可能会严重影响用户系统的稳定性和正确性。

由于这些缘由,咱们的工具能够安全应用到生产环境中,以分析那些在离线环境中很难复现的问题。

兼容性

OpenResty XRay 产品提供的 Lua 级别 CPU 火焰图的采样工具,同时支持 LuaJITGC64 模式 或非 GC64 模式,也支持任意的 OpenResty 或 Nginx 的二进制程序,包括用户使用任意构建选项本身编译的、优化或未优化的二进制程序。

OpenResty XRay 也能够对在 Docker 或 Kubernetes 容器内运行的 OpenResty 和 Nginx 服务器进程进行透明的分析,并生成完美的 Lua 级别的 CPU 火焰图,不会有任何问题。

咱们的工具还能够分析由 restyluajit 命令行工具运行的那些基于控制台的用户 Lua 程序。

咱们也支持较老的 Linux 操做系统和内核,好比使用 2.6.32 内核的 CentOS 6 老系统。

其余类型的 Lua 级别火焰图

如前文所述,火焰图能够用于可视化任意一种系统资源或性能指标,而不只限于 CPU 时间。所以,咱们的 OpenResty XRay 产品中也提供了其余类型的 Lua 级别火焰图,好比 off-CPU 火焰图、垃圾回收(GC)对象大小和数据引用路径火焰图、新 GC 对象分配火焰图、Lua 协程弃权(yield)时间火焰图、文件 I/O 延时火焰图等等。

咱们的博客网站 将会发文详细介绍这些不一样类型的火焰图。

结论

咱们在本文中介绍了一种很是实用的可视化方法,火焰图,能够直观地分析任意软件系统的性能。咱们深刻讲解了其中的一类火焰图,即 Lua 级别 CPU 火焰图。这种火焰图可用于分析在 OpenResty 和 Nginx 服务器上运行的 Lua 应用。咱们分析了多个 Lua 样例程序,简单的和复杂的,同时使用 OpenResty XRay 生成的对应的 Lua 级别 CPU 火焰图,展现了动态追踪工具的威力。最后,咱们检查了采样分析的性能损耗,以及在线使用时的安全性和可靠性。

关于做者

章亦春是开源项目 OpenResty® 的创始人,同时也是 OpenResty Inc. 公司的创始人和 CEO。他贡献了许多 Nginx 的第三方模块,至关多 Nginx 和 LuaJIT 核心补丁,而且设计了 OpenResty XRay 等产品。

关注咱们

若是您以为本文有价值,很是欢迎关注咱们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注咱们的微信公众号:

咱们的微信公众号

翻译

咱们提供了英文版 原文和中译版(本文)。咱们也欢迎读者提供其余语言的翻译版本,只要是全文翻译不带省略,咱们都将会考虑采用,很是感谢!


  1. 一样地,虚拟机中的任何原语例程也不会成为分析的盲点。因此,咱们也能够同时对虚拟机自己进行性能分析。
  2. Linux 内核的 uprobes 机制,仍然会以一种确保安全的方式,轻微地改变目标进程中少数机器指令的内存状态以实现透明且安全的动态探针,而这种修改对目标进程是彻底透明的。
相关文章
相关标签/搜索