4

评论系统架构设计(一)功能拆分&架构设计

 3 years ago
source link: https://lailin.xyz/post/go-training-week5-comment-design-1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本系列为 Go 进阶训练营 笔记,预计 2021Q2 完成更新,访问 博客: Go 进阶训练营 即可查看当前更新进度,部分文章篇幅较长,使用 PC 大屏浏览体验更佳。3 月进度: 07/15 3 月开始会尝试爆更模式,争取做到两天更新一篇文章,如果感兴趣可以拉到文章最下方获取关注方式。

时间兜兜转转终于复习到 Week05 了,训练营第 0 期也结束了,第一期也马上开始了,个人感觉整体来看还是比较值的,因为在这里我们不仅能学到 Go 的知识其实也有很多架构设计,学习方法论,以及一些职场的话题。接下来我们就来到本次课程的第一个完整案例,评论系统架构设计,据 “某公司” 技术总监毛老师说这套架构设计支撑了 “某公司” 上亿 DAU 的评论系统。

功能模块拆分

经常能够看到很多产品经理和开发的段子,但是从完成一个好的产品来说的话,开发也是必须要有一些产品知识或者是说产品理念在吧,架构设计最忌讳的就是做需求的翻译机,最重要的就是要理解产品的定位以及背景,这样才能够做出最佳的设计和抽象。

ok,我们来看一下一个评论系统大概都有哪些功能

  • 发布评论: 支持回复楼层、楼中楼
    • 现在市面的楼中楼大概一般都是两层,基本上已经不存在无限嵌套的情况存在了
  • 读取评论: 按照时间、热度排序
  • 删除评论: 用户删除、作者删除
  • 管理评论: 作者置顶、后台运营管理(搜索、删除、审核等)

评论如果往小了做可能就是一个小的功能模块,例如视频的评论,但是如果往大了做可能就是一个评论平台甚至是评论中台,可以承接例如文章,文件等各种业务形态的评论,可以接入到不同的业务系统当中

需要注意的是,切忌一口气吃成一个大胖子,很多做技术的同学都非常有技术追求,但是如果涉及一个非常完美但是无法落地的架构,那是没有任何意义的,因为这样的系统无法产生的任何价值,但是在设计的时候也不能把未来的路堵死了,这会导致后续大量的返工。
所以我们在开始行动之前需要花大量的时间进行思考真正编码的时间不到 5%(这是毛大说的,但是个人更倾向二八原则,也就是80%时间思考设计,20%时间实际编码)
  • 架构设计一般习惯采用先把大块的服务画出来,然后用箭头标明数据流(毛老师)
  • 一个好的架构设计不应该太复杂,一个好的架构设计应该可以在一个信封上画出来(jeff.dean)
  • 尽量避免环形依赖,数据双向请求

架构设计还是有很多方法论的,例如 C4 什么的,这里课上没有细说,我也不扩展了,下面看一下这个评论系统的整体设计

  • envoy: 这一层是 API 网关层,这一层主要会做认证鉴权,流量管控相关的事情,是一个公共服务

  • comment: BFF 层

    • 前端和移动端服务的 API 都由这一层提供
    • 做服务相关的编排,例如去调用账号服务的 API 汇聚用户相关的信息,调用敏感词服务判定是否存在敏感词汇
    • 这一层抽象将评论本身的功能呢逻辑和业务平台的业务逻辑进行的隔离
  • comment-service: 评论主服务
    • 这一层只关注评论功能的 API,例如评论的 CRUD,以及服务的稳定性和可用性
罗马不是一天建成的,架构也不是一次就能设计好的,最开始其实 comment 和 comment service 是一个服务,这就导致这个服务很重,平台相关的业务逻辑与评论的本身的功能耦合在了一起导致很不好迭代,例如可能会有这种需求,新用户第一次评论就有一些积分奖励,这种气势在 comment 层做就可以了
  • comment-job: 例如 kafka 进行削峰填谷,避免大量请求将服务打挂
  • comment-admin: 面向运营和管理员的服务
    • 这个服务和 comment service 共用存储,他们本质上其实是一个服务,但是由于运营平台的权限相对来说会比较大,如果和用户的后台服务进行耦合会有安全风险。
    • 由于运营相关的服务的查询常常会比较复杂,这里利用 canal 定于 mysql 的 binlog 将数据写入到 es 中,然后通过 es 来进行检索。
这里其实需要注意,面向运营的后台会有很多复杂的查询,如果直接在 mysql 这种数据库进行查询的话一个是效率非常的低下,即使有索引也不一定能够命中,因为查询条件往往会比较复杂。其次是如果再没有做主从的情况下,直接查询线上数据库,可能一个复杂查询就会把线上数据库查挂,风险非常的高

