Elasticsearch 从cluster 到field

avatar
rpc工程师 @小厂

一、是什么

Elasticsearch(ES) 是近实时、高性能、高弹性的分布式搜索和分析引擎,存储格式基于json,由apache lucene提供单机的搜索和存储。

二、基础语法

2.1 新建索引

PUT juejin_hr_data_v1

{

  "settings": {

    "index": {

      "routing": {

        "allocation": {

          "enable": "all"

        }

      },

      "refresh_interval": "60s",

      "number_of_shards": "3"

    }

  },

  "mappings": {

    "dynamic": false,

    "properties": {

      "week": {

        "type": "keyword"

      },

      "team": {

        "type": "keyword"

      },

      "school":{

        "type":"integer"

      },

      "nowcoder":{

        "type":"integer"

      },

      "boss":{

        "type":"integer"

      },

      "maimai":{

        "type":"integer"

      }

    }

  }

}

2.2 基本增删改查

  1. 增加
POST juejin_hr_data_v1/_bulk // bulk api,批量写入

{"index":{"_id":"client_1"}}

{"week":1,"team":"客户端","school":0,"nowcoder":0,"boss":0,"maimai":0}
  1. 删除
POST juejin_hr_data_v1/_delete_by_query

{

  "query": {

    "match_all": {}

  }

}
  1. 更新
覆盖写

PUT juejin_hr_data_v1/_doc/1 //没有指定的index会创建

{

  "week":1,"team":"客户端","school":0,"nowcoder":0,"boss":0,"maimai":0

}

PUT juejin_hr_data_v1/_create/1 //没有指定的index会报错

{

  "week":1,"team":"客户端","school":0,"nowcoder":0,"boss":0,"maimai":0

}



更新部分字段

POST juejin_hr_data_v1/1/_update

{

  "doc":{

    "week":2

  }

}
  1. 查询
GET juejin_hr_data_v1/_search

{

  "query": {

    "match_all": {}

  }

}

三、整体架构

image.png

3.1 集群Cluster

  • Elasticsearch 集群由一个或多个节点(Node)组成,每个集群都有一个共同的集群名称作为标识。
  • Elasticsearch 实例即一个 Node,一台机器可以有多个实例,正常使用下每个实例应该会部署在不同的机器上。

3.1.1 集群属性

一个Elasticsearch集群的健康状态由Green、Yellow、Red三个枚举值来确认

  • Green:任取一个主分片/复制分片均可用
  • Yellow:存在复制分片不可用,任取一个主分片均可用
  • Red:存在主分片不可用

当集群状态为 red,它仍然正常提供服务,它会在现有存活分片中执行请求,需要尽快修复故障分片,防止查询数据的丢失;

es不像kafka和hbase依赖zookeeper,es有一套自己的集群维护机制

3.1.2 服务发现

es实现了自己的服务发现机制,称之为ZenDiscovery,服务发现是从单机节点形成集群的过程。当启动一个新的es节点或者主节点挂掉后,都会去触发服务发现的过程。

服务发现的起点是从多个host provider(settings、file、cloud等)提供的多个host(hostname+dns 或 ip)以及已知的具有选主资格的node节点开始。这个过程分成2个阶段:

阶段1. 每个节点尝试去连接各个种子节点(seed addresses)并确认连接的节点是符合要求的节点(有选主资格,master-eligible)

阶段2. 如果阶段1成功,则当前节点会与它这些连接的节点去共享当前节点已知的全部有资格的节点信息,同样远端节点也会一一去响应他已知的全部有选主资格的节点信息。这样当前节点就发现了一批新的节点,然后继续循环请求这些节点,直到整个集群构成了一张连通图,服务发现的过程就完成了。

如果当前节点不是有选主资格的节点,它会持续服务发现的过程,直到发现了被选举出的主节点,如果在本次服务发现的过程中没有发现主节点,节点会在discovery.find_peers_interval时间间隔后重试(默认是1秒)。

如果当前节点是有选主资格的节点,它会持续服务发现的过程直到它发现了一个被选举出的节点(elected master node)或者它发现了足够数量的不是主节点但有选主资格的节点去完成一次选举。如果这两个条件都不能满足,会在discovery.find_peers_interval后进行重试。

3.1.2 选主

ES是一种p2p(peer to peer)的分布式架构设计,集群中的每个节点都可以与其他任意节点进行通讯。这是不同于hadoop的master-slave的分布式系统。

ES中也存在master角色,但是其功能主要是维护集群的元信息(cluster status),当任意node上的信息修改时,将变更信息同步到其他剩余node上。也就是说每个node都具有一套完整的cluster status。

在es 7.0之后,es基于raft算法做了一些调整,并将其作为选主的实现。raft是工程上使用比较广泛的分布式的共识协议,即使在部分节点故障、网络延时、脑裂的情况下,依然可以多个节点对某个事情达成一致的看法。本文不做过多介绍,raft可以参考:深度解析 Raft 分布式一致性协议

3.2 节点 node

3.2.1 节点属性

  • Elasticsearch 的配置文件中可以通过 node.master、node.data 来设置节点类型,ES有多种节点类型,按照两个配置项,可枚举成四种,分别如下。

    • 主节点+数据节点(默认)
node.master: true 

node.data: true

节点即有成为主节点的资格,又存储数据。Elasticsearch 默认每个节点都是这样的配置

  • 主节点
node.master: true 

node.data: false

不会存储数据,有成为主节点的资格,可以参与选举,有可能成为真正的主节点。普通服务器即可(CPU、内存消耗一般)。

  • 数据节点
node.master: false 

node.data: true

节点没有成为主节点的资格,不参与选举,只会存储数据。在集群中需要单独设置几个这样的节点负责存储数据,后期提供存储和查询服务。主要消耗磁盘,内存。

  • 客户端节点
node.master: false 

node.data: false

不会成为主节点,也不会存储数据,主要是针对请求进行分发。

3.3 分片 shard

  • 分片是 Elasticsearch 在集群中分发数据的关键。把分片想象成数据的容器。文档存储在分片中,然后分片分配到集群中的节点上。当集群扩容或缩小,Elasticsearch 将会在节点间迁移分片,以使集群保持平衡。
  • 分片可以是 分片 (primary shard) 或者是复制 分片 (replica shard)

分片作为存储数据的单元,只存在于数据节点

复制分片只是主分片的一个副本,它可以防止硬件故障导致的数据丢失,同时可以提供读请求

3.2.1 分片备份

不支持在 Docs 外粘贴 block

在一个索引下,主分片会尽可能均匀的分布到每个节点中,而复制分片则不会分布到和主分片相同的实例。

Node2节点下线后,集群在短时间内会对分片进行重新分布,当然依赖遵循主、复制分片不会在同一个Node;如果Node1继续下线,所有主分片会集中在Node0,此时集群健康值:未连接。因为当前可用的主节点数 1 < discovery.zen.minimum_master_nodes 的默认值 2。

若把 discovery.zen.minimum_master_nodes 设置成 1,然后只启动一个节点,此时集群健康值:yellow 。这种情况下代表主分片全部可用,存在不可用的复制分片,5个复制分片没有分配到节点上,不过此时的集群是可用的,只是所有的操作都落到主分片上,而且可能引发单点故障。

3.2.2 写索引过程

  • ES 集群中每个节点通过路由都知道集群中的文档的存放位置,所以每个节点都有处理读写请求的能力。
  • 在一个写请求被发送到某个节点后,该节点即为协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。假设 shard = hash(_routing) % num_of_pshard ,则过程大致如下:

  1. 客户端向 ES1节点发送写请求,通过路由计算公式得到值为0,则当前数据应被写到主分片 S0 上。
  2. ES1 节点将请求转发到 S0 主分片所在的节点 ES3,ES3 接受请求并写入。
  3. 并发将数据复制到两个副本分片 R0 上,其中通过乐观并发控制数据的冲突。一旦所有的副本分片都报告成功,则节点 ES3 将向协调节点报告成功,协调节点向客户端报告成功。

默认情况下会把doc的_id作为_routing的值,也可以手动指定routing的字符串,例如对于文章的场景,可以指定文章的标签作为routing的值,会将相同标签的文章写入同一个/组shard,在查询指定标签下的文章时,直接指定routing,会减少很大的查询量。(默认情况下ES查询会查询每一个shard再做合并)

3.3 分段 segment

3.3.1 索引不可变

  • 写入磁盘的倒排索引是不可变的,优势主要表现在:

    • 不需要锁。因为索引不可变,所以就不用担心多个请求使用时产生不一致的问题
  • 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性,只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
  • 其它缓存在索引的生命周期内始终有效,不需要在每次数据改变时被重建,因为数据不会变化。
  • 写入单个大的倒排索引,可以压缩数据,较少磁盘 IO 和需要缓存索引的内存大小。

  • 同样,因为索引可不变,所以在对旧数据编辑时会存在一些问题:

    • 当对旧数据进行删除时,旧数据不会马上被删除,而是在 .del文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。
    • 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
    • 每次新增数据时都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会非常大。
    • 在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,这增加了查询的负担。

