1.lucene的分段机制
什么叫分段
众所周知,es的底层原理是基于lucene实现的。而每个es的分片,其本质就是一个lucene索引
lucene索引并不是一整个完整的索引构成,而是有多个分段索引组成,每一个分段索引,称为一个segment
segment是lucene索引的最小组成单元。其中包含了文档中的词典、词典的倒排索引以及Document的原始数据等信息。每个segment都具备搜索功能
分段内部的数据结构
词典
经过分词后形成的数据结构,是倒排索引最重要的组成部分。一个词典的查找效率高低直接影响了倒排索引的查询效率。
为了实现高效的单条查询和范围查询,es中将词典分为了前缀索引和后缀词条。前缀索引由FST数据结构或BKDTree构成,而后缀词条以一个有序列表的形式存储。
文档中的每个字段(除了设置index:false)都有其自己的词典。词典文件需要内存加载,存储于内存当中。
倒排索引的数据结构大致如图:
FST是一种查询效率极高,同时占用内存很小的数据结构(同样支持范围查询)。但是其数据结构复杂,输入时要求必须有序,且不易更新。 具体参考:FST详解、segment详解
倒排表
倒排表就是每个词条对应文档ID的集合,Lucene现使用的倒排表结构叫Frame of reference。需要注意的是,倒排表存储在磁盘当中。文档中的每个字段(除了设置index:false)都有其自己的倒排表
Frame of reference包括以下两个特点
- 数据压缩
- 基于跳表实现交集计算
正向文件
正向文件指的就是原始文档,Lucene对原始文档也提供了存储功能,它存储特点就是分块+压缩。
fdt文件:存放原始文档的文件,它占了索引库90%的磁盘空间,里面一个chunk就是一个块,Lucene索引文档时,先缓存文档,缓存大于16KB时,就会把文档压缩存储。一个chunk包含了该chunk起始文档、多少个文档、压缩后的文档内容。需要注意的是,fdt文件存在磁盘当中
fdx文件:索引文件,通过文档号(自增数字)快速得到文档位置,数据结构为跳跃表。需要注意的是,fdx文件存在内存当中
doc-value
上述所说的三种数据结构,可以实现通过倒排索引快速查询文档这一功能
但是当需要聚合,排序,进行某个字段的计算时,正向文件就不在适用,总不能因为想要获取某个值,就把整个文档检索出来,很明显这样的做法是不合理的
因此doc-value就是为了应对这个问题
Lucene目前有五种类型的DocValues:NUMERIC、BINARY、SORTED、SORTED_SET、SORTED_NUMERIC,针对每种类型Lucene都有特定的压缩方法。
doc-value可以理解为键值对映射,每个字段都有其自己的doc-value。查找时通过倒排索引获取到文档ID,进而通过ID查询doc-value快速获取对应的属性值进行后续操作。需要注意的是,doc-value文件存在内存和磁盘当中
分段不变性
lucene索引的每个segment都具有不变性。可以理解为一旦索引建立,后续就对于已经创建的数据,就不会有任何修改
优点
- 因为其不变性,所以对已创建的索引,不会有更新操作。因此无须担心高并发时同时修改数据的问题,不需要悲观锁那种阻塞锁来限制写操作
- 因为其不变性,所以不会存在数据不一致的问题,读入缓存的数据,只要缓存空间足够,可以一致存在。这样可以尽最大可能避免磁盘IO
因为其不变性,因此对于较大的倒排索引,是允许的高度压缩的。减少磁盘 IO 和内存占用率
缺点
当对旧数据进行删除时,旧数据不会马上被删除,而是在 .del 文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。
- 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,对查询的性能造成部分影响
分段的写操作
因为segment具有不变性,那么对于每个segment的操作,都遵循如下规则
新增
segment新增操作相对来说很简单,新写入的文档都被写入到一个新的段中,在后续落盘即可
删除
segment的删除操作为逻辑删除,并不是真实删除一个文档,而是将其文档信息写入.del文件。被标识了del的文档仍然会在查询时被命中,但是会在返回客户端时被过滤掉
修改
segment的修改操作可以分解为两个动作:删除与新增
在修改数据时,会将源文档的信息写入.del文件,标识为删除。同时新增一个文档信息写入到新的段中,新文档的版本号version会+1。在查询时可能新文档与老文档都会被命中,但是在返回客户端时老文档会被过滤掉分段合并
经过上述文档的描述可以了解到,segment具有不变形。删除与更新操作理论上都不会清理掉原文档。
那么就带来了一个问题,随着时间推移,大量的文档被标记为删除并持续占有磁盘空间,直到最终占满。
这个问题该如何解决?lucene中采用了segment合并的操作来处理这个问题
在elasticSearch中,es服务器会由后台进程定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。
段合并的时候会将那些被标记为删除的文档从文件系统中清除,被删除的文档不会被拷贝到新的大段中。合并的过程中不会中断索引和搜索。合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。合并结束后老的段会被删除,新的段被 Flush 到磁盘,同时提交一个新的提交点。
段合并的计算量庞大, 而且还要吃掉大量磁盘 I/O,段合并会拖累写入速率,如果任其发展会影响搜索性能。Elasticsearch 在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。2.写原理
因为ElasticSearch是基于分布式部署的,因此对于任意一个写请求,实际上对于es服务集群来说,都包含两步
请求转发的对应的分片服务器
- 在分片服务器上执行写操作
分片选择
分片算法
因为es每个节点本身也是一个协调节点,因此无论请求到哪个节点上,都可以通过请求转发的形式,将请求发送到真正执行数据读写的数据节点上
es中采用取模法进行分片服务器的选择,具体公式:
shard = hash(routing) % number_of_primary_shards
其中routing是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。
因为取模法在节点数量有变化时,几乎需要重新计算所有数据的具体存储分片。因此es中在索引模板设置时,在指定了分片数以后,就不可以再次修改了。如果想修改,只能重建一个新的索引并重新录入文档数据
具体步骤
如图,假定当前ElasticSearch集群共有四个实例(四个分片),其中M1节点为主节点,每个实例拥有两个副本实例(副本分片)。
数据的写入分为两种情况,需要根据取模法判定当前写操作数据要触发到哪些分片。写入当前节点和混合节点(包括当前节点和其他节点)
写入当前节点时
- 客户端发送请求到M2节点
- M2节点根据取模法算出写命令要操作当前节点
- 直接写入数据,并通过并发机制将数据写入副本分片。es中通过乐观锁来控制并发写问题,具体通过文档的version字段来控制。当触发更新操作时,新生成的文档版本号+1,老的文档版本号不变。
当写入其他节点时
- 客户端发送请求到M2节点,M2节点根据取模法算出具体操作节点为M3节点
- M2将请求转发给M3节点
- M3进行具体的写操作处理,并通过并发写将数据同步到副本
- 所有分片写入完成。M3写入完成响应给M2节点,此时M2就是一个协调节点
-
数据写入
经过上述文档的描述,了解到了写操作如果在分片之间操作并响应给客户端的。接下来,就需要了解下,在具体的分片上,数据的写入是如何操作的
数据存储位置
在明确具体写入原理流程之前,需要先了解一个数据的存储位置概念
jvm内存:因为es是由java语言编写的,因此每个es实例都是运行在jvm上的。那么对于每个es实例来说,都具有一个jvm内存的存储位置
- 操作系统内存:jvm也需要运行在一个服务器上,因此,服务器还具有一个操作系统内存的存储位置
- 硬盘:es并不是完全基于内存读写的数据引擎,最终的数据最终是要落在磁盘上的
refresh
首先,文档数据被写入到jvm内存当中。经过上文描述可以知道,每一个分片实际上就是一个lucene索引,lucene索引的最小组成单元就是segment。
当文档数据还在jvm内存中时,文档数据还未被写成segment,此时的数据不可以读,只可以写
es默认每1s清空jvm内存中相关文档数据,将jvm内存中的文档数据写入到操作系统内存当中,并形成一个segment。这个过程叫做refresh
此时的文档数据可以读,但是不可以写了(因为段的不变性)。这也是为什么es是近实时查询,因为存在这1s的延迟。
可以在索引创建时,调整索引模板中的refresh_interval属性调整refresh频率,默认单位为ms,支持指定时间单位。例如 refresh_interval = “30s”
flush
文档数据在被写入到操作系统内存后,具备了读取功能。但是如果此时服务器宕机,那么数据就会丢失。
为了应对这个问题,es也其他同类产品一样,引入了事务日志(translog)的概念。在数据被写入到jvm内存当中时,同时也会记录一份translog到操作系统内存。
每5s,translog都会执行一次刷盘操作,将日志写入磁盘,这样即使服务器宕机,最多也就是丢失5s的数据
当translog中待写入数据量大于512M或距离上一次刷盘操作超过30分钟时,es会将操作系统内存当中的segment新入磁盘,形成一个提交点。并删除旧的translog,创建一个新的translog。这个过程叫做flush
提交点之前的数据存在于磁盘和操作系统内存中,提交点之后的数据,存在于操作系统内存和translog中
整体流程梳理
如图:
- 文档数据先被写入jvm内存,此时文档不能被查询。同时会写入一份数据到操作系统内存的tranlog
- 每1s钟,jvm将数据写入到操作系统内存,形成一个segment(可以理解为建立倒排索引等数据结构)。此时数据可以查询,但是不能修改
- 每5s钟,操作系统内存中的translog数据刷新到磁盘一次
- 每30分钟或待写入数据量达到512M,操作系统内存中的segment刷新到磁盘一次。形成一个提交点,并且删除旧的translog,创建一个新的translog。每一个提交点都有一个.del文件,包含了这个提交点中被标记为删除的文档ID
es服务器会在后台持续的对操作系统内存和磁盘中的segment进行合并,包括写入磁盘与未写入磁盘的segment,新的段在合并完成后会被flush到磁盘。合并后的新segment不包含.del标记的文档,并且在合并后会删除原segment。
3.读原理
分片选择
读操作的分片的选择原理,基本上与写操作一致。
但是有一点需要注意,写操作只能是主节点进行,但是读操作是可以在副本节点进行的。因此对于读取操作,es中的协调节点在取模后,采用轮询方式的负载均衡方式进行请求转发数据读取
数据的读取大致可以分为单条数据和批量查询两种,接下来对其原理进行说明
单条查询
通常来说就是根据ID进行数据查询,大致分为以下步骤
客户端发送请求到协调节点
- 协调节点通过取模判定具体读取节点
- 如果是当前节点,直接根据ID通过segment中的正向文件获取文档内容
- 如果是其他节点。通过轮询负载均衡,将请求转发到具体读取节点
- 具体读取节点通过segment中的正向文件获取文档内容返回给协调节点
-
批量查询
包括分页,聚合等查询,大致分为以下步骤
请求发送到协调节点
- 协调节点通过轮询负载均衡,将查询条件发送给所有节点
- 每个节点通过查询条件在segment的倒排索引文件中进行查询,将查询的文档ID返回给协调节点
- 协调节点对其他节点的返回结果进行聚合,分页,排序等操作,得到最终的文档ID集合
- 协调节点再将文档ID集合发送给对应的节点,
- 对应节点通过segment的正向文件获取文档内容,在返回给协调节点
-
4.ElasticSearch调优
参数调优
给每个文档指定有序的具有压缩良好的文档 ID,避免随机的 UUID-4 这样的 ID,这样的 ID 压缩比很低,会明显拖慢 Lucene。
- 对于那些不需要聚合和排序的索引字段禁用 Doc values。
- 不需要做模糊检索的字段使用 Keyword 类型代替 Text 类型,这样可以避免在建立索引前对这些文本进行分词。
- 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 index.refresh_interval 改到 30s 。
如果你是在做大批量导入,导入期间你可以通过设置这个值为 -1 关掉刷新,还可以通过设置 index.number_of_replicas: 0 关闭副本。别忘记在完工的时候重新开启它。 - 避免深度分页查询建议使用 Scroll 进行分页查询。普通分页查询时,会创建一个 from+size 的空优先队列,每个分片会返回 from+size 条数据,默认只包含文档 ID 和得分 Score 给协调节点。
如果有 N 个分片,则协调节点再对(from+size)×n 条数据进行二次排序,然后选择需要被取回的文档。当 from 很大时,排序过程会变得很沉重,占用 CPU 资源严重。 - 文档中的字段尽可能精简,只提供需要检索,聚合或排序的字段。其他字段可存在其他存储设备上,例如 Hbase,在 ES 中得到结果后再去 Hbase 查询这些字段。
创建索引和查询时指定路由 Routing 值,这样可以精确到具体的分片查询,提升查询效率。路由的选择需要注意数据的分布均衡。
JVM调优
确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小。Elasticsearch 默认安装后设置的堆内存是 1GB。可通过 ../config/jvm.option 文件进行配置,但是最好不要超过物理内存的50%和超过 32GB。
- GC 默认采用 CMS 的方式。CMS垃圾收集器使用的是标记清除算法,单次GC效率高,但是会产生内存碎片。因此在一定的时间内,会触发多次垃圾回收,存在 STW 的问题,可以考虑使用 G1 收集器。8.3版本已经默认使用G1垃圾收集器
- ES 非常依赖文件系统缓存(操作系统内存),快速搜索。一般来说,应该至少确保物理上有一半的可用内存分配到文件系统缓存。