跳转到: 导航, 搜索

Swift/ideas/small files/experimentations

< Swift‎ | ideas‎ | small files

Swift 中小文件优化实验

(irc: rledisez)

注意:尽管部署指南有官方建议(以及性能影响),但在 OVH,我们使用具有 barrier=on 设置的 XFS 文件系统运行 Swift。因此,本摘要主要关注同步性能。性能数据是针对 4TB SAS 磁盘、C2750 CPU、16GB 内存给出的。“受限内存环境”意味着有意消耗 12GB 内存,以将可用内存减少到 4GB。

目标

  • 减少读取文件系统元数据(inodes)所需的 IO
  • 至少保持 XFS 的性能
    • 对象创建:43/s
    • 在内存受限环境中的对象读取:40/s

约束

  • 并发性:许多进程需要访问小文件存储(对象服务器、审计器、复制器、...)
  • 分配:不能浪费太多空间(节省空间不是目标,但会很好)
  • 完整性:不能出现数据损坏或存储损坏

想法

似乎满足并发性的简单方法是提供一个服务 RPC 来处理数据请求。有一些有趣的 RPC 协议允许在无需复制数据的条件下进行通信(例如:cap'n proto)。此外,作为副作用,RPC 服务器会将阻塞 IO 调用转换为非阻塞 RPC 调用(对于 python 来说很好:))

由于我们需要一个索引,我们应该通过它们的哈希值将所有对象存储在一个扁平的命名空间中。这将允许在无需操作的情况下增加或减少分区的能力,只需更新环即可。通过正确的结构,范围扫描非常高效,并允许模拟用于复制目的的分区。

尝试

开发一个文件系统

我们首先尝试开发一个文件系统,运行在 XFS 之上(以便受益于 VFS 层完成的所有缓存)。规范如下

  • 仅连续分配:没有碎片,因此我们不需要碎片整理或压缩逻辑
  • 将所有必要信息保存在索引中,以便运行审计器/复制器/重构器:无需访问数据即可执行 os.listdir()
  • 小巧的索引,以便它可以适合内存:可实现的目标似乎是每个对象 50 到 60 字节(更倾向于消耗 CPU 以节省内存)

虽然我们在几天内获得了一个可用的 POC,但将 POC 转化为生产环境(=可靠性、性能)所需的工作量被认为巨大,因此我们尝试寻找其他解决方案。

使用一个成熟的键值存储

想法是

  • 对于小文件:将数据存储在键值存储中
  • 对于较大的文件:将文件的文件句柄存储在键值存储中


存储文件句柄可以节省许多 IO,因为它变得不必要去执行所有通常的查找来“访问”一个文件(在读取文件 inode 之前读取所有父目录的 inodes)。文件句柄有一个缺点:它们绕过所有安全检查,因为文件直接访问,而无需检查父目录。因此,如果 Swift 进程没有在安全的环境中运行(例如:在 Docker 内部,在运行多个服务的服务器上),则应该可以禁用文件句柄的使用。

存储文件句柄还可以打开一扇门,摆脱当前的文件层次结构(part/sfx/hash/ts),从而极大地帮助修改分区能力。因为如果真实信息在数据库中,大文件在 XFS 中存储的方式不再重要,因此不再需要 hardlink & co 来更改分区能力。

尝试的键值存储是

  • kyotocabinet:同步模式下的性能不可接受
  • boltdb:随机插入性能差,随着数据库的增长而下降
  • lmdb:与 boltdb 相同
  • leveldb:随机插入性能差
  • forestdb:随机插入性能差
  • rocksdb:随机插入性能好,约为 XFS 的 1.8 倍,磁盘开销低(实际上与 XFS 相比节省了空间)。但使用一个基本示例(文档中的示例)很容易损坏。项目似乎还很年轻,不太成熟(github 上有最近的损坏报告)。


此解决方案的优点

  • 开发小,实现起来非常简单(一个键值存储前面的 RPC)
  • 不需要“增加分区能力”逻辑(扁平命名空间)
  • 双重查找(db+fs)的影响应该通过文件句柄来节省(TODO:需要进行基准测试)


缺点

  • 没有找到任何快速/可靠的项目


键值存储 + 事务组

想法是使用键值存储(参见前一点),但不是单独提交每个对象,而是将它们分组到一个事务组中(受 ZFS 事务组的启发),并每 N 毫秒一起提交它们。这可以轻松地使每秒的对象创建数量翻倍。缺点是,为了保持同步行为,我们必须在向客户端回复之前等待事务提交。因此,客户端可能需要等待最多 N 毫秒才能验证上传。10ms 在每秒创建对象方面提供了良好的结果。


使用 ZFS 的 DMU

Lustre 团队开发了一个基于 ZFS 的 OSD。不是 ZFS 作为文件系统(ZPL),而是作为对象存储(DMU)。这是一种有趣的方法,因为他们受益于 ZFS DMU 的所有很棒的功能(写时复制、事务、快照、...),但他们没有获得 ZPL 的开销(inodes)。想法是将数据写入 DMU 对象,并通过标识符(例如:哈希值)在 ZAP 中索引该对象。

围绕 ZFS 代码进行开发被证明非常容易,几天内就完成了可用的代码。

优点是

  • 无需证明,ZFS 非常可靠(这是事实:))
  • 随机写入性能非常好(与 XFS 相比,同步模式下约为 2.5 倍,异步模式下约为 16 倍)
  • 在受限内存环境中的随机读取性能略好于 XFS。它可能在 ZAP 替换后会更好(参见缺点)
  • 它被维护和激活,没有理由认为将来不会如此
  • 可以运行在 XFS 文件中的文件之上(性能损失约 5%),也可以直接在设备上运行,因此可以轻松地迁移路径
  • 可以在 Swift 中将来使用的很酷的功能(zfs send 用于复制?参见 https://www.ixsystems.com/blog/openzfs-devsummit-2016/ “Redacted send/receive” 和 “Compressed Send and Receive”)


缺点是

  • ZFS 代码非常底层(即使它在用户空间中运行),用 C 开发。即使它清晰且写得很好,也需要付出一些努力才能完全正确地理解它。
  • (已修复) 在 golang/cgo 中运行时出现了一些不稳定性,而使用 C 时则完全稳定(todo:尝试 runtime.LockOSThread())
  • ZAP 未针对我们的用例进行优化,一条记录约为 258 字节,而我们真正需要的只有大约 50-60 字节。它也不允许范围扫描。因此,我们最终可能会开发 ZAP 的替代品,即 DMU 之上的某种类型的 B 树或等效树(除非我们找到与我们的需求兼容的东西,也许 https://github.com/timtadh/fs2?)