扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

Nginx |内存层面性能优化秘诀:Google TCMalloc替代glibc+火焰图精准定位瓶颈

4小时前
163
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是 WeiyiGeek,一名深耕安全运维开发(SecOpsDev)领域的技术从业者,致力于探索DevOps与安全的融合(DevSecOps),自动化运维工具开发与实践,企业网络安全防护,欢迎各位道友一起学习交流、一起进步 ,若此文对你有帮助,一定记得点个关注⭐与小红星❤️或加入到作者知识星球『 全栈工程师修炼指南』,转发收藏学习不迷路  。

1.2 内存层面优化

在 Nginx 中每连接分配8KB或数十KB内存空间,但实际使用率低,导致并发连接数受限,另有,由于受到 NUMA 架构影响,Node 访问远端内存时延迟升高,从而降低整体内存访问速度。

此外,进程在不同CPU核心间频繁迁移,也会导致内存的 L1/L2/L3 缓存命中率下降,需绑定进程至固定CPU以提升缓存利用率,所以在 Nginx 配置中,可通过 worker_cpu_affinity 等指令绑定进程至固定CPU核心, 从而提升缓存命中率。

另外,也将相关常用数据连接通过内存进行缓存,以减少磁盘I/O,提升性能。

温馨提示:若文章代码块中存在乱码或不能复制,请联系作者,也可通过文末的阅读原文链接,加入知识星球中阅读,原文链接:https://articles.zsxq.com/id_i1qqni3rce3f.html

优化指南

TLS/SSL 握手性能优化指令

当 Nginx 配置了TLS/SSL协议,则在握手阶段会产生额外的CPU带宽消耗,可通过 Nginx 提供的SSL会话缓存(session cache)指令 ssl_session_cache 开启会话缓存,减少握手消耗,指令语法如下:

# 配置SSL会话缓存(session cache)
Syntax:	ssl_session_cache off | none | [builtin[:size]] [shared:name:size];
Default: ssl_session_cache none;
Context: http, server

# 参数说明
off:禁用且向客户端明示
none: 禁用但不通知客户端
builtin: 使用 OpenSSL 内存级缓存,仅同 worker 进程复用, 非 Nginx 提供的共享内存
shared:name:size: 基于共享内存的全局缓存,定义命名共享内存区域以及缓存大小,1MB缓存约支持4000个会话,通常设置为 10M (常用)

# 示例
ssl_session_cache builtin:1000 shared:SSL:10m;

另外一种方式便是使用 TLS/SSL 中的会话票证(session tickets),其机制是服务端将会话密钥加密后生成 tickets 发送至客户端;客户端后续握手携带 tickets,服务端解密成功即复用会话,跳过非对称加密。主要适用于 NGINX 集群环境,使得在多台 Nginx 中可以反复的使用其会话票据,依赖统一密钥(通过 ssl_session_ticket_key 配置),但存在安全风险:需定期轮换密钥,否则长期密钥泄露将导致历史流量被解密。

# 配置是否开启会话票据(session tickets)
Syntax:	ssl_session_tickets on | off;
Default: ssl_session_tickets on;
Context: http, server

# 配置会话票据密钥
Syntax:	ssl_session_ticket_key file;
Default:	—
Context:	http, server

# 例如:使用 openss 命令创建一个文件包含80或48字节的随机数据,其中包含用于加密和解密TLS会话票证的密钥
openssl rand -out ticket.key 80

使用 Google TCMalloc 优化内存分配及性能分析

Linux 中默认 glibc malloc (GDB malloc,通常作为一个单独的 PTMalloc2 包提供) 内存分配能力不足,成为 Nginx 极致性能的瓶颈(某些特殊场景),因此可以使用 Google 的 TCMalloc (Thread-Caching Malloc) 替代,其特点是减少内存碎片、擅长小内存管理、减少多线程间锁竞争提升分配效率,尤其在高并发场景下表现突出。

TCMalloc为每个线程分配一个线程本地缓存, 小的分配由线程本地缓存满足,对象根据需要从中央数据结构移动到线程本地缓存中,并且定期的垃圾收集用于将内存从线程本地缓存迁移回中央数据结构。

TCMalloc 处理大小<= 32K的对象(“小”对象)与处理较大对象的方式不同。大型对象使用页面级分配器直接从中央堆分配(页面是4K对齐的内存区域)。即,大对象总是页面对齐的并且占据整数个页面, 一系列的页面可以被分割成一系列的小对象,每个对象的大小相等。例如,一个页面(4K)的运行可以划分为32个大小为128字节的对象。

