前言
在我們日常業務開發過程中,或多或少都會用到并發的功能。那么在用到并發功能的過程中,就肯定會碰到下面這個問題
并發線程池到底設置多大呢?
通常有點年紀的程序員或許都聽說這樣一個說法 (其中 N 代表 CPU 的個數)
CPU 密集型應用,線程池大小設置為 N + 1
IO 密集型應用,線程池大小設置為 2N
這個說法到底是不是正確的呢?
其實這是極不正確的。那為什么呢?
首先我們從反面來看,假設這個說法是成立的,那我們在一臺服務器上部署多少個服務都無所謂了。因為線程池的大小只能服務器的核數有關,所以這個說法是不正確的。那具體應該怎么設置大小呢?
假設這個應用是兩者混合型的,其中任務即有 CPU 密集,也有 IO 密集型的,那么我們改怎么設置呢?是不是只能拋硬盤來決定呢?
那么我們到底該怎么設置線程池大小呢?有沒有一些具體實踐方法來指導大家落地呢?讓我們來深入地了解一下。
Little's Law(利特爾法則)
一個系統請求數等于請求的到達率與平均每個單獨請求花費的時間之乘積
假設服務器單核的,對應業務需要保證請求量(QPS):10 ,真正處理一個請求需要 1 秒,那么服務器每個時刻都有 10 個請求在處理,即需要 10 個線程
同樣,我們可以使用利特爾法則(Little’s law)來判定線程池大小。我們只需計算請求到達率和請求處理的平均時間。然后,將上述值放到利特爾法則(Little’s law)就可以算出系統平均請求數。估算公式如下
*線程池大小 = ((線程 IO time + 線程 CPU time )/線程 CPU time ) CPU數目**
具體實踐
通過公式,我們了解到需要 3 個具體數值
一個請求所消耗的時間 (線程 IO time + 線程 CPU time)
該請求計算時間 (線程 CPU time)
CPU 數目
請求消耗時間
Web 服務容器中,可以通過 Filter 來攔截獲取該請求前后消耗的時間
public?class?MoniterFilter?implements?Filter?{ private?static?final?Logger?logger?=?LoggerFactory.getLogger(MoniterFilter.class); @Override public?void?doFilter(ServletRequest?request,?ServletResponse?response,?FilterChain?chain)?throws?IOException, ????????????ServletException?{ long?start?=?System.currentTimeMillis(); ????????HttpServletRequest?httpRequest?=?(HttpServletRequest)?request; ????????HttpServletResponse?httpResponse?=?(HttpServletResponse)?response; ????????String?uri?=?httpRequest.getRequestURI(); ????????String?params?=?getQueryString(httpRequest); try?{ ????????????chain.doFilter(httpRequest,?httpResponse); ????????}?finally?{ long?cost?=?System.currentTimeMillis()?-?start; ????????????logger.info("access?url?[{}{}],?cost?time?[{}]?ms?)",?uri,?params,?cost); ????????} private?String?getQueryString(HttpServletRequest?req)?{ ????????StringBuilder?buffer?=?new?StringBuilder("?"); ????????Enumeration<String>?emParams?=?req.getParameterNames(); try?{ while?(emParams.hasMoreElements())?{ ????????????????String?sParam?=?emParams.nextElement(); ????????????????String?sValues?=?req.getParameter(sParam); ????????????????buffer.append(sParam).append("=").append(sValues).append("&"); ????????????} return?buffer.substring(0,?buffer.length()?-?1); ????????}?catch?(Exception?e)?{ ????????????logger.error("get?post?arguments?error",?buffer.toString()); ????????} return?""; ????} }
CPU 計算時間
CPU 計算時間 = 請求總耗時 - CPU IO time
假設該請求有一個查詢 DB 的操作,只要知道這個查詢 DB 的耗時(CPU IO time),計算的時間不就出來了嘛,我們看一下怎么才能簡潔,明了的記錄 DB 查詢的耗時。
通過(JDK 動態代理/ CGLIB)的方式添加 AOP 切面,來獲取線程 IO 耗時。代碼如下,請參考:
public?class?DaoInterceptor?implements?MethodInterceptor?{ private?static?final?Logger?logger?=?LoggerFactory.getLogger(DaoInterceptor.class); @Override public?Object?invoke(MethodInvocation?invocation)?throws?Throwable?{ ????????StopWatch?watch?=?new?StopWatch(); ????????watch.start(); ????????Object?result?=?null; ????????Throwable?t?=?null; try?{ ????????????result?=?invocation.proceed(); ????????}?catch?(Throwable?e)?{ ????????????t?=?e?==?null???null?:?e.getCause(); throw?e; ????????}?finally?{ ????????????watch.stop(); ????????????logger.info("({}ms)",?watch.getTotalTimeMillis()); ????????} return?result; ????} }
CPU 數目
邏輯 CPU 個數 ,設置線程池大小的時候參考的 CPU 個數
cat?/proc/cpuinfo|?grep?"processor"|?wc?-l
總結
合適的配置線程池大小其實很不容易,但是通過上述的公式和具體代碼,我們就能快速、落地的算出這個線程池該設置的多大。
不過最后的最后,我們還是需要通過壓力測試來進行微調,只有經過壓測測試的檢驗,我們才能最終保證的配置大小是準確的。
歡迎大家關注我的公種浩【程序員追風】,文章都會在里面更新,整理的資料也會放在里面。
最后
歡迎大家一起交流,喜歡文章記得點個贊喲,感謝支持!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。