SOSP 19′ 文件系统是否适合做分布式文件系统的后端——Ceph 的十年经验总结

过去十年里,Ceph 一直是在本地文件系统的基础上实现。这个是目前大部分分布式文件系统的选择,因为这样可以利用这些实际环境验证过的代码。然而,Ceph 的经验告诉我们这么做也是有代价的——首先,实现一个零开销的事务机制会很困难;其次,本地的元数据性能会极大影响分布式系统;第三支持新的存储硬件会变得很慢。

Ceph 通过一个新存储后端 BlueStore 来解决这些问题,BlueStore 设计为直接在块设备上运行。在其面世的短短两年里,BlueStore 已经被 70% 的生产客户所采用。通过运行在用户态和对 IO 栈的完全控制,BlueStore 实现了高效的元数据空间和数据校验、EC 数据快速覆写、在线压缩、减少了性能的波动而且避免了一系列本地文件系统的隐患(pitfalls)。最后,通过 BlueStore 还让支持一些原本不支持的存储硬件成为可能。

介绍

后端存储采用文件系统的好处:

  1. 无需自行解决数据持久和块分配的问题
  2. 提供熟悉的 POSIX 接口和抽象(文件、目录)
  3. 可以使用lsfind这些常用工具来管理

Ceph 开发 BlueStore 几个主要原因:

  1. 难以在现有文件系统上实现高效的事务操作。
    现有的实现要么有很高的性能损失、要么功能有限、要么接口或实现过于复杂,总之都没有直接集成到文件系统里。
    Ceph 使用用户态的 WAL 来实现,或者一个支持事务的 KV 存储,但性能都不能满意

  2. 本地文件系统的元数据性能极大影响分布式层。具体来说,Ceph 需要快速枚举(enumerate)有几百万条目的目录,但 Btrfs 和 XFS 都支持的不够好;如果拆分目录(directory splitting )来减小一个目录内的文件数量,这个操作在文件系统上有成本极大,会拖垮整个系统的性能

  3. 因为文件系统本身的过于成熟,会导致它对新存储硬件的支持非常慢。例如用来解决 HDD 容量问题的 SMR,用来解决 SSD 的 FTL 层性能损失的 ZSS SSD 都难以支持

BlueStore 的创新之处包括:

  1. 用 KV 数据库保存像 extent bitmap 这类底层的文件系统元数据,用来避免磁盘格式(on-disk format)的变化和减少实现的复杂度;

  2. 通过接口设计优化克隆操作,减小 extent refreence-counting 的成本;

  3. BlueFS,一个用户态文件系统,让 RocksDB 在裸设备上跑得更快

  4. 每 TB 消耗 35MB 内存的空间分配器(space allocator)

除此之外,这篇文章还做了一些实验评估从 FileStore 转到 BlueStore 中一些性能的影响因素,例如日志文件系统的影响、日志双写的影响、拆分目录性能的影响和原地更新的影响(与 COW 相反)。

背景

分布式文件系统,无论是 Lustre、GlusterFS、OrangeFS、BeeGFS、XtreemFS 还是之前的 Ceph,都有几个关键需求:

  1. 高效的事务

  2. 快速的元数据操作

  3. (可能不是通用的)对未来的不向后兼容的存储硬件的支持

因为大部分文件系统按 POSIX 标准实现,因此缺乏事务概念,因此分布式文件系统往往通过 WAL 或者基于文件系统的内部事务机制实现(Lustre)。

无法高效的列举目录内容或者 hanle 海量小文件也是分布式存储使用本地文件系统的一个痛点,为此分布式文件系统就需要通过元数据缓存、哈希、数据库或对本地文件系统打 patch 来解决。

根据硬件供应商的预测,2023 年半数的数据中心将使用 SMR HDD。此外 ZNS SSD 能够通过不提供 FTL 来避免 gc 带来的不可控的延迟。像这种新硬件也是 Ceph 希望支持的。

image.png-381.1kB

上图是 Ceph 的大致架构,考虑到 Ceph 架构的介绍文章很多,这里就赘述了,读者可以搜索任一篇 Ceph 架构的介绍文章。

Ceph 的 ObjectStore 第一个实现是一个叫 EBOFS(Extent and B-Tree-based Object File System ) 的用户态文件系统。2018 年 Btrfs 出现,有事务、去重、校验码、透明压缩都特性,因此 EBOFS 被基于 Btrfs 实现的 FileStore 取代。

FileStore 里,一个对象集合会被映射到目录,数据会被存储到文件。一开始对象的属性是被 POSIX 的 xattrs 保存的,但后来被移到了 LevelDB(xattrs 容量有限)。

Btrfs 被用作生产环境后端很多年,这个过程中 Btrfs 一直有不稳定和数据/元数据的 fragmentation 问题,但因为对象接口的不断演进导致已经不太可能退回到 EBOFS 了,因此 FileStore 被移植到过 XFS、ext4、ZFS,最终因为在 XFS 上良好的 scale 和元数据性能而成为 FileStore 的事实标准。

image.png-116.7kB

虽然基于 XFS 的 FileStore 已经比较稳定了,但是一直受元数据 fragmentation 和无法充分发挥硬件性能的问题困扰。因为缺乏原生的事务,所以用户态的 WAL 实现使用了完整数据的 journal,并受读取-修改-写入这一过程(read-modify-write workloads )的速度限制——这个正是 Ceph WAL 的典型操作过程。此外,XFS 不是一个 COW 文件系统,快照因为需要克隆操作受此影响就会很慢。

NewStore 是 Ceph 尝试通过基于文件系统解决元数据问题的第一次尝试。NewStore 不再使用目录来代表对象及合,而是用 RocksDB 保存元数据。此外 RocksDB 还用来实现 WAL,使得读取-修改-写入过程可以通过合并数据和元数据日志来加速。

这个方案整体来说就是通过文件保存数据、通过在日志文件系统上运行 RocksDB 来保存元数据。但这个方案带来沉重的一致性负担,最终促使了 BlueStore 的开发。

在本地文件系统上构建存储后端的难点

难点一 高效事务

在文件系统上实现事务有三种选择:

  1. Hook 到文件系统内部的事务机制

  2. 在用户态实现 WAL

  3. 使用有事务的 KV 数据库

方案一:Hook 到文件系统内部的事务机制

方案 1 的问题是功能有限,而且很多文件系统没有直接对用户暴露事务。功能有限例如没有回滚机制等。Btrfs 提供了一对系统调用使得内部的事务机制可以对用户暴露。基于 Btrfs 的 FileStore 第一版是依赖于这些系统调用的,但是它没有回滚机制导致很痛苦——具体来说,如果 Ceph OSD 在事务过程中遇到了一个 fatal 事件,例如软件崩溃或者 kill 信号,Btrfs 会提交一个部分(partial)事务,留给存储后端一个不一致状态。

Ceph 团队和 Btrfs 团队都接受的解决方法包括提供一个 entire transaction 系统调用,或者基快照实现回滚,但这两个方案都有很高的成本。最近 Btrfs 废弃掉了事务系统调用,和微软对 NTFS 的决定类似。

方案二:在用户态实现 WAL

方案二是可行的,但是受三个主要问题的影响:

  1. 读取-修改-写入速度

    一个用户态 WAL 实现每个事务需要三步:
    第一步、先对事务序列化,写入到日志;
    第二步、通过 fsync 持久化日志;
    第三步、执行事务内的操作

    这样最终导致整个 WAL 的延迟很高,无法实现高效的 pipeline

  2. 非幂等操作

    FileStore 中对象通过文件表示,对象集合会映射到目录。

    在这种数据模型下,crash 之后重放 WAL 因为一些操作非幂等会导致很有难度。在 WAL 定时 trim 时,总会有一个时间窗口事务日志已经提交到文件系统但事务还没有完成(a window of time when a committed transaction that is still in the WAL has already been applied to the file system)。
    举个例子,考虑一个事务包含三个操作:
    ① 克隆 a 到 b
    ② 更新 a
    ③ 更新 c

    如果在第二步之后发生 crash 了,replay WAL 会破坏 b
    在考虑另一个例子,事务有四个操作:
    ① 更新 b
    ② 将 b 重命名为 c
    ③ 将 a 重命名为 b
    ④ 更新 d

    如果在第三个操作之后发生了 crash,重放 WAL 会破坏 a(也就是现在的 b),然后因为 a 已经不存在而失败。

    基于 Btrfs 的 FileStore 通过对文件系统做周期性快照和对 WAL 做快找时间的标记来解决这一问题。当恢复时,最近的一个快照被恢复,然后 WAL 从相应时间点那一刻开始 replay。

    但因为现在已经使用 XFS 来替代 Btrfs,XFS 缺乏快照带来了两个问题。首先,XFS 上 sync 系统调用是将文件系统状态落盘的唯一选择,但对一个典型的多磁盘构成的节点来说,sync 过于昂贵因为会对所有磁盘生效。这个问题已经被增加 syncfs 调用解决——只同步指定的文件系统。

    第二个问题是在 WAL replay 后,恢复文件系统到指定状态会因为上面说的缺乏幂等性而产生问题。为此 Ceph 又引入了 Guards(序列号 sequence numbers )来避免 replay 非幂等操作。但庞大的问题空间导致在复杂操作下 guards 的正确性也很难验证。Ceph 通过工具产生复杂操作的随机排列,然后加上错误注入来半自动的验证正确性,但最终结果是 FileStore 的代码很脆弱而且难以维护。

  3. 双写。最后一个问题是数据会被写两次,一份到 WAL 一份到文件系统,减半了磁盘的带宽。核心原因是大部分文件系统都只对元数据修改记录到日志,允许在 crash 后丢失数据。然而 FileStore 对文件系统的使用(namespace、state)因为一些 corner case(例如对多文件部分写 partially written files)导致 FileStore 不能像文件系统一样只在日志中记录元数据修改。

    尽管可以说 FileStore 这种对文件系统的使用是有问题的,但这种选择也有技术原因的。如果不这么做就需要实现数据和元数据的内存 cache 以等待 WAL 的任何更新——而内核已经有了 page 和 inode 的缓存。

