Elasticsearch 優化
Elasticsearch是一個基於Lucene的搜尋伺服器,其搜尋的核心原理是倒排索引,今天談下在日常專案中使用它遇到的一些問題及優化解決辦法。
一. 搜尋的深度分頁問題
在日常專案中,經常會有分頁搜尋並支援跳頁的需求,類似百度、Google搜尋那樣,使用ES進行這類需求的搜尋時一般採用from/size的方式,from指明開始的位置,size指定獲取的條數,通過這種方式獲取資料稱為深度分頁。
通過這種分頁方式當我取 from為11000,size為10時,發現無法獲取:
ES報錯說超過了max_result_window
初步解決方案:
我修改了索引的設定,將max_result_window設定為了10000000:
PUT ccnu_resource/_settings
{
"index": {
"max_result_window": 10000000
}
}
這樣做雖然解決了問題,並且目前在效能上也沒有太大問題。一次當我用Google搜尋時時,突發奇想,想試試Google的最大分頁數:
我發現Google提示:Google為所有查詢的結果數都不會超過1000,然後我迅速嘗試了百度和微軟的BING:
百度只顯示76頁,當修改url時,76頁以外的也不會顯示,這時候會跳到第一頁,微軟BING只會顯示97頁,當你繼續下一頁時它會回退當前頁的前一頁,這時候我重新查閱了ES分頁遍歷相關資料,這種from/to的方式採用的是深度分頁機制,並且目前所有分散式搜尋引擎都存在深度分頁的問題。
ES深度分頁:
由於資料是分散儲存在各個分片上的,所以ES會從每個分片上取出當前查詢的全部資料,比如from:9990,size:10,ES會在每個分片上取10000個document,然後聚合每個分片的結果再排序選取前10000個document;所以當from的值越來越大,頁數越來越多時,ES處理的document就越多,同時佔用的記憶體也越來越大,所以當資料量很大、請求數很多時,搜尋的效率會大大降低;所以ES預設max_result_window為10000。
所以如果要使用from/size的方式分頁遍歷,最好使用ES預設的max_result_window,可以根據自己的業務需求適當增加或減少max_result_window的值,但是建議以跳頁的方式分頁最好控制頁數在1000以內,max_result_window的值最好不要修改。
二. Mapping設定與Query查詢優化問題
在ES中建立Mappings時,預設_source是enable=true,會儲存整個document的值,當執行search操作的時,會返回整個document的資訊。如果只想返回document的部分fields,但_source會返回原始所有的內容,當某些不需要返回的field很大時,ES查詢的效能會降低,這時候可以考慮使用store結合_source的enable=false來建立mapping。
PUT article_index
{
"mappings": {
"es_article_doc":{
"_source":{
"enabled":false
},
"properties":{
"title":{
"type":"text",
"fields":{
"keyword":{
"type":"keyword"
}
},
"store":true
},
"abstract":{
"type":"text",
"fields":{
"keyword":{
"type":"keyword"
}
},
"store":true
},
"content":{
"type":"text",
"store":true
}
}
}
}
}
可以設定_source的enable:false,來單獨儲存fields,這樣查詢指定field時不會載入整個_source,通過stored_fields返回指定的fields,並且可以對指定field做高亮顯示的需求:
GET article_index/_search
{
"stored_fields": [
"title"
],
"query": {
"match": {
"content": "async"
}
},
"highlight": {
"fields": {
"content": {}
}
}
}
使用store在特定需求下會一定程度上提高ES的效率,但是store對於複雜的資料型別如nested型別不支援:
# nested型別
PUT article_index_nested
{
"mappings": {
"es_article_nes_doc": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"comment": {
"type": "nested",
"properties": {
"username": {
"type": "keyword"
},
"content": {
"type": "text"
}
}
}
}
}
}
}
新增資料:
PUT article_index_nested/es_article_nes_doc/1
{
"title": "Harvard_fly 淺談 ES優化",
"comments": [
{
"username": "alice",
"date": "2018-11-13",
"content": "aaa"
},
{
"username": "bob",
"date": "2018-11-12",
"content": "bbb"
}
]
}
這種nested型別的store就不支援了,只能通過_source返回資料,如果需要返回指定field可以在search中通過_source指定field:
GET article_index_nested/_search
{
"_source": ["title","comments"],
"query": {
"bool": {
"must": [
{
"match": {
"comments.username": "alice"
}
},
{
"match": {
"comments.content": "aaa"
}
}
]
}
}
}
三. ES讀寫優化問題
ES讀效能的優化主要是查詢的優化,在查詢中儘量使用filter,如果遇到查詢慢可以使用explain進行慢查詢,進而優化資料模型和query;對於ES的寫優化,最好採用bulk批量插入,下面以python的api作為例子說明:
def bulk_insert_data(cls, qid_data_list):
"""
批量插入試題到ES庫
:param qid_data_list: qid ES結構列表
:return:
"""
if not isinstance(qid_data_list, (list, )):
raise ValueError('qid_data_list資料結構為列表')
es = connections.get_connection()
index_name = cls._doc_type.index
doc_type_name = cls.snake_case(cls.__name__)
def gen_qid_data():
for dt in qid_data_list:
yield {
'_index': index_name,
'_type': doc_type_name,
'_id': dt['qid'],
'_source': dt
}
bulk(es, gen_qid_data())
使用bulk批量插入資料到ES,在Python中bulk位於elasticsearch.helpers下,但是使用bulk並不是一次插入的資料量越大越好,當一次插入的資料量過大時,ES的寫效能反而會降低,具體跟ES硬體配置有關,我測試的一次插入3000道試題詳情資料會比一次2000道速度慢,3000道試題詳情大約30M左右。
如果追求寫入速度,還可以在寫入前將replicas副本設定為0,寫入完成後再將其設定為正常副本數,因為ES在寫入資料時會將資料寫一份到副本中,副本數越多寫入的速度會越慢,但一般不建議將replicas副本設定為0,因為如果在寫入資料的過程中ES宕機了可能會造成資料丟失。
四. ES配置優化問題
在ES的叢集配置中,master是ES選舉出來的,在一個由node1、node2、node3組成的叢集中初始狀態node1為主節點,node1由於網路問題與子節點失去聯絡,這時候ES重新選舉了node2為主節點,當node1網路恢復時,node1會維護自己的叢集node1為主節點,這時候叢集中就存在node1和node2兩個主節點了,並且維護不同的cluster state,這樣就會造成無法選出正確的master,這個問題就是腦裂問題。
腦裂問題的解決辦法(quorum機制):
quorum計算公式:quorum = 可選舉節點數/2 + 1
只有當可選舉節點數大於等於quorum時才可進行master選舉,在3個可選舉節點中,quorum=3/2+1=2 在node1失去網路響應後 node2和node3可選舉節點為2 可以選舉,當node1恢復後,node1只有一個節點,可選舉數為1,小於quorum,因此避免了腦裂問題;即設定discovery.zen.minimum_master_nodes:quorum,可避免腦裂問題