弱依赖“并发请求数阀值”这个值设置多少合适?
“并发请求数阀值”在大部分情况下可以理解为同时工作的线程数阀值,这个值不是越大越好,也不是越小越好,而是在最高QPS输出的情况下这个值越小越好。这个也是系统性能优化的一个方向,高QPS,少线程。
线程它是驱动业务逻辑执行的载体,在执行逻辑的生命周期中,它会申请各种各样的资源,会占有CPU,会申请内存对象并持有,会申请锁并持有,会申请IO资源并持有,然后再完成一些工作之后,在恰当的时间释放这些资源。如果这个生命周期越长,那么直接导致这些资源被持有的时间越长。导致资源的竞争更加厉害。在java应用中,一个明显的场景就是由于内存申请之后长时间不能释放,导致FULLgC非常频繁。
另一个角度,因为响应时间变长之后,会直接导致需要更多的线程数来满足不变的QPS,由于线程越多,导致对资源的持有和竞争更加激烈,表现CPU占用很高,Load很高,这是一个恶性循环,需要打破这个环。
以cache访问为例
假设客户端app(相对cache而言)本身的输入QPS为200(平均每秒有100个用户请求到此系统),每次业务请求会调用2次Cache,相当于对cache请求的QPS是200*2=400,也就是说如果要满足客户端app 200的qps,那么需要客户端app对Cache有400qps的访问能力,假设客户端app访问cache的平均响应时间为1ms。
用户--1次请求-->[APP]--2次请求-->[Cache]
根据公式:QPS=1000ms/RT(平均响应时间) * threadNum(并行线程数)
threadNum=QPS/1000 * RT = 400/1000 * 1
= 1
Cache响应时间为1ms,那么只需要一个线程就足以提供每秒400次的请求服务,其实理论上1个线程可以提供1000的qps,当然前提条件是cacheServer有非常高的吞吐容量
我们模拟一下故障
此时CacheServer发生了故障,对CacheServer的调用全部超时,响应时间变成了3000ms
根据公式计算:threadNum=QPS/1000 * RT = 400/1000 * 3000 =1200
此时需要1200个线程才能满足400QPS的流量 ,换而言之就是客户端会有1200个线程堵塞在CacheServer的访问请求上。这个线程数量早就超过了客户端配置的线程数量,app系统表现为hung住,几乎不能处理任何用户的请求,如果客户端配置的线程数是200,那么根据之前对detail,hesper等java应用系统的压测,200个app线程,会导致系统频繁的发生FullGC(原因是线程数量多,每个线程执行的时间长),此时app的实际QPS反而会比系统极限QPS低很多。
对于APP来说,此时应该是减少对cache的访问量,让少量的线程去试探是否恢复,而不是所有线程都堵塞在这里,原则上来说不管对什么资源的访问,都不能出现大量线程堵塞的情况。
最佳的做法是,限制对cache访问请求的线程数量。比如限制为10: 此时我们再来看一下上面的故障
此时APP的qps仍然为200(假设用户流量不会发生变化),那么可以计算得到app对CacheServer访问的qps变为3:QPS=1000ms/RT(平均响应时间) * threadNum(并行线程数)=1000/3000 * 10 = 3。由于APP只是堵塞了10个线程,超过10个请求后会直接return,并做好return之后相关逻辑处理。app本身并没有因为大量线程消耗大量的资源,app的状态依旧正常,只是cache不能命中。
接下去故障恢复
由于用户对APP的请求没有堆积,堵塞在访问cache的线程会因为超时设置,导致每隔3秒会有一个堆积请求退出,同时会放入新的线程,一旦cacheserver恢复,即便是1个线程理论上都可以提供1000的qps,所以系统会非常快速的恢复。
从结果上来看,“超时设置”策略是一种对每一个请求都会起作用的策略,而“并发请求阀值限制”策略是对一段时间持续发生系统变慢后才起作用。系统都正常的情况下超时设置会有一定的误杀,但是异常情况下由于超时的设置还是会导致一定线程的堵塞,所以“超时设置”加上“并发请求阀值设置”是一个很好的降级,流控策略,比单独使用一种有更好的效果。
另外阀值为什么是10,不是20,不是100,关于这个值到底是多少?可以通过计算得到
还是400QPS为例,对于客户端来说需要提供一个数据:你可以接受cache的响应时间是多少,平均响应时间为1ms,假设我们可以接受的是10ms:
QPS=1000ms/RT(平均响应时间) * threadNum(并行线程数)
threadNum = 400 * 10 / 1000 = 4
4个线程,没错是4个,不过设置4个线程阀值还是太小
如果是接受100ms:
threadNum = 400 * 100 / 1000 = 40
一般建议设置40个,40个线程阀值的效果等同于设置100ms超时时间。
另外,对于客户端APP来说可以接受堵塞多少线程?根据性能压测经验,一般平均50ms左右响应时间的java-APP,最好不要超过60,平均响应时间100ms的最好不要超过100。这个值是相对的,和系统有关系,特别是和系统单次请求所需内存开销,响应时间变化有关。
再谈一下系统的响应时间
正常情况RT这个值说应该是一个相对固定的值,因为代码的逻辑是一样的,干活的量也是一样的,好比一个卖票的窗口,卖一张票所需要的时间是固定的。那为什么有时候我们买票需要花更多的时间呢?原因是由于需求的QPS大于供给的QPS,我们被进入了排队,我们的时间消耗在了排队和资源争夺。而且一旦这个条件持续满足“需求的QPS大于供给的QPS”,响应时间会越来越慢,慢到无穷的大。
在系统做性能压测的情况下响应时间的曲线一般是这样的:
/
__________________/ x轴压测用户数,y轴响应时间
虽然响应时间变长,但是QPS不会变高,反而会下降,系统的极限QPS是由系统的瓶颈资源的极限QPS决定的,因为瓶颈资源的极限QPS几乎是一个固定的值,所以决定了系统只有一个最高QPS,听起来好像很奇怪,因为这个值并不会因为系统补充了其他资源而变的更高,有时候有人会认为我增加线程数量,是不是会增加这个最高QPS,答案肯定是不一定的。