如上篇提到,在 websocket 这种长驻应用里,我们需要特别小心 nginx 的内存使用
我们希望做到这两个点:
- 每条链接占用的内存能尽量少
- 随着时间的增长,内存不会持续增长
接下来,很自然的,我们想知道内存都去哪里了,具体是在哪一步,是谁,因为什么申请内存
带着这个问题,我们先大概的看一下,可能吃掉内存的几个角色
1. 系统内核
我们知道 tcp 的协议栈是在内核态实现的,socket 也被抽象为一个文件供应用层使用;
应用层的 socket.send 只是把数据写入内核缓冲区,socket.receive 只是从内核缓冲区读取数据
内核态为了维护 tcp 协议栈用的有限状态机,以及读写 buffer,自然是需要消耗内存的
嗯,这是个大黑盒,我就知道这么一点,哈哈
2. nginx
nginx 对内存的处理,非常有场景的特色
在 nginx 的世界里,每个请求从一开始,就会创建一个 `自增长` 的内存池 request_pool ,需要内存则向内存池申请,如果剩余空间不够,内存池会向操作系统申请一大段内存,实际上内存池是一串的大段内存空间,每段内存空间都是 request_pool_size 这个配置项,默认是 4k
如果申请大于 request_pool_size 的内存(实际更小一点),内存池会向系统申请一段 large_pool,大小就是申请的内存
申请内存统一走这个内存池,那么释放呢,很不幸,要等到请求结束的时候,才会一起释放(当然,large_pool 还是有中途主动释放的机会)
在 http 场景下,这个设计会很高效,极大程度的避免了内存碎片问题,也减轻了对内存管理的负担,但是对于长驻服务应用来说,确是需要特别小心谨慎的点
3. lua
ngx_lua 是将 lua 嵌入到 nginx,虽然 nginx 和 lua 在同一个进程空间,但是两者拥有独立的内存管理机制
nginx 用自己的内存池机制来管理内存的申请和释放,lua 则由 vm 来管理,需要内存由 vm 来申请,通过引用计数的方式来实现垃圾回收
lua 的 gc 是随时会发生的,并不会与请求的生命周期绑定,比如有些不再引被用的变量,可以在请求中途被 gc 掉
当然了,实际上lua里的很多都是跟请求绑定的,请求退出的时候,跟请求相关的很多都会被干掉,然后被 gc 掉
背景就先简单介绍到这里,网上有很多关于这方面更深入的分享,我们进入本篇的主题:
首先要进入本篇的主角神器:systemtap, stapxx,nginx-gdb-utils
我只想谈一点,要用这些工具分析,首先一个前提点,我们需要了解被分析的目标
简单说呢,就是要分析 nginx,那么看 nginx 源码是必须的,虽然不必全看懂,但是关心的点必须得知道嘛
哈哈,那叫一个 feel 倍爽,效率杠杠滴
下面我只是介绍我使用这些神器的具体过程,重点是过程,道理嘛,哈哈,暂时我也说不清楚
1. 系统内核
很抱歉,这个家伙我几乎一篇空白,最后只是用减法算出来,每条链接他吃了 7k 内存的样子
后续有时间,我会深挖一挖这个家伙,优化空间我相信是有的(对比一些国际友人的分享),哈哈
2. nginx
本篇重点也就是分析他了,因为 ngx_lua 里有不少操作也会从 nginx request_pool 申请内存
首先我们分析一下代码,nginx 的内存申请最终都会落入到这个单一入口:ngx_palloc [1]
那么我们很自然的想到,最好在每次调用 ngx_palloc 的时候,打印当前的调用栈,这个脚本很快可以出来 [2] (哈哈,模仿现有的嘛)
我们往测试环境发起来一条请求,得到如下的记录:
从这里我们可以清晰的看到,除了很多地方申请小内存,还有几个点值得关注:
1> request_pool_size 默认值是 4k,内存池每次不够了会再申请 4k
在我们这个场景里,调整到 1k 应该是合理的
2> 有两处地方也申请了 4k,但是进入了 large_pool
分析当前的调用栈,在结合源码,我们可以知道是 cosocket 在 receive 的时候会申请一段 chain buf,大小也是可配置的,lua_socket_buffer_size 调整的 1k 也够用了
为什么会两处呢,因为我们有全双工,一个请求有两个 ngx.thread,每个 thread 内的 chain buf 是可以重复使用的
3> 从 timestamp: 1439994118 开始,每隔三秒会有两次内存申请
这段很有规律的增长,是最头疼的,毕竟长驻服务嘛
从调用栈可以看到,这两个都是 cosocket:connect 的时候申请的,我们在仔细分析下代码,如果要全部改为从 lua space 申请内存,改动量比较大,所以有了这个简单地 patch,哈哈,针对我这种场景已经解决大部分问题了
-- 事实证明这是错误的,新的方案正在赶制中
然后我们再来分析一次,单链接 nginx request_pool 大小为 8.4k,而且没有增加,嗯,目前基本可用啦,哈哈
3. lua
lua的内存占用,我们可以不必想 nginx 那样担心,毕竟是有动态 gc 的嘛,只要不犯一些低级的错误,内存还是比较好控制的
但是,我们还是很好奇,到底它用了多少呢,哈哈,请出我们的另一个神器 nginx-gdb-utils
需要 python 7.6+ 和 python2.7+
我们看看在单 worker 12w 长链的场景下:
-- 目前我也就是先看看,分析尚未深入...
(gdb) lgc
The current memory size (allocated by GC): 512477992 bytes
(gdb) lgcstat
804634 str objects: max=1659, avg = 43, min=18, sum=34622842
387 upval objects: max=24, avg = 24, min=24, sum=9288
240643 thread objects: max=1792, avg = 700, min=424, sum=168670488
134 proto objects: max=2579, avg = 361, min=74, sum=48482
360791 func objects: max=144, avg = 28, min=20, sum=10104256
124 trace objects: max=1828, avg = 1164, min=160, sum=144360
38212 cdata objects: max=16, avg = 15, min=12, sum=611388
1683372 tab objects: max=2097192, avg = 100, min=32, sum=169378696
296854 udata objects: max=1855, avg = 420, min=32, sum=124679600
sizeof strhash 4194304
sizeof g->tmpbuf 512
sizeof ctype_state 2520
sizeof jit_state 6032
total sz 512477992
g->strnum 804634, g->gc.total 512477992
elapsed: 529.590000 sec
(gdb) lgcpath 10000 tab
path 000:[registry] ->Tab["_LOADED"] ->Tab["ffi"] ->Tab["gc"] ->cfunc ->env ->Tab sz:196640 (GCobj*)0x40908a68 ->END
path 001:[registry] ->Tab[tv=0x405d8198] ->Tab sz:2097192 (GCobj*)0x405d1268 ->END
(gdb) lval (GCtab*)0x40908a68
table (GCtab*)0x40908a68 (narr=0, nrec=8191):
key:
type cdata
cdata object: (GCcdata*)0x43aada90
cdata value pointer: (void*)0x43aada98
ctype object: (CType*)0x4091b710
ctype size: 8 byte(s)
ctype type: struct
value:
function lock.lua:55: (GCfunc*)0x40908a00
key:
type cdata
cdata object: (GCcdata*)0x5c3d06c8
cdata value pointer: (void*)0x5c3d06d0
ctype object: (CType*)0x4091b710
ctype size: 8 byte(s)
ctype type: struct
value:
function lock.lua:55: (GCfunc*)0x40908a00
key:
type cdata
cdata object: (GCcdata*)0x54fae2d0
cdata value pointer: (void*)0x54fae2d8
ctype object: (CType*)0x4091b710
ctype size: 8 byte(s)
ctype type: struct
value:
function lock.lua:55: (GCfunc*)0x40908a00
key:
type cdata
cdata object: (GCcdata*)0x5db73e28
cdata value pointer: (void*)0x5db73e30
ctype object: (CType*)0x4091b710
ctype size: 8 byte(s)
(gdb) lval (GCtab*)0x405d1268
table (GCtab*)0x405d1268 (narr=262145, nrec=0):
[0] =
number 141679
[1] =
thread: (lua_State*)0x41558c90
[2] =
thread: (lua_State*)0x40913580
[3] =
thread: (lua_State*)0x412d8f10
[4] =
thread: (lua_State*)0x41ef0218
[5] =
thread: (lua_State*)0x41558e68
[6] =
thread: (lua_State*)0x40fa4628
[7] =
thread: (lua_State*)0x4154e010
[8] =
thread: (lua_State*)0x4154f5d0
[9] =
thread: (lua_State*)0x4154f618
[10] =
thread: (lua_State*)0x404f3470
[11] =
thread: (lua_State*)0x404f3628
[12] =
thread: (lua_State*)0x41fe86a8
[13] =
thread: (lua_State*)0x41fe8860
[14] =
thread: (lua_State*)0x41fe9e28
[15] =
thread: (lua_State*)0x41fe9e90