Elasticsearch
ElasticSearch是一款非常强大的、基于Lucene的开源搜索及分析引擎,它是一个实时的分布式全文搜索分析引擎。
ES中的数据都是来自于MySQL,用ES的目的不是来持久化数据的,而是因为它的数据检索、复杂数据分析的效率极高,用它来完成检索、分析的功能。
它被用作全文检索、结构化搜索、分析以及这三个功能的组合:
- Wikipedia 使用 Elasticsearch 提供带有高亮片段的全文搜索,还有 search-as-you-type 和 did-you-mean 的建议。
- 卫报 使用 Elasticsearch 将网络社交数据结合到访客日志中,为它的编辑们提供公众对于新文章的实时反馈。
- Stack Overflow 将地理位置查询融入全文检索中去,并且使用 more-like-this 接口去查找相关的问题和回答。
- GitHub 使用 Elasticsearch 对1300亿行代码进行查询。
除了搜索,结合Kibana、Logstash、Beats开源产品,Elastic Stack(简称ELK)还被广泛运用在大数据近实时分析领域,包括:日志分析、指标监控、信息安全等。它可以帮助你探索海量结构化、非结构化数据,按需创建可视化报表,对监控数据设置报警阈值,通过使用机器学习,自动识别异常状况。
ElasticSearch是基于Restful WebApi,使用Java语言开发的搜索引擎库类,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。其客户端在Java、C#、PHP、Python等许多语言中都是可用的。
基础概念 - 全文搜索
从全文数据中进行检索。
数据可分为:
- 结构化数据
指具有固定格式或有限长度的数据,如数据库、元数据等,可以用二维表结构来逻辑表达实现的数据 - 非结构化数据
指不定长或无固定格式的数据,如邮件,word文档等。
非结构化数据是数据结构不规则或不完整,没有预定义的数据模型,不方便用数据库二维逻辑表来表现的数据,包括所有格式的办公文档、文本、图片、各类报表、图像和音频/视频信息等等。 - 半结构化数据
如XML、HTML等,当根据需要可按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。
对于结构化数据,因为它们具有特定的结构,所以我们一般都是可以通过关系型数据库(MySQL、Oracle的)的二维表(Table)的方式存储和搜索,对表可以建立索引。
对于非结构化数据,也即对全文数据的搜索主要有两种方法:
- 顺序扫描
按照顺序扫描的方式查询特定的关键字。 - 全文检索
将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。
基础概念 - Lucene
倒排索引
Lucene能实现全文搜索主要是因为它实现了倒排索引的查询结构。
Elasticsearch是通过Lucene的倒排索引技术实现比关系型数据库更快的过滤。
思考:其比关系型数据库的B+树索引快在哪里?为什么快?
如何理解倒排索引呢?假如现有三份数据文档,文档的内容分别是:
- Java is the best programming language.
- PHP is the best programming language.
- Javascript is the best programming language.
为了创建倒排索引,通过分词器将每个文档的内容域拆分成单独的词(称它为词条或Term),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。
结果如下所示:
Term | Doc_1 | Doc_2 | Doc_3 |
---|---|---|---|
Java | X | ||
is | X | X | X |
the | X | X | X |
best | X | X | X |
programming | x | X | X |
language | X | X | X |
PHP | X | ||
Javascript | X |
这种结构由文档中所有不重复词的列表构成,对于其中每个词都有一个文档列表与之关联。
这种由属性值来确定记录的位置的结构就是倒排索引。
- 词条(Term)
索引里面最小的存储和查询单元,对于英文来说是一个单词,对于中文来说一般指分词后的一个词。 - 词典(Term Dictionary)
或字典,是词条 Term 的集合。
搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。 - 倒排表(Post list)
一个文档通常由多个词组成,倒排表记录的是某个词在哪些文档里出现过以及出现的位置。每条记录称为一个倒排项(Posting)。倒排表记录的不单是文档编号,还存储了词频等信息。 - 倒排文件(Inverted FIle)
所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件被称之为倒排文件,倒排文件是存储倒排索引的物理文件。
词典和倒排表是 Lucene 中很重要的两种数据结构,是实现快速检索的重要基石。词典和倒排文件是分两部分存储的,词典在内存中,而倒排文件存储在磁盘上。
Lucene索引结构
Lucene的索引结构中有哪些文件?
文件的关系如下:
Lucene段概念
分段存储
在早期的全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中,如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。
所以,在搜索中引入了段的概念,将一个Lucene Index索引文件拆分为多个子文件,每个子文件叫做段,每个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。
段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件。
一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索。
在分段的思想下,对数据写操作的过程如下:
- 新增
当有新的数据需要创建索引时,由于段的不变形,所以选择新建一个段来存储新增的数据。 - 删除
当需要删除数据时,由于数据所在的段只可读,不可写,所以Lucene在索引文件下新增了一个.del
的文件,用来专门存储被删除的数据id。
当查询时,被删除的数据还是可以被查到的,只是在进行文档链表合并时,才把已经删除的数据过滤掉。被删除的数据在进行段合并时才会真正被移除。 - 更新
更新的操作其实就是删除和新增的组合,先在.del
文件中记录旧数据,再在新段中添加一条更新后的数据。
段不变性的优点如下:
- 不需要锁。因为数据不会更新,所以不用考虑多线程下的读写不一致情况。
- 可以常驻内存。段在被加载到内存后,由于具有不变性,所以只要内存的空间足够大,就可以长时间驻存,大部分查询请求会直接访问内存,而不需要访问磁盘,使得查询的性能有很大的提升。
- 缓存友好。在段的生命周期内始终有效,不需要在每次数据更新时被重建。
- 增量创建。分段可以做到增量创建索引,可以轻量级地对数据进行更新,由于每次创建的成本很低,所以可以频繁地更新数据,使系统接近实时更新。
段不变性的缺点如下:
- 当对数据进行删除时,旧数据不会被马上删除,而是在.del文件中被标记为删除。而旧数据只能等到段更新时才能真正被移除,这样会有大量的空间浪费。
- 更新。更新数据由删除和新增这两个动作组成。若有一条数据频繁更新,则会有大量的空间浪费。
- 由于索引具有不变性,所以每次新增数据时,都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源(如文件句柄)的消耗会非常大,查询的性能也会受到影响。
- 在查询后需要对已经删除的旧数据进行过滤,这增加了查询的负担。
延迟写策略
为了提升写的性能,ES 并没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写的策略。
每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存。
当达到默认的时间(1 秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统 上,稍后再被刷新到磁盘中并生成提交点。
这里的内存使用的是 ES 的 JVM 内存,而文件缓存系统使用的是操作系统的内存。
新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。
由内存刷新到文件缓存系统的时候会生成新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。
在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 Refresh (即内存刷新到文件缓存系统)。
默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是近实时搜索,因为文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。
段合并策略
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。
每一个段都会消耗文件句柄、内存和 CPU 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。
Elasticsearch 通过在后台定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。
根据段的大小先将段进行分组,再将属于同一组的段进行合并。但是由于对超级大的段的合并需要消耗更多的资源,所以Lucene会在段的大小达到一定规模,或者段里面的数据量达到一定条数时,不会再进行合并。
所以Lucene的段合并主要集中在对中小段的合并上,这样既可以避免对大段进行合并时消耗过多的服务器资源,也可以很好地控制索引中段的数量。
基础概念 - ElasticSearch
- cluster 集群
一个集群由一个唯一的名字标识,默认为“elasticsearch”。
每个节点配置相同的 cluster.name 即可加入集群,集群名称可以在配置文件中指定。 - node 节点
- Index 索引
一个索引是一个文档的集合。每个索引有唯一的名字,通过这个名字来操作它。一个集群中可以有任意多个索引。
- shard 分片
ES支持PB级全文搜索,当某个索引上的数据量太大的时候,ES通过水平拆分的方式讲一个索引上的数据拆分出来分配到不同的数据块上,拆分出来的数据块称为一个分片。
在创建索引的时候需要指定分片的数量,并且分片的数量一旦确定就不能修改,在一个多分片的索引中写入数据时,通过路由来确定具体写入哪一个分片中。
ES中的每个shard分片本质上是Lucene中的一个索引文件,一个ES索引是分片的集合。
当 Elasticsearch 在索引中搜索的时候,它发送查询到每一个属于索引的分片(Lucene 索引),然后合并每个分片的结果到一个全局的结果集。 - replica 副本
在一个网络 / 云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的, Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。
- shard 分片
- type 类型
指在一个索引中,可以索引不同类型的文档,如用户数据、博客数据。
从6.0版本起已废弃。7.0版本及之后,一个index中只有一个默认的type,即_doc。 - document 文档
被索引的一条数据,索引的基本信息单元,以JSON格式来表示。1
2
3
4
5
6
7
8
9
10
11
12
13
14{
"_index": , 代表操作的哪个 index(库)
"_type": , 代表操作的哪个 type(表)
"_id": , 每条记录 都有一个 唯一标识,当无法判断是更新,还是insert操作的时候,看展示的_id,有没有变化,
有就是insert了一条新记录,没有就是 更新操作
还有可能是 没有执行更新/insert操作
"_version": , 代表这条记录的版本号(根据版本号,可以得知此记录是否被修改过,修改一次,版本号就会变化一次)
"_source": , json记录本体信息
"_seq_no": , 并发控制字段,每次更新+1,用来做乐观锁
应用:在 更新请求后加上 ?if_seq_no = 此时记录的 seq_no & if_primary_term = 此时记录的 primary_term
这时如果,更新操作的时候,如果记录的 _seq_no != if_seq_no 的值,那么无法更新
这样当两个请求,同时操作这条记录的时候,一个请求已经更新了记录,那么 _seq_no + 1,下一个请求就无法 完成更新操作了
"_primary": , 集群,主分片重新分配,如重启,就会变化
}
集群 Cluster
ES 的集群搭建很简单,不需要依赖第三方协调管理组件,自身内部就实现了集群的管理功能。
ES 集群由一个或多个 Elasticsearch 节点组成,每个节点配置相同的 cluster.name 即可加入集群,默认值为 “elasticsearch”。
确保不同的环境中使用不同的集群名称,否则最终会导致节点加入错误的集群。
一个 Elasticsearch 服务启动实例就是一个节点(Node)。节点通过 node.name 来设置节点名称,如果不设置则在启动时给节点分配一个随机通用唯一标识符作为名称。
发现机制
ES 内部是如何通过一个相同的设置 cluster.name 就能将不同的节点连接到同一个集群的?
答案是 Zen Discovery。
Zen Discovery 是 Elasticsearch 的内置默认发现模块(发现模块的职责是发现集群中的节点以及选举 Master 节点)。
它提供单播和基于文件的发现,并且可以扩展为通过插件支持云环境和其他形式的发现。
Todo: Elasticsearch服务发现以及选主的具体流程?
由于它支持任意数目的集群( 1- N ),所以不能像 Zookeeper 那样限制节点必须是奇数,也就无法用投票的机制来选主,而是通过一个规则。
只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。
但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题。
大多数解决方案就是设置一个 Quorum 值,要求可用节点必须大于 Quorum(一般是超过半数节点),才能对外提供服务。
而 Elasticsearch 中,这个 Quorum 的配置就是 discovery.zen.minimum_master_nodes 。
节点的角色
每个节点既可以是候选主节点也可以是数据节点,通过在配置文件../config/elasticsearch.yml
中设置即可,默认都为 true。
1 | node.master: true //是否候选主节点 |
- 数据节点
数据节点负责数据的存储和相关的操作,例如对数据进行增、删、改、查和聚合等操作,所以数据节点(Data 节点)对机器配置要求比较高,对 CPU、内存和 I/O 的消耗很大。
通常随着集群的扩大,需要增加更多的数据节点来提高性能和可用性。 - 候选主节点
候选主节点可以被选举为主节点(Master 节点),集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。 - 主节点
主节点负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是非常重要的。
一个节点既可以是候选主节点也可以是数据节点,但是由于数据节点对 CPU、内存核 I/O 消耗都很大。
所以如果某个节点既是数据节点又是主节点,那么可能会对主节点产生影响从而对整个集群的状态产生影响。
因此为了提高集群的健康性,我们应该对 Elasticsearch 集群中的节点做好角色上的划分和隔离。可以使用几个配置较低的机器群作为候选主节点群。
主节点和其他节点之间通过 Ping 的方式互检查,主节点负责 Ping 所有其他节点,判断是否有节点已经挂掉。其他节点也通过 Ping 的方式判断主节点是否处于可用状态。
虽然对节点做了角色区分,但是用户的请求可以发往任何一个节点,并由该节点负责分发请求、收集结果等操作,而不需要主节点转发。
这种节点可称之为协调节点,协调节点是不需要指定和配置的,集群中的任何节点都可以充当协调节点的角色。
脑裂现象
同时如果由于网络或其他原因导致集群中选举出多个 Master 节点,使得数据更新时出现不一致,这种现象称之为脑裂,即集群中不同的节点对于 Master 的选择出现了分歧,出现了多个 Master 竞争。
“脑裂”问题可能有以下几个原因造成:
- 网络问题
集群间的网络延迟导致一些节点访问不到 Master,认为 Master 挂掉了从而选举出新的 Master,并对 Master 上的分片和副本标红,分配新的主分片。 - 节点负载
主节点的角色既为 Master 又为 Data,访问量较大时可能会导致 ES 停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。 - 内存回收
主节点的角色既为 Master 又为 Data,当 Data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应。
为了避免脑裂现象的发生,我们可以从原因着手通过以下几个方面来做出优化措施:
- 适当调大响应时间,减少误判。
通过参数discovery.zen.ping_timeout
设置节点状态的响应时间,默认为 3s,可以适当调大。
如果 Master 在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如 6s,discovery.zen.ping_timeout:6),可适当减少误判。 - 选举触发。
我们需要在候选集群中的节点的配置文件中设置参数discovery.zen.munimum_master_nodes
的值。
这个参数表示在选举主节点时需要参与选举的候选主节点的节点数,默认值是 1,官方建议取值(master_eligibel_nodes / 2) + 1
,其中master_eligibel_nodes
为候选主节点的个数。
这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于 discovery.zen.munimum_master_nodes 个候选节点存活,选举工作就能正常进行。
当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。 - 角色分离。
即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点“已死”的误判。
分片 Shard
ES 支持 PB 级全文搜索,当索引上的数据量太大的时候,ES 通过水平拆分的方式将一个索引上的数据拆分出来分配到不同的数据块上,拆分出来的数据库块称之为一个分片。
这类似于 MySQL 的分库分表,只不过 MySQL 分库分表需要借助第三方组件而 ES 内部自身实现了此功能。
在一个多分片的索引中写入数据时,通过路由来确定具体写入哪一个分片中,所以在创建索引的时候需要指定分片的数量,并且分片的数量一旦确定就不能修改。
分片的数量和下面介绍的副本数量都是可以通过创建索引时的 Settings 来配置,ES 默认为一个索引创建 5 个主分片, 并分别为每个分片创建一个副本。
ES 通过分片的功能使得索引在规模上和性能上都得到提升,每个分片都是 Lucene 中的一个索引文件,每个分片必须有一个主分片和零到多个副本。
副本 Replica
副本就是对分片的 Copy,每个主分片都有一个或多个副本分片,当主分片异常时,副本可以提供数据的查询等操作。
主分片和对应的副本分片是不会在同一个节点上的,所以副本分片数的最大值是 N-1(其中 N 为节点数)。
对文档的新建、索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。
ES 为了提高写入的能力这个过程是并发写的,同时为了解决并发写的过程中数据冲突的问题,ES 通过乐观锁的方式控制,每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。
一旦所有的副本分片都报告写成功才会向协调节点报告成功,协调节点向客户端报告成功。
将数据分片是为了提高可处理数据的容量和易于进行水平扩展,为分片做副本是为了提高集群的稳定性和提高并发量。
副本越多,集群的可用性就越高,但是由于每个分片都相当于一个 Lucene 的索引文件,会占用一定的文件句柄、内存及 CPU。并且分片间的数据同步也会占用一定的网络带宽,所以索引的分片数和副本数也不是越多越好。
映射 Mapping
映射是用于定义 ES 对索引中字段的存储类型、分词方式和是否存储等信息,就像数据库中的 Schema ,描述了文档可能具有的字段或属性、每个字段的数据类型。
ES(v6.8)中字段数据类型主要有以下几类:
类型 | 数据类型 |
---|---|
核心类型 | text、keywords、long、integer、short、double、data、boolean等 |
复杂类型 | Object、Nested |
地理类型 | geo_point、get_shape |
特殊类型 | ip、completion、token_count、join等 |
Text 用于索引全文值的字段,例如电子邮件正文或产品说明。这些字段是被分词的,它们通过分词器传递,以在被索引之前将字符串转换为单个术语的列表。
分析过程允许 Elasticsearch 搜索单个单词中每个完整的文本字段。文本字段不用于排序,很少用于聚合。
Keyword 用于索引结构化内容的字段,例如电子邮件地址,主机名,状态代码,邮政编码或标签。它们通常用于过滤,排序,和聚合。Keyword 字段只能按其确切值进行搜索。
Elastic Stack生态
Beats + Logstash + ElasticSearch + Kibana
Beats
Beats是一个面向轻量型采集器的平台,这些采集器可以从边缘机器向Logstash、ElasticSearch发送数据,它是由Go语言进行开发的,运行效率方面比较快。不同Beats的套件是针对不同的数据源。
Logstash
Logstash是动态数据收集管道,拥有可扩展的插件生态系统,支持从不同来源采集数据,转换数据,并将数据发送到不同的存储库中。其能够与ElasticSearch产生强大的协同作用,后被Elastic公司在2013年收购。
ElasticSearch
ElasticSearch对数据进行搜索、分析和存储,其是基于JSON的分布式搜索和分析引擎,专门为实现水平可扩展性、高可靠性和管理便捷性而设计的。
它的实现原理主要分为以下几个步骤:
- 首先用户将数据提交到ElasticSearch数据库中;
- 再通过分词控制器将对应的语句分词;
- 将分词结果及其权重一并存入,以备用户在搜索数据时,根据权重将结果排名和打分,将返回结果呈现给用户。
Kibana
Kibana实现数据可视化,其作用就是在ElasticSearch中进行民航。Kibana能够以图表的形式呈现数据,并且具有可扩展的用户界面,可以全方位的配置和管理ElasticSearch。
从日志收集系统看ES Stack的发展
一个典型的日志系统包括:
(1)收集:能够采集多种来源的日志数据
(2)传输:能够稳定的把日志数据解析过滤并传输到存储系统
(3)存储:存储日志数据
(4)分析:支持 UI 分析
(5)警告:能够提供错误报告,监控机制
beats+elasticsearch+kibana
beats+logstath+elasticsearch+kibana
beats+MQ+logstash+elasticsearch+kibana
Elastic Stack最佳实践
日志收集系统
Metric收集和APM性能监控
多数据中心方案
ES原理 - ElasticSearch & Lucene
- 一个 ES Index在集群模式下,有多个 Node (节点)组成。每个节点就是 ES 的Instance (实例)。
- 每个节点上会有多个 shard(分片), P0、P1是主分片, R0、R1是副本分片
- 每个分片上对应着就是一个 Lucene Index(底层索引文件)
- Lucene Index 是一个统称
- 由多个 Segment (段文件,单个倒排索引文件称为Segment)组成。每个段文件存储着就是 Doc 文档。
- commit point 记录了所有 segments 的信息。
Lucene处理流程
创建索引的过程
- 准备待索引的原文档,数据来源可能是文件、数据库或网络
- 对文档的内容进行分词组件处理,形成一系列的Term
- 索引组件对文档和Term处理,形成字典和倒排表
搜索索引的过程
- 对查询语句进行分词处理,形成一系列Term
- 根据倒排索引表查找出包含Term的文档,并进行合并形成符合结果的文档集
- 比对查询语句与各个文档相关性得分,并按照得分高低返回
ElasticSearch分析器
问题
- ES内部是如何运行的?
- 主分片和副本分片是如何同步的?
- 创建索引的流程是什么样的?
- ES 如何将索引数据分配到不同的分片上的?以及这些索引数据是如何存储的?
- 为什么说 ES 是近实时搜索引擎而文档的 CRUD (创建-读取-更新-删除) 操作是实时的?
- Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据?Translog(未持久化的数据)、段提交点(已持久化的段)
- 为什么删除文档不会立刻释放空间?
ElasticSearch中最重要原理是文档的索引和文档的读取
ES原理 - 索引文档流程详解(写)
文档索引步骤顺序
单个文档
某索引下新建单个文档所需要的步骤顺序:
- 客户端向 Node 1 发送新建、更新或者删除请求。
- 节点使用文档的 _id 确定文档属于分片 0。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
- Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。
多个文档
使用 bulk 修改多个文档步骤顺序:
- 客户端向 Node 1 发送 bulk 请求。
- Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。
- 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。
文档索引过程详解
整体的索引过程
- 在一个写请求被发送到某个节点后,该节点即为前面说过的协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。
1
2// Routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。
shard = hash(routing) % (num_of_primary_shards) - 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到Memory Buffer(ES的JVM内存);然后定时(默认是每隔1秒)写入到Filesystem Cache,这个从Momery Buffer到Filesystem Cache的过程就叫做refresh;
- 当然在某些情况下,存在Momery Buffer和Filesystem Cache的数据可能会丢失,ES是通过translog的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到translog中,当Filesystem cache中的数据写入到磁盘中时,才会清除掉,这个过程叫做flush。
- 在flush过程中,内存中的缓冲将被清除,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的translog将被删除并开始一个新的translog。 flush触发的时机是定时触发(默认30分钟)或者translog变得太大(默认为512M)时。
分步骤看数据持久化过程:write -> refresh -> flush -> merge
write过程
一个新文档过来,会存储在 in-memory buffer 内存缓存区(ES的JVM内存)中,顺便会记录 Translog(Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录)。
这时候数据还没到 segment ,是搜不到这个新文档的。数据只有被 refresh 后,才可以被搜索到。
refresh过程
refresh 默认 1 秒钟,执行一次上图流程。ES 是支持修改这个值的,通过 index.refresh_interval 设置 refresh (冲刷)间隔时间。
refresh 流程大致如下:
- in-memory buffer 中的文档写入到新的 segment 中,但 segment 是存储在文件系统的缓存中。此时文档可以被搜索到。
- 最后清空 in-memory buffer。注意: Translog 没有被清空,为了将 segment 数据写到磁盘。
文档经过 refresh 后, segment 暂时写到文件系统缓存,这样避免了性能 IO 操作,又可以使文档搜索到。
refresh 默认 1 秒执行一次,性能损耗太大。一般建议稍微延长这个 refresh 时间间隔,比如 5 s。因此,ES 其实就是准实时,达不到真正的实时。
flush过程
每隔一段时间(默认30分钟)或者 translog 变得越来越大(默认为512M),索引被刷新(flush):一个新的 translog 被创建,并且一个全量提交被执行。
文档从文件缓存写入磁盘的过程就是 flush。写入磁盘后,清空 translog。
具体过程如下:
- 所有在内存缓冲区的文档都被写入一个新的段。
- 缓冲区被清空。
- 一个Commit Point被写入硬盘。
- 文件系统缓存通过 fsync 被刷新(flush)。
- 老的 translog 被删除。
merge过程
由于自动刷新流程每秒会创建一个新的段,这样会导致短时间内的段数量暴增。
而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和cpu运行周期。
更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
Elasticsearch通过在后台进行Merge Segment来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。
当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。
一旦合并结束,老的段被删除。新的段被刷新(flush)到了磁盘,写入一个包含新段且排除旧的和较小的段的新提交点。
合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。
深入ElasticSearch索引文档的实现机制
写操作的关键点
在考虑或分析一个分布式系统的写操作时,一般需要从下面几个方面考虑:
- 可靠性:或者是持久性,数据写入系统成功后,数据不会被回滚或丢失。
由于Lucene的设计中不考虑可靠性,在Elasticsearch中通过Replica和TransLog两套机制保证数据的可靠性。 - 一致性:数据写入成功后,再次查询时必须能保证读取到最新版本的数据,不能读取到旧数据。
Lucene中的Flush锁只保证Update接口里面Delete和Add中间不会Flush,但是Add完成后仍然有可能立即发生Flush,导致Segment可读。这样就没法保证Primary和所有其他Replica可以同一时间Flush,就会出现查询不稳定的情况,这里只能实现最终一致性。 - 原子性:一个写入或者更新操作,要么完全成功,要么完全失败,不允许出现中间状态。
Add和Delete都是直接调用Lucene的接口,是原子的。当部分更新时,使用Version和锁保证更新是原子的。 - 隔离性:多个写入操作相互不影响。
仍然采用Version和局部锁来保证更新的是特定版本的数据。 - 实时性:写入后是否可以立即被查询到。
使用定期Refresh Segment到内存,并且Reopen Segment方式保证搜索可以在较短时间(比如1秒)内被搜索到。通过将未刷新到磁盘数据记入TransLog,保证对未提交数据可以通过ID实时访问到。 - 性能:写入性能,吞吐量到底怎么样。
Lucene的写
Elasticsearch内部使用了Lucene完成索引创建和搜索功能,Lucene中写操作主要是通过IndexWriter类实现,IndexWriter提供三个接口:
1 | public long addDocument(); |
通过这三个接口可以完成单个文档的写入、更新和删除功能,包括了分词、倒排创建、正排创建等等所有搜索相关的流程。
只要Doc通过IndesWriter写入后,后面就可以通过IndexSearcher搜索了,看起来功能已经完善了,但是仍然有一些问题没有解:
- 上述操作是单机的,而不是我们需要的分布式。
- 文档写入Lucene后并不是立即可查询的,需要生成完整的Segment后才可被搜索,如何保证实时性?
- Lucene生成的Segment是在内存中,如果机器宕机或掉电后,内存中的Segment会丢失,如何保证数据可靠性?
- Lucene不支持部分文档更新,但是这又是一个强需求,如何支持部分更新?
上述问题,在Lucene中是没有解决的,那么就需要Elasticsearch中解决上述问题。
ElasticSearch的写
ElasticSearch写入请求类型
ES原理 - 读取文档流程详解(读)
文档查询步骤顺序
单个文档
- 客户端向 Node 1 发送获取请求。
- 节点使用文档的 _id 来确定文档属于分片 0。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2。
- Node 2 将文档返回给 Node 1,然后将文档返回给客户端。
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
多个文档
- 客户端向 Node 1 发送 mget 请求。
- Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。
文档读取过程详解
所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档,这种在Elasticsearch中称为query_then_fetch。
- 在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。PS:在2. 搜索的时候是会查询Filesystem Cache的,但是有部分数据还在Memory Buffer,所以搜索是近实时的。
- 每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
- 接下来就是 取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
深入ElasticSearch读取文档的实现机制
读操作
- 一致性指的是写入成功后,下次读操作一定要能读取到最新的数据。对于搜索,这个要求会低一些,可以有一些延迟。但是对于NoSQL数据库,则一般要求最好是强一致性的。
- 结果匹配上,NoSQL作为数据库,查询过程中只有符合不符合两种情况,而搜索里面还有是否相关,类似于NoSQL的结果只能是0或1,而搜索里面可能会有0.1,0.5,0.9等部分匹配或者更相关的情况。
- 结果召回上,搜索一般只需要召回最满足条件的Top N结果即可,而NoSQL一般都需要返回满足条件的所有结果。
- 搜索系统一般都是两阶段查询,第一个阶段查询到对应的Doc ID,也就是PK;第二阶段再通过Doc ID去查询完整文档,而NoSQL数据库一般是一阶段就返回结果。在Elasticsearch中两种都支持。
目前NoSQL的查询,聚合、分析和统计等功能上都是要比搜索弱的。
Lucene的读
Elasticsearch使用了Lucene作为搜索引擎库,通过Lucene完成特定字段的搜索等功能,在Lucene中这个功能是通过IndexSearcher的下列接口实现的:
1 | // search接口实现搜索功能,返回最满足Query的N个结果 |
这三个功能是搜索中的最基本的三个功能点,对于大部分Elasticsearch中的查询都是比较复杂的,直接用这个接口是无法满足需求的,比如分布式问题。
这些问题都留给了Elasticsearch解决。