mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-12-29 11:36:07 +08:00
mod
This commit is contained in:
79
极客时间专栏/从0开始学微服务/阿忠伯的特别放送/微博技术解密(上) | 微博信息流是如何实现的?.md
Normal file
79
极客时间专栏/从0开始学微服务/阿忠伯的特别放送/微博技术解密(上) | 微博信息流是如何实现的?.md
Normal file
@@ -0,0 +1,79 @@
|
||||
<audio id="audio" title="微博技术解密(上) | 微博信息流是如何实现的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0a/83/0a17d693bce62d5def82675c65be8b83.mp3"></audio>
|
||||
|
||||
专栏结束后,有不少同学留言希望我能讲一些微博基础架构的知识。所以接下来的微博技术解密系列,我将分享微博在信息流架构、存储中间件等方面的经验,希望能给你带来启发和帮助。
|
||||
|
||||
今天我们先来看微博信息流架构,也就是微博的Feed是如何构建的。首先什么是Feed呢?根据我的理解,Feed是互联网2.0时代的产物,它与互联网1.0时代的产物——门户网站最大的不同之处就是Feed不需要用户在各个板块之间来回跳转获取信息,而是把不同的信息都聚合在一起,可以供用户源源不断地访问。这里就涉及了两个问题,一个是信息如何保存,另一个是信息如何聚合。这也是今天我要分享的主要内容,我会从存储架构的角度阐述微博Feed是如何存储的,然后会从业务架构的角度阐述微博Feed是如何聚合的。
|
||||
|
||||
## 微博Feed存储架构
|
||||
|
||||
我们知道,微博Feed是由关注人的微博聚合在一起组成的,所以要存储每个人发的微博,那么在设计存储架构时主要需要注意三个问题:
|
||||
|
||||
<li>
|
||||
每秒数据写入量,也就是每秒发博量是多大。
|
||||
</li>
|
||||
<li>
|
||||
每秒数据访问量,也就是每秒微博请求量是多大。
|
||||
</li>
|
||||
<li>
|
||||
是否有冷热数据之分,也就是微博的请求是否有时间特点。
|
||||
</li>
|
||||
|
||||
结合微博的业务场景,我来回答上面提出的三个问题。首先是每秒发博量,这里要考虑到极端情况,比如元旦零点,瞬间会有大量用户发博,达到数万QPS。再来看下每秒微博请求量,同样要考虑到在热点事件时,比如“春晚”时会有大量用户访问微博,请求量也会达到数万QPS;并且每个用户关注的不止是一个人,假设关注数的平均值是200,那么微博数据的请求量就是几百万QPS。除此之外,微博的访问也是有时间特点的,用户一般访问新发微博的概率要远远大于一周前发的微博,所以说微博数据也是有冷热之分的。
|
||||
|
||||
这三个问题共同决定了微博的存储架构应该如何设计。在讨论微博存储架构前,我们先来看看目前业界比较成熟的存储方案,主要分为下面几种。
|
||||
|
||||
<li>
|
||||
以MySQL为代表的关系型数据库。主要用来存储结构比较固定的数据,因为使用的是磁盘存储,所以写入和访问能力主要取决于磁盘的读写能力。而磁盘主要分为SAS盘和SSD盘,也就是机械盘和固态盘,两者的读写能力有一定的差距,SSD盘读写能力是SAS盘的3倍左右,不过QPS都在千级别。磁盘存储的特点是不易丢失数据,可以永久保存。
|
||||
</li>
|
||||
<li>
|
||||
以Memcached和Redis为代表的内存存储。服务器的内存大小一般要远小于磁盘,在几十GB到几百GB之间,而磁盘通常都是TB级。内存存储的优势就是读写速度快,读写能力能到几十万QPS,远远大于磁盘存储。但由于数据存储在内存中,如果进程挂掉或者机器重启,内存中的数据就清空了。
|
||||
</li>
|
||||
<li>
|
||||
HBase为代表的分布式存储。属于非关系型数据库,它的特点是数据结构不固定,因此适合非结构化的数据存储,而且由于采用了分布式存储,使用HDFS作为底层文件存储系统,所以可以存储海量数据,并且具备非常高的写入性能。
|
||||
</li>
|
||||
|
||||
讲到这里,结合前面提到的微博业务场景,你觉得存储架构该如何设计呢?
|
||||
|
||||
根据微博的实际业务情况,用户的微博需要永久保存,也就是进行持久化存储,而且微博的数据结构是固定的,优先考虑采用关系型数据库,因此MySQL是一种选择。但是考虑到单台MySQL读能力的极限是不到一万QPS,而微博的数据请求量是几百万QPS,如果单纯使用MySQL来应对的话,需要上千台服务器,成本非常高。还有一点就是微博数据的请求是有冷热之分的,一周外的数据访问的概率要远小于一周内的数据。综合以上几点考虑,可以在MySQL存储的前面,再加一层缓存,比如使用Memcached,存储最近一周内的微博,而用户的全量微博数据则持久化存储在MySQL中。假设用户访问一周内微博的概率是99%,那么对缓存的请求量就是几百万QPS,而对数据库的请求量就只有几万QPS了,而缓存的读写能力是几十万QPS,相比较而言,需要的机器数要远小于只使用数据库了。
|
||||
|
||||
## 微博Feed业务架构
|
||||
|
||||
经过前面的讲解,假设我们已经使用MySQL + Memcached的双层存储架构解决了微博的存储问题,接下来面临的问题就是,如何将存储的关注人的微博数据聚合成一股源源不断的信息流以供用户访问。
|
||||
|
||||
根据我的经验,信息流聚合一般有三种架构:**推模式、拉模式**以及**推拉结合**,下面我来详细讲解这三种架构。
|
||||
|
||||
**1. 推模式**
|
||||
|
||||
推模式,顾名思义就是把关注人的发的微博,主动推送给粉丝,就像下图描述的那样。推模式相当于有一个收件箱,当关注人发微博的时候,就给所有粉丝的收件箱推送这条微博,这样的话,不管你关注了多少人,他们发的微博都会主动推送到你的收件箱,你只需要访问自己的收件箱,就可以获取到所有关注人发的微博了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/35/f3/355aefd37d002b5c0377c01b330b81f3.png" alt="">
|
||||
|
||||
**2. 拉模式**
|
||||
|
||||
拉模式与推模式恰恰相反,就像图里那样每个人都有一个发件箱,发微博的时候就把微博存储到发件箱,这样的话,如果要获取所有关注人发的微博,就需要遍历关注人的发件箱列表,取出所有关注人的发件箱,然后按照时间顺序聚合在一起。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/1e/c2a9f7b3c71f58ad77dd185b7d045c1e.png" alt="">
|
||||
|
||||
我们先来对比下微博Feed采用哪种架构比较合适。首先来看推模式,它的特点是聚合简单,每个人只要查看自己的收件箱就可以获取到关注人发的所有微博,但缺点是即使粉丝没有请求Feed,每个人发的微博也都要给所有粉丝推送一遍,所以写入量会非常高,尤其是对于具有上亿粉丝的用户来说,发一条微博需要给上亿粉丝的收件箱推送这条微博,这个写入量是非常大的,给处理机和数据库带来的压力可想而知。并且同一条微博会存储多份,占用的存储空间也非常大,并且如果需要修改或者删除微博,需要请求所有粉丝的收件箱。考虑到微博是公开的社交媒体平台,拥有上千万粉丝的用户不在少数,所以采用推模式的话,存储成本以及数据更新成本会非常高,需要大量的缓存和数据库以及队列机来更新。早期Twitter的Feed就采用了推模式,经常出现更新延迟,就是因为大量的写入带来巨大压力所导致。
|
||||
|
||||
再来看下拉模式,它的特点是聚合逻辑复杂,每个人要想查看关注人发的所有微博,需要遍历关注人列表,获取所有微博后再按照时间聚合在一起,对数据的请求量和聚合带来的计算量要远远大于推模式。但因为每个用户的微博只存在自己的发件箱列表中,所以相比于推模式来说,存储成本要小得多,并且如果有数据修改,只需要修改自己的发件箱列表就可以了。
|
||||
|
||||
**3. 推拉结合**
|
||||
|
||||
在对比了推模式和拉模式各自的优缺点之后,也就自然而然产生一种新想法,是不是可以采用推拉结合的方式,各取两者的优点呢?针对关注的粉丝量大的用户采用拉模式,而对于一般用户来说,他们的粉丝量有限,采用推模式问题不大,这样的话一个用户要获取所有关注人的微博,一方面要请求粉丝量大的关注人的发件箱列表,另一方面要请求自己的收件箱列表,再把两者聚合在一起就可以得到完整的Feed了。
|
||||
|
||||
虽然推拉结合的方式看似更加合理,但是由此带来的业务复杂度就比较高了,因为用户的粉丝数是不断变化的,所以对于哪些用户使用推模式,哪些用户使用拉模式,维护起来成本就很高了。所以综合考量下来,微博Feed采用了拉模式。
|
||||
|
||||
前面提到采用拉模式的话,需要拉取所有关注人的发件箱,在关注人只有几十几百个的时候,获取效率还是非常高的。但是当关注人上千以后,耗时就会增加很多,实际验证获取超过4000个用户的发件箱,耗时要几百ms,并且长尾请求(也就是单次请求耗时超过1s)的概率也会大大增加。为了解决关注人数上千的用户拉取Feed效率低的问题,我们采用了分而治之的思想,在拉取之前把用户的关注人分为几组,并行拉取,这样的话就把一次性的聚合计算操作给分解成多次聚合计算操作,最后再把多次聚合计算操作的结果汇总在一起,类似于MapReduce的思路。经过我们的实际验证,通过这种方法可以有效地降级关注人数上千用户拉取Feed的耗时,长尾请求的数量也大大减少了。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲解了微博Feed的存储架构以及业务架构的选型,你可以看到最终方案都是结合微博的业务场景以及当时存储的成熟度做出的选择。微博诞生于2009年,并在2010年进行了平台化改造,确定了现在这一套存储和业务架构。当时的存储成熟度决定了MySQL + Memcached的组合是最优解,并没有使用Redis、HBase等后来更为先进的数据库;同时由于微博Feed的业务特点,选择了拉模式的业务架构,并针对关注人数上千的场景进行了拉取方式的优化,也被实践证明是行之有效的手段。
|
||||
|
||||
## 思考题
|
||||
|
||||
如果让你来设计微博的存储架构,除了使用MySQL + Memcached,你觉得还可以使用哪些存储?
|
||||
|
||||
欢迎你在留言区写下自己的思考,与我一起讨论。
|
||||
|
||||
|
||||
71
极客时间专栏/从0开始学微服务/阿忠伯的特别放送/微博技术解密(下)| 微博存储的那些事儿.md
Normal file
71
极客时间专栏/从0开始学微服务/阿忠伯的特别放送/微博技术解密(下)| 微博存储的那些事儿.md
Normal file
@@ -0,0 +1,71 @@
|
||||
<audio id="audio" title="微博技术解密(下)| 微博存储的那些事儿" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0e/f7/0ee276083d103a39f92ac419edd362f7.mp3"></audio>
|
||||
|
||||
今天是微博技术解密系列的第二期,我们来聊聊微博存储的使用经验。上一期“微博技术解密”我讲到微博主要使用了两大类存储:一类是数据库,主要以MySQL为主;一类是缓存,主要以Memcached和Redis为主。
|
||||
|
||||
今天我来分享一下微博在使用数据库和缓存方面的经验,也欢迎你给我留言一起切磋讨论。
|
||||
|
||||
## MySQL
|
||||
|
||||
上一期我讲到微博Feed的存储使用了两层的结构,为了减少对MySQL数据库的访问压力,在前面部署了Memcached缓存,挡住了99%的访问压力,只有1%的请求会访问数据库。然而对于微博业务来说,这1%的请求也有几万QPS,对于单机只能扛几千QPS的MySQL数据库来说还是太大了。为此我们又对数据库端口进行了拆分,你可以看下面的示意图,每个用户的UID是唯一的,不同UID的用户按照一定的Hash规则访问不同的端口,这样的话单个数据库端口的访问量就会变成原来的1/8。除此之外,考虑到微博的读请求量要远大于写请求量,所以有必要对数据库的读写请求进行分离,写请求访问Master,读请求访问Slave,这样的话Master只需要一套,Slave根据访问量的需要可以有多套,也就是“一主多从”的架构。最后考虑到灾备的需要,还会在异地部署一套冷备的灾备数据库,平时不对外提供线上服务,每天对所有最新的数据进行备份,以防线上数据库发生同时宕机的情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/b3/ac361340ab002db47cafaa596c4293b3.png" alt="">
|
||||
|
||||
## Memcached
|
||||
|
||||
在MySQL数据库前面,还使用了Memcached作为缓存来承担几百万QPS的数据请求,产生的带宽问题是最大挑战。为此微博采用了下图所示的多层缓存结构,即L1-Master-Slave,它们的作用各不相同。
|
||||
|
||||
L1主要起到分担缓存带宽压力的作用,并且如果有需要可以无限进行横向扩展,任何一次数据请求,都随机请求其中一组L1缓存,这样的话,假如一共10组L1,数据请求量是200万QPS,那么每一组L1缓存的请求量就是1/10,也就是20万QPS;同时每一组缓存又包含了4台机器,按照用户UID进行Hash,每一台机器只存储其中一部分数据,这样的话每一台机器的访问量就只有1/4了。
|
||||
|
||||
Master主要起到防止访问穿透到数据库的作用,所以一般内存大小要比L1大得多,以存储尽可能多的数据。当L1缓存没有命中时,不能直接穿透到数据库,而是先访问Master。
|
||||
|
||||
Slave主要起到高可用的目的,以防止Master的缓存宕机时,从L1穿透访问的数据直接请求数据库,起到“兜底”的作用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fc/2d/fc8a927caf6d2991b0a2562863441f2d.png" alt="">
|
||||
|
||||
## Redis
|
||||
|
||||
微博的存储除了大量使用MySQL和Memcached以外,还有一种存储也被广泛使用,那就是Redis。并且基于微博自身的业务特点,我们对原生的Redis进行了改造,因此诞生了两类主要的Redis存储组件:CounterService和Phantom。
|
||||
|
||||
**1. CounterService**
|
||||
|
||||
CounterService的主要应用场景就是计数器,比如微博的转发、评论、赞的计数。早期微博曾采用了Redis来存储微博的转发、评论、赞计数,但随着微博的数据量越来越大,发现Redis内存的有效负荷还是比较低的,它一条KV大概需要至少65个字节,但实际上一条微博的计数Key需要8个字节,Value大概4个字节,实际上有效的只有12个字节,其余四十多个字节都是被浪费的。这还只是单个KV,如果一条微博有多个计数的情况下,它的浪费就更多了,比如转评赞三个计数,一个Key是long结构,占用8个字节,每个计数是int结构,占用4个字节,三个计数大概需要20个字节就够了;而使用Redis的话,需要将近200个字节。正因为如此,我们研发了CounterService,相比Redis来说它的内存使用量减少到原来的1/15~1/5。而且还进行了冷热数据分离,热数据放到内存里,冷数据放到磁盘上,并使用LRU,如果冷数据重新变热,就重新放到内存中。
|
||||
|
||||
你可以看下面的示意图,CounterService的存储结构上面是内存下面是SSD,预先把内存分成N个Table,每个Table根据微博ID的指针序列,划出一定范围。任何一个微博ID过来先找到它所在的Table,如果有的话,直接对它进行增减;如果没有,就新增加一个Key。有新的微博ID过来,发现内存不够的时候,就会把最小的Table dump到SSD里面去,留着新的位置放在最上面供新的微博ID来使用。如果某一条微博特别热,转发、评论或者赞计数超过了4个字节,计数变得很大该怎么处理呢?对于超过限制的,我们把它放在Aux Dict进行存放,对于落在SSD里面的Table,我们有专门的Index进行访问,通过RDB方式进行复制。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5a/7b/5ac693490234b05eb37cec6841a3097b.png" alt="">
|
||||
|
||||
**2. Phantom**
|
||||
|
||||
微博还有一种场景是“存在性判断”,比如某一条微博某个用户是否赞过、某一条微博某个用户是否看过之类的。这种场景有个很大的特点,它检查是否存在,因此每条记录非常小,比如Value用1个位存储就够了,但总数据量又非常巨大。比如每天新发布的微博数量在1亿条左右,是否被用户读过的总数据量可能有上千亿,怎么存储是个非常大的挑战。而且还有一个特点是,大多数微博是否被用户读过的存在性都是0,如果存储0的话,每天就得存上千亿的记录;如果不存的话,就会有大量的请求最终会穿透Cache层到DB层,任何DB都没有办法抗住那么大的流量。
|
||||
|
||||
假设每天要存储上千亿条记录,用原生的Redis存储显然是不可行的,因为原生的Redis,单个KV就占了65个字节,这样每天存储上千亿条记录,需要增加将近6TB存储,显然是不可接受的。而用上面提到的微博自研的CounterService来存储的话,一个Key占8个字节,Value用1个位存储就够了,一个KV就占大约8个字节,这样每天存储上千亿条记录,需要增加将近800GB存储。虽然相比于原生的Redis存储方案,已经节省了很多,但存储成本依然很高,每天将近1TB。
|
||||
|
||||
所以就迫切需要一种更加精密的存储方案,针对存在性判断的场景能够最大限度优化存储空间,后来我们就自研了Phantom。
|
||||
|
||||
就像下图所描述的那样,Phantom跟CounterService一样,采取了分Table的存储方案,不同的是CounterService中每个Table存储的是KV,而Phantom的每个Table是一个完整的BloomFilter,每个BloomFilter存储的某个ID范围段的Key,所有Table形成一个列表并按照Key范围有序递增。当所有Table都存满的时候,就把最小的Table数据清除,存储最新的Key,这样的话最小的Table就滚动成为最大的Table了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/1c/5caa16bfda4b80f635276501b257871c.png" alt="">
|
||||
|
||||
下图描述了Phantom的请求处理过程,当一个Key的读写请求过来时,先根据Key的范围确定这个Key属于哪个Table,然后再根据BloomFilter的算法判断这个Key是否存在。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/d4/3dd0fc260df19565c223f981564a1cd4.png" alt="">
|
||||
|
||||
这里我简单介绍一下BloomFilter是如何判断一个Key是否存在的,感兴趣的同学可以自己搜索一下BloomFilter算法的详细说明。为了判断某个Key是否存在,BloomFilter通过三次Hash函数到Table的不同位置,然后判断这三个位置的值是否为1,如果都是1则证明Key存在。
|
||||
|
||||
来看下面这张图,假设x1和x2存在,就把x1和x2通过Hash后找到的三个位置都设置成1。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/80/018815b6621a6e55fb5a69e0764f1180.png" alt="">
|
||||
|
||||
再看下面这张图,判断y1和y2是否存在,就看y1和y2通过Hash后找到的三个位置是否都是1。比如图中y1第二个位置是0,说明y1不存在;而y2的三个位置都是1,说明y2存在。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/47/9a/4710ee8196a1d979515457718d926e9a.png" alt="">
|
||||
|
||||
Phantom正是通过把内存分成N个Table,每一个Table内使用BloomFilter判断是否存在,最终每天使用的内存只有120GB。而存在性判断的业务场景最高需要满足一周的需求,所以最多使用的内存也就是840GB。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我给你讲解了微博业务中使用范围最广的三个存储组件:一个是MySQL,主要用作持久化存储数据,由于微博数据访问量大,所以进行了数据库端口的拆分来降低单个数据库端口的请求压力,并且进行了读写分离和异地灾备,采用了Master-Slave-Backup的架构;一个是Memcached,主要用作数据库前的缓存,减少对数据库访问的穿透并提高访问性能,采用了L1-Master-Slave的架构;一个是Redis,基于微博自身业务需要,我们对Redis进行了改造,自研了CounterService和Phantom,分别用于存储微博计数和存在性判断,大大减少了对内存的使用,节省了大量机器成本。
|
||||
|
||||
专栏更新到这里就要跟同学们说再见了,感谢你们在过去大半年时间里的陪伴,值此新春到来之际,老胡给您拜年啦,恭祝各位同学在新的一年里,工作顺顺利利,生活开开心心!
|
||||
|
||||
|
||||
69
极客时间专栏/从0开始学微服务/阿忠伯的特别放送/阿忠伯的特别放送 | 答疑解惑01.md
Normal file
69
极客时间专栏/从0开始学微服务/阿忠伯的特别放送/阿忠伯的特别放送 | 答疑解惑01.md
Normal file
@@ -0,0 +1,69 @@
|
||||
<audio id="audio" title="阿忠伯的特别放送 | 答疑解惑01" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/12/eb/124024d898790a736b8e3987d43f73eb.mp3"></audio>
|
||||
|
||||
你好,我是胡忠想,我的专栏虽然已经结束了,但我还会一直在专栏里为同学们答疑解惑。所以,即使你现在刚加入到专栏学习中,也可以随时留下你的疑问。同学们问得比较多的问题我会记录下来专门写成一期答疑文章,希望和你一起详细讨论。
|
||||
|
||||
今天是答疑的第一期,我选取了前面几期比较有代表性的问题,其中很多也是我在实践过程中踩过的坑,针对这些问题我来分享一下我的体会。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/2e/dc985041d4cf7691014f3a8be94fad2e.jpeg" alt=""><img src="https://static001.geekbang.org/resource/image/0d/78/0d99b3e1eb65599dffda535d8aafd778.jpeg" alt=""><img src="https://static001.geekbang.org/resource/image/0e/e6/0efa0b0356e169dd2b2cf714b4418de6.jpeg" alt="">
|
||||
|
||||
这三个问题都是关于服务拆分的,下面我就从**微服务拆分粒度、拆分方式以及微博微服务拆分实践三个方面**来谈谈服务拆分那些事儿。
|
||||
|
||||
首先来看微服务拆分粒度。微服务拆分的粒度可以很粗也可以很细,具体要视团队的规模和业务复杂度而定。通常来讲,当团队的规模逐渐变大时,一个业务模块同时就会有多个开发人员修改,在需求研发期间,经常需要协调合并代码,沟通打包上线,研发成本越来越高,这时就需要对这个业务进行微服务拆分了,变成多个独立的微服务,分别进行代码开发、打包上线,实现研发的解耦。但是也要避免一个极端,就是把微服务拆分得太细,出现一个开发需要维护十几个以上服务的情况,这样的话团队维护的成本太高也不可取。
|
||||
|
||||
再来看下微服务拆分方式。在具体进行微服务拆分时,通常有两种方式,一种是按照服务的关联维度进行拆分,一种是按照数据库隔离维度进行拆分。按照服务关联维度拆分指的是两个服务是否具有紧密耦合的业务逻辑,如果能从业务逻辑上进行解耦的话,不同的业务逻辑就可以拆分为单独的微服务。按照数据库隔离维度拆分是指如果两个业务逻辑依赖的数据库不同,那么就可以把它们拆分为两个微服务,分别依赖各自的数据库。
|
||||
|
||||
实际在进行微服务拆分时,通常是两种方式相结合,**先进行数据库隔离维度的拆分,再进行服务关联维度的拆分**。以微博的业务为例,一开始是个大的单体应用,所有的业务逻辑代码都耦合部署在一起,一个业务需求往往涉及多个业务逻辑,代码提交和打包上线的过程需要多个开发人员都参与其中。为了减少代码耦合带来的研发效率低的问题,首先按照数据库隔离维度进行拆分,微博内容相关的业务依赖的是微博内容数据库、微博用户相关的业务依赖的是微博用户数据库,对应的就把微博内容相关的业务拆分为内容服务、把微博用户相关的业务拆分为用户服务,从而实现对内容服务和用户服务进行解耦,分别部署成单独的服务。然后再按照服务关联维度进行拆分,又把内容服务中的微博服务和互动服务进行了拆分,跟微博相关的业务逻辑拆分为微博服务,跟互动相关的业务逻辑拆分为互动服务。
|
||||
|
||||
除此之外,在实际进行微服务拆分的时候,还需要考虑其他因素,比如**微服务拆分后的性能是否可以接受**。通常来讲,从单体应用拆分为微服务,服务调用由进程内调用变成不同服务器上进程之间的调用,经过网络传输必定会带来一定的损耗,所以在实际拆分时,要看业务对这个开销的敏感程度。以微博的feed业务为例,要展示用户关注的好友的timeline,需要首先聚合用户关注的所有人,再获取每个人的微博列表,最后把所有人的微博列表按照时间进行排序。在单体应用时期,feed接口的平均耗时在200ms左右,而把调用用户服务获取关注人列表,从进程内调用拆分为RPC调用后,平均耗时从5ms左右增加到8ms左右,这个损耗对于业务来说是完全可以接受的,所以我们就把用户服务单独拆分为一个单独的微服务。另外我讲一个服务化拆分的反例,在微博的直播card展示时,需要聚合播放计数,最开始采用的方案是进行服务化拆分,把播放计数单独拆分为一个微服务,这样的话直播card聚合播放计数时,就通过RPC的方式来调用播放计数服务。但直播card本身业务耗时不到10ms,而拆分为微服务后,调用播放计数接口耗时从2ms增加到5ms左右,这样相对来说开销就有点大了,并且这个接口的调用量也很高,所以后来我们又把直播card聚合播放计数从RPC调用改成了进程内的调用。所以说具体进行服务化拆分时,一般都是先进行稍微大粒度的拆分,业务验证稳定后,再逐步进行细化的服务拆分。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/70/20515bffab7540109eaa36888273e470.jpeg" alt="">
|
||||
|
||||
微博的大多数微服务都是从以前的单体应用中拆分出来的,所以为了保持业务上的延续性,接口的定义依然保持跟原先保持一致,只不过是单独抽取到一个公共的JAR包中,这样的话服务的每个调用方都需要引入服务的接口定义所在的JAR包,对于服务调用方来说跟原先的单体应用内的使用方式没有什么区别。你可以理解为是通过Java代码来定义接口规范,不需要额外的接口文档、元数据之类的。
|
||||
|
||||
还有一种业务场景,就是去年我们开始推进的跨语言服务化改造,就是把原先的PHP业务方调用Java业务,从HTTP调用改造成RPC调用,这个时候就涉及到如何定义接口了。在采用HTTP调用的时期,接口定义是通过wiki文档来维护的,服务调用方通过查看接口的wiki定义,就可以知道接口的参数、返回值以及错误代码等。迁移到RPC调用时,为了做到平滑过渡,服务调用方可以继续参照原先接口的wiki文档,接口调用的参数仍然保持不变,返回值也继续沿用原有的JSON格式,只是调用的URL从原先的HTTP格式修改为扩展类型的URL。
|
||||
|
||||
目前比较规范的接口定义方式主要有两种,一种是以Swagger为代表,可以用来描述Restful API,并且具备可视化的API编辑功能;一种是以PB文件为代表的,用来描述跨语言的接口定义,你可以根据自己的实际需要来决定采用哪种方式。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/26/7d1b287167bb5526f2844ee548a47226.jpeg" alt="">
|
||||
|
||||
一般来说,服务化框架都会集成监控日志功能。以微博的服务化框架Motan为例,会将每一次服务调用的数据进行收集,然后以JSON格式的方式,统一输出到服务部署路径下logs目录中的profile.log,它的格式你可以参考下面的代码。
|
||||
|
||||
```
|
||||
2018-11-18 16:58:32 {"type":"MOTAN","name":"brha_zdelay_cn.sina.api.data.service.SinaUserService.getBareSinaUsers(long[])","slowThreshold":"200","total_count":"266","slow_count":"0","avg_time":"10.00","interval1":"0","interval2":"266","interval3":"0","interval4":"0","interval5":"0","p75":"10.00","p95":"10.00","p98":"10.00","p99":"10.00","p999":"10.00","biz_excp":"0","other_excp":"0","avg_tps":"26","max_tps":"31","min_tps":"23"}
|
||||
|
||||
```
|
||||
|
||||
上面这段profile.log主要分为三个部分。
|
||||
|
||||
1.“type”字段,标识这个监控项是什么类型,比如Motan代表的是接口调用。
|
||||
|
||||
2.“name”字段,标识这个监控项的名字是什么,对应上面这段监控log,名字就是
|
||||
|
||||
```
|
||||
brha_zdelay_cn.sina.api.data.service.SinaUserService.getBareSinaUsers(long[])
|
||||
|
||||
```
|
||||
|
||||
3.自定义字段,用来具体打印监控项的各种指标,比如total_count字段代表是调用总量,avg_time字段代表的是平均耗时,p999字段代表的是99.9%的调用耗时在多少以下等。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/93/417aace7b4697ed058b7a632eb676793.jpeg" alt="">
|
||||
|
||||
数据处理的实时操作,在服务追踪中主要用于定位线上问题,主要有两个业务场景。
|
||||
|
||||
**第一个场景是快速定位线上服务问题**。以下图所示的微博业务为例,一次用户feed请求,需要经过链路上的多个环节,如果用户的feed请求变慢,就需要快速定位到底是链路上的哪个环节导致。此时就需要能够将服务追踪收集到的每个环节的调用数据,进行实时聚合,计算每个环节的平均耗时和接口成功率等,然后输出到监控Dashboard上。这样的话,通过一览监控Dashboard上各个环节的实时服务状况,就能快速定位问题了。
|
||||
|
||||
**第二个场景是确定某一次用户请求失败的原因**。继续以图中所示的微博业务为例,某一次用户请求失败了,需要能定位具体在哪个环节导致调用失败,这时候就需要根据用户请求的traceId和spanId,把一次用户请求经过各个环节的服务调用串联起来,并且找出调用的前后文,这样的话就能从MAPI的调用开始,接着查看Feed API的调用,再继续查看Feed RPC的调用,以此类推,一层层往下查看,最终就可以定位到底是在哪一层发生了错误。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b6/7b/b63bd8abd8e90ff75fb012068d419f7b.png" alt="">
|
||||
|
||||
数据处理的离线操作,一般应用在需要统计一段时间内某个服务的调用量、平均耗时、成功率等,这一段时间可能是一个小时,也有可能是一天、一周或者一个月等,所以需要根据一段时间内产生的追踪日志,进行离线计算才可以得出。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/c6/233e10fb8c02ea82039a83a9e446dec6.jpeg" alt="">
|
||||
|
||||
先来回答“僵尸节点”的问题。在微博的业务场景下,服务进行缩容有两个步骤,第一步是调用注册中心的反注册接口把节点从服务节点列表中删除,第二步是回收服务器,并把节点从服务池列表中删除。这里就存在一种可能是第一步操作失败,第二步操作成功,那么注册中心中就会残存一些已经不存在的节点,这些节点也不会汇报心跳,因此会被注册中心标记为“dead”状态,也就是我们所说的“僵尸节点”,因此需要每天定时清理注册中心中存在的“僵尸节点”。我是通过比对服务池的节点列表与注册中心的节点列表,如果某个节点在注册中心中存在,但在服务池的节点列表中找不到,就认为是“僵尸节点”,需要把它删除。
|
||||
|
||||
再来看批量反注册接口的问题。在微博的业务场景下,存在一个节点同时部署了多个微服务的情况,所以在进行节点缩容时,需要多次调用注册中心的反注册接口,以把同一个节点从多个服务的节点列表中删除。有一种简单的方案,是在反注册的时候,直接根据节点的IP进行反注册,这样的话注册中心会查询这个节点注册的所有服务,然后一次性把这个节点从所有服务的列表中删除。理论上批量删除只需要调用一次就可以把节点从注册中心中删除,相比于之前多次调用成功率要高很多,并且从我们实际使用效果上来看,也不会再存在“僵尸节点”的情况了。
|
||||
|
||||
今天的答疑就到这里啦,欢迎你在留言区写下自己学习、实践微服务的心得和体会,与我和其他同学一起讨论。你也可以点击“请朋友读”,把今天的内容分享给好友,或许这篇文章可以帮到他。
|
||||
|
||||
|
||||
41
极客时间专栏/从0开始学微服务/阿忠伯的特别放送/阿忠伯的特别放送 | 答疑解惑02.md
Normal file
41
极客时间专栏/从0开始学微服务/阿忠伯的特别放送/阿忠伯的特别放送 | 答疑解惑02.md
Normal file
@@ -0,0 +1,41 @@
|
||||
<audio id="audio" title="阿忠伯的特别放送 | 答疑解惑02" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0d/6b/0d445e869c50708fcbcb33ccf7f06a6b.mp3"></audio>
|
||||
|
||||
你好,我是胡忠想。今天我继续来给同学们做答疑,第二期答疑主要涉及微服务架构、注册中心和负载均衡算法,需要一定的基础,如果对这些内容不了解,可以先返回[专栏第14期](http://time.geekbang.org/column/article/39809)、[第17期](http://time.geekbang.org/column/article/40684)和[第18期](http://time.geekbang.org/column/article/40883)复习一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/d0/93d4e17e36d4a31c7d44507aa84bb9d0.png" alt="">
|
||||
|
||||
专栏里我主要讲的是基于RPC通信的微服务架构,除此之外还有一种微服务架构是基于MQ消息队列通信的,下面我就从这两种架构不同的适用场景来给你讲讲它们的区别。
|
||||
|
||||
基于RPC通信的微服务架构,其特点是一个服务依赖于其他服务返回的结果,只有依赖服务执行成功并返回后,这个服务才算调用成功。这种架构适用于用户请求是读请求的情况,就像下图所描述的那样,比如微博用户的一次Feed API请求,会调用Feed RPC获取关注人微博,调用Card RPC获取微博中的视频、文章等多媒体卡片信息,还会调用User RPC获取关注人的昵称和粉丝数等个人详细信息,只有在这些信息都获取成功后,这次用户的Feed API请求才算调用成功。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/d4/5d4b66935a2887ad9d79e55cebe3a0d4.png" alt="">
|
||||
|
||||
而基于MQ消息队列通信的架构,其特点是服务之间的交互是通过消息发布与订阅的方式来完成的,一个服务往MQ消息队列发布消息,其他服务从MQ消息队列订阅消息并处理,发布消息的服务并不等待订阅消息服务处理的结果,而是直接返回调用成功。这种架构适用于用户请求是写请求的情况,就像下图所描述的那样,比如用户的写请求,无论是发博、评论还是赞都会首先调用Feed API,然后Feed API将用户的写请求消息发布到MQ中,然后就返回给用户请求成功。如果是发博请求,发博服务就会从MQ中订阅到这条消息,然后更新用户发博列表的缓存和数据库;如果是评论请求,评论服务就会从MQ中订阅到这条消息,然后更新用户发出评论的缓存和数据库,以及评论对象收到评论的缓存和数据库;如果是赞请求,赞服务就会从MQ中订阅到这条消息,然后更新用户发出赞的缓存和数据库,以及赞对象收到的赞的缓存和数据库。这样设计的话,就把写请求的返回与具体执行请求的服务进行解耦,给用户的体验是写请求已经执行成功,不需要等待具体业务逻辑执行完成。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ce/7e/ce4f9ddfb2f6bb6d9238c6be9fa6a77e.png" alt="">
|
||||
|
||||
总结一下就是,基于RPC通信和基于MQ消息队列通信的方式都可以实现微服务的拆分,两者的使用场景不同,RPC主要用于用户读请求的情况,MQ主要用于用户写请求的情况。对于大部分互联网业务来说,读请求要远远大于写请求,所以针对读请求的基于RPC通信的微服务架构的讨论也更多一些,但并不代表基于MQ消息队列不能实现,而是要区分开它们不同的应用场景。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/5b/420ee09db55e76e380a8f9dbc47a6a5b.png" alt="">
|
||||
|
||||
要回答上面这三个问题,需要我来详细讲讲微博在使用注册中心时遇到的各种问题以及解决方案,主要包括三部分内容。
|
||||
|
||||
**1. 心跳开关保护机制**。在专栏第17期,我讲过心跳开关保护机制是为了防止网络频繁抖动时,引起服务提供者节点心跳上报失败,从而导致注册中心中可用节点不断变化,使得大量服务消费者同时去请求注册中心获取最新的服务提供者节点列表,把注册中心的带宽占满。为了减缓注册中心带宽的占用,一个解决方案是,只给其中1/10的服务消费者返回最新的服务提供者节点列表信息,这样注册中心带宽就能减少到原来的1/10。在具体实践时,我们每次随机取10%,所以对于任意服务消费者来说,获取到最新服务提供者节点列表信息的时刻都是不固定的。在我的实践过程中,对于一个拥有上千个服务消费者的服务来说,某个服务消费者可能长达半小时后仍然没有获取到最新的服务提供者节点列表信息。所以说这种机制是有一定缺陷的,尤其是在服务正常情况下,心跳开关应该是关闭的,只有在网络频繁抖动时才打开。当网络频繁抖动时,注册中心的带宽就会暴涨,可以轻松把千兆网卡的前端机带宽打满,此时监控到带宽被打满时,就应该立即开启心跳开关保护机制。
|
||||
|
||||
**2. 服务节点摘除保护机制**。设计心跳开关保护机制的目的,就是为了应对网络频繁抖动时引起的服务提供者节点心跳上报失败的情况。这个时候,注册中心会大量摘除服务提供者节点,从而引起服务提供者节点信息的变化。但其实大部分服务提供者节点本来是正常的,注册中心大量摘除服务提供者节点的情况是不应该发生的,所以可以设置一个服务节点摘除的保护机制,比如设置一个上限20%,正常情况下也不会有20%的服务节点被摘除,这样的话即使网络频繁抖动,也不会有大量节点信息变更,此时就不会出现大量服务消费者同时请求注册中心获取最新的服务提供者节点列表,进而把注册中心的带宽给占满。但这个机制也有一个缺陷,就是一些异常的节点即使心跳汇报异常应该被摘除,但也会因为摘除保护机制的原因没有从服务的可用节点列表中去掉,因此可能会影响线上服务。
|
||||
|
||||
**3. 静态注册中心机制**。心跳开关保护机制和服务节点摘除保护机制都是治标不治本的权宜之计,不能根本解决网络频繁抖动情况下,引起的注册中心可用服务节点列表不准确的问题。所以我们提出了静态注册中心的机制,也就是注册中心中保存的服务节点列表只作为服务消费者的参考依据,在每个服务消费者这一端都维护着各自的可用服务节点列表,是否把某个服务节点标记为不可用,完全取决于每个服务消费者自身调用某个服务节点是否正常。如果连续调用超过一定的次数都不正常,就可以把这个服务节点在内存中标记为不可用状态,从可用服务节点列表中剔除。同时每个服务消费者还都有一个异步线程,始终在探测不可用的服务节点列表中的节点是否恢复正常,如果恢复正常的话就可以把这个节点重新加入到可用服务节点列表中去。
|
||||
|
||||
当然服务消费者也不是完全不与注册中心打交道,在服务启动时,服务消费者还是需要去注册中心中拉取所订阅的服务提供者节点列表信息,并且服务消费者还有一个异步线程,每隔一段时间都会去请求注册中心以查询服务提供者节点列表信息是否有变更,如果有变更就会请求注册中心获取最新的服务提供者节点列表信息。所以在有节点需要上下线时,服务消费者仍然能够拿到最新的服务提供者节点列表信息,只不过这个节点上下线的操作,一般是由开发或者运维人员人工操作,而不是像动态注册中心那样,可以通过心跳机制自动操作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d2/4a/d22ad01d93c6ba1d791ee8cc4d5a604a.png" alt="">
|
||||
|
||||
关于最少活跃连接算法和自适应最优选择算法,它们的含义你可以返回[专栏第18期](http://time.geekbang.org/column/article/40883)回顾一下,本质上这两种算法都可以理解为局部最优解。
|
||||
|
||||
首先来看最少活跃连接算法,当客户端的请求发往某个服务端节点时,就给客户端同这个服务端节点的连接数加一;当某个服务端节点返回请求结果后,就给客户端同这个服务端节点的连接数减一,客户端会在本地内存中维护着同服务端每个节点的连接计数。从理论上讲,服务端节点性能越好,处理请求就快,同一时刻客户端同服务端节点之间保持的连接就越少,所以客户端每次请求选择服务端节点时,都会选择与客户端保持连接数最少的服务端节点,所以叫作“最少活跃连接算法”。但最少活跃连接算法会导致服务端节点的请求分布不均,我曾经在实践中见过一种极端情况,当服务端节点性能差异较大时,性能较好的节点的请求数量甚至达到了性能较差的节点请求数量的两倍。出现这种情况,一方面会导致某些服务端节点不能被充分利用,另一方面可能会导致请求量过高的服务端节点无法应对突发增长的流量而被压垮。
|
||||
|
||||
再来看下自适应最优选择算法,一方面客户端发往服务端节点每一次调用的耗时都会被记录到本地内存中,并且每隔一分钟计算客户端同服务端每个节点之间调用的平均响应时间,并在下一次调用的时候选择平均响应时间最快的节点。显然这样是收益最大的,尤其是服务跨多个数据中心部署的时候,同一个数据中心内的调用性能往往要优于跨数据中心的调用;另一方面客户端并不是每一次都选择平均响应时间最快的节点发起调用,为了防止出现类似最少活跃连接算法中服务端节点请求量差异太大的情况发生,把服务端节点按照平均响应时间进行排序,找出最差的20%的节点并适当降低调用权重,从而达到有效减少长尾请求的目的。
|
||||
|
||||
欢迎你在留言区写下自己学习、实践微服务的心得和体会,与我和其他同学一起讨论。你也可以点击“请朋友读”,把今天的内容分享给好友,或许这篇文章可以帮到他。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user