作者 | cynrikluo
内存不是有限的,总有不够用的时刻,linux内核用三个机制来处置这种状况:内存回收、内存规整、oom-kill。
当发现内存无余时,内核会先尝试内存回收,从一些进程手里拿回一些页;假设这样还是不能满足放开需求,则触发内存规整;再不行,则触发oom被动kill掉一个不太关键的进程,监禁内存。
低内存状况下,内核的处置逻辑
内存放开的**函数是__alloc_pages_nodemask:
struct page struct page pageunsigned int alloc_flags gfp_t alloc_mask struct alloc_context ac
__alloc_pages_nodemask会先尝试调用get_page_from_freelist从同伴系统的freelist里拿闲暇页,假设能拿到就间接前往:
假设拿不到,则进入慢速门路:
__alloc_pages_slowpath,慢速门路,望文生义,就是拿得慢一点,须要做一些操作再拿。
首先,__alloc_pages_slowpath会唤醒kswapd:
kswapd是一个守护进程,专门启动内存回出操作,口头门路:
它被唤醒后,会立1刻开局启动回收,效率高的话,freelist上会立刻多出很多闲暇页。
所以__alloc_pages_slowpath会马上再次尝试从freelist失掉页面,失掉成功则间接前往了。
若还是失败,__alloc_pages_slowpath则会进入direct_reclaim阶段:
direct_reclaim,望文生义,就是间接内存回收,回收到的页不用放回freelist再get_page_from_freelist这么费事了,也不用唤醒某个进程帮助回收,而是由当行进程(current)亲身下场去回收,口头门路:
假设direct_reclaim也回收不过去,__alloc_pages_slowpath还会负隅顽抗下,做一下内存规整,尝试把零散的页辗转腾挪,拼成为大order页(仅在放开order>0的页时有用)。
假设还是不可满足要求,则进入oom-kill了:
总结上方的逻辑:内存放开时,首先尝试间接从freelist里拿;失败了则先唤醒kswapd帮助回收内存;若内存低到让kswapd也心无余而力缺乏,则进入direct reclaim间接回收内存;若direct reclaim也无能为力,则oom:
三条水线
实践上,从freelist上拿页不是便捷地间接拿,而是先审核下该zone能否满足水线要求,不满足那就间接失败。
内核给内存治理划了三条水线:MIN、LOW、HIGH。
三者大小相关从字面即可推断,MIN < LOW < HIGH。
在初次尝试从freelist拿页时,门槛水线是LOW;唤醒kswapd后再次尝试拿页,门槛水线是MIN。
所以实践逻辑如下:
所以,可以便捷地以为,可用内存低于LOW水线时,唤醒kswapd;低于MIN水线时,启动direct reclaim;而HIGH水线,是kswapd的回收终止线:
为什么内存回收时,磁盘IO会被打满?
可以看到,kswapd和direct_reclaim最终都是走到了shrink_node:
shrink_node是内存回收的**函数,望文生义,让整个node启动一次性“收缩”,把不要的数据清掉,空出闲暇页。
get_scan_count选择本次扫描多少个anon page和file page。
anon page就是Anonymous Page,匿名页,是进程的堆栈、数据段等。内核回收匿名页时,将这些数据启动紧缩(紧缩比大略为3),而后移动到内存中的一个小角落中(swap空间),这个环节并没有与磁盘出现交互,因此不会发生IO,但须要紧缩数据,所以耗CPU。
file page就是文件页,是进程的代码段、映射的文件。内核回收文件页时,先将“脏”数据回写到磁盘,而后监禁掉这些缓存数据,洁净的数据则间接监禁掉。这个环节触及到写磁盘,因此会发生IO。
便捷总结一下get_scan_count的逻辑:
所以说,不论开没开swap,内存回收都是偏差于回收file page。
假设file page中有脏页,那内存回收大略率就会发生一些IO,无非是IO量多少罢了。
以上状况IO或许会打满或许暴增:
为什么低内存有时会引发hungtask?
低内存时,通常不是一般进程触发了direct reclaim,而是少量进程都在direct reclaim。
大家都要回写脏页,于是IO被打满了。
这时刻,进程会频繁地被IO阻塞,被阻塞的进程为了不占用CPU,会调用io_schedule_timeout或io_schedule来挂起自己,直到IO成功。
这种期待是D形态的,一旦超越了120S,就会触发hungtask。当然,这是十分极其的状况,IO曾经齐全有救的状况。
大局部时刻,IO只管打满了,但是总能周转过去,所以这些进程并不会等太久。
但是,这些进程若是来自同一个业务,则大略率会访问同一个数据,这就须要经过mutex、rwsem、semaphore同等步机制来控制访问行为。
而这些同步机制的基本接口都是uninterruptible性质的,以semaphore为例:
extern struct semaphore sem // 基本接口。失掉信号量,失掉不到则进入uninterruptible睡眠extern int __must_check struct semaphore sem extern int __must_check struct semaphore sem extern int __must_check struct semaphore sem extern int __must_check struct semaphore sem long jiffies
所谓uninterruptible性质,即当进程失掉不到同步资源时,间接进入D形态期待其余进程监禁资源。
其余同步资源,rwsem、mutex等,都有这样的uninterruptible性质接口。
反常状况下,只需持有同步资源的进程反常运转不卡顿,那么即使有上百个进程来争抢这些同步资源,关于排序靠后的进程来说,期间也是够的,普通不会期待超越120s。
但在低内存状况下,大家都在等IO,这些持有资源的进程也不能幸免,引发堵车连锁反响。
假设此时同步资源的waiter们已累计了几十个甚至上百个,那么就算只要一瞬间的io卡顿,排序靠后的waiter也容易期待超越120s,触发hungtask。
一个十分典型的案例,一台CVM在延续报了几条hungtask warning后,彻底无照应了,经过魔术建触发重启。
系统消息如下:
内存状况不容失望,典型的低内存:
log上有很多hungtask warning,超时要素都是等rwsem太长,写者waiter和读者waiter都有:
这些进程在同等一个rwsem,这个rwsem的地址为:ffff880e9703f370
进一步探求,发现对ffff880e9703f370有援用的进程为19个,11个正在读,8个排队。
而这11个正在读的进程,都在做同一件事——direct reclaim,并且都卡在IO期待:
这11个进程,只管也是D形态,但因为时不时能调度到IO,相当于D形态的继续期间不时重置,所以自身并没有触发hungtask。
而这8个waiter进程就没这个喜气了,被前面11个进程你方唱罢我退场地阻塞,继续期间也没无时机重置,最终超越120s,引发hungtask了。
提升低内存处置
咱们曾经知道了低内存会造成IO突增,甚至造成hungtask,那要如何防止呢?
可以从两方面来防止。
(1) 调整脏页回刷频率
将往常的脏页回刷频率调高,这样内存回收时,须要回收的脏页就更少,降落IO的增量。
调高水线,可以更早地进入内存回收逻辑,这样可以将free维持在一个较高水平,防止堕入极其场景。因为low和min同时受min_free_kbytes管控,所以可以间接调整min_free_kbytes值。
调高/proc/sys/vm/min_free_kbytes