我们最近使用 OpenResty XRay 帮助一个销售 CDN 和流量网关服务的企业客户优化了他们的 OpenResty/Nginx 服务器的内存使用。这个客户在他们的 OpenResty/Nginx 配置文件中定义了许多虚拟服务器和 URI location。OpenResty XRay 在客户的生产环境中自动进行了大部分分析,基于分析结果给出的方案让 nginx
进程的内存占用减少了大约 30%。
和我们的 OpenResty Edge 的 nginx worker 进程相比显示, 进一步的优化将会继续减少约 90%。
OpenResty XRay 是一个动态追踪产品,它可以自动分析正在运行中的应用程序,以排除性能问题、行为问题和安全漏洞,并提供可行的建议。在底层实现上,OpenResty XRay 由我们的 Y 语言驱动,可以在不同环境下支持多种不同的运行时,如 Stap+, eBPF+, GDB 和 ODB。
挑战
这个 CDN 供应商使用一个超大的“nginx.conf”配置文件来为他们的 OpenResty 服务器中的近万个虚拟主机服务。每个 nginx
主进程在启动后占用了好几个 G 的内存,在一次或多次 HUP reload 后,内存几乎翻倍。从下面 OpenResty XRay 生成的图中可以看出,最大内存占用约为 4.60GB。
我们可以从 OpenResty XRay 的应用层面内存使用明细表中看到,Glibc 分配器占用了大部分常驻内存,有 4.55GB。
而 OpenResty XRay 发现 Nginx cycle pool 占用了大量的内存:
当 Nginx 加载配置文件时,我们都知道它为这个 cycle pool 内的配置数据分配了数据结构。虽然庞大到有 1.62GB,但远远小于上面提到的 4.60GB。
RAM 依然是昂贵和稀缺的硬件资源,特别是在 AWS 和 GCP 这样的公有云上。客户希望通过降级到内存较小的机器来节约成本。
分析
OpenResty XRay 对客户的在线进程进行了深入分析。它不需要客户的应用程序进行任何协作。
- 没有额外的插件、模块或库。
- 没有代码注入或补丁。
- 没有特殊的编译或启动选项。
- 甚至不需要重新启动应用程序进程。
分析完全是以“事后”的方式进行的。多亏了 Openresty XRay 采用的动态追踪技术。
太多的空闲区块
OpenResty XRay 用 Glibc 内存分配器的分析器自动对在线 nginx
进程进行采样。分析器生成了以下柱状图,显示了分配器管理的空闲块的大小是如何分布的。
Glibc 分配器通常不会立即释放空闲块给操作系统(OS)。它可能会保留一些空闲块,以加快后续的分配速度。但有意保留的通常很小,不可能到上 G 字节。这里我们看到空闲块的大小累计已经达到 2.3GB。因此,更常见的原因是内存碎片。
查看普通堆中的内存碎片问题
大多数小的内存分配通过 brk
Linux 系统调用,发生在“普通堆”中。这个堆就像一个线性的 “堆“,只能通过移动其”顶部"指针来增加或减少。在堆中间的所有空闲块不能被释放给操作系统。直到它们上面的所有块也变成空闲,它们才会被释放。
OpenResty XRay 的内存分析器可以帮助我们查看这种堆的状态。请看下面的堆图,它是在 nginx 主进程响应 HUP 信号加载新配置后的采样图。
我们可以看到,堆是向上增长的,也就是说,向高位内存地址增长。注意 brk top
指针,这是唯一可以移动的东西。绿色框属于 Nginx 的新 “cycle pool“,而粉色框属于旧 ”cycle pool”。一个有趣的现象是,Nginx 会保留旧的 cycle pool 或旧的配置数据,直到新的 cycle pool 被成功加载。这种行为是由于 Nginx 的保护机制,当新的配置加载失败时,会优雅地退回到旧的配置。不幸的是,正如我们在上面看到的,旧的配置数据的盒子(绿色)在新的数据(粉色)下面,因此只有当新的配置数据也被释放后,它们才能释放到操作系统。
事实上,在 Nginx 释放了旧的配置数据和旧的 cycle pool 后,它们原来的位置变成了空闲块,被卡在新的 cycle pool 的块下面。
这是一个教科书式的内存碎片化的例子。普通堆只能在顶部释放内存;因此,它比其他内存分配机制,如 mmap
系统调用,更容易受到内存碎片的影响。但是,mmap
会在这里拯救我们吗?不一定。
mmap 的世界
Glibc 分配器也可以通过 mmap
系统调用来分配内存。这些系统调用分配离散的内存块或内存段,这些内存块或内存段可能位于进程地址空间的几乎任何地址,并跨越任何数量的内存页。
这听起来是一个缓解上述内存碎片问题的好方法。但是当我们有意阻断普通堆的增长方式时,根据 OpenResty XRay 的分析器所产生的图表,类似程度的内存碎片仍然发生。
当应用程序(这里是 Nginx)请求分配较小内存块的时候,Glibc 倾向于分配相对较大的内存段,这里是 1MB。因此,内存碎片仍然会发生在这些 1MB 的 mmap 段内。如果一个小内存块仍在使用,那么整个内存段就不会被释放给操作系统。
在上图中,我们可以看到旧的 cycle pool 块(粉红色)和新的 cycle pool 块(绿色)仍然在许多 mmap 段中交错。
解决方案
我们为客户提出了几个解决方案。
简单方式
最简单的方式是直接解决内存碎片的问题。根据上面我们使用 OpenResty XRay 做的分析,我们应该做以下一个或多个变动。
- 避免在“普通堆”中分配 cycle pool 内存(即取消这种分配的
brk
系统调用)。 - 要求 Glibc 使用适当的 mmap 段内存大小(不要太大!)来满足 cycle pool 的内存分配请求。
- 将不同 cycle pool 的内存块干净地分离到不同的 mmap 段中。
我们为 OpenResty XRay 的付费客户提供详细的优化说明。因此根本就不需要编码。
更好的方式
是的,还有一个更好的方式。开源的 OpenResty 软件提供了 Lua APIs 和 Nginx 配置指令,以动态加载(和卸载)Lua 层面的新的配置数据,而不需要通过 Nginx 配置文件机制。这使得使用一个小的恒定大小的内存来处理更多的虚拟 server 和 location 的配置数据成为可能。同时,Nginx 服务器的启动和重新加载时间也大大缩短(从很多秒到几乎为零)。事实上,有了动态配置加载,HUP reload 操作本身变得非常罕见。这种方式的一个缺点是,这需要在我们用户侧进行一些额外的 Lua 编码。
我们的 OpenResty Edge 软件产品以 OpenResty 作者所设想的最佳方式实现了这种动态配置加载和卸载。它不需要用户进行任何编码。所以这也是一个容易的选项。
结果
这位客户决定先尝试简单的方式,结果在几次 HUP reload 后,总的内存占用减少了 30%。
仍然有一些剩余的片段值得进一步关注。但我们的客户已经很满意了。此外,上面提到的更好的方式可以节省超过 90% 的总内存占用(就像在我们的 OpenResty Edge 产品中一样):
关于作者
章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。
章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴, 是 “边缘计算“、”动态追踪 “和 “机器编程 “的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,并编写过 60 多个开源软件库。
关注我们
如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。
我们也在 B 站上也有 OpenResty 官方的视频分享空间,欢迎订阅。
同时欢迎扫码关注我们的微信公众号: