原始资料

Haystack 的设计发表在 OSDI 上,论文可以从 usenix.org 获取。此外还有一些开源的 Golang 实现可以参考,例如 seaweedfs,思路相近。

动机:NFS 撑不住海量小图片

facebook 原来的方案是把图片存在 NFS 上。这个方案在图片数量增长后暴露出两个问题。

问题一:元数据太大,无法全部缓存

由于细小的文件太多,NFS 的元数据比较大(论文中描述为 which is hundreds of bytes large),导致无法把元数据都缓存到内存中。这意味着每次读一张图片都可能要访问磁盘获取元数据。

问题二:一次读图至少三次磁盘 IO

在 NFS 的方案下,每次要读取一个图片,至少需要 3 次磁盘 IO:

  1. 第一次读取目录元信息;
  2. 第二次读取 inode;
  3. 第三次读取图片数据本身。

对于一次图片请求来说,这个放大倍数相当高。当请求量达到海量规模时,磁盘 IO 就成为瓶颈。

长尾请求的挑战

另一个关键因素是访问分布。对于 facebook 的图片请求,存在很多长尾请求,也就是很长时间以来都很冷门的图片——个人相册本身就是非热门数据。

这会带来一个直接后果:很多请求既不能命中 CDN,也不能命中自身的 cache,最终直接落到磁盘上。也就是说,缓存层无法像处理热点数据那样把这些请求拦住,磁盘必须有能力直接承受这部分压力。

这一点对存储系统的设计影响很大:如果缓存命中率足够高,磁盘层的性能要求可以放宽;但当长尾请求占比可观时,磁盘层必须自己就足够高效。

于是有了 Haystack

综合上面两个问题,可以看出 Haystack 要解决的核心矛盾是:用为通用场景设计的文件系统去存海量小文件,元数据开销与 IO 次数都被放大到了不可接受的程度

对应的设计方向也就清晰了:

  • 把元数据从文件系统里拿出来:不再依赖目录项与 inode 来定位一张图片,从而免去前置的元数据磁盘访问;
  • 减少一次读图的 IO 次数:理想情况下一次磁盘寻址就能取到需要的图片数据;
  • 针对长尾访问优化:让磁盘层本身足够高效,而不寄希望于缓存兜住所有请求;
  • 为只读为主的图片场景定制:图片写入一次、读取极多次,这个访问特征允许设计上做大量取舍。

对今天的启示

Haystack 虽然是十多年前的设计,但它提出的问题至今仍然成立,而且在小文件存储场景中反复出现:

  1. 元数据与数据分离是海量小文件存储的基本手段,把定位信息集中管理,避免每读一个对象都要多次元数据 IO;
  2. IO 放大是核心指标,评估存储方案时应计算一次逻辑读取实际产生多少次物理 IO,而不是只看吞吐数字;
  3. 访问分布决定架构,长尾占比高意味着缓存策略的有效性有限,必须正视磁盘层的直接性能;
  4. 面向访问模式优化,通用方案往往在特定负载下效率不高,针对读多写少等特征做定制能带来数量级的改善。

理解这些动机,比记住具体实现细节更有价值。因为当你在自己的系统里遇到「小文件太多、元数据撑不住」时,思路是可以直接复用的。

NFS 方案的主要瓶颈
元数据体积海量小文件使元数据达到数百字节量级,无法全部缓存进内存
IO 次数读取一张图片至少需要目录元信息、inode、数据三次磁盘 IO
长尾访问冷门图片请求难以命中 CDN 与本地缓存,压力直接落到磁盘
访问特征图片写入一次但读取极多次,属于典型读多写少场景
设计方向元数据与数据分离、压缩 IO 次数、针对长尾与只读特征做定制