This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View 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你觉得还可以使用哪些存储
欢迎你在留言区写下自己的思考,与我一起讨论。

View 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/151/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分别用于存储微博计数和存在性判断大大减少了对内存的使用节省了大量机器成本。
专栏更新到这里就要跟同学们说再见了,感谢你们在过去大半年时间里的陪伴,值此新春到来之际,老胡给您拜年啦,恭祝各位同学在新的一年里,工作顺顺利利,生活开开心心!

View 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 {&quot;type&quot;:&quot;MOTAN&quot;,&quot;name&quot;:&quot;brha_zdelay_cn.sina.api.data.service.SinaUserService.getBareSinaUsers(long[])&quot;,&quot;slowThreshold&quot;:&quot;200&quot;,&quot;total_count&quot;:&quot;266&quot;,&quot;slow_count&quot;:&quot;0&quot;,&quot;avg_time&quot;:&quot;10.00&quot;,&quot;interval1&quot;:&quot;0&quot;,&quot;interval2&quot;:&quot;266&quot;,&quot;interval3&quot;:&quot;0&quot;,&quot;interval4&quot;:&quot;0&quot;,&quot;interval5&quot;:&quot;0&quot;,&quot;p75&quot;:&quot;10.00&quot;,&quot;p95&quot;:&quot;10.00&quot;,&quot;p98&quot;:&quot;10.00&quot;,&quot;p99&quot;:&quot;10.00&quot;,&quot;p999&quot;:&quot;10.00&quot;,&quot;biz_excp&quot;:&quot;0&quot;,&quot;other_excp&quot;:&quot;0&quot;,&quot;avg_tps&quot;:&quot;26&quot;,&quot;max_tps&quot;:&quot;31&quot;,&quot;min_tps&quot;:&quot;23&quot;}
```
上面这段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进行反注册这样的话注册中心会查询这个节点注册的所有服务然后一次性把这个节点从所有服务的列表中删除。理论上批量删除只需要调用一次就可以把节点从注册中心中删除相比于之前多次调用成功率要高很多并且从我们实际使用效果上来看也不会再存在“僵尸节点”的情况了。
今天的答疑就到这里啦,欢迎你在留言区写下自己学习、实践微服务的心得和体会,与我和其他同学一起讨论。你也可以点击“请朋友读”,把今天的内容分享给好友,或许这篇文章可以帮到他。

View 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%的节点并适当降低调用权重,从而达到有效减少长尾请求的目的。
欢迎你在留言区写下自己学习、实践微服务的心得和体会,与我和其他同学一起讨论。你也可以点击“请朋友读”,把今天的内容分享给好友,或许这篇文章可以帮到他。