超詳細的Elasticsearch高性能優化實踐


ES 性能調優

 

ES 的默認配置,是綜合了數據可靠性、寫入速度、搜索實時性等因素。實際使用時,我們需要根據公司要求,進行偏向性的優化。

 

寫優化

 

假設我們的應用場景要求是,每秒 300 萬的寫入速度,每條 500 字節左右。

 

針對這種對於搜索性能要求不高,但是對寫入要求較高的場景,我們需要盡可能的選擇恰當寫優化策略。

 

綜合來說,可以考慮以下幾個方面來提升寫索引的性能:

  • 加大 Translog Flush ,目的是降低 Iops、Writeblock。

  • 增加 Index Refresh 間隔,目的是減少 Segment Merge 的次數。

  • 調整 Bulk 線程池和隊列。

  • 優化節點間的任務分布。

  • 優化 Lucene 層的索引建立,目的是降低 CPU 及 IO。

 

①批量提交

 

ES 提供了 Bulk API 支持批量操作,當我們有大量的寫任務時,可以使用 Bulk 來進行批量寫入。

 

每次提交的數據量為多少時,能達到最優的性能,主要受到文件大小、網絡情況、數據類型、集群狀態等因素影響。 

 

通用的策略如下:Bulk 默認設置批量提交的數據量不能超過 100M。數據條數一般是根據文檔的大小和服務器性能而定的,但是單次批處理的數據大小應從 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 操作,因為沒什么用處。

 

②優化存儲設備

 

ES 是一種密集使用磁盤的應用,在段合並的時候會頻繁操作磁盤,所以對磁盤要求較高,當磁盤速度提升之后,集群的整體性能會大幅度提高。

 

磁盤的選擇,提供以下幾點建議:

  • 使用固態硬盤(Solid State Disk)替代機械硬盤。SSD 與機械磁盤相比,具有高效的讀寫速度和穩定性。

  • 使用 RAID 0。RAID 0 條帶化存儲,可以提升磁盤讀寫效率。

  • 在 ES 的服務器上掛載多塊硬盤。使用多塊硬盤同時進行讀寫操作提升效率,在配置文件 ES 中設置多個存儲路徑,如下所示:

path.data:/path/to/data1,/path/to/data2。

避免使用 NFS(Network File System)等遠程存儲設備,網絡的延遲對性能的影響是很大的。

 

③合理使用合並

 

Lucene 以段的形式存儲數據。當有新的數據寫入索引時,Lucene 就會自動創建一個新的段。

 

隨着數據量的變化,段的數量會越來越多,消耗的多文件句柄數及 CPU 就越多,查詢效率就會下降。

 

由於 Lucene 段合並的計算量龐大,會消耗大量的 I/O,所以 ES 默認采用較保守的策略,讓后台定期進行段合並,如下所述:

  • 索引寫入效率下降:當段合並的速度落后於索引寫入的速度時,ES 會把索引的線程數量減少到 1。

    這樣可以避免出現堆積的段數量爆發,同時在日志中打印出“now throttling indexing”INFO 級別的“警告”信息。

  • 提升段合並速度:ES 默認對段合並的速度是 20m/s,如果使用了 SSD,我們可以通過以下的命令將這個合並的速度增加到 100m/s。

PUT /_cluster/settings
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "100mb"
    }
}
 
        
④減少 Refresh 的次數

 

Lucene 在新增數據時,采用了延遲寫入的策略,默認情況下索引的 refresh_interval 為 1 秒。

 

Lucene 將待寫入的數據先寫到內存中,超過 1 秒(默認)時就會觸發一次 Refresh,然后 Refresh 會把內存中的的數據刷新到操作系統的文件緩存系統中。

 

如果我們對搜索的實效性要求不高,可以將 Refresh 周期延長,例如 30 秒。

 

這樣還可以有效地減少段刷新次數,但這同時意味着需要消耗更多的Heap內存。

 

如下所示:

index.refresh_interval:30s
⑤加大 Flush 設置

 

Flush 的主要目的是把文件緩存系統中的段持久化到硬盤,當 Translog 的數據量達到 512MB 或者 30 分鍾時,會觸發一次 Flush。

 

index.translog.flush_threshold_size 參數的默認值是 512MB,我們進行修改。

 

增加參數值意味着文件緩存系統中可能需要存儲更多的數據,所以我們需要為操作系統的文件緩存系統留下足夠的空間。

 

⑥減少副本的數量

 

ES 為了保證集群的可用性,提供了 Replicas(副本)支持,然而每個副本也會執行分析、索引及可能的合並過程,所以 Replicas 的數量會嚴重影響寫索引的效率。

 

當寫索引時,需要把寫入的數據都同步到副本節點,副本節點越多,寫索引的效率就越慢。

 

如果我們需要大批量進行寫入操作,可以先禁止 Replica 復制,設置 index.number_of_replicas: 0 關閉副本。在寫入完成后,Replica 修改回正常的狀態。

 

讀優化

 

①避免大結果集和深翻

 

在上一篇講到了集群中的查詢流程,例如,要查詢從 from 開始的 size 條數據,則需要在每個分片中查詢打分排名在前面的 from+size 條數據。

 

協同節點將收集到的n×(from+size)條數據聚合,再進行一次排序,然后從 from+size 開始返回 size 條數據。

 

當 from、size 或者 n 中有一個值很大的時候,需要參加排序的數量也會增長,這樣的查詢會消耗很多 CPU 資源,從而導致效率的降低。

 

為了提升查詢效率,ES 提供了 Scroll 和 Scroll-Scan 這兩種查詢模式。

 

Scroll:是為檢索大量的結果而設計的。例如,我們需要查詢 1~100 頁的數據,每頁 100 條數據。

 

如果使用 Search 查詢:每次都需要在每個分片上查詢得分最高的 from+100 條數據,然后協同節點把收集到的 n×(from+100)條數據聚合起來再進行一次排序。

 

每次返回 from+1 開始的 100 條數據,並且要重復執行 100 次。

 

如果使用 Scroll 查詢:在各個分片上查詢 10000 條數據,協同節點聚合 n×10000 條數據進行合並、排序,並將排名前 10000 的結果快照起來。這樣做的好處是減少了查詢和排序的次數。

 

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" }}]}}
 
        

以上查詢語句的含義是,在 chandler 索引的 test type 里查詢字段 name 包含“錢丁君”的數據。

 

scroll=2m 表示下次請求的時間不能超過 2 分鍾,size 表示這次和后續的每次請求一次返回的數據條數。

 

在這次查詢的結果中除了返回了查詢到的結果,還返回了一個 scroll_id,可以把它作為下次請求的參數。

 

再次請求的命令,如下所示:

 

因為這次並沒有到分片里查詢數據,而是直接在生成的快照里面以游標的形式獲取數據。

 

所以這次查詢並沒有包含 index 和 type,也沒有查詢條件:

  • "scroll": "2m":指本次請求的時間不能超過 2 分鍾。 

  • scroll_id:是上次查詢時返回的 scroll_id。

 

Scroll-Scan:Scroll 是先做一次初始化搜索把所有符合搜索條件的結果緩存起來生成一個快照,然后持續地、批量地從快照里拉取數據直到沒有數據剩下。
 
而這時對索引數據的插入、刪除、更新都不會影響遍歷結果,因此 Scroll 並不適合用來做實時搜索。
 
其思路和使用方式與 Scroll 非常相似,但是 Scroll-Scan 關閉了 Scroll 中最耗時的文本相似度計算和排序,使得性能更加高效。

 

為了使用 Scroll-Scan,需要執行一個初始化搜索請求,將 search_type 設置成 Scan,告訴 ES 集群不需要文本相似計算和排序,只是按照數據在索引中順序返回結果集:

$ 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"] 進行代替。

 

Scroll 和 Scroll-Scan 有一些差別,如下所示:

  • Scroll-Scan不進行文本相似度計算,不排序,按照索引中的數據順序返回。

  • Scroll-Scan 不支持聚合操作。

  • Scroll-Scan 的參數 Size 代表着每個分片上的請求的結果數量,每次返回 n×size 條數據。而 Scroll 每次返回 size 條數據。

 

②選擇合適的路由

 

ES 中所謂的路由和 IP 網絡不同,是一個類似於 Tag 的東西。在創建文檔的時候,可以通過字段為文檔增加一個路由屬性的 Tag。在多分片的 ES 集群中,對搜索的查詢大致分為如下兩種。

 

ES 內在機制決定了擁有相同路由屬性的文檔,一定會被分配到同一個分片上,無論是主分片還是副本。

 

