微服务该如何应对过量请求?

本文涉及的产品
性能测试 PTS,5000VUM额度
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 微服务该如何应对过量请求?

1. go-zero 稳定性能力概览

经过这么多年大流量服务端架构设计的沉淀,go-zero 在保护服务的稳定性上下足了功夫,不管是 CPU 密集型还是 IO 密集型服务,go-zero 都能很好的保护服务在如下场景不被拖垮或卡死:

  • 远超服务容量的突发大流量
  • CPU 打满
  • 上下游故障或者超时
  • MySQL、MongoDB、Redis 等中间件故障或者超负载(典型的是 CPU 飙高)

如图,我们从三个方面来保护系统的稳定性:

  • 服务端自适应过载保护
  • 服务端自适应熔断
  • 客户端自适应熔断

当然,我们还有自动适配后端服务能力的负载均衡算法,对稳定性进一步保驾护航。本文主要讲解自适应过载保护的原理、场景和表现。

2. 自适应过载保护压测

用过 Windows 的同学对这个界面应该都不陌生,这就是典型 CPU 打满服务不可用的表现。此时,我们一般都是心里默默骂一句,然后点左边那个按钮,对吧? 4f86170d8d241c2d8bba5b5880daa360.png

那我们想想,如果我们的服务 CPU 被打满了,是不是后面所有的请求也都被卡住了?等服务处理完请求的时候,用户那里可能已经超时离开了,结果服务器很忙,但都是做的无用功。如果这里不能理解,停下来好好思考一番,如果还不懂的话,可以来 go-zero 群里讨论讨论。。。

2.1 模拟 CPU 密集型服务

有人可能会问 CPU 密集型服务怎么定义?你的服务 CPU 会打满吗?处理请求会包含复杂的计算逻辑吗?你经常需要通过 cpu profiling 来优化性能吗?可以理解为服务的 IO 比较快,或者比较少,瓶颈是在 CPU 消耗上。

你可以直接用 goctl quickstart -t mono 命令生成一个 HTTP 服务,然后在 logic 代码里加上模拟 CPU 负载的请求处理代码。

模拟 CPU 计算的代码:https://gist.github.com/kevwan/ccfaf45aa190ac44003d93c094a12c3f

benchmarkCPU-10          330    3600743 ns/op

benchmark 结果可以看出单个请求的逻辑处理需要 3.6ms CPU资源(不包括服务端中间件处理消耗)。对于两核的容器来说,qps 上限约为 550(2000/3.6)。但是我们是一个 HTTP server,肯定还有接受请求、处理请求等逻辑处理的开销,实际上是达不到 550qps 的。

这个模拟 CPU 的代码本身不重要,就不做介绍了。

2.2 压测场景

2.2.1 场景一(不开启过载保护)

Timeout: 1000
Middlewares:
  Breaker: false
  Shedding: false
  • 服务跑在两核的容器内
  • 不开启过载保护
  • 超时 1s
  • loops 2 hey -c 200 -z 60m "http://localhost:8888/ping"
  • loops 是我的一个 aliasloops='fs() {for i in {1..$1}; do ${@:2} & done; wait}; fs',用来并行执行给定命令指定的次数

380f1999628120c83ef28b6c3dabbdd1.png

  • 可以看到系统总共只处理了大概 500qps 的请求,其中 400qps 多一点是成功的,近 100qps 是超时的(返回了 503 状态码)

f6d2e93da197fd65a437362c4f806a07.png

  • 随着请求的堆积,很快就会大量请求都超时了,并且p99,甚至p90都已经超过 1s 了
  • 这里进一步解释一下,超时的请求意味着对系统资源的浪费,比如接受到一个请求,花了不少cpu时间处理完了,然后返回结果时,发现请求已经超时了,用户已经收到了类似“服务器繁忙,请稍后再试!”的提示。这里可能有用户会说,go不是有超时控制吗?这里有两点:
  • 超时不是在代码的每个指令处都能先判断是否超时再执行,比如我们有一个for循环,不会每次都先判断ctx是否超时,然后再执行下一次迭代,如果你真的这样写了,性能可能需要特别关注,得看你每次循环的计算和判断超时的开销对比
  • 即使能够比较好的判断超时,在侦测到超时之前也已经白费了一些系统资源处理请求了

2.2.2 场景二(开启过载保护)

Timeout: 1000
Middlewares:
  Breaker: false

2.2.2 场景二(开启过载保护)

