前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何全方位设计一个高并发博客系统?(包含热点文章, 热点key, Feed流解决方案)

如何全方位设计一个高并发博客系统?(包含热点文章, 热点key, Feed流解决方案)

原创
作者头像
天下之猴
修改2024-09-18 16:00:59
3000
修改2024-09-18 16:00:59
举报

前提背景

现在有一个博客系统, 主要功能分为三块, 发布博客, 获取博客, 关注好友, 发布博客也就是可以发布文字, 视频, 图片,

关注好友为,用户可以关注其他人, 获取博客也就是用户可以刷新手机页面, 一次性获取固定条数(这里 以20为例)的微博, 到达底部后继续刷新按照时间顺序显示后续20条博客, 其他功能转发, 评论, 收藏, 点赞这里不做讨论

性能指标估计

系统按10亿用户设计,按20%日活估计,大约有2亿日活用户(DAU),其中每个日活用户每天发表一条微博,并且平均有500个关注者。

而对于系统所需的存储空间,我们做如下估算。

文本内容存储空间

遵循惯例,每条博客140个字,如果以UTF8编码存储汉字计算,则每条博客需要small140times3=420𝑠𝑚𝑎𝑙𝑙140𝑡𝑖𝑚𝑒𝑠3=420个字节的存储空间。除了汉字内容以外,每条博客还需要存储博客ID、用户ID、时间戳、经纬度等数据,按80个字节计算。那么每天新发表博客文本内容需要的存储空间为100GB。

small2亿times(420B+80B)=100GB/天𝑠𝑚𝑎𝑙𝑙2亿𝑡𝑖𝑚𝑒𝑠(420𝐵+80𝐵)=100𝐺𝐵/天

多媒体文件存储空间

除了140字文本内容,博客还可以包含图片和视频,按每5条博客包含一张图片,每10条博客包含一个视频估算,每张图片500KB,每个视频2MB,每天还需要60TB的多媒体文件存储空间。

small2亿div5times500KB+2亿div10times2MB=60TB/天𝑠𝑚𝑎𝑙𝑙2亿𝑑𝑖𝑣5𝑡𝑖𝑚𝑒𝑠500𝐾𝐵+2亿𝑑𝑖𝑣10𝑡𝑖𝑚𝑒𝑠2𝑀𝐵=60𝑇𝐵/天

对于刷博客的访问并发量,我们做如下估算。

QPS

假设两亿日活用户每天浏览两次博客,每次向上滑动或者进入某个人的主页10次,每次显示20条博客,每天刷新博客次数40亿次,即40亿次博客查询接口调用,平均QPS大约5万。

small40亿div(24times60times60)=46296/秒𝑠𝑚𝑎𝑙𝑙40亿𝑑𝑖𝑣(24𝑡𝑖𝑚𝑒𝑠60𝑡𝑖𝑚𝑒𝑠60)=46296/秒

高峰期QPS按平均值2倍计算,所以系统需要满足10万QPS。

网络带宽

10万QPS刷新请求,每次返回博客20条,那么每秒需访问200万条博客。按此前估计,每5条博客包含一张图片,每10条博客包含一个视频,需要的网络总带宽为4.8Tb/s。

small(200万div5times500KB+200万div10times2MB)times8bit=4.8Tb/s𝑠𝑚𝑎𝑙𝑙(200万𝑑𝑖𝑣5𝑡𝑖𝑚𝑒𝑠500𝐾𝐵+200万𝑑𝑖𝑣10𝑡𝑖𝑚𝑒𝑠2𝑀𝐵)𝑡𝑖𝑚𝑒𝑠8𝑏𝑖𝑡=4.8𝑇𝑏/𝑠

大致设计

Get请求链路分析

get请求主要用来用户获取博客数据用, 用户请求首先通过CDN访问数据中心, 图片以及视频等极耗带宽的请求,绝大部分可以被CDN缓存命中,也就是说,4.8Tb/s的带宽压力,90%以上可以通过CDN消化掉。

借用阿里云官网的例子,来简单介绍CDN的工作原理。假设通过CDN加速的域名为www.a.com,接入CDN网络,开始使用加速服务后,当终端用户(北京)发起HTTP请求时,处理流程如下:

当终端用户(北京)向www.a.com下的指定资源发起请求时,首先向LDNS(本地DNS)发起域名解析请求。

LDNS检查缓存中是否有www.a.com的IP地址记录。如果有,则直接返回给终端用户;如果没有,则向授权DNS查询。

当授权DNS解析www.a.com时,返回域名CNAME www.a.tbcdn.com对应IP地址。

域名解析请求发送至阿里云DNS调度系统,并为请求分配最佳节点IP地址。

LDNS获取DNS返回的解析IP地址。

用户获取解析IP地址。

用户向获取的IP地址发起对该资源的访问请求。

如果该IP地址对应的节点已缓存该资源,则会将数据直接返回给用户,例如,图中步骤7和8,请求结束。

如果该IP地址对应的节点未缓存该资源,则节点向源站发起对该资源的请求。获取资源后,结合用户自定义配置的缓存策略,将资源缓存至节点,例如,图中的北京节点,并返回给用户,请求结束。

没有被CDN命中的请求,一部分是图片和视频请求,其余主要是用户刷新博客请求、查看用户信息请求等,这些请求到达数据中心的反向代理服务器。反向代理服务器检查本地缓存(例如NGINX服务器上的缓存)是否有请求需要的内容。如果有,就直接返回;如果没有,对于图片和视频文件,会通过分布式文件存储集群获取相关内容并返回。分布式文件存储集群中的图片和视频是用户发表博客的时候,上传上来的。

对于用户博客内容等请求,如果反向代理服务器没有缓存,就会通过负载均衡服务器到达应用服务器处理。应用服务器首先会从Redis缓存服务器中,检索当前用户关注的好友发表的最新微博,并构建一个结果页面返回。如果Redis中缓存的博客数据量不足,构造不出一个结果页面需要的20条博客,应用服务器会继续从MySQL分片数据库中查找数据。

相当于设计了设计了一个三级缓存(CDN缓存, NGINX缓存, Redis缓存), 来避免大量查询走数据库

Post链路分析

当用户发布一篇博客时, 不需要走CDN缓存, 直接通过负载均衡到应用服务器上, 应用服务器之后将博客数据写到Redis和分片数据库中(也就是分表), 但是如果直接写数据库的话, 如果有高并发的写请求可能导致数据库过载, 因此一系列写请求比如: 发表博客、关注好友、评论博客等,都写入到消息队列服务器, 再由消费者程序从消息队列中按照一定的速度来消费消息, 并写入数据库, 保证数据库的负载压力不会突然增加

详细设计

关于发表, 订阅问题

当用户关注好友后, 如何快速得到所有好友的最新发表的博客内容,即发表/订阅问题,是这类系统的核心业务问题。也就是Feed流该如何设计,这里我们详细展开讲一下

拉模式

一部分工程师认为应该在查询时首先查询用户关注的所有创作者 uid,然后查询他们发布的所有文章,最后按照发布时间降序排列

使用拉模型方案用户每打开一次「关注页」系统就需要读取 N 个人的文章(N 为用户关注的作者数), 因此拉模型也被称为读扩散。

拉模型不需要存储额外的数据,而且实现比较简单:发布文章时只需要写入一条 articles 记录,用户关注(或取消关注)也只需要增删一条 followings 记录。特别是当粉丝数特别多的头部作者发布内容时不需要进行特殊处理,等到读者进入关注页时再计算就行了。

拉模型的问题同样也非常明显,每次阅读「关注页」都需要进行大量读取和一次重新排序操作,若用户关注的人数比较多一次拉取的耗时会长到难以接受的地步。对于用户来说并不友好

推模式

另一部分工程师认为在创作者发布文章时就应该将新文章写入到粉丝的关注 Timeline,用户每次阅读只需要到自己的关注 Timeline 拉取就可以了

使用推模型方案创作者每次发布新文章系统就需要写入 M 条数据(M 为创作者的粉丝数),因此推模型也被称为写扩散。推模型的好处在于拉取操作简单高效,但是缺点一样非常突出。

首先,在每篇文章要写入 M 条数据,在如此恐怖的放大倍率下关注 Timeline 的总体数据量将达到一个惊人数字。而粉丝数有几十万甚至上百万的头部创作者每次发布文章时巨大的写入量都会导致服务器地震。

通常为了发布者的体验文章成功写入就向前端返回成功,然后通过消息队列异步地向粉丝的关注 Timeline 推送文章。

在线推, 离线拉

优点

缺点

读取操作快

逻辑复杂 消耗大量存储空间 粉丝数多的时候会是灾难 系统不友好

逻辑简单 节约存储空间

读取效率低下,关注人数多的时候会出现灾难

简而概之就是一句话,推模式对用户友好,对系统不友好而拉模式系统友好,而对用户不友好, 但在实际中, 一般一个用户关注2000人的都比较少, 所以乍看上去拉模型优点多多,但是 Feed 流是一个极度读写不平衡的场景,读请求数比写请求数高两个数量级也不罕见,对于服务器来说, 这使得拉模型消耗的 CPU 等资源反而更高。

此外推送可以慢慢进行,但是用户很难容忍打开页面时需要等待很长时间才能看到内容(很长:指等一秒钟就觉得卡)。 因此拉模型读取效率低下的缺点使得它的应用受到了极大限制。

我们回过头来看困扰推模型的这个问题「粉丝数多的时候会是灾难」,我们真的需要将文章推送给作者的每一位粉丝吗?

仔细想想这也没有必要,我们知道粉丝群体中活跃用户是有限的,我们完全可以只推送给活跃粉丝,不给那些已经几个月没有启动 App 的用户推送新文章。

至于不活跃的用户,在他们回归后使用拉模型重新构建一下关注 Timeline 就好了。因为不活跃用户回归是一个频率很低的事件,我们有充足的计算资源使用拉模型进行计算。

因为活跃用户和不活跃用户常常被叫做「在线用户」和「离线用户」,所以这种通过推拉结合处理头部作者发布内容的方式也被称为「在线推,离线拉」。我们的博客系统采用这种方式

关于缓存的存储策略

通过前面的分析可以看到, 博客系统是一个高并发读写操作的场景, 10万QPS刷新请求,每个请求需要返回20条微博,如果全部到数据库中查询的话,数据库的QPS将达到200万,即使是使用分片的分布式数据库,这种压力也依然是无法承受的。所以,我们需要大量使用缓存以改善性能,提高吞吐能力。

但是缓存的空间是有限的,我们必定不能将所有数据都缓存起来。一般缓存使用的是LRU淘汰算法,即当缓存空间不足时,将最近最少使用的缓存数据删除,空出缓存空间存储新数据。

但仔细分析一下, LRU算法适合我们业务场景么, 在拉模式下, 当用户刷新微博的时候, 我们需要确保其关注的好友最新发表的博客都能展示出来,如果其关注的某个好友较少有其他关注者,那么这个好友发表的博客就很可能会被LRU算法淘汰删除出缓存。对于这种情况,系统就不得不去数据库中进行查询。

而最关键的是,系统并不能知道哪些好友的数据通过读缓存就可以得到全部最新的博客,而哪些好友需要到数据库中查找。因此不得不全部到数据库中查找,这就失去了使用缓存的意义。

基于此,我们在该系统中使用时间淘汰算法,也就是将最近一定天数内发布的博客全部缓存起来,用户刷新的时候,只需要在缓存中进行查找。如果查找到的文章数满足一次返回的条数(20条),就直接返回给用户;如果缓存中的文章数不足,就再到数据库中查找。

特别的, 对于热点文章, 这种高并发访问的博客, 由于访问压力都集中在一格缓存key上, 会给单台Redis服务器造成极大的负载压力, 因此从而导致热点key问题, 下面我们再来详细展开讲讲,热点Key问题该如何解决

热点Key问题

热点key问题并不陌生,它可能由各种各样的事件引发。比如,明星结婚、离婚、出轨等特殊突发事件,奥运、春节等重大活动或节日,以及诸如秒杀、双12、618等线上促销活动,都很容易导致某些数据成为热点key。这些热点key的突然增加会给我们的系统带来巨大的压力,可能导致性能下降、服务不稳定甚至宕机。

如何提前发现热点文章?

历史数据分析:

  • 通过分析历史数据,我们可以识别出在特定事件或节日期间经常被访问的key。例如,对于双11或其他大型促销活动,我们可以查看过去几年的数据,找出哪些key在这些活动期间访问量较高。
  • 利用时间序列分析等技术,可以识别出周期性热点key,这些key可能会在固定时间段内频繁出现,例如每周、每月或每年的特定时段。

业务分析:

  • 了解业务的运作方式和用户行为模式对于发现潜在的热点key至关重要。例如,对于电商平台来说,研究用户购物行为和产品热度可以帮助我们预测哪些商品的key可能会成为热点。
  • 与业务团队紧密合作,获取他们的见解和预测,可以帮助我们更准确地识别潜在的热点key。

实时监控:

  • 建立实时监控系统,监测系统中各个key的访问情况。一旦某个key的访问量突然增加,就可以立即发出警报,以便及时采取措施应对。
  • 利用监控工具或自定义脚本来实时跟踪热点key的访问情况,以便在发现异常时快速响应。

用户行为分析:

  • 对用户行为进行深入分析,可以帮助我们理解他们的兴趣和偏好,从而预测哪些key可能会成为热点。
  • 利用用户行为数据和用户画像技术,可以识别出具有潜在热度的key,例如某些热门商品、热门活动或热门话题。

机器学习和预测模型:

  • 借助机器学习和预测模型,可以基于历史数据和实时数据来预测未来可能的热点key。这些模型可以自动识别出模式并进行预测,帮助我们提前做好准备。
  • 通过不断优化和训练模型,我们可以逐渐提高预测的准确性,使其成为发现热点key的有力工具。

解决方案

分布式存储:

  • 将热点key分散存储在多个缓存节点上是一种常见的解决方案。通过将数据分片或分散到不同的节点上,可以降低单个节点的负载压力,从而减少热点key对系统的影响。
  • 这种方法可以通过哈希分片或一致性哈希等算法来实现,确保热点key在不同节点上均匀分布,避免单个节点成为瓶颈。

主从复制和扩容:

  • 针对缓存集群,可以采用主从复制的方式,将热点key的读写操作分散到多个节点上,提高系统的负载能力和可用性。
  • 当系统负载增加时,可以通过垂直扩容或水平扩容的方式,动态地添加新的节点,以应对不断增长的请求量。

前置缓存:

  • 在应用程序内部引入前置缓存,可以有效减轻后端缓存的压力。通过在应用程序中缓存常用数据或频繁访问的key,可以减少对后端缓存的请求量,提高系统的响应速度和性能。
  • 需要注意的是,前置缓存的大小和更新策略需要根据实际情况进行合理的配置,以避免缓存空间不足或数据过期导致的性能问题。

定时刷新和实时感知:

  • 针对延迟不敏感的热点key,可以采用定时刷新的方式,定期更新缓存中的数据,确保数据的新鲜性。
  • 对于实时感知的热点key,则需要建立实时监控系统,及时发现并处理异常情况。可以利用监控工具或自定义脚本来实时跟踪热点key的访问情况,以便在发现异常时及时调整策略或扩容节点。

限制逃逸流量:

  • 类似于缓存穿透的问题,可以采取限制逃逸流量的措施,对于单个请求进行数据回源并刷新前置缓存,避免大量无效请求直接击穿缓存层。
  • 可以通过设置请求频率限制、缓存数据的过期时间等方式来限制逃逸流量,保护后端系统的稳定性。

多级缓存:

  • 通过多级缓存来减轻每个缓存服务器压力,例如在jvm中使用咖啡因缓存数据

兜底逻辑:

  • 最后但同样重要的是,建立兜底逻辑。无论经过多么精心的设计和预测,都无法完全避免意外情况的发生。因此,需要在系统中设置兜底方案,确保在极端情况下系统仍能正常运行。
  • 兜底逻辑可以是简单的降级策略,也可以是针对特定情况的应急处理方案,例如请求排队、自动报警或人工介入等。

这里我们采用多级缓存, 为了效率考虑, 我们采用本地缓存,即应用服务器在内存中缓存特别热门的博客内容,应用构建博客刷新页的时候,会优先检查博客ID对应的博客内容是否在本地缓存中。

最后确定的本地缓存策略是:针对拥有100万以上关注者的头部博主,缓存其48小时内发表的全部博客. 总体缓存架构如下

数据库的分片策略

前面我们分析过, 该系统每天新增2亿条博客。也就是说,平均每秒钟需要写入2400条博客,高峰期每秒写入4600条博客。这样的写入压力,对于单机数据库而言是无法承受的。而且,每年新增700亿条博客记录,这也超出了单机数据库的存储能力。因此,本系统的数据库需要采用分片部署的分布式数据库。分片的规则可以采用用户ID分片或者博客 ID分片。

如果按用户ID(的hash值)分片,那么一个用户发表的全部博客都会保存到一台数据库服务器上。这样做的好处是,当系统需要按用户查找其发表的博客的时候,只需要访问一台服务器就可以完成。

但是这样做也有缺点,对于一个头部博主,其数据访问会成热点,进而导致这台服务器负载压力太大。同样地,如果某个用户频繁发表博客,也会导致这台服务器数据增长过快。

要是按博客 ID(的hash值)分片,虽然可以避免上述按用户ID分片的热点聚集问题,但是当查找一个用户的所有博客时,需要访问所有的分片数据库服务器才能得到所需的数据,对数据库服务器集群的整体压力太大。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前提背景
    • 性能指标估计
      • 文本内容存储空间
      • 多媒体文件存储空间
      • QPS
      • 网络带宽
  • 大致设计
    • Get请求链路分析
      • Post链路分析
      • 详细设计
        • 关于发表, 订阅问题
          • 拉模式
          • 推模式
          • 在线推, 离线拉
        • 关于缓存的存储策略
          • 热点Key问题
        • 数据库的分片策略
        相关产品与服务
        内容分发网络 CDN
        内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档