目錄
- 如何給 tomcat 配置合適的線程池
- 如何監(jiān)控 tomcat 線程池的工作情況
- tomcat 線程池擴(kuò)縮容
- tomcat 是如何避免原生線程池的缺陷的
如何給 tomcat 配置合適的線程池
任務(wù)分為 CPU 密集型和 IO 密集型
對于 CPU 密集型的應(yīng)用來說,需要大量 CPU 計算速度很快,線程池如果過多,則保存和切換上下文開銷過高反而會影響性能,可以適當(dāng)將線程數(shù)量調(diào)小一些
對于 IO 密集型應(yīng)用來說常見于普通的業(yè)務(wù)系統(tǒng),比如會去查詢 mysql、redis 等然后在內(nèi)存中做簡單的數(shù)據(jù)組裝和邏輯判斷,此時由于有 epoll、dma 等支持,消耗較長時間的線程不會長時間占據(jù) CPU 時間,所以可以適當(dāng)將線程數(shù)量調(diào)大一些
對于線程數(shù)到底該設(shè)置多大,網(wǎng)上有很多的方案
我們在實(shí)際情況中基于 wrk 等壓測工具進(jìn)行壓測,壓測邏輯相對復(fù)雜請求量相對較高的接口先配置一個相對保守的值
先預(yù)估假設(shè)當(dāng)前接口處理平均耗時 300MS,1S 一個線程平均處理 3 個請求,最大 100 個線程 1S 能處理 300 TPS
粗略估算每個請求會消耗多大的內(nèi)容空間,假如是 2KB,那么每秒會消耗 600KB 以此來估算 YGC 多久會觸發(fā)一次,假設(shè)年輕代為 1GB 那么大約 29 分鐘觸發(fā)一次 YGC
然后再看 100 個線程持續(xù)處理 300TPS 的時候 CPU 消耗情況
觀察內(nèi)存幾分鐘 GC 一次,或者 CPU 使用率穩(wěn)定在 50% 左右就好,假設(shè)此時線程池 70 個線程都在運(yùn)作
那么監(jiān)控系統(tǒng)采集到線程池線程使用率達(dá)到了 80% 就開始擴(kuò)容,因?yàn)閿U(kuò)容拉起新的服務(wù)器到提供服務(wù)可能也需要1-2分鐘的時間,所以需要預(yù)留服務(wù)器資源能夠處理彈性擴(kuò)容期間的流量
實(shí)際場景中是相對比較復(fù)雜的,前期最好設(shè)置一個相對保守的擴(kuò)容閾值,如果發(fā)現(xiàn)在達(dá)到這個閾值前服務(wù)器已經(jīng)頂不住了就可以實(shí)時的縮小閾值
同時還可以根據(jù)在達(dá)到擴(kuò)容閾值擴(kuò)容的時候,觀察線上真實(shí)的 CPU 和內(nèi)存使用情況,可以基于 promethues + grafana 來進(jìn)行監(jiān)控,如果發(fā)現(xiàn)擴(kuò)容時候 CPU 使用率和內(nèi)存使用率 GC 評率比較低,那么可以再配置中心動態(tài)的調(diào)整線程池的大小
所以說與其尋找一個準(zhǔn)確的計算線程池數(shù)量的配置方式,不如提供一個可以動態(tài)調(diào)整的線程池作為 tomcat 的線程池
如何監(jiān)控 tomcat 線程池的工作情況
spring boot 在啟動過程中會調(diào)用 TomcatProtocolHandlerCustomizer 的實(shí)現(xiàn)類,此處可以自定化 tomcat 線程池,也可以獲取到 tomcat 線程池
public class MyTomcatProtocolHandlerCustomizer implements TomcatProtocolHandlerCustomizer<ProtocolHandler> { private final ThreadPoolExecutor tomcatThreadPoolExecutor; public TomcatProtocolHandlerCustomizer(ThreadPoolExecutor tomcatThreadPoolExecutor) { this.tomcatThreadPoolExecutor = tomcatThreadPoolExecutor; } @Override public void customize(ProtocolHandler protocolHandler) { protocolHandler.setExecutor(tomcatThreadPoolExecutor); } public ThreadPoolExecutor getThreadPoolExecutor() { return tomcatThreadPoolExecutor; } }
然后將線程池裝入一個容器 bean 中注冊到 promethues 監(jiān)控中,當(dāng)每秒進(jìn)行采集的時候通過回調(diào)的方法去獲取線程池的最新指標(biāo)
promethues 每秒采集一次容器的線程池運(yùn)行指標(biāo)、服務(wù)器測 CPU 使用率當(dāng)滿足定義的擴(kuò)容閾值時就拉起新的 POD
grafana 每秒采集一次 promethues 的數(shù)據(jù)匯聚圖標(biāo)展示
@Bean public AllServersThreadPoolCollector allServersThreadPoolCollector(@Qualifier(value = "GrpcThreadPoolExecutor") ThreadPoolExecutor GrpcThreadPoolExecutor, @Qualifier(value = "jdTomcatThreadPoolExecutor") org.apache.tomcat.util.threads.ThreadPoolExecutor TomcatThreadPoolExecutor, MeterRegistry registry) { return new AllServersThreadPoolCollector(GrpcThreadPoolExecutor, jdTomcatThreadPoolExecutor, registry); }
public void init() { try { createGauge(this, "grpc_tomcat_core_pool_size", pool -> this.getCorePoolSize()); createGauge(this, "grpc_tomcat_maximum_pool_size", pool -> this.getMaximumPoolSize()); createGauge(this, "grpc_tomcat_busy_pool_size", pool -> this.getActiveCount()); createGauge(this, "grpc_tomcat_wait_queue_size", pool -> this.getWaitQueueSize()); } catch (Exception e) { log.error("注冊 all servers 監(jiān)控失敗", e); } } private void createGauge(AllServersThreadPoolCollector weakRef, String metric, ToDoubleFunction<AllServersThreadPoolCollector> measure) { Gauge.builder(metric, weakRef, measure) .register(this.registry); } public int getWaitQueueSize() { return grpcThreadPoolExecutor.getQueue().size() + tomcatThreadPoolExecutor.getQueue().size(); }
tomcat 線程池擴(kuò)縮容
Java 線程池是支持直接修改 corePoolSize、maximumPoolSize 的
- 修改 corePoolSize
(1)數(shù)量小于 maximumPoolSize 拋異常
(2)如果 corePoolSize – 原來的 = delta,delta 大于 0 那么創(chuàng)建等待隊(duì)列任務(wù)數(shù)量和 delta 個線程來處理等待處理的任務(wù)
(3)如果正在運(yùn)行的線程數(shù)量 > corePoolSize 那么就中斷多余的線程
- 修改 maximumPoolSize
(1)maximumPoolSize 小于 corePoolSize 拋錯
(2)如果運(yùn)行的線程大于 maximumPoolSize 中斷掉一些空閑的線程
基于這些機(jī)制就能在運(yùn)行期間動態(tài)調(diào)整線程池內(nèi)容
無需擔(dān)心會中斷掉正在運(yùn)行的任務(wù),因?yàn)榫€程池 worker 線程每執(zhí)行一個任務(wù)的時候
tomcat 是如何避免原生線程池的缺陷的
原生線程池的工作原理
(1)運(yùn)行的線程數(shù)小于核心線程,就創(chuàng)建一個 worker 線程去執(zhí)行任務(wù)
(2)運(yùn)行的線程數(shù) >= 核心線程了,將任務(wù)全部積壓到隊(duì)列中
(3)隊(duì)列如果滿了繼續(xù)創(chuàng)建非核心線程 worker 去執(zhí)行任務(wù)
假如 tomcat 采用了原生線程池,核心線程為 10 個,最大線程為 100,隊(duì)列為 200,并發(fā)來了 100 個請求,那么同時系統(tǒng)只能處理 10 個,剩下 90 個都得放入隊(duì)列中讓 10 個核心線程慢慢排隊(duì)處理,延時必然非常高
tomcat 如何優(yōu)化線程池,核心在于阻塞隊(duì)列的實(shí)現(xiàn),因?yàn)樽枞?duì)列滿了才會繼續(xù)創(chuàng)建非核心 worker 線程處理任務(wù)
(1)運(yùn)行的線程數(shù)小于核心線程,就創(chuàng)建一個 worker 線程去執(zhí)行任務(wù)
(2)當(dāng)前已經(jīng)創(chuàng)建的核心+非核心線程數(shù)等于最大線程數(shù),任務(wù)壓入隊(duì)列
(3)正在處理的請求數(shù)量小于核心+非核心線程數(shù),任務(wù)壓入隊(duì)列
(4)當(dāng)前已經(jīng)創(chuàng)建的核心+非核心線程數(shù)小于最大線程數(shù),創(chuàng)建 worker 線程處理請求
總結(jié)就是一句話當(dāng)高并發(fā)流量過來的時候,會去創(chuàng)建最大線程數(shù)的 worker 去處理請求用以降低尾延遲,超過最大線程后,任務(wù)將被壓入隊(duì)列中進(jìn)行處理
以上就是tomcat 集群監(jiān)控與彈性伸縮詳解的詳細(xì)內(nèi)容,更多關(guān)于tomcat 集群監(jiān)控彈性伸縮的資料請關(guān)注其它相關(guān)文章!