Elasticsearch 是最近两年异军突起的一个兼有搜索引擎和NoSQL数据库功能的开源系统,基于Java/Lucene构建。最近研究了一下,感受 Elasticsearch 的架构以及其开源的生态构建都有许多可借鉴之处,因此整理成文章分享下。本文的代码以及架构分析主要基于 Elasticsearch 2.X 最新稳定版。html
Elasticsearch 看名字就能大概了解下它是一个弹性的搜索引擎。首先弹性隐含的意思是分布式,单机系统是无法弹起来的,而后加上灵活的伸缩机制,就是这里的 Elastic 包含的意思。它的搜索存储功能主要是 Lucene 提供的,Lucene 至关于其存储引擎,它在之上封装了索引,查询,以及分布式相关的接口。java
分布式系统要解决的第一个问题就是节点之间互相发现以及选主的机制。若是使用了 Zookeeper/Etcd 这样的成熟的服务发现工具,这两个问题都一并解决了。但 Elasticsearch 并无依赖这样的工具,带来的好处是部署服务的成本和复杂度下降了,不用预先依赖一个服务发现的集群,缺点固然是将复杂度带入了 Elasticsearch 内部。node
服务发现以及选主 ZenDiscovery 程序员
Elasticsearch 将以上服务发现以及选主的流程叫作 ZenDiscovery 。因为它支持任意数目的集群(1-N),因此不能像 Zookeeper/Etcd 那样限制节点必须是奇数,也就没法用选举的机制来选主,而是经过一个规则,只要全部的节点都遵循一样的规则,获得的信息都是对等的,选出来的主节点确定是一致的。但分布式系统的问题就出在信息不对等的状况,这时候很容易出现脑裂(Split-Brain)的问题,大多数解决方案就是设置一个quorum值,要求可用节点必须大于quorum(通常是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个quorum的配置就是 discovery.zen.minimum_master_nodes 。 说到这里要吐槽下 Elasticsearch 的方法和变量命名,它的方法和配置中的master指的是master的候选节点,也就是说可能成为master的节点,并非表示当前的master,我就被它的一个 isMasterNode 方法坑了,开始一直没能理解它的选举规则。web
弹性伸缩 Elastic 算法
Elasticsearch 的弹性体如今两个方面:spring
Elasticsearch 节点启动的时候只须要配置discovery.zen.ping.unicast.hosts,这里不须要列举集群中全部的节点,只要知道其中一个便可。固然为了不重启集群时正好配置的节点挂掉,最好多配置几个节点。节点退出时只须要调用 API 将该节点从集群中排除 (Shard Allocation Filtering),系统会自动迁移该节点上的数据,而后关闭该节点便可。固然最好也将不可用的已知节点从其余节点的配置中去除,避免下次启动时出错。sql
分片(Shard)以及副本(Replica) 分布式存储系统为了解决单机容量以及容灾的问题,都须要有分片以及副本机制。Elasticsearch 没有采用节点级别的主从复制,而是基于分片。它当前还未提供分片切分(shard-splitting)的机制,只能建立索引的时候静态设置。mongodb
(elasticsearch 官方博客的图片)数据库
好比上图所示,开始设置为5个分片,在单个节点上,后来扩容到5个节点,每一个节点有一个分片。若是继续扩容,是不能自动切分进行数据迁移的。官方文档的说法是分片切分红本和从新索引的成本差很少,因此建议干脆经过接口从新索引。
Elasticsearch 的分片默认是基于id 哈希的,id能够用户指定,也能够自动生成。但这个能够经过参数(routing)或者在mapping配置中修改。当前版本默认的哈希算法是MurmurHash3。
Elasticsearch 禁止同一个分片的主分片和副本分片在同一个节点上,因此若是是一个节点的集群是不能有副本的。
恢复以及容灾
分布式系统的一个要求就是要保证高可用。前面描述的退出流程是节点主动退出的场景,但若是是故障致使节点挂掉,Elasticsearch 就会主动allocation。但若是节点丢失后马上allocation,稍后节点恢复又马上加入,会形成浪费。Elasticsearch的恢复流程大体以下:
但若是该节点上的分片没有副本,则没法恢复,集群状态会变为red,表示可能要丢失该分片的数据了。
分布式集群的另一个问题就是集群整个重启后可能致使不预期的分片从新分配(部分节点没有启动完成的时候,集群觉得节点丢失),浪费带宽。因此 Elasticsearch 经过如下静态配置(不能经过API修改)控制整个流程,以10个节点的集群为例:
好比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。
Elasticsearch 除了支持 Lucene 自己的检索功能外,在之上作了一些扩展。
Elasticsearch 能够做为数据库使用,主要依赖于它的如下特性:
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-* 的索引都生效。
这两个功能我建议新的数据库均可以借鉴下。
Elasticsearch 的依赖注入用的是guice,网络使用netty,提供http rest和RPC两种协议。
Elasticsearch 之因此用guice,而不是用spring作依赖注入,关键的一个缘由是guice能够帮它很容易的实现模块化,经过代码进行模块组装,能够很精确的控制依赖注入的管理范围。好比 Elasticsearch 给每一个shard单独生成一个injector,能够将该shard相关的配置以及组件注入进去,下降编码和状态管理的复杂度,同时删除shard的时候也方便回收相关对象。这方面有兴趣使用guice的能够借鉴。
ClusterState
前面咱们分析了 Elasticsearch 的服务发现以及选举机制,它是内部本身实现的。服务发现工具作的事情其实就是跨服务器的状态同步,多个节点修改同一个数据对象,须要有一种机制将这个数据对象同步到全部的节点。Elasticsearch 的ClusterState 就是这样一个数据对象,保存了集群的状态,索引/分片的路由表,节点列表,元数据等,还包含一个ClusterBlocks,至关于分布式锁,用于实现分布式的任务同步。
主节点上有个单独的进程处理 ClusterState 的变动操做,每次变动会更新版本号。变动后会经过PRC接口同步到其余节点。主节知道其余节点的ClusterState 的当前版本,发送变动的时候会作diff,实现增量更新。
Rest 和 RPC
Elasticsearch 的rest请求的传递流程如上图(这里对实际流程作了简化):
这样作的好处是将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。
(3个节点的集群链接示意,来源 Elasticsearch 官方博客)
每一个节点默认都会建立13个到其余节点的链接,而且节点之间是互相链接的,每增长一个节点,该节点会到每一个节点建立13个链接,而其余每一个节点也会建立13个连回来的链接。
线程池
因为java不支持绿色线程(fiber/coroutine),我前面的《并发之痛》那篇文章也分析了线程池的问题,线程池里保留多少线程合适?如何避免慢的任务占用线程池,致使其余比较快的任务也得不到执行?不少应用系统里,为了不这种状况,会随手建立线程池,最后致使系统里充塞了大的量的线程池,浪费资源。而 Elasticsearch 的解决方案是分优先级的线程池。它默认建立了10多个线程池,按照不一样的优先级以及不一样的操做进行划分。而后提供了4种类型的线程池,不一样的线程池使用不一样的类型:
这种解决方案虽然要求每一个用到线程池的地方都须要评估下执行成本以及应该用什么样的线程池,但好处是限制了线程池的泛滥,也缓解了不一样类型的任务互相之间的影响。
之后每篇分析架构的文章,我都最后会提几个和该系统相关的改进或者扩展的想法,称为脑洞时间,做为一种锻炼。不过只提供想法,不深刻分析可行性以及实现。
还记得10年前在大学时候捣鼓 Lucene,弄校园内搜索,还弄了个基于词典的分词工具。毕业后第一份工做也是用 Lucene 作站内搜索。当时搭建的服务和 Elasticsearch 相似,提供更新和管理索引的api给业务程序,固然没有 Elasticsearch 这么强大。当时是有想过作相似的一个开源产品的,后来发现apache已经出了 Solr(2004年的时候就建立了,2008年1.3发布,已经相对成熟),感受应该没啥机会了。但 Elasticsearch 硬是在这种状况下成长起来了(10年建立,14年才发布1.0)。 两者的功能以及性能几乎都不相上下(开始性能上有些差距,但 Solr 有改进,差很少追上了),参看文末比较连接。
我以为一方面是 Elasticsearch 的简单友好的分布式机制占了先机,也正好遇上了移动互联网爆发移动应用站内搜索需求高涨的时代。第一波站内搜索是web时代,也是 Lucene 诞生的时代,但web的站内搜索能够简单的利用搜索引擎服务的自定义站点实现,而应用的站内搜索就只能靠本身搭了。另一方面是 Elasticsearch 的周边生态以及目标市场看把握的很是精准。Elasticsearch 如今的主要目标市场已经从站内搜索转移到了监控与日志数据的收集存储和分析,也就是你们常谈论的ELK。
Elasticsearch 如今主要的应用场景有三块。站内搜索,主要和 Solr 竞争,属于后起之秀。NoSQL json文档数据库,主要抢占 Mongo 的市场,它在读写性能上优于 Mongo(见文末比较连接),同时也支持地理位置查询,还方便地理位置和文本混合查询,属于歪打正着。监控,统计以及日志类时间序的数据的存储和分析以及可视化,这方面是引领者。
听说 Elasticsearch 的创始人当初建立 Elasticsearch 的时候是为了给喜欢作菜的媳妇搭建个菜谱的搜索网站,虽然菜谱搜索网站最后一直没作出来,但诞生了 Elasticsearch。因此程序员坚持一个业余项目也是很重要的,万一无意插柳就成荫了呢?
相关阅读