方案三:使用有事务的 KV 数据库

在 NewStore 方案中,元数据保存在 RocksDB,一个有序 KV 数据库,而对象数据继续在文件系统上以文件形式表示。这样,元数据操作直接在数据库执行;数据的覆盖写被记录到 RocksDB 然后延迟执行。下面介绍 NewStore 如何解决前面说到的用户态 WAL 的三个问题,然后介绍后面因为在一个日志文件系统上运行带来的极高的一致性成本。

首先,因为 KV 数据库的接口允许我们直接读取对象状态而不需要等待上一个事务完成,从而避免了缓慢的“读取-修改-写入”。

其次 replay 非幂等操作的问题通过在准备事务时在读取侧解决。举个例子,克隆 a 到 b,如果对象比较小,那么就复制一份并插入到事务,如果对象比较大,那么就是用 COW 机制,将 a 和 b 指向到同一数据,并把数据标记为只读。

最后,双写的问题也解决了,因为对象的命名空间已经和目录结构解耦,新对象的数据都会先写到文件系统然后自动添加引用到数据库。

尽管上面说了许多好处,但与 journal on journal 类似,日志文件系统与 RocksDB 的组合会带来很高的一致性开销。在 NewStore 上创建对象需要两步:

  1. 写入一个文件并执行fsync
  2. 同步将对象元数据写入到 RocksDB,也会导致一次fsync

理想状态下,每次fsync会导致一次昂贵的FLUSH CACHE 操作到磁盘。但实际上在日志文件系统上每次fsync会带来两次 flush command:一次是写数据,一次是文件系统提交元数据日志。这样导致在 NewStore 上创建对象会产生四次昂贵的flush操作。

下面用一个模拟测试来展示这一开销,测试方法是模拟存储后端创建大量对象,每轮会先写 0.5MB 数据然后插入 500Byte 的元数据到 RocksDB。先模拟 NewStore (在 XFS 上)的实现,然后模拟在裸盘上的实现。

image.png-188.1kB

可以看到裸盘实现比 XFS 实现在 HDD 快 80%,Nvme SSD 上快 70%。

难点二 快速元数据操作

本地文件系统缺乏元数据操作可以说是分布式文件系统艰难的源泉。Ceph FileStore 元数据操作一大挑战就是本地文件系统在对大目录读取(readdir)的性能差和缺乏结果排序。

RADOS 中的对象都会根据名字哈希映射到 PG,然后按照哈希顺序枚举。对于像 scrubbing、recovery 或者 librados list 对象这些操作,这个枚举操作都是必须的。对于具有很长名字的对象,FileStore 通过文件的扩展属性来突破文件系统对名字长度的限制,这样的话就需要一个stat调用才能确定对象的名字。FileStore 采用了一个通用做法来解决这一问题:使用一个大的、散开的(large fan-out)目录层级结构,对象分布在多个目录里,然后组合读取目录的内容并排序。

为了快速排序和减少潜在的stat 操作的开销,目录要保持在比较小的规模(几百个条目这样),这样就要当数量比较大的时候对目录进行拆分。这个在规模较大时会成为一个显著影响性能的操作,有两个原因:

  1. 一次处理百万个 inode 会降低 dentry cache 的命中率,造成大量的磁盘小 IO;
  2. XFS 将子目录放在一个不同的allocation groups里来确保将来有足够的空间把目录条目放置在一起(XFS places subdirectories in different allocation groups to ensure there is space for future directory entries to be located close together),因此随着对象数量的增长,一个目录内容的会不断散开(spread out),然后拆分目录会因为 seek 花费越来越多的时间。

最终的结果是当所有 OSD 一齐执行目录拆分时,会显著的影响性能,这个问题因为已经影响了很多 Ceph 用户数年而被广为人知。

为了展示这一影响,我们配置了一个 16 节点的 Ceph 集群,将 PG 数量设置为推荐值的一半来特意增加目录拆分的压力,RADOS 层队列深度为 128,插入了百万个 4KB 的对象。下图展示了拆分目录的影响,一开始没有什么感觉,第二次拆分导致了性能的急剧下降,并在后面 7 分钟内偶有明显的性能损失,这里展示的是 SSD 集群,对于 HDD 集群,可以观察到 120 分钟的性能损失(没有展示图)。因此这造成在全 HDD 集群里,恢复操作因为seek的高开销而需要比 SSD 高一个数量级的时间成本。

image.png-270.3kB

难点三 对新硬件的支持

以 SMR 为例,如果使用供应商提供的向后兼容的drive-managed SMR接口,会导致性能不可预期(unpredictable performance),如果想达到预期性能,就要使用host-managed SMR提供的不向后兼容的 Zone 接口——这种接口鼓励开发者使用 log-structured、COW 的设计,与现有的成熟文件系统设计完全不同(in-place 覆盖)。

另一个例子就是 OpenCannel SSD,现在主要供应商都开始提供新的 NVMe 标准即 Zoned Namespaces 来定义无 FTL 的 SSD 的接口。去掉 FTL 有很多好处——减少写放大、改善延迟、改善吞吐、减少超分、还能通过减少 DRAM 来降低成本。

这两种新硬件目前都没有成熟的文件系统支持。

其他难点

很多公有云和私有云依赖像 Ceph 这样的分布式存储来提供存储服务,但如果没有对 IO 栈的完整控制,很难定义存储的延迟 SLO。其中一个原因是文件系统为基础的存储会使用系统的 page cache。为了提升用户体验,大部分系统都会设计基于 write-back 的 page cache,这样数据可以 buffer 在内存,当系统的 IO 很少,或者达到了预定的周期时间,就将 page cache 回写到磁盘。对于一个复杂系统,write-back 行为会受一些列复杂策略影响,导致不可预测。

对 Ceph FileStore 来说,尽管有自己周期性的fsync,但它无发现 inode 元数据的 write-back,导致性能不稳定。

再一个难点是基于文件系统的存储后端在实现像 COW 这样的操作。如果后端文件系统是 COW 的,
那么这些操作的实现会很搞笑。但是,他也有一些其他缺点,例如在 Btrfs 上会产生碎片。反过来如果文件系统不支持 COW,那么这些操作就需要成本很高的对对象完整复制,导致快照、EC 的覆盖写代价非常高昂。

BlueStore:一种全新的方法

BlueStore 是一个用来解决上面提到的各种问题的、从头开始写的存储后端,BlueStore 的一些主要设计目标包括:

  1. 快速元数据操作
  2. 对象写入时没有一致性开销
  3. 支持 COW
  4. 没有日志双写问题
  5. 对 HDD 和 SSD 具有优化的 IO pattern

BlueStore 用了两年时间完成了上面的所有目标,并成为了 Ceph 的默认存储后端。这么快的达成(相比通用 POSIX 文件系统需要十年计)有两个关键因素:

  1. BlueStore 只实现了少量的、专用的接口,而不是完整的 POSIX 标准
  2. BlueStore 在用户态实现,可以复用很多完整测试过、高性能的第三方代码

BlueStore 的整体架构如下图所示,BlueStore 运行在裸盘上,BlueSore 的 space allocator 决定新数据的位置,然后数据会通过 Driect IO 异步写入到磁盘。内部元数据和用户定义元数据被保存在运行在 BlueFS 的 RocksDB,BlueFS 是一个很小的为 RocksDB 量身定做的用户态文件系统。space allocator 和 BlueFS 会定时通讯来平衡容量。

image.png-172.2kB

BlueFS 和 RocksDB

BlueStore 通过将原数据保存到 RocksDB 来实现快速的元数据操作;通过下面两点来避免一致性开销
1. 直接写数据到裸盘,从而只有一次 cache flush;
2. 修改 RocksDB 将 WAL 作为 circular buffer 使用,从而达到元数据写入只有一次 cache flush——这个 feature 已经 upstream 到上游。

BlueFS 实现了像openmkdirpwrite这些 RocksDB 所需的基本系统调用。BlueFS 的磁盘布局如下图。

image.png-210.1kB

BlueFS 为每一个文件维护一个 inode,其中包含为这个文件分配的 extent 信息。superblock 保存在固定位置,包含 journal 的 inode。journal 有文件系统元数据的唯一副本,mount 时加载到内存。每当有元数据操作例如创建目录、文件和分配 extent 时,journal 和内存里的元数据会被更新。journal 不保存在固定位置,它的 extent 会与文件的 extent 有交错。每当达到一个阈值时,journal 会被压缩并写到新的位置,这个新的位置被记录到 superblock 里。这样设计之所以可行是因为得益于大文件和周期压缩会限制任一时刻 volume 元数据的数量。

关于元数据组织,BlueStore 在 RocksDB 中使用了多个命名空间,每个命名空间用来保存不同类型的元数据。举例来说对象信息都保存在命名空间 O 中(也就是说 RocksDB 中 O 开头的 key 都表示对象的元数据),块分配元数据保存在命名空间 B,集合元数据(collection metadata)保存在命名空间 C。每个集合(collection)映射到一个 PG,并代表 pool namespace 得一个 shard。collection 的名字包含 pool 的标识和collection 里对象名字的统一 prefix。

举个例子,一个 kv:C12.e4-6标识 pool 12 的一个集合,这个集合里的对象的哈希以 e4 的 6 个最高有效位开头(hash values starting with the 6 significant bits of e4)。例如对象 012.e532 就是这个集合的成员(前六位是111001),而 012.e832 就不是(前六位是111010)。这种元数据组织方式允许只通过修改有效位数的数量(the number of significant bits)把数百万的对象分割成多个集合。这样比如有加入的 OSD 增加了总容量或者现有 OSD 因为失效从集群移除时,FileStore 在拆分 collection 时就需要昂贵的目录拆分,而 BlueStore 就简单很多。

数据路径和空间分配

BlueStore 是一个 COW 的后端。对于大于最小分配大小的写请求(对于 HDD 是 64KB、SSD 是 16KB),数据会被分配到一个新分配的 extent。当数据持久化之后,对应元数据就会插入到 RocksDB。这允许 BlueStore 提供高效的克隆操作。克隆操作只需要增加所需要的 extent 的引用计数,然后将新写入指向到新的 extent。这允许 BlueStore 对大于最小分配大小的这部分写、部分写请求避免日志双写

对于小于最小分配大小的写请求,数据和元数据都都会被先保存到 RocksDB 然后将来随事务异步写入到磁盘。这个延迟写的机制有两个目的:
1. 合并提交小 IO 来提高效率——写一个新数据需要两次 IO 而插入到 RocksDB 只需要一次
2. 根据设备类型优化 IO,通过在 HDD 上异步写 64KB 以下的数据来避免在读操作过程中 seek(avoid seeks during reads),再 SSD 让原地覆盖写(in-place overwrite)仅发生在 16KB 以内的 IO。

关于空间分配,BlueStore 使用两个机制来分配空间:FreeList manager 和 Allocator。FreeList 作为一个磁盘当前使用的持久化记录。就像 BlueStore 的所有元数据一样,它首先被保存到 RocksDB。FreeList manager 的第一版实现被设计为通过 offset 和 length 的键值对表示已使用的 region。这个设计的缺陷在于必须对事务进行序列化——为了避免 free list 不一致,需要先删除旧的 key,然后插入新的 key。第二版设计为基于 bitmap。分配和回收操作使用了 RocksDB 的 merge 操作符来反转受影响的 block 所对应的 bit,从而消除了排序这一要求。RocksDB 中的 merge 操作符执行延迟的、原子的读取-修改-写入操作(deferred atomic read-modify-write operation),与原方法相比不会改变语义也不需要查询的开销。

Allocator 负责为新数据分配空间。他保存了一份 free list 的内存拷贝,并且会在分配后通知 FreeList Manager。Allocator 的第一版实现是基于 extent 的,将可用 extent 划分到 2
的 n 次幂的容器中(power-of-two-sized bins)。随着磁盘使用量的增加,这个设计容易产生碎片。第二个设计使用索引结构,这个结构在一个“一位表示一个 block”(single-bit-per-block)的描述之上来跟踪块的所有区域(track whole regions of blocks)。通过查询高层和低层索引,可以有效的找到大的和小的 extent。这种实现对每 PB 使用固定的 35MB 内存。

关于 Cache,因为 BlueStore 实现在用户空间且通过 Direct IO 访问磁盘,所以它不能够利用到操作系统的 page cahce。所以 BlueStore 在用户层使用 scan resistant 2Q 算法实现了自身的 write-through 缓存。缓存通过 shard 来并发。它使用了和 Ceph OSD 一样的 shard 模式,对到多个集合的请求通过不同 core 来 shard。这样避免了 false sharing,这样同一个 CPU 的上下文始终访问它对应的 2Q 数据结构。

得益于 BlueStore 所实现的功能

本节会介绍由于具备了对 IO 栈的完整控制后,BlueStore 所能实现的过去无法实现的功能。

高空间利用率的 checksum

Ceph 会每天 scrub 元数据、每周 scrub 数据,但即使有 scrub 机制,数据如果在不同副本间不一致也很难确定哪个副本是受损坏的那个。因此 checksum 对分布式存储很重要,特别是对于 PB 级的数据,几乎是必然会发生位翻转这些错误。

绝大多数本地文件系统是不支持 checksum 的。一些支持的,比如 Btrfs,会对每 4KB 计算校验和以方便覆盖写 4KB block。那么对于 10TB 的数据,为每 4KB 数据存储 32 位的 checksum 需要最终占用 10GB 的空间,这将导致难以将 checksum 缓存到内存来做快速验证。

另一方面,大部分存在分布式文件系统的数据是只读的,可以以更大粒度来计算 checksum。BlueSTore 对每次写请求计算一个 check,并且在每次读取时计算。BlueStore 支持多种 checksum 算法,其中 crc32c 是默认选项——因为它在 x86 和 arm 上都有良好的优化,而且也足以探测随机的位错误。由于对 IO 栈的完整控制,BlueStore 可以根据 IO 迹象(hint)来选择 checksum 的 block size。举例来说,如果根据 IO 推测写请求来自 S3 兼容的 RGW 服务,那么对象是只读的,checksum 可以以 128KB 为粒度计算。如果 IO 是需要压缩的对象,那么 checksum 在压缩后计算,显著的减小了 checksum 的大小。

EC 数据的覆盖写

Ceph 从 2014 年 FileStore 就支持了 EC pool,然而这个支持仅限追加写和删除操作,因为覆盖写在这个设计下太慢了,几乎无法使用。结果是,EC 池尽在 RGW 场景有用,对于 RBD 和 CephFS 是无法使用。

为了避免 Raid write hole 问题,多步骤的数据更新时,如果发生 crash 会导致系统不一致,Ceph 在 EC 池的覆盖写使用两阶段提交。首先,所有存储这个对象的一个 chunk 的 OSD 都会复制这个 trunk 来为失败回退做准备。当所有 OSD 收到新的数据内容要覆盖写 trunk 时,旧的 trunk 副本就会被丢弃掉。在 XFS 为基础的 FileStore 下,第一步是非常昂贵的,因为需要一次物理复制。而对 BlueStore 来说得益于 COW 就避免了完整的物理复制。

透明压缩

透明压缩对横向扩展分布式文件系统是很关键的,因为 3 副本会增加存储的成本,BlueStore 实现了透明压缩,在写数据时会在其落盘前自动压缩数据。

要充分发挥压缩衣的优势,需要被压缩的 chunk 至少是 128KB 以上,而且是对整个对象压缩。对于压缩对象的覆盖写,BlueStore 会先把放在单独为止,然后更新元数据指向到它。当压缩对象碎片化比较严重时,BlueStore 会执行 compact 操作。实践中,BlueStore 会使用简单的启发式策略和 hint 只压缩那些不大可能会被覆盖写的对象。

新接口

因为不受本地文件系统的 block-based 设计约束,BlueStore 在探索新的接口和数据分布上有了更高的自由度。最近,RocksDB 和 BlueFS 已经被移植到 host-managed SMR 上,而在这类设备上存储数据也已经在作为下一个努力方向。此外 Ceph 社区还在探索一些新的后端,尝试将持久化内存和新的 NVMe 设备组合,例如 ZSN SSD 和 KV SSD。

评估

以下评估都是在 16 节点通过 Cisco 3264-Q 交换机连接起来的 Ceph 集群,每个节点有一个 16 核 E5-2698Bv3 2Ghz 处理器,64GB 内存,400GB P3600 NVMe SSD,4TB 7200 RPM Segate HDD,Mellanox 40Gb 网卡。系统是 Ubuntu18.04,内核为 4.15,使用 Luminous 版本(v12.2.11),使用 Ceph 默认配置。

直接对 RADOS 测试

写吞吐数据:
image.png-246.9kB

写延迟数据:
image.png-325.8kB

在读性能上 BlueStore 没有表现出优势,因为 FileStore 实现了 Read ahead,而 BlueStore 可以没有实现。

前面提到的拆分对 FileStore 的影响与 BlueStore 在相同条件下做对比,BlueStore 前面的性能下降是因为 RocksDB 的 compact 影响没有到达一个稳定状态:

image.png-183.7kB

RBD块设备测试

测试前会 drop 系统 cache 和重启 BlueStore OSD 来避免 cache 的影响。
image.png-267.2kB

可以看到首先 BlueStore 的吞吐更好——这个很大一部分来源于规避了双写,其次方差更小——因为 BlueStore 直接将数据写入磁盘,而 FileStore 的系统 write back 触发和 Ceph 前台 WAL 相冲突引起较长的 latency。

对于小于 64KB 的写请求,BlueStore 要比 FileStore 好 20%,因为 BlueStore 会将数据写入到 RocksDB 就返回。(MatheMatrix:无图)

在读性能上 BlueStore 没有表现出优势,和前面一样,因为 FileStore 实现了 Read ahead,而 BlueStore 可以没有实现。

EC数据的覆盖写

image.png-147kB

在裸设备上实现高性能存储后端所面临的挑战

cache 大小和 writeback

文件系统可以直接利用内核的 Page Cache,但基于裸设备的存储就得自己从头实现类似的机制。具体参考4.2 节。

高效的 KV 存储

Ceph 团队的经验是将所有元数据放到有序的像 RocksDB 这样的 KV存储可以有效提高原数据性能。然而,在整合过程中也遇到一些问题:

  1. RocksDB 的 compact 写放大和导致无法充分使用 NVMe 的性能;
  2. 因为把 RocksDB 视为一个黑盒,因此序列化和反序列化数据花了更多的 CPU 时间;
  3. RocksDB 有自己的线程模型,限制了自定义 sharding。

高效的 CPU 和内存使用

