Elasticsearch系列---實戰零停機重建索引

前言

我們使用Elasticsearch索引文檔時,最理想的情況是文檔JSON結構是確定的,數據源源不斷地灌進來即可,但實際情況中,沒人能夠阻攔需求的變更,在項目的某個版本,可能會對原有的文檔結構造成衝擊,增加新的字段還好,如果要修改原有的字段,只能重建索引了。

概要

本篇以實戰方式講解如何零停機完成索引重建的三種方案。

外部數據導入方案

整體介紹

系統架構設計中,有關係型數據庫用來存儲數據,Elasticsearch在系統架構裏起到查詢加速的作用,如果遇到索引重建的操作,待系統模塊發佈新版本後(若重建索引不是因爲客戶端修改導致的,可以不停機發版,直接操作),可以從數據庫將數據查詢出來,重新灌到Elasticsearch即可。

執行步驟

建議的功能方案:數據庫 MQ 應用模塊 Elasticsearch,可以在MQ控制檯發送MQ消息來觸發重導數據,按批次對數據進行導入,整個過程異步化處理,請求操作示意如下所示:

詳細操作步驟:

  1. 通過MQ的web控制檯或cli命令行,發送指定的MQ消息
  2. MQ消息被微服務模塊的消費者消費,觸發ES數據重新導入功能
  3. 微服務模塊從數據庫裏查詢數據的總數及批次信息,並將每個數據批次的分頁信息重新發送給MQ消息,分頁信息包含查詢條件和偏移量,此MQ消息還是會被微服務的MQ消息者接收處理。
  4. 微服務根據接收的查詢條件和分頁信息,從數據庫獲取到數據後,根據索引結構的定義,將數據組裝成ES支持的JSON格式,並執行bulk命令,將數據發送給Elasticsearch集羣。

這樣就可以完成索引的重建工作。

方案特點

MQ中間件的選型不做具體要求,常見的rabitmq、activemq、rocketmq等均可。

在微服務模塊方面,提供MQ消息處理接口、數據處理模塊需要事先開發的,一般是創建新的索引時,配套把重建的功能也一起做好。整體功能共用一個topic,針對每個索引,有單獨的結構定義和MQ消息處理tag,代碼儘可能複用。處理的批次大小需要根據實際的情況設置。

微服務模塊實例會部署多個,數據是分批處理的,批次信息會一次性全部先發送給MQ,各個實例處理的數據相互不重疊,利用MQ消息的異步處理機制,可以充分利用併發的優勢,加快數據重建的速度。

方案缺點

  1. 對數據庫造成讀取壓力,短時間內大量的讀操作,會佔用數據庫的硬件資源,嚴重時可能引起數據庫性能下降。
  2. 網絡帶寬佔用多,數據畢竟是從一個庫傳到另一個庫,雖說是內網,但大量的數據傳輸帶寬佔用也需要注意。
  3. 數據重建時間稍長,跟遷移的數據量大小有關。

基於scroll bulk 索引別名方案

整體介紹

利用Elasticsearch自帶的一些工具完成索引的重建工具,當然在方案實際落地時,可能也會依賴客戶端的一些功能,比如用Java客戶端持續的做scroll查詢、bulk命令的封裝等,但與上一方案相比,最明顯的區別就是:數據完全自給自足,不依賴其他數據源。

執行步驟

假設原索引名稱是music,新的索引名稱爲musicnew,Java客戶端使用別名musicalias連接Elasticsearch,該別名指向原索引music。

  1. 若Java客戶端沒有使用別名,需要給客戶端分配一個:
    PUT /music/_alias/music_alias
  2. 新建索引music_new,將mapping信息,settings信息等按新的要求全部定義好。
  3. 使用scroll api將數據批量查詢出來
GET /music/_search?scroll=1m
{
    "query": {
        "match_all": {}
    },
    "sort": ["_doc"],
    "size":  1000
}

  1. 採用bulk api將scoll查出來的一批數據,批量寫入新索引
POST /_bulk
{ "index":  { "_index": "music_new", "_type": "children", "_id": "1" }}
{ "name":    "wake me, shake me" }

  1. 反覆執行步驟3和步驟4,查詢一批導入一批,可以藉助Java Client或其他語言的API支持。
  2. 切換別名musicalias到新的索引musicnew上面,此時Java客戶端仍然使用別名訪問,也不需要修改任何代碼,不需要停機。
POST /_aliases
{
    "actions": [
        { "remove": { "index": "music", "alias": "music_alias" }},
        { "add":    { "index": "music_new", "alias": "music_alias" }}
    ]
}

  1. 驗證別名查詢的是否爲新索引的數據

方案特點

