在Linux系统运维中,卡顿、响应迟缓是高频痛点,多数人往往聚焦于硬件升级或进程优化,却忽视了核心的水位配置这一“隐形性能开关”。内存、IO、CPU等核心资源的水位阈值,直接决定了系统资源调度逻辑,配置不当会导致资源浪费或过载崩溃,合理优化则能让系统潜能充分释放。
本文聚焦实战场景,将从三线管控的基础概念剖析入手,深入解读三线管控的工作原理,同时厘清OOM Killer作为系统最后防线的核心作用,拆解水位控制这一内存管理预警机制的核心逻辑,结合真实案例剖析卡顿场景下的配置痛点,最终落脚于实战解决。无论你是面临高并发压力的运维工程师,还是追求系统高效运行的开发者,都能通过这份解析避开配置误区,快速实现从系统卡顿到性能飞升的突破,让Linux系统适配多样化业务需求。
一、Linux 内存水位线
在深入探究这次系统问题之前,我们先来了解一下 Linux 内存水位线的基本概念,这是理解后续问题的关键。
1.1水位线定义
Linux 内核将物理内存划分为不同的内存区域(Memory Zones),如 ZONE_NORMAL、ZONE_DMA 和 ZONE_HIGHMEM 等 ,以便管理具有不同特性的内存。每个内存区域都关联着三个关键的水位线:最小水位线(min)、低水位线(low)和高水位线(high) 。
- 最小水位线(min):这是触发直接回收的关键阈值。当一个内存区域的可用页数下降到或低于这个水位线时,系统就已经处于严重的内存压力之下了,此时任何尝试从该区域分配内存的进程都会触发直接回收,以立即释放内存满足当前的分配请求。这就好比水库的水位已经降到了警戒线以下,必须马上采取措施来补充水量,否则就会面临干涸的危险。
- 低水位线(low):它是唤醒 kswapd 后台回收守护进程的阈值。当可用内存降至低水位线以下时,说明内存有一定压力,但还没到最危急的时刻,kswapd 会被唤醒,开始异步扫描内存页,并尝试回收内存,直到可用内存达到高水位线 。可以把它想象成水库的一个预警水位,当水位下降到这个位置时,就需要启动一些辅助措施来维持水位。
- 高水位线(high):这是 kswapd 停止后台回收的目标阈值。一旦可用内存达到或超过高水位线,就意味着内存比较充足,kswapd 就会进入睡眠状态,直到可用内存再次低于低水位线 。就如同水库的水位已经恢复到正常水平,辅助措施就可以暂时停止了。
1.2水位线与内存分配回收的关系
内存分配和回收与这三条水位线紧密相关。当一个进程申请内存时,系统首先会检查当前的可用内存是否高于 high 水位线。如果高于 high 水位线,说明内存非常充足,系统会直接快速地分配内存给进程,这个过程就像从一个装满水的大桶里轻松舀水一样简单,几乎不会有任何延迟,内存分配处于快速路径。
要是可用内存低于 high 水位线但高于 low 水位线,虽然内存没有那么充裕了,但还在安全范围内,系统依然会尝试分配内存给进程。不过这个时候,分配过程可能会稍微复杂一些,进入慢速路径。但只要内存没有低于 low 水位线,一般不会对系统性能产生太大影响。一旦可用内存低于 low 水位线,系统就会意识到内存开始有压力了,这时就会唤醒 kswapd 线程进行异步内存回收 。
kswapd 主要回收以下三类内存:
- 文件页缓存:当我们读取磁盘上的文件时,系统会把文件的内容缓存到内存中,形成文件页缓存,这样下次再读取相同文件时,就可以直接从内存中读取,大大提高了读取速度。但当内存紧张时,kswapd 会通过 shrink_page_list () 函数来回收这些文件页缓存 。它会检查文件页的使用情况,如果某个文件页在一段时间内没有被访问过,就会被认为是可以回收的对象。回收时,如果文件页没有被修改过(即干净页),就直接丢弃;如果文件页被修改过(即脏页),则需要先将其写回磁盘,然后再丢弃。这就好比清理仓库里的货物,如果货物是新的(干净页),直接扔掉就好;如果货物被使用过(脏页),就需要先把它放回原来的地方(写回磁盘),然后再处理。
- 匿名页:匿名页主要用于存储进程的堆、栈和数据段等数据 。由于这些数据没有对应的文件,不能像文件页缓存那样直接丢弃或写回磁盘。所以,kswapd 会通过 shrink_anon () 函数将匿名页交换到交换分区(Swap) 。交换分区就像是一个内存的 “备用仓库”,当内存空间不足时,把暂时用不到的匿名页数据存放到这里,等需要时再从这里读取回来。这个过程就像把家里暂时不用的物品存放到仓库里,等要用的时候再取回来。
- Slab 缓存:Slab 缓存主要用于缓存内核对象,如目录项缓存(dentry)、索引节点缓存(inode)等 。kswapd 会通过 shrink_slab () 函数根据内核定义的收缩函数(Shrinker)来释放 Slab 缓存 。内核会根据不同的内核对象类型,定义相应的收缩策略,以确保在回收 Slab 缓存时,不会影响到系统的正常运行。
kswapd 在后台默默地工作,不断地扫描和回收这些可回收的内存,直到系统的空闲内存回到高水位线,它才会暂时 “休息”。在这个过程中,由于 kswapd 是在后台异步运行的,所以对系统的正常运行影响较小,用户几乎感觉不到它的存在。
当可用内存进一步下降,低于 min 水位线时,情况就变得比较危急了。此时,系统会认为内存严重不足,为了避免系统因内存耗尽而崩溃,内核会直接阻塞正在申请内存的进程,并立即启动直接回收流程。这个直接回收过程由请求线程自身执行,它会同步地扫描 LRU(最近最少使用)链表,淘汰那些不活跃的页面,释放物理页。这个过程会导致调用线程被长时间阻塞,因为它必须等待回收足够的内存后,才能继续执行内存分配操作,这就像是在一个快干涸的井里打水,必须先清理井里的杂物,才能打出水来,而这个清理杂物的过程就会耗费不少时间,进而影响到应用程序的响应速度和系统的整体性能。
尽管 kswapd 很努力地在后台回收内存,但有时候内存消耗的速度实在太快,当进程申请内存时,如果检测到空闲内存低于高水位线,并且 kswapd 的回收工作还没有完成,此时就会触发直接回收机制 。这就好比一个人急需用水,而蓄水池里的水已经快不够了,后台的加水工作(kswapd 回收内存)又还没完成,那就只能直接从当前用水的地方(当前进程)开始想办法找水(回收内存)了
直接回收机制具有阻塞当前进程的特性 。也就是说,当直接回收被触发时,当前正在申请内存的进程会被阻塞,它必须等待回收工作完成后,才能获得内存继续执行。这是因为直接回收需要立即释放内存来满足当前进程的申请需求,所以只能暂停当前进程,让它亲自参与到内存回收的工作中。在回收过程中,直接回收会执行与 kswapd 相同的回收逻辑,即回收文件页缓存、匿名页和 Slab 缓存等。这个过程可能会涉及到将文件缓存写入磁盘、遍历一些内核结构体等操作,这些操作都需要一定的时间,所以会导致系统出现明显的卡顿,就像道路突然被堵住了,车辆都无法通行一样。
1.3水位控制与 OOM Killer 的协作
水位控制和 OOM Killer 在 Linux 内存管理中扮演着不同的角色,但它们之间又紧密协作,共同维护着系统的内存稳定 。当 kswapd 和直接回收都失败,且空闲内存近似为 0 时,就好比蓄水池里的水已经完全干涸,所有的加水方法(内存回收)都不管用了,此时 OOM Killer 就会作为终极手段被触发。
具体的协作流程如下:当进程申请内存时,系统首先检查物理内存是否充足 。如果充足,就正常分配物理页;如果不足,就尝试触发直接回收。如果直接回收获胜,即成功回收了足够的内存,就可以正常分配内存给进程;如果直接回收失败,就唤醒 kswapd 进行后台回收。
kswapd 会不断地在后台扫描和回收内存,如果它成功回收到足够的内存,也可以正常分配内存;但如果 kswapd 回收失败,并且此时空闲内存近似为 0,就说明系统已经处于内存耗尽的边缘,OOM Killer 就会被触发。OOM Killer 会遍历系统中的所有进程,根据每个进程的内存使用情况、优先级等因素计算 oom_score,然后选择分数最高的进程进行终止,以释放内存资源,试图挽救系统。
二、三线管控的工作原理详解
2.1 内存分配流程中的水位判断
在 Linux 内核中,内存分配的关键函数是__alloc_pages_nodemask,其核心逻辑涉及对内存水位线的判断。当一个进程请求内存分配时,内核首先会检查当前内存域(zone)的可用内存情况 。下面是一段简化后的代码逻辑,展示了水位判断在内存分配中的作用:
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist, nodemask_t *nodemask){ struct page *page; struct zone *zone; // 遍历内存域列表 for_each_zone_zonelist(zone, zonelist, gfp_mask) { // 检查可用内存是否高于high水位线 if (zone_watermark_ok(zone, order, high_wmark_pages(zone), 0, gfp_mask)) { // 直接从伙伴系统分配内存 page = rmqueue(zone, order); if (page) { return page; } } } // 若可用内存低于high水位线,进入慢速路径,尝试其他分配方式 // 可能会触发内存回收等操作 return NULL;}
在这段代码中,zone_watermark_ok 函数用于判断当前内存域的可用内存是否高于指定的水位线(这里是 high 水位线) 。如果高于 high 水位线,说明内存充足,系统会通过 rmqueue 函数直接从伙伴系统(Buddy System)中分配内存,这是快速分配路径。但要是可用内存低于 high 水位线,就会进入慢速路径,此时系统可能会尝试唤醒 kswapd 进行内存回收,或者进行内存规整(memory compaction)等操作,以满足内存分配请求 。
这就好比在一个超市里购物,当商品库存充足(高于 high 水位线)时,顾客可以快速拿到商品;但要是库存不足,超市就需要从仓库补货(内存回收)或者重新整理货架(内存规整),这个过程就会花费更多的时间。
2.2 kswapd 与直接回收机制
kswapd 是 Linux 内核中的一个后台线程,每个内存域都有对应的 kswapd 线程,例如kswapd0对应 Node 0 的内存域 。当系统的可用内存低于 low 水位线时,kswapd 就会被唤醒,开始异步回收内存。它主要通过扫描 LRU 链表,将不活跃的页面回收并释放物理页 。以下是 kswapd 的工作流程:
- kswapd 被唤醒后,会调用balance_pgdat函数,对内存节点(pgdat)进行内存回收操作。
- 在balance_pgdat函数中,会遍历内存节点中的各个内存域(zone)。
- 对于每个内存域,kswapd 会调用shrink_zone函数,扫描 LRU 链表,回收不活跃的页面 。例如,对于文件缓存页面,如果长时间没有被访问,就会被回收;对于匿名页面,如果内存压力较大,也会被交换到磁盘的交换分区(swap)中 。
- kswapd 持续回收内存,直到内存域的可用内存达到 high 水位线,此时 kswapd 会进入睡眠状态,等待下一次内存水位低于 low 水位线时再次被唤醒。
当可用内存进一步下降,低于 min 水位线时,直接回收机制就会被触发 。直接回收由请求内存分配的线程自身执行,会同步地进行内存回收操作 。这是因为此时系统内存已经非常紧张,必须立即释放内存以满足当前的分配请求,否则系统可能会因为内存耗尽而崩溃 。直接回收的执行过程如下:
- 当内存分配请求无法满足,且可用内存低于 min 水位线时,内核会调用__alloc_pages_direct_reclaim函数,启动直接回收流程。
- 在__alloc_pages_direct_reclaim函数中,会调用shrink_node函数,对内存节点进行内存回收 。shrink_node函数会遍历内存节点中的所有内存域,对每个内存域调用shrink_zone函数进行页面回收,与 kswapd 的回收操作类似,但这里是同步执行,会阻塞当前请求线程 。
- 直接回收会一直进行,直到回收足够的内存满足分配请求,或者无法再回收更多内存。如果最终还是无法满足内存分配请求,系统可能会触发 OOM(Out Of Memory) Killer,选择并杀死一些占用内存较多的进程,以释放内存 。这就像是在一个紧急情况下,为了保证整个系统的运行,不得不牺牲一些不太重要的进程,就如同在资源匮乏时,为了保证团队的生存,可能需要放弃一些非关键的物资一样。
2.3 min_free_kbytes 的关键作用
min_free_kbytes 是一个内核参数,它定义了系统必须保留的最小空闲内存量(单位为 KB) 。这个参数在内存水位线的计算中起着关键作用 。系统在启动时,会根据min_free_kbytes的值为每个内存域计算min水位线 。具体的计算方式如下:
// 假设total_memory为系统总内存大小,zone_size为当前内存域大小zone->watermark[WMARK_MIN] = (min_free_kbytes * zone_size) / total_memory;zone->watermark[WMARK_LOW] = zone->watermark[WMARK_MIN] * 1.25;zone->watermark[WMARK_HIGH] = zone->watermark[WMARK_MIN] * 1.5;
从上述公式可以看出,min_free_kbytes 的值直接影响 min 水位线的大小,而 low 和 high 水位线又是基于min水位线按一定比例计算得出 。当 min_free_kbytes 增大时,min 水位线会相应提高,low 和 high 水位线也会随之升高 。这意味着系统会预留更多的内存,从而更早地触发 kswapd 进行内存回收 。比如,在一个内存紧张的系统中,如果将 min_free_kbytes 设置得过大,可能会导致 kswapd 频繁启动,虽然系统的内存安全性提高了,但应用程序可用的内存会减少,可能会影响应用程序的性能 。
相反,如果 min_free_kbytes 设置得过小,系统预留的内存不足,当内存压力增大时,可能会频繁触发直接回收,导致系统性能下降,甚至触发 OOM Killer 。在实际应用中,需要根据系统的负载情况和内存需求,合理调整min_free_kbytes 的值,以平衡系统的稳定性和应用程序的性能 。这就好比在规划一个城市的物资储备时,需要根据城市的人口规模和日常需求,合理确定储备物资的数量,既不能储备过多导致资源浪费,也不能储备过少而在紧急情况下无法满足需求 。
三、OOM Killer 系统的最后防线
在 Linux 系统中,为了提高内存的利用率,采用了内存过度提交(Overcommit)的策略 。简单来说,就是系统允许进程申请的内存总量超过实际物理内存容量。这是基于一种常见的情况,即进程申请的内存并不一定都会立即被使用,实际使用量往往小于申请量。就好比你去图书馆借书,你可以先登记借 10 本书,但实际上你可能并不会马上把这 10 本书都带走阅读,可能只带走 2 - 3 本。在内存管理中,这种策略可以让系统在一定程度上更灵活地分配内存,提高内存的整体使用效率。
然而,这种策略也存在风险。当多个进程同时开始大量使用它们之前申请的内存时,就可能导致内存耗尽的情况发生。想象一下,图书馆里很多人都登记借了大量的书,一开始大家都没怎么取走,但突然有一天,所有人都来把自己登记借的书全部取走,这时候图书馆的书就可能不够了。在 Linux 系统中,当内存耗尽,内核无法满足新的内存分配请求时,OOM Killer 机制便会被触发,它就像是图书馆的管理员,在书不够的时候,采取一些措施来应对这种危机情况。
3.1 OOM Killer工作原理
当系统内存不足且无法通过常规手段(如回收缓存、使用交换空间等)释放足够内存时,内核就会调用 out_of_memory () 函数,启动 OOM Killer 机制。这就好比当一个城市的水资源严重短缺,常规的节水措施(如限制居民用水、回收中水等)都无法满足需求时,就需要采取更极端的措施。
OOM Killer 机制启动后,会遍历系统中的所有进程,根据每个进程的内存使用情况、优先级等因素计算一个分数,这个分数被称为 oom_score。简单理解,就是给每个进程打个分,看看哪个进程在当前情况下最 “应该” 被终止。然后,OOM Killer 会选择分数最高的进程进行终止,以释放内存资源。比如在一个班级里,老师要选一个学生来完成一项特殊任务(释放内存资源),会根据学生的各项表现(内存使用、优先级等)给每个学生打分,然后选择分数最高的学生去完成任务。通过这种方式,OOM Killer 试图挽救系统,使其不至于因为内存耗尽而完全崩溃。
3.2影响决策的因素
进程占用的物理内存越多,oom_score 就越高,被终止的可能性也就越大。这很好理解,占用内存多就好比一个人在图书馆借了大量的书却不归还,导致其他人无书可借。在系统内存紧张时,这样的进程自然更容易被 “请出” 系统,以释放内存给更需要的进程使用。比如一个大型数据库进程,它可能占用了大量的内存来存储数据和运行查询,当系统内存不足时,它就很可能成为 OOM Killer 的目标。
通过调整 /proc/[PID]/oom_score_adj 文件的值,可以影响进程的 oom_score。该值的范围为 - 1000 到 1000,值越高,进程被终止的可能性越大;当值为 - 1000 时,进程将被保护,不会被 OOM Killer 终止。这就像在一个公司里,不同员工有不同的优先级,重要项目的核心成员(值为 - 1000 的进程)会受到保护,而一些非关键岗位的员工(值高的进程)在公司裁员(内存不足)时更容易被裁掉。例如,对于一些系统关键进程,如负责网络通信的进程,如果将其 oom_score_adj 值设置为 - 1000,就可以确保在内存紧张时它不会被 OOM Killer 误杀,保证系统的基本网络功能正常运行。
内核参数 /proc/sys/vm/overcommit_memory 和 /proc/sys/vm/panic_on_oom 等也会影响 OOM Killer 的行为。其中,overcommit_memory 参数控制内存过度提交的策略,它有三个取值:0 表示启发式策略,系统会根据一定的算法来判断是否允许内存过度提交,这是默认值;1 表示总是允许内存过度提交,适用于一些科学计算等对内存使用比较特殊的场景;2 表示严格限制内存过度提交,不允许进程申请的内存总量超过物理内存加上交换空间乘以 vm.overcommit_ratio 的值(vm.overcommit_ratio 也是一个系统参数,默认值为 50,表示允许超额分配的物理内存百分比)。而 panic_on_oom 参数则决定在内存耗尽时系统是触发 OOM Killer 还是直接崩溃,当它的值为 0 时,触发 OOM Killer;为 1 时,系统直接崩溃。这些参数就像是系统内存管理的规则手册,不同的设置会导致 OOM Killer 在不同的情况下采取不同的行动。
3.3案例分析
曾经有一个线上的电商系统,在一次促销活动期间,流量暴增。系统中的 MySQL 数据库进程由于需要处理大量的订单数据和用户请求,占用了大量的内存。随着内存使用量的不断攀升,系统内存逐渐不足。OOM Killer 开始工作,它在遍历所有进程后,发现 MySQL 进程的内存使用量远远超过其他进程,其 oom_score 计算出来的值也非常高。
最终,OOM Killer 果断终止了 MySQL 进程,试图释放内存来挽救系统。然而,这一行为导致了整个电商系统的数据查询和订单处理功能无法正常运行,大量用户在下单时出现错误提示,给业务带来了严重的影响。
后来经过排查,发现是数据库的配置参数不合理,没有为高并发场景做好优化,同时也没有对系统内存使用进行有效的监控和预警,才导致了这样的事故发生。
代码示例:高内存占用触发 OOM 风险的场景
#include <iostream>#include <vector>#include <unistd.h> // 用于sleep函数,持续运行的进程// 数据库处理订单时的内存申请逻辑voidprocessOrderData(int orderCount){ // 无限制申请内存(对应数据库配置不合理,未限制内存使用) std::vector<char*> memoryBlocks; try { for (int i = 0; i < orderCount; ++i) { // 每次申请10MB内存,处理单条订单的数据缓存 char* block = new char[10 * 1024 * 1024]; memoryBlocks.push_back(block); // 数据写入(仅占位,无实际业务逻辑) memset(block, '0', 10 * 1024 * 1024); // 每处理1000条订单打印内存占用提示 if (i % 1000 == 0) { std::cout << "已处理" << i << "条订单,当前申请内存块数:" << memoryBlocks.size() << std::endl; } } } catch (const std::bad_alloc& e) { std::cerr << "内存申请失败:" << e.what() << std::endl; } // 进程持续运行(未释放内存,对应数据库连接池/缓存未优化) while (true) { sleep(1); // 保持进程存活,持续占用内存 } // 注:实际场景中此处应有内存释放逻辑,但因配置问题未执行,导致内存泄漏/持续占用 for (auto block : memoryBlocks) { delete[] block; }}intmain(){ std::cout << "电商促销活动启动,数据库进程开始处理订单..." << std::endl; // 促销期间海量订单(10万条),触发大量内存申请 processOrderData(100000); return 0;}
- 内存申请逻辑:通过循环申请 10MB 大小的内存块模拟订单数据缓存,对应 MySQL 处理订单时的内存分配行为,因未限制申请数量(配置不合理),内存持续飙升;
- 进程持续运行:while(true) 循环模拟数据库进程长期存活,占用的内存未及时释放,贴合线上系统中数据库连接池、查询缓存未优化的场景;
- 无内存限制:代码中未设置内存申请上限,对应 MySQL 配置(如innodb_buffer_pool_size)未根据服务器内存调整,最终导致内存耗尽触发 OOM Killer。
这个案例充分说明了 OOM Killer 在实际中的决策过程,以及它的决策可能对业务系统产生的重大影响。
四、避免内存崩溃的策略
4.1优化内存使用
在开发应用程序时,应根据实际需求选择合适的数据结构。例如,对于需要频繁查找的数据,使用哈希表(Hash Table)或红黑树(Red - Black Tree)可以大大提高查找效率,减少内存占用。相比之下,如果使用链表(Linked List)进行大量数据的查找,可能会导致时间复杂度增加,并且链表节点本身也会占用一定的内存空间。以一个电商系统的商品管理模块为例,如果使用链表来存储商品信息,每次查找某个商品时,都需要从头开始遍历链表,效率较低。而使用哈希表,通过商品的唯一标识作为键值,能够快速定位到商品信息,不仅提高了查找速度,还减少了不必要的内存遍历,从而优化了内存使用。
在程序中,对于不再使用的内存,要及时进行释放。在 C/C++ 语言中,使用 malloc 分配的内存,在使用完毕后,必须调用 free 函数进行释放;使用 new 关键字分配的内存,要使用 delete 关键字进行释放。如果不及时释放内存,就会导致内存泄漏,随着程序的运行,可用内存会越来越少,最终可能引发内存崩溃。例如,在一个图像处理程序中,如果每次处理完一张图片后,没有释放用于存储图片数据的内存,那么随着处理图片数量的增加,内存泄漏问题会逐渐加剧,最终导致程序因内存不足而崩溃。
内存池是一种内存管理技术,它在程序启动时预先分配一块较大的内存空间,然后在程序运行过程中,当需要分配内存时,直接从内存池中获取,而不是每次都向操作系统申请内存。当内存使用完毕后,也不是立即释放回操作系统,而是放回内存池,供下次使用。这样可以减少内存分配和释放的次数,提高内存分配效率,同时也能减少内存碎片的产生。比如在一个高并发的网络服务器程序中,频繁地为每个网络连接分配和释放内存会带来较大的开销,使用内存池可以大大提高内存管理的效率,降低内存崩溃的风险。
选择高效的算法可以减少内存的使用。例如,在排序算法中,快速排序(Quick Sort)的平均时间复杂度为 O (n log n),空间复杂度为 O (log n),相比冒泡排序(Bubble Sort),其时间复杂度为 O (n²),空间复杂度为 O (1),虽然冒泡排序空间复杂度较低,但在处理大量数据时,快速排序的效率更高,并且在实际应用中,由于其递归调用的特性,通过合理的优化,其空间复杂度也能得到较好的控制,从而减少内存的占用。在一个大数据量的文件排序场景中,使用快速排序算法可以在更短的时间内完成排序任务,并且占用更少的内存。
4.2配置内存限制
(1)cgroups 机制介绍:cgroups(Control Groups)是 Linux 内核提供的一种资源管理机制,它可以对一组进程进行资源限制和隔离,包括 CPU、内存、磁盘 I/O、网络带宽等资源。通过 cgroups,可以为不同的进程组设置不同的资源配额,防止单个进程或进程组占用过多的资源,从而保证系统的稳定性。在一个多租户的云服务器环境中,通过 cgroups 可以为每个租户的应用程序设置独立的内存限制,避免某个租户的应用程序因内存泄漏或异常占用过多内存,影响其他租户的正常使用。
(2)使用 cgroups 设置内存限制的步骤:
- 挂载 cgroup 文件系统:在大多数现代 Linux 发行版中,cgroup 文件系统会默认挂载。可以通过mount -t cgroup命令查看 cgroup 文件系统的挂载情况。通常,会看到类似/sys/fs/cgroup/cpu、/sys/fs/cgroup/memory等路径,这些就是不同资源控制器的挂载点。如果系统是较新的,可能会看到统一的 cgroup2 挂载点/sys/fs/cgroup。
- 创建控制组:在内存子系统目录下创建一个目录,用于代表控制组。例如,要创建一个名为my_memory_limit_group的内存控制组,可以执行命令sudo mkdir /sys/fs/cgroup/memory/my_memory_limit_group。
- 配置内存限制:进入创建的控制组目录,通过修改memory.limit_in_bytes文件来设置内存使用上限。假设要将内存限制设置为 512MB(512 * 1024 * 1024 字节),可以执行命令echo 536870912 > /sys/fs/cgroup/memory/my_memory_limit_group/memory.limit_in_bytes。此外,还可以调整memory.swappiness文件,控制该组进程的内存回收策略,memory.swappiness的值表示将内存数据交换到磁盘交换空间的倾向程度,取值范围是 0 - 100,值越大,表示越倾向于使用交换空间。
- 将进程添加到控制组:将目标进程的 PID 写入到控制组目录下的tasks文件(cgroup v1)或cgroup.procs文件(cgroup v2)中。假设进程 PID 是 12345,对于 cgroup v1,可以执行命令echo 12345 > /sys/fs/cgroup/memory/my_memory_limit_group/tasks;对于 cgroup v2,可以执行命令echo 12345 > /sys/fs/cgroup/my_memory_limit_group/cgroup.procs。也可以在新启动一个进程时直接将其放入某个 cgroup,比如使用cgexec命令(需要安装 cgroup - tools 包),例如cgexec -g memory:my_memory_limit_group your_command,这样启动的your_command进程就会被限制在my_memory_limit_group控制组的内存配额内运行。
4.3调整内核参数
- vm.watermark_scale_factor:这个内核参数用于调整内存水位线的比例因子,它会影响系统对内存压力的感知和反应时机。默认情况下,系统根据内存总量和一些固定的算法来计算高水位线和低水位线。通过调整vm.watermark_scale_factor,可以改变这些水位线的计算方式。如果将其值调小,那么系统会更早地感知到内存压力,kswapd 会更早地被唤醒进行内存回收,从而在一定程度上降低内存耗尽的风险。但如果调得太小,可能会导致系统频繁地进行内存回收操作,影响系统性能。例如,在一个内存资源相对紧张的服务器环境中,适当降低vm.watermark_scale_factor的值,可以让系统提前做好内存回收的准备,避免内存不足的情况发生。
- vm.swappiness:vm.swappiness的值表示系统将内存数据交换到磁盘交换空间(Swap)的倾向程度,取值范围是 0 - 100。默认值通常是 60,表示系统有 60% 的倾向将内存数据交换到磁盘。如果将其值设置为较低的值,比如 10,系统会更倾向于优先回收页面缓存,而不是将内存数据交换到磁盘,这样可以减少磁盘 I/O 操作,提高系统性能。但如果设置得过低,当内存真正不足时,系统可能无法及时利用交换空间,导致 OOM Killer 更容易被触发。对于一些对磁盘 I/O 性能要求较高,且内存相对充足的系统,如高性能数据库服务器,可以将vm.swappiness设置为较低的值,以减少磁盘 I/O 对数据库性能的影响;而对于一些内存资源紧张,且允许一定磁盘 I/O 开销的系统,可以适当提高vm.swappiness的值,以增加系统应对内存不足的能力。
- vm.min_free_kbytes:vm.min_free_kbytes用于设置系统保留的最小空闲内存量(单位为 KB)。系统会尽力保持至少有这么多的空闲内存,以确保在内存紧张时,有足够的内存来处理一些紧急的内存分配请求,避免系统因瞬间内存不足而触发 OOM Killer。例如,在一个运行着多个关键服务的服务器上,合理设置vm.min_free_kbytes的值,可以保证在某个服务突然出现内存需求高峰时,系统仍能有足够的内存来维持其他服务的正常运行,不至于因为内存不足而导致整个系统崩溃。
4.4监控与预警
在 Linux 系统中,可以通过多种工具来监控内存水位线及相关指标,以便及时了解系统的内存状态。/proc/zoneinfo文件提供了丰富的内存状态信息,其中就包括内存水位线 。通过以下命令可以查看各个内存域(zone)的 min、low、high 水位线:
cat /proc/zoneinfo | grep -E "Node|min|low|high"
执行上述命令后,会输出类似以下的内容:
Node 0, zone DMA pages free 30387 min 1485 low 1856 high 2227Node 0, zone Normal pages free 460692 min 1378 low 1852 high 2326
从输出结果中,可以清晰地看到每个内存域的当前空闲页数以及对应的 min、low、high 水位线数值 。这有助于我们直观地了解系统内存的使用情况和水位线状态,判断内存是否紧张或充足 。
三种常见的监控工具如下:
- dstat:dstat 是一个全能的系统资源统计工具,它可以同时监控 CPU、内存、磁盘 I/O、网络等多项指标,并且支持自定义插件扩展。通过 dstat,可以直观地看到系统内存的使用情况,包括已用内存、空闲内存、缓存内存、交换内存的使用量等信息。例如,执行dstat -m命令,可以专门查看内存相关的统计信息,实时了解内存的动态变化。
- vmstat:vmstat 提供了虚拟内存统计信息,能够显示关于进程、内存、分页、块 I/O、陷阱及 CPU 活动的信息。通过vmstat 1命令(其中 1 表示每 1 秒更新一次统计信息),可以实时监控内存的使用情况,如空闲内存的变化、内存分页的情况等,帮助管理员及时发现内存使用的异常趋势。
- top:top 是一个常用的实时监控工具,它显示了系统中各个进程的资源占用情况,包括内存使用。通过 top 命令的交互式界面,用户可以查看到总的内存使用情况以及每个进程的详细内存消耗,还可以按 Shift + M 键按照内存使用量对进程进行排序,方便快速定位占用内存较多的进程。
接下来,我们将重点对vmstat进行详细介绍,深入了解其功能、使用方法及核心输出指标的含义。
Vmstat(Virtual Memory Statistics,虚拟内存统计)是一款内置的系统监控工具,适用于Linux、Unix等类Unix操作系统,主要用于监控系统的内存状态、进程状态、CPU活动、磁盘I/O等核心资源的运行情况。使用vmstat命令可以查看内存的相关指标,如空闲内存(free)、已用内存(used)、交换空间使用情况(swap)等 。通过观察这些指标的变化趋势,也能间接了解内存水位线的变化情况 。例如,执行以下命令:
上述命令会每秒输出一次系统性能统计信息,其中与内存相关的部分包括:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 1843752 3852 10828 0 0 0 0 390 648 1 1 98 0 0
在这个输出中,free字段表示当前系统的空闲内存大小(单位为 KB) 。通过持续观察free字段的值,可以了解系统内存的使用趋势 。如果free值持续下降,接近或低于 min 水位线对应的内存值,就需要警惕内存紧张的情况 。同时,结合si(从磁盘交换到内存的数据量)和so(从内存交换到磁盘的数据量)字段,可以判断系统是否频繁进行内存交换,这也与内存水位线和内存回收机制密切相关 。
(2)设置预警策略:可以结合监控工具和一些系统监控告警工具,如 Zabbix、Prometheus + Grafana 等,设置内存使用的预警策略。例如,在 Zabbix 中,可以设置当系统内存使用率超过 80% 时,触发告警通知管理员;或者当某个关键进程的内存使用量在短时间内增长超过一定阈值时,发送告警信息。通过这样的预警策略,可以让管理员在内存问题变得严重之前,及时采取措施,如手动释放内存、调整程序配置、重启相关服务等,避免内存崩溃的发生。
五、Linux水位配置实战
5.1问题初现
那天上午,我像往常一样,打开监控系统查看服务器状态,一切看似正常。然而没过多久,用户反馈系统变得异常卡顿,一些原本秒级响应的操作,现在要等待十几秒甚至更长时间。我立刻警觉起来,赶紧登录到服务器进行查看。当我尝试执行一些简单的命令时,也能明显感觉到服务器的迟缓,命令的执行结果要很久才返回。查看应用程序的日志,发现大量的请求超时错误,这表明服务响应出现了严重问题。同时,通过监控工具查看服务器的资源利用率,发现 CPU 使用率虽然没有达到 100%,但一直处于较高的波动状态,而内存使用率看似在正常范围内,却没有明显的空闲内存可供使用。
为了进一步了解情况,我尝试在服务器上启动一些新的进程,结果发现新进程的启动也变得异常缓慢,甚至有些进程直接因为内存分配失败而启动失败。这些现象都表明,服务器正面临着严重的性能问题,而问题的根源很可能与内存有关。
面对这些异常现象,我首先想到的是通过一些常用的命令来查看系统资源的使用情况。我使用top命令,查看系统中各个进程的资源占用情况,试图找出是否有某个进程占用了大量的 CPU 或内存资源。在top命令的输出中,我发现并没有某个进程的 CPU 或内存使用率特别高,所有进程的资源占用看起来都比较正常,这让我感到有些困惑。
接着,我使用vmstat命令来查看系统的虚拟内存统计信息,包括内存、CPU、磁盘 I/O 等方面的情况。通过vmstat的输出,我发现内存的si(Swap In)和so(Swap Out)值都比较高,这意味着系统正在频繁地进行内存和磁盘之间的数据交换,即 Swap 操作。频繁的 Swap 操作通常是由于物理内存不足导致的,这进一步印证了我的猜测,问题很可能出在内存上。
同时,我还注意到vmstat输出中的free内存值非常低,几乎接近于 0,而buff/cache的值却比较高。这表明系统的缓存占用了大量的内存,导致可用的空闲内存减少。但仅仅从这些信息中,我还无法确定问题的根本原因,是系统内存真的不足,还是内存分配和回收机制出现了问题呢?为了找到答案,我决定进一步深入分析系统的内存水位配置和内存回收情况。
5.2探寻水位配置
查看水位配置,为了找出系统频繁触发直接回收的原因,首先需要查看当前系统的内存水位配置。在 Linux 系统中,可以通过/proc/zoneinfo文件来获取各个内存区域的水位线信息。这个文件包含了丰富的内存管理相关数据,对于我们诊断问题至关重要。使用以下命令查看/proc/zoneinfo文件中与水位线相关的内容:
cat /proc/zoneinfo | grep -E "Node|min|low|high|managed"
执行上述命令后,会得到类似以下的输出:
Node 0, zone DMA pages free 3968 min 77 low 96 high 115 spanned 4095 present 3997 managed 3976Node 0, zone DMA32 pages free 43834 min 12202 low 15252 high 18302 spanned 1044480 present 782288 managed 759709Node 0, zone Normal pages free 5754 min 4615 low 5768 high 6921 spanned 262144 present 262144 managed 236760
从输出中可以看到,每个内存区域(如DMA、DMA32、Normal)都有对应的free(空闲页数)、min(最小水位线)、low(低水位线)、high(高水位线)、spanned(总页数)、present(实际存在的页数)和managed(被管理的页数)等信息 。这些信息反映了当前内存区域的状态和水位配置情况,是我们后续分析问题的重要依据。
通过查看水位配置信息,我们可以进一步分析为什么系统会频繁触发直接回收。在我处理的这个案例中,经过仔细观察发现,Normal内存区域的min水位线设置相对较低 。这意味着,当系统内存使用量稍有增加,可用内存很容易就会下降到min水位线以下,从而触发直接回收。
举个例子,假设系统中某个时刻的内存使用情况如下:Normal区域的可用内存为 5000 页,而min水位线设置为 4615 页 。当一个新的内存分配请求到来时,即使这个请求只需要少量的内存,比如 100 页,由于可用内存 5000 页减去 100 页后就会低于min水位线 4615 页,系统就会立即触发直接回收 。这种频繁的直接回收操作会导致分配内存的进程被阻塞,从而影响整个系统的性能,导致应用程序出现卡顿现象。
此外,还需要考虑到系统中内存使用的动态变化。如果系统中存在一些内存使用量波动较大的应用程序,或者有大量的并发内存分配请求,那么较低的min水位线就更容易使系统陷入内存紧张的状态,频繁触发直接回收 。而且,直接回收是同步进行的,它会阻塞当前请求内存的进程,直到回收足够的内存。这就像是在一条繁忙的道路上,每辆车都要停下来等待道路清理,交通自然就会变得拥堵不堪,系统的响应速度也会大大降低 。
通过对水位配置的查看和分析,我们基本确定了系统频繁触发直接回收的原因是内存水位线设置不合理,特别是min水位线过低,导致系统在内存稍有压力时就进入了直接回收状态 。那么,接下来该如何解决这个问题呢?我们将在下一部分详细探讨解决方案。
5.3调整水位配置
经过一番研究和分析,我们决定通过调整内存水位配置来解决系统频繁触发直接回收的问题。具体来说,就是增大vm.min_free_kbytes的值 ,提高min水位线。这样做的目的是让系统在内存使用量增加时,有更多的缓冲空间,不至于过快地触发直接回收 ,从而减少对应用程序性能的影响。
vm.min_free_kbytes这个参数设置了系统保留的全局最小空闲内存(单位为 KB) ,它直接影响着各个内存区域的min水位线,而low和high水位线则是基于min水位线计算得出的 。在之前的分析中,我们发现当前系统的min水位线较低,导致系统很容易进入直接回收状态。因此,适当增大vm.min_free_kbytes的值,可以提高min水位线,使系统在内存管理上更加稳健。
在确定具体的调整数值时,我们参考了系统的总内存大小以及当前的内存使用情况。一般来说,可以根据系统总内存的一定比例来设置vm.min_free_kbytes,比如 1% - 3% 。对于我们这台服务器,总内存为 16GB,经过综合考虑,我们决定将vm.min_free_kbytes的值设置为 335544(即 320MB,约为总内存的 2%) 。这样既能保证系统有足够的空闲内存来应对突发的内存分配请求,又不会过度占用内存资源,影响系统的整体性能。
确定好调整方案后,接下来就是实际的操作步骤了。在 Linux 系统中,可以通过两种方式来调整vm.min_free_kbytes的值:一种是使用sysctl命令进行临时调整,这种方式在系统重启后设置会失效;另一种是修改/etc/sysctl.conf文件进行永久调整 。下面我们分别来看这两种方法的具体操作步骤。
(1)使用 sysctl 命令临时调整: sysctl命令是 Linux 系统中用于动态修改内核参数的工具,使用它可以方便地临时修改vm.min_free_kbytes的值。具体操作如下:
# 临时设置vm.min_free_kbytes为335544KB(320MB)sysctl -w vm.min_free_kbytes=335544
执行上述命令后,系统会立即将 vm.min_free_kbytes 的值设置为 335544KB 。可以使用以下命令来验证设置是否生效:
sysctl vm.min_free_kbytes
如果输出结果为vm.min_free_kbytes = 335544,则说明设置成功 。这种临时调整方式适用于快速验证调整效果,或者在不需要长期保持设置的情况下使用 。但需要注意的是,一旦系统重启,设置就会恢复为原来的值。
(2)修改 /etc/sysctl.conf 文件永久调整:如果希望调整后的vm.min_free_kbytes值在系统重启后仍然生效,就需要修改/etc/sysctl.conf文件。具体步骤如下:
# 使用文本编辑器(如vi或nano)打开sysctl.conf文件sudo vi /etc/sysctl.conf
在打开的文件中,找到 vm.min_free_kbytes 这一行(如果没有,则在文件末尾添加) ,并将其值修改为 335544,如下所示:
# 设置vm.min_free_kbytes为335544KB(320MB)vm.min_free_kbytes = 335544
修改完成后,保存并退出文件编辑器 。接下来,需要执行以下命令使修改后的配置生效:
# 加载/etc/sysctl.conf文件中的配置sudo sysctl -p
执行sysctl -p命令后,系统会读取/etc/sysctl.conf文件中的配置,并应用到当前系统中 。同样,可以使用sysctl vm.min_free_kbytes命令来验证设置是否生效 。通过这种方式修改的vm.min_free_kbytes值会在系统重启后仍然保持,是一种更为持久的配置方式 。
完成水位配置调整后,我们还需要密切观察系统的运行情况,确保问题得到彻底解决,并且没有引入新的问题。在下一部分,我们将详细介绍调整后的效果验证和后续的观察过程。
5.4效果验证
在完成内存水位配置的调整后,我迫不及待地通过监控工具(Prometheus + Grafana)查看系统的各项指标变化 。这就像是给系统安装了一个精密的 “健康监测仪”,能够实时反馈系统的运行状态。在 Grafana 的仪表盘上,内存使用情况一目了然。之前一直处于低位徘徊的可用内存,如今有了明显的提升 。原本几乎触底的空闲内存曲线,现在稳定在一个相对较高的水平,不再像之前那样轻易地逼近最小水位线。这表明系统有了更充足的内存缓冲空间,能够更好地应对各种内存分配请求。再看直接回收次数的统计图表,变化更是惊人。
调整前,直接回收次数频繁到几乎成为一条密集的 “锯齿状” 折线,每一次的直接回收都像是给系统性能带来一次冲击。而调整后,直接回收次数大幅下降,折线变得平缓许多,偶尔出现的几次直接回收,也在系统可承受的范围内 。这说明调整后的水位配置有效地减少了直接回收的触发频率,让系统不再频繁地陷入紧急内存回收状态。
此外,通过 Prometheus 采集的其他指标,如内存利用率、kswapd 后台回收的活跃度等,也都显示出良好的变化趋势。内存利用率更加稳定,不再出现剧烈波动;kswapd 后台回收的频率也处于合理区间,能够在系统内存有一定压力时,有条不紊地进行内存回收工作,而不是像之前那样频繁地被唤醒,又在短时间内陷入忙碌状态 。这些指标的积极变化,让我对这次调整充满了信心。
除了监控指标的明显改善,系统的实际性能也得到了显著恢复。曾经卡顿得让人抓狂的服务器,如今又恢复了往日的流畅。应用程序的响应速度大幅提升,原本需要等待十几秒甚至更长时间的操作,现在几乎瞬间就能完成。用户的投诉电话也逐渐减少,业务系统重新回到了高效运行的轨道。在服务器上执行各种命令时,再也感受不到之前的那种迟缓,命令的执行结果迅速返回,就像给系统换上了一颗强劲的 “心脏”,让它能够快速而稳定地处理各种任务。新启动的进程也能顺利运行,不再因为内存分配失败而出现问题。
整个系统的运行状态焕然一新,仿佛重获新生。从业务层面来看,由于系统性能的恢复,服务的可用性和稳定性得到了极大保障。用户体验明显提升,业务交易量也逐渐回升,这对于公司的业务发展来说,无疑是一个积极的信号。这次成功解决系统频繁触发直接回收的问题,让我深刻体会到了深入理解系统原理和正确配置参数的重要性 。在今后的运维工作中,我也将更加注重对系统细节的把控,及时发现并解决潜在的性能问题,确保生产环境的稳定运行。