首页 > 技术文章 > Nginx 内存占用高排查

fsckzy 2020-09-07 12:44 原文

背景

线上两台 OpenResty 占用内存过高,8c32G 的机器用了 28G 内存,总觉得不正常,使用简单的重启大法,并没什么用处,今天刚好排查一下。

free
[root@iZ1w4igf11Z conf]# free -g
             total       used       free     shared    buffers     cached
Mem:            31         26          5          0          0         24
-/+ buffers/cache:          1         29
Swap:            0          0          0
top -M(按内存占用排序)
top - 11:19:40 up 137 days, 23:46,  1 user,  load average: 0.03, 0.06, 0.07
Tasks: 180 total,   1 running, 179 sleeping,   0 stopped,   0 zombie
Cpu(s):  5.2%us,  0.9%sy,  0.0%ni, 93.1%id,  0.0%wa,  0.4%hi,  0.4%si,  0.0%st
Mem:  32877652k total, 27437576k used,  5440076k free,   343712k buffers
Swap:        0k total,        0k used,        0k free, 25292856k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                           
14706 root      20   0  124m  55m 3740 S  6.9  0.2   1:48.64 nginx                             
14704 root      20   0  124m  53m 3744 S  6.9  0.2   1:54.05 nginx                             
14711 root      20   0  119m  51m 3460 S  3.4  0.2   1:46.87 nginx                             
14705 root      20   0  119m  50m 3592 S  6.9  0.2   1:49.36 nginx                             
14709 root      20   0  119m  50m 3464 S 10.3  0.2   1:45.98 nginx                             
14707 root      20   0  119m  50m 3468 S  0.0  0.2   1:50.33 nginx                             
14710 root      20   0  119m  50m 3476 S  6.9  0.2   1:47.90 nginx                             
14708 root      20   0  119m  49m 3488 S  3.4  0.2   1:53.28 nginx                             
12494 root      20   0 93480  28m 2892 S  0.0  0.1   0:18.17 nginx                

排查过程

strace

因为使用 OpenResty 安装了几个第三方模块,怀疑是不是这些模块导致了内存泄露,于是用 strace -o /tmp/nginx.txt -p pid 跟踪进程,并没有看到什么有用的信息。

epoll_wait(50, {{EPOLLOUT, {u32=2908154745, u64=140160575925113}}}, 512, 3087) = 1
write(64, "\224|4\347\355\313\202w\\-\332\v\314\220u\262\353\257<wB\352\212o|\325\33nUH\211="..., 12264) = 12264
write(64, "\27\3\3@\30T\332Yu\254D,\227M\207\256V\361z\7\232G\1\244\16\204\16\212\342Pp\236"..., 16413) = 16413
write(64, "\27\3\3@\30T\332Yu\254D,\230Vcs\326\340\247\276y\207\220\30\223\365\313f[\243\253q"..., 16413) = 16413
write(64, "\27\3\3@\30T\332Yu\254D,\231\257d\"*\330.yz,q\237#\350\344\271\323w~\215"..., 16413) = 3870
write(64, "\267C9\303o\33\20{\357v\3202\325I\215\256\237D\302\256u\310\275\35\214\223'f\177\211\313\232"..., 12543) = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(50, {{EPOLLIN, {u32=2908139864, u64=140160575910232}}}, 512, 3067) = 1
read(37, "\27\3\3\2\373\0\0\0\0\0\0\0\1\307\36\241\350)E\233H\374\27\26>\330C\333Q\361v\315"..., 33093) = 768
read(37, 0x30abc73, 33093)              = -1 EAGAIN (Resource temporarily unavailable)
epoll_ctl(50, EPOLL_CTL_MOD, 37, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2908139864, u64=140160575910232}}) = 0
socket(PF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 31
ioctl(31, FIONBIO, [1])                 = 0
fcntl(31, F_SETFD, FD_CLOEXEC)          = 0
epoll_ctl(50, EPOLL_CTL_ADD, 31, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2908175329, u64=140160575945697}}) = 0
connect(31, {sa_family=AF_INET, sin_port=htons(10000), sin_addr=inet_addr("192.168.209.150")}, 16) = -1 EINPROGRESS (Operation now in progress)
epoll_wait(50, {{EPOLLOUT, {u32=2908139864, u64=140160575910232}}}, 512, 3039) = 1
epoll_wait(50, {{EPOLLOUT, {u32=2908175329, u64=140160575945697}}}, 512, 3038) = 1
getsockopt(31, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
writev(31, [{"POST /push/v6/t/bind HTTP/1.0\r\nH"..., 446}, {"expire_time=1599494399244&key=Lf"..., 344}], 2) = 790
epoll_wait(50, {{EPOLLOUT, {u32=2908154745, u64=140160575925113}}}, 512, 3038) = 1
write(64, "\267C9\303o\33\20{\357v\3202\325I\215\256\237D\302\256u\310\275\35\214\223'f\177\211\313\232"..., 12543) = 12543
write(64, "\27\3\3@\30T\332Yu\254D,\232fr\374\266\245Y\217\250\333$\264J\231\331\340\252\307-\331"..., 16413) = 16413
write(64, "\27\3\3@\30T\332Yu\254D,\2337\333\240\276\372\307c\302\374)\351\317Y\370\216\335\257\v*"..., 16413) = 16413
write(64, "\27\3\3@\30T\332Yu\254D,\234\276\30\253\235\270L\203\330\307\231F\370\316\177\250\245\211\251y"..., 16413) = 14471
write(64, "\33\220\234*\323\5\177\0342H .\366\322\336\10X(6-a\371\305\204\327\356\315m\370T\31-"..., 1942) = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(50, {{EPOLLIN|EPOLLOUT, {u32=2908175329, u64=140160575945697}}}, 512, 3029) = 1
recvfrom(31, "HTTP/1.1 200 OK\r\nServer: Apache-"..., 262144, 0, NULL, NULL) = 199
epoll_wait(50, {{EPOLLIN|EPOLLOUT|EPOLLRDHUP, {u32=2908175329, u64=140160575945697}}}, 512, 3017) = 1
readv(31, [{"UAA4GNADCBiQKBgQCiWydoXPuXeTu3IL"..., 261945}], 1) = 0
close(31)                               = 0
message

查看 /var/log/messages 无异常

Nginx buffer 配置

查看目前的 buffer,尝试调低

# grep buffer nginx.conf
    large_client_header_buffers 4 16k;
    client_body_buffer_size 128k;
    proxy_busy_buffers_size 256k;
    client_header_buffer_size 256k;
    proxy_buffer_size 256k;
    proxy_buffers 64 128k;
    access_log  logs/access.log access buffer=32k;
    gzip_buffers 16 64k;

修改为:

[root@iZ1rp1vunvZ conf]# grep buffer nginx.conf
    large_client_header_buffers 4 16k;
    client_body_buffer_size 64k;
    proxy_busy_buffers_size 64k;
    client_header_buffer_size 64k;
    proxy_buffer_size 64k;
    proxy_buffers 8 32k;
    access_log  logs/access.log access buffer=32k;
    gzip_buffers 32 4k;

reload 后,无变化。

进一步查看meminfo
[root@iZ1rp1vunvZ conf]# cat /proc/meminfo 
MemTotal:       32877652 kB
MemFree:         5626820 kB
Buffers:          477252 kB
Cached:         25028124 kB
SwapCached:            0 kB
Active:         18819564 kB
Inactive:        7086080 kB
Active(anon):     401336 kB
Inactive(anon):      856 kB
Active(file):   18418228 kB
Inactive(file):  7085224 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:             45564 kB
Writeback:             0 kB
AnonPages:        400660 kB
Mapped:            23288 kB
Shmem:              1828 kB
Slab:            1118776 kB
SReclaimable:    1071452 kB
SUnreclaim:        47324 kB
KernelStack:        2504 kB
PageTables:         7608 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    16438824 kB
Committed_AS:    1204760 kB
VmallocTotal:   34359738367 kB
VmallocUsed:       65224 kB
VmallocChunk:   34359637096 kB
HardwareCorrupted:     0 kB
AnonHugePages:    245760 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:        6144 kB
DirectMap2M:    33548288 kB

发现 Slab 占用挺多的,其中的 Slab,查看相关资料:
通常的说法是:内核数据结构缓存的大小,可以减少申请和释放内存带来的消耗
这里的说法太笼统了

详细的说法如下:
在 linux 内核中会有许多小对象,这些对象构造销毁十分频繁,比如 i-node,dentry。这么这些对象如果每次构建的时候就向内存要一个页,而其实际大小可能只有几个字节,这样就非常浪费,为了解决这个问题就引入了一种新的机制来处理在同一页框中如何分配小存储器区,这个机制可以减少申请和释放内存带来的消耗,这些小存储器区的内存称为 Slab

df -i 查看 inode,占用并不大

[root@iZ1rp1vunvZ conf]# df -ih
Filesystem     Inodes IUsed IFree IUse% Mounted on
/dev/xvda1       1.3M  150K  1.2M   12% /
tmpfs            4.0M     1  4.0M    1% /dev/shm
/dev/xvdb1       6.3M  101K  6.2M    2% /usr/local/openresty/nginx/html/image
陷入僵局

我这么多内存去哪了?

没办法了,只能去打扰大神了。

请教大神

首先查看进程到底占用了多少内存:

[root@iZ1rp1vunvZ conf]# ps aux | awk '{print $6/1024 " MB\t\t" $11"\t"$NF}' | sort -nr|head -10
50.8242 MB		nginx:	process
50.4219 MB		nginx:	process
50.3633 MB		nginx:	process
50.3477 MB		nginx:	process
50.2734 MB		nginx:	process
50.2305 MB		nginx:	process
50.2266 MB		nginx:	process
49.8516 MB		nginx:	process
28.7969 MB		nginx:	nginx
21.0898 MB		./node_exporter	./node_exporter

free -m发现了 cached

[root@iZ1rp1vunvZ conf]# free -g
             total       used       free     shared    buffers     cached
Mem:            31         25          5          0          0         23
-/+ buffers/cache:          1         29
Swap:            0          0          0

难道都变成 cached 了?尝试着清理

sync
echo 1 > /proc/sys/vm/drop_caches
echo 2 > /proc/sys/vm/drop_caches
echo 3 > /proc/sys/vm/drop_caches

我丢失的内存回来了。。。

原理

上面执行的清除原理如下:

  • sync:将所有未写的系统缓冲区写到磁盘中,包含已修改的 i-node、已延迟的块 I/O 和读写映射文件
  • echo 1 > /proc/sys/vm/drop_caches:清除page cache
  • echo 2 > /proc/sys/vm/drop_caches:清除回收 Slab分配器中的对象(包括目录项缓存和 inode 缓存)。Slab 分配器是内核中管理内存的一种机制,其中很多缓存数据实现都是用的 pagecache。
  • echo 3 > /proc/sys/vm/drop_caches:清除 pagecache 和 Slab分配器中的缓存对象。
    /proc/sys/vm/drop_caches 的值,默认为0

最后了解下两个概念 buff 和 cache

  • buff(Buffer Cache)是一种 I/O 缓存,用于内存和硬盘的缓冲,是 io设备的读写缓冲区。根据磁盘的读写设计的,把分散的写操作集中进行,减少磁盘碎片和硬盘的反复寻道,从而提高系统性能。
  • cache(Page Cache)是一种高速缓存,用于 CPU 和内存之间的缓冲 ,是文件系统的 cache。
    把读取过的数据保存起来,重新读取时若命中(找到需要的数据)就不要去读硬盘了,若没有命中就读硬盘。其中的数据会根据读取频率进行组织,把最频繁读取的内容放在最容易找到的位置,把不再读的内容不断往后排,直至从中删除。

它们都是占用内存。两者都是RAM中的数据。简单来说,buff是即将要被写入磁盘的,而cache是被从磁盘中读出来的。

目前进程正在实际被使用的内存的计算方式为used-buff/cache,通过释放buff/cache内存后,我们还可以使用的内存量free+buff/cache。通常我们在频繁存取文件后,会导致buff/cache的占用量增高。

总结

一开始认真的点看的话,直接清理就完事了。就不用花这么多时间,不过重温一下知识点也好。

推荐阅读