Timeout: 1000
Middlewares:
  Breaker: false
  • 开启过载保护(默认)
  • 超时 1s
  • loops 2 hey -c 200 -z 60m "http://localhost:8888/ping"

e9a89c3ee6b19232bab20a9ce3175ea8.png

  • 总 qps 大概在 10000 左右,流量大约是系统容量的 20 倍
  • 拒绝了约 95% 的过载请求

d2a3064b5825e64521b9dfd542a89bb0.png

  • 成功处理请求在 360-400 qps,大概损失了 10% 的 qps,被拒绝的近 1000qps 请求也需要消耗少量系统资源(从接受请求到被拒绝)

974e44630656ca4c3f43d6575ef2a483.png

  • 处理时延 p99 不到 700ms

3255c9f11b7f1354e69ce129819c22d3.png

  • 处理时延 p90 不到 25ms

2.3 压测结论:

  • 流量未知的情况下,保障系统不卡死(无过载保护情况下,CPU 满载一般表现为大量请求超时),且保证了系统容量的 400 qps 没有大幅下降
  • 自动拒绝了过量的请求,避免过量请求浪费系统资源(即使处理,系统最后返回给用户的也是不可用错误、超时错误等)

3. 自适应过载保护原理

先上一张总的设计图,从整体上说明了自适应过载保护的设计思路,也可以看完下面原理解析再回头来看这张图。

d3ccf2ae28d2cb8ed78d265f8212c047.png

3.1 CPU 使用率

CPU 使用率计算是第一个难点,很难算准,依赖于系统繁忙程度,是否能够及时调度到读取 CPU 使用率相关文件的执行。

3.1.1 CPU 检测场景

过载保护的一个触发条件就是当系统的 CPU 高于一个阈值时,go-zero 会自动触发过载保护,那么我们怎么检测 CPU 使用率呢?

首先,我们要明确需要覆盖的场景,当前无外乎虚机和容器两大类了。而容器里又分为 cgroup v1cgroup v2,所以总的有三类需要处理:

  • 虚机(不同云厂商有不同的叫法,比如 ECS, EC2 等)
  • 容器 cgroup v1
  • 容器 cgroup v2

这里有个特别需要关注的点是:容器是否设置了 cpu limit,如果没设,就只能用可以调度的 cpu 个数来计算,比如 cgroup v2 里可以读取 /sys/fs/cgroup/cpuset.cpus.effective 文件。

这里详细读取 cgroup/proc 下文件的方法我就不细述了,详见 go-zero 代码。

3.1.2 CPU 使用率计算方法(以 cgroup v2 为例)

