推ter 架构如何支持上亿用户

CassandraEp 7年前
   <p>谈到设计推ter, 我们首先要问一个本质问题: <strong>设计推ter的基本方法论是什么?</strong></p>    <p>其实是我们计算机设计最基本的方法: <strong>分治法(Divide and Conquer)。</strong></p>    <p>什么是分治法呢?就是把问题不断的拆解,拆解到你可以解决为止,它的艺术在于,从哪个维度来拆解非常考验我们能力。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/c70fe81a4ad5f55cec78375056bb3689.jpg"></p>    <p><img src="https://simg.open-open.com/show/e9282a2600e055e7b28359353d965a1b.png"></p>    <p>如果要求一周开发出推ter,你会怎么做?</p>    <p>你的架构是什么样的呢?</p>    <p>相信你一定不会给出复杂的架构。前端是各种各样的业务逻辑,后端是MySQL数据库,这样就够了。因为这已经解决了当时的问题,满足了一周开发出来的要求。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/6d2f1ed18ed030d33370dcc19338e051.png"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>但随着推ter的成长,我们会遇到各种各样新的挑战:</p>    <ul>     <li> <p>MySQL难扩展。</p> <p>为什么难扩展?因为它把同样的一个数据分成各种关系存在里面,每次取的时候,都要通过Join来进行复杂的操作,这个Join当数据被切分的时候存在更多的服务器上,会变得越来越复杂,所以很难扩展。</p> </li>     <li> <p>小变化也要全部部署。</p> <p>任何变化都需要部署到所有机器上,因为服务不断升级,就变成了每天不断部署,变成了daily deployment。所以每次部署的时候耽误时间很长。</p> </li>     <li> <p>性能差。</p> <p>因为所有服务都要部署在一起,造成了它的内存占用率大,而且部分核心模块还因为最初当时的单线程设计成为了各种瓶颈。</p> </li>     <li> <p>架构混乱。</p> <p>因为所有模块在一起很混乱。</p> </li>    </ul>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>那要怎么破解这些问题?</p>    <p>答案是将问题拆解开来。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e59250f2ba8de79ad76dfc8c8059309f.png"></p>    <p>第一刀将存储切开。</p>    <p>我们看后台,可以拆成存Tweets,存User,存Timeline,存Social Graph不同内容,Timeline可以拿Redis这样的数据库来保存,而对于其它的数据比如Gizzard其实是一个分布式的MySQL数据库。并不是所有数据都不适合用MySQL分布,我们可以把这些适合的用MySQL来shutting一下,而不适合的用别的数据库来存,就是一个存储切开的方法。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/4f52a1de72dda19bcfccedf139bef6ef.jpg"></p>    <p>第二刀是将路由、展示、逻辑切开。</p>    <p>我们可以看到又多出了一层,逻辑层。Tweets,User,Timeline它们分别对应后边的各种数据存储。前端会有外部访问的接口,API访问接口,并且它还保存了以前数据的整个架构,用来进行一些小规模的使用。最前面需要有一个Routing,来将不同的请求分布到不同的API上。所以之后推ter就变成了上百上千个小小的数据模块,包括它的服务模块,他们通过之间的相互调用来完成具体的请求。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/31596c45da7095d9705804984386a7d4.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>如果Lady Gaga发了个推文,会发生什么呢?</p>    <p>首先她发出一个推文,会到达最上面一层的外部模块,负责把推文写出来;接着到了API模块,负责接收这个推文;再往后是Fanout,Fanout将推文的ID推荐给所有订阅这个用户的信息收件箱里,收件箱就是Timeline,比如有400万人订阅了Lady Gaga,就会有400万的收件箱收到这个消息;紧接着由于我们Timeline要发到收件箱里,收件箱必然是一个最复杂的操作,为了优化它的性能,就把它们用分布式的方式存储在Redis里面,Redis是偏向内存的数据库,所以能够很快的存储这些信息;但Redis里存的只是ID,所以当一个用户具体要她推文的时候,实际上要通过Timeline服务去找到这个用户对应的是哪个Redis服务器上存了推文的 Timeline list,接着找到所有ID,然后这个Timeline service把ID的具体内容通过另外的数据库装载进来,最终得到结果反馈给用户。这些保证了我们推整个数据的时候速度最快,能够达到每秒30万次的性能。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2d30311611f1cd2187af7062d1c7f01e.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>虽然有了这样的过程,如何支持搜索呢?</p>    <p>很简单的想法是当我们写入API,它Fanout到每个用户的Timeline list的时候,我们可以拿另外的Ingester把这个推文放到里面,Ingesert最后把它放到Earlybird,就是所谓的倒排索引中。比如有个推文发过来“我喜欢太阳”,就把我,喜欢,太阳,拆成三个词,把这些单词,进行倒排索引,存到Search index里面。这时候实际会用到很多很多Earlybird,这样就能建立很多倒排索引,能够并行的去做。当用户一个请求过来之后,比如搜索“早上吃饭”,就会把morning和eat作为两个关键词发到我的Earlybird集群里,得到结果后Blender会把它组合到一块,并反馈结果。当然很多用户可以并行的向Blender发送请求,从而得到最终结果,这就是我们的搜索服务。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/6828e29ef412b920c189d3702b46d4af.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>如何通知用户新消息到达呢?</p>    <p>第一个方法是另外再开一个Write API,在有新东西发生以后,我把它放到一个Push的服务里,那所有用户只要都连到这个后台里,HTTP PUSH,就会通知他有新消息产生了。同理对Mobile ,会有Mobile Push,当然大家对不同的信息有不同的对接方法,大家可以去仔细考虑下怎么能做到这个样子。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/4cba521d6e72f1ff8728a126b74de8bd.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>如何搭建这样的服务呢?</p>    <p>最基本的是开源项目推ter-Server。</p>    <ul>     <li> <p>配置服务,IP之类;</p> </li>     <li> <p>管理服务,哪些down了、控制、启动等;</p> </li>     <li> <p>日志服务,运行怎么样,以后出问题找谁等;</p> </li>     <li> <p>生命周期服务,什么时候启,什么时候关,什么时候控制;</p> </li>     <li> <p>监控服务,到底有没有出错,出错以后怎么办,互相报警等</p> </li>    </ul>    <p>这些东西合到一起,就构成了这样服务的基本架构。</p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>各个服务之间如何交流?</p>    <p>这就是传统的RPC(远程的进程调用),大家能够通讯的不仅是数据,而且可以通讯命令或请求之类的。这时需要开源项目Finagle。</p>    <ul>     <li> <p>能够提供服务发现,因为有很多服务,所以要找谁发送这个请求呢;</p> </li>     <li> <p>负载均衡,可能有十个人提供服务,先放到谁那儿?</p> </li>     <li> <p>重试,如果失败了怎么办,是否需要重试?</p> </li>     <li> <p>基本的线程池和链接池,大家可以复用,不用每次去创建,浪费资源了;</p> </li>     <li> <p>统计信息的收集;</p> </li>     <li> <p>分布式调试。</p> </li>    </ul>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>如何调用一个服务?</p>    <p>因为从A调用B,大家之间各种远程,写代码上要怎么做呢?可以使用函数来调用(Service as a Function),背后实际上是Function programming的思想。</p>    <p>可以看下这个基本例子:trait Service[Req,Rep]extends(Req=>Future[Rep])</p>    <p>简单理解为:我有一个请求想得到一个Response,就是Request到Response。这里面实际上可以先面向未来实现,之后当它执行的时候,就会得到相对应的结果。</p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>多个服务的调用如何整合在一起?</p>    <p>我们看一看它要如何运行?比如我们有一个请求是得到一个用户的所有Timeline数据。它其实有很多步,第一步是得到User ID;第二步得到User timeline list里的那些消息ID;下一步是针对每一个消息,得到它的每一个具体内容,比如“我早上吃饭”这些内容,这些内容里面可能还有图片,还需要得到图片的数据。这是一个很复杂的过程,但这个箭头表达了它们最基本的执行顺序。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/b0cab807c1a03d77143e4f6be3fc26b2.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>最佳的执行路径是什么?</p>    <p>首先是得到ID,再得到Timeline以后可以并行地读取每个tweet,可能会有的快有的慢,然后又得到一些它的具体信息,比如说图片之类,所以这就是最优策略。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/50f343e098318adb8a4c1b92e53fdcda.jpg"></p>    <p>下面我们来看一看代码:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/6920d6719e3de6f70d05122560f96fbc.jpg"></p>    <p>第一行是得到用户ID,用一个面向未来的方程得到一个用户ID。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/438d064a526ca03eec711809cd3d9c82.jpg"></p>    <p>第二行根据ID拿FlatMap,FlatMap其实是后面针对每一个ID执行的函数,这个函数来得到这个ID用户的Timeline list。所以这也是我们Function programming,或者如果用Spark也会经常碰见的函数。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/0c2fb1caa0245dc76a5be2d9ae1abe4f.jpg"></p>    <p>得到每一个ID以后,实际上要针对每个ID执行,所以需要Map。Map每一个ID是做什么的,得到这个Tweet的具体信息。代码第二行就是根据ID得到他的具体内容。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/0c01a8f490c86f589626692f1aa4df35.jpg"></p>    <p>当然如果这个东西有图片,那需要得到图片。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/b3229f7fb3afc72bc5fea3426d4d9a51.jpg"></p>    <p>中间一步还需要把这些东西并行起来,大家都去单独得到Tweet,并且把它集合放到一块儿去。整个过程我们就得到了最终的代码。</p>    <p><img src="https://simg.open-open.com/show/ebedda8842b4be4038894d1fefd37d03.jpg"></p>    <p>这其实就是 <strong>第三刀:切开“做什么”和“怎么做”。</strong> 在这个代码过程中,所谓面向函数的编程,只是写了想做什么事儿,但具体怎么做,比如从哪个机器找、连接服务器、从谁那儿抓取,怎么并行等都没有写,实际上这些东西都被封装到了一个底层的库里面。所以现在推ter就可以有两个团队,一个团队负责写代码,“做什么”;另一个团队负责写底层是怎么实现的。这样就能实现并行开发,这也是Function programming的一个好处。</p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>一个服务器的架构长什么样呢?</p>    <p>上方是你的服务,下方就是你的服务器,包括一些集群的管理、内存、Java的虚拟机、操作系统和硬件。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1548c0735ff920c6f9e663bee991b6b7.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>有很多服务如何整合?</p>    <p>结果整合到一起就是很多层,底层都是一样的,但上层跑了不同的服务:HTTP、聚集器、时间timeline服务等。我们所有东西加在一起,系统就会跑上千个不同的小服务器,而且之间会有各种各样的备份。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/95405c0251696b35be666aa28389e3c8.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>那如何来统计出现的服务器情况呢?</p>    <p>比如会统计平均延迟,或统计一些信息来验证服务器是不是出问题了,实际上,在这种大规模里面,一个比较好的方法就是不要拿平均值统计,因为特殊情况会拉低所有情况。举个例子,假设北京有一个人,他年收入是100万,另外99个人每个人年收入都是1元。那平均下来每个人收入都是1万块钱,但实际上大多数人是很穷困的。所以不能拿平均数来算,尤其是在服务器的情况下。所以大家一般都会取中位数(median),百分之90的点,百分之99的点和百分之999的点。我们可以看到,下图是一个服务发生故障的前后。在服务故障发生前,百分之99点的平均延迟是100毫秒,但故障发生后变成400毫秒,发现这个问题以后就会去抓。但如果用平均值,很可能就发现不了这个问题。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/eca282cc74f342e796804265b710a325.jpg"></p>    <p>这样还有一个好处,当我们做到上面整合的架构以后,每一个负责写代码的团队,只负责处理跟它相关的上下层就行,而不用做那么多交互,所以每个人的东西都非常简单,这也是所谓微服务的一个好处。</p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>如何监控一个服务呢?</p>    <p>实际上针对这个服务在整个栈里面用的时间,会有一个图表达出来。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/bbd929c00f21968f834d2977816d929a.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>如何监控一个请求呢?</p>    <p>实际上,基于一个请求,会有一个整个运行的最佳策略,这就需要运行的整个过程的图。这就是我们之前讲的,得到一个用户所有Timeline内容信息的锯齿图,Zipkin,这也是一个开源项目。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/8b4b978ba18e1cfe07e971b87f6bd319.jpg"></p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>如何监控系统的运行情况呢?</p>    <p>因为这个时候失败已经成为常事了。举个例子,假设每秒有30万的请求,有99.99%的成功率,那每秒一定会有30个失败的,所以不能说每次失败怎么样,统计失败率会比单独的失败更重要,而且写代码的时候要为这个失败来进行自动重试。大家不用纠结每次的失败,只要大部分过去就行了。</p>    <p><img src="https://simg.open-open.com/show/b23870a6bd5891bd16e7dedafc63cd45.png" alt="推ter 架构如何支持上亿用户" width="550" height="62"></p>    <p>最后做下总结</p>    <ul>     <li> <p>学好分治法,走遍天下都不怕。</p> </li>     <li> <p>函数式设计切分做什么和怎么做。做什么是由函数式设计来写,而怎么做由底层的语言和编译器来优化。</p> </li>     <li> <p>面向错误让我们使用了统计域监控。99.9%的点出什么问题了,这样就叫做基于统计域的监控方式。</p> </li>    </ul>    <p>参考资料:《Real-Time Systems at 推ter》</p>    <p> </p>    <p> </p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s/c9mFDmHRLbuKbqS6aGUdiQ</p>    <p> </p>