设计一个微博系统


设计一个微博系统


1.引言

在三面的时候有道面试题印象挺深刻的。

记得当时的面试官十分专业,问了很多项目相关且思考性很强的问题,还有几个是我平日在开发过程中积累过的优质 BUG,也都问到了。

正在我沾沾自喜,以为考的都会,答的全对时,面试官开始发难了:“既然你项目上的思考还挺多,那我考你个项目设计的题目吧! 首先说明,这些题目不是为了难倒谁,只是想看一下候选人掌握技术知识的深度和广度!”

“啊,对对对!是这样的”,虽然心里百般忐忑,确实日常需求都是 CRUD(增删改查),架构类的设计很久没在项目里做过了,但还是表现出不慌不忙、胸有成竹之色。

毕竟面试宝典有曰:面试中一半的时间都是在打心理战!只要做足功课,在考场上自信满满地防御,凭面试官一两个小时的进攻,我们也不会败下阵来。

2.需求分析

面试官问:“微博作为国民常用的社交类 APP,平时用吗?可以说一下微博常用的几个功能吗?”

微博还真没咋用,不过最近刚看了一篇关于微博的系统设计方案,于是不慌不忙地答到:“微博常见功能有刷微博, 发帖子以及用户关注,此外,用户还可以点赞、评论、收藏和转发微博。”

“好,那如果现在让你设计一个微博系统,结合刚才你说的几个核心功能,你会怎么设计?需要考虑系统高并发、高性能以及高可用性。”

拿到 “产品需求” 之后,我首先在脑海中构建了一个核心功能的用例图,如下:

每个需求说明如下:

  • 刷微博:用户打开手机 APP 端的微博主页,显示关注的好友最近发表的微博,按最近时间排序;
  • 发微博:用户可以发表不超过 140 字文本的内容,可以包含图片和视频;
  • 关注好友:用户可以关注其他用户,被关注者可以看到粉丝信息和粉丝数。

3.概要设计

微博的业务功能不难理解,但是并发量数据量都非常大:

  • 10 亿级别的用户量,平均每个用户上千个帖子数,每个用户可关注上千个好友;
  • 高并发量,每秒十万级的平均页面访问,每秒万级的帖子发布量;
  • 用户分布不均匀,部分明星用户的帖子数量或者粉丝数量,超出普通用户几个数量级;
  • 时间分布不均匀,某个用户可能突然在某个时间点成为热点用户,其粉丝也可能陡增几个数量级。

其具有一个典型社交类系统的特征,可归结为三点:海量数据高访问量非均匀性, 接下来从其最常见的关注好友、刷微博、发帖子三个功能来做概要设计。

3.1 关注好友

如上图微博打开某个博主的主页面,页面包含最基础的关注功能:包括 Ta 的关注用户、Ta 的粉丝数,以及我们可以点击关注此博主。 而从博主的主页面,我们还可以进入他的关注和粉丝两个子页面:

  • 关注(attention,后文同)页,展示该用户关注的所有用户信息
  • 粉丝(follower,后文同)页,展示该用户的所有粉丝用户信息

在上述页面中,用户可以关注某个用户,也可以删除粉丝,即取消其他某个用户对自己的关注, 功能的业务交互如下(数据及文件写入、删除,所以分布式对象存储服务器在用户服务器集群处):

用户关注好友时,需要先经过负载均衡服务器,然后把请求发送到微博的用户服务器集群,这里涉及到用户信息的展示, 所以用户服务器可能会访问对象存储服务器获取图片和视频,以及从 Redis 或 MySQL 中取出文本数据。

最后,如果修改了用户的关注状态,就会把信息写入到 Redis 及 MySQL 集群中。

整体起来,就是先读取,后写入。

3.2 刷微博

刷微博系统的核心是解决高并发问题。系统整体部署模型如下(静态文件读取,所以分布式对象存储服务器在反向代理服务器集群处):

首先,我们采用 CDN(Content delivery network,内容分发网络)来快速返回用户请求, 它的原理是将服务器部署在用户广泛使用的地区节点,当用户访问系统的时候, 通过全局负载技术将请求分发到最近的服务器上,由它们直接给用户提供服务:

当没有采用 CDN 时,每次用户请求都会直接到达系统的应用服务器集群,这将对系统服务器产生极大的流量压力; 而采用 CDN 后,用户请求会根据 CDN 负载均衡器返回的目标服务器,将访问发配到这些用户邻近的节点上。

采用 CDN 的优势是能够极大地避免网络拥堵,使得内容传输更快、更稳定,CDN 可以看作是系统缓存, 对于微博这种很少更改数据的场景下尤为适用,正常情况下 CDN 可以过滤掉 90% 以上的请求,直接返回数据。

因此,当用户通过 CDN 访问微博系统时,绝大部分都可以被 CDN 缓存命中。也就是说,图片以及视频等极耗带宽的请求压力, 90% 以上可以通过 CDN 消化掉。没有被 CDN 命中的请求,会到达数据中心的反向代理服务器, 反向代理服务器检查本地缓存是否有请求需要的内容。如果有就直接返回; 若没有,才到分布对象存储集群中获取相关图片和视频,或者从应用服务器应用获取微博文字内容。

从微博应用服务器集群获取内容时,会先从 Redis 缓存服务器中,检索当前用户关注的好友发布的最新微博, 并构建一个结果页面返回。如果 Redis 中缓存的微博数量不够 20 条,则继续从 MySQL 数据库中查找数据(这样下来,时间可能是乱序)。

3.3 写微博

写微博时不需要 CDN 和反向代理,而是直接通过负载均衡服务器到达应用服务器集群。 应用服务器一方面会把发布的微博写入 Redis 缓存集群,另一方面写入 MySQL 分片数据库中。

注意,我们在写入数据库时,如果直接写库,当有高并发的写请求突然到来,可能会导致数据库过载, 进而引发系统阻塞或崩溃(参照 “缓存雪崩” 问题)。所以,数据库写操作可以先写入消息队列里(比如 Kafka集群), 由消息队列的消费者程序从消息队列中按照一定的速度消费消息,并写入数据库中,以保证 DB 的负载压力不会激增引发异常。

4. 详细设计

4.1 表设计

用户、粉丝、关注和帖子表:

  • user 表:主键id,用户信息(名称,头像,注册时间,大v认证,手机号等)
  • relation 表:主键id,followId,attentionId(粉丝和被关注者ID)
  • post 表:主键id,userId,postTime(发布时间(精确到毫秒即可)),content

索引优化

post 表可以用组合索引 userId+postTime 查询某个用户最近的帖子,这里组合索引作为二级索引,故需要回表。 为了减少回表次数,我们可以把 userId 和时间戳拼接位 postId 作为主键:比如,前 20 位作为时间戳精确到毫秒+用户Id, 转成 62 进制(0-9a-zA-Z)的字符串作为 postId,也能保证主键是递增。

但这样会导致索引树占用更多空间,且查询时没有纯数字主键那么快,所以最后可以根据实际情况比对两者的优劣选择最合适的主键类型。

4.2 分库分表

当 follow、attention、user 表数据量突破千万级、亿万级之后,微博读写数据库的压力非常大, 对于单机数据库肯定是无法承受的。所以微博 DB 需要采用分片部署的分布式数据库。

垂直拆表

上述 relation 表 (id, followId, attentionId) 存储粉丝和关注信息,当用户增多时就会出现一个问题,那就是分库分表的拆分 key 不好选择:

  • 如果选择 followId 做 hashKey,查询当前用户的关注列表时都在同一个分片,但查询用户所有粉丝时就需要在多个分片上;
  • 如果选择 attentionId 做 hashKey,查询某个用户的所有粉丝列表时都在同一个分片,但查询当前用户的关注列表时又需要分片。

所以我们将 relation 表拆分为:

  • follow 表:主键id,userId,followId
  • attention 表:主键id,userId,attentionId

(这里读下来,follow 表中还是与 relation 表一摸一样的信息,userId就是attentionId,没看懂…)

水平拆表

水平拆分即采用 hash 分片的方式部署分布式数据库,分片规则可采用用户 ID 或者帖子 ID。

如果按用户 ID 分片,同一个用户发表的全部帖子都会保存到同一台数据库服务器上,好处是当系统需要查找某个用户发表的微博时, 只需要访问一台服务器即可完成。缺点是,对于明星用户,其数据访问会非常多,热点数据的访问导致该服务器负载压力过大。 同样地,如果一个用户频繁发布微博,也会导致单个服务器数据增长过快。

如果按帖子 ID 分片,虽然可以避免上述用户 ID 分片带来的热点聚集问题,但是当查找某个用户的所有微博时, 需要访问的分片数据库服务器是随机的,对整个数据库服务器集群的压力太大。

综合考虑,用户 ID 分片带来的热点问题,可以通过优化缓存来改善。某个用户频繁发表微博的问题, 可以通过设置每日发表帖子上限来解决(比如每天每个用户最多发表 50 条微博)。最终,微博采用按用户 ID 分片的策略。

分片逻辑可以用 hash 取模,也可以用一致性 hash 映射等方式。

(这种设计不错,同一个用户发布的帖子在同一个表中,不需要跨表查询)

4.3 热点用户问题

拆完表之后,虽然一定程度解决了快速查表的问题,但是对一些热点明星用户的查询还需要优化。比如以下几种场景:

  • 明星热点用户有很多粉丝,在查询粉丝数量时通过 follow 表查询 count 时扫描的行数非常多,而且这种低效的操作会由于粉丝数的增多而扩大。
  • 刷微博时,明星用户有很多帖子都是重复被查看的,如果每次粉丝去查询时都到 DB 中获取,无疑会出现严重的性能压力。

解决以上两种场景下的 DB 查询问题,可以引入缓存。但是缓存空间有限,我们必然不能把所有数据都缓存起来, 设置一个好的缓存淘汰策略是我们讨论的重点。

时间淘汰策略

对于热点话题来说,无论是热点帖子、热点用户,都需要加入缓存,缓存的淘汰策略可以设置为时间淘汰算法。

将最近 n 天内发布的微博全部缓存起来,用户刷新微博时,只需要在缓存中查找。某用户获取的微博列表如果有 10 条, 就直接返回给用户;如果缓存中的微博数量不足,再去数据库中查找。

所以,我们可以缓存 7 天内发布的全部微博文本,其中缓存 key 为用户 ID,value 为最近 7 天发表的帖子 ID 列表。 同时,帖子 ID 和帖子内容也分别作为 key 和 value 缓存起来。

本地缓存模式

此外,对于特别热门的微博,比如明星结婚/离婚/出 GUI 等情况,由于高并发访问全都集中在一个 key 上, 会给单台 redis 服务器带来极大的负载压力。所以微博系统可以启用本地缓存模式, 即应用服务器将特别热门的微博内容缓存在服务器内存中,当用户在刷微博时,会优先检查帖子 ID 对应的微博内容是否在本地缓存中。

我们可以针对 500w 以上粉丝数的大 V 用户,缓存其 48 小时内发表的全部微博,进一步减轻热点数据带来的查库压力。

5.微博发布/订阅问题

微博用户关注好友后,如何快速得到所有好友的最新发布内容,即发布/订阅问题,是微博的核心业务问题。

5.1 推模式

当用户发布一条帖子以后,立即将消息推送给粉丝。但是此时粉丝不一定在线,那么就需要将数据存储起来。 这样,用户每新增一条帖子,都需要将帖子推到它的粉丝所在 DB 的分片上,粉丝每次浏览新消息时,直接查询自己分片所存储的数据即可。

推模式一个明显的问题:如果一个用户有上千万的粉丝,那用户每发布一条微博,就需要在订阅表中插入上千万条记录, 即 “写扩散”。粉丝之中不乏有很多的僵尸粉(上线频率极低的用户),带来的后果就是数据库压力非常大,导致阻塞或崩溃。

5.2 拉模式

当用户发布一条帖子以后,只保存在自己的业务表。当粉丝上线以后再去关注的人业务表里读取帖子,然后按照时间顺序排序后返回。

拉模式的问题是:如果一个用户关注了 500 个明星用户,每次查询时都需要到各分片中查询不同明星发表的帖子, 一个明星有千万级甚至亿万级的粉丝数,意味着可能会同时有千万级甚至亿万级的读数据操作,即 “读扩散”, 带来的问题就是数据库分片的读数据减压效果体现不出来。

所以,微博系统首先需要限制用户的关注数,微博普通用户的关注上限是 2000 人,VIP 用户上限为 5000 人。 其次,尽量减少刷新微博页面时查询数据库的次数,多用缓存读取帖子。

5.3 推拉结合

我们发现,即便对微博用户限制好友数,但单一的 “推模式” 或者 “拉模式” 都难以解决微博系统的订阅/发布问题, 所以我们最终采用 “推拉结合” 的模式,具体有如下两种实现方式。

1)区分大 v 明星

对于大 v 明星用户(粉丝数大于 500w)来说,为了防止写扩散,我们只需要将数据同步到 100 个数据库分片上即可(假设有 100 个数据分片), 它至少需要三个字段:userId,postId,postTime。无论多少粉丝,就只复制 100 份, 这样就避免了很多几乎永远不会上线的僵尸粉因为数据写浪费网络性能和存储资源。

对于普通用户来说,还是继续采用推的模式,这样大多数用户在读取最新帖子的时候只需要在自己用户对应的分片上就可以获取到数据了。

这样设计的方式较为简单,但是用户在刷微博的时候,查询过程可能需要查询两次,分别是自己订阅表下的帖子信息,和关注用户发布表下的帖子信息

2)区分在线状态

第二种方式是根据用户状态来判断推拉:如果用户当前在线,就使用推模式,系统会在 Redis 缓存中为其创建一个好友最新发表的微博列表。 关注的好友如果有新微博发布,就立即将帖子信息插入列表的头部,当该用户刷新微博时,只需要将这个列表返回即可。

如果用户不在线,系统就将列表删除,当用户再次登录刷新时,用拉模式为其重新构建列表。

如何确定用户在线呢?一方面可以通过用户操作时间间隔来判断,即心跳机制; 另一方面也可以通过机器学习,预测用户的上线时间,利用系统空闲时间,提前为其构建最新的微博列表。

6.后记

面试官听了我的分析,心想这小子思考得还挺全面,但脸上也不能表现出来。于是微微点头,说:“我的问题就到这了,那你有什么想问的吗?”

于是抱着劫后余生的想法,问了几个不痛不痒的业务问题后,就结束面试了。毕竟,面试官要逮着架构设计的细节问,我可能就再也记不住了。

不仅感叹这八股文、架构题,背得是真慢,忘得是真快呀!

还好平时有总结架构设计的习惯,不至于出大丑。






参考资料

听说你学过架构设计?来,弄个微博系统 https://mp.weixin.qq.com/s/R40Ref3y-kIBPXzHL7yziA


返回