Elasticsearch 数据写入涉及多个步骤过程,以此来确保数据能够高效、安全地存储在集群中。本文将带你探索 Elasticsearch 对数据的操作流程。
一、Index 流程
创建索引时,一个 Document 文档需要先经过路由规则定位到主 Shard,发送这个 Doc 到主 Shard 上建索引,成功后在发送这个 Doc 到这个 Shard 的副本上建索引,等副本上建索引成功后才返回成功。
那 Doc 是经过什么样的路由规则定位到 Shard 的呢?下面看一下路由规则。
路由规则
写入数据,数据应该保存在哪个主分片呢?有个计算叫路由Hash:hash(id) % 主分片数量,对主键进行hash计算,在模主分片数。
写入数据时,在连接集群前是没有路由规则的,集群的信息还不知道。连接时可能连接任意机器,然后经过路由计算,协调节点将数据发送到正确的机器上进行数据写入,主分片写完后,在写入副本分片,然后才给用户返回写入成功。这样会影响性能,可以设置参数来设置数据一致性的程度,如 consistency 参数设置为 one,只要主分片状态 OK 就允许执行写入成功;设置为 all,必须主分片和所有副本分片的状态都没问题才执行成功;设置为 quorum,即大多数的分片副本状态为 OK,即为执行成功,默认为 quorum。
查询数据,不一定从主分片查,还能从副本查询,那查询时如何确定从哪查呢?上图中的分片和副本数量可以保证数据在任何机器上都有,所有查询时查询数据时,查询哪台机器都能查询的到。这叫做分片控制,什么叫分片控制:用户可以访问任何一个节点都能获取数据,这个节点称之为协调节点,这个节点可能会很忙,会把请求转发到另外的节点进行查询。一般的策略是轮询。
写入流程
在每一个Shard中,写入分为两部分,先写入Lucene,在写入TransLog。
写入请求到达 Shard 后,先写 Lucene 文件,创建好索引,此时索引还在内存里面,接着去写 TransLog,写完 TransLog 后,刷新 TransLog 数据到磁盘上,写磁盘成功后,请求返回给用户。这里有几个关键点,一是和数据库不同,数据库是先写 CommitLog,然后再写内存,而Elasticsearch 是先写内存,最后才写 TransLog,这样的原因:
- Lucene 的内存写入会有很复杂的逻辑,很容易失败,比如分词,字段长度超过限制等,比较重,为了避免 TransLog 中有大量无效记录,减少 recover 的复杂度和提高速度,所以就把写 Lucene 放在了最前面。
- 写 Lucene 内存后,并不是可被搜索的,需要通过Refresh把内存的对象转成完整的Segment 后,然后再次 reopen 后才能被搜索,一般这个时间设置为1秒钟,导致写入Elasticsearch 的文档,最快要1秒钟才可被从搜索到,所以 Elasticsearch 在搜索方面是NRT(Near Real Time)近实时的系统。
- 当 Elasticsearch 作为 NoSQL 数据库时,查询方式是 GetById,这种查询可以直接从TransLog 中查询,这时候就成了 RT(Real Time)实时系统。
- 每隔一段比较长的时间,比如30分钟后,Lucene 会把内存中生成的新 Segment 刷新到磁盘上,刷新后索引文件已经持久化了,历史的 TransLog 就没用了,会清空掉旧的TransLog。
在创建索引时,应考虑合适的主分片数(primary shards)和副本分片数(replicas)。一旦索引创建完成,其分片数量就不可更改。
Elasticsearch提供了近实时搜索能力,但并非强一致性的系统。对于需要立即反映最新写入状态的应用场景,需要了解其最终一致性的特点。
二、更新流程
更新数据时会有并发问题,同一条数据两请求同时更新,那到底以哪个为准呢?
Elasticsearch 采用了乐观锁。Elasticsearch 是分布式的,当文档创建、更新或删除时,新版本的文档必须复制到集群中的其他节点。Elasticsearch 也是异步和并发的。这意味着这些复制请求被并行发送,并且到达目的地时也许顺序是乱序的。Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新版本。
每个文档都有一个 _version(版本号),当文档被修改时版本号递增。Elasticsearch 使用这个version 来确保变更正确的顺序执行。如果旧版本的数据在新版本之后到达,可以被简单的忽略。
Lucene 不支持部分字段的 update,所以需要再 Elasticsearch 中实现该功能,流程如下:
- 客户端通过 Elasticsearch REST API 发送一个更新请求到集群中的任意节点,收到 update 请求后,从 Segment 或者 TransLog 中读取 id 完整的Doc,记录版本号V1。
- 将版本 V1 的全量 Doc 和请求中的部分字段 Doc 合并为一个完整的 Doc,同时更新内存中的 versionMap,获取到完整 Doc 后,update 请求就变成了 Index 请求。
- 加锁。
- 再次从 versionMap 中读取该 id 最大版本号 V2,如果 versionMap 中没有,则从 Segment或 TransLog 中读取,这里基本都会从 versionMap 中获取。
- 检查版本是否冲突(V1==V2),如果冲突则回退到开始”Update Doc“阶段,重新执行。如果不冲突,则执行罪行的add请求。
- 在 Index Doc 阶段,首先将 version + 1 得到 V3,再将 Doc 加入到 Lucene 中去,Lucene中会先删除同id下已经存在的 doc id,然后再增加 doc。写入 Lucene 成功后,将当前 V3更新到 versionMap中。
- 释放锁,部分更新流程结束。
- 更新操作完成后,主分片会将更新同步给相应的副本分片,副本分片同样完成更新并确认。
更新时注意事项
- 不推荐直接替换整个文档来进行更新,而是尽可能地使用部分更新或脚本来修改特定字段。这有助于减少磁盘I/O和内存消耗,同时保持索引大小更小。
- 批量更新:如果需要更新大量文档,应考虑使用批量更新操作(如
_bulk
API),它能够显著提高性能并减少网络开销。不过要注意批量操作也有其限制,比如单次请求的大小以及系统资源消耗。 - 并发更新策略:在高并发场景下,可能出现频繁的版本冲突。可以通过设置合理的重试策略(如
retry_on_conflict
参数)来处理此类问题。
正确理解和运用Elasticsearch的数据更新机制对于保证数据的一致性和系统的稳定性至关重要。同时,针对不同场景采取合适的更新策略和最佳实践,有助于提升集群整体性能。
总之,在Elasticsearch中操作数据时,要熟悉其 RESTful API,合理运用各种查询和更新机制,并根据实际需求采取合适的优化策略。