查詢時可以根據 Routing 信息,直接定位到目標分片,避免查詢所有的分片,再經過協調節點二次排序。

 

如圖 1 所示:

 

圖 1

 

如果在查詢條件中不包含 Routing,在查詢時就遍歷所有分片,整個查詢主要分為 Scatter、Gather 兩個過程:

  • Scatter(分發):請求到達協調節點之后,協調節點將查詢請求分發給每個分片。

  • Gather(聚合):協調點在每個分片上完成搜索,再將搜索到的結果集進行排序,將結果數據返回給用戶。

 

如圖 2 所示:

 

圖 2

 

通過對比上述兩種查詢流程,我們不難發現,使用 Routing 信息查找的效率很高,避免了多余的查詢。

 

所以我們在設計 Elasticsearch Mapping 時要合理地利用 Routing 信息,來提升查詢的效率。

 

例如,在大型的本地分類網站中,可以將城市 ID 作為 Routing 的條件,讓同一個城市的數據落在相同的分片中。

 

默認的公式如下:

shard = hash(routing)%number_of_primary_shards

 

不過需要注意的是,根據城市 ID 進行分片時,也會容易出現分片不均勻的情況。

 

例如,大型城市的數據過多,而小城市的數據太少,導致分片之間的數據量差異很大。

 

這時就可以進行必要的調整,比如把多個小城市的數據合並到一個分片上,把大城市的數據按區域進行拆分到不同分配。

 

③SearchType

 

在 Scatter、Gather 的過程中,節點間的數據傳輸和打分(SearchType),可以根據不同的場景選擇。

 

如下所示:

  • QUERY_THEN_FETCH:ES 默認的搜索方式。第一步,先向所有的分片發請求,各分片只返回文檔的相似度得分和文檔的 ID,然后協調節點按照各分片返回的分數進行重新排序和排名,再取出需要返回給客戶端的 Size 個文檔 ID。

    第 2 步,在相關的分片中取出文檔的詳細信息並返回給用戶。

  • QUERY_AND_FETCH:協調節點向所有分片發送查詢請求,各分片將文檔的相似度得分和文檔的詳細信息一起返回。

    然后,協調節點進行重新排序,再取出需要返回給客戶端的數據,將其返回給客戶端。由於只需要在分片中查詢一次,所以性能是最好的。

  • DFS_QUERY_THEN_FETCH:與 QUERY_THEN_FETCH 類似,但它包含一個額外的階段:在初始查詢中執行全局的詞頻計算,以使得更精確地打分,從而讓查詢結果更相關。

    QUERY_THEN_FETCH 使用的是分片內部的詞頻信息,而 DFS_QUERY_THEN_FETCH 使用訪問公共的詞頻信息,所以相比 QUERY_THEN_FETCH 性能更低。

  • DFS_QUERY_AND_FETCH:與 QUERY_AND_FETCH 類似,不過使用的是全局的詞頻。

 

④定期刪除

 

由於在 Lucene 中段具有不變性,每次進行刪除操作后不會立即從硬盤中進行實際的刪除,而是產生一個 .del 文件記錄刪除動作。

 

隨着刪除操作的增長,.del 文件會越來也多。當我們進行查詢操作的時候,被刪除的數據還會參與檢索中,然后根據 .del 文件進行過濾。.del 文件越多,查詢過濾過程越長,進而影響查詢的效率。

 

當機器空閑時,我們可以通過如下命令刪除文件,來提升查詢的效率:

$ curl -XPOST localhost:9200/chandler/_forcemerge?only_expunge_deletes=true
{"_shards":{"total":10,"successful":5,"failed":0}}

定期對不再更新的索引做 optimize (ES 2.0 以后更改為 Force Merge API)。

 

這 Optimze 的實質是對 Segment File 強制做合並,可以節省大量的 Segment Memory。

 

堆大小的設置

 

ES 默認安裝后設置的內存是 1GB,對於任何一個現實業務來說,這個設置都太小了。

 

如果是通過解壓安裝的 ES,則在 ES 安裝文件中包含一個 jvm.option 文件,添加如下命令來設置 ES 的堆大小:

-Xms10g
-Xmx10g

Xms 表示堆的初始大小,Xmx 表示可分配的最大內存,都是 10GB。

 

