从DBA的角度系统学习一下内存管理
1前言
前天中午一名同事找到我说,业务反馈有大量的接口显式获取连接速度很慢(getConnection),虽说最后是一场乌龙,与数据库没有太大关联,是应用层面的锅,但是还是学到了很多内存方面的知识。借此乌龙,让我们再拎一下内存,剪不断理还乱。
2现象
最开始怀疑是数据库出了幺蛾子,但是经过一番检查并未发现什么异常,等待事件也大都集中在ClientRead,CPU使用率也不高,平均30%左右,日志里也未看到有什么状况。
没法,数据库暂时没有实质进展,于是我联系业务,让将Thread Dump信息转储出来,查看之后,发现了大量的 Wait on condition,表示正处于等待资源或等待某个条件的发生,具体的堆栈由于非专业人员,就不班门弄斧了。堆栈显示在等待某个资源和条件,那在等什么东西呢?莫非是网络带宽?但是主机组同事协查了一下,带宽也是充裕的。
于是我去瞅了眼OS日志,在这里发现了一点端倪,操作系统日志中一直在打印如下信息
Apr 4 12:15:25 xxxxxxxxxx kernel: kworker/u448:5: page allocation failure: order:6, mode:0x10c0d0
Apr 4 12:15:25 xxxxxxxxxx kernel: CPU: 63 PID: 140124 Comm: kworker/u448:5 Kdump: loaded Tainted: G OE ------------ 3.10.0-957.27.2.el7.x86_64 #1
Apr 4 12:15:25 xxxxxxxxxx kernel: Workqueue: mlx5_esw_wq esw_vport_change_handler [mlx5_core]
Apr 4 12:15:25 xxxxxxxxxx kernel: Call Trace:
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9ed64147>] dump_stack+0x19/0x1b
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e7bdec0>] warn_alloc_failed+0x110/0x180
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9ed5f74e>] __alloc_pages_slowpath+0x6b6/0x724
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e7c2524>] __alloc_pages_nodemask+0x404/0x420
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e80f438>] alloc_pages_current+0x98/0x110
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e7bcc6e>] __get_free_pages+0xe/0x40
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e81ab4e>] kmalloc_order_trace+0x2e/0xa0
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e80f438>] ? alloc_pages_current+0x98/0x110
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e81e721>] __kmalloc+0x211/0x230
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffffc063dfb0>] mlx5_query_nic_vport_mac_list+0x80/0x1f0 [mlx5_core]
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffffc0666d95>] esw_update_vport_addr_list+0x125/0x370 [mlx5_core]
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e81d5f6>] ? kfree+0x106/0x140
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffffc066736e>] esw_vport_change_handle_locked+0x38e/0x5e0 [mlx5_core]
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e634919>] ? sched_clock+0x9/0x10
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffffc06675f9>] esw_vport_change_handler+0x39/0x50 [mlx5_core]
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e6baf9f>] process_one_work+0x17f/0x440
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e6bc036>] worker_thread+0x126/0x3c0
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e6bbf10>] ? manage_workers.isra.25+0x2a0/0x2a0
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e6c2e81>] kthread+0xd1/0xe0
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e6c2db0>] ? insert_kthread_work+0x40/0x40
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9ed76c1d>] ret_from_fork_nospec_begin+0x7/0x21
Apr 4 12:15:25 xxxxxxxxxx kernel: [<ffffffff9e6c2db0>] ? insert_kthread_work+0x40/0x40
Apr 4 12:15:25 xxxxxxxxxx kernel: Mem-Info:
Apr 4 12:15:25 xxxxxxxxxx kernel: active_anon:25654925 inactive_anon:3142251 isolated_anon:0#012 active_file:49244040 inactive_file:44436012 isolated_file:0#012 unevictable:34160 dirty:106649 writeback:542 unstable:0#012 slab_reclaimable:3893591 slab_unreclaimable:360597#012 mapped:9900911 shmem:10924924 pagetables:731630 bounce:0#012 free:1822317 free_pcp:2918 free_cma:0
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 DMA free:15904kB min:12kB low:12kB high:16kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:15996kB managed:15904kB mlocked:0kB dirty:0kB writeback:0kB mapped:0kB shmem:0kB slab_reclaimable:0kB slab_unreclaimable:0kB kernel_stack:0kB pagetables:0kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? yes
Apr 4 12:15:25 xxxxxxxxxx kernel: lowmem_reserve[]: 0 1102 515104 515104
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 DMA32 free:1128720kB min:1120kB low:1400kB high:1680kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:1722192kB managed:1128728kB mlocked:0kB dirty:0kB writeback:0kB mapped:0kB shmem:0kB slab_reclaimable:0kB slab_unreclaimable:0kB kernel_stack:0kB pagetables:0kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? yes
Apr 4 12:15:25 xxxxxxxxxx kernel: lowmem_reserve[]: 0 0 514002 514002
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 Normal free:6144644kB min:523148kB low:653932kB high:784720kB active_anon:102619700kB inactive_anon:12569004kB active_file:196976160kB inactive_file:177744048kB unevictable:136640kB isolated(anon):0kB isolated(file):0kB present:534773760kB managed:526338188kB mlocked:136640kB dirty:426596kB writeback:2168kB mapped:39603644kB shmem:43699696kB slab_reclaimable:15574364kB slab_unreclaimable:1442388kB kernel_stack:82240kB pagetables:2926520kB unstable:0kB bounce:0kB free_pcp:11664kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? no
Apr 4 12:15:25 xxxxxxxxxx kernel: lowmem_reserve[]: 0 0 0 0
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 DMA: 0*4kB 0*8kB 0*16kB 1*32kB (U) 2*64kB (U) 1*128kB (U) 1*256kB (U) 0*512kB 1*1024kB (U) 1*2048kB (M) 3*4096kB (M) = 15904kB
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 DMA32: 6*4kB (M) 5*8kB (UM) 7*16kB (UM) 3*32kB (UM) 6*64kB (UM) 7*128kB (UM) 5*256kB (UM) 5*512kB (UM) 5*1024kB (UM) 6*2048kB (UM) 270*4096kB (M) = 1128720kB
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 Normal: 227860*4kB (UEM) 346644*8kB (UEM) 116568*16kB (UEM) 11561*32kB (UEM) 2407*64kB (UEM) 494*128kB (UEM) 30*256kB (EM) 1*512kB (U) 0*1024kB 0*2048kB 0*4096kB = 6145104kB
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=1048576kB
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
Apr 4 12:15:25 xxxxxxxxxx kernel: 104607223 total pagecache pages
Apr 4 12:15:25 xxxxxxxxxx kernel: 0 pages in swap cache
Apr 4 12:15:25 xxxxxxxxxx kernel: Swap cache stats: add 0, delete 0, find 0/0
Apr 4 12:15:25 xxxxxxxxxx kernel: Free swap = 5242876kB
Apr 4 12:15:25 xxxxxxxxxx kernel: Total swap = 5242876kB
Apr 4 12:15:25 xxxxxxxxxx kernel: 134127987 pages RAM
Apr 4 12:15:25 xxxxxxxxxx kernel: 0 pages HighMem/MovableOnly
Apr 4 12:15:25 xxxxxxxxxx kernel: 2257282 pages reserved
乍一看又和内存相关。由于之前我处理过太多和内存相关的案例,不管是double buffer,还是direct memory reclaim导致RT增加等等,都让我对内存这个玩意又爱又恨。所谓一朝被蛇咬十年怕井绳,于是我又本能地以为内存导致了数据库异常。
3日志分析
气氛都到这了,不分析一下也说不过去。那让我们再系统地学习一下内存管理和回收机制吧。
首先看前两条日志,
Apr 4 12:15:25 xxxxxxxxxx kernel: kworker/u448:5: page allocation failure: order:6, mode:0x10c0d0
Apr 4 12:15:25 xxxxxxxxxx kernel: CPU: 63 PID: 140124 Comm: kworker/u448:5 Kdump: loaded Tainted:
kworker是Kernal Worker的缩写,顾名思义,内核工作进程,作用是处理内核中的异步工作任务,比如页缓存回写、网络数据包的处理、中断等,假如我们发现kworker的占据不少CPU资源的时候,可以针对性地去看下是否发生了大量中断或回写等,同时辅以/proc/stack观测正在做什么。kworker显式的格式是 kworker/%u:%d%s,不带u的就是绑定到特定cpu的worker queue,由于kworker线程会争抢物理核资源,故也可以绑定到特定cpu上。
比如下方我的小破云主机显式的kworker/0:2,则跟第二个core有关。
[root@xiongcc ~]# ps -ef | grep kworker
root 4 2 0 2022 ? 00:00:00 [kworker/0:0H]
root 5 2 0 2022 ? 00:01:39 [kworker/u2:0]
root 248 2 0 2022 ? 00:01:55 [kworker/0:1H]
root 3220 2 0 03:25 ? 00:00:03 [kworker/0:2] ---👈🏻与第二个core有关
root 5743 5687 0 09:51 pts/0 00:00:00 grep --color=auto kworker
root 10273 2 0 Mar03 ? 00:00:08 [kworker/u2:2]
root 31108 2 0 Apr05 ? 00:00:00 [kworker/0:1] ---👈🏻与第一个core有关
[root@xiongcc ~]# cat /proc/10273/stack
[<ffffffff95cbf059>] worker_thread+0x1d9/0x3c0
[<ffffffff95cc5e61>] kthread+0xd1/0xe0
[<ffffffff96395df7>] ret_from_fork_nospec_end+0x0/0x39
[<ffffffffffffffff>] 0xffffffffffffffff
第三四行打印了堆栈
Apr 4 12:15:25 xxxxxxxxxx kernel: Workqueue: mlx5_esw_wq esw_vport_change_handler [mlx5_core]
Apr 4 12:15:25 xxxxxxxxxx kernel: Call Trace:
Apr 4 12:15:25 xxxxxxxxxx kernel: [] dump_stack+0x19/0x1b
Apr 4 12:15:25 xxxxxxxxxx kernel: [] warn_alloc_failed+0x110/0x180
Apr 4 12:15:25 xxxxxxxxxx kernel: [] __alloc_pages_slowpath+0x6b6/0x724
Apr 4 12:15:25 xxxxxxxxxx kernel: [] __alloc_pages_nodemask+0x404/0x420
Apr 4 12:15:25 xxxxxxxxxx kernel: [] alloc_pages_current+0x98/0x110
Apr 4 12:15:25 xxxxxxxxxx kernel: [] __get_free_pages+0xe/0x40
esw_vport_change_handler,根据名称来看,与以太网交换机(Ethernet Switch)中的虚拟端口(Virtual Port)有关,看样子涉及到了网卡驱动这块?把我吓一哆嗦。根据大致流程,kmalloc_order_trace -> get_free_pages -> warn_alloc_failed,kmalloc是一个动态内存分配函数,用于在内核空间中动态地分配一块指定大小的内存块,类似于glibc中的malloc函数,但与之不同的是,kmalloc只能在内核态中使用,会根据申请的内存大小来决定来决定使用块分配器(slab/slub/slob)或页分配器进行内存分配,接着get_free_pages查找空闲内存(page frame),最后告警提示分配失败warn_alloc_failed,光看告警信息,又是内存不足。
接着日志打印了详细的内存信息Mem-Info
Apr 4 12:15:25 xxxxxxxxxx kernel: Mem-Info:
Apr 4 12:15:25 xxxxxxxxxx kernel: active_anon:25654925 inactive_anon:3142251 isolated_anon:0#012 active_file:49244040 inactive_file:44436012 isolated_file:0#012 unevictable:34160 dirty:106649 writeback:542 unstable:0#012 slab_reclaimable:3893591 slab_unreclaimable:360597#012 mapped:9900911 shmem:10924924 pagetables:731630 bounce:0#012 free:1822317 free_pcp:2918 free_cma:0
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 DMA free:15904kB min:12kB low:12kB high:16kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:15996kB managed:15904kB mlocked:0kB dirty:0kB writeback:0kB mapped:0kB shmem:0kB slab_reclaimable:0kB slab_unreclaimable:0kB kernel_stack:0kB pagetables:0kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? yes
信息包括了匿名页,file cache,slab,shared memory等,实际环境中,我们还可以通过/proc/meminfo查看详细内存使用情况。
接着打印了Node更加详细的内存信息,分为了DMA、DMA32和Normal。
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 DMA free:15904kB min:12kB low:12kB high:16kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB isolated(anon):0kB isolated(file):0kB present:15996kB managed:15904kB mlocked:0kB dirty:0kB writeback:0kB mapped:0kB shmem:0kB slab_reclaimable:0kB slab_unreclaimable:0kB kernel_stack:0kB pagetables:0kB unstable:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB writeback_tmp:0kB pages_scanned:0 all_unreclaimable? yes
由于Linux中物理内存被划分成了一个一个的内存节点(也就是我们常说的NUMA),在每个NUMA节点内部又将其所管理的物理内存按照功能不同划分成了不同的内存区域ZONE:
ZONE_DMA:该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU并且需要连续的缓冲区,所以为了实现这两点,必须从物理地址空间专门划分一段区域用于DMA(DMA直接内存访问是一种能力,允许在计算机主板上的设备直接把数据发送到内存中去,数据搬运不需要CPU的参与)。 ZONE_DMA32:与 ZONE_DMA 区域类似,该区域内的物理页面可用于执行 DMA 操作,不同之处在于该区域是提供给 32 位设备(只能寻址 4G 物理内存)执行 DMA 操作时使用的。该区域只在 64 位系统中起作用,因为只有在 64 位系统中才会专门为 32 位设备提供专门的 DMA 区域。 ZONE_NORMAL:这个区域的物理页都可以直接映射到内核中的虚拟内存,由于是线性映射,内核可以直接进行访问。 ZONE_HIGHMEM:这个区域包含的物理页就是我们说的高端内存,内核不能直接访问这些物理页,这些物理页需要动态映射进内核虚拟内存空间中(非线性映射)。该区域只在 32 位系统中才会存在,因为 64 位系统中的内核虚拟内存空间太大了(128T),都可以进行直接映射。以下来自GPT的回答
当然在内核定义中,其实还有ZONE_MOVABLE和ZONE_DEVICE这两个区域,ZONE_MOVABLE的存在因为随着系统的运行会伴随着不同大小的物理内存页的分配和释放,这种内存不规则的分配释放随着系统的长时间运行就会导致内存碎片,内存碎片会使得系统在明明有足够内存的情况下,依然无法为进程分配合适的内存。内核通过迁移页面来规整内存,这样就可以避免内存碎片,从而得到一大片连续的物理内存,以满足内核对大块连续内存分配的请求。
以我的小破主机为例,可以看到是没有ZONE_HIGHMEM的
[postgres@xiongcc ~]$ cat /proc/zoneinfo | grep Node
Node 0, zone DMA
Node 0, zone DMA32
Node 0, zone Normal
Node 0, zone Movable
Node 0, zone Device
并且细心的老铁可能也发现了,操作系统日志中还打印了这样一条信息
lowmem_reserve[]: 0 1102 515104 515104
这是什么鬼?其实这个是操作系统预留的一些内存,以备不时之需。lowmem_reserve 数组则是用于规定每个内存区域必须为自己保留的物理页数量,防止更高位的内存区域对自己的内存空间进行过多的侵占挤压。一些用于特定功能的物理内存必须从特定的内存区域中进行分配,比如外设的 DMA 控制器就必须从 ZONE_DMA 或者 ZONE_DMA32 中分配内存。但是一些用于常规用途的物理内存则可以从多个物理内存区域中进行分配,当 ZONE_HIGHMEM 区域中的内存不足时,内核可以从 ZONE_NORMAL 进行内存分配,ZONE_NORMAL 区域内存不足时可以进一步降级到 ZONE_DMA 区域进行分配。
但是内核又不会允许高位内存区域对低位内存区域的无限制挤压占用,因为毕竟低位内存区域有它特定的用途,所以每个内存区域会给自己预留一定的内存,防止被高位内存区域挤压占用。这便是 lowmem_reserve 数组,用于规定每个内存区域必须为自己保留的物理页数量,防止更高位的内存区域对自己的内存空间进行过多的侵占挤压。
每个物理内存区域 struct zone 都为操作系统预留了一部分内存,这部分预留的物理内存用于内核的一些核心操作,这些操作无论如何是不允许内存分配失败的。其实关于内存的使用按照大的框架来分类,无外乎两类
当进程请求内核分配内存时,如果此时内存比较充裕,那么进程的请求会被立刻满足,如果此时内存已经比较紧张,内核就需要将一部分不经常使用的内存进行回收,从而腾出一部分内存满足进程的内存分配的请求,在这个回收内存的过程中,进程会一直阻塞等待,也就是我们熟知的水位线,kswapd/direct memory reclaim 另一种内存分配场景,进程是不允许阻塞的,内存分配的请求必须马上得到满足,比如执行中断处理程序或者执行持有自旋锁等临界区内的代码时,进程就不允许睡眠,因为中断程序无法被重新调度。这时就需要内核提前为这些核心操作预留一部分内存,当内存紧张时,可以使用这部分预留的内存给这些操作分配。
每个内存区域是按照一定的比例来计算自己的预留内存的,这个比例我们可以通过 cat /proc/sys/vm/lowmem_reserve_ratio 命令查看:
[postgres@xiongcc ~]$ cat /proc/sys/vm/lowmem_reserve_ratio
256 256 32 0 0
从左到右分别代表了 ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_MOVABLE,ZONE_DEVICE 物理内存区域的预留内存比例,因为服务器是64位的,所以没有ZONE_HIGHMEM。
接着日志里打印了关于每个ZONE的详细内存信息
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 DMA: 04kB 08kB 016kB 132kB (U) 264kB (U) 1128kB (U) 1256kB (U) 0512kB 11024kB (U) 12048kB (M) 34096kB (M) = 15904kB
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 DMA32: 64kB (M) 58kB (UM) 716kB (UM) 332kB (UM) 664kB (UM) 7128kB (UM) 5256kB (UM) 5512kB (UM) 51024kB (UM) 62048kB (UM) 2704096kB (M) = 1128720kB
Apr 4 12:15:25 xxxxxxxxxx kernel: Node 0 Normal: 2278604kB (UEM) 3466448kB (UEM) 11656816kB (UEM) 1156132kB (UEM) 240764kB (UEM) 494128kB (UEM) 30256kB (EM) 1512kB (U) 01024kB 02048kB 04096kB = 6145104kB
这里要涉及到内存管理了——伙伴系统Buddy,Buddy系统是Linux最底层的内存管理机制,以减少内存碎片的产生。它使用 Page 粒度来管理内存,通常情况下一个 Page 的大小为 4K(PAGE_SIZE,一般为4K),在 Buddy 系统中分配、释放、回收的最小单位都是 Page。Linux会把所有空闲的页分组为11个页块链表,每个链表管理相应大小的页块,有1、2、4、8、16、32、64、128、256、512、1024个连续页的页块。最大可以申请1024个连续的页,对应4M大小的连续内存。分配时,如果一个空闲块的大小不能被任一长度整除,它就从大到小逐个分解成多个 (2^order x Page) 块来挂载;在释放时,首先把内存释放到对应长度的链表中,随后看看和该内存大小相同、地址相邻的兄弟块(Buddy)是不是free的,如果可以和 buddy 块合并成一个大块挂载到更高一阶的链表,在挂载的时候继续尝试合并。
既然说到了buddy,再说下slab,它的基本思想是将内核中经常使用的对象放到高速缓存中,并且由系统保持为初始的可利用状态,以减少分配、初始化和释放对象的时间开销。比如进程描述符,内核中会频繁对此数据进行申请和释放。slab是buddy之上的一层,Linux内核使用伙伴系统算法来管理内存页,但伙伴系统算法分配的单位是内存页,就是至少要分配一个或以上的内存块。但很多时候我们并不需要分配一个内存页,例如我们要申请一个大小为200字节的结构体时,如果使用伙伴系统分配算法至少申请一个内存页,但只使用了200字节的内存,那么剩余的3896字节就被浪费掉了,这个也就是内部的内存碎片。为了解决小块内存申请的问题,Linux内核又引入了 SLAB 分配算法。cat /proc/slabinfo或者 cat /proc/meminfo | grep -i slab 就可以看见系统中存在的slab信息,slab有的可回收有的不可回收,其中可回收的通过"SReclaimable"表示,不可回收的通过"SUnreclaim"表示,我们通常使用的drop cache也和这二者息息相关。
假如发现了slab占据了大量内存,就要去针对性的分析一下了。以下脚本可以帮助分析哪个slab占用内存较多
[root@xiongcc ~]# while sleep 1; do cat /proc/slabinfo | awk '{name=$1; size=$2*$4/4096; printf "%s %lu\n", name, size;}' | sort -n -r -k 2 | head -n 20; echo "--------------";done;
buffer_head 6211
radix_tree_node 2632
ext4_inode_cache 2557
inode_cache 1155
dentry 1003
kmalloc-4096 702
kernfs_node_cache 601
kmalloc-2048 264
kmalloc-1024 239
proc_inode_cache 226
vm_area_struct 206
kmalloc-192 177
shmem_inode_cache 139
kmalloc-64 139
task_struct 134
selinux_inode_security 94
idr_layer_cache 92
kmalloc-256 90
kmalloc-8192 72
kmalloc-512 70
至于图上的紫色部分在之前内存管理文章中,已经有所提及。内核主要对进程使用的页进行回收,属于内核的大部分页框是不能够进行回收的,比如内核栈、内核代码段、内核数据段以及大部分内核使用的页,主要是两个方面:1、直接将一些页释放。2、将页回写保存到磁盘(也就是write back),然后再释放。对于第一种,最明显的就是进程代码段的页,这些页都是只读的,因为代码段是禁止修改的,对于这些页,直接释放掉就好,因为磁盘上对应的数据与页中的数据是一致的。那么对于进程需要回写的页,内核主要将这些页放到磁盘的两个地方,当进程使用的页中的数据是映射于具体文件的,那么只需要将此页中的数据回写到对应文件所在磁盘位置就可以了。而对于那些没有映射磁盘对应文件的页,内核则将它们存放到swap分区中,也就是匿名页这个东西。
进程堆、栈、数据段使用的匿名页:存放到swap分区中 进程代码段映射的可执行文件的文件页:直接释放 打开文件进行读写使用的文件页:如果页中数据与文件数据不一致,则进行回写到磁盘对应文件中,如果一致,则直接释放 进行文件映射mmap共享内存时使用的页:如果页中数据与文件数据不一致,则进行回写到磁盘对应文件中,如果一致,则直接释放 进行匿名mmap共享内存时使用的页:存放到swap分区中 进行shmem共享内存时使用的页:存放到swap分区中
针对此情况,Linux引进了LRU链表,实际上整个内存的回收,做的事情就是处理LRU链表。
LRU_INACTIVE_ANON:称为非活动匿名页lru链表,此链表中保存的是此zone中所有最近没被访问过的并且可以存放到swap分区的页描述符,在此链表中的页描述符的PG_active标志为0。
LRU_ACTIVE_ANON:称为活动匿名页lru链表,此链表中保存的是此zone中所有最近被访问过的并且可以存放到swap分区的页描述符,此链表中的页描述符的PG_active标志为1。
LRU_INACTIVE_FILE:称为非活动文件页lru链表,此链表中保存的是此zone中所有最近没被访问过的文件页的页描述符,此链表中的页描述符的PG_active标志为0。
LRU_ACTIVE_FILE:称为活动文件页lru链表,此链表中保存的是此zone中所有最近被访问过的文件页的页描述符,此链表中的页描述符的PG_active标志为1。
LRU_UNEVICTABLE:此链表中保存的是此zone中所有禁止换出的页的描述符。
虽然现在内存已经很大了,动辄上TB,但是内存对计算机系统来说依旧是一项宝贵且紧缺的资源,所以经常性地需要进行回收。首先内核会尝试的是内存规整,也就是内存碎片整理,比如说系统当前有10个不连续的空闲page,但是你要分配两个连续的page,显然是无法分配的,此时就要进行内存规整,通过移动movable page,使空闲page尽量连在一起,这样能有可能分配出多个连续的page了,也就是前面所说的ZONE_MOVABLE。如果内存规整之后还是无法分配到内存,此时就会进行页面回收了,这个我们比较熟悉,抄一下之前的文章吧
当整机free内存低于黄线low阈值时,内核的异步内存回收线程kswapd开始被唤醒,kswapd会在其他进程申请内存的同时回收内存。当整机free内存触达红线min阈值时,触发整机直接内存回收,所有来自用户空间的内存申请将被阻塞住,线程状态同时转换为D状态。此时只有来自内核空间的内存申请可以继续使用min值以下的free内存。后续当整机free内存逐步恢复到绿线high阈值以上后,kswapd线程停止内存回收工作。尽管kswapd是很努力的,但它毕竟是周期性执行的,难免出现某个时刻系统中剩余的内存极少,少到可能连回收内存操作本身需要的内存都不够了。这时候只能使出终极武器了,那就是OOM killer,做法是选择一个进程,然后kill掉,把它占用的内存释放出来,牺牲了这个进程,保全了整个系统。虽然OOM killer有时候可能导致严重的损失,但总比系统完全崩溃要好。这个时候,oom_score的作用就出来了,有点像免死金牌,系统会根据/proc/xxx/oom_score计算得到一个值,取值范围0 –- 1000,0代表never kill,1000代表aways kill,值越大,进程被选中的概率越大。我们可以按需调整PostgreSQL的一些后端进程的值,在内存紧张的系统中尽可能避免被误伤。
关于当前内存情况,我们可以通过/proc/buddyinfo观察,以下是告警时间段的状态,可以看到128、256、512、1024个连续页的页块都为0了。
[postgres@xxxxx ~]$ cat /proc/buddyinfo
Node 0, zone DMA 0 0 0 1 2 1 1 0 1 1 3
Node 0, zone DMA32 6 5 7 3 6 7 5 5 5 6 270
Node 0, zone NORMAL 28752 3487 870542 2249876 399395 64496 5344 0 0 0 0 ---👈🏻
内核也提供了一个手动规整的接口,强制回收内存碎片 /proc/sys/vm/compact_memory,至于cat /sys/kernel/debug/extfrag/extfrag_index 则是内核开发者也为我们提供观察内存指数的接口,当指数趋近 0 则表示内存分配将因内存不足而失败,所以此时不宜做内存规整而是做内存回收。当指数趋近 1000 时则表示内存分配将因外部碎片过多导致失败,所以不适合做内存回收而是做内存规整
最后面的日志开始打印
0 pages in swap cache、0 pages HighMem/MovableOnly
说明进程堆、栈、数据段使用的匿名页和匿名mmap等所在的swap cache里面没有找到可以回收的页,HighMem和MovableOnly也没有找到页。
因此,通过前面的八股文显式,系统的内存又双叒叕出现了瓶颈。
4内存回收
当内存紧张的时候,有一个常用的手段就是使用下面的命令来手工回收 cache:$ echo 1 > /proc/sys/vm/drop_caches,drop_caches接受以下三种值:
To free pagecache:echo 1 > /proc/sys/vm/drop_caches To free reclaimable slab objects (includes dentries and inodes):echo 2 > /proc/sys/vm/drop_caches,表示清除回收slab分配器中的对象(包括目录项缓存和inode缓存) To free slab objects and pagecache:echo 3 > /proc/sys/vm/drop_caches,表示清除pagecache和slab分配器中的缓存对象
但是可能有的人发现,即使drop cache,也不可能把cache值降到0,甚至有时候cache值几乎没有下降,这是为什么呢?因为page cache中包含的tmpfs和共享内存(共享内存是系统提供给我们的一种常用的进程间通信(IPC)方式)是不能通过drop_caches回收的,并且dirty pages不能回收,需要先做sync同步到磁盘上,参照上方的LRU链表内存回收。
page cache我们知道目的是用于缓存文件里的数据(可以使用fincore观察),并且Linux的机制是尽可能多的将cache全部缓存,用户平衡不同设备之前的速度,我们可以调节vm.dirty_background_ratio等参数控制脏页的比例。page cache不仅包括普通的磁盘文件,还包括了tmpfs文件,tmpfs文件系统是将一部分内存空间模拟成文件系统(TPCC跑分的时候可以用来作弊🤣),由于背后并没有对应着磁盘,无法进行换页,只能交换到swap cache,在执行drop_cache操作的时候tmpfs对应的page cache并不会回收。所以总结一下来说
当cache作为文件缓存被释放的时候会引发IO变高,这是cache加快文件访问速度所要付出的成本。 tmpfs中存储的文件会占用cache空间,除非文件删除否则这个cache不会被自动释放。 使用shmget方式申请的共享内存会占用cache空间,除非共享内存被ipcrm或者使用shmctl去IPC_RMID,否则相关的cache空间都不会被自动释放。 使用mmap方法申请的MAP_SHARED标志的内存会占用cache空间,除非进程将这段内存munmap,否则相关的cache空间都不会被自动释放。 实际上shmget、mmap的共享内存,在内核层都是通过tmpfs实现的,tmpfs实现的存储用的都是cache。
说来也巧,正好上周在austin群里有人还问了tmpfs的问题,因为PostgreSQL的动态共享内存DSM默认是posix的,那么也会在/dev/shm/ ,该目录文件系统正是tmpfs。之前也写过共享内存的生产案例,感兴趣的老铁可以翻一下,主要是removeIPC导致的。
5小结
虽然最后应用找到了问题(FULL GC),但是通过这么一个乌龙事件,再次让我去系统学习了一下Linux的内存管理,不得不说,内存管理里面的水太深了,学习了几年还没有摸太清楚。慢慢来,岂能一口吃个胖子。还是那句话,数据库相比其他基础组件占据比较核心的位置,向上是各种应用的支撑引擎,向下调动计算、网络、存储等基础资源,数据库和软硬件息息相关,所以操作系统的知识十分重要。
正好最近需要去评测一下ptmalloc、jemalloc、tcmalloc、scudo等优秀的malloc库在PostgreSQL上的行为异同,让我们拭目以待。
6参考
Buddy 内存管理机制(上)
【Linux内核】kmalloc分配内存大小
Linux内核内存管理--内存碎片整理(实现流程)
Linux OOM 基本原理解析
深入理解Linux内存子系统