开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情
在转换系统时,有时您必须搭建一个小脚手架。在 Instagram,我们最近不得不这样做:出于遗留原因,我们需要将大约 3 亿张照片映射回创建它们的用户 ID,以便知道要查询哪个分片(请参阅有关我们的更多信息分片设置)。虽然最终所有客户端和 API 应用程序都会更新以向我们传递完整信息,但仍有很多人缓存了旧信息。我们需要一个解决方案:
- 非常快速地查找键和返回值
- 将数据放入内存中,最好是在 EC2 高内存类型之一(17GB 或 34GB,而不是 68GB 实例类型)中
- 很好地融入我们现有的基础设施
- 坚持不懈,这样我们就不必在服务器死机时重新填充它
这个问题的一个简单解决方案是将它们简单地存储为数据库中的一堆行,其中包含“媒体 ID”和“用户 ID”列。然而,考虑到这些 ID 从未更新(仅插入)、不需要事务性并且与其他表没有任何关系,SQL 数据库似乎有点过分了。
相反,我们求助于Redis,这是一种我们在 Instagram 广泛使用的高级键值存储(例如,它为我们的主要提要提供支持)。Redis 是一把键值瑞士军刀;它不像 Memcached 那样只是普通的“设置键,获取键”机制,它提供了强大的聚合类型,如排序集和列表。它有一个可配置的持久性模型,它以指定的时间间隔在后台保存,并且可以在主从设置中运行。我们所有的 Redis 部署都以主从方式运行,从属设置为大约每分钟保存一次到磁盘。
起初,我们决定尽可能以最简单的方式使用 Redis:对于每个 ID,键是媒体 ID,值是用户 ID:
SET media:1155315 939
GET media:1155315
> 939
然而,在设计这个解决方案的原型时,我们发现 Redis 需要大约 70 MB 才能以这种方式存储 1,000,000 个键。推断出我们最终需要的 300,000,000 个,它看起来大约有 21GB 的数据价值——已经大于 Amazon EC2 上的 17GB 实例类型。
我们向 Redis 的核心开发人员之一、总是乐于助人的Pieter Noordhuis询问了意见,他建议我们使用 Redis 哈希。Redis 中的哈希是可以非常有效地在内存中编码的字典;Redis 设置“hash-zipmap-max-entries”配置了一个哈希可以拥有的最大条目数,同时仍然被有效编码。我们发现此设置最好在 1000 左右;任何更高的 HSET 命令都会导致明显的 CPU 活动。更多详细信息,您可以查看 zipmap 源文件。
为了利用哈希类型,我们将所有媒体 ID 放入 1000 个桶中(我们只取 ID,除以 1000,然后丢弃余数)。这决定了我们落入哪个键;接下来,在位于该键的散列中,媒体 ID 是查找键在散列中,用户 ID 是值。举个例子,假设媒体 ID 为 1155315,这意味着它落入桶 1155 (1155315 / 1000 = 1155):
HSET "mediabucket:1155" "1155315" "939"
HGET "mediabucket:1155" "1155315"
> "939"
尺寸差异非常显着;使用我们的 1,000,000 个密钥原型(编码为 1,000 个散列,每个散列有 1,000 个子密钥),Redis 只需要 16MB 来存储信息。扩展到 3 亿个密钥,总数略低于 5GB — 事实上,这甚至适合亚马逊上便宜得多的 m1.large 实例类型,大约是我们原本需要的更大实例成本的 1/3。最重要的是,哈希中的查找仍然是 O(1),这使得它们非常快。