我们接下来讲负载均衡
负载均衡为什么对可用性也非常重要
其实我们之前了解负载均衡的时候了解比较浅
可能就是知道
像随机、轮询都是一些非常简单的策略
因为负载均衡的节点都是同质的
发给任何一个感觉上是没有区别
但是实际上在一个分布式网络环境下它会变得非常复杂
对于数据中心内部的负载均衡来说
理想情况下
某个服务的负载会完全均匀地分发给所有后端任务
你要请求
比方servicea调账号服务
感觉任何的时刻
最忙和最不忙的节点
也就是account service最忙和最不忙的节点
永远消耗同样数量的CPU
目标肯定是这样
我们希望能够比较均衡
因为负载均衡肯定是希望节点之间流量是均衡的
负载也是均衡的
所以做负载均衡几个目标
第一我们期望能够比较均衡地流量分发
第二能够可靠地识别异常节点
比方我之前说过
万一运维进去机房然后手握一下那条线
握一下光纤
然后某一个链路就会丢包
如果你还是这样没有任何区分对待地
把流量分发到其中有问题链路的节点就会大量报错
要可靠地能够识别出异常的节点
第三
能够scale-out
那么在扩容缩容的时候
负载均衡不会有太大的影响
并且通过扩节点
因为节点其实本质上大部分微服务都是无状态的
扩容意味着流量吞吐会更好
它的状态层可能是Cache和DB
当然Cache和DB可能也会有一些瓶颈
最后一个就是通过加更多的节点来减少错误提高可用性
一般来说至少是部署三个节点
一个节点能够满足需求情况下
我们通常都是N+2
为什么
一个节点是一个单点
一挂就全没了
两个节点挂一个
另外一个节点要扛100%的流量
风险非常大
所以我们通常都是N+2
就是一个节点可以扛住至少要多冗余两个节点
风险就小得多
我们发现在backend之间
在后端节点之间
load差异非常大
看我们这张图
我们期望的是这样的
每个节点之间的CPU是比较挨在一起的
我们实际上发现是这样的一个情况
节点之间的差异非常大
为什么
因为每个QPS虽然是均匀的
比方做的轮询是均匀地分发下去
但是每个请求的处理成本不同
跟我们之前讲限流一样
取一个用户和取一百个用户
QPS消耗的资源是不一样的
这是第一
第二物理机环境的差异
举个例子就是我们虽然做负载均衡是按照
比方都是4核4G的容器
但是老的CPU可能CPU更差一点
新的CPU可能更好一点
那么如果没有把差异拍平的话
新的2.5赫兹的和3.0赫兹的CPU
可能服务能力有所区别
到时候CPU利用率上就会有些区别
这个是第一
第二
的确在不同的节点之间是存在共享资源争用的
虽然我们都上了容器
有CGroup
有内存CPU的隔离
但是像内存的一些缓存
有一些lO的buffer
有带宽的一些征用
有上下文切换
有网络上下文切换导致的一些内核开销
还有一些IO的原因等等
这些共享资源其实内核没有完完全全做到隔离的
这些你所处的物理机环境的差异
会导致节点之间是有一些diff的
这个是一个
还有一些其他的
像性能的因素
比方说有些节点可能正在做FullGC
这个时候发一个请求进去可能在做GC的节点
它的响应是不是会变得慢一些
就像是我们Golang一样
如果它这个节点正好在做GC
你发到一个请求过去
它肯定会比别的节点没有在做GC的会差一些
它肯定会带来一些
虽然stop the world这个时间比较短
但是它多多少少是会有些影响
并且CPU负载会高一些
另外就是像JVM有JIT
可能刚启动的节点
可能需要做一些JIT的编译
提前要做一些Just in Time的编译
可能也对我们的接口耗时有些影响
这是一个
我们再举个例子
我们通常使用一个叫JSQ的最闲轮训
就是哪个节点连接数、请求数最少
就把请求发过去
很常见的一个JSQ的负载均衡算法
它的问题是什么我们看一下
在这个图里面有三个Client
LB A、LB B和LB C这三个Client
那么现在有三个后端
Server X、Server Y、Server Z
LB A有一个请求发到X
当前正在发的请求
LB A还有一个请求发向Z
对于LB B来说它有一个请求发向Y
对于LB C来说
它发一个请求发向Y和发向Z
请问这个时候有个新请求进来
有个新请求进来到LBA
那么基于JSQ的最闲轮训算法
它会选谁
它肯定会选Y
这个时候我们从全局视角来看
X有一个请求
Y正在处理两个请求
Z处理两个请求
如果这个时候由JSQ轮训算法
发给它的话
Y就等于3
那么就是1、3、2
是不是意味着从全局来看它不是一个最优解
为什么JSQ会出现这样的问题呢
因为它缺乏从服务端视角
从backend的视角
其他的一些统计信息
导致它做的决策是一个局部最优
不是一个全局最优
我们最终要做负载均衡算法
不是说简单地用个轮询
用一个带权重轮询就解决的
实际上在一个复杂的多节点的环境下
它是由很多因素决定的
所以我们要考虑什么
第一要考虑负载
第二要考虑可用性
只有考虑这两个因素
才能把负载均衡算法做得好
我是什么情况找到这个问题的
一个是我们通过Google SRE的这些书籍阅读
发现人家在负载均衡算法上其实花了很多心思
另外就是你看
像我们的监控图有一次看到
节点与节点之间CPU的差异非常大
比方说这条绿色的线
我当时登录进去看它物理机
它的确存在很强的内核的上下文切换的一些争用
网络的一些争用
导致它的负载特别高
它的CPU变得比较高
这个是我们当时截的图
这个是当时的现状
你看我这边圈出来的地方
结合到现状以及我们了解到别人、海外
包括像谷歌、Netflix、Uber这些公司做的一些尝试
我们最后参考了一个
叫The power of two choices
in randomized load balancing的paper
一篇这样的论文
它简写叫p2c
就是pick two choice
选两个节点来比分的算法
这个算法其实一句话就描述清楚了
就是随机从负载均衡的连接池里面挑两个节点出来打分
优先选分更高的节点进行调度
是不是很简单
这里面的核心就是如何去进行打分
我们之前讲了
我们会考虑哪些指标
我们之前是缺乏服务端视角的统计信息
那么服务端视角的统计信息什么东西最关键
就是CPU
就是它的负载
所以跟前面介绍自保护负载CPU的统计信息一模一样
仍然是用这个属性
用前面统计CPU
就是自保护统计CPU的方式一模一样
每个backend它自己会去统计它自己的CPU利用率
怎么能够把CPU拿回来
实际上是通过GRPC的metadata的传递
因为GRPC metadata传递既可以叫request header
它还有一个response header
所以它既可以请求带header
也可以响应的时候带回header
我们就在每一次RPC response回来的时候
把CPU当前的利用率给带回来
如果我的QPS越高
意味着我分发到各个节点
收集到这些节点CPU的信息的收敛速度越快
因为QPS越高
意味着CPU的信息获取得越实时
这个是一种
第二
如果对于低QPS的那些服务
因为RPC之间它还有Health Check
它会定期Ping-Pong
这种低QPS的服务
可以通过Health Check把负载信息给拿下来
这两种手段
我们叫主动和被动的方式
能够把CPU的信息给采集回来
有了服务端的负载信息可能还不够
从Client的角度
从Client的角度它关注什么
成功率
调那个接口老是不通
我们做负载均衡算法应该能够把可靠地识别出故障的节点
流量就要少发过去
inflight表示的是当前服务QPS情况
就是当前有多少个请求正在运行
这个指标其实表示的是
Client发到各个backend
发到各个服务的情况
这样的话其实也是作为一个调度的指标
因为我们要负载均衡
也是要考虑发往各个节点的情况
当然如果你只有客户端维度的inflight指标可能不够
所以我们前面讲到
我们会选取backend
选取后端的一些CPU
这两个一结合其实就是说
在保证backend CPU相对均衡的前提下
也是尽可能均匀地分发QPS
那么latency其实也是一个非常重要的指标
有些情况下某一个peer到某一个peer
某一个端点到某个端点
它的网络通信质量或者一系列的原因
导致它的网络延迟也好
或者它链路变慢也好
如果你是正常地发送过去肯定会受到影响
因为它耗时偏高
当然还有别的原因
比方说它正在FullGC等等
这些也是作为一个指标去参考的
所以总结来说
我们从后端角度就是要对方的负载
那么从Client角度
其实因为Client角度是最真正体现RPC质量的
包括健康度
包括QPS包括latency
它都是真正体现服务质量的
这两个一结合
我们会使用一个简单的线性方程进行打分
每一个指标我们肯定要做一个对齐
要做个归一
用简单的加法或者是乘法都可以
我们待会去看一下
kratos内部怎么去实现的
这里面还有第二个点是什么呢
对新启动的节点
我们一般会使用一个常量的惩罚值
为什么要做这一步
我们有些节点可能刚启动
尤其像带JVM带虚拟机的一些语言
它刚启动的时候可能代码不是那么热
因为它可能有些Class之前没有运行过
它可能会涉及到一个即时编译的问题
这种情况下如果瞬间发送大量请求过去
有可能会导致它的响应会变得特别得差
所以newstartup就是新启动的节点
我们会给它一个惩罚
这个惩罚随着时间慢慢再让它恢复
慢慢地放个别的请求
慢慢地再放一些个别请求
等它慢慢进行预热
预热之后流量就慢慢得变多
我觉得这个也是相对来说是一个不错的机制
第三个我们有没有想过一种情况
如果某一个节点它的得分特别低
意味着我在打分的时候它取出来和别人比
永远都会比出来自己是比较差的
那么自己比较差
它就永远得不到调度
得不到调度它得不到流量
所以它的统计值又无法更新
这种情况下它会进入到一种叫做永久黑名单的case
就无法恢复
也就是说它得分比较差
大家跟它比都会不被调度
不被调度它的统计无法更新
这样的话其实这个节点最终就没有流量了
不是我们预期的情况
因为你短暂的得分差
不代表你永远是这样的不健康的情况
怎么办
我们当时想法就是
随着时间的推移
它之前我们统计出来的一些指标
比方说健康度
类似latency一些值
inflight跟CPU因为它偏实时这些指标
主要是这两个值
我们期望随着时间的推移
能够让它健康度或者latency的指标
做一个统计上的衰减
随着时间的推进
它的健康度慢慢能够回到100%
可能之前很差50%
我能让它慢慢回到100%
然后试探性地去放一些流量进去看一看
因为你放一个流量进去
或者说放一波流量进去
其实你立马就知道它健康度是不是真的还是那么差
如果它还是差
可能又会进入到一个得分比较低的情况
它的流量就会倾斜
会变得更少
之后再随着时间漂移
又尝试放一些流量去试探性看看这个节点有没有恢复
所以这个机制也是非常重要的
最后一个点是什么呢
我们发现每一个节点它的latency变化
刚刚虽然在方程里面是加了一个latency指标
但是我们认为如果它的lag
它的latency比大部分节点
应该是50线
就是比50线的节点都要差
我们可能会尝试做一些惩罚
这个算法的敏感程度就非常高
最终我们利用这套公式看一下效果
它的CPU就趋向均匀了
并且因为一些意外的单个节点的latency变化
或者是它的健康度变差
那么它流量立马会减少
这样的话它会把流量分发到其他的
相对来说健康的节点上
所以这个算法其实结合了实时采样的一些指标
以及对端CPU的负载情况做了一个综合的决策
在我们kratos里面其实也有类似的机制
我们看一下
大家可以看一下
实际上因为大家知道GRPC的balance
这个是对外暴露的
所以你可以自己自定义去实现balance的接口
我们看看它的主干逻辑
它是怎么去进行节点选取的
因为我们知道它p2c这个算法
实际上就是先选两个节点出来
所以我们看这里
从整个连接池里面选两个a和b节点
选完a、b节点之后
我们先会做一轮过滤
一开始它特别差
有一些节点特别差
我设定一个阈值
低于多少分我直接跳过
这是一种相对
也就是说选两个节点
有可能运气不好选出来的其中的一个节点特别差
我就先把它踢掉了
做一个for循环
尝试三次去挑相对好一点的
我就换一轮挑一个相对好的节点
这是我们第一个做法
筛出来相对好的节点以后
这里面还有一种情况
如果你挑来挑去
三轮挑出来都很差
说明有可能大概率情况下节点质量都很差
没办法
但是你的业务还要进行下去
所以你的RPC请求还是要发过去
我们挑出来节点以后
会进行进行一个打分的对比
我们在这个地方看
一旦这个地方prePick选出两个节点以后
会让loadA节点的load打分乘以loadB的health
我们把公式左右移一下其实也是可以
因为这个地方是除法
把它挪过去做一个除法就好了
这样的话我们就不需要做除法了
直接用乘法就好了
故意把它挪一下
负载和health乘以权重就要用乘法来做
乘法
我们再看一下health是怎么计算的
health其实比较简单
就是它的健康度
就是成功率
那么我们再看一下load是怎么算的
load因为有三个指标
load因为既取决于它latency的情况
也取决于它并行的inflight请求的数量
也取决于CPU的情况
当然我们把初始节点还会做一个惩罚
也是在这里面计算的
我们这地方归一做的比较简单
看起来不是很科学
其实就是把CPU乘以latency乘以inflight
但是latency
因为latency
1秒100秒会差异非常大
所以这个地方会对latency做一个简单的归一
可能就是开平方根
这个公式其实不是很好
我们后续也讨论过
可能会用一个a乘以x
加上b乘以y
加上c乘以z
那么a、b、c可能是个系数
a可能是代表CPU的系数
b可能代表的是latency的系数
每个指标在分别地做归一
之后可能这个算法会做一些调整
但我们当时测下来简单用个乘法
基本上打分的情况还可以
我们当时用乘法测下来
虽然看上去不是很科学
这样做完以后
我最终就挑出两个节点
就挑出哪个会被调度
哪个不会
upc就是哪个不会被调度
刚刚我们还说到一个机制
就是黑名单的机制
永久黑名单
我们当时测的就是如果有个节点它超过一个forceGap
超过一定的周期都没被调度
会强制调度一次
利用强制调一次机会
我们就有办法去把它的统计做一些衰竭
怎么衰减呢
GRPC的接口有个callback
就是当我的RPC接口调用完毕以后
它有个callback会去触发
我们在 callback里面实际上
比方说这个地方inflight
在调之前加一
在调完之后减一
这个其实都还蛮简单的
我们在调完以后其实也会去更新它latency的情况
包括它的health情况
这几个指标我们都是用滑动均值来计算的
但是滑动均值
我们之前知道滑动均值是beta乘以v1
加上1-beta乘以 v2
那么beta的系数以前可能是拍的一个值
我们之前讲CPU的滑动均值的时候是拍的
这个地方的beta有些区别
因为我们之前讲
要随着时间漂移统计做衰减
所以这个beta的计算方式会稍微有些区别
上一次节点调度的时间和当前的时间做一个减法
减出来以后
你得出来经过了多少时间
比方说经过了5秒没被调度
我们再定一个半衰期
比方说我定600毫秒为一个半衰期
那么把5秒除以600毫秒
得出它经过了多少个半周期
然后求一个它的exp
求一个它的指数级
叫若干次幂的导数
相当于是Math.exp
类似这个地方的一个公式
也就是说这个w相当于是衰减的情况
如果你的时间越久
算出来这个w就会越小
比方说最终可能等于0.1甚至更小
也就是说因为当前的样本权重会变高
过去的样本随着时间漂移它的权重会降低
因为它是根据衰减周期来的
当然这个公式大家也可以看一下
我们再看后面
这个地方看看latency是怎么算的
就是用过去的latency乘以w
这个w因为是随着衰减周期
随着时间越久它的w就越小
再加上当前的latency乘以1.0减去它
这个就是latency的用衰减周期把它还原成一个正常值
同样的success
就是所谓的Client维度的健康度
也是用同样的公式来做的
这两个指标一旦被刷新
衰减成一个正常周期
之前永久黑名单的问题也解决了
所以其实是非常巧妙的算法
最终我们要的效果是什么
CPU它其实是趋向均匀的
并且各个节点出现一些抖动
它也能够立马恢复
所以这个就是负载均衡上做的一些尝试
References里面也讲了
像Uber、Netflix还有Swift这些框架
其实都做了类似的一些事情
我也希望大家后面也去翻一下资料
去研究一下它们大概是怎么实现的
那么负载均衡我们就讲完了
后面的话
我们总结一下一些最佳实践
第一个叫变更管理
我们真的要围绕可用性去做
我建议大家每一次出事故
一定要做事后总结
事后总结的核心要解决的问题就是避免犯重复的错误
就是你在事后总结里面去复盘
哪些问题不应该重复发生
或者说这个问题发生了
我下一次应该怎么去解决
因为你只有不断总结
去改进你的研发的过程流程
甚至你代码的鲁棒性
最终我们的可用性质量才能做起来
我们总结下来就是实际上70%的问题都是由变更引起的
所以恢复可用代码并不总是坏事
就是回滚
我们第一感觉一定是先考虑最近有没有变更
有发版
或者说业务pattern
就是业务的使用姿势有些变化
先询问一圈
尽快先止损
先让代码恢复成之前可用的状态
所以变更管理实际上是非常重要的
我们大部分事故都是变更的不合理导致的
有代码的
有业务使用姿势的
也有配置文件导致的
那么先用强管理的手段去把变更流程
有一个标准的SOP
有个标准的上线
比方说上线一般会怎么样
先会做线下测试
线下测试之后会做测试环境的回归测试
回归测试到线上去做预发布
预发布之后我们开始做灰度
灰度完之后慢慢放量
在放量以后
我们其实就要
灰度也好
或者放量的时候
或者预发也好
我们其实都要开始关注日志
看错误的
看正确的
看指标
然后去自己手动测一测它的功能是否正常
其实有一个很好的SOP
在发布过程中已经解决大部分问题
因为到全量的时候发现
它其实是相对比较稳定的状态
如果发现出事故了
第一时间把它回滚
这个其实是非常重要的
第二个就是避免过载
避免过载两个很重要的点
第一就是过载保护
我们讲load saving那一节
核心就是让服务本身不会挂掉
本身不挂掉
你就有大量的机会去尝试查问题也好
或者说降级也好
或者说接下来进行一系列的手段也好
它都是给你留出buff时间
其实还有在过载的情况下
我们应该把流量调度
如果我们做了多活
做了多机房
甚至说做了多集群
想办法把流量迁移走
这也是一种思路
过载保护核心就是围绕流量的卸载
就是丢弃流量和流量的调度
这两块是非常重要的
第三块是依赖管理
我们做一个微服务
它本身要依赖大量其他的微服务
本身还依赖大量其他的中间件
这些任何依赖都可能故障
我们在引入任何一个网络组件
引入任何一个第三方组件
一定第一感觉就是依赖的组件它不是绝对可靠的
比方依赖ZooKeeper做分布式锁
如果ZooKeeper挂了怎么办
所有依赖的第三方
或者是带网络的一些服务组件
包括Redis数据库
它都不是一定完全可用的
那么再出现这些情况
这些服务不稳定或者延迟很高的情况下
你服务的表现是什么样的
我建议大家经常要做chaos monkey testing
就是做一些这种混沌的注入故障测试
注入模拟某一些服务
产生了一些高延迟或者不可用的情况
你的服务是什么表现
在谷歌内部有个叫dirt的这种栽赃实验
什么意思呢
有一些服务
比方说像他们的分布式的协调服务
类似像ZooKeeper这种服务
大家用久了之后觉得它可能性非常高
以为它永远不挂
所以他们在满足SOA比方说4个9
全年4个9
我发现某个季度指标完全够用
是不是炸几个小时可能也能满足当前的KPI
他们会故意模拟让服务故障
看全局哪些系统产生了一些意外不可恢复的情况
因为任何其他你依赖的组件产生故障
你不应该跟着连锁故障
所以这个是非常重要的
压测也好
或者说chaos monkey testing这种混沌的故障注入测试
其实也是非常重要的点
第四个就是优雅降级
在优雅降级的核心就是有损服务
就是你要搞清楚你的核心链路是什么
如果是一些旁路的不可用的服务
影响到主干链路是得不偿失的
这个时候就要考虑把它们降级
避免核心链路的依赖故障是我们经常要做的
优雅降级手动也好
自动也好
一定要经常做演练
自动不应该被经常触发
手动应该有操作手册
这两个是非常重要的
还有就是重试
重试的几个核心
我们知道用户出现服务不可用的时候
他本身会大规模地进行尝试
我们想办法去控制这些流量
要丢弃这些流量
因为就是在重试的时候
如果产生故障
不要立马重试
而是有一个退让的算法
让这种重试的波峰尽可能打散
还有就是我刚刚说的
出现故障的时候有一个冻结时间
不要某一个时刻都去重试
还有一个很关键的点
就是这种策略性的代码
我们通常都是通过一个
比方移动端是通过一个配置下发的策略
但我更倾向调API的时候
如果你要被报错
或者被降级
或者当前的Cuda不够用
应该是通过API的一些协议response
带回去
虽然报错
可以带回去一些策略
这样的话它整个配置的
就是所谓的控制策略
它的下发速度很快
因为你抓到请求到服务端
那么它因为一些原因出现故障
这个策略在response接收以后
策略的响应度就非常及时
而不是靠一个旁路的组件去下发所谓的配置
这个其实也是不太好的
还有一个就是超时时间
超时时间Failfast
是让进程不会被oom
或者不会被线程爆掉
因为只有做到了全链路的进程内
和跨进程间、服务间的超时控制
你才有机会不会被海量的流量打爆
有点像一个是丢弃流量
一个是快速消耗掉流量
最后就是我们在压测
一定是极限压测
不要报错的时候就停掉
其实测不出问题
我们一定是持续加压
看在高负载的情况下
我们的服务表现是什么样的
其实我得出的真理是什么
我们很多时候会觉得自己服务做的看上去还蛮稳定
平常也没什么事故
一旦做活动经常出一些锅
其实就是没有经常去做压测
最好是场景压测
针对一个比方说
从购物车
添加购物车到下单支付
整个场景去压测
放大流量去看各个底层服务会产生什么样的表现
反正从我的经验来说
自从我们公司有了压测平台
其实稳定性它就变得越来越好
尤其在大型活动的时候
因为我觉得压测出真理
只有实践才会发现你系统中隐藏的一些问题
最后就是我们在出现事故的时候应对
一定是有一个标准的流程
比方说先扩容
不行
重启大法
重启大法不行
可能是不是有些有害的流量导致服务不可用
通过我们的WAF
七层Nginx的WAF去摘这些有害流量
甚至在做这些事情的时候
一定要有一个人辅助你来做一些信息同步
避免大老板或者说外面的用户
完全不知道产生了一些什么影响
一定要做一些信息同步
方便PR的同事
市场的同事
客服同事
对外有个统一的口径
这些其实都是很重要的点
我希望我们作为研发也好
作为运维也好
都要去关注可用性
这个就是我自己
或者是结合SRE的一些想法整理的一些最佳实践
整个可用性相关的内容基本就到这了
大家一定要去翻阅后面的一些References
它其实涉及到的方方面面是非常多
无论你是在工作中
包括面试的时候
都会被面试官反复问到
可用性你是怎么去思考和实践的
这个是从纯基础库
或者说从我们的一些手段去解决的
当然架构本身设计上也会有更多可用性的做法
我们在第六周的时候就会讲
具体的一个案例
比方说评论系统
海量用户的评论系统是怎么设计
我们就到这