基础概念
文档(Document) :Elasticsearch是面向文档的,文档是所有可搜索数据的最小单位,如:日志文件中的一行日志。文档被序列化为JSON格式,保存在Elasticsearch中。每个文档都有一个Unique ID,可以手动指定,如果不指定Elasticsearch会自动生成。
索引(Index) :索引是文档的容器,是一类文档的结合。索引的Mapping定义文档字段的类型;Setting定义不同的数据分布。
Type:在7.0开始,一个索引只能创建一个Type:_doc
与数据库类比结构
RDBMS(Relational Database Management System) | Elasticsearch |
---|---|
Table | Index(Type) |
Row | Document |
Column | Field |
Schema | Mapping |
SQL | DSL |
传统关系型数据库和Elasticsearch的区别:
- Elasticsearch:格式灵活(Schemaless)、相关性、高性能全文检索
- RDBMS:事务性、Join
分布式原理
共识性Consensus
共识性是分布式系统中最基础也是最重要的一个组件,在分布式系统中所有节点必须对给定的数据或者节点状态达成共识。Elasticsearch并没使用Raft、Paxos共识算法,也没有用Zookeeper,而是实现了共识系统zen discovery:基于Gossip协议实现单播,即节点间的通讯是一对一。
主要原因是:Elasticsearch认为zen discovery不仅能够实现共识系统的选择工作,还能够很方便的监控集群的读写状态是否健康。
不过在ES 7.0后,zen discovery不再使用,采用了类似Raft的算法。
并发Concurrency
Elasticsearch是一个分布式系统,写请求在发送到主分片时,同时会并发的发送到副本分片中,但是这些请求到达可能是无序的。
Elasticsearch使用乐观并发控制(Optimistic Concurrency Control)来保证新版本的数据不会被旧版本数据覆盖。乐观并发控制是一种乐观锁,另外一种乐观锁即多版本并发控制(Multi-Version Concurrency Control)。区别如下:
- 乐观并发控制(OCC):是一种用来解决写-写冲突的无锁并发控制,认为事务间的竞争不激烈,就先进行修改,在提交事务前检查数据有没有变化,如果没有就提交,如果有就放弃并重试。乐观并发控制类似于自旋锁,适用于低数据竞争且写冲突比较少的环境。
- 多版本并发控制(MVCC):是一种用来解决读-写冲突的无锁并发控制。为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读取该事务开始前的数据库快读。这样读操作不用阻塞操作,且写操作不用阻塞读操作的同时,避免脏读和不可重复读问题。
一致性Consistency
Elasticsearch集群保证写一致的方式,是在写入前检查有多少个分片可以写入。如果达到写入条件,则进行写操作,否则Elasticsearch需要等待更多的分片出现,默认1分钟。请求时可以携带配置wait_for_active_shards来判断是否允许写操作,或者通过Index Setting:index.write.wait_for_active_shards
:
- all:只有当主分片和所有副本分片可用时,才允许写操作。
- 正数:当索引的分片数可用(number_of_replicas+1)时才允许写操作。默认值为1,只需要主分片可用即可写入。
Elasticsearch集群保证读一致性的方式是,为了保证搜索请求的返回结果是最新的文档,写操作在主分片和副本分片同时完成后才返回写请求的结果。(从最新的资料来看,写副本分片已经没有异步的逻辑了,之前有些旧的资料还会说明有异步的情况)
脑裂
当一个集群中出现多个Master节点时,对集群状态的判断,出现了不一致。
脑裂问题解决:
7.0之前,限制选举条件,设置quorum(仲裁节点),只有Master eligible节点数大于quorum时,才能进行选举:
quorum = (master节点总数 / 2) + 1例如3个master eligible时,设置discovery.zen.minimum_master_nodes=2,即可避免脑裂。
7.0之后,移除了discovery.zen.minimum_master_nodes配置,让ES自己选择可以形成仲裁的节点。通过记录一个节点列表,这个列表保存了所有具备Master资格的节点,这个节点列表也会动态维护,选举时将会使用到这个列表。
分布式相关概念
Elasticsearch分布式特性:
- 好处:存储可以水平扩容;提升系统的可用性,部分节点宕机,整个集群服务不受影响。
- 架构:不同集群通过不同名字区分,默认名称为"elasticsearch",可以通过配置文件修改名称;一个集群可以有一个或者多个节点。
节点:节点就是一个Elasticsearch的实例,本质就是一个Java进程;每个节点都有名字,可以通过配置文件配置;每个节点启动后,会分配一个UID,保存在data目录下。
节点类型
Master Node
每个节点启动后,默认是一个Master eligible(有资格的)节点(可以通过node.master: false禁止);Master eligible节点可以参加选主流程,成为Master节点。当第一个节点启动的时候,它会将自己选举成Master节点。每个节点都保存了集群的状态,只有Master节点才能修改集群的状态信息,因为如果任意节点都能修改信息,会导致数据不一致。集群的状态信息,一般是为了一个集群中必要的信息,如:
- 所有的节点信息
- 所有的索引和其相关的Mapping与Setting信息。
- 分配的路由信息。
Master eligible选主过程:
各节点相互Ping对方,Node Id 低的会成为被选举的节点。ES 7.0之后采用类似Raft算法选主。- 其他节点会加入集群,但是不承担Master节点的角色。一旦发现被选中的主节点丢失,就会选举出新的Master节点。
Data Node
可以保存数据的节点,叫作Data Node。负责保存分片数据,在数据扩展上起到了重要作用。
Coordinating Node
- 可以接收Client的请求,将请求分发到合适的节点,最终把结果汇集到一起,返回给Client。
- 每个节点默认都是Coordinating Node
Ingest Node
可以看做是数据前置处理转换的节点,支持pipeline管道设置,可以使用ingest对数据进行过滤、转换等操作。类似于logstash中的filter的作用。
Hot & Warm Node
不同硬件配置的Data Node,用来实现Hot & Warm架构,降低集群部署成本。
Machine Learning Node
负责跑机器学习的Job,用来做异常检测。
Tribe Node
- 5.3开始使用Cross Cluster Search
- Tribe Node连接到不同的Elasticsearch集群,并且支持将这些集群当成一个单独的集群处理。
节点类型 | 配置参数 | 默认值 |
---|---|---|
Maste rligible | node.master | true |
data | node.data | true |
ingest | node.ingest | true |
coordinating only | 无 | 每个节点默认都是coordinating。其他参数设置为false时,就变成只是coordinating |
machine learning | node.ml | true(需要enable x-pack) |
分片
分片是ES的底层工作单元,实际上是一个Lucene的实例,仅保存全部数据的一部分。ES利用分片将数据分发到集群上的各个节点。当集群扩大或者缩小时,ES会在各个节点中迁移分片,使数据均匀分布在集群里。
主分片(Primary Shard)
用以解决数据水平扩展的问题。通过主分片,可以将数据分布到集群内的所有节点上
- 一个分片就是一个Lucene的实例。
- 主分片数在索引创建时指定,后续不允许修改,除非是Reindex操作。
副本(Replica Shard)
用于解决数据高可用问题,副本分片是主分片的拷贝。
- 副本分片数,可以动态调整。
- 增加副本数,还可以在一定程度上提高服务的可用性以及读取的吞吐。
分片设置
- 分片设置过小:当值后续无法增加加点实现水平扩展;单个分片数据量太大,导致数据重新分配耗时(在节点扩缩容的情况下)
- 分片设置过大:影响搜索结果的相关性打分,影响统计结果的准确性;单个节点上过多的分片,会导致资源浪费,同时也会影响性能(如搜索需要往更多的分片中查找数据)。
- 从7.0开始,默认主分片设置为1,解决over-sharding(集群中有太多的索引或者分片)的问题。
官方文档对分片大小的说明:www.elastic.co/guide/en/el…
以下创建blogs的索引,此处指定主分片数为3个,和2份副本(每个主分片拥有2个副本分片)。当集群中只有一个节点时,指定副本分片数,这些副本分片将无法分配到任何节点,这是因为同一个节点既保存原始数据,又保存副本数据是没有意义的。
PUT /blogs
{
"settings" : {
"number_of_shards" : 2,
"number_of_replicas" : 2
}
}
在三个节点下,分片分布的情况如下:P为主节点,R为副本节点
集群健康状态
- green:所有主分片和副本分片都正常运行
- yellow:所有主分片正常运行,但部分副本分片没有正常运行
- red:部分主分片没有正常运行
集群路由
路由一个文档到分片中,默认根据以下规则:
shard = hash(routing) % number_of_primary_shards
- Hash算法可以确保文档均匀分布到不同的分片中。
- routing是一个可变值,默认情况是文档的_id,也可以设置成一个自定义的值(可以通过API的routing参数,设置路由)。
- 这也解释为什么创建索引定好的主分片数量不能修改,因为主分片数量修改的话,之前路由的值都会无效,文档再也找不到了。
分布式操作
新建、删除文档
假设Node1为协调节点,并且Client刚好请求到它。
对于更新、删除需要说明一点的是ES不是在原来的记录上进行操作,而是先将旧的文档标记为删除,然后插入一个新的文档。在Merge操作的时候,会真正把删除的文档从物理上删除。后文会再次提到。
- 客户端向
Node 1
发送新建/删除请求。 - 节点使用文档的_id,确定文档属于分片0。请求将会被转发到
Node 3
,因为分片0的主分片目前被分配在Node 3
上。 Node 3
在主分片上执行请求,如果成功了,它将请求并发转发到Node 1
和Node 2
的副本分片上。一旦所有的副本分片都报告成功,Node 3
向Node 1
报告成功,Node 1
向客户端响应成功。在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完了,但这个牺牲了性能。
检索
搜索是一个更加复杂的操作,因为不知道查询会命中哪些文档。一个搜索请求必须对索引所有的分片,来确认他们是否含有任何匹配的文档。找到所有匹配的文档仅仅完成事情的一半,多分配的结果必须组合成单个排序列表。这个过程也称为query then fetch
。
Query
查询会到索引的每个分片(主分片或者副本分片)。每个分片在本地执行搜索并构建一个匹配文档的优先队列。优先队列是一个存有top-n匹配文档的有序列表。优先队列的大小取决于分页参数的from和size。如:from=90,size=10,那么优先队列大小为100。
查询包含以下三个阶段:
- 客户端发送一个search请求到
Node 3
,Node 3
会创建一个大小为from +size的空优先队列。 Node 3
将查询请求转发到索引的每个主分片或者副本分片。每个分片在本地查询并添加结果到from + size的本地有序优先队列中。- 每个分片返回各自优先队列中所有文档的ID和排序值,给
Node 3
,它合并这些值到自己的优先队列来产生一个全局排序后的结果队列。
Fetch
Node 3
辨别出哪些文档需要被取回,并向相关分片提交多个GET请求(multi-get请求)。- 每个分片加载文档,如果有需要的话,会把文档返回给
Node 3
。 - 一旦所有的文档都被取回,
Node 3
会把结果返回给客户端。
Node 3
首先要决定哪些文档需要被取回,如指定了from=90,size=10,那么最初的90个结果会被丢弃,只有从第91个开始的10个结果需要被取回,而这些结果可能分布在不同的分片上。
深度分页
从上述流程可以看出:每个分片必须创建from + size长度的队列返回给协调节点,而协调节点需要根据 number_of_shards * (from + size)排序文档,来找到被包含在size里的文档。
如果使用较大的from值,排序过程可能会变得非常沉重,使用大量的CPU、内存和带宽。所以,最好不要使用深度分页。
此外ES限制了查询最大的数量为10000,可以通过index.max_result_window
对索引进行设置修改。
解决方式:scroll,适用一次性获取较大的数据集、search_after:根据上一页的最后一条数据,来确定下一页的位置。
分页方式 | 性能 | 优点 | 缺点 | 场景 |
---|---|---|---|---|
form+size | 差 | 灵活、简答 | 深度分页 | 数据量小,能容忍深度分页 |
scroll | 中 | 解决深度分页 | 无法反应数据实时性 | 数据导出 |
search_after | 好 | 解决深度分页,且能查实时数据 | 实现复杂,需要有一个全局唯一字段;无法实现跳页或者实现会很负责 | 海量数据分页 |
参考资料
分片内部原理
倒排索引不可变性
-
倒排索引采用Imutable Design,一旦生成,不可更改。
-
不可变性,有以下好处:
- 无需考虑并发写文件问题,避免锁机制带来的性能问题。
- 一旦读入内核的文件系统缓存,便可以长时间缓存。只要文件系统有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升性能。
- 缓存容易生成和维护,数据也可以被压缩。
-
不可变性,也会有一些弊端:如果需要让一个新的文档可以被搜索,需要重建整个索引。
Lucene Index
在Lucene中,单个倒排索引文件被称为Segment,Segment是不可变更的。多个Segment汇总在一起,称为Lucene的Index(其实就是ES的分片)。所以一个分片内部会有多个Segment。
当有新文档写入时,会生成新的Segment(Refresh操作后),查询时会同时查询所有Segment,并且对结果汇总。Lucene中有一个文件,用来记录所有的Segment信息,叫做Commit Point
删除的文档信息,保存在.del
文件中,所以ES的更新索引,是记录删除的信息,再写入一条记录,查询时会通过.del
文件过滤处理。当然也会有Segment的合并,合并的时候会把删除的信息做一次整理。
Refresh
ES文档的写入,会先写在In-memory Buffer中,在In-memory Buffer中的数据时无法被搜索的。将In-memory Buffer数据写入到Segment的过程叫Refresh。Refresh的操作基本上不会执行fsync操作,也就是还没有落盘,只是写到了文件系统的缓存中。
Refreash触发:
- 默认1秒发生一次,可以通过index.refresh_interval配置,Refresh后,数据就可以被搜索到。这也是ES被称为近实时搜索的原因,请求的时候可以携带_refresh参数,但这并不是一个好的选择。
- In-memory Buffer被占满时,也会出发Refresh。In-memory Buffer的默认值,时JVM的10%。
如果系统有大量的数据写入,那么就会产生很多的Segment。
Refresh操作示意图如下:In-memory buffer清空,写入到一个新的Segment,但是这个Segment还没有落盘。
Transaction Log
transaction log 被称为translog
为了保证数据不丢失,数据在往In-memory Buffer写入的时候,同时还会写Translog。每个分片有一个Translog。高版本开始(比如ES 7.0),Translog默认每次都落盘。对于Translog落盘的行为,可以通过Index Setting进行设置修改。
- 默认情况下配置为
index.translog.durability:request
,每个请求都会调用fsync进行落盘。 index.translog.durability:async
、index.translog.sync_interval: 5s
:可以修改为异步落盘,每5s执行,sync_interval不能低于100ms。- 对于translog的大小也可以设置:
index.translog.flush_threshold_size:512mb
,默认是512MB
以下是写数据时,同时还会写Translog示意图:
在ES Refresh操作的时候,In-memory Buffer会被清空,但是Translog不会清空。
Flush
或称为Lucene Commit
会执行以下逻辑:
- 调用Refresh,In-memory Buffer清空,写入Segment
- 调用fsync,将缓存中的Segment写入到磁盘。
- 清空Translog(因为数据都已经落盘,所以这个数据没用了)
触发机制:
- 手动触发:针对blogs索引
POST /blogs/_flush
或者针对所有索引POST /_flush
- 默认30分钟调用一次。
- 当Translog满(默认512MB)的时候调用。
Flush操作示意图:
参考资料
Merge
从前面可以得到,Lucene内部就是不断创建Segment,如果频繁的生成Segment,就可能出现大量的Segment,对于搜索需要扫描更新的Segment,性能更低且耗费更多的内存。另外因为Segment是不可变的,所以更新/删除不能从物理上删除。Merge的时候,那些被标记为删除的文档不会合并到新的Segment中,可以减少合并后Segment的文档段。
Merge会消耗I/O和CPU,甚至可能会影响搜索性能。
ES内部会自动执行Merge,大致操作:会选择一小部分大小相似的段,并且在后台将它们合并成更大的段,这个过程仍然可以搜索。当新的段合并完成后,会进行落盘,然后新的段用于搜素,旧的段将被删除。(还是不明确具体merge的机制是什么,官方文档上没有明确阐述)。也可以手动Merge操作:POST my_index/_forcemerge
Merge是自动进行的,可以通过参数index.merge.scheduler.max_thread_count
控制一个分片的Merge执行线程。
参考资料:
Shrink
Shrink API,是ES5.0之后提供的新功能,其可以缩小分片数量。但并不对源索引直接进行缩小操作。而是使用与源索引相同配置创建一个新索引,仅降低分片数。新索引的主分片数必须是源索引分片数的因数。如,8个主分片可以缩小为4、2、1个分片,如果源分片数为素质,则目标分片数只能为1。
参考资料
- ES 7.17官方文档:www.elastic.co/guide/en/el…
- 《Elasticsearch权威指南》,基于2.x版本:www.elastic.co/guide/cn/el…
- 极客时间-《Elasticsearch核心技术与实战》
- ES相关技术文章:learn.lianglianglee.com/%e4%b8%93%e…
- learnku.com/articles/40…
- blog.csdn.net/qq_27529917…