内存密集型应用的内核参数(选)

Posted by keys961 on July 6, 2021

1. 概要

这里简要整理一下一些内存密集型应用需要调整的操作系统内核参数,参数不全也请见谅。

2. 预留物理内存

为保证服务稳定,操作系统需要预留一块内存,保证系统不挂。

涉及到的参数为:

  • vm.min_free_kbytes:系统预留的最低物理内存KB数

通常,该参数不应过小或过大,通常配为物理内存的1%~3%

该参数和交换分区有关系,这里说明kswapd守护进程,它会周期检查内存使用压力,并在压力大的时候回收内存。

内存压力通过3个水位线表示:

  • min:即vm.min_free_kbytes
  • low:即min * 5/4
  • high:即min * 3/2

当:

  • 内存剩余在high以上,说明内存没什么压力
  • 内存剩余在low以上但在high以下,说明内存存在一定压力
  • 内存剩余在min以上但在low一下,说明内存压力较大,kswapd会被唤醒以回收内存,直到剩余达到high为止
  • 内存剩余在min一下,触发直接回收,此时性能会大减

3. 交换分区

内存密集型应用最重要的内核参数就是交换分区的参数。

众所周知Linux使用虚拟内存+段页管理的技术管理内存,当物理内存不够时,就会使用交换分区,页就会在磁盘和内存之间换入换出,此时会极大降低进程的性能。

涉及到的参数为:

  • vm.swappiness:值越高,越积极地使用交换分区,默认60

通常而言,为了不去使用交换分区,我们会设置其为下面2个值:

  • 0
    • 3.5内核以前:当可用内存小于vm.min_free_kbytes,开始使用交换分区
    • 3.5内核以后:只有当剩余内存+文件cache内存小于high水位线时,才开始使用交换分区
  • 1:最低限度使用交换空间

4. 透明大页

页是内存寻址的最小单位。普通页的大小为4KB,而透明大页大小为2MB,它的好处是:

  • 页表更小
  • TLB的cache miss几率更小

透明大页不需要修改代码,大页通过运行时分配;而静态大页需要修改代码

然而对于内存密集型应用,特别是针对随机内存访问,透明大页是不适合的,以至于Redis推荐将其关闭:

  • 透明大页是运行时的分配管理,它动态将4KB的页交换成大页,过程中有各种锁等操作,有一定延迟
  • 换入换出的开销大
  • 内存空间浪费

关闭它的方法很简单:

1
2
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

5. NUMA Reclaim

这涉及多CPU节点的情形。

NUMA(Non-uniform Memory Access)使得每个CPU都有自己专属的内存区域。当CPU访问自己绑定的内存时,响应时间短,即local access;而访问其他CPU绑定的内存时,需要通过inter-connect通道访问,响应时间长,即remote access。

NUMA架构可能导致CPU内存使用不均衡,一种情况是:

  • 某个NUMA节点,CPU和内存不够用,需要频繁回收内存,并导致使用交换分区
  • 其他NUMA节点很空闲
  • 系统抖动很厉害,但是topfree下,CPU占用和内存占用都很低

涉及到的参数为:

  • vm.zone_reclaim_mode:NUMA内存回收策略
    • 0:当本NUMA节点内存不够时,可从其它NUMA节点找可用内存,即关闭NUMA Reclaim
    • 1:只会在本NUMA节点进行内存回收
    • 2:本地回收内存时,可以将cache的数据写回磁盘,以回收内存
    • 4:本地回收内存时,可以使用交换分区

默认情况下为0,一般情况下够用,不需要调整。

若场景为内存需求大于缓存,且要避免跨NUMA节点的性能开销,则可以开启NUMA Reclaim模式。

6. Cache回收

由于有dumprestore数据的需要,它们都要读写磁盘,所以系统cache回收也是需要调整的。

6.1. 命令行回收

根据Aerospike文档,缓存分为4类:

  • dirty cache:脏数据,需要通过sync刷回磁盘
  • clean cache:干净数据,通常是从文件读取的内容
  • inode cache:inode数据结构的缓存
  • slab cache:用于slab分配的缓存

后三种都可以通过命令行进行回收:

1
2
3
4
5
6
# clear page cache (type 2,3)
echo 1 > /proc/sys/vm/drop_caches
# clear slab cache (type 4)
echo 2 > /proc/sys/vm/drop_caches
# clear page and slab cache (types 2,3,4)
echo 3 > /proc/sys/vm/drop_caches

6.2. inodedentry的回收

这里涉及到1个参数:

  • vm.vfs_cache_pressure:当小于100时,系统倾向于保留dentryinode缓存;超过100时,系统倾向于重新声明dentryinode,它的缓存倾向于被清除

不过该参数设置很高,内核清理该缓存的速度都比较较低,实践中会设置为10000

6.3. 脏页写回

这里涉及到4个参数:

  • vm.dirty_background_ratio:可以填充的脏数据占内存的百分比,若超过该比例,内核的守护进程(如pdflush)会将其刷回磁盘
  • vm.dirty_expire_centisecs:设置多少时间后,脏页过期,内核会检查过期脏页并写回磁盘

  • vm.dirty_ratio:可以填充的脏数据占内存百分比的绝对上限,若超过这个值,新I/O会被阻塞,直到脏数据写回磁盘
  • vm.dirty_writeback_centisecs:每隔多少时间,刷脏页的守护进程(如pdflush等)会被唤醒

通常会将第1个参数设小(如5),第2个参数设小(如1000,即10秒),第3个参数设大(如80),第4个参数不修改。这样的配置,既可以加快脏页回收,又可以应对突发的I/O,使得性能更加平滑。

参考:https://feichashao.com/dirty_ratio_and_dirty_background_ratio/

7. 内存分配的Overcommit

Redis运维对内存的overcommit做了配置要求。

众所周知,malloc返回0时,表示内存申请失败。而在Linux中,内存不足的时候,通常不会返回0,而是尽量让其成功,这就是overcommit。

注意,申请和分配是不同的概念,只有内存被使用时才会分配。

这里涉及到2个参数:

  • vm.overcommit_memory:Overcommit下的内存分配策略,有3个值可选
    • 0:内核将检查是否有足够的可用内存,如果有则申请通过,否则内存申请失败,该决策是启发式的
    • 1:内核允许超量使用内存到完
    • 2:绝不过量使用内存,上限为交换分区+N%的RAM值
  • vm.overcommit_ratio:即上面的N

Redis推荐将vm.overcommit_memory设置为1,其原因是为了防止后台保存RDB失败,因为后台RDB需要fork新进程,当数据修改时,COW机制会产生额外内存,若内存申请失败,会导致任务失败,或者很可能被OOM Killer宰掉。

8. 资源隔离

资源隔离可以用很多工具实现,例如docker可以配置CPU Affinity等,还有cgroups等工具。

通常内存密集型的资源隔离,通常需要配置CPU Affinity, NUMA以及内存使用等。

cgroups为例,需要使用到的命令行为cgcreate, cgset, cgclassify等,docker/docker-compose启动参数也可以配置。具体可参考官方文档。

Docker底层还是使用namespacecgroups

  • 前者对系统环境隔离(如网络栈、进程列表、挂载点等)
  • 后者对系统资源隔离(如CPU、内存等)