87

深度解读:Twitter架构如何支持上亿用户

 6 years ago
source link: http://mp.weixin.qq.com/s/c9mFDmHRLbuKbqS6aGUdiQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深度解读:Twitter架构如何支持上亿用户

Original 🐯 BitTiger 2017-11-23 01:54 Posted on

Image







欢迎回看本文原始视频

http://t.cn/RYZ1SDg



谈到设计Twitter, 我们首先要问一个本质问题:设计Twitter的基本方法论是什么?

其实是我们计算机设计最基本的方法:分治法(Divide and Conquer)。

什么是分治法呢?就是把问题不断的拆解,拆解到你可以解决为止,它的艺术在于,从哪个维度来拆解非常考验我们能力。

Image
Image

如果要求一周开发出Twitter,你会怎么做?

你的架构是什么样的呢?

相信你一定不会给出复杂的架构。前端是各种各样的业务逻辑,后端是MySQL数据库,这样就够了。因为这已经解决了当时的问题,满足了一周开发出来的要求。

Image
Image

但随着Twitter的成长,我们会遇到各种各样新的挑战:

  • MySQL难扩展。

    为什么难扩展?因为它把同样的一个数据分成各种关系存在里面,每次取的时候,都要通过Join来进行复杂的操作,这个Join当数据被切分的时候存在更多的服务器上,会变得越来越复杂,所以很难扩展。

  • 小变化也要全部部署。

    任何变化都需要部署到所有机器上,因为服务不断升级,就变成了每天不断部署,变成了daily deployment。所以每次部署的时候耽误时间很长。

  • 性能差。

    因为所有服务都要部署在一起,造成了它的内存占用率大,而且部分核心模块还因为最初当时的单线程设计成为了各种瓶颈。

  • 架构混乱。

    因为所有模块在一起很混乱。

Image

那要怎么破解这些问题?

答案是将问题拆解开来。

Image

第一刀将存储切开。

我们看后台,可以拆成存Tweets,存User,存Timeline,存Social Graph不同内容,Timeline可以拿Redis这样的数据库来保存,而对于其它的数据比如Gizzard其实是一个分布式的MySQL数据库。并不是所有数据都不适合用MySQL分布,我们可以把这些适合的用MySQL来shutting一下,而不适合的用别的数据库来存,就是一个存储切开的方法。

Image

第二刀是将路由、展示、逻辑切开。

我们可以看到又多出了一层,逻辑层。Tweets,User,Timeline它们分别对应后边的各种数据存储。前端会有外部访问的接口,API访问接口,并且它还保存了以前数据的整个架构,用来进行一些小规模的使用。最前面需要有一个Routing,来将不同的请求分布到不同的API上。所以之后Twitter就变成了上百上千个小小的数据模块,包括它的服务模块,他们通过之间的相互调用来完成具体的请求。

Image
Image

如果Lady Gaga发了个推文,会发生什么呢?

首先她发出一个推文,会到达最上面一层的外部模块,负责把推文写出来;接着到了API模块,负责接收这个推文;再往后是Fanout,Fanout将推文的ID推荐给所有订阅这个用户的信息收件箱里,收件箱就是Timeline,比如有400万人订阅了Lady Gaga,就会有400万的收件箱收到这个消息;紧接着由于我们Timeline要发到收件箱里,收件箱必然是一个最复杂的操作,为了优化它的性能,就把它们用分布式的方式存储在Redis里面,Redis是偏向内存的数据库,所以能够很快的存储这些信息;但Redis里存的只是ID,所以当一个用户具体要她推文的时候,实际上要通过Timeline服务去找到这个用户对应的是哪个Redis服务器上存了推文的 Timeline list,接着找到所有ID,然后这个Timeline service把ID的具体内容通过另外的数据库装载进来,最终得到结果反馈给用户。这些保证了我们推整个数据的时候速度最快,能够达到每秒30万次的性能。

Image
Image

虽然有了这样的过程,如何支持搜索呢?

很简单的想法是当我们写入API,它Fanout到每个用户的Timeline list的时候,我们可以拿另外的Ingester把这个推文放到里面,Ingesert最后把它放到Earlybird,就是所谓的倒排索引中。比如有个推文发过来“我喜欢太阳”,就把我,喜欢,太阳,拆成三个词,把这些单词,进行倒排索引,存到Search index里面。这时候实际会用到很多很多Earlybird,这样就能建立很多倒排索引,能够并行的去做。当用户一个请求过来之后,比如搜索“早上吃饭”,就会把morning和eat作为两个关键词发到我的Earlybird集群里,得到结果后Blender会把它组合到一块,并反馈结果。当然很多用户可以并行的向Blender发送请求,从而得到最终结果,这就是我们的搜索服务。

Image
Image

如何通知用户新消息到达呢?

第一个方法是另外再开一个Write API,在有新东西发生以后,我把它放到一个Push的服务里,那所有用户只要都连到这个后台里,HTTP PUSH,就会通知他有新消息产生了。同理对Mobile ,会有Mobile Push,当然大家对不同的信息有不同的对接方法,大家可以去仔细考虑下怎么能做到这个样子。

Image
Image

如何搭建这样的服务呢?

最基本的是开源项目Twitter-Server。

  • 配置服务,IP之类;

  • 管理服务,哪些down了、控制、启动等;

  • 日志服务,运行怎么样,以后出问题找谁等;

  • 生命周期服务,什么时候启,什么时候关,什么时候控制;

  • 监控服务,到底有没有出错,出错以后怎么办,互相报警等

这些东西合到一起,就构成了这样服务的基本架构。

Image

各个服务之间如何交流?

这就是传统的RPC(远程的进程调用),大家能够通讯的不仅是数据,而且可以通讯命令或请求之类的。这时需要开源项目Finagle。

  • 能够提供服务发现,因为有很多服务,所以要找谁发送这个请求呢;

  • 负载均衡,可能有十个人提供服务,先放到谁那儿?

  • 重试,如果失败了怎么办,是否需要重试?

  • 基本的线程池和链接池,大家可以复用,不用每次去创建,浪费资源了;

  • 统计信息的收集;

  • 分布式调试。

Image

如何调用一个服务?

因为从A调用B,大家之间各种远程,写代码上要怎么做呢?可以使用函数来调用(Service as a Function),背后实际上是Function programming的思想。

可以看下这个基本例子:trait Service[Req,Rep]extends(Req=>Future[Rep])

简单理解为:我有一个请求想得到一个Response,就是Request到Response。这里面实际上可以先面向未来实现,之后当它执行的时候,就会得到相对应的结果。

Image

多个服务的调用如何整合在一起?

我们看一看它要如何运行?比如我们有一个请求是得到一个用户的所有Timeline数据。它其实有很多步,第一步是得到User ID;第二步得到User timeline list里的那些消息ID;下一步是针对每一个消息,得到它的每一个具体内容,比如“我早上吃饭”这些内容,这些内容里面可能还有图片,还需要得到图片的数据。这是一个很复杂的过程,但这个箭头表达了它们最基本的执行顺序。

Image
Image

最佳的执行路径是什么?

首先是得到ID,再得到Timeline以后可以并行地读取每个tweet,可能会有的快有的慢,然后又得到一些它的具体信息,比如说图片之类,所以这就是最优策略。

Image

下面我们来看一看代码:

Image

第一行是得到用户ID,用一个面向未来的方程得到一个用户ID。

Image

第二行根据ID拿FlatMap,FlatMap其实是后面针对每一个ID执行的函数,这个函数来得到这个ID用户的Timeline list。所以这也是我们Function programming,或者如果用Spark也会经常碰见的函数。

Image

得到每一个ID以后,实际上要针对每个ID执行,所以需要Map。Map每一个ID是做什么的,得到这个Tweet的具体信息。代码第二行就是根据ID得到他的具体内容。

Image