所以,既要保证索引不变时的效率,又要尽可能避免因此产生的问题,那么就引入的段(Segment)

3.3.2 分段

  • 索引文件被拆分为多个子文件,每个子文件叫作, 每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。

3.3.2 段的更新

  • 新增,段的新增很好处理,由于数据是新的,所以只需要对当前文档新增一个段即可。
  • 删除,由于不可修改,所以对于删除操作,不会把文档从旧的段中移除,而是通过新增一个 .del文件,包含了段上已经被删除的文档。当一个文档被删除,它实际上只是在.del文件中被标记为删除,依然可以匹配查询,但是最终返回之前会被从结果中删除
  • 更新,不能修改旧的段来进行反映对文档的更新,其实更新相当于是删除和新增这两个动作组成。会将旧的文档在 .del文件中标记删除,然后文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。

3.3.3 段的合并

  • 因为对分段新增、删除及更新的特殊处理,会产生过多的段。而过多的段除了耗费文件句柄,内存,cpu资源之外,还会因为搜索时检索的段过多而拖慢查询时间

  • 所以当段的数量过多时,会将各个小段合并为一个大的段。段合并的主要操作如下:

    • 新的段flush到了硬盘。
    • 新的提交点写入新的段,排除旧的段。
    • 新的段打开供搜索。
    • 旧的段被删除。

3.3.4 reflesh

