Elasticsearch集羣 / Java客戶端

6.1、集羣節點

ELasticsearch的集羣是由多個節點組成的,通過cluster.name設置集羣名稱,並且用於區分其它的集羣,每個節點
通過node.name指定節點的名稱。
在Elasticsearch中,節點的類型主要有4種:

  • master節點     

          配置文件中node.master屬性爲true(默認爲true),就有資格被選爲master節點。

          master節點用於控制整個集羣的操作。比如創建或刪除索引,管理其它非master節點等。

  • data節點

       配置文件中node.data屬性爲true(默認爲true),就有資格被設置成data節點。

      data節點主要用於執行數據相關的操作。比如文檔的CRUD。

  • 客戶端節點

       配置文件中node.master屬性和node.data屬性均爲false。

       該節點不能作爲master節點,也不能作爲data節點。
       可以作爲客戶端節點,用於響應用戶的請求,把請求轉發到其他節點

  • 部落節點

       當一個節點配置tribe.*的時候,它是一個特殊的客戶端,它可以連接多個集羣,在所有連接的集羣上執行
       搜索和其他操作。

6.2、搭建集羣

        

#啓動3個虛擬機,分別在3臺虛擬機上部署安裝Elasticsearch
mkdir /admin/es-cluster

#分發到其它機器
scp -r es-cluster [email protected]:/admin
#node01的配置:
cluster.name: es-admin-cluster
node.name: node01
node.master: true
node.data: true
network.host: 0.0.0.0
http.port: 9200
discovery.zen.ping.unicast.hosts: ["192.168.40.133","192.168.40.134","192.168.40.135"]
discovery.zen.minimum_master_nodes: 2
http.cors.enabled: true
http.cors.allow-origin: "*"

#node02的配置:
cluster.name: es-admin-cluster
node.name: node02
node.master: true
node.data: true
network.host: 0.0.0.0
http.port: 9200
discovery.zen.ping.unicast.hosts: ["192.168.40.133","192.168.40.134","192.168.40.135"]
discovery.zen.minimum_master_nodes: 2
http.cors.enabled: true
http.cors.allow-origin: "*"


#node03的配置:
cluster.name: es-admin-cluster
node.name: node02
node.master: true
node.data: true
network.host: 0.0.0.0
http.port: 9200
discovery.zen.ping.unicast.hosts: ["192.168.40.133","192.168.40.134","192.168.40.135"]
discovery.zen.minimum_master_nodes: 2
http.cors.enabled: true
http.cors.allow-origin: "*"
#分別啓動3個節點
./elasticsearch

查看集羣

 創建索引:

 

 查詢集羣狀態:/_cluster/health

響應:

{ 
  cluster_name: "es-admin-cluster"
  status: "green"
  timed_out: false
  number_of_nodes: 3
  number_of_data_nodes: 3
  active_primary_shards: 5
  active_shards: 10
  relocating_shards: 0
  initializing_shards: 0
  unassigned_shards: 0
  delayed_unassigned_shards: 0
  number_of_pending_tasks: 0
  number_of_in_flight_fetch: 0
  task_max_waiting_in_queue_millis: 0
  active_shards_percent_as_number: 100
}

集羣狀態的三種顏色:

顏色 意義
green 所有主要分片和複製分片都可用
yellow 所有主要分片可用,但不是所有複製分片都可用
red 不是所有的主要分片都可用

6.3、分片和副本

爲了將數據添加到Elasticsearch,我們需要索引(index)——一個存儲關聯數據的地方。實際上,索引只是一個用來
指向一個或多個分片(shards)的“邏輯命名空間(logical namespace)”.

  • 一個分片(shard)是一個最小級別“工作單元(worker unit)”,它只是保存了索引中所有數據的一部分。
  • 我們需要知道是分片就是一個Lucene實例,並且它本身就是一個完整的搜索引擎。應用程序不會和它直接通信。
  • 分片可以是主分片(primary shard)或者是複製分片(replica shard)。
  • 索引中的每個文檔屬於一個單獨的主分片,所以主分片的數量決定了索引最多能存儲多少數據。
  • 複製分片只是主分片的一個副本,它可以防止硬件故障導致的數據丟失,同時可以提供讀請求,比如搜索或者從別的shard取回文檔。
  • 當索引創建完成的時候,主分片的數量就固定了,但是複製分片的數量可以隨時調整。

6.4、故障轉移

6.4.1、將data節點停止

這裏選擇將node02停止:

 

 說明:

當前集羣狀態爲黃色,表示主節點可用,副本節點不完全可用

 過一段時間觀察,發現節點列表中看不到node02,副本節點分配到了node01和node03,集羣狀態恢復到綠色。

 

 將node02恢復:

./node02/bin/elasticsearch

 可以看到,node02恢復後,重新加入了集羣,並且重新分配了節點信息。

6.4.2、將master節點停止

 接下來,測試將node01停止,也就是將主節點停止。

 從結果中可以看出,集羣對master進行了重新選舉,選擇node03爲master。並且集羣狀態變成黃色。
等待一段時間後,集羣狀態從黃色變爲了綠色:

 恢復node01節點:

./node01/bin/elasticsearch 

重啓之後,發現node01可以正常加入到集羣中,集羣狀態依然爲綠色:

 特別說明:
如果在配置文件中discovery.zen.minimum_master_nodes設置的不是N/2+1時,會出現腦裂問題,之前宕機
的主節點恢復後不會加入到集羣。

 6.5、分佈式文檔

 6.5.1、路由

首先,來看個問題:

 如圖所示:當我們想一個集羣保存文檔時,文檔該存儲到哪個節點呢? 是隨機嗎? 是輪詢嗎?
實際上,在ELasticsearch中,會採用計算的方式來確定存儲到哪個節點,計算公式如下:

shard = hash(routing) % number_of_primary_shards 

  •  routing值是一個任意字符串,它默認是_id但也可以自定義。
  • 這個routing字符串通過哈希函數生成一個數字,然後除以主切片的數量得到一個餘數(remainder),餘數的範圍永遠是0到number_of_primary_shards - 1,這個數字就是特定文檔所在的分片。

這就是爲什麼創建了主分片後,不能修改的原因。

 

6.5.2、文檔的寫操作

新建、索引和刪除請求都是寫(write)操作,它們必須在主分片上成功完成才能複製到相關的複製分片上。

 

下面我們羅列在主分片和複製分片上成功新建、索引或刪除一個文檔必要的順序步驟:

1. 客戶端給 Node 1 發送新建、索引或刪除請求。
2. 節點使用文檔的 _id 確定文檔屬於分片 0 。它轉發請求到 Node 3 ,分片 0 位於這個節點上。
3. Node 3 在主分片上執行請求,如果成功,它轉發請求到相應的位於 Node 1 和 Node 2 的複製節點上。當所有
的複製節點報告成功, Node 3 報告成功到請求的節點,請求的節點再報告給客戶端。

客戶端接收到成功響應的時候,文檔的修改已經被應用於主分片和所有的複製分片。你的修改生效了。

 

6.5.3、搜索文檔(單個文檔)

文檔能夠從主分片或任意一個複製分片被檢索

 下面我們羅列在主分片或複製分片上檢索一個文檔必要的順序步驟:
   1. 客戶端給 Node 1 發送get請求。
   2. 節點使用文檔的 _id 確定文檔屬於分片 0 。分片 0 對應的複製分片在三個節點上都有。此時,它轉發請求到
      Node 2 。
   3. Node 2 返回文檔(document)給 Node 1 然後返回給客戶端。
對於讀請求,爲了平衡負載,請求節點會爲每個請求選擇不同的分片——它會循環所有分片副本。
可能的情況是,一個被索引的文檔已經存在於主分片上卻還沒來得及同步到複製分片上。這時複製分片會報告文檔未
找到,主分片會成功返回文檔。一旦索引請求成功返回給用戶,文檔則在主分片和複製分片都是可用的。

6.5.4、全文搜索

對於全文搜索而言,文檔可能分散在各個節點上,那麼在分佈式的情況下,如何搜索文檔呢?
搜索,分爲2個階段,搜索(query)+取回(fetch)。

搜索(query)

 查詢階段包含以下三步:
    1. 客戶端發送一個 search(搜索) 請求給 Node 3 , Node 3 創建了一個長度爲 from+size 的空優先級隊
    2. Node 3 轉發這個搜索請求到索引中每個分片的原本或副本。每個分片在本地執行這個查詢並且結果將結果到
        一個大小爲 from+size 的有序本地優先隊列裏去。
   3. 每個分片返回document的ID和它優先隊列裏的所有document的排序值給協調節點 Node 3 。 Node 3 把這些
       值合併到自己的優先隊列裏產生全局排序結果。

取回(fetch)

 分發階段由以下步驟構成:
      1. 協調節點辨別出哪個document需要取回,並且向相關分片發出 GET 請求。
      2. 每個分片加載document並且根據需要豐富(enrich)它們,然後再將document返回協調節點。
      3. 一旦所有的document都被取回,協調節點會將結果返回給客戶端。

 

 

 

7、Java客戶端

在Elasticsearch中,爲java提供了2種客戶端,一種是REST風格的客戶端,另一種是Java API的客戶端。https://www.elastic.co/guide/en/elasticsearch/client/index.html

 

 7.1、REST客戶端

 Elasticsearch提供了2種REST客戶端,一種是低級客戶端,一種是高級客戶端。

  • Java Low Level REST Client:官方提供的低級客戶端。該客戶端通過http來連接Elasticsearch集羣。用戶在使用該客戶端時需要將請求數據手動拼接成Elasticsearch所需JSON格式進行發送,收到響應時同樣也需要將返回的JSON數據手動封裝成對象。雖然麻煩,不過該客戶端兼容所有的Elasticsearch版本。
  • Java High Level REST Client:官方提供的高級客戶端。該客戶端基於低級客戶端實現,它提供了很多便捷的API來解決低級客戶端需要手動轉換數據格式的問題。

7.2、構造數據

POST /haoke/house/_bulk
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1001","title":"整租 · 南丹大樓 1居室 7500","price":"7500"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1002","title":"陸家嘴板塊,精裝設計一室一廳,可拎包入住誠意租。","price":"8500"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1003","title":"整租 · 健安坊 1居室 4050","price":"7500"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1004","title":"整租 · 中凱城市之光+視野開闊+景色秀麗+拎包入住","price":"6500"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1005","title":"整租 · 南京西路品質小區 21213三軌交匯 配套齊* 拎包入住","price":"6000"}
{"index":{"_index":"haoke","_type":"house"}}
{"id":"1006","title":"祥康裏 簡約風格 *南戶型 拎包入住 看房隨時","price":"7000"}

 

7.3、REST低級客戶端

 7.3.1、創建工程

創建工程admin-elasticsearch:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
    <!--路徑改爲實際路徑-->
  <groupId>cn.china.elasticsearch</groupId>
  <artifactId>admin-elasticsearch</artifactId>
  <version>1.0-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>org.elasticsearch.client</groupId>
      <artifactId>elasticsearch-rest-client</artifactId>
      <version>6.5.4</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.4</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <!-- java編譯插件 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.2</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

7.3.2、編寫測試用例

package cn.china.es.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpHost;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.client.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TestESREST {
  private static final ObjectMapper MAPPER = new ObjectMapper();
  private RestClient restClient;
  @Before
  public void init() {
    RestClientBuilder restClientBuilder = RestClient.builder(
        new HttpHost("172.16.55.185", 9200, "http"),
        new HttpHost("172.16.55.185", 9201, "http"),
        new HttpHost("172.16.55.185", 9202, "http"));
    restClientBuilder.setFailureListener(new RestClient.FailureListener() {
      @Override
      public void onFailure(Node node) {
        System.out.println("出錯了 -> " + node);
     }
   });
    this.restClient = restClientBuilder.build();
 }
  @After
  public void after() throws IOException {
     restClient.close();
 }
  // 查詢集羣狀態
  @Test
  public void testGetInfo() throws IOException {
    Request request = new Request("GET", "/_cluster/state");
    request.addParameter("pretty","true");
    Response response = this.restClient.performRequest(request);
    System.out.println(response.getStatusLine());
    System.out.println(EntityUtils.toString(response.getEntity()));
 }
  // 新增數據
  @Test
  public void testCreateData() throws IOException {
    Request request = new Request("POST", "/haoke/house");
    Map<String, Object> data = new HashMap<>();
    data.put("id","2001");
    data.put("title","張江高科");
    data.put("price","3500");
    request.setJsonEntity(MAPPER.writeValueAsString(data));
    Response response = this.restClient.performRequest(request);
    System.out.println(response.getStatusLine());
    System.out.println(EntityUtils.toString(response.getEntity()));
 }
  // 根據id查詢數據
  @Test
  public void testQueryData() throws IOException {
    Request request = new Request("GET", "/haoke/house/G0pfE2gBCKv8opxuRz1y");
    Response response = this.restClient.performRequest(request);
    System.out.println(response.getStatusLine());
    System.out.println(EntityUtils.toString(response.getEntity()));
 }
  // 搜索數據
  @Test
  public void testSearchData() throws IOException {
    Request request = new Request("POST", "/haoke/house/_search");
    String searchJson = "{\"query\": {\"match\": {\"title\": \"拎包入住\"}}}";
    request.setJsonEntity(searchJson);
    request.addParameter("pretty","true");
    Response response = this.restClient.performRequest(request);
    System.out.println(response.getStatusLine());
    System.out.println(EntityUtils.toString(response.getEntity()));
 }
}

 

 

 

7.4、REST高級客戶端

7.4.1、引入依賴

<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
  <version>6.5.4</version>
</dependency>

7.4.2、編寫測試用例

package cn.china.es.rest;
import org.apache.http.HttpHost;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class TestRestHighLevel {
  private RestHighLevelClient client;
  @Before
  public void init() {
    RestClientBuilder restClientBuilder = RestClient.builder(
        new HttpHost("172.16.55.185", 9200, "http"),
        new HttpHost("172.16.55.185", 9201, "http"),
        new HttpHost("172.16.55.185", 9202, "http"));
    this.client = new RestHighLevelClient(restClientBuilder);
 }
  @After
  public void after() throws Exception {
    this.client.close();
 }
  /**
  * 新增文檔,同步操作
  *
  * @throws Exception
  */
  @Test
  public void testCreate() throws Exception {
    Map<String, Object> data = new HashMap<>();
    data.put("id", "2002");
    data.put("title", "南京西路 拎包入住 一室一廳");
    data.put("price", "4500");
    IndexRequest indexRequest = new IndexRequest("haoke", "house")
       .source(data);
    IndexResponse indexResponse = this.client.index(indexRequest,
RequestOptions.DEFAULT);
    System.out.println("id->" + indexResponse.getId());
    System.out.println("index->" + indexResponse.getIndex());
    System.out.println("type->" + indexResponse.getType());
    System.out.println("version->" + indexResponse.getVersion());
    System.out.println("result->" + indexResponse.getResult());
    System.out.println("shardInfo->" + indexResponse.getShardInfo());
 }
  /**
  * 新增文檔,異步操作
  *
  * @throws Exception
   */
  @Test
  public void testCreateAsync() throws Exception {
    Map<String, Object> data = new HashMap<>();
    data.put("id", "2003");
    data.put("title", "南京東路 最新房源 二室一廳");
    data.put("price", "5500");
    IndexRequest indexRequest = new IndexRequest("haoke", "house")
       .source(data);
    this.client.indexAsync(indexRequest, RequestOptions.DEFAULT, new
ActionListener<IndexResponse>() {
      @Override
      public void onResponse(IndexResponse indexResponse) {
        System.out.println("id->" + indexResponse.getId());
        System.out.println("index->" + indexResponse.getIndex());
        System.out.println("type->" + indexResponse.getType());
        System.out.println("version->" + indexResponse.getVersion());
        System.out.println("result->" + indexResponse.getResult());
        System.out.println("shardInfo->" + indexResponse.getShardInfo());
     }
      @Override
      public void onFailure(Exception e) {
        System.out.println(e);
     }
   });
   
    System.out.println("ok");
    Thread.sleep(20000);
 }
  @Test
  public void testQuery() throws Exception {
    GetRequest getRequest = new GetRequest("haoke", "house",
"GkpdE2gBCKv8opxuOj12");
    // 指定返回的字段
    String[] includes = new String[]{"title", "id"};
    String[] excludes = Strings.EMPTY_ARRAY;
    FetchSourceContext fetchSourceContext =
        new FetchSourceContext(true, includes, excludes);
    getRequest.fetchSourceContext(fetchSourceContext);
    GetResponse response = this.client.get(getRequest, RequestOptions.DEFAULT);
    System.out.println("數據 -> " + response.getSource());
 }
   /**
  * 判斷是否存在
  *
  * @throws Exception
  */
  @Test
  public void testExists() throws Exception {
    GetRequest getRequest = new GetRequest("haoke", "house",
"GkpdE2gBCKv8opxuOj12");
    // 不返回的字段
    getRequest.fetchSourceContext(new FetchSourceContext(false));
    boolean exists = this.client.exists(getRequest, RequestOptions.DEFAULT);
    System.out.println("exists -> " + exists);
 }
  /**
  * 刪除數據
  *
  * @throws Exception
  */
  @Test
  public void testDelete() throws Exception {
    DeleteRequest deleteRequest = new DeleteRequest("haoke", "house",
"GkpdE2gBCKv8opxuOj12");
    DeleteResponse response = this.client.delete(deleteRequest,
RequestOptions.DEFAULT);
    System.out.println(response.status());// OK or NOT_FOUND
 }
  /**
  * 更新數據
  *
  * @throws Exception
  */
  @Test
  public void testUpdate() throws Exception {
    UpdateRequest updateRequest = new UpdateRequest("haoke", "house",
"G0pfE2gBCKv8opxuRz1y");
    Map<String, Object> data = new HashMap<>();
    data.put("title", "張江高科2");
    data.put("price", "5000");
    updateRequest.doc(data);
    UpdateResponse response = this.client.update(updateRequest,
RequestOptions.DEFAULT);
    System.out.println("version -> " + response.getVersion());
 }



   /**
  * 測試搜索
  *
  * @throws Exception
  */
  @Test
  public void testSearch() throws Exception {
    SearchRequest searchRequest = new SearchRequest("haoke");
    searchRequest.types("house");
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    sourceBuilder.query(QueryBuilders.matchQuery("title", "拎包入住"));
    sourceBuilder.from(0);
    sourceBuilder.size(5);
    sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
    searchRequest.source(sourceBuilder);
    SearchResponse search = this.client.search(searchRequest,
RequestOptions.DEFAULT);
    System.out.println("搜索到 " + search.getHits().totalHits + " 條數據.");
    SearchHits hits = search.getHits();
    for (SearchHit hit : hits) {
      System.out.println(hit.getSourceAsString());
   }
 }
}

 參考資料:

https://www.bilibili.com/video/av67957955/?p=56&t=49

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章