確保 Xmx 和 Xms 的大小是相同的,其目的是為了能夠在 Java 垃圾回收機制清理完堆區后不需要重新分隔計算堆區的大小而浪費資源,可以減輕伸縮堆大小帶來的壓力。 

 

也可以通過設置環境變量的方式設置堆的大小。服務進程在啟動時候會讀取這個變量,並相應的設置堆的大小。比如:

export ES_HEAP_SIEZE=10g

也可以通過命令行參數的形式,在程序啟動的時候把內存大小傳遞給 ES,如下所示:

./bin/elasticsearch -Xmx10g -Xms10g

這種設置方式是一次性的,在每次啟動 ES 時都需要添加。

 

假設你有一個 64G 內存的機器,按照正常思維思考,你可能會認為把 64G 內存都給 ES 比較好,但現實是這樣嗎, 越大越好?雖然內存對 ES 來說是非常重要的,但是答案是否定的!

 

因為 ES 堆內存的分配需要滿足以下兩個原則:

  • 不要超過物理內存的 50%:Lucene 的設計目的是把底層 OS 里的數據緩存到內存中。

    Lucene 的段是分別存儲到單個文件中的,這些文件都是不會變化的,所以很利於緩存,同時操作系統也會把這些段文件緩存起來,以便更快的訪問。

    如果我們設置的堆內存過大,Lucene 可用的內存將會減少,就會嚴重影響降低 Lucene 的全文本查詢性能。

  • 堆內存的大小最好不要超過 32GB:在 Java 中,所有對象都分配在堆上,然后有一個 Klass Pointer 指針指向它的類元數據。

    這個指針在 64 位的操作系統上為 64 位,64 位的操作系統可以使用更多的內存(2^64)。在 32 位的系統上為 32 位,32 位的操作系統的最大尋址空間為 4GB(2^32)。

    但是 64 位的指針意味着更大的浪費,因為你的指針本身大了。浪費內存不算,更糟糕的是,更大的指針在主內存和緩存器(例如 LLC, L1等)之間移動數據的時候,會占用更多的帶寬。

 

Java 使用內存指針壓縮(Compressed Oops)技術來解決這個問題。它的指針不再表示對象在內存中的精確位置,而是表示偏移量。

 

這意味着 32 位的指針可以引用 4GB 個 Byte,而不是 4GB 個 bit。也就是說,當堆內存為 32GB 的物理內存時,也可以用 32 位的指針表示。

 

不過,在越過那個神奇的邊界 32GB 時,指針就會變為普通對象的指針,每個對象的指針都變長了,就會浪費更多的內存,降低了 CPU 的性能,還要讓 GC 應對更大的內存。

 

事實上,當內存到達 40~40GB 時,有效的內存才相當於內存對象指針壓縮技術時的 32GB 內存。

 

所以即便你有足夠的內存,也盡量不要超過 32G,比如我們可以設置為 31GB:

-Xms31g
-Xmx31g

 

32GB 是 ES 一個內存設置限制,那如果你的機器有很大的內存怎么辦呢?現在的機器內存普遍增長,甚至可以看到有 300-500GB 內存的機器。

 

這時我們需要根據業務場景,進行恰當內存的分配:

  • 業務場景是以全文檢索為主:依然可以給 ES 分配小於 32GB 的堆內存,剩下的交給 Lucene 用作操作系統的文件系統緩存,所有的 Segment 都緩存起來,會加快全文檢索。

  • 業務場景中有很多的排序和聚合:我們可以考慮一台機器上創建兩個或者更多 ES 節點,而不要部署一個使用 32+GB 內存的節點。

    仍然要堅持 50% 原則,假設你有個機器有 128G 內存,你可以創建兩個 Node,使用 32G 內存。也就是說 64G 內存給 ES 的堆內存,剩下的 64G 給 Lucene。

 

服務器配置的選擇

 

Swapping 是性能的墳墓:在選擇 ES 服務器時,要盡可能地選擇與當前應用場景相匹配的服務器。

 

如果服務器配置很低,則意味着需要更多的節點,節點數量的增加會導致集群管理的成本大幅度提高。

 

如果服務器配置很高,而在單機上運行多個節點時,也會增加邏輯的復雜度。

 

在計算機中運行的程序均需在內存執行,若內存消耗殆盡將導致程序無法進行。為了解決這個問題,操作系統使用一種叫作虛擬內存的技術。

 

當內存耗盡時,操作系統就會自動把內存中暫時不使用的數據交換到硬盤中,需要使用的時候再從硬盤交換到內存。

 

如果內存交換到磁盤上需要 10 毫秒,從磁盤交換到內存需要 20 毫秒,那么多的操作時延累加起來,將導致幾何級增長。

 

不難看出 Swapping 對於性能是多么可怕。所以為了使 ES 有更好等性能,強烈建議關閉 Swap。

 

關閉 Swap 的方式如下:

 

①暫時禁用。如果我們想要在 Linux 服務器上暫時關閉,可以執行如下命令,但在服務器重啟后失效:

sudo swapoff -a

②永久性關閉。我們可以修改 /etc/sysctl.conf(不同的操作系統路徑有可能不同),增加如下參數:

vm.swappiness = 1      //0-100,則表示越傾向於使用虛擬內存。

 

注意:Swappiness 設置為 1 比設置為 0 要好,因為在一些內核版本,Swappness=0 會引發 OOM(內存溢出)。

 

Swappiness 默認值為 60,當設置為 0 時,在某些操作系統中有可能會觸發系統級的 OOM-killer,例如在 Linux 內核的內存不足時,為了防止系統的崩潰,會自動強制 Kill 一個“bad”進程。

 

③在 ES 中設置。如果上面的方法都不能做到,你需要打開配置文件中的 mlockall 開關,它的作用就是運行 JVM 鎖住內存,禁止 OS 交換出去。

 

在 elasticsearch.yml 配置如下:

bootstrap.mlockall: true

硬盤的選擇和設置

 

所以,如果條件允許,則請盡可能地使用 SSD,它的讀寫性能將遠遠超出任何旋轉介質的硬盤(如機械硬盤、磁帶等)。基於 SSD 的 ES 集群節點對於查詢和索引性能都有提升。

 

另外無論是使用固態硬盤還是使用機械硬盤,我們都建議將磁盤的陣列模式設置為 RAID 0,以此來提升磁盤的寫性能。

 

接入方式

 

ES 提供了 Transport Client(傳輸客戶端)和 Node Client(節點客戶端)的接入方式,這兩種方式各有利弊,分別對應不同的應用場景。

 

①Transport Client:作為一個集群和應用程序之間的通信層,和集群是安全解耦的。
 
由於與集群解耦,所以在連接集群和銷毀連接時更加高效,適合大量的客戶端連接。

 

②Node Client:把應用程序當作一個集群中的 Client 節點(非 Data 和 Master 節點)。
 
由於它是集群的一個內部節點,意味着它可以感知整個集群的狀態、所有節點的分布情況、分片的分布狀況等。

 

由於 Node Client 是集群的一部分,所以在接入和退出集群時進行比較復雜操作,並且還會影響整個集群的狀態,所以 Node Client 更適合少量客戶端,能夠提供更好的執行效率。

 

角色隔離和腦裂

 

①角色隔離

 

ES 集群中的數據節點負責對數據進行增、刪、改、查和聚合等操作,所以對 CPU、內存和 I/O 的消耗很大。

 

在搭建 ES 集群時,我們應該對 ES 集群中的節點進行角色划分和隔離。 

 

候選主節點:

node.master=true
node.data=false

數據節點:

node.master=false
node.data=true

最后形成如圖 3 所示的邏輯划分:

 

圖 3

 

②避免腦裂

 

網絡異常可能會導致集群中節點划分出多個區域,區域發現沒有 Master 節點的時候,會選舉出了自己區域內 Maste 節點 r,導致一個集群被分裂為多個集群,使集群之間的數據無法同步,我們稱這種現象為腦裂。

 

為了防止腦裂,我們需要在 Master 節點的配置文件中添加如下參數:

discovery.zen.minimum_master_nodes=(master_eligible_nodes/2)+1        //默認值為1

其中 master_eligible_nodes 為 Master 集群中的節點數。這樣做可以避免腦裂的現象都出現,最大限度地提升集群的高可用性。

 

只要不少於 discovery.zen.minimum_master_nodes 個候選節點存活,選舉工作就可以順利進行。

 

ES 實戰

 

ES 配置說明

 

在 ES 安裝目錄下的 Conf 文件夾中包含了一個重要的配置文件:elasticsearch.yaml。

 

ES 的配置信息有很多種,大部分配置都可以通過 elasticsearch.yaml 和接口的方式進行。

 

下面我們列出一些比較重要的配置信息:

  • cluster.name:elasticsearch:配置 ES 的集群名稱,默認值是 ES,建議改成與所存數據相關的名稱,ES 會自動發現在同一網段下的集群名稱相同的節點。

  • node.nam: "node1":集群中的節點名,在同一個集群中不能重復。節點的名稱一旦設置,就不能再改變了。當然,也可以設置成服務器的主機名稱,例如 node.name:${HOSTNAME}。

  • noed.master:true:指定該節點是否有資格被選舉成為 Master 節點,默認是 True,如果被設置為 True,則只是有資格成為 Master 節點,具體能否成為 Master 節點,需要通過選舉產生。

  • node.data:true:指定該節點是否存儲索引數據,默認為 True。數據的增、刪、改、查都是在 Data 節點完成的。

  • index.number_of_shards:5:設置都索引分片個數,默認是 5 片。也可以在創建索引時設置該值,具體設置為多大都值要根據數據量的大小來定。如果數據量不大,則設置成 1 時效率最高。

  • index.number_of_replicas:1:設置默認的索引副本個數,默認為 1 個。副本數越多,集群的可用性越好,但是寫索引時需要同步的數據越多。

  • path.conf:/path/to/conf:設置配置文件的存儲路徑,默認是 ES 目錄下的 Conf 文件夾。建議使用默認值。

  • path.data:/path/to/data1,/path/to/data2:設置索引數據多存儲路徑,默認是 ES 根目錄下的 Data 文件夾。切記不要使用默認值,因為若 ES 進行了升級,則有可能數據全部丟失。

    可以用半角逗號隔開設置的多個存儲路徑,在多硬盤的服務器上設置多個存儲路徑是很有必要的。

  • path.logs:/path/to/logs:設置日志文件的存儲路徑,默認是 ES 根目錄下的 Logs,建議修改到其他地方。

  • path.plugins:/path/to/plugins:設置第三方插件的存放路徑,默認是 ES 根目錄下的 Plugins 文件夾。

  • bootstrap.mlockall:true:設置為 True 時可鎖住內存。因為當 JVM 開始 Swap 時,ES 的效率會降低,所以要保證它不 Swap。

  • network.bind_host:192.168.0.1:設置本節點綁定的 IP 地址,IP 地址類型是 IPv4 或 IPv6,默認為 0.0.0.0。

  • network.publish_host:192.168.0.1:設置其他節點和該節點交互的 IP 地址,如果不設置,則會進行自我判斷。

  • network.host:192.168.0.1:用於同時設置 bind_host 和 publish_host 這兩個參數。

  • http.port:9200:設置對外服務的 HTTP 端口,默認為 9200。ES 的節點需要配置兩個端口號,一個對外提供服務的端口號,一個是集群內部使用的端口號。

    http.port 設置的是對外提供服務的端口號。注意,如果在一個服務器上配置多個節點,則切記對端口號進行區分。

  • transport.tcp.port:9300:設置集群內部的節點間交互的 TCP 端口,默認是 9300。注意,如果在一個服務器配置多個節點,則切記對端口號進行區分。

  • transport.tcp.compress:true:設置在節點間傳輸數據時是否壓縮,默認為 False,不壓縮。

  • discovery.zen.minimum_master_nodes:1:設置在選舉 Master 節點時需要參與的最少的候選主節點數,默認為 1。如果使用默認值,則當網絡不穩定時有可能會出現腦裂。

    合理的數值為(master_eligible_nodes/2)+1,其中 master_eligible_nodes 表示集群中的候選主節點數。

  • discovery.zen.ping.timeout:3s:設置在集群中自動發現其他節點時 Ping 連接的超時時間,默認為 3 秒。

    在較差的網絡環境下需要設置得大一點,防止因誤判該節點的存活狀態而導致分片的轉移。

 

常用接口

 

雖然現在有很多開源軟件對 ES 的接口進行了封裝,使我們可以很方便、直觀地監控集群的狀況,但是在 ES 5 以后,很多軟件開始收費。

 

了解常用的接口有助於我們在程序或者腳本中查看我們的集群情況,以下接口適用於 ES 6.5.2 版本。

 

①索引類接口

 

