前言 在工作生活中,我们经常会遇到一些性能问题:好比手机用久了,在滑动窗口或点击 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 次数的变更: 目前内核提供了两种大页机制,一种是需求提早预留的静态大页方式,另一种是透明大页(THP, Transparent Huge Page) 方式。 1. 静态大页 首先来看静态大页,也叫做 HugeTLB。静态大页能够设置 cmdline 参数在系统启动阶段预留,好比指定大页 size 为 2M,一共预留 512 个这样的大页: 还能够在系统运转时动态预留,但该方式可能由于系统中没有足够的连续内存而预留失败。
当预留的大页个数小于已存在的个数,则会释放多余大页(前提是未被运用)。 编程中能够运用 mmap(MAP_HUGETLB) 申请内存。 细致运用能够参考内核文档 :https://www.kernel.org/doc/Documentation/admin-guide/mm/hugetlbpage.rst 这种大页的优点是一旦预留胜利,就能够满足进程的分配央求,还避免该部分内存被回收;缺陷是: (1) 需求用户显式地指定预留的大小和数量。 (2) 需求应用程序适配,好比: - mmap、shmget 时指定 MAP_HUGETLB; - 挂载 hugetlbfs,然后 open 并 mmap
预留太多大页内存后,free 内存大幅减少,容易触发系统内存回收以至 OOM
2. 透明大页 再来看透明大页,在 THP always 方式下,会在 Page Fault 过程中,为契合请求的 vma 尽量分配大页进行映射;假如此时分配大页失败,好比整机物理内存碎片化严重,无法分配出连续的大页内存,那么就会 fallback 到普通的 4K 进行映射,但会记载下该进程的地址空间 mm_struct;然后 THP 会在后台启动khugepaged 线程,定期扫描这些记载的 mm_struct,并进行合页操作。由于此时可能曾经能分配出大页内存了,那么就能够将此前 fallback 的 4K 小页映射转换为大页映射,以进步程序性能。整个过程完整不需求用户进程参与,对用户进程是透明的,因而称为透明大页。 固然透明大页运用起来十分方便、智能,但也有一定的代价: (1)进程内存占用可能远大所需:由于每次Page Fault 都尽量分配大页,即便此时应用程序只读写几KB (2)可能构成性能颤动:
因而 THP 还支持 madvise 方式,该方式需求应用程序指定运用大页的地址范围,内核只对指定的地址范围做 THP 相关的操作。这样能够愈加针对性、愈加细致地优化特定应用程序的性能,又不至于构成反向的负面影响。 能够经过 cmdline 参数和 sysfs 接口设置 THP 的方式: cmdline 参数: sysfs 接口: 细致运用能够参考内核文档 : 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 的等候。 内核社区特地封装了 mmap_lock 相关函数,并在其中增加了 tracepoint,这样能够运用 bpftrace 等工具统计持有写锁的进程、调用栈等,方便排查询题,肯定优化方向。 跨 numa 内存访问 在 NUMA 架构下,CPU 访问本地 node 内存的速度要大于远端 node,因而应用程序应尽可能访问本地 node 上的内存。能够经过 numastat 工具查看 node 间的内存分配状况:
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,需求剖析出细致的性能瓶颈,从而采取对应的调优措施,不能一刀切的进行设置。在没有发现明显性能颤动的状况下,常常能够继续坚持当前配置。 |