调整线程池的重要性
無(wú)論您是否知道,您的Java Web應(yīng)用程序很可能都使用線程池來(lái)處理傳入的請(qǐng)求。 這是許多人忽略的實(shí)現(xiàn)細(xì)節(jié),但是遲早您需要了解如何使用該池以及如何為您的應(yīng)用程序正確調(diào)整池。 本文旨在說(shuō)明線程模型,什么是線程池以及正確配置線程池所需執(zhí)行的操作。
單螺紋
讓我們從一些基礎(chǔ)知識(shí)開(kāi)始,并隨著線程模型的發(fā)展而前進(jìn)。 無(wú)論您使用哪種應(yīng)用程序服務(wù)器或框架, Tomcat , Dropwizard , Jetty ,它們都使用相同的基本方法。 一個(gè)深埋在Web服務(wù)器內(nèi)部的套接字。 該套接字正在偵聽(tīng)傳入的TCP連接,并接受它們。 一旦被接受,就可以從新建立的TCP連接中讀取數(shù)據(jù),進(jìn)行解析并將其轉(zhuǎn)換為HTTP請(qǐng)求。 然后將此請(qǐng)求移交給Web應(yīng)用程序,以完成其所需的操作。
為了理解線程的作用,我們將不使用應(yīng)用程序服務(wù)器,而是從頭開(kāi)始構(gòu)建一個(gè)簡(jiǎn)單的服務(wù)器。 該服務(wù)器反映了大多數(shù)應(yīng)用程序服務(wù)器的功能。 首先,單線程Web服務(wù)器可能看起來(lái)像這樣:
ServerSocket listener = new ServerSocket(8080); try {while (true) {Socket socket = listener.accept();try {handleRequest(socket);} catch (IOException e) {e.printStackTrace();}} } finally {listener.close(); }此代碼在端口8080上創(chuàng)建一個(gè)ServerSocket ,然后在一個(gè)緊密循環(huán)中,ServerSocket檢查是否接受新連接。 一旦接受,套接字將傳遞給handleRequest方法。 該方法通常將讀取HTTP請(qǐng)求,執(zhí)行所需的任何過(guò)程,然后編寫(xiě)響應(yīng)。 在此簡(jiǎn)單示例中,handleRequest讀取一行,并返回簡(jiǎn)短的HTTP響應(yīng)。 handleRequest做一些更復(fù)雜的事情是正常的,例如從數(shù)據(jù)庫(kù)中讀取或進(jìn)行某種其他類型的IO。
final static String response =“HTTP/1.0 200 OK\r\n” +“Content-type: text/plain\r\n” +“\r\n” +“Hello World\r\n”;public static void handleRequest(Socket socket) throws IOException {// Read the input stream, and return “200 OK”try {BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));log.info(in.readLine());OutputStream out = socket.getOutputStream();out.write(response.getBytes(StandardCharsets.UTF_8));} finally {socket.close();} }由于只有一個(gè)線程處理所有接受的套接字,因此在接受下一個(gè)請(qǐng)求之前,必須完全處理每個(gè)請(qǐng)求。 在實(shí)際的應(yīng)用程序中,等效的handleRequest方法返回大約100毫秒的時(shí)間可能是正常的。 如果是這種情況,服務(wù)器將被限制為每秒僅處理10個(gè)請(qǐng)求,一個(gè)接一個(gè)。
多線程
即使handleRequest可能在IO上被阻止,CPU也可以自由處理更多請(qǐng)求。 使用單線程方法是不可能的。 因此,可以通過(guò)創(chuàng)建多個(gè)線程來(lái)改進(jìn)此服務(wù)器以允許并發(fā)操作:
public static class HandleRequestRunnable implements Runnable {final Socket socket;public HandleRequestRunnable(Socket socket) {this.socket = socket;}public void run() {try {handleRequest(socket);} catch (IOException e) {e.printStackTrace();}} }ServerSocket listener = new ServerSocket(8080); try {while (true) {Socket socket = listener.accept();new Thread(new HandleRequestRunnable(socket)).start();} } finally {listener.close(); }在這里,仍然在單個(gè)線程內(nèi)的緊密循環(huán)中調(diào)用accept(),但是一旦接受TCP連接并且有可用的套接字,就會(huì)產(chǎn)生一個(gè)新線程。 這個(gè)產(chǎn)生的線程執(zhí)行一個(gè)HandleRequestRunnable,它從上面簡(jiǎn)單地調(diào)用相同的handleRequest方法。
創(chuàng)建新線程后,現(xiàn)在可以釋放原始的accept()線程來(lái)處理更多的TCP連接,并允許應(yīng)用程序同時(shí)處理請(qǐng)求。 該技術(shù)被稱為“每個(gè)請(qǐng)求線程”,是最流行的方法。 值得注意的是,還有其他方法,例如事件驅(qū)動(dòng)的異步模型NGINX和Node.js部署,但是它們不使用線程池,因此不在本文討論范圍之內(nèi)。
在“每個(gè)請(qǐng)求的線程”方法中,創(chuàng)建新線程(然后銷毀它)可能會(huì)很昂貴,因?yàn)镴VM和OS都需要分配資源。 另外,在上述實(shí)現(xiàn)中,正在創(chuàng)建的線程數(shù)不受限制。 不受限制是很成問(wèn)題的,因?yàn)樗鼤?huì)很快導(dǎo)致資源枯竭。
資源枯竭
每個(gè)線程都需要一定數(shù)量的內(nèi)存用于堆棧。 在最新的64位JVM上, 默認(rèn)堆棧大小為1024KB。 如果服務(wù)器收到大量請(qǐng)求,或者h(yuǎn)andleRequest方法變慢,則服務(wù)器可能會(huì)出現(xiàn)大量并發(fā)線程。 因此,要管理1000個(gè)并發(fā)請(qǐng)求,僅用于線程堆棧的1000個(gè)線程將消耗1GB的JVM RAM。 另外,在每個(gè)線程中執(zhí)行的代碼將在處理請(qǐng)求所需的堆上創(chuàng)建對(duì)象。 這非常Swift地加起來(lái),并且可能超過(guò)分配給JVM的堆空間,從而對(duì)垃圾收集器施加壓力,從而導(dǎo)致崩潰并最終導(dǎo)致OutOfMemoryErrors 。
線程不僅消耗RAM,而且可能使用其他有限資源,例如文件句柄或數(shù)據(jù)庫(kù)連接。 超過(guò)這些可能導(dǎo)致其他類型的錯(cuò)誤或崩潰。 因此,為了避免耗盡資源,重要的是避免無(wú)限制的數(shù)據(jù)結(jié)構(gòu)。
不是靈丹妙藥,但是可以通過(guò)使用-Xss標(biāo)志調(diào)整堆棧大小來(lái)緩解堆棧大小問(wèn)題。 較小的堆棧將減少每個(gè)線程的開(kāi)銷,但可能導(dǎo)致StackOverflowErrors 。 您的里程會(huì)有所不同,但是對(duì)于許多應(yīng)用程序,默認(rèn)的1024KB過(guò)多,因此較小的256KB或512KB值可能更合適。 Java允許的最小值是16KB。
線程池
為了避免連續(xù)創(chuàng)建新線程并限制最大數(shù)量,可以使用簡(jiǎn)單的線程池。 簡(jiǎn)而言之,該池跟蹤所有線程,在需要達(dá)到上限時(shí)創(chuàng)建新線程,并在可能的情況下重用空閑線程。
ServerSocket listener = new ServerSocket(8080); ExecutorService executor = Executors.newFixedThreadPool(4); try {while (true) {Socket socket = listener.accept();executor.submit( new HandleRequestRunnable(socket) );} } finally {listener.close(); }現(xiàn)在,此代碼不是直接創(chuàng)建線程,而是使用ExecutorService,該服務(wù)提交要在線程池中執(zhí)行的工作(用Runnables術(shù)語(yǔ))。 在此示例中,四個(gè)線程的固定線程池用于處理所有傳入的請(qǐng)求。 這限制了“進(jìn)行中”請(qǐng)求的數(shù)量,因此限制了資源的使用。
除了newFixedThreadPool之外 ,Executors實(shí)用程序類還提供了newCachedThreadPool方法。 這受到較早的無(wú)限線程數(shù)量的困擾,但是只要有可能,就利用先前創(chuàng)建但現(xiàn)在空閑的線程。 通常,這種類型的池對(duì)于不會(huì)阻塞外部資源的短暫請(qǐng)求很有用。
ThreadPoolExecutors可以直接構(gòu)造,從而可以自定義其行為。 例如,可以定義池中線程的最小和最大數(shù)量,以及何時(shí)創(chuàng)建和銷毀線程的策略。 簡(jiǎn)短的示例。
工作隊(duì)列
在固定線程池的情況下,細(xì)心的讀者可能想知道如果所有線程都忙,并且有新請(qǐng)求進(jìn)入,會(huì)發(fā)生什么情況。那么ThreadPoolExecutor使用隊(duì)列來(lái)容納線程可用之前的待處理請(qǐng)求。 默認(rèn)情況下,Executors.newFixedThreadPool和Executors.newCachedThreadPool都使用無(wú)界LinkedList。 再次,這會(huì)導(dǎo)致資源耗盡問(wèn)題,盡管速度要慢得多,因?yàn)槊總€(gè)排隊(duì)的請(qǐng)求都小于完整線程,并且通常不會(huì)使用那么多資源。 但是,在我們的示例中,每個(gè)排隊(duì)的請(qǐng)求都持有一個(gè)套接字(取決于OS)將占用一個(gè)文件句柄。 這是操作系統(tǒng)將限制的資源類型,因此除非有必要,否則最好不要保留它。 因此,限制工作隊(duì)列的大小也很有意義。
public static ExecutorService newBoundedFixedThreadPool(int nThreads, int capacity) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(capacity),new ThreadPoolExecutor.DiscardPolicy()); }public static void boundedThreadPoolServerSocket() throws IOException {ServerSocket listener = new ServerSocket(8080);ExecutorService executor = newBoundedFixedThreadPool(4, 16);try {while (true) {Socket socket = listener.accept();executor.submit( new HandleRequestRunnable(socket) );}} finally {listener.close();} }同樣,我們創(chuàng)建了一個(gè)線程池,但是我們沒(méi)有使用Executors.newFixedThreadPool幫助器方法,而是自己創(chuàng)建了ThreadPoolExecutor,并傳遞了一個(gè)限制為16個(gè)元素的有界LinkedBlockingQueue 。 或者,可以使用ArrayBlockingQueue ,它是有界緩沖區(qū)的實(shí)現(xiàn)。
如果所有線程都忙,并且隊(duì)列已滿,則下一步將由ThreadPoolExecutor的最后一個(gè)參數(shù)定義。 在此示例中,使用了DiscardPolicy ,它僅丟棄將使隊(duì)列溢出的所有工作。 還有其他政策,如AbortPolicy它拋出一個(gè)異常,或CallerRunsPolicy執(zhí)行該調(diào)用者的線程上的工作。 該CallerRunsPolicy提供了一種簡(jiǎn)單的方法來(lái)自我限制可以添加作業(yè)的速率,但是,這可能是有害的,阻塞了一個(gè)應(yīng)保持暢通的線程。
一個(gè)好的默認(rèn)策略是“放棄”或“中止”,這兩者都會(huì)放棄工作。 在這些情況下,很容易向客戶端返回一個(gè)簡(jiǎn)單的錯(cuò)誤,例如HTTP 503“服務(wù)不可用” 。 有人會(huì)爭(zhēng)辯說(shuō)只是增加隊(duì)列大小,然后所有工作最終都會(huì)運(yùn)行。 但是,用戶不愿永遠(yuǎn)等待,如果從根本上說(shuō)工作的執(zhí)行速度超過(guò)了可以執(zhí)行的速度,那么隊(duì)列將無(wú)限期地增長(zhǎng)。 相反,該隊(duì)列僅應(yīng)用于消除突發(fā)請(qǐng)求,或處理處理中的短暫停頓。 在正常操作中,隊(duì)列應(yīng)為空。
有多少個(gè)線程?
現(xiàn)在我們了解了如何創(chuàng)建線程池,困難的問(wèn)題是應(yīng)該有多少個(gè)線程可用? 我們確定最大數(shù)量應(yīng)該限制為不導(dǎo)致資源耗盡。 這包括所有類型的資源,內(nèi)存(堆棧和堆),打開(kāi)的文件句柄,打開(kāi)的TCP連接,遠(yuǎn)程數(shù)據(jù)庫(kù)可以處理的連接數(shù)以及任何其他有限資源。 相反,如果線程是CPU綁定的,而不是IO綁定的,則應(yīng)將物理核心的數(shù)量視為有限,并且每個(gè)核心最多只能創(chuàng)建一個(gè)線程。
這一切都取決于應(yīng)用程序正在做的工作。 用戶應(yīng)使用各種池大小和實(shí)際的請(qǐng)求混合來(lái)運(yùn)行負(fù)載測(cè)試。 每次增加它們的線程池大小直到斷點(diǎn)。 這樣就可以找到資源耗盡時(shí)的上限。 在某些情況下,明智的做法是增加可用資源的數(shù)量,例如為JVM提供更多的RAM,或調(diào)整OS以允許更多的文件句柄。 但是,在某個(gè)時(shí)候?qū)⑦_(dá)到理論上限,應(yīng)該注意,但這還不是故事的結(jié)局。
利特爾定律
排隊(duì)論,尤其是利特爾定律 ,可以用來(lái)幫助理解線程池的屬性。 簡(jiǎn)單來(lái)說(shuō),利特爾定律描述了三個(gè)變量之間的關(guān)系。 L進(jìn)行中的請(qǐng)求數(shù)量,λ新請(qǐng)求到達(dá)的速率,W平均處理請(qǐng)求的時(shí)間。 例如,如果每秒有10個(gè)請(qǐng)求到達(dá),并且每個(gè)請(qǐng)求花費(fèi)一秒鐘的時(shí)間來(lái)處理,則在任何時(shí)間平均有10個(gè)正在進(jìn)行的請(qǐng)求。 在我們的示例中,這映射為使用10個(gè)線程。 如果處理單個(gè)請(qǐng)求的時(shí)間增加了一倍,則運(yùn)行中的平均請(qǐng)求數(shù)也將增加一倍,達(dá)到20,因此需要20個(gè)線程。
了解執(zhí)行時(shí)間對(duì)進(jìn)行中的請(qǐng)求的影響非常重要。 某些后端資源(例如數(shù)據(jù)庫(kù))停頓是很常見(jiàn)的,導(dǎo)致請(qǐng)求花費(fèi)更長(zhǎng)的時(shí)間來(lái)處理,從而很快耗盡了線程池。 因此,理論上限可能不是池大小的適當(dāng)限制。 相反,應(yīng)該對(duì)執(zhí)行時(shí)間設(shè)置一個(gè)限制,并與理論上限結(jié)合使用。
例如,假設(shè)在JVM超過(guò)其內(nèi)存分配之前,可以處理的最大傳輸中請(qǐng)求為1000。 如果我們預(yù)算每個(gè)請(qǐng)求的時(shí)間不超過(guò)30秒,那么在最壞的情況下,我們應(yīng)該期望每秒處理不超過(guò)33個(gè)請(qǐng)求。 但是,如果一切正常,并且請(qǐng)求僅用500毫秒即可處理,則應(yīng)用程序每秒只能在1000個(gè)線程上處理2000個(gè)請(qǐng)求。 指定可以使用隊(duì)列來(lái)消除短暫的延遲突發(fā)也可能是合理的。
為什么要麻煩?
如果線程池中的線程太少,則存在以下風(fēng)險(xiǎn):資源利用不足,不必要地將用戶拒之門(mén)外。 但是,如果允許太多線程,則會(huì)發(fā)生資源耗盡,這可能會(huì)造成更大的破壞。
不僅會(huì)耗盡本地資源,還可能對(duì)其他資源產(chǎn)生不利影響。 例如,多個(gè)應(yīng)用程序查詢同一個(gè)后端數(shù)據(jù)庫(kù)。 數(shù)據(jù)庫(kù)通常對(duì)并發(fā)連接數(shù)有硬性限制。 如果一個(gè)行為異常的應(yīng)用程序消耗了所有這些連接,它將阻止其他應(yīng)用程序訪問(wèn)數(shù)據(jù)庫(kù)。 造成大范圍的中斷。
更糟糕的是,可能會(huì)發(fā)生級(jí)聯(lián)故障。 想象一下一個(gè)環(huán)境,其中有一個(gè)應(yīng)用程序的多個(gè)實(shí)例,位于一個(gè)公共負(fù)載均衡器的后面。 如果實(shí)例之一由于正在進(jìn)行的請(qǐng)求過(guò)多而開(kāi)始用盡內(nèi)存,那么JVM將花費(fèi)更多時(shí)間進(jìn)行垃圾收集,并減少處理請(qǐng)求的時(shí)間。 這種減慢速度將降低該實(shí)例的容量,并迫使其他實(shí)例處理更高比例的傳入請(qǐng)求。 隨著他們現(xiàn)在使用其無(wú)限制線程池處理更多請(qǐng)求,會(huì)發(fā)生相同的問(wèn)題。 它們耗盡了內(nèi)存,然后再次開(kāi)始積極地進(jìn)行垃圾回收。 這個(gè)惡性循環(huán)在所有實(shí)例之間級(jí)聯(lián),直到出現(xiàn)系統(tǒng)性故障。
我經(jīng)常觀察到?jīng)]有進(jìn)行負(fù)載測(cè)試,并且允許任意數(shù)量的線程。 在通常情況下,應(yīng)用程序可以使用少量線程以傳入速率愉快地處理請(qǐng)求。 但是,如果處理請(qǐng)求取決于遠(yuǎn)程服務(wù),并且該服務(wù)暫時(shí)變慢,則W的增加(平均處理時(shí)間)的影響會(huì)很快耗盡池。 由于從未對(duì)應(yīng)用程序進(jìn)行最大數(shù)量的負(fù)載測(cè)試,因此會(huì)出現(xiàn)之前概述的所有資源耗盡問(wèn)題。
有多少個(gè)線程池?
在微 服務(wù)或面向服務(wù)的體系結(jié)構(gòu) (SOA)中,訪問(wèn)多個(gè)遠(yuǎn)程后端服務(wù)是正常的。 此設(shè)置特別容易發(fā)生故障,因此應(yīng)仔細(xì)解決這些問(wèn)題。 如果遠(yuǎn)程服務(wù)的性能下降,則可能導(dǎo)致線程池快速達(dá)到其極限,從而丟棄后續(xù)請(qǐng)求。 但是,并非所有請(qǐng)求都可能需要此不正常的后端,但是由于線程池已滿,因此不必要地刪除了這些請(qǐng)求。
通過(guò)提供特定于后端的線程池,可以隔離每個(gè)后端的故障。 在這種模式下,仍然只有一個(gè)請(qǐng)求工作程序池,但是如果請(qǐng)求需要調(diào)用遠(yuǎn)程服務(wù),則工作將轉(zhuǎn)移到該后端的線程池。 這使主請(qǐng)求池不會(huì)受到單個(gè)緩慢后端的負(fù)擔(dān)。 這樣,只有需要特定后端池的請(qǐng)求才會(huì)在故障時(shí)受到影響。
多個(gè)線程池的最后一個(gè)好處是,它有助于避免某種形式的死鎖。 如果由于尚未處理的請(qǐng)求而導(dǎo)致每個(gè)可用線程都被阻塞,則將發(fā)生死鎖,并且沒(méi)有線程可以前進(jìn)。 當(dāng)使用多個(gè)池并充分了解它們執(zhí)行的工作時(shí),可以在某種程度上緩解此問(wèn)題。
截止日期和其他最佳做法
常見(jiàn)的最佳做法是確保所有遠(yuǎn)程呼叫都有最后期限。 也就是說(shuō),如果遠(yuǎn)程服務(wù)在合理時(shí)間內(nèi)沒(méi)有響應(yīng),則該請(qǐng)求將被放棄。 可以在線程池中使用相同的技術(shù)。 具體來(lái)說(shuō),如果線程正在處理一個(gè)請(qǐng)求的時(shí)間超過(guò)了定義的期限,則應(yīng)終止該線程。 為新請(qǐng)求騰出空間,并在W上設(shè)置上限。這似乎是一種浪費(fèi),但是如果用戶(通常是Web瀏覽器)正在等待響應(yīng),則30秒后,瀏覽器可能只會(huì)給出無(wú)論如何,還是用戶可能會(huì)變得急躁并離開(kāi)。
快速失敗是在為后端創(chuàng)建池時(shí)可以采用的另一種方法。 如果后端發(fā)生故障,則線程池將Swift填充等待連接到無(wú)響應(yīng)后端的請(qǐng)求。 相反,可以將后端標(biāo)記為不正常,所有后續(xù)請(qǐng)求都可能立即失敗,而不是不必要地等待。 但是請(qǐng)注意,需要一種機(jī)制來(lái)確定后端何時(shí)再次恢復(fù)健康。
最后,如果一個(gè)請(qǐng)求需要獨(dú)立地調(diào)用多個(gè)后端,則應(yīng)該可以并行而不是順序地調(diào)用它們。 這將減少等待時(shí)間,但以增加線程為代價(jià)。
幸運(yùn)的是,有一個(gè)很棒的庫(kù)hystrix ,它打包了許多這些最佳實(shí)踐,并以簡(jiǎn)單安全的方式公開(kāi)了它們。
結(jié)論
希望本文能增進(jìn)您對(duì)線程池的了解。 通過(guò)了解應(yīng)用程序的需求,并結(jié)合使用最大線程數(shù)和平均響應(yīng)時(shí)間,可以確定適當(dāng)?shù)木€程池。 這不僅可以避免級(jí)聯(lián)故障,而且可以幫助計(jì)劃和配置您的服務(wù)。
即使您的應(yīng)用程序可能未顯式使用線程池,但它們還是被應(yīng)用程序服務(wù)器或更高級(jí)別的抽象隱式使用。 Tomcat , JBoss , Undertow , Dropwizard都為其線程池(執(zhí)行servlet的池)提供了多個(gè)可調(diào)參數(shù)。
翻譯自: https://www.javacodegeeks.com/2015/12/importance-tuning-thread-pools.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
- 上一篇: 什么是逆锋起笔 逆锋起笔的意思是什么
- 下一篇: adf时间作用域_ADF任务流:页面片段