通過下面的接口創建一個索引名稱為 indexname 且包含 3 個分片、1 個副本的索引:

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 的索引,通過下面的接口可以刪除多個索引:

DELETE http://localhost:9200/indexname1,indexname2
DELETE http://localhost:9200/indexname*

通過下面的接口可以刪除集群下的全部索引:

DELETE http://localhost:9200/_all
DELETE http://localhost:9200/*

 

進行全部索引刪除是很危險的,我們可以通過在配置文件中添加下面的配置信息,來關閉使用 _all 和使用通配符刪除索引的接口,使用刪除索引職能通過索引的全稱進行。

action.destructive_requires_name: true

通過下面的接口獲取索引的信息,其中,Pretty 參數用語格式化輸出結構,以便更容易閱讀:

GET http://localhost:9200/indexname?pretty
 
        

 

通過下面的接口關閉、打開索引:

POST http://localhost:9200/indexname/_close
POST http://localhost:9200/indexname/_open

 

通過下面的接口獲取一個索引中具體 Type 的 Mapping 映射:

GET http://localhost:9200/indexname/typename/_mapping?pretty

當一個索引中有多個 Type 時,獲得 Mapping 時要加上 Typename。

 

②Document 操作

 

安裝 ES 和 Kibana 之后,進入 Kibana 操作頁面,然后進去的 DevTools 執行下面操作:

#添加一條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 元數據:代表這個 Document 存放在哪個 Idnex 中,類似的數據放在一個索引,非類似的數據放不同索引,Index 中包含了很多類似的 Document。

  • _type 元數據:代表 Document 屬於 Index 中的哪個類別(type),一個索引通常會划分多個 Type,邏輯上對 Index 中有些許不同的幾類數據進行分類。

  • _id 元數據:代表 Document 的唯一標識,id 與 Index 和 Type 一起,可以唯一標識和定位一個 Document,可以理解為數據庫中主鍵。我們可以指定 Document 的 id,也可以不指定,由ES自動為我們創建一個 id。

 

接口應用

 

①Search 接口

 

Search 是我們最常用的 API,ES 給我提供了豐富的查詢條件,比如模糊匹配 Match,字段判空 Exists,精准匹配 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 的數據:

GET /test_index/test_type/_search

 

 

查詢索引為 test_index,doc 類型為 test_type,docment 字段 num10 為 4 的數據:

GET /test_index/test_type/_search?pretty=true
{
  "query": { 
    "bool": { 
      "filter": [ 
        { "term":  { "num10": 4 }}
      ]
    }
  }
}
 
        

 

更多查詢條件的組合,大家可以自行測試。

 

②修改 Mapping
PUT /my_index/_mapping/my_type
{
  "properties": {
       "new_field_name": {
           "type":     "string"         //字段類型,string、long、boolean、ip
       }
   }
}
 

 

如上是修改 Mapping 結構,然后利用腳本 Script 給字段賦值:

POST my_index/_update_by_query
{
  "script": {
    "lang": "painless",
    "inline": "ctx._source.new_field_name= '02'"
  }
}
 
        
③修改別名

 

如下給 Index 為 test_index 的數據綁定 Alias 為 test_alias:

POST /_aliases
{
  "actions": [
    {
      "add": {      //add,remove
        "index": "test_index",
        "alias": "test_alias"
      }
    }
  ]
}

 

 

驗證別名關聯,根據別名來進行數據查詢,如下:

GET /test_alias/test_type/3

 

④定制返回內容

 

_source 元數據:就是說,我們在創建一個 Document 的時候,使用的那個放在 Request Body 中的 Json 串(所有的 Field),默認情況下,在 Get 的時候,會原封不動的給我們返回回來。

 

定制返回的結果,指定 _source 中,返回哪些 Field:

#語法:
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"
  }
}
#也可返回多個field使用都好分割
GET /test_index/test_type/1?_source=test_field2,test_field1

 

 
        

Java 封裝

 

組件 elasticsearch.jar 提供了豐富 API,不過不利於我們理解和學習,現在我們自己來進行封裝。 

 

組件 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查詢失敗");
    }
}

 

測試用例,代碼如下:

/**
 * ES交互驗證-查詢、更新等等操作
 *
 * @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);
    }
}

 

控台輸出:

----------->{
  "_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"
  }
}

 

轉載:

作者:錢丁君

資料參考:《可伸縮服務架構》 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM