Software-defined far memory in warehouse-scale computers

论文重点

  • 采用软件zswap的方式进行cold memory数据压缩
  • google的业务有32%的memory数据是cold memory,其中20%的数据是可压缩的,压缩效率中位数为3倍,那么TCO有4.2%的收益
  • 该压缩策略保证了SLA不受影响:通过cold memory和promotion rate的模型保证
  • 压缩解压缩的开销不大,完全可以接受
  • 使用ML进行autotuner

1 introduction

问题:
  • 随着资源的scaling,一个资源瓶颈会影响整个scaling的TCO。
  • DRAM成为scaling的瓶颈,memory的TCO减少逐渐放缓
  • 随着大数据workload的普及,主流的in-memory computing的方式加速了DRAM的需求
方向:far memory(介于DRAM和DISK之间的存储)
far memroy需要解决的问题:
  • 不影响应用的性能
  • 应用数量多且种类多样,不能对每个应用单独优化(不现实),需要对业务透明且robust的机制,有效使用Far memory
  • 动态的cold数据行为:业务的数据冷热是会变化的,比如不同该时间段

 


Autopilot: workload autoscaling at Google

autopilot 算是 google 今年混部领域最重磅的论文了吧

论文原址:https://dl.acm.org/doi/pdf/10.1145/3342195.3387524

在传统的数据中心里,资源超售最简单的方式就是把闲置资源回收再分配,total = total – allocated + reclaim

有时候,业务为了应对峰值流量(特别是互联网服务这种有明显流量特征的),都会购买多一些冗余资源。但是这些冗余资源会因为各种原因,不是那么准确。比如流量随着时间流逝会逐渐发生变化、或者用户过于焦虑

用户填的Quota永远都是不准的

这会带来一些问题:

  1. 集群资源浪费,由于Quota不准,导致机器资源被大量闲置和浪费。比如 Quota 分配出去了100c,但实际使用了10c
  2. 集群负载不均衡,同样两个机器,分配率一样,但是实际使用差别很大,对混部性能的影响就不一样

为了解决这个问题,google设计了autopilot,尝试通过自动的校准用户的 Quota,实现一定程度的超售,降低成本


Borg: the next generation

论文原址:https://dl.acm.org/doi/pdf/10.1145/3342195.3387517

这篇论文主要是分析了google内部8个集群的workload数据,并和2011年(1个集群)的数据集做了一些对比,以此来跟踪数据中心这几年的workload变化

相较于2011年的workload数据,可以发现,无论是资源模型、负载分布、混部密度、系统架构,等,都有很大的变化。这里面的很多内容和想法,和我们过去做过的还有正在做的,都不谋而合

1. 资源模型

2011年的时候,google的资源模型只有4个优先级,分别是:

  1. Free tier:免费资源,基本上不承诺任何SLA,通常是研发用来跑一些测试任务
  2. Best-effort Batch (beb) tier:同样不承诺SLA,低优先级资源,一般用来跑离线任务
  3. Production tier:生产级别,承诺SLA,一般用来托管在线服务,比如以下 long runing service
  4. Monitoring tier:监控级别,具有最高级别的SLA,用来运行数据中心的基础系统

cgroup 内存管理之 tmpfs

1. tmpfs 内存简介

tmpfs 文件系统是 pod 中常见的一种“存储”介质,也叫 ram disk,都是一个东西
tmpfs 的特殊的地方在于:
  1. 首先它是个文件系统
  2. 但是它的文件数据是完全存放在内存里面的,不在磁盘上
所以要讲 tmpfs 的话,就得把这两部分都讲清楚,一个是文件系统的实现,一个是底层“持久化”层内存的管理
通常应用程序之间会通过 tmpfs 文件系统来实现高效的数据共享
/dev/shm 就是一个最典型的 tmpfs 文件系统,是操作系统为了解决大多数程序数据共享而默认挂在的一个 tmpfs

2. tmpfs 文件系统的实现

我们知道 file 是linux内核最重要的设计,一切皆文件
除了普通的文件,平时我们接触到的,unix管道、socket、proc、cgroup 等等,都是基于文件的实现
为了实现灵活可扩展的文件系统架构,Linux设计了 virtual file system 抽象层,简称 vfs,对用户台程序屏蔽了所有具体的底层文件系统的实现细节,提供统一的文件系统接口

2.1. virtual file system 接口定义

vfs 属于一个专题,我们这里不讲那么复杂,有时间可以专门展开讲
vfs 定义了文件系统实现最关键的2个接口:
  1. 一个是 struct file_operations:文件读写的接口
  2. 一个是 struct inode_operations:inode操作接口