weiyigeek.top-TCMalloc overview图

由 TCMalloc 页面可查看到其与PTMalloc2 单元性能测试数据与分析结果。

测试环境与对象:

RedHat 9 Linux glibc-2.3.2
超线程的 2.4GHz 双Xeon系统

首先,对于不同数量的线程 TCMalloc 与 (Glic) PTMalloc 2,每秒运行的总操作数(百万)与最大分配大小,结果如下图所示。

    • 单线程性能对比:从128B起性能明显提升,峰值约提升1倍,32KB后提升趋缓。

纵轴: 分配/释放对象大小(64B → 128KB)

横轴:mops/cpu second 指 CPU 在一秒内能够完成的百万次操作数量,它是一个衡量处理器综合处理能力的指标,涵盖了计算、寻址、数据传输等多种类型的操作。其中虚线(PT malloc)为 glibc 默认分配器,实线(TC malloc)为 Google 提供的 TCMalloc。

    • 多线程扩展性对比: 在线程数递增(1→3→4→5→8→12→16→20)的情况下,小对象(<2KB)场景下,TCMalloc 优势随线程数增加急剧扩大,在 20 线程时小块内存分配吞吐量差距极为显著。

weiyigeek.top-不同数量的线程性能对比图

还可从另外一个维度,针对固定大小的字节数,我们做分配释放内存的时候,我们采用不同的线程数,例如:对于64节的时候这种小块内存,我们只要增加线程并发数,那么它的性能一定是比 Glibc 越来越好的。

纵轴: 线程数 0→20。 横轴: CPU 在一秒内能够完成的百万次操作数量

    • 64B小块:线程数增加,TCMalloc性能持续远超glibc;
    • 1KB:仍有明显优势;• 4KB:优势减弱;• ≥32KB:单线程基本持平,多线程仍保持可观差距;
    • 64KB/128KB:TCMalloc 仍具性能提升,整体值得采用;

weiyigeek.top-不同固定大小的字节数性对比图

想要了解更多 TCMalloc 介绍说明的小伙伴们,可访问 Google TCMalloc 官方文档 或 https://cloud.tencent.com.cn/developer/article/2064566 查看学习。

先在问题来了?如何获取 TCMalloc 库以及如何集成在 Nginx 呢?

步骤 01,访问 Github 中 google/gperftools 仓库,从 README 文档中可知,如果要使用 TCmalloc,只需通过 -ltcmalloc 链接器标志将tcmalloc链接到您的应用程序,或者使用 $ LD_PRELOAD="/usr/lib/libtcmalloc.so"  方式(不推荐),另外,如果您更愿意链接到不包含堆分析器和检查器的TCMalloc版本(可能是为了减少静态二进制文件的二进制文件大小),则可以链接到 libtcmalloc_minimal

Just linkin -ltcmalloc or -ltcmalloc_minimal to get the advantages of
tcmalloc -- a replacement for malloc and new.  See below for some
environment variables you can use with tcmalloc, as well.

tcmalloc functionality is available on all systems we've tested; see
INSTALL for more details.  See README_windows.txt for instructions on
using tcmalloc on Windows.

步骤 02,进入 Releases ,下载最新的  gperftools 源码包,编译安装 TCMalloc 动态/静态库,温馨提示作者在 OpenEuler 24.04 环境下实践的。

# 方式1
wget /https://github.com/gperftools/gperftools/releases/download/gperftools-2.18.1/gperftools-2.18.1.tar.gz
tar -zxvf gperftools-2.18.1.tar.gz -C /usr/local/src
cd /usr/local/src/gperftools-2.18.1
./configure --prefix=/usr/local/gperftools/tcmalloc
make -j$(nproc) && make install

# 方式2
# 支持 autoconf 以及 cmake的构建方式,所以采用 cmake 来构建
# wget https://down.wygk.eu.org/https://github.com/gperftools/gperftools/archive/refs/tags/gperftools-2.18.1.tar.gz
# tar -zxvf gperftools-2.18.1.tar.gz -C /usr/local/src
# cd /usr/local/src/gperftools-gperftools-2.18.1
# mkdir build && cd build
# cmake -DCMAKE_INSTALL_PREFIX=/usr/local/bin/ ..
# make -j$(nproc) && make install

