关于java:服务限流怎么计算配额

5次阅读

共计 4461 个字符,预计需要花费 12 分钟才能阅读完成。

起源:https://zhenbianshu.github.io

问题

申请被限流

之前的文章提到过咱们服务应用 Hystrix 进行服务限流,应用的是信号量形式,并依据接口的响应工夫和服务的峰值 QPS 设置了限流的配额。

限流配额的计算形式为:

咱们接口单机单个接口的峰值 QPS 为 1000,均匀影响时长 15ms,咱们认为 Hystrix 的信号量是并发量,那么一个信号量在一秒内能容许 1000ms/15ms~66 个申请通过,那么服务 1000QPS 配置 15 个信号量就足够了。

当然这是在疏忽上下文切换和 GC 工夫的状况下,思考上这些因素,每个并发量每秒能服务的时长约为 900ms,用同样的公式计算所须要的信号量是 17,为了应酬突发流量,我将这个值设置为了 30。

本认为这样就居安思危了,没想到看谬误日志中偶尔发现了有报错:

HystrixRuntimeException occurred! , failureType:REJECTED_SEMAPHORE_EXECUTION, message:apiHystrixKey could not acquire a semaphore for execution and fallback failed.

我把信号量配置进步到了 50,没想到还是没看到问题有明显好转,这就比拟诡异了。

解决

排查步骤

首先我列了一下排查的步骤,也整顿一下呈现这种问题的可能。

  1. 看失常申请的均匀耗时,排除实在 block 的可能。接口均匀耗时 17ms,QPS 1000,如果代码都被 block 在某处,接口耗时肯定会突增。
  2. 查看一下 hystrix 代码看是否可能有状况导致信号量未开释。简略扫了一遍 hystrix 相干代码,信号量的开释在申请完结的 callback 里,如果有透露,肯定会导致可用信号量越来越少,最终为 0。
  3. 写一个小 demo,压测看是否能复现。在 demo 里运行,问题只在刚启动服务未初始化实现时复现,后续就安稳了。

Jdk 的 Bug?

从整体上看不出来,就只好从宏观工夫点上看了,可这个问题呈现是一瞬间的事,jstack 也无能为力,尽管 jmc 倒是适合,但它部署有点吃力,而且还会在察看的时候影响到服务,于是优先从历史工夫点上排查。

从谬误日志里找了一个服务回绝数校多的工夫点,再察看服务过后的状态。谬误日志上除了一些申请被回绝的报错外就没有其余的了,但我在 gclog 里发现了奇怪的日志。

2020-03-17T13:01:26.281+0800: 89732.109: Application time: 2.1373599 seconds
2020-03-17T13:01:26.308+0800: 89732.136: Total time for which application threads were stopped: 0.0273134 seconds, Stopping threads took: 0.0008935 seconds
2020-03-17T13:01:26.310+0800: 89732.137: Application time: 0.0016111 seconds
2020-03-17T13:01:26.336+0800: 89732.163: [GC (Allocation Failure) 2020-03-17T13:01:26.336+0800: 89732.164: [ParNew
Desired survivor size 429490176 bytes, new threshold 4 (max 4)
- age 1: 107170544 bytes, 107170544 total
- age 2: 38341720 bytes, 145512264 total
- age 3: 6135856 bytes, 151648120 total
- age 4: 152 bytes, 151648272 total
: 6920116K->214972K(7549760K), 0.0739801 secs] 9292943K->2593702K(11744064K), 0.0756263 secs] [Times: user=0.65 sys=0.23, real=0.08 secs]
2020-03-17T13:01:26.412+0800: 89732.239: Total time for which application threads were stopped: 0.1018416 seconds, Stopping threads took: 0.0005597 seconds
2020-03-17T13:01:26.412+0800: 89732.239: Application time: 0.0001873 seconds
2020-03-17T13:01:26.438+0800: 89732.265: [GC (GCLocker Initiated GC) 2020-03-17T13:01:26.438+0800: 89732.265: [ParNew
Desired survivor size 429490176 bytes, new threshold 4 (max 4)
- age 1: 77800 bytes, 77800 total
- age 2: 107021848 bytes, 107099648 total
- age 3: 38341720 bytes, 145441368 total
- age 4: 6135784 bytes, 151577152 total
: 217683K->215658K(7549760K), 0.0548512 secs] 2596413K->2594388K(11744064K), 0.0561721 secs] [Times: user=0.49 sys=0.18, real=0.05 secs]
2020-03-17T13:01:26.495+0800: 89732.322: Total time for which application threads were stopped: 0.0824542 seconds, Stopping threads took: 0.0005238 seconds

我看到间断产生了两次 YGC,它们之间的距离才 0.0001873s,能够认为是进行了一次很长时间的 GC,总耗时达到了 160ms。再仔细观察第二次 GC 时的内存散布,能够看到它作为一次 ParNew GC,产生时 eden 区的内存才应用了 200M,这就不合乎常理了。

再看 GC 产生的起因,日志里标识的是 GCLocker Initiated GC。在应用 JNI 操作字符串或数组时,为了避免 GC 导致数组指针产生偏移,JVM 实现了 GCLocker,它会在产生 GC 的时候阻止程序进入临界区,并在最初一个临界区内的线程退出时,产生一次 GCLocker GC。

至于这次的 GC,是 JDK 的一个 Bug,JDK-8048556,而咱们的 Java 版本低于修复版本,呈现这种问题实属失常,可是,这个问题就归究于 jdk 的 bug 吗?降级了 jdk 版本就肯定会好吗?

“均匀”的陷阱

从新来计算一下,即便 JVM 每秒都有 160ms 在进行 GC,可零碎有服务工夫也还有 840ms,应用上文中的公式,信号量的还是齐全足够的。

一时想不明确,进来倒了杯水,走了走,突然想到原来本人站错了角度。我始终用秒作为工夫的根本单位,用一秒的均匀状态来代表零碎的整体状态,认为一整秒内如果没有问题,服务就不应该会产生问题,可是疏忽了工夫从来不是一秒一秒进行的。

试想,如果安稳运行的服务,突然产生了一次 160ms 的 GC,那么这 160ms 内的申请会平均分配到残余 840ms 内吗?并不会,它们会挤在第 161ms 一次发送过去,而咱们设置的信号量限度会作出什么反馈呢?

@Override
public boolean tryAcquire() {int currentCount = count.incrementAndGet();
    if (currentCount > numberOfPermits.get()) {count.decrementAndGet();
        return false;
    } else {return true;}
}

下面是 Hystrix 源码中获取信号量的代码,能够发现,代码里没有任何 block,如果以后应用的信号量大于配置值,就会间接回绝。

这样就说得通了,如果进行了 160ms 的 GC,再加上申请解决的均匀耗时是 15ms,那零碎就有可能在霎时沉积 1000q/s * 0.175s = 175 的申请,如果信号量有余,申请就会被间接回绝了。

也就是说即便 jdk 的 bug 修复了,信号量限度起码还是要设置为 95 才不会拒绝请求。

限流配额的正确计算形式

概念

那么限流配额的正确计算形式是怎么的呢?

在此之前咱们要明确设置的限流配额都是 并发量 ,它的单位是 ,这一点要 辨别于咱们罕用的服务压力指标 QPS,因为 QPS 是指一秒内的申请数,它的单位是 个 /S,因为单位不同,它们是不能间接比拟的,须要并发量再除以一个工夫单位才能够。

正确的公式该当是 并发量(个)/ 单个申请耗时(s) > QPS(个 /s)

但因为 Java GC 的个性,咱们不得不思考 GC 期间申请沉积的可能,要解决这种状况,第一种是间接回绝,像 Hystrix 的实现(有点坑),第二种是做一些缓冲。

信号量缓冲

其实信号量并不是无奈做缓冲的,只是 Hystrix 内的”信号量”是本人实现的,比拟 low。

比拟”正统”的形式是应用 jdk 里的 java.util.concurrent.Semaphore,它获取信号量有两种形式,第一种是 tryAcquire(),这相似于 Hystrix 的实现,是不会 block 的,如果以后信号量被占用或有余,会返回 false。

第二种是应用 acquire() 办法,它没有返回值,意思是办法只有在拿到信号量时才会返回,而这个工夫是不确定的。

我猜测这可能也是 Hystrix 不采纳这种形式的起因,毕竟如果应用 FairSync 会有很多拿到信号量发现接口超时再摈弃的行为,而应用 UnFairSync 又会使接口的影响时长无奈确定。

线程池缓冲

线程池的缓冲比信号量要灵便得多,设置更大的 maximumPoolSizeBlockingQueue 都能够,设置 rejectHandler 也是很好的方法。

只是应用线程池会有上下文切换的损耗,而且应答突发流量时,线程池的扩容也比拟拙技。

思考到它的灵活性,以及能够通过 Future.get() 的超时工夫来管制接口的最大响应工夫,和信号量比,没有哪一种形式更好。

小结

解决了一个服务暗藏了很久的问题,又积攒了排查此类问题的教训,失去了问题不能只从一个角度对待的教训,还是比拟开心的。

当然,也又一次证实了看源码的重要性,遇到问题追一追源码,总会有些收益。

近期热文举荐:

1.600+ 道 Java 面试题及答案整顿(2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0