Pinterest访问量一直以指数级的速度发展着, 每一个半月翻一番。在这两年里Pinterest的访问量从0提升到数百亿每月,从两个创始人1个工程师发展到40余个工程师,从一个MySQL服务器发展到180台Web服务器,240台API服务器,88台MySQL服务器(cc2.8xlarge)+每个节点一个从库,110个Redis实例,以及 200个Memcache实例。
令人叹为观止的增长!想一探Pinterest的传奇?我们邀请到Pinterest的Yashwanth Nelapati 和 Marty Weiner,他们以Scaling Pinterest为题讲述了Pinterest 架构演化的传奇故事。一年半前他们发展迅猛,面临着诸多抉择,如果那时他们能听到这么一场演讲,可以少走很多弯路。
这是一场非常棒的演讲。它充满了细节,而且非常接地气,它包含的策略几乎对所有人都适用。高度推荐。
从这篇演讲中我学到的最重要的两点如下:
-
正确的架构应该是当访问量增长时只需要简单的添加相同的东西即可。当你想砸钱解决问题的时候,只要在遇到瓶颈的部分增加容器即可。如果你的架构能支持这点,那就非常赞了。
-
当一些东西达到一个临界点的时候,各种技术都会以各自不同的方式崩溃。这导致他们在技术选型的时候主要考虑以下几点:成熟,好且简单,知名度高,用的人多,支持好,一贯好的表现,很少失败,开源。根据这些标准,他们选择了: MySQL、Solr、Memcache和Redis。放弃了:Cassandra 和Mongo。
这两点是互相关联的。满足第二条的技术都可以通过简单的增加容器来扩容。当负载增加的时候,成熟的产品问题更少。当你碰到问题的时候,至少也有一个社区可以帮你解决问题。当你选择的技术过于技巧、过于繁琐的时候,你将会面对一座无法翻越的高墙。
关于为什么分片比集群强的讨论,我认为是这篇演讲的精华,在这部分中你可以看到通过增加资源来应对增长,更少崩溃的模式,成熟,简单以及很好的支持,在这里全都实现了。请注意他们选择的工具以添加分片的方式增长,而不是集群。关于他们为什么选择分片和他们如何做分片是很有趣的事,这很可能触及到你以前未考虑过的场景。
现在,让我们看看Pinterest如何扩容:
基本概念
-
Pins 是一副图,附带了一些其它信息,一段用户的描述,另外可以链回用户找到该图片的地方。
-
Pinterest是一个社交网络。你可以关注人或专辑。
-
Database:保存了用户以及用户的专辑,专辑的pins;关注和转发的关心,认证信息等。
2010年3月启动--寻找自我的时代
那时你甚至不知道你自己到底要做一个什么样的产品。你有各种创意,因此对产品不断快速地修改迭代。最终被一堆奇怪的MySQL查询所终结。
早期的一些数字:
-
2个创始人
-
1个工程师
-
Rackspace托管
-
1 小型web服务器
-
1 小型MySQL数据库
2011年1月
依然处于保密阶段,产品在根据用户的反馈不断进化。这时的数字是:
-
Amazon EC2 + S3 + CloudFront服务
-
1 台NGinX, 4 台Web 服务(作为冗余,并不完全负载)
-
1 台MySQL 数据库 + 1 台只读的从库 (以备主库故障时使用)
-
1 个任务队列+ 2个任务处理器
-
1 台MongoDB数据库 (作为计数器)
-
2 个工程师
至2011年9月--试运行时代
以一种疯狂的模式运行,每一个半月翻一番——疯狂的增长。
-
当增速如此之快的时候,所有的东西每天每夜都在崩溃。
-
这个时间点,你读了很多白皮书,上面说只要加一点就能解决。但是当他们增加了各种技术,却全都失败了。
-
最终得到了一个复杂的架构:
-
Amazon EC2 + S3 + CloudFront
-
2台NGinX, 16 台Web 服务器+ 2 台API服务器
-
5 Functionally sharded MySQL DB + 9 只读从库
-
4个 Cassandra 节点
-
15 个Membase节点(3 个分开的集群)
-
8个Memcache节点
-
10个Redis节点
-
3 个任务路由 + 4 个任务处理器
-
4 个Elastic Search 节点
-
3 个Mongo 集群
-
3 个工程师
-
5 种主要的数据库技术来存储各自的数据。
-
迅速的增长使MySQL负载很高,而其它技术也接近他们的极限。
-
当有什么接近极限的时候,各种技术都以它们各自的方式挂掉。
-
开始放弃各种技术,反思自己到底想要一种什么样的系统。开始对所有事情做大量重构。
2012年1月-- 成熟时代
-
当所有的东西重构完之后,现在的系统看起来像这样:
-
Amazon EC2 + S3 + Akamai, ELB
-
90 台Web 服务器 + 50 台API服务器
-
66 台MySQL 数据库 (m1.xlarge) + 每个1台从库
-
59个Redis实例
-
51 个Memcache实例
-
1 个Redis任务管理器 + 25 任务处理者
-
以分片方式部署Solr
-
6 个工程师
-
现在以分片方式部署MySQL, Redis, Memcache, and Solr。优势是简单而且技术成熟。
-
Web的流量依然保持着增长的势头而iPhone的流量也快速增长起来了。
至2012年10月12日--回归时代
4倍于1月份的访问量
-
这时各种数字是这样的:
-
Amazon EC2 + S3 + Edge Cast,Akamai, Level 3
-
180 台Web 服务器+ 240 台API 服务器
-
88 台MySQL 服务器(cc2.8xlarge) + 每个一台从库
-
110 个Redis实例
-
200 个Memcache实例
-
4 个Redis任务管理器 + 80个 Task 处理器
-
以分片的方式部署Solr
-
40 个工程师(持续增长中)
-
现在架构开始做正确的事情了,只要简单的添加相同的东西就可以应付增长。现在遇到问题可以做到砸钱扩容了。在需要的时候简单的添加容器即可。
-
准备向SSD迁移。
为什么选择Amazon EC2/S3
-
非常好的可靠性。数据中心也会挂。Multitenancy增加了一些风险,但也不坏。
-
良好的报表与支持。他们有非常好的架构师,而且能判断出问题在哪。
-
良好的周边生态,特别是你在增长中的时候。你可以专注于应用内部,而使用它们管理的缓存、负载均衡、MapReduce、数据库等其他部分。你可以基于这些Amazon的服务起步,然后当你有工程师的时候再来改进。
-
秒级获取新的实例。云服务的力量的展现。特别是当你只有两个工程师的时候,你无须担心扩容计划,或者需要等两周才能得到新的memcache。几分钟就可以加上10个新的memcache实例。
-
缺点:选择比较有限。直到最近才能用上SSD,而且到现在都没有大内存的配置。
-
优点:还是有限的选择。不用为面对一堆不同配置的容器发愁了。
为什么选择MySQL?
-
非常成熟
-
非常稳定。从不宕机或丢数据。
-
招人比较容易。有很多工程师懂MySQL。
-
响应时间根据请求数量线性增长。有些技术当请求飙升的时候表现很差。
-
非常好的周边软件生态 ——XtraBackup, Innotop, Maatkit
-
非常好的社区。可以轻易的找到问题的答案。
-
很好的厂商支持,比如Percona。
-
开源–当你没有初始资金的时候,这点非常重要。
为什么选择Memcache?
-
成熟。
-
非常简单。它就是一个可以通过socket访问的哈希表。
-
性能一贯的好。
-
知名度高用的人多。
-
从不崩溃。
-
开源。
为什么选择Redis?
-
不够成熟,但是非常好而且简单。
-
提供多种数据结构。
-
提供持久化及复制功能,可以选择自己喜欢的方式来实现。如果你想选择MySQL式的持久化,没问题,可以实现。或者只需要3小时的持久化,没问题,也能实现。
-
用户首页的Feed是存储在Redis当中的,每三个小时存储一次。3小时内没有备份,每三个小时备份一次。
-
如果你存储数据的节点挂了,那他们只有几个小时的备份。这不是非常可靠,但是非常简单。你不需要复杂的持久化跟复制。这样简单很多,而且架构更廉价。
-
非常知名,用的人很多。
-
性能一贯的好。
-
很少有故障。只有一些微妙的故障你需要了解。这些就是需要更成熟的地方。先了解一下这些东西,然后度过这段时间吧。
-
开源。
Solr
-
一个非常好的即装即用型的产品。装好了以后几分钟你就可以用它进行检索了。
-
不能扩充到多余一个节点上。 (最新的版本并非如此)
-
尝试过Elastic Search,但是这种架构对于大量的小型文本以及大量的查询有些问题。
-
现在使用Websolr。不过他们有一个搜索引擎的团队,将来估计会自己进行开发。
集群Vs 分片
-
在快速的增长中,他们认识到他们需要将数据均匀的分散开,以应对增长的负载。
-
确定了问题讨论的范围,他们面对这两个选择集群和分片。
集群--所有事情都是自动化的:
-
例如: Cassandra, MemBase, HBase
-
结论:非常可怕,可能将来是他们的时代,但是现在这些太复杂了,有很多的故障点。
-
特点:
-
数据自动化分布
-
数据可移动
-
可重新进行分布均衡
-
节点间互相通讯,大量互相握手、对话。
-
优点:
-
自动伸缩数据存储。至少白皮书是这么说的。
-
安装简单。
-
在空间中分布存储你的数据。你的数据中心可以在不同的区域,这些由数据库自动管理。
-
高可用性
-
负载均衡
-
不存在单点故障
-
缺点 (根据第一手使用经验):
-
还是太年轻不够成熟。
-
底层太过复杂。一大堆节点必须有对称的协议,这点在生产环境中很难解决。
-
社区支持不够。在社区中分裂成了多条生产线,因此每个阵营的支持都比较少。
-
很少有工程师具备相关支持,估计很少有工程师用过Cassandra。
-
复杂和可怕的升级机制。由于他们都使用api进行互相之间的通讯,因此如果不想他们因为升级API变得混乱,会导致升级的路径非常复杂。
-
集群管理算法是一个 SPOF单点故障,如果管理端有bug会影响所有节点,这个使他们(Pinterest)宕机了4次。
-
集群管理器是一堆每个节点上都会部署的复杂代码,而且会有以下宕机模式:
-
数据重新平衡中断:添加一个新的节点,这时数据开始复制,然后被卡住了。这时候怎么办?并没有工具能发现到底发生了什么。没有社区可以提供帮助,因此他们就卡住了,然后他们还原回了MySQL。
-
所有节点数据损坏。如果有一个bug,将一些坏的内容写入到的了写日志当中,传递给所有节点。这时读延迟明显增加。所有数据混乱甚至数据丢失。
-
错误的是数据均衡很难修复。非常常见。你有10个节点,但是所有的数据都在一个节点上。因为有一个手动的处理方式,但是会将所有的负载分布到一个单节点上。
-
权威数据失效。集群的方案是很智能的。有一种情况,当他们引入一个新副本。大约80%的副本声称他是主数据,主数据变成了副本数据,然后你就丢了20%的数据。丢了20%比丢了所有数据还可怕。因为你不知道丢了哪部分。
分片--一切皆是手工:
-
结论:分片是赢家。注意,我觉得他们分片的架构与Flickr的架构非常像
-
特点:
-
去掉集群方式所有不好的特点,你就得到了分片。
-
人工对数据进行分布。
-
不移动数据。他们(Pinterest团队)从不移动数据,虽然有些人移动数据,这使得他们在一个更高的层次。
-
通过切分数据来分担负载。
-
节点间并不知道互相的存在。一些主节点控制着所有的事情。
-
优点:
-
可以通过切分数据库来扩大容量
-
在空间上分布组合数据
-
高可用
-
负载均衡
-
放置数据的算法很简单。主要原因是,虽然也存在单点,但是这个只有半页代码,不像Cluster Manager那么复杂。第一天过后就知道这个是能行还是不行了。
-
ID生成简单
-
缺点:
-
大多数表连接都不发实现
-
无事务功能。写入时可能写入一个数据库成功了,但写入另一个数据库失败了。
-
很多约束必须转移到应用层来实现。
-
表结构修改需要多规划。
-
报表需要在每个分片上进行查询,然后自己进行聚合。
-
表连接只能在应用层实现。
-
你的应用必须能接受以上所有。
何时分片?
-
如果你的项目有TB级的数据,那你要尽快还是分片。
-
当Pin表超过10亿行的时候,索引超出内存限制,然后交换到了硬盘。
-
他们选出最大的表,然后放入单独的数据库。
-
单个数据库空间耗尽。
-
他们不得不开始分片。
向分片过渡
-
过渡由一个特性的冻结开始。
-
然后他们考虑分片的方式。想要在一个单独页面渲染中调用尽量少的查询,查询尽量少的数据库。
-
移除所有MySQL 表连接。由于表可能存储在不同的分区,表连接会失效。
-
添加大量的缓存。基本上每条查询都要被缓存。
-
大致流程如下:
-
1 DB + 外键 + 表连接
-
1 DB + 非规范化+ 缓存
-
1 DB + 只读从库 + 缓存
-
数个根据函数分片的DB + 只读从库 + 缓存
-
根据ID分片 DB + 备份从库 + 缓存
-
由于主从延迟问题,早期只读从库成了一个问题。一个读操作可能分配到了从库上,但是主库还没有把这条数据复制到从库,因此看起来这条数据丢失了。最后用缓存解决这个问题。
-
他们有自己的后台脚本来备份数据库,检查数据数据约束、引用的完整性。
-
用户表是部分片的。他们用了一个大的数据库,而且在用户姓名跟email上加了唯一约束,如果不唯一,添加用户会失败。然后他们在已分片的数据库上进行了大量的写操作。
如何进行分片?
-
可以参考 Cassandra’s 的ring模型,Membase。以及 Twitter’s Gizzard。
-
坚信:在节点移动的数据越少,架构越稳定。
-
Cassandra存在一个数据平衡以及数据权威性的问题,因为节点间并不确定谁有哪一份数据。但是他们认为应用需要决定数据放在哪,这样就不会有问题。
-
计划他们未来5年的发展,然后根据计划容量进行分片。
-
初始化时创建大量虚拟分片。8个物理服务器,每个都有512个数据库。所有的数据库都有所有的表。
-
由于高可用需要,他们采用了多主复制的模式。每个主节点关联到不同的可用域。当宕机时,切换到另一个主节点,然后创建一个新节点来代替。
-
当数据库负载加重时:
-
查看代码提交,确认是否是有新的功能,缓存问题或者其他问题导致的。
-
如果是单纯的负载提升,他们分割数据库,然后告诉应用程序去新的节点上找数据。
-
在分割数据库之前他们对要分割的主节点开启从库复制。然后他们将应用代码按照新的数据库进行配置。在最初传数据的几分钟里,写操作在旧的节点上进行然后复制到新的节点上,然后操作被分分到两个节点上。
ID 结构
-
64 位:
-
分片ID: 16 位
-
类型: 10 位- Pin, 专辑,用户,或者其它对象类型
-
本地ID –余下的位数是表里的ID。用MySQL自增的ID。
-
Twitter使用一个映射表来把ID映射到物理主机。这需要一次查询。由于 Pinterest 部署在AWS 上,MySQL 的查询要消耗大概3ms,他们决定不要这个中间层,直接把位置信息构建在了ID里。
-
新用户随机分布在各个分片。
-
同一个用户的所有数据 (pins、专辑等) 都在同一个分片上,这样做的优势是在部分页面避免跨分片查询,例如用户的详情页,这样会快很多。
-
每个专辑都与作者存储在同一分片,这样渲染的时候只需要查询一个数据库。
-
分片ID足够65536个分片使用,但是他们最初只开启了4096个分片,以后可以横向扩展。这样当用户数据库满了的时候,他们可以开启新的分片,然后把新的用户的信息存储在新分片中。
查找
-
例如,如果有50个需要查找,他们把ID区分开,然后并行执行这些查询,因此延迟是最慢的查询需要的时间。
-
每个应用都有一个配置文件,把一组分片映射到一个物理主机上。
-
“sharddb001a”: : (1, 512)
-
“sharddb001b”: : (513, 1024) - backup hot master
如果你想找一个用户ID落在sharddb003a:分片的用户。
-
将ID进行分解
-
在分片映射中进行查询
-
连接指定分片,进入指定存储指定类型信息的数据库,然后根据本地ID去找到正确的用户最后返回序列化信息。
对象和映射
-
所有的数据都是一个对象(如pin,专辑,用户,评论)或者一种映射(如用户有专辑,pin有“喜欢”)。
-
对于对象,本地ID映射到一个MySQL的blob。Blob的格式最初使用的是JSON,后面转到了thrift序列化。
-
对于映射,有一个映射表。你可以查询一个用户的所有专辑。ID中含有一个时间戳,这样能做到按时间排序。
-
同样还存在反向映射,多对多的表,来解决查询所有喜欢这个pin的人这种问题。
-
数据库的命名规范为名词_动词_名词:user_likes_pins, pins_like_user.
-
所有的查询都是主键查找或者索引查找 (无表连接).
-
不像集群模式,数据不会在节点之间移动。例如:一旦一个用户落在的20号分片,那所有的用户信息也落在那,不会移动。64位的ID包含分片ID,因此它不能移动。你可以把物理数据移到另一个数据库,但是他还是关联到同一个分片。
-
在每个分片上都有所有的表。没有特别的分片,当然用于检测用户名冲突的巨型表除外。
-
不需要任何结构的变更,一个新的索引需要一个新表。
-
由于每个键对应的值都是blob,你可以不破坏数据库结构的基础上添加字段。每一个blob都有一个版本号,因此应用可以查到版本号,然后把这条记录改成新的格式然后回写到数据库当中。并不是所有的数据都需要立即更新,在读数据的时候会自动更新。
-
这是一个巨大的胜利,因为修改表结构可能锁表数个小时甚至数天。如果需要一个新的索引,那创建一个新的表,然后往表里添加数据。当你不需要的时候,删了即可。(不过没有提到这种更新怎么做到事务安全)
渲染一个用户详情页
-
根据URL获取用户名。去单独的大库里找到用户ID。
-
获取用户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 or redis),因此实际上没有很多数据库查询。
脚本
-
当你移动到一个新的分片架构上,你有两种基础构造。一种是旧的,还没有分片的系统,一种是新的,已经分片的系统。脚本是指那些把旧的那种转换到新的那种的代码。
-
他们移动了5亿个pin和16亿行的关注信息等。
-
不要低估项目中这一部分的分量。当初他们以为把数据转移到分片需要花两个月的时间,但实际用了4到5个月。注意,这段时间还冻结了新需求的开发。
-
应用必须同时往旧的和新的两种结构中写数据。
-
一旦你确认所有的数据都在新的结构上的时候。开始讲读操作移到新的结构上,然后慢慢增加负载,来测试后端。
-
构建一个脚本池。启动更多的进程来更快的完成任务,像把旧的表格移到新的架构上等。
-
实现一个Pyres,一个Github’sResque队列的Python接口,这个队列基于redis构建。支持优先级和重试。他们觉得这个很好,然后用这个代替了Celery跟RabbitMQ。
-
过程中出了很多错。用户发现了一些比如专辑丢失了等。不得不多跑几遍这个脚本,来确保数据转换正确。
开发
-
最初尝试给开发者整个系统的一部分,每个部分都有自己的mysql服务器,但是事情发展太快,这种不奏效了。
-
转向Facebook的模式,每个人都对所有的事情有操作权限,因此你必须小心操作。
未来的方向
-
基于服务的架构
-
当然们注意到很多数据库的负载的时候,他们建立很多应用服务和一堆其它的服务。所有的这些服务都连接MySQL跟memcache。这意味着有30k连接连在memcache上,这要消耗数个G的内存,把memcache的daemon挤到了交换分区。
-
作为一个修复,这些都移到了一个服务架构上。比如有一个关注服务,只提供关于关注者的查询。这样就隔离了30个机器来连接数据跟缓存,减少了连接的数量。
-
促进功能隔离、促进根据服务组织团队以及提供对服务的支持。促进安全性,开发者不能操作其它的服务。
学到的知识
-
保持项目简单。
-
保持项目有趣。会有很多人加入到这个团队。如果你给他们一个庞大复杂的系统,这不会有趣的。保持架构简单是个巨大的胜利。新人第一周就可以贡献代码了。
-
当一些东西大到一个临界点的时候,各种技术都会以各自不同的方式崩溃
-
正确的架构应该是当访问量增长时只需要简单的添加相同的东西即可。当你想砸钱解决问题的时候,只要在遇到瓶颈的部分增加容器即可。如果你的架构能支持这点,那就非常赞了。
-
集群管理算法是一个单点故障。如果有bug会影响所有节点。这让他们宕机了4次。
-
为了应付快速的增长,你需要把数据均匀的分散开,来解决增长的负载。
-
在节点间移动的数据越少,架构越稳定。这就是他们为什么从集群迁移到了分片。
-
面向服务架构原则。根据功能隔离,促进连接减少,团队组织,团队支持以及增强安全性。
-
问问自己到底需要的是什么。放弃不符合的技术,即使需要重构所有东西。
-
不要害怕丢失一点数据。他们把用户数据放在内存里,然后定时持久化。最多丢失几小时的数据,但是结果是系统更简单更健壮,这很关键。
相关文章
-
Discuss on Hacker News
-
Pinterest Architecture Update - 18 Million Visitors, 10x Growth,12 Employees, 410 TB Of Data
-
A Short On The Pinterest Stack For Handling 3+ Million Users
-
Pinterest Cut Costs From $54 To $20 Per Hour By Automatically Shutting Down Systems
-
Instagram Architecture Update: What’s New With Instagram?
-
7 Scaling Strategies Facebook Used To Grow To 500 Million Users and The Four Meta Secrets Of Scaling At Facebook –我想你可以在他们的处理当中发现一些近似之处。