inode_opertions 定义如下:
struct inode_operations {
        int (*create) (struct user_namespace *, struct inode *,struct dentry *, umode_t, bool);
        int (*symlink) (struct user_namespace *, struct inode *,struct dentry *,const char *);
        int (*mkdir) (struct user_namespace *, struct inode *,struct dentry *,umode_t);
        int (*rmdir) (struct inode *,struct dentry *);
        /* 省略一万字 */
};
是不是很熟悉?


cgroup 内存管理之 page cache 回收

page cache 的管理,是内核内存管理里最复杂的一块,也是容器混部场景下,问题最多的地方
我们这里只关注读 cache 的处理,脏页的控制单独讲。所以这篇文章里,无特殊说明 page cache 默认不包括脏页部分
当我们谈到 page cache 时, 我们会关注什么?
有以下几个关键的点
  1. 什么时机会触发 page cache 回收?
  2. 回收过程是什么样的
  3. 不可回收的页面有哪些?
  4. 不容易回收的页面有哪些?
  5. 回收力度如何控制
接下来,我们就这几点,来讲一讲 page cache 的一些内核实现内幕。以及混部场景下,可能会遇到的一些坑
实际上,不同的回收方式,其时机、回收的页面范围、力度、算法都稍有不同,所以下面我们将按照不同的回收方式来详细讲

1. 整机 drop_caches 回收

内核接口 /proc/sys/vm/drop_caches
内核的代码实现入口在 fs/drop_caches.c 里面
这个接口支持3种方式:
  1. echo 1,清理 page cache
  2. echo 2,清理 slab,比如 dentry cache 通常也很消耗内存
  3. echo 3,两种都清理
我们这里只讨论方式1

1.1. 回收时机、力度、算法

只有人为的 echo xx > /proc/sys/vm/drop_caches 时,才会触发 page cache 回收
每次触发 drop_caches,基本上都会把系统能回收的 clean page 一次性全部回收回来,注意,是全部能回收的
所以,这里其实也没有什么的特殊的回收算法了,简单全遍历就完了

1.2. 回收过程

内核代码 fs/drop_caches.c
简单来说,就是
  1. 遍历所有的超级块,super_block
  2. 遍历每个超级块上的所有 inode 对象
  3. 根据 inode->i_mapping 找到每个 inode 的 address_space 空间
  4. 遍历 address_space 下的所有 page
    1. 将 page 从 radix tree 上删除
    2. 调用文件系统的 releasepage 函数释放文件系统资源。这个可以忽略,我看 fs/* 几乎所有文件系统都不实现这个函数了
  5. 释放所有能释放的 page 内存(引用计数为0)
核心逻辑的调用栈如下:
  • drop_caches_sysctl_handler
    • iterate_supers(drop_pagecache_sb, NULL)
      • drop_pagecache_sb, list_for_each_entry(inode, &sb->s_inodes, i_sb_list)
        • invalidate_mapping_pages(inode->i_mapping, 0, -1) // 这个函数的实现在 mm/truncate.c 文件里,Invalidate all the unlocked pages of one inode
          • invalidate_inode_page(page) for page in pagevec_lookup_entries(&pvec)
            • invalidate_complete_page() 删除page的mapping,并从 page cache 的radix-tree 里面剔除,因为下一步就直接 free 内存了
        • pagevec_release(&pvec) // 释放所有的 page 内存空间

1.3. 回收范围

drop_caches 是一个非常轻量级的回收过程,只回收能够立即释放的 page
从 invalidate_inode_page() 我们可以看到,有3种页面,是不会被回收的:
  1. 脏页
  2. 正在回写的页
  3. mmap + MAP_SHARED 方式映射到 page table 的页
  4. PG_SyncReadahead 需要多次drop才能回收
int invalidate_inode_page(struct page *page)
{
        struct address_space *mapping = page_mapping(page);
        if (!mapping)
                return 0;
        if (PageDirty(page) || PageWriteback(page))
                return 0;
        if (page_mapped(page))
                return 0;
        return invalidate_complete_page(mapping, page);
}
注意,page_mapping() 和 page_mapped() 不是一个东西。另外,!mapping 这段代码我没看懂是过滤的啥?
page_mapping() 返回 page 的 address_space,读的是 page->mapping 信息
(1)返回 NULL,说明该页要么是 slab cache,要么是 anon
(2)返回非空,可能是 swap_address_space(),或者就是正常页所在的一个 address_space
struct address_space *page_mapping(struct page *page)
{
        struct address_space *mapping;
        page = compound_head(page);
        /* This happens if someone calls flush_dcache_page on slab page */
        if (unlikely(PageSlab(page)))
                return NULL;
        if (unlikely(PageSwapCache(page))) {
                swp_entry_t entry;

                entry.val = page_private(page);
                return swap_address_space(entry);
        }
        mapping = page->mapping;
        if ((unsigned long)mapping & PAGE_MAPPING_ANON)
                return NULL;
        return (void *)((unsigned long)mapping & ~PAGE_MAPPING_FLAGS);
}

