弱依賴“并發請求數閥值”這個值設定多少合适?
“并發請求數閥值”在大部分情況下可以了解為同時工作的線程數閥值,這個值不是越大越好,也不是越小越好,而是在最高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,答案肯定是不一定的。