Luga Lee
作者Luga Lee·2023-04-07 10:31
系统架构师·None

Kubernetes 之 Swap 浅析

字数 15936阅读 1070评论 0赞 3

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。 S wap D isk 是一个专门用于做 Swap 的块设备,作为裸设备提供给 Swap 机制操作;而后者 S wap F ile 则是存放在文件系统上的一个特定文件,其实现依赖于不同的文件系统,会有所差异,我们可通过如下参考示意图,具体如下所示:

(此图源自: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 对应一个 S wap 分区。 Swap 分区内部会以 Page 大小为单位划分出多个 Swap Slot,同时通过 swap_map 对每个 Slot 的使用情况进行记录,为 0 代表空闲,大于 0 则代表该 Slot 被 Map 的进程数量。

在此,基于源码角度,我们来简要了解下 Swap In 和 Swap Out 的相关工作流程,具体如下。

Swap In

Swap I n 的入口为 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时,使用交换空间。

v m.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/swappiness30

针对 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。

其实,从某种意义上讲,S wap 可以认为是针对之前内存小的一种优化,不过现在几乎大部分主机的内存容量都较为充裕,故在某些特定的业务场景中进行开启。

那么,针对 Kubernetes ,为什么要禁用 Swap 呢?当然,此种策略跟其底层原理相关联。基于其出发点 ,Kubernetes 云原生的实现目的是将运行实例紧密包装到尽可能接近 100%。所有的部署、运行环境应该与 CPU 以及内存限定在一个可控的空间内。所以如果调度程序发送一个 Pod 到某一台节点机器,它不应该使用 Swap。 毕竟,若开启 Swap ,将会减慢速度。 因此,关闭 Swap 主要是为了性能考虑。 当然,除此之外,基于资源 节省 的场景角度考虑,比如,能尽可能最大限量运行较多的 容器数量 。

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

3

添加新评论0 条评论

Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广