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,可避免腦裂問題