深度剖析Elasticsearch的bulk api使用以及底層原理

一、api

1、概念

就是批量操作,將多條PUT/POST/DELETE命令合併成一個bulk命令進行操作,節省代碼量也提高性能。

2、語法

PUT /_bulk
{"action":{"metadata"}}
{"data"}

action取值(如下是常用的):
index:普通的PUT操作,可以是創建文檔,也可以是全量替換
create:PUT /index/_doc/id/_create,強制創建
delete:刪除一個文檔,只要1個metadata就可以了,無需{“data”}部分
update:執行的partial update操作


metadata:json,對應的是這部分內容 PUT /product/_doc/1


data:具體操作內容的json,比如

{
   "name": "huawei shouji",
   "desc": "4G 5G",
   "tags": ["shouji"]
}

3、Demo

3.1、需求一

比如要創建一個index名稱爲test_index1且_id爲1的一個document,document裏面包含如下兩個字段

{
	"test_field1" : "test1", 
	"test_field2" : "test2"
}
PUT /_bulk
{"index" : {"_index" : "test_index1", "_id" : "1"}}
{"test_field1" : "test1", "test_field2" : "test2"}

查看數據進行驗證

GET /test_index1/_search
{
  "query": {
    "match_all": {}
  }
}

結果:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "test_index1",
        "_type" : "test_type",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "test_field1" : "test1",
          "test_field2" : "test2"
        }
      }
    ]
  }
}

3.2、需求二

綜合應用

POST /_bulk
{"delete":{"_index":"test_index2","_id":"1"}}
{"create":{"_index":"test_index2","_id":"2"}}
{"test_field":"test2"}
{"index":{"_index":"test_index2","_id":"3"}}
{"test_field":"test3"}
{"update":{"_index":"test_index2","_id":"2","retry_on_conflict":3}}
{"doc":{"test_field2":"bulk test"}}

返回結果是對每一條操作都做一個結果,彼此之間互不影響,也就是說比如第一條delete報錯了,不會影響下面的語句,粗糙理解成“沒mysql的事務控制”。但是再返回結果裏,會告訴你異常日誌。具體返回結果如下,比如第一個delete是404了。:

{
  "took" : 394,
  "errors" : false,
  "items" : [
    {
      "delete" : {
        "_index" : "test_index2",
        "_type" : "_doc",
        "_id" : "1",
        "_version" : 1,
        "result" : "not_found",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 404
      }
    },
    {
      "create" : {
        "_index" : "test_index2",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test_index2",
        "_type" : "_doc",
        "_id" : "3",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 2,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "update" : {
        "_index" : "test_index2",
        "_type" : "_doc",
        "_id" : "2",
        "_version" : 2,
        "result" : "updated",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 3,
        "_primary_term" : 1,
        "status" : 200
      }
    }
  ]
}

二、補充

1、格式

比如嚴格遵守如下格式,多一個回車符號都會報錯

PUT /_bulk
{"action":{"metadata"}}
{"data"}

比如如下會報錯:

PUT /_bulk
{"index" : {"_index" : "test_index1", "_id" : "1"}}
{
  "test_field1" : "test1", "test_field2" : "test2"
  
}

返回結果

{
  "error" : {
    "root_cause" : [
      {
        "type" : "illegal_argument_exception",
        "reason" : "Malformed action/metadata line [3], expected START_OBJECT but found [VALUE_STRING]"
      }
    ],
    "type" : "illegal_argument_exception",
    "reason" : "Malformed action/metadata line [3], expected START_OBJECT but found [VALUE_STRING]"
  },
  "status" : 400
}

2、優化

bulk size建議大小:

bulk request會加載到內存裏,如果太大的話,性能反而會下降,因此需要反覆嘗試一個最佳的bulk size。一般從1000~5000條數據開始,嘗試逐漸增加,另外,如果看大小的話,最好是在515MB之間。

三、底層原理

1、問題

爲什麼bulk api那麼的醜陋不堪?換行、格式化都會報錯,比如那麼醜陋的格式才能執行?

2、答案

因爲bulk中的每組操作(index/create/delete/update)都可能要轉發到不同的node的shard上去執行。
如果採取優雅的json格式,如下:

[
  {
    "action" : {},
    "data" : {}
  }
]

首先,整個可讀性非常棒,讀起來很爽,但是ES拿到那種標準格式的JSON串以後,要按照下述流程去進行處理
(1)將JSON數組解析爲JSONArray對象,這個時候,整個數據,就會在內存中出現一份一模一樣的拷貝,一份數據是JSON文本,一份數據是JSONArray對象。
(2)解析JSON數組裏的每個JSON,對每個請求中的document進行路由
(3)爲路由到同一個shard上的多個請求,創建一個請求數組。
(4)將這個請求數組序列化
(5)將序列化後的請求數組發送到對應的節點上去

再看這種醜陋的bulk json格式

{"action" : {"meta"}}
{"data"}
{"action" : {"meta"}}
{"data"}

(1)不用將其轉換爲JSON對象,不會出現內存中的相同數據的拷貝,直接按照換行符切割JSON
(2)對每兩個一組的json,讀取meta,進行document路由
(3)直接將對應的json發送到node上去

兩種格式對比:
(1)優雅格式:
耗費更多的內存,更多的JVM GC開銷
我們之前提到過 bulk size最佳大小的問題,一般建議說在幾千條那樣,然後大小在10MB左右,所以說,可怕的事情來了,假設說現在100個bulk請求發送到了一個節點上去,然後每個請求是10MB,100個請求就是1000MB=1GB。然後每個請求的json都copy一份爲JSONArray對象,此時內存中的佔用就會翻倍,就會佔用2GB內存,甚至還不止,因爲弄成JSONArray後,還可能會多搞一些其他的數據結構,2GB+的內存佔用。
佔用更多的內存可能就會積壓其他請求的內存使用量,比如說最重要的搜索請求,分析請求,等等,此時就可能會導致其他請求的性能急速下降
另外的話,佔用內存更多,就會導致ES的java虛擬機的垃圾回收次數更多,更頻繁,每次要回收的垃圾對象更多,耗費的時間更多,導致ES的java虛擬機停止工作線程的時間更多。

(2)醜陋的JSON格式:
最大的優勢在於,不需要將JSON數組解析爲一個JSONArray對象,形成一份大數據的拷貝,浪費內存空間,儘可能的保證性能。

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