本篇开始将会详细解释 Node 实例化的过程,从 Node 实例化这个操作为源点,了解 ElasticSearch 的编码思想, 由于 Node 内容众多,所以会分篇叙述.
Node 概览
前不久的分析中说到了,Node 是 ElasticSearch 启动的重中之重,一个 Node 代表在一个集群 (cluster.name) 中的一个节点.为了使用客户端对集群进行操作,客户端可以使用 Node 中的 client() 来取得 org.elasticsearch.client.Client 的实例.
任何时候,启动一个 elasticsearch 实例都是启动 Node 的一个实例,多个 Node 实例的集合叫做 Cluster.
集群中的节点默认都可以使用 HTTP 和 Transport 两种方法通信.transport 的通信可以使用 Java TransportClient,而 HTTP 就只能使用 Rest Client 了.
集群中的 Node 都能相互发现,并转发请求到合适节点.而且每个 Node 会有以下的一个或多个作用:
通过设定 node.master 属性值为 true(true 为默认值)被选举为 Master 节点
通过设定 node.Data 属性值为 true(true 为默认值)来充当数据节点,顾名思义,这种节点持有数据且能做数据的关联操作
通过设定 node.ingest 属性值为 true(true 为默认值)来充当 ingest node.ingest node 是 5.0 新增的特性,简单点说就是 elasticsearch 内置的数据处理器,目前提供了 convert,grok 之类的操作,相信用过 Logstash 的同学一定不会陌生.
通过设置 tribe. 属性来使 node 成为 Tribe node*,它是一个特殊的客户端,它可以连接多个集群,在所有连接的集群上执行搜索和其他操作
Node 类首先构造了三个 Setting 属性,分别是:
属性名 | key 值 | 作用 |
---|---|---|
WRITE_PORTS_FILE_SETTING | node.portsfile | 用于控制是否将文件写入到包含给定传输类型端口的日志目录中 |
NODE_DATA_SETTING | node.data | 使该 node 被选举为 data 节点 |
NODE_MASTER_SETTING | node.master | 使该 node 被选举为 master 节点 |
NODE_INGEST_SETTING | node.ingest | 使该 node 被选举为 ingest 节点 |
NODE_LOCAL_STORAGE_SETTING | local_storage | 控制节点是否需要持久化元数据到磁盘,这和 data node 没有必然联系,但是如果 local_storage 为 false,node.data 和 node.master 的值必须为 false |
NODE_NAME_SETTING | node.name | 节点名称 |
NODE_ATTRIBUTES | node.attr. | 添加 gateway,zone,rack_id 等参数 key |
BREAKER_TYPE_KEY | indices.breaker.type | 断路器类型,提供参数有 hierarchy,none 两种,主要是防止内存溢出后 elasticsearch 宕机 |
Node 实例化
三个 Node 的构造参数:
Node 的构造参数
最重要的构造方法是:
protected Node(final Environment environment, Collection < Class < ?extends Plugin >> classpathPlugins)
该构造方法所做的工作:
用当前节点名称设定临时 Logger(因为后续可能节点名称会变动所以设定成临时 Logger)
根据参数 environment 中的 settings 变量构造新的 settings 实例,添加默认的 CLIENT_TYPE="node" 值.
用生成的新的 settings 实例和 environment 参数构建新的节点环境(NodeEnvironment)
构造 plugins
加载 LocalNodeFactory
构造 ThreadPool,接收参数为 setting 和 plugins 的 builder
构造 scriptModule,analysisModule,settingsModule
通过 pluginsService 构造 NetworkService
通过 pluginsService 构造 ClusterPugins
构造 IngestService
构造 DiskThresholdMonitor
构造 ClusterInfoService
构造 UsageService
实例化 ModulesBuilder
通过 pluginsService 构造 SearchModule
通过 settingsModule 构造 CircuitBreakerService
构造 ActionModule
构造 NamedXContentRegistry
构造 MetaStateService
构造 IndicesService
构造 RestController
构造 NetworkModule
构造 MetaDataUpgrader
构造 TransportService
构造 ResponseCollectorService
构造 SearchTransportService
构造 DiscoveryModule
构造 NodeService
向构造好的 ModuleBuilder 中添加所有需要的服务
通过 ModuleBuilder 得到 Guice 注入类
构件 LifecycleComponent 集合
初始化 NodeClient
我们的源码解析也会按照这个流程来开展.
构建默认的 Setting
在 Node 刚开始构造的时候,这个时候 Node 对象中还没有存在 Setting 实例的,有的配置只有在 BootStrap 方法中传过来的 Environment 实例,这个 Envi 的实例(environment)其实就是解析了启动环境中若干的配置路径(lib 路径,module 路径,logs 路径),在对 environment 的 setting 化后(调用 Environment 的 settings() 方法,就是对初始的环境变量标准化为 Settings 类型的对象),如下图:
Environment 的 settings() 方法
在构造完这个最初始版本的 Settings 后,代码视图取得配置中的 node.name,为什么会在 Node 刚开始初始化的时候就去查找 node 的 name 呢?在跟进源码后会知道,ElasticSearch 这么做是为了给 Logger 的实例增加 marker 这个参数,相信对 log4j 熟悉的同学会对这个参数很熟悉,merker 是 log4j 中 LayoutPattern 的参数之一,作用是 event 元素中的标记元素,这种标记元素仅在日志消息中使用标记时出现,且具有继承性.如下图:
logger 中的 marker 元素
当然如果配置了 node.name, 且在 log4j.properties 中配置了属性 appender.console.layout.pattern 包含元素 %marker,那么在控制台中会很容易看到形如下图中的日志打印,这就能很容易区分出日志的归属 Node.
logger 中的 marker
当然到这里我们都还没给 Node 设置名称.
接下来给 Node 设置了 client.type 的值为 node,这个也是写在代码里的配置.
private static final String CLIENT_TYPE = "node";
接下来开始就开始构建 NodeEnvironment 实例了.
NodeEnvironment 的实例化
首先说明 Environment 和 NodeEnvironment 是没有任何继承关系的,只是在 NodeEnvironment 的实例化过程中,Environment 作为了构建所必需的参数.NodeEnvironment 主要是针对单个节点的包含所有数据路径的构件对象,说白了这个类就是 xxx,直接看 NodeEnvironment 构造函数.构造函数中通过累加 possibleLockId 的值来新增数据存储的路径,这个值是从 0 开始的,所以才会在 ElasticSearch 的数据存储页面生成如下图的文件夹:
数据存储路径
接下来使用
FSDirectory.open(dir, NativeFSLockFactory.INSTANCE)
获取存储索引的目录,FSDirectory 是对文件系统目录的操作
第一个参数 java.nio.file.Path:dir 这个参数是 NIO 的一个类 Path,接收字符串参数创建的.
第二个参数 org.apache.lucene.store.LockFactory:这个参数是 Lucene 中的索引锁.因为 Lucene 必须知道一份索引是否已经被某个 IndexWriter 打开,所以必须使用锁的机制来保证写索引的同步性.首先大家要明确一个问题,在 ElasticSearch 异常退出,或是 JVM 异常关闭的情况下,在下次重启 ElasticSearch,索引依然能够正确读写,就是这么神奇.这是怎么实现的呢?秘密就在这个 NativeFSLockFactory.INSTANCE 参数中,他是 FSDirectory 提供的默认锁,他的最大优势就是当程序异常退出后,可以由操作系统负责解除索引的锁,操作系统会释放文件上所有的引用,以确保索引可以正确读写.LockFactory 还提供了其他类型的锁,由于涉及到 Lucene 的深层次知识点,这里就不展开叙述.
通过
locks[dirIndex] = luceneDir.obtainLock(NODE_LOCK_FILENAME)
;取得锁后生成一个内部类 NodePath 的实例,到这里锁就持久化到磁盘上了.
node.lock
补充一句,这个地方涉及到了 ElasticSearch 的参数 max_local_storage_nodes,这个配置限制了单节点上可以开启的 ES 存储实例的个数,如果我们需要开多个实例,就要把这个配置写到配置文件中,并为这个配置赋值为 2 或者更高,这样的话 ElasticSearch 就会用 for 循环创建多个 NodePath,而不只是创建唯一的那个 ID 为 0 的实例.
在 NodeEnvironment 中加载或创建 Node 元数据
接下类是构造 NodeMetaData 节点元数据,这个元数据有个关键数据叫 nodeId,构造出来后是形如 D2_COg3LTUeQcrYjcj_fQQ 这样的字符串.
程序执行到这个地方,其内部类 NodePath 的对象里已经保存了节点目录 xxxx\data\nodes\0 和节点索引目录 xxxx\data\nodes\0\indices,如下图所示:
NodePath 实例
程序首先通过
DirectoryStream < Path > paths = Files.newDirectoryStream(stateDir)
遍历 data\nodes\0_state 文件夹下的状态文件,再通过匹配正则表达式
\Qnode - \E(\d + )(.st) ?
,查找到状态文件 node-xxx.st.
注意,如果有多个数据存储路径,那么状态文件夹下可能会有多个最新状态版本.这种情况下,只会取最高的版本.如果至少有一个状态文件使用了新的格式(format,也就是编码中的 legacy==false),那么最新的状态文件肯定是最新的的格式(format).如果不是使用最新的状态文件,那编码中的 pathAndStateIds 值是空的,且会在日志中报加载状态文件失败的错误.
状态文件
最后从 node-xxx.st 文件中读出 ID,至此 NodeMetaData 对象的 nodeId 字段就被赋值了.而这个 ID 的前缀也被作为 Logger 的 marker 值被注入.
至此
nodeEnvironment = new NodeEnvironment(tmpSettings, environment);
的工作就结束了,总而言之就是载入了状态参数到内存中.
来源: http://www.jianshu.com/p/5ffb64c92850