一些Linux内核内存性能调优笔记!

news2025/1/16 18:58:55

前言

在工作生活中,我们时常会遇到一些性能问题:比如手机用久了,在滑动窗口或点击 APP 时会出现页面反应慢、卡顿等情况;比如运行在某台服务器上进程的某些性能指标(影响用户体验的 PCT99 指标等)不达预期,产生告警等;造成性能问题的原因多种多样,可能是网络延迟高、磁盘 IO 慢、调度延迟高、内存回收等,这些最终都可能影响到用户态进程,进而被用户感知。

在 Linux 服务器场景中,内存是影响性能的主要因素之一,本文从内存管理的角度,总结归纳了一些常见的影响因素(比如内存回收、Page Fault 增多、跨 NUMA 内存访问等),并介绍其对应的调优方法。

内存回收

操作系统总是会尽可能利用速度更快的存储介质来缓存速度更慢的存储介质中的内容,这样就可以显著的提高用户访问速度。比如,我们的文件一般都存储在磁盘上,磁盘对于程序运行的内存来说速度很慢,因此操作系统在读写文件时,都会将磁盘中的文件内容缓存到内存上(也叫做 page cache),这样下次再读取到相同内容时就可以直接从内存中读取,不需要再去访问速度更慢的磁盘,从而大大提高文件的读写效率。上述情况需要在内存资源充足的前提条件下,然而在内存资源紧缺时,操作系统自身难保,会选择尽可能回收这些缓存的内存,将其用到更重要的任务中去。这时候,如果用户再去访问这些文件,就需要访问磁盘,如果此时磁盘也很繁忙,那么用户就会感受到明显的卡顿,也就是性能变差了。

在 Linux 系统中,内存回收分为两个层面:整机和 memory cgroup。

在整机层面

设置了三条水线:min、low、high;当系统 free 内存降到 low 水线以下时,系统会唤醒kswapd 线程进行异步内存回收,一直回收到 high 水线为止,这种情况不会阻塞正在进行内存分配的进程;但如果 free 内存降到了 min 水线以下,就需要阻塞内存分配进程进行回收,不然就有 OOM(out of memory)的风险,这种情况下被阻塞进程的内存分配延迟就会提高,从而感受到卡顿。

​图 1. per-zone watermark

这些水线可以通过内核提供的 /proc/sys/vm/watermark_scale_factor 接口来进行调整,该接口合法取值的范围为 [0, 1000],默认为 10,当该值设置为 1000 时,意味着 low 与 min 水线,以及 high 与 low 水线间的差值都为总内存的 10% (1000/10000) 。

针对 page cache 型的业务场景,我们可以通过该接口抬高 low 水线,从而更早的唤醒 kswapd 来进行异步的内存回收,减少 free 内存降到 min 水线以下的概率,从而避免阻塞到业务进程,以保证影响业务的性能指标。

在 memory cgroup 层面

目前内核没有设置水线的概念,当内存使用达到 memory cgroup 的内存限制后,会阻塞当前进程进行内存回收。不过内核在 v5.19内核 中为 memory cgroup提供了 memory.reclaim 接口,用户可以向该接口写入想要回收的内存大小,来提早触发 memory cgroup 进行内存回收,以避免阻塞 memory cgroup 中的进程。

Huge Page

内存作为宝贵的系统资源,一般都采用延迟分配的方式,应用程序第一次向分配的内存写入数据的时候会触发 Page Fault,此时才会真正的分配物理页,并将物理页帧填入页表,从而与虚拟地址建立映射。

​图 2. Page Table

此后,每次 CPU 访问内存,都需要通过 MMU 遍历页表将虚拟地址转换成物理地址。为了加速这一过程,一般都会使用 TLB(Translation-Lookaside Buffer)来缓存虚拟地址到物理地址的映射关系,只有 TLB cache miss 的时候,才会遍历页表进行查找。

页的默认大小一般为 4K,随着应用程序越来越庞大,使用的内存越来越多,内存的分配与地址翻译对性能的影响越加明显。试想,每次访问新的 4K 页面都会触发 Page Fault,2M 的页面就需要触发 512 次才能完成分配。

另外 TLB cache 的大小有限,过多的映射关系势必会产生 cacheline 的冲刷,被冲刷的虚拟地址下次访问时又会产生 TLB miss,又需要遍历页表才能获取物理地址。

对此,Linux 内核提供了大页机制 。上图的 4 级页表中,每个 PTE entry 映射的物理页就是 4K,如果采用 PMD entry 直接映射物理页,则一次 Page Fault 可以直接分配并映射 2M 的大页,并且只需要一个 TLB entry 即可存储这 2M 内存的映射关系,这样可以大幅提升内存分配与地址翻译的速度 。

因此,一般推荐占用大内存应用程序使用大页机制分配内存 。当然大页也会有弊端:比如初始化耗时高,进程内存占用可能变高等。

可以使用 perf 工具对比进程使用大页前后的 PageFault 次数的变化:

perf stat -e page-faults -p-- sleep 5

目前内核提供了两种大页机制,一种是需要提前预留的静态大页形式,另一种是透明大页(THP, Transparent Huge Page) 形式。

 

资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈

1. 静态大页

首先来看静态大页,也叫做 HugeTLB。静态大页可以设置 cmdline 参数在系统启动阶段预留,比如指定大页 size 为 2M,一共预留 512 个这样的大页:

hugepagesz=2M hugepages=512

还可以在系统运行时动态预留,但该方式可能因为系统中没有足够的连续内存而预留失败。

  • 预留默认 size(可以通过 cmdline 参数 default_hugepagesz=指定size)的大页:

echo 20 > /proc/sys/vm/nr_hugepages
  • 预留特定 size 的大页:

echo 5 > /sys/kernel/mm/hugepages/hugepages-*/nr_hugepages
  • 预留特定 node 上的大页:

echo 5 > /sys/devices/system/node/node*/hugepages/hugepages-*/nr_hugepages

当预留的大页个数小于已存在的个数,则会释放多余大页(前提是未被使用)。

编程中可以使用 mmap(MAP_HUGETLB) 申请内存。详细使用可以参考内核文档 :https://www.kernel.org/doc/Documentation/admin-guide/mm/hugetlbpage.rst

这种大页的优点是一旦预留成功,就可以满足进程的分配请求,还避免该部分内存被回收;缺点是:

(1) 需要用户显式地指定预留的大小和数量。

(2) 需要应用程序适配,比如:

  • mmap、shmget 时指定 MAP_HUGETLB;

  • 挂载 hugetlbfs,然后 open 并 mmap

当然也可以使用开源 libhugetlbfs.so,这样无需修改应用程序

预留太多大页内存后,free 内存大幅减少,容易触发系统内存回收甚至 OOM

紧急情况下可以手动减少 nr_hugepages,将未使用的大页释放回系统;也可以使用 v5.7 引入的HugeTLB + CMA 方式,细节读者可以自行查阅。

2. 透明大页

再来看透明大页,在 THP always 模式下,会在 Page Fault 过程中,为符合要求的 vma 尽量分配大页进行映射;如果此时分配大页失败,比如整机物理内存碎片化严重,无法分配出连续的大页内存,那么就会 fallback 到普通的 4K 进行映射,但会记录下该进程的地址空间 mm_struct;然后 THP 会在后台启动khugepaged 线程,定期扫描这些记录的 mm_struct,并进行合页操作。因为此时可能已经能分配出大页内存了,那么就可以将此前 fallback 的 4K 小页映射转换为大页映射,以提高程序性能。整个过程完全不需要用户进程参与,对用户进程是透明的,因此称为透明大页。

虽然透明大页使用起来非常方便、智能,但也有一定的代价:

(1)进程内存占用可能远大所需:因为每次Page Fault 都尽量分配大页,即使此时应用程序只读写几KB

(2)可能造成性能抖动:

  • 在第 1 种进程内存占用可能远大所需的情况下,可能造成系统 free 内存更少,更容易触发内存回收;系统内存也更容易碎片化。

  • khugepaged 线程合页时,容易触发页面规整甚至内存回收,该过程费时费力,容易造成 sys cpu 上升。

  • mmap lock 本身是目前内核的一个性能瓶颈,应当尽量避免 write lock 的持有,但 THP 合页等操作都会持有写锁,且耗时较长(数据拷贝等),容易激化 mmap lock 锁竞争,影响性能。

