本篇為elasticsearch原始碼分析系列文章的第十篇,本篇延續上一篇ElasticSearch的Plugin引出的內容,進行各種Plugin中執行緒池的分析。
上篇講到了ElasticSearch中外掛的基本概念,以及Node例項化中涉及到的PluginService初始化編碼,本篇將會繼續研究Node例項化的過程中PluginsService發揮的作用,也就是通過PluginsService中的引數構建執行緒池框架。
執行緒池在何時初始化
當Node完成了PluginsService的構造後,緊接會通過getExecutorBuilders
方法取得執行緒池的Executor構造器列表,程式碼如下:
List<ExecutorBuilder<?>> executorBuilders = pluginsService.getExecutorBuilders(settings)
複製程式碼
此時PluginsService物件中已經有了需要載入的所有plugin了,包含modules
路徑和plugins
路徑中的所有元件,這裡統稱為plugin。如下圖所示總共是包含了13個已載入的Plugin,分別是modules路徑中的預設必須載入的12個和Plugins路徑中的自定義安裝的1個(ICU分詞器)。如下圖所示
構建執行緒池框架
初始化ExecutorBuilder集合
Node例項化過程中,通過程式碼:
List<ExecutorBuilder<?>> executorBuilders = pluginsService.getExecutorBuilders(settings);
複製程式碼
查詢到自定義的執行緒池Executor構建器。再獲得自定義執行緒池構建器集合後,開始構建執行緒池(ThreadPool)。
ThreadPool threadPool = new ThreadPool(settings, executorBuilders.toArray(new ExecutorBuilder[0]));
複製程式碼
首先通過程式碼獲得處理器CPU的數量,
Runtime.getRuntime().availableProcessors()
複製程式碼
當然這個值是可以被Setting中設定的變數processors來覆蓋的。這個變數在程式碼中被標記為availableProcessors。然後建立變數
- halfProcMaxAt5,這個變數的意思是availableProcessors的一半,但最大不超過5。
- halfProcMaxAt10,這個變數的意思是availableProcessors的一半,但最大不超過10。
這兩個變數在後面建立各種執行緒池構造器中反覆用到。
在確定了可使用的處理器數量後,就能確定執行緒池的最小值(genericThreadPoolMax),ElasticSearch中是確定為:可用CPU處理器數量的4倍,且固定範圍為最小128,最大為512。
由此可見如果用一般伺服器的話,執行緒池上限最終會被確定為128,可以說還是比較高的設定了。
接下來開始構造執行不同操作時執行緒池Executor,ElasticSearch中把各個操作的Executor構造為Map,Map<String, ExecutorBuilder>
,下面是各個Executor物件的解釋:
-
普通操作的Executor:構建一個可伸縮的Executor構建器,value為ScalingExecutorBuilder物件。接收引數和對應操作如下:
- name:執行緒池執行者的名稱,也就是generic。
- core:執行緒池中執行緒的最小值,固定為4。將
thread_pool.generic.core
的設為這個值。 - max:執行緒池中執行緒的最大值,對應上面提到的genericThreadPoolMax,在本機跑的結果是128
- keepAlive:超過4個執行緒後,執行緒保持活躍的時間。這個值固定為30秒。這個引數被設定為變數
thread_pool.generic.keep_alive
-
索引操作的Executor:構建一個固定的Executor構建器。key為
index
,value為FixedExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.index.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是idnex。
- size:執行緒的固定大小,和引數name一起構造配置變數
thread_pool.index.size
的值為size的值,本機跑的結果是4。 - queueSize:阻塞佇列的大小,構造配置變數
thread_pool.index.queue_size
的值為200,注意這個值固定為200。
- settings:Node的配置settings。設定配置變數
-
批處理操作的Executor:構建一個固定的Executor構建器。key為
bulk
,value為FixedExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.bulk.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是bulk。
- size:執行緒的固定大小,和引數name一起構造配置變數
thread_pool.bulk.size
的值為size的值,本機跑的結果是4。 - queueSize:阻塞佇列的大小,構造配置變數
thread_pool.bulk.queue_size
的值為200,注意這個值固定為200。
- settings:Node的配置settings。設定配置變數
-
get操作的Executor:構建一個固定的Executor構建器。key為
get
,value為FixedExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.get.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是get。
- size:執行緒的固定大小,和引數name一起構造配置變數
thread_pool.get.size
的值為size的值,本機跑的結果是4。 - queueSize:阻塞佇列的大小,構造配置變數
thread_pool.get.queue_size
的值為1000,注意這個值固定為1000。
- settings:Node的配置settings。設定配置變數
-
查詢操作的Executor:構建一個根據利特爾法則自動擴充套件長度的Executor構建器,這個構建器的邏輯與其他構建器不同,也顯得比較複雜,也說明了對於查詢操作,ElasticSearch做了特殊的優化。key為
search
,value為AutoQueueAdjustingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.search.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是search。
- size:執行緒的固定大小,和引數name一起構造配置變數
thread_pool.search.size
的值為size的值,本機跑的結果是7。 - initialQueueSize:初始化佇列的大小,固定設定為1000,造配置變數
thread_pool.search.queue_size
的值為200 - minQueueSize:佇列的最小長度,固定設定為1000設定配置變數
thread_pool.search.min_queue_size
的值為1000 - maxQueueSize:佇列的最大長度,固定設定為1000,設定配置變數
thread_pool.search.max_queue_size
的值為1000 - frameSize:佇列的步進長度,固定設定為2000,構造配置變數
thread_pool.search.auto_queue_frame_size
的值為200,注意這個值固定為200。 thread_pool.search.target_response_time
針對search操作的相應被設定為1S,
- settings:Node的配置settings。設定配置變數
-
管理操作的Executor:構建一個可伸縮的Executor構建器。key為
management
,value為ScalingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.management.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是management,
- size:執行緒的固定大小,和引數name一起構造配置變數
thread_pool.management.size
的值為size的值,本機跑的結果是1。 - queueSize:阻塞佇列的大小,構造配置變數
thread_pool.management.queue_size
的值為200,注意這個值固定為200。 - keepAlive:超過1個執行緒後,執行緒保持活躍的時間。這個值固定為5分鐘。這個引數被設定為變數
thread_pool.management.keep_alive
。
- settings:Node的配置settings。設定配置變數
-
監聽操作的Executor:構建一個固定的Executor構建器。key為
listener
,value為FixedExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.listener.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是listener,
- size:執行緒的固定大小,上文提到的halfProcMaxAt10,和引數name一起構造配置變數
thread_pool.listener.size
的值為size的值,本機跑的結果是2。 - queueSize:阻塞佇列的大小,構造配置變數
thread_pool.listener.queue_size
的值為**-1**,意思就沒有阻塞佇列。
- settings:Node的配置settings。設定配置變數
-
flush操作的Executor:構建一個可伸縮的Executor構建器。key為
flush
,value為ScalingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.flush.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是flush,
- size:執行緒的固定大小,上文提到的halfProcMaxAt5,和引數name一起構造配置變數
thread_pool.flush.size
的值為size的值,本機跑的結果是4。 - keepAlive:超過1個執行緒後,執行緒保持活躍的時間。這個值固定為5分鐘。這個引數被設定為變數
thread_pool.management.keep_alive
。
- settings:Node的配置settings。設定配置變數
-
refresh操作的Executor:構建一個可伸縮的Executor構建器。key為
refresh
,value為ScalingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.refresh.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是refresh,
- size:執行緒的固定大小,上文提到的halfProcMaxAt10,和引數name一起構造配置變數
thread_pool.refresh.size
的值為size的值,本機跑的結果是4。 - keepAlive:超過1個執行緒後,執行緒保持活躍的時間。這個值固定為5分鐘。這個引數被設定為變數
thread_pool.management.keep_alive
。
- settings:Node的配置settings。設定配置變數
-
warmer操作的Executor:構建一個可伸縮的Executor構建器。key為
warmer
,value為ScalingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.warmer.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是warmer,
- size:執行緒的固定大小,上文提到的halfProcMaxAt5,和引數name一起構造配置變數
thread_pool.warmer.size
的值為size的值,本機跑的結果是4。 - keepAlive:超過1個執行緒後,執行緒保持活躍的時間。這個值固定為5分鐘。這個引數被設定為變數
thread_pool.management.keep_alive
。
- settings:Node的配置settings。設定配置變數
-
snapshot操作的Executor:構建一個可伸縮的Executor構建器。key為
snapshot
,value為ScalingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.snapshot.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是snapshot,
- size:執行緒的固定大小,上文提到的halfProcMaxAt5,和引數name一起構造配置變數
thread_pool.snapshot.size
的值為size的值,本機跑的結果是4。 - keepAlive:超過1個執行緒後,執行緒保持活躍的時間。這個值固定為5分鐘。這個引數被設定為變數
thread_pool.management.keep_alive
。
- settings:Node的配置settings。設定配置變數
-
碎片處理操作的Executor:構建一個可伸縮的Executor構建器。key為
fetch_shard_started
,value為ScalingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.fetch_shard_started.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是fetch_shard_started,
- size:執行緒的固定大小,和引數name一起構造配置變數
thread_pool.fetch_shard_started.size
的值為size的值,本機跑的結果是4。 - queueSize:阻塞佇列的大小,構造配置變數
thread_pool.fetch_shard_started.queue_size
的值為200,注意這個值固定為200。
- settings:Node的配置settings。設定配置變數
-
強制merge操作的Executor:構建一個可伸縮的Executor構建器。key為
force_merge
,value為ScalingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.force_merge.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是force_merge,
- size:執行緒的固定大小,和引數name一起構造配置變數
thread_pool.force_merge.size
的值為size的值,本機跑的結果是4。 - queueSize:阻塞佇列的大小,構造配置變數
thread_pool.force_merge.queue_size
的值為200,注意這個值固定為200。
- settings:Node的配置settings。設定配置變數
-
獲取碎片操作的Executor:構建一個可伸縮的Executor構建器。key為
fetch_shard_store
,value為ScalingExecutorBuilder物件,接收引數和對應操作如下:- settings:Node的配置settings。設定配置變數
thread_pool.fetch_shard_store.size
的值為該引數中cpu的數量 - name:執行緒池執行者的名稱,也就是fetch_shard_store,
- size:執行緒的固定大小,和引數name一起構造配置變數
thread_pool.fetch_shard_store.size
的值為size的值,本機跑的結果是4。 - queueSize:阻塞佇列的大小,構造配置變數
thread_pool.fetch_shard_store.queue_size
的值為200,注意這個值固定為200。
- settings:Node的配置settings。設定配置變數
至此就完成了org.elasticsearch.threadpool.ThreadPool物件的建立。
ThreadPool物件的作用
得到ThreadPool的物件後,通過執行緒池進行了NodeClient的構建。
client = new NodeClient(settings, threadPool);
複製程式碼
和ResourceWatcherService物件的構建,
final ResourceWatcherService resourceWatcherService = new ResourceWatcherService(settings, threadPool);
複製程式碼
後面還有很多的元件都用到了執行緒池,比如:
- IngestService
- ClusterInfoService
- MonitorService
- ActionModule
- IndicesService
- NetworkModule
- TransportService
- DiscoveryModule
- NodeService
可以看出都是ElasticSearch的核心元件,這些元件的功能和原理,我都會在以後的文章中講解,而像ElasticSearch這種儲存搜尋系統來說IO操作肯定非常頻繁,而執行緒池是專門致力於解決系統的IO問題,它在這些服務元件中的作用也顯得愈發重要。
利特爾法則
查詢操作中提到的利特爾法則是一種描述穩定系統中,三個變數之間關係的法則。
其中L表示平均請求數量,λ表示請求的頻率,W表示響應請求的平均時間。舉例來說,如果每秒請求數為10次,每個請求處理時間為1秒,那麼在任何時刻都有10個請求正在被處理。回到我們的話題,就是需要使用10個執行緒來進行處理。如果單個請求的處理時間翻倍,那麼處理的執行緒數也要翻倍,變成20個。
理解了處理時間對於請求處理效率的影響之後,我們會發現,通常理論上限可能不是執行緒池大小的最佳值。執行緒池上限還需要參考任務處理時間。
假設JVM可以並行處理1000個任務,如果每個請求處理時間不超過30秒,那麼在最壞情況下,每秒最多隻能處理33.3個請求。然而,如果每個請求只需要500毫秒,那麼應用程式每秒可以處理2000個請求。