Elasticsearch集群扩展和性能优化

目录

1、节点的角色类型

1.1、master角色

1.2、data角色

1.3、ingest角色

1.4、remote_cluster_client角色

1.5、ml角色

1.6、transform角色

1.7、协调节点        

2、在CentOS 7上搭建Elasticsearch集群

2.1、准备工作

2.1.1、关闭防火墙

2.1.2、卸载OpenJDK

2.1.3、下载Elasticsearch 7.9.1

2.1.4、增大文件句柄数

2.1.5、增大线程池的大小

2.1.6、关闭内外存交换

2.1.7、增大虚拟内存地址

2.2、安装集群

2.2.1、解压安装包 

2.2.2、添加用户并授权

2.2.3、编辑elasticsearch.yml文件

2.2.4、修改JVM堆内存大小

2.2.5、启动集群

2.3、验证安装

2.3.1、检查文件句柄数和线程池大小

2.3.2、检查虚拟内存配置

2.3.3、检查集群服务的健康状态和节点数

2.3.4、检查内存锁的开启状态

3、推荐的集群配置

3.1、主候选节点不是越多越好

3.2、每个节点的内存最好是64GB

3.3、有条件使用固态盘更好

3.4、限制单个节点能够容纳的分片上限

3.5、限制单个索引在每个节点上的分片上限

3.6、遵守集群高可用的原则

3.7、大规模的集群可以考虑分区容错

4、集群监控

4.1、监控集群的状态信息

4.2、监控集群的健康状态

4.3、监控集群节点的统计指标

4.4、监控节点的热点线程

4.5、查看慢搜索日志

4.6、查看慢索引日志

5、索引分片数的设置与横向扩容

5.1、容量规划

5.2、创建索引模板

5.3、创建索引并写入数据

6、优化索引的写入速度

6.1、避免写入过大的文档

6.2、合并写入请求

6.2.1、delete_by_query按查询结果删除

6.2.2、update_by_query按查询更新

6.3、适当增大写入的线程数和索引缓冲区

7、优化搜索的响应速度

7.1、避免深度分页

7.2、合并搜索请求

7.2.1、Multi get多主键查询

7.2.2、Multi search批量搜索

7.3、使用缓存加快搜索速度

7.3.1、分片的请求缓存    

7.3.2、节点的查询缓存

7.4控制搜索请求的路由

8、集群的重启

8.1、全集群重启

8.1.1、关闭所有写入进程并冲刷数据到磁盘

8.1.2、禁用分片的分配

8.1.3、重启集群

8.1.4、待每个节点加入集群后,启动分片的分配

8.2、滚动重启

8.2.1、关闭所有写入进程并冲刷数据到磁盘

8.2.2、禁用分片的分配

8.2.3、选择一个节点重启Elasticsearch服务

8.2.4、待该节点加入集群后,启动分片的分配

9、集群的备份与恢复

9.1、搭建共享文件目录

9.1.1、在每个节点中指定共享文件目录的地址

9.1.2、安装SSHFS创建共享文件目录

9.1.3、挂载131服务器的/opt/backup到本地文件目录/mnt/share

9.2、备份集群数据


1、节点的角色类型

        Elasticsearch将集群中的节点分成了6种不同的角色,每种角色有不同的功能。在默认情况下,每个节点拥有全部角色,分别如下。

1.1、master角色

        拥有该角色的节点会成为主候选节点,如果你想让一个节点成为专门的主候选节点而不处理其他操作,需要在elasticsearch.yml中配置:

node.roles: [ master ]

        如果想让主候选节点参与投票选举而不成为主节点,可以配置:

node.roles: [ master, voting_only ]

1.2、data角色

        拥有该角色的节点将成为数据节点,它会存放索引的分片数据并处理增删改查的读写请求。如果你要配置一个专门的数据节点,需要配置:

node.roles: [ data ]

1.3、ingest角色

        拥有该角色的节点将成为预处理节点,该节点在数据写入索引之前可以对数据做一些转换和修改。你可以配置一些专用的预处理节点在数据写入索引之前完成数据转换,这样的节点在elasticsearch.yml中的配置如下。

node.roles: [ ingest ]

1.4、remote_cluster_client角色

        拥有该角色的节点可以连接到远程集群以发起对远程集群的搜索。配置一个专门的远程集群客户端节点需要在elasticsearch.yml中添加如下代码。

node.roles: [ remote_cluster_client ]

1.5、ml角色

        也就是机器学习(machine learning)角色,拥有该角色的节点可以完成一些机器学习方面的操作,实现该功能需要使用X-Pack插件。创建一个专门的机器学习节点需要在elasticsearch.yml中添加如下代码。

node.roles: [ ml ]

1.6、transform角色

        拥有该角色的节点可以完成一些转换操作。一次转换操作可以把一个索引的搜索结果转换后写入另一个索引,这在某些数据分析场景中会很有用,该功能的实现也需要使用X-Pack插件。创建一个专门的数据转换节点需要在elasticsearch.yml中添加如下代码。

node.roles: [ transform ]

1.7、协调节点        

        如果你把一个节点的node.roles参数配置为空,它将成为一个专用的协调节点。

node.roles: [ ]

        协调节点既不能存放、转换数据,也不能选举主节点,它就像一个负载均衡器一样只能够处理客户端请求的转发操作。由于所有的节点都自带这种能力,通常并没有必要单独配置协调节点。

        在一个正常的Elasticsearch集群中,主候选节点和数据节点是必选的,其他的节点类型是可选的。所以一般情况下,配置好主候选节点和数据节点就可以了。即使你保持默认设置让每个节点拥有所有角色,这往往也没有什么错。

2、在CentOS 7上搭建Elasticsearch集群

        在生产环境中,通常需要搭建Elasticsearch的集群来支撑大数据场景的搜索和统计分析,本节就来谈谈在CentOS 7上搭建Elasticsearch集群的方法。

2.1、准备工作

        在安装Elasticsearch集群以前,你需要做一些准备工作,以便安装过程可以顺利进行。首先需要准备3台虚拟机(安装好CentOS 7操作系统),然后进行下述一系列的配置修改。

2.1.1、关闭防火墙

        3台虚拟机均需要关闭防火墙,否则Elasticsearch集群无法正常工作。

systemctl stop firewalld
systemctl disable firewalld
2.1.2、卸载OpenJDK

        CentOS 7已经安装了OpenJDK,但是Elasticsearch 7.9.1自带了JDK11,所以最好先卸载OpenJDK。

rpm -qa|grep java
rpm -e --nodeps java-1.7.0-openjdk-headless-1.7.0.191-2.6.15.5.el7.x86_64
rpm -e --nodeps java-1.8.0-openjdk-headless-1.8.0.181-7.b13.el7.x86_64

        卸载后,可以使用java -version进行测试,如果代码不能运行则说明OpenJDK卸载成功。

2.1.3、下载Elasticsearch 7.9.1

        本节将使用tar.gz格式的安装包搭建集群。登录Elastic官方网站,在Elasticsearch 7.9.1的下载页面选择LINUX X86_64的安装包并下载。

2.1.4、增大文件句柄数

        Elasticsearch在运行时会消耗大量的文件句柄,使用前应该增大文件句柄数的值以防止文件句柄不够用导致丢失数据。以管理员权限打开/etc/security/limits.conf,在末尾添加以下两行代码。        

* soft nofile 65535
* hard nofile 65535

        这两行代码的意思是,对于任意用户(*),将应用软件(soft)级别和操作系统(hard)级别的最大可打开文件数设置为65535。

2.1.5、增大线程池的大小

        Elasticsearch运行时会拥有很多线程池,各种线程池能处理不同类型的请求。为了让线程池拥有的线程够用,需要增大线程池中可创建线程的数量。打开/etc/security/limits.conf,在末尾添加以下两行代码。

* soft nproc 9000
* hard nproc 9000

        由于上述配置受到/etc/security/limits.d/20-nproc.conf的限制,需要在这个文件中把普通用户的线程数量也增大到9000。

*          soft    nproc     9000
root       soft    nproc     unlimited
2.1.6、关闭内外存交换

        打开内存锁对于提升Elasticsearch的搜索性能非常重要。为了打开内存锁,需在文件/etc/security/limits.conf的末尾添加如下代码。

* hard memlock unlimited
* soft memlock unlimited
2.1.7、增大虚拟内存地址

        由于Elasticsearch默认使用mmapfs文件系统存储数据,它会消耗很多虚拟内存,因此需要增大虚拟内存地址空间。在文件/etc/sysctl.conf的末尾添加以下代码。

vm.max_map_count = 262144

   在3台虚拟机中修改完以上配置后,需要重启虚拟机以使配置生效。

2.2、安装集群

        本节准备好的3台虚拟机的IP地址分别为192.168.145.131、192.168.145.132、192.168.145.133,现在就使用这3个节点搭建一个Elasticsearch集群。

2.2.1、解压安装包 

        先将Elasticsearch安装到opt目录下,使用以下解压命令。

tar -zxvf elasticsearch-7.9.1-linux-x86_64.tar.gz
2.2.2、添加用户并授权

        由于TAR包不能通过root用户启动,需要单独创建用户elasticsearch并授予其安装目录的读写权限。

useradd elasticsearch
chown -R elasticsearch:elasticsearch /opt/elasticsearch-7.9.1/
2.2.3、编辑elasticsearch.yml文件

        修改节点192.168.145.131的elasticsearch.yml文件,代码如下。注意,配置项discovery.seed_hosts和cluster.initial_master_nodes要包含整个集群的主候选节点列表,这里把3个节点都配置为主候选节点。

cluster.name: my-application
node.name: node1
path.data: /opt/elasticsearch-7.9.1/data
path.logs: /opt/elasticsearch-7.9.1/logs
bootstrap.memory_lock: true
network.host: 192.168.145.131
http.port: 9200
discovery.seed_hosts: ["192.168.145.131", "192.168.145.132", "192.168.145.133"]
cluster.initial_master_nodes: ["node1", "node2", "node3"]
gateway.expected_data_nodes: 3

        修改节点192.168.145.132的elasticsearch.yml文件,代码如下。

cluster.name: my-application
node.name: node2
path.data: /opt/elasticsearch-7.9.1/data
path.logs: /opt/elasticsearch-7.9.1/logs
bootstrap.memory_lock: true
network.host: 192.168.145.132
http.port: 9200
discovery.seed_hosts: ["192.168.145.131", "192.168.145.132", "192.168.145.133"]
cluster.initial_master_nodes: ["node1", "node2", "node3"]
gateway.expected_data_nodes: 3

        修改节点192.168.145.133的elasticsearch.yml文件,代码如下。

cluster.name: my-application
node.name: node3
path.data: /opt/elasticsearch-7.9.1/data
path.logs: /opt/elasticsearch-7.9.1/logs
bootstrap.memory_lock: true
network.host: 192.168.145.133
http.port: 9200
discovery.seed_hosts: ["192.168.145.131", "192.168.145.132", "192.168.145.133"]
cluster.initial_master_nodes: ["node1", "node2", "node3"]
gateway.expected_data_nodes: 3

        注意:非主候选节点不需要配置cluster.initial_master_nodes,它用于指定集群引导时的主候选节点列表,该配置只有在新集群启动时才有效。

2.2.4、修改JVM堆内存大小

        由于这里使用的节点内存为1GB,因此堆内存大小应该设置为256m。在实际中要根据服务器的内存进行配置,确保压缩对象指针是开启的。在jvm.options文件中配置以下两行内容。

-Xms256m
-Xmx256m

        注意!!如果es启动报错,一定要检查这个内存设置是否正常,可以调小或者调大试试。

我这边之前设置的是512m,一直报错,后来改成256m就可以正常启动了

2.2.5、启动集群

        在3个节点上分别运行以下代码。

su elasticsearch
cd /opt/elasticsearch-7.9.1
./bin/elasticsearch -d -p pid

        上述启动命令使用了-d实现在后台运行Elasticsearch服务,并且将服务的进程id写入pid文件进行保存。如果需要关闭Elasticsearch服务,直接可以使用以下代码。

kill -9 `cat pid`

2.3、验证安装

2.3.1、检查文件句柄数和线程池大小

        在控制台输入并运行以下代码。

su elasticsearch
ulimit -a

        从以下结果可以确定open files应该为65536、max user processes应该为9000。

core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 3795
max locked memory       (kbytes, -l) unlimited
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65536
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 9000
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
2.3.2、检查虚拟内存配置

        在控制台输入并运行以下代码。

sysctl -a|grep vm.max_map_count

        可以在控制台看到vm.max_map_count的值为262144。

2.3.3、检查集群服务的健康状态和节点数

        在Kibana中运行以下代码。

GET _cat/health?v&format=json

        得到集群的健康状态是green,还可以看到集群的节点数为3。

[
  {
    "epoch" : "1699414125",
    "timestamp" : "03:28:45",
    "cluster" : "my-application",
    "status" : "green",
    "node.total" : "3",
    "node.data" : "3",
    "shards" : "8",
    "pri" : "4",
    "relo" : "0",
    "init" : "0",
    "unassign" : "0",
    "pending_tasks" : "0",
    "max_task_wait_time" : "-",
    "active_shards_percent" : "100.0%"
  }
]
2.3.4、检查内存锁的开启状态

        在Kibana中运行以下代码。

GET _nodes?filter_path=**.mlockall

        可得到3个节点的内存锁的开启结果,mlockall为true表示开启成功。

{
  "nodes" : {
    "vVJyFfg0TvmHkCNeabsEOA" : {
      "process" : {
        "mlockall" : true
      }
    },
    "Li0X0iKLRViIkRx8sv8LBg" : {
      "process" : {
        "mlockall" : true
      }
    },
    "bmL_o31FRFenpqbOia7hHA" : {
      "process" : {
        "mlockall" : true
      }
    }
  }
}

        如果以上配置均无误,那么恭喜你集群已搭建成功。

3、推荐的集群配置

        你已经搭建了一个拥有3个节点的集群。然而在实际项目中,有一些配置在搭建集群时虽然不是必选的,但是其有利于使整个集群在更好的状态下运行,建议在实际的项目中尽可能采用这些可选配置。

3.1、主候选节点不是越多越好

        在介绍搭建集群时,给cluster.initial_master_nodes配置了3个节点,用于在集群启动时指明主候选节点的列表。在配置主候选节点的列表时,往往选择集群中比较稳定、很少会下线的节点。在集群环境下,主候选节点至少要有3个。对于比较小的集群而言,例如20个节点的集群,3个或5个主候选节点就足够了。当主候选节点有一半以上没有成功启动时,整个集群将处于不可用的状态,这在集群重启或者有大量主候选节点需要下线时非常不方便。因此在集群中挑选少数几(奇数)个比较固定的节点使其作为主候选节点是比较明智的。如果你想一次性下线超过一半的主候选节点,则需要调用以下REST端点来把它们从投票配置中删除。例如,删除两个主候选节点的投票配置node1和node2可以使用以下代码。

POST _cluster/voting_config_exclusions?node_names=node1,node2

        为了能看到效果,你可以从集群的元数据中找到投票配置的节点信息。

GET /_cluster/state?filter_path=metadata.cluster_coordination

        可以看到响应结果的voting_config_exclusions中的就是已经删除的主候选节点。

{
  "metadata" : {
    "cluster_coordination" : {
      "term" : 39,
      "last_committed_config" : [
        "Li0X0iKLRViIkRx8sv8LBg"
      ],
      "last_accepted_config" : [
        "Li0X0iKLRViIkRx8sv8LBg"
      ],
      "voting_config_exclusions" : [
        {
          "node_id" : "bmL_o31FRFenpqbOia7hHA",
          "node_name" : "node1"
        },
        {
          "node_id" : "vVJyFfg0TvmHkCNeabsEOA",
          "node_name" : "node2"
        }
      ]
    }
  }
}

        而last_committed_config中的就是当前有效投票配置的节点id列表,它会在主节点选举的时候进行投票。要下线的主候选节点被删除后,它们就可以安全地下线而不会影响集群的正常运转。

        如果希望下线的节点回到集群,你可以使用以下REST端点清空投票配置删除的节点列表。

DELETE /_cluster/voting_config_exclusions?wait_for_removal=false

        运行上述代码后,可以从集群的元数据看到投票配置的节点信息又恢复成了3条

GET /_cluster/state?filter_path=metadata.cluster_coordination


{
  "metadata" : {
    "cluster_coordination" : {
      "term" : 40,
      "last_committed_config" : [
        "bmL_o31FRFenpqbOia7hHA",
        "vVJyFfg0TvmHkCNeabsEOA",
        "Li0X0iKLRViIkRx8sv8LBg"
      ],
      "last_accepted_config" : [
        "bmL_o31FRFenpqbOia7hHA",
        "vVJyFfg0TvmHkCNeabsEOA",
        "Li0X0iKLRViIkRx8sv8LBg"
      ],
      "voting_config_exclusions" : [ ]
    }
  }
}

3.2、每个节点的内存最好是64GB

        在实际项目中,如果你有条件使用虚拟机,请为安装Elasticsearch的每个节点分配64GB的内存,这会让Elasticsearch运行在最佳的状态。如果节点内存小于16GB,运行时内存容易被耗尽。如果内存大于64GB,你依然应把Elasticsearch的JVM的堆内存配置在31GB以内,以开启对象压缩指针从而提高内存的使用效率。

3.3、有条件使用固态盘更好

        Elasticsearch最终会将索引的数据持久化到硬盘上,对于搜索时不在内存中的数据也需要到硬盘中读取,使用读写速度更快的固态盘对提升索引和搜索的效率都有好处。由于硬盘的写入速度比内存的慢很多,它可能会成为索引写入性能的短板,因此如果条件允许,建议多使用固态盘。

3.4、限制单个节点能够容纳的分片上限

        一个节点拥有的分片数越多,查询时需要消耗的内存就越大。使用时,节点的每1GB堆内存不要容纳超过20个分片,否则就容易出现内存耗尽的风险。对于一个堆内存大小为30GB的节点而言,你可以使用以下请求配置集群中每个节点的分片不超过600个。

PUT /_cluster/settings
{
  "persistent" : {
    "cluster.routing.allocation.total_shards_per_node" : "600"
  }
}

3.5、限制单个索引在每个节点上的分片上限

        如果同一个索引在单个节点上的分片数过多,在搜索时就很可能会选择来自同一个节点的多个分片,这些分片会消耗同一个节点上的硬件资源。当分片的容量较大时很容易耗尽内存,会导致集群崩溃。为了避免这种情况,你可以限制某个索引在每个节点上分配的分片上限。例如:

PUT user
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1,
    "index.routing.allocation.total_shards_per_node":2
  },
  "mappings": {
    "properties": {
      "name":{
        "type": "text"
      }
    }
  }
}

        配置项index.routing.allocation.total_shards_per_node指定索引user在每个节点上最多拥有两个分片。此时查看user索引的分片分配情况,会返回error,因为没有足够的节点可以用来分配分片。为了让集群重新回归到健康状态,你需要新增节点,让每一个分片都得到分配。

GET _cat/shards/user?v

{
  "error" : {
    "root_cause" : [
      {
        "type" : "illegal_argument_exception",
        "reason" : "duration cannot be negative, was given [-6889284642556]"
      }
    ],
    "type" : "illegal_argument_exception",
    "reason" : "duration cannot be negative, was given [-6889284642556]"
  },
  "status" : 400
}

3.6、遵守集群高可用的原则

        为了确保集群在生产环境中能够保持高可用,以下原则必须遵守。

        (1)集群节点至少要有3个,主候选节点也至少要有3个。

        (2)存放索引分片的数据节点至少要有2个,以避免出现单点故障。

        (3)每个索引分片至少要有1个副本分片,以避免节点掉线引发数据丢失。

        (4)集群健康状态要为绿色,以确保所有的副本分片都得到分配。

3.7、大规模的集群可以考虑分区容错

        针对大规模的集群,可以使用分片分配的感知把分片分发到不同的区域上。可以使用同一服务器机架、同一机房或同一地区作为分区的边界,这样即使同一区域的节点全部掉线也不会影响集群的正常运转。分区至少要有3个,因为只拥有2个分区的集群会导致某个分区挂掉,可能会引起整个集群不可用。下图展示了只拥有2个分区的集群存在的问题,当zone1挂掉时会引起整个集群不可用。这是因为不管3个主候选节点在2个分区中如何分配,拥有主候选节点多的分区一旦挂掉,整个集群就会处于不可用的状态。

        当分区达到3个或3个以上时,你就可以从中挑出3个分区,每个分区指定1个主候选节点,这样任何一个分区的节点全部挂掉,依然至少能保证2个主候选节点正常,使得整个集群依然可用,如图所示。

4、集群监控

        集群运行以后,可以使用一系列的监控端点对Elasticsearch的运行状态进行监控,以便于开发和运维人员及时掌握Elasticsearch的运行情况。

4.1、监控集群的状态信息

        集群状态信息是一种数据结构,里面包含节点的信息、集群的配置、索引的映射和每个节点分片分配的情况信息等元数据。首先你必须了解集群状态信息发布的整个过程,然后使用以下请求查看集群的状态信息。

GET _cluster/state

        得到的数据结果如下。

{
  "cluster_name" : "my-application",
  "cluster_uuid" : "118bZkX8Sdap_bEmn0nebw",
  "version" : 262,
  "state_uuid" : "lrhNxZNZTLa4ejbaecEzqA",
  "master_node" : "Li0X0iKLRViIkRx8sv8LBg",
  "blocks" : { },
  "nodes" : {
    "Li0X0iKLRViIkRx8sv8LBg" : {
      "name" : "node3",
      "ephemeral_id" : "4XzurtpqRh2TL_5o2KQaZg",
      "transport_address" : "192.168.145.133:9300",
      "attributes" : {
        "ml.machine_memory" : "1019383808",
        "ml.max_open_jobs" : "20",
        "xpack.installed" : "true",
        "transform.node" : "true"
      }
    },
    "bmL_o31FRFenpqbOia7hHA" : {
      "name" : "node1",
      "ephemeral_id" : "Ovqx-LUoTDqBrbDZnOZG1Q",
      "transport_address" : "192.168.145.131:9300",
      "attributes" : {
        "ml.machine_memory" : "1019383808",
        "ml.max_open_jobs" : "20",
        "xpack.installed" : "true",
        "transform.node" : "true"
      }
    },
    "vVJyFfg0TvmHkCNeabsEOA" : {
      "name" : "node2",
      "ephemeral_id" : "DnUu0C3pRUCI5pR2qxuKxQ",
      "transport_address" : "192.168.145.132:9300",
      "attributes" : {
        "ml.machine_memory" : "1019609088",
        "ml.max_open_jobs" : "20",
        "xpack.installed" : "true",
        "transform.node" : "true"
      }
    }
  },
  "metadata" : {
    "cluster_uuid" : "118bZkX8Sdap_bEmn0nebw",
    "cluster_uuid_committed" : true,
    "cluster_coordination" : {
      "term" : 40,
      "last_committed_config" : [
        "bmL_o31FRFenpqbOia7hHA",
        "vVJyFfg0TvmHkCNeabsEOA",
        "Li0X0iKLRViIkRx8sv8LBg"
      ],
      "last_accepted_config" : [
        "bmL_o31FRFenpqbOia7hHA",
        "vVJyFfg0TvmHkCNeabsEOA",
        "Li0X0iKLRViIkRx8sv8LBg"
      ],
      "voting_config_exclusions" : [ ]
    },
      "templates" : {……},
      "indices" : {……},
      "component_template" :{……},
      "index-graveyard" : {……},
      "index_template" : {……},
      "index_lifecycle":{……},
      "ingest" : {……}
  },
  "routing_table" : {……},
  "routing_nodes" : {……}
}

        其中nodes代表的是集群节点的列表;metadata代表的是集群的元数据,包含集群的投票配置、索引映射、索引模板等信息;routing_table中罗列了每个索引的分片在集群中分布的位置信息;routing_nodes列出了每个节点包含的索引分片的列表和未得到分配的分片列表。由于这种数据结构比较大,查看不太方便,你可以通过传参的方式只查看部分结果,代码如下。

GET /_cluster/state/routing_table/order

        这个请求表示只查看索引order的routing_table信息,也就是分片分配的信息。

        以下请求表示只返回状态数据中的节点列表信息,使用时可以根据实际的需要来选择有用的状态信息进行查看。

GET /_cluster/state/nodes

4.2、监控集群的健康状态

        首先了解一下监控索引的健康状态,集群的健康状态跟健康状态最差的索引的是一致的,也就是说,如果集群中某个索引的健康状态是红色,则集群的健康状态也是红色。查看集群的健康状态可以使用以下请求

GET _cluster/health

        可以得到如下结果。

{
  "cluster_name" : "my-application",
  "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.0
}

        你可以从status中看出集群的健康状态是绿色,还可以看到集群的节点总数、分片总数、未得到分配的分片总数等信息。如果你想查看某个索引的健康状态,可以在该请求中添加索引名称的参数,代码如下。

GET _cluster/health/order

        结果如下

{
  "cluster_name" : "my-application",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 3,
  "number_of_data_nodes" : 3,
  "active_primary_shards" : 3,
  "active_shards" : 6,
  "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.0
}

4.3、监控集群节点的统计指标

         首先了解一下索引的统计指标的监控方法,本小节来介绍如何监控整个集群或某个节点的统计数据。要查看整个集群的统计指标,可以使用以下请求。

GET _cluster/stats

        该请求返回的结果如下。

{
  "_nodes" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "cluster_name" : "my-application",
  "cluster_uuid" : "118bZkX8Sdap_bEmn0nebw",
  "timestamp" : 1699425817598,
  "status" : "green",
  "indices" : {……},
  "nodes" : {……}
}

        其中indices部分包含索引在整个集群中拥有的分片数、文档数、占用空间、缓存量等统计数据;nodes部分包含整个集群的节点数、JVM内存大小等统计数据。

        如果你想查看每个节点的统计指标,可以使用_nodes端点返回各个节点的统计数据的列表。

GET _nodes/stats

        该请求返回每个节点的统计结果,而非整个集群的统计结果。结果如下。

