关于linux:配置-ZRAM实现-Linux-下的内存压缩零成本低开销获得成倍内存扩增

16次阅读

共计 18125 个字符,预计需要花费 46 分钟才能阅读完成。

因为我的项目需要,笔者最近在一台 Linux 服务器上部署了 ElasticSearch 集群,却发现运行过程中经常出现查问速度忽然升高的问题,登录服务器后发现是物理内存不足,导致机器频繁产生页面替换。因为只是长期内存需要,没有晋升配置的必要,而 ElasticSearch 中存储的数据次要是文本数据,因而笔者想到了应用 ZRAM 对内存进行压缩,以防止磁盘 IO 导致性能稳定,成果显著。介于互联网上对于 Linux 配置 ZRAM 的文章少之又少,本文将为读者介绍在 Linux 中配置与应用 ZRAM 的过程,并借此机会介绍 ZRAM 以及 Linux 内存局部的运作机制。
本文原载于未命名小站,由作者自己同步至 SegmentFault,转载请注明原作者博客地址或本链接,谢谢!

0x01 ZRAM 介绍

随着古代应用程序的多样化和复杂化倒退,那个已经只须要 640KB 内存就能运行市面上所有软件的时代曾经一去不复返,而比应用程序倒退更快的则是用户对多任务的需要。当初的支流操作系统都提供了内存压缩的性能,以保障沉闷应用程序领有尽可能多的可用内存:


macOS 从 OS X 10.9 之后默认开启内存压缩


Windows 从 Win10 TH2 之后默认开启内存压缩


大部分 Android 手机厂商都默认开启了内存压缩

仔细的读者会发现,在 Android 内存压缩的图中,并没有明确表明内存压缩,这是因为 Android(Linux)的内存压缩是依附 Swap 机制实现的,大部分状况下是应用 ZRAM 技术来模仿 Swap。

ZRAM 早在 2014 年就随同 Linux 3.14 内核合入主线,但因为 Linux 用处非常宽泛,这一技术并非默认启用,只有 Android 和少部分的 Linux 桌面发行版如 Fedora 默认启用了这一技术,以保障多任务场景下内存的正当分层存储。

0x02 ZRAM 运行机制

ZRAM 的原理是划分一块内存区域作为虚构的块设施(能够了解为反对通明压缩的内存文件系统),当零碎内存不足呈现页面替换时,能够将本来应该替换进来的页压缩后放在内存中,因为局部被『替换进来』的页失去了压缩,因而可用的物理内存就能随之变多。

因为 ZRAM 并没有扭转 Linux 内存模型的根本构造,因而咱们只能利用 Linux 中 Swap 的优先级能力,将 ZRAM 作为高优先级 Swap 对待,这也解释了为什么闪存比拟软弱的手机上会呈现 Swap,其本质还是 ZRAM。

随着局部手机开始应用真正的固态硬盘,也有将 Swap 放在硬盘上的手机,然而个别也都会优先应用 ZRAM。

因为这一运行机制的存在,ZRAM 能够设计得足够简略:内存替换策略交给内核、压缩算法交给压缩库,ZRAM 自身基本上只须要实现块设施驱动,因而具备极强的可定制性和灵活性,这也是 Windows、macOS 等零碎所无法比拟的。

Linux5.16 中对于 ZRAM 的源码只有不到 100KB,实现十分精简,对 Linux 驱动开发感兴趣的敌人也能够从这里开始钻研:linux/Kconfig at v5.16 · torvalds/linux

0x03 ZRAM 配置与自启动

互联网上对于 ZRAM 的材料少之又少,因而笔者打算从 Linux 主线中 ZRAM 相干源码和官网文档着手剖析。

1. 确认内核是否反对 & 有无启用 ZRAM

既然 ZRAM 是内核模块,就须要先查看以后 Linux 机器的内核是否存在这一模块。

在配置之前,须要读者先确认一下本人的内核版本是否在 3.14 以上,局部 VPS 因为仍旧应用 XenOpenVZ 等虚构 / 容器化技术,内核版本往往卡在 2.6,那么这样的机器是无奈开启 ZRAM 的。笔者用作示例的机器装置的 Linux 内核版本为5.10,因而能够启用 ZRAM:

但依据内核版本判断毕竟不牢靠,如 CentOS 7,尽管内核版本是 3.10,却反对 ZRAM,也有极少数发行版或嵌入式 Linux 为了升高资源占用,抉择不编译 ZRAM,因而咱们最好应用 modinfo 命令来检查一下有无 ZRAM 反对:

如图所示,尽管这台服务器是 CentOS 7.8,内核版本只到 3.10,却反对 ZRAM,因而应用 modinfo 命令是比拟无效的。

局部发行版会默认启用但不配置 ZRAM,咱们能够应用 lsmod 查看 ZRAM 是否启用:

lsmod | grep zram

2. 启用 ZRAM 内核模块

如果确定 ZRAM 没有被启用,咱们能够新建文件 /etc/modules-load.d/zram.conf,并在其中输出 zram。重启机器,再执行一次 lsmod | grep zram,当看到如下图所示的输入时,阐明 ZRAM 曾经启用,并能反对开机自启:

上文咱们提到,ZRAM 实质上是块设施驱动,那么当咱们输出 lsblk 会产生什么呢?

能够看到,其中并没有 zram 相干字眼,这是因为咱们须要先新建一个块设施。

关上 ZRAM 源码 中的 Kconfig 文件,能够找到如下阐明:

Creates virtual block devices called /dev/zramX (X = 0, 1, …).
Pages written to these disks are compressed and stored in memory
itself. These disks allow very fast I/O and compression provides
good amounts of memory savings.

It has several use cases, for example: /tmp storage, use as swap
disks and maybe many more.

See Documentation/admin-guide/blockdev/zram.rst for more information.

其中提到了一篇位于 Documentation/admin-guide/blockdev/zram.rst 的 阐明文档。

依据阐明文档,咱们能够应用 modprobe zram num_devices=1 的形式来让内核在启用 ZRAM 模块时开启一个 ZRAM 设施(个别只须要一个就够了),但这样开启设施的形式仍旧在重启后就会生效,并不不便。

好在 modprobe 的 阐明文档 中提到了 modprobe.d 的存在:modprobe.d(5) – Linux manual page。

持续浏览 modprobe.d 的文档,咱们会发现它次要用于 modprobe 时预约义参数,即只须要输出 modprobe zram,辅以 modprobe.d中的配置,就能够主动加上参数。因为咱们应用 modules-load.d 实现了 ZRAM 模块的开机自启,因而只须要在 modprobe.d 中配置参数即可。

依照上文所述的文档,新建文件/etc/modprobe.d/zram.conf,在其中输出options zram num_devices=1,即可配置一个 ZRAM 块设施,同样重启后失效。

3. 配置 zram0 设施

重启后输出lsblk,却发现所需的 ZRAM 设施仍旧没有呈现?

不必放心,这是因为咱们还没有为这个块设施建设文件系统,lsblk尽管名字听起来像是列出块设施,但实质上读取确实是 /sys 目录里的文件系统信息,再将其与 udev 中的设施信息比对。

浏览 udev 的文档:udev(7) – Linux manual page,其中提到 udev 会从 /etc/udev/rules.d 目录读取设施信息,依照文档中的批示,咱们新建一个名为 /etc/udev/rules.d/99-zram.rules 的文件,在其中写入如下内容:

KERNEL=="zram0",ATTR{disksize}="30G",TAG+="systemd"

其中,KERNEL属性用于指明具体设施,ATTR属性用于给设施传递参数,这里咱们须要浏览 zram 的文档,其中提到:

Set disk size by writing the value to sysfs node ‘disksize’. The value can be either in bytes or you can use mem suffixes. Examples:

# Initialize /dev/zram0 with 50MB disksize
echo $((50*1024*1024)) > /sys/block/zram0/disksize

# Using mem suffixes
echo 256K > /sys/block/zram0/disksize
echo 512M > /sys/block/zram0/disksize
echo 1G > /sys/block/zram0/disksize

Note: There is little point creating a zram of greater than twice the size of memory since we expect a 2:1 compression ratio. Note that zram uses about 0.1% of the size of the disk when not in use so a huge zram is wasteful.

即该块设施承受名为 disksize 的参数,且不倡议调配内存容量两倍以上的 ZRAM 空间。笔者的 Linux 环境领有 60G 内存,思考到理论应用状况,设置了 30G 的 ZRAM,这样一来现实状况下就能取得 60G - (30G / 2) + 30G > 75G 以上的内存空间,曾经足够应用(为什么要这样计算?请浏览『ZRAM 监控』一章)。读者能够依据本人的理论状况抉择 ZRAM 空间大小,一般来说一开始能够设置小一些,不够用再扩充。

TAG属性用于标记设施类型(设施由谁治理),依据 systemd.device 的文档:systemd.device(5) – Linux manual page,大部分块设施和网络设备都倡议标记 TAG 为 systemd,这样systemd 就能够将这个设施视作一个 Unit,便于管制服务的依赖关系(如块设施加载胜利后再启动服务等),这里咱们也将其标记为systemd 即可。


能够应用如 dev-zram0.device 或者 dev-sda1.device 等形式获取设施的 Device Unit

配置完结后,再次重启 Linux,就能在 lsblk 命令中看到zram0 设施了:

4. 将 zram0 设施配置为 Swap

获取到了一个 30G 大小的 ZRAM 设施,接下来须要做的就是将这个设施配置为 Swap,有教训的读者应该曾经猜到接下来的操作了:

mkswap /dev/zram0
swapon /dev/zram0

是的,将 zram0 设施配置为 Swap 和将一个一般设施 / 分区 / 文件配置为 Swap 的形式是截然不同,但该如何让这一操作开机主动执行呢?

首先想到的天然是应用 fstab,但好巧不巧,启用 ZRAM 内核模块应用的modules-load.d 也难逃 Systemd 的魔爪:modules-load.d(5) – Linux manual page。既然从一开始就上了 Systemd 的贼船,那就贯彻到底吧!

在 Systemd 的体系下,开机自启的命令能够被注册为一个 Service Unit,咱们新建一个文件/etc/systemd/system/zram.service,在其中写入如下内容:

[Unit]
Description=ZRAM
BindsTo=dev-zram0.device
After=dev-zram0.device

[Service]
Type=oneshot
RemainAfterExit=true
ExecStartPre=/sbin/mkswap /dev/zram0
ExecStart=/sbin/swapon -p 2 /dev/zram0
ExecStop=/sbin/swapoff /dev/zram0

[Install]
WantedBy=multi-user.target

接下来运行 systemctl daemon-reload 重载配置文件,再运行 systemctl enable zram --now,如果没有呈现报错,能够运行swapon -s 查看 Swap 状态,如果看到存在名为/dev/zram0 的设施,祝贺你!当初 ZRAM 就曾经配置实现并能实现自启动了~


这里为了帮忙读者理解 ZRAM 和 Systemd 的原理,因而采取了全手动的配置形式。如果读者感觉比拟麻烦,或有大规模部署的需要,能够应用 systemd/zram-generator: Systemd unit generator for zram devices,大部分默认启用 ZRAM 的发行版(如 Fedora)都应用了这一工具,编写配置文件后运行 systemctl enable /dev/zram0 --now 即可启用 ZRAM。

5. 配置双层 Swap(可选)

上一节咱们配置了 ZRAM,并将其设置为了 Swap,但此时 ZRAM 仍旧是不失效的。为什么呢?眼尖的读者应该发现了,/.swapfile的优先级高于/dev/zram0,这导致当 Linux 须要替换内存时,依旧会优先将页换入/.swapfile,而非 ZRAM。

解决这个问题能够通过两种形式:禁用 Swapfile,或者升高 Swapfile 的优先级,这里为了防止 ZRAM 耗尽后呈现 OOM 导致服务掉线,咱们采取后者,即配置双层 Swap,当高优先级的 ZRAM 耗尽后,会持续应用低优先级的 Swapfile。

咱们关上 Swapfile 的配置文件(笔者的配置文件在 /etc/fstab 中),减少如下图所示参数:

如果应用其余形式配置 Swapfile(如 Systemd),只有保障执行 swapon 时携带 -p 参数即可,数字越低,优先级越低。对于 ZRAM 同理,如上文的 zram.service 中就配置 ZRAM 的优先级为 2。

设置后重启 Linux,再次执行 swapon -s 查看 Swap 状态,保障 ZRAM 优先级高于其余 Swap 优先级即可:

0x04 ZRAM 监控

启用 ZRAM 后,咱们该如何查看 ZRAM 的理论效用,如压缩前后大小,以及压缩率等状态呢?

最间接的方法天然是查看驱动的 源码,和 文档,能够发现函数 mm_stat_show() 定义了 /sys/block/zram0/mm_stat 文件的输入后果,从左到右别离代表:

$ cat /sys/block/zram0/mm_stat
orig_data_size - 以后压缩前大小 (Byte)
4096

compr_data_size - 以后压缩后大小 (Byte)
74

mem_used_total - 以后总内存耗费,蕴含元数据等 Overhead(Byte)12288

mem_limit - 以后最大内存耗费限度(页)0

mem_used_max - 历史最高内存用量(页)1223118848

same_pages - 以后雷同(可被压缩)的页
0

pages_compacted - 历史从 RAM 压缩到 ZRAM 的页
50863

huge_pages - 以后无奈被压缩的页(巨页)0

该文件适宜输入到各种监控软件进行监控,但无论是 Byte 还是页,这些裸数值仍旧不便浏览,好在 util-linux 包提供了一个名为 zramctl 的工具(和 systemctl 其实是雷锋与雷峰塔的关系),在装置 util-linux 后执行zramctl,即可取得下图所示的后果:

依据上文 mm_stat 的输入,能够类推每项数值的含意,或者咱们能够找到 zramctl 的 源码,理解每项输入的含意与单位:

static const struct colinfo infos[] = {[COL_NAME]      = {"NAME",      0.25, 0, N_("zram device name") },
    [COL_DISKSIZE]  = {"DISKSIZE",     5, SCOLS_FL_RIGHT, N_("limit on the uncompressed amount of data") },
    [COL_ORIG_SIZE] = {"DATA",         5, SCOLS_FL_RIGHT, N_("uncompressed size of stored data") },
    [COL_COMP_SIZE] = {"COMPR",        5, SCOLS_FL_RIGHT, N_("compressed size of stored data") },
    [COL_ALGORITHM] = {"ALGORITHM",    3, 0, N_("the selected compression algorithm") },
    [COL_STREAMS]   = {"STREAMS",      3, SCOLS_FL_RIGHT, N_("number of concurrent compress operations") },
    [COL_ZEROPAGES] = {"ZERO-PAGES",   3, SCOLS_FL_RIGHT, N_("empty pages with no allocated memory") },
    [COL_MEMTOTAL]  = {"TOTAL",        5, SCOLS_FL_RIGHT, N_("all memory including allocator fragmentation and metadata overhead") },
    [COL_MEMLIMIT]  = {"MEM-LIMIT",    5, SCOLS_FL_RIGHT, N_("memory limit used to store compressed data") },
    [COL_MEMUSED]   = {"MEM-USED",     5, SCOLS_FL_RIGHT, N_("memory zram have been consumed to store compressed data") },
    [COL_MIGRATED]  = {"MIGRATED",     5, SCOLS_FL_RIGHT, N_("number of objects migrated by compaction") },
    [COL_MOUNTPOINT]= {"MOUNTPOINT",0.10, SCOLS_FL_TRUNC, N_("where the device is mounted") },
};

