原載于Elastic中文社區(qū): https://elasticsearch.cn/article/269
上周有用戶在社區(qū)發(fā)了一例Kibana讀取超時的問題:question#2319 压彭。周末找時間幫其調(diào)查了下穴墅,發(fā)現(xiàn)某些較新的ES版本和Kibana搭配俺叭,會產(chǎn)生意想不到的緩慢問題纵菌。 考慮到這個問題比較普遍菱阵,因此在這里總結(jié)一下問題的根源和解決辦法颁褂,希望用到問題版本的用戶不要踩到坑席里。
首先問題的現(xiàn)象在上面的問題鏈接里有描述签则,簡而言之就是對于一個硬件配置比較高的集群遥诉,每天寫入一個20億左右數(shù)據(jù)的索引拇泣,通過kibana的discovery面板查看數(shù)據(jù)會一直超時。即使時間范圍放到最近半小時矮锈,超時依舊霉翔,有些蹊蹺。
周末拿到用戶給的測試賬號苞笨,登陸集群看了下狀態(tài)债朵。 從機器的硬件配置,集群和索引的配置看瀑凝,沒找到什么特別不對勁的地方序芦。然而點擊到Discovery面板,的確數(shù)據(jù)顯示不出來粤咪。 集群監(jiān)控數(shù)據(jù)看谚中,并沒有其他用戶在做查詢,cpu利用率和集群負載都比較低寥枝。因此初步可以判定宪塔,就是查詢本身比較緩慢所致。
對于診斷查詢緩慢問題囊拜,我通常的做法某筐,就是將對應面板下的查詢拷貝出來,在Kibana Dev Console里手動執(zhí)行冠跷,然后再加上"profile":true
選項南誊,看看查詢是如何解析和執(zhí)行的。對應的查詢形如下面這樣:
{
"profile": true,
"query": {
"bool": {
"must": [
{
"query_string": {
"analyze_wildcard": true,
"query": "*"
}
},
{
"range": {
"@timestamp": {
"gte": "now-1h",
"lte": "now",
"format": "epoch_millis"
}
}
}
]
}
}
}
因為用戶query框什么都沒有輸入蜜托,因此默認查詢串被Kibana設(shè)置為*
抄囚, 然后根據(jù)選擇的時間范圍加了一個range查詢。 profile的輸出讓我稍微有些吃驚盗冷,其中 query_string的里的*
居然被解析成非常復雜的DisjunctionMaxQuery
怠苔,主要查詢耗時都在這里了。
{
"type": "DisjunctionMaxQuery",
"description": "(ConstantScore(_field_names:remote_addr.keyword) | ConstantScore(_field_names:geoip.country_isocode) | ConstantScore(_field_names:geoip.country_name.keyword) | ConstantScore(_field_names:via) | ConstantScore(_field_names:domain.keyword) | ConstantScore(_field_names:request_method.keyword) | ConstantScore(_field_names:protocol) | ConstantScore(_field_names:xff.keyword) | ConstantScore(_field_names:host) | ConstantScore(_field_names:geoip.city_name.keyword) | ConstantScore(_field_names:client_ip) | ConstantScore(_field_names:host.keyword) | ConstantScore(_field_names:geoip.longitude) | ConstantScore(_field_names:geoip.subdivision_name.keyword) | ConstantScore(_field_names:geoip.country_code) | ConstantScore(_field_names:upstream_addr.keyword) | ConstantScore(_field_names:@version.keyword) | ConstantScore(_field_names:request_uri) | ConstantScore(_field_names:tags) | ConstantScore(_field_names:idc_tag) | ConstantScore(_field_names:size) | ConstantScore(_field_names:http_referer) | ConstantScore(_field_names:message.keyword) | ConstantScore(_field_names:domain) | ConstantScore(_field_names:geoip.latitude) | ConstantScore(_field_names:xff) | ConstantScore(_field_names:protocol.keyword) | ConstantScore(_field_names:geoip.country_code.keyword) | ConstantScore(_field_names:status) | ConstantScore(_field_names:upstream_addr) | ConstantScore(_field_names:http_referer.keyword) | ConstantScore(_field_names:tags.keyword) | ConstantScore(_field_names:client_ip.keyword) | ConstantScore(_field_names:request_method) | ConstantScore(_field_names:upstream_status) | ConstantScore(_field_names:request_time) | ConstantScore(_field_names:geoip.location) | ConstantScore(_field_names:@version) | ConstantScore(_field_names:geoip.country_name) | ConstantScore(_field_names:user_agent) | ConstantScore(_field_names:idc_tag.keyword) | ConstantScore(_field_names:remote_addr) | ConstantScore(_field_names:geoip.country_isocode.keyword) | ConstantScore(_field_names:geoip.city_name) | ConstantScore(_field_names:via.keyword) | ConstantScore(_field_names:message) | ConstantScore(_field_names:user_agent.keyword) | ConstantScore(_field_names:request_uri.keyword) | ConstantScore(_field_names:@timestamp) | ConstantScore(_field_names:upstream_response_time) | ConstantScore(_field_names:geoip.subdivision_name))",
"time": "5535.127008ms",
"time_in_nanos": 5535127008
也就是說仪糖, ES將只含一個*
的query_string query
解析成了針對mapping里能找到的所有字段的field:*
查詢柑司,然后合并所有的查詢結(jié)果迫肖。 可想而知,對于比較大攒驰,字段比較多的索引這個查詢是非常耗時的蟆湖。而我對于*
的認知,是其應該被rewrite成一個match_all query
即可玻粪,這樣幾乎沒有什么開銷隅津。
為什么會這樣? 查詢了一下ES官方關(guān)于Query String Query的文檔劲室,其中的default_field和all_fields起到了一定作用:
elasticsearch/reference/5.5/query-dsl-query-string-query.html
default_field
The default field for query terms if no prefix field is specified. Defaults to the index.query.default_field index settings, which in turn defaults to _all.
all_fields
Perform the query on all fields detected in the mapping that can be queried. Will be used by default when the _all field is disabled and no default_field is specified (either in the index settings or in the request body) and no fields are specified.
根據(jù)解釋伦仍,查詢的時候可以帶一個default_field
選項,其默認值為索引級別設(shè)置index.query.default_field
很洋,如果這個設(shè)置沒有設(shè)置充蓝,則默認為_all
。 但一般用戶索引日志的時候喉磁,都會關(guān)掉_all
字段谓苟,用于節(jié)省磁盤空間,提升索引速率协怒。那么這時候default_field
是什么呢涝焙? 答案是all_fields
,也就是ES會將查詢轉(zhuǎn)換為對所有字段的查詢孕暇。
為了驗證這個是問題所在仑撞,我在索引里加了一個default_field
的設(shè)置,隨意挑選了一個字段芭商。 果然問題就解決了派草,discovery面板渲染速度快了差不多有10倍搀缠。
但仔細想想铛楣,這也只是繞過了問題。 問題的根源艺普,為什么*
不被rewrite成match_all
呢簸州?
這時候想到我們自己生產(chǎn)的集群似乎沒有這個問題,于是用我們自己的集群測試了一下,*
果然是正常解析成match_all
了歧譬。 于是對比了一下集群ES的版本岸浑,我們正常工作的是5.3.2
,用戶的集群是5.5.0
瑰步。
接下來矢洲,我想找到這些版本之間,ES對于query string的解析源碼層面做了什么改動缩焦。經(jīng)過一番探查读虏,找到了下面這個變更歷史:
可以看到责静,在pull/23433里,為了修復一個
foo:*
解析歧義的問題盖桥,對于未指定field名稱灾螃,光提供一個*
的Query string查詢,不再被解析成match_all
了揩徊,而是擴展成全部字段的DisjunctionMaxQuery查詢腰鬼。 由此Kibana默認的*
,會引起非常嚴重的性能問題塑荒。
注意: 這個問題會影響5.4和5.5兩個小版本的ES/Kibana熄赡。
順著這個issue里的鏈接摸下去,找到了對應Kibana相關(guān)問題討論:issues#12097齿税,以及對應的修復: pull/13047本谜,修復版本默認發(fā)出的查詢串是match all
。
修復的版本則是5.5.2
及5.6.0
偎窘, 因此有用到5.4.0
到5.5.1
之間版本的ELK用戶一定要安排升級乌助!