Twitter发送峰值创新高,工程师在背后做了哪些改进

【感谢@NULL_文龙 的热心翻译。如果其他朋友也有不错的原创或译文,可以尝试推荐给伯乐在线。】

背景补充:日本网民一直都有在电视节目播出的同时,在网络平台上吐槽或跟随片中角色喊出台词的习惯,被称作“实况”行为。宫崎骏监督的名作动画《天空之城》于2013年8月2日晚在NTV电视台迎来14次电视重播。当剧情发展到男女主角巴鲁和希达共同念出毁灭之咒“Blase”时,众多网友也在推特上同时发出这条推特,创造了每秒推特发送数量的新纪录。

Castle in the Sky

根据推特日本官方帐号,当地时间8月2日晚11时21分50秒,因为“Blase祭”的影响,推特发送峰值达到了143,199次/秒。这一数字高于此前推特发送峰值的最高纪录,2013年日本时区新年时的33,388次/秒。更高于拉登之死(5106次/秒)、东日本大地震(5530次/秒)、美国流行天后碧昂斯宣布怀孕(8868次/秒)。(前两段摘编自微漫画

下图是峰值发生的邻近时间段的访问频率图,我们通常每天的推文数是 5 亿条,平均下来每秒大概产生5700条。这个峰值大概是稳定状态下访问量的25倍!

在这个峰值期间,用户并没有感觉到暂时性的功能异常。无论世界上发生了什么,Twitter始终在你身边,这是我们的目标之一。

“新的Tweets峰值诞生:143,199次Tweets每秒。通常情况:5亿次每天;平均值5700Tweets每秒”

这个目标在3年前还是遥不可及的,2010年世界杯直接把Twitter变成了全球即时沟通的中心。每一次射门、罚球、黄牌或者红牌,用户都在发推文,这反复地消耗着系统带宽,从而使其在短时间内无法访问。工程师们在这期间彻夜工作,拼命想找到并实现一种方法可以把整个系统的负载提升一个量级。不幸的是,这些性能的提升很快被Twitter用户的快速增长所淹没,工程师们已经开始感到黔驴技穷了。

经历了那次惨痛的经历,我们决定要回首反思。那时我们决定了要重新设计Twitter,让它能搞定持续增长的访问负载,并保证平稳运行。从那开始我们做了很大努力,来保证面临世界各地发生的热点事件时,Twitter仍能提供稳定的服务。我们现在已经能扛住诸如播放“天空之城”,举办超级碗,庆祝新年夜等重大事件带来的访问压力。重新设计/架构,不但使系统在突发访问峰值期间的稳定性得到了保证,还提供了一个可伸缩的平台,从而使新特性更容易构建,其中包括不同设备间同步消息,使Tweets包含更丰富内容的Twitter卡,包含用户和故事的富搜索体验等等特性。其他更多的特性也即将呈现。

下文将详述我们做了的事情。我们学到了很多。我们改变了我们的工程师组织架构。并且,在将来的数个星期,我们将发布更多的文章对本文提到的一些主题进行深入讲解。

 

开始重新架构

2010年世界杯尘埃落定,我们总览了整个项目,并有如下的发现:

  • 我们正运行着世界上最大的Ruby on Rails集群,我们非常快速的推进系统的演进–在那时,大概200个工程师为此工作,无论是新用户数还是绝对负载都在爆炸式的增长,这个系统没有倒下。它还是一个统一的整体,我们的所有工作都在其上运行,从管理纯粹的数据库,memcache连接,站点的渲染,暴露共有API这些都集中在一个代码库上。这不但增加了程序员搞清整个系统的难度,也使管理和同步各个项目组变得更加困难。
  • 我们的存储系统已经达到阈值–我们依赖的MySQL存储系统是临时切分的,它只有一个单主节点。这个系统在消化/处理快速涌现的tweets时会陷入麻烦,我们在运营时不得不不断的增加新的数据库。我们的所有数据库都处于读写的热点中。
  • 我们面临问题时,只是一味的靠扔进更多的机器来扛住,并没有用工程的方式来解决它–根据机器的配置,前端Ruby机器的每秒事务处理数远没有达到我们预定的能力。从以往的经验,我们知道它应该能处理更多的事务。
  • 最后,从软件的角度看,我们发现自己被推到了一个”优化的角落“,在那我们以代码的可读性和可扩展性为代价来换取性能和效率的提升。

结论是我们应该开启一个新工程来重新审视我们的系统。我们设立了三个目标来激励自己。

  • Twitter一直都需要一个高屋建瓴的建构来确保性能/效率/可靠性,我们想要保证在正常情况下有较好的平均系统响应时间,同时也要考虑到异常峰值的情况,这样才能保证在任何时间都能提供一致的服务和用户体验。我们要把机器的需求量降低10倍,还要提高容错性,把失败进行隔离以避免更大范围的服务中断–这在机器数量快速增长的背景下尤为重要,因为机器数的快速增长也意味着单体机器故障的可能性在增加。系统中出现失败是不可避免的,我们要做的是使整个系统处于可控的状态。
  • 我们要划清相关逻辑间的界限,整个公司工作在一个的代码库上的方式把我们搞的很惨,所以我们开始尝试以基于服务的松耦合的模式进行划分模块。我们曾经的目标是鼓励封装和模块化的最佳实践,但这次我们把这个观点深入到了系统层次,而不是类/模块或者包层。
  • 最重要的是要更快的启动新特性。以小并自主放权的团队模式展开工作,他们可以内部决策并发布改变给用户,这是独立于其他团队的。

针对上面的要求,我们构建了原型来证明重新架构的思路。我们并没有尝试所有的方面,并且即使我们尝试的方面在最后也可能并像计划中那样管用。但是,我们已经能够设定一些准则/工具/架构,这些使我们到达了一个憧憬中的更靠谱的状态。

 

The JVM VS the Ruby VM

首先,我们在三个维度上评估了前端服务节点:CPU,内存和网络。基于Ruby的机器在CPU和内存方面遭遇瓶颈–但是我们并未处理预计中那么多的负载,并且网络带宽也没有接近饱和。我们的Rails服务器在那时还不得不设计成单线程并且一次处理一个请求。每一个Rails主机跑在一定数量的Unicorn处理器上来提供主机层的并发,但此处的复制被转变成了资源的浪费(这里译者没太理清,请高手矫正,我的理解是Rails服务在一台机器上只能单线程跑,这浪费了机器上多核的资源)。归结到最后,Rails服务器就只能提供200~300次请求每秒的服务。

Twitter的负载总是增长的很快,做个数学计算就会发现搞定不断增长的需求将需要大量的机器。

在那时,Twitter有着部署大规模JVM服务的经验,我们的搜索引擎是用Java写的,我们的流式API的基础架构还有我们的社交图谱系统Flock都是用Scala实现的。我们着迷于JVM提供的性能。在Ruby虚拟机上达到我们要求的性能/可靠性/效率的目标不是很容易,所以我们着手开始写运行在JVM上的代码。我们评估了这带来的好处,在同样的硬件上,重写我们的代码能给我们带来10倍的性能改进–现今,我们单台服务器达到了每秒10000-20000次请求的处理能力。

Twitter对JVM存在相当程度的信任,这是因为很多人都来自那些运营/调配着大规模JVM集群的公司。我们有信心使Twitter在JVM的世界实现巨变。现在我们不得不解耦我们的架构从而找出这些不同的服务如何协作/通讯。

 

编程模型

在Twitter的Ruby系统中,并行是在进程的层面上管理的:一个单个请求被放进某一进程的队列中等待处理。这个进程在请求的处理期间将完全被占用。这增加了复杂性,这样做实际上使Twitter变成一个单个服务依赖于其他服务的回复的架构。基于Ruby的进程是单线程的,Twitter的响应时间对后台系统的响应非常敏感,二者紧密关联。Ruby提供了一些并发的选项,但是那并没有一个标准的方法去协调所有的选项。JVM则在概念和实现中都灌输了并发的支持,这使我们可以真正的构建一个并发的编程平台。

针对并发提供单个/统一的方式已经被证明是有必要的,这个需求在处理网络请求是尤为突出。我们都知道,实现并发的代码(包括并发的网络处理代码)是个艰巨的任务,它可以有多种实现方式。事实上,我们已经开始碰到这些问题了。当我们开始把系统解耦成服务时,每一个团队都或多或少的采用了不尽相同的方式。例如,客户端到服务的失效并没有很好的交互:这是由于我们没有一致的后台抗压机制使服务器返回某值给客户端,这导致了我们经历了野牛群狂奔式的疯狂请求,客户端猛戳延迟的服务。这些失效的区域警醒我们–拥有一个统一完备的客户/服务器间的库来包含连接池/失效策略/负载均衡是非常重要的。为了把这个理念深入人心,我们引入了”Futures and Finagle”协议。

现在,我们不仅有了一致的做事手段,我们还把系统需要的所有东西都包含进核心的库里,这样我们开新项目时就会进展飞速。同时,我们现在不需要过多的担心每个系统是如何运行,从而可以把更多的经历放到应用和服务的接口上。

 

独立的系统

我们实施了架构上的重大改变,把集成化的Ruby应用变成一个基于服务的架构。我们集中力量创建了Tweet时间线和针对用户的服务–这是我们的核心所在。这个改变带给组织更加清晰的边界和团队级别的责任制与独立性。在我们古老的整体/集成化的世界,我们要么需要一个了解整个工程的大牛,要么是对某一个模块或类清楚的代码所有者。

悲剧的是,代码膨胀的太快了,找到了解所有模块的大牛越来越难,然而实践中,仅仅依靠几个对某一模块/类清楚的代码作者又不能搞定问题。我们的代码库变得越来越难以维护,各个团队常常要像考古一样把老代码翻出来研究才能搞清楚某一功能。不然,我们就组织类似“捕鲸征程”的活动,耗费大量的人力来搞出大规模服务失效的原因。往往一天结束,我们花费了大量的时间在这上面,而没有精力来开发/发布新特性,这让我们感觉很糟。

我们的理念曾经并一直都是–一个基于服务的架构可以让我们并行的开发系统–我们就网络RPC接口达成一致,然后各自独立的开发系统的内部实现–但,这也意味着系统的内部逻辑是自耦合的。如果我们需要针对Tweets进行改变,我们可以在某一个服务例如Tweets服务进行更改,然后这个更改会在整个架构中得到体现。然而在实践中,我们发现不是所有的组都在以同样的方式规划变更:例如一个在Tweet服务的变更要使Tweet的展现改变,那么它可能需要其他的服务先进行更新以适应这个变化。权衡利弊,这种理念还是为我们赢得了更多的时间。

这个系统架构也反映了我们一直想要的方式,并且使Twitter的工程组织有效的运转。工程团队建立了高度自耦合的小组并能够独立/快速的展开工作。这意味着我们倾向于让项目组启动运行自己的服务并调用后台系统来完成任务。这实际也暗含了大量运营的工作。

 

存储

即使我们把我们板结成一坨的系统拆开成服务,存储仍然是一个巨大的瓶颈。Twitter在那时还把tweets存储在一个单主的MySQL数据库中。我们采用了临时数据存储的策略,数据库中的每一行是一个tweet,我们把tweet有序的存储在数据库中,当一个库满了我们就新开一个库然后重配软件开始往新库中添加数据。这个策略为我们节省了一定的时间,但是面对突发的高访问量,我们仍然一筹莫展,因为大量的数据需要被串行化到一个单个的主数据库中以至于我们几台局部的数据库会发生高强度的读请求。我们得为Tweet存储设计一个不同的分区策略。

我们引入了Gizzard并把它应用到了tweets,它可以创建分片并容错的分布式数据库。我们创造了T-Bird(没懂啥意思,意思是我们的速度快起来了?)。这样,Gizzard充当了MySQL集群的前端,每当一个tweet抵达系统,Gizzard对其进行哈希计算,然后选择一个适当的数据库进行存储。当然,这意味着我们失去了依靠MySQL产生唯一ID的功能。Snowflake很好的解决了上述问题。Snowflake使我们能够创建一个几乎可以保证全局唯一的ID。我们依靠它产生新的tweet ID,作为代价,我们将没有“把某数加1产生新ID”的功能。一旦我们得到一个ID我们靠Gizzard来存储它。假设我们的哈希算法足够好,从而我们的tweets是接近于均匀的分布于各个储存的,我们就能够实现用同样数量的数据库承载更多的数据。我们的读请求同样也接近平均的分布于整个分布式集群中,这也增加了我们的吞度量。

 

可观察性和可统计性

把那坨脆弱的板结到一起的系统变成一个更健壮的/良好封装的/但也蛮复杂的/基于服务的应用。我们不得不搞出一些工具来使管理这头野兽变得可能。基于大家都在快速的构建各种服务,我们需要一种可靠并简单的方式来得到这些服务的运行情况的数据。数据为王是默认准则,我们需要是使获取上述的数据变得非常容易。

当我们将要在一个快速增长的巨大系统上启动越来越多的服务,我们必须使这种工作变得轻松。运行时系统组开发为大家开发了两个工具:Viz和Zipkin。二者都暴露并集成到了Finagle,所以所有基于Finagle的服务都可以自动的获取到它们。

上面的代码就是一个服务生成统计报告给Via所需做的唯一事情。从那里,任何Viz用户都可以写一个查询来生成针对一些有趣的数据的时间/图表,例如第50%和第99%的request_latency_ms。

 

运行时配置和测试

最后,当我们把所有的好东西放一起时,两个看似无关的问题摆在面前:第一,整个系统的启动需要协调多个系列的不同的服务,我们没有一个地方可以把Twitter这个量级的应用所需要的服务弄到一起。我们已经不能依靠通过部署来把新特性展现给客户,应用中的各个服务需要协调。第二,Twitter已经变得太庞大,在一个完全封闭的环境下测试整个系统变得越来越困难。相对而言,我们测试自己孤立的系统是没有问题的–所以我们需要一个办法来测试大规模的迭代。我们接纳了运行时配置。

我们通过一个称作Decider的系统整合所有的服务。当有一个变更要上线,它允许我们只需简单开启一个开关就可以让架构上的多个子系统都和这个改变进行几乎即时的交互。这意味着软件和多个系统可以在团队认为成熟的情况下产品化,但其中的某一个特性不需要已经被激活。Decider还允许我们进行二进制或百分比的切换,例如让一个特性只针对x%的用户开放。我们还可以先把完全未激活并完全安全的特性部署上线,然后梯度的开启/关闭,知道我们有足够的自信保证特性可以正确的运行并且系统可以负担这个新的负荷。所有这些努力都可以减轻我们进行团队之间沟通协调的活动,取而代之我们可以在系统运行时做我们想要的定制/配置。

 

(译注:Twitter 官博文章后面还有一段内容,不过有点“吹牛”嫌疑,就不翻译了。有兴趣的朋友,请自己去看。)

 

1 收藏 2 评论

关于作者:NULL_文龙

一个生活在宇宙中心的Intel码农。(新浪微博:@NULL_文龙) 个人主页 · 我的文章 · 1

相关文章

可能感兴趣的话题



直接登录
最新评论
  • 老码农 其实,我是一个作家 2013/08/26

    First, we evaluated our front-end serving tier across three dimensions: CPU, RAM, and network. Our Ruby-based machinery was being pushed to the limit on the CPU and RAM dimensions –– but we weren’t serving that many requests per machine nor were we coming close to saturating our network bandwidth. Our Rails servers, at the time, had to be effectively single threaded and handle only one request at a time. Each Rails host was running a number of Unicorn processes to provide host-level concurrency, but the duplication there translated to wasteful resource utilization. When it came down to it, our Rails servers were only capable of serving 200 - 300 requests / sec / host.

    翻译成"每一个Rails主机跑在一定数量的Unicorn处理器上来提供主机层的并发,但此处的复制被转变成了资源的浪费" 是可以的,不清晰的原因主要还是背景知识,这个翻译确实有点难度,可以增加一些解释。

    其实这里的意思是说因为他们的Rails服务被设计为单线程的,也就是一个服务器每次只能处理一个请求,所以请求队列会在访问量爆发的时候不断增长。为了解决并发的问题,他们尝试在底层的Unix或Linux服务器和Rails服务器两层架构之间增加了一个Unicorn服务器,Unicorn的作用是调用Unix/Linux的fork机制,复制多个系统进程,形成多个虚拟Unix/Linux服务器,这样可以在一台服务器上加载多个Rails实例,在主机层形成虚拟的多线程服务,这就是文中所说的duplication。但是这样的做法效果并不好,只是消耗了更多的CPU和内存,网络带宽却还绰绰有余,所以作者说这种fork的虚拟多线程造成的只是无效的资源利用率。

    Unicorn本身没有什么问题,Linux + Unicorn + Rails也是很常见的搭配,从这里看不出为什么用Unicorn实现并发没有效果。也许是他们的系统太庞大,单个进程消耗的资源就很多,虚拟并发之后多个系统进程争夺资源导致频繁swap?Rails框架对于资源的需求确实比较大,特别是metaProgramming这些特性,在提供灵活性的同时也增加了系统负载。因此单线程架构本身并不是错,搭配轻量级的异步框架性能会非常好,比如LinkedIn从Rails转到Node.js就一下砍掉了90%的服务器。

跳到底部
返回顶部