ES之概述
elasticsearch 设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。es 的分布式架构原理核心思想就是在多台机器上启动多个 es 进程实例,组成了一个 es 集群。
ES结构
es 中存储数据的基本单位是索引,比如说你现在要在 es 中存储一些订单数据,你就应该在 es 中创建一个索引,order_idx,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是 mysql 里的一张表。index -> type -> mapping -> document -> field。
index
ES 中的 index 类似于 mysql 中的表。
type
ES 中的 type 没法跟 mysql 里去对比,一个 index 里可以有多个 type,每个 type 的字段都是差不多的,但是有一些略微的差别。
比如,有一个 index,是订单 index,里面专门是放订单数据的。就好比说你在 mysql 中 建表,有些订单是实物商品的订单,就好比说一件衣服,一双鞋子;有些订单是虚拟商品的订单,就好比说游戏点卡,话费充值。就两种订单大部分字段是一样的,但是少部分字段可能有略微的一些差别。所以就会在订单 index 里,建两个 type,一个是实物商品订单 type,一个是虚拟商品订单 type,这两个 type 大部分字段是一样的,少部分字段是不一样的。
很多情况下,一个 index 里可能就一个 type,但是确实如果说是一个 index 里有多个 type 的情况,你可以认为 index 是一个类别的表,具体的每个 type 代表了具体的一个 mysql 中的表。
mapping
ES 中的每个 type 有一个 mapping,如果你认为一个 type 是一个具体的一个表,index 代表了多个 type 的同属于的一个类型,mapping 就是这个 type 的表结构定义,你在 mysql 中创建一个表,肯定是要定义表结构的,里面有哪些字段,每个字段是什么类型。
mapping 就代表了这个 type 的表结构的定义,定义了这个 type 中每个字段名称,字段是什么类型的,然后还有这个字段的各种配置。
document
ES 中 index 里的一个 type 里面的一条数据,叫做一条 document,一条 document 就代表了 mysql 中某个表里的一行。
field
ES 中每个 document 有多个 field,每个 field 就代表了这个 document 中的一个字段的值。
ES shard
ES 索引可以拆分成多个 shard,每个 shard 存储部分数据。 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 primary shard,
负责写入数据,但是还有几个 replica shard。primary shard 写入数据之后,会将数据同步到其他几个 replica shard 上去。
通过这个 replica 的方案,每个 shard 的数据都有多个备份,如果某个机器宕机了,还有别的数据副本在别的机器上呢以实现高可用。
ES 集群多个节点,会自动选举一个节点为 master 节点,这个 master 节点其实就是干一些管理的工作的,比如维护索引元数据拉,负责切换 primary shard 和 replica shard 身份之类的。要是 master 节点宕机了,那么会重新选举一个节点为 master 节点。如果是非 master 节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。急着你要是修复了那个宕机机器,重启了之后,
master 节点会控制将缺失的 replica shard 分配过去,同步后续修改的数据之类的,让集群恢复正常。
ES数据写入过程
ES 写入概述
- 客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node(协调节点)。
- coordinating node ,对 document 进行 路由 ,将 请求 转发 给对 应的 node (有 primary shard)。
- 实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node。
- coordinating node,如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端。
ES 写入详细过程
- 先写入 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件
- 如果 buffer 快满了,或者到一定时间,就会将 buffer 数据 refresh 到一个新的 segment file 中,但是此时数据不是直接进入 segment file 的磁盘文件的,而是先进入 os cache 的。这个过程就是 refresh。
- 每隔 1 秒钟,es 将 buffer 中的数据写入一个新的 segment file,每秒钟会产生一个新的磁盘文件,segment file,这个 segment file 中就存储最近 1 秒内 buffer 中写入的数据,但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,每秒创建换一个空的 segment file,如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。
- 操作系统里面,磁盘文件其实都有一个东西,叫做 os cache,操作系统缓存,就是说数据写入磁盘文件之前,会先进入 os cache,先进入操作系统级别的一个内存缓存中去,只要 buffer 中的数据被 refresh 操作,刷入 os cache 中,就代表这个数据就可以被搜索到了
- 为什么叫 es 是准实时的?NRT,near real-time,准实时。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。
可以通过 es 的 restful api 或者 java api,手动执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 os cache 中,让数据立马就可以被搜索到。只要数据被输入 os cache 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。
- 只要数据进入 os cache,此时就可以让这个 segment file 的数据对外提供搜索了。
- 重复 1~3 步骤,新的数据不断进入 buffer 和 translog,不断将 buffer 数据写入一个又一个新的 segment file 中去,每次 refresh 完 buffer 清空,translog 保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 commit 操作。buffer 中的数据,倒是好,每隔 1 秒就被刷到 os cache 中去,然后这个 buffer 就被清空了。所以说这个 buffer 的数据始终是可以保持住不会填满 es 进程的内存的。每次一条数据写入 buffer ,同时会写入一条日志到 translog 日志文件中去,所以这个 translog 日志文件是不断变大的,当 translog 日志文件大到一定程度的时候,就会执行 commit 操作。
- commit 操作发生第一步,就是将 buffer 中现有数据 refresh 到 os cache 中去,清空 buffer。
- 将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file。
- 强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。
- translog 日志文件的作用是什么?就是在你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器宕机,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件,translog 日志文件中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和
os cache 中去。 - commit 操作:1、写 commit point;2、将 os cache 数据 fsync 强刷到磁盘上去;3、清空 translog 日志文件。
- 将现有的 translog 清空,然后再次重启启用一个 translog,此时 commit 操作完成。默认每隔 30 分钟会自动执行一次 commit,但是如果 translog 过大,也会触发 commit。整个 commit 的过程,叫做 flush 操作。我们可以手动执行 flush 操作,就是将所有 os cache 数据刷到磁盘文件中去。这个过程不叫做 commit 操作,flush 操作。es 中的 flush 操作,就对应着 commit 的全过程。我们也可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去,记录一个 commit point,清空 translog 日志文件。
- translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会丢失 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。
- 其实 es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的,你的数据有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,有 5 秒的数据不在磁盘上,此时如果宕机,会导致 5 秒的数据丢失。如果你希望一定不能丢失数据的话,你可以设置参数,每次写入一条数据,都是写入 buffer,同时写入磁盘上的 translog,但是这会导致写性能、写入吞吐量会下降一个数量级。本来一秒钟可以写 2000 条,现在你一秒钟只能写 200 条,都有可能。
- 如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据.del 文件就知道这个 doc 被删除了。
- 如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。
- buffer 每次 refresh 一次,就会产生一个 segment file,所以默认情况下是 1 秒钟一个 segment file,segment file 会越来越多,此时会定期执行 merge。
- 每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,这里会写一个 commit point,标识所有新的 segment file,然后打开 segment file 供搜索使用,同时删除旧的 segment file。es 里的写流程,有 4 个底层的核心概念,refresh、flush、translog、merge 当 segment file 多到一定程度的时候,es 就会自动触发 merge 操作,将多个 segment file 给 merge 成一个 segment file。
ES 读数据过程
查询,GET 某一条数据,写入了某个 document,这个 document 会自动给你分配一个全局唯一的 id,doc id,同时也是根据 doc id 进行 hash 路由到对应的 primary shard 上面去。也可以手动指定 doc id,比如用订单 id,用户 id。你可以通过 doc id 来查询,会根据 doc id 进行 hash,判断出来当时把 doc id 分配到了哪个shard 上面去,从那个 shard 去查询,查询过程如下:
- 客户端发送请求到任意一个 node,成为 coordinate node。
- coordinate node 对 document 进行路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。
- 接收请求的 node 返回 document 给 coordinate node。
- coordinate node 返回 document 给客户端。
ES 搜索数据过程
- 客户端发送请求到一个 coordinate node。
- 协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard 也可以。
- query phase:每个 shard 将自己的搜索结果(其实就是一些 doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。
- fetch phase:接着由协调节点,根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端。
ES 关键概念
segment file: 众所周知,Elasticsearch 存储的基本单元是 shard, ES 中一个 Index 可能分为多个 shard , 事实上每个 shard 都是一个 Lucence 的 Index ,并且每个 Lucence Index 由多个 Segment 组成, 每个 Segment 事实上是一些倒排索引的集合, 每次创建一个新的 Document , 都会归属于一个新的 Segment , 而不会去修改原来的 Segment ; 且每次的文档删除操作,会仅仅标记 Segment 中该文档为删除状态, 而不会真正的立马物理删除, 所以说 ES 的 index 可以理解为一个抽象的概念。es 每秒都会生成一个 segment 文件,当文件过多时 es 会自动进行 segment merge(合并文件),合并时会同时将已经标注删除的文档物理删除;
commit: 为了数据安全, 每次的索引变更都最好要立刻刷盘, 所以 Commit 操作意味着将 Segment 合并,并写入磁盘。保证内存数据尽量不丢。刷盘是很重的 IO 操作, 所以为了机器性能和近实时搜索, 并不会刷盘那么及时。
** commit point:** 记录当前所有可用的 segment ,每个 commit point 都会维护一个 .del 文件( es 删除数据本质是不属于物理删除),当 es 做删改操作时首先会在 .del 文件中声明某个 document 已经被删除,文件内记录了在某个 segment 内某个文档已经被删除,当查询请求过来时在 segment 中被删除的文件是能够查出来的,但是当返回结果时会根据 commit point 维护的那个 .del 文件把已经删除的文档过滤掉;
translog: translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。为了防止 elasticsearch 宕机造成数据丢失保证可靠存储,es 会将每次的操作同时写到 translog 日志中。新文档被索引意味着文档会被首先写入内存 buffer ,操作会被写入 translog 文件。每个 shard 都对应一个 translog 文件;translog 会每隔 5 秒异步执行或者在每一个请求完成之后执行一次 fsync 操作,将 translog 从缓存刷入磁盘,这个操作比较耗时,如果对数据一致性要求不是跟高时建议将索引改为 async ,如果节点宕机时会有 5 秒数据丢失;
refresh: 写入和打开一个新 segment 的轻量的过程,es 接收数据请求时先存入内存中,默认每隔一秒会从内存 buffer 中将数据写入 filesystem cache 中的一个 segment,内存 buffer 被清空,这个时候索引变成了可被搜索的,这个过程叫做 refresh;
fsync: fsync 是一个 Unix 系统调用函数, 用来将内存 buffer 中的数据存储到文件系统. 这里作了优化, 是指将 filesystem cache 中的所有 segment 刷新到磁盘的操作;
flush: es 默认每隔 30 分钟或者操作数据量达到 512mb ,会将内存 buffer 的数据全都写入新的 segment 中,内存 buffer 被清空,一个 commit point 被写入磁盘,并将 filesystem cache 中的数据通过 fsync 刷入磁盘,同时清空 translog 日志文件,这个过程叫做 flush;
近实时搜索: 提交(Commit)一个新的 segment 到磁盘需要一个 fsync 来确保 segment 被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 fsync 操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着 fsync 要从整个过程中被移除。在 es 和磁盘之间是 filesystem cache 。 像之前描述的一样, 在内存缓冲区中的文档会被写入到一个新的段中。 但是这里新段会被先写入到文件系统缓存,这一步代价会比较低,稍后再被刷新(flush)到磁盘,这一步代价比较高。不过只要文件已经在 filesystem cache 中, 就可以像其它文件一样被打开和读取了。Lucene 允许新 segment 被写入和打开,使其包含的文档在未进行一次完整提交时便对搜索可见。 这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。而 es 中底层搜索的此种方式基于 refresh ,refresh 的默认值是 1s ,所以搜索不是实时的,而是近实时。
在数据量很大的情况下(数十亿级别)如何提高查询效率?
提高 ES节点内存 -- filesystem cache
- 在往 es 里写的数据时,实际上都写到磁盘文件里去了,磁盘文件里的数据操作系统会自动将里面的数据缓存到 os cache 里面去,es 的搜索引擎严重依赖于底层的 filesystem cache,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 indx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高,一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。
- 归根结底,你要让 es 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳总数据量的一半。
减少不必要的字段存储
- 数据存储到 ES 中进行检索时,只存储需要查询的字段,而不是把所有字段都存储进去。如果全把字段全部存储进去,会导致每个 document 的变大,占用的机器 os cache 越大,这就会导致相同内存时,所容纳的 document 越少,进而影响性能。
- 可以把必要字段存储到 ES 中,在 ES 检索出主键时,再从 mysql/cache/hbase 查询出完整数据,以保证查询性能。
数据预热
对于一些大促活动,可以考虑把热销商品提前从磁盘刷到 os cache中。基于此最好做一个专门的缓存预热子系统,就是对热数据,每隔一段时间,把数据刷到 os cache 中。
document 模型设计避免使用复杂语法查询,比如 join
es 里面的复杂的关联查询,复杂的查询语法,尽量别用,一旦用了性能一般都不太好。
分页性能优化
es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。用 es 做分页的时候,翻页的时候,翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长。所以用 es 做分页的时候,你会发现越翻到后面,就越是慢。