gitbook/李智慧 · 高并发架构实战课/docs/495931.md

157 lines
15 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 13 | 微博系统设计:怎么应对热点事件的突发访问压力?
你好,我是李智慧。
微博microblog是一种允许用户即时更新简短文本比如140个字符并可以公开发布的微型博客形式。今天我们就来开发一个面向全球用户、可以支持10亿级用户体量的微博系统系统名称为“Weitter”。
我们知道微博有一个重要特点就是部分明星大V拥有大量的粉丝。如果明星们发布一条比较有话题性的个人花边新闻比如宣布结婚或者离婚就会引起粉丝们大量的转发和评论进而引起更大规模的用户阅读和传播。
这种突发的单一热点事件导致的高并发访问会给系统带来极大的负载压力,处理不当甚至会导致系统崩溃。而这种崩溃又会成为事件热点的一部分,进而引来更多的围观和传播。
因此Weitter的技术挑战一方面是微博这样类似的信息流系统架构是如何设计的另一方面就是如何解决大V们的热点消息产生的突发高并发访问压力保障系统的可用性。今天我们就来看看这样的系统架构该怎么设计。
## 需求分析
Weitter的核心功能只有三个发微博关注好友刷微博。
![图片](https://static001.geekbang.org/resource/image/27/0a/27dbd5c5f28b1d834a8be005391e7e0a.jpg?wh=1920x1077)
1. 发微博用户可以发表微博内容包含不超过140个字的文本可以包含图片和视频。
2. 关注好友:用户可以关注其他用户。
3. 刷微博用户打开自己的微博主页主页显示用户关注的好友最近发表的微博用户向下滑动页面或者点刷新按钮主页将更新关注好友的最新微博且最新的微博显示在最上方主页一次显示20条微博当用户滑动到主页底部后继续向上滑动会按照时间顺序显示当前页面后续的20条微博。
4. 此外,用户还可以收藏、转发、评论微博。
#### 性能指标估算
系统按10亿用户设计按20%日活估计大约有2亿日活用户DAU其中每个日活用户每天发表一条微博并且平均有500个关注者。
而对于**发微博所需的存储空间**,我们做如下估算。
* **文本内容存储空间**
遵循惯例每条微博140个字如果以UTF8编码存储汉字计算则每条微博需要$\\small 140\\times3=420$个字节的存储空间。除了汉字内容以外每条微博还需要存储微博ID、用户ID、时间戳、经纬度等数据按80个字节计算。那么每天新发表微博文本内容需要的存储空间为100GB。
$\\small 2亿 \\times (420B +80B) = 100GB/天$
* **多媒体文件存储空间**
除了140字文本内容微博还可以包含图片和视频按每5条微博包含一张图片每10条微博包含一个视频估算每张图片500KB每个视频2MB每天还需要60TB的多媒体文件存储空间。
$\\small 2亿\\div5\\times500KB+2亿\\div10\\times2MB=60TB/天$
对于**刷微博的访问并发量**,我们做如下估算。
* **QPS**
假设两亿日活用户每天浏览两次微博每次向上滑动或者进入某个人的主页10次每次显示20条微博每天刷新微博次数40亿次即40亿次微博查询接口调用平均QPS大约5万。
$\\small 40亿\\div24\\times60\\times60=46296/秒$
高峰期QPS按平均值2倍计算所以系统需要满足10万QPS。
* **网络带宽**
10万QPS刷新请求每次返回微博20条那么每秒需访问200万条微博。按此前估计每5条微博包含一张图片每10条微博包含一个视频需要的**网络总带宽**为4.8Tb/s。
$\\small 200万\\div5\\times500KB+200万\\div10\\times2MB\\times8bit=4.8Tb/s$
## 概要设计
在需求分析中我们可以看到Weitter的业务逻辑比较简单但是**并发量**和**数据量**都比较大,所以,**系统架构的核心就是解决高并发的问题**,系统整体部署模型如下。
![图片](https://static001.geekbang.org/resource/image/c3/4d/c3f3c3abe0708f5ebe31bf25eec67f4d.jpg?wh=1920x1054)
这里包含了“Get请求”和“Post请求”两条链路Get请求主要处理刷微博的操作Post请求主要处理发微博的请求这两种请求处理也有重合的部分我们拆分着来看。
我们先来看看**Get请求**的部分。
![图片](https://static001.geekbang.org/resource/image/c1/99/c19yy285078b6c40513c36503eee4799.jpg?wh=1920x1054)
用户通过CDN访问Weitter的数据中心、图片以及视频等极耗带宽的请求绝大部分可以被CDN缓存命中也就是说4.8Tb/s的带宽压力90%以上可以通过CDN消化掉。
没有被CDN命中的请求一部分是图片和视频请求其余主要是用户刷新微博请求、查看用户信息请求等这些请求到达数据中心的反向代理服务器。反向代理服务器检查本地缓存是否有请求需要的内容。如果有就直接返回如果没有对于图片和视频文件会通过分布式文件存储集群获取相关内容并返回。分布式文件存储集群中的图片和视频是用户发表微博的时候上传上来的。
对于用户微博内容等请求如果反向代理服务器没有缓存就会通过负载均衡服务器到达应用服务器处理。应用服务器首先会从Redis缓存服务器中检索当前用户关注的好友发表的最新微博并构建一个结果页面返回。如果Redis中缓存的微博数据量不足构造不出一个结果页面需要的20条微博应用服务器会继续从MySQL分片数据库中查找数据。
以上处理流程主要是针对读http get请求那如果是发表微博这样的写http post请求呢我们再来看一下**写请求**部分的图。
![图片](https://static001.geekbang.org/resource/image/98/84/980db8e510ef320d44af9c53244d7e84.jpg?wh=1920x1054)
你会看到客户端不需要通过CDN和反向代理而是直接通过负载均衡服务器到达应用服务器。应用服务器一方面会将发表的微博写入Redis缓存集群一方面写入分片数据库中。
在写入数据库的时候,如果直接写数据库,当有高并发的写请求突然到来,可能会导致数据库过载,进而引发系统崩溃。所以,数据库写操作,包括发表微博、关注好友、评论微博等,都写入到消息队列服务器,由消息队列的消费者程序从消息队列中按照一定的速度消费消息,并写入数据库中,保证数据库的负载压力不会突然增加。
## 详细设计
用户刷新微博的时候如何能快速得到自己关注的好友的最新微博列表10万QPS的并发量如何应对如何避免数据库负载压力太大以及如何快速响应用户请求详细设计将基于功能需求和概要设计主要讨论这些问题。
#### **微博的发表/订阅问题**
Weitter用户关注好友后如何快速得到所有好友的最新发表的微博内容即发表/订阅问题,是微博的核心业务问题。
一种简单的办法就是“推模式”即建一张用户订阅表用户关注的好友发表微博后立即在用户订阅中为该用户插入一条记录记录用户id和好友发表的微博id。这样当用户刷新微博的时候只需要从用户订阅表中按用户id查询所有订阅的微博然后按时间顺序构建一个列表即可。也就是说**推模式是在用户发****微博****的时候推送给所有的关注者**如下图用户发表了微博0他的所有关注者的订阅表都插入微博0。
![图片](https://static001.geekbang.org/resource/image/91/84/91c929429edf6302fdea61e9e41dfa84.jpg?wh=1920x804)
推模式实现起来比较简单,但是推模式意味着,如果一个用户有大量的关注者,那么该用户每发表一条微博,就需要在订阅表中为每个关注者插入一条记录。而对于明星用户而言,可能会有几千万的关注者,明星用户发表一条微博,就会导致上千万次的数据库插入操作,直接导致系统崩溃。
所以对于10亿级用户的微博系统而言我们需要使用“拉模式”解决发表/订阅问题。也就是说,用户刷新微博的时候,根据其关注的好友列表,查询每个好友近期发表的微博,然后将所有微博按照时间顺序排序后构建一个列表。也就是说,**拉模式是在用户刷微博的时候拉取他关注的所有好友的最新微博**,如下图:
![图片](https://static001.geekbang.org/resource/image/95/37/95a92ce90c758f9ba814c15724be7137.jpg?wh=1920x691)
拉模式极大降低了发表微博时写入数据的负载压力,但是却又急剧增加了刷微博时候读数据库的压力。因为对于用户关注的每个好友,都需要进行一次数据库查询。如果一个用户关注了大量好友,查询压力也是非常巨大的。
所以首先需要限制用户关注的好友数在Weitter中普通用户关注上限是2000人VIP用户关注上限是5000人。其次需要尽量减少刷新时查询数据库的次数也就是说微博要尽量通过缓存读取。
但即使如此你会发现每次刷新的查询压力还是太大所以Weitter最终采用“推拉结合”的模式。也就是说如果用户当前在线那么就会使用推模式系统会在缓存中为其创建一个好友最新发表微博列表关注的好友如果有新发表微博就立即将该微博插入列表的头部当该用户刷新微博的时候只需要将这个列表返回即可。
如果用户当前不在线,那么系统就会将该列表删除。当用户登录刷新的时候,用拉模式为其重新构建列表。
那么如何确定一个用户是否在线?一方面可以通过用户操作时间间隔来判断,另一方面也可以通过机器学习,预测用户的上线时间,利用系统空闲时间,提前为其构建最新微博列表。
#### 缓存使用策略
通过前面的分析我们已经看到Weitter是一个典型的高并发读操作的场景。10万QPS刷新请求每个请求需要返回20条微博如果全部到数据库中查询的话数据库的QPS将达到200万即使是使用分片的分布式数据库这种压力也依然是无法承受的。所以我们需要大量使用缓存以改善性能提高吞吐能力。
但是缓存的空间是有限的我们必定不能将所有数据都缓存起来。一般缓存使用的是LRU淘汰算法即当缓存空间不足时将最近最少使用的缓存数据删除空出缓存空间存储新数据。
但是LRU算法并不适合微博的场景因为在拉模式的情况下当用户刷新微博的时候我们需要确保其关注的好友最新发表的微博都能展示出来如果其关注的某个好友较少有其他关注者那么这个好友发表的微博就很可能会被LRU算法淘汰删除出缓存。对于这种情况系统就不得不去数据库中进行查询。
而最关键的是,系统并不能知道哪些好友的数据通过读缓存就可以得到全部最新的微博,而哪些好友需要到数据库中查找。因此不得不全部到数据库中查找,这就失去了使用缓存的意义。
基于此我们在Weitter中使用**时间淘汰算法******也就是将最近一定天数内发布的微博全部缓存起来用户刷新微博的时候只需要在缓存中进行查找。如果查找到的微博数满足一次返回的条数20条就直接返回给用户如果缓存中的微博数不足就再到数据库中查找。
最终Weitter决定缓存7天内发表的全部微博需要的缓存空间约700G。缓存的key为用户IDvalue为用户最近7天发表的微博ID列表。而微博ID和微博内容分别作为key和value也缓存起来。
此外对于特别热门的微博内容比如某个明星的离婚微博这种针对单个微博内容的高并发访问由于访问压力都集中一个缓存key上会给单台Redis服务器造成极大的负载压力。因此微博还会启用**本地缓存模式**即应用服务器在内存中缓存特别热门的微博内容应用构建微博刷新页的时候会优先检查微博ID对应的微博内容是否在本地缓存中。
Weitter最后确定的本地缓存策略是针对拥有100万以上关注者的大V用户缓存其48小时内发表的全部微博。
现在我们来看一下Weitter整体的缓存架构。
![图片](https://static001.geekbang.org/resource/image/b0/8b/b039901b8cfaa6d1038007703ae1468b.jpg?wh=1920x691)
#### 数据库分片策略
前面我们分析过Weitter每天新增2亿条微博。也就是说平均每秒钟需要写入2400条微博高峰期每秒写入4600条微博。这样的写入压力对于单机数据库而言是无法承受的。而且每年新增700亿条微博记录这也超出了单机数据库的存储能力。因此Weitter的数据库需要采用分片部署的分布式数据库。分片的规则可以采用用户ID分片或者微博 ID分片。
如果按用户ID的hash值分片那么一个用户发表的全部微博都会保存到一台数据库服务器上。这样做的好处是当系统需要按用户查找其发表的微博的时候只需要访问一台服务器就可以完成。
但是这样做也有缺点对于一个明星大V用户其数据访问会成热点进而导致这台服务器负载压力太大。同样地如果某个用户频繁发表微博也会导致这台服务器数据增长过快。
要是按微博 ID的hash值分片虽然可以避免上述按用户ID分片的热点聚集问题但是当查找一个用户的所有微博时需要访问所有的分片数据库服务器才能得到所需的数据对数据库服务器集群的整体压力太大。
综合考虑用户ID分片带来的热点问题可以通过优化缓存来改善而某个用户频繁发表微博的问题可以通过设置每天发表微博数上限每个用户每天最多发表50条微博来解决。最终Weitter采用按用户ID分片的策略。
## 小结
微博事实上是**信息流应用产品**中的一种,这类应用都以滚动的方式呈现内容,而内容则被放置在一个挨一个、外观相似的版块中。微信朋友圈、抖音、知乎、今日头条等,都是这类应用。因此这些应用也都需要面对微博这样的发表/订阅问题:**如何为海量高并发用户快速构建页面内容**
在实践中,信息流应用也大多采用文中提到的**推拉结合模式**区别只是朋友圈像微博一样推拉好友发表的内容而今日头条则推拉推荐算法计算出来的结果。同样地这类应用为了加速响应时间也大量使用CDN、反向代理、分布式缓存等缓存方案。所以熟悉了Weitter的架构就相当于掌握了信息流产品的架构。
## 思考题
面对微博的高并发访问压力,你还能想到哪些方案可以优化系统?
欢迎在评论区分享你的思考,我们共同进步。