关于openresty:Lua-级别-CPU-火焰图简介

7次阅读

共计 9201 个字符,预计需要花费 24 分钟才能阅读完成。

在 OpenResty 或 Nginx 服务器中运行 Lua 代码现在曾经变得越来越常见,因为人们心愿他们的非阻塞的 Web 服务器可能兼具超高的性能和很大的灵活性。有些人应用 Lua 实现一些非常简单的工作,比方检查和批改某些申请头和响应体数据,而有些人则利用 Lua 创立非常复杂的 Web 利用、CDN 软件和 API 网关等等。Lua 以简略、内存占用小和运行效率高而著称,尤其是在应用 LuaJIT 这样的的即时编译器 (JIT) 的时候。但有些时候,在 OpenResty 或 Nginx 服务器上运行的 Lua 代码也会耗费过多的 CPU 资源。通常这是因为程序员的编程谬误,比方调用了一些低廉的 C/C++ 库代码,或者其余起因。

要想在一个 在线的 OpenResty 或 Nginx 服务器中疾速地定位所有的 CPU 性能瓶颈,最好的办法是应用 OpenResty XRay 产品提供的 Lua 语言级别 CPU 火焰图的采样工具。这个工具 须要对 OpenResty 或 Nginx 的指标过程做任何批改,也不会对生产环境中的过程产生任何可发觉的影响。

本文将解释什么是火焰图,以及什么是 Lua 级别的 CPU 火焰图,会交叉应用多个玲珑且独立的 Lua 代码实例来做演示。咱们将利用 OpenResty XRay 来生成这些示例的火焰图来进行解说和剖析。咱们抉择小例子的起因是,它们更容易预测和验证各种性能剖析的后果。雷同的分析方法和工具也实用于那些最简单的 Lua 利用。过来这几年,咱们应用这种技术和可视化形式,胜利地帮忙了许多领有忙碌网站或利用的企业客户。

什么是火焰图

火焰图是由 Brendan Gregg 创造的一种可视化办法,用于展现某一种系统资源或性能指标,是如何定量散布在目标软件里所有的代码门路上的。

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

而“代码门路”能够定义为目标软件代码中的调用栈轨迹。调用栈轨迹通常是由一组函数调用帧组成的,通常呈现在 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 函数之间是如何互相调用的,从而形成了“代码门路”的一种近似示意。

而上文中的“所有代码门路”,实际上是从统计学的角度来看,并不是要真的要去枚举和遍历程序中的每一条代码门路。显然在事实中,后者的开销极其昂扬,因为组合爆炸的问题。咱们只有确保所有那些开销不太小的代码门路,都有机会呈现在咱们的图中,并且咱们能以足够小的误差去量化他们的开销。

本文会聚焦在一种特定类型的火焰图下面。这种火焰图专用于展现 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 火焰图:

咱们从图上能够察看到下列景象:

  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 火焰图:

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

  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 火焰图中。

此次咱们失去了上面这张火焰图:

乏味的是,咱们果然在那两条次要的 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 火焰图:

咱们能够看到在每一个函数帧上方都多了一个源代码行的伪帧。例如,在函数 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):

从图上能够看到,Web 利用防火墙(WAF)占用的 CPU 工夫最多,内置 DNS 服务器也占用了很大一部分 CPU 工夫。咱们布署在寰球范畴的”迷你 CDN“网络为咱们本人经营的多个网站,比方 openresty.org 和 openresty.com 提供了平安和减速反对。

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

采样开销

咱们应用的是基于采样的办法,而不是全量埋点,因而为生成 Lua 级别 CPU 火焰图所产生的运行时开销通常能够忽略不计。无论是数据量还是
CPU 损耗都是极小的,所以这类工具非常适合于生产环境和在线环境。

如果咱们通过固定速率的申请来拜访 nginx 指标过程,并且 Lua 级别 CPU 火焰图工具同时在进行密集采样,则该指标过程的 CPU 使用率随工夫的变动曲线如下所示:

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

在咱们进行工具采样之后,同一个 nginx 工作过程的 CPU 使用量曲线依然十分类似:

咱们凭肉眼很难看出前后两条曲线之间有什么差别。所以,工具进行剖析和采样的开销的确是非常低的。

而当工具不在采样时,对指标过程的性能影响严格为零,毕竟咱们并不需要对指标过程做任何的定制和批改。

安全性

因为应用了动静追踪技术,咱们不会扭转指标过程的任何状态,甚至不会批改其中哪怕一比特的信息2。这样能够确保指标过程无论是在采样时,还是没有采样时,其行为(简直)是完全相同的。这就保障了指标过程本身的可靠性(不会有意外的行为变动或过程解体),其行为不会因为剖析工具的存在而受到任何影响。指标过程的体现齐全没有变动,就像是为一只活体动物拍摄 X 光片一样。

传统的利用性能治理(APM)产品可能要求在目标软件中加载非凡的模块或插件,甚至在目标软件的可执行文件或过程空间里强行打上补丁或注入本人的机器代码或字节码,这都可能会重大影响用户零碎的稳定性和正确性。

因为这些起因,咱们的工具能够平安利用到生产环境中,以剖析那些在离线环境中很难复现的问题。

兼容性

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

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

咱们的工具还能够剖析由 resty 或 luajit 命令行工具运行的那些基于控制台的用户 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 机制,依然会以一种确保安全的形式,轻微地扭转指标过程中多数机器指令的内存状态以实现通明且平安的动静探针,而这种批改对指标过程是齐全通明的。↩
正文完
 0