通向高可扩展性之路(推特篇) ---- 一个推特用来支撑1亿5千万活跃用户、30万QPS、22MB每秒Firehose、以及5秒内推送信息的架构

时间:2022-02-25 15:27:03

原文链接:http://highscalability.com/blog/2013/7/8/the-architecture-twitter-uses-to-deal-with-150m-active-users.html

写于2013年7月8日,译文如下:

“可以解决推特所面临的挑战”的玩具般的方案是一个常用在扩展性上的比喻。每个人都觉得推特很容易实现。稍微具备一些系统架构的知识我们就可以构建一个推特,就这么简单。但是根据推特软件开发部门的VP Raffi Krikorian在 Timelines at Scale 中所做的极其详尽的演说来看,这一切并不是那么简单。如果你想知道推特到底是怎么工作的,那么现在就开始了:

推特的成长是渐进式的,以至于你可能会忽略他们的增长。它一开始仅仅是一个三层结构的Ruby on Rails网站,然后慢慢就变成了一个靠谱的以服务为导向的核心网站,以至于当我们想确定网络是不是出问题时会去尝试连接推特。多大的一个变化啊!

推特现在在世界范围内有1.5亿活跃用户、每秒处理30万请求来生成时间轴以及一个可以每秒输出22MB的Firehose。每天这个系统要接受4亿条消息。不出5分钟,Lady Gaga的一条消息就会出现在她3千一百万的粉丝面前。

一些看点:

1. 推特不再想要仅仅成为一个网站应用。它希望成为一套全球移动客户端使用的API,成为这个星球上最大的实时消息系统之一。

2. 推特主要是消费机制,而不是生产机制。每秒仅有6千的写请求,但是会有30万关于时间轴的读请求。

3. 那些拥有巨量粉丝的名人们越来越多。这样的人发一条消息,需要被扩散到所有他的粉丝面前,速度可能因此变慢。推特试图把它控制在5秒以内。但这并不是总能成功,尤其是当越来越多的名人之间互相转发消息时。一个后果就是可能回复比原本的消息更早被收到。推特正在试图针对高价值用户做出一些改变,将一部分的工作从写请求转到读请求时候来做。

4. 你自己的时间轴主页存在一个Redis集群中,最多可以有800条记录。

5. 推特可以从你是谁的粉丝以及你点击什么连接中知道很多东西。当双方没有互粉时,可以根据隐性社会契约推测出很多事情。

6. 虽然用户很关心推特上的消息,但是其实这些消息的内容跟推特的基础架构几乎没有什么关系。

7. 一个如此复杂的堆栈需要一个非常复杂的监控和调试系统来跟踪性能问题的根源。而且现在已经不合时的旧决策常常会继续影响这个系统。

那么推特到底怎么工作的?你可以从下面这个Raffi的精彩演说的大纲中了解一下。

挑战:

1. 在1.5亿活用用户和30万关于时间轴(包括主页和搜索)的QPS面前,简单的实现会很慢。

2. 事实上推特曾经试过由大量SELECT语句组成的简单实现,但是行不通。

3. 解决方案是一个以写为基础的扩散过程。当消息到达时通过很多步骤决定消息应该被存在哪里。这样读的时候就会又简单又迅捷,不需要任何计算。这样的做法导致写的速度慢于读的速度,写性能大概在4千QPS。

组织结构:

1. 平台服务组负责推特的核心可扩展基础设施:

  a. 他们负责时间轴服务、消息服务、用户服务、社交图服务、以及所有支撑推特平台的服务。

  b. 内部客户和外部客户用的API几乎是一样的。

  c. 有超过1百万应用注册使用推特的第三方API

  d. 保障产品组不需要为产品规模太大而烦恼

  e. 容量规划,设计可扩展的后台系统,在网站朝着计划外的方向发展时及时更换基础设施。

2. 推特有一个架构组,负责推特的整体架构,维护一个技术负债书(上面是他们想要去除的技术)

推送?提取?

1. 人们每时每刻都在推特上记录内容。推特的职责就是找到一个办法如何把这些内容同步发送出去,发送到你的粉丝面前。