weiyigeek.top-编译安装 gperftools 图

步骤 03.查看编译安装产物,找到 libtcmalloc.so 动态库文件路径,至此我们得到了 tcmalloc 库

$ find /usr/local/gperftools/tcmalloc -name libtcmalloc.*
/usr/local/gperftools/tcmalloc/lib/libtcmalloc.so
/usr/local/gperftools/tcmalloc/lib/libtcmalloc.so.4.6.5
/usr/local/gperftools/tcmalloc/lib/libtcmalloc.la
/usr/local/gperftools/tcmalloc/lib/libtcmalloc.so.4
/usr/local/gperftools/tcmalloc/lib/libtcmalloc.a
/usr/local/gperftools/tcmalloc/lib/pkgconfig/libtcmalloc.pc

步骤 04.为了让运行时的程序能找到这个库,需要将库路径添加到 /etc/ld.so.conf.d/ 中,并刷新缓存,如果看到如下图中所示的 libtcmalloc.so.4 的路径信息,说明添加成功。

# 1. 创建配置文件(如果文件不存在)
echo"/usr/local/gperftools/tcmalloc/lib" | sudotee /etc/ld.so.conf.d/gperftools.conf

# 2. 刷新动态链接库缓存
sudo ldconfig

# 3. 验证是否生效
ldconfig -p | grep tcmalloc

weiyigeek.top-将 gperftools 相关lib库加入到系统图

步骤 04.编译 Nginx 源码加入编译 --with-ld-opt="-L/usr/local/gperftools/tcmalloc/lib -ltcmalloc" --with-google_perftools_module 选项参数,集成 TCMalloc 库。

./configure --prefix=/usr/local/nginx --user=nginx --group=nginx --sbin-path=/usr/sbin/nginx --conf-path=/usr/local/nginx/conf/nginx.conf --pid-path=/usr/local/nginx/nginx.pid --error-log-path=/var/log/nginx/logs/error.log --http-log-path=/var/log/nginx/logs/access.log --lock-path=/var/run/nginx.lock --modules-path=/usr/local/nginx/modules --with-http_stub_status_module --with-http_realip_module --with-http_v2_module --with-http_ssl_module --with-http_slice_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt=-O2 --with-compat --with-debug --with-ld-opt="-L/usr/local/gperftools/tcmalloc/lib -ltcmalloc" --with-google_perftools_module
make -j$(nproc) && make install

特别注意:在 C 语言生成可执行文件的两个步骤编译与链接,此外必须显式指定链接TCMalloc库,若缺失则TCMalloc完全不生效,若 TCMalloc安装路径非标准,需用-L指定其lib目录位置,例如:--with-ld-opt="-L/usr/local/lib -ltcmalloc"

• 编译阶段:通过 --with-cc-opt 传递编译器选项;

• 链接阶段:通过 --with-ld-opt 传递链接器选项。

至此,Nginx 二进制文件将动态链接 libtcmalloc.so.4 ,使之接入 TCmalloc 内存分配优化工具,运行时所有 malloc/free 调用由 TCMalloc 接管,实现高并发小内存极致性能。

步骤 05.使用 Google Perftools 工具对 Nginx Worker 进程进行性能分析,前面我们已经在编译 Nginx 时已经集成了 ngx_google_perftools_module 模块,它以 profile 方式分析程序中消耗执行时间最多的函数,在生产环境中需针对高耗时函数及其所属功能模块进行针对性优化。

语法参数:

    • google_perftools_profiles :设置一个文件名,该文件用于保存 NGINX worker 进程的性能分析信息,worker 进程的 ID 始终是文件名的一部分,并附加在文件名的末尾,在点号之后。
Syntax:	google_perftools_profiles path;
Default:	—
Context:	main

# 示例
vim nginx.conf
main {
...
google_perftools_profiles /tmp/nginx_tcmalloc;
...
}

# 结果:/tmp/nginx_tcmalloc.<worker_pid>

weiyigeek.top-启用google_perftools_profiles图

温馨提示:生成的 profile 文件,需要借助 pprof 工具进行分析。

什么是 pprof 工具?

pprof 是 Google 提供的一个profiling功能用于可视化和程序性能分析数据的工具,它可以帮助开发者理解程序的性能瓶颈和内存使用情况。通过 pprof,你可以生成各种视图(如文本、图形等)来展示程序中的函数调用关系和时间消耗, 可快速定位应用程序中的性能瓶颈,不仅限于 Nginx,还可用于其他语言编写的应用程序。

