猜测渲染出来的 button
display:inline-block
,所以无法用 margin: 2em auto
居中。
最简单的做法给父元素加上:
.parent
display flex
flex-direction column
align-items center
爱编程,爱旅游,爱吐槽。
今年的第一目标是做兴趣使然的分享者,每月至少做一次直播,把肉山小课堂做到线下,帮助尽可能多的小伙伴。前三个月先把坑填上,嗯嗯。
欢迎广州的小伙伴来“肉山小课堂线下版”找我。
《Electron + Vue 实战开发》创作中……
Meathill 回答了问题 · 1月19日
猜测渲染出来的 button
display:inline-block
,所以无法用 margin: 2em auto
居中。
最简单的做法给父元素加上:
.parent
display flex
flex-direction column
align-items center
关注 3 回答 2
Meathill 回答了问题 · 1月18日
服务器要配置,404 访问指向 index.html。如果是 nginx,用 try_files
即可:
location / {
try_files $uri $uri/ /index.html$args;
}
关注 4 回答 3
Meathill 回答了问题 · 1月18日
100%
是父容器的 100%,未必准确关注 2 回答 1
Meathill 回答了问题 · 1月15日
兼容核心就一个 Proxy,caniuse 查一下对应的版本即可。目测新点的机器都能用。
不过需要注意的是,有些国产系统的浏览器核心未必标准,说不定遇到写奇葩问题。
关注 2 回答 1
Meathill 回答了问题 · 1月15日
直接在 wepback 里试用 lazy load 就行了,参考:
关注 3 回答 2
Meathill 回答了问题 · 1月12日
貌似应该反过来,把访问 /12345.jpg
的请求指向你生成图片的程序,然后传参,搞成 ?imgid=12345
。然后再延长缓存时间。
没有调试,思路是这样,你可以试一下:
location ~ "/imgs/(?<page>\d+).jpg$" {
alias /var/www/image-generator/img.php?imgid=$page;
expires 30d;
}
关注 7 回答 5
Meathill 发布了文章 · 1月9日
2020 年是我正式参加工作的第十四个年头,我仍然在一线写代码,我很开心。在这个到处弥漫着程序员 35 岁事业恐慌的年代,还能稳扎稳打写代码,对我这个写码爱好者来说,也是一件幸事。
接下来开始总结。
前几年,我尝试过投身知识付费,然后失败了——投入产出比太低,家庭条件有限无法做更多尝试。总结经验教训之后,2020 年我只保留了最低限度的尝试:跟思否签了一个实战课程。目前还在拖稿(视频)中,春节前一定会交稿,一定会的!
博客仍然在坚持写,去年总共写了 69 篇(不算草稿箱里的半成品),简单回顾一下吧,可以了解我去年的技术经历。
年初升级 Webpack 到 4,年底又升级到 5。顺便升级了一大票相关插件,研究了规则、配置、缩减包体积的方法,等等。
发现了 webpack-mock-server 这个工具,可以很方便的添加 mock API,跑测试非常方便。
厂里发了个树莓派,于是就把一些常见的软件,比如 WordPress、Ghost、Flarum 等都装在上面,以及 OpenResty、MariaDB、PostgreSQL 等软件。
如今我大部分后端软件和代码都跑在上面,这样最大的好处,就是我的后端开发环境可以随身携带,更大的扩宽了我远程开发的范围。
2020 年前端届的一件大事。年初准备迎接 Vue 3.0,年中关注它的进度,Q3 正式发布,Q4 开始升级到 Vue 3.0。
Vue 3 升级比想象中的麻烦,也比想象中的简单,目前仍在摸索中。
比如自动完成的问题、模拟用户点击动作、扩展开发等。其实我去年大量的工作都围绕在浏览器扩展开发上,不过似乎写的不够多。
Chrome 86 开始支持读取本地文件和文件夹,这个发展需要大家注意。
去年久违的装了台新电脑,就给老电脑装上了 Ubuntu 20.04,然后顺带着就摸索了一些 Ubuntu 的使用技术,比如科学上网、配置网卡什么的。
这也是前端领域值得关注的一件大事。Bootstrap 作为最受关注的前端(CSS)框架,它的发展很能体现页面搭建方面的进程。去年最成功的框架应该是 Tailwind CSS,一篇《从零到200w美金的生意》刷遍了我关注的圈子,今年希望有时间深入体验一把。
Bootstrap icons 则体现了另一方面的循环往复,webfont 从无到有到滥用到回归,值得思考。
然后从去年的博客中选出几篇我觉得值得一读的文章,兼顾新技术、老技术、技术生涯发展:
去年计划学习 flutter、TypeScript、Vue3、开发工具链。现在回望一下,完成了一半,Vue3 和开发工具链都有比较大的进步,flutter 和 TS 则基本一点没看。那就顺延到下一年吧。
Vue3 方面,迁移了一个自用 UI 组件库到 Vue3,同时保持 Vue2&3 两边同步更新。另外启动了一个 Chrome extension 项目使用 Vue3。整体来说迁移成本不高,但是开发成本略高,主要原因是周边生态落后于主框架,SourceMap 也无法正常工作,给开发带来很多困难。
因为工作的关系,去年大量接触到 Chrome Extension 的开发,对浏览器的工作机制加深了很多。比如一次 click
事件包含哪些动作、一次 keypress
包含哪些动作、如何模拟用户输入,等等。
为了让 Navigator Extension 能够适应更多的场景,我选择 PouchDB 作为本地存储方案,它可以很方便的和其它 PouchDB 或者 CouchDB 实例同步数据,很适合用来进行团队协作。
我厂开发了大量的 DSL,在线写代码是刚需。所以经过一些实践,基本掌握了 CodeMirror 解析代码和添加样式的方法,加深了对 tokenizer 模式的理解。如今让我自己开发语言感觉也不在话下了。
2020 年,比较困难,也有不少收获。2021,虽然前途仍然不明朗,不过也只有努力,才可能会有更好的将来。所以,加油吧。
本文参与了 SegmentFault 思否征文「2020 总结」,欢迎正在阅读的你也加入。
查看原文赞 3 收藏 2 评论 0
Meathill 回答了问题 · 1月8日
多说几句吧。这个问题你要这么分析:
先说(1)。JS 无法判断自身在哪个节点,所以你有两个选择:1. 给当前节点加标记;2. 查找最后一个 <script>
。
(2)比较简单,直接 document.createElement()
。(3)也简单,找到父元素,insertBefore()
。
最终代码大约是:
const newElement = document.createElement('div');
// 采用方案二
const scripts = document.getElementsByTagName('script');
const script = scripts[scripts.length - 1];
const parent = script.parentElement;
parent.insertBefore(newElement, script);
关注 3 回答 2
Meathill 回答了问题 · 2020-12-31
一般来说就是数据字典+常量吧。
不过有几点:
关注 7 回答 6
Meathill 回答了问题 · 2020-11-26
主要看目标 url 的 header,如果能解析且没有标记为 attachment,都会打开,不然就是下载。
关注 4 回答 2
Meathill 回答了问题 · 2020-11-23
首先它不是普通数组,不过你可以把它理解成视频数据,只需要丢给 <video>
就能播放。所以你需要把它转换成 url,所以你需要 URL.createObjectURL()
。最终代码大概是这样:
const data = fetch();
const url = URL.createObjectURL(data.body);
video.src = url;
不过这样会有一个问题,视频大部分是流媒体,边下边播,你这样相当于先完全下载然后再播,体验不好,也浪费钱。
关注 5 回答 2
Meathill 回答了问题 · 2020-11-12
多页面的话,每个页面都是独立且完整的环境,所以都要引入并实例化全部框架代码。如果不想这样做就要用 vue-router 这样的库实现单页应用。
关注 3 回答 2
Meathill 回答了问题 · 2020-11-09
git reset --mixed 版本号
恢复到合并之前的版本git push -f
强制替换线上版本git pull
拉下来最新版本关注 2 回答 1
Meathill 回答了问题 · 2020-10-14
直接使用 webpack 的 entry 分离即可,不同页面会独立打包,不会包含非本页需要的代码。
关注 3 回答 3
Meathill 回答了问题 · 2020-10-11
emit 不就是全局事件总线嘛……建议用 Vuex,支持响应式。
学习要知其然还要知其所以然,不然就会脑补出各种“xxx废除了”,“xxx不要用”这种都不知道哪里来的原则。
关注 3 回答 4
Meathill 回答了问题 · 2020-10-11
前端的核心是 提供优秀的用户体验,所以这里的合适做法是前端一起开始计时,超过 5 分钟就不要再让用户提交了。当然要注意这里要跟后端一起维护,不然就出 bug 了。
关注 3 回答 4
Meathill 收藏了文章 · 2020-10-08
在 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 样例程序,并将使用 OpenResty XRay 分析真实的 nginx 进程,生成 Lua 级别的 CPU 火焰图,并验证图中显示的性能情况。我们将检查不同的案例,例如开启了
JIT 即时编译的 Lua 代码、禁用了 JIT 编译的 Lua 代码(即被解释执行),以及调用外部 C 库代码的 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()
先后调用 foo
和 bar
各 一次,并通过 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 应用忙碌起来,我们可以使用 ab
或 weighttp
这样的压测工具,向 URI http://localhost:8080/t
施加请求压力,或者使用 OpenResty XRay 产品自带的负载生成器。无论使用何种方式,当目标 OpenResty 应用的 nginx 工作进程保持活跃时,我们可以在 OpenResty XRay 的 Web 控制台里得到类似下面这张 Lua 级别的 CPU 火焰图:
我们从图上可以观察到下列现象:
content_by_lua(nginx.conf:24)
。这符合预期。图中主要显示了两个代码路径,分别是
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
函数帧。这也不出所料。
bar
函数的代码路径的宽度,是右侧涉及 foo
的代码路径宽度的一半。换言之,这两个代码路径在图中 x 轴上的宽度比为 1:2,即 bar
代码路径占用的 CPU 时间,只有 foo
代码路径的50%。将鼠标移动到图中的 test.lua:bar
帧(即方框)上,我们可以看到它占据总样本量(即总 CPU 时间)的 33.3%,而 test.lua:foo
所占的比例为66.7%. 显然,与我们之前的预测相比较,这个火焰图提供的比例数字非常精确,尽管它所采取的是采样和统计分析的方法。ngx.say()
等其他代码路径,毕竟它们与那两个调用了 heavy()
的 Lua 代码路径相比,所占用的 CPU 时间微乎其微。在火焰图中,那些微不足道的代码路径本就是小噪音,不会引起我们的关注。我们可以始终专注于那些真正重要的部分,而不会为其他东西分心。那两条热代码路径(即调用栈轨迹)的顶部帧是完全相同的,都是 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 代码通常能够得到最完美的的调用栈轨迹和火焰图样本。如果我们的采样工具能够正确处理 JIT 即时编译后的 Lua 代码,那么在分析解释的 Lua 代码时,效果只会更好。LuaJIT 既有一个 JIT 编译器,又同时有一个解释器。它的解释器的有趣之处在于,几乎完全是用手工编写的汇编代码实现的(当然,LuaJIT 引入了自己的一种汇编语言记法,叫做 DynASM)。
对于我们一直在使用的那个 Lua 样例程序,我们需要在此做少许修改,即在 server {}
配置块中添加下面的 nginx.conf
配置片段:
init_by_lua_block {
jit.off()
}
然后重新加载(reload)或重启服务器进程,并保持流量负载。
这回我们得到了下面这张 Lua 级别 CPU 火焰图:
这张新图与前一张图在以下方面都极其相似:
bar
代码路径和 foo
代码路径。bar
代码路径依旧占用了总 CPU 时间的三分之一左右,而 foo
占用了余下的所有部分(即大约三分之二)。content_by_lua
那一帧。然而,这张图与前图相比仍然有一个重要的区别:代码路径的顶帧不再是 "trace" 伪帧了。这个变化也是预期的,因为这一回没有 JIT 编译过的 Lua 代码路径了,于是代码路径的顶部或顶帧变成为 lj_BC_IFORL
和 lj_BC_ADDVV
等函数帧。而这些被 C:
前缀标记出来的 C 函数帧其实也并非 C 语言函数,而是属于汇编代码帧,对应于实现各个 LuaJIT 字节码的汇编例程,它们被标记成了 lj_BC_IFORL
等符号。自然地,lj_BC_IFORL
用于实现 LuaJIT 字节码指令 IFORL
,而 lj_BC_ADDVV
则用于字节码指令 ADDVV
。IFORL
用于解释执行 Lua代码中的 for
循环, 而 ADDVV
则用于算术加法。这些字节码的出现,都符合我们的 Lua 函数 heavy()
的实现方式。另外,我们还可以看到一些辅助的汇编例程,例如如 lj_meta_arith
和 lj_vm_foldarith
。
通过观察这些函数帧的比例数值,我们还得以一窥 CPU 时间在 LuaJIT 虚拟机和解释器内部的分布情况,为这个虚拟机和解释器本身的优化铺平道路。
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 工作进程上的时候,这种全进程组粒度的采样分析是非常实用的。
我们也可以从非常复杂的 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 老系统。
如前文所述,火焰图可以用于可视化任意一种系统资源或性能指标,而不仅限于 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. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:
我们提供了英文版 原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!
Meathill 赞了文章 · 2020-10-08
在 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 样例程序,并将使用 OpenResty XRay 分析真实的 nginx 进程,生成 Lua 级别的 CPU 火焰图,并验证图中显示的性能情况。我们将检查不同的案例,例如开启了
JIT 即时编译的 Lua 代码、禁用了 JIT 编译的 Lua 代码(即被解释执行),以及调用外部 C 库代码的 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()
先后调用 foo
和 bar
各 一次,并通过 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 应用忙碌起来,我们可以使用 ab
或 weighttp
这样的压测工具,向 URI http://localhost:8080/t
施加请求压力,或者使用 OpenResty XRay 产品自带的负载生成器。无论使用何种方式,当目标 OpenResty 应用的 nginx 工作进程保持活跃时,我们可以在 OpenResty XRay 的 Web 控制台里得到类似下面这张 Lua 级别的 CPU 火焰图:
我们从图上可以观察到下列现象:
content_by_lua(nginx.conf:24)
。这符合预期。图中主要显示了两个代码路径,分别是
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
函数帧。这也不出所料。
bar
函数的代码路径的宽度,是右侧涉及 foo
的代码路径宽度的一半。换言之,这两个代码路径在图中 x 轴上的宽度比为 1:2,即 bar
代码路径占用的 CPU 时间,只有 foo
代码路径的50%。将鼠标移动到图中的 test.lua:bar
帧(即方框)上,我们可以看到它占据总样本量(即总 CPU 时间)的 33.3%,而 test.lua:foo
所占的比例为66.7%. 显然,与我们之前的预测相比较,这个火焰图提供的比例数字非常精确,尽管它所采取的是采样和统计分析的方法。ngx.say()
等其他代码路径,毕竟它们与那两个调用了 heavy()
的 Lua 代码路径相比,所占用的 CPU 时间微乎其微。在火焰图中,那些微不足道的代码路径本就是小噪音,不会引起我们的关注。我们可以始终专注于那些真正重要的部分,而不会为其他东西分心。那两条热代码路径(即调用栈轨迹)的顶部帧是完全相同的,都是 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 代码通常能够得到最完美的的调用栈轨迹和火焰图样本。如果我们的采样工具能够正确处理 JIT 即时编译后的 Lua 代码,那么在分析解释的 Lua 代码时,效果只会更好。LuaJIT 既有一个 JIT 编译器,又同时有一个解释器。它的解释器的有趣之处在于,几乎完全是用手工编写的汇编代码实现的(当然,LuaJIT 引入了自己的一种汇编语言记法,叫做 DynASM)。
对于我们一直在使用的那个 Lua 样例程序,我们需要在此做少许修改,即在 server {}
配置块中添加下面的 nginx.conf
配置片段:
init_by_lua_block {
jit.off()
}
然后重新加载(reload)或重启服务器进程,并保持流量负载。
这回我们得到了下面这张 Lua 级别 CPU 火焰图:
这张新图与前一张图在以下方面都极其相似:
bar
代码路径和 foo
代码路径。bar
代码路径依旧占用了总 CPU 时间的三分之一左右,而 foo
占用了余下的所有部分(即大约三分之二)。content_by_lua
那一帧。然而,这张图与前图相比仍然有一个重要的区别:代码路径的顶帧不再是 "trace" 伪帧了。这个变化也是预期的,因为这一回没有 JIT 编译过的 Lua 代码路径了,于是代码路径的顶部或顶帧变成为 lj_BC_IFORL
和 lj_BC_ADDVV
等函数帧。而这些被 C:
前缀标记出来的 C 函数帧其实也并非 C 语言函数,而是属于汇编代码帧,对应于实现各个 LuaJIT 字节码的汇编例程,它们被标记成了 lj_BC_IFORL
等符号。自然地,lj_BC_IFORL
用于实现 LuaJIT 字节码指令 IFORL
,而 lj_BC_ADDVV
则用于字节码指令 ADDVV
。IFORL
用于解释执行 Lua代码中的 for
循环, 而 ADDVV
则用于算术加法。这些字节码的出现,都符合我们的 Lua 函数 heavy()
的实现方式。另外,我们还可以看到一些辅助的汇编例程,例如如 lj_meta_arith
和 lj_vm_foldarith
。
通过观察这些函数帧的比例数值,我们还得以一窥 CPU 时间在 LuaJIT 虚拟机和解释器内部的分布情况,为这个虚拟机和解释器本身的优化铺平道路。
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 工作进程上的时候,这种全进程组粒度的采样分析是非常实用的。
我们也可以从非常复杂的 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 老系统。
如前文所述,火焰图可以用于可视化任意一种系统资源或性能指标,而不仅限于 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 收藏 1 评论 0
推荐关注