当然如果这个东西有图片,那需要得到图片。

Image

中间一步还需要把这些东西并行起来,大家都去单独得到Tweet,并且把它集合放到一块儿去。整个过程我们就得到了最终的代码。

Image

这其实就是第三刀:切开“做什么”和“怎么做”。在这个代码过程中,所谓面向函数的编程,只是写了想做什么事儿,但具体怎么做,比如从哪个机器找、连接服务器、从谁那儿抓取,怎么并行等都没有写,实际上这些东西都被封装到了一个底层的库里面。所以现在Twitter就可以有两个团队,一个团队负责写代码,“做什么”;另一个团队负责写底层是怎么实现的。这样就能实现并行开发,这也是Function programming的一个好处。

Image

一个服务器的架构长什么样呢?

上方是你的服务,下方就是你的服务器,包括一些集群的管理、内存、Java的虚拟机、操作系统和硬件。

Image
Image

有很多服务如何整合?

结果整合到一起就是很多层,底层都是一样的,但上层跑了不同的服务:HTTP、聚集器、时间timeline服务等。我们所有东西加在一起,系统就会跑上千个不同的小服务器,而且之间会有各种各样的备份。

Image
Image

那如何来统计出现的服务器情况呢?

比如会统计平均延迟,或统计一些信息来验证服务器是不是出问题了,实际上,在这种大规模里面,一个比较好的方法就是不要拿平均值统计,因为特殊情况会拉低所有情况。举个例子,假设北京有一个人,他年收入是100万,另外99个人每个人年收入都是1元。那平均下来每个人收入都是1万块钱,但实际上大多数人是很穷困的。所以不能拿平均数来算,尤其是在服务器的情况下。所以大家一般都会取中位数(median),百分之90的点,百分之99的点和百分之999的点。我们可以看到,下图是一个服务发生故障的前后。在服务故障发生前,百分之99点的平均延迟是100毫秒,但故障发生后变成400毫秒,发现这个问题以后就会去抓。但如果用平均值,很可能就发现不了这个问题。

Image

这样还有一个好处,当我们做到上面整合的架构以后,每一个负责写代码的团队,只负责处理跟它相关的上下层就行,而不用做那么多交互,所以每个人的东西都非常简单,这也是所谓微服务的一个好处。

Image

如何监控一个服务呢?

实际上针对这个服务在整个栈里面用的时间,会有一个图表达出来。

Image
Image

如何监控一个请求呢?

实际上,基于一个请求,会有一个整个运行的最佳策略,这就需要运行的整个过程的图。这就是我们之前讲的,得到一个用户所有Timeline内容信息的锯齿图,Zipkin,这也是一个开源项目。

Image
Image

如何监控系统的运行情况呢?

因为这个时候失败已经成为常事了。举个例子,假设每秒有30万的请求,有99.99%的成功率,那每秒一定会有30个失败的,所以不能说每次失败怎么样,统计失败率会比单独的失败更重要,而且写代码的时候要为这个失败来进行自动重试。大家不用纠结每次的失败,只要大部分过去就行了。

Image

最后做下总结

  • 学好分治法,走遍天下都不怕。

  • 函数式设计切分做什么和怎么做。做什么是由函数式设计来写,而怎么做由底层的语言和编译器来优化。

  • 面向错误让我们使用了统计域监控。99.9%的点出什么问题了,这样就叫做基于统计域的监控方式。

参考资料:《Real-Time Systems at Twitter》

想要亲手搭建Microservice组成的基于LBS的工业级项目?

欢迎了解BitTiger后端工程师直通车

BitTiger直通车

三个月、三个工业级别大项目

全面提升编程实力与简历背景

硅谷一线IT公司导师、了解痛点的助教

全方位指导及答疑

修改简历、模拟面试、职业咨询

Career Service全程护航

独家面试题库精讲

上百小时基础及面试视频资源助

点击“阅读原文”查看BitTiger直通车

http://t.cn/RYZ1yIh




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK