elasticsearch 应用知识体系解析

前言

使用 elasticsearch 做开发一年多了,主要查的文档是权威指南和接口详情,这篇文章只是把相关的知识点和重点罗列一下。其中部分经验仅适用于资源较少的情况,资源较多、稳定性要求高的场景应设计更适合的配置和运维流程。

我没有升级过大版本,一直都是用的 5.x 版本。最开始查资料,很多查询都是基于 2.x 的接口写的,已经没有用了。所以以下内容仅基于 5.x 版本。

ES 相关的知识点主要可以分成以下几块:

  1. 基本增删查改(类比 WHERE)和聚合查询语句(类比 GROUP)
  2. 集群内的原理、分片内部原理、索引原理
  3. 集群管理、监控和部署
  4. 数据建模 (类比关系型数据库的表设计)
  5. 其他高级特性(高级搜索、高级数据类型)
  6. elasticsearch-sql、elasticsearch-dsl 等扩展

前端或者后端(非数据开发)岗位有需要时熟悉 1 即可,学习 2、6 有助于提高开发效率和查询执行效率。

数据开发岗位需要熟悉 1、2、4,学习 3、6 有助于提高开发效率和便于调试。需要使用新特性时需要学习 5。

运维岗位需要熟悉 3,集群规模较大时也需要熟悉 2。

学习以上知识点时强烈推荐从权威指南中学习概念,然后到官方网站中查阅与使用的 ES 版本对应的 Reference。

基本增删查改和聚合查询语句

增删改主要是针对的单独一个文档,对应的是接口是 Document APIs,而查对应的搜索接口是 Search APIsQuery DSL文档在 ES 中是最小可索引单位,类比于关系型数据库中的记录。

名词解释:索引

API 约定

ES 文档的原始的地址由三部分组成,索引名称(index)、类型名称(type)和 ID(id),操作方法符合 RESTful 的风格。

相关的接口在 API Conventions。这里只说下重点。

  1. 索引名称的位置可以使用逗号分隔、通配符、时间数学计算、别名等方式来支持同时对多个索引名称进行操作,但是只能在一个索引名称上生效的接口会报错。
  2. 类型名称在未来的版本中会被弃用,不要使用。

文档接口简介

Single document APIs

Multi-document APIs

  • Multi Get API 获取多个文档,需要这些文档的完整路径
  • Bulk API 操作多个文档,需要分别指定对每个文档的操作方式
  • Delete By Query API 删除符合搜索条件的所有文档,涉及到搜索语句
  • Update By Query API 修改符合搜索条件的所有文档,涉及到搜索语句
  • Reindex API 重索引,即将相关文档从一个索引名称中迁移到另外一个索引名称中,常用来做大批量导出。

以上接口中需要注意的是 bulk 和 reindex。

  • bulk
    • bulk 的 body 部分是一行操作一行操作内容间隔的, 拼起来比较麻烦,容易出错,在数据开发中不推荐手写 body,推荐使用各个语言的 Elasticsearch Client,有帮助函数可以使用。
    • 用于客户端对 ES 集群批量操作数据。
    • 以下是 python clinet 的代码片段,用于将一篇文章通过 bulk 接口发到集群中。
1
2
3
4
5
6
7
8
9
10
11
from elasticsearch.helpers import streaming_bulk
actions = [
{
"_op_type": op_type,
"_index": index,
"_type": type,
"_id": id,
"_source": _source
}
]
result = streaming_bulk(es_client, actions, raise_on_error=False)
  • reindex
    • 字面意思是重新索引,实际上就是把一批文档从一个索引名称迁移到另外一个索引名称中。这个接口比较灵活。
    • 默认是迁移索引名称中的全部文档,也可以增加查询语句只选中指定文档进行迁移。
    • 默认是在本机上的索引名称之间进行迁移,也可以在相互信任(需要配置)的 es 集群中迁移。
    • 默认是重新索引原始文档,也可以通过脚本在迁移过程中对文档内容进行修改、添加字段等。
    • 用于集群内部或集群之间的索引名称级别的数据迁移。
    • 以下请求用于将远程的一个索引重索引到本地的同名索引中,并设置了较高的限速,每秒 5000 个文档。另外的命令是用来查询 reindex 任务的执行状态的、取消任务的执行。
1
2
3
POST _reindex?requests_per_second=5000
GET /_tasks?detailed=true&actions=*reindex
POST _tasks/9SsDuTAwQ72Z0VXVUsVt4g:808539/_cancel

查询条件简介

这一部分专门介绍 Query DSL 语句,该语句一般作为 query 的值用在各个接口中,最为常见的就是用在请求体搜索和聚合搜索中。

Query DSL 语句可以分为 Leaf query clauses 和 Compound query clauses,即叶查询子句和复合查询子句。

  • 叶查询子句是用于具体字段的具体查询的,例如 match, termrange。这些查询可以单独使用,不可嵌套。
  • 复合查询子句用于将叶查询子句和其他复合查询子句以一定的逻辑关系组合起来。

利用这两种子句可以组合出非常灵活的查询条件。查询子句的实际查询行为在查询上下文和过滤上下文中并不相同,效率相差很大,在不影响结果的情况下应当尽量将查询条件放在过滤上下文中。具体原理见 查询与过滤

搜索接口

全部的搜索相关接口文档可以参考 Search APIs,这里讲四个常用的接口。

  • URI Search 叫查询字符串搜索,是指将查询需求用 Query String Query 的语法写进一个请求变量 q 里面,仅适用于简单的、内部的查询,对复杂的查询支持不好,且安全性难以保障,不适用于复杂的、开放的查询。
  • Request Body Search 叫请求体搜索,请求体的查询条件部分即 Query DSL ,用于全功能的复杂查询,由于可以将用户的输入赋值到查询的任意位置,因此更加安全。这部分查询条件部分比较重要,在很多接口中缩小文章范围都需要用到。
  • Count API 即匹配文档数查询,查询条件部分与请求体相同,只返回匹配的文档数和各节点的状态。由于不返回具体文档,具体文档相关的参数是不可用的。
  • Explain API 用于解释单篇文档是否符合查询条件,以及匹配分数的详细计算过程。

聚合查询简介

Elasticsearch有一个功能叫做聚合(aggregations),它允许你在数据上生成复杂的分析统计。它很像SQL中的GROUP BY但是功能更强大。接口文档 Aggregations

类似于 DSL 查询表达式, 聚合也有可组合的语法:独立单元的功能可以被混合起来提供你需要的自定义行为。这意味着只需要学习很少的基本概念,就可以得到几乎无尽的组合。

要掌握聚合,你只需要明白两个主要的概念:

  • 桶(Buckets)

    满足特定条件的文档的集合

  • 指标(Metrics)

    对桶内的文档进行统计计算

聚合语句可以和查询语句一起使用,只对符合查询条件的文档进行聚合。script 与 query 类似,也是在多处都会用到的通用组件,本文不做详细介绍,可以参考 Painless Language Specification

推荐学习内容

学习 基础入门 中没有强调补充、高级的相关章节、搜索相关部分以及以上相关接口文档(Getting Started API Conventions Document APIs Search APIs Aggregations Query DSL)。

集群内的原理、分片内部原理

集群原理简介

集群这部分原理主要是通过比较高层次的概念,以节点和分片为最小粒度讲解集群的工作原理。简介的流程将是自上而下的。

集群是由一个或者多个拥有相同 cluster.name 配置的节点组成。

节点是一个运行中的 Elasticsearch 实例。

节点有不同的类型,通过node.master 和 node.data 两个参数配置:

  • 主节点负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。
  • 数据节点保存数据和执行数据相关的操作,如增删改查,搜索,和聚合。
  • 客户端节点 作为集群的一部分,可以响应用户的情况,把相关操作发送到其他节点。
  • 部落节点(Tribe Node)是协调集群与集群之间的节点,这里不做过多介绍。

我们可以将请求发送到 集群中的任何节点 ,包括主节点,该节点负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端,可以简称该节点为协调节点。

索引名称实际上是指向一个或者多个物理分片的逻辑命名空间。

分片有两种:

  • 主分片是必须存在的,所有主分片的数据加起来就是全部数据。创建索引名称后主分片数量不能修改。
  • 副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。

分片是一个分片是一个 Lucene 的实例,即底层的工作单元,它仅保存了全部数据的一部分,但是具有完整的搜索功能。创建索引名称时至少有一个主分片,可以没有副本分片。

  1. 当我们创建索引时,协调节点将创建请求转发到主节点,主节点负责创建主、副本分片并将结果返回。

  2. 分布式文档存储

    当我们增删查改或者使用 mget、bulk 请求时,协调节点仅将请求转发到包含对应文档主分片的节点上,待该节点完成副本分片的更新后返回,然后协调节点将各对应节点的结果收集并返回。此时协调节点与所需节点通信一次。

  3. 执行分布式检索

    当我们搜索、聚合时,协调节点首先需要使用一个优先级队列来管理从所需节点获取的文档元数据(聚合数据)、排序数据,然后再排序,最后再根据真正匹配的文档元数据再与所需节点通信获取返回文档信息。
    如果 size 为 0,此时协调节点仍只与所需节点通信一次,如果 size 不为 0,则协调节点不仅需要 (size + from) * number_of_shards 长度的优先队列重排序,而且需要与所需节点通信第二次取回命中文档的具体字段。
    如果聚合是非全量的、需要排序的,则有可能出现聚合结果不准确的情况。 聚合不准确的例子文档

分片原理简介

分片(shards) 是最小的工作单元,这部分简介分片是如何工作的。我们不妨假设在一个已有一定数据的索引名称中添加一个文档,看看它是怎么工作的。

倒排索引:倒排索引包含一个有序列表,列表包含所有文档出现过的不重复个体,或称为 词项 ,对于每一个词项,包含了它所有曾出现过文档的列表。每个被索引的字段都有自己的倒排索引。倒排索引是不可变的,这带来了很多速度上的优势,为了这些优势,es 宁可定期重建索引。

段(segment): 每一段本身都是一个倒排索引段,也是不可修改的。一个分片包含一个提交点(一个列出了所有已知段的文件)和多个段。搜索分片就是对所有段的结果进行聚合,修改和删除只是在提交点做一个标记,需要到段合并的时候才真正进行。

事务日志(translog):每一次对 Elasticsearch 进行操作时均进行了日志记录。

  1. 文档被写入内存缓冲区和事务日志
  2. 如果设置了每次写请求完成之后执行 tranlog 落盘,在整个请求被 fsync 到主分片和复制分片的translog之前,你的客户端不会得到一个 200 OK 响应。
  3. 如果到了刷新(refresh)时间,内存缓冲区会被清空,事务日志保留,内存缓冲区的文档被写到一个临时段中可以被搜索到。
  4. 如果到了刷新(flush)时间,内存缓冲区和事务日志都会被清空,一个最新的段通过 fsync 写入硬盘。
  5. 如果 flush 时存在多个较小的段,可能会触发段合并,多个较小的段被合并到一个大段中,同时进行实质的文档删除。大的段搜索性能更好。

从 refresh、flush 到段合并,消耗的系统资源逐渐增大,目前我们的数据实时搜索的要求不那么高,因此目前采取的 30s 的 refresh 间隔和 30min 的 flush 间隔,并且不对段合并采取强制的限制。

索引原理简介

默认情况下:一个文档入索引名称时,需要根据映射中的配置决定:

    1. 根据路由决定进入哪个分片

shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

修改 routing 会影响分片的选择。

    1. 根据每个域的配置,决定是否创建倒排索引、是否单独保存域值、是否创建用于聚合排序的字段

是否创建倒排索引,默认开启:object 类型使用 enabled 控制,nested 无,其他类型使用 index 。

是否单独保存域值,默认关闭:object 和 nested 无,其他类型使用 store 控制。

是否创建用于聚合排序的字段,默认除 text 外均开启:object 和 nested 无,text 使用 fielddata,其他类型使用 doc_value 控制。

doc_value 是在索引文档时保存到硬盘。fielddata 是在查询时在内存中生成,会消耗大量资源。一般保留默认值即可。

创建倒排索引的分析器可以配置,自由度很高。

    1. 根据映射中元域(meta-field)的配置决定是否保存原始文档、是否开启 _all 字段。

_source 字段默认为 true,保存原始文档。

_all 字段默认为 true,用于非特定字段的全文搜索。

推荐学习内容

学习 基础入门 补充、高级章节。

集群管理、监控和部署

管理、监控和部署

本书大部分介绍了使用 Elasticsearch 作为后端创建应用程序。本章节稍微不同。在这里,你将学习到如何管理 Elasticsearch 自身。Elasticsearch 是一个复杂的软件,有许多可移动组件,大量的 API 设计用来帮助管理你的 Elasticsearch 部署。

在这个章节,我们涵盖三个主题:

  • 根据监控你的集群重要数据的统计,去了解哪些行为是正常的,哪些应该引起警告,并解释 Elasticsearch 提供的各种统计信息。
  • 部署你的集群到生产环境,包括最佳实践和应该(或不应该!)修改的重要配置。
  • 部署后的维护,如 Rolling Restart 或备份你的集群

监控

监控 文档。权威指南中提到的 marvel 监控现在已经作为 x-pack 的一部分,集成到了 kibana 中,即 Monitoring 部分。Monitoring 的界面简单易懂,易于使用。

当 Monitoring 卡死,很久打不开时,说明集群负载极高,或者已经挂掉了,这个时候需要使用一些简单的监控接口,直接获取集群的状态。

集群的状态有三种:

  • green
    所有的主分片和副本分片都已分配。你的集群是 100% 可用的。
  • yellow
    所有的主分片已经分片了,但至少还有一个副本是缺失的。不会有数据丢失,所以搜索结果依然是完整的。不过,你的高可用性在某种程度上被弱化。如果 更多的 分片消失,你就会丢数据了。把 yellow 想象成一个需要及时调查的警告。
  • red
    至少一个主分片(以及它的全部副本)都在缺失中。这意味着你在缺少数据:搜索只能返回部分数据,而分配到这个分片上的写入请求会返回一个异常。

全部的相关接口在 Cluster APIscat APIs ,这些接口大都不常用。

部署

部署 文档。

硬件:内存至少 8G,最多 64G,CPU 倾向于多核,硬盘易成为瓶颈,最好使用 SSD。总得来讲,中配或者高配机器更好。

系统环境:使用推荐范围内最新的 JVM,文件描述符 数量足够。

配置文件:当服务器较多时务必使用配置管理工具( Puppet,Chef,Ansible),保证配置文件的正确。

当前使用的插件:x-pack(监控,收费) 和 ik(分词)

当前的部署信息:
应用安装路径:/home/es
log4j2.properties、x-pack 的配置使用的均为默认值,未作修改。

部署后

可以通过 Cluster Update Settings 在系统运行时动态地修改配置,使其临时或永久生效。尤其是在大量操作数据时,常常需要临时修改一些配置。不过动态修改的一般是某个或某几个索引名称的配置,因此需要使用 Get Settings 相关接口,不需要重启集群。

  • 大批量导入时可以把 index.refresh_interval 设置为 -1 关闭刷新,数据导完再设置为 30s(注意单位,一定要有 s)

  • 大批量导入时理论上也可以把 index.number_of_replicas 临时设置为 0,我们该参数一般也设为 0,以占用更小的硬盘为代价放弃了数据拷贝。

运维实战

按照分布式系统的惯例,在同一时刻只能有不超过一半机器安全下线,否则就需要人工进行故障恢复。

查看线程池状态

ES 的 bulk、搜索等操作会放到缓冲队列中,由线程池缓慢消费,有时需要诊断 ES 的写入和查询是否超过了其所能承受的极限,可以通过查看 bulk 队列来观察写入和查询是否持续积压中。

1
GET _cat/thread_pool

bulk 队列常用于批量写,当 queue 积压得很高,说明写入压力比较大。