而 page_mapped 是用来判断 page 是否在 page table 里面。这里用 page_mapped() 主要是用来判断当前 page 是否是一个 mmap + MAP_SHARED 产生的页(因为 MAP_PRIVATE 产生的页不会填充到 page table 里面,具体可以自己看下代码)


cgroup 内存管理之 mlock

我们在使用容器的过程中,可能会遇到一个问题,容器利用率很低,但是经常发生 oom,这是什么情况?
很可能是业务使用了一些不可回收的 page cache,其中最主要的应该就是 mlock

1. mlock 的背景

mlock 的作用就是防止页面被换出到 swap 分区,或者 page cache 被内核回收
由于线上服务器,基本默认都会关闭 swap 分区,所以这种情况暂不讨论,防止 page cache 被内核回首是主要作用
什么样的 page 需要 mlock?
1)通过 mmap 方式映射到内存中的 page cache,比如一些关键的索引数据,需要经常访问,那就得 mlock 住,否则 pgmajfault 带来的性能抖动是很大的
2)程序自身依赖的一些 so 动态链接库,由于内核的 page cache 回收算法,并不感知 page 具体是普通的文件 cache,还是 so 动态链接库,所以当容器内存不足时,内核通过一些粗略的回收算法回收 page cache,一旦把 so 的缓存页回收掉了,程序在调用相关函数时,会出现严重的性能抖动
因此,通过 mlock,显式的把一些关键的不希望被回收的 page cache 锁定起来,达到保证业务性能的目的


PerfIso: Performance Isolation for Commercial Latency-Sensitive Services

论文原址:https://www.usenix.org/system/files/conference/atc18/atc18-iorgulescu.pdf

演讲PPT:https://www.usenix.org/sites/default/files/conference/protected-files/atc18_slides_iorgulescu.pdf

这是微软在18年 usenix 会议上公开的一篇混部论文。这也是微软在混部这个领域公开的为数不多的论文之一。论文中描述了bing业务(非公有云)如何通过在离线混部 + CPU Blind Isolation 这个技术,将 indexserver 集群的利用率从21%提升到66%的过程

其中最关键的,还是 CPU Blind Isolation 这个技术,很有意思。


cgroup 进程调度之 Borrowed-virtual-time (BVT) scheduling

规避 CFS 的非公平性问题(睡眠补偿等等),99年发表论文,15年heracles论文重新对 bvt 做了改进,从论文作者的名字,我扒到了对应的源码,这哥们把源码放到gist上了

1. cfs 睡眠补偿机制

在讲bvt之前,有必要先介绍一下 cfs 的睡眠补偿机制
cfs 调度器的目标是公平,cfs 希望每个进程得到调度的机会是一样的,这个“机会”是用 vruntime 来衡量的
但是如果一个进程一直在睡眠,那么它的 vruntime 是非常小的,当睡眠中的进程被唤醒时,基于 CFS 的调度逻辑,会一直持续运行当前进程,直到 vruntime 不是最小的时候,才会选择下一个进程来调度。
内核为了解决 sleep 进程获得过长时间的问题,增加了一个阈值限制,当进程被唤醒时,取当前运行队列的最小vruntime,并 + 上一个偏移量,这个偏移量默认是 1/2 个调度周期,12ms


overlayfs 差分文件系统原理

overlay文件系统的主要目的是要实现文件系统重叠,docker中的查分机制所依赖的文件系统分层就是依赖这种技术来实现的

1. upper and lower

overlay机制允许将两个文件系统重叠成一个文件系统,其中一个是upper,另一个是lower,对用户的可视顺序是:
upper -> lower
简单来说,如果upper和lower同时存在一个相同的文件,那么用户看到的是upper中的文件,lower中的同路径文件会被自动隐藏
overlay只关心文件,目录是会被穿透的,所以严格来说,overlay重叠的是目录树,而不是“文件系统”
所有的修改都会写入upper,lower是只读的。upper的文件系统必须支持trusted.*扩展属性,所以upper是不支持NFS的

2. 用法

mount -t overlay overlay -olowerdir=/lower,upperdir=/upper,workdir=/work /merged
如果不写upper和workdir,就是只读挂载
mount -t overlay overlay -olowerdir=/lower /merged


docker image 存储剖析

从docker pull开始,看 docker image 的存储过程
# docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
5ba4f30e5bea: Pull complete
6874f9870f5f: Pull complete
4c876570bd7d: Pull complete
10fb34ebccea: Pull complete
Digest: sha256:f1b592e2de671105255a0c0b7b2f71a92b829403e8fc845e3482667ecc301780
Status: Downloaded newer image for ubuntu:latest
# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              12543ced0f6f        2 weeks ago         122.4 MB
其中image名字是ubuntu,image的id是12543ced0f6f,在docker中,几乎所有的ID都是通过UUID或者sha256等方式计算出来的