1 问题初现 —— “文件用得好好的为什么要换” 初始设定我们有一块普通的硬盘想在上面存 Ceph 的对象比如一张图片。操作系统教我们先格式化一个文件系统比如 XFS然后open一个文件把数据write进去。❓灵魂拷问 1直接用操作系统的文件系统存数据有什么不对吗在早期的cephFileStore时代我们就是这么干的。但运行久了发现了几个及其别扭的“拧巴”现象拧巴点一写 4KB 数据磁盘写了两次。为了保证断电不丢数据必须先写一份Journal日志AOF等日志落盘了再回过头修改XFS的文件。问这像不像寄快递时你先在本子上记一笔日志再把包裹塞进快递柜硬盘结果本子和柜子各占一份空间拧巴点二修改文件中间的1个字硬盘忙半天如果你只改一个对象文件中间的 4KB文件系统没法直接覆盖。你必须把整个对象读出来Read改掉那 4KB再把整个对象写回去Modify-Write。问这不就是“为了换灯泡把整个房子拆了重建”吗2 破局之问 —— “我能不能不要中间商”❓灵魂拷问 2既然 XFS 这个“中间商”老在中间赚差价带宽和添乱那如果我直接接管这块硬盘不要文件系统了行不行当然行操作系统本身就允许我们使用O_DIRECT的方式绕过内核的Page Cache直接write到硬盘的某个扇区LBA地址写数据❓灵魂拷问 3没有了XFS的文件系统那么对于一个文件路径/pg1/obj1就没了。当我收到客户端请求“给我对象obj1”时我如何具体的对应到硬盘的哪个扇区偏移量上呢这就需要一张“地图”以前文件系统XFS帮我们维护了这张地图文件名-inode-磁盘块现在如果不使用XFS就必须自己维护这张地图3 分而治之 —— “数据地图存放在哪里”❓灵魂拷问 4既然要维护“对象名 - 磁盘扇区”的映射关系那这张“地图”本身总得找一个地方存吧这里我们必须把“真正的大块数据比如 10GB 的电影”和“记录位置的元数据比如 10MB 的索引”分开。数据Data大块、大流量。我们直接调用write丢进硬盘深处的大块连续空间。元数据Metadata高频、小块、极其重要。但是为什么要分开呢IO 模型不匹配用户数据block是大块顺序 IOHDD 擅长这个但 RocksDB 的元数据是高频随机小块 IO放在 HDD 上会产生大量机械寻道导致性能骤降。避免相互干扰如果混盘元数据的小 IO 会打断数据的大 IO 流磁头来回摆动整体吞吐量会损失 50% 以上还会引发元数据锁的队头阻塞。成本最优解用 90% 成本的廉价 HDD 存海量数据用 10% 成本的高性能 SSD 存敏感元数据以最低的经济代价撬动最高的集群性能。这本质就是分布式存储里的‘冷热分层’思想。”总体来说就是HDD读取速度慢但是成本低SSD读取速度快但是成本高。让“大块吃肉顺序读”和“小块喝汤随机写”互不打扰各自安好。因此Ceph的bluestore把一块物理盘从逻辑上或物理上拆成了三条路组件名部署目录存的是什么IO 特性是什么该用什么盘为什么block真正的用户数据大块、顺序读写吞吐量型大容量 HDD机械硬盘顺序跑满带宽性价比最高。block.dbRocksDB 的索引表元数据极度随机的小块读写 LSM 树后台合并IOPS 敏感型SSD / NVMe必须用 SSD 的微秒级随机读写来扛住高频查表和后台整理。block.walRocksDB 的预写日志极其频繁的顺序追加写 刷盘延迟极度敏感型最快的 NVMe SSD客户端在等 ACK每慢 0.1ms 都会累积成巨大延迟必须用最快的盘。❓灵魂拷问 5元数据适合用什么数据库来存我们写 C 的天然想到 LevelDB 或 RocksDB。它们是基于LSM 树的 KV 数据库。LSM 树有个神仙优点写入极快只做追加写并且天然按 Key 排序。想到这里你该兴奋了以前 FileStore 要列举一个目录下的 10 万个对象需要遍历 XFS 目录并排序O(n log n)慢死了现在只要问 RocksDB“给我列出 Key 前缀为obj的所有项”RocksDB 因为存的时候就是排好序的瞬间就能返回O(log n)。4 死循环难题 —— “鸡生蛋蛋生鸡肉”❓灵魂拷问 6RocksDB本身就是一个嵌入式数据库它也需要在硬盘上存文件(.sst和WAL日志)才能持久化。而我们把XFS砍掉了Rocksdb不知道该调用哪个函数去创建自己的文件怎么办 我们的第三个伟大决策BlueStore 最秀的操作骗过 RocksDB我们写一个极小的“文件系统”起名BlueFS。这个BlueFS什么都不干它只实现了Rocksdb需要的open、write、mkdir这几个极简的API当Rocksdb说“我要创建log.sst”文件时BlueFS 说“好的你稍等。”然后 BlueFS 偷偷转过头调用我们底层管理硬盘的代码Allocatorceph自身实现的在硬盘上划出一块空间告诉 RocksDB“文件创建好了你的数据尽管往这个地址写”本质BlueFS 就是个“翻译官”兼“傀儡”它让 RocksDB 以为自己活在 XFS 下实际上数据最终全被我们直接塞进了裸盘。5 原子炸弹 —— “断电如何处理”现在我们有了三层的架构底层老大blockdevice复制直接读写硬盘扇区大脑RocksDB BlueFS负责记地图管家Allocator负责管理哪些扇区空着哪些满了❓灵魂拷问 7如果我正在写一个对象突然服务器断电了。数据只写了一半元数据地图还没来得及更新。重启后数据对不上怎么办以前的FileStore采用的时双重写入解决断电问题。我们现在是裸盘还搞双重写入么那不是又回到原来的问题了不既然元数据存储在了Rocksdb上而Rocksdb本身就有自己的WAL预写日志用来保证ACID。我们解法事务原子性我们把“数据落盘”和“元数据更新”打包成一个事务Transaction。先把用户数据直接写进硬盘的某个位置假设为0X1000然后在rocksdb启动事务把这个记录写进去obj1-0x1000rocksdb会把这条修改先在自己的WAL日志里刷盘再写进自己的SST表关键来了如果第 2 步写完元数据后断电重启后 RocksDB 回放 WAL发现obj1 - 0x1000这条记录在那对应 0x1000 的数据肯定也在因为第 1 步先于第 2 步落盘了。如果第 1 步写完断电第 2 步没写RocksDB 里没有obj1的映射那 0x1000 那块空间就成了垃圾我们回收它就好用户没收到 ACK数据不算数。6 覆盖写之痛 —— “我改了几个字却要复制整个文件”❓灵魂拷问 8用户想覆盖写对象的一个小片段。如果我们在原地0x1000 地址直接覆盖修改万一写到一半断电数据就损坏了。而且原地修改非常慢。能不能不覆盖对于覆盖写业界有两种解决的方案写时复制COW这是一种“空间换时间”的策略。当需要修改数据时不直接在原位置修改而是先在磁盘的新位置写入修改后的完整数据然后通过更新元数据指针将对象指向新的数据位置优点将随机写改为了顺序写性能高天然支持快照缺点可能导致磁盘空间碎片化读-改-写RMW这是一种“就地更新”In-Place Update的策略。当需要修改数据时必须先把整个数据块Chunk从磁盘读出来在内存中修改目标部分然后再将整个修改后的数据块写回原位置。优点数据在磁盘上保持连续空间利用率高。缺点引入了额外的读操作写放大明显性能开销大了解上面的内容之后我们发现无论是COW或是RMW都有缺点因此Bluestore采用了分而治之的混合策略在性能和空间利用率之间取得最佳平衡。它决策的核心依据是写入操作是否与“最小分配单元”min_alloc_sizeSSD通常为4KBHDD为64KB对齐任何一个写请求根据磁盘块大小将其切分为三个部分即首尾非块大小对齐部分和中间块大小对齐部分然后针对中间块对齐部分采用COW策略首尾非块对齐部分采用RMW策略。考虑到RMW策略存在数据损坏的隐患还需要针对这类操作引人日志即将对应的数据先写入日志盘后才去真正更新数据盘数据盘更新完成之后才能释放日志我们可以根据官方文档BlueStore Internals中的描述将这个策略总结如下写入场景采用策略内部代号说明全量写入新对象直接写入U直接分配新空间并写入。覆盖写块大小对齐COW重定向写W首选方案。写入大小正好是min_alloc_size的整数倍且对齐。直接在新位置写入性能最佳。覆盖写小于块大小RMWRW或X写入大小不足一个min_alloc_size。必须读取整个块修改后再写回。部分写入现有对象COW 或 RMWP或W写入一个现有对象中未使用的部分。如果该部分与块对齐则可能用W否则可能退化为P。压缩写入直接写入C数据压缩后写入新块。因此ceph对于覆盖写并不是非此即彼的而是“大块、对齐的覆盖写走高效的 COW 路径小块、不对齐的覆盖写走 RMW 路径并通过 WAL 进行延迟优化。”7 性能涡轮 —— “有了这几个问题你就全懂了”走到这里我们已经把 BlueStore 最核心的灵魂设计全“推导”出来了。最后我们再带着问题看一眼它的物理部署你就全通了❓终极拷问 9为什么生产环境建议把block.db和block.wal放在 SSD 上而block数据放在 HDD 上block数据大块数据现在HDD顺序读写能力很强200MB/S以上放HDD不亏blcok.dbRocksdb的SST文件因为LSM数的Compaction合并会产生大量的随机小块读写IO而HDD最怕随机IO所以必须放在SSD上扛住随机IOblock.walRocksDB的日志每次写事务都要线刷WAL日志这是延迟敏感型操作。问谁延迟最低NVMe SSD所以放最快的盘
带着问题去探险:亲手“设计”出 Ceph BlueStore
1 问题初现 —— “文件用得好好的为什么要换” 初始设定我们有一块普通的硬盘想在上面存 Ceph 的对象比如一张图片。操作系统教我们先格式化一个文件系统比如 XFS然后open一个文件把数据write进去。❓灵魂拷问 1直接用操作系统的文件系统存数据有什么不对吗在早期的cephFileStore时代我们就是这么干的。但运行久了发现了几个及其别扭的“拧巴”现象拧巴点一写 4KB 数据磁盘写了两次。为了保证断电不丢数据必须先写一份Journal日志AOF等日志落盘了再回过头修改XFS的文件。问这像不像寄快递时你先在本子上记一笔日志再把包裹塞进快递柜硬盘结果本子和柜子各占一份空间拧巴点二修改文件中间的1个字硬盘忙半天如果你只改一个对象文件中间的 4KB文件系统没法直接覆盖。你必须把整个对象读出来Read改掉那 4KB再把整个对象写回去Modify-Write。问这不就是“为了换灯泡把整个房子拆了重建”吗2 破局之问 —— “我能不能不要中间商”❓灵魂拷问 2既然 XFS 这个“中间商”老在中间赚差价带宽和添乱那如果我直接接管这块硬盘不要文件系统了行不行当然行操作系统本身就允许我们使用O_DIRECT的方式绕过内核的Page Cache直接write到硬盘的某个扇区LBA地址写数据❓灵魂拷问 3没有了XFS的文件系统那么对于一个文件路径/pg1/obj1就没了。当我收到客户端请求“给我对象obj1”时我如何具体的对应到硬盘的哪个扇区偏移量上呢这就需要一张“地图”以前文件系统XFS帮我们维护了这张地图文件名-inode-磁盘块现在如果不使用XFS就必须自己维护这张地图3 分而治之 —— “数据地图存放在哪里”❓灵魂拷问 4既然要维护“对象名 - 磁盘扇区”的映射关系那这张“地图”本身总得找一个地方存吧这里我们必须把“真正的大块数据比如 10GB 的电影”和“记录位置的元数据比如 10MB 的索引”分开。数据Data大块、大流量。我们直接调用write丢进硬盘深处的大块连续空间。元数据Metadata高频、小块、极其重要。但是为什么要分开呢IO 模型不匹配用户数据block是大块顺序 IOHDD 擅长这个但 RocksDB 的元数据是高频随机小块 IO放在 HDD 上会产生大量机械寻道导致性能骤降。避免相互干扰如果混盘元数据的小 IO 会打断数据的大 IO 流磁头来回摆动整体吞吐量会损失 50% 以上还会引发元数据锁的队头阻塞。成本最优解用 90% 成本的廉价 HDD 存海量数据用 10% 成本的高性能 SSD 存敏感元数据以最低的经济代价撬动最高的集群性能。这本质就是分布式存储里的‘冷热分层’思想。”总体来说就是HDD读取速度慢但是成本低SSD读取速度快但是成本高。让“大块吃肉顺序读”和“小块喝汤随机写”互不打扰各自安好。因此Ceph的bluestore把一块物理盘从逻辑上或物理上拆成了三条路组件名部署目录存的是什么IO 特性是什么该用什么盘为什么block真正的用户数据大块、顺序读写吞吐量型大容量 HDD机械硬盘顺序跑满带宽性价比最高。block.dbRocksDB 的索引表元数据极度随机的小块读写 LSM 树后台合并IOPS 敏感型SSD / NVMe必须用 SSD 的微秒级随机读写来扛住高频查表和后台整理。block.walRocksDB 的预写日志极其频繁的顺序追加写 刷盘延迟极度敏感型最快的 NVMe SSD客户端在等 ACK每慢 0.1ms 都会累积成巨大延迟必须用最快的盘。❓灵魂拷问 5元数据适合用什么数据库来存我们写 C 的天然想到 LevelDB 或 RocksDB。它们是基于LSM 树的 KV 数据库。LSM 树有个神仙优点写入极快只做追加写并且天然按 Key 排序。想到这里你该兴奋了以前 FileStore 要列举一个目录下的 10 万个对象需要遍历 XFS 目录并排序O(n log n)慢死了现在只要问 RocksDB“给我列出 Key 前缀为obj的所有项”RocksDB 因为存的时候就是排好序的瞬间就能返回O(log n)。4 死循环难题 —— “鸡生蛋蛋生鸡肉”❓灵魂拷问 6RocksDB本身就是一个嵌入式数据库它也需要在硬盘上存文件(.sst和WAL日志)才能持久化。而我们把XFS砍掉了Rocksdb不知道该调用哪个函数去创建自己的文件怎么办 我们的第三个伟大决策BlueStore 最秀的操作骗过 RocksDB我们写一个极小的“文件系统”起名BlueFS。这个BlueFS什么都不干它只实现了Rocksdb需要的open、write、mkdir这几个极简的API当Rocksdb说“我要创建log.sst”文件时BlueFS 说“好的你稍等。”然后 BlueFS 偷偷转过头调用我们底层管理硬盘的代码Allocatorceph自身实现的在硬盘上划出一块空间告诉 RocksDB“文件创建好了你的数据尽管往这个地址写”本质BlueFS 就是个“翻译官”兼“傀儡”它让 RocksDB 以为自己活在 XFS 下实际上数据最终全被我们直接塞进了裸盘。5 原子炸弹 —— “断电如何处理”现在我们有了三层的架构底层老大blockdevice复制直接读写硬盘扇区大脑RocksDB BlueFS负责记地图管家Allocator负责管理哪些扇区空着哪些满了❓灵魂拷问 7如果我正在写一个对象突然服务器断电了。数据只写了一半元数据地图还没来得及更新。重启后数据对不上怎么办以前的FileStore采用的时双重写入解决断电问题。我们现在是裸盘还搞双重写入么那不是又回到原来的问题了不既然元数据存储在了Rocksdb上而Rocksdb本身就有自己的WAL预写日志用来保证ACID。我们解法事务原子性我们把“数据落盘”和“元数据更新”打包成一个事务Transaction。先把用户数据直接写进硬盘的某个位置假设为0X1000然后在rocksdb启动事务把这个记录写进去obj1-0x1000rocksdb会把这条修改先在自己的WAL日志里刷盘再写进自己的SST表关键来了如果第 2 步写完元数据后断电重启后 RocksDB 回放 WAL发现obj1 - 0x1000这条记录在那对应 0x1000 的数据肯定也在因为第 1 步先于第 2 步落盘了。如果第 1 步写完断电第 2 步没写RocksDB 里没有obj1的映射那 0x1000 那块空间就成了垃圾我们回收它就好用户没收到 ACK数据不算数。6 覆盖写之痛 —— “我改了几个字却要复制整个文件”❓灵魂拷问 8用户想覆盖写对象的一个小片段。如果我们在原地0x1000 地址直接覆盖修改万一写到一半断电数据就损坏了。而且原地修改非常慢。能不能不覆盖对于覆盖写业界有两种解决的方案写时复制COW这是一种“空间换时间”的策略。当需要修改数据时不直接在原位置修改而是先在磁盘的新位置写入修改后的完整数据然后通过更新元数据指针将对象指向新的数据位置优点将随机写改为了顺序写性能高天然支持快照缺点可能导致磁盘空间碎片化读-改-写RMW这是一种“就地更新”In-Place Update的策略。当需要修改数据时必须先把整个数据块Chunk从磁盘读出来在内存中修改目标部分然后再将整个修改后的数据块写回原位置。优点数据在磁盘上保持连续空间利用率高。缺点引入了额外的读操作写放大明显性能开销大了解上面的内容之后我们发现无论是COW或是RMW都有缺点因此Bluestore采用了分而治之的混合策略在性能和空间利用率之间取得最佳平衡。它决策的核心依据是写入操作是否与“最小分配单元”min_alloc_sizeSSD通常为4KBHDD为64KB对齐任何一个写请求根据磁盘块大小将其切分为三个部分即首尾非块大小对齐部分和中间块大小对齐部分然后针对中间块对齐部分采用COW策略首尾非块对齐部分采用RMW策略。考虑到RMW策略存在数据损坏的隐患还需要针对这类操作引人日志即将对应的数据先写入日志盘后才去真正更新数据盘数据盘更新完成之后才能释放日志我们可以根据官方文档BlueStore Internals中的描述将这个策略总结如下写入场景采用策略内部代号说明全量写入新对象直接写入U直接分配新空间并写入。覆盖写块大小对齐COW重定向写W首选方案。写入大小正好是min_alloc_size的整数倍且对齐。直接在新位置写入性能最佳。覆盖写小于块大小RMWRW或X写入大小不足一个min_alloc_size。必须读取整个块修改后再写回。部分写入现有对象COW 或 RMWP或W写入一个现有对象中未使用的部分。如果该部分与块对齐则可能用W否则可能退化为P。压缩写入直接写入C数据压缩后写入新块。因此ceph对于覆盖写并不是非此即彼的而是“大块、对齐的覆盖写走高效的 COW 路径小块、不对齐的覆盖写走 RMW 路径并通过 WAL 进行延迟优化。”7 性能涡轮 —— “有了这几个问题你就全懂了”走到这里我们已经把 BlueStore 最核心的灵魂设计全“推导”出来了。最后我们再带着问题看一眼它的物理部署你就全通了❓终极拷问 9为什么生产环境建议把block.db和block.wal放在 SSD 上而block数据放在 HDD 上block数据大块数据现在HDD顺序读写能力很强200MB/S以上放HDD不亏blcok.dbRocksdb的SST文件因为LSM数的Compaction合并会产生大量的随机小块读写IO而HDD最怕随机IO所以必须放在SSD上扛住随机IOblock.walRocksDB的日志每次写事务都要线刷WAL日志这是延迟敏感型操作。问谁延迟最低NVMe SSD所以放最快的盘