微服务
服务发现
基本流程
通过appid(服务名)和Addr(IP:Port)定位实例
Provider 注册后定期(30s)心跳一次,注册, 心跳,下线都需要进行同步,注册和下线需要进行长轮询推送
Consumer 启动时拉取实例,发起30s长轮询
Server 定期(60s) 检测失效(90s)的实例,失效则剔除。短时间里丢失了大量的心跳连接(15分钟内心跳低于期望值*85%),开启自我保护,保留服务不过期。
重要步骤
1.心跳的复制(Peer to Peer)检查数据一致性(网络抖动或分区导致数据不一致):
注册时根据当前时间生成dirtyTime, A向B同步实例心跳时:
返回-404 表示 (1)B中不存在实例 (2)B中dirtyTime较小 A携带自身dirtyTime向B发起注册请求,把最新信息同步过去。
返回-409 表示 (1)B中dirtyTime较大 不同意采纳A的信息而是将自身实例的信息放在消息体里返回。
2.网络分区时的自我保护
当网络分区发生时,每个Discovery节点,会持续的对外提供服务,接收该分区下新实例的服务注册和发现。
短时间丢失大量心跳,进入自我保护,保证健康的实例不被剔除,同时保留”好数据“与”坏数据“
自我保护:每分钟的心跳数小于阈值(实例数量*2*85%),每15分钟重置心跳阈值, 在自我保护状态下, 不对任何实例进行剔除
非自我保护下,随机分批逐次剔除,尽量避免单个应用被全部过期。
3.客户端
长轮询+服务端推送变化,让服务发现的实时性更高
订阅式客户端,推送客户端关注的实例变化
Eureka客户端缓存,即便所有的都失效,依旧可以调用服务
多注册中心
1.同一个机房内部,discovery节点通过广播进行注册信息同步。
2.跨机房注册信息同步时,discovery节点会将信息推送到其他的机房slb,再由slb通过负载均衡把信息推送到机房内部节点。机房内部在进行广播同步。
- 注意: 跨机房同步时,注册信息为单向同步,slb收到信息后LB到机房内部节点,内部节点只在内部广播,不会推送到其他slb。
系统架构
架构图
多注册中心架构图
熔断模式的介绍
熔断模式
该模式借鉴了电路熔断的理念,如果一条线路电压过高,保险丝会熔断,防止火灾。
如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
还是之前的银行柜员的例子,假定处理每个业务的时间是5分钟,当某个柜员的处理速度降低了,超过了5分钟,或者干脆去吃午饭等等原因根本不在座位上,此时熔断机制将不会允许再有客户到其窗口前排队(如果有排队的也无法正常办理业务,只能造成阻塞的发生)。
下面来看看Hystrix是如何熔断的。
熔断器:Circuit Breaker
熔断器是位于线程池之前的组件。
用户请求某一服务之后,Hystrix会先经过熔断器,此时如果熔断器的状态是打开(跳起),则说明已经熔断,这时将直接进行降级处理,不会继续将请求发到线程池。
套用银行柜员的例子,柜员相当于服务,窗口前排队的是线程池,大堂经理则可以看成是熔断器。通常的流程是:客户进门,告诉大堂经理要办什么业务,这时他会判断客户请求的窗口是否在正常处理业务,如果正常,他就会让客户到该窗口排队(也就是进入了线程池),如果不正常,他根本不会让客户去排队。
熔断器相当于在线程池之前的一层屏障。
下面来看一下熔断器的工作原理
每个熔断器默认维护10个bucket
每秒创建一个bucket
每个blucket记录成功,失败,超时,拒绝的次数
当有新的bucket被创建时,最旧的bucket会被抛弃
熔断算法
判断是否进行熔断的依据是:
根据bucket中记录的次数,计算错误率。
当错误率超过预设的值(默认是50%)且10秒内超过20个请求,则开启熔断。
1 | max(0, (requests - K*accepts) / (requests + 1)) |
熔断恢复
对于被熔断的请求,并不是永久被切断,而是被暂停一段时间(默认是5s)之后,允许部分请求通过,若请求都是健康的(RT<250ms)则对请求健康恢复(取消熔断),如果不是健康的,则继续熔断。
服务调用的各种结果(成功,异常,超时,拒绝),都会上报给熔断器,计入bucket参与计算。
kratos熔断支持
目前kratos支持熔断的能力,用户可以自定义熔断器,目前框架中集成的熔断有http熔断,grpc熔断,db熔断这3种,熔断在默认情况下是开启的,目前不能选择关闭熔断,但是可以通过参数调整起到关闭熔断的效果
熔断在不配置的情况下,默认参数如下
1 | SwitchOff = true //开启熔断的开关,此开关目前是无效的,也就是设置任何值,熔断器都是开启的 |
kratos熔断机制
- kratos的熔断在http请求,rpc请求,db请求下强制为开启状态,如果想要关闭,可以配置熔断参数K为一个很大的值,但是不建议去关闭熔断器,本身也不应该通过该参数控制(因为SwitchOff参数无效)
- 2.kratos的熔断默认配置如上面描述所示,如果需要调整参数,请参考kratos熔断配置进行调整
- kratos的熔断不会一刀切,也没有半开状态(或者说可以说只有关闭和半开两个状态)
不会熔断的请求数为周期内已统计的成功数K,其他的请求会被熔断,当前请求会不会熔断是基于概率计算的,熔断率为(total-successK)/(total+1) (注: 请求放行率约等于k*success/total,成功率越低,放行率越低)
服务限流
限流算法的整理
在资源有限的情况下,遇到突发流量(如双十一)或系统 RT 剧增,为了保证系统不被拖垮引起更大规模的雪崩,必须进行限流。也就是说限流是系统的自我保护。限流本质上是根据系统处理能力,限制单位时间内处理的请求数量。
滑动窗口
这里的窗口是一个时间窗口,比如把一分钟划分为 6 个窗口,则每个窗口的时间范围是 10 秒。通过移动窗口,统计窗口期内流量,可以实现窗口期的限流。但是滑动窗口存在精度问题,在精度范围内统计到的窗口期流量可能在限流范围内,但进一步细分就会看到仍有突增流量。
滑动窗口时间范围越小,就越平滑,限流的效果越精确,但时间细分的粒度受到客观限制。
漏桶算法
漏桶算法,英文名 Leaky Bucket,是网络中流量整形(Traffic Shaping)和速率限制(Rate Limiting)常用的一种算法。漏桶算法调控了访问流量,使得突发流量可以被整形、去毛刺,为系统提供稳定的访问流量。如果漏桶溢出,请求就会被丢弃。
桶以固定的速率流出流量,无论水龙头进入的流量是多少,都不改变流出速率。上图中,水龙头处存在突发流量,一共进入 30Mb 数据,分布不均匀,对系统有冲击。经过漏桶算法处理,漏桶以 3 Mbps 速率持续流出数据,为系统做了很好的缓冲。
漏桶算法中,如果桶未满,可以持续接收流量;如果桶已满,流量溢出,后续的流量将无法入桶,会被丢弃。漏桶算法限制的是流出速率,无论流入是多少,都能保证后续系统的请求是平稳的。后续系统感知的流量相对固定,但可能在系统仍有能力处理更多流量的时候,也会被漏桶限制住。
令牌桶算法
令牌桶算法,英文名 token bucket,也是网络中流量整形或速率限制常用的一种算法。令牌桶算法以一个恒定的速率向桶里放入令牌,如果有新的请求进来希望进行处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。令牌桶算法限制的是流入速率,允许一定规模的突增流量,最大速率受限于桶的容量和令牌生成速率。可以支持一定程度的突发流量,更适合具有突发特性的流量。
令牌桶算法的简单描述如下:
每隔 1/r 秒向桶中增加一个 token;
桶最多只能存放 b 个 token。如果放置 token 时桶已经满了,丢弃这个 token;
当一个包含 n 个字节的数据包进来的时候,
- 如果桶中有足够的 token,将从桶中移除 n 个 token,然后把这个数据包发送出去;
- 如果桶中没有足够的 token,不会移除任何 token,返回拒绝服务;
自适应限流
使用自适应阈值可以动态调整限流,通过运行中不断试探,最终可以在系统处理能力和限流之间找到一个动态平衡。既能最大限度利用系统处理能力,又能确保系统稳定性。目前比较简单的自定义算法是参考 TCP 拥塞算法的斜率算法。斜率算法设置一个基准值,可以结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标评估系统负载,负载高了降低限流阈值,负载低了提高限流阈值。
目前比较简单的方案是使用平均 rt (响应时间)计算斜率,公式为
gradient =(RTTnoload / RTTactual)
newLimit = currentLimit×gradient + queueSize
其中,RTTnoload 表示无负载时的 rt,RTTactual 表示当前实际 rt。gradient 小于 1 表示负载偏高,大于 1 表示负载偏低。经过一段时间的试探,就能把系统流量控制在合理范围。
B站的自适应限流
kratos 借鉴了 Sentinel 项目的自适应限流系统,通过综合分析服务的 cpu 使用率、请求成功的 qps 和请求成功的 rt 来做自适应限流保护。
####核心目标
- 自动嗅探负载和 qps,减少人工配置
- 削顶,保证超载时系统不被拖垮,并能以高水位 qps 继续运行
#####限流规则
吞吐量介绍
计算吞吐量:利特尔法则 L = λ * W
如上图所示,如果我们开一个小店,平均每分钟进店 2 个客人(λ),每位客人从等待到完成交易需要 4 分钟(W),那我们店里能承载的客人数量就是 2 * 4 = 8 个人
同理,我们可以将 λ
当做 QPS, W
呢是每个请求需要花费的时间,那我们的系统的吞吐就是 L = λ * W
,所以我们可以使用利特尔法则来计算系统的吞吐量。
指标介绍
指标名称 | 指标含义 |
---|---|
cpu | 最近 1s 的 CPU 使用率均值,使用滑动平均计算,采样周期是 250ms |
inflight | 当前处理中正在处理的请求数量 |
pass | 请求处理成功的量 |
rt | 请求成功的响应耗时 |
滑动窗口
在自适应限流保护中,采集到的指标的时效性非常强,系统只需要采集最近一小段时间内的 qps、rt 即可,对于较老的数据,会自动丢弃。为了实现这个效果,kratos 使用了滑动窗口来保存采样数据。
如上图,展示了一个具有两个桶(bucket)的滑动窗口(rolling window)。整个滑动窗口用来保存最近 1s 的采样数据,每个小的桶用来保存 500ms 的采样数据。
当时间流动之后,过期的桶会自动被新桶的数据覆盖掉,在图中,在 1000-1500ms 时,bucket 1 的数据因为过期而被丢弃,之后 bucket 3 的数据填到了窗口的头部。
限流公式
判断是否丢弃当前请求的算法如下:
1 | cpu > 800 AND (Now - PrevDrop) < 1s AND (MaxPass * MinRt * windows / 1000) < InFlight |
MaxPass 表示最近 5s 内,单个采样窗口中最大的请求数。
MinRt 表示最近 5s 内,单个采样窗口中最小的平均响应时间。
windows 表示一秒内采样窗口的数量,默认配置中是 5s 50 个采样,那么 windows 的值为 10。
算法
请求对列优化
除了自适应限流,我们还做了 Codel 队列,传统的队列都是先进先出,但是我们发现微服务可能不太适合这种做法,这是因为微服务会有超时,肯定不可能无限期的等下去,可能你的 SLP 已经设置了 800 毫秒的超时,如果这时放行的是一个老的请求,该请求的成功率就会变低,因为它可能已经排队了好长时间。
所以这时我们需要一个基于处理时间丢弃的队列,当系统处于高负载的时候,实行后进先出的策略,也就是说要主动丢弃排队久的请求,并让新的请求直接通过,利用这个队列来弥补之前算法中的缓冲问题,吸收突增的流量。
方案
接口幂等性,有哪些方案?
Select 查询
天然支持幂等,而写操作
多次执行可能会导致数据错误,下面简单列举常见的解决方案:
- 表中加唯一约束, 有些表不适合添加
唯一约束
,可以单独建一张防重表
,在防重表
插入成功,在操作其它业务表
- 加悲观锁 ,
select * from order where order_id= 100000 for update;
对操作行锁定。 - 加乐观锁,表中增加一个
attribute_cc
自增字段,借助CAS
机制控制并发 - 采用
分布式锁
,第一次请求可以成功加锁,后续请求加锁失败,认为是重复请求。 - token机制,client 首先请求获取token,提交时除了业务参数外还要带上这个token,server端会对这个token核销,只能核销一次。如果 server 查询不到token,则认为是重复请求。
分布式服务接口的幂等性如何设计?
所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确得。比如不能多扣款。不能多插入一条数据,不能将统计值多加了 1,这就是幂等性。
其实保证幂等性主要是三点:
- 对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单 ID,一个订单 ID 最多支付一次。
- 每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见得方案是再 MySQL 中记录个状态啥得,比如支付之前记录一条这个订单得支付流水,而且支付流水采用 order id 作为唯一键(unique key)。只有成功插入这个支付流水,才可以执行实际得支付扣款
- 每次接收请求需要进行判断之前是否处理过得逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,order id 已经存在了,唯一键约束生效,报错插入不进去得。然后你就不用再扣款了
架构
一个基础的rpc框架包含哪些部分
- 客户端和服务端建立网络连接模块( server模块、client模块 )
- 服务端处理请求模块
- 传输协议模块
- 序列化和反序列模块。
- 服务发现
设计一个RPC框架,可以从PRC包含的几个模块去考虑,对每一个模块分别进行设计。
客户端和服务端如何建立网络连接?
服务端如何处理请求?
数据传输采用什么协议?
数据该如何序列化和反序列化?
服务发现采用什么方式
如何设计一个rpc 框架
- 注册中心:首先服务就得去[注册中心]注册吧,你是不是得有个注册中心,保留各个服务的信息,可以用zookeeper来做吧
- 客户端服务发现:然后你的消费者需要去注册中心拿对应的服务信息吧,而且每个服务可能会存在于多台机器上
- 动态代理:接着你就该发起一次请求了,怎么发起请求?懵逼了吧,当然是基于动态代理了! 你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址
- 负载均衡算法:然后找哪个机器发送请求?那肯定得有个负载均衡算法了,比如最简单的可以随机轮询是不是
- io 模型 + 发送协议:找到一台机器后,就可以跟它发送请求了 - 咋发送呢? 你可以说用netty了,nio方式 - 发送什么格式的数据? 你可以说用hessian序列化协议了,或者是别的,对吧。然后请求过去了
- 服务端的服务代理: 那边一样的,需要针对你自己的服务生成一个动态代理,监听某个网络端口,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码.
grpc 为什么选择http2.0 作为 传输协议
- HTTP/2 是一个经过实践检验的标准
HTTP/2是先有实践再有标准,这个很重要。很多不成功的标准都是先有一大堆厂商讨论出标准后有实现,导致混乱而不可用,比如CORBA。HTTP/2的前身是Google的SPDY,没有Google的实践和推动,可能都不会有HTTP/2 - HTTP/2 天然支持物联网、手机、浏览器
实际上先用上HTTP/2的也是手机和手机浏览器。移动互联网推动了HTTP/2的发展和普及。 - 基于HTTP/2 多语言的实现容易
只讨论协议本身的实现,不考虑序列化。 - 每个流行的编程语言都会有成熟的HTTP/2 Client
HTTP/2 Client是经过充分测试,可靠的
用Client发送HTTP/2请求的难度远低于用socket发送数据包/解析数据包
HTTP/2支持Stream和流控
在业界,有很多支持stream的方案,比如基于websocket的,或者rsocket。但是这些方案都不是通用的。
- HTTP/2 是一个经过实践检验的标准
选择http2.0 作为grpc传输协议的缺点
rpc的元数据的传输不够高效
尽管HPAC可以压缩HTTP Header,但是对于rpc来说,确定一个函数调用,可以简化为一个int,只要两端去协商过一次,后面直接查表就可以了,不需要像HPAC那样编码解码。
可以考虑专门对gRPC做一个优化过的HTTP/2解析器,减少一些通用的处理,感觉可以提升性能。HTTP/2 里一次gRPC调用需要解码两次
一次是HEADERS frame,一次是DATA frame。
HTTP/2 标准本身是只有一个TCP连接,但是实际在gRPC里是会有多个TCP连接,使用时需要注意。
领域模型相关
公司层面领域拆分维度
- 核心领域
- 通用领域
- 支撑领域
微服务架构和SOA巨石架构
微服务即是SOA(面向服务的架构)的一种具体实现, 但是微服务的职责更单一、服务即产品、可移植性更好、错误影响面更小;
DDD 的核心思想
让业务架构和系统架构行成绑定的关系
微服务划分原则
- 按照部门职能
- 确定模块的边界和紧密程度
- 按照服务的请求负载
- 按照故障率和变化率
传统三层模型和DDD领域模型
传统三层模型(贫血模型): 容易导致service层流水账,逻辑冗余在api方法中,难以迁移和维护
DDD: 将数据处理和装载逻辑交给领域模型对象,service只做对领域对象的编排
DDD主要需要考虑的点 1. 对领域的划分 2. 领域模型的装载和逻辑交给领域模型(对领域模型的处理和逻辑都在单独领域内完成) 3. service 层只负责对领域的编排,用以对外提供服务
1 | DTO -> DO 需要在领域模型内完成, 数据装载的interface 需要在领域模型内repo中完成, 数据实际获取以及 data -> DO 需要在data层完成 |
服务端 高并发、高可用可以考虑的点
- 微服务的拆分, 数据资源、计算资源、运行资源都进行拆分和隔离(根据服务等级和业务范围),;
- 读多服务, 可以考虑redis缓存、local 缓存、多级缓存、缓存降级等; 写多服务可以考虑优先写kv, 通过消息队列异步更新db和缓存;
- 数据库层面可以做到分库分表(水平和垂直),主从读写分离;
- 索引优化, 查询优化;
- 部署方面可以做到微服务容器的动态伸缩;
- 服务的限流、熔断、重试、降级等机制,避免对整条调用链路的影响;
- 多机房主备部署;
常见负载均衡算法
轮询法
加权轮询法
加权随机法
最小连接数法
随机法
源地址哈希法
最闲轮训
P2C
B站负载均衡算法
参考JSQ(最闲轮训)负载均衡算法带来的问题,缺乏的是服务端全局视图,因此我们目标需要综合考虑:负载+可用性。
参考了《The power of two choices in randomized load balancing》的思路,我们使用 p2c 算法,随机选取的两个节点进行打分,选择更优的节点:
- 选择 backend:CPU,client:成功率、inflight(处理中请求)、RT(响应时间) 作为指标,使用一个简单的线性方程进行打分。
- 对新启动的节点使用常量惩罚值(penalty),以及使用探针方式最小化放量,进行预热;
- 打分比较低的节点,避免进入“永久黑名单”而无法恢复,使用统计衰减的方式,让节点指标逐渐恢复到初始状态(即默认值)。
reids 和zookeeper 分布式锁的区别
redis 分布式锁
- 优点:redis本身的读写性能很高,因此基于redis的分布式锁效率比较高
- 缺点:锁删除失败 过期时间不好控制;非阻塞,操作失败后,需要轮询,占用cpu资源;
zookeeper的分布式锁
- 优点:不存在redis的超时、数据一致性可以保障,可靠性很高,基于watch + 回调的方式获取锁,实现简单。
- 缺点:保证了可靠性的同时牺牲了一部分效率(但是依然很高)。性能不如redis。