同理,search 队列积压得很高,说明读压力比较大,搜索延迟会很高。

恢复失联分片

因为异常原因,某些分片挂了

查看分片

1
GET _cat/shards

尝试由集群自动重路由分配失败的分片

1
POST _cluster/reroute?retry_failed

重分配多次后,仍然不可用,应手动迁移分片

1
2
3
POST _cluster/reroute
{
}

查看手动迁移时所需的 node id

1
GET _nodes/process

数据冷热迁移

每当硬盘不足时,我们会登录机器,按照磁盘情况将比较古老的冷数据归档到冷节点上。

第一步 备份数据

参考集群的备份和恢复

第二步 给 ES 实例打标签

登录机器修改 ES 配置文件,node.attr.<tag_name>: "<tag_value>"

比如我们 ES master 机器的配置是 node.attr.box_type: "hot"

第三步 将任意一个索引根据 box_type 迁移到指定节点

1
2
PUT /<index_name>/_settings
{"index.routing.allocation.include.<tag_name>" : "<tag_value>"}

修改配置后,ES 集群会自动生成再平衡任务,将索引数据按照标签传输到对应的节点上。

第四步 查看当前传输任务进度

1
GET _cat/recovery active_only==true

集群扩容

添加一个新的机器节点,或者在现有节点上添加一个新的 ES 实例。

扩容前要确认的:

  1. 新机器配置项是否和原有机器相同,尤其确认 XPack、队列长度和 Discovery 配置项(discovery.zen.ping)
  2. 线上是否还有压力比较大的写入 ES 的进程,暂停它们
  3. 阅读故障恢复手册中的分片恢复部分
  4. 确认 ES 配置,限制内网流量(重要)

扩容后要确认的:

  1. 观察集群内网流量,防止流量太大导致磁盘写满、机器故障或脑裂(偶数台节点)
  2. 如果脑裂,应迅速取消再平衡,调低内网流量配置,等待集群恢复的同时手动重分配分片
  3. 如果是 master 节点,负载均衡加入新机器,按照新节点配置分配权重
  4. 第二天早上看日志,看高负载情况下有没有发生脑裂和宕机

滚动重启

滚动重启(用于存在复制分片的索引)

关闭再平衡功能,防止重启时再平衡导致异常流量

1
2
3
4
5
6
PUT /_cluster/settings
{
"transient" : {
"cluster.routing.allocation.enable" : "none"
}
}

关闭、升级、开启服务

开启再平衡功能

1
2
3
4
5
6
PUT /_cluster/settings
{
"transient" : {
"cluster.routing.allocation.enable" : "all"
}
}

数据强制迁移

将数据从某个节点的某块硬盘迁移到另一块硬盘或另一个节点

手动迁移分片

参见 恢复失联分片

通过机器属性标签平衡数据

参见 数据冷热迁移

查看迁移进度

1
GET _cat/recovery active_only==true

配置调优

以下是一次与知乎工程师交流 ES 使用经验的记录。

  1. 提前创建好 index, 不要通过 Logstash 做。因为会在同一时刻去做这个事,会造成 Logstash 一段时间无法写入,而且 ES 会压力很大,同时收到太多创建 Index 请求。最好放在晚上压力不高的时候去创建。
  2. Mapping 里如果提前知道字段,就不要用自动创建的 dynamic_string。
  3. 查询时用 bool query
  4. Sharding 要平衡,否则可能导致一个节点上 Load 特别高
  5. LogStash 慎用,性能有问题,如果数据量大的话, Logstash 是需要很多才能追上数据,还要去调优参数。并且会经常线程跑到 100% 死掉
  6. 使用 MQ,削峰填谷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"persistent": {},
"transient": {
"cluster": {
"routing": {
"allocation": {
"balance": {
"index": "0.998",
"threshold": "1.0",
"shard": "0.002"
},
"disk": {
"watermark": {
"low": "75%",
"high": "85%"
}
},
"enable": "all"
}
}
}
}
}

ES master 节点 分配 shards 线程 CPU 100%: 这个是由于之前设置了一些 disk 相关的限制,导致每次分配都会遇到很长时间的检查过程,导致创建 indices 速度非常慢。解决方法:把 ES 中的 disk 相关的限制去掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"transient": {
"cluster": {
"routing": {
"allocation": {
"enable": "all",
"disk": {
"threshold_enabled": false
}
}
}
}
}
}

ES 查询分配数量的坑: “action.search.shard_count.limit” : “15000” , 提高这个值对性能有一定消耗,但是会使得尽量多个 topics 可以同时查询。
扩容 ES 节点的时候, 由于新加入的节点没有任何 indices 和 shards, 会导致 第二天的 indices 和 shards 的尽可能的分布在新加的节点上。所以加入新 ES 节点的时候, 最好有一批机器同时加入,这样能保证第二天的稳定。由于目前在 ES 中的数据都是 Rolling 的, 所以这个过程最终一定会稳定。

由于一些 ES 节点 压力很大,会频繁的 GC, 过程中可能导致 ES master 节点认为 节点离开集群,所以目前 ES 设置了 cluster.routing.rebalance.enable: “none”, 来避免无用的 rebalance。

推荐学习内容

管理、监控和部署 的全部内容以及上述设计的接口文档。

数据建模

主要分为两部分,一是单个 index 的映射怎么写,另外一个是如何处理文档间的关联关系。 相关的文档大概有:

Mapping

数据建模

映射类型

字段类型:表示应该将该字段看成哪种数据格式进行索引,不同的数据格式索引的方式不同。elasticsearch 是基于 JSON 的,对应的字段类型是 Object datatype 。文档最外层的参数与 object 意义和用法是相同的。

有一个很重要的概念要动态映射 Dynamic Mapping ,是 elasticsearch 新建索引的默认行为,根据第一次传入的字段数值通过一个进行类型推断,然后进行索引

  • 数组类型 Array datatype:es 并没有真正的数组类型,任意真实类型的字段都可以索引多个值,对象、嵌套类型比较特殊,单独讲。
  • 数字类型 Numeric datatypes:long、integer、short 等不同范围的整数类型,以及 double、float 等不同范围的浮点数类型,在聚合中的行为是基本一致的。
  • 字符串类型 String datatype:在较新的版本中有 keyword 和 text 两种字符串类型,索引方式不同,keyword 仅支持精确匹配,text 仅支持全文检索。
  • 日期类型 Date datatype :输入时为字符串,通过解析器(可自定义)索引成数字,支持范围查询,按天、按小时查询比较方便,需要注意时区问题。
  • 对象类型 Object datatype :即索引一个 JSON 文档,可以取子字段进行查询。当该数据类型索引多值时,同一个文档内部字段的关联被忽略了。不适用于需要搜索的文档。
  • 嵌套类型 Nested datatype :与对象类型相比,保留了同一个文档内部字段的关联,适用于需要搜索的文档,搜索语法略有不同。

##索引下的映射操作

Put Mapping

Get Mapping

已创建的字段映射类型不可修改,只能增加新字段、修改已有的部分参数,使用的接口就是上面两个,一个获取,一个更新。使用一下接口时必须注意请求体中 JSON 的嵌套层级,层级错了是肯定会报错的。

关联关系

关于数据建模的东西我实践的也不多,es 这方面也不是强项,可以用别的工具来处理。

其他高级特性

深入搜索

处理人类语言

聚合

地理位置

Analysis

……

附相关信息来源

官方网站中文首页:https://www.elastic.co/cn/

官方英文文档首页:https://www.elastic.co/guide/index.html

官方英文社区:https://discuss.elastic.co/

官方中文社区:https://elasticsearch.cn/

官方中文社区 github:https://github.com/elasticsearch-cn

官方中文社区微信公众号(WeChat ID: elastic-cn):https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzI2NDExNTk5Mg==&scene=124&#wechat_redirect

Elastic 情报局(聚合了所有相关资源的搜索工具):https://index.elasticsearch.cn/

坚持原创技术分享,您的支持将鼓励我继续创作!