存储 频道

ElasticSearch架构反向思路

作者:猪儿笨笨 来源:云栖社区 

原文链接: https://yq.aliyun.com/articles/617479?spm=a2c4e.11153940.bloghomeflow.526.162d291aRoS8gf


我曾经在多个场合说过,我分析一个系统的设计思路,往往不是一开始就去看看这个系统的设计文档或者源代码,而是去看系统的基本介绍,特别是框架类的功能详细介绍,然后根据介绍可以大概了解这样一个系统用来解决什么问题,有哪些特色,然后基于自己对这些问题的想法,根据自己的经验来同样设计一个系统,看包含哪些内容,使用哪些架构模式和思路,然后带着自己设计的东西再去看另一个系统的设计思路,可能再更加清楚,也会反思自己的设计是否哪些地方存在问题,可以加以改进。

最近正好准备玩ElasticSearch,本来在2013年就想玩这个,但由于工作原因耽误了,现在又翻出来看看有什么好玩的,下面就详细地记录了我对ElasticSearch的反向架构思考。顺便补充一句,目前用来研究的ElasticSearch的版本号是6.3

先来看看一份对ElasticSearch比较典型的介绍:

Elasticsearch是一个基于Apache Lucene(TM)的开源搜索引擎。无论在开源还是专有领域,Lucene可以被认为是迄今为止非常先进、性能较好的、功能最全的搜索引擎库。
 
但是,Lucene只是一个库。想要使用它,你必须使用Java来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。
 
Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
 
不过,Elasticsearch不仅仅是Lucene和全文搜索,我们还能这样去描述它:
 
分布式的实时文件存储,每个字段都被索引并可被搜索
分布式的实时分析搜索引擎
可以扩展到上百台服务器,处理PB级结构化或非结构化数据
而且,所有的这些功能被集成到一个服务里面,你的应用可以通过简单的RESTful API、各种语言的客户端甚至命令行与之交互。
 
上手Elasticsearch非常容易。它提供了许多合理的缺省值,并对初学者隐藏了复杂的搜索引擎理论。它开箱即用(安装即可使用),只需很少的学习既可在生产环境中使用。
 
随着你对Elasticsearch的理解加深,你可以根据不同的问题领域定制Elasticsearch的高级特性,这一切都是可配置的,并且配置非常灵活。

幸亏以前使用过Lucene做IDE底层项目模型关系的管理,对Lucene还算比较熟悉,否则还得先去看看Lucene的功能和用法。

从上面的介绍可以看出几个关键内容:

  1. Lucene在做索引的时候本身就有存储功能,所以存储这个东西是天然就有的,反而不用花时间考虑。

  2. 性能是一个比较关键的东西,特别是要做实时引擎,怎么保证高性能。

  3. ElasticSearch是一个分布式的系统,那么必然存在多结点通讯,协作等问题,比如使用ZooKeeper之类的系统进行注册和协同,当然也保不齐他自己玩一套。

  4. 既然是分布式系统,那么数据存储就不可能完全单机化,也就是存在Sharding的情况,如何Sharding,如何同步,在查找结果的时候,如何聚合。

  5. 分布式系统,只要涉及到数据更新,必然存在数据不一致问题,怎么解决。

  6. 由于索引本身原因,一旦出现Sharding,就很难做联合的查询,这个应该不能实现的,至少说不可能很简单得实现。

  7. 有一个网络层或者说对外服务接口层,用来进行交互,看介绍,支持多种协议,比如Client直接调用,或者是Restful风格。

  8. 参考服务接口层,还允许很多地方进行配置,那么很显然,应该是使用了类似于插件的技术来支持很多功能。

