High cardinality下對(duì)持續(xù)寫入的Elasticsearch索引進(jìn)行聚合查詢的性能優(yōu)化

High cardinality下對(duì)持續(xù)寫入的Elasticsearch索引進(jìn)行聚合查詢的性能優(yōu)化

背景

最近使用騰訊云Elasticsearch Service的用戶提出,對(duì)線上的ES集群進(jìn)行查詢,響應(yīng)越來(lái)越慢,希望能幫忙優(yōu)化一下。
查詢?cè)絹?lái)越慢的語(yǔ)句如下:

{
  "_source": false,
  "size": 0,
  "aggs": {
    "traceId": {
      "aggs": {
        "timestamp_millis": {
          "min": {
            "field": "timestamp_millis"
          }
        }
      },
      "terms": {
        "field": "traceId",
        "order": {
          "timestamp_millis": "desc"
        },
        "size": 10
      }
    }
  },
  "query": {
    "bool": {
      "filter": {
        "bool": {
          "must": [
            {
              "range": {
                "timestamp_millis": {
                  "from": 1556431798000,
                  "include_lower": true,
                  "include_upper": true,
                  "to": 1556435398000
                }
              }
            },
            {
              "term": {
                "user": "1275813850"
              }
            }
          ]
        }
      }
    }
  }
}

從查詢語(yǔ)句上看到用戶使用了聚合查詢(aggregation query), 第一反應(yīng)就是聚合查詢影響了查詢速度。但是又發(fā)現(xiàn),用戶的索引是按天創(chuàng)建的,查詢昨天的數(shù)據(jù)量較大的索引(300GB)響應(yīng)并不慢,可以達(dá)到ms級(jí)別,但是查詢當(dāng)天的正在寫入數(shù)據(jù)的索引就很慢,并且響應(yīng)時(shí)間隨著寫入數(shù)據(jù)的增加而增加。

原因分析

初步分析查詢性能瓶頸就在于聚合查詢,但是又不清楚為什么查詢舊的索引會(huì)比較快,而查詢正在寫入的索引會(huì)越來(lái)越慢。所以趁機(jī)找了些資料了解了下聚合查詢的實(shí)現(xiàn),最終了解到:

  1. 聚合查詢會(huì)對(duì)要進(jìn)行聚合的字段構(gòu)建Global Cardinals, 字段的唯一值越多(high cardinality),構(gòu)建Global Cardinals構(gòu)建的越慢,參考文章: https://blog.csdn.net/zwgdft/article/details/83215977
  2. 聚合查詢時(shí)構(gòu)建好的Global Cardinals是存放在內(nèi)存中的,如果索引不再發(fā)生變化(沒(méi)有新數(shù)據(jù)寫入而產(chǎn)生新的segment或者segment merge時(shí)), Global Cardinals就不需要重新構(gòu)建,第一次進(jìn)行聚合查詢時(shí)會(huì)構(gòu)建好Global Cardinals,后續(xù)的查詢就會(huì)使用在內(nèi)存中已經(jīng)緩存好的Global Cardinals了
  3. 嘗試在查詢時(shí)增加execute_hit:map參數(shù),結(jié)果無(wú)效,原因是用戶使用的6.4.3版本的集群該功能存在bug,雖然通過(guò)該參數(shù)execute_hit指定了不創(chuàng)建Global Cardinals,但是實(shí)際上還是創(chuàng)建了,后續(xù)版本已經(jīng)修復(fù)了這個(gè)問(wèn)題, 參考https://github.com/elastic/elasticsearch/issues/37705

優(yōu)化方案

經(jīng)過(guò)最終討論,決定從業(yè)務(wù)角度對(duì)查詢性能進(jìn)行優(yōu)化,既然對(duì)持續(xù)寫入的索引構(gòu)建Global Cardinals會(huì)越來(lái)越慢,那就降低索引的粒度,使得持續(xù)寫入的索引數(shù)據(jù)量降低,同時(shí)增加了能夠使用Global Cardinals緩存的索引數(shù)據(jù)量。

詳細(xì)的優(yōu)化方案如下:

  1. 降低索引的粒度,按小時(shí)創(chuàng)建索引
  2. 寫入時(shí)只寫入當(dāng)前小時(shí)的索引,查詢時(shí)根據(jù)時(shí)間范圍查詢對(duì)應(yīng)的索引
  3. 為了防止索引數(shù)量和分片數(shù)量膨脹,可以把舊的按小時(shí)創(chuàng)建的索引定期reindex到一個(gè)以當(dāng)天日期為后綴的索引中,reindex完成之后再刪除按小時(shí)創(chuàng)建的索引。

實(shí)戰(zhàn)過(guò)程

根據(jù)優(yōu)化方案,需要實(shí)現(xiàn)的內(nèi)容包括:

  1. 按小時(shí)創(chuàng)建索引,寫入數(shù)據(jù)
  2. 每小時(shí)執(zhí)行一次reindex, 把按小時(shí)建的索引reindex到按天建的索引中
  3. 定期刪除按小時(shí)建的索引

其中,第一步需要在client端進(jìn)行,寫入數(shù)據(jù)時(shí)根據(jù)當(dāng)前時(shí)間指定索引名稱,如當(dāng)前時(shí)間是
"2019-05-07 03:50:06", 則寫入的索引名稱為2019-05-07-03;第二步和第三步都是定時(shí)任務(wù),實(shí)戰(zhàn)時(shí)嘗試使用SCF(騰訊云Serverless云函數(shù))進(jìn)行簡(jiǎn)單的配置即可。

1.創(chuàng)建SCF云函數(shù)

在騰訊云SCF控制臺(tái)中,選擇"新建",進(jìn)入云函數(shù)創(chuàng)建頁(yè)面:


image

配置函數(shù)名稱,選擇名為"ES寫入函數(shù)"的模板,該模板自帶elasticsearch模塊,可以使用es的api操作集群。

創(chuàng)建完成后,需要在"函數(shù)配置"TAB頁(yè)對(duì)函數(shù)的網(wǎng)絡(luò)進(jìn)行配置,選擇和Elasticsearch集群同vpc下的網(wǎng)絡(luò):


image

接下來(lái),就可以配置函數(shù)代碼和觸發(fā)方式,并進(jìn)行測(cè)試。

1. 定期reindex

定期reindex的函數(shù)代碼如下:

# -*- coding: utf8 -*-
from datetime import datetime
from elasticsearch import Elasticsearch
import random
import time

# ES集群地址
ESServer = Elasticsearch("10.0.128.35:9200")

def reindex_hourly_2_daily():
    # 索引前綴,到月份
    index_prefix = "test-index-"+time.strftime( "%Y-%m" ,time.localtime(time.time())) +"-"

    # 當(dāng)前天
    current_day = time.localtime(time.time()).tm_mday
    # 當(dāng)前小時(shí),因?yàn)镾CF是UTC時(shí)間,所以加8個(gè)小時(shí),如果不在SCF里運(yùn)行,則不用加8個(gè)小時(shí),也不用進(jìn)行時(shí)區(qū)轉(zhuǎn)換
    current_hour = time.localtime(time.time()).tm_hour + 8
    # 時(shí)區(qū)轉(zhuǎn)換
    if current_hour >=24:
        current_hour= current_hour-24
        current_day = current_day +1
    
    # 前一個(gè)小時(shí)
    last_hour = current_hour -1
    # 前一天
    last_day = current_day-1

    # 前一個(gè)小時(shí)的索引
    last_hour_index=''
    # 按天建的索引
    daily_index=''

    # 如果是0點(diǎn),則把前一天23點(diǎn)的索引遷移到前一天按天建的索引
    if current_hour ==0:
        last_hour=23
        last_day = current_day-1
        # 構(gòu)造出如2019-05-05格式的索引,日期中的天數(shù)小于10則補(bǔ)0
        if last_day<10:
            daily_index = index_prefix +  "0"+ str(last_day)
        else:
            daily_index = index_prefix +  str(last_day)
        # 構(gòu)造出如2019-05-05-01格式的索引,日期中的小時(shí)數(shù)小于10則補(bǔ)0
        if last_hour<10:
            last_hour_index = daily_index+  "-0"+ str(last_hour)
        else:
            last_hour_index = daily_index+  "-"+str(last_hour)
        
    else:
         # 構(gòu)造出如2019-05-05格式的索引
        if current_day<10:
            daily_index = index_prefix +  "0"+ str(current_day)
        else:
            daily_index = index_prefix +  str(current_day)
        if last_hour<10:
            last_hour_index = daily_index+ "-0"+ str(last_hour)
        else:
            last_hour_index = daily_index+ "-"+ str(last_hour)
        


    # 自動(dòng)創(chuàng)建按天建的索引
    ESServer.indices.create(daily_index, ignore=400)

    body= {}
    source ={
        'index':last_hour_index
    }
    dest = {
        'index':daily_index
    }
    body={
        'source':source,
        'dest':dest
    }

    # 執(zhí)行reindex,source和index相同的情況下,重復(fù)執(zhí)行多次也不會(huì)造成數(shù)據(jù)重復(fù)
    rsp = ESServer.reindex(body=body,wait_for_completion=False)
    # 執(zhí)行reindex返回taskId, 可以通過(guò)輪詢taskId判斷操作是否完成
    print rsp


def main_handler(event,context):
    reindex_hourly_2_daily()

