文山文山市网站建设,建网站内容,视频网站前台怎么做,潍坊商城网站建设前言
在工作生活中#xff0c;我们时常会遇到一些性能问题#xff1a;比如手机用久了#xff0c;在滑动窗口或点击 APP 时会出现页面反应慢、卡顿等情况#xff1b;比如运行在某台服务器上进程的某些性能指标#xff08;影响用户体验的 PCT99 指标等#xff09;不达预期…前言
在工作生活中我们时常会遇到一些性能问题比如手机用久了在滑动窗口或点击 APP 时会出现页面反应慢、卡顿等情况比如运行在某台服务器上进程的某些性能指标影响用户体验的 PCT99 指标等不达预期产生告警等造成性能问题的原因多种多样可能是网络延迟高、磁盘 IO 慢、调度延迟高、内存回收等这些最终都可能影响到用户态进程进而被用户感知。
在 Linux 服务器场景中内存是影响性能的主要因素之一本文从内存管理的角度总结归纳了一些常见的影响因素比如内存回收、Page Fault 增多、跨 NUMA 内存访问等并介绍其对应的调优方法。
内存回收
操作系统总是会尽可能利用速度更快的存储介质来缓存速度更慢的存储介质中的内容这样就可以显著的提高用户访问速度。比如我们的文件一般都存储在磁盘上磁盘对于程序运行的内存来说速度很慢因此操作系统在读写文件时都会将磁盘中的文件内容缓存到内存上也叫做 page cache这样下次再读取到相同内容时就可以直接从内存中读取不需要再去访问速度更慢的磁盘从而大大提高文件的读写效率。上述情况需要在内存资源充足的前提条件下然而在内存资源紧缺时操作系统自身难保会选择尽可能回收这些缓存的内存将其用到更重要的任务中去。这时候如果用户再去访问这些文件就需要访问磁盘如果此时磁盘也很繁忙那么用户就会感受到明显的卡顿也就是性能变差了。
在 Linux 系统中内存回收分为两个层面整机和 memory cgroup。
在整机层面
设置了三条水线min、low、high当系统 free 内存降到 low 水线以下时系统会唤醒kswapd 线程进行异步内存回收一直回收到 high 水线为止这种情况不会阻塞正在进行内存分配的进程但如果 free 内存降到了 min 水线以下就需要阻塞内存分配进程进行回收不然就有 OOMout 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 遍历页表将虚拟地址转换成物理地址。为了加速这一过程一般都会使用 TLBTranslation-Lookaside Buffer来缓存虚拟地址到物理地址的映射关系只有 TLB cache miss 的时候才会遍历页表进行查找。
页的默认大小一般为 4K随着应用程序越来越庞大使用的内存越来越多内存的分配与地址翻译对性能的影响越加明显。试想每次访问新的 4K 页面都会触发 Page Fault2M 的页面就需要触发 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 个这样的大页
hugepagesz2M hugepages512
还可以在系统运行时动态预留但该方式可能因为系统中没有足够的连续内存而预留失败。 预留默认 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_hugepagemadvise
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) 比如有时可以使用 madviseMADV_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需要分析出具体的性能瓶颈从而采取对应的调优方法不能一刀切的进行设置 。在没有发现明显性能抖动的情况下往往可以继续保持当前配置。