ES 是怎么做到近实时全文搜索?

  • 磁盘是瓶颈。提交一个新的段到磁盘的操作开销较大,严重影响性能,当写数据量大的时候会造成 ES 停顿卡死,查询也无法做到快速响应。
  • 所以持久化过程不能在每个文档被索引的时就触发,需要一种更轻量级的方式使新的文档可以被搜索。为了提升写的性能,ES 没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写的策略。
  • 每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存,当达到默认的时间(1秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统上,稍后再被刷新到磁盘中并生成提交点
  • 新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。由内存刷新到文件缓存系统的时候会生成了新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。
  • 在 Elasticsearch 中,这种写入和打开一个新段的轻量的过程就是reflesh,默认情况下每个分片会每秒自动刷新一次。

3.3.5 flush

虽然通过定时 Refresh 获得近实时的搜索,但是 Refresh 只是将数据挪到文件缓存系统,没有对数据进行持久化。为了避免丢失数据,Elasticsearch添加了Translog,事务日志记录了所有还没有持久化到磁盘的数据。整个flush过程如下:

  1. 当一个文档被索引,它被加入到内存缓存,同时加到事务日志。不断有新的文档被写入到内存,同时也都会记录到事务日志中。这时新数据还不能被检索和查询。

  1. 当达到默认的刷新时间或内存中的数据达到一定量后,会触发一次 refresh:
  • 内存缓冲区的文档写入到段中,但没有fsync。
  • 段被打开,使得新的文档可以搜索。
  • 缓存被清除

  1. 随着更多的文档加入到缓存区,写入日志,这个过程会继续

  1. 随着新文档索引不断被写入,当日志数据大小超过 512M 或时间超过 30 分钟时,会进行一次全提交
  • 内存缓存区的所有文档会写入到新段中
  • 清除缓存
  • 一个提交点写入硬盘
  • 文件系统缓存通过fsync操作flush到硬盘
  • 事务日志被清除

3.4 文档 doc

3.4.1 schema

schema,对应到es,其实就是mapping,es数据的交互形式是json,doc可以做到开箱即用,在写入doc时如果没有预先定义的mapping,doc的每一个field会根据传过来的json数据确定类型,默认规则(dynamic field mapping)如下:

json类型es类型
null不会增加field
booleanboolean
stringdate(通过Date detection)double/long(通过Numeric detection)text(带keyword的sub field)
numberfloat/long
objectObject
arrayarray(array的item类型取决于第一个非null元素的类型)

同时es还支持定义模板 dynamic_template,来对默认的规则进行扩充、修改,例如下例就是修改了默认的string映射,意思是在匹配到string类型后,使用的es类型为text:

{

  "mappings": {

    "dynamic_templates": [

      {

        "strings_as_keywords": {

          "match_mapping_type": "string",

          "mapping": {

            "type": "text"

          }

        }

      }

    ]

}

不过如果没有动态字段的需求,个人不建议使用es的dynamic mapping,使用不当的话会污染mapping,所以可以指定dynamic为false来关闭动态mapping。

当然可以使用put mapping api来预定义index的mapping结构,包括字段类型、使用的分析器(text类型)、是否索引等等。

es官方也非常推荐将相同的字段以不同的方式索引到es中,例如一个字符串类型的值可以使用索引成text类型来进行全文检索,也可以索引成keyword类型进行排序、聚合。

建议使用别名(alias),es对mapping的拓展是开放的,但对mapping的修改是禁止的。例如,可以为mapping增加一个字段,但是不能删除/修改字段。所以使用alias指向真正的index,这样,在有field需要修改的场景可以使用reindex api重建索引、再使用alias api更改指向,可以实现无缝的切换。

3.4.2 metadata

es中每个doc都会有一些关联的元数据,如下:

  1. _index,当前doc所属的index
  2. _type,当前doc的mapping type
  3. _id,doc的唯一标识,index内唯一
  4. _source,doc的json原始数据
  5. _size,doc的长度
  6. _rounting,上文介绍过的自定义路由的值

3.5 字段 field

field是ES中最小的数据单位。默认情况下,es会索引每个字段中的所有数据,每个字段类型都会有专有的经过优化过后的数据类型,比如,字符串类型(例如text和keyword)倒排索引进行存储,数据类型(例如interger、float等)则会使用BKD tree进行索引存储。对不同的字段类型使用不同的索引方式,这也是ES为什么这么快的原因之一。

3.4.1 字段类型

es支持如下字段类型

stringtext使用配置的分析(分词)器对原始串做加工后写入倒排
keyword直接将字段作为词根存入倒排
numberlong取值范围: -2^63~2^63-1
long取值范围:-2^31~2^31-1
short取值范围:-32,768~32,767
byte取值范围:-128~127
double双精度浮点数
float单精度浮点数
half_float16位长浮点数
datestringstring格式的时间
numbernumber格式的时间,一般是unix时间戳(毫秒/秒)
booleanbooleantrue / false
binarybinary二进制数据
objectarray数组
range范围
objectjson对象
nested有关联关系的json对象,默认的object对象的各个字段都是打平的
geo地图坐标
ipipv4或ipv6
completion自动补全类型,底层使用字典树索引
.......

3.4.2 倒排索引

倒排索引(英语:Inverted index),也常被称为反向索引置入文件反向文件,是一种索引方法,被用来存储全文搜索下某个单词在一个文档或者一组文档中的存储位置映射。它是文档检索系统中最常用的数据结构。 ----摘自维基百科

而ES最基础的索引结构就是倒排,举个例子来介绍下倒排,有三个文档,分别是:

doc 0:it is what it is

doc 1:what is it

doc 2:it is a banana

与之对应的倒排如下:

 "a":      {2}

 "banana": {2}

 "is":     {0, 1, 2}

 "it":     {0, 1, 2}

 "what":   {0, 1}

对应到es中,倒排的key就是文本串,倒排的value就是doc id list。

3.4.3 FST

FST(Finite State Transducer),有限状态自动机,类似trie树。

es使用FST数据结构来存储倒排的term字典,参考下图。

3.4.4 跳表 skiplist

es使用skiplist来来存储倒排value的doc id列表,方便对docid做检索。

在and条件中,一次query会涉及对多条倒排链的合并,基本合并规则如下,假设有3条倒排链

  1. 在termA开始遍历,得到第一个元素docId=$docid

  2. Set currentDocId=$docid

  3. 循环 advance(currentDocId) = 1 (返回大于等于currentDocId的一个doc),

    1. 因为currentDocId ==1,继续
    2. 如果currentDocId 和返回的不相等,执行2,然后继续
    3. 如果相等的次数等于倒排链-1,则将docid加入结果集,取当前倒排链的next作为新的docid加入结果集,取当前倒排链的next作为新的docid
  4. 直到某个倒排链到末尾。

3.4.5 BKD tree索引

Bkd树是一种动态索引数据结构,能高效且可伸缩地索引大的多维点数据集。它有 (1) 极高的空间利用率和 (2) 优秀的查询、(3) 更新性能——且这三种属性在高强度更新下依旧成立。

四、总结

es的内容还是非常多的,本文是从物理/逻辑上的粒度进行了拆分,从cluster依次到field进行了讲解。

参考文档:

www.cnblogs.com/caoweixiong…

www.cnblogs.com/duanxz/p/52…

www.elastic.co/guide/en/el…

www.cppblog.com/mysileng/ar…