两年内从零到每月十亿 PV 的发展来谈 Pinterest 的架构设计

jopen 11年前

  Pinterest 正经历了指数级曲线般的增长,每隔一个半月翻翻。在这两年里,Pinterest,从 每月 PV 量 0 增长到 10 亿,从两名成立者和一个工程师成长为四十个工程师,从一台 MySQL 服务器增长到 180 台 Web 服务器(Web Engine),240 台接口服务器(API Engine), 88 台 MySQL 数据库 (cc2.8xlarge) ,并且每台 DB 有一个备份服务器,110 台 Redis 实例服务(Redis Instance),200 台 Memcache 实例服务(Memcache  Instance)。

  令人叹为观止的增长。想一探 Pinterest 的传奇吗?我们请来了 Pinterest 的两位创立者 Yashwanth Nelapati 和 Marty Weiner,他们将以 Scaling Pinterest 为题讲述关于 Pinterest 架构的充满戏剧化的传奇故事。他们说如果能在一年半前飞速发展时能看到有人做类似题材的演讲的话,他们就会有更多的选择,以避免自己在这一年半里做出的很多错误的决定。

  这是一个很不错的演讲,充满了令人惊讶的细节。同时这个演讲也是很务实的,归根结底,它带来了可让大家选择的策略。极度推荐

  这篇演讲中有两个我最为看重的经验:

  1. 强大的架构在处理增长时通过简单增加相同的东西(服务器)来应对,同时还能保证系统的正确性。当遇到某种(性能)问题时,你想通过砸钱来扩容指的是你可以简单增加服务器(boxes)。如果你的架构能够做到这一点,那它就如金子一般强大而珍贵!

  2. 当某些(性能问题)快到极限时大多数技术都会以他们自己的方式失败。这导致他们在审核工具时要考虑以下一些特性:成熟,好且简单,有名气且用的人多,良好的支持,持续的优异性能,很少失败,开源。按照这样的标准,他们选择了:MySQL, Solr, Memcache, and Redis,放弃了 Cassandra ,Mongo。

  这两点经验是相互联系的。遵循(2)中提到的标准的工具可以在扩容时简单增加服务器(boxes).当负载增加了,成熟的产品更少会有问题。当你遇到问题时,你至少希望它的社区团队能够帮助解决。当你使用的工具过于技巧化和过于讲究时,你会发现你遇到一堵无法逾越的墙。

  在这段演讲里,碎片化(sharding)优于集群(clusterting)的观点是我认为最好的一部分。为了应对增长,通过增加资源,更少失败的模式,成熟,简单,良好的支持,最终圆满完成。请注意他们选择的工具以 sharding 的方式增长,而不是 clustering。关于他们为什么选择 sharding 和他们如何做 sharding 是很有趣的事,这很可能触及到你以前未考虑过的场景。

  现在,让我们看看 Pinterest 如何扩容:

  基本概念

  • Pins 是一幅关于其他信息的集合的图片,描述了为什么它对于用户来说很重要,可以链回到他们发现它的地方。
  • Pinterest 是一个社交网络。你可以追踪人或者板报(boards).
  • Database:它包含了拥有 pins 的板报(boards)和拥有板报(boards)的人 ,可以追踪或重新建立(repin)联系,还包含认证信息。

  启动于 2010 年三月–自我发现时期

  此时此刻,你甚至不知道你在做的这个产品将要做什么。你有想法,迭代开发更新产品的频率很高。最终因遇到一些在现实生活中永远不会遇到的奇怪的简短的 MySQL 查询而结束。

  早期的一些数字:

  • 两个创始人
  • 一个工程师
  • Rackspace 托管服务器
  • 一个小型 web 引擎
  • 一个小型 MySQL 数据库

  2011 年 1 月

  扔在潜伏前进中,产品得到了一些用户反馈。以下是数据:

  • Amazon EC2 + S3 + CloudFront 云服务
  • 一台 NGinX,4 台 Web 引擎(作冗余用,不是真正为了负载)
  • 一台 MySQL 数据库+一台读备份服务器(防止主服务器宕机)
  • 一个任务队列+两个任务处理
  • 一台 MongoDB(为了计数)
  • 两个工程师

  至 2011 年 9 月–试运行阶段

  每一个半月翻翻的疯狂增长阶段。

  • 当高速发展时每个晚上每个星期都会有技术失败的情况发生
  • 这时,你阅读大量白皮书,它会告诉你把这个增加进来就行了。当他们添加了大量技术时,毫无例外都失败了。
  • 最终你得到一个极为复杂的架构图:
  • Amazon EC2 + S3 + CloudFront
  • 2NGinX, 16 Web Engines + 2 API Engines
  • 5 Functionally Sharged MySQL DB + 9 读备份
  • 4 Cassandra 节点
  • 15 Membase 节点(分成三个单独的集群)
  • 8 Memcache 节点
  • 10 Redis 节点
  • 3 任务路由(Task Routers)+ 4 Task Processors
  • 4 ElasticSearch 节点
  • 3 Mongo 集群
  • 3 名工程师
  • 5 种主要的数据库技术只为了应付他们自己的数据
  • 增长极快以至 MySQL 负载很高,而其他一些技术都快到达极限
  • 当你把某些技术的应用推至极限时,他们又以自己的方式宣告失败。
  • 放弃一些技术并问它们到底能做什么。对每一件事情重新构架,海量工作量。

  架构成熟 2012 1 月

  重新设计的系统架构如下:

  • Amazon EC2 + S3 + Akamai, ELB
  • 90 Web Engines + 50 API Engines
  • 66 MySQL DBs (m1.xlarge) + 1 slave each
  • 59 Redis Instances
  • 51 Memcache Instances
  • 1 Redis Task Manager + 25 Task Processors
  • Sharded Solr
  • 6 Engineers .使用 Mysql,Redis,Memcache Solr,他们的优势是简单高效并且是成熟的技术。 随着 Web 流量增加,Iphone 的流量也随之开始越来越大。

      稳定期 2012 10 月 12 仅仅在一月份以后,大概就有 4 倍的流量增长。 系统架构数据如下: The numbers now looks like:

  • Amazon EC2 + S3 + Edge Cast,Akamai, Level 3
  • 180 Web Engines + 240 API Engines
  • 88 MySQL DBs (cc2.8xlarge) + 1 slave each
  • 110 Redis Instances
  • 200 Memcache Instances
  • 4 Redis Task Manager + 80 Task Processors
  • Sharded Solr
  • 40 Engineers (and growing)

  注意到,此时的架构应该是合理的,只是通过增加更多的服务器。你认为此时通过更多的投入来应对这么大的规模的流量,投入更多的硬件来解决这个问题, 下一步迁移到 SSDs

  为什么是 Amazon EC2/S3

  • 相当的可靠。数据中心也会宕机, Multitenancy 加入了不少风险,但不是坏处。
  • 良好的汇报和支持。他们确实有很不错的架构师而且他们知道问题在哪里。
  • 良好的额外服务支持(peripherals),特别是当你的应用处于增长时期。你可能在 App Engine 中转晕,你不用亲自去实现,只需要简单和他们的服务打交道,例如 maged cache,负载均衡,映射和化简,数据库和其他所有方面。Amazon 的服务特别适合起步阶段,之后你可以招聘工程师来优化程序。
  • 分秒钟获得新的服务实例。这是云服务的威力。特别是当你只有两名工程师,你不用担心容量规划或者为了 10 台 memcache 服务器等上两周。10 台 memcache 服务器几分钟内就能加完。
  • 反对的理由:有限的选择。直到最近你才能用 SSD 而且还没高内存配置的方案。
  • 赞成的理由:还是有限的选择。你不需要面对一大堆配置迥异的服务器。

  为什么是 MySQL?

  • 非常成熟
  • 非常耐用。从不宕机且不会丢失数据。
  • 招聘方便,一大堆工程师懂 MySQL.
  • 反应时间和请求数量(requies rate,我认为是 request rate 参考下面)是线性增长的。有些数据库技术的反应时间在请求飙升时不是很好。
  • 很好的软件支持– XtraBackup, Innotop, Maatkit
  • 很好的社区,问的问题总能轻易获取到答案
  • 很好的厂商支持,譬如 Percona
  • 开源–这一点很重要,特别是你刚开始没有很多资金支持时。

  为什么选择 Memcache?

  • 非常成熟
  • 非常简单。它就是一个 socket 的哈希表。
  • 性能一直很好
  • 很多人知道并喜欢
  • 从不崩溃
  • 免费

  为什么选择 Redis?

  • 还不成熟,但它是非常好并且相当简单。
  • 提供了各种的数据结构。
  • 可以持久化和复制,并且可以选择如何实现它们。你可以用 MySQL 风格持久化,或者你可以不要持久化,或者你只要 3 小时的持久化。
  • Redis 上的数据只保存 3 个小时,没有 3 小时以上的复本。他们只保留 3 个小时的备份。
  • 如果存储数据的设备发生故障,而它们只是备份了几个小时。这不是完全可靠的,但它很简单。你并不需要复杂的持久化和复制。这是一个更简单,更便宜的架构。
  • 很多人知道并喜欢
  • 性能一直很好
  • 很少的一些故障。你需要了解一些小故障,学习并解决它们,使它越来越成熟。
  • 免费

  Solr

  • 只需要几分钟的安装时间,就可以投入使用
  • 不能扩展到多于一台的机器上(最新版本并非如此)
  • 尝试弹性搜索,但是以 Pinterest 的规模来说,可能会因为零碎文件和查询太多而产生问题。
  • 选择使用 Websolr,但是 Pinterest 拥有搜索团队,将来可能会开发自己的版本。

  集群 vs. 分片

  • 在迅速扩展的过程中,Pinterest 认识到每次负载的增加,都需要均匀的传播他们的数据。
  • 针对问题先确定解决方案的范围,他们选择的范围是集群和分片之间的一系列解决方案。

  集群 —— 所有事情都是自动化的

  • 示例: Cassandra, MemBase, HBase
  • 结论: 太可怕了,不是在现在,可能在将来,但现在太复杂了,有非常多的故障点
  • 属性:
  • 自动化数据分布
  • 可移动数据
  • 可重新进行分布均衡
  • 节点间可通讯,大量的握手、对话
  • 有点:
  • 自动伸缩数据存储,至少白皮书上是这么说的
  • 安装简单
  • 在空间中分布存储你的数据,可在不同区域有数据中心
  • 高可用性
  • 负载均衡
  • 没有单点故障
  • 缺点 (来自用户一手的体验):
  • 还是相当年轻不成熟
  • 还是太复杂,一大堆节点必须对称的协议,这是一个在生产环境中难以解决的问题。
  • 很少的社区支持,有一个沿着不同产品线的分裂社区会减少每个阵营的支持。
  • 很少工程师有相关的知识,可能是很多工程师都没用过 Cassandra.
  • 复杂和和可怕的升级机制
  • 集群管理算法是一个 SPOF 单点故障,如果有个 bug 影响每个节点,这可能会宕机 4 次。
  • 集群管理器编码复杂,有如下一些失败的模式:
  • 数据重新均衡中断:当一个新机器加入然后数据开始复制,它被卡住了。你做什么工作?没有工具来找出到底发生了什么。没有社会的帮助,所以他们被困。他们又回到了 MySQL。
  • 所有节点的数据损坏. What if there’s a bug that sprays badness into the write log across all of them and compaction or some other mechanism stops? Your read latencies increase. All your data is screwed and the data is gone.
  • 均衡不当而且很难修复. 非常常见,如果你有 10 个节点,你会注意到所有节点都在一个节点上,有一个手工处理方式,但会将所有负载分布到一个单节点上
  • 权威数据失效. 集群方案是很智能的。In one case they bring in a new secondary. At about 80% the secondary says it’s primary and the primary goes to secondary and you’ve lost 20% of the data. Losing 20% of the data is worse than losing all of it because you don’t know what you’ve lost.

  分片(sharding) – 全凭人手

  • 裁决: 分片是赢家。我觉得他们分片的方案与 Flicker 非常相似。
  • 特点:
  • 如果去掉集群方式下所有不好的特点,就得到了分片。
  • 人工对数据进行分布。
  • 不移动数据。
  • 通过切分数据来分担负荷。
  • 节点不知道其它节点的存在。某些主节点控制一切。
  • 优点:
  • 可以通过切分数据库来扩大容量。
  • 在空间上分布数据。
  • 高可用。
  • 负载均衡。
  • 放置数据的算法十分简单。这是最主要的原因。虽然存在单点(SPOF),但只是很小的一段代码,而不是复杂到爆的集群管理器。过了第一天就知道有没有问题。
  • ID 的生成很简单。
  • 缺点:
  • 无法执行大多数连接。
  • 没有事务功能。可能会出现写入某个数据库失败、而写入其它库成功的情况。
  • 许多约束只能转移到应用层实现。
  • schema 的修改需要更多的规划。
  • 如果要出报表,必须在所有分片上分别执行查询,然后自己把结果合起来。
  • 连接只能转移到应用层实现。
  • 应用必须应付以上所有的问题。

  何时选择分片?

  • 当有几 TB 的数据时,应该尽快分片。
  • 当 Pin 表行数达到几十亿,索引超出内存容量,被交换到磁盘时。
  • 他们选出一个最大的表,放入单独的数据库。
  • 单个数据库耗尽了空间。
  • 然后,只能分片。

  分片的过渡

  • 过渡从一个特性的冻结开始。
  • 确认分片该达到什么样的效果——希望尽少的执行查询以及最少数量的数据库去呈现一个页面。
  • 剔除所有的 MySQL join,将要做 join 的表格加载到一个单独的分片去做查询。
  • 添加大量的缓存,基本上每个查询都需要被缓存。
  • 这个步骤看起来像:
  • 1 DB + Foreign Keys + Joins
  • 1 DB + Denormalized + Cache
  • 1 DB + Read Slaves + Cache
  • Several functionally sharded DBs+Read Slaves+Cache
  • ID sharded DBs + Backup slaves + cache
  • 早期的只读奴节点一直都存在问题,因为存在 slave lag。读任务分配给了奴节点,然而主节点并没有做任何的备份记录,这样就像一条记录丢失。之后 Pinterest 使用缓存解决了这个问题。
  • Pinterest 拥有后台脚本,数据库使用它来做备份。检查完整性约束、引用。
  • 用户表并不进行分片。Pinterest 只是使用了一个大型的数据库,并在电子邮件和用户名上做了相关的一致性约束。如果插入重复用户,会返回失败。然后他们对分片的数据库做大量的写操作。

  如何进行分片?

  • 可以参考 Cassandra 的 ring 模型、Membase 以及 推ter 的 Gizzard。
  • 坚信:节点间数据传输的越少,你的架构越稳定。
  • Cassandra 存在数据平衡和所有权问题,因为节点们不知道哪个节点保存了另一部分数据。Pinterest 认为应用程序需要决定数据该分配到哪个节点,那么将永远不会存在问题。
  • 预计 5 年内的增长,并且对其进行预分片思考。
  • 初期可以建立一些虚拟分片。8 个物理服务器,每个 512DB。所有的数据库都装满表格。
  • 为了高有效性,他们一直都运行着多主节点冗余模式。每个主节点都会分配给一个不同的可用性区域。在故障时,该主节点上的任务会分配给其它的主节点,并且重新部署一个主节点用以代替。
  • 当数据库上的负载加重时:
  • 先着眼节点的任务交付速度,可以清楚是否有问题发生,比如:新特性,缓存等带来的问题。
  • 如果属于单纯的负载增加,Pinterest 会分割数据库,并告诉应用程序该在何处寻找新的节点。
  • 在分割数据库之前,Pinterest 会给这些主节点加入一些奴节点。然后置换应用程序代码以匹配新的数据库,在过渡的几分钟之内,数据会同时写入到新旧节点,过渡结束后将切断节点之间的通道。

  ID 结构

  • 一共 64 位
  • 分片 ID:16 位
  • Type:10 位—— Board、User 或者其它对象类型
  • 本地 ID——余下的位数用于表中 ID,使用 MySQL 自动递增。
  • 推ter 使用一个映射表来为物理主机映射 ID,这将需要备份;鉴于 Pinterest 使用 AWS 和 MySQL 查询,这个过程大约需要 3 毫秒。Pinterest 并没有让这个额外的中间层参与工作,而是将位置信息构建在 ID 里。
  • 用户被随机分配在分片中间。
  • 每个用户的所有数据(pin、board 等)都存放在同一个分片中,这将带来巨大的好处,避免了跨分片的查询可以显著的增加查询速度。
  • 每个 board 都与用户并列,这样 board 可以通过一个数据库处理。
  • 分片 ID 足够 65536 个分片使用,但是开始 Pinterest 只使用了 4096 个,这允许他们轻易的进行横向扩展。一旦用户数据库被填满,他们只需要增加额外的分片,然后让新用户写入新的分片就可以了。

  查找

  • 如果存在 50 个查找,举个例子,他们将 ID 分割且并行的运行查询,那么延时将达到最高。
  • 每个应用程序都有一个配置文件,它将给物理主机映射一个分片范围。
  • “sharddb001a”: : (1, 512)
  • “sharddb001b”: : (513, 1024)——主要备份主节点
  • 如果你想查找一个 ID 坐落在 sharddb003a 上的用户:
  • 将 ID 进行分解
  • 在分片映射中执行查找
  • 连接分片,在数据库中搜寻类型。并使用本地 ID 去寻找这个用户,然后返回序列化数据。

  对象和映射

  • 所有数据都是对象(pin、board、user、comment)或者映射(用户由 baord,pin 有 like)。
  • 针对对象,每个本地 ID 都映射成 MySQL Blob。开始时 Blob 使用的是 JSON 格式,之后会给转换成序列化的 Thrift。
  • 对于映射来说,这里有一个映射表。你可以为用户读取 board,ID 包含了是时间戳,这样就可以体现事件的顺序。
  • 同样还存在反向映射,多表对多表,用于查询有哪些用户喜欢某个 pin 这样的操作。
  • 模式的命名方案是:noun_verb_noun: user_likes_pins, pins_like_user。
  • 只能使用主键或者是索引查找(没有 join)。
  • 数据不会向集群中那样跨数据的移动,举个例子:如果某个用户坐落在 20 分片上,所有他数据都会并列存储,永远不会移动。64 位 ID 包含了分片 ID,所以它不可能被移动。你可以移动物理数据到另一个数据库,但是它仍然与相同分片关联。
  • 所有的表都存放在分片上,没有特殊的分片,当然用于检测用户名冲突的巨型表除外。
  • 不需要改变模式,一个新的索引需要一个新的表。
  • 因为键对应的值是 blob,所以你不需要破坏模式就可以添加字段。因为 blob 有不同的版本,所以应用程序将检测它的版本号并且将新记录转换成相应的格式,然后写入。所有的数据不需要立刻的做格式改变,可以在读的时候进行更新。
  • 巨大的胜利,因为改变表格需要在上面加几个小时甚至是几天的锁。如果你需要一个新的索引,你只需要建立一张新的表格,并填入内容;在不需要的时候,丢弃就好。

  呈现一个用户文件界面

  • 从 URL 中取得用户名,然后到单独的巨型数据库中查询用户的 ID。
  • 获取用户 ID,并进行拆分
  • 选择分片,并进入
  • SELECT body from users WHERE id = <local_user_id>
  • SELECT board_id FROM user_has_boards WHERE user_id=<user_id>
  • SELECT body FROM boards WHERE id IN (<boards_ids>)
  • SELECT pin_id FROM board_has_pins WHERE board_id=<board_id>
  • SELECT body FROM pins WHERE id IN (pin_ids)
  • 所有调用都在缓存中进行(Memcache 或者 Redis),所以在实践中并没有太多连接数据库的后端操作。

  脚本相关

  • 当你过渡到一个分片架构,你拥有两个不同的基础设施——没有进行分片的旧系统和进行分片的新系统。脚本成为了新旧系统之间数据传输的桥梁。
  • 移动 5 亿的 pin、16 亿的 follower 行等。
  • 不要轻视项目中的这一部分,Pinterest 原认为只需要 2 个月就可以完成数据的安置,然而他们足足花了 4 至 5 个月时间,别忘了期间他们还冻结了一项特性。
  • 应用程序必须同时对两个系统插入数据。
  • 一旦确认所有的数据都在新系统中就位,就可以适当的增加负载来测试新后端。
  • 建立一个脚本农场,雇佣更多的工程师去加速任务的完成。让他们做这些表格的转移工作。
  • 设计一个 Pyres 副本,一个到 GitHub Resque 队列的 Python 的接口,这个队列建立在 Redis 之上。支持优先级和重试,使用 Pyres 取代 Celery 和 RabbitMQ 更是让他们受益良多。
  • 处理中会产生大量的错误,用户可能会发现类似丢失 board 的错误;必须重复的运行任务,以保证在数据的处理过程中不会出现暂时性的错误。

  动态

  • 最初试图给开发者一个分片系统。每个开发者都能拥有自己的 MySQL 服务器,但事情发生了快速的变化,导致不能工作。变
  • 去了 非死book 的模式:每个人可以获得一切。所以,你不得不非常谨慎

  未来发展方向

  • 基于服务的架构。
  • 当他们开始看到了很多的数据库负载,便像产卵一样,导致了很多的应用服务器和其他服务器堆在一起。所有这些服务器连接到 MySQL 和 Memcache。这意味着有 30K 上的 memcache 连接了一对夫妇演出的 RAM 引起的 memcache 守护进程交换。
  • 作为一个修补程序,这些都是移动的服务架构。有一个跟随服务,例如,将只回答跟随查询。此隔离的机器数目至 30 访问数据库和高速缓存,从而减少了连接。
  • 帮助隔离功能。帮助组织,解决和支持这些服务的团队。帮助开发人员,为了安全开发人员不能访问其他服务。

  学到的知识

  • 为了应对未来的问题,让其保持简单。
  • 让其变的有趣。只要应用程序还在使用,就会有很多的工程师加入,过于复杂的系统将会让工作失去乐趣。让架构保持简单就是大的胜利,新的工程师从入职的第一周起就可以对项目有所贡献。
  • 当你把事物用至极限时,这些技术都会以各自不同的方式发生故障。
  • 如果你的架构应对增长所带来的问题时,只需要简单的投入更多的主机,那么你的架构含金量十足。
  • 集群管理算法本身就用于处理 SPOF,如果存在漏洞的话可能就会影响到每个节点。
  • 为了快速的增长,你需要为每次负载增加的数据进行均匀分配。
  • 在节点间传输的数据越少,你的架构越稳定。这也是他们弃集群而选择分片的原因
  • 一个面向服务的架构规则。拆分功能,可以帮助减少连接、组织团队、组织支持以及提升安全性。
  • 搞明白自己究竟需要什么。为了匹配愿景,不要怕丢弃某些技术,甚至是整个系统的重构。
  • 不要害怕丢失一点数据。将用户数据放入内存,定期的进行持久化。失去的只是几个小时的数据,但是换来的却是更简单、更强健的系统!来自: 开源中国社区