在數據傳輸上基本自給自足,不依賴於其他數據源,Java客戶端不需要停機等待數據遷移,網絡傳輸佔用帶寬較小。

只是scroll查詢和bulk提交這部分,數據量大時需要依賴一些客戶端工具。

補充一點

在Java客戶端或其他客戶端訪問Elasticsearch集羣時,使用別名是一個好習慣。

Reindex API方案

Elasticsearch v6.3.1已經支持Reindex API,它對scroll、bulk做了一層封裝,能夠 對文檔重建索引而不需要任何插件或外部工具。

最基礎的命令:

POST _reindex
{
  "source": {
    "index": "music"
  },
  "dest": {
    "index": "music_new"
  }
}

響應結果:

{
  "took": 180,
  "timed_out": false,
  "total": 4,
  "updated": 0,
  "created": 4,
  "deleted": 0,
  "batches": 1,
  "version_conflicts": 0,
  "noops": 0,
  "retries": {
    "bulk": 0,
    "search": 0
  },
  "throttled_millis": 0,
  "requests_per_second": -1,
  "throttled_until_millis": 0,
  "failures": []
}

注意:如果不手動創建新索引music_new的mapping信息,那麼Elasticsearch將啓動自動映射模板對數據進行類型映射,可能不是期望的類型,這點要注意一下。

version_type 屬性

使用reindex api也是創建快照後再執行遷移的,這樣目標索引的數據可能會與原索引有差異,version_type屬性可以決定樂觀鎖併發處理的規則。

reindex api可以設置version_type屬性,如下:

POST _reindex
{
  "source": {
    "index": "music"
  },
  "dest": {
    "index": "music_new"
    "version_type": "internal"
  }
}

version_type屬性含義如下:

  • internal:直接拷貝文檔到目標索引,對相同的type、文檔ID直接進行覆蓋,默認值
  • external:遷移文檔到目標索引時,保留version信息,對目標索引中不存在的文檔進行創建,已存在的文檔按version進行更新,遵循樂觀鎖機制。

op_type 屬性和conflicts 屬性

如果op_type設置爲create,那麼遷移時只在目標索引中創建ID不存在的文檔,已存在的文檔,會提示錯誤,如下請求:

POST _reindex
{
  "source": {
    "index": "music"
  },
  "dest": {
    "index": "music_new",
    "op_type": "create"
  }
}

有錯誤提示的響應,節選部分:

{
  "took": 11,
  "timed_out": false,
  "total": 5,
  "updated": 0,
  "created": 1,
  "deleted": 0,
  "batches": 1,
  "version_conflicts": 4,
  "noops": 0,
  "retries": {
    "bulk": 0,
    "search": 0
  },
  "throttled_millis": 0,
  "requests_per_second": -1,
  "throttled_until_millis": 0,
  "failures": [
    {
      "index": "music_new",
      "type": "children",
      "id": "2",
      "cause": {
        "type": "version_conflict_engine_exception",
        "reason": "[children][2]: version conflict, document already exists (current version [17])",
        "index_uuid": "dODetUbATTaRL-p8DAEzdA",
        "shard": "2",
        "index": "music_new"
      },
      "status": 409
    }
   ]
}

如果加上"conflicts": "proceed"配置項,那麼衝突信息將不展示,只展示衝突的文檔數量,請求和響應結果將變成這樣:

請求:

POST _reindex
{
  "conflicts": "proceed",
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter",
    "op_type": "create"
  }
}

響應:

{
  "took": 12,
  "timed_out": false,
  "total": 5,
  "updated": 0,
  "created": 1,
  "deleted": 0,
  "batches": 1,
  "version_conflicts": 4,
  "noops": 0,
  "retries": {
    "bulk": 0,
    "search": 0
  },
  "throttled_millis": 0,
  "requests_per_second": -1,
  "throttled_until_millis": 0,
  "failures": []
}

query支持

reindex api支持數據過濾、數據排序、size設置、_source選擇等,也支持腳本執行,這裏提供一個簡單示例:

POST _reindex
{
  "size": 100, 
  "source": {
    "index": "music",
    "query": {
      "term": {
        "language": "english"
      }
    },
    "sort": {
      "likes": "desc"
    }
  },
  "dest": {
    "index": "music_new"
  }
}

小結

本篇介紹了零停機索引重建操作的三個方案,從自研功能、scroll bulk到reindex,我們作爲Elasticsearch的使用者,三個方案的參與度是逐漸弱化的,但穩定性卻是逐漸上升的,我們需要清楚地去了解各個方案的優劣,適宜的場景,然後根據實際的情況去權衡,哪個方案更適合我們的業務模型,僅供參考,謝謝。

專注Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區可以掃左邊二維碼添加好友,邀請你加入Java架構社區微信羣共同探討技術Java架構社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章