微服务弹性设计
什么是系统弹性(Resiliency)
微服务的弹性主要由以下几个方面来考虑:
- 可靠性(Robustness):系统能够吸收所预期的扰动;
- 反弹能力(Rebound):系统遭受实质性打击仍能恢复原状;
- 优雅扩展(Graceful Extensibility):系统在遇到意想不到的情况时如何应对;
- 持续适应(Sustained Adaptability):系统能够不断适应变化环境。
隔离
隔离,本质上是对系统或资源进行分割,从而实现当系统发生故障时能限定传播范围和影响范围,即发生故障后只有出问题的服务不可用,保证其他服务仍然可用。
服务隔离
动静分离
隔离加速是提高系统可用性的一种方式,从 CPU 的 cacheline 到数据库表设计及架构设计中的图片、静态资源的缓存加速,目的都是通过降低应用服务器负载、降低对象存储费用以及获得大量的存储空间来实现。
读写分离
即将系统中的写操作与读操作逻辑分开,有利于一方面提高并发请求的处理能力,同时也有利于提高系统的性能和效率。如 MySQL 主从,CQRS。
轻重隔离
轻重隔离的目的是把系统中重要及核心业务独立出来,从而对关键业务进行优先保护,让核心流量不受外界影响,也就是说可以把系统请求划分为重度或轻度操作,以保证核心操作正常运行。
物理隔离
物理隔离指的是在多个服务之间按照不同纬度进行物理隔离,主要采用线程、进程、集群等方式进行实现,以保证服务的运行不互相影响。
超时控制
超时控制的场景
- Kit 库兜底默认超时:实际业务开发中,我们依赖的微服务的超时策略并不清楚,或者随着业务迭代耗时超生了变化,意外的导致依赖者出现了超时。
- 进程内超时控制:一个请求在每个阶段(网络请求)开始前,就要检查是否还有足够的剩余来处理请求,以及继承他的超时策略,使用 Go 标准库的
context.WithTimeout
。- 级联传递需要将默认超时和收到的超时对比,取最小的超时。
- 跨进程传递:在 gRPC 框架中,它通过使用 gRPC Metadata Exchange、HTTP2 的 Headers 和 grpc-timeout 字段来传递超时参数,从而帮助构建拥有超时处理的上下文环境。
双峰分布
- 双峰分布: 95%的请求包含不超过 100ms 的响应时间,而 5%的 请求可能永远不会返回结果(如超长超时)。
- 在监控请求时,要看耗时分布统计,而不仅仅是通过 mean 值,例如 95th 和 99th 等也是非常重要的数据。
- 必须设置合理的超时,拒绝超长请求,或者在服务器不可用时失败。
过载保护
负载保护是一种对过载以及高延迟的流量处理技术,它主要通过将计算系统临近过载时的峰值吞吐作为限流的阈值,来控制流量,以达到自我保护的目的,以保证系统稳定。常用的技术有:
- 利特尔法则:使用 CPU 和内存等资源作为信号量进行节流;
- 队列管理法:通过控制队列长度和 LIFO 等方式对请求进行管理
- 可控延迟算法:利用CoDel这种算法实现可控延迟。
限流
服务都有其能够处理请求的容量限制,因此需要使用一些策略来控制在单位时间内接收的请求数量,以控制流量。一般有三种情况需要限流 突发流量、恶意流量、业务需求(比如云服务,不同价格服务 QPS、TPS 不一样)。
限流需要考虑的问题
-
依据什么限流?
- 每秒事务数(Transaction per Second,TPS):是衡量信息系统吞吐量的最终标准。
- 每秒请求数(Hit per Second,HPS):单位时间内处理的请求数量。通常应用于网关或负载均衡器的指标。主流使用这个作为限流依据。
- 每秒查询数(Query per Second,QPS):是指一台服务能够响应的查询次数。通常用于数据库和缓存的指标。
- 按流量限制:下载、视频、直播等 I/O 密集型系统。
- 在线用户数:网络游戏等基于长连接的应用,会把登录用户数作为限流指标。
-
如何限流?
- 流量计数器模式
- 滑动时间窗模式
- 漏桶模式
- 令牌桶模式
-
超额流量如何处理?
- 直接拒接
- 放入队列等待
- 根据服务的轻重,降低服务质量
如何限流
基于计数的限流算法
通过维护一个计数器来实现。当每次有新的请求到来时,计数器会自动+1,同时定时清零计数器以控制一定时间内的请求数量。当请求次数达到设定的阈值时,禁止进一步的请求。
单纯按照固定时间周期来限制请求流量,会存在的问题:
- 流量不均:假设每分钟处理 100 个请求,第一个一分钟的 00:00:50 的时候收到 100 个请求,第二分钟的 00:00:05 收到 100 个请求,那第一个一分钟的 00:00:50 到第二分钟的 00:00:05 之间的收到了 200 个请求。
- 误杀请求:假设每秒处理 100 个请求,10s 的时间片段中,前 3 秒平局值到了 150,后 7 秒平局处理 30 个。如果每个请求的超时时间是 10 秒,450+210 个请求在 10 秒内是可以处理完的。
滑动窗口算法(Sliding Window Algorithm)
滑动窗口算法(Sliding Window Algorithm)在计算机科学的很 多领域中都有成功的应用,譬如编译原理中的窥孔优化(Peephole Optimization)、TCP 协议的阻塞控制(Congestion Control)等都使 用到滑动窗口算法。
按照指定的时间间隔进行时间窗口的滑动,例如 1 秒钟一个时间窗口。在每个时间窗口内,记录此期间内的请求次数,并将其与特定限流阈值(例如每秒最多处理 100 个请求)进行比较。当请求次数超过阈值时,就禁止进一步的请求进入并触发限制。
- 优点:可以实时控制请求次数不超过限流的阈值,不会出现基于计数的限流算法流量不均问题。在单机限流或者分布式服务单点网关中经常使用。
- 缺点:只适用于否决式限流,即超过阈值的流量必须进行强制失败或降级,难以进行阻塞等待处理,并且难以在细粒度上对流量曲线进行整形,无法起到削峰填谷的作用。
漏桶算法(Leaky Bucket Algorithm)
漏桶算法是一种常见的流量控制算法,工作原理是将请求放入一个固定容量大小的“桶”中,然后按照一定的速率去处理这些请求,在没有请求时,桶内部会持续地自动漏水,保持桶内通过率大致相等。当请求量超出桶的容量时,请求会被拒绝或排队等待,从而实现对流量的限制。
漏桶算法是一种固定容量的队列,它以恒定的速率流出一些请求,假设每个请求为一个水滴,向漏桶中添加水滴时,如果没有额外的空间,则请求将被舍弃。 这种算法可以防止突发请求的情况,但会丢失部分合法的请求,因此,在设置 桶大小 和 水流速度 时需要仔细衡量优缺点。该算法的优点是简单易于实现;缺点是不能处理瞬时峰值流量。
leaky-bucket rate limit algorithm
令牌桶算法(Token Bucket Algorithm)
令牌桶算法是一种常见的限流算法,它将请求看作一个个令牌,并为每个请求分配一个令牌。桶中按照一定速率往里面放入令牌,当请求过来时,如果桶中有足够的令牌,则该请求被处理并从桶中取走一个令牌;否则该请求会被拒绝或排队等待,直到获取到足够的令牌后再进行处理。
令牌桶算法的优点包括可以对请求的处理速率进行动态调整,较滑动窗口算法更适合于动态系统;还可以进行平滑限流,防止出现流量的突然峰值,保证服务的稳定性。而缺点则是可能会导致请求排队等待,会降低请求处理的实时性。
token-bucket rate limit algorithm
总结
- 滑动窗口算法缺点是需要比较大的内存开销,但能够立即响应请求,适用于时间敏感场景;
- 漏桶算法需要维护一个队列,可能会产生较大的时延,更适合后台任务这样可以接受一定时延的场景。
- 令牌桶算法也像漏桶一样有可能引起请求被长时间缓冲的问题,不太适用于某些时间敏感的场景。
分布式限流
如何基于单个节点按需申请,并且避免出现不公平的现象?
- 利用最大最小公平分配(Max-Min Fairness)来使所有用户都能够公平地获取到满足他们需求的资源。 这一策略首先为所有用户分配低于他们需要的最小配额,然后平均分配剩余的资源。这样保证了无论是对于需要“大量资源”的用户,还是要求“少量资源”的用户,都能够获得十分公平的分配。
降级
降级是一种减少工作量的方式,也可以丢弃不必要的请求。我们需要了解哪些流量可以进行降级,并能够区分不同的请求。通常,我们可以提供质量略低的回复来减少计算量和时间,避免影响服务性能。 降级本质为: 提供有损服务。
确定限流指标时,比如 CPU、延迟、队列长度、线程数量、错误等,考虑三点:
- 进入降级模式后需要执行什么操作?
- 流量抛弃和优雅降级在哪一个层面上实现?
- 降级机制是否简单。
我们还应该注意:
- 优雅降级不应该经常被触发,因此需要避免容量规划失误导致的意外负载。
- 期有少部分的流量进行演练,保证优雅降级的机制正常。
重试
对于 backend 部分节点出现过载的情况,限制重试次数以及使用基于重试分布的策略是很有必要的,它可以帮助我们避免流量放大(重试比率: 10%)。
另外,客户端也应当收集重试次数的直方图,并传递给 server 用来进行分布判定,从而拒绝一些无意义的请求。
在失败的这层正确的进行重试也很重要,当重试仍然失败时,最好将不再重试作为一个全局约定错误码来返回给用户,而不会选择去进行级联重试。
负载均衡
在理想情况下,某个服务的负载会完全均匀地分发给所有的后端任务。在任何时刻,最忙和最不忙的节点永远消耗同样的 CPU。
为了达成这个目标,我们需要做几件事情:
- 流量分发的均衡。
- 精准的发现异常节点。
- 伸缩管理:服务伸缩扩容。
- 降低误差率,提高可用性。
然而,通常存在一些问题是导致负载不均衡:
- 每个请求消耗的处理时间可能不尽相同。
- 服务器之间很难强制保持绝对的同质性,很容易发生共享资源抢占(例如内存缓存、带宽、IO 等)的情况。
- 运行指标的差异(比如 Full GC、JVM JIT)
当发现在 Backend 之间存在很大的负载差异时,可尝试使用 P2C(最闲轮训)负载均衡算法来解决:需要综合考虑负载+可用性。
实现方法:
- 选择 backend:CPU / client 方面的 health、inflight、latency 等指标来进行打分,使用一个线性方程来评估分数;
- 对新启动的节点使用常量惩罚及探针方式最小化流量放量;
- 打分较低的节点,避免无法恢复进入“永久黑名单”,使用统计衰减回复到正常;
- 超过 predict lagtency 的请求将获得惩罚。