Swap,性能之鸿沟,生死之地,存亡之道,不可不省也。这一句话足以表明 Swap 在操作系统生态中的特殊地位,以及能否正确运用,对业务架构或多或少产生较大影响。
在进行部署 Kubernetes 时,我们往往会发现这样一种场景:官方强烈建议在环境初始化时关闭 Swap 空间 。比如在进行 K8S 相关操作时,给予如下提示:
Running with swap on is not supported, please disable swap! or set --fail-swap-on flag to false
或者抛以下错误信息:
[ERROR Swap]: running with swap on is not supported. Please disable swap
当然,其实,不止 Kubernetes ,其他的组件,例如,Hadoop、ES 等等集群同样也不建议开启 Swap。
那么,什么是 Swap 呢?
通常来讲,Swap Space 是开辟在操作系统磁盘上的一块区域,此块区域可以是一个分区,也可以是一个文件,或者是他们的组合。基于其场景特性,也就是说:当操作系统物理内存不够用时,Linux 系统会将内存中不常访问的数据同步至 Swap 上,这样系统就有更多的物理内存为各个进程服务;反之,当操作系统需要访问 Swap 上存储的内容时,再将Swap 上的数据加载到内存中,这就是我们常说的 Swap Out 和 Swap In。
关于 Swap In 和 Swap Out 相关活动状态关联,可参考如下示意图:
针对Swap分区的大小以及使用情况,通常可以借助 free -m 命令行进行查看,具体如下所示:
[administrator@JavaLangOutOfMemory ~ %] free -m total used free shared buffers cached Mem: 32167 2055 20785 0 28 296 -/+ buffers/cache: 530 470 Swap: 0 0 0
基于上述查询结果所示,Swap 大小为 0 M,表明当前操作系统没有使用 Swap 分区。除此,我们还可以使用 Swapon 命令查看当前 Swap 相关信息:例如 Swap 空间是 Swap Partition,Swap Size,以及使用情况等详细信息,具体如下所示:
[administrator@JavaLangOutOfMemory ~ %] swapon -s
基于当前的操作系统属性,Linux中存在两种形式的 Swap 分区:Swap Disk 和 Swap File。Swap Disk 是一个专门用于做 Swap 的块设备,作为裸设备提供给 Swap 机制操作;而后者 Swap File 则是存放在文件系统上的一个特定文件,其实现依赖于不同的文件系统,会有所差异,我们可通过如下参考示意图,具体如下所示:
(此图源自:http://jake.dothome.co.kr/wp-content/uploads/2019/10/swap-8a.png)
针对上述两种不同的 Swap 分区,我们可通过 mkswap 命令可以将一个 Swap Disk 或Swap File 转换为 Swap 分区的格式。随后可通过 Swapon 和 Swapoff 命令开启或关闭对应的 Swap 分区。通过 cat /proc/swaps 或 swapon -s 可以查看使用中的 Swap 分区的状态。针对 Swap 的数据结构,在 Linux 操作系统中,内核中使用 swap_info_struct 结构体对 Swap 分区进行管理,具体如下所示:
/* * The in-memory structure used to track swap areas. */ struct swap_info_struct { unsigned long flags; /* SWP_USED etc: see above */ signed short prio; /* swap priority of this type */ struct plist_node list; /* entry in swap_active_head */ signed char type; /* strange name for an index */ unsigned int max; /* extent of the swap_map */ unsigned char *swap_map; /* vmalloc'ed array of usage counts */ struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */ struct swap_cluster_list free_clusters; /* free clusters list */ unsigned int lowest_bit; /* index of first free in swap_map */ unsigned int highest_bit; /* index of last free in swap_map */ unsigned int pages; /* total of usable pages of swap */ unsigned int inuse_pages; /* number of those currently in use */ unsigned int cluster_next; /* likely index for next allocation */ unsigned int cluster_nr; /* countdown to next cluster search */ struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */ struct swap_extent *curr_swap_extent; struct swap_extent first_swap_extent; struct block_device *bdev; /* swap device or bdev of swap file */ struct file *swap_file; /* seldom referenced */ unsigned int old_block_size; /* seldom referenced */ #ifdef CONFIG_FRONTSWAP unsigned long *frontswap_map; /* frontswap in-use, one bit per page */ atomic_t frontswap_pages; /* frontswap pages in-use counter */ #endif spinlock_t lock; /* * protect map scan related fields like * swap_map, lowest_bit, highest_bit, * inuse_pages, cluster_next, * cluster_nr, lowest_alloc, * highest_alloc, free/discard cluster * list. other fields are only changed * at swapon/swapoff, so are protected * by swap_lock. changing flags need * hold this lock and swap_lock. If * both locks need hold, hold swap_lock * first. */ spinlock_t cont_lock; /* * protect swap count continuation page * list. */ struct work_struct discard_work; /* discard worker */ struct swap_cluster_list discard_clusters; /* discard clusters list */ struct plist_node avail_lists[0]; /* * entries in swap_avail_heads, one * entry per node. * Must be last as the number of the * array is nr_node_ids, which is not * a fixed value so have to allocate * dynamically. * And it has to be an array so that * plist_for_each_* can work. */ };
如上所示,一个 swap_info_struct 对应一个 Swap 分区。Swap 分区内部会以 Page 大小为单位划分出多个 Swap Slot,同时通过 swap_map 对每个 Slot 的使用情况进行记录,为 0 代表空闲,大于 0 则代表该 Slot 被 Map 的进程数量。
在此,基于源码角度,我们来简要了解下 Swap In 和 Swap Out 的相关工作流程,具体如下。
Swap In
Swap In 的入口为 do_swap_page,由于物理页面被回收了,所以进程再次访问一块虚拟地址时,就会产生缺页中断,最终进入到 do_swap_page,在这个函数中会重新分配新的页面,然后再从 Swap 分区读回这块虚拟地址对应的数据。部分代码分析如下所示:
int do_swap_page(struct vm_fault *vmf) { …… entry = pte_to_swp_entry(vmf->orig_pte); //从pte中获取swap entry,即把orig_pte 强制类型转换成swp_entry_t类型 …… page = lookup_swap_cache(entry, vma, vmf->address); //在 swap cache 中查找entry 对应的page swapcache = page; if (!page) { //如果在swap cache中没找到,则进入if代码段 struct swap_info_struct *si = swp_swap_info(entry); //获取swap分区描述符 …… page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vmf); //分配一个page,并从swap分区中读出数据填充到page中,再把page放入swap cache中缓存,此时page的PG_lock被置位了,需要等待IO读操作完成才清零,即page被lock住,如果别人想lock该page,则需要等待该page被unlock swapcache = page; …… } locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags); //此时尝试去lock该page,成功则返回1,失败则返回0 …. if (!locked) { //显然此时返回是0,即page的IO读操作仍末完成。 ret |= VM_FAULT_RETRY; //设置返回标记为retry goto out_release; //返回重新尝试 do_swap_page,但在重新尝试do_swap_page时则可以从page cache 中直接获取到该page,不需要再从swap分区中读数据了 } //程序走到这表明该page的IO读操作已经完成 …… pte = mk_pte(page, vmf->vma_page_prot); //根据page的物理地址,以及该page的保护位生成pte if ((vmf->flags & FAULT_FLAG_WRITE) && reuse_swap_page(page, NULL)) { //如果该缺页中断为写访问异常时,并且page只有一个进程使用,则把该page从swap cache 中删除,并清除对应在swap分区中的数据,下面会分析reuse_swap_page函数 pte = maybe_mkwrite(pte_mkdirty(pte), vmf->vma_flags); //设置pte中的可写保护位和PTE_DIRTY位 vmf->flags &= ~FAULT_FLAG_WRITE; ret |= VM_FAULT_WRITE; exclusive = RMAP_EXCLUSIVE; } …… set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte); 更新该虚拟地址对应的pte …… do_page_add_anon_rmap(page, vma, vmf->address, exclusive); //建立新的匿名映射 mem_cgroup_commit_charge(page, memcg, true, false); activate_page(page); //把该page放入active anonymouns lru链表中 …… swap_free(entry); //更新页槽的counter,即减一,如果counter等于0,说明需要该存储块数据的人已经全部读回到内存,并且该page也不在swap cache 中,那么直接清除存储块数据,即回收页槽,释放更多的swap空间 if (mem_cgroup_swap_full(page) || (vmf->vma_flags & VM_LOCKED) || PageMlocked(page)) try_to_free_swap(page); //如果swap分区满了,则尝试回收无用页槽 unlock_page(page); //解锁 PG_lock …… out: return ret; …… }
Swap Out
Swap Out 的入口是在 shrink_page_list, 此函数对 page_list 链表中的内存依次处理,回收满足条件的内存即当系统需要回收物理内存时发生 Swap Out 的动作。第一次 Shrink时,内存页会通过 add_to_swap 分配到对应的 Swap Slot,设置为脏页并进行回写,最后将该 Page 加入到 Swap Cache 中,但不进行回收。第二次 Shrink 时,若脏页已经回写完成,则将该 Page 从 Swap Cache 中删除并回收。部分代码分析如下所示:
static unsigned long shrink_page_list(struct list_head *page_list, struct pglist_data *pgdat, struct scan_control *sc, enum ttu_flags ttu_flags, struct reclaim_stat *stat, bool force_reclaim) { LIST_HEAD(ret_pages); //初始化返回的链表,即把此次shrink无法回收的页面放入该链表中 LIST_HEAD(free_pages); //初始化回收的链表,即把此次shrink 可以回收的页面放入该链表中 … while (!list_empty(page_list)) { … page = lru_to_page(page_list); list_del(&page->lru); // 从 page_list 中取出一个 page,page_list 需要回收的page链表 if (!trylock_page(page)) //先判断是用否有别的进程在使用该页面,如果没有则设置PG_lock,并返回1, 这个flag多用于io读, 但此时第一次shrink时大多数情况下是没有别的进程在使用该页面的,所以接着往下走 goto keep; may_enter_fs = (sc->gfp_mask & __GFP_FS) || (PageSwapCache(page) && (sc->gfp_mask & __GFP_IO)); if (PageAnon(page) && PageSwapBacked(page)) { //判断是否是匿名页面并且不是lazyfree的页面,显然这个条件是满足的 if (!PageSwapCache(page)) { //判断该匿名页面是否是 swapcache ,即通过page的 PG_swapcache 的flag 来判断,此时该页面第一次 shrink,所以这里是否,进入if里面的流程 … if (!add_to_swap(page)) //为该匿名页面创建swp_entry_t,并存放到page->private变量中,把page放入 swap cache,设置page的PG_swapcache和PG_dirty的flag,并更新swap_info_struct的页槽信息,该函数是通往 swap core 和swap cache的接口函数,下面会分析 { … goto activate_locked; // 失败后返回 } … /* Adding to swap updated mapping */ mapping = page_mapping(page); // 根据page中的swp_entry_t获取对应的swapper_spaces[type][offset],这里可回顾一下数据结构章节中的swapper_spaces的介绍。 } } else if (unlikely(PageTransHuge(page))) { /* Split file THP */ if (split_huge_page_to_list(page, page_list)) goto keep_locked; } /* * The page is mapped into the page tables of one or more * processes. Try to unmap it here. */ if (page_mapped(page)) { enum ttu_flags flags = ttu_flags | TTU_BATCH_FLUSH; if (unlikely(PageTransHuge(page))) flags |= TTU_SPLIT_HUGE_PMD; if (!try_to_unmap(page, flags, sc->target_vma)) { // unmap, 即与上层的虚拟地址解除映射关系,并修改pte,使其值等于 page->private,即swp_entry_t变量,等到swapin 时就直接把pte强制类型转换成swp_entry_t 类型的值,就可以得到entry了。 nr_unmap_fail++; goto activate_locked; } } if (PageDirty(page)) { //由于add_to_swap 函数最后把该页面设置为脏页面,所以该if成立,进入if里面 … /* * Page is dirty. Flush the TLB if a writable entry * potentially exists to avoid CPU writes after IO * starts and then write it out here. */ try_to_unmap_flush_dirty(); switch (pageout(page, mapping, sc)) { // 发起 io 回写请求,并把该page 的flag 设置为PG_writeback,然后把PG_dirty清除掉 …… case PAGE_SUCCESS: //如果请求成功,返回 PAGE_SUCCESS if (PageWriteback(page)) //该条件成立,跳转到 keep goto keep; …… } } …… keep: list_add(&page->lru, &ret_pages); //把该页面放到 ret_pages链表里,返回时会把该链表中的所有页面都放回收lru 链表中,即不回收页面 VM_BUG_ON_PAGE(PageLRU(page) || PageUnevictable(page), page); } …… list_splice(&ret_pages, page_list); …… return nr_reclaimed; } 接下来再看一下add_to_swap函数实现 int add_to_swap(struct page *page) { swp_entry_t entry; int err; …… entry = get_swap_page(page); //为该页面分配一个swp_entry_t,并更新swap_info_struct的页槽信息 if (!entry.val) return 0; …… err = add_to_swap_cache(page, entry, __GFP_HIGH|__GFP_NOMEMALLOC|__GFP_NOWARN); //把页面加入到swap cache 中,设置PG_swapcache,并把entry 保存到page->private变量中,跟随page传递 /* -ENOMEM radix-tree allocation failure */ set_page_dirty(page); // 设置该页面为脏页 return 1; …… }
以上仅显示部分代码,针对代码中的相关函数解析,在后续的文章中进行描述。敬请关注。
接下来,我们再思考下,操作系统在什么环境下以及在怎样的场景下会使用 Swap Space 呢?其实,从本质上而言,是 Linux 通过一个参数 Swappiness 来控制。当然还涉及到复杂的算法。
Swappiness,Linux内核参数,控制换出运行时内存的相对权重。Swappiness 参数值可设置范围在0到100之间。低参数值会让系统内核尽量避免使用 Swap,更高参数值会使内核更多的去使用 Swap Space。默认值为60(参考网络资料:当剩余物理内存低于40% 时,开始使用 Swap Space)。对于大多数操作系统,设置为100可能会影响整体性能,而设置为更低值(甚至为0)则可能减少响应延迟。具体,可参考如下:
vm.swappiness = 0 仅在内存不足的情况下,当剩余空闲内存低于vm.min_free_kbytes limit时,使用交换空间。
vm.swappiness = 1 内核版本 V3.5及以上、Red Hat 内核版本 2.6.32-303 及以上,进行最少量的交换,而不禁用交换。
vm.swappiness = 10 当系统存在足够内存时,推荐设置为该值以提高性能。
vm.swappiness = 60 默认值
vm.swappiness = 100 内核将积极的使用交换空间。
对于内核版本为 V3.5及以上,Red Hat 内核版本 2.6.32-303 及以上,多数情况下,设置为1可能比较好,0 则适用于理想的情况下(it is likely better to use 1 for cases where 0 used to be optimal)。具体如下所示:
[administrator@JavaLangOutOfMemory ~ %] cat /proc/sys/vm/swappiness 30
针对 Swappiness 参数的调整,主要有以下策略:基于临时调整和永久性调整,针对临时性策略,其命令行操作如下:
[administrator@JavaLangOutOfMemory ~ %] echo 10 > /proc/sys/vm/swappiness
或如下命令行操作:
[administrator@JavaLangOutOfMemory ~ %] sysctl vm.swappiness=10
针对永久性调整策略,在配置文件 /etc/sysctl.conf 里面修改 vm.swappiness 参数的值,然后重启系统。其命令行操作如下:
[administrator@JavaLangOutOfMemory ~ %] echo 'vm.swappiness=10' >>/etc/sysctl.conf
摒弃操作系统内核层面的诉求,在实际的业务场景中,基于 Swap Space 的使用,总结下来主要取决于以下层面,具体如下所示:
1、基于目标的导向
在某些特定的业务场景,我们更倾向于内存加速,比如,Mysql 内存索引、Redis 等场景环境下,尽可能避免使用 Swap 。
正如之前所述,不仅仅 Hadoop,包括 ES 在内绝大部分 Java 的应用都强烈建议关闭 Swap,毕竟,此类场景和 JVM 的 GC 相关,当虚拟机进行 GC 的时候会遍历所有分配到的堆内存,如果这部分内存是被 Swap 出去,遍历的时候就会对磁盘 IO 产生较大影响。
2、基于结果的优化
在某些特定的业务场景需求下,在我们所构建的集群环境中,尽可能不希望出现任何抖动、增加延迟以及出现响应延迟等现象,基于系统所固有的横向伸缩的能力,可以完全严格不使用 Swap。
其实,从某种意义上讲,Swap 可以认为是针对之前内存小的一种优化,不过现在几乎大部分主机的内存容量都较为充裕,故在某些特定的业务场景中进行开启。
那么,针对 Kubernetes ,为什么要禁用 Swap 呢?当然,此种策略跟其底层原理相关联。基于其出发点,Kubernetes 云原生的实现目的是将运行实例紧密包装到尽可能接近 100%。所有的部署、运行环境应该与 CPU 以及内存限定在一个可控的空间内。所以如果调度程序发送一个 Pod 到某一台节点机器,它不应该使用 Swap。 毕竟,若开启 Swap ,将会减慢速度。因此,关闭 Swap 主要是为了性能考虑。当然,除此之外,基于资源节省的场景角度考虑,比如,能尽可能最大限量运行较多的容器数量。