[10]elasticsearch原始碼深入分析——執行緒池的封裝

飛來來發表於2019-02-27

本篇為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分詞器)。如下圖所示

路徑中的plugin物件

記憶體中的plugin物件

構建執行緒池框架

初始化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
  • 批處理操作的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
  • 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
  • 查詢操作的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,
  • 管理操作的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
  • 監聽操作的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**,意思就沒有阻塞佇列。
  • 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
  • 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
  • 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
  • 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
  • 碎片處理操作的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
  • 強制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
  • 獲取碎片操作的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

至此就完成了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問題,它在這些服務元件中的作用也顯得愈發重要。

利特爾法則

查詢操作中提到的利特爾法則是一種描述穩定系統中,三個變數之間關係的法則。

[10]elasticsearch原始碼深入分析——執行緒池的封裝

其中L表示平均請求數量,λ表示請求的頻率,W表示響應請求的平均時間。舉例來說,如果每秒請求數為10次,每個請求處理時間為1秒,那麼在任何時刻都有10個請求正在被處理。回到我們的話題,就是需要使用10個執行緒來進行處理。如果單個請求的處理時間翻倍,那麼處理的執行緒數也要翻倍,變成20個。

理解了處理時間對於請求處理效率的影響之後,我們會發現,通常理論上限可能不是執行緒池大小的最佳值。執行緒池上限還需要參考任務處理時間。

假設JVM可以並行處理1000個任務,如果每個請求處理時間不超過30秒,那麼在最壞情況下,每秒最多隻能處理33.3個請求。然而,如果每個請求只需要500毫秒,那麼應用程式每秒可以處理2000個請求。

相關文章