Elasticsearch知识整理,基于ES6.7.1 主要涉及ES集群各模块功能及使用说明,集群选举流程 读取/写入流程等等,不涉及dsl等具体操作,后面会持续更新!
需要完整文件 到我 公众号 ‘Lucene Elasticsearch 工作积累’ 回复 ‘es基础’ 就可以了!
如果喜欢搜索技术请来我的公众号吧 ‘Lucene Elasticsearch 工作积累’
每天会持续更新搜索相关技术
以下是脑图中的具体内存大纲。
集群节点角色
主节点
Master node )
维护集群状态。集群状态信息,只由Master节点进行维护,并且同步到集群中所有节点,其他节点只负责接收从Master同步过来的集群信息而没有维护的权利。集群状态包括以下信息:
集群层面的配置
集群内有哪些节点
各索引的设置,映射,分析器和别名等
索引内各分片所在的节点位置
【思维拓展】ES集群中的每个节点都会存储集群状态,知道索引内各分片所在的节点位置,因此在整个集群中的任意节点都可以知道一条数据该往哪个节点分片上存储。反之也知道该去哪个分片读。所以,Elasticsearch不需要将读写请求发送到Master节点,任何节点都可以作为数据读写的切入点对请求进行响应。这样进一步减轻了Master节点的网络压力,同时提高了集群的整体路由性能。
数据节点
( Data node)
负责保存数据、执行数据相关操作: CRUD、搜索、聚合 等。数据节点对 CPU、内存、 I/O 要求较高。一般情况下(有 一些例外,后续章节会给出),数据读写流程只和数据节点交互,不 会和主节点打交道(异常情况除外)。
通过配置 node.data : true (默认)来使一个节点成为数据节点,也可以通过下面的配置 创 建一个数据节点 :
node.master : false
node.data: true
node.ingest: false
预处理节点
( Ingest node)
这是从 5.0 版本开始引入的概念。预处理操作允许在索引文档之前,即写入数据之前,通 过事先定义好的一系列的 processors (处理器) 和 pipeline (管道〉,对数据进行某种转换 、 富化 。 processors和 pipeline拦截 bulk{日 index请求 ,在应用 相 关操作后将文档 传回 给 index或 bulkAPI。
默认情况下, 在所有的节点上启用 ingest,如果想在某个节点上禁用 ingest,则可以添加配 置 node.ingest: false,也可以通过下面的配置创建一个仅用于预处理的节点:
node.master : false
node . data : false
node.ingest : true
协调节点
(Coordinatingnode)
客户端请求可以发送到集群的任何节点,每个节点都知道任意文档所处的位置,然后转发
这些请求,收集数据井返回给客户端,处理客户端请求的节点称为协调节点。
协调节点将请求转发给保存数据的数据节点。每个数据节点在本地执行请求,并将结果返
回协调节点。协调节点收集完数据后,将每个数据节点的结果合井为单个全局结果。对结果收
集和排序的过程可能需要很多 CPU 和内 存资源。 通过下面的配置创建一个仅用于协调的节点:
node.master: false
node.data: false
node . ingest : false
部落节点
(Tribe node)
的bes (部落)功能允许部落节点在多个集群之间充当联合客户端。
在 ES 5.0 之前还有一个客户端节点( Node Client)的角色,客户端节点有 以下属性 :
node.master: false
node .data : false
它不做主节点,也不做数据节点,仅用于路由请求,本质上是-个智能负载均衡器(从负 载均衡器的定义来说,智能和非智能的区别在于是否知道访问的内容存在于哪个节点〉,从 5.0 版本开始,这个角色被协调节点( Coordinating only node)取代 。
集群状态
集群状态元数据是全局信息,元数据包括内容路由信息、配置信息等,其中 最重要 的是内 容路由信息,它描述了“哪个分片位于哪个节点”这种信息 。
集群状态由主节点负责维护,如果主节点从数据节点接收更新,则将这些更新广播到集群
的其他节点,让每个节点上的集群状态保持最新。 ES 2.0版本之后,更新的集群状态信息只发 增量 内容,并且是被压缩的 。
客户踹API
RESTAPI
是对原生REST接口的封装。 REST接口、 JavaRESTAPI使用9200端口通 信, 采用 JSONoverHTTP方式, JavaAPI使用 9300端口通信, 数据序列化为二进制。
使用JavaAPI理论上来说效率更高一些,但是后来官方发现实际上相差无几, 但是版本法代中却因为 Java API 向下兼容性的限制不得不做出许多牺牲, Java API 带来的微弱效率优势远 不及带来的缺点 。 ES 不是高 QPS 的应用,写操作非常消耗 CPU 资源,因此写操作属于比较长 的操作,聚合由于涉及数据 量 比较大,延迟也经常到秒级,查询 一般也不密集。因此 RPC 框架 的效率没有那么高的要求 。 后续 Java API 将逐渐被 Java 阻 ST API 取代 。 官方计划从 ES 7.0 开 始不建议使用 JavaAPI,并且从 8.0 版本开始完全移除 。
实现
JestHttpClient
String url = “http://172.18.130.89:9200/”;
JestClientFactory factory = new JestClientFactory();
factory.setHttpClientConfig(new HttpClientConfig.Builder(url).multiThreaded(true)
.connTimeout(connTimeout).readTimeout(readTimeout)
.maxTotalConnection(1600)
.defaultMaxTotalConnectionPerRoute(800)
.build());
clientES = (JestHttpClient) factory.getObject();
RestHighLevelClient
HttpHost host = new HttpHost(“xx.xx.xx.xx”, 9200);
RestClientBuilder builder = RestClient.builder(host);
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(“admin”, “***”));
builder.setHttpClientConfigCallback(f -> f.setDefaultCredentialsProvider(credentialsProvider));
clientES = new RestHighLevelClient(builder);
主要内部模块
Cluster
Cluster模块是主节点执行集群管理的封装实现,管理集群状态,维护集群层面的配置信息 。 主 要功能如下:
. 管理集群状态,将新生成的集群状态发布到集群所有节点 。
. 调用 allocation模块执行分片分配,决策哪些分片应该分配到哪个节点
. 在集群各节点中直接迁移分片,保持数据平衡 。
allocation
封装了分片分配相关的功能和策略,包括主分片的分配和副分片的分配,本模块由主节点 调用。创建新索引、集群完全重启都需要分片分配的过程。
Discovery
发现模块负责发现集群中的节点 ,以 及选举主节点。当节点加入或退出集群时,主节点会 采取相应的行动。从某种角度来说,发现模块起到类似 ZooKeep町的作用,选主并管理集群拓 扑。
gateway
负责对收到 Master 广播下来的集群状态( cluster state)数据的持久化存储,并在集群完全 重启 时恢复它们 。
Indices
索引模块管理全局级的索引设置,不包括索引级的(索引设置分为全局级和每个索引级)。 它还封装了索引数据恢复功能 。 集群启动阶段需要的主分片恢复和副分片恢复就是在这个模块 实现的 。
HTTP
模块允许通过 JSON over HTTP 的方式访 问 ES 的 APL HTTP 模块本质上是完全异 步的,这意味着没有阻塞线程等待响应。 使用异步通信进行 HTTP 的好处是解决了 CIOk 问题
。 Ok 量级 的并发连接) 。
在部分场景下,可考虑使用 HTTP keepalive 以提升性能。注意: 不要在客户端使用 HTTP
chunking 。
Transport
传输模块用于集群内节点之间的内部通信 。 从一个节点到另 -个节点的每个请求都使用传 输模块 。
如同 HTTP 模块 ,传输模块本质上也是完全异步的。
传输模块使用 TCP 通信,每个节点都与其他节点维持若干 TCP 长连接 。 内 部节点间的所
有通信都是本模块承载的。
Engine
Engine模块封装了对 Lucene 的操作及 translog 的调用,它是对一个分片读写操作的最终提 供者 。
Guice
ES使用 Guice框架进行模块化管理。 Guice是 Google开发的轻量级依赖注入框架 CioC)。 软件设计中经常说要依赖于抽象而不是具象 , IoC 就是这种理念的实现方式,并且在内部实现了对象的创建和管理。
集群启动流程
总体流程
Transport模块启动流程
节点之间通讯、节点与客户端之间通讯(包括java api和rest api)等所有网络数据传输默认都是transport模块实现的。
transport模块分为LocalTransport和NettyTransport两种,默认是第二种,底层用的是Netty:基于NIO的客户、服务器端编程框架
TransportRequest层有五种类型的请求,在节点之间的TCP长连接上传输,默认情况下,每个节点都会与集群的其他节点保持13个TCP连接,每个连接根据不同类型的业务用作固定的用途。这13个连接分别是:
recovery 数据恢复使用,默认:2
bulk 批量请求使用,默认:3
reg ,默认类型,如:请求加入集群,集群数据同步。默认:6
state ClusterState信息等,默认:1
ping ,用作nodeFD或masterFD的ping,默认:1
配置加载
从解析的配置文件中获取:
workerCount
工作线程数,serverBootstrap和clientBootstrap都会创建大小为workerCount的线程池,默认为CPU数量x2,ES5.1之后的版本 限制为最大32.因为在CPU核心数很多的机器上占用非常多内存。
connectionsPerNode
每种请求的连接数
receiveBufferSizePredictorFactory
确定接收缓冲大小。默认最大512k,根据配置的transport.netty.receive_predictor_min和transport.netty.receive_predictor_max确定使用固定大小还是动态大小
启动
根据配置(IP、端口、socket选项等)创建一个ClientBootstrap和若干个ServerBootstrap(都是Netty中的)。然后分别设置各自的PipelineFactory。
PipelineFactory是Netty中的概念,Netty中,Channel是通讯的载体(一个连接),ChannelHandle负责Channel中的处理逻辑,ChannlePipeline可以理解成ChannleHandler的容器,一个Channel包含一个ChannelPipeline,所有ChannelHandler都会注册到ChannelPipeline中。
通讯配置
节点之间的通讯
transport.profiles.default.port
transport.profiles.default.bind_host
client到节点直接的通讯
transport.profiles.client.port
transport.profiles.client.bind_host
自动发现数据通讯
transport.profiles.dmz.port
transport.profiles.dmz.bind_host
建立到集群其他节点的链接
TCP服务已监听,有哪些客户端会连接进来取决于运行时逻辑。
TCP客户端已准备好,本节点启动过程中,探测到集群的节点信息后, 调用
transportService.connectToNode()->NettyTransport.ConnectToNodes()
连接到其他node,一个连接就是一个Netty中的Channel:
nodeChannels = new NodeChannels(new Channel[connectionsPerNodeRecovery], new Channel[connectionsPerNodeBulk], new Channel[connectionsPerNodeReg], new Channel[connectionsPerNodeState], new Channel[connectionsPerNodePing]);
发送request
sendRequest封装了向节点发送数据的流程。首先根据目标节点和type(上面五种之一)选择一个连接(channle),根据选项看情况压缩数据,通过CompressorFactory.defaultCompressor().streamOutput创建一个带压缩的StreamOutput,写入version和action,写入request,转换成ChannelBuffer,调用targetChannel.write(buffer);发送数据
消息处理
如果某个模块需要监听收到的消息进行处理,他需要调用transportService.registerRequestHandler为不同的action注册不同的handle;如果一个模块要发送请求,他也需要提供一个handle准备处理Response。在NettyTransport类中,依据Netty的ChannelHandle处理机制,两个处理类被注册到Netty的ChannelPipeline中:
SizeHeaderFrameDecoder 负责处理消息头,检查非法请求,去掉头部数据
MessageChannelHandler 负责处理消息体,在messageReceived函数中判断是否需要解压,根据是否request调用handleRequest或handleResponse进行处理。
handleRequest过程:
从收到的数据中解析出action,根据action获取对应的 handle,handle在TransportService.requestHandlers中注册。交给对应的handle处理
handleResponse过程:
根据requestId获取handle,handle在TransportService.clientHandlers中注册。
加入集群流程
获取集群node列表
在UnicastZenPing构造函数中,向discovery.zen.ping.unicast.hosts配置的节点列表发送请求,获取到DiscoveryNode列表
findMaster
通过UnicastZenPing发送ping,从response信息中找到master,如果没有master,进入选主流程
selectMaster ->
选取主节点模块
↓
选主流程
1、需要参选结点数过半后确定临时Master,因为每个节点选主列表不一至(排序最最大),导致结果不一至
2、某节点被选为主,须判断加入他的节点数过半,才确认 master 身份。解决第一个问题。
3、master 节点,当探测到节点离开事件,须判断当前节点数是否过半。如果不到配置节点数,则放弃 master身份,重新加入集群。避免脑残问题(多区双主)
流程图
gateway ->
选举集群和
索引级别的元信息
↓
被选出的 Master 和集群元信息的新旧程度没有关系。因此他的第一个任务是选举元信息, 让各节点把其各自存储的元信息发过来,根据版本号确定最新的元信息,然后把这个信息广播下去,这样,集群的所有节点都有了最新的元信息。
参与选举的元信息数量需要过半,在选举过程中,不接受新节点的加入请求
元信息选举级别
集群级
索引级
allocation ->
选择shard级别的元信息
(或者说是分配分片)
↓
选举 shard 级元信息,构建路由表,初始阶段,所有的 shard 都处于UNASSIGNED状态。此时,首先要做的是分配主分片。
选主分片
他向集群的所有节点询问:大家把分片的元信息发给我。然后,Master 等待所有的请求返回回来,现在有了 shard的多份信息,取决于副本数设置了多少。现在考虑把谁做主分片。
es5以下的版本,通过对比shard 级元信息的版本号来决定。在多副本的情况下,考虑到如果只有一个shard 信息汇报上来,那他一定会被选为主分片,但也许数据不是最新的,版本号比他大的那个 shard 所在节点还没启动。
es5开始实施一种新的策略:给每个 shard 都设置一个 UUID,然后在集群级的元信息记录,哪个 shard 是最新的,因为 es 是先写主分片,再由主分片节点转发请求去写副分片,所以主分片所在节点肯定是最新的,如果他转发失败了,就要求 Master 删除那个节点。所以,es5中,主分片是通过集群级元信息中记录的最新主分片的列表来确定主分片的:汇报信息中存在,并且这个表中也存在。
“cluster.routing.allocation.enable”: “none”
只要磁盘可分配主分片的 shard,会强制分配主分片。因此,在设置了上述选项的情况下,集群重启后的状态为 yellow,而非 red。
选副分片
主分片选举完成后,从上一个过程汇总的 shard 信息中选择一个副本做副分片(如果是配置一个副本的情况下)。如果汇总信息中不存在,分配一个全新副本的操作,最后,allocation 过程中允许新启动的节点加入集群。
流程图
构建路游表
每个索引分片都分配完成后 构建路游表
获取连接_cluster/state
结构
routing_table:{
indices:{
索引名:{
shards:[{
0:{
state: “STARTED”,
primary: false,
node: “72RHSya-T5itjxXQg2_6zg”,
relocating_node: null,
shard: 0,
index: “dkbs_v1”,
allocation_id: {
id: “f7qLIR5ZTo6eWGPuDJ3IRg”
}
}
1:{}
n:{}
}]
}
}
}
recovery 恢复索引
数据恢复或者叫做数据重新分布。当有节点加入或退出时时它会根据机器的负载对索引分片进行重新分配,当挂掉的节点再次重新启动的时候也会进行数据恢复。
1. 如果某个主分片在,而复制分片所在的节点挂掉了,那么master需要另行选择一个可用节点,将这个主分片的复制分片分配到可用节点上,然后进行主从分片的数据复制。
2.如果某个主分片所在的节点挂掉了,复制分片还在,那么master会主导将复制分片升级为主分片,然后再做主从分片数据复制。
3.如果某个分片的主副分片都挂掉了,则暂时无法恢复,而是要等持有相关数据的节点重新加入集群后,master才能主持数据恢复相关操作。
主分片 recovery
主分片来说,这些未刷盘数据可以从 translog 恢复,每次刷盘完毕,translog 都会被清空,因此把 translog 中的数据全部重放,建立 lucene 索引,如此完成主分片的 recovery,主分片recovery基本都是在集群重启后进行。
副分片 recovery
第一阶段:把 shard 数据拷贝到副本节点。如果主副两 shard 有相同的 syncid 且 doc 数相同,
则跳过这个阶段。在第一阶段完毕前,会向副分片阶段发送告知对方启动 engine,在第二阶段开始之前,副分片就可以正常处理写请求了。
第二阶段:对 translog 做快照,这个快照里包含从第一个节点开始的新增索引,发送所有的 translog operation 到对端节点,不限速。
ES 2.0前:
phase1:将主分片的 lucene做快照,发送到 target。期间不阻塞索引操作,新增数据写到主分片的 translog
phase2:将主分片 translog 做快照,发送到 target 重放,期间不阻塞索引操作。
phase3:为主分片加写锁,将剩余的translog 发送到 target。此时数据量很小,写入过程的阻塞很短。
从第一阶段开始,就要阻止 lucene 执行commit 操作,避免 translog 被刷盘后清除。
本质上来说,只要流程上允许将写操作阻塞一段时间,实现主副一致是比较容易的
ES 2.0~ES5.0:
为了避免这种做法产生过大的 translog,translog 模块被修改为支持多个文件,同时引入 translog.view 的概念,创建 view 可以获取到后续的所有操作。这样实现了在第一阶段允许 lucene commit。
phase3被删除。对于如何做到主副一致的,描述的很模糊。分析完相关代码后,整理流程如下:
先创建一个 Translog.view,然后
phase1:将主分片的 lucene 做快照,发送到 target。期间允许索引操作和 flush 操作。发送完毕后,告知 target 启动 engine,phase2开始之前,新的索引操作都会转发副分片正常执行。
phase2:将主分片的 translog 做快照,发送到 target 去重放。
写流程中做异常处理,通过版本号过滤掉过期操作来。写操作有三种类型:索引新文档,更新,删除。索引新文档不存在冲突问题,更新和删除操作采用相同的处理机制。每个操作都有一个版本号,这个版本号就是预期 doc 版本,他必须大于当前 lucene 中的 doc 版本号。否则就放弃本次操作。对于更新操作来说,预期版本号是 lucene doc 版本号+1。主分片节点写成功后新数据的版本号会放到写副本的请求中,这个请求中的版本号就是预期版本号。这样,时序上存在错误的操作被忽略,对于特定 doc,只有最新一次操作生效,保证了主副分片一致
ES6.0后:
translog.view 被 移除。 引入 TranslogDeletionPolicy 的概念,负责维护 活跃(liveness)的translog文件。这个类的实现非常简单,它将 translog做一个快照来保持 translog 不被清理。
在保证 translog 不被清理后,恢复核 心处理过程 由两个内 部阶段( phase) 组成 。
两个phase1/phase2阶段不变,
由于 phase1 需要通过网络复制大量数据,过程非常漫长,在 ES 6.x 中,有两个机会可以跳
过 phase1:恢复时通过对比两个序列号,计算出缺失的数据范围,然后通过translog重放这部分数据,同时 translog 会为此保留更长的时间.
(1)如果可以基于恢复请求中的 SequenceNumber(globalcheckpoint和 local checkpoint)进行恢复,则跳过 phase!。
(2)如果主副两分片有相同的 syncid 且 doc 数相同,则跳过 phase!。
触发条件包括:从快照备份恢复,节点加入和离开,索引的_open操作等
recovery 慢的原因分析
最慢的过程在于副本分片恢复的第一阶段,各节点单独执行分段合并逻辑,合并后的分段基本不会相同,所以拷贝 lucene 分段是最耗时的,其中有一些相关的限速配置:
cluster.routing.allocation.node_concurrent_recoveries 单个节点最大并发进/出 recovery 数,默认2
indices.recovery.max_bytes_per_sec 默认40m
indices.recovery.concurrent_streams 单个节点恢复时可以打开的网络流数量,默认3
即使关闭限速,这个阶段仍然可能非常漫长,目前最好的方式就是先执行 synced flush, 但是 syncd flush 并且本身也可能比较慢,因为我们常常为了优化写入速度而加大 translog 刷盘周期,也会延长 translog 恢复阶段时间
在 es 6.0中再次优化这个问题,思路是给每次写入成功的操作都分配一个序号,通过对比序号就可以计算出差异范围,在实现方式上, 添加了global checkpoint 和 local checkpoint,checkpoint,主分片负责维护global checkpoint,代表所有分片都已写入到了这个序号的位置,local checkpoint代表当前分片已写入成功的最新位置,恢复时通过对比两个序列号,计算出缺失的数据范围,然后通过translog重放这部分数据,同时 translog 会为此保留更长的时间.
synced flush 机制
es 为了解决副本分片恢复过程第一阶段的漫长过程引入synced flush,默认情况下5分钟没有写入操作的索引被标记为inactive,执行 synced flush,生成一个唯一的 syncid,写入到所有 shard, 这个 syncid是shard 级,拥有相同syncid的 shard具有相同的 lucene 索引.
synced flush的实现思路是先执行普通的 flush 操作,各分片 flush 成功后,他们理应有相同的 lucene 索引内容,无论分段是否一致.于是给大家分配一个 id, 表示数据一致.但是显然 synced flush 期间不能有新写入的内容,对于这种情况, es 的处理是:让 synced flush 失败,让写操作成功.在没有执行 flush 的情况下已有 syncid 不会失效.当某个 shard 上执行了普通 flush 操作会删除已有 syncid,因此,synced flush操作是一个不可靠操作,只适用于冷索引.
recovery 状态的命令
curl localhost:9200/{index}/_stats?level=shards&pretty
curl localhost:9200/{index}/_recovery?pretty&human&detailed=true
curl localhost:9200/_cat/recovery
集群启动曰志
节点Node
启动流程
主线程执行的启动流程大概做了三部分工作:加载配置、检查外部环境和内部环境、初始化内部资源。最后启动各个子模块和keepalive线程。
org.elasticsearch.bootstrap.Elasticsearch: 启动入口,main方法就在这个类里面,执行逻辑对应图中绿色部分
org.elasticsearch.bootstrap.Bootstrap:包含主要启动流程代码,执行逻辑对应图中红色部分
org.elasticsearch.node.Node:代表集群中的节点,执行逻辑对应图中蓝色部分
流程
启动入口
解析配置:
包括命令行参数、主配置文件,log配置文件
1.main方法
2.设置了一个空的SecurityManager:用于捕获错误信息和安全检查并在控制台打印,这个功能比较靠前。
3.注册ShutdownHook,用于关闭系统时捕获IOException到terminal
4.进入Command类的mainWithoutErrorHandling方法,final OptionSet options = parser.parse(args);//根据提供给解析器的选项规范解析给定的命令行参数 args 启动传进来的参数。
5.进入EnvironmentAwareCommand的execute方法,确保home/data/logs给定的设置存在,如果尚未设置,则从系统属性中读取它。-Des.home 中获取。
6.进入InternalSettingsPreparer的prepareEnvironment方法,读取elasticsearch.yml并创建Environment。
7.判断是否有-v参数,没有则准备进入init流程,如果有 -v 参数,打印版本号后直接退出,获取daemonize,pidFile,queite, Environment配置传入init方法进行初始化。
8.调用Bootstrap.init
启动流程
启动keepAive线程,初始化运行环境:
主线程执行完启动流程后会退出,keepalive线程是唯一的用户线程,作用是保持进程运行,java程序当没有用户线程是进程会退出。
JVM检测:
JVM运行模式检查:client、server,ES对效率要求较高,建议使用server模式,否则打印警告;
串行收集检查
串行收集器(serial collector)适合单逻辑 CPU 的机器或非常小的堆,不适合 ES。使用串 行收集器对 ES有非常大的负面影响。本项检查就是确保没有使用串行收集器。ES默认使用 CMS 收集器 。
G1GC 中金查
JDK8 的早期版本有些问题,会导致索引损坏, JDK8u40之前的版本都会受影响。本项检 查验证是否是早期的 HotspotJVM版本。
JVM版本的检查:如果是Oracle,检查版本对应JVM_OPTS必要参数,如果是IBM,版本要求2.8以上。
系统相关初始化
是否root权限启动:可以调整配置文件运行以root权限启动,但是不建议.
是否使用seccomp模式,是否启用mlockall:避免使用swap,建议开启
内部初始化:
初始化两种probes:ProcessProbe和OsProbe,提供进程和OS层面的信息
添加shutdown hook,当JVM关闭时调用node.close
jar hell检测:类重复校验
构建node实例:根据设置构建node
ModulesBuilder modules = new ModulesBuilder();
modules.add(new XXXModule(this.settings));
进入Boostrap.start
9.实例化Boostrap。保持keepAliveThread存活,用于监控,并添加到shutdownHook用于关闭KeepAliveThread。
10.加载elasticsearch.keystore文件,重新创建Environment,然后调用LogConfigurator的静态方法configure,读取config目录下log4j2.properties然后配log4j属性。
11.创建pid文件,检查lucene版本,不对应则抛出异常。
12.设置ElasticsearchUncaughtExceptionHandler用于打印fatal日志(设置未捕获异常处理程序)
13.进入Boostrap.setup
14.spawner.spawnNativePluginControllers(environment);尝试为给定模块生成控制器(native Controller)守护程序。 生成的进程将通过其stdin,stdout和stderr流保持与此JVM的连接,但对此包之外的代码不能使用对这些流的引用。
15.初始化本地资源 initializeNatives():检查用户是否作为根用户运行,是的话抛异常;系统调用和mlockAll检查;尝试设置最大线程数,最大虚拟内存,最大FD等。
初始化探针initializeProbes(),用于操作系统,进程,jvm的监控。
16.又加一个ShutdownHook(添加IOUtil/logs/config)资源关闭。
17.检查jar hell 打印ifconfig配置Security信息。
18.实例化Node,重写validateNodeBeforeAcceptingRequests方法。具体主要包括三部分,第一是启动插件服务(es提供了插件功能来进行扩展功能,这也是它的一个亮点),加载需要的插件,第二是配置node环境,最后就是通过guice加载各个模块。下面22~32就是具体步骤。
19.进入Boostrap.start
20.node.start启动节点
21.keepAliveThread.start
集群节点
22.Node实例化第一步,创建NodeEnvironment
23.生成nodeId,打印nodeId,nodeName和jvmInfo和进程信息
24.创建 PluginsService 对象,创建过程中会读取并加载所有的模块和插件
25.又创建Environment
26.创建ThreadPool,然后给DeprecationLogger设置ThreadContext
27.创建NodeClient,用于执行actions
28.创建各个Service:ResourceWatcherService、NetworkService、ClusterService、IngestService、ClusterInfoService、UsageService、MonitorService、CircuitBreakerService、MetaStateService、IndicesService、MetaDataIndexUpgradeService、TemplateUpgradeService、TransportService、ResponseCollectorService、SearchTransportService、NodeService、SearchService、PersistentTasksClusterService
29.创建并添加modules:ScriptModule、AnalysisModule、SettingsModule、pluginModule、ClusterModule、IndicesModule、SearchModule、GatewayModule、RepositoriesModule、ActionModule、NetworkModule、DiscoveryModule
30.Guice绑定和注入对象
31.初始化NodeClient
32.初始化rest处理器,这个非常重要,后面会专门讲解
33.修改状态为State.STARTED
34.启动pluginLifecycleComponents
35.通过 injector 获取各个类的对象,调用 start() 方法启动(实际进入各个类的中 doStart 方法): LifecycleComponent、IndicesService、IndicesClusterStateService、SnapshotsService、SnapshotShardsService、RoutingService、SearchService、MonitorService、NodeConnectionsService、ResourceWatcherService、GatewayService、Discovery、TransportService
36.启动HttpServerTransport和TransportService并绑定端口
启动命令
bin/elasticsearch 脚本:
ec \ #执行命令
”$JAVA” \ #Java程序路径
KaTeX parse error: Expected 'EOF', got '#' at position 16: ES JAVA OPTS \ #̲JVM 选项 -Des.pat…ES HOME” #设置 path.home 路径
-Des . path.conf=”KaTeX parse error: Expected 'EOF', got '#' at position 17: …S_PATH_CONF" \ #̲设直 path.conf 路径…ES_CLASSPATH” #设置 java classpath
org.elasticsearch .bootstrap .Elasticsearch \ #指定 main 函数所在类 ”$@" #传递给main 函数命令行参数
ES_JAVA OPTS 变量保存了 NM 参数,其内容来自对 config/jvm.options配置文件的解析。 如果执行启动脚本时添加了 -d 参数: bin/elasticsearch -d
参数:
节点关闭
ES 进程会捕获 SIGTERM 信号( kill 命令默认信号)进行处理,调用各模块的 stop 方法, 让它们有机会停止服务,安全退出。
进程重启期间,如果主节点被关闭,则集群会重新选主,在这期间,集群有一个短暂的无
主状态。如果集群中的主节点是单独部署的,则新主当选后,可以跳过 gateway 和 recovery 流 程,否则新主需要重新分配旧主所持有的分片: 提升其他副本为主分片,以及分配新的副分片。
如果数据节点被关闭,则读写请求的 TCP 连接也会因此关闭,对客户端来说写操作执行失 败。但写流程已经到达 Engine 环节的会正常写完,只是客户端无法感知结果。此时客户端重试, 如果使用自动生成 ID,则数据内容会重复。
关闭流程分析
在节点启动过程中, Bootstrap#setup 方法中添加了 shutdown hook , 当进程收到系统
SIGTERM (kill 命令默认信号)或 SIGINT 信号时,调用 Node#close 方法,执行节点关闭流程。
各模块的关闭顺序关系
关闭快照和 HTTPServer,不再响应用户 REST 请求 。
关闭集群拓扑管理,不再响应 ping 请求。
关 闭网络模块,让节点 离线。
执行各个插件的关闭流程 。
关闭 IndicesService。
最后 才 关 闭 IndicesService, 是 因为 这期 间 需要等待释放的资源最多 , 时间最长。
关闭顺序:
分片读写过程中执行关闭
写入过程中关闭 : 线程在写入数据时,会对 Engine 加写锁。 IndicesService 的 doStop 方法 对本节点 上全部索引并行执行 removeIndex, 当执行到 Engine 的 flushAndClose (先 flush 然后关 闭 Engine), 也会对 Engine 加写锁。由于写入操作已经加了写锁,此时写锁会等待,直到写入 执行完毕。因此数据写入过程不会被中断 。 但是由于网络模块被关闭,客户端的连接会被断开 。 客户端应当作为失败处理,虽然 ES服务端的写流程还在继续。
读取过程中关闭: 线程在读取数据时,会对 Engine加读锁。 flushAndClose时的写锁会等待 读取过程执行完毕。但是由于连接被关闭,无法发送给客户端,导致客户端读失败 。
节点关闭过程中, IndicesService的doStop对Engine设置了超时, 如果flushAndClose一直 等待 ,则 CountDownLatch.await 默认 l 天才会继续后面的流程。
主节点被关闭
主节点被关闭时, 没有想象中的特殊处理, 节点正常执行关闭流程,当 TransportService模
块被关闭后 , 集群重新选举新 Master。因 此,滚动重启期间会有 一段 时间处于无主状态 。
选主
为什么使用 Master
Elasticsearch的典型场景中的另一个简化是集群中没有那么多节点。 通常,节点的数量远远小于单个节点能够维护的连接数,并且网格环境不必经常处理节点加入和离开。 这就是为什么领导者的做法更适合Elasticsearch。
选主算法
Bully算法:它假定所有节点都有一个惟一的ID,选取最大ID节点,当原master恢复也不会成为master,通过推迟选举直到当前的 Master 失效。
触发选主
1、集群启动,从无主状态到产生新主时
2、集群在正常运行过程中,Master探测到节点离开时(NodesFaultDetection)
3、集群在正常运行过程中,非Master节点探测到Master离开时(MasterFaultDetection)
注意,即使一个节点认为 Master 失效也会进入选主流程
Master扩容是否会触发选主?只扩容Master节点不会触发选主,只要当前设置的法定人数不变,ES集群就认为自己的选举是合法的。
相关配置
防止脑裂、防止数据丢失的极其重要的参数:
discovery.zen.minimum_master_nodes=(master_eligible_nodes)/2+1
这个参数的实际作用早已超越了其表面的含义,会用于至少以下多个重要时机的判断:
1、触发选主:进入选举临时的Master之前,参选的节点数需要达到法定人数。
2、决定Master:选出临时的Master之后,得票数需要达到法定人数,才确认选主成功。
3、gateway选举元信息:向有Master资格的节点发起请求,获取元数据,获取的响应数量必须达到法定人数,也就是参与元信息选举的节点数。
4、Master发布集群状态:成功向节点发布集群状态信息的数量要达到法定人数。
5、NodesFaultDetection事件中是否触发rejoin:当发现有节点连不上时,会执行removeNode。接着审视此时的法定人数是否达标(discovery.zen.minimum_master_nodes),不达标就主动放弃Master身份执行rejoin以避免脑裂。
6、Master减容场景:缩容与扩容是完全相反的流程,需要先缩减Master节点,再把quorum数降低。修改Master以及集群相关的配置一定要非常谨慎!配置错误很有可能导致脑裂,甚至数据写坏、数据丢失等场景。
7、Master扩容场景:目前有3个master_eligible_nodes,可以配置quorum为2。如果将master_eligible_nodes扩容到4个,那么quorum就要提高到3。此时需要先把discovery.zen.minimum_master_nodes配置设置为3,再扩容Master节点。这个配置可以动态设置:
PUT /_cluster/settings
{
“persistent”: {
“discovery.zen.minimum_master_nodes”: 3
}
}
构建Master节点列表
流程分析
选举临时Master,如果本节点当选,等待确立Master,如果其他节点当选,等待加入Master,然后启动节点失效探测器。
选举临时Master流程
(1)“ping”所有节点,获取节点列表fullPingResponses,ping结果不 包含本节点,把本节点单独添加到fullPingResponses中。
(2)构建两个列表:
activeMasters列表:存储集群当前活跃Master列表。遍历第一步获 取的所有节点,将每个节点所认为的当前Master节点加入activeMasters 列表中(不包括本节点),这个过程是将集群当前已存在的Master加入activeMasters列表,正常情况下只有一个,考虑到异常情况下,可能各个节点看到的当前Master不同
masterCandidates列表:存储master候选者列表。遍历第一步获取列 表,去掉不具备Master资格的节点,添加到这个列表中。
在遍历过程中,跳过不具备Master资格,配置如下:
discovery.zen.master_election.ignore_non_master_pings 为 true(默认为 false)
(3)如果activeMasters为空,则从masterCandidates中选举,结果可 能选举成功,也可能选举失败。如果不为空,则从activeMasters中选择 最合适的作为Master。
投票
发送投票就是发送加入集群(JoinRequest)请求。
得票就 是申请加入该节点的请求的数量。 收集投票,进行统计,这里的投票就是加入它的连接数,
当节点检查收到的投票是否足够时,就是检查加入它的连接数是否足够, 其中会去掉没有Master资格节点的投票。
确认master
临时Master为本地节点
1、等待足够多的具备Master资格的节点加入本节点(投票达到 法定人数),以完成选举。
2、超时(默认为30秒,可配置)后还没有满足数量的join请求, 则选举失败,需要进行新一轮选举。
3、成功后发布新的clusterState。
临时Master为非本地节点
1、不再接受其他节点的join请求。
2、向Master发送加入请求,并等待回复。超时时间默认为1分钟 (可配置),如果遇到异常,则默认重试3次(可配置)。
3、最终当选的Master会先发布集群状态,才确认客户的join请求,并且已经收到了集群状态。本步骤检查收到的集群状态中的Master节点如果为 空,或者当选的Master不是之前选择的节点,则重新选举。
流程概述
1. 每个节点计算最低的已知节点ID(新版本以集群状态版本号高的节点放在 前面如果相同时,节点的Id越小,优先级越高。),
并向该节点发送领导投票。
2. 如果一个节点收到足够多的票数,并且该节点也为自己投票,那么它将扮演领导者的角色,开始发布集群状态。
3. 所有节点都会参数选举,并参与投票,但是,只有有资格成为 master 的节点的投票才有效(node.master为true)。
(就像皇上只在皇子中选一样。)
4.选举临时Master,如果本节点当选,等待确立Master,如果其他节点当选,等待加入Master,然后启动节点失效探测器。
节点失效检测
选主流程已执行完毕,Master身份已确认,非Master节 点已加入集群。
节点失效检测会监控节点是否离线,然后处理其中的异常,不执行失效检测可能会产生脑裂 (双主或多主)。
NodesFaultDetection和MasterFaultDetection都是通过定期(默认为1秒)发送的ping请求探测节点是否正常的,当失败达到一定次数(默认 为3次),或者收到来自底层连接模块的节点离线通知时,开始处理节点离开事件。
在Master节点
启动NodesFaultDetection,简称NodesFD。定期探 测加入集群的节点是否活跃。
检查当前集群总节点数是否达到法定节点数(过半),如果不 足,则会放弃 Master 身份,重新加入集群,从而避免产生双主问题。
在非Master节点
启动MasterFaultDetection,简称MasterFD。定期探 测Master节点是否活跃。
探测Master离线的处理很简单,重新加入集群。本质上就是该节点 重新执行一遍选主的流程。
相关配置
discovery. zen.ping.un工cast.hosts: 集群的种子节点列表, 构建集群时本节点会尝试 连接这个节点列表,那么列表中的主机会看到整个集群中都有哪些主机。 可以配置为部分或全 部集群节点。
discovery.zen.ping.unicast.hosts.resolve timeout: DNS 解析超时时间, 默认为 5 秒。
discovery. ze口. j oin_timeout: 节点加入现有集群时的超时时间,默认为 ping_timeout 的 20 倍。
discovery.zen.]oin retry attemptsjoin_timeout:超时之后的重试次数,默认为3次。
discovery.zen.join_retry_delayjoin_timeout: 超时之后,重试前的延迟时间,默认为100 毫秒。
discove ry . zen .master election . ignore non master p工ngs: 设置为 true 时, 选主阶
段将忽略来自不具备 Master资格节点(node.master: false) 的 ping请求, 默认为 false。
discovery. zen. fd.ping_iinterval: 故障检测间隔周期, 默认为 l 秒。
discovery.zen.fd.ping_t工meout:故障检测请求超时时间,默认为 30秒。
discovery.zen.fd.ping retries:故障检测超时 后的重试次数,默认为 3 次。
写入过程
Elasticsearch写操作,是先在主分片执行成功后,转发请求到其他副本分片进行处理,所有分片执行成功,返回响应给主分片,主分片拿到结果,返回客户端。可以通过wait_for_active_shards参数指定需要确认的分片数,默认为1,即主分片写入成功就返回结果(假设该参数为3,但只有主分片可用,可以观察到,客户端会被阻塞)。
流程
1、客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)。
协调节点流程
1、参数检查 如有以下异常则拒绝请求
收到用户请求后首先检测请求的合法性,有问题就直接拒绝。
index 不可为空
type 不可为空
source 不可为空
contentType 不可为空
opType 当前操作类型为创建索引,则校验 VersionType必须为internal,且Version不为MATCH_DELETED
resolvedVersion 校验解析的Version是否合法
VersionType 不可为FORCE类型,此类型已废弃
id 非空时,长度不可大于512,为空时对versionType和resolvedVersion进行检查。
2、处理pipeline请求
数据预处理(ingest)工作通过定义pipeline和processors实现。pipeline是一系列processors的定义,processors按照声明的顺序执行。如果Index或Bulk请求中指定了pipeline参数,则先使用相应的pipeline进行处理。
3、自动创建索引
如果配置为允许自动创建索引(默认允许),则计算请求中涉及的索引,可能有多个,其中有哪些索引是不存在的,然后创建它。如果部分索引创建失败,则涉及创建失败索引的请求被标记为失败。其他索引正常执行写流程。
主要检查:
1.是否存在该索引或别名(存在则无法创建);
2.该索引是否被允许自动创建(二次检查,为了防止check信息丢失);
3.动态mapping是否被禁用(如果被禁用,则无法创建);
4.创建索引的匹配规则是否存在并可以正常匹配(如果表达式非空,且该索引无法匹配上,则无法创建
创建索引请求被发送到Master节点,待收到全部创建请求的Response(无论成功还是失败的)之后,才进入下一个流程。
Master节点什么时候返回Response?在Master节点执行完创建索引流程,将新的clusterState发布完毕才会返回。
那什么才算发布完毕呢?默认情况下,Master发布clusterState的Request收到半数以上的节点Response,认为发
布成功。负责写数据的节点会先执行一遍内容路由的过程以处理没有收到最新clusterState的情况。
4、对请求的预先处理
由于上一步可能有创建索引操作,所以在此先获取最新集群状态信息。然后遍历所有请求,从集群状态中获取对应索引的元信息,检查mapping、routing、id等信息。如果id不存在,则生成一个UUID作为文档id。
5、检测集群状态
协调节点在开始处理时会先检测集群状态,若集群异常则取消写入。例如,Master节点不存在,会阻塞等待Master节点直至超时。因此索引为Red时,如果Master节点存在,则数据可以写到正常shard,Master节点不存在,协调节点会阻塞等待或取消写入。
6、内容路由,构建基于shard的请求将用户的 bulkRequest 重新组织为基于 shard 的请求列表。例如,原始用户请求可能有10个写操作,如果这些文档的主分片都属于同一个,则写请求被合并为1个。所以这里本质上是合并请求的过程。此处尚未确定主分片节点。
判断是否为本节点,如果不是,则转发请求到对应节点,如果该分片在当前节点,则继续执行,如果分片在当前节点,task当前阶段置为“waiting_on_primary”,否则为“rerouted”,两者都走到同一入口,即performAction(…)。
2、由协调节点将写入请求
发给相关shard主节点
主分片写入流程
1、检查请求
检测要写的是否是主分片,AllocationId是否符合预期,索引是否处于关闭状态等。
AllocationId是区分不同分片的唯一标识,集群级的元数据中记录了一个被认为是最新shard的AllocationIID集合,称之为in-sync AllocationI IDs,也就是说,这里的检查请求,1是通过AllocationId检查该shard是否为指定主分片,二是检查该主分片是否在in-sync AllocationI IDs集合中。
2、是否延迟执行
判断请求是否需要延迟执行,如果需要延迟则放入队列,否则继续下面的流程。
3、判断主分片是否已经发生迁移
如果已经发生迁移,则转发请求到迁移的节点。在高并发下,有概率在该主分片接收到请求时发生故障,已经发生角色迁移。
4、检测写一致性
在开始写之前,检测本次写操作涉及的shard,活跃shard数量是否足够,不足则不执行写入。默认为1,只要主分片可用就执行写入。
5、元数据处理
Index or Update or Delete
循环执行每个Single Write Request,对于每个Request,根据操作类型(CREATE/INDEX/UPDATE/DELETE)选择不同的处理逻辑。其中,Create/Index是直接新增Doc,Delete是直接根据_id删除Doc
ranslate Update To Index or Delete
这一步是Update操作的特有步骤,在这里,会将Update请求转换为Index或者Delete请求。首先,会通过GetRequest查询到已经存在的同_id Doc(如果有)的完整字段和值(依赖_source字段),然后和请求中的Doc合并。同时,这里会获取到读到的Doc版本号,记做V1。
Parse Doc
这里会解析Doc中各个字段。生成ParsedDocument对象,同时会生成uid Term。在Elasticsearch中,_uid = type # _id,对用户,_Id可见,而Elasticsearch中存储的是_uid。这一部分生成的ParsedDocument中也有Elasticsearch的系统字段,大部分会根据当前内容填充,部分未知的会在后面继续填充ParsedDocument。
Update Mapping
Elasticsearch中有个自动更新Mapping的功能,就在这一步生效。会先挑选出Mapping中未包含的新Field,然后判断是否运行自动更新Mapping,如果允许,则更新Mapping。
Get Sequence Id and Version
由于当前是Primary Shard,则会从SequenceNumber Service获取一个sequenceID和Version。SequenceID在Shard级别每次递增1,SequenceID在写入Doc成功后,会用来初始化LocalCheckpoint。Version则是根据当前Doc的最大Version递增1。LocalCheckpoint存储的是恢复点。
Checkpoint,分为LocalCheckpoint和GlobalCheckpoint,checkpoint是在6.x加入的概念,即保存每次操作的最新位置,LocalCheckpoint保存当前分片执行操作的结果位置(我现在执行到了哪个操作),GlobalCheckpoint保存全局操作位置(我成功执行到了哪个操作,与主分片的GlobalCheckpoint是否一致),用于保证各节点与主分片节点的操作一致(如果我的GlobalCheckpoint和LocalCheckpoint不一致,且比主分片的小,说明我的操作有丢失,需要再执行一次操作)。主分片在每次操作完后,先更新LocalCheckpoint,更新成功后,如果LocalCheckpoint比GlobalCheckpoint大,说明本次操作是追加的,需要更新主分片上的GlobalCheckpoint;请求转发到副本分片的节点上,同样,副本分片在执行操作后更新LocalCheckpoint;在ES6.7之前,副本分片的GlobalCheckpoint是在下一次请求过来时,再检查当前LocalCheckpoint与主分片的GlobalCheckpoint是否一致,如果一致,说明操作正常,将副本分片的GlobalCheckpoint更新至LocalCheckpoint;而在6.7之后,副本分片的GlobalCheckpoint更新放在了当前请求结束之后,不会等待下一次请求到来时再更新(这样做的好处,是可以在数据恢复时,减少要恢复的操作;假设副本分片A的GlobalCheckpoint当前为2,但是已经执行到了5,且是正常的,如果这个时候开始恢复,之前版本会从3开始,将3~5全部执行一遍,而6.7之后,不会做重复操作)。
6、Lucene和translog处理
写Lucene和事务日志
遍历请求,处理动态更新字段映射,然后调用InternalEngine#index逐条对doc进行索引。Engine封装了Lucene和translog的调用,对外提供读写接口。
在写入Lucene之前,先生成Sequence Number和Version,Sequence Number每次递增1,Version根据当前doc的最大版本加1。
索引过程为先写Lucene,后写translog
flush translog
根据配置的translog flush策略进行刷盘控制,定时或立即刷盘。
写副分片
现在已经为要写的副本shard准备了一个列表,循环处理每个shard,跳过unassigned状态的shard,向目标节点发送请求,等待响应。这个过程是异步并行的。
转发请求时会将SequenceID、PrimaryTerm、GlobalCheckPoint、version等传递给副分片。
在等待Response的过程中,本节点发出了多少个Request,就要等待多少个Response。无论这些Response是成功的还是失败的,直到超时。
收集到全部的Response后,执行finish()。给协调节点返回消息,告知其哪些成功、哪些失败了。
处理副分片写失败情况
Primary会执行一些重试逻辑,尽可能保障Replica中写入成功,如果一个Replica最终写入失败,
主分片所在节点将发送一个shardFailed请求给Master。然后Master会更新集群状态,在新的集群状态中,这个shard将:
从in_sync_allocations列表中删除;
在routing_table的shard列表中将state由STARTED更改为UNASSIGNED;
添加到routingNodes的unassignedShards列表。
在请求转发副本分片时,会通过监听器监听副本操作的结果,成功则更新checkpoint构造响应等,不多说;失败时,可以看到会出现一些常见的问题(此处只对代码流程简单说明,不再跟踪具体代码)。
副本写入失败,会向master发送一个内部请求internal:cluster/shard/failure,master接收到该请求,会根据shardId和allocationId去匹配,如果匹配关系不成立,说明主分片节点的routingTable不对,会更新clusterStats,生成StaleShard;如果匹配关系成立,说明路由信息正确,但分片无法写入,会生成FailedShard,FailedShard处理过程就是上图代码流程,生成UnassignedInfo,并将分片加到unassigned,该分片重试了5次后就一直处于unassigned了。
遇到的问题总结一下:1.重启时写入不中断,shard failed日志刷屏,task大量堆积,原因即为副本写入失败,不断在发送请求,需要master更新ClusterStats,即上述StaleShard,较早的版本对于task唯一性决定有误,即相同的请求的task不一致,导致task数据量异常,而新版本修改逻辑后,仍然是单个分片的task唯一,仍会出现该问题,只是进行了改善;2. 回合真实内存熔断后,如果内存不足,会出现分片不断处于初始化过程,即上述的FailedShard,原因即为副本写入失败,会被移到unassigned队列中去,社区答复为,es需要确保主副分片写入一致,因此做了这个操作。
3、副分片写入流程
1、Index or Delete
根据请求类型是Index还是Delete,选择不同的执行逻辑。这里没有Update,是因为在Primary Node中已经将Update转换成了Index或Delete请求了。
2、Parse Doc
3、Update Mapping
同主分片
4、Get Sequence Id and Version
Primary Node中会生成Sequence ID和Version,然后放入ReplicaRequest中,这里只需要从Request中获取到就行。
5、Add Doc To Lucene
由于已经在Primary Node中将部分Update请求转换成了Index或Delete请求,这里只需要处理Index和Delete两种请求,不再需要处理Update请求了。比Primary Node会更简单一些。
6、Write Translog
7、Flush Translog
同主分片
8、Doc写入流程
数据写入buffer缓冲和translog日志文件
每隔一秒钟,buffer中的数据被写入新的segment file,并进入os cache,此时segment被打开并供search使用。
buffer被清空。
重复1~3,新的segment不断添加,buffer不断被清空,而translog中的数据不断累加
当translog长度达到一定程度的时候,commit操作发生。
buffer中的所有数据写入一个新的segment,并写入os cache,打开供使用
buffer被清空
一个commit ponit被写入磁盘,标明了所有的index segment
filesystem cache中的所有index segment file缓存数据,被fsync强行刷到磁盘上
现有的translog被清空,创建一个新的translog
流程图
相关配置
wait_for_active_shards:参数(可在Index的setting、请求中设置)可保证Es集群数据一致性更高的可能性。该参数设置每次写入shard至少具有的active副本数,如果副本数小于参数值,此时不允许写入,默认为1。
refresh
refresh调用的就是Lucene的flush,刷新后重启打开searcherManager.maybeRefresh(),打开一个新search用于检索。
默认refresh每秒执行一次,将内存中的数据刷到操作系统缓存,生成一个新的 segment 的过程。
当多线程调用 IndexWriter执行写入时,IndexWriter会为每个线程分配一个 DocumentsWriterPerThread对象,简称 DWPT,每个 DWPT内部包括一个 buffer,这个 buffer最终会 flush 为单独的 segment 文件。看到这里,你已经能够想到为什么会产生很多小 segment,这与客户在单个分片上执行的写入并发量有关。
整个IndexWriter所有的DWPT 中 buffer 使用量达到阈值,es 中使用这种方式,阈值根据 indexing buffer 来计算,默认为堆内存的10%,则对IndexWriter中buffer 最大的 DWPT 标记为等待 flush。
在 es 周期性执行 refresh,或手工触发 refresh 的时候,也不会阻塞bulk写入,es 的refresh最终调用到 lucene 的flushAllThreads()实现,这个 flush 过程会先调用flushControl.markForFullFlush();将所有的 DWPT 标记为flushPending状态(等待 flush),然后将这些 DWPT 添加到fullFlushBuffer和flushQueue两个列表,后面对 DWPT 执行 flush 操作的时候直接从flushQueue列表里取。
flush
es的flush调用lucene的commit方法,把操作系统缓存段合并刷入到磁盘中。
Lucene IndexWriter 对并发写入的支持
查询过程
搜索方式
GET/MGET(批量GET):
需要指定_index、_type、_id。也就是根据id从正排索引中获取内容。
Search:
Search不指定_id,根据关键词从哪个倒排索引中获取内容
GET详细分析
GET/MGET流程涉及两个节点:协调节点和数据节点。
对于Get类请求,查询的时候是先查询内存中的TransLog,如果找到就立即返回,如果没找到再查询磁盘上的TransLog,如果还没有则再去查询磁盘上的Segment。这种查询是实时(Real Time)的。这种查询顺序可以保证查询到的Doc是最新版本的Doc,这个功能也是为了保证NoSQL场景下的实时性要求。
协调节点
内容路由
1、获取集群状态,主要是获取分片状态,red、yelllow、green。
2、计算目标shard列表,根据内容路由算法,计算目标shardId,也就是文档应该落在哪个分片中。
3、计算出目标shardId后,结合请求参数中指定的优先级和集群状态确定目标节点,由于分片可能存在多个副本,因此计算出的是一个列表。
当查询的时候,从三个节点中根据Request中的preference参数选择一个节点查询。preference可以设置_local,_primary,_replica以及其他选项。如果设置了其他参数,那么可能会查询到R1或者R2。
转发请求
1、先检查是否为本地节点。
2、如果为本地节点,则直接执行获取数据。
3、如果非本地节点,异步发送请求(sendRequest),并等待回复。
4、如果数据节点执行成功,则返回数据给客户端;如果执行失败,则进行重试,将请求发送给另一个副本节点。
数据节点
1、接收来自协调节点的请求,执行messageReceived。
2、根据参数判断是否需要执行refresh,然后调用indexShard.getService().get()读取数据并存储到GetResult中。
读取和过滤数据
1、调用indexShard.get获取Engine.GetResult读取数据,底层会调用InternalEngine.get方法。
2、通过ShardGetService#innerGetLoadFromStroredFields,通过type、id等参数对指定的field source进行过滤,结果存于GetResult中。
Search流程
粗粒度来理解search流程,可以分为这三步:
1、所有分片参与搜索。
2、协调节点合并结果。
3、根据上一步的文档id获取文档内容。
Search Type
query and fetch
向索引的所有分片 ( shard)都发出查询请求, 各分片返回的时候把元素文档 ( document)和计算后的排名信息一起返回,在协调节点不进行二次排序。
优点:速度快。
缺点:数据量大,不排序。
query then fetch(默认)
第一步, 先向所有的 shard 发出请求, 各分片只返回文档 id(注意, 不包括文档 document)和排名相关的信息(也就是文档对应的分值), 然后按照各分片返回的文档的分数进行重新排序和排名, 取前 size 个文档。
第二步, 根据文档 id 去相关的 shard 取 document。 这种方式返回的 document 数量与用户要求的大小是相等的。
优点:返回的数据量是准确的。
缺点:性能一般,并且数据排名不准确。
DFS query and fetch
这种方式比第一种方式多了一个 DFS 步骤,因为写入的时候通过_routing保证Doc分布均匀,但是没法保证TF和DF均匀,那么就有会导致局部的TF和DF不准的情况出现,这个时候基于TF、DF的算分就不准,所以会先收集所有Shard中的TF和DF值,然后将这些值带入请求中,再次执行query_then_fetch,这样算分的时候TF和DF就是准确的,其实就是IDF不准。
优点:数据排名准确
缺点:性能一般;返回的数据量不准确, 可能返回(N*分片数量)的数据
DFS query then fetch
与DFS query and fetch不同之处是分两个阶段,查询和获取数据,其它没一至。
优点:返回的数据量是准确的;数据排名准确
缺点:性能最差【 这个最差只是表示在这四种查询方式中性能最慢, 也不至于不能忍受,
如果对查询性能要求不是非常高, 而对查询准确度要求比较高的时候可以考虑这个】
Client Node
注册Action
Elasticsearch中,查询和写操作一样都是在ActionModule.java中注册入口处理函数的。
如果请求是Rest请求,则会在RestSearchAction中解析请求,检查查询类型,不能设置为dfs_query_and_fetch或者query_and_fetch,这两个目前只能用于Elasticsearch中的优化场景,然后将请求发给后面的TransportSearchAction处理。然后构造SearchRequest,将请求发送给TransportSearchAction处理。
1. Get Remove Cluster Shard
判断是否需要跨集群访问,如果需要,则获取到要访问的Shard列表。
2. Get Search Shard Iterator
获取当前Cluster中要访问的Shard,和上一步中的Remove Cluster Shard合并,构建出最终要访问的完整Shard列表。
这一步中,会根据Request请求中的参数从Primary Node和多个Replica Node中选择出一个要访问的Shard。
3. For Every Shard:Perform
遍历每个Shard,对每个Shard执行后面逻辑。
4. Send Request To Query Shard
将查询阶段请求发送给相应的Shard。
5. Merge Docs
上一步将请求发送给多个Shard后,这一步就是异步等待返回结果,然后对结果合并。这里的合并策略是维护一个Top N大小的优先级队列,每当收到一个shard的返回,就把结果放入优先级队列做一次排序,直到所有的Shard都返回。
6. Send Request To Fetch Shard
选出Top N个Doc ID后发送给这些Doc ID所在的Shard执行Fetch Phase,最后会返回Top N的Doc的内容。
Query Phase
第一阶段查询的步骤:
1. Create Search Context
创建Search Context,之后Search过程中的所有中间状态都会存在Context中,这些状态总共有50多个,具体可以查看DefaultSearchContext或者其他SearchContext的子类。
2. Parse Query
解析Query的Source,将结果存入Search Context。这里会根据请求中Query类型的不同创建不同的Query对象,比如TermQuery、FuzzyQuery等,最终真正执行TermQuery、FuzzyQuery等语义的地方是在Lucene中。
这里包括了dfsPhase、queryPhase和fetchPhase三个阶段的preProcess部分,只有queryPhase的preProcess中有执行逻辑,其他两个都是空逻辑,执行完preProcess后,所有需要的参数都会设置完成。
由于Elasticsearch中有些请求之间是相互关联的,并非独立的,比如scroll请求,所以这里同时会设置Context的生命周期。
同时会设置lowLevelCancellation是否打开,这个参数是集群级别配置,同时也能动态开关,打开后会在后面执行时做更多的检测,检测是否需要停止后续逻辑直接返回。
3. Get From Cache
判断请求是否允许被Cache,如果允许,则检查Cache中是否已经有结果,如果有则直接读取Cache,如果没有则继续执行后续步骤,执行完后,再将结果加入Cache。
4. Add Collectors
Collector主要目标是收集查询结果,实现排序,对自定义结果集过滤和收集等。这一步会增加多个Collectors,多个Collector组成一个List。
FilteredCollector:先判断请求中是否有Post Filter,Post Filter用于Search,Agg等结束后再次对结果做Filter,希望Filter不影响Agg结果。如果有Post Filter则创建一个FilteredCollector,加入Collector List中。
PluginInMultiCollector:判断请求中是否制定了自定义的一些Collector,如果有,则创建后加入Collector List。
MinimumScoreCollector:判断请求中是否制定了最小分数阈值,如果指定了,则创建MinimumScoreCollector加入Collector List中,在后续收集结果时,会过滤掉得分小于最小分数的Doc。
EarlyTerminatingCollector:判断请求中是否提前结束Doc的Seek,如果是则创建EarlyTerminatingCollector,加入Collector List中。在后续Seek和收集Doc的过程中,当Seek的Doc数达到Early Terminating后会停止Seek后续倒排链。
CancellableCollector:判断当前操作是否可以被中断结束,比如是否已经超时等,如果是会抛出一个TaskCancelledException异常。该功能一般用来提前结束较长的查询请求,可以用来保护系统。
EarlyTerminatingSortingCollector:如果Index是排序的,那么可以提前结束对倒排链的Seek,相当于在一个排序递减链表上返回最大的N个值,只需要直接返回前N个值就可以了。这个Collector会加到Collector List的头部。EarlyTerminatingSorting和EarlyTerminating的区别是,EarlyTerminatingSorting是一种对结果无损伤的优化,而EarlyTerminating是有损的,人为掐断执行的优化。
TopDocsCollector:这个是最核心的Top N结果选择器,会加入到Collector List的头部。TopScoreDocCollector和TopFieldCollector都是TopDocsCollector的子类,TopScoreDocCollector会按照固定的方式算分,排序会按照分数+doc id的方式排列,如果多个doc的分数一样,先选择doc id小的文档。而TopFieldCollector则是根据用户指定的Field的值排序。
5. lucene::search
这一步会调用Lucene中IndexSearch的search接口,执行真正的搜索逻辑。每个Shard中会有多个Segment,每个Segment对应一个LeafReaderContext,这里会遍历每个Segment,到每个Segment中去Search结果,然后计算分数。
搜索里面一般有两阶段算分,第一阶段是在这里算的,会对每个Seek到的Doc都计算分数,为了减少CPU消耗,一般是算一个基本分数。这一阶段完成后,会有个排序。然后在第二阶段,再对Top 的结果做一次二阶段算分,在二阶段算分的时候会考虑更多的因子。二阶段算分在后续操作中。
6. rescore
根据Request中是否包含rescore配置决定是否进行二阶段排序,如果有则执行二阶段算分逻辑,会考虑更多的算分因子。二阶段算分也是一种计算机中常见的多层设计,是一种资源消耗和效率的折中。
Elasticsearch中支持配置多个Rescore,这些rescore逻辑会顺序遍历执行。每个rescore内部会先按照请求参数window选择出Top window的doc,然后对这些doc排序,排完后再合并回原有的Top 结果顺序中。
7. suggest::execute()
如果有推荐请求,则在这里执行推荐请求。如果请求中只包含了推荐的部分,则很多地方可以优化。推荐不是今天的重点,这里就不介绍了,后面有机会再介绍。
8. aggregation::execute()
如果含有聚合统计请求,则在这里执行。Elasticsearch中的aggregate的处理逻辑也类似于Search,通过多个Collector来实现。在Client Node中也需要对aggregation做合并。aggregate逻辑更复杂一些,就不在这里赘述了,后面有需要就再单独开文章介绍。
上述逻辑都执行完成后,如果当前查询请求只需要查询一个Shard,那么会直接在当前Node执行Fetch Phase。
Fetch Phase
Fetch阶段的目的是通过DocID获取到用户需要的完整Doc内容。这些内容包括了DocValues,Store,Source,Script和Highlight等,具体的功能点是在SearchModule中注册的,系统默认注册的有:
ExplainFetchSubPhase
DocValueFieldsFetchSubPhase
ScriptFieldsFetchSubPhase
FetchSourceSubPhase
VersionFetchSubPhase
MatchedQueriesFetchSubPhase
HighlightPhase
ParentFieldSubFetchPhase
除了系统默认的8种外,还有通过插件的形式注册自定义的功能,这些SubPhase中最重要的是Source和Highlight,Source是加载原文,Highlight是计算高亮显示的内容片断。
上述多个SubPhase会针对每个Doc顺序执行,可能会产生多次的随机IO,这里会有一些优化方案,但是都是针对特定场景的,不具有通用性。
Fetch Phase执行完后,整个查询流程就结束了。
分布式搜索过程
一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配,它们可能是主分片,也可能是副分片。也就是说,一次搜索请求只会命中所有分片副本中的一个。
协调节点流程
两阶段相应的实现位置:
查询(Query)阶段:search.InitialSearchPhase;
取回(Fetch)阶段:search.FetchSearchPhase。
Query阶段
在初始查询阶段,查询会广播到索引中每一个分片副本(主分片或副分片)。每个分片在本地执行搜索并构建一个匹配文档的优先队列。
QUERY_THEN_FETCH搜索类型的查询阶段步骤如下:
1、客户端发送search请求到NODE 3。NODE3会解析请求,会将本集群shard列表和远程集群shard列表合并。
2、Node 3将遍历所有shard,将请求转发到索引的每个主分片或副分片中。
3、每个分片在本地执行查询,并使用本地的Term/Document Frequency信息进行打分,添加结果到大小为from +size的本地有序优先队列中。
4、每个分片返回各自优先队列中所有文档的ID和排序值给协调节点。协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。
协调节点广播查询请求到所有相关分片时,可以是主分片或副分片,协调节点将在之后的请求中轮询所有的分片副本来分摊负载。
查询阶段并不会对搜索请求的内容进行解析,无论搜索什么内容,只看本次搜索需要命中哪些shard,然后针对每个特定shard选择一个副本,转发搜索请求。
Fetch阶段
Query阶段知道了要取哪些数据,但是并没有取具体的数据,这就是Fetch阶段要做的。
Fetch阶段相当于GET
协调节点向相关NODE发送GET请求。
分片所在节点向协调节点返回数据。
协调节点等待所有文档被取得,然后返回给客户端。
片所在节点在返回文档数据时,处理有可能出现的_source字段和高亮参数。
协调节点首先决定哪些文档“确实”需要被取回,例如,如果查询指定了{ "from": 90, "size":10 },则只有从第91个开始的10个结果需要被取回。
为了避免在协调节点中创建的number_of_shards * (from + size)优先队列过大,应尽量控制分页深度。
执行数据节点流程
响应Qurey请求
1、Query请求为: indices:data/read/search/[phase/query]
2、检测是否允许cache,默认为true,查询时,先从cache获取,cache由节点所有分片共享,基于LRU算法实现,空间满时会删除最近最少使用的数据。cache并不会缓存全部的检索结果。
3、使用lucene实现检索。核心方法为:queryPhase.execute(context)
响应Fetch请求,以常见的基于id进程fetch请求为例:
indices:data/read/search/[phase/fetch/id]
主要过程是执行Fetch,然后发送Response:
对Fetch响应的实现封装在searchService.executeFetchPhase中,核心是调用fetchPhase.executor(context),按照命中的文档获取相关内容,填充到SearchHits中,最终封装到FetchSearchResult中。
Query和Fetch请求之间是无状态的,除非是scroll方式。
Meta(元数据)
Meta是用来描述数据的数据。在ES中,Index的mapping结构、配置、持久化状态等就属于meta数据,集群的一些配置信息也属于meta。ES中的meta数据只能由master进行更新,master相当于是集群的大脑。
Meta的组成
MetaData
MetaData中需要持久化的包括:
String clusterUUID:集群的唯一id。
long version:当前版本号,每次更新加1
Settings persistentSettings:持久化的集群设置
ImmutableOpenMap<String, IndexMetaData> indices: 所有Index的Meta
ImmutableOpenMap<String, IndexTemplateMetaData> templates:所有模版的Meta
ImmutableOpenMap<String, Custom> customs: 自定义配置
ClusterState
ClusterState内容包括:
long version: 当前版本号,每次更新加1
String stateUUID:该state对应的唯一id
RoutingTable routingTable:所有index的路由表
DiscoveryNodes nodes:当前集群节点
MetaData metaData:集群的meta数据
ClusterBlocks blocks:用于屏蔽某些操作
ImmutableOpenMap<String, Custom> customs: 自定义配置
ClusterName clusterName:集群名
IndexMetaData
IndexMetaData中需要持久化的包括:
long version:当前版本号,每次更新加1。
int routingNumShards: 用于routing的shard数, 只能是该Index的numberOfShards的倍数,用于split。
State state: Index的状态, 是个enum,值是OPEN或CLOSE。
Settings settings:numbersOfShards,numbersOfRepilicas等配置。
ImmutableOpenMap<String, MappingMetaData> mappings:Index的mapping
ImmutableOpenMap<String, Custom> customs:自定义配置。
ImmutableOpenMap<String, AliasMetaData> aliases: 别名
long[] primaryTerms:primaryTerm在每次Shard切换Primary时加1,用于保序。
ImmutableOpenIntMap<Set> inSyncAllocationIds:处于InSync状态的AllocationId,用于保证数据一致性,下一篇文章会介绍。
ShardStateMetaData
包含是否是primary和allocationId等信息。ShardStateMetaData是在IndexShard模块中管理,与其他Meta关联不大
Meta的存储
– nodes
-- 0 |-- _state | |-- global-1.st |
– node-0.st
|-- indices
| -- 2Scrm6nuQOOxUN2ewtrNJw | |-- 0 | | |-- _state | | |
– state-0.st
| | |-- index
| | | |-- segments_1
| | | -- write.lock | |
– translog
| | |-- translog-1.tlog
| | -- translog.ckp |
– _state
| -- state-2.st
– node.lock
nodes/0/_state/:
这层目录在节点级别,该目录下的global-1.st文件存储的是上文介绍的MetaData中除去IndexMetaData的部分,即一些集群级别的配置和templates。node-0.st中存储的是NodeId。
nodes/0/indices/2Scrm6nuQOOxUN2ewtrNJw/_state/:
这层目录在index级别,2Scrm6nuQOOxUN2ewtrNJw是IndexId,该目录下的state-2.st文件存储的是上文介绍的IndexMetaData。
nodes/0/indices/2Scrm6nuQOOxUN2ewtrNJw/0/_state/:
这层目录在shard级别,该目录下的state-0.st存储的是ShardStateMetaData,包含是否是primary和allocationId等信息。ShardStateMetaData是在IndexShard模块中管理,
集群相关的MetaData和Index的MetaData是在不同的目录中存储的。另外,集群相关的Meta会在所有的MasterNode和DataNode上存储,而Index的Meta会在所有的MasterNode和存储了该Index数据的DataNode上存储。
Gateway模块用于存储es集群的MetaData。MetaData每一次改变(比如增加删除索引等),都要通过Gateway模块进行持久化。当集群第一次启动的时候,这些信息就会从Gateway模块中读出并应用。
Meta的恢复
ES集群需要先进行Master选举,选出Master后,才会进行故障恢复,当Master进程决定进行恢复Meta时,它会向集群中的MasterNode和DataNode请求其机器上的MetaData。对于集群的Meta,选择其中version最大的版本。对于每个Index的Meta,也选择其中最大的版本。然后将集群的Meta和每个Index的Meta再组合起来,构成当前的最新Meta。
ClusterState的更新流程
master进程内不同线程更改ClusterState时要保证是原子的,每次需要更新ClusterState时提交一个Task给MasterService,MasterService中只使用一个线程来串行处理这些Task。
ClusterState commit操作
引入了两阶段提交的方式,
是把Master发布ClusterState分成两步,第一步是向所有节点send最新的ClusterState,当有超过半数的master节点返回ack时,再发送commit请求,要求节点commit接收到的ClusterState。如果没有超过半数的节点返回ack,那么认为本次发布失败,同时退出master状态,执行rejoin重新加入集群。
场景:
1、NodeA本来是Master节点,但由于某些原因NodeB成了新的Master节点,而NodeA由于探测不及时还未发现。
2、NodeA认为自己仍然是Master,于是照常发布新的ClusterState。
3、由于此时NodeB是Master,说明超过半数的Master节点认为NodeB才是新的Master,于是超过半数的Master节点不会返回ack给NodeA。
4、NodeA收集不到足够的ack,于是本次发布失败,同时退出master状态。
5、新的ClusterState不会在任何节点上commit,于是没有不一致发生。
数据模型
_id
Doc的主键,写入时可以指定该Doc的ID值,指定_id 会产生数据偏移和多一步检查指定id合法性。如果不指定,则系统自动生成一个唯一的UUID值,
通过_id值(ES内部转换成_uid)可以唯一在Elasticsearch中确定一个Doc,保证doc不重复。
_id只是一个用户级别的虚拟字段,并不会映射到Lucene中,所以不会在索引中存储。
_uid
_uid的格式是:type + ‘#’ + id,在Lucene中存储_uid,而不是_id的原因是,在6.0.0之前版本里面,_uid可以比_id表示更多的信息。
_version
每个Doc都会有一个Version,该Version可以由用户指定,也可以由系统自动生成。如果是系统自动生成,那么每次Version都是递增1。
_version是实时的,不受搜索的近实时性影响,原因是可以通过_uid从内存中versionMap或者TransLog中读取到。
version是索引级别的。
Elasticsearch通过使用version来保证对文档的变更能以正确的顺序执行,避免乱序造成的数据丢失:
首次写入Doc的时候,会为Doc分配一个初始的Version:V0,该值根据VersionType不同而不同。
再次写入Doc的时候,如果Request中没有指定Version,则会先加锁,然后去读取该Doc的最大版本V1,然后将V1+1后的新版本号写入Lucene中。
再次写入Doc的时候,如果Request中指定了Version:V2,则继续会先加锁,然后去读该Doc的最大版本V2,判断V1V2,如果不相等,则发生版本冲突。否则版本吻合,继续写入Lucene。
当做部分更新的时候,会先通过GetRequest读取当前id的完整Doc和V1,接着和当前Request中的Doc合并为一个完整Doc。然后执行一些逻辑后,加锁,再次读取该Doc的最大版本号V2,判断V1V2,如果不相等,则在刚才执行其他逻辑时被其他线程更改了当前文档,需要报错后重试。如果相等,则期间没有其他线程修改当前文档,继续写入Lucene中。这个过程就是一个典型的read-then-update事务。
recovery分片恢复时,会根据Version来过滤掉不需要的操作。
_source
_source,存储原始文档,也可以通过过滤设置只存储特定Field。_source其实是将文档中所有Field都打包到一个名为_source的虚拟Field,然后存储为Store类型。
_source字段可以实现以下功能:
Update:部分更新时,需要从文档读取到保存在_source字段中的原文,然后和请求中的部分字段合并为一个完整文档。如果没有_source,则不能完成部分字段的Update操作。
Rebuild:最新的版本中新增了rebuild接口,可以通过Rebuild API完成索引重建,过程中不需要从其他系统导入全量数据,而是从当前文档的_source中读取。如果没有_source,则不能使用Rebuild API。
Script:不管是Index还是Search的Script,都可能用到存储在Store中的原始内容,如果禁用了_source,则这部分功能不再可用。
Summary:摘要信息也是来源于_source字段。
_seq_no
严格递增的顺序号,每个文档一个,Shard级别严格递增,保证后写入的Doc的_seq_no大于先写入的Doc的_seq_no。
任何类型的写操作,包括index、create、update和Delete,都会生成一个_seq_no。
_seq_no在Primary Node中由SequenceNumbersService生成,每次递增1。
每个文档在使用Lucene的document操作接口之前,会获取到一个_seq_no,这个_seq_no会以系统保留Field的名义存储到Lucene中,文档写入Lucene成功后,会标记该seq_no为完成状态,这时候会使用当前seq_no更新local_checkpoint。
checkpoint分为local_checkpoint(每个shard)和global_checkpoint(集群),主要是用于保证有序性,以及减少Shard恢复时数据拷贝的数据拷贝量,恢复时本地shard与集群对比执行大于local_checkpoint的操作。
_primary_term
_primary_term也和_seq_no一样是一个整数,每当Primary Shard发生重新分配时,比如重启,Primary选举等,_primary_term会递增1。
_primary_term主要是用来恢复数据时处理当多个文档的_seq_no一样时的冲突,避免Primary Shard上的写入被覆盖。
选Shard主时用的。
_routing
在mapping中,或者Request中可以指定按某个字段路由。默认是按照_Id值路由。
文档级别的_routing主要有两个目的,一是可以查询到使用某种_routing的文档有哪些,当发生_routing变化时,可以对历史_routing的文档重新读取再Index,这个需要倒排Index。另一个是查询到文档后,在Response里面展示该文档使用的_routing规则,这里需要存储为Store,只是文档级,还有集群级那个是真正管理写入与查询路由。
_field_names
该字段会索引某个Field的名称,用来判断某个Doc中是否存在某个Field,用于exists或者missing请求。
写入速度优化
优化点
1、加大translog flush间隔,目的是降低iops(iops指的是每秒的输入输出量(或读写次数),是衡量磁盘性能的主要指标之一)、writeblock。
2、加大index refresh间隔,除了降低I/O,更重要的是降低了segment merge频率。
3、调整bulk请求。
4、优化磁盘间的任务均匀情况,将shard尽量均匀分布到物理主机的 各个磁盘。
5、优化节点间的任务分布,将任务尽量均匀地发到各节点。
6、优化Lucene层建立索引的过程,目的是降低CPU占用率及I/O,例如,禁用_all字段
translog flush间隔调整
这是影响 ES 写入速度的最大因素。如果系统可以接受一定概率的数据丢失,则调整translog持久化策略为周期性和一定大小的时候“flush”。
配置:
index.translog.durability: request (默认每个请求都 flush)
index.translog.durability: async (周期和大小时flush)
设置为async表示translog的刷盘策略按sync_interval配置指定的时间 周期进行。
index.translog.sync_interval: 120s (加大translog刷盘间隔时间。默认为5s,不可低于100ms。)
index.translog.flush_threshold_size: 1024mb (超过这个大小会导致refresh操作,默认值为 512MB。)
索引刷新间隔refresh_interval
默认情况下索引的refresh_interval为1秒,写1秒后就 可以被搜索到,每次索引的refresh会产生一个新的Lucene段,segment在复合一定条件后,会自动合并,因此这会导致频繁的segment merge行为(),如果不需要这么高的搜索实时性,应该降低 索引refresh周期
配置:
index.refresh_interval: 120s
段合并优化
segment merge操作对系统I/O和内存占用都比较高
配置:
index.merge.scheduler.max_thread_count (最大线程数) 默认 Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors()/2))
merge策略index.merge.policy有三种:tiered(默认策略)/log_byete_size/log_doc。
index.merge.policy.segments_per_tier (每层分段的数量,取值越小则最终segment越少,因此需要merge的操作更多,默认10)
index.merge.policy.max_merge_at_once (一次最多归并segment的个数,默认也为 10 个 segment)
index.merge.policy.max_merged_segment (指定了单个segment的最大容量,默认为5GB)
indexing buffer
indexing buffer在为doc建立索引时使用,当缓冲满时会刷入磁盘, 生成一个新的segment,这是除refresh_interval刷新索引外,另一个生成 新segment的机会。每个shard有自己的indexing buffer,下面的这个buffer 大小的配置需要除以这个节点上所有shard的数量
配置:
indices.memory.index_buffer_size 默认为整个堆空间的10%。
indices.memory.min_index_buffer_size 默认为48MB。
indices.memory.max_index_buffer_size 默认为无限制。
使用bulk请求
太大的请求可能会给集群带来内存压 力,因此每个请求最好避免超过几十兆字节。
bulk线程池和队列
建立索引的过程属于计算密集型任务,应该使用固定大小的线程池 配置,来不及处理的任务放入队列。线程池最大线程数量默认为CPU 核心数+1。队列大小可以适当增加,但一定要严格控制大小,过大的队列导致 较高的GC压力,并可能导致FGC频繁发生。
并发执行bulk请求
bulk写请求是个长任务,如果CPU没有压满,则应该提高写入端的并发数量,但是要注意bulk线 程池队列的reject情况,出现reject代表ES的bulk队列已满,客户端收到429错误(TOO_MANY_REQUESTS),在评估极限的写入能力时,客户端的极限写入并发量应该控制在不产生reject前提下的最大值为宜。
磁盘间的任务均衡
ES在处理多路径时,优先将shard分配到可用空间百分比最多的磁盘上,因此短时间内创建的shard可能被集中分配到这个磁盘上,即使可用空间是99%和98%的差别。如果单一的机制不能解决所有的场景,那么至少应该为不同场景准备多种选择。
为此,我们为ES增加了两种策略:
简单轮询:在系统初始阶段,简单轮询的效果是最均匀的。
基于可用空间的动态加权轮询:以可用空间作为权重,在磁盘之间加权轮询。
节点间的任务均衡
为了节点间的任务尽量均衡,数据写入客户端应该把bulk请求轮询 发送到各个节点。
1、使用Java API时,当设置client.transport.sniff为true(默认为false) 时,列表为所有数据节点,否则节点列表为构建客户端对象时传入的节 点列表。
2、使用REST API时,列表为构建对象时添加进去的节点。
要观察bulk请求在不同节点间的均衡性,可以通过cat接口观察bulk 线程池和队列情况: _cat/thread_pool
3、我们可能通过Nigex来代理9200轮询协调节点的访问。
索引过程调整和优化
自动生成doc ID
写入doc时如果外部指定了id,则ES会先尝试读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘的操作,通过自动生成doc ID可以避免这个环节。
调整字段Mappings
1、减少字段数量,对于不需要建立索引的字段,不写入ES。
2、将不需要建立索引的字段index属性设置为not_analyzed或no。
对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。 尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词
通常没有什么意义。
3、减少字段内容长度。
禁用_all字段
从ES 6.0开始,_all字段默认为不启用,而在此前的版本中,_all字 段默认是开启的。_all字段中包含所有字段分词后的关键词,作用是可以在搜索的时候不指定特定字段
对Analyzed的字段禁用Norms
Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其 禁用
index_options 设置
index_options用于控制在建立倒排索引过程中,哪些内容会被添加到倒排索引,例如,doc数量、词频、positions、offsets等信息,优化这些设置可以一定程度降低索引过程中的运算任务,节省CPU占用率。
搜索速度优化
为了让搜索时的成本更低,文档应该合理建模。特别是应该避免:
join操作,嵌套(nested)会使查询慢几倍。
父子(parent-child)关系可能使查询慢数百倍。
预索引数据
还可以针对某些查询的模式来优化数据的索引方式。例如,如果所 有文档都有一个 price字段,并且大多数查询在一个固定的范围上运行 range聚合,那么可以通过将范围“pre-indexing”到索引中并使用terms聚 合来加快聚合速度。
避免使用脚本
一般来说,应该避免使用脚本。如果一定要用,则应该优先考虑 painless和expressions。
优化日期搜索
在使用日期范围检索时,使用now的查询通常不能缓存,因为匹配 到的范围一直在变化。但是,从用户体验的角度来看,切换到一个完整的日期通常是可以接受的,这样可以更好地利用查询缓存。
为只读索引执行force-merge
预热文件系统cache
如果ES主机重启,则文件系统缓存将为空,此时搜索会比较慢。可 以使用index.store.preload设置,通过指定文件扩展名,显式地告诉操作 系统应该将哪些文件加载到内存中。
如果文件系统缓存不够大,则无法保存所有数据,那么为太多文件 预加载数据到文件系统缓存中会使搜索速度变慢,应谨慎使用。
调节搜索请求中的batched_reduce_size
该字段是搜索请求中的一个参数。默认情况下,聚合操作在协调节 点需要等所有的分片都取回结果后才执行,使用 batched_reduce_size 参 数可以不等待全部分片返回结果,而是在指定数量的分片返回结果之后 就可以先处理一部分(reduce)。这样可以避免协调节点在等待全部结 果的过程中占用大量内存,避免极端情况下可能导致的OOM。该字段 的默认值为512,从ES 5.4开始支持。
利用自适应副本选择(ARS)提升ES响应速度
为了充分利用计算资源和负载均衡,协调节点将搜索请求轮询转发 到分片的每个副本,轮询策略是负载均衡过程中最简单的策略,任何一 个负载均衡器都具备这种基础的策略,缺点是不考虑后端实际系统压力 和健康水平。
ES的ARS实现基于这样一个设定:对每个搜索请求,将分片的每个 副本进行排序,以确定哪个最可能是转发请求的“最佳”副本。与轮询方 式向分片的每个副本发送请求不同,ES选择“最佳”副本并将请求路由到那里。
索引恢复流程分析
触发条件
索引恢复的触发条件包括从快照备份恢复、节点加入和离开、索引的 open操作等。
恢复阶段
init:恢复尚未启动
index:恢复Lucene文件,以及在节点间复制索引数据
verify_index:验证索引
Translog:启动engine,重放translog,建立Lucene索引
Finalize:清理工作
Done:完毕
相关配置
indices.recovery.max_bytes_per_sec
副分片恢复的 phase! 过程中,主副分片节点之间传输数 据的速度限制默认为 40MB/s,单位为字节 。 设置为 0 则不限速
indices.recovery.retry_delay_state_sync
由于集群状态同步导致 recovery 失败时,重试 r巳covery 前的等待时间,默认为 500 ms
indices.recovery.retry_delay_network
由于网络问题导致 recovery失败时,重试 recovery前的
等待时间, 默认为5s
indices.recovery.internal_action_timeout
用于某些恢复请求的 RPC 超时时间,默认为 15 min。 例如 : prepare translog、 clean files 等
indices.recovery.internal action long timeout
与上面的用处相同,但是超时更长,默认为前者的 2 倍
indices.recovery.recovery_activity_timeout
不活跃的 recovery 超时时 间, 默认值等于 indices.recovery.internal action long timeout
主分片恢复
INIT 阶段
一个分片的恢 复流程中,从开始执行恢复的那 一 刻起,被标记为 INIT 阶段。
恢复流程在新的线程池中开始执行,开始阶段主要是一些验证工作,例如,校验 当前分片是否为主分片,分片状态是否异常等。
INDEX 阶段
本阶段从 Lucene读取最后一次提交的分段信息,获取其中的版本号,更新当前索引版本
VERIFY INDEX 阶段
VERIFY INDEX 中的时DEX 指 Lucene index,因此本阶段的作用是验证当前分片是否损坏,是 否进行本项检查取决于配置项:
在索引的数据量较大 时, 分片检查会消耗更多 的时间 。
验证对比 checksum与 Lucene文件的实际值 ,或者调用 Lucene Checklndex
配置项:index.shard .check on startup
false: 默认值,打开分片时不检查分片是否损坏
checksum: 检查物理损坏
true: 检查物理和逻辑损坏,这将消耗大量的内存和 CPU 资源
fix: 检查物理和逻辑损坏。损坏的分段将被集群自动删除 ,这将导致数据丢失。使用 时请考虑清楚。
TRANSLOG阶段
本阶段需要重放事务日志中尚未刷入磁盘的信息,因 此, 根据最后一次提交的信息做快照, 来确定事务日志中哪些数据需要重放。重放完毕后将新生成的 Lucene 数据刷入磁盘。
1、根据最后一次提交的信息生成 translog快照
2、重放这些日志
3、将重放后新生成的数据刷入硬盘
遍历所有需要重放的事务日志 ,执行具体的写操作 ,如 同写入过程一样,事务日志重放完毕后, StoreRecovery向1temalRecoverFromStore 方法调用 indexShar她 回izeRecoveryO 进入 FINALIZE 阶段。
FINALIZE 阶段
本阶段执行刷新( refresh)操作,将缓冲的数据写入文件,但不刷盘,数据在操作系统的
cache 中 。
DONE 阶段
DONE阶段是恢复工作的最后一个阶段, 进入DONE阶段之前再次执行refresh, 然后更新
分片状态 。
如果恢复成功,向 Master 发送 action 为 internal:cluster/shard/start时的 RPC 请求。
如果恢复失败,关闭 Engine,向 Mast巳r发送 internal:cluster/shard/failure 的 RPC 请求。
副分片恢复
副分片恢复的核心思想是从主分片拉取 Lucene 分段和 translog 进行恢复 。 按数据传递的方 向, 主分片节点称为 Source,副分片节点称为 Target。
恢复分两个阶段进行:
第一阶段:把 shard 数据拷贝到副本节点。如果主副两 shard 有相同的 syncid 且 doc 数相同,
则跳过这个阶段。在第一阶段完毕前,会向副分片阶段发送告知对方启动 engine,在第二阶段开始之前,副分片就可以正常处理写请求了。
第二阶段:对 translog 做快照,这个快照里包含从第一个节点开始的新增索引,发送所有的 translog operation 到对端节点,不限速。
ES 2.0前:
phase1:将主分片的 lucene做快照,发送到 target。期间不阻塞索引操作,新增数据写到主分片的 translog
phase2:将主分片 translog 做快照,发送到 target 重放,期间不阻塞索引操作。
phase3:为主分片加写锁,将剩余的translog 发送到 target。此时数据量很小,写入过程的阻塞很短。
从第一阶段开始,就要阻止 lucene 执行commit 操作,避免 translog 被刷盘后清除。
本质上来说,只要流程上允许将写操作阻塞一段时间,实现主副一致是比较容易的
ES 2.0~ES5.0:
为了避免这种做法产生过大的 translog,translog 模块被修改为支持多个文件,同时引入 translog.view 的概念,创建 view 可以获取到后续的所有操作。这样实现了在第一阶段允许 lucene commit。
phase3被删除。对于如何做到主副一致的,描述的很模糊。分析完相关代码后,整理流程如下:
先创建一个 Translog.view,然后
phase1:将主分片的 lucene 做快照,发送到 target。期间允许索引操作和 flush 操作。发送完毕后,告知 target 启动 engine,phase2开始之前,新的索引操作都会转发副分片正常执行。
phase2:将主分片的 translog 做快照,发送到 target 去重放。
写流程中做异常处理,通过版本号过滤掉过期操作来。写操作有三种类型:索引新文档,更新,删除。索引新文档不存在冲突问题,更新和删除操作采用相同的处理机制。每个操作都有一个版本号,这个版本号就是预期 doc 版本,他必须大于当前 lucene 中的 doc 版本号。否则就放弃本次操作。对于更新操作来说,预期版本号是 lucene doc 版本号+1。主分片节点写成功后新数据的版本号会放到写副本的请求中,这个请求中的版本号就是预期版本号。这样,时序上存在错误的操作被忽略,对于特定 doc,只有最新一次操作生效,保证了主副分片一致
ES6.0后:
translog.view 被 移除。 引入 TranslogDeletionPolicy 的概念,负责维护 活跃(liveness)的translog文件。这个类的实现非常简单,它将 translog做一个快照来保持 translog 不被清理。
在保证 translog 不被清理后,恢复核 心处理过程 由两个内 部阶段( phase) 组成 。
两个phase1/phase2阶段不变,
由于 phase1 需要通过网络复制大量数据,过程非常漫长,在 ES 6.x 中,有两个机会可以跳
过 phase1:恢复时通过对比两个序列号,计算出缺失的数据范围,然后通过translog重放这部分数据,同时 translog 会为此保留更长的时间.
(1)如果可以基于恢复请求中的 SequenceNumber(globalcheckpoint和 local checkpoint)进行恢复,则跳过 phase!。
(2)如果主副两分片有相同的 syncid 且 doc 数相同,则跳过 phase!。
synced flush 机制
为了解决副分片恢复过程第 一 阶段时间太漫长而引入了 synced flush(ES1.6前比较的段,但每个shard的独立运行并meger segmengt,会照成相同数据段但判断不一至,所以ES1.6后使用syncid来判断), 默认情况下 5 分钟没 有写入操作的索引被标记为 inactive,执行 syncedflush, 生成一个唯一的 syncid, 写入分片的所 有副本中。这个 syncid 是分片级,意味着拥有相同 syncid 的分片具有相同的 Lucene 索引 。synced flush本质上是一次普通的 flush操作,只是在 Lucene的 commit过程中多写了一个 syncid。
需要注意的是synced flush只对冷索引有效,对于热索引(5分钟内有更新的索引)无效,如果重启的节点包含有热索引,那还是免不了大量的拷贝。如果要重启一个包含大量热索引的节点,可以按照以下步骤执行重启过程,可以让recovery过程瞬间完成:
暂停数据写入
关闭集群的shard allocation
手动执行 POST /_flush/synced
重启节点
重新开启集群的shard allocation
等待recovery完成,当集群的health status是green后
重新开启数据写入
恢复流程
INIT 阶段
本阶段在副分片节点执行 。
与主分片恢复的 INIT 阶段类似,恢复任务开始时被设置为 INIT 阶段,构建准备发往主分片的 StartRecoveryRequest请求,请求中包括将本次要恢复的 shard相关 信息,如 shardid、 metadataSnapshot等。 metadataSnapshot 中包含 syncid。
INDEX阶段
INDEX 阶段负 责将主分片 的 Lucene 数据 复制 到副 分 片节点 。
向主分片 节点 发送 action 为 intemal:indexJshard/recovery/start_recovery 的 PRC 请求,线程阻塞等待 INDEX 阶段完成,然后直接到 DONE 阶段。在这期间,主分片节点会发送几 次RPC调用,把 Lucene和 translog发送给副分片,通知副分片节点启动 Engine,执行清理等操作。 VE阳FY_INDEX和 TRANSLOG 阶段也是由主分片节点的 RPC 调用触发的
VERIFY INDEX阶段
副分片的索引验证过程与主分片相 同, 是否进行验证取决于配置。默认为不执行索 引验证。
主分片节点执行完 phase] 后,调用 prepareTargetForTranslog方法,向副分片节点发送 action 为 internal:index/shard/recovery/prepare_translog的 RPC请求。副分片对此 action的主要处理是启 动Engine,使副分片可以正常接收写请求。 副分片的VE阳FY_INDEX、 TRANSLOG两阶段也 是在对这个 action 的处理中触发的。
TRANSLOG 阶段
TRANSLOG 阶段负责将主分片的 translog数据复制到副分片节点进行重放。
先创建新的 Engine, 跳过 Engine 自身的 translog恢复。 此时主分片的 phase2 尚未开始, 接 下来的 TRANSLOG 阶段就是等待主分片节点将 translog 发到副分片节点进行重放,也就是 phase2 的执行过程。
FINALIZE 阶段
主分 片节 点 执 行完 phase2 ,调用 finalizeRecovery, 向副 分 片节 点发送 action 为 internal:index/shard/recoverγ/finalize 的 RPC 请求 , 副分片节点对此 action 的处理为先更新全局 检查点,然后执行与主分片相 同的清理操作:
DONE 阶段
主要处理是调用 indexShard#postRecovery,与主分片的
postRecovery 处 理过程相同,包括对恢复成功或失败的处理,也和 主 分片的处理过程相同 。
1、 首先获取一个保留锁,使得 translog不被清理
2、判断是否可以从 SequenceNumber恢复
3、检测和版本号检测,判断请求的序列号是否小于主分片节点的 localCheckpoint,以及 translog 中的数据是否足以 恢复。
4、以请求的序列号作为最小值做一个快照,遍历这个值从开始到最新的数据之间的操作,检 查序列号验证事务日志中的操作是否完整。
5、以请求的序列号作为最小值做一个快照,遍历这个值从开始到最新的数据之间的操作,检 查序列号验证事务日志中的操作是否完整。
如果可以基于 SequenceNumber 恢复,则跳过 phasel,否则调用 Lucene 接口对分片做快照, 执行 phasel。
6、等待 phasel 执行完毕,主分片节点通知副分片节点启动此分片的 Engine
7、该方法会阻塞处理,直到分片 Engine 启动完毕。待副分片启动 Engine 完毕,就可以正常 接收写请求了。注意,此时 phase2 尚未开始,此分片的恢复流程尚未结束。等待当前操作处理完成后,以 startingSeqNo为起始点,对 translog做快照,开始执行 phase2
如果基于 SequenceNumber 恢复,则 startingSeqNo 取值为恢复请求中的序列号,从请求的
序列号开始快照 translog。否则取值为 0,快照完整的 translog。
phase1检查目标节点上的段文件,井对缺失的部分进行复制。只有具有相同大小和校验和 的段才能被重用。但是由于分片副本执行各自的合并策略,所以合并 出来的段文件相同的概率 很低。
在对比分段之前,先检查主副两分片是否都有 syncid,如果 syncid 相同,且 doc 数相同,
则跳过 phase1
phase2将 translog批量发送到副分片节点,发送时将待发送的 translog组合成一批来提高发
送效率,默认的批量大小为 512阻, 不支持配置。
速度优化
集群完全重启,或者 Master 节点挂掉后,新选出的 Master 也有可能执行这个过程。所以应该把mster和数据节点分开。
配置项 cluster.routing.allocation.node_concurrent_recoveries 决定了单个节点执行副分片
recove可时的 最大并发数(进/出),默认为 2,适当提高此值可 以增加 recove可 井发数。
配置项 indices.recovery.max bytes_per_sec 决定节点 间复制 数据时 的限速,可以适 当提 高此值或取消限速。
配置项 clust巳r.routing.allocation.node_initial_primaries_recoveries决定了单个节点执行主 分片 recove可时的最大并发数,默认为 4。 由于主分片的恢复不涉及在网络上复制数据, 仅在本地磁盘读写,所以在节点配置了多个数据磁盘的情况下,可以适当提高此值 。
在重启 集群之前,先停止写入端,执行 sync flush,让恢复过程有机会跳过 phase!。
适当地多保留些回nslog,配置项 index.translog.retention.size 默认最大保留 512MB, index.translog.retention.age 默认为不超过 12 小时 。 调整这两个配置可让恢复过程有机 会跳过 phase1。
合并 Lucene 分段,对于冷索引甚至不再更新的索引执行 forcemerge, 较少的 Lucene 分段可以提升恢复效 率 ,例如,减少对比,降低文件传输请求数 量。
最后,可以考虑允许主副分片存在一定程度的不一致,修改 ES 恢复流程,少量的不一致 则跳过 phase1。
如何保证副分片和主分片一致
索引 新文档 、 更新、删除 。 索引新文挡不存在冲突问题,更新和删除操作采用相同的处理机制。每 个操作都有 一个版本号,这个版本号就是预期 doc 版本,它必须大于当前 Lucene 中的 doc 版本 号, 否则就放弃本次操作。对于更新操作来说, 预期版本号是 Lucenedoc版本号十1。主分片节 点写成功后新数据的版本号会放到写副本的请求中 , 这个请求中的版本号就是预期版本号。
这样,时序上存在错误的操作被忽略,对于特定 doc,只有最新一次操作生效, 保证了主 副分片一致 。
如果 translog重放的操作在写一条“老”数据,则 compareOpToLuceneDocBasedOnVersions 会返回 OpVsLuceneDocStatus.OP_ST ALE_OR EQUAL。
如果 translog重放的是一个“老”的删除操作,则 compareOpToLuceneDocBasedOnVersions
会返回 OpVsLucen巳DocStatus.OP_STALE_OR EQUAL。
recovery 相关监控命令
cat/recovery 列出活跃的和己完成的 recovery信息
{index}/ recovery 此 API 展示特定索 引 的 recovery 所处阶段,以及每个分片、每个阶段的详细信息 。
_stats statsAPI可以给出分片级信息,包括分片的 sync_id、 local checkpoint、 global_checkpoint等,可以通过指定索引名称实现, 或者使用_all输出全部索 引的信息。
减少集群full restart
造成的数据来回拷贝
有了主节点,该主节点会立刻主持recovery的过程。但此时,这个集群数据还不完整,会导致本地已有的shard还没启动但被分配到其它节点导致shard间相互拷贝,此种情况可以限定 master/datanoda节点启动的个数或时间,具体配置如下:
gateway.expected_nodes: 3 master节点(有master资格的节点)和data节点都算在内,达到数量就执行recovery过程。
gateway.expected_master_nodes: 3 有几个master节点启动成功,就执行recovery的过程。
gateway.expected_data_nodes: 3 有几个data节点启动成功,就执行recovery的过程。
当集群在期待的节点数条件满足之前,recovery过程会等待gateway.recover_after_time指定的时间,一旦等待超时,则会根据以下条件判断是否执行recovery的过程:
gateway.recover_after_nodes: 3 # 3个节点(master和data节点都算)启动成功
gateway.recover_after_master_nodes: 3 # 3个有master资格的节点启动成功
gateway.recover_after_data_nodes: 3 # 3个有data资格的节点启动成功
例如配置如下集群:
gateway.expected_data_nodes: 10
gateway.recover_after_time: 5m
gateway.recover_after_data_nodes: 8
此时的集群在5分钟内,有10个data节点都加入集群,或者5分钟后有8个以上的data节点加入集群,都会启动recovery的过程。
重启单个节点,也会造成不同节点之间来复制,为了避免这个问题,可以在重启之前,关闭集群的shard allocation。
PUT _cluster/settings
{
“transient”: {
“cluster.routing.allocation.enable”:“none”
}
}
当节点重启后,再重新打开:
PUT _cluster/settings
{
“transient”: {
“cluster.routing.allocation.enable”:“all”
}
}
索引恢复分种类
EXISTING_STORE : 主分片从本地恢复
PEER :副分片从远程主分片恢复
snapshot:从快照中恢复
API 查看某索引恢复进度
http://172.18.130.48:9200/dkbs_pangu/_recovery
stage: “DONE” 时表示恢复完成。
“type” : “snapshot”, 表示恢复类型。
source : 哈希描述了作为恢复来源的特定快照和仓库。
percent:百分比 恢复进度。
取消操作
DELETE /restored_index_3
gateway 模块
gateway 模块负 责集群元信息的存储和集群重启时的恢复 。
数据
ES 中存储的数据
state 元数据 信息
index Lucene 生成的索引文件
translog 事务日志
ES中元数据信息
nodes/OJ state/.st, 集群层面元信息
nodes/O/indices/{index_uuid}I_state/.st,索引层面元信息
nodes/O/indices/{index uuid}/0/_state/*.st,分片层面元信息
ES 中的数据结构
MetaData (集群层), 主要是 clusterUUID、 settings、 templates等: lndexMetaData (索引层), 主要是 numberOfShards、 mappings等: ShardStateMetaData (分片层),主要是 version、 indexUUID、 primary 等。
上述信息不包括路由,路由信息依靠gateway的recovery过程重建RoutingTable。
元数据的持久化
只有具备 Master资格的节点和数据节点可以持久化集群状态。当收到主节点发布的集群状
态时, 节点判断元信息是否发生变化,如果发生变化, 则将其持久化到磁盘中。
执行文件写入的过程封装在 MetaDataStateFormat类中, 全局元信息和索引级元信息的写入 都执行三个流程: 写临时文件、刷盘、“move”成目标文件。
元数据的恢复
gateway 的 recovery 负 责找到正确的元数据,应用到集群 ,恢复的配置条件就是recovery一样。
gateway模块负责集群层和索引层的元数据恢复,分片层的元数据恢复 在 allocation模块实现。
触发获取 shard级元数据的操作, 这个 Fetch过程是异步的, 根据集群分片数量规模, Fetch过程可能比较长,执行完后由allocation模块恢复分片元数据使用
恢复流程
(1) Master选举成功之后,判断其持有的集群状态中是否存在 STATE NOT RECOVE阻D BLOCK,如果不存在, 则说明元数据已经恢复,跳过 gateway恢复过程, 否则等待。
( 2 ) Master 从各个节点主动获取元数据信息。
(3)从获取的元数据信息中选择版本号最大的作为最新元数据 , 包括集群级、索引级。
(4)两者确定之后,调用 allocation 模块的 reroute, 对未分配的分片执行分配,主分片分 配过程中会异步获取各个 shard 级别元数据,默认超时为 13秒。
从 gateway到 allocation流程的转换
两者之间没有明显的界限, gateway 的最后一步执行 reroute, 等待这个函数返回,然后打印 gateway选举结果的日志, 集群完全重启时, reroute 向各节点发起的询问 shard级元数据的操作基本还没执行完,因此一般只有少数主分片被选举完了, gateway流程的结束只是集群级和索
引级的元数据己选举完毕,主分片的选举正在进行中。
allocation 模块
allocators 作用 分配shard,构建reroute路由。
allocators负责为某个特定的分片分配目 的节点。每个 allocator的主要工作是根据某种逻辑 得到一个节点列表, 然后调用 deciders去决策,根据决策结果选择一个目的 node。
gatewayAllocator
是为了找到现有 分片
pnmaryShardA!locator:找到那些拥有某分片 最新数据的节点:
replicaShardA!locator: 找到磁盘上拥有这个分片数据的节点;
shardsAllocator
是根据权重策略在集群的各节点间均衡分片分布
alancedShardsAllocator:找到拥 有最 少 分片个数的节点 。
触发时机
index 增删 ;
node 增加;
手工 reroute;
replica 数量改变;
集群重启。
执行过程由AllocationService . reroute方法完成
deciders
分配限制决策器
实现的接口有:
canRebalance ,给定分片是否 可以 “re balanced”到给定 allocation;
canAllocate ,给定分片是否可以分配到给定节点 ;
canRemain, 给定分片是否可以保留在给定节点;
canForceAllocatePrimary, 给定主分片 是否 可以强制分配在给定节点。
大至分类
负载均衡类
SameShardAl!ocationDecider:避免主副分片分配到 同一个节 点。
AwarenessAilocationDecider 感知分配器,感知服务器、机架等,尽量分散存储 shard。
ShardsLimitAllocationDecider 同一个节点上允许存在的同一个 index 的 shard数目。
并发控制类
recovery 阶段的限速
ConcurrentRebalanceAllocationDecider
rebalance 并发控 制
DiskThresholdDecider
根据磁盘空间进行决策的分配器 。
条件限制类
RebalanceOnlyWhenActiveA!locationDecider
所有 shard都处在 active状态下,才可以执行 rebalance操作。
ReplicaAfterPrimaryActiveAllocationDecider
保证只在主分片分配完毕才开始分配分片副本。
相关配置
cluster.routing.allocation.awareness.attributes: arck_id
cluster.routing.allocation.awareness.attributes: zone
recovery 阶段的限速配置,包括:
cluster .routing.allocation .node concurrent_recoveries
cluster. routing.allocation . node_initial primaries_recoveries
cluster . routing . allocation.node_concurrent_incoming_recoveries
cluster .routing .allocation .node concurrent_outgoing_recoveries
rebalance 并发控 制,可以通过下面的 参数配置 :
cluster . routing.allocat 工on . cluster concurrent rebalance
条件限制配置
配置的目标为节点 IP 或节点名等。 clust巳r级别设置会覆盖 index 级别设置。
index.routing.allocation.require . * [必 须 ]
index.routing.allocation.require . * [允 许]
index.routing.allocation.require . * [排 除 ]
cluster.routing.allocation.requitre.*
cluster.routing.allocation.include.*
cluster.routing.allocation.exclude.*
通过集群中 active 的 shard 状态来决定是否可以执行 rebalance,通过下面的配置控制 , 可以动态生效:
cluster.routing.allocation.allow_rebalance 配置值如下
indices all active 当集群所有的节点分配完毕,才认定集群 rebalance 完成(默认)
indices_primaries_always 只要所有主分片分配完毕,就可以认定集群 rebalance 完成
active 即使当主分片和分片副本都没有分配,也允许 rebalance操作
reroute
核心实现
gatewayAllocator, 用于分配现实己存在的分片,从磁盘中找到它 们;
shardsAllocator,用于平衡分片在节点间的分布 。
流程分析
gateway 阶段恢复的集群状态中 ,我们己经知道集群一共有多少个索引,每个索引的主副分 片各有多少个,但是不知道它们位于哪个节点,现在需要找到它们都位于哪个节点。集群完全 重启 的初始状态,所有分片都被标记为未分配状态,此处也被称作分片分配过程 。因此分片分 配的概念不仅仅是分配一个全新分片 。
对于索引某个特定分片的分配过程中,先分配其主分片,后分配其副分片。
allocateUnassigned 的流 程是:遍历所有 unassigned shard,依次处理,通过 decider 决策分配, 期间需要 fetchData 获取这个 shard 对应的元数据。如果决策结果为 YES,则将其初始化。
在 routingChangesObserver.shardinitialized 中设置 RoutingNodes 己更新。更新的内容大约就 是某个 shard被分配到了某个节点,这个 shard是主还是副,副的话会设置 recoverySource为 PEER, 但只是一个类型,并没有告诉节点 recovery 的时候从哪个节点恢复,节点恢复时自己从集群状 态中的路由表中查找。
然后, Mast巳r 把新的集群状态广播下去,当数据节点发现某个分片分配给自己 ,开始执行 分片的 recove可。
主分片的分配过程中有另外一层逻辑:如果被decider 拦截,返回 NO,则尝试强制分配。给 buildNodesToAllocate 的最后一个参数传入 true,接下来尝试强制分配,
从 allocation 流程到 recovery流程的转换
makeAllocationDecision 成功后, unassignedlterator.initialize 初始化这个 shard,创建一个新 的 ShardRouting对象,把相关信息添加到集群状态,设置 routingChangesObserv巳r为已经发生变 化,后面的流程就会把新的集群状态广播出去。 到此, reroute 函数执行完毕。
接下来 lndicesService.createShard 开始执行 recovery。
节点重启分配问题
一个集群节点重启前要先临时禁用自动分配,设置cluster.routing.allocation.enable为none,否则节点停止后,当前节点的分片会自动分配到其他节点上,本节点启动后需要等其他节点RECOVERING后才会RELOCATING,也就是分片在其他节点恢复后又转移回来,浪费大量时间
首先禁用自动分配
curl -XPUT http://127.0.0.1:9200/_cluster/settings -d ‘{
“transient” : {
“cluster.routing.allocation.enable” : “none”
}
}’
然后再重启集群
集群启动后再改回配置
curl -XPUT http://127.0.0.1:9200/_cluster/settings -d ‘{
“transient” : {
“cluster.routing.allocation.enable” : “all”
}
}’
Cluster模块
触发情况
集群拓扑变化 ;
模板、索引 map、别名的变化;
索 引操作:创 建、删除 、 open、 close;
pipeline 的增删 ;
脚本的增删 ;
gateway 模块发布选举出的集群状态;
分片分配;
快照、 reroute api 等触发 。
集群状态的发布过程
发布集群状态是一个分布式事务操作,分布式事务需要实现原子性
ES 实现二段提交与标准二段提交有一些区别 , 发布集群状态到参与者的数量并非定义为全 部,而是多数节点成功就算成功。多数的定义取决于配置项 :
discovery.zen .minimum master nodes
两个阶段过程
发布阶段 : 发布集群状态,等待响应。
提交阶段 : 收到的响应数量大于 minimum master nodes 数量 , 发送 commit 请求。
ThreadPool 模块
ES 版本中的线程池
generiC
用于通用的操作(例如,节点发现),线程池类型为 scaling。
index
用于 index/delete操作,线程池类型为 fixed,大小为处理器的数量,队列大小为 200,允许设置的最大线程数为 l+处理器数量。
search
用于 count/search/suggest操作。线程池类型为 fixed,大小为 int((处理器数量 3)/2)+I, 队列 大小为 1000。
get
用于 get操作。线程池类型为自xed,大小为处理器的数量,队列大小为 1000。
bulk
用于 bulk操作,线程池类型为 fixed,大小为处理器的数量,队列大小为 200,该线程池允 许设置的最大线程数为 l+处理器数量。
snapshot
用于 snaphost/restore 操作。线程池类型为 scaling, 线程保持存活时间为 5min,最大线程数为min(5, (处理器数量)/2)。
warmer
用于 segmentwarm叩操作。线程池类型为 scaling,线程保持存活时间为 5min,最大线程 数为 min(5,(处理器数量)/2)。
refresh
用于 refresh 操作。线程池类型为 scaling, 线程空闲保持存活时间为 5min, 最大线程数为 mm(lO, (处理器数量)/2)。
listener
主要用于 Java客户端线程监昕器被设置为 true 时执行动作 。线程池类型为 scaling,最大线
程数为 min(lO, (处理器数量)/2)。
same
在调用者线程执行 ,不转移到新的线程池 。
management
管理工作 的线程池,例如, Node info、 Node tats、 List tasks 等。
flush
用于索引数据的 flush操作。
force_merge
顾名思义 ,用于 Lucene 分段 的 force_merge。
fetch_shard_started
用于 TransportNodesActiono。
fetch_shard_store
用于 TransportNod巳sListShardStoreMetaData。
线程池和队列的大小可以通过配置文件进行调整,例如,为 search增加线程数和队列大小:
thread.pool.search.size : 30
设置线程
νo 密集型任务的线程数可以稍大一些,因为 1/0 密集型任务大部分时间阻塞在 110 过程, 适当增加线程数可以增加并发处理能力。而上下文切换的代价相对来说已经不那么敏感 。 但是 线程数量不一定设置为 2N+l,具体需要看 I/O 等待时间有多长。等待时间越长,需要越多的线 程,等待时间越少,需要越少的线程。因此也可以参考下面的公式:
最佳线程数= ((线程等待时间+线程CPU执行时间)/线程CPU执行时间) * cpu数
Cache缓存
NodeQueryChache
早期版本也叫做filter cache。 Node级别,被所有shard共享。 主要用于缓存在filter上下文里执行的Query,比如Range Query。 缓存的是压缩过的bitset,对应满足Query条件的docID列表。 这个缓存有一些智能的地方: 比如不是所有的Query都会被缓存,而是记录最近256个Query,只有重用过的,才会被挑选出来放到缓存; 对于小的segment会跳过不缓存; 5.1 以后对于term query完全不缓存; 新写入的文档会增量加入到bitset,而无需反复重建整个bitset。
ShardRequestCahe
shard级别的缓存。 主要用于缓存size=0的聚合信息,
Fielddata Cache
不是用于缓存Aggs,而是缓存所谓的field data。 早期版本,ES没有doc values这样的数据结构,只有倒排索引。 做数据聚合和排序的时候,需要将倒排索引的数据读取出来,重新组织成一个根据指定字段,按照文档序排列的值列表。 之后才能够高效的做排序和聚合计算。由于这个转换工作很耗资源,转换好的列表就会被缓存到fielddata cache,提升速度。 但是因为这个cache是在heap内部的,海量数据聚合的时候,生成的这些fielddata可能heap都放不下,很容易引起性能问题,甚至JVM OOM。 从ES 2.0开始,提供了doc values特性,将field data的构建放在了index time,并且这些数据直接放到磁盘上,通过memory mapped file的方式来访问。 使得海量数据的聚合可以有效利用堆外内存,性能和稳定性都有提高。 因此支持doc values特性的字段类型,比如keyword, 数值型等等,不会再用到fielddata cache。 由于text字段没有doc values支持,所以对text类型字段做排序和聚合的时候,依然会构造field data,填充到cache里。
OSPageCache(文件系统缓存)
ES对于索引的访问是通过memory mapped file来访问的,经常访问的segment,只要没有合并,再次访问时可以直接从page cache里读取。 所以索引里被经常访问的热数据片段,等同于内存读取。
Global Ordinals
用于加速排序和Terms aggregation。这个数据的构建类似field data比较繁重,因此也会被缓存起来。 其没有专门的cache空间,而是放在Fielddata Cache。
如果喜欢搜索技术请来我的公众号吧 ‘Lucene Elasticsearch 工作积累’
每天会持续更新搜索相关技术