建立索引的时候,咱们经过Mapping 映射定义好索引的基本结构信息,接下来咱们确定须要往 ES 里面新增业务文档数据了,例如用户,日志等业务数据。新增的业务数据,咱们根据 Mapping 来生成对应的倒排索引信息 。缓存
咱们一直说,Elasticsearch是一个基于Apache Lucene 的开源搜索引擎。Elasticsearch的搜索高效的缘由并非像Redis那样重依赖内存的,而是经过创建特殊的索引数据结构--倒排索引实现的。因为它的使用场景:处理PB级结构化或非结构化数据,数据量大且须要持久化防止断电丢失,因此 Elasticsearch 的数据和索引存储是依赖于服务器的硬盘。这也是为何咱们在ES性能调优的时候能够将使用SSD硬盘存储做为其中一个优化项来考虑。服务器
倒排索引的概念,我相信你们都已经知道了,这里就不在赘述,倒排索引能够说是Elasticsearch搜索高效和支持非结构化数据检索的主要缘由了,可是倒排索引被写入磁盘后是不可改变 的:它永远不会修改。markdown
倒排索引的不可变性,这点主要是由于 Elasticsearch 的底层是基于 Lucene,而在 Lucene 中提出了按段搜索的概念,将一个索引文件拆分为多个子文件,则每一个子文件叫做***段***,每一个段都是一个独立的可被搜索的数据集,而且段具备不变性,一旦索引的数据被写入硬盘,就不可再修改。数据结构
段 的概念提出主要是由于:在早期全文检索中为整个文档集合创建了一个很大的倒排索引,并将其写入磁盘中。若是索引有更新,就须要从新全量建立一个索引来替换原来的索引。这种方式在数据量很大时效率很低,而且因为建立一次索引的成本很高,因此对数据的更新不能过于频繁,也就不能保证时效性。app
并且在底层采用了分段的存储模式,使它在读写时几乎彻底避免了锁的出现,大大提高了读写性能。说到这,大家可能会想到 ConcurrentHashMap 的分段锁 的概念,其实原理有点相似。分布式
并且 Elasticsearch 中的倒排索引被设计成不可变的,有如下几个方面优点:微服务
- 不须要锁。若是你历来不更新索引,你就不须要担忧多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里。因为其不变性,只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提高。
- 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不须要在每次数据改变时被重建,由于数据不会变化。
- 写入单个大的倒排索引容许数据被压缩,减小磁盘 I/O 和 须要被缓存到内存的索引的使用量。
每个段自己都是一个倒排索引,但索引在 Lucene 中除表示全部段的集合外,还增长了提交点的概念。性能
为了提高写的性能,Lucene并无每新增一条数据就增长一个段,而是采用延迟写的策略,每当有新增的数据时,就将其先写入内存中,而后批量写入磁盘中。如有一个段被写到硬盘,就会生成一个提交点,提交点就是一个列出了全部已知段和记录全部提交后的段信息的文件。优化
上面说过 ES 的索引的不变性,还有段和提交点的概念。那么它的具体实现细节和写入磁盘的过程是怎样的呢?搜索引擎
用户建立了一个新文档,新文档被写入到一个新段中,而后首先被添加到内存索引缓存中。
不时地, 缓存被提交,这时缓存中新段会被先写入到文件缓存系统而不是直接被刷到磁盘。 这是由于,提交一个新的段到磁盘须要一个fsync
来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 可是 fsync
操做代价很大;若是每次索引一个文档都去执行一次的话会形成很大的性能问题,可是这里新段会被先写入到文件系统缓存,这一步代价会比较低。
新的段被写入到文件缓存系统,这时内存缓存被清空。在文件缓存系统会存在一个未提交的段。虽然新段未被提交(刷到磁盘),可是文件已经在缓存中了, 此时就能够像其它文件同样被打开和读取了。
到目前为止索引的段还未被刷新到磁盘,若是没有用 fsync
把数据从文件系统缓存刷(flush)到硬盘,咱们不能保证数据在断电甚至是程序正常退出以后依然存在。Elasticsearch 增长了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操做时均进行了日志记录。如上图所示,一个文档被索引以后,就会被添加到内存缓冲区,而且同时追加到了 translog。
每隔一段时间,更多的文档被添加到内存缓冲区和追加到事务日志(translog),以后新段被不断从内存缓存区被写入到文件缓存系统,这时内存缓存被清空,可是事务日志不会。随着 translog 变得愈来愈大,达到必定程度后索引被刷新,在刷新(flush)以后,段被全量提交,一个提交点被写入硬盘,而且事务日志被清空。
从整个流程咱们能够了解到如下几个问题:
因为自动刷新流程每秒会建立一个新的段 ,这样会致使短期内的段数量暴增。而段数目太多会带来较大的麻烦。 每个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每一个搜索请求都必须轮流检查每一个段;因此段越多,搜索也就越慢。
Elasticsearch经过在后台进行段合并来解决这个问题。小的段被合并到大的段,而后这些大的段再被合并到更大的段。
段合并的时候会将那些旧的已删除文档 从文件系统中清除。 被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。
上文阐述了索引的持久化流程和倒排索引被设定为不可修改以及这样设定的好处。由于它是不可变的,你不能修改它。可是若是你须要让一个新的文档可被搜索,这就涉及到索引的更新了,索引不可被修改但又须要更新,这种看似矛盾的要求,咱们须要怎么作呢?
ES 的解决方法就是:用更多的索引。什么意思?就是原来的索引不变,咱们对新的文档再建立一个索引。这样说完不知道你们有没有疑惑或者没理解,咱们经过图表的方式说明下。
假如咱们现有两个日志信息的文档,信息以下:
这时候咱们获得的倒排索引内容(省略一部分)是:
词项(term) | 文档(Doc) |
---|---|
the | doc 1,doc 2 |
request | doc 1 |
param | doc 1,doc 2 |
is | doc 1,doc 2 |
name | doc 1 |
response | doc 2 |
result | doc 2 |
... | ... |
若是咱们这时新增一个文档 doc 3:the request param is name = 'li si' and sex is femal,或者修改文档 doc 2的内容为:the response result is code = 9999 and msg = 'false'。这时 ES 是如何处理的呢?
正如上文所述的,为了保留索引不变性,ES 会建立一个新的索引,对于新增的文档索引信息以下:
词项(term) | 文档(Doc) |
---|---|
the | doc 3 |
request | doc 3 |
param | doc 3 |
is | doc 3 |
name | doc 3 |
sex | doc 3 |
... | ... |
对于修改的文档索引信息以下;
词项(term) | 文档(Doc) |
---|---|
the | doc 2 |
response | doc 2 |
result | doc 2 |
is | doc 2 |
code | doc 2 |
sex | doc 2 |
... | ... |
经过增长新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每个倒排索引都会被轮流查询到(从最先的开始),查询完后再对结果进行合并。
正如上文所述那样,对于修改的场景来讲,同一个文档这时磁盘中同时会有两个索引数据一个是原来的索引,另外一个是修改以后的索引。
以正常逻辑来看,咱们知道搜索的时候确定以新的索引为标准,可是段是不可改变的,因此既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每一个提交点会包含一个 .del
文件,文件中会列出这些被删除文档的段信息。
当一个文档被 “删除” 时,它实际上只是在.del
文件中被 标记 删除。一个被标记删除的文档仍然能够被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
文档更新也是相似的操做方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
我的公众号:JaJian
欢迎长按下图关注公众号:JaJian!
按期为你奉上分布式,微服务等一线互联网公司相关技术的讲解和分析。