超詳細(xì)的Elasticsearch高性能優(yōu)化實(shí)踐
原創(chuàng)【51CTO.com原創(chuàng)稿件】之前分享的一篇《掌握它才說明你真正懂 Elasticsearch》相信讓大家對(duì) ES 的原理都有所了解,這篇將從 ES 的 API 應(yīng)用測(cè)試,性能優(yōu)化,開發(fā)使用等方面展開,深入學(xué)習(xí) ES。
ES 性能調(diào)優(yōu)
ES 的默認(rèn)配置,是綜合了數(shù)據(jù)可靠性、寫入速度、搜索實(shí)時(shí)性等因素。實(shí)際使用時(shí),我們需要根據(jù)公司要求,進(jìn)行偏向性的優(yōu)化。
寫優(yōu)化
假設(shè)我們的應(yīng)用場(chǎng)景要求是,每秒 300 萬(wàn)的寫入速度,每條 500 字節(jié)左右。
針對(duì)這種對(duì)于搜索性能要求不高,但是對(duì)寫入要求較高的場(chǎng)景,我們需要盡可能的選擇恰當(dāng)寫優(yōu)化策略。
綜合來說,可以考慮以下幾個(gè)方面來提升寫索引的性能:
- 加大 Translog Flush ,目的是降低 Iops、Writeblock。
- 增加 Index Refresh 間隔,目的是減少 Segment Merge 的次數(shù)。
- 調(diào)整 Bulk 線程池和隊(duì)列。
- 優(yōu)化節(jié)點(diǎn)間的任務(wù)分布。
- 優(yōu)化 Lucene 層的索引建立,目的是降低 CPU 及 IO。
①批量提交
ES 提供了 Bulk API 支持批量操作,當(dāng)我們有大量的寫任務(wù)時(shí),可以使用 Bulk 來進(jìn)行批量寫入。
每次提交的數(shù)據(jù)量為多少時(shí),能達(dá)到很好性能,主要受到文件大小、網(wǎng)絡(luò)情況、數(shù)據(jù)類型、集群狀態(tài)等因素影響。
通用的策略如下:Bulk 默認(rèn)設(shè)置批量提交的數(shù)據(jù)量不能超過 100M。數(shù)據(jù)條數(shù)一般是根據(jù)文檔的大小和服務(wù)器性能而定的,但是單次批處理的數(shù)據(jù)大小應(yīng)從 5MB~15MB 逐漸增加。
我們可以跟著,感受一下 Bulk 接口,如下所示:
- $ vi request
- $ cat request
- { "index" : { "_index" : "chandler","_type": "test", "_id" : "1" } }
- { "name" : "錢丁君","age": "18" }
- $ curl -s -H "Content-Type: application/json" -XPOST localhost:9200/_bulk --data-binary @request; echo
- {"took":214,"errors":false,"items":[{"index":{"_index":"chandler","_type":"test","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1,"status":201}}]}
- $ curl -XGET localhost:9200/chandler/test/1?pretty
- {
- "_index" : "chandler",
- "_type" : "test",
- "_id" : "1",
- "_version" : 1,
- "found" : true,
- "_source" : {
- "name" : "錢丁君",
- "age" : "18"
- }
- }
Bulk 不支持 Gget 操作,因?yàn)闆]什么用處。
②優(yōu)化存儲(chǔ)設(shè)備
ES 是一種密集使用磁盤的應(yīng)用,在段合并的時(shí)候會(huì)頻繁操作磁盤,所以對(duì)磁盤要求較高,當(dāng)磁盤速度提升之后,集群的整體性能會(huì)大幅度提高。
磁盤的選擇,提供以下幾點(diǎn)建議:
- 使用固態(tài)硬盤(Solid State Disk)替代機(jī)械硬盤。SSD 與機(jī)械磁盤相比,具有高效的讀寫速度和穩(wěn)定性。
- 使用 RAID 0。RAID 0 條帶化存儲(chǔ),可以提升磁盤讀寫效率。
- 在 ES 的服務(wù)器上掛載多塊硬盤。使用多塊硬盤同時(shí)進(jìn)行讀寫操作提升效率,在配置文件 ES 中設(shè)置多個(gè)存儲(chǔ)路徑,如下所示:
- path.data:/path/to/data1,/path/to/data2。
避免使用 NFS(Network File System)等遠(yuǎn)程存儲(chǔ)設(shè)備,網(wǎng)絡(luò)的延遲對(duì)性能的影響是很大的。
③合理使用合并
Lucene 以段的形式存儲(chǔ)數(shù)據(jù)。當(dāng)有新的數(shù)據(jù)寫入索引時(shí),Lucene 就會(huì)自動(dòng)創(chuàng)建一個(gè)新的段。
隨著數(shù)據(jù)量的變化,段的數(shù)量會(huì)越來越多,消耗的多文件句柄數(shù)及 CPU 就越多,查詢效率就會(huì)下降。
由于 Lucene 段合并的計(jì)算量龐大,會(huì)消耗大量的 I/O,所以 ES 默認(rèn)采用較保守的策略,讓后臺(tái)定期進(jìn)行段合并,如下所述:
- 索引寫入效率下降:當(dāng)段合并的速度落后于索引寫入的速度時(shí),ES 會(huì)把索引的線程數(shù)量減少到 1。
這樣可以避免出現(xiàn)堆積的段數(shù)量爆發(fā),同時(shí)在日志中打印出“now throttling indexing”INFO 級(jí)別的“警告”信息。
- 提升段合并速度:ES 默認(rèn)對(duì)段合并的速度是 20m/s,如果使用了 SSD,我們可以通過以下的命令將這個(gè)合并的速度增加到 100m/s。
- PUT /_cluster/settings
- {
- "persistent" : {
- "indices.store.throttle.max_bytes_per_sec" : "100mb"
- }
- }
④減少 Refresh 的次數(shù)
Lucene 在新增數(shù)據(jù)時(shí),采用了延遲寫入的策略,默認(rèn)情況下索引的 refresh_interval 為 1 秒。
Lucene 將待寫入的數(shù)據(jù)先寫到內(nèi)存中,超過 1 秒(默認(rèn))時(shí)就會(huì)觸發(fā)一次 Refresh,然后 Refresh 會(huì)把內(nèi)存中的的數(shù)據(jù)刷新到操作系統(tǒng)的文件緩存系統(tǒng)中。
如果我們對(duì)搜索的實(shí)效性要求不高,可以將 Refresh 周期延長(zhǎng),例如 30 秒。
這樣還可以有效地減少段刷新次數(shù),但這同時(shí)意味著需要消耗更多的Heap內(nèi)存。
如下所示:
- index.refresh_interval:30s
⑤加大 Flush 設(shè)置
Flush 的主要目的是把文件緩存系統(tǒng)中的段持久化到硬盤,當(dāng) Translog 的數(shù)據(jù)量達(dá)到 512MB 或者 30 分鐘時(shí),會(huì)觸發(fā)一次 Flush。
index.translog.flush_threshold_size 參數(shù)的默認(rèn)值是 512MB,我們進(jìn)行修改。
增加參數(shù)值意味著文件緩存系統(tǒng)中可能需要存儲(chǔ)更多的數(shù)據(jù),所以我們需要為操作系統(tǒng)的文件緩存系統(tǒng)留下足夠的空間。
⑥減少副本的數(shù)量
ES 為了保證集群的可用性,提供了 Replicas(副本)支持,然而每個(gè)副本也會(huì)執(zhí)行分析、索引及可能的合并過程,所以 Replicas 的數(shù)量會(huì)嚴(yán)重影響寫索引的效率。
當(dāng)寫索引時(shí),需要把寫入的數(shù)據(jù)都同步到副本節(jié)點(diǎn),副本節(jié)點(diǎn)越多,寫索引的效率就越慢。
如果我們需要大批量進(jìn)行寫入操作,可以先禁止 Replica 復(fù)制,設(shè)置 index.number_of_replicas: 0 關(guān)閉副本。在寫入完成后,Replica 修改回正常的狀態(tài)。
讀優(yōu)化
①避免大結(jié)果集和深翻
在上一篇講到了集群中的查詢流程,例如,要查詢從 from 開始的 size 條數(shù)據(jù),則需要在每個(gè)分片中查詢打分排名在前面的 from+size 條數(shù)據(jù)。
協(xié)同節(jié)點(diǎn)將收集到的n×(from+size)條數(shù)據(jù)聚合,再進(jìn)行一次排序,然后從 from+size 開始返回 size 條數(shù)據(jù)。
當(dāng) from、size 或者 n 中有一個(gè)值很大的時(shí)候,需要參加排序的數(shù)量也會(huì)增長(zhǎng),這樣的查詢會(huì)消耗很多 CPU 資源,從而導(dǎo)致效率的降低。
為了提升查詢效率,ES 提供了 Scroll 和 Scroll-Scan 這兩種查詢模式。
Scroll:是為檢索大量的結(jié)果而設(shè)計(jì)的。例如,我們需要查詢 1~100 頁(yè)的數(shù)據(jù),每頁(yè) 100 條數(shù)據(jù)。
如果使用 Search 查詢:每次都需要在每個(gè)分片上查詢得分較高的 from+100 條數(shù)據(jù),然后協(xié)同節(jié)點(diǎn)把收集到的 n×(from+100)條數(shù)據(jù)聚合起來再進(jìn)行一次排序。
每次返回 from+1 開始的 100 條數(shù)據(jù),并且要重復(fù)執(zhí)行 100 次。
如果使用 Scroll 查詢:在各個(gè)分片上查詢 10000 條數(shù)據(jù),協(xié)同節(jié)點(diǎn)聚合 n×10000 條數(shù)據(jù)進(jìn)行合并、排序,并將排名前 10000 的結(jié)果快照起來。這樣做的好處是減少了查詢和排序的次數(shù)。
Scroll 初始查詢的命令是:
- $ vim scroll
- $ cat scroll
- {
- "query": {
- "match": {
- "name": "錢丁君"
- }
- },
- "size":20
- }
- $ curl -s -H "Content-Type: application/json; charset=UTF-8" -XGET localhost:9200/chandler/test/_search?scroll=2m --data-binary @scroll; echo
- {"_scroll_id":"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAAGFlB6Y3QtNk9oUmdpc09Tb21rX2NXQXcAAAAAAAAABxZQemN0LTZPaFJnaXNPU29ta19jV0F3AAAAAAAAAAgWUHpjdC02T2hSZ2lzT1NvbWtfY1dBdwAAAAAAAAAJFlB6Y3QtNk9oUmdpc09Tb21rX2NXQXcAAAAAAAAAChZQemN0LTZPaFJnaXNPU29ta19jV0F3","took":14,"timed_out":false,"_shards":{"total":5,"successful":5,"skipped":0,"failed":0},"hits":{"total":1,"max_score":0.8630463,"hits":[{"_index":"chandler","_type":"test","_id":"1","_score":0.8630463,"_source":{ "name" : "錢丁君","age": "18" }}]}}
以上查詢語(yǔ)句的含義是,在 chandler 索引的 test type 里查詢字段 name 包含“錢丁君”的數(shù)據(jù)。
scroll=2m 表示下次請(qǐng)求的時(shí)間不能超過 2 分鐘,size 表示這次和后續(xù)的每次請(qǐng)求一次返回的數(shù)據(jù)條數(shù)。
在這次查詢的結(jié)果中除了返回了查詢到的結(jié)果,還返回了一個(gè) scroll_id,可以把它作為下次請(qǐng)求的參數(shù)。
再次請(qǐng)求的命令,如下所示:
因?yàn)檫@次并沒有到分片里查詢數(shù)據(jù),而是直接在生成的快照里面以游標(biāo)的形式獲取數(shù)據(jù)。
所以這次查詢并沒有包含 index 和 type,也沒有查詢條件:
- "scroll": "2m":指本次請(qǐng)求的時(shí)間不能超過 2 分鐘。
- scroll_id:是上次查詢時(shí)返回的 scroll_id。
Scroll-Scan:Scroll 是先做一次初始化搜索把所有符合搜索條件的結(jié)果緩存起來生成一個(gè)快照,然后持續(xù)地、批量地從快照里拉取數(shù)據(jù)直到?jīng)]有數(shù)據(jù)剩下。
而這時(shí)對(duì)索引數(shù)據(jù)的插入、刪除、更新都不會(huì)影響遍歷結(jié)果,因此 Scroll 并不適合用來做實(shí)時(shí)搜索。
其思路和使用方式與 Scroll 非常相似,但是 Scroll-Scan 關(guān)閉了 Scroll 中最耗時(shí)的文本相似度計(jì)算和排序,使得性能更加高效。
為了使用 Scroll-Scan,需要執(zhí)行一個(gè)初始化搜索請(qǐng)求,將 search_type 設(shè)置成 Scan,告訴 ES 集群不需要文本相似計(jì)算和排序,只是按照數(shù)據(jù)在索引中順序返回結(jié)果集:
- $ vi scroll
- $ cat scroll
- {
- "query": {
- "match": {
- "name": "錢丁君"
- }
- },
- "size":20,
- "sort": [
- "_doc"
- ]
- }
- $ curl -H "Content-Type: application/json; charset=UTF-8" -XGET 'localhost:9200/chandler/test/_search?scroll=2m&pretty=true' --data-binary @scroll
- {
- "_scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAABWFlB6Y3QtNk9oUmdpc09Tb21rX2NXQXcAAAAAAAAAVxZQemN0LTZPaFJnaXNPU29ta19jV0F3AAAAAAAAAFgWUHpjdC02T2hSZ2lzT1NvbWtfY1dBdwAAAAAAAABZFlB6Y3QtNk9oUmdpc09Tb21rX2NXQXcAAAAAAAAAWhZQemN0LTZPaFJnaXNPU29ta19jV0F3",
- "took" : 3,
- "timed_out" : false,
- "_shards" : {
- "total" : 5,
- "successful" : 5,
- "skipped" : 0,
- "failed" : 0
- },
- "hits" : {
- "total" : 1,
- "max_score" : null,
- "hits" : [
- {
- "_index" : "chandler",
- "_type" : "test",
- "_id" : "1",
- "_score" : null,
- "_source" : {
- "name" : "錢丁君",
- "age" : "18"
- },
- "sort" : [
- 0
- ]
- }
- ]
- }
- }
注意:Elasticsearch 2.1.0 版本之后移除了 search_type=scan,使用 "sort": [ "_doc"] 進(jìn)行代替。
- Scroll 和 Scroll-Scan 有一些差別,如下所示:
- Scroll-Scan不進(jìn)行文本相似度計(jì)算,不排序,按照索引中的數(shù)據(jù)順序返回。
- Scroll-Scan 不支持聚合操作。
Scroll-Scan 的參數(shù) Size 代表著每個(gè)分片上的請(qǐng)求的結(jié)果數(shù)量,每次返回 n×size 條數(shù)據(jù)。而 Scroll 每次返回 size 條數(shù)據(jù)。
②選擇合適的路由
ES 中所謂的路由和 IP 網(wǎng)絡(luò)不同,是一個(gè)類似于 Tag 的東西。在創(chuàng)建文檔的時(shí)候,可以通過字段為文檔增加一個(gè)路由屬性的 Tag。在多分片的 ES 集群中,對(duì)搜索的查詢大致分為如下兩種。
ES 內(nèi)在機(jī)制決定了擁有相同路由屬性的文檔,一定會(huì)被分配到同一個(gè)分片上,無論是主分片還是副本。
查詢時(shí)可以根據(jù) Routing 信息,直接定位到目標(biāo)分片,避免查詢所有的分片,再經(jīng)過協(xié)調(diào)節(jié)點(diǎn)二次排序。
如圖 1 所示:
圖 1
如果在查詢條件中不包含 Routing,在查詢時(shí)就遍歷所有分片,整個(gè)查詢主要分為 Scatter、Gather 兩個(gè)過程:
- Scatter(分發(fā)):請(qǐng)求到達(dá)協(xié)調(diào)節(jié)點(diǎn)之后,協(xié)調(diào)節(jié)點(diǎn)將查詢請(qǐng)求分發(fā)給每個(gè)分片。
- Gather(聚合):協(xié)調(diào)點(diǎn)在每個(gè)分片上完成搜索,再將搜索到的結(jié)果集進(jìn)行排序,將結(jié)果數(shù)據(jù)返回給用戶。
如圖 2 所示:
圖 2
通過對(duì)比上述兩種查詢流程,我們不難發(fā)現(xiàn),使用 Routing 信息查找的效率很高,避免了多余的查詢。
所以我們?cè)谠O(shè)計(jì) Elasticsearch Mapping 時(shí)要合理地利用 Routing 信息,來提升查詢的效率。
例如,在大型的本地分類網(wǎng)站中,可以將城市 ID 作為 Routing 的條件,讓同一個(gè)城市的數(shù)據(jù)落在相同的分片中。
默認(rèn)的公式如下:
- shard = hash(routing)%number_of_primary_shards
不過需要注意的是,根據(jù)城市 ID 進(jìn)行分片時(shí),也會(huì)容易出現(xiàn)分片不均勻的情況。
例如,大型城市的數(shù)據(jù)過多,而小城市的數(shù)據(jù)太少,導(dǎo)致分片之間的數(shù)據(jù)量差異很大。
這時(shí)就可以進(jìn)行必要的調(diào)整,比如把多個(gè)小城市的數(shù)據(jù)合并到一個(gè)分片上,把大城市的數(shù)據(jù)按區(qū)域進(jìn)行拆分到不同分配。
③SearchType
在 Scatter、Gather 的過程中,節(jié)點(diǎn)間的數(shù)據(jù)傳輸和打分(SearchType),可以根據(jù)不同的場(chǎng)景選擇。
如下所示:
- QUERY_THEN_FETCH:ES 默認(rèn)的搜索方式。先向所有的分片發(fā)請(qǐng)求,各分片只返回文檔的相似度得分和文檔的 ID,然后協(xié)調(diào)節(jié)點(diǎn)按照各分片返回的分?jǐn)?shù)進(jìn)行重新排序和排名,再取出需要返回給客戶端的 Size 個(gè)文檔 ID。
第 2 步,在相關(guān)的分片中取出文檔的詳細(xì)信息并返回給用戶。
- QUERY_AND_FETCH:協(xié)調(diào)節(jié)點(diǎn)向所有分片發(fā)送查詢請(qǐng)求,各分片將文檔的相似度得分和文檔的詳細(xì)信息一起返回。
然后,協(xié)調(diào)節(jié)點(diǎn)進(jìn)行重新排序,再取出需要返回給客戶端的數(shù)據(jù),將其返回給客戶端。由于只需要在分片中查詢一次,所以性能很好。
- DFS_QUERY_THEN_FETCH:與 QUERY_THEN_FETCH 類似,但它包含一個(gè)額外的階段:在初始查詢中執(zhí)行全局的詞頻計(jì)算,以使得更精確地打分,從而讓查詢結(jié)果更相關(guān)。
QUERY_THEN_FETCH 使用的是分片內(nèi)部的詞頻信息,而 DFS_QUERY_THEN_FETCH 使用訪問公共的詞頻信息,所以相比 QUERY_THEN_FETCH 性能更低。
- DFS_QUERY_AND_FETCH:與 QUERY_AND_FETCH 類似,不過使用的是全局的詞頻。
④定期刪除
由于在 Lucene 中段具有不變性,每次進(jìn)行刪除操作后不會(huì)立即從硬盤中進(jìn)行實(shí)際的刪除,而是產(chǎn)生一個(gè) .del 文件記錄刪除動(dòng)作。
隨著刪除操作的增長(zhǎng),.del 文件會(huì)越來也多。當(dāng)我們進(jìn)行查詢操作的時(shí)候,被刪除的數(shù)據(jù)還會(huì)參與檢索中,然后根據(jù) .del 文件進(jìn)行過濾。.del 文件越多,查詢過濾過程越長(zhǎng),進(jìn)而影響查詢的效率。
當(dāng)機(jī)器空閑時(shí),我們可以通過如下命令刪除文件,來提升查詢的效率:
- $ curl -XPOST localhost:9200/chandler/_forcemerge?only_expunge_deletes=true
- {"_shards":{"total":10,"successful":5,"failed":0}}
定期對(duì)不再更新的索引做 optimize (ES 2.0 以后更改為 Force Merge API)。
這 Optimze 的實(shí)質(zhì)是對(duì) Segment File 強(qiáng)制做合并,可以節(jié)省大量的 Segment Memory。
堆大小的設(shè)置
ES 默認(rèn)安裝后設(shè)置的內(nèi)存是 1GB,對(duì)于任何一個(gè)現(xiàn)實(shí)業(yè)務(wù)來說,這個(gè)設(shè)置都太小了。
如果是通過解壓安裝的 ES,則在 ES 安裝文件中包含一個(gè) jvm.option 文件,添加如下命令來設(shè)置 ES 的堆大?。?/p>
- -Xms10g
- -Xmx10g
Xms 表示堆的初始大小,Xmx 表示可分配的內(nèi)存,都是 10GB。
確保 Xmx 和 Xms 的大小是相同的,其目的是為了能夠在 Java 垃圾回收機(jī)制清理完堆區(qū)后不需要重新分隔計(jì)算堆區(qū)的大小而浪費(fèi)資源,可以減輕伸縮堆大小帶來的壓力。
也可以通過設(shè)置環(huán)境變量的方式設(shè)置堆的大小。服務(wù)進(jìn)程在啟動(dòng)時(shí)候會(huì)讀取這個(gè)變量,并相應(yīng)的設(shè)置堆的大小。比如:
- export ES_HEAP_SIEZE=10g
也可以通過命令行參數(shù)的形式,在程序啟動(dòng)的時(shí)候把內(nèi)存大小傳遞給 ES,如下所示:
- ./bin/elasticsearch -Xmx10g -Xms10g
這種設(shè)置方式是一次性的,在每次啟動(dòng) ES 時(shí)都需要添加。
假設(shè)你有一個(gè) 64G 內(nèi)存的機(jī)器,按照正常思維思考,你可能會(huì)認(rèn)為把 64G 內(nèi)存都給 ES 比較好,但現(xiàn)實(shí)是這樣嗎, 越大越好?雖然內(nèi)存對(duì) ES 來說是非常重要的,但是答案是否定的!
因?yàn)?ES 堆內(nèi)存的分配需要滿足以下兩個(gè)原則:
- 不要超過物理內(nèi)存的 50%:Lucene 的設(shè)計(jì)目的是把底層 OS 里的數(shù)據(jù)緩存到內(nèi)存中。
Lucene 的段是分別存儲(chǔ)到單個(gè)文件中的,這些文件都是不會(huì)變化的,所以很利于緩存,同時(shí)操作系統(tǒng)也會(huì)把這些段文件緩存起來,以便更快的訪問。
如果我們?cè)O(shè)置的堆內(nèi)存過大,Lucene 可用的內(nèi)存將會(huì)減少,就會(huì)嚴(yán)重影響降低 Lucene 的全文本查詢性能。
- 堆內(nèi)存的大小不要超過 32GB:在 Java 中,所有對(duì)象都分配在堆上,然后有一個(gè) Klass Pointer 指針指向它的類元數(shù)據(jù)。
這個(gè)指針在 64 位的操作系統(tǒng)上為 64 位,64 位的操作系統(tǒng)可以使用更多的內(nèi)存(2^64)。在 32 位的系統(tǒng)上為 32 位,32 位的操作系統(tǒng)的尋址空間為 4GB(2^32)。
但是 64 位的指針意味著更大的浪費(fèi),因?yàn)槟愕闹羔槺旧泶罅?。浪費(fèi)內(nèi)存不算,更糟糕的是,更大的指針在主內(nèi)存和緩存器(例如 LLC, L1等)之間移動(dòng)數(shù)據(jù)的時(shí)候,會(huì)占用更多的帶寬。
Java 使用內(nèi)存指針壓縮(Compressed Oops)技術(shù)來解決這個(gè)問題。它的指針不再表示對(duì)象在內(nèi)存中的精確位置,而是表示偏移量。
這意味著 32 位的指針可以引用 4GB 個(gè) Byte,而不是 4GB 個(gè) bit。也就是說,當(dāng)堆內(nèi)存為 32GB 的物理內(nèi)存時(shí),也可以用 32 位的指針表示。
不過,在越過那個(gè)神奇的邊界 32GB 時(shí),指針就會(huì)變?yōu)槠胀▽?duì)象的指針,每個(gè)對(duì)象的指針都變長(zhǎng)了,就會(huì)浪費(fèi)更多的內(nèi)存,降低了 CPU 的性能,還要讓 GC 應(yīng)對(duì)更大的內(nèi)存。
事實(shí)上,當(dāng)內(nèi)存到達(dá) 40~40GB 時(shí),有效的內(nèi)存才相當(dāng)于內(nèi)存對(duì)象指針壓縮技術(shù)時(shí)的 32GB 內(nèi)存。
所以即便你有足夠的內(nèi)存,也盡量不要超過 32G,比如我們可以設(shè)置為 31GB:
- -Xms31g
- -Xmx31g
32GB 是 ES 一個(gè)內(nèi)存設(shè)置限制,那如果你的機(jī)器有很大的內(nèi)存怎么辦呢?現(xiàn)在的機(jī)器內(nèi)存普遍增長(zhǎng),甚至可以看到有 300-500GB 內(nèi)存的機(jī)器。
這時(shí)我們需要根據(jù)業(yè)務(wù)場(chǎng)景,進(jìn)行恰當(dāng)內(nèi)存的分配:
- 業(yè)務(wù)場(chǎng)景是以全文檢索為主:依然可以給 ES 分配小于 32GB 的堆內(nèi)存,剩下的交給 Lucene 用作操作系統(tǒng)的文件系統(tǒng)緩存,所有的 Segment 都緩存起來,會(huì)加快全文檢索。
- 業(yè)務(wù)場(chǎng)景中有很多的排序和聚合:我們可以考慮一臺(tái)機(jī)器上創(chuàng)建兩個(gè)或者更多 ES 節(jié)點(diǎn),而不要部署一個(gè)使用 32+GB 內(nèi)存的節(jié)點(diǎn)。
仍然要堅(jiān)持 50% 原則,假設(shè)你有個(gè)機(jī)器有 128G 內(nèi)存,你可以創(chuàng)建兩個(gè) Node,使用 32G 內(nèi)存。也就是說 64G 內(nèi)存給 ES 的堆內(nèi)存,剩下的 64G 給 Lucene。
服務(wù)器配置的選擇
Swapping 是性能的墳?zāi)梗涸谶x擇 ES 服務(wù)器時(shí),要盡可能地選擇與當(dāng)前應(yīng)用場(chǎng)景相匹配的服務(wù)器。
如果服務(wù)器配置很低,則意味著需要更多的節(jié)點(diǎn),節(jié)點(diǎn)數(shù)量的增加會(huì)導(dǎo)致集群管理的成本大幅度提高。
如果服務(wù)器配置很高,而在單機(jī)上運(yùn)行多個(gè)節(jié)點(diǎn)時(shí),也會(huì)增加邏輯的復(fù)雜度。
在計(jì)算機(jī)中運(yùn)行的程序均需在內(nèi)存執(zhí)行,若內(nèi)存消耗殆盡將導(dǎo)致程序無法進(jìn)行。為了解決這個(gè)問題,操作系統(tǒng)使用一種叫作虛擬內(nèi)存的技術(shù)。
當(dāng)內(nèi)存耗盡時(shí),操作系統(tǒng)就會(huì)自動(dòng)把內(nèi)存中暫時(shí)不使用的數(shù)據(jù)交換到硬盤中,需要使用的時(shí)候再?gòu)挠脖P交換到內(nèi)存。
如果內(nèi)存交換到磁盤上需要 10 毫秒,從磁盤交換到內(nèi)存需要 20 毫秒,那么多的操作時(shí)延累加起來,將導(dǎo)致幾何級(jí)增長(zhǎng)。
不難看出 Swapping 對(duì)于性能是多么可怕。所以為了使 ES 有更好等性能,強(qiáng)烈建議關(guān)閉 Swap。
關(guān)閉 Swap 的方式如下:
①暫時(shí)禁用。如果我們想要在 Linux 服務(wù)器上暫時(shí)關(guān)閉,可以執(zhí)行如下命令,但在服務(wù)器重啟后失效:
- sudo swapoff -a
②關(guān)閉。我們可以修改 /etc/sysctl.conf(不同的操作系統(tǒng)路徑有可能不同),增加如下參數(shù):
- vm.swappiness = 1 //0-100,則表示越傾向于使用虛擬內(nèi)存。
注意:Swappiness 設(shè)置為 1 比設(shè)置為 0 要好,因?yàn)樵谝恍﹥?nèi)核版本,Swappness=0 會(huì)引發(fā) OOM(內(nèi)存溢出)。
Swappiness 默認(rèn)值為 60,當(dāng)設(shè)置為 0 時(shí),在某些操作系統(tǒng)中有可能會(huì)觸發(fā)系統(tǒng)級(jí)的 OOM-killer,例如在 Linux 內(nèi)核的內(nèi)存不足時(shí),為了防止系統(tǒng)的崩潰,會(huì)自動(dòng)強(qiáng)制 Kill 一個(gè)“bad”進(jìn)程。
③在 ES 中設(shè)置。如果上面的方法都不能做到,你需要打開配置文件中的 mlockall 開關(guān),它的作用就是運(yùn)行 JVM 鎖住內(nèi)存,禁止 OS 交換出去。
在 elasticsearch.yml 配置如下:
- bootstrap.mlockall: true
硬盤的選擇和設(shè)置
所以,如果條件允許,則請(qǐng)盡可能地使用 SSD,它的讀寫性能將遠(yuǎn)遠(yuǎn)超出任何旋轉(zhuǎn)介質(zhì)的硬盤(如機(jī)械硬盤、磁帶等)?;?SSD 的 ES 集群節(jié)點(diǎn)對(duì)于查詢和索引性能都有提升。
另外無論是使用固態(tài)硬盤還是使用機(jī)械硬盤,我們都建議將磁盤的陣列模式設(shè)置為 RAID 0,以此來提升磁盤的寫性能。
接入方式
ES 提供了 Transport Client(傳輸客戶端)和 Node Client(節(jié)點(diǎn)客戶端)的接入方式,這兩種方式各有利弊,分別對(duì)應(yīng)不同的應(yīng)用場(chǎng)景。
①Transport Client:作為一個(gè)集群和應(yīng)用程序之間的通信層,和集群是安全解耦的。
由于與集群解耦,所以在連接集群和銷毀連接時(shí)更加高效,適合大量的客戶端連接。
②Node Client:把應(yīng)用程序當(dāng)作一個(gè)集群中的 Client 節(jié)點(diǎn)(非 Data 和 Master 節(jié)點(diǎn))。
由于它是集群的一個(gè)內(nèi)部節(jié)點(diǎn),意味著它可以感知整個(gè)集群的狀態(tài)、所有節(jié)點(diǎn)的分布情況、分片的分布狀況等。
由于 Node Client 是集群的一部分,所以在接入和退出集群時(shí)進(jìn)行比較復(fù)雜操作,并且還會(huì)影響整個(gè)集群的狀態(tài),所以 Node Client 更適合少量客戶端,能夠提供更好的執(zhí)行效率。
角色隔離和腦裂
①角色隔離
ES 集群中的數(shù)據(jù)節(jié)點(diǎn)負(fù)責(zé)對(duì)數(shù)據(jù)進(jìn)行增、刪、改、查和聚合等操作,所以對(duì) CPU、內(nèi)存和 I/O 的消耗很大。
在搭建 ES 集群時(shí),我們應(yīng)該對(duì) ES 集群中的節(jié)點(diǎn)進(jìn)行角色劃分和隔離。
候選主節(jié)點(diǎn):
- node.master=true
- node.data=false
數(shù)據(jù)節(jié)點(diǎn):
- node.master=false
- node.data=true
形成如圖 3 所示的邏輯劃分:
圖 3
②避免腦裂
網(wǎng)絡(luò)異常可能會(huì)導(dǎo)致集群中節(jié)點(diǎn)劃分出多個(gè)區(qū)域,區(qū)域發(fā)現(xiàn)沒有 Master 節(jié)點(diǎn)的時(shí)候,會(huì)選舉出了自己區(qū)域內(nèi) Maste 節(jié)點(diǎn) r,導(dǎo)致一個(gè)集群被分裂為多個(gè)集群,使集群之間的數(shù)據(jù)無法同步,我們稱這種現(xiàn)象為腦裂。
為了防止腦裂,我們需要在 Master 節(jié)點(diǎn)的配置文件中添加如下參數(shù):
- discovery.zen.minimum_master_nodes=(master_eligible_nodes/2)+1 //默認(rèn)值為1
其中 master_eligible_nodes 為 Master 集群中的節(jié)點(diǎn)數(shù)。這樣做可以避免腦裂的現(xiàn)象都出現(xiàn),提升集群的高可用性。
只要不少于 discovery.zen.minimum_master_nodes 個(gè)候選節(jié)點(diǎn)存活,選舉工作就可以順利進(jìn)行。
ES 實(shí)戰(zhàn)
ES 配置說明
在 ES 安裝目錄下的 Conf 文件夾中包含了一個(gè)重要的配置文件:elasticsearch.yaml。
ES 的配置信息有很多種,大部分配置都可以通過 elasticsearch.yaml 和接口的方式進(jìn)行。
下面我們列出一些比較重要的配置信息:
- cluster.name:elasticsearch:配置 ES 的集群名稱,默認(rèn)值是 ES,建議改成與所存數(shù)據(jù)相關(guān)的名稱,ES 會(huì)自動(dòng)發(fā)現(xiàn)在同一網(wǎng)段下的集群名稱相同的節(jié)點(diǎn)。
- node.nam: "node1":集群中的節(jié)點(diǎn)名,在同一個(gè)集群中不能重復(fù)。節(jié)點(diǎn)的名稱一旦設(shè)置,就不能再改變了。當(dāng)然,也可以設(shè)置成服務(wù)器的主機(jī)名稱,例如 node.name:${HOSTNAME}。
- noed.master:true:指定該節(jié)點(diǎn)是否有資格被選舉成為 Master 節(jié)點(diǎn),默認(rèn)是 True,如果被設(shè)置為 True,則只是有資格成為 Master 節(jié)點(diǎn),具體能否成為 Master 節(jié)點(diǎn),需要通過選舉產(chǎn)生。
- node.data:true:指定該節(jié)點(diǎn)是否存儲(chǔ)索引數(shù)據(jù),默認(rèn)為 True。數(shù)據(jù)的增、刪、改、查都是在 Data 節(jié)點(diǎn)完成的。
- index.number_of_shards:5:設(shè)置都索引分片個(gè)數(shù),默認(rèn)是 5 片。也可以在創(chuàng)建索引時(shí)設(shè)置該值,具體設(shè)置為多大都值要根據(jù)數(shù)據(jù)量的大小來定。如果數(shù)據(jù)量不大,則設(shè)置成 1 時(shí)效率很高。
- index.number_of_replicas:1:設(shè)置默認(rèn)的索引副本個(gè)數(shù),默認(rèn)為 1 個(gè)。副本數(shù)越多,集群的可用性越好,但是寫索引時(shí)需要同步的數(shù)據(jù)越多。
- path.conf:/path/to/conf:設(shè)置配置文件的存儲(chǔ)路徑,默認(rèn)是 ES 目錄下的 Conf 文件夾。建議使用默認(rèn)值。
- path.data:/path/to/data1,/path/to/data2:設(shè)置索引數(shù)據(jù)多存儲(chǔ)路徑,默認(rèn)是 ES 根目錄下的 Data 文件夾。切記不要使用默認(rèn)值,因?yàn)槿?ES 進(jìn)行了升級(jí),則有可能數(shù)據(jù)全部丟失。
可以用半角逗號(hào)隔開設(shè)置的多個(gè)存儲(chǔ)路徑,在多硬盤的服務(wù)器上設(shè)置多個(gè)存儲(chǔ)路徑是很有必要的。
- path.logs:/path/to/logs:設(shè)置日志文件的存儲(chǔ)路徑,默認(rèn)是 ES 根目錄下的 Logs,建議修改到其他地方。
- path.plugins:/path/to/plugins:設(shè)置第三方插件的存放路徑,默認(rèn)是 ES 根目錄下的 Plugins 文件夾。
- bootstrap.mlockall:true:設(shè)置為 True 時(shí)可鎖住內(nèi)存。因?yàn)楫?dāng) JVM 開始 Swap 時(shí),ES 的效率會(huì)降低,所以要保證它不 Swap。
- network.bind_host:192.168.0.1:設(shè)置本節(jié)點(diǎn)綁定的 IP 地址,IP 地址類型是 IPv4 或 IPv6,默認(rèn)為 0.0.0.0。
- network.publish_host:192.168.0.1:設(shè)置其他節(jié)點(diǎn)和該節(jié)點(diǎn)交互的 IP 地址,如果不設(shè)置,則會(huì)進(jìn)行自我判斷。
- network.host:192.168.0.1:用于同時(shí)設(shè)置 bind_host 和 publish_host 這兩個(gè)參數(shù)。
- http.port:9200:設(shè)置對(duì)外服務(wù)的 HTTP 端口,默認(rèn)為 9200。ES 的節(jié)點(diǎn)需要配置兩個(gè)端口號(hào),一個(gè)對(duì)外提供服務(wù)的端口號(hào),一個(gè)是集群內(nèi)部使用的端口號(hào)。
- http.port 設(shè)置的是對(duì)外提供服務(wù)的端口號(hào)。注意,如果在一個(gè)服務(wù)器上配置多個(gè)節(jié)點(diǎn),則切記對(duì)端口號(hào)進(jìn)行區(qū)分。
- transport.tcp.port:9300:設(shè)置集群內(nèi)部的節(jié)點(diǎn)間交互的 TCP 端口,默認(rèn)是 9300。注意,如果在一個(gè)服務(wù)器配置多個(gè)節(jié)點(diǎn),則切記對(duì)端口號(hào)進(jìn)行區(qū)分。
- transport.tcp.compress:true:設(shè)置在節(jié)點(diǎn)間傳輸數(shù)據(jù)時(shí)是否壓縮,默認(rèn)為 False,不壓縮。
- discovery.zen.minimum_master_nodes:1:設(shè)置在選舉 Master 節(jié)點(diǎn)時(shí)需要參與的最少的候選主節(jié)點(diǎn)數(shù),默認(rèn)為 1。如果使用默認(rèn)值,則當(dāng)網(wǎng)絡(luò)不穩(wěn)定時(shí)有可能會(huì)出現(xiàn)腦裂。
合理的數(shù)值為(master_eligible_nodes/2)+1,其中 master_eligible_nodes 表示集群中的候選主節(jié)點(diǎn)數(shù)。
- discovery.zen.ping.timeout:3s:設(shè)置在集群中自動(dòng)發(fā)現(xiàn)其他節(jié)點(diǎn)時(shí) Ping 連接的超時(shí)時(shí)間,默認(rèn)為 3 秒。
在較差的網(wǎng)絡(luò)環(huán)境下需要設(shè)置得大一點(diǎn),防止因誤判該節(jié)點(diǎn)的存活狀態(tài)而導(dǎo)致分片的轉(zhuǎn)移。
常用接口
雖然現(xiàn)在有很多開源軟件對(duì) ES 的接口進(jìn)行了封裝,使我們可以很方便、直觀地監(jiān)控集群的狀況,但是在 ES 5 以后,很多軟件開始收費(fèi)。
了解常用的接口有助于我們?cè)诔绦蚧蛘吣_本中查看我們的集群情況,以下接口適用于 ES 6.5.2 版本。
①索引類接口
通過下面的接口創(chuàng)建一個(gè)索引名稱為 indexname 且包含 3 個(gè)分片、1 個(gè)副本的索引:
- PUT http://localhost:9200/indexname?pretty
- content-type →application/json; charset=UTF-8
- {
- "settings":{
- "number_of_shards" : 3,
- "number_of_replicas" : 1
- }
- }
通過下面都接口刪除索引:
- DELETE http://localhost:9200/indexname
通過該接口就可以刪除索引名稱為 indexname 的索引,通過下面的接口可以刪除多個(gè)索引:
- DELETE http://localhost:9200/indexname1,indexname2
- DELETE http://localhost:9200/indexname*
通過下面的接口可以刪除集群下的全部索引:
- DELETE http://localhost:9200/_all
- DELETE http://localhost:9200/*
進(jìn)行全部索引刪除是很危險(xiǎn)的,我們可以通過在配置文件中添加下面的配置信息,來關(guān)閉使用 _all 和使用通配符刪除索引的接口,使用刪除索引職能通過索引的全稱進(jìn)行。
- action.destructive_requires_name: true
通過下面的接口獲取索引的信息,其中,Pretty 參數(shù)用語(yǔ)格式化輸出結(jié)構(gòu),以便更容易閱讀:
- GET http://localhost:9200/indexname?pretty
通過下面的接口關(guān)閉、打開索引:
- POST http://localhost:9200/indexname/_close
- POST http://localhost:9200/indexname/_open
通過下面的接口獲取一個(gè)索引中具體 Type 的 Mapping 映射:
- GET http://localhost:9200/indexname/typename/_mapping?pretty
當(dāng)一個(gè)索引中有多個(gè) Type 時(shí),獲得 Mapping 時(shí)要加上 Typename。
②Document 操作
安裝 ES 和 Kibana 之后,進(jìn)入 Kibana 操作頁(yè)面,然后進(jìn)去的 DevTools 執(zhí)行下面操作:
- #添加一條document
- PUT /test_index/test_type/1
- {
- "test_content":"test test"
- }
- #查詢
- GET /test_index/test_type/1
- #返回
- {
- "_index" : "test_index",
- "_type" : "test_type",
- "_id" : "1",
- "_version" : 2,
- "found" : true,
- "_source" : {
- "test_content" : "test test"
- }
- }
put/index/type/id 說明如下:
- _index 元數(shù)據(jù):代表這個(gè) Document 存放在哪個(gè) Idnex 中,類似的數(shù)據(jù)放在一個(gè)索引,非類似的數(shù)據(jù)放不同索引,Index 中包含了很多類似的 Document。
- _type 元數(shù)據(jù):代表 Document 屬于 Index 中的哪個(gè)類別(type),一個(gè)索引通常會(huì)劃分多個(gè) Type,邏輯上對(duì) Index 中有些許不同的幾類數(shù)據(jù)進(jìn)行分類。
- _id 元數(shù)據(jù):代表 Document 的標(biāo)識(shí),id 與 Index 和 Type 一起,可以標(biāo)識(shí)和定位一個(gè) Document,可以理解為數(shù)據(jù)庫(kù)中主鍵。我們可以指定 Document 的 id,也可以不指定,由ES自動(dòng)為我們創(chuàng)建一個(gè) id。
接口應(yīng)用
①Search 接口
Search 是我們最常用的 API,ES 給我提供了豐富的查詢條件,比如模糊匹配 Match,字段判空 Exists,精準(zhǔn)匹配 Term 和 Terms,范圍匹配 Range:
- GET /_search
- {
- "query": {
- "bool": {
- "must": [ //must_not
- { "match": { "title": "Search" }},
- { "match": { "content": "Elasticsearch" }},
- {"exists":{"field":"字段名"}} //判斷字段是否為空
- ],
- "filter": [
- { "term": { "status": "published" }},
- { "terms": { "status": [0,1,2,3] }},//范圍
- { "range": { "publish_date": { "gte": "2015-01-01" }}} //范圍gte:大于等于;gt:大于;lte:小于等于;lt:小于
- ]
- }
- }
- }
查詢索引為 test_index,doc 類型為 test_type 的數(shù)據(jù):
- GET /test_index/test_type/_search
查詢索引為 test_index,doc 類型為 test_type,docment 字段 num10 為 4 的數(shù)據(jù):
- GET /test_index/test_type/_search?pretty=true
- {
- "query": {
- "bool": {
- "filter": [
- { "term": { "num10": 4 }}
- ]
- }
- }
- }
更多查詢條件的組合,大家可以自行測(cè)試。
②修改 Mapping
- PUT /my_index/_mapping/my_type
- {
- "properties": {
- "new_field_name": {
- "type": "string" //字段類型,string、long、boolean、ip
- }
- }
- }
如上是修改 Mapping 結(jié)構(gòu),然后利用腳本 Script 給字段賦值:
- POST my_index/_update_by_query
- {
- "script": {
- "lang": "painless",
- "inline": "ctx._source.new_field_name= '02'"
- }
- }
③修改別名
如下給 Index 為 test_index 的數(shù)據(jù)綁定 Alias 為 test_alias:
- POST /_aliases
- {
- "actions": [
- {
- "add": { //add,remove
- "index": "test_index",
- "alias": "test_alias"
- }
- }
- ]
- }
驗(yàn)證別名關(guān)聯(lián),根據(jù)別名來進(jìn)行數(shù)據(jù)查詢,如下:
- GET /test_alias/test_type/3
④定制返回內(nèi)容
_source 元數(shù)據(jù):就是說,我們?cè)趧?chuàng)建一個(gè) Document 的時(shí)候,使用的那個(gè)放在 Request Body 中的 Json 串(所有的 Field),默認(rèn)情況下,在 Get 的時(shí)候,會(huì)原封不動(dòng)的給我們返回回來。
定制返回的結(jié)果,指定 _source 中,返回哪些 Field:
- #語(yǔ)法:
- GET /test_index/test_type/1?_source=test_field2
- #返回
- {
- "_index" : "test_index",
- "_type" : "test_type",
- "_id" : "1",
- "_version" : 3,
- "found" : true,
- "_source" : {
- "test_field2" : "test field2"
- }
- }
- #也可返回多個(gè)field使用都好分割
- GET /test_index/test_type/1?_source=test_field2,test_field1
Java 封裝
組件 elasticsearch.jar 提供了豐富 API,不過不利于我們理解和學(xué)習(xí),現(xiàn)在我們自己來進(jìn)行封裝。
組件 API 使用 RestClient 封裝 Document 查詢接口:
- /**
- * @param index
- * @param type
- * @param id
- * @param fields
- * 查詢返回字段,可空
- * @return
- * @throws Exception
- * @Description:
- * @create date 2019年4月3日下午3:12:40
- */
- public String document(String index, String type, String id, List<String> fields) throws Exception {
- Map<String, String> paramsMap = new HashMap<>();
- paramsMap.put("pretty", "true");
- if (null != fields && fields.size() != 0) {
- String fieldValue = "";
- for (String field : fields) {
- fieldValue += field + ",";
- }
- if (!"".equals(fieldValue)) {
- paramsMap.put("_source", fieldValue);
- }
- }
- return CommonUtils.toString(es.getRestClient()
- .performRequest("GET", "/" + index + "/" + type + "/" + id, paramsMap).getEntity().getContent());
- }
工程使用,封裝:
- public String searchDocument(String index, String type, String id, List<String> fields) {
- try {
- return doc.document(index, type, id, fields);
- } catch (Exception e) {
- log.error(e.getMessage());
- ExceptionLogger.log(e);
- throw new RuntimeException("ES查詢失敗");
- }
- }
測(cè)試用例,代碼如下:
- /**
- * ES交互驗(yàn)證-查詢、更新等等操作
- *
- * @version
- * @author 錢丁君-chandler 2019年4月3日上午10:27:28
- * @since 1.8
- */
- @RunWith(SpringRunner.class)
- @SpringBootTest(classes = Bootstrap.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
- public class ESManagerTest {
- @Autowired
- private ESBasicManager esBasicManager;
- @Test
- public void query() {
- String result = esBasicManager.searchDocument(ESTagMetadata.INDEX_ALIAS, ESTagMetadata.DOC_TYPE,
- "188787665220752824", ImmutableList.of("signup_time", "tag_days_no_visit_after_1_order"));
- System.out.println("----------->" + result);
- }
- }
控臺(tái)輸出:
- ----------->{
- "_index" : "crm_tag_idx_20181218_708672",
- "_type" : "crm_tag_type",
- "_id" : "188787665220752824",
- "_version" : 1,
- "found" : true,
- "_source" : {
- "signup_time" : "2017-12-24",
- "tag_days_no_visit_after_1_order" : "339"
- }
- }
我只是拋磚引玉,大家可以自行進(jìn)行各種操作的封裝,不管對(duì)于理解 ES 的使用,還是對(duì)代碼質(zhì)量提升都有很多幫助。
作者:錢丁君
簡(jiǎn)介:就職于永輝云創(chuàng),擔(dān)任基礎(chǔ)架構(gòu)開發(fā),有多年基礎(chǔ)架構(gòu)經(jīng)驗(yàn),主要從事電商新零售、互聯(lián)網(wǎng)金融行業(yè)。技術(shù)發(fā)燒友,涉獵廣泛。熟悉 Java 微服務(wù)架構(gòu)搭建、推進(jìn)、衍化;多種中間件搭建、封裝和優(yōu)化;自動(dòng)化測(cè)試開發(fā)、代碼規(guī)約插件開發(fā)、代碼規(guī)范推進(jìn);容器化技術(shù) Docker、容器化編排技術(shù) Kubernetes,有較為豐富的運(yùn)維經(jīng)驗(yàn)。
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】