现代编译器会对内存里的数据做 align 和 pad 来提高 CPU 读取的性能,但对复杂的数据结构,这种做法会浪费大量内存。对大部分应用这件事都不需要太过关心,因为他们的内存里的数据结构生命周期都很短。但对于绕过 page cahce 的存储后端,长时间这么运行会几乎占掉机器所有的内存,Ceph 团队花了很久时间,主要通过 delta and variable-integer encoding 等技巧 pack 保存到 RocksDB 的元数据来减少元数据大小和降低 compact 的开销。

另一个问题是在高端 NVMe 设备上,BlueStore 性能受 CPU 所限制,因此对于下一代后端,Ceph 社区在探索减少对 CPU 的使用,例如降低数据的 serialization-deserialization、结合使用 SeaStar 和 shared-nothing 模型——避免 lock 造成的上下文切换。

相关工作

关于事务,in-kernel 实现可以参考 Btrfs、NTFS、Valor、TxFS 等,基于用户文件系统的数据库可以参考 Amino(基于 Berkerly DB)、Inversion(基于 Postgres),操作系统级的实现可以参考 QuickSilver、TxOS。

关于元数据优化,相关工作有很多,例如 BetrFS、DualFS、hFS、FFS、TableFS、DeltaFS。后两者将元数据保存在 LevelDB 之上。

总结

大部分分布式文件系统都是基于本地文件系统实现,然而却导致了极大地复杂性,这种要基于本地文件系统设计的信念源于从头实现成本巨大,需要十年以上才能成熟。而 Ceph 的经验告诉我们这种想法并不准确,而且带来了——性能优势、支持新设备、通过对完整 IO 栈的控制实现过去难以实现的 feature。

Copy hostname from chrome address bar rather than whole url

I often copy hostname from chrome address bar to shell, but it always with some prefix like “http://” .

Search “chrome copy without http” in Google and you will 208,000,000 results, there is a bug reported in tracker but status is “Won’t fix”.

Luckily some body developed this extension:

https://chrome.google.com/webstore/detail/hostcopy/ebnjnkfienhcidbgmifkjkkidheihcpj

there are some others has same function, you can try and choose your favorite.

colored tail -f with /var/log/messages

Vim has beautiful color profile for messages:

But I usually use tail -f to monitor my logs, while it can not use vim color profile, so I installed grc.
Clone and run ./install.sh, try grc tail -f /var/log/messages and you will see:

Oh, that’s not what I want.

Luckily grc can easyly customize profile, so this is my final effect:

And this is my profile, play and enjoy 😀

[root@dev1-1 grc]# cat /usr/local/share/grc/conf.log
# this configuration file is suitable for displaying kernel log files


# example of text substitution
#regexp=\bda?emon
#replace=angel
#colours=red
#======
# example of text substitution
#regexp=(\d\d):(\d\d):(\d\d)
#replace=\1h\2m\3s
#======

# display this line in yellow and stop further processing
regexp=.*last message repeated \d+ times$
colours=magenta
count=stop
=====
# this is process
regexp=^\w+?\s{1,2}\d{1,2}\s\d{2}:\d{2}:\d{2}\s.+?\s[^ ]+?:\s
colours="\033[0;38;5;172m"
count=once
count=once
======
# number
regexp=\d
colours=red
count=more
======
# this is hostname
regexp=^\w+?\s{1,2}\d{1,2}\s\d{2}:\d{2}:\d{2}\s.+?\s
colours=cyan
count=once
======
# this is date
regexp=^... (\d| )\d
colours=red
=====
# this is time
regexp=(\s)\d\d:\d\d:\d\d(\s)
colours=green
count=once
=====
# everything in parentheses
regexp=\(.+?\)
colours=green
count=more
======
# everything in `'
regexp=\`.+?\'
colours=bold yellow
count=more
======
# this is probably a pathname
regexp=/[\w\-\_/\.]+
colours=green
count=more
======
# name of process and pid
regexp=([\w/\.\-]+)(\[\d+?\])
colours=bold blue, bold red
count=more
======
# ip number
regexp=\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
colours=bold magenta
count=more
======
# connect requires special attention
regexp=connect
colours=on_red
count=more
======
# connect requires special attention
regexp=\skernel:\s
colours="\033[0;38;5;97m"
count=once

pyroute2 is not so fast

接上篇。

由于发现了 subprocess.popen() 的线程泄露问题,我们对这个调用加了线程锁,最简单的绕过这个问题,与此同时我将一些频繁调用的地方从 bash 调用改成 linux 系统调用(比如直接使用 read,而避免 bash 调用 cat),在很多场景有了不错的性能提升。

于是我怀疑 kvmagent 的一些性能问题和我们的 bash 的有些滥用有关系,我们知道 pyroute2 是直接调用 netlink 的,因此我做了这个测试:

可以看到 read 最快,bash+cat 次之,pyroute 居然最慢。

考虑到 pyroute 可能初始化的 workload 更大,我们可以试试先初始化好 import 和必要的初始化工作:

so, pyroute2 is not so fast.

devconf 19′: virtio 硬件加速

前言

devconf 也是我比较关注的一个 summit,devconf 的内容当然比较偏实践,但有一些东西还是比较前沿的。

很多人会认为 virtio 是一套实现(virtio-net, virtio-blk 等等),但实际上 virtio 是一套标准(或者说抽象层),因为 virtio 通过半虚拟化的方式来加速虚拟化的性能,那么就需要 hypervisor 和 guest 的协作来达到目的,其中 hypervisor 端我们称为 backend driver,guest 端称为 frontend driver。

virtio 的具体介绍在 developer works 有一篇很好的文章,如果对 virtio 不了解的话可以参考这篇: https://www.ibm.com/developerworks/cn/linux/l-virtio/index.html,进一步的,还可以阅读 Rusty 写的原论文:https://www.ozlabs.org/~rusty/virtio-spec/virtio-paper.pdf

这里简单介绍一下 virtio 的基本架构,就是下面这张图:

image.png-54kB

可以看到 IO 的核心就是 virtqueue,virtqueue 定义了 add_bufget_bufkick 等几个关键 IO 接口。

virtio 刚提出时其实是很先进的,因为通过共享内存替代了完整的 trap/模拟过程,大大提升了性能,但是随着底层 IO 设备性能的越来越强,大家对 virtio 也逐渐提出了更高的要求,例如通过 vhost 来加速等等,但是即使像 vhost、vhost-user 这些技术也解决不了对 hypervisor 资源(特别是 CPU)的占用问题,因此需要更加适合高性能 IO 设备的技术了。

一种思路是设备透传,作者在这里简单讲了下设备透传的缺点:

image.png-117.3kB

主要是一来热迁移很难做,当然并不是说完全不能,今年的 KVM Forum 上就有 topic 讲 GPU 透传怎么做热迁移,Netdev 也有讲 SRIOV 网卡怎么做透传,但有几个问题:

  1. 热迁移实现和透传设备类型强相关,例如上面 GPU 的热迁移和 SRIOV 网卡的设计完全不同
  2. 需要很多的代码改动,不能复用现有的 virtio 设备热迁移框架
  3. 更加灵活可控

为此 oasis 现在发布了 virtio 1.1 spec,在 devconf 时还是 draft 阶段,现在已经正式发布了:https://docs.oasis-open.org/virtio/virtio/v1.1/csprd01/virtio-v1.1-csprd01.html。virtio 解决的核心问题就是性能,这个不仅包括软件实现的性能,也包括硬件实现的性能(和实现的难度)。

设计

Packed virtqueue

首先 virtio 1.1 最重要的改变之一就是 virtqueue 的改变,由 split virtqueue 转为 packed virtqueue。这里我要先讲一下 split virtqueue 是什么,以及遇到了什么问题再讲 packed virtqueue。

image.png-117.1kB

本节的图来自去年的 DPDK Summit,Jason Wang(Redhat)和 Tiwei Bie(Intel)在去年 DPDK Summit 对 Virtio 1.1 做了很好的介绍,推荐阅读,原 Slide 在:https://www.dpdk.org/wp-content/uploads/sites/35/2018/09/virtio-1.1_v4.pdf

Split virtqueue 顾名思义,queue 会有多个 ring,分别是 available ring、descriptor ring 和 used ring,每条记录都通过
next 指针来标识下一条记录,这样就会有下图所示的几个问题,而且对于硬件实现来说,这些跳转会带来开销比较高的 pci transaction,不利于性能提高。

image.png-182.8kB

下面的是 packed virtqueue,packed queue 把原本分散在三个 ring 的元数据组合在了一起,这样元数据读取的软件实现可以减少 cache miss,硬件实现可以只用一个 PCI transaction 解决。

image.png-141.7kB

其他

另外就是一些新的特性,这些特性需要在设备 negotiate 时决定是否开启,比如:

  1. in-order completion。以往 ring 的完成是可以乱序完成的,这样 driver 实现就需要做的更复杂,也不利于优化(比如不好做批量动作)
  2. 支持内存访问有限制的设备(比如设备的内存访问要经过 IOMMU)
  3. 支持开启关闭特定 ring buffer 的 notification,硬件实现可以减少 PCI transaction
  4. notification 增加更多的信息,这样硬件实现上可以并行做更多事情,而且减少了在 PCI bus 上来回获取信息需要的时间。

目前的状态

硬件实现是需要看供应商的,所以这里除了 paper work 之外我们还可以说下目前软件实现的状态。

Packed virtqueue

packed virtqueue spec 其实已经定下来了,也发布在了 1.1 spec 中,对应实现需要 front 和 backed 都改,所以当前状态如下:

image.png-58kB

dpdk 这边进展是比较快的,通过 dpdk 理应已经可以测试 packed queue 带来的效果。

vDPA

image.png-227kB

即使有了上述规范,实现一个硬件 backend 的 virtio 设备也是比较繁琐的,因此 Intel 提出了 vDPA 这个框架,可以理解为如果你真的打算用硬件来做 virtio backend,vDPA 帮你把通用的一些工作已经做好了,例如硬件设备抽象、IO 路径等等,Intel 目前给出了 vDPA 下两个驱动,一个是 IFCVF,用来支持 Intel FPGA 100G,不过这个 FPGA 开发板可是很贵,后来 Intel 又提供了 vDPA Sample,这样你可以从中学习 vDPA 的工作方式。

vhost-mdev

vDPA 其实挺好的,但 Intel 后来又提出了 vhost-mdev,上面 vDPA 的架构图你也能看到有个 vhost/mdev,这是为什么呢?在我看来主要是因为 vDPA 原本设计比较面向网络,我们要先看下 vDPA 的原本设计细节架构(摘自 kvm forum 2018):

image.png-422.7kB

可以看到 vDPA 做了数据面,但是控制的部分其实是 virtio-net(vhost-user)实现的,这样在刚开始做 vDPA 来说很方便,因为减少了很多工作,但如果面向别的类型设备准备实现 virtio 硬件加速,比如存储啊、一些辅助加速设备啊什么的就会发现 vDPA 帮我们减少了数据面的代码量,但控制面还是需要很多工作。就像 vDPA 在 DPDK 里做 vdpa driver 这样解决 vhost 到 vDPA 这个过程。

有没有什么通用 IO driver 呢,其实是有的,vfio。

所以我们可以在前端使用 vfio,在后端做一个 mdev 对接下面具体的 virtio 加速设备,厂商可以自定义 MMIO、PCI 空间等等这些控制面细节。

image.png-406.8kB

vhost-mdev 目前还处于比较早期的阶段。

Intel Cascade Glacier

我对 FPGA 很多内容也不了解,只知道这个是 Intel 去年正式发布的 FPGA 智能网卡,可以实现 virtio 加速,OVS 卸载,从 Intel 在去年 OVS Conf 的介绍看,Intel 为之提供了一套 SDK 和软件栈。

image.png-342.5kB

此外还支持 P4:

WX20190508-003724@2x.png-742kB

总结

  • 越来越多供应商开始对 virtio 硬件加速感兴趣
  • 目前已经有至少一个硬件 ready
  • 有通用的软件框架支持各种硬件
  • Virtio 1.1 为硬件实现做了很多优化和改进

Eurosys 19′ Notes:Ursa: Hybrid Block Storage for Cloud-Scale Virtual Disks

Ursa 是美团云 16 就发布过的面向 IaaS 云主机的块存储系统,目前 Ursa 主要有几篇公开文章讨论其架构:

最早:https://tech.meituan.com/2016/03/11/block-store.html 介绍了 motivation、和其他块存储的比较
17 年在知乎专栏发表了基于混合存储的效率优化,和这次 Eurosys 19′ 内容相关:https://zhuanlan.zhihu.com/p/27695512
17 年还有一篇 USENIX 17′ 的文章:https://tech.meituan.com/2017/05/19/speculative-partial-writes-erasure-coded-systems.html 介绍了对 EC 的优化

下面介绍这篇文章,原文地址:https://www.cs.jhu.edu/~huang/paper/ursa-eurosys19.pdf or https://dl.acm.org/citation.cfm?id=3303967

简介

通过追踪块存储的 IO pattern 可以发现其 IO 的 locality 很差,因此相对于使用 SSD 作为 cache layer,Ursa 选择了底层直接使用 SSD-HDD 混布方案,将主副本放在 SSD 上,备副本放在 HDD 上,通过 Journal 来弥补 SSD 和 HDD 之间的性能差距。实验显示之中模式在大部分情况下可以达到与全 SSD 相同的性能,与全 SSD 的 Ceph 和 Sheepdog 相比效果也很好,而且有更高的 CPU 效率。

最近几年有一些提升虚拟磁盘吞吐的研究,例如 NSDI 14′ 发表的 https://www.usenix.org/system/files/conference/nsdi14/nsdi14-paper-mickens-james.pdf ,但低成本的提高虚拟磁盘效率还是很难。通过生产环境的实验以及过去的研究(https://www.usenix.org/legacy/event/fast08/tech/full_papers/narayanan/narayanan.pdf )可以发现块存储 IO 有两个典型特征:

  1. 大部分情况都是小 IO 为主,偶尔有顺序大 IO
  2. 读和写的 locality 都很差

因为小 IO 为主,因此 SSD 在构建块存储上一定优于 HDD。但是 SSD 的价格和能耗使得全 SSD 成本太高。因此考虑到第二个特征——locality 弱,使用 SSD 作为 cache layer 效果也并不会好,根据之前的研究,考虑到高端 SSD 在延迟和 IOPS 上高 HDD 三个数量级,因此哪怕 1% 的 cache miss 也会导致平均 IO 性能降低到预期的 1/10。此外,SSD cache 对长尾延迟帮助很小,而这个是云供应商 SLA 的一个重点。最后,额外的 Cache 层还会造成块存储层的一致性问题,例如 Facebook 在 2010 年就发生过这样的悲剧。

根据上面的介绍,Ursa 使用了 SSD、HDD 混布方案,SSD 作为主副本,HDD 作为备副本,为了弥补之间的巨大性能差距,通过 journal 来将 HDD 上的随机写转化为顺序的日志追加写,再把日志异步 replay、merge 回磁盘。为了提高效率,偶尔的大的顺序写还是直接发到 HDD,跳过 Journal。这篇 Paper 将介绍几个方面:

  1. SSD-HDD 混布方案的设计,同时为了解决 Journal 和副本的结合带来的复杂性,设计了 efficient LSMT(log-structured merge-tree) 来达到快速的 invalidate 无效 journal 和在故障恢复时快速读取 journal;
  2. Ursa 中的多个级别的并行,包括磁盘并行 IO、磁盘间的条带化和网络 pipeline,通过这些方法来提高吞吐性能;
  3. Ursa 的满足强一致性(线性一致性)的复制协议,rich-featured client 和在线升级的高效机制。

实验显示 Ursa 在混布模式下提供了接近全 SSD 的性能,与全 SSD 模式的 Ceph 和 Sheepdog 相比还实现了更高的 CPU 效率。实际环境验证显示能在更少的 SSD 数量下提供全 SSD 部署的 AWS、腾讯云的块存储相媲美甚至更好的性能。

动机

首先我们研究了微软在之前文章发布的数据以及自己收集的数据,显示可以看到 70% 的 IO 大小都小于 8KB,几乎所有 IO 都不大于 64KB,这说明块存储中小 IO 是主要组成。

image.png-56.8kB

因为 HDD 在随机小 IO 上的低性能,高性能存储往往使用 HDD 来构建,传统的 SATA SSD 比 HDD 在 IOPS 和延迟上要好两个数量级,PCIe SSD 的话更好。而且 SSD 比 HDD 有更低的故障率和相似的寿命。

SSD 的主要缺点就是价格,特别是基于副本的存储。一种办法是使用 SSD、RAM 做 Cache,但是 Cache 效果并不好。

image.png-37.9kB

上图显示了在读方面很低的 cache 命中率,因为这里很多数据都是只读一次。再考虑到 SSD 和 HDD 随机 IO 巨大的性能区别,稍低一点 cache 命中率就会很大影响整体性能,而且这种不稳定的 cache 命中会影响云服务的 SLA。

因此这里介绍的 SSD-HDD 混合块存储直接将主副本保存在 SSD 上,然后再复制到 HDD 上,所有客户端读一般来源于 SSD,所以没有 cache miss 的问题,不过问题是如果每次写都要同步复制到 HDD 的话,HDD 的性能会直接拖累这种简单的 SSD-HDD 混布设计,这样就没有意义做混布了,所以这里通过 journal 来弥补之间的差距,在异步 replay。尽管长期看起来看平均写性能还是受限于 journal 在 HDD 上的 replay,但实践中客户端感受到的随机小 IO 要比 journal 的顺序写性能好。

设计

架构概览

image.png-81.4kB

  1. Chunk Server。每个 data chunk 有一个主副本和多个备副本保存在不同机器,每个机器插多个 SSD 和 HDD,既保存 primary 也保存 backup。
  2. Client。VMM 通过 client 使用 NBD 接触块存储。client 查询和管理元数据(例如云盘的创建、打开、删除)时与 master 交互。这种交互是 stateless 的。
  3. Master。Ursa 中有一个类似 GFS 中的 master 来简化管理操作。master 不参与正常的 IO 路径来避免成为瓶颈。master 提供 coordination,包括云盘创建、打开、删除,元数据查询,状态监控,故障恢复等等。

SSD-HDD 混合存储架构

对于一个读请求,primary server 直接读取来自 SSD 的数据,而写请求则先写到 SSD,然后复制到 backup(backup 会在写完 journal 返回),最后返回到客户端。

journal 既可以保存在 SSD 也可以保存在 HDD 上,Ursa 选在保存在 SSD 上因为有更好的并发 IO 性能,此外使用一个 in-memory index 来保存 chunk offset 到 journal offset 的对应。

长期来看,journal 这种方法也会受限于 HDD 的随机写性能,但是:

  1. 写操作里有很大比例是在 overwrite,而 overwrite 到 journal 可以合并
  2. 通过 journal 重放时做合并和调度可以减少 HDD 磁头移动

Journal 都保存在 backup 本地,因为本地 journal replay 要比跨机器的 replay 简单很多。根据实践经验,Ursa 使用 SSD 1/10 的容量存放 journal。

按需 journal 扩展

当一个 SSD 耗尽 journal 的空间后,Ursa 可以动态扩展 journal 到另一个负载最低的 SSD(其 journal 空间没有用完)。如果所有 SSD 的 journal 空间都用完了,那就使用 HDD 的空间存放 journal。理论上讲 HDD journal 可以按需任意大,但是和日志文件系统不同(例如 https://www.researchgate.net/publication/221235948_DualFS_A_new_journaling_file_system_without_meta-data_duplication),journal replay 是必须的,不仅是空间效率的问题,还有快速恢复的原因。

HDD journal 设计与 SSD 几乎一样,除了 HDD journal replay 只会发生在 HDD 空闲时。在 Ursa 线上部署的两年时间里,HDD journal 从来没有用到过。这是因为条带将数据分布到很多机器上上,这样很难出现某一台机器集中产生大量写入。而且如果一个客户端产生大量的 IO 会先被 master 限速而不是把 SSD journal 耗尽。

Journal bypassing

大于 64KB 的 IO 会跳过 journal 直接写入 HDD。

Client direct replication

为了降低极小 IO 的延迟,考虑到很小的 IO 只占用极小带宽,小于 8KB 的 IO 会在 client 直接发到 primary 和 backup,不通过 primary 来复制。这个是这样算出来——每个机器两个万兆网口,希望最多只用一半的带宽,然后提供给客户端最高 40K 的 IOPS,考虑到三副本,那就是 20Gb/2/40K/3 ~= 10.4KB,所以设置为 8KB。

Journal index

因为上面介绍的 bypass,数据复制和恢复的复杂度会上升。为此 Ursa 设计了 per-chunk in-memory index 结构,map 了每个 chunk ofsset 对应 journal offset,这样可以快速 invalidate journal 和快速恢复。

journal index 一般通过 LSMT 设计,然后原本的 LSMT 无法满足 Ursa 在 invalidate 和 recovery 时的要求:

  1. LSMT 的 index 的 key space 是连续整数
  2. LSMT 的 index 查询和更新对 journal 读写有着严重影响,所以会直接决定 Ursa 的 IO 性能

因此 Ursa 对 LSMT 做了一下优化,显著提高了 range query 和 range insertion 的性能。

Composite keys

如果 key 对应的 journal offset 是连续的,那就合并为一个 composite key {offset, length}。composite key 之间定义了 LESS 关系:x LESS y iff x's offset+length <= y's offset,这样 composite key 形成了一个有序关系。

Index operations

因为上面定义的有序关系,因此快速的查询和更新就可以很快。

image.png-32.5kB

Index storage

KV({offset, length} -> j_offset)保存为一个 8 字节的结构体。Ursa 在 KV 中保存的是一个两级结构,第一级是红黑树,高插入效率,低存储效率,第二级是有序数组,插入效率低但存储效率和查询效率高。

当新增一个 KV 的时候,Ursa 首先快速插入到红黑树,然后由后台的低优先级工作线程异步合并到数组。因此数组中保存的 KV 可能是过期的(红黑树中的数据还没有同步到数组),所以查询查询时会先查询红黑树,然后在对 missed range 去数组查询。这样一级红黑树实际上是二级数组的一层小容量 cache。对于数组,8GB 的内存可以保存十亿条记录,相当于在 16TB journal 上全部以 16KB 的 IO 写入所需要的记录数量(16TB/16KB)。

多个层面的并行

磁盘层面

为了充分利用 SSD 的并行 IO 能力,每个 SSD 会运行多个 chunk server 进程,每个进程使用一个 libaio 线程。进程内 Ursa 会把 libaio 事件转换为协程,通过一个同步调用接口来隐藏 libaio,相反的,HDD 上只运行一个单线程进程,不使用 libaio。

跨磁盘层面

跨磁盘层面有三种并行机制,包括:

  1. 条带化
  2. 乱序执行
  3. 乱序完成

首先,Ursa 将一个虚拟机磁盘分割成多个固定大小的 chunk,然后两个或多个 chunk 组成 strip group,这样大的读写可以并行在多个 chunk 上。service manager 来保证数据 placement policy,即一个 strip group 的所有 chunk 不在同一磁盘或物理机上。

其次只要 IO 落在在不同的 chunk 上,Ursa 就允许它们乱序执行。

最后 Ursa 支持乱序返回。比如对某个 chunk IO 请求顺序是先有请求 r1,然后是请求 r2,那么返回时可以相反。

网络层面

网络拥塞和操作系统调度可能造成端到端的延迟。Ursa 的做法是使用对每个链接使用 pipeline 来处理 IO 请求,这样网络延迟对总体的 IOPS 和吞吐的影响就小很多。不过这样显然会有对上层的崩溃一致性的问题(因为完成顺序和发起顺序不一致),这个应当由上层来保证,例如 Linux Ext4、XFS 都有 journal 机制,OptFS 有 osync 和 dsync 来保证最终一致和持久化时保持一致(MatheMatrix:我觉得可以参考 OSDI 14′ 的这篇文章 https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-pillai.pdf,以及我们实际遇到过的,一些文件系统并不一定达到了完整的崩溃一致性)。

一致性

概览

Ursa 通过 chunk server 间的 totally ordering write 来提供对每个 chunk 的线性一致性。简单来说,Ursa 的复制协议和 Paxos、Raft 这样的 RSM(Replicated State Machine)是同源的。每个 chunk 有版本号,chunk server 和 master 共同维护持久化的视图号(view number),view number 会在故障恢复后分配新 chunk server 时更新。

当客户端打开虚拟磁盘时,客户端会从 master 获得每个 chunk 的视图号和位置,然后向所有 replica 查询它上面的视图号。一旦视图号得到确认,client 会选择一个 replica 作为主副本(一般是 SSD 的那个),然后对主副本做读写请求,如下图。

image.png-82.5kB

如果 client 检测到某个 replica 失效,就会汇报到 master 重新分配一个 replica,更新 chunk 的位置,增长视图号。

尽管 Ursa 的复制协议 follow 了经典的设计原则,但是还是有两方面明显不同。其一是 Ursa 保证任意时刻只有一个 client 可以 access 虚拟磁盘(MatheMatrix:如果客户需要共享云盘?后面有解答);其二是混合错误模型来区别对待副本和网络失效。

Single client

Ursa 通过租约和锁协议来保证任何时候最多只有一个虚拟机可以某一个云盘读写数据。这样大大简化了强一致性设计。Client 会周期性 renew 租约,一般是 10s。如果有多个 VM 挂载同一个云盘,会在任意一个物理机上使用一个虚拟机通过类似 OCFS2 的集群文件系统协调 IO 请求,对所有挂载的 VM 提供服务。

混合错误模型

不同 GFS 这种同步复制系统(f+1 份拷贝,允许 f 个失效)或其他的异步复制系统(2f +1 份拷贝,允许 f 个失效),Ursa 将副本和网络的失效区分对待,类似 VFT、XFT 这样。Ursa 中 client 会尝试写到所有副本,但会有一个超时时间,如果发生了超时,只要主副本成功即可 commit(上图的第六步)。同时 client 会通知 master 来修复问题,例如分配一个新的 replica 来替换。

与同步复制系统相比,这样可以在一些 replica 失效时始终保持系统可用,和异步复制系统相比持久性更好,具体来说,Ursa 可以在下面的情况保持持久性:

  1. 2f+1 中不多于 f 个副本失效
  2. (失效副本数)+(链接故障数)< 总副本数

(MatheMatrix: 这里我一开始没看懂,后来咨询了本文主要作者之一李慧霸博士,这里的核心含义是 Ursa 会先尝试同步复制,但超时则降级到异步复制,从而实现用户视角的服务可用性)

Ursa 复制协议

一般情况

初始化

当打开虚拟磁盘时,客户端将从 master 获得所有 chunk 的位置和视图号,然后一步查询每个 chunk 的所有 replica 的版本号和视图号。如果一个 chunk 的所有 replica 都有与 master 保存的一致的 view number,那么就可以从中任意选一个作为 primary(优先选择 SSD 上那个);否则,客户端会先向 cluster director 通知修复一致性,然后再重试。因为前面介绍的 single client 设计,client 其实是可以在任意时间更换 primary 的选择。

读和写

一旦视图号和 primary 得到确认,client 就可以向 primary 发送读写请求了。其中读请求优先由 primary 处理,写请求则会带着 client 的 view number 和 version number。还是上面的图,当收到写请求时,primary 会先检查本地的 view number、version number 是否和这个写请求一致,如果一直就在本地写,同时复制到 backup,增长 version number,最后回复给 client。backup 上的操作也是类似的。

然而,如果 client 的 view number 和请求的不一致,那么 primary 会拒绝并回复给客户端。client 需要从 master 获得现在 view number,如果 client 的 view number 大于 primary 上的,那么 primary 将尝试通过增量修复来更新自己的状态,增量修复就是从其他副本同步修改过的数据。如果 client 的 版本号等于 primary 的版本号减 1,那么 primary 本地不会做这个写请求(因为 primary 实际已经执行过这个请求),但是会转发到其他 backup。考虑到 single client 的前提,client 的版本和 primary 的差不会大于 1.

前面介绍的混合错误模型一方面提升了系统的可用性,但也增加了数据持久性的难度。具体来说:

  • 正常情况下,所有 replica 写入成功即 commit
  • 如果 primary 无法从所有 replica 获得成功写入的消息,那么会等待一定时间看能否获得半数成功(这样加上 primary 就是多数成功)
  • 与此同时,client 会通知 master 去修复不一致问题,或者可能最终就是分配一个新的 replica 替代失效的 replica
更换主副本

如果 client 读写发到了一个 fail 的 primary,那么 client 应该会自主更换 primary 到一个有最新数据的 backup 上,这样来实现高可用。考虑到使用了 SSD journal,即使此时是在 HDD backup 上,写入性能也不会太受影响,不过读取性能此时还是会受影响的。此时 cluster director 会同步在 SSD 上再创建一个新 replica,最终 client 会将 primary 重新移回到 SSD 上。

增量修复

为了支持增量修复,每个 replica(包括 SSD 和 HDD)都在内存里保存了一个 journal lite,cache 最近的写请求的 position、offset 和 version number。当一个 primary 或者 backup 从临时故障恢复时(例如网络分区),相关的 replica 会发送他自身当前的 version number 到其他 replica,其他 replica 收到时:

  1. 根据请求的 version number 查询 journal lite,找到修改的数据
  2. 根绝 journal lite 里的信息构造修复消息
  3. 修复消息里加上新的 version number,回复给 replica

如果无法通过 journa lite 找到数据(比如因为垃圾回收),那么就会传送完整数据。

Client-directed replication

前面提过这个事情,就是小于等于 8KB 的 IO 会由 client 直接送到所有副本,而不是通过 primary,其 version number 的维护是类似的,这样可以显著降低小 IO 的延迟。

错误恢复(View Change)

当发生了不一致问题时,master 会向具有最高 version number 的 replica 发送请求来做增量修复。反之如果是 chunk 有问题,那么 master 最终会分配一个新的 replica 来替代问题的,view number 会更新为 i+1,client 会在下次读写时得知这次 view number 的变化。

更具体地说,master 会从多数 replica 中收集 version number,然后选择其中最大的 version number vm 作为最新 state,将数据分发到新分配的 replica,如果需要的话,同时对已存在的 replica 做增量修复。

最后,所有的 replica 将 view number 更新为 i+1,因为它们此时有相同的数据(以及相同的 version number vm)。如果 master 和一个 replica 同时故障了,那么先修复 master,然后修复 replica。

故障恢复的核心思想就是 master 从 quorum 中找到最高的 version number,这个和异步复制系统是一样的。这个方案的缺点就是如果多数 replica crash 则存在丢数据的可能。相反同步复制系统可以在哪怕只有一个幸存者时工作。

下面这里的讨论看起来是如何通过识别 majority 中的永久 crash replica 来达到即使只有一个幸存者 依然可以找回数据,目前 Ursa 的做法还比较依赖手工。

讨论

特性丰富的 Client

Ursa 将很多特性例如 tiny write replication、striping、snapshot、client-side caching 都放在了 client。这和与 Qemu 的紧密集成有关。

Client 被设计为装饰器模型的 pluggable modules,所有 module 实现了公共的 read()/write() 接口,client 可以在线无感升级。

在线升级

Client

当 client 和 VMM(qemu)断开连接时,VMM 不会自动重连,所以想在不影响 Guest 的前提下升级还是比较难的。一种思路是把代码尽可能放到 shared library,这样升级就是 dynamic reloading,就很简单,但是也有很多限制,比如现有很多静态库不适合,接口不好升级等等。因此 Ursa 升级粒度是 process 而不是 library。Ursa 将 client 分为两个 process——core 和 shell。当升级时:

  1. core 停止从 VMM 接受新 IO,将 pending IO 完成
  2. 将状态保存到一个临时文件
  3. 退出并返回一个特殊返回码

shell 进程接收到 exit code 启动一个新的 core,从临时文件读取状态并恢复。

(MatheMatrxi:大体就是一个通过外包壳来分离核心代码的思路,升级期间 IO 应该都 pending 了)

Master

Master 升级是比较简单的,因为不涉及 IO 路径,只要关掉旧的立刻启动新的即可。升级期间磁盘创建和空间分配都会失败,不过客户端会自动重试。

Chunk Server

这个就比较难了,特别是在 chunk server 升级中发生 failure 时会对 failure handle 产生 confuse。所以 Ursa 设计了一个优雅热升级的策略:

  1. 发送一个特别信号到 chunk server;
  2. 关闭服务端口,停止接收 IO;
  3. 等待 in-flight io 结束;
  4. 等待新的 chunk server 启动;
  5. 检查新 chunk server 正常工作。

如果热升级成功了,旧的 chunk server 关闭所有连接( MatheMatrix:比如管理端口的连接)退出,客户端自动重连到新的 chunk server。反之如果失败了,旧的 chunk server 会杀死新的 chunk server,重新打开服务端口继续提供服务。

逐个升级

一个 Ursa 集群有众多服务进程做成,升级时每次升级一个进程,检查状态再升级下一个。所以升级集群可能会持续好几天时间。所有组件保持向后兼容性,到目前 Ursa 在一个部署超过两年的集群里对 replication protocol 升级过四次版本,每次升级都会增加新的操作并保证之前的操作不会发生变化来达到向后兼容。

发挥磁盘并行性

根据经验,Ursa 对 SATA SSD 和 PCIe SSD 会跑两个和四个进程,HDD 上跑单线程单进程,这个线程同时处理 journal replay、small write 和 replication( large write)。评估显示单线程进程只用电梯算法就可以跑满 HDD,多进程、多线程这些都会扰乱电梯算法,降低性能。

硬件可靠性

根据统计(下面的表格),HDD 贡献了 70% 的错误,比 SSD 高一个数量及。而 SSD 的 wear leveling(损耗平均)保护了 SSD 不会被频繁的 journal 写影响寿命。

image.png-20.3kB

不过虽然 SSD 平均故障率比 HDD 低,但是有一个潜在风险就是 SSD 可能因为固件 bug 造成同一批、同一个供应商的 SSD 批量出现问题。(MatheMatrix:然后举了腾讯云去年的例子 /捂脸)

为了解决这个影响,需要增加购买 SSD 的多样性,而且把 primary chunk 和 backup chunk 的 journal 存放在不同批次、供应商的 SSD 上。和全 SSD 存储不同的是 Ursa 可以通过 HDD 避免因为 SSD 固件 bug 造成的批量 SSD 下线造成数据丢失。

局限

  1. primary replica 故障恢复时客户就需要忍受 HDD 的性能影响,这要求我们尽可能缩短降级时间;
  2. 因为每个机器能提供的 SSD journal 大小有限制,SSD journal 无法持续性提供长时间很高的随机写(这个在美团云服务中比较少见),这个通过 HDD journal 和限速来 work around;
  3. SSD 的实效恢复要比全 SSD 更 urgent(MatheMatrix:和 1 好像一样)
  4. 对 SSD 的大小要求比 SSD cache 大很多

评估

这里做了一些测试,我直接展示数据吧,比较了 Ursa(SSD 和 SSD-HDD 混布)、Ceph 和 Sheepdog:

image.png-86.5kB

QD(queue depth)最大为 16 因为 qemu NBD driver 只能提供到 16。Sheepdog 和 Ceph 都是部署在全 SSD 环境的。(c) 这里表现比较差主要是因为 1MB 的 BS 大于 64KB 的 journal pass 阈值,所以 backup 都直接写入 HDD 跳过 journal 了。

image.png-84.2kB

image.png-84kB

图 10 主要是因为 Ursa 很依赖 journal 的 range query 的性能,因此和 PebblesDB 做了一个比较。

image.png-156.9kB

图 14 这里的选取了三种有代表性的 trace 数据 prxy_0proj_0mds_1 分别代表不同的 IO pattern。

相关工作

这里举了一些块存储、EC、混合存储、文件系统、对象存储和一致性的文章

总结

未来计划 RDMA/DPDK/SPDK 来达到超低延迟;提高顺序写的 IOPS。

Ursa 的一部分组件是开源在 http://nicexlab.com/ursa/ 的,包括:

  • st-pio 前面说的 libaio 的包装
  • st-redis hiredis 的包装
  • logging lib 支持东岱更新配置的轻量级 logging library
  • crc32 高性能的 crc32 library
  • testing script

后面是致谢和线性一致性的正面,包括 normal case 和故障恢复。

使用 Clion 查看 DRBD(Kernel Module)代码

因为内核里有很多编译参数,所以需要配置下。

可以参考 http://ybin.cc/tools/clion-for-linux-driver-developer/

我的最终配置是:

...
include_directories(../kernel-3.10.0-327.36.1.el7/linux-3.10.0-327.36.1.el7/include)
include_directories(../kernel-3.10.0-327.36.1.el7/linux-3.10.0-327.36.1.el7/include/linux)
include_directories(../kernel-3.10.0-327.36.1.el7/linux-3.10.0-327.36.1.el7/mm)
include_directories(../kernel-3.10.0-327.36.1.el7/linux-3.10.0-327.36.1.el7/arch/x86/include)
include_directories(../kernel-3.10.0-327.36.1.el7/linux-3.10.0-327.36.1.el7/include/uapi)
include_directories(../kernel-3.10.0-327.36.1.el7/linux-3.10.0-327.36.1.el7/arch/x86/include/uapi)
include_directories(.)
include_directories(drbd)
include_directories(drbd/compat)
include_directories(drbd/linux)

add_definitions(-imacros ../kernel-3.10.0-327.36.1.el7/linux-3.10.0-327.36.1.el7/include/linux/kconfig.h)
add_definitions(-D__KERNEL__)
add_definitions(-DKBUILD_MODNAME)
add_definitions(-DCONFIG_BLOCK)
add_definitions(-DCONFIG_HZ)
add_definitions(-DMODULE)
add_definitions(-std=gnu89)
...

NSDI 2019 Notes

前言

NSDI 2019 里有两篇容器网络相关的话题,这篇还是比较有意思的,the morning paper 也谈到了这篇文章:https://blog.acolyer.org/2019/03/22/slim-os-kernel-support-for-a-low-overhead-container-overlay-network/。原版的视频、Slides、文章在 NSDI 官网都可以看:https://www.usenix.org/conference/nsdi19/presentation/zhuo。同时作者在 Github 上开源了实现:https://github.com/danyangz/Slim

大致思路是容器里的应用的流量送到另一个容器里的应用需要经过四次协议栈。

除了底层物理机的协议栈之外,主要是有一层 network namespace:

因此主要思路就是绕过这一层 stack,其效果还是不错的:

  • memcached 吞吐提高 71%,延迟降低 42%,CPU 占用减少 56%
  • Nginx CPU 占用减少 22-24%
  • PostgreSQL CPU 占用介绍 22%
  • Kafka CPU 占用减少 10%

介绍

容器网络往往使用 overlay 网络,但是 overlay 网络会带来显著地性能影响。测试显示 overlay 网络和 host 网络相比的吞吐会下降 23~48%,每个报文的延迟会增长 34~85%,CPU 占用会提高 93%,现有的加速技术往往是针对虚拟化的,对容器支持不够。

这里的核心问题就是一个包要在一个物理机上穿越两次协议栈,来回就是四次。这种设计显示受虚拟化的影响,因为虚拟机是有自己的协议栈的,宿主机不知道任何 Guest 的协议栈知识,但是容器不然,宿主机知道每个网络连接的完整信息。

因此作者设计了一种容器网络,核心思想就是让一个物理机上报文只经过一次协议栈。

这个设计有几个挑战:

  1. 网络虚拟化不能要求应用作出修改
  2. 需要支持与现在的网络相同的网络策略(network policy)
  3. 支持现在的容器网络相同的安全模型

Slim 的优点在上一节描述过了,缺点也有几个:

  1. 增加了连接建立的复杂性,因此连接建立慢了 106%;
  2. 不支持容器热迁移;
  3. network policy 由 packet-based 转为了 connection-based;
  4. 只支持 TCP 这种状态协议

背景

容器网络一般有几种通信模型:

  • bridge mode
  • host mode
  • macvlan mode
  • overlay mode

image.png-25.1kB

其中:

  • bridge mode 只用于同 host 通信;
  • host mode 性能好,但是管理和部署非常复杂,实际上像 Kubernetes 都不支持这种模型;
  • macvlan mode 类似于硬件机制(例如 SR-IOV),macvlan 让容器 IP 可以在 host 网络路由,但增加了数据中心网络路由的复杂性,因此大部分 cloud provider 已经 block 了 macvlan mode;
  • 所以最终最流行的方案还是 overlay,这个类似于虚拟机,目前有很多这样的实现,例如 Weave、Flannel、Docker Overlay 等。

overlay 网络的核心之一是 vswitch,提供了:

  • network bridging,允许同 host 通信
  • network tunneling,跨物理网络通信

vswitch 一般用 ovs,使用 vxlan 作为 tunneling protocol。

然后就是各种 network policy(例如 access control、rate limit、 QoS 等),具体实现往往通过 iptables、ovs 等。

这种 overlay 网络的问题很明显,就是基于 packet,因此在 network namespace 要经过打包、封包,在 host 上协议栈要再来一次包识别、封包,到了对端则要再解包、识别,再解包再识别,因此 overhead 就很重了。

下面的表格展示了实测中 overlay 带来的性能成本:

WX20190423-125222.png-69.5kB

下图展示了通过 Weave 实测的 CPU 占用成本:

WX20190423-125411.png-52.6kB

可以看到 CPU 主要陈本在 soft irq 上。

当然现在有一些技术来解决这个问题,例如 packet steering,创建多个 queue,对应每个 CPU 核心,用 hash 来 map 报文和 queue,这样跨核心的成本可以下降很多,下面的表格展示了使用 packet steering 带来吞吐和延迟提升:

image.png-60.5kB

packet steering 提升了 91% 的 TCP 吞吐,但并不能缓解延迟,而且 CPU 占用也有影响。

整体来说,这种容器网络,特别是 overlay 这种我们可以统称为 packet-based virtualization,可以用下图概括:

image.png-112.3kB

设计

Slim 通过减少报文进协议栈的次数来优化,其思路可以简称为 connection-based virtualization:

image.png-95.2kB

设计时有几个设计目标:

  1. 方便部署,支持未修改的应用;
  2. flexible,支持各种 network policy;
  3. 安全,容器不能去获得物理网络的信息们不能直接在 host network 上创建连接,或者提高 traffic priority

image.png-37.4kB

上图是 Slim 的整体设计,有三个主要组件:

  1. 用户态有一层叫做 SlimSocket,与应用相连接;
  2. 用户态的 SlimRouter,跑在 namespace 里;
  3. 一个小的可选的内核模块 SlimKernModule,可以实现一些高级功能,例如动态更改 access control rules、安全配置等

这里面 SlimSocket 暴露了 POSIX socket 接口,来 intercept 应用的 socket 相关 sys call。当 SlimSocket 探测到应用尝试建立连接,就发送请求到 SlimRouter,SlimRouter 负责创建连接,再以文件描述符(fd)返回给应用,下面应用就是用这个 fd 来 send/receive 报文。因为 SlimSocket 是 POSIX 接口,Slim 会将 SlimSocket 动态链接到应用,所以应用不需要改动。

下面是解决控制策略、安全策略,如果再回到 packet based 方法的话就太低效了,因此这里的方法是在创建 connection 之前通过 SlimRouter 检查。如果是连接建立起来之后规则改变了,那么 SlimRouter 会扫描所有现有连接然后 remove 不能满足规则的 fd。

这种将 host 的 fd 直接返回给容器内的进程显然会引起很多安全方面的 concern,为此 slim 提供了一个安全模式,当开启安全模式时,SlimKernModule 会限制返回给 container 的 fd 的能力,SlimKernModule 有三个功能:

  1. track fd 在 container 中 propagate;
  2. 根据 SlimRouter 请求 revoke fd;
  3. 禁止这个 fd 的不安全的 sys call(getpeername、connect、setsocket),SlimSocket 会对非恶意的应用的模拟这些 sys call。

下图是 blocking io 为例演示连接建立的整个过程:

image.png-88kB

这部分在 slide 中很好的演示,上图也很清晰,我就不详细介绍了。

现在很多应用都是使用异步 API 了(select、epoll),而不是上面演示的同步 API,以 epoll 为例,epoll_create 会创建一个 meta fd,meta fd 实际表示了一个 fd set,应用使用 epoll_wait 来等待 fd set 的任一事件。在连接建立时,我们需要修改 epoll fd set 中的 fd。SlimSocket 会通过 epoll_ctl track epoll fd 和 epoll fd set 的对应。比如 epoll fd set 中的 fd acceptconnect 时,SlimSocket 会将原 fd 从 set 中移除,并增加 host namespace 的 fd 到 set。

这个方案有一个限制是对现有的 IT 工具不友好(host 上使用的 IT 工具),如果你想用 iptables 什么的搞一些之前的一些 packet-based 的策略的话,就不能用 Slim。

实现

大致上在上一节都提到过了,SlimSocket、SlimRouter、SlimKernModule 分别用了 1184 行 C、1196 行 C++ 和 1438 行 C,SlimSocket 使用 LD_PRELOAD 来动态链接到应用,SlimSocket 和 SlimRouter 之间用 Domain Socket 通信。非安全模式时,SlimRouter 用 sendmsg 直接传递 fd 到 SlimSocket,安全模式则经过 SlimKernModule 通过 fd duplication method。

SlimRouter 通过 JSON 文件获得 access control 的内容,提供一个 CLI 来 reload JSON。rate limit 和 QoS 通过 tc 实现。

SlimRouter 和 SlimKernModule 之间通过 procfs 中的 dummy file 通信。

SlimKernModule 需要替换 sys call table 中的函数指针,用 hash table 保存 taged fd 和 unsafe 的 sys call 列表( MatheMatrix:tagged fd 原文说类似于 OSDI 10′ https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Enck.pdf,是 SlimKernModule 实现里很重要的内容,但我没有具体看)

评估

直接看图了,测试环境是 40G 网络,使用 iperf3、NPtcp、mpstat 这些开源软件评估。RFS 为 weave 打开 rfs 的性能。

image.png-144.5kB

下图展示了 network policy 的效果。

image.png-166kB

此外还有一些使用应用的测试结果,有 Memcache、Nginx、PostgreSQL、Kafka

image.png-155.3kB

image.png-279.2kB

image.png-200.5kB

image.png-212.4kB

此外作者还测试了容器迁移,Slim 会轻微的影响迁移性能。

image.png-156.7kB

讨论

  • 连接建立时间。Slim 会比较明显的延长连接建立时间,优化思路吗,就是尽量少关 SlimSocket,都需要一些方法;
  • 容器热迁移。前面虽然测了容器迁移,但其实不是热迁移,Docker 现在有实验项目 criu,但还比较弱,Slim 从设计上将,增加了容器热迁移的难度;
  • UDP。UDP 本质上可以支持,但问题是做网络策略就比较难了,另外就是 UDP 在数据中心的典型使用场景是减少连接建立时间,而 Slim 正好相反;
  • Packet-based 网络策略。这个实在不好做,真的需要的话只能用 overlay 网络;
  • LD_PRELOAD。这个其实对应用有要求,因为一些 system 可能假设了一些应用(例如 Go 编写的一些)是静态链接的;
  • Error code。一些情况下,error code 与 overlay 网络的结果不同,不过这个是可以解决的;
  • SmartNic。SmartNic 对容器网络可能帮助并不大,比如 Catapult 可能需要 Linux 的改变,SR-IOV 有 macvlan 一样的问题,FlexNIC 可能还行,但还是实验阶段。