業務背景
當我們在使用多維度檢索時,mysql顯然已經不能滿足我的的使用場景,尤其涉及到表之間的join且數據量較大時,mysql的查詢性能顯得捉襟見肘。
這時候ES的多維檢索功能就派上用場了。我們可以將兩張或者多張業務表,制作成一個比較寬的索引,監聽業務的binlog,並將數據保存到ES中。
這樣就可以快速的支持業務檢索了。
業務需求
通常情況下,會使用ES的動態模板,之后添加其他的維度過濾會更加方便。
都知道ES底層存儲的是文檔,當使用POST往動態模板中添加了字段之后,之前的數據不會像mysql一樣可以設置默認值。
如果產品側又需要支持老數據的過濾時,這時候我們就涉及到刷ES索引的問題。
分析
按照數據的組織方式,將數據重新往ES插入一遍的方案肯定是不可行的,那么我們有沒有命令可以類似mysql的set default值這樣的方式呢?
於是我去翻閱ES的官方文檔,看到update是可以支持這種操作的。下面以 es動態索引中增加type類型為例演示解決過程。要實現的將es中原始的doc添加上type=0並支持索引。
現有文檔數
GET index_test/_count?pretty
{
"count" : 2000,
"_shards" : {
"total" : 12,
"successful" : 12,
"skipped" : 0,
"failed" : 0
}
}
可以看到文檔又2000條,
使用ES的term查詢:
term其實是分桶聚合查詢,可以理解為mysql的group by
POST index_test/_search?pretty
{
"size" : 0,
"aggs" : {
"aggType" : {
"terms" : {
"field" : "type"
}
}
}
}
查詢結果:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 12,
"successful" : 12,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2000,
"relation" : "gte"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"aggType" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 1,
"doc_count" : 5
},
{
"key" : 2,
"doc_count" : 4
},
{
"key" : 3,
"doc_count" : 4
}
]
}
}
}
可以看到,type=0 的數據沒有,只有type = 1,2,3 新生成數據,分別為5,4,4條一共13條,與數據總條數2000差了1987條,這些數據都是老數據,無法支持該字段的檢索。
使用update更新
POST index_test/_update/1
{
"script": {
"lang": "painless",
"source": """
if (ctx._source.type == null) { ctx._source.type=0 }
"""
}
}
ES的更新支持script,這樣我們在更新文檔時可以根據其他的一個或幾個字段確認新增字段的值,在這里我使用
如果type為空,將type賦值為默認值0。
更新完成后使用term查詢結果
```json
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 12,
"successful" : 12,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2000,
"relation" : "gte"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"aggType" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 0,
"doc_count" : 1
}
{
"key" : 1,
"doc_count" : 5
},
{
"key" : 2,
"doc_count" : 4
},
{
"key" : 3,
"doc_count" : 4
}
]
}
}
}
發現只增加了一條,重復執行更新命令也不會再增加了,通過分析update語句發現,其命令行update后的1指的是docId。這種方式顯然不能使用,我再猜想有沒有類似mysql中根據條件update的語句呀?查看官方文檔后,返現ES支持
update_by_query的操作
使用update_by_query
使用update_by_query語句,在這里我刪除了script中的條件判斷,改成使用query
POST index_test/_update_by_query
{
"script": {
"lang": "painless",
"source": "ctx._source.type=0"
},
"query": {
"bool": {
"must_not": {
"exists": {
"field": "type"
}
}
}
}
}
其實使用scrpit的腳本判斷要比query中使用must_not要慢。我理解使用script要access all 全表掃描。
如果使用了must_not 而且只有一個條件,我理解ES的執行引擎會使用倒排索引,查詢出有的type字段的,然后取反,把不存在type字段的doc ID返回。根據id去逐條更新,這樣判斷的次數從O(n)降到了理論的O(1)。
待更新結果返回后,重新使用term查詢結果:
POST index_test/_search?pretty
{
"size" : 0,
"aggs" : {
"aggType" : {
"terms" : {
"field" : "type"
}
}
}
}
查詢結果:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 12,
"successful" : 12,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2000,
"relation" : "gte"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"aggType" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 0,
"doc_count" : 1987
}
{
"key" : 1,
"doc_count" : 5
},
{
"key" : 2,
"doc_count" : 4
},
{
"key" : 3,
"doc_count" : 4
}
]
}
}
}
可以看到聚合結果中key=0的文檔相較之前增加了,而且key = 0,1,2,3,4 的枚舉加起來正好為2000.
證明更新成功了。
控制更新速度
在更新的過程中,如果要控制更新的速度,可以在更新的語句后添加參數,目前ES更新支持兩個方式
按照索引分片更新
POST index_test/_update_by_query?routing=1
其中routing為集群的第幾個分片。
- 優點:單分片更新,如果分片被更新壞了,可以找運維刪除分片,副本分片會主動替換主分片,並重新分配副本分片,在這期間索引的狀態可能是黃色。
- 缺點:更新不是原子的,而且需要清楚集群主分片數才能操作。
按照分頁更新
POST index_test/_update_by_query?scroll_size=10000
其中scroll_size的最大值為集群配置的允許的最大值,可以通過_settings命令查詢。
- 優點:可以控制集群中數據的更新速度,降低修復數據時,集群的負載。
- 缺點:需要判斷使用合理的分頁,一旦集群崩潰就會影響線上環境。
觸類旁通
ES集群使用的SSD的硬盤,而且對內存要求較高,當集群的存儲超過一半時(超過了一半ES就無法再實現段合並了,高並發寫入會產生較多分段Segment)。一半情況下,業務的數據都是按照日期存儲的,這時候我們可以把較早的數據備份到HDFS系統上,然后在ES的集群上執行delete_by_query刪除部分歷史數據,這樣可以使ES集群一直處於比較好的性能階段。