我的习惯是从使用者角度来倒推系统架构

  1. 对外服务,称为Interface,这个其实还相对简单,应该提供两个基本功能,即BuildIndex(不一定要区分Create和Update,但Delete肯定要有)和Query(应该基于主Key和Condition两种查询),把这两个基本接口设计好,然后在上面加不同的封装或者通过Netty之类网络架构提供Rest服务,也可能基于Stub类似的机制提供RPC调用。

  2. 查询功能,是采用SQL还是Query模型的方式,我更倾向于后者,因为关联查询等很多功能是无法提供的,SQL校验会是比较麻烦的事情。

  3. 不管是BuildIndex还是Query,肯定要找到一台机器或者多台机器进行处理,由于这是一个分布式系统,而且还支持Sharding,那么可以肯定,需要分组,即Group,一个Group中包括若干个Node,用来支持服务。

  4. 怎么分组,正常可能是分两级,一种是基于模型定义的,比如对于某一些数据,象商品,用户这些数据可能分成一类数据对应一个Group来处理,这种处理比较直观,也简单。也就是说每一类模型会对应一个Group,而一个Group可能对着多个模型,特别是数据相对较少的时候。还有一种就是Sharding,通常来说,是对一类数据,根据某一个或者几个字段(Field),进行条件分组,也就说在这种分组情况下,每个Node的数据都是不全的,需要将多个Node合并在一起,才会形成完整的数据集。这两种分组都需要支持的。

  5. 对于BuildIndex和Query,当系统分成多个Group的时候,肯定要有一个Router的概念,即一个BuildIndex或者Query服务来的时候,得找到相应的Group(应该是Group下的Node),因为Lucene中的Document和Term特性,应该需要设计一个类似于数据库中的Table模型,一个Group负责处理多个Table。在BuildIndex和Query请求里,1. 必须带有Table的准确定义,比如User,Item等。 
    按照前面的思考,Group是肯定应该存在的,但是每个Group否需要一个MasterNode呢?

  6. 当一个Query请求定义清楚后,会以路由的方式找到一个Group,如果数据量不大的话,一个Group中的Node应该是数据对等的,那么请求落到任何一个Node上都可以得到相应的结果。如果数据量很大,出现Sharding,就分两种情况,一种是Query中的条件,能够符合Sharding的定义条件,那么落到任何一个Node上以后,通过转发的方式,总是可以拿到请求,应该有两种实现方式,一是请求发到某个Node上以后,由Node分析后,将可以导向的Node返回,由请求方再次将指定的Node发送请求,二是任意Node直接向可以导向的Node转发请求,并拿到结果后返回给请求方,第二种对客户端友好,但如果数据量大的话,可能不太合适。还有一种情况就是,如果Query中的条件不能够符合Sharding定义,那么就出现类似于数据库查询的FullScan,由收到的Node将请求转发给相应的Node,构成全量搜索,然后由该Node合并后,返回。如果这样看,最好的方式还是Node统一处理,对请求方更友好一些,也更一致。

  7. 当BuildIndex的时候,必然是发给一个Node,由其完成Index后,再同步给其它Node,此时同步,是有一个MasterNode还是没有好呢?感觉设计一个MasterNode可能使得逻辑更简单。即大的Group里,MasterNode主要负责协作和BuildIndex同步,而Query则可以尽可能地落到DataNode侧。

  8. 虽然有了MasterNode,但仍然是可以将BuildIndex请求发给DataNode,由DataNode转发给MasterNode,这样会更加简单和友好。

  9. 考虑到BuildIndex和Query会有不同步的情况,那么怎么减少这种不一致性呢?如果由MasterNode或者指定的一个DataNode进行BuildIndex的时候,对其它Node的Query都会产生数据不一致性问题。假设由MasterNode给其它DataNode全部上锁,此时查询性能急速下降,这种方法不是非常建议,容易形成堵塞,不过如果数据很少更新,而且对数据一致性有较高要求,也可以支持,那里可能得在这个地方允许用户配置一致性优先还是性能优先了。如果是后者的话,按照我对Lucene的了解,此时每个DataNode最好有一个DiskStore和一个MemoryStore,查询时将两者合并查询,这样在保证高性能的情况下可以减少不一致性。或者更灵活一点,允许在BuildIndex的时候允许指定是否加锁,但这样可能会增加复杂度,需要再思考一下。

  10. 同样是数据不一致问题,除了上面的内容以外,还需要使用Log,这样MasterNode先记录Log,然后进行Index,同时分发给DataNode,DataNode也是先记录Log,这样一旦出现问题,可以随时在启动时从Log处Redo。

  11. 维护和管理功能:动态扩容,Reindex(扩容时肯定要用到),启动时先与多个DataNode同步Log,再根据Log进行Redo,保证数据的一致性。

  12. 插件化设计没什么难点,不管是类似于OSGi,还是说直接写一个Plugin的接口,然后加一个PluginManager都可以解决问题。但关键是Plugin需要在哪些情况下调用,以便让开发者可以更多的加入自己的定制。我猜可能有以下几个点:网络请求的Before和After处理(比如支持不同的数据模型,不同的安全检查等,记录日志,流量控制等),启动后的After处理(比如对Log进行Check,以便Redo),BuildIndex和Query的Before和After处理(其实就可以通过这个扩展来处理数据同步的问题)。

  13. 上面说的插件化设计并不难,但是否使用统一的Plugin接口,还是分开,需要考虑一下,毕竟可以提供扩展点的地方太多了。如果是我设计,大概是三大级继承,最顶层的有一个Plugin或者Extension的接口,提供Name,Desription,Dependecy等内容的定义,这个和Equinox都类似,其实不带任何业务支持的,第二层是业务级别的,比如说网络请求的,日志处理的,第三层就是具体实现了。再多就有点复杂了,有一个最顶层接口的好处是,在Eclipse里,查下继承关系,就得到所有实现了,方便分析代码,如果只设计二和三层,哈哈,就有得找了。

基于以上分析,可以列出来几个基本的元素和服务:

  1. Node+Group+MasterNode+DataNode

  2. Table+Field+Key+Condition

  3. BuildIndex+Query

  4. Log

  5. Plugin

下面是大致的架构域图:

还有几个难点,需要再考虑一下:

  1. Query可能会有Paging的需要,那么一旦出现Sharding的话,需要将多个DataNode的结果Merge后,进行Sort,再计算Paging后返回。这个对性能的要求比较高,特别是当页面翻到几十页的时候,性能损失非常大,如何处理?还是说技术层面上不做解决,直接让业务方来自行规划。

  2. 因为ElasticSearch是基于Lucene的,而Lucene并不提供事务操作,比如先行锁再Update,因此一旦出现冲突时,因为网络延时等原因,有可能后面的数据覆盖前面的数据,这种情况怎么考虑,是加一个时间版本号还是忽略这种情况?

  3. 另外ElasticSearch对数据一致性不可能提供太好的解决方案,因此最好还是将一些非核心业务数据进行查询,比如日志,就不会出现修改,再比如电商中的商品表,修改相对并不频繁,但如果商品表里包含商品数量,那么就挂了,所有必须减少将频繁更新的数据放入搜索。

  4. 有点记不清楚Lucene的存储机制了,是否支持类似于数据库的Update语句,只更新部分数据。如果不支持,那么ElasticSearch是否需要支持呢?如果是我,应该不会支持,做太多的事情更容易出错。

  5. 当MasterNode当掉,显然可以通过选举或者别的方法找到一个新的MasterNode,但如果一个MasterNode或者DataNode收到一个BuildIndex请求后,再当掉,最好是通知Client失败,由Client发起重试。由于所有BuildIndex请求都是发给MasterNode来处理的,那么就相对简单了,如果MasterNode失败后重新加入Group,由于此时它不再是Master,就可以丢弃这个日志,保证数据一致性。这块的细节会比较多,记录Log,然后如何Redo,如何Sync,如何抛弃,都需要深入分析。不在这里折腾了。

0
相关文章