在分布式系统中,容错(fault tolerence)一直都是系统的主要设计目标,ElasticSearch既是分布式查询系统也是分布式存储系统。今天我们来分析和梳理一下写入流程容错策略
client
client向ES发起写入请求
coordinating node
- TransportAction执行this.action.doExecute如果抛出RuntimeException,则将异常作为参数触发listener,listener将异常结果返回给client
- TransportBulkException.doExecute执行中遇到非预期的request例如时间戳被配置了,则抛出IllegalArgumentException
- 当写入索引不存在异步创建索引时,如果索引创建失败且原因不是索引已经存在,则suppressed Exception将结果放到responses列表中,继续执行bulk因为可能部分request的索引存在或创建成功了
- 当遍历处理请求时,可能遇到预期内的失败,比如RoutingMissingException或者request配置有问题,则将Exception将结果放到responses列表
- 遍历index request发送到各个数据节点时,如果有异常会保存到responses列表,请求间独立
- 有个主要的重试机制实现方法是向clusterApplierService注册一个listener,当集群状态变更时,再次执行ReroutePhase#doRun。如果超过超时时间(默认1min)则返回失败给listener。有以下几种情况需要重试:
- 执行请求发送时,如果遇到cluster block将调度重试,因为cluster可能会恢复
- 如果缺失primary shard/node,将异步等待primary可用直到超时
- 请求primary收到response为ConnectTransportException/NodeClosedException/(primary&(RetryOnPrimaryException | isShardNotAvailableException |isRetryableClusterBlockException))
primary data node
- AsyncPrimaryAction是AbstractRunnable的子类,运行过程中抛出Exception就会触发onFailure将结果返回给client
- 由于写入过程中集群状态可能发生变化,例如当前节点不再是primary shard所在节点、请求来自之前的primary term或者allocation id不同,这些情况会中止写入
- 在primary换届(relocation / replica promotion会触发primary换届)的过程中可能有一致性问题
- 我们需要确保只有在旧primary中的所有写入都已完全复制(其中包括primary和replica)后,我们才开始将数据写入新primary。 这可确保新的primary在正确的doc version上运行。
写入primary时,doc version会增加,然后在副本上使用,以确保较新的文档不会被较旧的文档覆盖(在存在并发复制的情况下)。
一种场景是:1primary 2replica。将 id“K”和值“X”的文档写入旧primary(获取版本 1)并将其复制到新primary和replica。
假设在新primary获得具有值“X”的“K”的复制写入之前,在新主数据库上写入 ID 为“K”但值为“Y”的另一个文档。 在不知道其他写入的情况下,它会将相同的版本号(即 1)分配给值为“Y”的文档并将其复制到replica。
根据来自旧primary和新primary的复制写入到达副本的顺序,副本将存储“X”或“Y”,这意味着新主服务器和副本可能会变得不同步(一致)。 - 我们必须确保一旦开始写入新的primary,就不会在旧的primary上进行新的写入操作。 这有助于以下场景。
假设主节点relocation完成并且主节点广播集群状态,该状态现在仅包含新的主节点。 由于 Elasticsearch 的分布式特性,集群状态不会在所有节点上完全同步应用。
在短时间内,集群中的节点对于哪个节点是主节点有不同的看法。 特别是,持有旧primary的节点(节点 A)可能仍然认为是主节点,而持有新primary的节点(节点 B)也认为是主节点。 如果我们将文档发送到节点 B,它将被索引到新的主节点并得到确认(但不会存在于旧的主节点上)。
如果我们随后向节点 A 发出同一文档的删除请求(如果我们向节点round-robin发送请求,则可能会发生这种情况),那么该节点将无法在其旧primary中找到该文档,从而导致请求失败。
解决方法:
在完成relocation之前,节点 A(持有primary的relocation source)停用对其本地分片的写入(并临时将该分片的所有新传入请求放入队列中),然后等待所有正在进行的操作完全replicated。
完成后,它将所有新传入请求委托给节点 B(持有新的primary的relocation target),并将队列中的所有请求发送到那里。 它使用特殊操作将请求委托给节点 B,该操作在接受请求时绕过标准reroute phase,因为标准reroute phase基于节点上的当前集群状态。
此时,直接发送到节点 B 的索引请求仍将被重新路由回具有旧主节点的节点 A。 这意味着节点 A 仍然负责索引,但将使用节点 B 上的物理分片副本来执行此操作。 节点 B 最后请求主节点激活新的主节点(完成重定位)。
然后,主节点广播一个新的集群状态,其中节点 A 上的旧主节点被删除,而节点 B 上的新主节点处于active。 现在集群状态应用到节点 A 和 B 上的顺序并不重要:
- 如果集群状态首先应用在节点 B 上,则两个节点都会将其索引请求发送到节点 B 上的分片副本。
- 如果集群状态首先应用在节点 A 上,则对节点 A 的请求将被重新路由到节点 B,对节点 B 的请求将被重新路由到节点 A(因为B认为A是主节点)。
为了防止在节点 A 和节点 B 上的集群状态期间发生重定向循环 ,#16274 使得当节点B的cluster state版本小于节点A时,来自节点 A 的请求在节点 B 上等待,直到 B 更新集群状态
- ES对index使用的内存有限制,如果超过阈值会将使用内存最大的分片写入限制为单线程,但7.9.1版本有bug导致策略没生效。新版本更改选择要限制分片的策略,改为round-robin,官方测试性能更好
- 对于一些写入时的异常,可能是因为engine有问题,所以es会关闭engine,并且请求master更新集群状态将primary换掉
- primary 同步数据到replica时有重试机制,遇到CircuitBreakingException EsRejectedExecutionException ConnectTransportException会进行重试,前两个exception可能意味着replica暂时忙
- 如果写入replica失败会请求master更新cluster state,包括将replica剔除in sync id、分片状态从start -> unassigned、更新routing table
replica node
- replica会校验primary term和allocation id,保证不是旧primary的请求或者分片和当前replica分片状态没有变化,如果primary发送请求后replica掉线落后一些数据有回到集群那么allocation id会变化。
allocation id是一个分片的唯一标识,allocation时进行分配,集群重启时可以查看 in sync列表中的allocation id找到分片副本,如果用shard id,可能有很多数据不全的shard - 如果部分数据在primary写入失败了,也不会在replica进行写入,保证数据一致性
总结
- ES写入流程中对于一些重试可能成功操作会进行重试,例如primary写入replica遇到因replica负载过高导致的EsRejectedExecutionException
- ES写入流程中对于一些依赖集群状态的操作,当当前状态不符合预期时会尝试等待集群状态更新再重试,例如primary处于未分配状态
- ES写入流程中对于一些确定的分片不可用的情况会请求master节点更新分片状态使得后续的操作可能成功,例如primary写入过程失败的话将请求master更新集群状态,如果有replica master将把它提升为primary,这样后续的写操作路由到新节点将成功