函數(shù)代碼說(shuō)明:

  • 使用該函數(shù)時(shí)需要把ES集群地址修改為自己的集群地址
  • SCF執(zhí)行時(shí)使用的時(shí)間是UTC時(shí)間而不是東八區(qū),所以在編寫函數(shù)代碼的時(shí)候需要注意進(jìn)行時(shí)區(qū)轉(zhuǎn)換
  • 調(diào)用reindex api時(shí)指定wait_for_completion為false, 讓reindex操作異步執(zhí)行,同時(shí)返回一個(gè)taskId, 后續(xù)可以通過(guò)task api輪詢?cè)搕ask查看任務(wù)是否完成;可以選擇在reindex完成后刪除按小時(shí)建的索引, 也可以選擇延遲刪除,后續(xù)定期清理掉按小時(shí)建的索引
  • 無(wú)需擔(dān)心函數(shù)重復(fù)執(zhí)行造成數(shù)據(jù)重復(fù)的情況,reindex執(zhí)行的是一個(gè)upsert操作, 如果source index中的docId在dest index中不存在,則插入該doc,否則更新該doc

配置定期reindex函數(shù)的觸發(fā)方式為每小時(shí)的第1分鐘執(zhí)行:


image

2. 定期刪除按小時(shí)建的索引

根據(jù)需要,可以選擇在每天凌晨0點(diǎn)到5點(diǎn)這個(gè)時(shí)間段,業(yè)務(wù)請(qǐng)求量不大時(shí),刪除前一天按小時(shí)建的索引,避免過(guò)多的重復(fù)數(shù)據(jù),以及避免分片數(shù)量膨脹。

函數(shù)代碼如下:

# -*- coding: utf8 -*-
from datetime import datetime
from elasticsearch import Elasticsearch
import random
import time

ESServer = Elasticsearch("10.0.128.35:9200")

def delete_old_index():
    # 索引前綴,到月份
    index_prefix = "test-index-"+time.strftime( "%Y-%m" ,time.localtime(time.time())) +"-"

    # 當(dāng)前天
    current_day = time.localtime(time.time()).tm_mday
    # 當(dāng)前小時(shí),因?yàn)镾CF是UTC時(shí)間,所以加8個(gè)小時(shí),如果不在SCF里運(yùn)行,則不用加8個(gè)小時(shí),也不用進(jìn)行時(shí)區(qū)轉(zhuǎn)換
    current_hour = time.localtime(time.time()).tm_hour + 8
    
    # 前一天
    last_day = current_day-1
    
    if current_hour >=24:
        last_day = current_day
    

    # 需要?jiǎng)h除的索引,以通配符表示,如2019-05-05-*,表示刪除前一天所有的按小時(shí)建的索引
    will_delete_index_prefix=''

    if last_day<10:
        will_delete_index_prefix = index_prefix +  "0"+ str(last_day) +"-"
    else:
        will_delete_index_prefix = index_prefix +  str(last_day)+"-"
  
    for i in range(24):
        hour = ""
        if i<10:
            hour = "0"+str(i)
        else:
            hour = str(i)
        ESServer.indices.delete(will_delete_index_prefix+hour, ignore=[400, 404])


def main_handler(event,context):
    delete_old_index()

函數(shù)說(shuō)明:

  • 該函數(shù)用于刪除前一天的按小時(shí)建的索引,如當(dāng)前天是2019-06-07, 則函數(shù)執(zhí)行時(shí)會(huì)刪除
    2019-06-06-00到2019-06-06-23全部24個(gè)索引

配置定期刪除索引函數(shù)的觸發(fā)方式為每天的2點(diǎn)執(zhí)行(SCF使用的是UTC時(shí)間,所以cron表達(dá)式中需要加8個(gè)小時(shí)):

image

總結(jié)

  • 經(jīng)過(guò)以上分析與實(shí)戰(zhàn),我們最終降低了High cardinality下對(duì)持續(xù)寫入的Elasticsearch索引進(jìn)行聚合查詢的時(shí)延,在利用緩存的情況下,聚合查詢響應(yīng)在ms級(jí)
  • 相比按天建索引,采用按小時(shí)建索引的優(yōu)化方案,增加了部分冗余的數(shù)據(jù),分片的數(shù)量也有增加;因?yàn)槊啃r(shí)的數(shù)據(jù)量相比每天要小的多,所以按小時(shí)建的索引分片數(shù)量可以設(shè)置的低一些,防止出現(xiàn)分片數(shù)量過(guò)多而大量占用內(nèi)存的情況
  • 如果數(shù)據(jù)量比較大,reindex會(huì)比較慢,可以通過(guò)snapshot api把按小時(shí)建的索引數(shù)據(jù)導(dǎo)入到按天建的索引中,數(shù)據(jù)導(dǎo)入的速度會(huì)比較快,可以參考文檔
    https://cloud.tencent.com/document/product/845/19549
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容