针对 SUSE Linux SWAP 占用率的分析及应对策略
【作者】张希尧,2016年加入民生银行,从事x86平台软硬件运维工作,主要负责Linux操作系统及虚拟化平台部分。
一、场景介绍
我们在运维过程中发现我们的Linux计算内存使用率明明不高,但是却用到了swap空间,使用率到了一定程度就触发了监控告警。本文主要讨论我们的Linux操作系统在什么情况下使用swap空间,我们该如何应对swap利用率上升。
二、 swap的使用及内存回收
Linux的swap空间用途主要是在计算内存利用率达到换页水位线时起到对计算内存的补充,另外还有一个特别的用途区别于其他操作系统,是在匿名内存页(anonymous memory page)回收时充当交换区域,因此今天我们主要讨论回收匿名内存页这部分。
在讨论这个之前,我们先来简单介绍一下内存回收。Linux内存回收主要分为以下两种情况:在系统内存不够用触发直接回收(direct reclaim)和通过内核回收线程kswapd周期性回收。Linux可以被回收的内存主要分为两类,文件缓存(file cache)和匿名内存(anonymous memory)。在进行内存回收(memory reclaim)时内核通过扫描来确定我们分配的内存是何种状态inactive或active,而将可以回收的内存放入链表描述符lruvec四个链表中,分别为:
LRU_INACTIVE_ANON
LRU_ACTIVE_ANON
LRU_INACTIVE_FILE
LRU_ACTIVE_FILE
在源代码有头文件中枚举定义了这些链表,见下图:
enum lru_list {
LRU_INACTIVE_ANON = LRU_BASE,
LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE
+ LRU_ACTIVE,
LRU_UNEVICTABLE,NR_LRU_LISTS
}
现在简单说一下内存回收的过程,首先内存回收是以zone(memcg我们暂时不讨论)的单位进行的,我们在系统中可以通过cat /proc/zoneinfo查看zone的信息。而无论是直接回收还是周期性回收唯一的内核函数入口都是shrink_zone函数,而它的输入包括一个叫做scan_control的结构体,这个结构体的功能就是之前提到的kernel对内存的扫描。
static bool shrink_zone(struct zone *zone, struct scan_control *sc,
bool is_classzone)
{
…
.
…
return reclaimable;
}
针对于匿名内存页kernel扫描时会将使用率高的匿名放入active_anon链表反之放入inactive_anon链表。如下源码判定是否要做链表转移的操作。
#ifdef CONFIG_SWAP
static bool inactive_anon_is_low_global(struct zone *zone)
{
unsigned long active, inactive;
active = zone_page_state(zone,
NR_ACTIVE_ANON);
inactive = zone_page_state(zone,
NR_INACTIVE_ANON);
/*针对于anon页通过inactive与inactive率的乘积与active比较返回真假,判定是否inactive页太少*/
return inactive * zone->inactive_ratio < active;
}
/*inactive_anon_is_low检查某个zone中是否应该把active_anon放入inactive_anon,返回true表示zone中没有足够的inactive_anon,需要此操作*/
static bool inactive_anon_is_low(struct lruvec *lruvec)
{
/*如有系统中无swap空间,就返回false不做此类操作*/
if (!total_swap_pages)
return false;
if (!mem_cgroup_disabled())
return mem_cgroup_inactive_anon_is_low
(lruvec);
/*返回到全局的inactive_anon检查函数*/
return inactive_anon_is_low_global
(lruvec_zone(lruvec));
}
#else
static inline bool inactive_anon_is_low
(struct lruvec *lruvec)
{
return false;
}
#endif
当所有的内存页通扫描完成链表转移后,kernel将最终需要回收的页面放到page_list这个场所中,通过调用shrink_page_list回收,回收过程大致可以理解为:
扫描->隔离->更新页状态->回收->回收后剩余归还->更新归还页状态->返回回收统计结果(实际过程复杂很多,细节不做讨论)
static noinline_for_stack unsigned long
shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,struct scan_control *sc, enum lru_list lru)
{
LIST_HEAD(page_list);
/*nr_为各种计数变量*/
unsigned long nr_scanned;
unsigned long nr_reclaimed = 0;
unsigned long nr_taken;
unsigned long nr_dirty = 0;
unsigned long nr_congested = 0;
unsigned long nr_unqueued_dirty = 0;
unsigned long nr_writeback = 0;
unsigned long nr_immediate = 0;
isolate_mode_t isolate_mode = 0;
int file = is_file_lru(lru);
/*链表描述符对应的内存zone*/
struct zone *zone = lruvec_zone(lruvec);
/*zone回收的状态*/
struct zone_reclaim_stat *reclaim_stat = &lruvec- >reclaim_stat;
while (unlikely(too_many_isolated(zone, file, sc))) {
congestion_wait(BLK_RW_ASYNC, HZ/10);
if (fatal_signal_pending(current))
return SWAP_CLUSTER_MAX;
}
lru_add_drain();
if (!sc->may_unmap)
isolate_mode |= ISOLATE_UNMAPPED;
if (!sc->may_writepage)
isolate_mode |= ISOLATE_CLEAN;
/*给zone中的链表上自旋锁*/
spin_lock_irq(&zone->lru_lock);
/*通过扫描将lru链表中欲回收页面隔离至page_list中, 并返回隔离的个数*/
nr_taken = isolate_lru_pages(nr_to_scan, lruvec,&page_list,&nr_scanned, sc, isolate_mode, lru);
/*隔离后更新lru的大小*/
update_lru_size(lruvec, lru, -nr_taken);
/*修改zone中被隔离的页的状态*/
__mod_zone_page_state(zone,NR_ISOLATED_ANON + file, nr_taken);
reclaim_stat->recent_scanned[file] += nr_taken;
if (global_reclaim(sc)) {
/*修改zone中已扫描页的状态*/
__mod_zone_page_state(zone,NR_PAGES_SCANNED, nr_scanned);
/*统计通过kswap周期回收线程扫描的事件数*/
if (current_is_kswapd())
__count_zone_vm_events(PGSCAN_KSWAPD,zone, nr_scanned);
/*统计通过直接回收线程扫描的事件数*/
else
__count_zone_vm_events(PGSCAN_DIRECT,zone, nr_scanned);
}
/*解开zone的自旋锁*/
spin_unlock_irq(&zone->lru_lock);
if (nr_taken == 0)
return 0;
/*对page_list中的页进行回收,返回回收页的数量*/
nr_reclaimed = shrink_page_list(&page_list, zone, sc, 0,&nr_dirty, &nr_unqueued_dirty,&nr_congested,&nr_writeback, &nr_immediate,false);
/*对lru加自旋锁*/
spin_lock_irq(&zone->lru_lock);
if (global_reclaim(sc)) {
/*统计通过kswap回收偷页的事件数*/
if (current_is_kswapd())
__count_zone_vm_events(PGSTEAL_KSWAPD,zone,nr_reclaimed);
/*统计通过直接回收偷页的事件数*/
else
__count_zone_vm_events(PGSTEAL_DIRECT,zone,nr_reclaimed);
}
/*将回收后剩余的页放回对应的链表*/
putback_inactive_pages(lruvec, &page_list);
/*再次修改zone中被隔离的页的状态*/
__mod_zone_page_state(zone, NR_ISOLATED_ANON + file,-nr_taken);
/*释放链表自旋锁*/
spin_unlock_irq(&zone->lru_lock);
mem_cgroup_uncharge_list(&page_list);
free_hot_cold_page_list(&page_list, true);
/*判断因隔离产生的回写,这部分不包含脏页回写,并标记*/
if (nr_writeback && nr_writeback == nr_taken)
set_bit(ZONE_WRITEBACK, &zone->flags);
if (sane_reclaim(sc)) {
/*判断因设备拥堵的待回收脏页,并标记*/
if (nr_dirty && nr_dirty == nr_congested)
set_bit(ZONE_CONGESTED, &zone->flags);
/*判断被隔离的脏页,并标记*/
if (nr_unqueued_dirty == nr_taken)
set_bit(ZONE_DIRTY, &zone->flags);
/*如果因为回收产生的回写,等待设备0.1秒*/
if (nr_immediate && current_may_throttle())
congestion_wait(BLK_RW_ASYNC, HZ/10);
}
if (!sc->hibernation_mode && !current_is_kswapd() &&
current_may_throttle())
wait_iff_congested(zone, BLK_RW_ASYNC, HZ/10);
/*追踪扫描回收过程*/
trace_mm_vmscan_lru_shrink_inactive(zone>zone_pgdat- >node_id,zone_idx(zone),nr_scanned, nr_reclaimed,sc->priority,trace_shrink_flags(file));
/*返回回收页的数量*/
return nr_reclaimed;
}
三、生产案例
操作系统为suse 11sp4的运行java应用。系统配置32G内存,4G swap空间,swap使用了1435M,系统内还有至少一半的内存在buffer/cache使用,因此可以说我们真正用于计算的内存只有50%左右,远没有达成因内存耗尽才使用swap的那种场景。
使用top查看系统内存使用的排序如下图,占用率较高的均为java应用。我们可以看到排名前六位的实际使用内存(RES)不超过2.5G,但虚拟内存(VIRT)是基本都是实际使用内存的1.5-5倍。
通过与运维人员了解,运行的应用为多线程环境。在glibc2.11版本之后操作系统为解决多线程应用的内存挣用问题,会预先分配给线程类型为anon内存池。而我们使用的suse 11sp4正好内置glibc-2.11.3-17.84.1,已经引入了这个机制。虽然我们java程序使用的是JVM管理内存但是底层还是会与操作系统进行交互进行内存分配,正因为这种机制我们看到的虚拟内存会比实际内存高很多。
当然,仅从这些输出我们还不能确定是否是这些java程序引起的swap使用。通过个人编写的swap分析脚本,对进程使用swap空间从高到低进行排序,取前30位得到如下结果,我们可以看到排名前6的进程PID正好与top输出吻合。
有了这些进程的信息,我们就可以通过pmap来查看进程内存的映射,以PID9624这条进程的分析,我们可以看到发生swap的内存地址段除了heap区基本都是[anon]内存。这也就印证了之前我们说的匿名内存的释放需要使用swap空间。
案例分析结论:从以上分析我们可以得出这套环境的swap空间均为java多线程程序中的匿名内存交换中使用。
四、SWAP使用率高的应对策略
首先,不能单一以swap的利用率判定系统是否出现性能问题,要结合系统内的swap in/out频率和操作系统计算内存使用率这些指标综合关注。关注重点更多的放在页的换入换出比较合适,因为页的换入换出会有额外的I/O开销,而且这种情况很可能暗示我们的计算内存非常紧张。
其次,为了应对java多线程预分配anon内存,致使应用进程virtual内存过高,最后导致swap的情况。我们可以在应用用户的profile文件配置一个参数来限定预先分配给多线程的内存池的个数。修改的方法如下:
vi应用用户配置文件,添加如下一行,保存退出.
export MALLOC_ARENA_MAX=1
arena就是预先分配的匿名内存池,在一个64位的linux操作系统中一条线程默认可以有cpu核数*8个arena,而如果我们设置了MALLOC_ARENA_MAX=1那么一条线程只能使用1个arena。这样我们可以大大降低线程分配的anon内存,相应的就会降低了swap发生的几率。
五、总结
最后总结一下,对于swap使用的情况,我们主要关注计算内存是否真的使用率很高,还有swap的换入换出是否很频繁。后续我们可以通过调整监控告警的方式优化对swap的监控。
本文由公众号【民生运维】授权转载 觉得本文有用,请转发或点击“在看”,让更多同行看到
欢迎关注社区 “系统运维”技术主题 ,将会不断更新优质资料、文章。地址:
http://www.talkwithtrend.com/Topic/199
*本公众号所发布内容仅代表作者观点,不代表社区立场