概要
本篇我们来看看shard内部的一些操作原理,了解一下人家是怎么玩的。
倒排索引
倒排索引的结构,是非常适合用来做搜索的,Elasticsearch会为索引的每个index为analyzed的字段建立倒排索引。
基本结构
倒排索引包含以下几个部分:
- 某个关键词的doc list
- 某个关键词的所有doc的数量IDF(inverse document frequency)
- 某个关键词在每个doc中出现的次数:TF(term frequency)
- 某个关键词在这个doc中的次序
- 每个doc的长度:length norm
- 某个关键词的所有doc的平均长度
记录这些信息,就是为了方便搜索的效率和_score分值的计算。
不可变性
倒排索引写入磁盘后就是不可变的,这样有几个好处:
- 不需要锁,如果不更新索引,不用担心锁的问题,可以支持较高的并发能力
- 如果cache内存足够,不更新索引的话,索引可以一直保存在os cache中,可以提升IO性能。
- 如果数据不变,filter cache会一直驻留在内存。
- 索引数据可以压缩,节省cpu和io开销。
doc底层原理
前面提到倒排索引是基于不可变模式设计的,但实际Elasticsearch源源不断地有新数据进来,那光是建立、删除倒排索引,岂不是非常忙?
如果真是不停地建立,删除倒排索引,那ES压力也太大了,肯定不是这么实现的。ES通过增加新的补充索引来接收新的文档和修改的文档,而不是直接用删除重建的方式重写整个索引。
doc写入
整个写入过程如下图所示:
- 新文档先写入内存索引缓存
- 当间隔一定时间(1秒),将缓存的数据进行提交,这个过程会创建一个Commit Point,Commit Point包含index segment的信息。
- 缓存的数据写入新的index segment。
- index segment的数据先写入os-cache中
- 等待操作系统将os-cache的数据强制刷新到磁盘中
- 写入磁盘完成后,新的index segment被打开,此时segment内的文档可以被搜索到。
- 同时buffer的数据被清空,等待下一次新的文档写入。
index segment翻译过来叫"段",每秒会创建一个,ES把这个1秒内收到的、需要处理的文档都放在这个段里,可以把段认为是倒排索引的一个子集。
索引、分片、段的关系如下:
索引包含多个分片,每个分片是一个Lucene索引实例,一个分片下面有多个段。如果把分片看作是一个独立的倒排索引结构,那么这个倒排索引是由多个段文件的集合。
三者之间是包含关系:索引包含多个分片,分片包含多个段。
doc删除和更新
当文档被删除时,Commit Point会把信息记录在.del文件中,在.del文件中会标识哪些文档是有deleted标记的,但该文档还是存在于原先的index segment文件里,同样能够被检索到,只是在最终结果处理时,标记为deleted的文档被会过滤掉。
更新也是类似的操作,更新会把旧版本的文档标记为deleted,新的文档会存储在新的index segment中。
近实时搜索
上面的流程细节的童鞋可以会发现,每次都需要fsync磁盘,数据才是可搜索的,那IO压力将特别大,耗费时间比较长,并且执行周期由操作系统控制,从一个新文档写入到可以被搜索,超过1分钟那是常有的事。
所以Elasticsearch对此做了一个改进:
index segment信息写入到os-cache中,即完成上面的第4步,该segment内的文档信息就可以被搜索到了。fsync操作就不立即执行了,
os-cache的写入代价比较低,最耗时的fsync操作交由操作系统调度执行。
上述的index segment写入到os-cache,并打开搜索的过程,叫做refresh,默认是每隔1秒refresh一次所以,es是近实时的,数据写入到可以被搜索,默认是1秒。
refresh的时间也可以设置,比如我们一些日志系统,数据量特别大,但实时性要求不高,我们为了优化资源分配,就可以把refresh设置得大一些:
PUT /music
{
"settings": {
"refresh_interval": "30s"
}
}
此参数需要在创建索引时使用,要注意一下的是除非有充分的依据,才会对refresh进行设置,一般使用默认的即可。
translog机制
上述的写入流程当中,如果fsync到磁盘的操作没执行完成,服务器断电宕机了,可能会导致Elasticsearch数据丢失。Elasticsearch也设计了translog机制,跟关系型数据库的事务日志机制非常像,整个写入过程将变成这样:
- 新文档写入内存buffer的同时,也写一份到translog当中。
- 内存buffer的数据每隔1秒写入到index segment,并写入os-cache,完成refresh操作。
- 内存buffer被清空,但translog一直累加。
- 每隔5秒translog信息fsync到磁盘上。
- 默认每30分钟或translog累积到512MB时,执行全量commit操作,os-cache中的segment信息和translog信息fsync到磁盘中,持久化完成。
- 生成新的translog,旧的translog归档(6.x版本translog做归档操作,不删除)。
flush API
这个执行一个提交并且归档translog的行为称作一次flush。分片每30分钟被自动刷新(flush),或者在 translog 太大的时候(默认512MB)也会刷新,当然也可以手动触发flush的执行,如下请求:
POST /music/_flush
但任其自动flush就够了。如果重启节点前担心会对索引造成影响,可以手动flush一下。毕竟节点重启后需要从translog里恢复数据,translog越小,恢复就越快。
durability同步和异步
translog写磁盘行为主要有两种,是由index.translog.durability配置项决定的:
- request:同步写磁盘,每次写请求完成之后立即执行(新增、删除、更新文档),以及primary shard和replica shard同步都会触发,数据安全有保障,不丢失,但会带来一些性能损失。如果是bulk数据导入,每个文档平摊下来的损失是比较小的。
- async:异步写磁盘,默认5秒fsync一次,如果有宕机事件,可能会丢失几秒的数据,适用于允许偶尔有数据丢失的场景,如日志系统。
如果系统不接受数据丢失,用translog同步方式,示例设置:
# 异步方式
PUT /music_new
{
"settings": {
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
}
# 同步方式
PUT /music_new
{
"settings": {
"index.translog.durability": "request"
}
}
segment合并
Elasticsearch针对活跃的索引,每秒都会生成一个新的index segment,这些segment最终会以文件的形式存储在磁盘里,如果不对其进行处理,那么索引运用一段时间后,会有特别多的文件,零碎的文件太多了,也不是什么好事情,更耗费更多的文件资源,句柄等,搜索过程也会变慢。
合并过程
Elasticsearch会在后台对segment进行合并,减少文件的数量,同时,标记为deleted的文档在合并时会被丢弃(delete请求只是将文档标记为deleted状态,真正的物理删除是在段合并的过程中),合并过程不需要人工干预,让Elasticsearch自行完成即可。
两个已经提交的段和一个未提交的段合并成为一个大的段文件
合并时会挑一些大小接近的段,合并到更大的段中,段合并过程不阻塞索引和搜索。
合并完成后,新的更大的段flush到磁盘中,并完成refresh操作,老的段被删除掉。
optimize API
optimize命令可以强制合并API,并指定最终段的数量,如下命令:
POST /music_new/optimize
{
"max_num_segments": 1
}
指定segment最大数量为1,表示该索引最终只有一个segment文件。
适用场景
- 正常活跃的、经常有更新的索引不建议使用
- 日志类的索引,对老数据进行优化时,可以将每个分片的段进行合并
使用建议
- 一般不需要人工干预合并过程
- optimize操作会消耗大量的IO资源,使用要慎重考虑
小结
本篇主要介绍shard内部的原理,包含写入、更新删除,translog机制,segment合并等,了解数据库的童鞋对translog机制应该非常熟悉,原理上大同小异,仅作抛砖引玉,谢谢。
专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区
可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术