我们继续
我们接下来就要讲结合微服务的一些理解
然后RPC服务发现
我们要介绍多集群和多租户
这两个实际上都是微服务体系里面非常实用的
并且能够解决你们生产环境测试环境的一些问题的
这两个还是非常重要的
所以大家一定要认真听
包括课后可能还要去做很多拓展的阅读
因为多租户这一块实际上是我们实现全链路压测也好
还有构建多测试环境也好
以及所谓的staging deploy
生产环境的发布
都是非常实用的一些技术
我们先看一下多集群的概念
像L0的服务就是非常重要的服务
类似账号
我们以前的做法都是一套大集群
一旦账号集群出现故障
它的影响是非常巨大的
所以要从几个角度去考虑多集群的必要性
简单来说什么是大集群
比方账号服务非常重要
我们一般部署的方式是什么呢
从单一集群角度考虑的话
多个节点来保障可用性
至少是要N+2的方式来冗余
也就是说你的系统需要N个节点
我们通常会至少多布两个节点来冗余
为什么呢
比如三个节点能满足需求
一旦挂一个节点
它的流量负载到剩下两个节点
它的比例太大了
但是我们如果部署五个节点
挂一个节点的话它的影响就会好很多
所以对于L0的重要服务
我们当然希望冗余的节点数量本身是非常多的
但是从单一集群故障
它带来的影响面的角度是非常大的
比方我们之前就出现过账号故障
账号故障我们实际上好多业务都会受影响
比方说直播、主站、游戏、电商
很多它都会联动的
受到影响的
那么这种情况下
我们就应该从单一集群的角度
演进到多集群的角度
其实简单来说从一个机房内
我们把账号看成是一个集群
我们如果部署多套冗余的集群
来提升更好的冗余能力
多套集群
它不仅仅是指我们节点又部署得更多
而是指对应的逻辑集群里面
我们依赖的Cache资源也是会独立
这样的话其实我们通过所谓的Front-end Cluster
前端的一些业务的应用集群
让它独占一些Cache资源
带来更好的冗余能力和吞吐能力
这个其实是非常有效果的
多集群实际上是解决单个机房内集群故障的问题
当然如果是机房故障
肯定是要考虑多机房
我们在很后面的一个课程里面会介绍
类似多活
我们怎么使用多机房的技术
来解决我们整个机房故障的情况
我们现在的话先Focus在
单个机房内一套集群如果出现故障
我们如何去应对和解决
多集群
我们后来建立在之前的一个Case上
当时就是账号集群
它作为一个L0的服务出现问题
然后影响面是非常巨大的
我们后来就在想
我们在思考能不能提供多套冗余的集群
并且它们独占缓存
带来更好的性能和冗余能力
所以我们后来就推动PaaS平台
也就是我们的K8s
然后让它在逻辑上支持
同一个应用指的是同一个应用ID
比方账号集群名字肯定都是一样的
比方就叫account.service
给某一个App ID的服务
从逻辑上把它划分出一个叫Cluster的概念
在我们的管理平台上面
它可以新建出Cluster-01
Cluster-02两套这种集群
它们启动以后
为什么是在我们发布平台
或者说在我们的K8s平台上
去做这个功能
我们希望在维护Cluster信息
在容器启动的时候
把Cluster信息注入到容器里面
从环境变量最终可以去获取出来
当前应用处于哪一套集群
这个信息从我们的应用管理平台注入到容器
我们再从环境变量里面取出来到应用
我们的应用最后再把这些元信息
注册到服务发现、注册中心
最终其实它是一路这样带过来的
就是我们首先
从管理层面去划分多套
也是有对应的多个实例
然后带到环境变量
然后再一路带到
我们最终带到注册中心里面去
同样的我们不同的集群
可能配置文件也是有区别的
因为我们配置中心其实也是会
支持像多机房、多环境、多集群
类似我们在配置中心也抽象了一个多集群的概念
其实就是它们依赖的Cache地址是有区别的
比方说图里面
account-service01和02
我们依赖的是两套Cache资源
我们内部叫Overlord
Overlord是一个Cache Proxy
Cache Proxy底层
可能是划分两个独占的Cache的资源
那么多套冗余集群
我觉得实际上就是看中它独占的缓存
带来更好的性能和冗余能力
当然从物理节点层面上它也划分了
我们一开始是这么设计的
直到因为故障迫使我们做了这个事情
我们当时的想法是什么样呢
A业务用A集群
B业务用B集群
比方说直播业务用直播账号集群
然后主站业务用主站账号集群
游戏业务用游戏账号集群
我们一开始是这么设想的
但是划分完以后
我们会发现一旦直播集群
直播的账号集群出故障
我们要去做切换的时候就比较痛苦了
因为它涉及的服务非常多
因为直播业务
它每一个人
每一个使用账号的Consumer
都要去配置一个
指定一个Cluster等于Live
等于直播
等于这样的一个关键字
那么这样的话
一旦整个直播集群出故障
我要去切换流量很麻烦的
因为你配置文件是指定了
你配置文件里面指定要用这个的
这是一种办法
但还有一种办法就是
我们把这个功能做到我们的注册中心
或者服务发现中心
让它能够去从服务端的角度
就是从注册中心维度去切换流量
这也是一种
但是对注册中心侵入的功能会强一些
因为你在注册中心调度流量
必然要引入一些逻辑
这个是一种
即便是做了流量迁移的功能
你能很方便地从直播账号集群切到
比方我现在要去复用主站的账号集群
即便做到这个事情
我们在实际的一次事故中
我们发现切换以后因为业务隔离
导致集群切换的时候
带来了Cache的命中率下降
我们叫cache hit ratio
它下降得很明显
简单来说就是玩直播的人可能不玩主站
玩游戏的人可能不玩直播
那么你在切换的时候
可能会间接地导致Cache命中率下降
如果Cache命中率引起的缓存不热全部会
穿透到你的存储层
你的数据库
那么它会压力非常大
我们退而求其次
因为你首先路由切换这个逻辑本来就很重
因为你存在业务正交
存在数据正交
也会带来一些影响
我们退而求其次
让Consumer连接整个Cluster
也就是说我在物理上划分出了
Clster-01、Cluster-02
并且它们Cache都独占
但是我实际上给别人用的时候
给Consumer来用的时候
Cluster-01/02全部连接上
也就是说通过负载均衡
最终Cluster01和02的缓存
应该理论上都是预热得很不错的
因为不需要存在切换了
假设这个时候Cluster-01出现故障
我其实唯一要做的就是去注册中心
把Cluster-01的节点全部踢下线
因为假设它出大面积故障
我只要踢下线
流量就会倾斜到剩下的一些集群里面
但是Cache其实命中率已经提前预热过了
所以这种副作用和影响就小得多
这个是多集群带来的一些问题
统一为一套逻辑集群
就是账号你从逻辑角度来看
它其实是一套
但是物理上
我们实际上划分出的好多套Cluster
我们现在做法是什么
做法是默认全部连接
全连
比方说账号随着规模体量越来越大
账号01集群有100个节点
账号02集群有100个节点
账号03有100个节点
节点数量越来越多
账号集群它非常特殊
刚好是好多人都需要使用
好多人都需要依赖
我们上游的连接数是不是很多
上游的消费者也非常多
比方说我现在有300个账号总节点
我的上游使用方可能有1万个
因为好多人都要使用账号信息
这时候它的连接数量实际上是非常多的
相当于一个人每一个节点
都要建立多少个连接
建立300个
假设我有1万个
就是1万个乘以300
它连接数量是爆炸的
有一天我比较凑巧
我登录到我们账号的普罗米修斯监控上面去查看
发现了在一个深夜的时刻
就是流量不那么多
我们的业务低峰期
我去看为什么CPU的使用率高达30%
我当时觉得很奇怪
我去打开了它在线的Profiling
并且我看了一下普罗米修斯的RPC请求分布
为什么发现它的health check的占比
占得非常多
在低峰期为什么很多人来做health check
就说来做一个ping请求
ping-pong
CPU开销低峰期也不低
在低峰期理论上也没有什么流量
应该CPU使用率是非常低的
所以我们当时发现CPU占得非常高
第二个就是我们有一些RPC框架
如果长链接长期没有业务流量进来
它会把长链接退化成短链接
即便是退化成短链接
如果health check
因为变成短链接资源就释放出来
但是如果health check很高频
频繁的建连其实也会带来一定的资源成本和开销
基于这两个原因
我们后来发现在Google SRE这本书里面
它介绍了一个算法叫子集算法
也叫Subset算法
我后来发现它的思路非常符合我们的要求
简单来说
我们之前的思路是全部连接
我们现在假设有一种算法
能够从全部连接里面挑选20~100个后端建立连接
我去挑选一些节点
我不是全连
这样的话
不全连的话资源肯定就变少了
就是连接资源变少
同时health check的开销成本肯定变低
所以他就使用了子集算法的做法
在做子集算法的时候你就要考虑几个点
因为无论是Provider还是Consumer
它们都是会动态变化的
如果你的算法感知不到Provider
或者Consumer在变化
最终随着节点的
Provider也好Consumer也好
它们的节点变化
你的连接如果不能够重新Rebalance
那么你就会出现某一些节点连接数更多
意味着它请求数更多
最终会导致CPU不均衡
这个算法要保障后端尽可能均分给客户端
另外就是在我们的客户端重启或者是扩缩容
要保障重新均衡
同时均衡算法要尽可能保障
不应该新扩容一个Provider
或者新缩容一个Consumer
导致大量的人需要大面积的重连
因为这样的话也是一个问题
我这里面把这个算法给抠出来了
大家有兴趣的话可以看一下
我们简单理解一下这个算法可以理解为
把连接Back-ends所有连接拿过来
然后我们设定你期望的子集大小
比方我期望连接50个
然后它会把后端的这些Back-ends划分成好多块
让每一个不同的消费者用一套洗牌算法
把它重新排序
然后你再从里面看
我们做洗牌算法的时候
它也要设定一个种子
这个种子其实是基于每一个消费者
可能基于它的IP
或者基于它的IP加端口哈希出来的种子
比方我有50个节点
那么我把1000个
按照50或者100来划分
假设划出10个子集
那么这个种子肯定最终要
余10的话就把它收敛到0~9之间分布
相当于每一个人的洗牌是不同的
一共有10种洗牌的结果
这是第一种
每个人是命中不同的洗牌算法
第二
我再从同一个种子洗牌的结果集里面
去取它不同的分布
比方说你是数组0数组5
这样的话其实就是用这样的算法
把后端尽可能均分给客户端
大家可以用Python代码去做些演练和尝试
如果大家感兴趣的话
我会回头再发一些资料
因为References也会提到
这个算法其实对节点的机器名是有一些要求的
它需要有一定的连续性
所以我们现在的做法就是用子集算法不全连
然后通过使用Subset的算法
来保障我们的集群连接数
尽可能选择比较少并且是能够比较均衡
全局来看是比较均衡的
所以这个算法是非常有意思的
如果你的规模非常小
我建议就是全集群全部连接
一定要注意数据正交
因为你去指定集群消费的话
一定要注意切换集群的时候带来数据正交的影响
这个是多集群
我们再来看一下多租户
多租户是我们另外一个场景
在一个微服务架构中
我们允许多系统共存
什么叫多系统共存
就是指某一个服务有多个版本
这样的话我就可以想办法
让一个流量路由到各种不同的版本
我们一般把这种允许多系统共存的方式叫做多租户
这样的话你可以利用这种模型满足很多玩法
简单来说像多租户
租户可以是某一种测试环境
它可以是金丝雀发布
所谓的预发布
也可以是影子系统叫Shadow System
影子系统一般用于全链路压测
因为你知道全链路压测在线上压测
你肯定期望不影响
能检验生产环境
但是又不影响生产环境
你希望一套完整的测试环境能够虚拟出来
压的是线上的资源
但是又不影响在线的业务
这种叫影子系统
甚至我希望在线上去演练整个完整功能的时候
不影响在线业务
这个流量只允许测试同学发起
但是线上用户不影响
部署在生产环境的时候
你如何能做到你的RPC
你部署一个服务肯定流量就进来了
你肯定会受影响
有没有办法实现类似像租户的方式
能够保证代码的隔离
并且基于流量、租户来做一个路由决策
其实非常有意思的地方了
举个例子
右边这个图里面
流量正常情况下有点像这种模型
A到B
B调C、B调D
如果现在对B这个服务做一些改变
但是我们需要确保它仍然能够和服务A
和C和D能够正常交互
在微服务架构中
比方说我们就需要做一些集成测试场景
我需要测
你对B改变了
B'
我要去测它和A、C、D的交互有没有问题
这种情况下就有可能一套测试环境无法满足
因为你去线上做生产环境测试的话
这样其实是有风险的
即便你是灰度发布
假设你有20个节点你灰1个节点
那么它的影响是1/20
你灰度方面也是有影响的
有没有办法把这种我们刚发布的一些代码
我们肯定希望先在测试环境去验证
但是测试环境这样一种验证方式的话
肯定是有一套刚刚说的稳定的测试环境
但是你现在做了一些变更
我希望能够继续测试它和A、C、D的交互
你不可能再搭一套
我见过很多公司是一套稳定的测试环境
然后再搭一套
01再搭到02再03
然后大家商量着来
我在群里面吼一下
我去用01测试环境
或者在群里面吼一下
我去占02测试环境
但是实际这种运转过程中
很难保证测试环境的代码不被人冲掉
导致无法继续测试
所以这其实也是多租户非常有价值的点
那么如果你要完成并行测试
也就是多功能的同时测试
是在测试环境下去完成这个功能
你不可能去完整搭N套这种测试环境
因为我们知道在并行测试这个过程中
成本是真的非常高的
在并行测试中工程团队一般
首先会完成生产服务的一次变动
就是说代码会开发完
然后将变动的代码部署到测试环境里面
那么这种方式
这种部署到测试环境去测肯定不影响线上
是非常好的
但是最头疼的就是这种并行测试环境你不好维护
因为它有多套
你不好搭建
如果我们的应用非常多
有1000个节点
有1000个应用
你不可能全部去搭建一套
它的挑战是非常大的
因为你混用测试环境
假设我们现在是混用
指的是什么
我现在B改变了
但是我把B'放进去
那么有可能B'有一些Bug
混用测试环境其实可能会导致测试结果不可靠
这个可以理解吧
B'如果有Bug
别人测得好好的
突然你布一个有bug的
测别的和B'没关系的一些功能的人
也会受到影响
他们的测试无法继续
这个是混用测试环境
但是多套测试环境又带来
复杂的硬件成本和维护成本
因为你在撤销这些B'的这些发布以后
你要把正确的B重新部署进去
你要始终保持这三套环境
都是用最新的代码并且是稳定代码
而且在测试功能结束以后
你要回退到正常的版本
其实想想就知道了
维护成本必然是非常高的
另外就是测试环境模拟出N套
几乎是没办法做压测的
测试环境做压测基本就是不可靠
它无法仿真线上的真实流量情况
这些都是需要考虑多租户非常重要的一些节点
举个例子像右边图里面FAT1、FAT2
我们通常都是搭两套这种做法
其实我们早期也是经历过这个阶段
就是搭两套然后商量着来
但我觉得长线来看它是无法去运转的
怎么办
我们现在的方法
既然叫多租户
我们内部其实名字叫染色发布
我们可以把待测试的服务B
在一个隔离的沙箱环境里面去启动
并且在沙盒环境里面
你可以访问之前稳定的集成测试环境
也就是说我现在有一套稳定的测试环境
现在我B'有变更
我的稳定测试环境不动
我单独布一个B'出来
因为它就这一个B'有变化
我就单独把它布出来
现在问题就是说我如何让正常流量不被引入到B'
而正常的流量到之前的B是没有问题的
就相当于我正常流量还是过以前的节点
那么我需要测试的流量能够有一种机制
有一种办法能够路由过去
就像右边这张图里面
正常流量还是A到B、B到C、B到D
我们又期望有一种机制
像虚线的箭头
A能够到B'
B'能够到C和D
也希望这种能够运转起来
所以我们要做什么
我们需要做流量的路由
那么B'怎么知道它是一种特殊的发布
我们就要在K8s的管理平台里面
当然包括发布平台
我们在测试的时候
我们开发完了
我们这个时候就把B'发布当成是一个染色发布
比方我们商量个代号
比方叫红色
当然你可以随便取名字
比方说就叫高级功能001
我取个这样的标签名字部署
部署以后实际上就是它他能够
在之前的环境里面正常启动没有任何区别
只不过我一旦选了一个标签
比方刚刚说的红色
打进去以后我就会像多集群一样
打到它的环境变量里面
多集群是通过打到环境变量里面
然后让它注册到注册中心
同样的染色发布也是一样的
给它带了另外一个字段
比方就叫Color
它的值等于Red
然后也把它注册到注册中心
注册到注册中心以后
我们的RPC框架怎么办
实际上就是需要支持
在建立负载均衡连接的时候
在做节点筛选的时候
因为我要去注册中心去捞IP端口
里面有个字段叫元数据
元数据里面有个值
肯定是有一个等于集群的或者是等于染色的
我现在默认情况下取染色等于空的
所以说正常的节点
流量就是路由到染色等于空的
没有任何影响
但是我们现在需要一种机制
能够让流量路由到带着颜色的
所以说我们在构建连接池的时候
既构建正常的连接池
同时我再构建一个连接池
这个连接池是用map来维护的
key为空的map
正常等于空的就命中到连接池A
另外一个连接池叫染色连接池
它可能用map维护
map的key是什么
就是染色的名称
比方说等于红色
它的value就是连接池
这样的话我就有一种机制
能够让他有办法找到对应的连接池
这个就是流量路由
这样的话我们通过这样的机制能够保障隔离
能够可靠地隔离测试和生产中的资源
对关键业务的微服务
测试环境或者线上这样玩
其实都没有副作用
我们一定要注意
用灰度发布的这种方式
对线上实际上是有影响的
20个节点会一个影响用户1/20
实际上它是有一定影响的
通过这种染色的发布在线上去玩
我们做预发布也好
线上的一些回归测试也好
实际上对正常用户没有任何影响
当然了如果你的程序Bug
是那种update一个值
没有外条件这种全局影响
数据层面的影响肯定是有的
我们指的是服务本身是对业务不产生影响
因为流量进不去
我们需要一种机制才能把流量路由进去
你就可以把流量路由权交给测试人员
他来去构建一个生产环境的预发布测试
或者是生产环境的一个完整的功能测试
这个其实是非常有意思的
怎么做呢
我们看着这个图在右边
A、B、C、D是正常的流量
我现在通过刚刚说的染色发布
我让A'
就是ABCD我们把它全部构建出来
然后假设给它们全部打上一个标签都叫红色
我就可以完成一次大规模的发布
因为相当于ABCD都被我单独部署了
同时正常流量不会进去
怎么能够让流量进去呢
我们是不是能够
比方说发HTTP请求的时候
我加上一个特殊的head
比方说head就等于
color等于red
我带上一个特殊的标签
带上这个标签以后
我把它挂载到Go的
context.context上下文里面
那么在RPC负载均衡的框架代码里面
我只要尝试去读一下context上下文里面
有没有一个叫做color的元信息
然后取出来以后也等于red
我在负载均衡算法里面尝试找到red对应的
连接的节点
我找到这些节点以后流量再路由进去
这样一层套一层
不断地可以往下游去传递
你就能构建出一个完整的大规模的发布
并且不影响线上
这是一种玩法
我们回过来看之前那张图
如果我只是想测B'
B'下游C和D又是正常的
其实这种情况也能做得到
同样我们把流量从A注入进来
然后它带上一个标签等于red
A尝试找负载均衡连接池的时候找到了B'
B'这个时候要调
因为它也会尝试去找C和D有没有带染色的环境信息
结果发现他找不到
找不到它就要降级
降级就是调到正常的节点
也就是C和D
这样的话我们在测试环境里面
只测B'的时候也行得通
在复杂的测试环境里面
ABCD都部署了也行得通
这样的话最终整个流量路由这个逻辑就形成了
当然了
如果要让这套平台非常好用
最好是在你的指标、日志
各种这些中间件里面
包括你的存储、消息队列
最核心的就是日志、指标
最好能够
包括链路追踪
你能够把这些染色信息打进去
因为我可能需要区分它是线上流量
还是这种待测试的流量
能够区分出来方便去查问题也好
或者是怎么着也好
我们再想象一下
如果你是做全链路压测
你去做全链路压测
我是不是也可以同样的机制
把需要测的服务
待测的服务打上一个统一的标签
大家都用同样的机制
这样的话线上的流量也进不来
并且只有压测流量进来
压测流量进来以后
你去完整的
只会压测到你部署的特殊容器
你对线上的一些容器可能没有影响
当然了我们做全链路压测
一般在线上做全链路压测
对于底层的数据库
缓存、消息队列其实是有一些依赖的
因为如果你单独在线上做全链路压测
你去单独布个数据库
成本肯定是非常高的
我们怎么办
我们通常的做法
如果大家去看过京东、淘宝
他们经常在微信上发布一些全链路压测的文章
建议大家去看一看
他们通常比方说
在Redis里面可以使用不同的DB
这样就隔离了
我测完之后就全部drop掉
同样的如果是数据库的话
我们也是在同一个数据库实例上
建不同的名字
比方说有个叫DB_mirror
它是个镜像的数据库
它的Schema跟正式数据库一模一样
然后数据是你自己导入的
这样的话你也可以做一个完整的测试
来做一个压力测试是没问题的
有些情况我们需要在消息队列里面
也要去传递这些数据
你可以考虑一种方式说
就是说建立一个新的topic
消息队列里面建立新的topic
发到新的topic里面去做测试
这个也是一种方式
当然有一些全链路压测
有可能需要调银行的接口发短信
那些服务建议你最好单独起一个短信的服务
然后把它Mock掉
最好是把它Mock掉
Mock掉它在就不需要发短信了
因为可能对用户会有打扰
你就要把它屏蔽掉
其实多租户架构
本质上你可以理解为从源头传递一个标签
比方说通过HTTP head
然后挂载在Do的上下文里面
然后基于RPC的负载均衡流量来做路由
路由到你指定的想要的节点
通过这样的一个机制
你可以完成只搭建一套测试环境
但是你可以虚拟出无数多套并行的测试环境
因为你都是通过刚刚说的类似B'、B''、B'''
但是它剩下的流量又依赖稳定测试环境
就是B'
我只搭一个这样的节点
但是我剩下依赖的C和D
我不需要再重复搭建了
我还是把它能够路由到C和D
那是不是意味着其实这样的一种机制
能够让你虚拟出无数多套测试环境
这样测试环境的问题就能够解决了
在我们公司其实也是一样的
就是利用这样的多租户流量路由
染色发布的机制
能够保障一套K8s平台
虚拟出无数多套测试环境
这样的话其实是非常好玩的
这一块的知识我们基本上就介绍完了
我们本周整个微服务的概览
包括治理的一些知识基本上就结束了
我希望大家课后还是要去深度阅读一下
关于微服务的一些理念
它的一些设计思路
包括我们提到的一些多租户多集群
非常实用的这种方案
如果你课后阅读
去拓展阅读之后
我相信你一定能够解决好你们公司的
关于微服务的一些多测试环境的治理
其实这个是微服务里面最重要的点
就是让你的生产线能够非常顺畅
非常顺利
我们讲完微服务以后你有一些初步的概念
我们接下来因为做后端开发
我们接下来就是讲Go的一些知识点
比方说如何处理异常
如何处理并发
如何处理上下文
包括Go的一些工程化
如何跟微服务去结合上
这些就是我们接下来要去介绍的