3.1.2.1 实时 CPU 使用率的计算
  • 方法一
  • 8bb788b2bc5e1f4bb04c886e73049dbe.png
  • /sys/fs/cgroup/cpu.max 里面第一个值
  • 可能是 max,表示无限制,那此时总的限额就是 /sys/fs/cgroup/cpuset.cpus.effective里的 CPU 个数乘以 /sys/fs/cgroup/cpu.max 里面第二个值 period
  • 还有一种情况是/sys/fs/cgroup/cpuset.cpus.effective里的 CPU 个数 < /sys/fs/cgroup/cpu.max 里的第一个值 $max / 第二个值 $period,此时需要用 CPU 个数乘以 $period 作为 quota
  • /proc/stat 文件里第一行的内容,计算方法参考 go-zero 实现
  • /sys/fs/cgroup/cpu.stat 里第一行 usage_usec 后面的值用来计算两次的差值
  • cpu 使用率 单位为 millicpu (也称之为 millicores
  • ∆usage
  • ∆system
  • quota
  • 这里为啥要乘以 cores 并除以 quota 呢?举个例子,16 核(cores)的机器上,容器的 cpu limit 是 4 核(quota),但是从 /proc/stat 里看到的是 16 核的 cpu 总量。如果不做这一步的操作,那么容器内 cpu 使用率最多只会算到 25%,其实对于容器来说,已经是 cpu 使用率 100% 了,这不操作就是做了一个缩放。
  • 再补充一点帮助理解:对于 go-zero 的 250ms 的检测周期来说,如果系统时钟和调度都是严格控制在 250ms 的话,那么对于 16 核的机器来说,∆system(系统 cpu 总量)就应该是 4 秒(250ms * 16)。但是受系统调度的影响,250ms 的周期不会完全准,并且读取两个文件本身不是一个原子操作,go-zero 里针对这种情况做了容差处理。
  • 方法二 92f343dd34a02e4f0bddb65ea7271e39.png
  • /sys/fs/cgroup/cpu.stat 里第四行 nr_periods 后面的值用来计算两次的差值
  • ∆periods
  • 其它内容解释见方法一

go-zero使用了方法一,因为我考虑到 periods 更新间隔是 100ms,而 go-zero 检测窗口期是 250ms,这样两次检测的 ∆periods 就有时是 2 有时是 3,对计算结果造成非常可见的影响。

3.1.2.2 防止 CPU 使用率毛刺

go-zero 里使用了滑动平均算法(Moving Average)来避免 CPU 的毛刺。比如我们看股票价格曲线时,都会有 MA 线,如图,MA 线在价格波动较大时能够反应较长时间段内的价格变化趋势。对于我们的场景来说,刚好可以反应 CPU 变化趋势的同时也消除了 CPU 毛刺,避免一次小的抖动触发了过载保护。简单的理解滑动平均就是取之前的 N 个值的平均值。

dfd2907c9650e9192768785dfa470dde.png

go-zero 里对于滑动平均的超参 beta 取值 0.95,相当于最后 20 个值(1 / (1-0.95) = 20)的均值,所以 CPU 达到 90% 后约 5 秒会触发过载保护。

3.2 系统容量计算

容量计算是第二个难点,怎么动态评估系统当前容量是算法的关键点之一。计算公式如下:

5b5227d628d3bab26560bf664fa4d69f.png

先解释一下各个参数的含义:

  • maxPass 是当前记录的所有窗口里面最大的成功处理的请求数,go-zero 是 100ms 一个窗口,记录了 50 个窗口,这里就是算过去 50 个窗口里最多成功处理请求的一个窗口里的请求数
  • minRt 是指以窗口为单位的最小平均请求耗时,单位毫秒,比如某个窗口里的 RT 是 50ms,且是所有窗口里最小的,那么这个 minRt 就是 50
  • windows 是指每秒有多少个窗口

我来一步一步推导一下这个公式怎么来的。

  • maxPass * windows 就是每秒系统能成功处理的请求数
  • minRt / 1000 是个缩放系数,用来把每秒能处理的请求数缩放到 minRt 时间长度上系统能处理的请求数
  • 一个请求的处理时长保守估算为 minRt,所以 maxPass * windows * minRt / 1000 代表着系统能处理的保守并发数。这里相对比较难理解,可以想想一个请求的时间跨度为 minRt,那么把每秒的请求数 maxPass * windows 平铺到 1s 的时间线上,是不是任意点的并发请求数可以估算为 QPS * minRt / 1000。从概率的角度可以理解为均匀分布的区间积分,即 1 秒内均匀分布的 n 个请求,在 minRt 区间上的请求数量。

如果当前并发请求数大于这里算出的系统容量,那么就会拒绝请求,所以这里估算系统容量是关键所在,也是整个算法最难理解的部分。

附一个图来说明怎么计算任一时间点的并发请求数的,假设 QPS 是 1000,每个请求的 minRt 是 10ms。

9d1d7635d94623e85683b3167e0af304.png

3.3 CPU 负载反馈因子

当 CPU 负载超过了设置的阈值时,我们期望 CPU 越高,对请求的拒绝比例越高,否则 CPU 依然有可能越来越靠近 100%。

反馈因子计算公式如下:

75c7aa1906c4a4d2644861412c987f9f.png

反馈因子的效果类似于神经网络中的 ReLU 激活函数。其中 0.1(兜底的经验值)是用来保证不管负载多高,至少放过估算出来的系统容量的 10% 的请求,否则整个服务就完全不可用了。CPU 负载反馈因子随着 CPU 负载的变化如下图:

0fd8295c250f8330d3ee076254347a83.png

对比有无 CPU 反馈因子的实际表现:

b03c350b3aff293e0da103bf83fc38e7.png

  • 加了反馈因子后能接受更多请求,从不到 3000qps 上升到了 5000qps 左右,上图是拒绝的请求

65b637b3342071e8ff2ec22018ef2693.png

  • 成功处理的请求数有略微下降,因为进来的请求变多了,拒绝请求消耗了少量资源

c5dea083b709cd4c65ac79190dff5f54.png

  • 加了反馈因子 P99 时延从 900ms 左右,降低到 700ms 左右

4f72c927abdd0c7c7fe3f61fdcdf9756.png

  • 加了反馈因子 P90 从 250ms 降到了 50ms

由此可见,CPU 负载反馈因子的作用还是很明显的。

3.4 过载保护计算公式

把上面所有算法细节和逻辑归总到计算公式里,如下图:

6b50380778d34e0e01912b2b57f820ab.png

这就是计算当前系统允许的最大并发请求数,超过这个值就会拒绝请求。

3.5 如何判断是否拒绝请求

对于一个请求是否需要拒绝。判断逻辑如下:

d3ff8fbf1deafef70bd668d44879f4cf.png

有以下情况之一,计算是否超过系统容量,如超过,则拒绝该请求

  • 首先判断 CPU 是否超标(默认 90%)
  • 再判断是否在冷却期(1s)
  • 两者有其一,则计算是否拒绝
  • 计算当前系统容量  maxPass * windows * minRt / 1000
  • 并发请求数是否大于当前系统容量,如是则拒绝请求

至此,原理基本讲完了,还有一些实现细节和技巧你可以翻看 go-zero 源码。

我们再来看一下系统容量 10 倍流量的场景下整体表现:

5271d1f739c78261fffd0105a7ae3fc3.png

  • 成功处理的 qps 在 400 左右

e243c5c161824168402866ceb4608ea3.png

  • 拒绝了大概 90% 的请求

9b8f1e67dbdf9d49c9a127281dbb7210.png

  • P99 时延控制在 20-24ms 之间

2a263908e0928b30e9f740e473b9d9e2.png

  • P90 时延在 5ms 以下

a42044d6382d196eb354e72b5b1220cf.png

  • CPU 峰值控制在 95% 以下

如文档开始的压测数据,如果不是过载保护,在不到 600 qps 的情况下,P99 甚至 P90 都已经到了 1s 的超时阈值了,服务基本已经开始不可用了。

3.6 跟 Kubernetes HPA 的协同

当我们在使用 Kubernetes 并设置了 HPA 根据 CPU 使用率自动伸缩的时候,Kubernetes 默认会在 4 个连续的 15 秒探测周期探测到 CPU 使用率超标(有 0.9 - 1.1 的容忍幅度)时,启动增加 pod 来应对系统容量不足,但这需要分钟级扩容,且当系统资源不够或者 pod 数达到最大设置时不生效。此时,过载保护可以在 Kubernetes 未来得及扩容或者集群容量不足时保护我们的系统不被打到卡死。

但这里有个点需要注意,go-zero 默认设置的过载保护触发的 CPU 阈值是 90%,Kubernetes HPA 自动扩容默认 CPU 阈值是 80%,当你在设置这两个参数的时候,不要让 HPA 的 CPU 阈值大于 go-zero 的过载保护 CPU 阈值,否则可能会抑制 HPA 的生效。

当然整个系统并不是链路上所有服务和中间件都可以自动或及时扩容的,这里就牵出另一个稳定性能力 - 自适应熔断了。有了自适应过载保护和自适应熔断的双重加持,流量再大(上限是所有 CPU 都用在降载熔断等能力上),服务也不会挂。后续文章我会深度分析自适应熔断的场景压测和实现原理。

4. 总结

自适应过载保护的算法有如下要点:

  • CPU 使用率检测。要在各种不同环境尽可能保证检测结果准确。这里要防止检测周期受系统调度影响出现较大偏差,需要消除异常值,且通过滑动平均的方式来避免毛刺对算法的影响。
  • 系统容量评估。类似于 BBR 算法里检测带宽RTT,我们需要探测的是 系统能承载的 QPSminRT(最小处理时间),不同于 BBR 的不能同时探测,我们是可以同时探测的,所以我们不需要有复杂的状态机,只需要通过滑动窗口来记录过去一段周期内的请求状况即可。
  • CPU 负载反馈因子。类似于神经网络中的激活函数,基于 CPU 负载对系统容量做调节。
  • 过载保护冷却时间。用于避免过载保护在 CPU 临界值周边来回启停,有利于大流量下的请求抑制效果。
  • 跟 HPA 的协同。一定要避免 HPA 的 CPU 扩容配置低于过载保护设置的阈值,否则过载保护会抑制 HPA 的生效。但一般不太会发生,HPA 默认值是 80%,过载保护默认值是 90%,不建议把 HPA 的配置往上调。

go-zero 的三类服务都已经默认集成了自适应过载保护:

  • Restful service
  • gRPC service
  • Gateway service

确保你在使用 go-zero 的时候,无需额外代码和配置,服务默认都是有自适应过载保护的。这也秉承了 go-zero 一贯追求最简原则,不给用户带来额外的心智负担。

这篇文章花了我春节不少时间,既要讲原理,又要写代码给出压测数据,但依然感觉不是那么容易懂,不过好在所有算法细节和知识点都已经集成在 go-zero 源码里了,如果想了解实现细节,可以阅读源码。

项目地址

https://github.com/zeromicro/go-zero

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
Java 微服务 Spring
从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(六)(优化篇)开发篇-如何解决微服务开发环境请求实例转发到别人机器问题
从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(六)(优化篇)开发篇-如何解决微服务开发环境请求实例转发到别人机器问题
从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(六)(优化篇)开发篇-如何解决微服务开发环境请求实例转发到别人机器问题
|
1月前
|
XML Java 数据库
在微服务架构中,请求常跨越多个服务,涉及多组件交互,问题定位因此变得复杂
【9月更文挑战第8天】在微服务架构中,请求常跨越多个服务,涉及多组件交互,问题定位因此变得复杂。日志作为系统行为的第一手资料,传统记录方式因缺乏全局视角而难以满足跨服务追踪需求。本文通过一个电商系统的案例,介绍如何在Spring Boot应用中手动实现日志链路追踪,提升调试效率。我们生成并传递唯一追踪ID,确保日志记录包含该ID,即使日志分散也能串联。示例代码展示了使用过滤器设置追踪ID,并在日志记录及配置中自动包含该ID。这种方法不仅简化了问题定位,还具有良好的扩展性,适用于各种基于Spring Boot的微服务架构。
44 3
|
2月前
|
消息中间件 缓存 Kafka
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
|
2月前
|
负载均衡 Java API
深度解析SpringCloud微服务跨域联动:RestTemplate如何驾驭HTTP请求,打造无缝远程通信桥梁
【8月更文挑战第3天】踏入Spring Cloud的微服务世界,服务间的通信至关重要。RestTemplate作为Spring框架的同步客户端工具,以其简便性成为HTTP通信的首选。本文将介绍如何在Spring Cloud环境中运用RestTemplate实现跨服务调用,从配置到实战代码,再到注意事项如错误处理、服务发现与负载均衡策略,帮助你构建高效稳定的微服务系统。
72 2
|
5月前
|
JavaScript 前端开发 网络协议
KOI 微服务提供者接收请求,提供服务并传回给 Orchestra
KOI 微服务提供者接收请求,提供服务并传回给 Orchestra
|
5月前
|
负载均衡 前端开发 Java
字节后端面试题(前端发送请求到后端的过程(MVC),网关gateway作用,怎么解决跨域,各微服务组件作用)
字节后端面试题(前端发送请求到后端的过程(MVC),网关gateway作用,怎么解决跨域,各微服务组件作用)
400 0
|
API 微服务 容器
微服务组件之OpenFeign配置信息及RequestInterceptor请求拦截器
OpenFeign配置信息及RequestInterceptor请求拦截器
|
Web App开发 开发框架 前端开发
SpringCloud微服务实战——搭建企业级开发框架(三十九):使用Redis分布式锁(Redisson)+自定义注解+AOP实现微服务重复请求控制
通常我们可以在前端通过防抖和节流来解决短时间内请求重复提交的问题,如果因网络问题、Nginx重试机制、微服务Feign重试机制或者用户故意绕过前端防抖和节流设置,直接频繁发起请求,都会导致系统防重请求失败,甚至导致后台产生多条重复记录,此时我们需要考虑在后台增加防重设置。
559 53
SpringCloud微服务实战——搭建企业级开发框架(三十九):使用Redis分布式锁(Redisson)+自定义注解+AOP实现微服务重复请求控制
|
负载均衡 前端开发 Java
从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(六)开发篇-如何解决微服务开发环境请求实例转发到别人机器问题
从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(六)开发篇-如何解决微服务开发环境请求实例转发到别人机器问题
从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(六)开发篇-如何解决微服务开发环境请求实例转发到别人机器问题
|
Go 微服务
《Go 构建日请求千亿级微服务的最佳实践》电子版地址
Go 构建日请求千亿级微服务的最佳实践
97 0
《Go 构建日请求千亿级微服务的最佳实践》电子版地址