{
  "_nodes" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "cluster_name" : "my-application",
  "nodes" : {
    "Li0X0iKLRViIkRx8sv8LBg" : {
      "timestamp" : 8588710654170,
      "name" : "node3",
      "transport_address" : "192.168.145.133:9300",
      "host" : "192.168.145.133",
      "ip" : "192.168.145.133:9300",
......

        Elasticsearch还为使用者提供了一个usage端点用来查看每个节点处理每种请求的次数,这个端点的响应结果也能够作为节点的统计依据,代码如下。

GET _nodes/usage

        结果如下

{
  "_nodes" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "cluster_name" : "my-application",
  "nodes" : {
    "Li0X0iKLRViIkRx8sv8LBg" : {
      "timestamp" : 8588710839062,
      "since" : 1699828608634,
      "rest_actions" : {
        "document_update_action" : 67,
        "nodes_info_action" : 239,
        "document_index_action" : 7,
        "ml_get_jobs_action" : 7,
        "indices_stats_action" : 7,
        "update_by_query_action" : 161,
        "monitoring_bulk" : 111,
        "xpack_info_action" : 35,
        "search_action" : 644,
        "document_get_action" : 60,
        "delete_by_query_action" : 6,
        "bulk_action" : 17,
        "main_action" : 4
      },
      "aggregations" : {
        "date_histogram" : {
          "date" : 7
        },
        "terms" : {
          "bytes" : 7
        },
        "scripted_metric" : {
          "other" : 49
        },
        "sum" : {
          "numeric" : 7
        },
        "filters" : {
          "other" : 12
        },
        "nested" : {
          "other" : 13
        },
        "cardinality" : {
          "bytes" : 534
        }
      }
    },
    "bmL_o31FRFenpqbOia7hHA" : {
      "timestamp" : 1699426189651,
      "since" : 1699422713770,
      "rest_actions" : {
        "nodes_usage_action" : 1,
        "document_update_action" : 172,
        "get_index_template_action" : 73,
        "nodes_info_action" : 915,
        "get_mapping_action" : 73,
        "create_index_action" : 10,
        "ml_get_jobs_action" : 9,
        "nodes_stats_action" : 2,
        "delete_index_action" : 4,
        "update_by_query_action" : 837,
        "monitoring_bulk" : 122,
        "xpack_info_action" : 171,
        "document_get_action" : 286,
        "delete_by_query_action" : 3,
        "cluster_health_action" : 2,
        "main_action" : 27,
        "cat_shards_action" : 15,
        "get_settings_action" : 2,
        "document_index_action" : 7,
        "indices_stats_action" : 8,
        "document_create_action" : 1,
        "cluster_state_action" : 6,
        "cluster_stats_action" : 2,
        "put_index_template_action" : 1,
        "search_action" : 1329,
        "clear_voting_config_exclusions_action" : 1,
        "get_aliases_action" : 73,
        "bulk_action" : 72,
        "cluster_update_settings_action" : 1
      },
      "aggregations" : {
        "cardinality" : {
          "bytes" : 548
        }
      }
    },
    "vVJyFfg0TvmHkCNeabsEOA" : {
      "timestamp" : 9180305045192,
      "since" : 1699421338020,
      "rest_actions" : {
        "document_update_action" : 61,
        "nodes_info_action" : 202,
        "document_index_action" : 9,
        "create_index_action" : 2,
        "ml_get_jobs_action" : 7,
        "indices_stats_action" : 8,
        "update_by_query_action" : 206,
        "document_create_action" : 2,
        "monitoring_bulk" : 111,
        "xpack_info_action" : 41,
        "search_action" : 613,
        "document_get_action" : 71,
        "delete_by_query_action" : 8,
        "bulk_action" : 24,
        "main_action" : 4
      },
      "aggregations" : {
        "date_histogram" : {
          "date" : 10
        },
        "terms" : {
          "bytes" : 10
        },
        "scripted_metric" : {
          "other" : 87
        },
        "sum" : {
          "numeric" : 10
        },
        "filters" : {
          "other" : 22
        },
        "nested" : {
          "other" : 21
        },
        "cardinality" : {
          "bytes" : 537
        }
      }
    }
  }
}

        其中since表示节点的启动时间、timestamp表示请求响应的时间,可以从结果中看到节点处理各种操作的统计数据,例如search_action表示处理搜索的次数,get_mapping_action表示查询映射的次数。

4.4、监控节点的热点线程

        使用监控节点的热点线程能够找出CPU占用多的操作,可便于开发和运维人员及时发现集群的性能瓶颈。发送监控节点的热点线程的请求如下。

GET _nodes/hot_threads

        以下返回结果是每个节点的一段文本,结果文本包含各个节点热点线程的描述。

::: {node3}{Li0X0iKLRViIkRx8sv8LBg}{4XzurtpqRh2TL_5o2KQaZg}{192.168.145.133}{192.168.145.133:9300}{dilmrt}{ml.machine_memory=1019383808, ml.max_open_jobs=20, xpack.installed=true, transform.node=true}
   Hot threads at 2242-03-02T09:03:18.804Z, interval=500ms, busiestThreads=3, ignoreIdleThreads=true:

::: {node1}{bmL_o31FRFenpqbOia7hHA}{Ovqx-LUoTDqBrbDZnOZG1Q}{192.168.145.131}{192.168.145.131:9300}{dilmrt}{ml.machine_memory=1019383808, xpack.installed=true, transform.node=true, ml.max_open_jobs=20}
   Hot threads at 2023-11-08T06:52:29.466Z, interval=500ms, busiestThreads=3, ignoreIdleThreads=true:
   
    0.1% (271.3micros out of 500ms) cpu usage by thread 'elasticsearch[node1][scheduler][T#1]'
     9/10 snapshots sharing following 2 elements
       java.base@14.0.1/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
       java.base@14.0.1/java.lang.Thread.run(Thread.java:832)

::: {node2}{vVJyFfg0TvmHkCNeabsEOA}{DnUu0C3pRUCI5pR2qxuKxQ}{192.168.145.132}{192.168.145.132:9300}{dilmrt}{ml.machine_memory=1019609088, ml.max_open_jobs=20, xpack.installed=true, transform.node=true}
   Hot threads at 2260-11-29T12:46:44.959Z, interval=500ms, busiestThreads=3, ignoreIdleThreads=true:
   
    5.2% (26ms out of 500ms) cpu usage by thread 'elasticsearch[node2][transport_worker][T#2]'
     2/10 snapshots sharing following 61 elements
       app//org.elasticsearch.rest.action.search.RestSearchAction.parseSearchRequest(RestSearchAction.java:137)
       app//org.elasticsearch.rest.action.search.RestSearchAction.lambda$prepareRequest$1(RestSearchAction.java:113)
       app//org.elasticsearch.rest.action.search.RestSearchAction$$Lambda$5305/0x0000000801985840.accept(Unknown Source)
       app//org.elasticsearch.rest.RestRequest.withContentOrSourceParamParserOrNull(RestRequest.java:470)
       app//org.elasticsearch.rest.action.search.RestSearchAction.prepareRequest(RestSearchAction.java:112)
       app//org.elasticsearch.rest.BaseRestHandler.handleRequest(BaseRestHandler.java:94)
       org.elasticsearch.xpack.security.rest.SecurityRestFilter.handleRequest(SecurityRestFilter.java:81)
       app//org.elasticsearch.rest.RestController.dispatchRequest(RestController.java:236)
       app//org.elasticsearch.rest.RestController.tryAllHandlers(RestController.java:318)
       app//org.elasticsearch.rest.RestController.dispatchRequest(RestController.java:176)
       app//org.elasticsearch.http.AbstractHttpServerTransport.dispatchRequest(AbstractHttpServerTransport.java:318)
       app//org.elasticsearch.http.AbstractHttpServerTransport.handleIncomingRequest(AbstractHttpServerTransport.java:372)
       app//org.elasticsearch.http.AbstractHttpServerTransport.incomingRequest(AbstractHttpServerTransport.java:308)
       org.elasticsearch.http.netty4.Netty4HttpRequestHandler.channelRead0(Netty4HttpRequestHandler.java:42)
       org.elasticsearch.http.netty4.Netty4HttpRequestHandler.channelRead0(Netty4HttpRequestHandler.java:28)
       io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       org.elasticsearch.http.netty4.Netty4HttpPipeliningHandler.channelRead(Netty4HttpPipeliningHandler.java:58)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
       io.netty.handler.codec.MessageToMessageCodec.channelRead(MessageToMessageCodec.java:111)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
       io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.timeout.IdleStateHandler.channelRead(IdleStateHandler.java:286)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
       io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
       io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
       io.netty.channel.nio.NioEventLoop.processSelectedKeysPlain(NioEventLoop.java:615)
       io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:578)
       io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
       io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
       io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
       java.base@14.0.1/java.lang.Thread.run(Thread.java:832)
     8/10 snapshots sharing following 55 elements
       org.elasticsearch.xpack.security.rest.SecurityRestFilter.handleRequest(SecurityRestFilter.java:81)
       app//org.elasticsearch.rest.RestController.dispatchRequest(RestController.java:236)
       app//org.elasticsearch.rest.RestController.tryAllHandlers(RestController.java:318)
       app//org.elasticsearch.rest.RestController.dispatchRequest(RestController.java:176)
       app//org.elasticsearch.http.AbstractHttpServerTransport.dispatchRequest(AbstractHttpServerTransport.java:318)
       app//org.elasticsearch.http.AbstractHttpServerTransport.handleIncomingRequest(AbstractHttpServerTransport.java:372)
       app//org.elasticsearch.http.AbstractHttpServerTransport.incomingRequest(AbstractHttpServerTransport.java:308)
       org.elasticsearch.http.netty4.Netty4HttpRequestHandler.channelRead0(Netty4HttpRequestHandler.java:42)
       org.elasticsearch.http.netty4.Netty4HttpRequestHandler.channelRead0(Netty4HttpRequestHandler.java:28)
       io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       org.elasticsearch.http.netty4.Netty4HttpPipeliningHandler.channelRead(Netty4HttpPipeliningHandler.java:58)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
       io.netty.handler.codec.MessageToMessageCodec.channelRead(MessageToMessageCodec.java:111)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
       io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.handler.timeout.IdleStateHandler.channelRead(IdleStateHandler.java:286)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
       io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
       io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
       io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
       io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
       io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
       io.netty.channel.nio.NioEventLoop.processSelectedKeysPlain(NioEventLoop.java:615)
       io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:578)
       io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
       io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
       io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
       java.base@14.0.1/java.lang.Thread.run(Thread.java:832)

4.5、查看慢搜索日志

        Elasticsearch还允许你通过慢搜索日志查看那些搜索速度比较慢的请求,你可以设置每个索引查询耗时的阈值来界定哪些慢搜索请求会被写入慢搜索日志,例如:

PUT order/_settings
{
  "index.search.slowlog.threshold.query.warn": "10ms",
  "index.search.slowlog.threshold.query.info": "0ms",
  "index.search.slowlog.threshold.query.debug": "0ms",
  "index.search.slowlog.threshold.query.trace": "0ms",
  "index.search.slowlog.threshold.fetch.warn": "10ms",
  "index.search.slowlog.threshold.fetch.info": "0ms",
  "index.search.slowlog.threshold.fetch.debug": "0ms",
  "index.search.slowlog.threshold.fetch.trace": "0ms",
  "index.search.slowlog.level": "info"
}

        该设置表示把order索引的query请求阶段耗时达到10ms的请求输出为warn日志,fetch取回阶段超过10ms的请求也输出为warn日志。配置slowlog.level为info表示只记录info及其以上级别的日志。查询索引后,超过阈值并达到相应级别的慢日志会被记录在查询涉及的每个节点中,可以到各个节点的logs文件夹中查看,慢日志文件名为 my-application_index_search_slowlog.log。例如在node1上,做一次match_all查询后,logs文件夹生成的慢日志片段如下。

[2023-11-08T14:56:38,966][WARN ][i.s.s.query              ] [node1] [order][1] took[682.9ms], took_millis[682], total_hits[0 hits], types[], stats[], search_type[QUERY_THEN_FETCH], total_shards[3], source[{"query":{"match_all":{"boost":1.0}}}], id[],

        慢日志是分片级别的,这个片段只展示了match_all查询在node-1上耗费的时间、命中数量等信息,进入其他节点可以看到查询请求在其他节点分片上产生的慢搜索日志。

4.6、查看慢索引日志

        慢索引日志允许你把写入索引较慢的请求记录到日志中,用于定位写入速度慢的请求,以便有针对性地优化它们。首先,设置慢索引日志的记录阈值。

PUT order/_settings
{
  "index.indexing.slowlog.threshold.index.warn": "10ms",
  "index.indexing.slowlog.threshold.index.info": "0ms",
  "index.indexing.slowlog.threshold.index.debug": "0ms",
  "index.indexing.slowlog.threshold.index.trace": "0ms",
  "index.indexing.slowlog.level": "info",
  "index.indexing.slowlog.source": "1000"
}

        以上设定的意思是,写入索引order耗时超过10ms的请求将生成一个warn日志,slowlog.level限定了只记录info及其以上级别的日志。配置slowlog.source为1000表示在日志中最多记录索引数据的前1000个字符。向order中写入一条数据

PUT order/_doc/1
{
  "name":"苹果一斤'"
}

        然后在logs文件夹下打开my-application_index_indexing_slowlog.log,可以看到以下片段。

[2242-03-02T17:12:31,082][WARN ][i.i.s.index              ] [node3] [order/1wRgN7dpREaPW1RWpaf5YA] took[364ms], took_millis[364], type[_doc], id[1], routing[], source[{"name":"苹果一斤'"}]

        该日志记录了写入请求的数据、耗时、节点名称以及日志的级别。慢索引日志也是分片级别的,因此只有数据写入分片所在的节点才能看到该数据的慢索引日志。

5、索引分片数的设置与横向扩容

        在9.x版本的Elasticsearch中,新建的索引默认包含5个主分片,每个主分片有1个副本分片。而在7.9.1版本中,新建的索引默认只包含一个主分片和一个副本分片。那么一个索引的主分片到底设置为多大比较合适呢?本节就来详细地探讨这个问题。

        一个索引拥有的主分片越多,它就能容纳越多的数据。由于主分片个数属于索引的静态配置,一旦索引被创建就无法修改,这意味着索引创建以后它能够容纳的数据的上限就基本固定了。为了维持集群的稳定性,单个分片的数据容量不宜过大,最好不要超过25GB。过大的分片会使得分片的恢复极为缓慢,而且在搜索时也会消耗更多的内存。所以,当你为一个索引确定分片个数时,你要考虑该索引需要容纳的存量数据有多少,每个月的增量数据有多少,然后来设置主分片的个数。如果在每个分片中容纳的数据已经达到25GB,还有新的数据需要写入,就需要把它们保存到新的索引中。

        假如有一些订单每个月会产生100GB数据,订单产生的数据的字段说明如表所示。

订单字段说明
字段名说明
order_id订单ID
name订单名称
amount订单金额
type订单类型
status订单状态
create_time订单创建时间

        为了在索引中保存上述结构的订单数据并进行搜索和数据分析,下面将采取一系列的步骤来完成数据的存储并对索引容量进行线性扩展。

5.1、容量规划

        根据订单每个月的增量数据大小100GB,为每个分片规划25GB大小,可以按照月份对索引进行切分。每月写入1个索引,每个索引包含4个主分片,每个主分片有1个副本分片即可。为了使用时间条件快速定位索引,可以把年月数据放入索引的名称,例如2023年1月的数据保存的名称为order-202301,其他月份的索引以此类推。

5.2、创建索引模板

        使用索引模板的好处是可以方便开发者快速定义一批映射结构相同的索引。为索引模板配置一个别名order-all作为全局搜索的范围,最新的数据总是会写入最后一个索引,效果如图所示。

        创建索引模板order-template,该模板会匹配所有前缀为order的索引,并在模板中设置主分片个数为4、副本分片个数为1,添加订单的字段列表并配置别名order-all,代码如下。

PUT _index_template/order-template
{
  "index_patterns": ["order-*"],
  "template": {
    "settings": {
      "number_of_shards": 4,
      "number_of_replicas": 1
    },
    "mappings": {
      "properties": {
        "order_id":{
          "type": "keyword"
        },
        "name":{
          "type": "text",
          "fields": {
            "keyword":{
              "type":"keyword"
            }
          }
        },
        "amoun":{
          "type": "double"
        },
        "type":{
          "type": "keyword"
        },
        "status":{
          "type": "integer"
        },
        "create_time":{
          "type": "date",
          "format": ["yyyy-MM-dd HH:mm:ss"]
        }
      }
    },
    "aliases": {
      "order-all": {}
    }
  },
  "priority": 200,
  "version": 1,
  "_meta": {
    "description":"this is order templates"
  }
}

5.3、创建索引并写入数据

        当使用别名写入索引数据时,最多只有一个索引的is_write_index属性可以设置为true,这在写入时显得不够灵活。因为除了写入最新的数据,某些历史数据也可能需要修改。因此,在写入数据时,直接根据数据的create_time字段推断出索引的名称直接写入即可;搜索数据时,使用别名device-all搜索全部数据。

        当写入时间为2023-01-08 15:00:30的数据时,可以执行以下写入请求。

PUT order-202301/_doc/o1-20230108150030
{
  "order_id":"o1",
  "name":"小米手机",
  "amount":"88.00",
  "type":"补货订单",
  "status":808,
  "create_time":"2023-01-08 15:00:30"
}

        根据时间可以判断写入的索引为order-202301,写入后可以查到该索引的映射结构和别名,它们跟之前在索引模板中定义的一致。

GET order-202301/_alias


{
  "order-202301" : {
    "aliases" : {
      "order-all" : { }
    }
  }
}

        如果需要写入时间为2023-02-01 10:00:00的数据,则写入索引的名称需要相应修改,以确保每个月的索引只存储当月的数据。

PUT order-202302/_doc/o2-20230201100000
{
  "order_id":"o2",
  "name":"华为手机",
  "amount":"88.00",
  "type":"补货订单",
  "status":808,
  "create_time":"2023-02-01 10:00:00"
}

        在搜索和统计分析时,直接使用索引别名order-all来搜索全部的数据。

GET order-all/_search
{
  "query": {
    "match": {
      "name": "手机"
    }
  }
}

        结果如下

{
  "took" : 2103,
  "timed_out" : false,
  "_shards" : {
    "total" : 8,
    "successful" : 8,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.5753642,
    "hits" : [
      {
        "_index" : "order-202301",
        "_type" : "_doc",
        "_id" : "o1-20230108150030",
        "_score" : 0.5753642,
        "_source" : {
          "order_id" : "o1",
          "name" : "小米手机",
          "amount" : "88.00",
          "type" : "补货订单",
          "status" : 808,
          "create_time" : "2023-01-08 15:00:30"
        }
      },
      {
        "_index" : "order-202302",
        "_type" : "_doc",
        "_id" : "o2-20230201100000",
        "_score" : 0.5753642,
        "_source" : {
          "order_id" : "o2",
          "name" : "华为手机",
          "amount" : "88.00",
          "type" : "补货订单",
          "status" : 808,
          "create_time" : "2023-02-01 10:00:00"
        }
      }
    ]
  }
}

        以上便是简单的对索引横向扩容的过程,实际项目中需要根据每个月增量数据的大小来设置主分片的大小,避免所有的数据全部写入同一个索引导致分片过大引起集群故障。

6、优化索引的写入速度

        当你有大量数据需要写入时,你肯定希望数据写入的速度很快,本节将列举几种常规的优化索引的写入速度的办法。

6.1、避免写入过大的文档

        虽然Elasticsearch支持二进制类型的字段,但保存文件显然不是Elasticsearch的强项。对于非结构化数据的保存请尽量使用专门的分布式文件系统,把它们写入Elasticsearch对Elasticsearch的性能是一种损害。另外,在建立索引映射时,字段的数量越少越好。在实际项目中,你可能会遇到一张表动辄有200~300个字段,这么多字段都写入Elasticsearch显然并不明智。通常写入Elasticsearch的字段需要符合以下几种情况。

        (1)该字段被用作检索条件。

        (2)该字段用于统计分析。

        (3)该字段经常用于前端展示。

        (4)该字段是文档主键。

        对于文档中不满足以上4种情况的字段并无必要写入Elasticsearch,写入太多的字段既浪费存储空间又影响搜索速度,费力且效果不好。如果偶尔需要查看其余的字段,可以配合其他的存储方案一起使用。例如你可以在HBase中保存完整的文档数据,需要查看完整的字段时直接使用主键到HBase中抓取即可。

6.2、合并写入请求

        使用bulk和reindex操作批量地向索引中写入数据,它们确实能够大大提高索引的建立效率。在使用bulk操作时,你可以逐步增大每一批文档的数量,并观察索引写入的速度以便让建索引的效率变得更高。除了这些操作之外,本小节会再讲几种写入操作,其用于加快特定场景数据的写入速度。

        测试数据:

PUT user
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "userid":{
        "type": "long"
      },
      "username":{
        "type": "text",
        "fields": {
          "keyword":{
            "type":"keyword",
            "ignore_above":256
          }
        }
      },
      "age":{
        "type": "integer"
      },
      "sex":{
        "type": "boolean"
      },
      "born":{
        "type": "date",
        "format": ["yyyy-MM-dd HH:mm:ss"]
      }
    }
  }
}

POST user/_bulk
{"index":{"_id":"1"}}
{"userid":"1","username":"张三","age":"18","sex":"true","born":"2000-01-26 19:00:00"}
{"index":{"_id":"2"}}
{"userid":"2","username":"李四","age":"28","sex":"false","born":"2000-02-26 19:00:00"}
{"index":{"_id":"3"}}
{"userid":"3","username":"王五","age":"48","sex":"true","born":"2000-04-26 19:00:00"}
{"index":{"_id":"4"}}
{"userid":"4","username":"马六","age":"9","sex":"false","born":"2000-05-26 19:00:00"}
{"index":{"_id":"5"}}
{"userid":"5","username":"李大头","age":"26","sex":"true","born":"2000-07-26 19:00:00"}
{"index":{"_id":"6"}}
{"userid":"6","username":"李二狗","age":"88","sex":"true","born":"2000-08-26 19:00:00"}
{"index":{"_id":"7"}}
{"userid":"7","username":"赵四","age":"18","sex":"true","born":"2000-09-26 19:00:00","address":"北京"}
{"index":{"_id":"8"}}
{"userid":"8","username":"宋小宝","age":"28","sex":"false","born":"2000-10-26 19:00:00","address":"上海"}
{"index":{"_id":"9"}}
{"userid":"","username":"后羿","age":"18","sex":"false","born":"2000-10-26 19:00:00","address":"杭州"}
6.2.1、delete_by_query按查询结果删除

        通常情况下开发者会使用主键删除索引中的数据,然而删除大量文档数据时,这个操作会变得比较缓慢。Elasticsearch提供了按查询结果删除的方法,你可以传入一个查询条件,所有命中查询结果的文档会被自动批量删除,使用该方法能够大大提高删除文档的效率。

        以下请求将批量删除索引user中的全部文档。

