好 我们开始第5课
第5课主要是重点讲微服的可用性设计
这一块的内容可以说是干货满满
因为我们出去面试或者说我们工作工作中应该会大量使用到
因为大家知道微服务从巨石架构演进到分布式架构
它的复杂性会变高
我们一个进程内的进程间的通讯
那么进程间的通讯又经过网络
整个的链路环节是非常复杂的
那么在复杂的环境中
我们可能会遇到各种各样的错误
那么这些错误可能是一个局部的单点的错误
但是它很有可能会引起一个连锁的故障
我们整个第5周其实都是会围绕可用性
包括隔离 超时处理 容错 熔断 降级 限流等等
甚至我们还会讲介绍一些关于负载均衡的一些知识
这些都是尽可能去解决我们微服务落地过程中可用性的一些问题
我们看一下大纲
包括隔离
包括超时控制
过载保护 限流 降级 重试 负载均衡
然后我会介绍一些最佳实践
在每一个章节里面我会带上一些case study
我们之前遇到的一些事故
从这个事故中我们总结出什么样的经验
其实我也非常希望大家在自己工作过程中
如果遇到了一些事故
通过case study的方式对事故进行总结
避免犯重复的错误
然后形成一些所谓的todo list这些
todo list有可能是针对架构的
有可能是针对我们整个生产
整个从开发到上线
线上到运维整个环境的一个流程的一些改进
这些都是对可用性有很大帮助的
其实我们也是一样的
就是踩了很多次坑
然后遇到很多次事故总结出的这些经验
OK 我们先看一下第一大块
隔离本质上是对系统或者资源进行分割
从而实现当系统发生故障的时候限定传播范围和影响范围
也就是说我可能的的确确是有局部有故障
但是即便发生故障后
只有出问题的服务不可用
它不会影响别的人
因为我们有时候像我们的BFF
可能依赖其中的一些链路的微服务
那么如果其中有一个微服出的故障
但是我这个时候局部是可以做降级或者做一些容错
但是我的整个核心场景其实并不会受到影响
我们就是要保证局部单点的一些问题不会影响到全局
就像我们右边这张图
大家知道船舱隔离这种方式
有一个地方漏水
我只要把门锁上
然后把它关起来
漏的水其实它只会影响其中的一个船舱
我们看这个地方其实都是物理隔离的
都是有时限隔离
那么它其实是控制影响面
那么从我们微服务的角度
或者从我们后端架构角度
它分为几个层面
第一是叫服务隔离
服务本身的一些属性
比方说动静的分离和读写分离
我们待会会展开来讲
包括像轻重隔离
因为有核心的服务
然后有处理的快的处理慢的
可能有些热点我们需要识别出来把它隔离处理
然后还有像物理隔离
从线程级别到进程 到集群 到机房
我们先看一下隔离里面的服务隔离
第一个要讲的就是动静隔离
小到CPU的cacheline
大家知道cacheline它是一个固定一个大小
比方4k或者多少k的一个类似这样的一个内存单元
CPU里面的一个缓存单元
加速的单元
我们小到cacheline
其实每4k或者每8k或者每64k这样的隔离
相当于你用第一块
我用第二块这样的方式说
避免产生内存的false sharing
false sharing这个问题其实在面试过程中也会经常被问到
大家可以去点一下我们PPT里面我标示的那个地址
可以了解一下
我们看小的CPU的cacheline
然后再到我们数据库设计中像MySQL
我们有时候会去设计两张表
一张表可能经常更新
另外一张表可能不怎么更新
从数据角度我们一般会避免把这两张表放在一块
因为有可能他们之间会产生一些相互影响
大家都是在一个DataPage的cache
就是在MySQL的BufferPool里面
因为你一更新它整个配置一过期
它就会什么
频繁从BuffPool里面被剔除
那所以到表 到数据库设计
然后再往大的想
我们大的架构设计中的图片
静态资源等缓存的一些加速
本质上都是体现一样的思路
即加速缓存访问变换频次比较小的
我们去加速或者缓存
那些容易不怎么变更的那些资源
我们就要把它做一个静态的缓存
典型的像我只举了个case
像CDN的场景里面
我刚开始工作的时候其实不太清楚整个后端的一些架构
尤其像CDN那些架构
你像我们的做前端 做外部网站
我们知道有些图片css js这些文件
以前我们做一个web服务的时候
会把静态文件和我们的API打包成一个服务
我相信肯定有公司是这么做的
或者我不知道大家项目中有没有这种架构
的确 我们以前把前端的资源文件和
我们后端的一些这种API基本上是打包在一块的
等到大规模化以后
我才知道静态资源应该和动态的API产生一个分离
那么本质上也是体现在这种服务层面的一个隔离
为什么要这样做呢
我们知道前端的静态资源可以放到对象存储里面
比方说我们现在在用阿里云
我可以把它上传到像我们的静态脚本 附件 图片视频 音频
都可以放到云存储的一个服务对吧
在阿里云的OSS上
通过使用CDN
那么CDN来加速让用户就近访问接入
那么它可以什么
CDN可以回源到我们的oss
也就是我们的对象存储上
它可以通过回源的方式
那么我们CDN一旦回源以后
我们的CDN就会缓存之前访问过的这些资源
比方说我们图片或者静态资源一般都有个maxage
缓存多久
那么这样的话你下次再来访问
它就会命中到就近的CDN节点
然后直接返回给用户
这样的话首先
第一
它降低了应用服务器的负载
也就是说你的API的服务器
不会被这种静态的一些资源所受影响
它全部会尽可能的加速或者缓冲到CDN的节点上
所以它是不是体现了我们说的
我们加速或者缓存访问变化频次相对比较小的
第二点就是说我们对象存储的存储费用其实也是更低的
就是把这些静态文件放到我们的web容器里面
因为我们可能要租一台服务器做一台虚拟机
可能是一台ECS
这肯定是对象存储相对来说存储成本更低
另外就是说有一天我们的图片或者是我们的小文件
甚至我们视频音频
导致我们当季的外部程序如果放的下的时候
这些存储其实也是你可以无限的扩容了
而不是说耦合在你的web应用程序里面
那么你的web应用程序
因为利用本地磁盘去存这些东西的话 它是带状态的对吧
所以我觉得这个也是不太好
的呃最终的话静态cdn的带宽加速它成本也更低
所以从我们大的架构来看
服务隔离是一个比较典型的思路
我们往小里面看
那么服务隔离里面的比较典型的
就是MySQL的表设计
我们曾经内部的像视频稿件这张表
你看看我们是怎么做的一些优化
首先我们有一张稿件表叫archive
那么它存储的是稿件的名称 作者 分类 标签 状态
然后我们一般来说每张表都会有一个
create_at和modify_at
就是所谓的创建时间和修改时间
就是每张表都会有这两个比较基础的一个字段
那么在一个完整的业务投稿过程中
一旦我们用户比方up主投递的一个稿件
那么稿件被创建以后
一般你很少会去改名称
这些字段你基本上不太会去改
但是我们围绕稿件它会有一些统计表
什么叫统计表
比方说我们稿件的播放数 点赞数 收藏数硬币数这些数字
我以前见过一个什么样的事故
有人把这种统计类的那种需求用什么count *去做一下
这种基本上把数据库一下就打死了
大家不要小看这个
有很多这种统计类的技术的这种业务逻辑
都是用数据库的count *来做
大家知道这个其实是比较低效的
假设我有100万的收藏
你要去count *里面where条件
肯定效率低
所以一般我们都会设计一张叫archive_stat的一张加速表
那么这张加速表就是缓存或者说存储了
围绕这场稿件的很多统计类的一些指标信息
为什么我们会把这两张表进行一个隔离去
设计它们之间有一个id的主键的映射
也就是这个achieve的id主键
和achive_id主键本身是一样的
就是它们是一一映射的关系
那么为什么去隔离
因为稿件表本身是非常低频去进行更新操作的
但是对于achive_stat这张统计表来说
它是非常高频去更新的
我们知道MySQL的这种BufferPool
它一般是用来缓存我们数据库MySQL底层
存储上的所谓的一个叫DataPage
可以理解为就是缓存了表的行
就DataPage缓存了很多表的行
那么如果你频繁更新这张表
比方说你把这些技术耦合在achive这张表里面
它会导致DataPage会不断会置换出去
因为它过期了
有一个更新操作
它间接的就会导致命中率下降
命中率下降它就会命中到什么
命中到磁盘
所以我们在表设计过程中仍然沿用类似的思路
即我们的主表很少更新
我们的achive_stat这种旁路表或者叫统计表
它会很高频更新
我在设计上我就会把这两张表去进行什么
进行一个分离
所以即便有时候上游的cache没命中
上一个cache就是我redis可能没命中
我透传到MySQL
因为MySQL本身有BufferPool的一个缓存
所以我们尽可能让achive本身的一些元信息能够被缓存到MySQL的BufferPool
那么如果当然我们BufferPool还没有
它就最终去磁盘里面去再把它检索出来
往小底看表格设计也是类似的一个思路
这里面有个小细节
那么这张achive_stat表的这些技术是怎么来的
我们做完微服拆分以后
点赞收藏可能都是个独立的服务
我们当时的设计是随着我们的拆分微幅的拆分
我们会设计从各个收藏服务
从那个点赞服务
从投币服务去订阅这些数字
然后订阅这些数字以后再去更新这张achive_stat表
就是说它可能是从别的服务里面
它保存的是别的服务里面真实的一些数据
但是我会订阅这些数据
从binlog里面
然后再更新到这张achive_stat
那么这一张achive_stat的表本质上它是一张什么
一张加速表
专门用来做稿件统计信息的一段加速查询
所以它实际上是有一些冗余的
为什么这个时候因为我假设要获取稿件信息
我就避免说获取稿件信息还要调用像点赞收藏
对吧
我就不需要再扇出去请求下游的微服务的
因为可能这些技术都散在各个微服务之间
我做一张这样的加速表也是有很多优势的
那么像我们服务隔离里面还有所谓的叫读写分离
我们现在的话基本上是一主三从
这些从库其实也可以
如果你对一致性要求没有那么高
你也可以使用主从分离
它本质上也是一种隔离的一种思想
像redis本身有master slave
也是有副本的
那也是可以在master slave之间都进行一个读写分离
还有就是CQRS
我应该记得在第一周讲微服务的一个模型里面
应该提过CQRS的思路
所以CQRS的思路即命令即职责
就是查询和命令两个职责分开
本质上我认为它也是一种服务隔离的形式
那么除了服务隔离还有什么轻重隔离
轻重隔离第一个就是叫核心隔离
我们一般业务它不是所有的线上服务都是最重要的
我们所以会按照L0/L1/L2来进行一个不同等级的划分
比方说L0的服务
像稿件基本上大家都要去查询
那么账号它也是一个L0的服务
因为账号也是很多人依赖
那么可能次一级的像会员用户信息投稿
或者说关系链动态这些可能是L1的
那么可能再次要一点的
比方说像我们的验证码 位置信息 敏感词过滤
可能分成L2
在对待核心和非核心的这些微服务
你要考虑故障率的问题
故障率是指什么
比方说它们机器资源到底是要独占还是非独占
像账号这种服务我们认为它非常的重要
那么我们会划一批机器池
让他们独立部署在这些物理机上
避免相互被其他的微服务所影响
L0的这些服务它会差别对待
当然了对于L0的服务
我们还会使用我们第一周讲的微服务的叫多集群
我们可能会冗余多套集群
记得吧
冗余多套集群
然后它们每个集群之间会独占cache资源底层的存储 像DB
因为存储不太好是scare out对吧
所以我们存储还是会只用一份
但是我的redis可能是有多份副本
这样的话我的整个吞吐和容灾的能力会变得更强
那么多集群的话因为连接数非常多
我们使用子集算法去选部分连接进行rpc的一个建联
其实也是一个
那么轻重隔离里面
还有一个点是快慢隔离
快慢隔离
我们通常可以把微服务它的一个吞吐想象成一个蓄水池
当我们有洪流进来的时候
池子它需要一定的时间才能把这些洪水排出去
大家看过什么大三峡大坝放水这种情况
这时候如果有其他支流
可能有一个其他的支流的一些水流进来
那么它在池子里面待的时间取决于
我们前面放洪的时候它的什么
排放时间对吧
如果你整个池子存在这样的洪流
这些支流的水要被排走
是不是意味着它的在池子里面的耗时就会变高
它需要花更多的时间等前面的排完它才能继续排
耗时会增加
所以对小请求它就会产生latens影响
它就会变得非常卡非常慢
我这边举了一个非常典型的例子
我加入到大数据体系以后
我们看一下这张图
我们一般来说做大数据的数据传输
就是把我们手机端的一些event tracking
所谓的叫事件追踪或者说用户行为数据
我们通过动态CDN加速上报到我们的比方大数据的机房
那么大数据机房它会通过一个网关把这个数据投到kafka
kafka有人来消费
消费完之后会把它落地到HDFS的存储上
也就大数据的hive表
或者是大数据的HDFS存储上
当然可能这个kafka还有一些别的下游
这个时候它可能会需要落地到HDFS
也可能会需要落地到Elasticsearch落地到es
可能还会落地到MySQL
有些日志流它可能需要直接落库
那么这样的一个多seek
我们叫sink
sink端就是一个叫source或者sink
我要sink到某个地方去
如果你有这种多sink的场景
我们可以想象一下
有一个消费者从池子里面从kafka里面拉了一条数据
它要把它写到三个地方去
那么如果三个sink端
HDFS es或者MySQL
它们之间存在一个差速耗时比较高
有些耗时比较低
它们存在一个消费的吞吐能力的区别
有些人写入比较快
有些人写入比较慢
那么你这个时候就会产生相互的影响
最终的话你从kafka里面拖的数据就会受到
写的最慢的sink端就会影响
你这整个数据全部会反压
对积到kafka
也就是说间接影响到了数据被落地到下游的这种吞吐能力就会下降
这是其中一个问题
当时我们的架构除了这一个缺陷
还有一个地方是什么
我们当时只用一套kafka
并且只有一个topic
因为当时我们是怎么想的呢
整个事件流你流过来以后
我如果只设计一个toipc
意味着我这个topic在不同的kafka的broker
我们broker其实就是一个卡夫卡的broker server一个接收端
那么broker的本地会有所谓的topic的partition数据
就是分区数据
因为大家知道kafka是按照topic内部partition把这个数据分布在不同的broker上的
那么如果我建一个logid
logid其实意味着一种日志流
比方说点击是一种日志流
展现是一种日志流
那么如果是两种不同的日志流 两个topic
假设我不同的业务线有不同的点击
有不同的播放
我是不是要设计很多个logid
意味着我就有很多个kafka的topic
那么topic一多呢
以前kafka的优势是顺序io
因为你的数据都是追加到某一个partition的
那么你topic一多我就什么退化成了随机IO
当时基于这个考虑
我们就只设计了一个卡夫卡的topic
结果什么
结果我们当时出了一个case
就是某一个部门上报数据量突然变得非常的大
我们当时是在做S9 S10的
一个好像是一个比赛的场景
然后它报什么
它报只要播放器产生丢包就上报一条数据
结果在这种大型赛事的时候
因为在线人数非常多
导致上报的数据量激增
某一个不太重要的一个技术的埋点激增
因为你只有一个topic
大家可以想象到
前面堆积了一堆兄弟部门的技术数据
那业务的数据其实就会什么
就像蓄水池一样对吧
来了洪流其他的支流全部进不了
那么整个消费能力全部下降
整体的吞吐也会变得非常差
因为你从端到端的交付来看
某一个重要业务的数据落地
它就会变得非常的慢
那怎么办
最终我们架构做了一些优化
首先是按c端隔离
c端到底怎么做
我从kafka topic消费数据的时候
我是不是可以建不同的group
我三个人建不同的group
那么只要我的sink端HDFS Elasticsearch MySQL都存在一些差速的时候
就是它们是消费能力有区别的时候
不会相互影响
因为大家各有自己的kafka的offset偏移
大家的消费能力是不一样的
这是第一个
第二个就是说我们考虑再三
我们会按照部门业务线
甚至说细力度到logid去拆topic
刚刚我们提到的kafka的topic变多以后
它的顺序io就变成了随机
这个问题怎么办
是不是前期我们可以按照部门拆分集群
A部门用A kafka
B部门用B kafka
同样的按照业务产业也是可以区分topic
最终我们如果到了logid的话
可能tipic的数量会变多
我们长线来看其实要彻底解决这个问题
要从kafka本身的一个能力去做一些演进和优化
但我目前的话基本上按部门和业务拆不同的集群
达到的效果也是还不错的
所以我们看其实这些可用性的问题其实都是跟隔离
都可以让我们的业务或者让我们的架构变得更简单
并且说它的可用性也会变得更好
因为隔离是最容易实现的
就是要资源相对来说比较简单
这个是一个case
还有一个就叫热点隔离
什么叫热点
热点就是比方说我们之前大家知道微博上经常有
明星出轨或者明星离婚事件
就像王宝强那个事情
一旦出现这种社会性的爆点事件
大家是不是都吃个瓜好个奇
看一看发生啥事
这个时候我们就会去微博上搜鹿晗老婆是谁
然后去搜或者王宝强怎么回事
然后我们找到这个数据之后就很high
然后都去看热闹
所以它就会产生一个热点
我们很多时候呢这些热点可能会引起事故
就像当时说微博的同事正在结婚
然后突然哪个明星又出轨了
然后不得不搬出笔记本现场修bug
同样的我们当时我遇到个case
不知道大家认不认识b站的局座
当时局座在b站直播
然后很多人在线
这是一个case
因为局座很火
然后来突然来直播
很多人关注他
这是第一个
第二个就是我们当时在做一个应该是做国庆阅兵的时候的一个直播
也有好多人看看直播
在线人数很多
那么还有一种场景就是
像那种跨年拜年纪那种演唱会的时候
在线人数非常多
之前有出现过一个一个叫现场的放送事故
比方说我现在正在放演唱会
直播小哥扛着他的肩扛摄像机
然后手一抖
电源线被人踢掉了
然后或者说网线被拔掉了
用户就打不开了
这肯定就是一些雪花点
或者说直接整个直播房间就是看不到了就卡掉了
怎么样
这个时候是不是会猛刷新房间
然后大胆尝试去刷新页面
然后尽可能恢复观看时候呢其实
也是容易产生一些热点数据的
这是很典型的一些场景
我觉得有两种思路去优化它
第一个就是说有一些必经路径
像我们的首页
首页有很多这种运营位置
大家做过这些业务
有些地方要配一些banner
就配一些这种运营活动
配一些位置
这些位置的这些数据是不是很高频的访问
因为如果大家每个人都要去访问首页的话
基本上都会访问这什么banner一些数据
那么banner的数据如果你把它放成remotecache放到redis里面
是不是非常高频去访问他
那么有可能使用redis cluster
或者其他的redis的proxy
那么你用一致性哈希它可能就会命中到某一个节点
你用redis它使用crc16去求key的时候
一定会命中到某一个sought
最终命中的某一个redis server
那么很容易产生redis我的一个单点的一个瓶颈
那么对于不怎么更新或者说更新频次偏低的这种业务的数据
我们通常都会做从remotecache提升为localcache
那怎么做呢
其实很简单
你的服务启动以后
你开个goroutine
goroutine只做一件事情
就是从数据库里面或者从某个地方把这些数据load的回来
load回来之后大家可能会想我到底用什么存
一般大家都会想到用sinkmap
以前sinkmap我们知道它不能同时读写
那么可能会使用读写锁
那么Go忘记哪个版本它引入了叫atomic.Value
那么你可以用atomic.Value做一个原子的替换
也就是说有一个v1的数据
用atomic.Value指向的它
那么现在有个v2的数据
是一个新的数据
然后你可以用atomic.Value变量指向v2的数据
那么v1的数据就会被GC掉
这样的话其实可以尽可能无锁地去访问
所以atomic有一点点的成本
但是它是一个CPU底层的一个CAS指令
它的整个访问也会比较快
因为你冲突概率小的话
它访问是非常快的
是第一种很高频访问的不怎么变的
那么第二种
比如直播房间这种高在线的情况下
万一出现一些大量刷新的情况怎么做呢
我觉得有两种思路
你看我们这张图
我们可以通过live-streaming
就是知道大概它的在线的情况
我们可以写一个room-monitor这样的一个监控服务
根据我的在线房间人数
比方说达到一个阈值5万或者10万
我就把这个数据广播到所有的room-service
就是提供房间微服务的这么一个房间服务房间模块
我发个广播rpc告诉它们这个房间麻烦你做一下主动的cache 主动的预热
这个时候就把cache变成一个什么
提升为一个localcache
那么它TTL
哪怕它的TTL只有3秒5秒
我认为在应对洪峰的时候
因为你的room-service节点非常多50个100个
那么你在应对洪峰的时候
因为变成了localcache的一个访问
所以说问题不大
如果localcahe没有命中
它当然还是会请求redis
但是一旦请求redis以后
因为我可以把它立马又变成一个localcache
所以说这个问题倒不是特别大
就是我们只要高在线的通过room-monitor的服务
标记了这一些房间id要让它缓存这些房间信息
你就不容易被用户猛刷的时候
相关的那些页面上依赖的一些接口或者资源被穿透
或者说被打爆
这种情况下就不会出现
当然了我们这个地方讲的是一种旁路的监控式的方式
找到热点
然后主动广播的思路
其实还有一个更好的思路
就是我们进程级别
进程级别自我去发现热点
这样的话你其实就不用写这么多服务再去旁路监控它
而是说通过我的在一个进程内的它的访问频次是多少
做一个滑动窗口可以让它主动去做一个预热
主动变成一个localcache
这个也是可以做得到的
如果你把这些功能下沉到你的基础库里面
你的整个可用性会变得更加的健壮
整个服务会变得更加健壮
我认为这个也是很重要的一个点
叫热点隔离
除此之外还有一个就是叫线程隔离
线程隔离是什么
我们看一下这个图里面的Tomcat
一个用户请求发送到Tomcat
那么Tomcat有自己的线程池
这个线程池里面它可能是一个首页的请求
它可能会获取用户信息
获取推荐信息流或者推荐千人千面的一些AI推荐信息
然后获取积分服务
然后组合这些信息返回的浏览器
假设这个时候出现一个情况
我的推荐服务不可用
那是不是有可能我这个线程池最终会因为推荐服务不可用
或者说它的高耗时导致线程池堆积了
大量的请求夯在推荐服务里面
导致线程池最终会被耗尽
这个是对于Java服务来说是这样一个情况
那么对于Go来说
因为所有的IO都是Nonblocking
就是非阻塞式的
那么且托管给了Runtime
就是Go的lO都是托管给它们来处理的
那么它最终只会阻塞goroutine
它不会阻塞我们底层的线程
它只会阻塞goroutine
因为我们知道如果一个goroutine会被堵住了
你线程你M跟P它会产生一个就是解绑的关系
那么这个P在后面讲Go Runtime的时候
大家可能会知道它是个工作队列
工作队列和工作线程会进行解绑
那么这个时候M它就会去做别的事情
它可能会去抢这样一个别的P来继续
获取工作队里面的任务
继续处理
但是对于goroutine来说它的的确确是堆积的
它的的确确是阻塞的
所以对于Go来说
你只需要考虑在这种堆积或者说下游服务堵住的情况下
你只要考虑goroutine的总量的控制 不会炸
因为你goroutine数量越来越多
会产生什么情况
内存是不是越积越多
最终就oom掉
那么对于Java来说很可能就是出现一个线程耗尽
叫PE thread exhausted类似这种错误
那么对于Go的话
它只会越来越多goroutine
然后这种oom掉
所以你要做好可行性
那么线程级别的隔离
或者说我们goroutine的总量的控制就变得非常关键了
我们再看这个Tomcat
我们非常容易想到的是什么呢
我们按照用户请求发到Tomcat的线程时可能是自己的
那么他完完全全
完完全全可以做什么
按照用户服务的线程池
推荐服务的线程池
积分服务的线程池
还要通过开展自己的线程池
划分不同的池位
如果推荐服务挂了
它只会耗尽推荐服务的线程池
那么对其他的线程池可能不受影响
因为你这个线程池一忙
我就可以抛弃掉这个请求
让我最终线程不会彻底全被耗尽
所以这也是一种线程级别的隔离
我们再来看看刚刚我们讲的一种case
是通过线程池打满以后
然后我就可以去掉请求
让请求失败
当然了 如果线程池被打满
它可能可以触发一个fallback
fallback就是降级的一个逻辑
像我们highstrick的话
如果你正常情况下调用是ok的
那么如果它调失败的话
它会就走一个fallback的降级逻辑
当然了最终我用线程池可能还要结合熔断
因为持续的高错误率还是对我们的服务是有些影响的
那么这个时候就要让它彻底的挂掉
让它直接不发送请求
它可能会结合熔断的角度去做一些设计
除此之外
Java其实除了线程池隔离
我也见过基于信号量的这种做法
类似像阿尔法j这种思路
可能是
因为信号量大家可以理解为一个数字
就要一个就少一个
要到等于0的时候就没有了
没有的话你要么就让请求直接失败掉
所以它最终可以控制并发
比方说大概20个请求
如果超过的话就可以直接失败
类似的思路可能会比线程池实现稍微好一点点
那个是forkjoin
我看到它是类似的一个做法
后面我会告诉大家
我们有一种手段叫自适应限流
通过自适应限流就有办法限制住goroutine的总量
我们看你无论是使用信号量还是线程池
你都有什么大量的手工的配置这个值
它会变得非常的麻烦
你因为你要手工去设定这个值
你到底是多大对吧
其实是一个叫magic number就非常难去设定的
我们尽量的达到一个效果是什么
做到不需要做手工的一些配置
能够让它自己去自适应的去设定
这个值是最好的
好 我们再看一下进程隔离
我们大家知道容器化docker
包括容器编排引擎k8s这个火了以后
大家基本上的进程都是放在容器里面
我们大概当时是这样的节奏
我们15年是在KVM上部署的
16年使用Docker Swarm
17年我们迁移到Kubernetes
包括后面我们直接跟公有云做一些公有云的一个混合调度
那么在我们20年的时候
我们离线的Yarn和在线的k8s
还做了一些错峰的在离线分布
这个是我们整个演进的一个路子
当然现在可以K8s那么火
让我们一个进程运行的容器里面其实已经变得非常常见了
我就不多讲了
在后面的话就是集群隔离
我们刚刚也之前讲过
就是多集群的思路是非常常见
那么除了集群隔离以外
它还有一个是什么呢
我们的多机房
因为你最终如果你的进程级别的隔离和集群级别的隔离都不产生效果
因为它可能整个机房就挂掉了
那么我们可能就还需要多活的一个建设
那么在我们后面讲CDN讲网络讲多活的时候
我会重点去介绍我们多活的它的一个架构设计
这个地方是我们当时做的一个账号的
我们账号上云上阿里云腾讯云上各种云
它怎么去做的一个架构设计
我们看一下我们当时经历过什么样的一些case
第一个非常典型就是我们的早期的转码集群被大视频攻击
导致转码大量延迟
当时出现一个什么样的case
就是我们有一个有一个恶意的用户
他制造了一个视频
这个视频时长8个小时
然后特别的大
然后我们可以想象一下
因为每个人投稿进来
它都会类似像放到一个队列里面
我有很多的转码机器
这些转码机器每个机器上都会有个worker
有个工作队列
有一个工作进程
他会从我们的队列里面去捞一个任务回来
然后进行一个就是转码的处理完之后再进行到下一步
如果大视频要处理的时间非常长
意味着我的转码的周期时间会变得长
那么会让我这一台机器做这一个任务就被独占了
因为你处理的时间非常长
那么如果我大量投类似的这些视频
是不是意味着我的这些工作机啊
有可能都会被这个视频给堵住
最终我们的很多小姐姐跳舞的
这种5分钟视频就会全部卡住了
因为前面都是一个好大的好大的8小时的视频
所以攻击者当时是构造了一个这样的场景
这个视频就是一个8小时的新闻联播女主持人站在那里坐在那里那个讲
然后一动不动
那么我们其实也很好解决这个问题
我们可以是不是按照视频的一个规格
比方说超大视频走a级群
普通视频走b级群
重要的视频走c级群
进行一个隔离
假设还出现类似的一些情况
大视频的一个攻击
那么它其实最终影响的只是那些超长视频的一些转码的机器
它不会全局的影响
它只会有局部的一些故障
但是从我们系统的角度来说
它的可用性也会比之前全部故障要好得多
这是第一个case
那么第二个case也是一样的
我们有一天首页上了一些gif的功能
大家知道传的图片其实规格都会假设我们
都是800*500的这样一张图片
但是你的设备安卓iOS它的设备的分辨率有区别
我们一般来说都会按照设备的分辨率会设定几个档位
让它裁剪出一个最合适的尺寸给它去展示
也就是说我们其实并不会说预生成
这些所有的裁剪过的这些缩略图
我们都是实时缩略的
因为缩略完之后
我最终可以让它在CDN被命中
所以我们只是实时缩略
我们突然上了一个功能是gif的一个功能
gif实际上每一帧都要会被缩略
然后再合成一个gif返回回去
所以它的耗时会比正常的jpg png做裁剪和所有的时间要久得多
其实也是一样的
gif堵住了大量的这种做图片缩略的进程
进程里面很多线程都在做gif的缩略
所以说以至于我很多小图片jpg png全部会堵住
大家都进不来它全部被堵住了
所以说它也会产生影响
后面的话其实也很容易想到我们把大图gif和普通图片分割成两个机群
gif请走这边
正常的图片走b集群
那么gif走a集群
那这样的话其实我们的整个系统也会说从一个
全局故障变成了一个局部故障的一种case
如果这个时候gif处理慢一些
的的确确gif会超时
但是我的正常小图是不会受影响的
还有一些bad case就是说早期我们虽然应用都上了容器上的k8s
但是我们数据库一直没有去上容器
还是在物理机上
但物理机我们一开始大大家相互的
3306 3307 3308 3309
我们会开很多个端口在部署到这些物理机上
然后早期没有上cgroup去隔离CPU内存
后面哪一天有一个大SQL一个group by这样一个操作
导致说某一个MySQL CPU突然飙高
那么你可以想象的到这个时候你肯定你的
宿主机这台物理机上的所有的mysqld都会变得非常慢
它其实也是会局部故障变成了一个全局故障
因为这上面的MySQL可能会影响好几个业务
那么我们第4个点是叫INFO的日志量过大
导致ERROR日志采集的延迟
我们以前在日志采集的时候
就是大家日志会先落盘
落盘之后通过我们的log agent
它会去采集一些日记上报到kafka
kafka再消费到Elasticsearch
我们最终打开kibana去做一个检索
做个检索就可以把这些日志给看到了
ok为什么会产生这样一个故障
当时是这样的
有一个同学中午快吃饭的时候很开心
然后把代码写完了
然后准备发版的时候
另外一个同学可能之前上的一个业务
他特别喜欢把response body
里面的一大坨json写到日志里面
导致日志量膨胀得非常厉害
另外一个同学发完版以后
他当时看了一下
他去打开kibana去搜日志
搜了一下error
类似像我们Java里面搜一个exception
搜完之后发现没有报错
然后他就很开心的去吃饭了
结果吃到一半的时候
客服反馈有大量的报账
用户大量投诉
这时候找到他
他匆匆忙忙再回来再回滚
这里面其实有很多问题
第一就是说我们在发版以后一般会有一个
所谓的sop或者叫check list
我们要做一些巡检
看一下这个服务是否正常
这个同学他当时只看了error日志
所以从我们有个启示是什么
我们发完版以后
我们既要看它的正常日志输出
也要看它异常日志输出
也要去看它的监控图关于指标的一个情况
它的一些metric它的一些指标的情况
最好的话你还可以你部署的服务会影响哪些业务
你自己可以去点一点
看看有没有影响
那么这个呢这几个同学可能都没做好
所以他就去吃饭了
第二个我们尽量不要在高峰的时候
不要在没人的时候去发
因为它的影响面可能会变得非常大
这个同学吃饭的时候运维都不在
可能开发同学他的兄弟也不在都出去吃饭了
这时候其实你的整个故障时间就会变长
所以这几点都不好
那么更关键的是什么呢
本质上我们的系统我们的日志
采集系统它没有做很好的隔离
未来的话其实我们知道按照日志分级
如果info日志和error日志它分属在两个topic
不会因为说另外一个同学把日志response body
里面那种超大的json打进去
因为我的这个时候es肯定存储会变慢
我的kafka消费也会变慢
我的error就可能堆积在info的日志后面
如果我拆成两个toipc
至少我的error其实当时那个人可能搜一下error可能就能搜出来了
因为它不会受到超大的info的日志的影响
那么从设计角度来说
info error的日志可以区分两个topic
甚至未来可以区分两个Elasticsearch的集群
因为它们重要性会不一样
因为报账error它是非常重要的一个日志
是我们用来诊断服务的一个非常重要的手段
所以隔离其实看上去都是一些架构设计的思想
一些大家比较容易实现做到的一些点
我们可以去梳理一下内部的一些服务非常重要
哪些服务不太重要
哪些地方存在热点
哪些地方存在服务隔离做得不好的
我们就可以进行一些架构的优化