2. 真正的挑战来自实时性。目标是要把一条消息在不超过5秒内送达一个用户。

  a. 送达意味着以最快的速度收集内容,把它送到互联网上,然后再把它从互联网上下载下来。

  b. 送达意味着把iOS,黑莓以及安卓的提醒、邮件还有短信都送进运行于内存中的时间轴集群里。

  c. 推特平均每个活跃用户发送短信的数量是这个世界上最高的。

  d. 美国的选举是产生最多互动内容的话题之一。

3. 两种主要时间轴:用户时间轴和主页时间轴

  a. 一个用户时间轴是一个用户发送的所有消息。

  b. 一个主页时间轴是一个临时的将所有你关注的人的用户时间轴合并所形成的东西。

  c. 这里运用了一些业务逻辑。例如,你不关注的人的回复会被屏蔽。一个用户重复发送的消息也会被过滤掉。

  d. 在推特这样的规模下做到这些事情很有挑战。

4. 以提取消息为基础:

  a. 特定时间轴。例如twitter.com和home_timeline API。被发送给你的消息一定是因为你请求接收到他们。提取消息为基础的送达:你通过一个REST API请求向推特申请获得这些数据。

  b. 查询时间轴。 搜索API。查询资料库。尽快的返回所有符合一个特定查询的所有消息。

5. 以推送消息为基础:

  a. 推特运行着一个最大的实时事件系统,可以按每秒22MB的速度通过Firehose推送消息:

    - 打开一个连接推特的通信管道,他们可以把所有的公共消息在150毫秒内发送给你。

    - 在任何时刻,大约有1百万个这样的通信管道与推送集群相连。

    - 使用类似搜索引擎的friehose客户端。所有公共消息都从这些通信管道中发出去。

    - 这不是真的。(因为你无法接受这样的事实。)

  b. 用户信息流连接。TweetDeck和Mac版推特就用这种方式驱动。当你登陆时,他们会查询你的社交图,然后只把你关注的人的消息发送给你,重新创建主页时间轴。与主动提取消息不同是,你会通过一个持久的连接来获得相同的时间轴。

  c. 查询API。发送一个对所有消息的查询。当有新产生的消息符合这个查询时,它们就会通过为这个查询注册的通信管道被发送出去。

以提取消息为基础的时间轴的总体表现:

1. 消息通过一个写API进入推特。它会通过一些负载均衡器以及一个TFE(推特前端终端)和一些他们内部的服务。

2. 这是一个很直接的路径。完全预先算好的主页时间轴。所有的业务逻辑都在消息进来的时候执行。

3. 扩展过程随后发生。消息进来之后被放在一个Redis集群中。一条信息会在三台不同的机器上备份三次。在推特这样的规模下,一天会有很多机器出故障。

4. 扩展过程会查询寄去Flock的社交图服务。Flock维护粉丝以及关注人名单。

  a. Flock 返回一个接收者的社交图,然后对Redis集群中存储的所有时间轴重复这一步骤。

  b. Redis集群有一些TB级的RAM。

  c. 可以同时对4千个目的地进行查询。

  d. 使用了Redis原生的list结构。

  e. 我们假设你有2万的粉丝,然后你发了一条消息。那扩展程序将会在Redis集群中查询所有那2万个用户的地址,然后把这条消息的ID插到那些用户的列表中。所以每写一个消息Redis集群就会执行2万个插入操作。

  f. 他们会插入消息的ID,产生消息的用户ID,以及4个字符用来表示这是一个转发、回复、或者别的东西。

  g. 你的主页时间轴也在Redis集群中,有800条记录。如果你足够耐心地一直往后翻页,你就会发现这个限制。RAM是影响时间轴能存多少记录的主要限制。

  h. 每一个活跃用户都存在RAM中来降低延时。

  i. 活跃用户指的是那些在30天内登录过推特的人。这个天数可能会因为推特网站的繁忙程度或者缓存的大小而改变。

  j. 如果你不是活跃用户,那你发的消息并不进入缓存中。

  k. 只有你的主页时间轴会需要读写硬盘。

  l. 如果你的资料被挤出Redis集群了,那你重新进入Redis集群时会需要走一个叫重建的流程:

    - 查询社交图服务,找到你关注谁,从硬盘上吧所有这些人都导入Redis中。

    - 他们用Gizzard来管理基于MySQL的硬盘存储,并用它来掩盖SQL交易并提供全局复制。

  m. 通过在一个数据中心内复制三次,即使有一个机器出了故障,他们也不需要重新生成那台机器上的所有时间轴。

  n. 如果一条消息是转发,那么会存一个指向原消息的指针。

5. 当你查询你的主页时间轴的时候,你用到了时间轴服务。时间轴服务只需要找到一台记录了你的主页时间轴的机器。

  a. 运行三个不同的哈希环,因为你的时间轴存在三个不同的地方。

  b. 他们找到第一个他们能最快到达的机器,然后尽快返回。

  c. 代价就是扩散会需要久一点,但是读取操作很快。大约只要2秒就可以从一个冷缓存到达浏览器。从一个API请求只需要400毫秒。

6. 由于时间轴只包含消息ID,他们还需要复原这些消息,找到对应的内容。给定一串ID,他们可以做一次多项获取,并行地从T-bird中获取消息内容。

7. Gizmoduck是用户服务。Tweetypie是消息对象服务。每个服务都有自己的缓存。用户缓存是一个memcache集群,存着所有用户数据。Tweetypie在自己的memcache集群中存有大约一半上个月产生的消息。这些消息是给内部客户用的。

8. 有时在出口处会做一些读取时的过滤。比如在法国过滤纳粹相关的内容,这种内容在送出去时会被删除掉。

搜索的总体表现:

1. 不同于提取消息。所有的计算都在读的时候执行,所以写操作很简单。

2. 当一条消息到达的时候,Ingester把消息分割,然后找到所有他们想要用来索引的关键字,然后把这条消息扔到一个Early Bird机器上。Early Bird是一个改良版的Lucene。索引被存在RAM中。

3. 在扩散时,一条消息可能存在N个人的主页时间轴中,取决于多少个人关注你。但是在Early Bird中,一条消息就被存在一台机器上(除了复制)。

4. Blender创建搜索时间轴。它需要在整个数据中心内分散收集数据。它查询每一个Early Bird,看他们是否有符合这条查询的内容。如果你查询“New York Times”,所有的机器都会被查询,结果会被返回、排序、合并和重新打分。重新打分基于社交因素,例如转发、点赞和回复的数量。

5. 有一个活动时间轴,它的活动信息是基于写操作来计算的。当你点赞和回复消息时,他们会更新你的活动时间轴。类似于主页时间轴,它是一串活动事件的ID,比如点赞ID,回复ID等。

6. 这些都会输入Blender,在读操作时用来重新计算、合并以及排序,然后返回一个你看到的搜索时间轴。

7. 发现是一个基于推特对你的了解而制定的搜索。他们对你的了解来源于你关注的很多人、对连接的点击等,这些信息都被用在发现搜索中。它还会基于这些信息给结果重新打分。

搜索和提取消息正好相反:

1. 搜索和提取消息看起来非常相似,但是他们有一个特征与对方相反

2. 主页时间轴:

  a. 写操作:当一个消息到达时,需要O(n)个步骤写入Redis集群,n是你的粉丝数量。对于Lady Gaga和Barack Obama这样的用户来说意味着千万级的插入操作。所有Redis集群都有硬盘备份,flock集群会把用户时间轴也存在硬盘上。但是总的来说,时间轴会在Redis集群的RAM中被找到。

  b. 读操作:通过API或者网页,只需要O(1)个步骤就可以找到正确的Redis机器。推特优化了主页时间轴的读取路径,使其十分方便。读取操作只需要10多个毫秒就可完成。推特主要是一个消费机制,而不是生产机制:每秒30万的读请求对应的是每秒6千的写请求。

3. 搜索时间轴:

  a. 写操作:当一个消息到达ingester时,只有一个Early Bird机器会响应。写操作是O(1)的。一个消息在5秒内可以完成排队等待以及写入对应的Early Bird机器。

  b. 读操作:当一个读操作到来时,需要在整个集群内做O(n)次的读取。大多数人不使用搜索,所以可以先快速把消息存储起来等待搜索。但是在读的时候就会付出代价。读操作需要数百毫秒。搜索不会从硬盘上读取内容。整个Lucene索引都在RAM中,所以分散搜集式的读取还是很有效率的, 它们不用从硬盘上读取。

4. 消息的内容几乎与大多数的基础设施没有任何关系。T-bird存储了所有消息的内容。一条消息的大部分内容存在RAM中。如果不是,那就去询问T-bird然后做一个select查询就可以把它们重新取出。消息的具体内容是什么除了在搜索、潮流、或者实时事件管道外几乎没什么影响。主页时间轴一点也不关心具体内容。

未来:

1. 如何使现在信息管道更快更有效率?

2. 扩散会很慢。试图把它控制在5秒以内,但是不是每次都能成功。总的来说很难,尤其是当名人发消息的时候。这种情况也越来越常见。

3. 推特的关注图是一个不对称图。消息只会发给一个特定时间点上关注的人们。推特可以通过你关注Lance Armstrong但是他不关注你知道很多东西。当双向关注不存在时,可以从这一隐性社会契约中推出很多东西。

4. 大基数的社交图是一个挑战。@ladygaga有3千1百万粉丝。@katyperry有2千8百万粉丝。@justinbieber有2千8百万粉丝。@barackobama有2千3百万粉丝。

5. 当上述这些人发消息的时候,数据中心内要写多得多的消息。当他们互动时(这个经常发生),这个挑战就更难了。

6. 这些高扩散用户是推特最大的挑战。名人发的消息经常比它们的回复更晚被看见。它们引起了竞争条件。如果一条Lady Gaga发的消息需要数分钟才能扩散到所有她的粉丝中去,那人们就会在不同的时间点看到她的消息。一些最近在关注她的人可能比很久之前关注她的人可以早5分钟看到这条消息。我们假设有个人早早地收到了这条消息然后回复了,如果原消息还在扩散过程中,那这条回复就会插在原消息之前被扩散,先被那些还没收到原消息的人收到。人们就会很疑惑。推特按照ID排序,因为他们的用户几乎总是单调增加的,但是这并不能解决这么大规模的问题。高价值用户的扩散队列总是满的。

7. 试图找出一个方法合并读和写路径。不在扩散高价值用户。像Taylor Swift这样的人并不在乎扩散了,反倒是可以在读的时候再合并她的时间轴。这样就可以平衡读和写的路径,节省10%以上的计算资源。

解耦:

1. 消息被复制传递了很多次,大多数是在互相独立的组之间。搜索、推送、兴趣邮件以及主要时间轴等组可以相互独立的工作。

2. 系统因为性能原因解耦。推特以前是完全同步的,但是两年前因为性能原因而被停止了。消化一条消息到API中需要145毫秒,然后所有客户端都会被断开连接。这是由于一些历史原因引起的。写路径是基于Ruby和MRI,一个单线程的服务器,所以每当一个Unicorn工作程序被新分配时,处理能力就会减少一些。他们希望尽快释放一个客户端的连接。一条消息进来,Ruby处理它,把它塞进一个队列,然后断开连接。他们一个机器只能跑45-48个程序,因而一台机器只能同时处理那么多的消息,所以他们希望可以尽快释放连接。

3. 现在消息被传入一个异步的通道,然后所有我们之前讲过的服务会从中提取消息。

监控:

1. 办公室有很多显示器实时显示系统运行状态。

2. 如果你有1百万的粉丝,只需要几秒就可以让他们都收到你的消息。

3. 推特输入端数据: 40亿条消息一天;平均每秒5千条;高峰时每秒7千条;有大型活动时超过每秒1万2千条。

4. 时间轴送达数据: 每天300亿次送达(大约2千1百万次每分钟);通向高可扩展性之路(推特篇) ---- 一个推特用来支撑1亿5千万活跃用户、30万QPS、22MB每秒Firehose、以及5秒内推送信息的架构p50的送达1百万粉丝所需时间为3.5秒;每秒30万次送达;p99会达到5分钟

5. 一个叫做VIZ的系统监控每一个集群。时间轴服务从Scala集群中取出数据的请求时间的中值是5毫秒。p99是100毫秒。p99.9是数百毫秒,这是因为他们需要从硬盘中读取了。

6. Zipkin基于谷歌Dapper系统。他们可以用它来跟踪一条请求,看这条请求经过了哪些服务,分别花了多久,然后他们就可以有一个很细致的针对每条请求的性能报告。你可以深入挖掘每一条请求,并理解所有不同的时间花在了哪里。有很多时间都是用来debug这个系统,看一个请求所用的时间都花在了哪里。他们也可以根据不同的阶段汇总数据,然后观察一个扩散或者送达需要多久。他们整整花了两年把活跃用户时间轴的获取时间降低到2毫秒。很多时候都是与停止GC、memcache查询做斗争,或是理解数据中心的拓扑、或是真的架设集群来获得这样的成功。