特别注意:由于 gperftools 最新的源码中不再提供 pprof 工具,网上大多数教程都在通过 gperftools 2.7 源码来编译,放在当下已经过时了,所以需要通过其他方式安装 pprof 工具,例如操作系统包管理器安装 pprof ,以及 go 安装 pprof,然后使用 pprof 分析生成的 profile 文件。

# 由于 pprof 图形化展示依赖于 graphviz 和 libunwind 库(源码),所以需要安装这两个依赖包。
yum install go libunwind graphviz
# https://github.com/libunwind/libunwind

# 安装 pprof 工具
$ go install github.com/google/pprof@latest

# 查看 pprof 工具安装路径
$ echo$GOPATH
/root/go/
$ ls$GOPATH/bin/
pprof

# 创建软连接
ln -s $GOPATH/bin/pprof /usr/local/bin/

# 查看帮助文档
$ pprof --help
usage:
# 以指定格式生成输出。
# format可以是以下之一:--text 文本 / --pdf 图形
  pprof <format> [options] [binary] <source> ...

# 使用其命令的交互式shell,生成配置文件的各种视图
  pprof [options] [binary] <source> ...

# 提供“-http”标志,以在指定的host:port处获得交互式web界面,该界面可用于浏览配置文件的各种视图。
  pprof -http [host]:[port] [options] [binary] <source> ...

步骤 06.配置验证完成后,重新启动 Nginx 服务,将会生成多个 nginx_tcmalloc.work 进程id 文件,此时文件大小为 0 Byte,在访问 nginx 站点页面后,例如之前文章中测速 http2 主动推送的站点示例 https://server.weiyigeek.top/video.html,然后再重启 Nginx 服务,此时文件将不再时 0 Byte,然后使用 pprof 工具在 OpenResty(Nginx)中分析函数执行时间的 profiling 能力,依据函数耗时数值判定优化优先级,并验证模块使用正确性。

$ nginx -s stop
$ nginx 
$ find /tmp/ -name "nginx_tcmalloc.*" -size +1k -execls -alh {} ;
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056558
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056559
-rw-rw-rw- 1 nginx nginx 8.4K Mar 17 12:11 /tmp/nginx_tcmalloc.4056560
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056561
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056562
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056563
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056564
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056565
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056566
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056567
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056568
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056569
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056570
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056572
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056571
-rw-rw-rw- 1 nginx nginx 7.6K Mar 17 12:10 /tmp/nginx_tcmalloc.4056573

# 使用 pprof 以文本格式分析一个最大的 profile 文件
pprof --text /tmp/nginx_tcmalloc.4056560
File: nginx
Type: cpu
# 采样总量非常小(仅 40ms),这意味着这可能是一个极短时间的采样或者程序压力并不大。
Showing nodes accounting for 40ms, 100% of 40ms total
      flat  flat%   sum%        cum   cum%
      20ms 50.00% 50.00%       20ms 50.00%  write
      10ms 25.00% 75.00%       10ms 25.00%  CRYPTO_gcm128_release
      10ms 25.00%   100%       10ms 25.00%  [libcrypto.so.3.0.12]
         0     0%   100%       10ms 25.00%  CRYPTO_gcm128_encrypt_ctr32
         0     0%   100%       10ms 25.00%  EVP_EncryptUpdate
         0     0%   100%       20ms 50.00%  SSL_rstate_string
         0     0%   100%       20ms 50.00%  SSL_write
         0     0%   100%       10ms 25.00%  X509_get0_reject_objects
         0     0%   100%       10ms 25.00%  [libssl.so.3.0.12]
         0     0%   100%       40ms   100%  __libc_init_first
         0     0%   100%       40ms   100%  __libc_start_main
         0     0%   100%       40ms   100%  _start
         0     0%   100%       40ms   100%  main
         0     0%   100%       40ms   100%  ngx_event_process_posted
         0     0%   100%       40ms   100%  ngx_http_copy_filter
         0     0%   100%       40ms   100%  ngx_http_output_filter
         0     0%   100%       40ms   100%  ngx_http_request_handler
         0     0%   100%       40ms   100%  ngx_http_v2_filter_send (inline)
         0     0%   100%       40ms   100%  ngx_http_v2_send_chain
         0     0%   100%       40ms   100%  ngx_http_v2_send_output_queue
         0     0%   100%       40ms   100%  ngx_http_write_filter
         0     0%   100%       40ms   100%  ngx_http_writer
         0     0%   100%       20ms 50.00%  ngx_log_error_core
         0     0%   100%       40ms   100%  ngx_master_process_cycle
         0     0%   100%       40ms   100%  ngx_output_chain
         0     0%   100%       40ms   100%  ngx_spawn_process
         0     0%   100%       40ms   100%  ngx_ssl_send_chain
         0     0%   100%       30ms 75.00%  ngx_ssl_write (inline)
         0     0%   100%       30ms 75.00%  ngx_ssl_write.part.0
         0     0%   100%       40ms   100%  ngx_start_worker_processes
         0     0%   100%       40ms   100%  ngx_worker_process_cycle
         0     0%   100%       20ms 50.00%  ngx_write_fd (inline)

# 指标含义
flat(Flat Time) : 函数自身执行消耗的时间(不包括它调用的子函数)。
flat%(Flat Percent) : 该函数自身耗时占总采样时间(例如:40ms)的百分比。
sum%(Sum Percent) : 当前行及上方所有行 flat% 的累加值。用来快速查看前几名消耗了多少比例。
cum(Cumulative Time) : 累积耗时。该函数自身耗时 + 它调用的所有子函数耗时的总和。
cum%(Cumulative Percent) : 累积耗时占总时间的百分比。
founction name : 函数名,如果是内联函数(inline),则会显示在括号中。

weiyigeek.top-Nginx进程采样分析图

步骤 07.之前的 40ms 采样时间太短,无法代表真实负载,现在通过压力测试工具(如 wrk 或 ab)进行压测 60 秒,并使用 pprof 生成在线火焰图查看热点函数执行调用情况,如下所示:

# 压力测试
# 400 并发连接,持续 60s 压力测试,12个线程,平均每个线程处理 约33-34个连接。
$ wrk -t12 -c400 -d60s https://server.weiyigeek.top/video.html
Running 1m test @ https://server.weiyigeek.top/video.html
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    65.41ms   42.19ms 618.03ms   73.91%
    Req/Sec   523.55    140.16     1.69k    60.85%
  373582 requests in 1.00m, 441.78MB read
Requests/sec:   6216.72
Transfer/sec:      7.35MB

# 重新加载 Nginx 配置,将分析结果写入文件
$ nginx -s reload

# 查看生成的 profile 采样文件
find /tmp/ -name "nginx_tcmalloc.*" -size +100k -execls -alh {} ;
-rw-rw-rw- 1 nginx nginx 148K Mar 17 13:23 /tmp/nginx_tcmalloc.4065265
-rw-rw-rw- 1 nginx nginx 184K Mar 17 13:23 /tmp/nginx_tcmalloc.4065266
....

# 直接生成交互式 Web 页面(推荐)
pprof -http=10.20.172.214:8008 /tmp/nginx_tcmalloc.4065266

# 或者生成pdf文件
pprof --pdf -output nginx_tcmalloc.pdf /tmp/nginx_tcmalloc.4065266
Generating report in nginx_tcmalloc.pdf

weiyigeek.top-flamegraph火焰图

还可选择 view 点击 graph 查看到具体的调用链信息,亦可分析 Nginx 模块函数的加载顺序,加深对 Nginx 工作流程有更深的理解。

weiyigeek.top-graph调用图

总结:本小节主要介绍 TCMalloc 相关知识,其优于缺省的 glibc 内存分配器,在海量高并发请求中频繁分配释放小内存(如HTTP请求头、短生命周期 buffer)时,提供远超 glibc 的吞吐量与稳定性,然后手动编译 perftools 源码生成 TCMalloc 动态和静态链接库,并将 TCMalloc 添加编译进行 Nginx 中以进行内存分配,最后通过  ngx_google_perftools_module 模块导出对 Nginx Worker 进程进行性能分析文件,之后使用 pprof 工具进行分析,最后通过火焰图直观展示热点函数执行时间,以助于可快速定位 OpenResty(Nginx)中的性能瓶颈。

最后有需要新手朋友们,可加入到作者知识星球中,完整系统的学习 Nginx 高性能Web服务器从入门到生产时间,后续也将持续更新 OpenResty 系列文章,感谢大家支持。

加入:作者【全栈工程师修炼指南】知识星球

相关推荐