Kubernetes 之 Swap 浅析

简介: Swap,性能之鸿沟,生死之地,存亡之道,不可不省也。这一句话足以表明 Swap 在操作系统生态中的特殊地位,以及能否正确运用,对业务架构或多或少产生较大影响。

    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 主要是为了性能考虑。当然,除此之外,基于资源节省的场景角度考虑,比如,能尽可能最大限量运行较多的容器数量

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
6月前
|
Kubernetes 应用服务中间件 API
Kubernetes(K8S)命令指南
Kubernetes(K8S)命令指南
235 0
|
26天前
|
存储 Kubernetes 监控
|
16天前
|
存储 Kubernetes 调度
深入理解Kubernetes中的Pod与Container
深入理解Kubernetes中的Pod与Container
25 0
|
3月前
|
存储 Kubernetes API
使用 Kubeadm 部署 Kubernetes(K8S) 安装 -- 持久化存储(PV&PVC)
使用 Kubeadm 部署 Kubernetes(K8S) 安装 -- 持久化存储(PV&PVC)
50 0
|
4月前
|
Kubernetes 数据库 Docker
Kubernetes Node删除镜像
【7月更文挑战第1天】
|
6月前
|
存储 Kubernetes Docker
Kubernetes学习笔记-Part.03 Kubernetes原理
Part.01 Kubernets与docker Part.02 Docker版本 Part.03 Kubernetes原理 Part.04 资源规划 Part.05 基础环境准备 Part.06 Docker安装 Part.07 Harbor搭建 Part.08 K8s环境安装 Part.09 K8s集群构建 Part.10 容器回退
126 0
Kubernetes学习笔记-Part.03 Kubernetes原理
|
存储 Kubernetes 应用服务中间件
k8s初探(7)-kubernetes volume(1)
k8s初探(7)-kubernetes volume(1)
159 0
|
Kubernetes API 容器
【kubernetes】kubelet 之 Pod 管理
【kubernetes】kubelet 之 Pod 管理
167 0
|
缓存 Kubernetes 监控
kubernetes Operator 【1】入门练习
kubernetes Operator 【1】入门练习
kubernetes Operator 【1】入门练习
|
Kubernetes 关系型数据库 MySQL
10 分钟开发 Kubernetes Operator
10 分钟开发 Kubernetes Operator
652 0
10 分钟开发 Kubernetes Operator