Elasticsearch 性能調優總結


吞吐量(throughput)和延遲(latency)是評估 Elasticsearch 集群性能的指標,前者代表每秒寫入(index)或查詢(search)文檔的數量,后者則代表單個請求的延遲。上述指標之間也有一定聯系:延遲越低,吞吐量就越高。

20210608105408

JVM 內存壓力

Elasticsearch 集群的吞吐量顯然與節點的負載相關,尤其是大量的請求將引起節點的 JVM 內存壓力升高。Elasticsearch 使用斷路器(Circuit Breaker)來防止節點出現 JVM 堆內存溢出。如果 Elasticsearch 評估一項操作將觸發斷路器,那么便會返回一個 HTTP 錯誤碼 429:

{
  'error': {
    'type': 'circuit_breaking_exception',
    'reason': '[parent] Data too large, data for [<http_request>] would be [123848638/118.1mb], which is larger than the limit of [123273216/117.5mb], real usage: [120182112/114.6mb], new bytes reserved: [3666526/3.4mb]',
    'bytes_wanted': 123848638,
    'bytes_limit': 123273216,
    'durability': 'TRANSIENT'
  },
  'status': 429
}

由於斷路器有多個,首先應先查看其具體的觸發情況:

GET _nodes/stats/breaker

// 返回
"breakers" : {
    "request" : {
        "limit_size_in_bytes" : 1278030643,
        "limit_size" : "1.1gb",
        "estimated_size_in_bytes" : 0,
        "estimated_size" : "0b",
        "overhead" : 1.0,
        "tripped" : 0
    },
    "fielddata" : {
        "limit_size_in_bytes" : 852020428,
        "limit_size" : "812.5mb",
        "estimated_size_in_bytes" : 1112,
        "estimated_size" : "1kb",
        "overhead" : 1.03,
        "tripped" : 0
    },
    ...
    "parent" : {
        "limit_size_in_bytes" : 2023548518,
        "limit_size" : "1.8gb",
        "estimated_size_in_bytes" : 1129775232,
        "estimated_size" : "1gb",
        "overhead" : 1.0,
        "tripped" : 0
    }
}

默認情況下,parent(父級)斷路器在 JVM 內存達到 95%時觸發。為了預防報錯的產生,我們需要在其持續超過 85%時采取相應措施:

  • 如果 fielddata 斷路器觸發,則應減少 fielddata 的使用;
  • 清除 fielddata 緩存:POST _cache/clear?fielddata=true
  • 避免“昂貴的”搜索(expensive search);
  • 避免“映射爆炸”(mapping explosions);
  • 將批量請求(bulk request)拆分為多個小的請求;
  • 升級節點內存;
  • 減少索引分片的數量

詳見官方文檔:High JVM memory pressure

索引分片策略

上文提到,我們可以通過減少索引分片的數量來降低節點的 JVM 內存壓力,這是因為:

  • 分片過多會導致底層的 Segment 過多,而 Segment 會消耗文件句柄、內存和 CPU 資源,並且每次搜索請求都必須輪流檢查每個 Segment,進而導致開銷增加;
  • 對於每個 Elasticsearch 索引,Mapping 和 State 的相關信息都保存在集群狀態 (GET /_cluster/state) 中。它們存儲在內存中,以便快速訪問。因此,如果集群中的索引和分片數量過多,而 Mapping 又比較復雜的話,將占用大量內存。

官方博客 給出了分片策略的相關建議:

  • 建議將分片的平均大小控制在幾 GB 到幾十 GB 之間。對時序型數據用例而言,分片大小通常介於 20GB 至 40GB 之間;
  • 將分片數量/節點內存(GB)保持在 20 以下。例如,某個節點擁有 30GB 的堆內存,那其最多可有 600 個分片。在此限值范圍內,設置的分片數量越少,性能就會越好;
  • 對於時序型索引,使用 shrink 或 rollover API 減少索引和分片的數量。

當然,分片的大小也不能無限制地擴大,因為會對集群的故障恢復造成不利影響。盡管並沒有關於分片大小的固定限值,但通常將 50GB 作為分片大小的上限,而這一限值在各種用例中都已得到驗證。

索引寫入性能

優化索引的寫入性能主要有以下途徑:

  • 使用批量請求寫入索引,而非單個文檔寫入;
  • 使用多個線程或進程向 Elasticsearch 發送數據;
  • 如果對實時搜索要求不高,可以將索引的 refresh 間隔時間 (index.refresh_interval) 從默認值 1s 提升到 30s 左右;
  • 先將索引的副本數 (index.number_of_replicas) 設置為 0,待寫入全部完成后,再將其恢復到原始值;
  • 文件系統緩存(Filesystem Cache)將用於緩沖 I/O 操作,因此應確保節點的內存至少有一半分配給了文件系統緩存;
  • 若文檔使用自定義的_id,那么 Elasticsearch 將在其寫入分片時檢查_id是否重復。這是一項代價高昂的操作,因此建議使用自動生成的_id
  • 物理存儲設備使用 SSD 而非 HDD 或 NFS;
  • 減少磁盤的使用率,詳見 Tune for disk usage

在硬件設備無法升級的情況下,第一個方法是我們提升索引寫入性能的常用手段。然而,大量的批量請求可能引發 Elasticsearch 的 429 錯誤:Too many requests。因此在搭建集群前,我們需要對單節點單分片進行基准測試(benchmark),從而確認批量請求的最佳大小。首先嘗試一次索引 100 個文檔,然后提高到 200,400,等等。在每次基准測試中,批量請求中的文檔數量加倍。當索引寫入速度開始趨於平穩時,則說明批量請求達到了最佳大小。

那么如果生產環境的 Elasticsearch 向客戶端返回 429 錯誤,我們有沒有什么辦法解決呢?這就要從批量請求的處理方式說起。Elasticsearch 節點使用線程池(Thread Pool)來管理內存消費,多個線程池隊列使得客戶端的請求能夠在緩沖區保留而非丟棄。這樣便可以防止客戶端大量的寫入請求造成集群的過載,進而提升集群的可靠性和穩定性。

當批量請求到達集群中的協調節點后,首先進入批量隊列中,並交由線程池中的線程進行處理。由於其中的文檔可能屬於多個不同的索引和分片,因此需要根據分片對其進行拆分。隨后拆分后的文檔會被路由到其主分片所在的數據節點上,進入該節點的批量隊列中。如果隊列中沒有多余空間,將會通知協調節點該子請求(sub-request)被拒絕。若索引的副本數不為 0,數據節點的線程池還要將文檔發往其副本所在的節點上。待同步完成后,數據節點同樣會向協調節點發送響應。一旦所有的子請求全部完成(或部分被拒絕),協調節點就會創建一個響應返回給客戶端。

Elasticsearch API 可以查看節點線程池中各線程的批量隊列配置:

Get _nodes/thread_pool

// 返回
...
"thread_pool": {
    "watcher": {
        "type": "fixed",
        "size": 50,
        "queue_size": 1000
    },
    "force_merge": {
        "type": "fixed",
        "size": 1,
        "queue_size": -1
    },
    "search": {
        "type": "fixed_auto_queue_size",
        "size": 25,
        "queue_size": 1000
    },
    "write": {
        "type": "fixed",
        "size": 16,
        "queue_size": 200
    }
}
...

其中,size 為線程數,queue_size 為待處理請求隊列的大小。write線程負責處理每個文檔的索引、刪除、更新操作以及批量請求,若存在大量拒絕,則說明集群的寫入性能達到了瓶頸:

GET _cat/thread_pool/write?v

// 返回
node_name           name  active queue rejected
instance-0000000015 write     16    75   912687
instance-0000000017 write      4     0   808414
instance-0000000016 write      4     0   514021

為解決這一問題,我們首先想到的是增加批量請求隊列的大小。但實際上它並不會增大集群的吞吐量,只是讓更多的數據在節點的內存中排隊,甚至可能導致批量請求的處理時間增長。隊列中的批量處理越多,被消耗的寶貴堆內存就越多。堆上壓力過大將引起性能的下降,甚至導致集群的不穩定。

Cat Thread Pool 的返回結果可以看出拒絕發生在整個集群還是在單個節點,從而判斷寫入壓力是否分布不均。根據 官方博客 中的測試結果,三節點集群的寫入性能顯著優於單節點和兩節點集群。而兩節點相比單節點提升不大,可能是因為兩節點分擔寫入壓力不夠完美且副本同步操作增加了集群的負載。因此生產環境建議使用三節點部署,這樣既實現了高可用,又大大提升了索引的寫入性能。

我們還可以通過降低發送批量請求的頻率來避免 Elasticsearch 出現 429 報錯。以數據源 Logstash 為例,涉及的主要參數如下:

  • pipeline.batch.size:單個工作線程在嘗試執行 filter,output 之前收集的最大事件數。數值越大,處理則通常更高效,但增加了內存開銷;
  • pipeline.batch.delay:當前工作線程中接收到事件后等待新消息的最大時間(毫秒)。在此時間過后,Logstash 開始執行 filter 和 output。

Logstash 從接收事件到 filter 處理事件之間等待的最大時間是pipeline.batch.delaypipeline.batch.size的乘積。將上述兩參數適當調大,可以增大每次批量請求的大小而降低發送的頻率,防止 Elasticsearch 集群過載。

索引搜索性能

提升索引的寫入性能有多種途徑,如:

  • 對文檔進行建模 (Document modeling);
  • 盡可能減少搜索的字段數量;
  • 使用 term 進行查詢,速度較快;
  • 減少腳本的使用...

更多調優方案詳見官方文檔中的說明:Tune for search speed

參考文獻

我在 Elasticsearch 集群內應該設置多少個分片?

Elasticsearch:針對日志和指標對 Elasticsearch 集群進行基准測試並確定集群規模

Why am I seeing bulk rejections in my Elasticsearch cluster?

Fix common cluster issues

Tune for indexing speed

Tune for disk usage

Tune for search speed


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM