Elasticsearch的架构原理剖析

Elasticsearch 是最近两年异军突起的一个兼有搜索引擎和NoSQL数据库功能的开源系统,基于Java/Lucene构建。
Elasticsearch 看名字就能大概了解下它是一个弹性的搜索引擎。首先弹性隐含的意思是分布式,单机系统是无法弹起来的,而后加上灵活的伸缩机制,就是这里的 Elastic 包含的意思。它的搜索存储功能主要是 Lucene 提供的,Lucene 至关于其存储引擎,它在之上封装了索引,查询,以及分布式相关的接口。java

Elasticsearch 中的几个概念
集群(Cluster)一组拥有共同的 cluster name 的节点。
节点(Node) 集群中的一个 Elasticearch 实例。
索引(Index) 至关于关系数据库中的database概念,一个集群中能够包含多个索引。这个是个逻辑概念。
主分片(Primary shard) 索引的子集,索引能够切分红多个分片,分布到不一样的集群节点上。分片对应的是 Lucene 中的索引。
副本分片(Replica shard)每一个主分片能够有一个或者多个副本。
类型(Type)至关于数据库中的table概念,mapping是针对 Type 的。同一个索引里能够包含多个 Type。
Mapping 至关于数据库中的schema,用来约束字段的类型,不过 Elasticsearch 的 mapping 能够自动根据数据建立。
文档(Document) 至关于数据库中的row。
字段(Field)至关于数据库中的column。
分配(Allocation) 将分片分配给某个节点的过程,包括分配主分片或者副本。若是是副本,还包含从主分片复制数据的过程。node

分布式以及 Elastic
分布式系统要解决的第一个问题就是节点之间互相发现以及选主的机制。若是使用了 Zookeeper/Etcd 这样的成熟的服务发现工具,这两个问题都一并解决了。但 Elasticsearch 并无依赖这样的工具,带来的好处是部署服务的成本和复杂度下降了,不用预先依赖一个服务发现的集群,缺点固然是将复杂度带入了 Elasticsearch 内部。算法

服务发现以及选主 ZenDiscovery
节点启动后先ping(这里的ping是 Elasticsearch 的一个RPC命令。若是 discovery.zen.ping.unicast.hosts 有设置,则ping设置中的host,不然尝试ping localhost 的几个端口, Elasticsearch 支持同一个主机启动多个节点)
Ping的response会包含该节点的基本信息以及该节点认为的master节点。
选举开始,先从各节点认为的master中选,规则很简单,按照id的字典序排序,取第一个。
若是各节点都没有认为的master,则从全部节点中选择,规则同上。这里有个限制条件就是 discovery.zen.minimum_master_nodes,若是节点数达不到最小值的限制,则循环上述过程,直到节点数足够能够开始选举。
最后选举结果是确定能选举出一个master,若是只有一个local节点那就选出的是本身。
若是当前节点是master,则开始等待节点数达到 minimum_master_nodes,而后提供服务。
若是当前节点不是master,则尝试加入master。
Elasticsearch 将以上服务发现以及选主的流程叫作 ZenDiscovery 。因为它支持任意数目的集群(1-N),因此不能像 Zookeeper/Etcd 那样限制节点必须是奇数,也就没法用投票的机制来选主,而是经过一个规则,只要全部的节点都遵循一样的规则,获得的信息都是对等的,选出来的主节点确定是一致的。但分布式系统的问题就出在信息不对等的状况,这时候很容易出现脑裂(Split-Brain)的问题,大多数解决方案就是设置一个quorum值,要求可用节点必须大于quorum(通常是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个quorum的配置就是 discovery.zen.minimum_master_nodes 。 说到这里要吐槽下 Elasticsearch 的方法和变量命名,它的方法和配置中的master指的是master的候选节点,也就是说可能成为master的节点,并非表示当前的master,我就被它的一个 isMasterNode 方法坑了,开始一直没能理解它的选举规则。spring

弹性伸缩 Elastic
Elasticsearch 的弹性体如今两个方面:
服务发现机制让节点很容易加入和退出。
丰富的设置以及allocation API。
Elasticsearch 节点启动的时候只须要配置discovery.zen.ping.unicast.hosts,这里不须要列举集群中全部的节点,只要知道其中一个便可。固然为了不重启集群时正好配置的节点挂掉,最好多配置几个节点。节点退出时只须要调用 API 将该节点从集群中排除 (Shard Allocation Filtering),系统会自动迁移该节点上的数据,而后关闭该节点便可。固然最好也将不可用的已知节点从其余节点的配置中去除,避免下次启动时出错。sql

分片(Shard)以及副本(Replica)
分布式存储系统为了解决单机容量以及容灾的问题,都须要有分片以及副本机制。Elasticsearch 没有采用节点级别的主从复制,而是基于分片。它当前还未提供分片切分(shard-splitting)的机制,只能建立索引的时候静态设置。
好比,开始设置为5个分片,在单个节点上,后来扩容到5个节点,每一个节点有一个分片。若是继续扩容,是不能自动切分进行数据迁移的。官方文档的说法是分片切分红本和从新索引的成本差很少,因此建议干脆经过接口从新索引。
Elasticsearch 的分片默认是基于id 哈希的,id能够用户指定,也能够自动生成。但这个能够经过参数(routing)或者在mapping配置中修改。
Elasticsearch 禁止同一个分片的主分片和副本分片在同一个节点上,因此若是是一个节点的集群是不能有副本的。数据库

恢复以及容灾
分布式系统的一个要求就是要保证高可用。前面描述的退出流程是节点主动退出的场景,但若是是故障致使节点挂掉,Elasticsearch 就会主动allocation。但若是节点丢失后马上allocation,稍后节点恢复又马上加入,不会形成浪费。Elasticsearch的恢复流程大体以下:
集群中的某个节点丢失网络链接
master提高该节点上的全部主分片的在其余节点上的副本为主分片
cluster集群状态变为 yellow ,由于副本数不够
等待一个超时设置的时间,若是丢失节点回来就能够当即恢复(默认为1分钟,经过 index.unassigned.node_left.delayed_timeout 设置)。若是该分片已经有写入,则经过translog进行增量同步数据。
不然将副本分配给其余节点,开始同步数据。
但若是该节点上的分片没有副本,则没法恢复,集群状态会变为red,表示可能要丢失该分片的数据了。
分布式集群的另一个问题就是集群整个重启后可能致使预期的分片从新分配(部分节点没有启动完成的时候,集群觉得节点丢失),浪费带宽。因此 Elasticsearch 经过如下静态配置(不能经过API修改)控制整个流程,以10个节点的集群为例:
gateway.recover_after_nodes: 8
gateway.expected_nodes: 10
gateway.recover_after_time: 5m
好比10个节点的集群,按照上面的规则配置,当集群重启后,首先系统等待 minimum_master_nodes(6)个节点加入才会选出master, recovery操做是在 master节点上进行的,因为咱们设置了 recover_after_nodes(8),系统会继续等待到8个节点加入, 才开始进行recovery。当开始recovery的时候,若是发现集群中的节点数小于expected_nodes,也就是还有部分节点未加入,因而开始recover_after_time 倒计时(若是节点数达到expected_nodes则马上进行 recovery),5分钟后,若是剩余的节点依然没有加入,则会进行数据recovery。
—————————————————————————————————————————————————————————
搜索引擎 Search
Elasticsearch 除了支持 Lucene 自己的检索功能外,在之上作了一些扩展。express

脚本支持
Elasticsearch 默认支持groovy脚本,扩展了 Lucene 的评分机制,能够很容易的支持复杂的自定义评分算法。它默认只支持经过sandbox方式实现的脚本语言(如lucene expression,mustache),groovy必须明确设置后才能开启。Groovy的安全机制是经过java.security.AccessControlContext设置了一个class白名单来控制权限的。
Suggester Elasticsearch 经过扩展的索引机制,能够实现像google那样的自动完成suggestion以及搜索词语错误纠正的suggestion。json

NoSQL 数据库
Elasticsearch 能够做为数据库使用,主要依赖于它的如下特性:
默认在索引中保存原始数据,并可获取。这个主要依赖 Lucene 的store功能。
实现了translog,提供了实时的数据读取能力以及完备的数据持久化能力(在服务器异常挂掉的状况下依然不会丢数据)。Lucene 由于有 IndexWriter buffer, 若是进程异常挂掉,buffer中的数据是会丢失的。因此 Elasticsearch 经过translog来确保不丢数据。同时经过id直接读取文档的时候,Elasticsearch 会先尝试从translog中读取,以后才从索引中读取。也就是说,即使是buffer中的数据还没有刷新到索引,依然能提供实时的数据读取能力。Elasticsearch 的translog 默认是每次写请求完成后统一fsync一次,同时有个定时任务检测(默认5秒钟一次)。若是业务场景须要更大的写吞吐量,能够调整translog相关的配置进行优化。
dynamic-mapping 以及 schema-free
Elasticsearch 的dynamic-mapping至关于根据用户提交的数据,动态检测字段类型,自动给数据库表创建表结构,也能够动态增长字段,因此它叫作schema-free,而不是schema-less。这种方式的好处是用户能必定程度享受schema-less的好处,不用提早创建表结构,同时由于其实是有schema的,能够作查询上的优化,检索效率要比纯schema-less的数据库高许多。但缺点就是已经建立的索引不能变动数据类型(Elasticsearch 写入数据的时候若是类型不匹配会自动尝试作类型转换,若是失败就会报错,好比数字类型的字段写入字符串”123”是能够的,但写入”abc”就不能够。),要损失必定的自由度。
另外 Elasticsearch 提供的index-template功能方便用户动态建立索引的时候预先设定索引的相关参数以及type mapping,好比按天建立日志库,template能够设置为对 log-* 的索引都生效。安全

丰富的QueryDSL功能
Elasticsearch 的query语法基本上和sql对等的,除了join查询,以及嵌套临时表查询不能支持。不过 Elasticsearch 支持嵌套对象以及parent外部引用查询,因此必定程度上能够解决关联查询的需求。另外group by这种查询能够经过其aggregation实现。Elasticsearch 提供的aggregation能力很是强大,其生态圈里的 Kibana 主要就是依赖aggregation来实现数据分析以及可视化的。服务器

系统架构
Elasticsearch 的依赖注入用的是guice,网络使用netty,提供http rest和RPC两种协议。
Elasticsearch 之因此用guice,而不是用spring作依赖注入,关键的一个缘由是guice能够帮它很容易的实现模块化,经过代码进行模块组装,能够很精确的控制依赖注入的管理范围。好比 Elasticsearch 给每一个shard单独生成一个injector,能够将该shard相关的配置以及组件注入进去,下降编码和状态管理的复杂度,同时删除shard的时候也方便回收相关对象。
ClusterState
前面咱们分析了 Elasticsearch 的服务发现以及选举机制,它是内部本身实现的。服务发现工具作的事情其实就是跨服务器的状态同步,多个节点修改同一个数据对象,须要有一种机制将这个数据对象同步到全部的节点。Elasticsearch 的ClusterState 就是这样一个数据对象,保存了集群的状态,索引/分片的路由表,节点列表,元数据等,还包含一个ClusterBlocks,至关于分布式锁,用于实现分布式的任务同步。
主节点上有个单独的进程处理 ClusterState 的变动操做,每次变动会更新版本号。变动后会经过PRC接口同步到其余节点。主节知道其余节点的ClusterState 的当前版本,发送变动的时候会作diff,实现增量更新。

Rest 和 RPC

Elasticsearch 的rest请求的传递流程如上图(这里对实际流程作了简化):
用户发起http请求,Elasticsearch 的9200端口接受请求后,传递给对应的RestAction。
RestAction作的事情很简单,将rest请求转换为RPC的TransportRequest,而后调用NodeClient,至关于用客户端的方式请求RPC服务,只不过transport层会对本节点的请求特殊处理。
这样作的好处是将http和RPC两层隔离,增长部署的灵活性。部署的时候既能够同时开启RPC和http服务,也能够用client模式部署一组服务专门提供http rest服务,另一组只开启RPC服务,专门作data节点,便于分担压力。
Elasticsearch 的RPC的序列化机制使用了 Lucene 的压缩数据类型,支持vint这样的变长数字类型,省略了字段名,用流式方式按顺序写入字段的值。每一个须要传输的对象都须要实现:
void writeTo(StreamOutput out)
T readFrom(StreamInput in)
两个方法。虽然这样实现开发成本略高,增删字段也不太灵活,但对 Elasticsearch 这样的数据库系统来讲,不用考虑跨语言,增删字段确定要考虑兼容性,这样作效率最高。因此 Elasticsearch 的RPC接口只有java client能够直接请求,其余语言的客户端都走的是rest接口。

网络层
Elasticsearch 的网络层抽象很值得借鉴。它抽象出一个 Transport 层,同时兼有client和server功能,server端接收其余节点的链接,client维持和其余节点的链接,承担了节点之间请求转发的功能。Elasticsearch 为了不传输流量比较大的操做堵塞链接,因此会按照优先级建立多个链接,称为channel。
recovery: 2个channel专门用作恢复数据。若是为了不恢复数据时将带宽占满,还能够设置恢复数据时的网络传输速度。
bulk: 3个channel用来传输批量请求等基本比较低的请求。
regular: 6个channel用来传输通用正常的请求,中等级别。
state: 1个channel保留给集群状态相关的操做,好比集群状态变动的传输,高级别。
ping: 1个channel专门用来ping,进行故障检测。
(3个节点的集群链接示意,来源 Elasticsearch 官方博客)
每一个节点默认都会建立13个到其余节点的链接,而且节点之间是互相链接的,每增长一个节点,该节点会到每一个节点建立13个链接,而其余每一个节点也会建立13个连回来的链接。

线程池
因为java不支持绿色线程(fiber/coroutine),线程池里保留多少线程合适?如何避免慢的任务占用线程池,致使其余比较快的任务也得不到执行?不少应用系统里,为了不这种状况,会随手建立线程池,最后致使系统里充塞了大的量的线程池,浪费资源。而 Elasticsearch 的解决方案是分优先级的线程池。它默认建立了10多个线程池,按照不一样的优先级以及不一样的操做进行划分。而后提供了4种类型的线程池,不一样的线程池使用不一样的类型:
CACHED 最小为0,无上限,无队列(SynchronousQueue,没有缓冲buffer),有存活时间检测的线程池。通用的,但愿能尽量支撑的任务。
DIRECT 直接在调用者的线程里执行,其实这不算一种线程池方案,主要是为了代码逻辑上的统一而创造的一种线程类型。
FIXED 固定大小的线程池,带有缓冲队列。用于计算和IO的耗时波动较小的操做。
SCALING 有最小值,最大值的伸缩线程池,队列是基于LinkedTransferQueue 改造的实现,和java内置的Executors生成的伸缩线程池的区别是优先增长线程,增长到最大值后才会使用队列,和java内置的线程池规则相反。用于计算和IO耗时都不太稳定,须要限制系统承载最大任务上限的操做。
这种解决方案虽然要求每一个用到线程池的地方都须要评估下执行成本以及应该用什么样的线程池,但好处是限制了线程池的泛滥,也缓解了不一样类型的任务互相之间的影响。

Elasticsearch 如今主要的应用场景有三块。站内搜索,主要和 Solr 竞争,属于后起之秀。NoSQL json文档数据库,主要抢占 Mongo 的市场,它在读写性能上优于 Mongo(见文末比较连接),同时也支持地理位置查询,还方便地理位置和文本混合查询,属于歪打正着。监控,统计以及日志类时间序的数据的存储和分析以及可视化,这方面是引领者。

内容来源:http://jolestar.com/elasticsearch-architecture/#rd?sukey=3997c0719f1515201d882aeeb35124573c66a29fa2f0bb6ba2023993366bae7928cfbdab55cb22992f061648df3fce69

相关文章
相关标签/搜索