# Twitter 如何使用 Redis 进行扩展-105TB RAM，39MM QPS，10,000 多个实例

> 原文： [http://highscalability.com/blog/2014/9/8/how-twitter-uses-redis-to-scale-105tb-ram-39mm-qps-10000-ins.html](http://highscalability.com/blog/2014/9/8/how-twitter-uses-redis-to-scale-105tb-ram-39mm-qps-10000-ins.html)

<iframe align="right" allowfullscreen="" frameborder="0" height="200" src="//www.youtube.com/embed/rP9EKvWt0zo?rel=0" width="220"></iframe>[姚悦](https://twitter.com/thinkingfish)自 2010 年以来一直在 Twitter 的 Cache 团队工作。 当然，这是关于 Redis 的，但不仅是关于 Redis 的。

姚明已经在 Twitter 工作了几年。 她看过一些东西。 她看到 Twitter 上缓存服务的增长从仅由一个项目使用到迅速增加到将近 100 个项目。 那就是成千上万台机器，许多集群和数 TB 的 RAM。

从她的讲话中可以很明显地看出，她来自一个真正的个人经历的地方，并且以一种探索问题的实用方式闪耀出来。 这个话题值得一看。

如您所料，Twitter 具有大量缓存。

Timeline Service for one datacenter using Hybrid List:

*   约 40TB 分配的堆
*   〜30MM qps
*   > 6,000 个实例

Use of BTree in one datacenter:

*   约 65TB 的已分配堆
*   〜9MM qps
*   > 4,000 个实例

稍后，您将了解有关 BTree 和 Hybrid List 的更多信息。

有几点值得注意：

*   Redis 是一个绝妙的主意，因为它占用服务器上未充分利用的资源并将其转变为有价值的服务。
*   Twitter 对 Redis 进行了专门化，提供了两种完全适合其用例的新数据类型。 因此他们获得了所需的性能，但是将它们锁定在基于旧代码的代码中，因此很难合并到新功能中。 我想知道，为什么要使用 Redis 这样的东西？ 只需使用您自己的数据结构创建时间轴服务即可。 Redis 真的为聚会增添了什么吗？
*   在网络饱和之前，请使用本地 CPU 功能汇总节点上的大量日志数据。
*   如果您想要高性能的产品，请将快速路径（即数据路径）与慢速路径（即命令和控制路径）分开，
*   Twitter 正在以 Mesos 作为作业调度程序进入容器环境。 这仍然是一种新方法，因此了解它的工作方式很有趣。 一个问题是 Mesos 浪费问题，该问题源于在复杂的运行时世界中指定硬资源使用限制的要求。
*   中央集群管理器对于使集群保持易于理解的状态非常重要。
*   JVM 慢，C 快。 他们的缓存代理层正在回到 C / C ++。

考虑到这一点，让我们进一步了解 Twitter 上 Redis 的用法：

## 为什么要使用 Redis？

*   Redis 驱动着 Twitter 最重要的服务时间轴。 时间轴是由 ID 索引的推文索引。 在列表中将推文链接在一起会生成“主页时间轴”。 用户时间轴（由用户已发布的推文组成）只是另一个列表。

*   为什么考虑使用 Redis 而不是 Memcache？ 网络带宽问题和长通用前缀问题。

*   网络带宽问题。

    *   Memcache 在时间表上不如 Redis 正常工作。 问题在于处理扇出。

    *   Twitter 读取和写入**的次数是递增的**，它们很小，但时间表本身却很大。

    *   生成推文时，需要将其写入所有相关的时间轴。 推文是一小块数据，附加到某些数据结构。 阅读时，最好加载一小部分推文。 向下滚动会加载另一个批次。

    *   本地时间行可能比较大，这对于观看者一组阅读是合理的。 例如，也许有 3000 个条目。 由于性能原因，应避免访问数据库。

    *   在大对象（时间轴）上进行增量写入和小型读取的读取-修改-写入周期太昂贵，并且会造成网络瓶颈。

    *   在每秒 100K +的千兆链接读写中，如果平均对象大小大于 1K，则网络将成为瓶颈。

*   长通用前缀问题（确实是两个问题）

    *   灵活的架构方法用于数据格式。 对象具有可能存在或可能不存在的某些属性。 可以为每个单独的属性创建一个单独的键。 这要求针对每个单独的属性发出单独的请求，并且并非所有属性都可以在缓存中。

    *   随时间推移观察到的度量标准具有相同的名称，每个样本具有不同的时间戳。 如果单独存储每个度量标准，则长公共前缀会被存储很多次。

    *   为了在两种情况下都更加节省空间，对于指标和灵活的架构，希望有一个分层的密钥空间。

*   **下的专用缓存群集 利用 CPU** 。 对于简单的情况，内存中的键值存储是 CPU 轻的。 对于较小的键值，一个盒子上 1％的 CPU 时间可以每秒处理超过 1K 个请求。 尽管对于不同的数据结构，结果可能有所不同。

*   **Redis 是个绝妙的主意** 。 它可以看到服务器可以做什么，但不能做什么。 对于简单的键值存储，服务器端在 Redis 等服务上有很多 CPU 余量。

*   Redis 于 2010 年在 Twitter 中首次用于时间轴服务。 广告服务中也使用它。

*   未使用 Redis 的磁盘功能。 部分原因是因为在 Twitter 内部，缓存和存储服务位于不同的团队中，因此他们使用他们认为最佳的任何机制。 部分原因可能是因为存储团队认为其他服务比 Redis 更适合他们的目标。

*   Twitter 分叉了 Redis 2.4 并为其添加了一些功能，因此它们停留在 2.4（2.8.14 是最新的稳定版本）上。 更改为：Redis 中的两个数据结构功能； 内部集群管理功能； 内部日志记录和数据洞察力。

*   热键是一个问题，因此它们正在构建具有客户端缓存的分层缓存解决方案，该解决方案将自动缓存热键。

## 混合列表

*   为**增加了可预测的内存性能**，为 Redis 添加了 Hybrid List。

*   时间轴是 Tweet ID 的列表，因此是整数列表。 每个 ID 都很小。

*   Redis 支持两种列表类型：ziplist 和 linklist。 Ziplist 节省空间。 链表是灵活的，但是由于双链表每个键都有两个指针的开销，因此给定 ID 的大小开销非常大。

*   为了有效利用内存，仅使用 ziplist。

*   Redis ziplist 阈值设置为时间轴的最大大小。 永远不要存储比压缩列表中更大的时间轴。 这意味着产品决策（时间轴中可以包含多少条推文）已链接到低级组件（Redis）。 通常不理想。

*   添加或删除 ziplist 效率低下，尤其是列表很大时。 从 ziplist 删除使用 memmove 来移动数据，以确保该列表仍然是连续的。 添加到 ziplist 需要内存重新分配调用，以为新条目腾出足够的空间。

*   由于时间轴大小，写入操作可能会出现**高延迟。 时间线的大小差异很大。 大多数用户的推文很少，因此他们的用户时间轴很小。 家庭时间表，尤其是那些涉及名人的时间表。 当更新较大的时间线并且高速缓存用完了堆时，这通常是在使用高速缓存时的情况，在有足够的连续 RAM 来处理一个大 ziplist 之前，将淘汰非常大的**小时间线** 。 由于所有这些缓存管理都需要时间，因此写操作可能会具有较高的延迟。**

*   由于写入被散布到许多时间轴，因此由于内存用于扩展时间轴，因此更有可能陷入**写等待时间陷阱**。

*   鉴于写入延迟的高度可变性，**很难为写入操作创建 SLA** 。

*   混合列表是 ziplist 的链接列表。 阈值设置为每个 ziplist 可以以字节为单位的大小。 以字节为单位，因为对于**内存有效**而言，它有助于**分配和取消分配相同大小的块**。 当列表经过时，它将溢出到下一个 ziplist 中。 ziplist 直到列表为空时才被回收，这意味着可以通过删除使每个 ziplist 只有一个条目。 实际上，推文并不是经常被删除。

*   在“混合列表”之前，一种变通方法是使较大的时间轴更快地过期，这为其他时间轴释放了内存，但是当用户查看其时间轴时，这是昂贵的。

## BTree

*   在 Redis 中添加了 BTree 以支持对层次结构键的范围查询，以返回结果列表。

*   在 Redis 中，处理辅助键或字段的方法是哈希映射。 为了对数据进行排序以便执行范围查询，使用了一个排序集。 排序后的订单按双倍得分排序，因此不能使用任意的辅助键或任意的名称进行排序。 由于哈希图使用线性搜索，因此如果有很多辅助键或字段，那就不好了。

*   BTree 是尝试解决哈希映射和排序集的缺点。 最好让**一个可以完成您想要的内容的数据结构**。 更容易理解和推理。

*   借用了 BTree 的 BSD 实现，并将其添加到 Redis 中以创建 BTree。 支持键查找以及范围查询。 具有良好的查找性能。 代码相对简单。 缺点是 **BTree 的内存效率不高**。 由于指针，它具有大量的元数据开销。

## 集群管理

*   集群出于单一目的使用多个 Redis 实例。 如果数据集大于单个 Redis 实例可以处理的数据量或吞吐量高于单个实例可以处理的数据量，则需要对键空间进行分区，以便可以将数据存储在一组实例中的多个分片中 。 路由选择一个密钥，并弄清楚该密钥的数据在哪个分片上。

*   认为集群管理是 Redis 采用率未激增的**首要原因。 当群集可用时，没有理由不**将所有用例迁移到 Redis** 。**

*   Tricky 正确安装 Redis 群集。 人们之所以使用 Redis 是因为作为数据结构服务器的想法是执行频繁的更新。 但是很多 Redis 操作都不是幂等的。 如果出现网络故障，则需要重试，并且数据可能会损坏。

*   Redis 集群**倾向于使用** **集中管理器** **来规定全局视图**。 对于内存缓存，很多集群都使用基于一致性哈希的客户端方法。 如果数据不一致，那就这样。 为了提供真正好的服务，集群需要一些功能，例如检测哪个分片已关闭，然后重播操作以恢复同步。 经过足够长的时间后，应清除缓存状态。 Redis 中的损坏数据很难检测。 当有一个列表并且缺少一个大块时，很难说出来。

*   Twitter 多次尝试构建 Redis 集群。 [Twemproxy](https://github.com/twitter/twemproxy) 并未在 Twitter 内部使用，它是为 [Twemcache](https://github.com/twitter/twemcache) 构建的 和 Redis 支持增加了。 另外两个解决方案基于代理样式路由。 其中一项与时间轴服务相关，并不意味着具有普遍性。 第二个是对 Timeline 解决方案的概括，该解决方案提供了群集管理，复制和碎片修复。

*   **群集中的三个选项**：服务器相互通信以达成群集外观的共识； 使用代理； 或进行客户端集群的客户端集群管理。

*   并非采用服务器方法，因为其理念是**使服务器保持简单** **，笨拙且快速**。

*   不接受客户端，因为 **更改很难传播** 。 Twitter 中约有 100 个项目使用缓存集群。 要更改客户端中的任何内容，必须将其推送到 100 个客户端，更改可能要花费数年的时间才能传播。 快速迭代意味着几乎不可能将代码放入客户端。

*   之所以使用代理样式路由方法和分区，有两个原因。 缓存服务是一种高性能服务。 如果您想要**高性能的产品，请将快速路径（即数据路径）与慢速路径** **（即命令和控制路径**）分开。 如果将群集管理合并到服务器中，则会使作为有状态服务的 Redis 代码变得复杂，每当您想要**修复错误或为群集管理代码提供**升级时，有状态 Redis 服务必须 也需要重新启动，这可能会丢弃大量数据。 群集的滚动重启很痛苦。

*   使用代理方法存在一个问题，即在客户端和服务器之间插入了另一个网络跃点。 **分析显示额外的跃点是一个神话**。 至少在他们的生态系统中。 通过 Redis 服务器的等待时间不到 0.5 毫秒。 在 Twitter 上，大多数后端服务都是基于 Java 的，并使用 Finagle 进行对话。 通过 Finagle 路径时，延迟接近 10 毫秒。 因此，额外的跃点并不是问题。 **JVM 内部是问题**。 在 JVM 之外，您几乎可以做任何您想做的事情，除非您当然要通过另一个 JVM。

*   代理失败并不重要。 在数据路径上引入代理层还不错。 客户不在乎与之交谈的代理。 如果超时后代理失败，则客户端将转到另一个代理。 在代理级别没有分片操作，它们都是无状态的。 要扩展吞吐量，只需添加更多代理。 权衡是额外的成本。 代理层被分配用于执行转发的资源。 群集管理，分片和查看群集的操作均在代理服务器外部进行。 代理不必彼此同意。

*   Twitter 的实例具有 **100K 开放连接** ，并且工作正常。 只是有开销要支付。 没有理由关闭连接。 只要保持打开状态，就可以改善延迟。

*   缓存集群用作 [后备缓存](http://www.quora.com/Is-Memcached-look-aside-cache) 。 缓存本身不负责数据补充。 客户端负责从存储中获取丢失的密钥，然后将其缓存。 如果某个节点发生故障，则分片将移至另一个节点。 出现故障的计算机恢复运行时将被刷新，因此不会留下任何数据。 所有这些都是由**集群负责人**完成的。 集中观点对于使集群保持易于理解的状态非常重要。

*   用 C ++编写的代理进行了实验。 **C ++代理看到** **性能显着提高**（未给出编号）。 代理层将移回 C 和 C ++。

## Data Insight

*   当有电话说缓存系统在大多数情况下都无法正常运行**时，缓存就很好**。 通常，客户端配置错误。 或者他们通过请求太多密钥来滥用缓存系统。 或一遍又一遍地请求相同的密钥，并使服务器或链接饱和。

*   当您告诉某人他们正在滥用您的系统**时，他们需要证明**。 哪个键？ 哪个分片不好？ 什么样的流量导致这种行为？ 证明需要可以显示给客户的指标和分析。

*   SOA 架构不会给您问题隔离或自动调试的便利。 您必须对组成系统的每个组件具有良好的可见性**。**

*   决定将 Insight 内置到缓存中。 缓存是用 C 语言编写的，并且速度很快，因此可以提供其他组件无法提供的数据。 其他组件无法处理为每个请求提供数据的负担。

*   **可以记录每个命令**。 缓存可以 100K qps 记录所有内容。 仅记录元数据，不记录值（关于 NSA 的笑话）。

*   **避免锁定和阻止**。 特别是不要阻止磁盘写入。

*   以 100 qps 和每条日志消息 100 字节的速度，每个盒子每秒将记录 10MB 数据。 开箱即用的大量数据。 万一出现问题，将使用 10％的网络带宽。 **在经济上不可行**。

*   **预计算日志可降低成本** 。 假设它已经知道将要计算什么。 进程读取日志并生成摘要，并定期发送该框视图。 与原始数据相比，该视图很小。

*   View 数据由 Storm 汇总，存储并在顶部放置一个可视化系统。 您可以获取数据，例如，这是您的前 20 个键； 这是您的点击量的第二个，并且有一个高峰，这表示点击量模式很尖锐； 这是唯一键的数量，这有助于容量规划。 当捕获每个日志时，可以做很多事情。

*   洞察力对于运营非常有价值。 如果存在丢包现象，通常可以将其与热键或尖刻的流量行为相关联。

## Redis 的愿望清单

*   显式内存管理 。

*   **可部署（Lua）脚本** 。 刚开始谈论。

*   **多线程** 。 将使群集管理更加容易。 Twitter 有很多“高个子”，其中主机具有 100+ GB 的内存和许多 CPU。 要使用服务器的全部功能，需要在物理机上启动许多 Redis 实例。 使用多线程，将需要启动更少的实例，这更易于管理。

## 获得的经验教训

*   **量表要求可预测性** 。 集群越大，客户越多，您希望服务成为更具可预测性和确定性的对象。 当有一个客户并且有问题时，您可以深入研究问题，并且很有趣。 当您有 70 个客户时，您将无法跟上。

*   **尾部延迟很重要** 。 当您扇出很多分片时，如果分片缓慢，则整个查询将变慢。

*   确定性配置在操作上很重要 。 Twitter 正朝着容器环境迈进。 Mesos 用作作业计划程序。 调度程序满足对 CPU，内存等数量的请求。监视器将杀死超出其资源需求的任何作业。 Redis 在容器环境中引起问题。 Redis 引入了外部碎片，这意味着您将使用更多内存来存储相同数量的数据。 如果您不想被杀死，则必须通过供过于求来弥补。 您必须考虑我的内存碎片率不会超过 5％，但我会多分配 10％作为缓冲空间。 甚至 20％。 或者，我想每台主机将获得 5000 个连接，但以防万一我为 10,000 个连接分配内存。 结果是巨大的浪费潜力。 如今，超低延迟服务在 Mesos 中不能很好地发挥作用，因此这些工作与其他工作是隔离的。

*   **在运行时了解您的资源使用情况确实很有帮助** 。 在大型集群中，会发生坏事。 您认为自己很安全，但是事情会发生，行为是无法预料的。 如今，大多数服务无法正常降级。 例如，当 RAM 达到 10GB 的限制时，请求将被拒绝，直到有可用的 RAM。 这只会使与所需资源成比例的一小部分流量失败。 很好 垃圾收集问题不是很正常，流量只是掉在地上，这个问题每天都会影响许多公司中的许多团队。

*   **将计算推入数据** 。 如果查看相对的网络速度，CPU 速度和磁盘速度，那么在进入磁盘之前进行计算并在进入网络之前进行计算是有意义的。 一个示例是在将节点上的日志推送到集中式监视服务之前对其进行汇总。 Redis 中的 LUA 是将计算应用于数据附近的另一种方法。

*   **LUA 今天尚未在 Redis 中投入生产** 。 按需脚本编写意味着服务提供商无法保证其 SLA。 加载的脚本可以执行任何操作。 哪个服务提供商会愿意冒别人代码的风险承担违反 SLA 的风险？ 部署模型会更好。 它将允许代码审查和基准测试，因此可以正确计算资源使用情况和性能。

*   **Redis 是下一代高性能流处理平台** 。 它具有 pub-sub 和脚本。 为什么不？

## 相关文章

*   [Google：驯服长时间延迟的尾巴-当更多的机器等于更差的结果时](http://highscalability.com/blog/2012/3/12/google-taming-the-long-latency-tail-when-more-machines-equal.html) [架构](http://highscalability.com/blog/2013/7/8/the-architecture-twitter-uses-to-deal-with-150m-active-users.html)
*   [Twitter 用于处理 1.5 亿活跃用户，300K QPS，22 MB / S 的 Firehose，并在 5 秒内发送推文](http://highscalability.com/blog/2013/7/8/the-architecture-twitter-uses-to-deal-with-150m-active-users.html)
*   [带有碎片的问题-我们可以从 Foursquare 事件中学习到什么？](http://highscalability.com/blog/2010/10/15/troubles-with-sharding-what-can-we-learn-from-the-foursquare.html)
*   [始终记录所有内容](http://highscalability.com/blog/2007/8/30/log-everything-all-the-time.html)
*   [低级可伸缩性解决方案-聚合集合](http://highscalability.com/blog/2013/3/6/low-level-scalability-solutions-the-aggregation-collection.html)

万一 Twitter 上有人读过我：为什么不禁用 EVAL（不是 EVALSHA），重命名 SCRIPT 并使用它来预加载脚本？ 然后，您可以发布您的客户端可以使用的 SHA 目录。

“ Redis 是下一代高性能流处理平台。它具有 pub-sub 和脚本功能。为什么不呢？”

更重要的是，2.8.9 在 PFADD / PFCOUNT / PFMERGE 命令中添加了 HyperLogLog（当然，Twitter 被困在 2.4 中）。 这样就可以在恒定内存中非常精确地近似估计流中唯一键的数量，而不必存储整个数据集。 例如，每天/每周/每月的唯一身份访问者数量。 这使大量分析成为可能，而不必使用像 Hadoop 这样的大数据大炮。

@Pierre Chapuis：我认为您的建议会奏效。 我认为这不是最干净的方法，但是：SCRIPT *是一个在线命令，如果我要离线加载（或者至少不通过数据路径完成加载），我会强烈建议这样做。 可能添加了一个配置选项，例如 SCRIPT_LOAD_PATH，以按照您的建议从目录中读取它们。

现在，如果我们允许即时重新加载配置（我在当前项目中已经计划过的事情），您甚至可以更新脚本而不会丢失所有数据或阻止流量。

MM 被用作百万的缩写吗？ 在技​​术环境中看到它似乎有些奇怪……这不是更多的财务缩写吗？ Just M 对我来说更容易。

我喜欢这样的观察：面向服务的体系结构的优点（概括地说）仅与您在服务边界处所做的工作一样好。 她说这是出于监视的目的（SOA 并没有因为服务 A 而神奇地将服务 B 隔离出问题）。 但这似乎同样适用于记录清晰的 API 合同，保持代码库清晰等。我在阐明将应用程序分解为服务方面对我有吸引力的方面时遇到了一些麻烦，而我认为那是缺少的部分-仅仅是 SOA 的概念不能提供大多数价值； 在边界上应用的整套实践可以提供帮助。

还发人深省（且合理）：像 Redis 这样的代码库往往不会由委员会维护，您需要进行认真的审查才能阻止糟糕的代码。 （最近阅读了很多 golang 的代码审查/变更列表流，因为它是公开的，有时它是保持语言状态最简单的方法，并且通常给它留下深刻的印象。）

关于脚本，我想知道如何在包装盒上找到包含数据的脚本，作为具有自己的容器和资源限制的普通 Redis 客户端。 您可以避免带宽限制； 您可以控制资源的使用（也许在分配内核的级别？）； 您不仅可以使用 Lua，还可以使用 Scala 或其他任何方法（尽管如此，但是，GC）。 Redis 的进程内 Lua 脚本可能没有通信成本，而它们会占用宝贵的 RAM 来存储数据，但这是一件容易的事。

对于 Twitter 开放源代码混合列表/树，这听起来像是最好的“自私”论据，它最终意味着更快地加入 2.8。 :)

@姚谢谢您的回答。 我同意选择离线加载（“东京霸王”）会更好。 另外，为 SHA 赋予别名是经常需要的功能，这将大有帮助，并使得可以以向后兼容的方式更新脚本的实现。

阅读这篇文章会让我觉得自己生活在一个平行的宇宙中，并且做着非常相似的事情。

嗨，
不错的演示。 我曾经遇到的一个问题是-您是否将 ssl 与 redis 一起使用？