局部未默认输入的数值能够通过 zramctl --output-all 输入:

这个工具的输入后果混同了 Byte 和页,混同了历史最高、累计和以后的数值,且将不设置的参数(如内存限度)显示为 0B,因而输入后果仅作为参考,可读性仍旧不高,一般来说只用理解 DATACOMPR字段即可。

联合 zramctlmm_stat的输入,不难发现咱们配置的 ZRAM 大小其实是未压缩的大小,而非是压缩后的大小,后面咱们提到了一个算法,当 ZRAM 大小为 30GB 且压缩率为 2:1 时,能够取得 60G - (30G / 2) + 30G > 75G 的可用内存,这就是假如了 30GB 的未压缩数据能够压缩到 15G,占用 15G 物理内存空间,即60G - (30G / 2),而后再加上 ZRAM 能存储最大的内存数据 30G 计算出来。

计算压缩率的形式为 DATA / COMPR,以引言中提到 ElasticSearch 的工作负载为例,如下图所示,默认压缩率为1.1G / 97M = 11.6。思考到 ElasticSearch 的数据次要是纯文本,而 JVM 的机制也是提前向零碎申请内存,能达到这样的压缩率曾经十分令人满意了。

0x05 ZRAM 调优

只管 ZRAM 的设置非常简单,其仍然提供了大量可配置项供用户调整,如果在默认配置 ZRAM 后仍旧感觉不称心,或者想要进一步挖掘 ZRAM 的后劲,就须要对其进行优化。

1. 抉择最适宜的压缩算法

如上图所示,ZRAM 目前的默认压缩算法个别是 lzo-rle,但其实 ZRAM 反对的压缩算法有很多,咱们能够通过 cat /sys/block/zram0/comp_algorithm 获取反对的算法,以后启用的算法被[] 括起来:

压缩是一个工夫换空间的操作,也就意味着这些压缩算法并不存在相对优劣,只存在不同状况下的取舍,有的压缩率高、有的带宽大、有的 CPU 耗费少……在不同的硬件上,不同的抉择也会遇到不同的瓶颈,因而只有进行实在的测试,能力帮忙抉择最适宜的压缩算法。

为了不影响生产环境,且防止 Dump 内存引起数据泄露,笔者应用另一台雷同硬件的虚拟机进行测试,该虚拟机有 2G 的 RAM 和 2G 的 ZRAM,同样运行着 ElasticSearch(但数据量较小),而后应用 stress 工具对内存施加 1GB 的压力,此时不沉闷的内存(即 ElasticSearch 中的局部数据)则会被换出到 ZRAM 中,如图所示:

应用如下脚本取得其中 512M 内存,将其 Dump 到文件中:

pv -s 512M -S /dev/zram0 > "./test-memory.bin"

接下来,笔者开释掉负载,空出足够的物理内存,在 root 用户下应用批改自 better_benchmarks.bash – Pastebin.com 的脚本对不同算法和不同page_cluster(下一节会提到)进行测试:LuRenJiasWorld/zram-config-benchmark.sh。

测试过程中咱们会看到新的 ZRAM 设施被建设,这是因为脚本会将之前 Dump 的内存写入到这些设施,每一个设施都配置了不同的压缩算法:

在应用此脚本之前,倡议先浏览一遍脚本内容,依据本人的须要抉择不同的内存大小、ZRAM 设施大小、测试算法和参数等配置。依据配置不同,测试可能会继续 20~40 分钟,这个过程中不倡议进行任何其余操作,防止干扰测试后果。

测试完结后,应用普通用户运行脚本,获取 CSV 格局的测试后果,将其保留成文件,导入到 Excel 中即可查看后果。以下是笔者环境下的测试数据:

algo   | page-cluster| "MiB/s"           | "IOPS"        | "Mean Latency (ns)"| "99% Latency (ns)"
-------|-------------|-------------------|---------------|--------------------|-------------------
lzo    | 1           | 9130.553415506836 | 1168710.932773| 5951.70401         | 27264
lzo    | 2           | 11533.120699842773| 738119.747899 | 9902.73897         | 41728
lzo    | 3           | 13018.8484358584  | 416603.10084  | 18137.20228        | 69120
lzo    | 0           | 6360.546916032226 | 1628299.941176| 3998.240967        | 18048
zstd   | 1           | 4964.754078584961 | 635488.478992 | 11483.876873       | 53504
zstd   | 2           | 5908.13019465625  | 378120.226891 | 19977.785468       | 85504
zstd   | 3           | 6350.650210083984 | 203220.823529 | 37959.488813       | 150528
zstd   | 0           | 3859.24347590625  | 987966.134454 | 7030.453683        | 35072
lz4    | 1           | 11200.088793330078| 1433611.218487| 4662.844947        | 22144
lz4    | 2           | 15353.485367975585| 982623        | 7192.215964        | 30080
lz4    | 3           | 18335.66823135547 | 586741.184874 | 12609.004058       | 45824
lz4    | 0           | 7744.197593880859 | 1982514.554622| 3203.723399        | 9920
lz4hc  | 1           | 12071.730649291016| 1545181.588235| 4335.736901        | 20352
lz4hc  | 2           | 15731.791228991211| 1006834.563025| 6973.420236        | 29312
lz4hc  | 3           | 19495.514164259766| 623856.420168 | 11793.367214       | 43264
lz4hc  | 0           | 7583.852120536133 | 1941466.478992| 3189.297915        | 9408
lzo-rle| 1           | 9641.897805606446 | 1234162.857143| 5559.790869        | 25728
lzo-rle| 2           | 11669.048803505859| 746819.092437 | 9682.723915        | 41728
lzo-rle| 3           | 13896.739553243164| 444695.663866 | 16870.123274       | 64768
lzo-rle| 0           | 6799.982996323242 | 1740795.689076| 3711.587765        | 15680
842    | 1           | 2742.551544446289 | 351046.621849 | 21805.615246       | 107008
842    | 2           | 2930.5026999082033| 187552.218487 | 41516.15757        | 193536
842    | 3           | 2974.563821231445 | 95185.840336  | 82637.91368        | 366592
842    | 0           | 2404.3125984765625| 615504.008403 | 12026.749364       | 63232

从上表咱们能够获取到十分多信息,表头从左到右别离为:

  • 压缩算法
  • page-cluster参数(下一节解释其含意)
  • 吞吐带宽(越大越好)
  • IOPS(越大越好)
  • 均匀提早(越小越好)
  • 99 分位提早(越小越好)(用于获取提早最大值)。

因为不同 page-cluster 状况下的压缩率是一样大的,该表格未能反映此状况,咱们须要执行以下命令获取指定工作负载下每个压缩算法的压缩率信息:

$ tail -n +1 fio-bench-results/*/compratio
==> fio-bench-results/842/compratio <==
6.57

==> fio-bench-results/lz4/compratio <==
6.85

==> fio-bench-results/lz4hc/compratio <==
7.58

==> fio-bench-results/lzo/compratio <==
7.22

==> fio-bench-results/lzo-rle/compratio <==
7.14

==> fio-bench-results/zstd/compratio <==
9.48

根据以上衡量标准,笔者选出了该零碎 + 该工作负载上最佳的配置(名称为 压缩算法 -page_cluster):

吞吐量最大:lz4hc-3 (19495 MiB/s)
提早最低:lz4hc-0 (3189 ns)
IOPS 最高:lz4-0 (1982514 IOPS)
压缩率最高:zstd (9.48)
综合最佳:lz4hc-0

依据工作负载和需要的不同,读者能够抉择适宜本人的参数,也能够联合下面提到的多级 Swap,将 ZRAM 进一步分层,应用最高效的内存作为高优先级 Swap,压缩率最高的内存作为中低优先级 Swap。

如果测试机和生产环境的架构 / 硬件存在差别,能够将测试过程中导出的内存拷贝到生产环境,前提是两者运行雷同的工作负载,否则测试内存无参考价值。

2. 配置 ZRAM 调优参数

上一张咱们选出了综合最佳的 ZRAM 算法和参数,接下来就将其利用到咱们的生产环境中。

2.1. 配置压缩算法,获得最佳压缩率

首先将压缩算法从默认的 lzo-rle 切换为 lz4hc,依据 ZRAM 的文档,只须要将压缩算法写入/sys/block/zram0/comp_algorithm 即可,思考到咱们配置 /sys/block/zram0/disksize 时的操作,咱们从新编辑 /etc/udev/rules.d/99-zram.rules 文件,将其内容批改为:

- KERNEL=="zram0",ATTR{disksize}="30G",TAG+="systemd"
+ KERNEL=="zram0",ATTR{comp_algorithm}="lz4hc",ATTR{disksize}="30G",TAG+="systemd"

须要留神的是,必须先指定压缩算法,再指定磁盘大小,无论是在配置文件中还是间接 echo 参数到 /sys/block/zram0 设施上,都须要依照文档的程序进行操作。

重启机器,再次执行cat /sys/block/zram0/comp_algorithm,就会发现以后压缩算法变成了lz4hc

2.2. 配置 page-cluster,防止内存带宽和 CPU 资源的节约

配置 comp_algorithm 后,接下来咱们来配置page-cluster

下面卖了不少关子,简略来说,page-cluster的作用就是每次从替换设施读取数据时多读 2^n 页,一些块设施或文件系统有簇的概念,读取数据也是按簇读取,假如内存页大小为 4KiB,而每次读取的一簇数据为 32KiB,那么把多读出来的数据也换回内存,就能避免浪费,缩小频繁读取磁盘的次数,这一点在 Linux 的文档中也有提到:linux/vm.rst · torvalds/linux。默认的 page-cluster 大小为 3,即每次会从磁盘读取 4K*2^3=32K 的数据。

理解了 page-cluster 的原理后,咱们会发现 ZRAM 并不属于传统的块设施,内存控制器默认设计就是按页读取,因而这一实用于磁盘设施的优化,在 ZRAM 场景下却是负优化,反而会导致过早涉及内存带宽瓶颈和 CPU 解压缩瓶颈而导致性能降落,这也就能够解释为什么下面的表格中随着 page-cluster 的晋升,吞吐量同样晋升,而 IOPS 却变得更小,如果内存带宽和 CPU 解压缩不存在瓶颈,那么 IOPS 实践上应该放弃不变。

思考到无论是实践上,还是理论测试,page-cluster都是一个多余的优化,咱们能够间接将其设置为 0。间接编辑/etc/sysctl.conf,在结尾新增一行:

vm.page-cluster=0

运行sysctl -p,即可让该设置失效,无需重启。

2.3. 让零碎更激进地应用 Swap

下面性能测试中,lz4hc压缩参数下最小的均匀读取提早是 3203ns,即 0.003ms。与之比照,在笔者的电脑上,间接读取内存的提早在 100ns 左右(0.0001ms,局部硬件能够到 50ns),SSD 读取一次的提早个别是 0.05ms,而机械硬盘读取一次的提早在 1ms~1000ms 之间。


不同层级存储设备读写的相对工夫与绝对工夫比照,能够看出它们之间指数级的差异。图片来自 cpu 内存访问速度,磁盘和网络速度,所有人都应该晓得的数字 | las1991,最先出自于 Jeff Dean 的 PPT:Dean keynote-ladis2009。
也能够参考 rule-of-thumb-latency-numbers-letter.pdf 获取更具体的信息。

0.003ms 比照 1ms,是 300 多倍的差异,而 0.0001ms 比照 0.003ms 只有 30 多倍,这也就意味着 ZRAM 相比拟 Swap 而言,更像是 RAM,既然如此,咱们天然能够让 Linux 更激进地应用 Swap,而且让 ZRAM 尽可能早地取得更多可用内存空间。

这里咱们须要理解两个常识:swappiness和内存碎片。

Swappiness 是一个内核参数,用于决定『内核有多偏向于在内存不足时换出到 Swap』,值越大偏向越大,对于 Swappiness,下一章会持续解释其作用,当初咱们能够先认为这个值在 ZRAM 配置下越大越好,就算是 100(%) 也无所谓。

内存碎片则是一种景象,呈现在长期运行的操作系统中,体现为内存有空余,但却因为没有足够大的间断空余导致无奈申请到内存。无论该零碎有无 MMU,都会不同水平在物理内存 / 虚拟内存 /DMA 中遇到没有更多的间断空间调配给所需程序这一问题(已经可用的间断空间都因为内存被无规律的开释后变得不间断,那些遗留且散落在内存空间中的数据就是所谓的内存碎片)。一般来说 Linux 会在呈现内存碎片且认为 有必要进行碎片整顿 时对内存进行整顿。

为防止内存碎片,Linux 内存的调配和整顿都遵循 Buddy System,即对内存页进行 2^n 次方分级,从 1 到 1024 总共 11 级,每一级都能够视为一组页框。例如一个过程须要 64 页的内存,操作系统会先从 64 页框的链表中寻找有无闲暇,如果没有,再去 128 页框的链表中寻找,如果有,则将 128 页框的低端 64 页(左侧)调配进来,再将高端 64 页指向一个新的 64 页框链表。当开释内存时,因为申请的内存都是向页框低端对齐,当内存开释后,刚好也能够开释出一整个页框。


图片来自 Buddy system 搭档分配器实现 – youxin – 博客园

Buddy System 的设计极为奇妙,实现极为优雅,感兴趣的读者能够浏览 Buddy system 搭档分配器实现 – youxin – 博客园 进一步理解其设计,此处不做过多赘述。Linux 中还有大量这样奇妙的设计,多理解有助于晋升编程品尝,笔者也处于学习过程中,心愿能和读者共同进步。

下面提到内存碎片个别是在内存失去充沛且长期利用后会呈现的问题,而 ZRAM 则会导致可用内存有余时呈现换页,这两个升高性能的操作基本上是同时产生的,也导致系统压力升高之后容易霎时呈现无响应景象,且可能因为来不及对内存进行碎片解决,导致 ZRAM 无奈获取内存,进一步造成零碎不稳固。

因而咱们须要做的另一个操作就是尽可能让内存有序,尽可能在所需级别页框有余时立刻触发碎片整顿,充沛压迫内存。Linux 中配置内存碎片整顿条件的参数是vm.extfrag_threshold,当页框碎片指数低于该阈值则会触发碎片整顿。碎片指数的计算方法能够参考 对于 linux 内存碎片指数 – _备忘录 – 博客园。总的来说,

综上所述,咱们持续在 /etc/sysctl.conf 文件后增加以下两行:

# 默认是 500(介于 0 和 1000 之间的值)vm.extfrag_threshold=0
# 默认是 60
vm.swappiness=100

这两个参数是笔者依据服务器内存应用状况、ZRAM 应用状况和碎片整顿机会的监控信息来决定的,不肯定适宜所有场景,不设置这些参数也没有任何影响,如果不确定或不想做试验,不设置也没关系,仅供理解即可。

须要留神的是,对于 swappiness 参数,互联网上存在大量谬论,如 swappiness 最大值为 100,大多都将这个值当做一个百分数对待,接下来笔者将会解释为什么这个论点是谬误的。

3. 为什么 ZRAM 没有被应用到

引言中提到的例子曾经是一年之前的事件,过后在更小的圈子内做过一次分享。记得分享完结第二天,有敌人找上我,问为什么他配置了 ZRAM,且配置 swappiness 为 100,ZRAM 却一点没有被应用到。

当初想想看,其实这是一个十分广泛的问题,因而只管这一节的题目为『为什么 ZRAM 没有被应用到』,接下来笔者想要分享确实是『到底在什么时候才会呈现页面替换』这一问题。

互联网上对于如何配置软件,往往存在大量的 Myth,尤其是与性能优化相干的场景。开源软件尚且如此,闭源软件如 Windows 就更不用说,过期和虚伪信息,夹杂着安慰剂效应成为了现今性能优化指南的一大特色,这些 Myth 不肯定是作者主观成心,但背地也反映出了作者和读者都存在的惰性思维。

在理解 swappiness 之前,咱们先理解 Linux 的内存回收机制。Linux 和其余古代操作系统一样,会偏向于提前应用内存,当咱们运行 free -m 命令时,能看到以下参数,它们别离代表的是:

  • total: 总内存量
  • used: 过程所应用的内存量(局部零碎下也蕴含 shared+buffer/cache,此时就是被操作系统应用的内存量)
  • free: 没有被操作系统应用的内存量
  • shared: 过程间共享内存量
  • buff/cache: 缓冲区和缓存区
  • available: 过程能够残余应用的最大内存量

当咱们说 Android 总是有多少内存吃多少时,咱们通常指的是内存中 free 数量较少,这是绝对于 Windows 而言,因为 Windows 将 free 定义为了 Linux 下 available 的含意,这并不代表 Windows 没有 buff/cache 的设计:


微软式中文经典操作:混同 Free 和 Available,有机会笔者会分享一下 Windows 下各项内存参数的实在含意(比 Linux 凌乱许多,而且很难自圆其说)。

简略来说,buffer是写入缓存,而 cache 是读取缓存,这两者统称文件页,Linux 会定期刷新写入缓存,但通常不会定期刷新读取缓存,这些缓存会占据内存的可用空间,当过程有新的内存需要时,Linux 依旧会从 free 中调配给过程,直到 free 的内存无奈满足过程需要,此时 Linux 会抉择回收文件页,供过程应用。

但如果将所有的文件页都给过程应用,就相当于 Linux 的文件缓存机制间接生效,只管满足了内存应用,对 IO 性能却造成了影响,因而除了回收文件页以外,Linux 还会抉择将页换出,即采纳 Swap 机制,将临时没有应用的内存数据(通常是匿名页,即堆内存)保留在磁盘中,称作换出。

一般来说,Linux 只有在内存缓和时才会抉择回收内存,那么该如何掂量缓和呢?达到紧张状态后,又以什么规范退出紧张状态呢?这就要提到 Linux 的一个设计:内存水位线(Watermark)。

负责回收内存的内核线程 kswapd0 定义了一套掂量内存压力的规范,即内存水位线,咱们能够通过 /proc/zoneinfo 文件获取水位线参数:

这个文件和上文提到的 /proc/buddyinfo 有一个共性,即它们都将内存进行了分区,如 DMA、DMA32、Normal 等,具体能够浏览 Linux 内存管理机制简析 – 舰队 – 博客园,此处不再赘述,x86_64 架构下,这里咱们只看 Normal 区域的内存即可。

$ cat /proc/zoneinfo
...
Node 0, zone   Normal
  pages free     12273184   # 闲暇内存页数,此处大略闲暇 46GB
        min      16053      # 最小水位线,大略为 62MB
        low      1482421    # 低水位线,大略为 5.6GB
        high     2948789    # 高水位线,大略为 11.2GB
...
      nr_free_pages 12273184         # 同 free pages
      nr_zone_inactive_anon 1005909  # 不沉闷的匿名页
      nr_zone_active_anon 60938      # 沉闷的匿名页
      nr_zone_inactive_file 878902   # 不沉闷的文件页
      nr_zone_active_file 206589     # 沉闷的文件页
      nr_zone_unevictable 0          # 不可回收页
...

须要留神的是,因为 NUMA 架构下,每个 Node 都有本人的内存空间,因而如果存在多个 CPU,每个内存区域的水位线和统计信息是独立的。

首先咱们来解释水位线,这里存在四种状况:

  1. pages free 小于 pages min,阐明所有内存耗尽,此时阐明内存压力过大,会开始触发同步回收,体现为零碎卡死,分配内存被阻塞,开始尝试碎片整顿、内存压缩,如果都不见效,则开始执行 OOM Killer,直到pages free 大于pages high
  2. pages freepages minpages low 之间,阐明内存压力较大,kswapd0线程开始回收内存,直到 pages free 大于pages high
  3. pages freepages lowpages high 之间,阐明内存压力个别,个别不会执行操作。
  4. pages free 大于pages high,阐明内存基本上没有压力,无需回收内存。

接下来咱们介绍 swappiness 这个参数的作用。从下面的过程能够得悉,swappiness参数决定了 kswapd0 线程回收内存的策略。因为存在两类可被回收的内存页:匿名页和文件页,swappiness决定的则是匿名页相比拟文件页被换出的比率,因为文件页的换出是间接将其回写到磁盘或销毁,这一参数也能够被解释为『Linux 在 内存不足 时回收匿名页的激进水平』。

同世间的其余事物一样,不存在相对的好或坏,也很难给这些事物采纳百分制打分,但咱们能够通过利害关系来评估做一件事件的性价比,以最小的代价和最高的收益来实现目标,Linux 同样如此。依据 mm/vmscan 局部的 源码,咱们能够发现 Linux 将 swappiness 带入如下算法:

anon_cost = total_cost + sc->anon_cost;
file_cost = total_cost + sc->file_cost;
total_cost = anon_cost + file_cost;

ap = swappiness * (total_cost + 1);
ap /= anon_cost + 1;

fp = (200 - swappiness) * (total_cost + 1);
fp /= file_cost + 1;

其中 anon_costfile_cost别离指对匿名页和文件页进行 LRU 扫描的难度。

假如过程须要内存,扫描两种内存页难度雷同,且残余内存小于低水位线,即当 swappiness 默认为 60 时,Linux 会抉择回收文件页更多,当 swappiness 等于 100 时,Linux 会平等回收文件页和匿名页,而当 swappiness 大于 100,Linux 会抉择回收匿名页更多。

依据 Linux 的 文档,如果应用 ZRAM 等比传统磁盘更快的 IO 设施,能够将 swappiness 设置到超过 100 的值,其计算方法如下:

For example, if the random IO against the swap device is on average 2x faster than IO from the filesystem, swappiness should be 133 (x + 2x = 200, 2x = 133.33).

通过这样的解释,置信读者应该既了解了 swappiness 的含意,也了解了为什么零碎负载不高时,不管怎么设置swappiness,新申请的内存空间仍旧不会被写入 ZRAM 中。

只有当内存不足,且 swappiness 较高时,配置 ZRAM 才会有比拟可观的收益,但 swappiness 也不宜过高,否则文件页将会驻留在内存中,造成匿名页大量沉积在较慢的 ZRAM 设施中,反而升高性能。而且因为 swappiness 无奈辨别不同级别的 Swap 设施,如果应用了 ZRAM 和 Swapfile 分层,也须要将这一参数设置得更激进一些,通常来说 100 就是最合适的值。

4. 没有银弹

通过上文的具体解释,心愿读者曾经对 ZRAM 和 Linux 的内存模型有了较为具体和零碎的了解。那么 ZRAM 是万能的吗?答案同样是否定的。

如果 ZRAM 是万能的,那么所有的发行版都应该默认启用 ZRAM,但状况并非如此。先不提 ZRAM 的配置取决于零碎硬件与架构,不存在一招鲜吃遍天的参数,关键问题在 ZRAM 的性能。咱们回到后面做性能测试输入的那张表格中,拿最好的 ZRAM 参数和间接拜访内存盘的数据做比照,咱们会失去以下后果:

algo   | page-cluster| "MiB/s"           | "IOPS"        | "Mean Latency (ns)"| "99% Latency (ns)"
-------|-------------|-------------------|---------------|--------------------|-------------------
lz4hc  | 0           | 7583.852120536133 | 1941466.478992| 3189.297915        | 9408
raw    | 0           | 22850.29382917787 | 6528361.294582| 94.28362           | 190

能够看到,在性能方面,就算是最佳 ZRAM 配置,相比间接拜访内存,仍旧存在三倍以上的性能差距,更别提 30 倍的访存提早(Apple Silcon 芯片之所以可能靠较低的功耗在各项性能上持平甚至当先 Intel,访存以及内存 / 显存复制的低提早 + 夸大的内存带宽功不可没)。

胜利的性能优化,绝不是一劳永逸的配置,如果真的那么简略,为什么软件不出厂就优化好呢?笔者认为,性能优化须要对架构和原理的充沛理解,对需要和指标的提前预估,以及一直尝试、试验和比照。

这三者缺一不少,也并非相互孤立,往往做出最优抉择须要在其中进行一直的取舍,正如上文在比照各种压缩算法和参数时,笔者列出来的那几项『吞吐量最大』、『提早最低』、『IOPS 最高』、『压缩率最高』。起初笔者别离尝试过这几项,在对 ElasticSearch 进行基准测试时,性能反而不如默认的压缩算法,那这些所谓的『最』,到底有什么意义呢?


本文系笔者利用疫情隔离的周末空闲,陆陆续续破费了 20 多小时写作实现,过程中为了避免出现错漏,查找了大量的材料,也进行了不少的试验,只心愿带给读者综合的感官享受,让常识分享能更乏味、更深刻,能启发读者进行扩大思考,联合本人的认知,让这篇文章能施展常识分享以外的价值就再好不过。其中局部内容因为笔者集体能力限度、工夫限度、审校不当等起因,可能存在疏漏甚至舛误,如您有更好的见解,欢送斧正,笔者肯定虚心听取,有则改之,无则加勉。

正文完
 0