因此 THP 还支持 madvise 模式,该模式需要应用程序指定使用大页的地址范围,内核只对指定的地址范围做 THP 相关的操作。这样可以更加针对性、更加细致地优化特定应用程序的性能,又不至于造成反向的负面影响。

可以通过 cmdline 参数和 sysfs 接口设置 THP 的模式:

cmdline 参数:

transparent_hugepage=madvise

sysfs 接口:

echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

详细使用可以参考内核文档 : https://www.kernel.org/doc/Documentation/admin-guide/mm/transhuge.rst

mmap_lock 锁

上一小节有提到 mmap_lock 锁,该锁是内存管理中的一把知名的大锁,保护了诸如mm_struct 结构体成员、 vm_area_struct 结构体成员、页表释放等很多变量与操作。

mmap_lock 的实现是读写信号量 ,当写锁被持有时,所有的其他读锁与写锁路径都会被阻塞。Linux 内核已经尽可能减少了写锁的持有场景以及时间,但不少场景还是不可避免的需要持有写锁,比如 mmap 以及 munmap 路径、mremap 路径和 THP 转换大页映射路径等场景。

应用程序应该避免频繁的调用会持有 mmap_lock 写锁的系统调用 (syscall) ,比如有时可以使用 madvise(MADV_DONTNEED)释放物理内存,该参数下,madvise 相比 munmap 只持有 mmap_lock 的读锁,并且只释放物理内存,不会释放 VMA 区域,因此可以再次访问对应的虚拟地址范围,而不需要重新调用 mmap 函数。

另外对于 MADV_DONTNEED,再次访问还是会触发 Page Fault 分配物理内存并填充页表,该操作也有一定的性能损耗 。如果想进一步减少这部分损耗,可以改为 MADV_FREE 参数,该参数也只会持有 mmap_lock 的读锁,区别在于不会立刻释放物理内存,会等到内存紧张时才进行释放,如果在释放之前再次被访问则无需再次分配内存,进而提高内存访问速度。

一般 mmap_lock 锁竞争激烈会导致很多 D 状态进程(TASK_UNINTERRUPTIBLE),这些 D 进程都是进程组的其他线程在等待写锁释放。因此可以打印出所有 D 进程的调用栈,看是否有大量 mmap_lock 的等待。

for i in `ps -aux | grep " D" | awk '{ print $2}'`; do echo $i; cat /proc/$i/stack; done

内核社区专门封装了 mmap_lock 相关函数,并在其中增加了 tracepoint,这样可以使用 bpftrace 等工具统计持有写锁的进程、调用栈等,方便排查问题,确定优化方向。

bpftrace -e 'tracepoint:mmap_lock:mmap_lock_start_locking /args->write == true/{ @[comm, kstack] = count();}'

跨 numa 内存访问

在 NUMA 架构下,CPU 访问本地 node 内存的速度要大于远端 node,因此应用程序应尽可能访问本地 node 上的内存。可以通过 numastat 工具查看 node 间的内存分配情况:

  • 观察整机是否有很多 other_node 指标(远端内存访问)上涨:

watch -n 1 numastat -s
  • 查看单个进程在各个node上的内存分配情况:

numastat -p

1. 绑 node

可以通过 numactl 等工具把进程绑定在某个 node 以及对应的 CPU 上,这样该进程只会从该本地 node 上分配内存。

但这样做也有相应的弊端,比如:该 node 剩余内存不够时,进程也无法从其他 node 上分配内存,只能期待内存回收后释放足够的内存,而如果进入直接内存回收会阻塞内存分配,就会有一定的性能损耗。

此外,进程组的线程数较多时,如果都绑定在一个 node 的 CPU 上,可能会造成 CPU 瓶颈,该损耗可能比远端 node 内存访问还大,比如 ngnix 进程与网卡就推荐绑定在不同的 node 上,这样虽然网卡收包时分配的内存在远端 node 上,但减少了本地 node 的 CPU 上的网卡中断,反而可以获得更好的性能提升。

2. numa balancing

内核还提供了 numa balancing 机制,可以通过 /proc/sys/kernel/numa_balancing 文件或者 cmdline 参数 numa_balancing=进行开启。

该机制可以动态的将进程访问的 page 从远端 node 迁移到本地 node 上,从而使进程可以尽可能的访问本地内存。

但该机制实现也有相应的代价,在 page 的迁移是通过 Page Fault 机制实现的,会有相应的性能损耗;另外如果迁移时找不到合适的目标 node,可能还会把进程迁移到正在访问的 page 的 node 的 CPU 上,这可能还会导致 cpu cache miss,从而对性能造成更大的影响。

因此需要根据业务进程的具体行为,来决定是否开启 numa balancing 功能 。

总结

性能优化一直是大家关注的话题,其优化方向涉及到 CPU 调度、内存、IO等,本文重点针对内存优化提出了几点思路。但是鱼与熊掌不可兼得,文章提到的调优操作都有各自的优点和缺点,不存在一个适用于所有情况的优化方法。针对于不同的 workload,需要分析出具体的性能瓶颈,从而采取对应的调优方法,不能一刀切的进行设置 。在没有发现明显性能抖动的情况下,往往可以继续保持当前配置。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/387396.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaEE学习笔记-SpringBoot快速上手、部分注解解释

SpringBoot快速上手 一、快速创建SpringBoot应用1.1利用IDEA提供的Spring Initializr创建Spring Boot应用1.2Spring Boot生成的项目结构介绍1.3初步测试后端是否OK(建立一个controll类)二、热部署2.1 添加依赖2.2 Setting处项目自动化设置2.3 具体项目设置2.4 待选步骤三、注…

渗透five86-3

本机kali ip为:192.168.56.104,靶机ip用nmap -sP或者arp-scan -l去查看靶机ip为192.168.56.121输入nmap -sS -sV -T5 -A 192.168.56.121 #进行靶机端口信息收集发现仅有80端口,同时发现了robots.txt文件,打开web端发现是Drupal 7的框架,之前的…

【蓝桥杯集训12】DFS(3 / 5)

目录 842. 排列数字 - DFS按位置枚举 843. n-皇后问题 - DFS按行枚举 165. 小猫爬山 - DFS枚举小猫 1209. 带分数 - DFS 3502. 不同路径数 - 842. 排列数字 - DFS按位置枚举 活动 - AcWing 题目: 给你一个整数n 要求将1~n的所有排列情况列出 比如&#xff1a…

stm32hal库硬件IIC使用OLED(SSD1306)

其实OLED算是非常常用的一个小模块了,之前用的一直用的是软件模拟的IIC,总的来说感觉还是都是有点"答辩",经常莫名其妙的就点不亮的,用了cubemx之后硬件iic配置就非常方便了,还是想用硬件iic,一方…

C++ 常用命令行开发工具(Linux)

文章目录1、简介2、gcc / g2.1 system(执行shell 命令)2.2 popen(建立管道I/O)2.3 vforkexec(新建子进程)3、clang3.1 下载和安装clang3.2 clang和gcc比较3.2.1 gcc3.2.2 clang3.2.3 LLVM4、make4.1 例子14…

DNS作用及工作原理

文章目录1. DNS作用2 DNS 三个组成部分:2.1 客户端2.2Local DNS2.3 权威域 DNS 服务器3 工作过程1. DNS作用 DNS 分为 Client 和 Server,Client 扮演发问的角色,也就是问 Server 一个 Domain Name,而 Server 必须要回答此 Domain…

MySQL实战解析底层---深入浅出索引

目录 前言 索引的常见模型 InnoDB 的索引模型 索引维护 后言 覆盖索引 最左前缀原则 索引下推 前言 一句话简单来说,索引的出现其实就是为了提高数据查询的效率,就像书的目录一样一本500 页的书,如果你想快速找到其中的某一个知识点&…

CentOS安装RStudio-Server的方法

R语言是生信分析、数据挖掘最常用最好用的软件之一,得到了广大生信工程师、数据分析师的厚爱。Rstudio 是 R 的集成开发环境,使得R语言的用户体验更强。一般个人电脑(PC, Personal Computer)使用单机版的 Rstudio 即可&#xff0c…

I.MX6ULL_Linux_系统篇(22) kernel移植

原厂 Linux 内核编译 NXP 提供的 Linux 源码肯定是可以在自己的 I.MX6ULL EVK 开发板上运行下去的,所以我们肯定是以 I.MX6ULL EVK 开发板为参考,然后将 Linux 内核移植到 I.MX6U-ALPHA 开发板上的。 配置编译 Linux 内核 和uboot一样,在编…

C#/Unity------抽象类与接口(一篇文章彻底搞懂...)

抽象类/接口以及区别 SUMMARY: 具体类→抽象类→接口:越来越抽象,内部实现的东西越来越少 一、抽象类 抽象类是未完全实现逻辑的类 抽象类为复用而生,专门用作基类 封装确定的,开放不确定的,推迟到合适的子类来实现…

初识STM32单片机

目录 初识STM32单片机 什么是单片机? STM系列单片机命名规则 STM32F103C8T6单片机简介 标准库与HAL库区别 通用输入输出端口GPIO 什么是GPIO? 定义 命名规则 内部框架图 推挽输出与开漏输出 如何点亮一颗LED灯 编程实现点灯 按键点亮LED灯…

【JavaWeb】CSS基础知识:引入方式 + 选择器

目录 CSS引入 行内样式表 内部样式表 外部样式表 CSS选择器 基础选择器 标签选择器 类选择器 ID选择器 通配符选择器 复合选择器 后代选择器 子选择器 CSS引入 CSS的引入有三种&#xff0c;三种的优缺点各不相同。 行内样式表 <!-- 行内样式表 --><!--…

Splunk Add-on 碰到证书SSL问题

最近Splunk add-on 在连接对方app 的 时候,经常报SSL的错: ERROR AdminManagerExternal - Unexpected error "<class requests.exceptions.SSLError>" from python handler: "HTTPSConnectionPool(host=app.com, port=443): Max retries exceeded with…

flex布局方式让最后一个(或第二个...n)元素居右显示

<div class"round"> <div class"income">收入</div> <div class"center"> <img style"width: 12px" src"../../img/big/up.png"> </div> <div class"rg"> <span cl…

项目实战典型案例1——redis只管存不管删除 让失效时间删除的问题

redis只管存不管删除 让失效时间删除的问题一&#xff1a;背景介绍二&#xff1a;思路&方案三&#xff1a;代码模拟1.错误示范通过班级id查询课程名称执行结果通过班级id修改课程名称&#xff08;并没有删除对应缓存&#xff09;执行结果2.正确示范在错误示范的更新接口上添…

详解进程 及 探查进程

进程的概念PCB是什么task_struct的作用如何执行进程进程的探查什么是bashps命令的使用&#xff08;查看进程&#xff09;创建进程探究父子进程进程的概念 简而言之&#xff0c;进程就是正在在执行的程序 之前说过&#xff0c;程序执行的第一步Windows是双击程序Linux是 ./ &a…

vue路由篇

路由的理解 介绍 路由(英文&#xff1a;route)就是对应关系&#xff0c; 路由分为前端路由和后端路由&#xff0c;比如前端路由就是 Hash 地址与组件之间的对应关系。 具体来说&#xff1a;一个路由就是一组key-value的对应关系 key 为 url 路径value 可能是函数function 或…

反弹shell数据不回显带外查询pikaqiu靶场搭建

P1 文件上传下载&#xff08;解决无图形化和解决数据传输&#xff09; 解决无图形化&#xff1a; 当我们想下载一个文件时&#xff0c;通常是通过浏览器的一个链接直接访问网站点击下载的&#xff0c;但是在实际的安全测试中&#xff0c;我们获取的权限只是一个执行命令的窗口…

Flink 提交模式

Flink的部署方式有很多,支持Local,Standalone,Yarn,Docker,Kubernetes模式等。而根据Flink job的提交模式,又可以分为三种模式: 模式1:Application Mode Flink提交的程序,被当做集群内部Application,不再需要Client端做繁重的准备工作。(例如执行main函数,生成JobG…

sizeof以及strlen的用法以及注意事项

大家都知道&#xff0c;在c中算字符串长度和所占空间大小事不可避免的&#xff0c;甚至再有的时候&#xff0c;我们在写代码的过程中&#xff0c;就会用到这些数据。比如&#xff0c;下面这道题 struct Test { int Num; char *pcName; short sDate; char cha[2]; short sBa[4];…