关于为什么有 API 网关和 BFF 层,感兴趣的话可以查看前面的这篇文章 微服务(一) 微服务概览

comment service

先看一下之前犯过的一些“错”:

  1. 看从 redis 当中读取缓存,发现缓存不存在
  2. 然后就从数据库中读取数据,并且重建缓存,重建缓存的时候会将所有的评论数据都捞出来
  3. 然后使用 errgroup 批量写入 redis

看看有什么问题:

  1. 首先是并发写入 redis 会导致用户的体验不好,因为可能会出现数据不一致的情况,例如先出现第 10 页的数据然后再出现第四页的数据,再刷新就好。
  2. 然后是捞取所有的数据的做法在数据量很小的时候还好,只要数据量稍微大一些,然后缓存抖动一下,就会触发大量的 cache miss 进行重建缓存。然后这个时候 comment service 服务就会很容易 OOM。
    1. 这个很明显可以想到的解决方法就是,重建的时候不要捞所有的数据,只捞一部分
    2. 这么做在服务的访问量不是特别大的情况下有用,但是如果 QPS 比较高的话,还是可能会出现 OOM 的情况,因为同一时间的 cache rebuild 请求太多了

那后面是怎么解决的呢,看下面的这张图

  1. 先从 redis 中读取缓存,缓存读取失败
  2. 就从数据库中获取当前页的数据
  3. 然后发送一条回源消息到 kafka
  4. 然后 comment job 从 kafka 中消费获取到回源指令之后,从数据库读取数据
  5. 然后写入到 redis 当中

我们来看问题解决了么

  • 用户读取数据不一致的问题:只要我们写入的时候不并发写就行了
  • OOM 的问题
    • comment service 不会 OOM,它不负责重建缓存了
    • comment job 也不会,首先 kafka 已经帮忙挡住了峰值流量
    • 其次我们还可以在 comment service 发送和 comment job 消费的时候加上 local cache 一段时间内只要是相同的 cache rebuild 我们就只执行一次,这样就可以避免重复执行
    • 除此之外 comment job 是一个无状态的服务,我们可以很容易的对它进行 HPA

这么做也有一个问题就是可能短时间内 mysql 的压力会大一些,不过也有部分方法可以缓解这个问题

写数据其实和读数据类似,首先还是从产品逻辑上,我们认为刚发布的评论存在短时间的延迟(通常只有几 ms)是可以接受的。

  • 所以我们写数据的时候直接写到消息队列当中
  • 然后通过 comment job 进行消费,然后写入到数据库当中
  • 我们可以通过 hash(comment_subject) % N(partitions) 的方式来分发消息,保证每个主题的评论都在同一个 partitions 当中(因为 kafka 是全局并行局部串行的,这么做可以提高吞吐)

comment admin

  • mysql binlog 中的数据被 canal 中间件流式消费,获取到业务的原始 CUD 操作,需要回放录入到 es 中
    • 但是 es 中的数据最终是面向运营体系提供服务能力,需要检索的数据维度比较多,在入 es 前需要做一个异构的 joiner,把单表变宽预处理好 join 逻辑,然后倒入到 es 中。
    • 例如我们数据库中可能只存了用户 id 这里就可以把用户名称头像等信息捞出来和当前评论组合在一起。
  • 一般来说,运营后台的检索条件都是组合的,使用 es 的好处是避免依赖 mysql 来做多条件组合检索,同时 mysql 毕竟是 oltp 面向线上联机事务处理的。通过冗余数据的方式,使用其他引擎来实现。

comment

comment 作为 BFF,是面向端,面向平台,面向业务组合的服务。所以平台扩展的能力,我们都在 comment 服务来实现,方便统一和准入平台,以统一的接口形式提供平台化的能力。

  • 依赖其他 gRPC 服务,整合统一平台测的逻辑(比如发布评论用户等级限定)。
  • 直接向端上提供接口,提供数据的读写接口,甚至可以整合端上,提供统一的端上 SDK。
  • 需要对非核心依赖的 gRPC 服务进行降级,当这些服务不稳定时。

在互联网服务当中,评论算是一个很常见的服务了,但是想要做一个支撑上亿日活的评论服务还是很不容易的,这一部分的话我最大的收获可能是在不同的数据选型上,什么时候我们应该使用 redis 什么时候我们应该使用 kafka 什么时候应该使用 es,如果这是一道系统设计题让我来进行设计的话,再次之前大概率是不会想到使用 canel 订阅 MySQL 数据到 ES 的这种操作的,这是上半部分,下半部分我们会一起看看存储设计和可用性设计。

  1. Go 进阶训练营-极客时间

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK