Netflix 发布了 基于Java的通用缓存项目Hollow来缓存元数据。
在数据保存到集中式存储时,常常使用缓存技术去突破网络延迟、带宽等限制。在缓存数据的过程中,开发者经常要面对一个挑战,就是如何缓存频繁访问的本地数据和保存很少访问的远程数据。但如果能缓存所有需要访问的数据,那么实现我们的应用会更容易。Hollow采用了将所有数据提供给每一个消费者的方法,而不是将数据分布到多台服务器上,不得不为数据的持久性而考虑用什么算法去复制。
Hollow的前身是 Zeno ,它把数据作为普通Java对象(POJO)来保存,导致存储空间受到了限制。Hollow则采用了一种紧凑、固定长度、强类型的数据编码方式避免了这个问题。
InfoQ采访了Netflix的高级软件工程师和主要贡献者Drew Koszewnik,向他请教了这个项目中的一些具体细节。
Drew Koszewnik:Memcached做得很好,在Netflix我们曾经用 EVCache 得到了很大收益,它可以适用于Hollow将要面对的缓存数据尺寸等问题。另一方面,Hollow在别的一些问题上比Memcached解决得更有效,特别是在跨服务器编队中如何复制整个数据集。虽然在很多情况下,工程师们对这样做是否可行持怀疑态度,但其实会发现它的表现比预期更好。你可以认为Hollow是一种把你的数据集压缩到内存的方法,还能对它的任何部分提供O(1)的访问速度。
Memcached的主要好处是它是一个分布式缓存系统,通过很多分布式的实例来形成整个内存缓存池,但它仍然是一个中心化的系统。而我们认为Hollow不是一个真正意义上的分布式缓存系统,它其实是一个弥散性的系统。我们选用弥散性这个单词(分布式的反义词)来表示把整个数据集复制给每一个消费者。
Hollow是为了满足Netflix对高吞吐量缓存的需求而创建的,大多数的设计决策最终都源自这个要求。对每一个消费者交互复制整个数据集意味着我们不必通过网络跳转来获取任何数据,所以访问数据集的任何部分所需的延迟是可以忽略不计的。对整个数据集范围内任何数据部分的访问所产生的CPU成本,Hollow也非常关心。和Memcached那样松散类型的键/值存储不同,Hollow采用强类型和结构化数据模型,这样可以定位消耗在数据访问上的成本问题,并在许多不同的场景中实现重用。我们选择去构建一个强类型的系统,先理解数据结构再建立各种工具。 History 和 diff 就是其中两个很好的工具,因为我们清楚数据模型,就能解释不同的两个状态之间究竟有什么变化,并围绕这些变化构建一些真正有用和通用性的应用界面元素。
顺理成章,有了完全本地结构化的数据后,在设计数据模型时你不必去考虑到底数据会被怎么使用。而在Memcached系统中,你不得不提前了解消费者会如何使用这些数据(它们只能利用预定义的键值来查询)。而在Hollow中,消费者团队可以自由地开发数据的访问模式,生产者团队也不会因此成为开发瓶颈。Hollow在消费者端采用 索引 而不必在生产者端生成预定义索引,通过这种方式实现了访问模式的解耦。
Koszewnik:是的,它适用于只读数据、单个生产者、可能多个消费者的情形。Hollow实现持久化的唯一机制是利用 BLOB存储 ,它只是一个文件存储,可能是S3、NFS、甚至一台FTP服务器。启动的时候,消费者们读取整个数据集的快照,并将数据集引导到内存中。通过增量的方法,可以保证内存中的数据集是最新的。对于每个消费者,内存中的数据备份是临时性的,如果消费者重新启动,需要从BLOB存储中重新加载快照。
实际上BLOB快照文件的格式很简单,它在很大程度上和在内存布局的结构相同,因此数据初始化主要是将BLOB的内容直接复制到内存中,这一步可以快速完成,确保了初始化的时间很短。
Koszewnik:因为Hollow不是传统意义上的分布式存储,针对这个网络连接的问题对于CAP定理会有一些不同的考虑。我想你可以认为,Hollow通过牺牲一致性来满足了可用性和分区容错性。
由于消费者都在RAM中保留了一份数据集的全拷贝,所以可用性是非常好的,一旦消费者完成初始化,完全不会受到由于环境不同而带来的问题所影响。至于分区容错性,它没有任何分区,对于每个消费者而言要么拥有整个数据,要么就没有。
一致性是比较有趣的话题,Hollow将随时间而变化的数据集切割成离散的数据状态,每一个部分是特定时间点的数据快照。按照有规律的节奏,可以每隔几分钟生成这些快照。因此,必须要在一定程度上容忍数据集的传播延迟。你可以采取一些别的措施来缓解这个问题,例如,通过单独的渠道发送紧急更新来覆盖数据。
Koszewnik:对,Hollow主要针对中小型数据集。根据经验,MB到GB的数据规模比较合适,TB或者PB就有点问题了。在因为一个数据集对于Hollow来说过大而被排除掉之前,不妨再考虑下,一个接近1TB的基于JSON的文档如果存在Hollow中会节省很多的空间。Hollow的超高效存储性能结合一个优化后的数据模型可以把大的数据集转变成中等的。
正如Google的Jeff Dean所教导的,系统设计应该考虑数据增长性,确保它能在10倍或20倍数据增长情况下正常工作,暂时还不需要考虑100倍时的情况。这里Hollow的规则是,面对不断增长的数据集,提供一些工具对堆栈使用率进行 细粒度分析 ,从而识别出要优化的目标。把分析出的指标按照时间做比较,这对于在早期发现潜在问题是非常有用的。特别要留意如果某个类型的数据出现了超线性的增长,也许就是数据模型设计得不够优化的信号,或者是新的增长维度被忽略了。
数据的建模在这里非常重要,数据结构化方法对Hollow删除重复数据的效果有着巨大影响,也决定了能压缩多少数据。一种通用的压缩算法是利用数据的特性来构造Huffman树,而Hollow是用 数据模型 里的 记录结构 作为 数据特性 。虽然这样做需要事先知道记录的结构,但关键是保持了随机访问的高吞吐量,同时实现了很好的压缩率。
Koszewnik:Hollow通过很多不同的方式实现压缩,其中包括去重、编码、打包、释放Java对象。虽然Zeno提供了去掉重复数据的方法,但如果不抛弃POJO,我们也许就不能实现编码、打包、对象释放了。
再来说说性能,我们需要一种手段,以便在访问Hollow数据集时既能对CPU影响最小,又能让数据显示所需的堆占用最小。在 数据布局 时,我们在数据末尾用了一个固定长度的位对齐,这样每个字段占用所有记录最大值所需的字节数。然后,利用非对齐内存访问等技术来最小化检索数据所需的指令数。如果这样优化,Hollow就可以支持x86-64框架,该框架目前要求用低字节序来排序,并且假定非对齐访问不会导致(或可以忽略)性能损失。在未来,会利用这些优化方案来实现更大的可移植性。
Koszewnik:我们用Java来实现Hollow,因为我们每一个客户系统都用基于JVM的开发语言来建立它们的视频元数据,而基于non-JVM的语言目前还不能利用Hollow。
Koszewnik:不,Hollow没有提供或者特指什么架构,只是把产生的BLOB从生产者扩散到消费者。如果潜在的用户开始阅读 快速启动 指南,它介绍了如何嵌入基于AWS的S3/DynamoDB架构,也用一个项目示例介绍了如何实现可伸缩性的。不过相对容易点的,是把这些例子当成指南,学会在任何架构下实现 Publisher 、 Announcer 、 HollowBlobRetriever 、 HollowAnnouncementWatcher 。
而对于不同的数据模型来说,它们各自的基准测试有很大的不同,所以我尽量避免下什么结论,因为用例的不同会误导出另一个方向的结论。可以下的结论,只是相比较POJO而言,Hollow使用记录来访问数据,以极小的代价大大的减少了堆占用。
也没有严格的发展路线图,虽然有几件事我真的很希望能去解决,但从发展的角度看更希望能从我们的 社区 得到一些反馈。特别希望通过我们的努力,这个项目能让可用性得到极大的改进。
Hollow的 github 网站提供了更多信息以帮助如何开始学习。
查看英文原文: Q&A with Drew Koszewnik on a Disseminated Cache,Netflix Hollow
感谢冬雨对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina )关注我们。