我们开始今天第6周
这一周会介绍一个就评论系统的架构设计
评论系统大家在各种互联网产品其实非常常见了
比方说你打开微博 头条 b站 快手
基本上针对某一个内容或者是某一个微博
它下面做大量的评论
其实评论系统是在一些像微博
大家应该知道
比方说出现一些热点事件
或者说一些超热门的视频
实际上都有大量的写操作和读操作
那么它就是个典型的一个高并发海量读写的一个场景
那么这样一个复杂的业务场景
我会去带着大家把完整的一个架构设计去缕一遍
因为我有自己的一些方法
比方说
我可能会先从功能模块去介绍整个评论系统它应该长什么样
为什么要去做一些功能模块的剖析或者是解析
另外就是说我们会从全貌上
了解一下它大概会有什么样的一些模块
那些服务数据流量的一些走向是什么样的
那么我们形成一个架构的一个草图
那么之后有了架构的一个模块分层以后
其实就是要做存储设计
我底层要去支持海量的数据的读和写
你应该怎么去做一些设计也好
或者说一些取舍也好
再之后实际上就要关注可用性设计
可用性设计针对各种热点 热点请求 热点读写
或者说一些边缘的一些case
比方说我们如果产生一些cache miss
那么我的流量到存储层
我这个时候我的系统就是对我的存储层产生一些保护
所以可用性设计其实围绕着更多我们各类读写的一些场景
各种包括缓存
包括存储的一些场景去把它整个系统变得健壮
那么基本上我们在做一个系统的时候都是这样的一个方法
所以它真正去写代码的时间并不会太多
因为我们一定要用比较多的时间投入在前期的
它的一个规划和设计上
如果你很草率的把一个架构做完
那么后面的话经常返工
其实它反而增加了大量的工作量
因为我们经历应该说把架构做好之后
最终是围绕它的业务迭代上去花花心思
所以其实也是非常重要的点
那么这个案例
评论其实是非常典型的写可能会非常密集
然后读要可能会非常密集的一个场景
所以我会选它来做一个比较好的一个case
我们其实第7周的内容也是去介绍一个完整的项目
结合到我们前面5周的一些内容
我希望大家初步从工程
再到我们的微服务
然后再到我们的具体的一些使用技巧
比方说errgroup error的处理
然后API的设计等等这些
你的一些小技巧结合起来产生一个完整的架构思维
然后去做一个具体的项目评论模块
不一定所有的同事都接触过这样的场景
但是其实我认为架构设计大同小异
因为你无非就是说视频的系统或者是一个订单系统
它最终其实都是变成了一个个的组件和模块
以及它的存储上去怎么思考的
当你接触的项目越来越多以后
其实很多系统之间都有些同质性
那么可以相互去借鉴
所以这个是需要你大量的实战 大量的积累
然后慢慢变成你自己的方法论
所以我期望也是说我抛一两个这样的一个case
然后让大家形成这样的架构的方法
我们评论的话大家基本上都用过了
比方说我举个例子
我们b站在某一个 对某一个评论功能
对打开一个视频
我们看到的小姐姐跳舞很好看
然后我们想对这个up主产生一些应援的行为
可能在他评论区给他做一些鼓励
然后甚至up主自己也会拉一些这种活跃度
其实都是很常见的一种交互形式
不知道大家反正用没用
所以可以去玩一下 就玩一下评论功能
那么我自己认为说在架构设计里面
最重要的就是理解整个产品体系在系统中的一个定位
这句话是什么意思呢
你首先要搞清楚你要做的服务
在整个产品矩阵里面
它处于一个什么样的定位
它是为什么服务的
举个例子评论可能是我们属于说
它偏社区的一些这样的一个功能
那么社区的功能它的服务体系
实际上是面向内容去产生它的一些衍生的内容
因为你针对某一个视频或者针对某一个漫画去产生的一些评论
看上去是个社区行为
实际上是针对内容本身在衍生出来的一些社区行为
程序员都有些毛病
就是说我们在接需求的时候通常是很技术的思维
尽量不要去做需求的翻译机
无脑的去接受我们上游的产品或者运营同事的一些需求
而是说先理解业务背后的一些本质
这个系统到底要解决什么问题
给用户带来什么样的体验
那么这个功能在这个产品体系里面
它的定位是什么
搞清楚事情的初衷
那么你在做真正架构设计的时候
你才会想得特别清楚
因为你大概知道这个系统它的定位
它的一个产品体系是什么样的
那么你在做一些架构演进的时候
你就会提前去构思
否则的话
他提一个需求过来
你无脑的去做一些架构设计
很有可能这个系统其实就变成一个什么东拼西凑的
变成一个需求brach需求分支的那种拼凑出来的一个代码
缺乏一个从上帝视角或者是从业务视角
看到的一个完整的一个产品形态
所以我们在做架构设计
应该是尽可能先成为业务领域的一个领域专家
然后再去做这个系统
那么你这个时候想的会更加全面
真的是要奉劝每一个做业务的同事
一定要深入到你的业务里面去
了解这个业务本身的一个本质
或者说商业的本质是什么
然后你再去做系统设计
我们千万不能说无脑做需求
然后我觉得其实这不是个好事
重要的事情要提三遍
就一定不要做需求的翻译机
而是说了解这个业务背后的本质是解决什么问题
非常重要
那么我们看一个评论系统
一开始评论系统我们当时场景很简单
就是针对某一个视频可以发评论
所以说你往小里面做
它就是一个视频的评论系统
但是你稍微发散一下
你在做系统设计的时候
它是不是说它是针对某一个主题去产生一个评论列表
所以说这个主题它可以是任意的一个载体
它可以是视频 可以是漫画 可以是商品
它可以是任何东西的构成的一个主题
产生了一个评论行为
产生了一个社区行为
所以我们当时在设计的时候就考虑到它未来可能会平台化
我们还不叫中台
我们其实先把它变成一个平台功能
那么平台有一个很强的能力是什么
它可以对接到各种各样的业务形态
它是跟着业务形态可以去把它变成大家都能去使用和接入的这么一套系统
所以我们不过分抽象
我们先做到一个能够接入各种主题的评论体系
所以你看第一部抽象已经出来了
但是说你未来可能会思考的更深一点
比方说各种主题
各种内容它可能会产生不同的策略
这个策略指的是什么
你们说多少用户等级才能发评论
用户评论是先审后发还是先发后审
评论是不是敏感词是不让发还是敏感词变成星号
它有一系列的我们叫定制化的一些小的一些需求
或者说小的一些功能的组件插件
我们或者是功能的一些策略
那么这些策略你把它各种各样的形态都考虑进去以后
它最终一定从平台又会变成一个中台
它可以适配所有业务类型
所有的业务策略
它是一个无比强大的一套系统
所以我们往小做针对视频去做
往中型系统去做 它就是个视频 是一个评论的平台
你往超大型的去做
就是一个评论的中台系统不是一蹴而就
一下就变成一个超级大的平台
这这不现实
所以我们当时也没有过分抽象
我们先完成基础的多业务的接入
所以我们一开始是做的评论的平台
那么评论平台它会包含一些什么样的功能呢
我们大致会分为写评论就发布评论
那么发布评论又支持回复某一个人的楼层
你比方说第一楼的帖子
第二楼可以回复他 像一个@
@第二楼我可以对它产生一个回复
那么也可以支持所谓的楼中楼
就是针对某一个评论产生二级楼层
当我们当时做的时候没有支持那种复杂的三四层五六层的那种楼主楼
因为它迭代的层级太复杂了
从产品形态上看起来有点反人类
有点像之前网易新闻那种叠了好多楼层
其实我觉得从用户视角看嵌套很多层次
看起来是比较比较奇怪的
我觉得现在应该很少见过这种超级复杂的这种楼中楼的需求了
应该基本上就是两层
就是一个主评论 一个二级评论
当然二级评论之间也可以回复别人 就是@别人
我觉得这种还是蛮常见的
就是这个是发布
第二块其实就是读取评论了
读取平台大家最常见的就是按照time line
按照时间线
因为你去看评论
你一般来说不可能从一楼开始看
你一般会倒序看
看最新的评论
然后慢慢的再去翻下面是一个什么样的行为
这是一种按时间倒序的
当然了这时间倒序你可能也可以选一些排序条件
可能是按时间顺序
或者是时间倒序都可以
还有一种其实也是产品里面很常见的
就是按照评论的热度
如果这个评论本身有很多点赞行为
或者是有很多楼中楼的回复楼层很多
我们认为评论是一个热评
那么他可能会按热度去做一些排序
当然可能说我们也会说避免一些叫哈利波特的这种现象
就是避免过于头部过于头部的一些现象
可能也会做一些打赏
总之它会有各种一些带策略的排序行为
那么这个叫读取
那么删除评论指的是用户可以删除他自己发的评论
所以他可能只能删自己的
那么对于up主可能可以删除
因为它相当于是主题的一个管理员
那么他可能可以删除别人的评论
对于up主来说或者对作者来说
他的权限可能会更大的一点点
那么除此之外
因为这些都是用户面向c端的一些产品功能
实际上从运营角度
他需要一些管理侧的功能
所以说对公司内部我们叫to b端的话
它会支持一些管理评论
比方说 这些管理评论当然也包含作者
作者本身自己可以制定自己的评论
或者说后台的一些管理需求 他可以去
因为肯定要做个后台
就针对某一个主题去搜索
搜索一些评论的内容
或者说按照各种筛选条件去做一些过滤
然后针对某一些评论你可以进行一些管理
那么管理功能其实就包含像删除
甚至是你的评论可能是要先发布
然后要经过审核的
那么可能是有一些审核功能等等
这些都是我们叫管理侧的一些功能
在动手写代码之前
在动手设计前
一定要反复的思考他的业务形态会长什么样
那么真正做完这些业务上的一些思考
做完我们系统上的一些设计
我认为编码的时间也就占5%左右
有大部分时间你都在思考这个系统应该做成什么样
它包含一些什么样的模块
模块之间怎么交互
模块本身的存储
或者是缓存是怎么设计的
那么整体系统的可用性又是长什么样的
这些你想清楚以后
真正编码的时间不会太复杂
他不会花太久
好多人觉得写代码其实很花时间
其实对我自己写过很多系统
我反而是前期的工作做得足够多
前期可能七八十的时间都是在做像各种思考
画下草图 画下一些所谓的一些系统设计 叫system designs
就是系统的一些设计
这些做完以后
编码的时间就跟记流水账一样
啪啪啪一下就写完了
花不了多长时间
所以一定要先认真想
不要说你要写一半发现
我靠 这里面很多问题要回过来重新搞
那肯定也是不好的
因为大家知道事前做的越多
事后你其实反工节省的成本就越来越少
其实浪费的时间也是更少的
那么它效率是最高的
那么我们了解完功能模块以后
因为大家自己可以打开b站的某一个视频
然后看一下评论它大概会包含一些什么样的功能
你可以先自己体验和尝试一下
然后发帖玩一玩
你了解完这个初步有了一个概念以后
我们来看一下这套系统它的架构设计长什么样
我们先看一下这张图
评论系统里面大概会包含这样一些组件
我们上层是一个api-gateway
它是一个网关
因为我们所有的流量都是经过Nginx
经过api-gateway转发到BFF
那么都是一样的
我们前置的流量都是api-gateway
那么api-gateway通过gRPC或者HTTP
它把流量转发到我们的一个叫comment的服务
那么这个其实是个接入层
它是个接入层的服务
那么接入层服务
它可能我们看到这两条线
它可能会依赖账号服务
依赖一个敏感词过滤的服务
那么这个时候它流量又往下走
它会走到一个叫comment-service的服务
这边是接入服务
下面这个服务看上去它依赖了Redis MySQL
它可能产生一些读写的行为
然后我们看到还一再的消息队列kafka
我们终于在系统里面引入了一个消息队列
我们看看我们在这个我们场景里面以后
我们依赖kafka来解决是什么问题
其实是非常巧妙的
我们再看看还有一个叫comment-job
这个job好像看上去也跟MySQL跟Redis也有一些交互
那么它是一个虚线
那么看上去是个异步的流程
那么它能解决什么问题
我们后面也会展开讲
我们再看一下这地方
我们用了一个中间件叫Canal
Canal它实际上就是解析MySQL的binlog
为什么要从MySQL里面处理一些数据
因为数据被持久化以后
我订阅它的变更行为
我地方一看
又有依赖的ElasticSearch
ElasticSearch做一个全文索引的组件
我这个地方有个叫comment-admin的一个模块
它会直接去操作ElasticSearch
那么大概是系统大概就长这么样
你先把简单的几个模块划清楚
我们再展开看一看不同的几个模块
它分别是做什么功能用的
我们先看一下BFF层
BFF层是comment复杂评论业务的一个服务编排
比方说你要访问一些账号服务进行一些等级判断
比方说某一个等级不能发评论对吧
那么这个时候
同时BFF它可能需要向移动端面向web端的场景来设计API
我们之前介绍过BFF它是面向业务场景
面向不同的终端来设计API的
可能API要做些聚合裁剪
那么为什么会这样一层抽象
这一层抽象评论本身内容列表的处理
比方说评论的加载分页排序和面向业务的这种接入
面向业务的这种接入我们进行了隔离
那么这个comment它关注在业务平台化的逻辑上
为什么我们这样设计
我们先看看service
就是底层微服务它干嘛的
那么 comment-service他叫服务层
它核心的就是去掉了平台化的一些逻辑
因为什么叫平台化的逻辑
平台化逻辑比方说有一些评论的主题
支持先发后审
有一些要求是先审后发
它是不是有些配置策略
或者说有一些评论的主题
可能是要账号是注册就可以发
有一些是要求账号大于一级才能发
那么每个业务线它有一些策略的变化
它有些策略的变化
那么这些策略的变化它实际上是很重的
一些不同主题就是不同业务线的一些策略
这些策略我们早期其实是把接入层
和我们的数据读写两个服务揉在一起的
也说只有一个reply服务
只有一个comment服务
它解决了所有事情
或者我们实际上考考虑一下
你既然要做平台化功能
那么平台化的逻辑和真正评论
评论系统本身的数据读写存储
我们就进行了一些分离
上面的关注平台化的逻辑
下面这一层关注的是评论的读写管理
把这些功能关注在稳定性 可用性 数据读取 数据写入
那么让上游的BFF来做一些平台化的灵活组织的一些逻辑能力
所以说我们认为这系统架构设计上
它处于一种叫关注分离
上层关注平台化的接入
下层关注评论场景本身数据的写读管理
可行性的一些设计
所以我们当时是这样的一个分法
但是其实也不是说一开始就变成这样的一个架构
其实我们一开始这两层还真是合在一起的
就没有这样拆分
那么job是什么呢
job我们现在就一句话
消息队列最大的用途是做削峰处理
我们看一下这个地方
comment-service写回源 这个地方写回源
这里有个写操作或者说回源操作
什么是回源后面我们会讲
那么它会把数据流到kafka
我们看一下comment-job
它核心就是订阅的kafka的消息
它来做一些逻辑处理
我们知道消息队列一般引入干嘛
和异构的系统进行解藕
或者说做一些削峰
因为你的数据先打到消息队里
我慢慢处理它是不是需要分开
我们先留一个悬念
它如何去削峰的
你也可以先想一想
我们再看一下admin
admin叫comment-admin
就这个模块 粉色的模块
对于面向公司内部管理
公司内部针对业务运营的一个平台
我们叫管理平台
为什么会去独立一个服务
我们按照安全等级来划分服务
尤其是划分出一个运营平台
是对内部的to b端的
划分出一个c端的面向用户的平台
这两个服务它实际上在微服务架构里面
它是共享存储
就是共享使用MySQL
共享使用Redis
我们一般来讲经常服务会说
微服务要独占这样一个MySQL独占一个Redis
但是它如果是一个业务体系里面的
那么它可能使用共享的一个模式
它会share底层相同的MySQL和Redis
为什么要这样做呢
因为我理解这种admin的服务
这种运营后台管理的这种服务
它通常安全的要求更高
可能是有一些非常高级别的一些API
比方说审核的功能
那么如果你的API设计是放在comment-service
那么实际上你这个代码你是无法组织
开发人员不小心把一个高级接口不小心对外
或者说不小心调错了
那么它可能会产生一些副作用
因为如果是c端用户调到这种审核功能
或者是删除评论的功能
那么其实是非常危险的
所以说我们从安全设计上也会把它拆分成两个服务
那么运营体系用的最多的就是搜索和检索功能
比方说搜某一个关键字
比方说我现在要系统性的一些盘查
发了某一个词的这种评论是什么
我要进行批量的一些删除操作
类似这种功能
或者说我要去搜某一个用户的
或者搜某一个主题下面有哪些评分
有大量的组合条件式的一些搜索运营的一些需求
那么我们一般的做法都不会去依赖它原始的线上的数据库
我们会把这个数据double冗余一份
使用一个别的检索引擎
比方说fulltext的ElasticSearch
fulltext的search系统
它是一个全文的检索的搜索引擎
那么由它来构建一个搜索需求
那么也就是说我的comment-admin基于ElasticSearch来做搜索
那么搜索完之后
你是不是拿到一坨评论的内容和id
那么在针对id去操作原始的数据库
让它变更状态也好
删除也好等等做一系列的操作
所以是先从ElasticSearch捞一些数据出来
展现出来
然后再做一个二步操作
去操作cache或者操作MySQL
达到这样的一个管理的需求
那么dependency我们再看一下
它其实我们comment还依赖一些第三方的服务
因为一个完整的一个场景里面
实际上你很难说自己独立的就可以完成所有的功能
你可能会依赖外部的服务
比方说你要依赖账号
你做等级判断
你要做节操值判断
我们当时b站的评论系统它可能依赖一些这种节操
比方说你经常去跟别人吵架
或者说发一些引战的一些这种评论或者是去骂人什么的
可能会扣掉你的节操
那么如果你的节操值特别低的话
可能就不让你发评论了
有些这样的一些能力
所以它会依赖账号
当然它也会依赖filter-service
我们这边叫敏感词过滤
可能会有一些这种涉黄就不希望它出现在社区里面
可能我们希望社区更加干净
类似的可能会依赖这些服务
架构设计实际上就等同于数据的一个流转的设计
数据是怎么流向的
数据的设计
因为你梳理清楚数据的走向和逻辑
那么你就能搞定一大半的架构设计
那么系统之间如何交互
其实就几个原则
我们尽量是从上到下
尽量是避免环形依赖
也避免双向请求
我调你尽量设计的时候是从上到下
有些情况下很难说完完全全的从上到下
那么这个时候你至少是要避免出现环
环调用或者是相互调用这种情况
那么这些是大概的几个模块
概览的我们看完之后我们深入去讲
我们先从comment-service这个模块来讲
这个模块我们之前讲过
它是专注在评论的数据处理
它只专注评论的数据处理
楼层怎么构建
然后楼中楼怎么去设计
所以你认真思考一下
实际上它就是一种叫Separation of Concerns
关注分离
它只专注在数据处理上
我刚才也讲了
我们一开始comment-service和comment和BFF接入它是一层的
它这一层耦合了大量的平台业务
功能性的业务和数据存储读写的一些业务
非常不利于迭代
因为我们知道策略是多变的
数据的存储 数据评论的构建
读写是稳定的
这个可以理解吧
你的系统这些大功能评论的读 排序
你的评论的回复 删除
这些是稳定的
但是策略是多变的
策略指的是主题要先审后发
这个主题要先发后审等等一系列的
或者某一个用户等级不能发
某个用户等级可以发
这些策略往往是多变的部分和稳定的部分通常怎么做
是不是把它们拆开
是不是底层的服务就相对稳定
可以在那持续跑
上游的快速做一些迭代是不是就更好
如果你耦合在一起的话
势必会给稳定的系统带来不确定性
和可能会破坏它的稳定性
这个是非常重要的一个点
那么迭代也是隔离的
我觉得这些都是
不是一蹴而就的
都是经过很多人的一些思考
慢慢演进成样子的
那么我们先来看一下它的读的核心逻辑是什么样的
我们知道一个海量的业务
尤其是独多写少的一个业务模型
它通常是什么主动写少
你是不是可以用缓存啊
我见过好多系统
其实都是透传到数据库去操作
你会发现真正的大型系统缓存设计是最最关键的一个点
第二重要的关键点是什么
是我们的消息队列的设计
最后是存储设计
只有你把缓存和消息队列用好
你的系统问题解决80%
剩下20%靠你的存储去解决
那么存储就是可能MySQL
或者是其他一些很高级的一些或者是性能很强大的一些存储引擎
能够解决你剩下的20%的问题 为什么
因为存储层不太容易被scare out或者scare up的
它的扩缩容是非常复杂的
可能要一个很强大的这种分布式系统来做一些分布式的存储
但是存储层它的的确确是最容易成为系统瓶颈的
所以一般先把上层做好
解决大部分问题
那么你的存储虽然不是性能最强大的
但是因为你的上层架构设计的好
那么你对存储的依赖就变弱了
变弱 因为你的系统的本身的鲁棒性更强
所以缓存非常重要
所以评论的读取一定是先读缓存 再读取存储
那么缓存没有命中我们叫Cache-Aside
就是缓存没有命中cache miss
再去读我们的数据库
我们当时没有那么高级
也没有什么中间件团队
就是用MySQL 就是用DB
大家其实会觉得MySQL是不是
没有那么强大一个单机版的
实际上我可以告诉大家
我们现在有很多业务其实都是单机版的一个MySQL
就没有做很多sharding 为什么
因为实际上MySQL的读写能力还是很强的
如果你的系统机器配置还不错
它还是很强的
只要你的表设计相对合理
真的很能扛
我觉得扛个几十亿数据是毫无问题的
只要你的表不超过几百个g是没什么大问题的
但是拆表其实你可以拆一拆
但是当表不超过几百个g
你想想已经可以存好多业务的
大家经常觉得几百万数据就拆个表
我们其实几十亿才开始拆
但我们当时一开始我们就知道我们数据量可能会非常大
所以我们一开始就拆了好多个表
但有一些公司可能有更好的一些中间件
可能底层有好多个MySQL
然后上面可以自动做sharding
包括像现在一些新型的像NewSQL像TiDB这种
它也是一开始就shrding好了
我们其实叫平民版平民架构
没有什么很高大上的一些黑科技核武器式的那种存储引擎
其实就是朴素平民的MySQL
其实也能看出我们这么大一个体量
我们这么高的dau的一个场景
像快手可能这种可能过几亿
其实都是朴素架构都能解决的
就是靠MySQL其实就能扛住的
大家不要觉得有很高级
好 ok 所以第一步我们就是先读缓存在读DB 很简单
就一个Cache-Aside cache miss独立的逻辑
但是这里面有一个坑啊
早期我们的cache rebuild
cache rebuild是什么
如果你产生了一次cache miss
那么cache miss行为是不是要去透传到DB
DB捞出来的数据是不是要回塞到缓存
你刚读的数据我们通常认为什么
接下来还会有人来读
你肯定就是做一些缓存的构建
那么刚开始我们缓存构建是直接做
comment-service里面的产生cache miss
然后去把它回填到Redis
或者是memcache里面
那么重建逻辑我们叫预读
因为用重建为什么要做
其实大概率用户访问第一页
其他用户也可能会访问第一页
或者说访问第一页的用户
接下来马上会访问第二页
对吧
所以说你一定要做缓存构建
这样的话你的缓存的命中率就会高
所以缓存会超前去加载
避免频繁的cache miss
那么问题就来了
我们现在是做到comment-service服务里面去做缓存的回填的
去填充 从DB里面捞了一些数据然后填充到Redis
当时我们出过一个事故
当时是我们的cache出生的一些抖动
或者说出现一些故障
那么这个时候产生了大量的cache miss
大家知道cache miss会透传到DB
DB当时没挂
但是缓存抖动
它本身上就是这种金群现象就会有大量的回源
那么大量的会员意味着触发大量的cache rebuild
就是缓存会重新去构建
使用这样的一种模型
大家可以想象一下
大量的人去透传到DB去查询
DB可能扛住了当时
但是发现我们的服务挂掉了
我们服务oom掉了
我们服务为什么会oom
当时一直在想
后来认真一思考
产生大量cache miss以后
虽然透传到DB
DB扛住了
那么它要做缓存回填
这个时候大量的人都会去做缓存回填
那么意味着你内存里面的数据
这一份评论数据
你是在你的comment-service里面去做的对吧
意味着它留有很多请求级别的这些缓存去慢慢回填
也就是说你的Go的服务里面有大量的小对象去申请
然后并且存着活的时间还比较久
那么就特别容易oom
因为你比方说有1000个cache miss
我就捞1000个去不同的主题里面捞很多评论
取到我的coment-service里面
然后慢慢的回填到Redis
那么一旦我的Redis稍微慢一点或者各种别的原因
可能是请求量非常大
然后因为你要回填
你要回填
所以在内存里面堆积的就特别容易oom
所以我们当时就出过一个事故
就大量回填的时候oom掉了 服务挂掉了
所以我们后来设计的时候啊
把回填缓存的逻辑
把它从comment-service里面拿掉了
所以我们看到图里面有一条绿线
它之前的kafka我们这个地方写了个回源
我们后来怎么做的
我们是不是可以投一个消息告诉kafka
告诉kafka某一个主题产生了一次cache miss
那么这个时候comment-job消费到这个消息以后
消费的指令以后
它再去MySQL去做一个加载
然后再回填到Redis
那么这个时候因为comment-job它是一个创新型消费的
前面一个处理完再处理下一个
所以说它的内存整个可控
它不会说一波流的这种请求进来
它就会加载这么多
它是说它是流式消费的一个主题
赶紧去做缓存的cache的处理
因为使用的kafka
所以我们comment-job其实是可以使用多副本的
使用多个节点
那么每一个节点可以负责几个kafka的partition
是所以我们当时大概是这么一个逻辑
使用异步回源
这时候读逻辑就变成说cache miss先读catch
如果miss读DB
DB读到了直接返回
同时产生cache miss 投递一个指令
投递一个消息
这个消息不是那条数据写到kafka
是我投一个很小的消息告诉你
一个topic产生的miss行为
那么这个时候我们的comment-job如果消费到某一个topic产生miss
那么它就把从MySQL里面捞出来数据
慢慢回填Redis
这样的话就把oom的问题下放到comment-job做了
所以当时看起来还好像挺稳定的
这个是一个非常巧妙的设计
我们看完读操作
我们再来看一下它的写操作
写的核心逻辑看一下这个图里面
comment-service还有一个
这里到kafka 还有个地方叫写
所以我们就可能猜到了我们写操作是不是也使用到了kafka
对的 为什么
因为我们特别担心像微博当时出现类似明星出轨
这种特别热点的一些事件发生
写和读相比较
写你可以认为是什么
所有的请求最终一定会透传到存储
它虽然也会去操作cache
你的所有写请求一定会落到我们的存储层
也就是我们的MySQL里面
系统的瓶颈也往往来自于这些
存储层或者叫有状态的这一层
那么在写的设计上
我认为刚发布的评论有极短的延迟是可接受的
就是你刚发布一条评论
它有极短的延迟
可能几个毫秒你再去刷
其实用户其实感觉不到的
所以当时想的是对存储的直接冲击
可以先把冲击下发到消息队列
因为消息队列它是一个顺序的io
它的吞吐会比MySWL可能好得多
它是一个顺序io
整个吞吐能力会很强
所以我们把这种写压力先放给kafka
那么再按照消息反压的思路
什么意思呢
如果你的存储我的MySQL latency非常高
延迟很高
那么意味着我的comment-job
它的消费能力就会下降
那么消费能力一下降
一旦消费能力赶不上写入的速度
消息会堆积在那里
不会堆积在comment-service
它会堆积在消息队列的kafka里面
所以说kafka为什么叫削峰
他因为能够做消息的暂存储以及去峰值的能力
因为你的峰值是因为你的底层存储
MySQL latency变高
导致它消费不过来
所以它叫消息反压
反压在kafka里面去堆积
那么你的comment-service
因为你的冲击都充到kafka了
所以comment-service也很稳定
也就是说它的comment-service的rpc的流入和流出和消费是ok的
那么现在瓶颈在哪 在MySQL
一旦MySQL又恢复了
我们的comment-job自然而然latency变低
那么它自然而然从kafka的消费速度又追上来了
最终实际上如果真的出现MySQL特别卡
压力特别大
我们的的确确系统这个时候是有损的
就像降级一样
最终它其实就变成一个消息的最终一致
那么它牺牲的是时效性
所以这个是我们当时的一个思考
另外就是说kafka它是按照partition
它有partition概念
kafka的一个主题一个topic
它内部是有好多个partition叫分片
你可以认为说一个partition就是一个文件
那么这个文件是顺序io的
那么它吞吐自然很好
那么你有n个partition是不是
因为你的并行的能力是乘以n的
你又n个partition是不是你的吞吐能力就变强了
这样看好像kafka不是一个顺序的消息队列
所以说kafka原则上它的吞吐模型是叫全局并行
并行消息就乱序
但是某一个局部的partition它一定是串行的
所以说我们在kafka的partition的路由的规则是你自己指定的
我们这个地方其实是按照主题来
把它sharding到一个partition
那么一个partition内的消息你可以去串行消费
所以这个是还不错的一个设计
当然了 你可能会问
如果这个时候产生的一个热点
某一个主题
比方说某一个某个明星被发现出轨
那么这个评论一定会爆火 是不是
所有的写操作都会命中到
你用一致性希也好
用哈希这个主题id求于求余
余数就是partition的数量
你一定会命中到某一个patition
patition是不是也可能会产生热点
这个是非常有意思的一个点
这个问题我们在后面找机会我们去介绍一下
这个问题怎么彻底解决
其实也是非常巧妙的一个点
那么我们现在先理解为就是
用普通的哈希求余的方式
那么意味着某个partition中的评论的某一个主题
具体的某一个主题的数据都在一起
并且是串行消费的
那么这样我们就可以去做串行的一些处理
同样的对于回源的那些指令
回源的一些消息
回源是指的是告诉我的kafka
告诉我的comment-job
这个时候产生了一个回源
那么对于回源操作
我要进行进行一个缓存构建
所以它的sharding的方式其实可以类似
其实是完全可以类似的
所以这个是我们基本的数据的流向
读和写是这么来做的
这里面其实还有一个点
comment-admin是怎么去玩的
我们common-job既然它是异步去做这个数据的持久化
我们看着这个图
我消费到一个写操作的行为之后
是不是直接先去更新DB
其实就是把评论消息插入进去
插入进去是不是在更新Redis
所以它其实就两个
一个先写数据库然后更新缓存
我们读操作我们再回顾一下
读就是先读Redis
然后cache miss了之后再读MySQL
然后再把回源的消息投到kafka
所以大概读写流程就这么简单
其实你看就这么平民的一个架构
撑着我们这么巨大的海量的一个系统
我觉得确实是如果你设计的越巧妙
你对基础设施的依赖就越弱
请记住这一点
有两种工作模式
在大厂里面让开发人员无脑的去做系统设计
强大的依赖我们的基础设施 这是一种
另外一种 如果你是在发展阶段发展过程中
经过你精心设计的系统
你弱依赖这些底层的基础设施
仍然可以达到同样的效果
当然我觉得最完美的一定是具有强大的基础设施
比方说MySQL cache都无比强大
你的系统设计非常巧妙
那么最终这个系统的鲁棒性一定是非常强的
请主意这一点
那么处于不同的阶段
你要做不同的取舍做
假设你现在没有很强大的基础设施
就这么平民版的架构
你就可以按这样的思路来做
系统设计去规避掉一些问题
这个其实是你的判断能力需要你去培养的
我们再来看一下comment-admin
之前我们介绍过comment-admin做运营管理的
那么我们这个地方其实核心订阅的
就是从MySQL binlog中
binlog有些哪些数据
有insert update delete
有这些事件
那么我们使用类似像阿里巴巴开源的一个Canal的中间件
它会去解析MySQL的binlog
它是模拟成MySQL的一个slave
然后把这些binlog抽到它的组件里面
然后我们把解析完了的这些binlog数据json或者protobuf化
然后把它序列化放到我们的kafka消息里面
把用业务原始的crud的操作把它投到kafka消息队列
然后我们再起一个消费的程序
然后最终它还会去join一些数据
为什么要join一些数据
因为你评论本身都是一些原始的一些表的数据
那有可能可能会需要join一些别数据源 凑字段
把一个评论的一张小表可能join一些表变成一个大表
我们就变成一个宽表
为什么变成宽表
因为你的检索条件可能是多样复杂的
可能需要join不同的一些维度的数据
把它变宽以后
再把它变成一个完整的json
再把它写入到
导入到我们的一些个ElasticSearch里面
那么一般来说运营后台的检索条件都是组合的
那么使用ElasticSearch的好处就是说
避免依赖MySQL来做条件组合检索
因为你在线上的MySQL建大量的这种组合索引
一是最终会影响我们的写操作
因为你索引越多写操作写io就越慢
那么可能会影响线上我们的业务
而是说我把这些数据订阅到另外一个检索系统
它可能做检索效果还更好
那么这样的话
其实通过冗余数据的方式
通过异构的不同的其他检索引擎来
达到多维度的一个检索效果
那么其实读压力就放到ElasticSearch了
那么它其实是一个异步的流程
这里面有一些
但是可能也就是个秒级或者是一个毫秒级的delay
所以对于运营来说基本上是完全足够的
我们ElasticSearch其实最终就是说展示 检索
返回一些各种数据的id
然后这些id
比方说你做评论的时候
你肯定会搜用户
你不可能把一个用户id就展在这个页面上
就是你做管理后台的时候
那么这个用户id你搜出来的数据
它不是有id
其实在comment-admin
它其实面向的是一个内部的BFF它会去join账号服务
把用户的昵称字段展示出来
你不会去存储原始的昵称
因为昵称会改来改去
它会去join原始的账号的服务
把uid账号id换成昵称展示在页面上
可能还会拿到评论的id之类的一些数据
这个时候你在管理的页面里面可以勾选
然后进行删除 审核通过 或者是置顶等等一系列的操作
你再会去操作MySQL和Redis
其实我们权杖的运营体系
基本都是基于ElasticSearch来搭建完成的
就是所有偏运营检索的一些需求和功能
我们都是基于ElasticSearch来做的
我们最后再看一下comment的BFF层
那么它最终其实是面向端 面向平台
面向各种业务组合的一个微服务
所以平台扩展的能力
我们都在comment这一层来做
为了方便统一和准入平台
以统一的接口形式提供平台化的能力
其实我当时在想comment它既然有面向c端的一些接口
面向c端的就是比方评论读写这种接口
其实还有个平台一些能力
实际上更严格来讲
它应该分为并向c端的和面向对公司内部的
就是b端的一些评论 主题的注册
业务策略的一些逻辑的控制
我觉得应该要分开的
我自己觉得应该分开会好一点
整体来讲它BFF这一层它会依赖其他的gRPC服务
包括你去做数据的组装也好
比方你账号
你的评论列表里面可能依赖账号的卡片信息
账号的卡片信息你要做一些数据的组装
另外就是说你可能针对不同的主题
要应用一些不同的策略
就BFF层可能真的有自己的数据库
但是它是一些策略的数据库的逻辑
比方说我们刚刚之前一直强调的像用户等级的限定
先发后审这些功能都比较重要
还有说你对一些非核心依赖的gRPC服务可能会进行降级
因为你这一层要依赖很多其他的rpc的一些服务
比方账号不可用
那么可能会返回一些
我们之前讲可用性的时候也讲过
可能会返回一些默认头像
默认的名称等等
所以这一层一个是面向端
面向c端用户去做接口
还有就是面向我们内部的去做数据接入
并且应用一些规则
应用一些平台策略
那这一层就是把策略和评论本身的内容构建分成两个服务的
我认为是这样是相对比较合理理解
并且它的迭代节奏是有些区别的
假设有一天我突然要对评论的底层存储引擎进行迭代
其实我的业务策略这块服务完全不会动
到它两个人发版的节奏可能也不太一样
我认为这个是比较好的一个划分的一种方式
那么我们大概讲完架构设计
接下来我们就是看一看它的存储设计是怎么去做的