POST user/_delete_by_query?conflicts=proceed
{
  "query":{
    "match_all":{
      
    }
  }
}

        该请求会返回删除的文档数目,如果有数据删除失败在结果中也会有相应的输出。

{
  "took" : 14647,
  "timed_out" : false,
  "total" : 9,
  "deleted" : 9,
  "batches" : 1,
  "version_conflicts" : 0,
  "noops" : 0,
  "retries" : {
    "bulk" : 0,
    "search" : 0
  },
  "throttled_millis" : 0,
  "requests_per_second" : -1.0,
  "throttled_until_millis" : 0,
  "failures" : [ ]
}

        按查询结果删除会先执行查询得到匹配的文档结果,然后按照每批删除1000个的速度删除文档,如果删除时文档的版本号发生了变化则删除失败。为了不中断删除的执行,上面的请求传入了conflicts=proceed参数,表示遇到错误会继续执行。即使删除中遇到错误,成功删除的文档不会被回滚。如果出现了删除失败的情况,你可以多尝试几次直到没有错误输出为止。

6.2.2、update_by_query按查询更新

        Elasticsearch提供了一个按查询更新的端点,你可以使用它快速地更新索引中的一组文档。下面先新建一个索引person并导入测试数据。

PUT person
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
       "age": {
        "type": "integer"
      }
    }
  }
}
POST person/_bulk
{"index":{"_id":"1"}}
{"name":"王朝","age":20}
{"index":{"_id":"2"}}
{"name":"马汉","age":30}

        假如你想使索引中每个文档的age字段加1,可以使用下面的请求。

POST person/_update_by_query?conflicts=proceed
{
  "script": {
    "source": "ctx._source.age++",
    "lang": "painless"
  },
  "query": {
    "match_all": {}
  }
}

        这个请求使用了一个简单的painless脚本,它的意思是将索引文档的age字段加1。query部分使用了一个match_all查询,表示所有的文档都要执行该脚本。你可以根据业务逻辑的需要调节脚本的内容来批量更新数据。
        当你给索引映射新增字段时,update_by_query也会很有用。例如,你想给name字段添加一个带fields参数的keyword字段,你可以先修改映射。

PUT person/_mapping
{
  "properties": {
    "name": {
      "type": "text",
      "fields": {
        "keyword": {
          "type": "keyword"
        }
      }
    }
  }
}

        虽然添加了一个name.keyword字段,但是由于此时该字段为空,无法在该字段上做精准搜索和聚集统计。为了解决这个问题,你只需要执行一次update_by_query操作。

POST person/_update_by_query?conflicts=proceed
{
  "query": {
    "match_all": {}
  }
}

        然后测试一下精准搜索的效果。

GET person/_search
{
  "query": {
    "term": {
      "name.keyword": {
        "value": "马汉"
      }
    }
  }
}

        会发现相应的数据确实能被搜索出来了。

{
  "took" : 880,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.6931471,
    "hits" : [
      {
        "_index" : "person",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6931471,
        "_source" : {
          "name" : "马汉",
          "age" : 31
        }
      }
    ]
  }
}

6.3、适当增大写入的线程数和索引缓冲区

        为了更大限度地利用集群资源,使用多线程写入索引数据能够提高索引构建的效率。多线程的数量可以通过实际测试来灵活调节,以确保集群能够“扛住”,在索引数据时不报错。

        数据写入Elasticsearch后会首先进入索引的缓冲区,如果缓冲区被写满,里面的文档会被冲刷到磁盘上。适当增大索引缓冲区的大小能够减少索引的冲刷次数从而提供更高的索引吞吐量。一般来讲,某个节点写入的索引的分片数越多则节点的缓冲区就应该设置得越大。默认情况下,每个节点索引缓冲区的大小为堆内存的10%,如果你想修改它的大小,可以在elasticsearch.yml文件中新增以下配置。

indices.memory.index_buffer_size: 15%

7、优化搜索的响应速度

        尽管搜索是Elasticsearch的强项,但你还是需要正确地使用搜索以便结果能够更快地被获取,本节将探讨几种常规的方法,它们可用来优化搜索的速度以改善使用体验。

7.1、避免深度分页

        当你使用普通分页时,from+size值超过10000就属于深度分页,该值越大就越消耗内存,不利于快速得到搜索结果。用户使用搜索引擎时往往只喜欢查阅非常相关的第一页数据,排名过于靠后的搜索结果通常展示的频率不应该太高。如果确实有类似的需求,请尽量使用滚动分页Search after分页

7.2、合并搜索请求

        你可能会遇到这样的场景,对于某个功能需要执行多次搜索才能获取最终结果,如果你能将这些查询条件合并到一个搜索请求中,搜索的效率就会得到提高。

7.2.1、Multi get多主键查询

        Multi get查询允许你使用多个主键一次性从一到多个索引中查询数据,当你有多个主键查询请求需要同时执行时,它会非常有效。例如:

GET _mget
{
  "docs":[
    {
      "_index":"user",
      "_id":3
    },
   {
      "_index":"person",
      "_id":2
    }
  ]
}

        这个请求会一次性查询出索引user中主键为3的文档和索引person中主键为2的文档,得到的结果如下。

{
  "docs" : [
    {
      "_index" : "user",
      "_type" : "_doc",
      "_id" : "3",
      "_version" : 1,
      "_seq_no" : 7,
      "_primary_term" : 2,
      "found" : true,
      "_source" : {
        "userid" : "3",
        "username" : "王五",
        "age" : "48",
        "sex" : "true",
        "born" : "2000-04-26 19:00:00"
      }
    },
    {
      "_index" : "person",
      "_type" : "_doc",
      "_id" : "2",
      "_version" : 3,
      "_seq_no" : 5,
      "_primary_term" : 1,
      "found" : true,
      "_source" : {
        "name" : "马汉",
        "age" : 31
      }
    }
  ]
}
7.2.2、Multi search批量搜索

        Multi search批量搜索允许你像bulk索引请求那样,把多个搜索请求的参数放在请求体中,让多个搜索一起执行,其中index部分指定搜索索引的名称,query部分指定搜索条件,如果有聚集统计的需要也可以将其加入参数。例如:

GET _msearch
{"index":"person"}
{"query":{"match_all":{}}}
{"index":"user"}
{"query":{"term":{"age":{"value":"26"}}}}

        该请求包含两个搜索,一个是索引person的全部文档搜索,另一个是索引user的term搜索。该请求得到的结果是一个数组,包含每个搜索的结果。

{
  "took" : 254,
  "responses" : [
    {
      "took" : 156,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 2,
          "relation" : "eq"
        },
        "max_score" : 1.0,
        "hits" : [
          {
            "_index" : "person",
            "_type" : "_doc",
            "_id" : "1",
            "_score" : 1.0,
            "_source" : {
              "name" : "王朝",
              "age" : 21
            }
          },
          {
            "_index" : "person",
            "_type" : "_doc",
            "_id" : "2",
            "_score" : 1.0,
            "_source" : {
              "name" : "马汉",
              "age" : 31
            }
          }
        ]
      },
      "status" : 200
    },
    {
      "took" : 232,
      "timed_out" : false,
      "_shards" : {
        "total" : 3,
        "successful" : 3,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 1,
          "relation" : "eq"
        },
        "max_score" : 1.0,
        "hits" : [
          {
            "_index" : "user",
            "_type" : "_doc",
            "_id" : "5",
            "_score" : 1.0,
            "_source" : {
              "userid" : "5",
              "username" : "李大头",
              "age" : "26",
              "sex" : "true",
              "born" : "2000-07-26 19:00:00"
            }
          }
        ]
      },
      "status" : 200
    }
  ]
}

7.3、使用缓存加快搜索速度

        Elasticsearch一共提供了3种缓存,分别是字段数据fielddata、分片的请求缓存和节点的查询缓存。这里介绍后两种缓存的使用方法。    

7.3.1、分片的请求缓存    

        当索引执行搜索请求时,每个分片会产生一个局部结果,这个结果的缓存就是分片的请求缓存。该缓存使用查询请求体的json参数作为key值,缓存的内容是该分片搜索到的总数、聚集统计结果和搜索建议。要命中分片的请求缓存需要满足3个条件。

        (1)搜索请求的size必须为0,否则不进行缓存,这也是该缓存不包含搜索结果列表的原因。

        (2)查询的请求内容必须跟缓存的key一样才能命中,如果请求的查询条件或参数变化则无法命中。

        (3)如果索引完成了刷新且数据发生了变化,则该缓存自动失效。

        因此,为了提高分片的请求缓存的命中率,当你对搜索结果的列表不关心时,把size设置为0是一个好的习惯,这在做聚集统计时也能明显提升查询效率。

        要查看索引person占用的分片的请求缓存的大小,可以使用以下请求。

GET person/_stats/request_cache

        可以得到如下结果。

{
  "_shards" : {
    "total" : 2,
    "successful" : 2,
    "failed" : 0
  },
  "_all" : {
    "primaries" : {
      "request_cache" : {
        "memory_size_in_bytes" : 0,
        "evictions" : 0,
        "hit_count" : 0,
        "miss_count" : 0
      }
    },
    "total" : {
      "request_cache" : {
        "memory_size_in_bytes" : 0,
        "evictions" : 0,
        "hit_count" : 0,
        "miss_count" : 0
      }
    }
  },
  "indices" : {
    "person" : {
      "uuid" : "XkgPKgcqQ2qUkq-WZBb53w",
      "primaries" : {
        "request_cache" : {
          "memory_size_in_bytes" : 0,
          "evictions" : 0,
          "hit_count" : 0,
          "miss_count" : 0
        }
      },
      "total" : {
        "request_cache" : {
          "memory_size_in_bytes" : 0,
          "evictions" : 0,
          "hit_count" : 0,
          "miss_count" : 0
        }
      }
    }
  }
}

        默认情况下,分片的请求缓存最多占用堆内存大小的1%,如果你需要修改它的大小,可以在elasticsearch.yml文件中新增以下配置。

indices.requests.cache.size: 5%
7.3.2、节点的查询缓存

        布尔查询使用过滤上下文查出的结果会被缓存,这里的缓存指的就是节点的查询缓存。要命中节点的查询缓存需要满足两个条件。

        (1)查询需要包含过滤上下文,比如布尔查询的filter和must_not部分、constant_score查询的filter部分以及filter过滤器聚集的部分。

        (2)该过滤上下文所查询的索引段的文档数大于10000且超过整个分片文档数的3%,否则对过滤结果不进行缓存。若索引进行了段合并,则该缓存失效。因此,在使用布尔查询时,不需要相关度得分的查询条件应都放到过滤上下文中,这样就有可能命中节点的查询缓存从而可提升查询性能。

        因此,在使用布尔查询时,不需要相关度得分的查询条件应都放到过滤上下文中,这样就有可能命中节点的查询缓存从而可提升查询性能。

        要查看节点的查询缓存占用的内存大小,可以使用节点的stats端点来实现。

GET /_nodes/stats/indices/query_cache

        可以得到如下结果。

{
  "_nodes" : {
    "total" : 3,
    "successful" : 3,
    "failed" : 0
  },
  "cluster_name" : "my-application",
  "nodes" : {
    "Li0X0iKLRViIkRx8sv8LBg" : {
      "timestamp" : 8588717974818,
      "name" : "node3",
      "transport_address" : "192.168.145.133:9300",
      "host" : "192.168.145.133",
      "ip" : "192.168.145.133:9300",
      "roles" : [
        "data",
        "ingest",
        "master",
        "ml",
        "remote_cluster_client",
        "transform"
      ],
      "attributes" : {
        "ml.machine_memory" : "1019383808",
        "ml.max_open_jobs" : "20",
        "xpack.installed" : "true",
        "transform.node" : "true"
      },
      "indices" : {
        "query_cache" : {
          "memory_size_in_bytes" : 0,
          "total_count" : 0,
          "hit_count" : 0,
          "miss_count" : 0,
          "cache_size" : 0,
          "cache_count" : 0,
          "evictions" : 0
        }
      }
    },
    "bmL_o31FRFenpqbOia7hHA" : {
      "timestamp" : 1699433325459,
      "name" : "node1",
      "transport_address" : "192.168.145.131:9300",
      "host" : "192.168.145.131",
      "ip" : "192.168.145.131:9300",
      "roles" : [
        "data",
        "ingest",
        "master",
        "ml",
        "remote_cluster_client",
        "transform"
      ],
      "attributes" : {
        "ml.machine_memory" : "1019383808",
        "xpack.installed" : "true",
        "transform.node" : "true",
        "ml.max_open_jobs" : "20"
      },
      "indices" : {
        "query_cache" : {
          "memory_size_in_bytes" : 0,
          "total_count" : 0,
          "hit_count" : 0,
          "miss_count" : 0,
          "cache_size" : 0,
          "cache_count" : 0,
          "evictions" : 0
        }
      }
    },
    "vVJyFfg0TvmHkCNeabsEOA" : {
      "timestamp" : 9180312181023,
      "name" : "node2",
      "transport_address" : "192.168.145.132:9300",
      "host" : "192.168.145.132",
      "ip" : "192.168.145.132:9300",
      "roles" : [
        "data",
        "ingest",
        "master",
        "ml",
        "remote_cluster_client",
        "transform"
      ],
      "attributes" : {
        "ml.machine_memory" : "1019609088",
        "ml.max_open_jobs" : "20",
        "xpack.installed" : "true",
        "transform.node" : "true"
      },
      "indices" : {
        "query_cache" : {
          "memory_size_in_bytes" : 0,
          "total_count" : 0,
          "hit_count" : 0,
          "miss_count" : 0,
          "cache_size" : 0,
          "cache_count" : 0,
          "evictions" : 0
        }
      }
    }
  }
}

        节点的查询缓存默认最多占用堆内存大小的10%,如果你需要修改它的大小,可以在elasticsearch.yml文件中新增以下配置。

indices.queries.cache.size: 5%

7.4控制搜索请求的路由

        当一个查询请求到达Elasticsearch的时候,它需要选择索引的分片(可以是主分片也可以是副本分片)进行数据检索,这个选择分片的过程就是搜索的分片路由。

        Elasticsearch有一套自带的分片选择方法,可尽可能减少搜索延迟。你也可以自定义分片选择的范围,这在很多时候可以利用缓存机制加快搜索响应的速度。以下请求通过将preference参数设置为_local来把分片选择的范围限制在节点拥有的分片内。

POST user/_search?preference=_local
{
  "query": {
    "query_string": {
      "default_field": "username",
      "query": "李 OR 狗"
    }
  }
}

        你也可以把preference的参数内容改为一个不以“_”开头的字符串,例如人名、地址等。每次给同一个查询传递相同的字符串,就能固定地选择某些分片,能够提高缓存的命中率、加快响应速度,比如:

POST user/_search?preference=abc
{
  "query": {
    "query_string": {
      "default_field": "username",
      "query": "李 OR 狗"
    }
  }
}

        了解过索引的数据路由的话,如果建索引时就带有路由参数,查询时只要带上这个路由参数的值,就能选择数据所在的分片的查询速度,这样也能提高查询效率。

POST user/_search?routing=张三
{
  "query": {
    "match": {
      "name": "张三"
    }
  }
}

8、集群的重启

        当服务器需要重启或者安装了新的插件时,你将不得不重启Elasticsearch集群。正确地重启集群能够减少分片的恢复次数并节约集群重启的时间。Elasticsearch集群的重启分为全集群重启和滚动重启。全集群重启指的是先逐一关闭所有节点再逐一启动所有节点,滚动重启指的是每次重启一个节点直到所有节点重启完成,本节将探讨这两种重启模式的使用方法。

8.1、全集群重启

        当你更新或安装了Elasticsearch的某些插件时,你可能需要进行全集群重启,整个过程需要经历以下几个步骤。

8.1.1、关闭所有写入进程并冲刷数据到磁盘

        为了防止数据丢失,你需要把所有写入Elasticsearch的服务关闭,并且进行索引的冲刷以实现把数据持久化地保存在磁盘上。例如:

POST _flush
8.1.2、禁用分片的分配

        为了避免节点数变化引起的分片分配,重启集群前需要在集群范围内禁止分片的分配。例如:

PUT /_cluster/settings
{
    "transient" : {
        "cluster.routing.allocation.enable" : "none"
    }
}
8.1.3、重启集群

        在每个节点上执行以下命令以关闭集群。

kill -9 `cat pid`

        然后在每个节点上执行以下命令以开启集群。

./bin/elasticsearch -d -p pid
8.1.4、待每个节点加入集群后,启动分片的分配

        使用以下端点查看集群的健康状态和节点数目。

GET _cat/health?v

        等待一段时间后,若集群健康状态从红色恢复到绿色,且每个节点都已加入集群,则重新启动分片的分配。

PUT /_cluster/settings
{
    "transient" : {
        "cluster.routing.allocation.enable" : "all"
    }
}

        至此,整个集群重启成功。

8.2、滚动重启

        滚动重启需要每次重启集群中的一个节点,重启后要确保集群健康状态恢复到绿色再重启下一个节点,在每个节点重启前都需要修改分片分配的设置,所以它比直接重启整个集群更麻烦一些。滚动重启需要经历以下几个步骤。

8.2.1、关闭所有写入进程并冲刷数据到磁盘

        为了防止数据丢失,你需要把所有写入Elasticsearch的服务关闭,并且进行索引的冲刷以实现把数据持久化地保存在磁盘上。例如:

POST _flush
8.2.2、禁用分片的分配

        为了避免节点数变化引起的分片分配,重启集群前需要在集群范围内禁止分片的分配。例如:

PUT /_cluster/settings
{
    "transient" : {
        "cluster.routing.allocation.enable" : "none"
    }
}
8.2.3、选择一个节点重启Elasticsearch服务

        从集群中选择一个节点,先关闭Elasticsearch服务。

kill -9 `cat pid`

        然后将其打开。

./bin/elasticsearch -d -p pid
8.2.4、待该节点加入集群后,启动分片的分配

        使用以下端点查看集群节点数目和集群健康状态。

GET _cat/health?v

        等待几分钟,若该节点成功加入集群,则启动分片的分配。

PUT /_cluster/settings
{
    "transient" : {
        "cluster.routing.allocation.enable" : "all"
    }
}

        等到集群健康状态变为绿色后,对其余节点重复步骤2~步骤4,直到整个集群重启完毕。

9、集群的备份与恢复

        在“大数据时代”,为了保证数据的安全性和完整性,定期备份数据是必不可少的运维操作。Elasticsearch提供了强大的API用于完成对集群数据的备份和恢复。

9.1、搭建共享文件目录

        在使用集群的备份功能以前,你需要搭建一个共享文件目录,集群中每个节点都应当拥有该目录的读写权限。备份数据时,每个节点会把要备份的数据写入共享文件目录;恢复数据时,节点则会从该目录中读取数据并将其恢复到集群。请按照以下步骤来完成共享文件目录的搭建。

9.1.1、在每个节点中指定共享文件目录的地址

        打开每个节点的elasticsearch.yml文件,在里面添加一行代码。

path.repo: /mnt/share

        上述配置为每个节点指定了共享文件目录的路径/mnt/share,该目录用于存放备份快照的仓库。然后重启整个集群使该配置生效。

9.1.2、安装SSHFS创建共享文件目录

        SSHFS是一个开源工具,你可以通过它将一个本地文件目录作为共享文件目录在整个集群中使用。在3个节点中执行下面的命令以安装SSHFS。

yum install -y epel-release
yum -y install fuse-sshfs

        为了将节点192.168.145.131的本地文件目录/opt/backup映射为共享文件目录,先在131服务器上创建文件目录/opt/backup并授权。

mkdir /opt/backup
chmod -R 777 /opt/backup

        然后在3个节点上各创建一个/mnt/share文件目录,用于映射131服务器的/opt/backup目录。

mkdir /mnt/share
chmod -R 777 /mnt
9.1.3、挂载131服务器的/opt/backup到本地文件目录/mnt/share

        在3个节点上执行下面的命令,把131服务器的/opt/backup文件目录挂载到本地的/mnt/share作为共享目录。

sshfs root@192.168.145.131:/opt/backup /mnt/share -o allow_other

        使用以下命令查看共享文件目录挂载结果。

df -h

        为了验证共享文件目录是可用的,你可以在一个节点上往目录/mnt/share中写入一些文件,如果能从其他节点上读取到这些文件,就说明共享文件目录/mnt/share成功搭建完毕。

        本节使用了共享文件目录来作为备份快照的仓库,但这并不是唯一选项。你可以使用第三方插件将备份快照的仓库保存在HDFS、谷歌云存储、微软的Azure、亚马逊的S3中。

9.2、备份集群数据

        在备份集群数据之前,你需要在共享文件目录中创建快照仓库,以便容纳备份的快照数据。发起一个新建快照仓库的请求,代码如下。

PUT /_snapshot/es-repo
{
  "type": "fs",
  "settings": {
    "location": "/mnt/share",
    "compress": true
  }
}

        该请求会创建一个名为es-repo的快照仓库,路径为/mnt/share。请求成功以后,可以使用下面的代码查看新建的仓库信息。

GET /_snapshot/es-repo

        得到如下结果。

{
  "es-repo" : {
    "type" : "fs",
    "settings" : {
      "compress" : "true",
      "location" : "/mnt/share"
    }
  }
}

        为了验证该快照仓库的有效性,可以使用下面的端点来进行测试。

POST /_snapshot/es-repo/_verify

        如果得到集群所有节点的列表,则说明每个节点都能正常连接到该快照仓库,该快照仓库是有效的。

  • 22
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值