关于Linux性能调优之内存负载调优

写在前面

  • 整理一些Linux内存调优的笔记,分享给小伙伴
  • 博文没有涉及的Demo,理论方法偏多,可以用作内存调优入门
  • 博文内容涉及:
    • Linux内存管理的基本理论
    • 寻找内存泄露的进程
    • 内存交换空间调优
    • 不同方式的内存回收
  • 食用方式
    • 需了解Linux基础知识
  • 理解不足小伙伴帮忙指正

原谅和忘记就意味着扔掉了我们获得的最贵经验 ——-《人生的智慧》叔本华


讲内存之前,简单介绍下调优的一些方法论,一般调优首先需要分析系统性能,然后执行系统变更测试应用。

分析系统性能的方法:

分析利用率、饱和、错误 (Utilization, Saturation and Errors (USE))和系统资源

指标 描述
资源 Resource 服务器中的硬件资源(CPU、内存、网络、 磁盘和控制器)
利用率 Utilization 资源提供服务的平均时间(70%)
饱和度 Saturation 资源承担额外工作量的能力,这类工作通常 排队(队列长度)
错误 Errors 错误事件数量

执行系统变更的步骤:

执行系统变更的步骤
设定基线:在调优中,执行性能测试,收集指标
执行变更:在系统上执行变更,建议查阅相应官方文档
验证变更有效性:执行变更后,重新运行测试,比较一些关键指标
取消变更并与基线对比:如有需要,可以取消变更并返回到基线状态
应用最终更改:应用并记录最终变更

在现代处理器中,与CPU执行代码或处理信息相比,向内存子系统保存信息或从中读取信息一般花费的时间更长

通常,在CPU执行指令或处理数据前,它会消耗相当多的空闲时间来等待从内存中取出指令和数据。处理器用不同层次的高速缓存(cache)来弥补这种缓慢的内存性能。

内存管理

内存是内核所做的比较复杂的事情之一。高效的内存管理对于系统中进程的良好性能至关重要。现代计算机系统使用分页来安全、灵活地管理系统内存。

为了提高效率,Linux将其分成内存“页”。当对内存进分配或传送时,Linux操作的单位是页,而不是单个字节。在报告一些内存统计数据时,Linux内核报告的是每秒页面的数量,该值根据其运行的架构可以发生变化。

系统上的物理RAM被分为页帧;一页帧保存一页数据。进程不直接寻址物理内存。相反,每个进程都有一个虚拟地址空间。当进程被分配内存时,页帧的物理地址被映射到进程的一个虚拟地址。从进程的角度来看,它有一个私有的内存空间,它只能看到映射到它的一个虚拟地址的物理页帧。

进程虚拟地址空间(页面)的大小取决于处理器架构。在32位i386系统上,一个进程的虚拟地址空间可以容纳2^32 个字节(4gib)的内存;在64位x86-64系统上,地址空间的大小是2^64 字节(16 EiB)。然而,单个进程通常不会使用它的整个地址空间;它的大部分是未分配的,也没有映射到任何实际的物理内存。但是虚拟地址空间的大小确实限制了进程可以拥有的最大内存

一些内存涉及的名词解释

交换分区(物理内存不足)

所有系统RAM芯片的物理内存容量都是固定的。即使应用程序需要的内存容量大于可用的物理内存,Linux内核仍然允许这些程序运行。Linux内核使用硬盘作为临时存储器,这个硬盘空间被称为交换分区(swap space)

尽管交换是让进程运行的极好的方法,但它却慢的要命。与使用物理内存相比,应用程序使用交换的速度可以慢到一千倍。如果系统性能不佳,确定系统使用了多少交换通常是有用的。

缓冲区(buffer)和缓存(cache)(物理内存太多)

缓存(cache)

相反,如果你的系统物理内存容量超过了应用程序的需求,Linux就会在物理内存中缓存近期使用过的文件,这样,后续访问这些文件时就不用去访问硬盘了。

对要频繁访问硬盘的应用程序来说,这可以显著加速其速度,显然,对经常启动的应用程序而言,这是特别有用的。

应用程序首次启动时,它需要从硬盘读取;但是,如果应用程序留着缓存中,那它就需要从更快速的物理内存读取。

缓冲区(buffer)

Linux还使用了额外的存储作为缓冲区。为了进一步优化应用程序,Linux为需要被写回硬盘的数据预留了存储空间。这些预留空间被称为缓冲区。如果应用程序要将数据写回硬盘,通常需要花费较长时间,Linux让应用程序立刻继续执行,但将文件数据保存到内存缓冲区。在之后的某个时刻,缓冲区被刷新到硬盘,而应用程序可以立即继续

高速缓存和缓冲区的使用使得系统内空闲的内存很少,默认情况下,Linux试图尽可能多的使用你的内存。这是好事。

如果Linux侦测到有空闲内存,它就会将应用程序和数据缓存到这些内存以加速未来的访问。由于访问内存的速度比访问硬盘的速度快了几个数量级,因此,这就可以显著地提升整体性能

如果系统需要缓存空间做更重要的事情,那么缓存空间将被擦除并交给系统。之后,对原来被缓存对象的访问就需要转向硬盘来满足。

活跃与非活跃内存

  • 活跃内存是指当前被进程使用的内存。
  • 不活跃内存是指已经被分配了,但暂时还未使用的内存。

这两种类型的内存没有本质上的区别。需要时,Linux找出进程最近最少使用的内存页面,并将它们从活跃列表移动到不活跃列表。当要选择把哪个内存页交换到硬盘时,内核就从不活跃内存列表中进行选择

内核的内存使用情况(分片)

除了应用程序需要分配内存外,Linux内核也会为了记账的目的消耗一定量的内存。

记账包括,比如跟踪从网络或磁盘I/O来的数据,以及跟踪哪些进程正在运行,哪些正在休眠。为了管理记账,内核有一系列缓存,包含一个或多个内存分片。每个分片为一组对象,个数可以是一个或多个。

内核消耗的内存分片数量取决于使用的是Linux内核的哪些部分,而且还可以随着机器负载类型的变化而变化。

进程内存

当进程试图访问虚拟内存中尚未分配物理页面的页面时, 即应用程序中没有加载的内存中数据,将触发页面错误

  • 如果需要分配一个新的物理内存页,这被称为次要页面故障
  • 如果内核需要从磁盘检索一页内存,则称为重大页面故障

发生重大页面错误的原因包括恢复已换出的虚拟内存页面,或将数据或代码从可执行文件映射到内存。次要页面错误会导致少量开销,但是重大页面错误会对性能产生重大影响。

要查看每个进程的次要和主要页面错误,你可以使用ps命令,询问minfltmajflt列:

┌──[root@liruilongs.github.io]-[~]
└─$ ps -o pid,cmd,comm,minflt,majflt $$PID CMD                         COMMAND         MINFLT MAJFLT12280 -bash                       bash             11203      0
┌──[root@liruilongs.github.io]-[~]
└─$ ps -o pid,cmd,comm,minflt,majflt 889PID CMD                         COMMAND         MINFLT MAJFLT889 /usr/bin/etcd --name=defaul etcd              7091     90

使用cgroups限制内存

进程可用的内存量可以用cgroup内存控制器来限制。在内存cgroup中有几个相关的文件涉及内核参数:

┌──[root@liruilongs.github.io]-[~]
└─$ ls /sys/fs/cgroup/memory/
cgroup.clone_children           memory.kmem.tcp.limit_in_bytes      memory.oom_control
cgroup.event_control            memory.kmem.tcp.max_usage_in_bytes  memory.pressure_level
cgroup.procs                    memory.kmem.tcp.usage_in_bytes      memory.soft_limit_in_bytes
cgroup.sane_behavior            memory.kmem.usage_in_bytes          memory.stat
docker                          memory.limit_in_bytes               memory.swappiness
memory.failcnt                  memory.max_usage_in_bytes           memory.usage_in_bytes
memory.force_empty              memory.memsw.failcnt                memory.use_hierarchy
memory.kmem.failcnt             memory.memsw.limit_in_bytes         notify_on_release
memory.kmem.limit_in_bytes      memory.memsw.max_usage_in_bytes     release_agent
memory.kmem.max_usage_in_bytes  memory.memsw.usage_in_bytes         system.slice
memory.kmem.slabinfo            memory.move_charge_at_immigrate     tasks
memory.kmem.tcp.failcnt         memory.numa_stat                    user.slice

看一个docker 进程的相关信息

  • memory.stat: :这个cgroup中内存和交换分区的使用情况以及内存总量的详细统计。

dockers配置了开机自动启动,从Cgroup角度看,所以他在是systemd这个slicp下面,作为一个service单元受CGroup管理

┌──[root@liruilongs.github.io]-[/sys/fs/cgroup/memory/system.slice/docker.service]
└─$ cat ./memory.stat
cache 3657728
rss 144375808
rss_huge 113246208
mapped_file 0
swap 0
pgpgin 25693
pgpgout 23278
pgfault 84168
pgmajfault 0
inactive_anon 4096
active_anon 144375808
inactive_file 397312
active_file 3256320
unevictable 0
hierarchical_memory_limit 9223372036854771712
hierarchical_memsw_limit 9223372036854771712
total_cache 3657728
total_rss 144375808
total_rss_huge 113246208
total_mapped_file 0
total_swap 0
total_pgpgin 25693
total_pgpgout 23278
total_pgfault 84168
total_pgmajfault 0
total_inactive_anon 4096
total_active_anon 144375808
total_inactive_file 397312
total_active_file 3256320
total_unevictable 0
  • memory.limit_in_bytes:在这里写入一个值,以限制这个cgroup中可以使用的用户内存(包括缓存)的数量。字节以外的单位可以通过在被设置的值后面加上k、m或g来使用。

所以我们可以通过修改这个值来限制docker的内存使用

┌──[root@liruilongs.github.io]-[/sys/fs/cgroup/memory/system.slice/docker.service]
└─$ cat ./memory.limit_in_bytes
9223372036854771712
$ echo 1G > ./memory.limit_in_bytes
  • memory.memsw.limit_in_bytes:类似于memory.limit_in_bytes,但这一次是内存+交换组合。
┌──[root@liruilongs.github.io]-[/sys/fs/cgroup/memory/system.slice/docker.service]
└─$ cat ./memory.memsw.limit_in_bytes
9223372036854771712

当然也可以在单位配置文件[Service]部分中添加下面的命令:

MemoryLimit=value

cgroup中执行的进程设定其可用内存的最大值,同样,MemoryAccounting 参数必须在同一单元中启用MemoryAccounting=yes打开此单元的进程和内核内存占。接受一个布尔参数 ,这里 MemoryLimit 参数可以控制 memory.limit_in_bytes Cgroup参数实现内存限制

其他的内核参数

  • memory.failcntmemory.memsw.failcnt:这些文件报告内存的频率。
┌──[root@liruilongs.github.io]-[/sys/fs/cgroup/memory/system.slice/docker.service]
└─$ cat ./memory.failcnt
0
┌──[root@liruilongs.github.io]-[/sys/fs/cgroup/memory/system.slice/docker.service]
└─$ cat memory.memsw.failcnt
0

要查看进程虚拟地址空间是如何使用的,可以使用pmap PID命令,或者查看/proc/PID/maps/proc/PID/maps

┌──[root@liruilongs.github.io]-[~]
└─$ pmap 889 | head  -10
889:   /usr/bin/etcd --name=default --data-dir=/var/lib/etcd/default.etcd --listen-client-urls=http://192.168.26.55:2379,http://localhost:2379
000000c000000000      8K rw---   [ anon ]
000000c41ffc6000   6376K rw---   [ anon ]
000000c420600000   1216K rw---   [ anon ]
00005618c7aaa000  15784K r-x-- etcd
00005618c8c14000   8672K r---- etcd
00005618c948c000    332K rw--- etcd
00005618c94df000    168K rw---   [ anon ]
00005618ca601000    132K rw---   [ anon ]
00007ff23c000000    132K rw---   [ anon ]
┌──[root@liruilongs.github.io]-[~]
└─$ cat /proc/889/maps | head  -10
c000000000-c000002000 rw-p 00000000 00:00 0
c41ffc6000-c420600000 rw-p 00000000 00:00 0                              [stack:889]
c420600000-c420730000 rw-p 00000000 00:00 0
5618c7aaa000-5618c8a14000 r-xp 00000000 08:01 272289927                  /usr/bin/etcd
5618c8c14000-5618c948c000 r--p 00f6a000 08:01 272289927                  /usr/bin/etcd
5618c948c000-5618c94df000 rw-p 017e2000 08:01 272289927                  /usr/bin/etcd
5618c94df000-5618c9509000 rw-p 00000000 00:00 0
5618ca601000-5618ca622000 rw-p 00000000 00:00 0                          [heap]
7ff23c000000-7ff23c021000 rw-p 00000000 00:00 0
7ff23c021000-7ff240000000 ---p 00000000 00:00 0
┌──[root@liruilongs.github.io]-[~]
└─$ cat /proc/889/smaps | head  -10
c000000000-c000002000 rw-p 00000000 00:00 0
Size:                  8 kB
Rss:                   8 kB
Pss:                   8 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         8 kB
Referenced:            8 kB
Anonymous:             8 kB
┌──[root@liruilongs.github.io]-[~]
└─$

找到内存泄漏

有时候进程在使用完内存后不能正确地释放内存

如果进程是一个短生命周期的进程,如lsnetstat,这不是一个大问题,因为当一个进程退出时,它的所有内存都会被内核释放,

如果它是一个长时间运行的进程,问题可能会变得相当严重。

除了杀死并重新启动进程外,系统管理员在修复内存泄漏方面所能做的事情并不多。识别内存泄漏是系统管理员的职责之一。

要识别内存泄漏,可以使用通用工具,如ps,top,free,sar -rsar -R

但也有专门的工具,如valgrind工具memcheck。要在memcheck工具下运行进程,可以使用如下命令:valgrind --tool=memcheck program args-pro

┌──[root@liruilongs.github.io]-[~]
└─$ yum -y install valgrind
┌──[root@liruilongs.github.io]-[~]
└─$ rpm -ivh bigmem-7.0-1.r29766.x86_64.rpm
准备中...                          ################################# [100%]
正在升级/安装...1:bigmem-7.0-1.r29766              ################################# [100%]

有两种不同类型的内存泄漏需要注意。

  • 在第一种情况下,内存泄露,程序通过malloc(一种分配内存块的函数)等系统调用请求内存,但实际上并不使用这些内存。这将导致程序的虚拟大小增加(VIRT),以及/proc/meminfo中的Committed_AS(当前在系统上分配的内存量。提交的内存是进程分配的所有内存的总和,即使它还没有被它们“使用”)行,但没有使用实际的物理内存。常驻大小(顶部的RSS)保持(几乎)不变。

对应的参数我么可以通过top命令获取,top版本不同,对应的列名有些差别

top - 11:48:07 up 14 min,  1 user,  load average: 0.07, 0.10, 0.13
Tasks: 273 total,   2 running, 271 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 32931532 total, 23319380 free,  8643596 used,   968556 buff/cache
KiB Swap: 10485756 total, 10485756 free,        0 used. 23759444 avail MemPID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND889 etcd      20   0 10.559g  29332  11008 S   1.3  0.1   0:10.94 etcd8089 nginx     20   0   41636  12148   1588 S   1.3  0.0   0:03.35 redis-server
选项 说明
%MEM 进程使用内存量占系统物理内存的百分比
VIRT(v3.x)/SIME(v2.x) 进程虚拟内存使用总量。其中包括了应用程序分配到但未使用的全部内存
SWAP 进程使用的交换区(单位为KB)总量
RSS(v2.x)/RES(v3.x) 应用程序实际使用的物理内存总量
SHARE(v 2.x)/SHR(V 3.x) 可与其他进程共享的内存总量(单位为KB)
Mem:total,used,free 对物理内存来说,该项表示的是其总量、使用量和空闲量
swap:total,used,free 对交换分区来说,该项表示的是其总量、使用量和空闲量
buff/cache 用于缓冲区写人硬盘的数值和缓存的物理内存总量(单位为KB)
  • 在第二种情况下,内存溢出,程序实际使用它分配的内存。这将导致驻留大小与虚拟大小同步增加,从而导致实际的内存短缺。虽然泄漏虚拟内存不是一件好事,但泄漏驻留内存将对系统造成更大的影响。

看一个书里的Demo,在运行下面的应用程序时,请跟踪内存统计数据。

┌──[root@liruilongs.github.io]-[~]
└─$ watch 'free -k;grep Committed_AS /proc/meminfo'
Every 1.0s: free -k;grep Committed_AS /proc/meminfo                                             Fri Jul 29 20:47:24 2022total        used        free      shared  buff/cache   available
Mem:       32927488     4922536    27100148       22264      904804    27579940
Swap:      10485756           0    10485756
Committed_AS:    6853280 kB

首先安装valgrind,然后在valgrind下运行bigmem命令,要求分配256mib的常驻内存。现在bigmem请求256 MiB的虚拟内存。

┌──[root@liruilongs.github.io]-[~]
└─$ valgrind --tool=memcheck bigmem 256
==65614== Memcheck, a memory error detector
==65614== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==65614== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==65614== Command: bigmem 256
==65614==
Attempting to allocate 256 Mebibytes of resident memory...
Press <Enter> to exit

现在我们确定bigmem正在泄漏内存,泄露的原因是bigmem 256 导致的

==65614== Command: bigmem 256
==65614==
Attempting to allocate 256 Mebibytes of resident memory...

观察内存变化,Committed_AS ,即分配的虚拟内存增加了

Every 1.0s: free -k;grep Committed_AS /proc/meminfo                                             Fri Jul 29 21:00:49 2022total        used        free      shared  buff/cache   available
Mem:       32927488     5157320    26858220       22296      911948    27345124
Swap:      10485756           0    10485756
Committed_AS:    7281208 kB

交换空间调优

嗯,这里为了方面,我们把交换分区,交换文件统一称交换分区,或者交换空间

交换分区会增加系统上的有效内存量。当可用内存减少时,可以将不使用的页面换出到磁盘,以释放空间供其他用途。当再次需要这些页时,会发生一个严重的页错误,在使用它们之前,需要将它们再次从磁盘页到内存中。

这为我们提供了另一种方法来释放正在运行的系统上的内存,并有效地使用我们拥有的内存。swap的缺点是,与RAM相比,大多数存储设备都非常慢。在内存中进行换出和换出会显著降低系统的速度。

vmstat实用程序可以为您提供关于系统是否正在进行分页交换(“交换”)的信息。vmstat输出中的关键列是si/so等等。每秒换入的页面和每秒换出的页面。

┌──[root@liruilongs.github.io]-[~]
└─$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st1  0      0 27056464   3104 912452    0    0    28    20  259  310  4  2 94  0  00  0      0 27055988   3104 912456    0    0     0     0 1583 2023  6  2 93  0  01  0      0 27044272   3104 912480    0    0    24    36  845 1214  1  1 98  0  01  0      0 27055892   3104 912480    0    0     0     0 1880 2929  5  2 94  0  04  0      0 27056416   3104 912484    0    0     0     0 1730 2107  6  2 92  0  0
┌──[root@liruilongs.github.io]-[~]
└─$

将内存页面放置在交换分区中不会损害性能,它可以通过将不需要的页面移出RAM,以便将RAM用于更有用的事情来提高性能。

影响性能的是频繁地将页面移进移出交换区:si和so报告了这一点。一个系统应该有多少互换?人们提出了许多经验法则。如果你仔细想想,你需要交换两个基本的东西。

首先,移动你不需要在内存中所有时间的页面,以便RAM可以用于更好的事情。
其次,提供应急储备,避免出现内存不足的情况。空间大小很大程度上取决于系统有多少RAM以及你在计算机上做什么。

因为这比经验法则更难计算,人们倾向于根据粗略的估计来设置交换分区。Red Hat提供的基本指导如下:

系统内存最大支持 推荐最小交换分区
4gb 至少2gb
4gb ~ 16gb 至少4gb
16gb ~ 64gb 至少8gb
64gb ~ 256gb 至少16gb

系统内存和页缓存进程并不是系统内存的唯一消耗者。内核可以为自己的代码使用内存,或者以其他方式加速系统。

其中一种方法是页缓存。当您运行vmstat或free命令时,即使在拥有大量内存的系统上,也没有多少内存被标记为“空闲”。

如果系统最近执行了大量的存储I/O,这一点尤其正确。其中一个原因是页缓存。内核使用大部分未分配的内存作为缓存来存储从磁盘读取或写入的数据。

下一次需要数据时,可以从RAM而不是磁盘中获取数据。这通常会带来显著的性能改进,因为存储通常比物理内存慢得多。保留少量RAM以备短期请求使用;从长远来看,页缓存很容易被释放用于其他用途。

$ free -htotal        used        free      shared  buff/cache   available
Mem:            15G        6.2G        316M        801M        9.0G        8.0G
Swap:          6.0G        107M        5.9G
  • swpd :当前交换到硬盘的内存总量
  • free :未被操作系统或应用程序使用的物理内存总量
  • buff : 系统缓冲区大小(单位为KB),或用于存放等待保存到硬盘的数据的内存大小(单位为KB)。该存储区允许应用程序向Linux内核发出写调用后立即继续执行(而不是等待直到数据被提交到硬盘)
  • cache :用于保存之前从硬盘读取的数据的系统高速缓存或内存的大小(单位为KB)。如果应用程序再次需要该数据,内核可以从内存而非硬盘抓取数据,由此可提高性能
$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st1  0 110304 323928     80 9453436    0    0     0     2    0    0  1  1 97  1  00  0 110304 322804     80 9453436    0    0     0     0 16190 24907  1  2 96  1  00  0 110304 323900     80 9453492    0    0     0     4 15462 24521  1  1 97  1  00  0 110304 324232     80 9453492    0    0     0     0 14469 23522  1  1 97  1  00  0 110304 324476     80 9453540    0    0     0    16 15790 24329  1  2 95  1  0
[iomyw@iomywapp1 ~]$ 

温习一下交换空间的创建

交换分区

为你的系统额外添加一个512MiB的交换分区,此交换分区应在系统启动时自动挂载,不删除或以任何方式改动系统上原有的交换分区。

$ fdisk /dev/vdb  #需要分区的硬盘
.. ..
Command (m for help): n #添加新分区
Partition number (2-128, default 2):  # 直接回车(默认)
First sector (4194304-20971486, default 4194304):  # 直接回车(默认)
Last sector, *sectors or +size{K,M,G,T,P} (4194304-20971486,default 20971486): +512M
Created a new partition 2 of type 'Linux filesystem' and of size 512 MiB.
Command (m for help): w  # 保存分区表,并退出
The partition table has been altered.
Syncing disks.
$ partprobe /dev/vdb  # 刷新分区表
$ mkswap /dev/vdb2  # 格式化自建分区 vdb2
$ vim /etc/fstab
/dev/vdb2 swap swap defaults 0 0
$ swapon -a  # 启用 fstab 中的交换设备
$ swapon -s  # 查看交换分区信息

交换文件

创建一个 1G 的交换文件,系统启动时挂载

┌──[root@liruilongs.github.io]-[~]
└─$ lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0  200G  0 disk
├─sda1   8:1    0  150G  0 part /
└─sda2   8:2    0   10G  0 part [SWAP]
┌──[root@liruilongs.github.io]-[/dev]
└─$ dd if=/dev/sda1 of=/swap_LRL bs=1024 count=1048576
记录了1048576+0 的读入
记录了1048576+0 的写出
1073741824字节(1.1 GB)已复制,7.93545 秒,135 MB/秒
┌──[root@liruilongs.github.io]-[/dev]
└─$ mkswap  /swap_LRL
mkswap: /swap_LRL: warning: wiping old xfs signature.
正在设置交换空间版本 1,大小 = 1048572 KiB
无标签,UUID=ddecd087-fa65-4047-860b-609476a942f3
┌──[root@liruilongs.github.io]-[/]
└─$ vim /etc/fstab
┌──[root@liruilongs.github.io]-[/dev]
└─$ cat /etc/fstab  | grep LRL
/swap_LRL                    swap  swap     defaults        0 0
┌──[root@liruilongs.github.io]-[/]
└─$ swapon -a
swapon: /swap_LRL:不安全的权限 0644,建议使用 0600。
┌──[root@liruilongs.github.io]-[/]
└─$ swapon -s
文件名                          类型            大小    已用    权限
/dev/sda2                               partition       10485756        0       -2
/swap_LRL                               file    1048572 0       -3

调整内核交换的方式

当内核想要释放一页内存时,它需要在两种选择之间进行权衡。它可以从进程内存中交换一个页(放到交换分区),也可以从页缓存中删除一个页。为了做出这个决定,内核将执行以下计算:

swap_tendency = mapped_ratio/2 + distress + vm_swappiness

如果swap_tendency小于100,内核将从页缓存中回收一个页,如果大于或等于100,属于进程内存空间的页将符合交换条件。

在这个计算中,

  • mapped_ratio是使用的物理内存的百分比。
  • Distress是衡量内核在释放内存方面有多大困难的指标。它将从0开始,但如果需要更多的尝试来释放内存,它将增加(到最大100)。
  • vm_swappiness 值来自sysctl vm.swappiness即位于内核参数中的一个值,交换分区的采用频率

关于 vm.swappiness 调优。交换空间会严重影响系统。

  • 设置vm.swappiness切换到100,系统几乎总是倾向于换出页面,而不是从页面缓存中回收页面。这将使用更多的内存用于页缓存,这可以大大提高I/O繁重工作负载的性能,个人理解有更多内存空间用于buff和cache,所以I/O。
  • 设置为0将迫使系统尽可能少地进行交换。这可能会使系统响应速度更快,但以牺牲文件系统性能为代价.因为没有更多的内存用过buff和cache,大量的IO操作会造成IO阻塞。

这里我们简单温习一下,Linux内核参数如何调整

sysctl -a 查看所有内核参数:

┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ sysctl -a  # 查看所有调优参数
abi.vsyscall32 = 1
crypto.fips_enabled = 0
debug.exception-trace = 1
debug.kprobes-optimization = 1
debug.panic_on_rcu_stall = 0
dev.hpet.max-user-freq = 64
..............

cat 根据变量找对应参数文件

┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ sysctl -a | grep net.ipv4.ip_forward
net.ipv4.ip_forward = 1
net.ipv4.ip_forward_use_pmtu = 0
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ cd ../net/ipv
ipv4/ ipv6/
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ cd ../net/ipv4/
┌──[root@liruilongs.github.io]-[/proc/sys/net/ipv4]
└─$ cat ip_forward
1

设置内核参数:临时调整

/proc 目录下的数据是存放在内存中数据,每次重启就没了。

┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ cat swappiness
30
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ echo 40 > swappiness  ## 临时调整
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ cat swappiness
40

设置调优参数:永久调整

┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ echo "vm.swappiness = 20" >>  /etc/sysctl.conf  ## 永久调整
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ sysctl -p
net.ipv4.ip_forward = 1
vm.swappiness = 20
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$

下面是vm.swappiness 参数修改后vmstat的两个输出示例。

  • 第一个图显示了在内存压力下更倾向于交换的系统
  • 第二个图显示了更倾向于收缩页缓存的系统

交换分区和文件调优

交换分区性能在很大程度上受到交换分区的位置和数量的影响。

  • 在旋转的硬盘驱动器(机械硬盘)上,由于ZCAV效应,将交换分区放置在盘片的外缘比将其放置在中心附近将提供更好的吞吐量
  • SSD存储(固态硬盘)上放置交换空间可能会由于其较低的延迟和较高的吞吐量而导致更好的性能。

需要注意的是,如果您使用SSD作为交换分区,并且交换分区经常使用,那么您应该确保您的设备支持适当的磨损均衡(基于SLC-的存储可能优于基于MLC的存储,SLC的特点是寿命长,同样规格的MLC寿命比SLC要低,性能则相反)否则,设备可能会过早磨损。

使用mkswap创建多个交换空间。它们可能基于磁盘分区文件。由于内核映射交换文件的方式,只要交换文件没有碎片化交换文件和交换分区的性能应该大致相似

当使用多个交换分区时,可以使用挂载选项pri=value来指定每个空间的使用优先级。

$ cat /etc/fstab |grep -i swap|grep -i mpath
/dev/mapper/mpath18        swap                    swap    sw,pri=60        0 0
  • 与较低数字的交换分区将首先被填满,然后再移动到较高的数字。通过这种方式,更快的磁盘可以优先于较慢的磁盘
  • 当以相同的优先级激活多个交换分区时,将以轮询方式使用它们,从而减少每个交换分区的访问次数,从而获得更好的性能。

内存回收

Linux 物理内存需要不时地回收,以防止内存被填满,从而导致系统不可用。

脏内存和非活动内存回收

在我们了解内存是如何回收的之前,我们必须首先了解内存页可能处于的不同状态。这些不同的状态是:

  • Free: 页面可以立即分配,空闲的内存;
  • inactive Clean: 该页处于非活动使用状态,其内容与磁盘上的内容相对应,因为它已经被回写或自读取以来没有更改
  • inactive Dirty: 该页不是活跃使用,但该页的内容已经被修改,从磁盘读取后,还没有写回
  • Active: 该页面正在活跃使用中,并且不是被释放的候选页面

当需要分配新页面时,标记为 inactive Clean 的页面可以被视为空闲页面,但是如果拥有该页面的进程以后再次需要它,就会发生重大的页面错误

通过 /proc/meminfo 可以获得系统范围内内存分配的概览。我们可以处理的是Inactive(file)Dirty:。

$ cat /proc/meminfo 
MemTotal:       33011552 kB
MemFree:        19579840 kB
MemAvailable:   22517876 kB
Buffers:             304 kB
Cached:          3497240 kB
SwapCached:        35568 kB
Active:          8894452 kB
Inactive:        2717480 kB
Active(anon):    7107136 kB
Inactive(anon):  1869124 kB
Active(file):    1787316 kB
Inactive(file):   848356 kB
....................
Dirty:               716 kB
....................
  • Inactive(file):可以回收而不会对性能产生巨大影响的页面缓存内存,free 命令中 cache 部分的回收
  • Dirty:等待写回磁盘的内存,free 命令中 buff 部分的回收

匿名页面Inactive(anon)(与磁盘上的文件没有关联的那些页面)不能轻易释放,需要将其交换到磁盘以释放它们。

Inactive(file)Dirty的强制回收需要修改内核参数vm.drop_caches = 0

$ sysctl  -a | grep drop_caches
vm.drop_caches = 0

手动执行sync命令(sync 命令将所有未写的系统缓冲区写到磁盘中,包含已修改的 i-node、已延迟的块 I/O 和读写映射文件)

┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ cat drop_caches  #缓存处理
0
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ sync

通过执行 echo 3 > /proc/sys/vm/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
To free slab objects and pagecache:echo 3 > /proc/sys/vm/drop_caches
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ free -mtotal        used        free      shared  buff/cache   available
Mem:           3935         212        3357          16         366        3440
Swap:         10239           0       10239
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ echo 3 > /proc/sys/vm/drop_caches
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$ free -mtotal        used        free      shared  buff/cache   available
Mem:           3935         200        3575          16         159        3504
Swap:         10239           0       10239
┌──[root@liruilongs.github.io]-[/proc/sys/vm]
└─$

脏页写入调优

对于每个进程视图,你可以使用/proc/pid/maps.这个文件详细说明了分配给进程的每个内存段,包括共享/私有干净内存脏内存的大小。为了解析它,我们可以编写一个小的awk程序,如下所示:

cat /proc/$$/smaps | awk  '/Shared_Clean/{SHCL+=$2}/Shared_Dirty/{SHDT+=$2}/Private_Clean/{PRCL+=$2}/Private_Dirty/{PRDT+=$2}  END{ print "Total Clean:",SHCL +PRCL  print "Total Dirty:",SHDT  +PRDT
}'
┌──[root@liruilongs.github.io]-[/dev]
└─$ cat /proc/$$/smaps | awk  '
> /Shared_Clean/{SHCL+=$2}
> /Shared_Dirty/{SHDT+=$2}
> /Private_Clean/{PRCL+=$2}
> /Private_Dirty/{PRDT+=$2}
> END{
>   print "Total Clean:",SHCL +PRCL
>   print "Total Dirty:",SHDT  +PRDT
> }'
Total Clean: 1808
Total Dirty: 1536

必须将脏页写入磁盘,以防止内存被无法释放的页填满。由于内存是易失的(断电时内容会丢失),脏页也需要写入磁盘,以防止断电时数据丢失。

在内核中,将脏页写入磁盘是由per-BDI flush线程处理的,这些线程将在必要时创建。Per-BDI flush线程将在进程列表中显示为flush-MAJOR: MINOR

有几个内核参数控制per-BDI刷新线程何时开始将数据写入磁盘。这样内核就不会因为某个进程修改了另一个字节的内存而连续地多次写入同一个页面。

┌──[root@liruilongs.github.io]-[~]
└─$ sysctl  -a | grep dirty_
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 1500
┌──[root@liruilongs.github.io]-[~]
└─$

vm.dirty_ratio = 20 :产生写操作的进程整个系统内存中处于脏状态的百分比,是绝对的脏数据限制,内存里的脏数据百分比不能超过这个值,如果超过将阻塞IO直到写出脏页

vm.dirty_background_ratio = 10 :系统总内存和脏页的百分比,即内存可以填充“脏数据”的百分比。在这个百分比事开始,内核会在后台开始写数据

vm.dirty_bytes = 0vm.dirty_background_bytes = 0 这两个参数为上面参数的byte单位时的值

vm.dirty_expire_centisecs = 3000:脏数据在符合写入磁盘条件之前必须有多旧(以1/100秒为单位),即可以存活多久。这样内核就不会因为某个进程修改了一个字节的内存而连续地多次写入同一个页面。

vm.dirty_writeback_centisecs = 1500内核唤醒刷新线程pdflush/flush/kdmflush以写入数据的频率(1/100秒)。设置为0将完全禁周期性回写

大多数调优配置文件至少修改上述设置之一。调优规则可以遵循下面的策略

  • 设置较低的比率将导致更频繁但更短的写操作,这适合交互式系统
  • 设置较高的比率将导致更少但更大的写操作,导致总体开销更小,但可能导致交互式应用程序响应时间更长.适合执行非交互的I/O任务处理,比如大文件生成之类。

内存不足处理和“OOM killer(内存杀手)”

当脏页的数据太多,同时没有可用的页面时,内核试图回收内存来满足请求。如果不能及时回收足够的内存,就会出现内存不足OOM的情况。

对于系统的级别的OOM,默认情况下,系统将启动OOM killer,选择并杀死一个或多个进程以释放内存,以便满足请求。具体的记录日志是在/var/log/messages中,如果出现了Out of memory字样,说明系统曾经出现过OOM,

在Linux内核参数中,我们可以通过vm.panic_on_oom参数来设置遇到OOM的情况,启动OOM killer的策略

如果内核参数sysctl vm.panic_on_oom设置为1而不是0,内核将会发生panic,即直接摆烂,什么时候挂掉算什么时候。默认为0.即自动启动OOM killer

┌──[root@liruilongs.github.io]-[~]
└─$ sysctl vm.panic_on_oom
vm.panic_on_oom = 0

出现内存不足的情况,就没有很多合理的恢复选项。终止进程以释放内存、放弃并终止系统或死锁都是可能的选择。

为了确定OOM杀手应该杀死哪个进程,内核为每个进程保持一个运行不良评分,可以在/proc/pid/oom_score中查看。

systemd进程的值

[root@ecs-liruilong ~]# cat /proc/1/oom_score
0

分数越高,进程越有可能被OOM杀手杀死。许多因素被用来计算这个分数:

  • VM大小(不是RSS大小),
  • 进程所有子进程的累积VM大小,
  • nice值(正的nice值会给出更高的分数),
  • 总运行时间(较长的总运行时间会降低分数),
  • 运行用户(根进程会得到轻微的保护),
  • 进程执行直接硬件访问,分数也会降低。
  • 内核本身和PID1 (sysemd)是免疫的OOM杀手。

可调的/proc/PID/oom_adj可以用来手动调整oom_score。配置该pid进程被oom killer杀掉的权重oom_adj可以的值从-17到15,其中0表示不改变(默认),越高的权重,意味着更可能被oom killer选中,-17表示免疫(永远不会杀死)。

[root@ecs-liruilong ~]# cat /proc/1/oom_adj
0

如果你希望强制的执行OOM Killer

可以echo f > /proc/sysrq-trigger,但请记住,至少会有一个进程被杀死。

[root@ecs-liruilong ~]# echo f > /proc/sysrq-triggerMessage from syslogd@ecs-liruilong at Aug  1 14:32:18 ...kernel:[340648.118967] Kernel panic - not syncing: Out of memory: system-wide panic_on_oom is enabled

输出将被发送到dmesg。

[root@ecs-liruilong ~]# cat /var/log/dmesg

博文参考


  • 《 Linux性能优化 》
  • 《 Red Hat Performance Tuning 442 》
  • https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#
  • https://access.redhat.com/solutions/406773
  • https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/s2-proc-meminfo
  • Linux vm运行参数之(二):OOM相关的参数: https://blog.csdn.net/u011677209/article/details/52769225
  • Linux内核OOM机制的理解: https://blog.csdn.net/zhoutimo/article/details/52024487

Published by

风君子

独自遨游何稽首 揭天掀地慰生平