Elasticsearch源码解析与优化实战(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-21 05:55:23

点击下载

作者:张超

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Elasticsearch源码解析与优化实战

Elasticsearch源码解析与优化实战试读:

前言

我们可以在不关心原理的情况下使用Elasticsearch(以下简称ES),但要想用好ES,就必须熟知其内部原理。

为什么要阅读代码?在传统软件行业,技术文档非常丰富。当开展一个项目时,从需求分析,到概要设计、详细设计,每个步骤都有相应的文档,从项目的整体架构、技术方案选型,到流程图、类图,细化到每个接口及参数。在这种情况下,想要搞清楚系统原理,并不需要阅读代码,文档上什么都有。但是互联网产品迭代快,技术文档不全,想要搞清楚原理,只能阅读代码,相当于从代码中逆向理解设计思想。

通过分析源码,我们可以有以下收获:

理解设计思想 当我们面临要解决的问题或实现的目标时,往往有多种方案可以选择。无论表面上看起来多么简单的架构,其背后都经过了深思熟虑。思考一下为什么使用现在的方案?有没有更好的解决方案?

探究内部机制的原理 某个技术点是怎么实现的?

搞明白执行流程 某个过程是什么样的,都做了什么?有几步?先做什么,后做什么?

熟悉代码结构 如果需要进行二次开发,则给出代码入口和调用关系,有时候找到某个逻辑的代码实现要花很多时间。

学以致用 借鉴其设计理念,掌握其解决问题的方式和方法,将来面对类似的问题时可以参考。本书结构

本书由四部分组成,第一部分为基础知识和环境准备(第1~2章);第二部分介绍 ES 的主要流程(第3~10章),包括集群启动流程、节点启动/关闭流程、选主流程、读写流程、搜索流程和索引恢复流程;第三部分主要介绍重要内部模块(第11~17章),包括gateway模块、allocation模块、Snapshot模块、Cluster模块、Transport模块和ThreadPool模块等;第四部分介绍优化和诊断方法(第18~22章),包括写入速度优化、搜索速度优化、磁盘使用量优化,以及在生产环境中的实际应用建议,第22章介绍常用的问题诊断方法,排查集群遇到的问题。术语约定

ES中有一些特有的概念,这些概念对应的中文翻译约定如下:

· 分片(shard);

· 主分片(primary shard),简称P;

· 分片副本(特指数据的一个分片,无论主分片,还是副分片);

· 副分片(replica shard),简称R;

· 分片分配(shard allocation);

· 集群状态(cluster state);

· 分配决策(allocation decision);

· 分配感知(allocation awareness);

· 分配标识(allocation IDs);

· 追踪(tracking);

· 事务日志(translog);

· 同步集合(in-sync set)。行文约定

虽然本书是一本源码分析类图书,但原则上尽量少贴代码,引用的代码只是为了说明原理,因此所引用的代码并不保证和源码完全一致,对非核心逻辑有所删减,同时在代码块中,函数参数可能被省略,省略的函数参数用“…”表示,如:

executeBulk(...);

在引用代码中的某个方法时,使用#号分隔类名与方法名:

类名#方法名

一个索引由许多分片组成。我们用如下方式表示索引website的第0个分片:

website[0]联系

读者有任何意见和建议都可以联系作者,邮箱:elasticsearchbook@163.com。

本书配套网站:www.elasticsearchbook.cn。致谢

感谢李欣杰和郭东东,他们带我走进搜索领域;感谢韩洪伟,他让我学到了很多搜索系统的知识。欣杰和老韩都是资深的搜索架构师,能够和优秀的团队共事是我的荣幸。感谢 ES 团队的同事段军义,我们互相学习,一起解决了很多麻烦的问题。感谢出版社的策划编辑陈晓猛先生,他为本书的写作提供了很多建设性意见,并且耐心地编校了本书,让本书得以顺利出版。

感谢我的妻子和三岁的女儿,我爱你们!张超第1章 走进Elasticsearch

本章主要介绍入门知识,对Elasticsearch基本概念已经很熟悉的读者可以跳过本章。1.1 基本概念和原理

Elasticsearch是实时的分布式搜索分析引擎,内部使用Lucene做索引与搜索。

何谓实时?新增到 ES 中的数据在1秒后就可以被检索到,这种新增数据对搜索的可见性称为“准实时搜索”。分布式意味着可以动态调整集群规模,弹性扩容,而这一切操作起来都非常简便,用户甚至不必了解集群原理就可以实现。按官方的描述,集群规模支持“上百”个节点,相比HDFS等上千台的集群,这个规模“小了点”。影响集群规模上限的原因将在后续的章节中分析。因此,目前我们认为ES适合中等数据量的业务,不适合存储海量数据。

Lucene是Java语言编写的全文搜索框架,用于处理纯文本的数据,但它只是一个库,提供建立索引、执行搜索等接口,但不包含分布式服务,这些正是 ES 做的。什么是全文?对全部的文本内容进行分析,建立索引,使之可以被搜索,因此称为全文。

基于ES,你可以很容易地搭建自己的搜索引擎,用于分析日志,或者配合开源爬虫建立某个垂直领域的搜索引擎。ES 易用的产品设计使得它很容易上手。除了搜索,ES 还提供了大量的聚合功能,所以它不单单是一个搜索引擎,还可以进行数据分析、统计,生成指标数据。而这些功能都在快速迭代,目前每2周左右就会发布新版本。1.1.1 索引结构

ES是面向文档的。各种文本内容以文档的形式存储到ES中,文档可以是一封邮件、一条日志,或者一个网页的内容。一般使用 JSON 作为文档的序列化格式,文档可以有很多字段,在创建索引的时候,我们需要描述文档中每个字段的数据类型,并且可能需要指定不同的分析器,就像在关系型数据中“CREATE TABLE”一样。

在存储结构上,由_index、_type和_id唯一标识一个文档。

_index指向一个或多个物理分片的逻辑命名空间,_type类型用于区分同一个集合中的不同细分,在不同的细分中,数据的整体模式是相同或相似的,不适合完全不同类型的数据。多个_type可以在相同的索引中存在,只要它们的字段不冲突即可(对于整个索引,映射在本质上被“扁平化”成一个单一的、全局的模式)。_id文档标记符由系统自动生成或使用者提供。

很多初学者喜欢套用RDBMS中的概念,将_index理解为数据库,将_type理解为表,这是很牵强的理解,实际上这是完全不同的概念,没什么相似性,不同_type下的字段不能冲突,删除整个_type也不会释放空间。在实际应用中,数据模型不同,有不同_type需求的时候,我们应该建立单独的索引,而不是在一个索引下使用不同的_type。删除过期老化的数据时,最好以索引为单位,而不是_type和_id。正由于_type在实际应用中容易引起概念混淆,以及允许索引存在多_type并没有什么实际意义,在ES 6.x版本中,一个索引只允许存在一个_type,未来的7.x版本将完全删除_type的概念。1.1.2 分片(shard)

在分布式系统中,单机无法存储规模巨大的数据,要依靠大规模集群处理和存储这些数据,一般通过增加机器数量来提高系统水平扩展能力。因此,需要将数据分成若干小块分配到各个机器上。然后通过某种路由策略找到某个数据块所在的位置。

除了将数据分片以提高水平扩展能力,分布式存储中还会把数据复制成多个副本,放置到不同的机器中,这样一来可以增加系统可用性,同时数据副本还可以使读操作并发执行,分担集群压力。但是多数据副本也带来了一致性的问题:部分副本写成功,部分副本写失败。我们随后讨论。

为了应对并发更新问题,ES将数据副本分为主从两部分,即主分片(primary shard)和副分片(replica shard)。主数据作为权威数据,写过程中先写主分片,成功后再写副分片,恢复阶段以主分片为准。

数据分片和数据副本的关系如下图所示。

分片(shard)是底层的基本读写单元,分片的目的是分割巨大索引,让读写可以并行操作,由多台机器共同完成。读写请求最终落到某个分片上,分片可以独立执行读写工作。ES利用分片将数据分发到集群内各处。分片是数据的容器,文档保存在分片内,不会跨分片存储。分片又被分配到集群内的各个节点里。当集群规模扩大或缩小时,ES 会自动在各节点中迁移分片,使数据仍然均匀分布在集群里。

索引与分片的关系如下图所示。

一个ES索引包含很多分片,一个分片是一个Lucene的索引,它本身就是一个完整的搜索引擎,可以独立执行建立索引和搜索任务。Lucene索引又由很多分段组成,每个分段都是一个倒排索引。ES每次“refresh”都会生成一个新的分段,其中包含若干文档的数据。在每个分段内部,文档的不同字段被单独建立索引。每个字段的值由若干词(Term)组成,Term是原文本内容经过分词器处理和语言处理后的最终结果(例如,去除标点符号和转换为词根)。

如果想了解Lucene分段由哪些文件组成,每个文件都存储了什么内容,则可以参考Apache Lucene 的手册:http://lucene.apache.org/core/7_3_0/core/org/apache/lucene/codecs/lucene70/package-summary.html#package.description。

索引建立的时候就需要确定好主分片数,在较老的版本中(5.x 版本之前),主分片数量不可以修改,副分片数可以随时修改。现在(5.x~6.x 版本之后),ES 已经支持在一定条件的限制下,对某个索引的主分片进行拆分(Split)或缩小(Shrink)。但是,我们仍然需要在一开始就尽量规划好主分片数量:先依据硬件情况定好单个分片容量,然后依据业务场景预估数据量和增长量,再除以单个分片容量。

分片数不够时,可以考虑新建索引,搜索1个有着50个分片的索引与搜索50个每个都有1个分片的索引完全等价,或者使用_split API来拆分索引(6.1版本开始支持)。

在实际应用中,我们不应该向单个索引持续写数据,直到它的分片巨大无比。巨大的索引会在数据老化后难以删除,以_id 为单位删除文档不会立刻释放空间,删除的 doc 只在 Lucene分段合并时才会真正从磁盘中删除。即使手工触发分段合并,仍然会引起较高的 I/O 压力,并且可能因为分段巨大导致在合并过程中磁盘空间不足(分段大小大于磁盘可用空间的一半)。因此,我们建议周期性地创建新索引。例如,每天创建一个。假如有一个索引website,可以将它命名为website_20180319。然后创建一个名为website的索引别名来关联这些索引。这样,对于业务方来说,读取时使用的名称不变,当需要删除数据的时候,可以直接删除整个索引。

索引别名就像一个快捷方式或软链接,不同的是它可以指向一个或多个索引。可以用于实现索引分组,或者索引间的无缝切换。

现在我们已经确定好了主分片数量,并且保证单个索引的数据量不会太大,周期性创建新索引带来的一个新问题是集群整体分片数量较多,集群管理的总分片数越多压力就越大。在每天生成一个新索引的场景中,可能某天产生的数据量很小,实际上不需要这么多分片,甚至一个就够。这时,可以使用_shrink API来缩减主分片数量,降低集群负载。1.1.3 动态更新索引

为文档建立索引,使其每个字段都可以被搜索,通过关键词检索文档内容,会使用倒排索引的数据结构。倒排索引一旦被写入文件后就具有不变性,不变性具有许多好处:对文件的访问不需要加锁,读取索引时可以被文件系统缓存等。

那么索引如何更新,让新添加的文档可以被搜索到?答案是使用更多的索引,新增内容并写到一个新的倒排索引中,查询时,每个倒排索引都被轮流查询,查询完再对结果进行合并。

每次内存缓冲的数据被写入文件时,会产生一个新的Lucene段,每个段都是一个倒排索引。在一个记录元信息的文件中描述了当前Lucene索引都含有哪些分段。

由于分段的不变性,更新、删除等操作实际上是将数据标记为删除,记录到单独的位置,这种方式称为标记删除。因此删除部分数据不会释放磁盘空间。1.1.4 近实时搜索

在写操作中,一般会先在内存中缓冲一段数据,再将这些数据写入硬盘,每次写入硬盘的这批数据称为一个分段,如同任何写操作一样。一般情况下(direct方式除外),通过操作系统write接口写到磁盘的数据先到达系统缓存(内存),write函数返回成功时,数据未必被刷到磁盘。通过手工调用flush,或者操作系统通过一定策略将系统缓存刷到磁盘。这种策略大幅提升了写入效率。从write函数返回成功开始,无论数据有没有被刷到磁盘,该数据已经对读取可见。

ES正是利用这种特性实现了近实时搜索。每秒产生一个新分段,新段先写入文件系统缓存,但稍后再执行flush刷盘操作,写操作很快会执行完,一旦写成功,就可以像其他文件一样被打开和读取了。

由于系统先缓冲一段数据才写,且新段不会立即刷入磁盘,这两个过程中如果出现某些意外情况(如主机断电),则会存在丢失数据的风险。通用的做法是记录事务日志,每次对ES进行操作时均记录事务日志,当ES启动的时候,重放translog中所有在最后一次提交后发生的变更操作。比如HBase等都有自己的事务日志。1.1.5 段合并

在ES中,每秒清空一次写缓冲,将这些数据写入文件,这个过程称为refresh,每次refresh会创建一个新的Lucene 段。但是分段数量太多会带来较大的麻烦,每个段都会消耗文件句柄、内存。每个搜索请求都需要轮流检查每个段,查询完再对结果进行合并;所以段越多,搜索也就越慢。因此需要通过一定的策略将这些较小的段合并为大的段,常用的方案是选择大小相似的分段进行合并。在合并过程中,标记为删除的数据不会写入新分段,当合并过程结束,旧的分段数据被删除,标记删除的数据才从磁盘删除。

HBase、Cassandra等系统都有类似的分段机制,写过程中先在内存缓冲一批数据,不时地将这些数据写入文件作为一个分段,分段具有不变性,再通过一些策略合并分段。分段合并过程中,新段的产生需要一定的磁盘空间,我们要保证系统有足够的剩余可用空间。Cassandra系统在段合并过程中的一个问题就是,当持续地向一个表中写入数据,如果段文件大小没有上限,当巨大的段达到磁盘空间的一半时,剩余空间不足以进行新的段合并过程。如果段文件设置一定上限不再合并,则对表中部分数据无法实现真正的物理删除。ES存在同样的问题。1.2 集群内部原理

分布式系统的集群方式大致可以分为主从(Master-Slave)模式和无主模式。ES、HDFS、HBase使用主从模式,Cassandra使用无主模式。主从模式可以简化系统设计,Master作为权威节点,部分操作仅由Master执行,并负责维护集群元信息。缺点是Master节点存在单点故障,需要解决灾备问题,并且集群规模会受限于Master节点的管理能力。

因此,从集群节点角色的角度划分,至少存在主节点和数据节点,另外还有协调节点、预处理节点和部落节点,下面分别介绍各种类型节点的职能。1.2.1 集群节点角色1. 主节点(Master node)

主节点负责集群层面的相关操作,管理集群变更。

通过配置 node.master: true(默认)使节点具有被选举为 Master 的资格。主节点是全局唯一的,将从有资格成为Master的节点中进行选举。

主节点也可以作为数据节点,但尽可能做少量的工作,因此生产环境应尽量分离主节点和数据节点,创建独立主节点的配置:

node.master: true

node.data: false

为了防止数据丢失,每个主节点应该知道有资格成为主节点的数量,默认为1,为避免网络分区时出现多主的情况,配置 discovery.zen.minimum_master_nodes原则上最小值应该是:(master_eligible_nodes / 2)+ 12. 数据节点(Data node)

负责保存数据、执行数据相关操作:CRUD、搜索、聚合等。数据节点对CPU、内存、I/O要求较高。一般情况下(有一些例外,后续章节会给出),数据读写流程只和数据节点交互,不会和主节点打交道(异常情况除外)。

通过配置node.data: true(默认)来使一个节点成为数据节点,也可以通过下面的配置创建一个数据节点:

node.master: false

node.data: true

node.ingest: false3. 预处理节点(Ingest node)

这是从5.0版本开始引入的概念。预处理操作允许在索引文档之前,即写入数据之前,通过事先定义好的一系列的processors(处理器)和pipeline(管道),对数据进行某种转换、富化。processors和pipeline拦截bulk和index请求,在应用相关操作后将文档传回给index或bulk API。

默认情况下,在所有的节点上启用ingest,如果想在某个节点上禁用ingest,则可以添加配置node.ingest: false,也可以通过下面的配置创建一个仅用于预处理的节点:

node.master: false

node.data: false

node.ingest: true4. 协调节点(Coordinating node)

客户端请求可以发送到集群的任何节点,每个节点都知道任意文档所处的位置,然后转发这些请求,收集数据并返回给客户端,处理客户端请求的节点称为协调节点。

协调节点将请求转发给保存数据的数据节点。每个数据节点在本地执行请求,并将结果返回协调节点。协调节点收集完数据后,将每个数据节点的结果合并为单个全局结果。对结果收集和排序的过程可能需要很多CPU和内存资源。

通过下面的配置创建一个仅用于协调的节点:

node.master: false

node.data: false

node.ingest: false5. 部落节点(Tribe node)

tribes(部落)功能允许部落节点在多个集群之间充当联合客户端。

在ES 5.0之前还有一个客户端节点(Node Client)的角色,客户端节点有以下属性:

node.master: false

node.data: false

它不做主节点,也不做数据节点,仅用于路由请求,本质上是一个智能负载均衡器(从负载均衡器的定义来说,智能和非智能的区别在于是否知道访问的内容存在于哪个节点),从5.0版本开始,这个角色被协调节点(Coordinating only node)取代。1.2.2 集群健康状态

从数据完整性的角度划分,集群健康状态分为三种:

· Green,所有的主分片和副分片都正常运行。

· Yellow,所有的主分片都正常运行,但不是所有的副分片都正常运行。这意味着存在单点故障风险。

· Red,有主分片没能正常运行。

每个索引也有上述三种状态,假设丢失了一个副分片,该分片所属的索引和整个集群变为Yellow状态,其他索引仍为Green。1.2.3 集群状态

集群状态元数据是全局信息,元数据包括内容路由信息、配置信息等,其中最重要的是内容路由信息,它描述了“哪个分片位于哪个节点”这种信息。

集群状态由主节点负责维护,如果主节点从数据节点接收更新,则将这些更新广播到集群的其他节点,让每个节点上的集群状态保持最新。ES 2.0版本之后,更新的集群状态信息只发增量内容,并且是被压缩的。1.2.4 集群扩容

当扩容集群、添加节点时,分片会均衡地分配到集群的各个节点,从而对索引和搜索过程进行负载均衡,这些都是系统自动完成的。

分片副本实现了数据冗余,从而防止硬件故障导致的数据丢失。

下面演示了当集群只有一个节点,到变成两个节点、三个节点时的shard迁移过程示例(图片来自官网)。

起初,在NODE1上有三个主分片,没有副分片,如下图所示。

其中,P代表Primary shard;R代表Replica shard。以后出现的内容使用相同的简称。

添加第二个节点后,副分片被分配到NODE2,如下图所示。

添加第三个节点后,索引的六个分片被平均分配到集群的三个节点,如下图所示。

分片分配过程中除了让节点间均匀存储,还要保证不把主分片和副分片分配到同一节点,避免单个节点故障引起数据丢失。

分布式系统中难免出现故障,当节点异常时,ES会自动处理节点异常。当主节点异常时,集群会重新选举主节点。当某个主分片异常时,会将副分片提升为主分片。1.3 客户端API

当需要实现一个客户端对集群进行读写操作时,可以选择REST接口、Java REST API,或者Java API。

Java REST API是对原生REST接口的封装。REST接口、Java REST API使用9200端口通信,采用JSON over HTTP方式,Java API使用9300端口通信,数据序列化为二进制。

使用Java API理论上来说效率更高一些,但是后来官方发现实际上相差无几,但是版本迭代中却因为Java API向下兼容性的限制不得不做出许多牺牲,Java API带来的微弱效率优势远不及带来的缺点。ES不是高QPS的应用,写操作非常消耗CPU资源,因此写操作属于比较长的操作,聚合由于涉及数据量比较大,延迟也经常到秒级,查询一般也不密集。因此RPC框架的效率没有那么高的要求。后续Java API将逐渐被Java REST API取代。官方计划从ES 7.0开始不建议使用Java API,并且从8.0版本开始完全移除。1.4 主要内部模块简介

在分析内部模块流程之前,我们先了解一下ES中几个基础模块的功能。Cluster

Cluster模块是主节点执行集群管理的封装实现,管理集群状态,维护集群层面的配置信息。主要功能如下:

· 管理集群状态,将新生成的集群状态发布到集群所有节点。

· 调用allocation模块执行分片分配,决策哪些分片应该分配到哪个节点

· 在集群各节点中直接迁移分片,保持数据平衡。allocation

封装了分片分配相关的功能和策略,包括主分片的分配和副分片的分配,本模块由主节点调用。创建新索引、集群完全重启都需要分片分配的过程。Discovery

发现模块负责发现集群中的节点,以及选举主节点。当节点加入或退出集群时,主节点会采取相应的行动。从某种角度来说,发现模块起到类似ZooKeeper的作用,选主并管理集群拓扑。gateway

负责对收到Master广播下来的集群状态(cluster state)数据的持久化存储,并在集群完全重启时恢复它们。Indices

索引模块管理全局级的索引设置,不包括索引级的(索引设置分为全局级和每个索引级)。它还封装了索引数据恢复功能。集群启动阶段需要的主分片恢复和副分片恢复就是在这个模块实现的。HTTP

HTTP模块允许通过JSON over HTTP的方式访问ES的API,HTTP模块本质上是完全异步的,这意味着没有阻塞线程等待响应。使用异步通信进行 HTTP 的好处是解决了 C10k 问题(10k量级的并发连接)。

在部分场景下,可考虑使用HTTP keepalive以提升性能。注意:不要在客户端使用HTTP chunking。Transport

传输模块用于集群内节点之间的内部通信。从一个节点到另一个节点的每个请求都使用传输模块。

如同HTTP模块,传输模块本质上也是完全异步的。

传输模块使用 TCP 通信,每个节点都与其他节点维持若干 TCP 长连接。内部节点间的所有通信都是本模块承载的。Engine

Engine模块封装了对Lucene的操作及translog的调用,它是对一个分片读写操作的最终提供者。

ES使用Guice框架进行模块化管理。Guice是Google开发的轻量级依赖注入框架(IoC)。

软件设计中经常说要依赖于抽象而不是具象,IoC 就是这种理念的实现方式,并且在内部实现了对象的创建和管理。1.4.1 模块结构

在Guice框架下,一个典型的模块由Service和Module类(类名可以自由定义)组成,Service用于实现业务功能,Module类中配置绑定信息。

以ClusterModule为例,类结构如下图所示。

AbstractModule是Guice提供的基类,模块需要从这个类继承。Module类的主要作用是定义绑定关系,例如:

protected void configure() {

//绑定实现类

bind(ClusterService.class).toInstance(clusterService);

}1.4.2 模块管理

定义好的模块由ModulesBuilder类统一管理,ModulesBuilder是ES对Guice的封装,内部调用Guice接口,主要对外提供两个方法。

· add方法:添加创建好的模块

· createInjector方法:调用Guice.createInjector创建并返回Injector,后续通过Injector获取相应Service类的实例。

使用ModulesBuilder进行模块管理的伪代码示例:

ModulesBuilder modules = new ModulesBuilder();

//以Cluster模块为例

ClusterModule clusterModule = new ClusterModule();

modules.add(clusterModule);

//省略其他模块的创建和添加

...

//创建Injector,并获取相应类的实例

injector = modules.createInjector();

setGatewayAllocator(injector.getInstance(GatewayAllocator.class)

模块化的封装让 ES 易于扩展,插件本身也是一个模块,节点启动时被模块管理器添加进来。第2章 准备编译和调试环境2.1 编译源码2.1.1 准备JDK和Gradle

Elasticsearch是Java语言编写的。运行和编译Elasticsearch时,对JDK版本的选择请参考手册,地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/_installation.html。推荐使用Oracle的JDK版本。本书使用的JDK版本为1.8.0_121。JDK的安装方式不在这里讨论,请读者参考Oracle官方网站。

从Elasticsearch 5.0开始,构建工具由Maven更改为Gradle,本书使用的Gradle版本为4.6,安装方式请参考官方网站。2.1.2 下载源代码

Elasticsearch的代码托管于GitHub,可以“git clone”最新的Master分支或某个tag,也可以到Releases或Tags下载各个版本的代码包:https://github.com/elastic/elasticsearch/releases。

解压源码包到自己的目录,以v6.1.2版本为例:

tar -xzvf elasticsearch-6.1.2.tar.gz2.1.3 编译项目,打包

本书以Linux平台编译为例。切换到解压后的源码目录,执行编译:

cd elasticsearch-6.1.2

./gradlew assemble

编译完成后,系统打印“BUILD SUCCESSFUL”代表编译成功。完整的打包文件位于./distribution目录,包含tar、zip等多种格式的包。以tar为例,包路径为./distribution/tar/build/distributions/ elasticsearch-6.1.2-SNAPSHOT.tar.gz。默认带有“SNAPSHOT”后缀,如果想去掉“SNAPSHOT”,则可以在编译时添加-Dbuild.snapshot=false,即:

./gradlew assemble -Dbuild.snapshot=false

Elasticsearch的版本号由四位组成,格式为:

主版本.次版本.修正版本.构建版本

但是最后一位“构建版本”不出现在构建后的包名称中。默认情况下每个号码均不会大于100。当完成对代码的修改,编译、发布时要修改版本号,添加自己的版本号,可以在原修正版本或构建版本的基础上增加。与修改版本号相关的两个文件:buildSrc/version.properties和./core/src/main/java/org/elasticsearch/Version.java。文件位置可能因版本不同而有所不同。2.1.4 将工程导入IntelliJ IDEA

本书使用的IDE以IntelliJ IDEA 2017.3(后续简称IDEA)为例。进入Elasticsearch源码根目录,执行:

gradle idea

生成IntelliJ的项目文件,正常完成会显示“BUILD SUCCESSFUL”。

首先配置SDK。打开IntelliJ IDEA,在欢迎界面右下角选择“Configure→Project Defaults→Project Structure”,弹出如下图所示的界面。

在Project SDK下拉列表中确认JDK的版本,必要时选择“New→JDK”,选择JDK Home目录,添加正确的JDK。

回到IntelliJ IDEA欢迎界面,选择“Import Project”,选择Elasticsearch-6.1.2目录,单击“Open”,在Import Project界面选择“Gradle”,如下图所示。

单击“Next”,设置工程名称,并确认Grande home和Grande JVM配置正确,如下图所示。

单击完成按钮,待后台任务运行完,工程导入完毕。2.2 调试Elasticsearch2.2.1 本地运行、调试项目

在IDE环境中运行可能会遇到很多问题,大部分问题都比较浅显且容易处理,有一些较为罕见的问题和Elasticsearch代码版本、IDE版本、JDK有关。对于初学者来说,不要在这些问题上占用太长时间,否则容易让人丧失学习的动力。

在 IntelliJ IDEA 菜单中选择“Run→Edit Configurations”,单击左侧的加号“+”,选择“Application”,在弹出的配置界面中填写相关配置,如下图所示。

· Name:为当前配置取一个名字,此处命名为local,以区别以后的远程调试。

· Main class:填写org.elasticsearch.bootstrap.Elasticsearch。

· Use classpath of module:对于本书使用的版本来说,填写core_main,不同版本可能会有所不同。

· VM options:填写内容为

-Des.path.home=/Users/zhangchao-so/Documents/mybook/eshome -Des.path.conf=/Users/zhangchao-so/Documents/mybook/eshome/config -Xms1g -Xmx1g -Dlog4j2. disable.jmx=true -Djava.security.policy=/Users/zhangchao-so/Documents/mybook/eshome/config/elasticsearch.policy

在解释上述选项之前,我们需要先为调试环境准备运行时要用的eshome目录,Elasticsearch需要从其中加载模块、读取配置,写入数据和日志。在你的环境上选择一个位置建立 eshome目录,名称可以任意。然后复制必要的模块和配置文件到该目录中,可以选择从对应版本的官方二进制包中复制,也可以将上一步编译打包的软件包解压,本书使用后者。切换到源码根目录,执行:

cd distribution/zip/build/distributions/

unzip elasticsearch-6.1.2.zip

cd elasticsearch-6.1.2

cp -r config modules plugins /Users/zhangchao-so/Documents/mybook/eshome

复制完上述文件之后,现在看一下VM options选项,如下表所示。

现在可以单击“运行”或“调试”按钮让程序在IDE中运行起来了!ES可以单节点运行,暂时不用调节任何配置。2.2.2 远程调试

除了在本地环境调试ES,有时需要远程调试某个节点。分布式系统总会遇到很多千奇百怪的问题,很多时候问题无法复现,或者某种问题只出现在特定环境中。在排查问题的过程中,除了查看重要的日志,远程调试节点也是一种不错的手段。

远程调试时,需要保证本地代码与远程环境运行的代码版本一致。在 IDEA 菜单中重新单击“Run→Edit Configurations”,单击左侧的加号“+”,选择“Remote”,弹出如下图所示的配置界面。

配置名称此处命名为remote,复制“Command line arguments for running remote JVM”下的JVM参数到远程节点的jvm.options文件中:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

重启远程节点,成功后日志信息中会有“Listening for transport dt_socket at address: 5005”信息。在Host中填写远程调试对象的IP地址,单击“OK”按钮。

单击Debug启动调试,控制台输出:

Connected to the target VM, address: '10.132.0.20:5005', transport: 'socket'

表示连接成功,接下来在本地代码上设置断点,远程节点运行到相应的逻辑就会停住,接下来就和本地调试一样。如果需要调试启动过程,则可以设置suspendcy,让程序等待调试器连接后再开始执行。

之后就在IDEA工具栏“Select Run/Debug Configuration”下拉框中选择调试本地还是远程节点,如下图所示。

远程调试节点时,停止调试不会终止远程节点进程。2.3 代码书签和断点组

Elasticsearch代码量庞大,模块间关系复杂,为了更好地调试、阅读代码,我们用到IDEA中的两个特性:代码书签和断点组。代码书签可以标记代码位置,并为它取名。断点组可以把某个流程的一系列断点分组保存,统一开闭,例如,在调试recovery流程时,又想回头跟一下写流程,可以很方便地进行切换。

代码书签:光标选中代码行,按F3键为当前行添加书签(这里以Mac平台idea快捷键为例,不同系统的快捷键可能不同,请参考 idea 官方手册)。添加后在行号右侧会多一个对号,按command+F3组合键弹出书签列表对话框,单击左侧的“Edit”按钮为当前书签命名,如下图所示。

断点组:单击菜单栏“Run→View Breakpoints”,弹出断点对话框,右键单击断点,选择“Move to group”,将断点“move”到对应的分组,或者创建新组。在断点组复选框中可以批量启用/禁用整组断点,如下图所示。

在IDEA中断点暂停有两种策略:All和Thread。默认策略为All,在这种策略下,在类的不同方法上设置两个断点,当其中一个断点被命中而暂停时,另一个线程进入第二个断点,IDEA将不会暂停。通常在调试多线程池程序时,我们希望设置的断点,不管任何线程进入都应暂停,因此我们将策略修改为Thread。在断点上单击右键,弹出如下图所示的对话框。

将单选按钮选中“Thread”,然后单击“Make Default”按钮,单击“Done”按钮,将其作为默认策略,但不会对之前设置的断点生效。第3章 集群启动流程

让我们从启动流程开始,先在宏观上看看整个集群是如何启动的,集群状态如何从Red变成Green,不涉及代码,然后分析其他模块的流程。

本书中,集群启动过程指集群完全重启时的启动过程,期间要经历选举主节点、主分片、数据恢复等重要阶段,理解其中原理和细节,对于解决或避免集群维护过程中可能遇到的脑裂、无主、恢复慢、丢数据等问题有重要作用。

集群启动的整体流程如下图所示。3.1 选举主节点

假设有若干节点正在启动,集群启动的第一件事是从已知的活跃机器列表中选择一个作为主节点,选主之后的流程由主节点触发。

ES的选主算法是基于Bully算法的改进,主要思路是对节点ID排序,取ID值最大的节点作为Master,每个节点都运行这个流程。是不是非常简单?选主的目的是确定唯一的主节点,初学者可能认为选举出的主节点应该持有最新的元数据信息,实际上这个问题在实现上被分解为两步:先确定唯一的、大家公认的主节点,再想办法把最新的机器元数据复制到选举出的主节点上。

基于节点ID排序的简单选举算法有三个附加约定条件:(1)参选人数需要过半,达到 quorum(多数)后就选出了临时的主。为什么是临时的?每个节点运行排序取最大值的算法,结果不一定相同。举个例子,集群有5台主机,节点ID分别是1、2、3、4、5。当产生网络分区或节点启动速度差异较大时,节点1看到的节点列表是1、2、3、4,选出4;节点2看到的节点列表是2、3、4、5,选出5。结果就不一致了,由此产生下面的第二条限制。(2)得票数需过半。某节点被选为主节点,必须判断加入它的节点数过半,才确认Master身份。解决第一个问题。(3)当探测到节点离开事件时,必须判断当前节点数是否过半。如果达不到 quorum,则放弃Master身份,重新加入集群。如果不这么做,则设想以下情况:假设5台机器组成的集群产生网络分区,2台一组,3台一组,产生分区前,Master位于2台中的一个,此时3台一组的节点会重新并成功选取Master,产生双主,俗称脑裂。

集群并不知道自己共有多少个节点,quorum值从配置中读取,我们需要设置配置项:

discovery.zen.minimum_master_nodes3.2 选举集群元信息

被选出的 Master 和集群元信息的新旧程度没有关系。因此它的第一个任务是选举元信息,让各节点把各自存储的元信息发过来,根据版本号确定最新的元信息,然后把这个信息广播下去,这样集群的所有节点都有了最新的元信息。

集群元信息的选举包括两个级别:集群级和索引级。不包含哪个shard存于哪个节点这种信息。这种信息以节点磁盘存储的为准,需要上报。为什么呢?因为读写流程是不经过Master的,Master 不知道各 shard 副本直接的数据差异。HDFS 也有类似的机制,block 信息依赖于DataNode的上报。

为了集群一致性,参与选举的元信息数量需要过半,Master发布集群状态成功的规则也是等待发布成功的节点数过半。

在选举过程中,不接受新节点的加入请求。

集群元信息选举完毕后,Master发布首次集群状态,然后开始选举shard级元信息。3.3 allocation过程

选举shard级元信息,构建内容路由表,是在allocation模块完成的。在初始阶段,所有的shard都处于UNASSIGNED(未分配)状态。ES中通过分配过程决定哪个分片位于哪个节点,重构内容路由表。此时,首先要做的是分配主分片。1. 选主分片

现在看某个主分片[website][0]是怎么分配的。所有的分配工作都是 Master 来做的,此时, Master不知道主分片在哪,它向集群的所有节点询问:大家把[website][0]分片的元信息发给我。然后,Master 等待所有的请求返回,正常情况下它就有了这个 shard 的信息,然后根据某种策略选一个分片作为主分片。是不是效率有些低?这种询问量=shard 数×节点数。所以说我们最好控制shard的总规模别太大。

现在有了shard[website][0]的分片的多份信息,具体数量取决于副本数设置了多少。现在考虑把哪个分片作为主分片。ES 5.x以下的版本,通过对比shard级元信息的版本号来决定。在多副本的情况下,考虑到如果只有一个 shard 信息汇报上来,则它一定会被选为主分片,但也许数据不是最新的,版本号比它大的那个shard所在节点还没启动。在解决这个问题的时候,ES 5.x开始实施一种新的策略:给每个 shard 都设置一个 UUID,然后在集群级的元信息中记录哪个shard是最新的,因为ES是先写主分片,再由主分片节点转发请求去写副分片,所以主分片所在节点肯定是最新的,如果它转发失败了,则要求Master删除那个节点。所以,从ES 5.x开始,主分片选举过程是通过集群级元信息中记录的“最新主分片的列表”来确定主分片的:汇报信息中存在,并且这个列表中也存在。

如果集群设置了:

"cluster.routing.allocation.enable": "none"

禁止分配分片,集群仍会强制分配主分片。因此,在设置了上述选项的情况下,集群重启后的状态为Yellow,而非Red。2. 选副分片

主分片选举完成后,从上一个过程汇总的 shard 信息中选择一个副本作为副分片。如果汇总信息中不存在,则分配一个全新副本的操作依赖于延迟配置项:

index.unassigned.node_left.delayed_timeout

我们的线上环境中最大的集群有100+节点,掉节点的情况并不罕见,很多时候不能第一时间处理,这个延迟我们一般配置为以天为单位。

最后,allocation过程中允许新启动的节点加入集群。3.4 index recovery

分片分配成功后进入recovery流程。主分片的recovery不会等待其副分片分配成功才开始recovery。它们是独立的流程,只是副分片的recovery需要主分片恢复完毕才开始。

为什么需要recovery?对于主分片来说,可能有一些数据没来得及刷盘;对于副分片来说,一是没刷盘,二是主分片写完了,副分片还没来得及写,主副分片数据不一致。1. 主分片recovery

由于每次写操作都会记录事务日志(translog),事务日志中记录了哪种操作,以及相关的数据。因此将最后一次提交(Lucene 的一次提交就是一次 fsync 刷盘的过程)之后的 translog中进行重放,建立Lucene索引,如此完成主分片的recovery。2. 副分片recovery

副分片的恢复是比较复杂的,在ES的版本迭代中,副分片恢复策略有过不少调整。

副分片需要恢复成与主分片一致,同时,恢复期间允许新的索引操作。在目前的6.0版本中,恢复分成两阶段执行。

· phase1:在主分片所在节点,获取translog保留锁,从获取保留锁开始,会保留translog不受其刷盘清空的影响。然后调用Lucene接口把shard做快照,这是已经刷磁盘中的分片数据。把这些shard数据复制到副本节点。在phase1完毕前,会向副分片节点发送告知对方启动engine,在phase2开始之前,副分片就可以正常处理写请求了。

· phase2:对translog做快照,这个快照里包含从phase1开始,到执行translog快照期间的新增索引。将这些translog发送到副分片所在节点进行重放。

由于需要支持恢复期间的新增写操作(让ES的可用性更强),这两个阶段中需要重点关注以下几个问题。

分片数据完整性:如何做到副分片不丢数据?第二阶段的 translog 快照包括第一阶段所有的新增操作。那么第一阶段执行期间如果发生“Lucene commit”(将文件系统写缓冲中的数据刷盘,并清空translog),清除translog怎么办?在ES 2.0之前,是阻止了刷新操作,以此让translog都保留下来。从2.0版本开始,为了避免这种做法产生过大的translog,引入了translog.view的概念,创建 view 可以获取后续的所有操作。从6.0版本开始,translog.view 被移除。引入TranslogDeletionPolicy的概念,它将translog做一个快照来保持translog不被清理。这样实现了在第一阶段允许Lucene commit。

数据一致性:在ES 2.0之前,副分片恢复过程有三个阶段,第三阶段会阻塞新的索引操作,传输第二阶段执行期间新增的translog,这个时间很短。自2.0版本之后,第三阶段被删除,恢复期间没有任何写阻塞过程。在副分片节点,重放translog时,phase1和phase2之间的写操作与phase2重放操作之间的时序错误和冲突,通过写流程中进行异常处理,对比版本号来过滤掉过期操作。

这样,时序上存在错误的操作被忽略,对于特定的 doc,只有最新一次操作生效,保证了主副分片一致。

第一阶段尤其漫长,因为它需要从主分片拉取全量的数据。在ES 6.x中,对第一阶段再次优化:标记每个操作。在正常的写操作中,每次写入成功的操作都分配一个序号,通过对比序号就可以计算出差异范围,在实现方式上,添加了global checkpoint和local checkpoint,主分片负责维护global checkpoint,代表所有分片都已写入这个序号的位置,local checkpoint代表当前分片已写入成功的最新位置,恢复时通过对比两个序列号,计算出缺失的数据范围,然后通过translog重放这部分数据,同时translog会为此保留更长的时间。

因此,有两个机会可以跳过副分片恢复的phase1:基于SequenceNumber,从主分片节点的translog恢复数据;主副两分片有相同的syncid且doc数相同,可以跳过phase1。3.5 集群启动日志

日志是分布式系统中排查问题的重要手段,虽然 ES 提供了很多便于排查问题的接口,但重要日志仍然是不可或缺的。默认情况下,ES输出的INFO级别日志较少,许多重要模块的关键环节是DEBUG 或TRACE 级别的,下图列出了集群启动过程相关的重要日志,部分调整到了INFO级别。3.6 小结

当一个索引的主分片分配成功后,到此分片的写操作就是允许的。当一个索引所有的主分片都分配成功后,该索引变为Yellow。当全部索引的主分片都分配成功后,整个集群变为Yellow。当一个索引全部分片分配成功后,该索引变为 Green。当全部索引的索引分片分配成功后,整个集群变为Green。

索引数据恢复是最漫长的过程。当shard总量达到十万级的时候,6.x之前的版本集群从Red变为Green的时间可能需要小时级。ES 6.x中的副本允许从本地translog恢复是一次重大的改进,避免了从主分片所在节点拉取全量数据,为恢复过程节约了大量时间。第4章 节点的启动和关闭

本章分析单个节点的启动和关闭流程。看看进程是如何解析配置、检查环境、初始化内部模块的,以及在节点被“kill”的时候是如何处理的。4.1 启动流程做了什么

总体来说,节点启动流程的任务是做下面几类工作:

· 解析配置,包括配置文件和命令行参数。

· 检查外部环境和内部环境,例如,JVM版本、操作系统内核参数等。

· 初始化内部资源,创建内部模块,初始化探测器。

· 启动各个子模块和keepalive线程。4.2 启动流程分析4.2.1 启动脚本

当我们通过启动脚本bin/elasticsearch启动ES时,脚本通过exec加载Java程序。代码如下:

ES_JAVA_OPTS变量保存了JVM参数,其内容来自对config/jvm.options配置文件的解析。

如果执行启动脚本时添加了-d参数:

bin/elasticsearch –d

则启动脚本会在exec中添加<&- &。<&-的作用是关闭标准输入,即进程中的0号fd。&的作用是让进程在后台运行。4.2.2 解析命令行参数和配置文件

目前支持的命令行参数有下面几种,默认启动时都不使用,如下表所示。

实际工程应用中建议在启动参数中添加-d和-p,例如:

bin/elasticsearch -d -p es.pid

此处解析的配置文件有下面两个,jvm.options是在启动脚本中解析的。

elasticsearch.yml #主要配置文件

log4j2.properties #日志配置文件4.2.3 加载安全配置

什么是安全配置?本质上是配置信息,既然是配置信息,一般是写到配置文件中的。ES的几个配置文件在之前的章节提到过。此处的“安全配置”是为了解决有些敏感的信息不适合放到配置文件中的,因为配置文件是明文保存的,虽然文件系统有基于用户权限的保护,但这仍然不够。因此ES把这些敏感配置信息加密,单独放到一

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载