浅谈 Elasticsearch 基础与架构

Elasticsearch 是一个基于Apache Lucene(TM)的开源搜索引擎,简称ES。在分布式系统中应用广泛,不需要理解其深层工作原理就可以实现搜索,容易上手,下面本文将简单介绍其起源、基础与架构,以及其分布式应用。

1. 起源

一个叫做Shay Banon的刚结婚不久的失业开发者,想为学厨妻子设计食谱搜索引擎,于是开发出了早期版本的Lucene,并抽象其代码以便Java程序员可以在应用中增添搜索功能。他发布了第一个开源项目“Compass”。后来他找到一份工作,他把Compass重写成一个服务——Elasticsearch。第一个公开版本在2010年2月,后来在Github上广受欢迎,代码贡献者超过300人,于是一家主营Elasticsearch的公司就此成立……

1

总的来说,Elasticsearch是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。

2. ES的基础与架构

2.1 搜索引擎

ES是一个搜索引擎,因此我们来介绍一下搜索程序的基本原理。搜索程序一般由索引链及搜索组件组成。

索引链功能的实现需要按照几个独立的步骤依次完成:检索原始内容、根据原始内容来创建对应的文档、对创建的文档进行索引。

搜索组件用于接收用户的查询请求并返回相应结果,一般由用户接口、构建可编程查询语句的方法、查询语句执行引擎及结果展示组件组成。

2.2 ES的架构

ES的架构如下图所示,Gateway表示ES索引的持久化存储方式;River代表的是一个数据源,这也是其他存储方式(比如:数据库)同步数据到ES的方法;discovery.zen代表 elasticsearch的自动节点发现机制,首先它会通过以广播的方式去寻找存在的节点,然后再通过多播协议来进行节点之间的通信,于此同时也支持点对点的交互操作;Transport代表 elasticsearch 内部的节点或者集群与客户端之间的交互方式。

除此之外ES还具有一下等特性:

  •   分布式索引、搜索

  • 索引自动分片、负载均衡

  • 自动发现机器、组件集群

  • 主持Rsetful风格接口

2.3 ES的基础

ES具有近实时性(Near Realtime[NRT]),这一点有两个意思,从写入数据到数据可以被搜索到有一个小延迟(大概1秒);基于es执行搜索和分析可以达到秒级。

ES硬件分为集群(Cluster),节点(Node),分片和复制(Shards & Replicas),一个集群可以有多个节点,一个节点就是一台服务器,可以存储多个主分片和复制分片。

ES的索引与传统数据库可以简单对照如下。一个索引包含多个type(Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x版将会彻底移除 Type),一个type可以包含多个文档,一个文档包含多个字段。

去掉type能够使数据存储在独立的index中,这样即使有相同的字段名称也不会出现冲突,就像ElasticSearch出现的第一句话一样“你知道的,为了搜索····”,去掉type就是为了提高ES处理数据的效率。除此之外,在同一个索引的不同type下存储字段数不一样的实体会导致存储中出现稀疏数据,影响Lucene压缩文档的能力,导致ES查询效率的降低。

2.4 ES交互方式

  1. 非客户端方式(RESTful API):通过HTTP方式的JSON格式进行调用,使用9200端口。

  2. 客户端方式(Java API):ES内置传输客户端TransportClient,使用ES传输协议,通过9300端口与Java客户端通信,集群中各个节点同上。

    • 节点客户端(node client):节点客户端以无数据节点(none data node)身份加入集群,换言之,它自己不存储任何数据,但是它知道数据在集群中的具体位置,并且能够直接转发请求到对应的节点上。

    • 传输客户端(Transport client):这个更轻量的传输客户端能够发送请求到远程集群。它自己不加入集群,只是简单转发请求给集群中的节点。

    3. 分布式特性

    3.1 ES分布式特性

    Elasticsearch致力于隐藏分布式系统的复杂性。以下这些操作都是在底层自动完成的:

    • 无论是增加节点,还是移除节点,分片都可以做到无缝的扩展和迁移

    • 将你的文档分区到不同的容器或者分片(shards)中,它们可以存在于一个或多个节点中。

    • 将分片均匀的分配到各个节点,对索引和搜索做负载均衡。

    • 冗余每一个分片,防止硬件故障造成的数据丢失。

    • 将集群中任意一个节点上的请求路由到相应数据所在的节点。

    3.2 分布式增删改查

    3.2.1 路由文档到分片

    当你索引一个文档,它被存储在单独一个主分片上。Elasticsearch是如何知道文档属于哪个分片的呢?当你创建一个新文档,它是如何知道是应该存储在分片1还是分片2上的呢? 根据一个简单的算法决定:

    routing值是一个任意字符串,它默认是_id但也可以自定义。这个routing字符串通过哈希函数生成一个数字,除以主切片的数量得到一个余数,余数的范围永远是0到number_of_primary_shards - 1,这个数字就是特定文档所在的分片。

    3.2.2 新建、索引和删除文档

    新建、索引和删除请求都是写(write)操作,它们必须在主分片上成功完成才能复制到相关的复制分片上。

     

    下面我们罗列在主分片和复制分片上成功新建、索引或删除一个文档必要的顺序步骤:

    1. 客户端给Node 1发送新建、索引或删除请求。

    2. 节点使用文档的_id确定文档属于分片0。它转发请求到Node3,分片0位于这个节点上。

    3. Node 3在主分片上执行请求,如果成功,它转发请求到相应的位于Node 1和Node 2的复制节点上。当所有的复制节点报告成功,Node 3报告成功到请求的节点,请求的节点再报告给客户端。

    客户端接收到成功响应的时候,文档的修改已经被应用于主分片和所有的复制分片。你的修改生效了。

    3.2.3  索引文档

    下面我们罗列在主分片或复制分片上检索一个文档必要的顺序步骤:

    1. 客户端给Node 1发送get请求。

    2. 节点使用文档的_id确定文档属于分片0。分片0对应的复制分片在三个节点上都有。此时,它转发请求到Node 2。

    3. Node 2返回文档(document)给Node 1然后返回给客户端。

    对于读请求,为了平衡负载,请求节点会为每个请求选择不同的分片——它会循环所有分片副本。

    可能的情况是,一个被索引的文档已经存在于主分片上却还没来得及同步到复制分片上。这时复制分片会报告文档未找

    到,主分片会成功返回文档。一旦索引请求成功返回给用户,文档则在主分片和复制分片都是可用的。

    3.2.4 局部更新

    下面我们罗列执行局部更新必要的顺序步骤:

    1. 客户端给Node 1发送更新请求。

    2. 它转发请求到主分片所在节点Node 3。

    3. Node 3从主分片检索出文档,修改_source字段的JSON,然后在主分片上重建索引。如果有其他进程修改了文档,它以retry_on_conflict设置的次数重复步骤3,都未成功则放弃。

    4. 如果Node 3成功更新文档,它同时转发文档的新版本到Node 1和Node 2上的复制节点以重建索引。当所有复制节点报告成功,Node 3返回成功给请求节点,然后返回给客户端。

    值得注意的是,当主分片转发更改给复制分片时,并不是转发更新请求,而是转发整个文档的新版本。记住这些修改转发到复制节点是异步的,它们并不能保证到达的顺序与发送相同。如果Elasticsearch转发的仅仅是修改请求,修改的顺序可能是错误的,那得到的就是个损坏的文档。

    3.2.5  多文档模式——批量请求

    mget和bulk API与单独的文档类似。差别是请求节点知道每个文档所在的分片。它把多文档请求拆成每个分片的对文档请求,然后转发每个参与的节点。

    一旦接收到每个节点的应答,然后整理这些响应组合为一个单独的响应,最后返回给客户端。

    下面我们将罗列通过一个mget请求检索多个文档的顺序步骤:

    1. 客户端向Node 1发送mget请求。

    2. Node 1为每个分片构建一个多条数据检索请求,然后转发到这些请求所需的主分片或复制分片上。当所有回复被接收,Node 1构建响应并返回给客户端。

    下面我们将罗列使用一个bulk执行多个create、index、delete和update请求的顺序步骤:

    1. 客户端向Node 1发送bulk请求。

    2. Node 1为每个分片构建批量请求,然后转发到这些请求所需的主分片上。

    3. 主分片一个接一个的按序执行操作。当一个操作执行完,主分片转发新文档(或者删除部分)给对应的复制节点,然后执行下一个操作。一旦所有复制节点报告所有操作已成功完成,节点就报告success给请求节点,后者(请求节点)整理响应并返回给客户端。

    Bulk API还可以在最上层使用replication和consistency参数,routing参数则在每个请求的元数据中使用。

    3.3 分布式搜索

    由于不知道哪个文档会匹配查询(文档可能存放在集群中的任意分片上),所以搜索需要一个更复杂的模型。一个搜索不得不通过查询每一个我们感兴趣的索引的分片副本,来看是否含有任何匹配的文档。

    但是,找到所有匹配的文档只完成了这件事的一半。在搜索(search)API返回一页结果前,来自多个分片的结果必须被组合放到一个有序列表中。因此,搜索的执行过程分两个阶段,称为 查询 然后 取回 (query then fetch)。

    3.3.1 查询阶段

    查询阶段包含三步:

    1. 客户端发送一个search(搜索)请求给Node 3,Node 3创建了一个长度为from+size的空优先级队列。

    2. Node 3 转发这个搜索请求到索引中每个分片的原本或副本。每个分片在本地执行这个查询并且结果将结果到一个大小为from+size的有序本地优先队列里去。

    3. 每个分片返回document的ID和它优先队列里的所有document的排序值给协调节点Node 3。Node 3把这些值合并到自己的优先队列里产生全局排序结果。

    协调节点将这些分片级的结果合并到自己的有序优先队列里。这个就代表了最终的全局有序结果集。到这里,查询阶段结束。

    整个过程类似于归并排序算法,先分组排序再归并到一起,对于这种分布式场景非常适用。

    注意:一个索引可以由一个或多个原始分片组成,所以一个对于单个索引的搜索请求也需要能够把来自多个分片的结果组合起来。一个对于多(multiple)或全部(all)索引的搜索的工作机制和这完全一致——仅仅是多了一些分片而已。

    3.3.2 取回 阶段

    查询阶段辨别出那些满足搜索请求的document,但我们仍然需要取回那些document本身。

    分发阶段由以下步骤构成:

    1. 协调节点辨别出哪个document需要取回,并且向相关分片发出GET请求。

    2. 每个分片加载document并且根据需要丰富(enrich)它们,然后再将document返回协调节点。

    3. 一旦所有的document都被取回,协调节点会将结果返回给客户端。

    3.3.3 搜索选项

    • preference(偏爱):避免结果震荡

    preference参数允许你控制使用哪个分片或节点来处理搜索请求。她接受如下一些参数 primary,primary_first, local, only_node:xyz, prefer_node:xyz和shards:2,3。然而通常最有用的值是一些随机字符串,它们可以避免结果震荡问题(the bouncing results problem)。

    想像一下,你正在按照timestamp字段来对你的结果排序,并且有两个docuent有相同的timestamp。由于搜索请求是在所有有效的分片副本间轮询的,这两个document可能在原始分片里是一种顺序,在副本分片里是另一种顺序。

    这就是被称为结果震荡(bouncing results)的问题:用户每次刷新页面,结果顺序会发生变化。避免这个问题方法是对于同一个用户总是使用同一个分片。方法就是使用一个随机字符串例如用户的会话ID(session ID)来设置prefeence参数。

    • timeout (超时): 避免单一问题节点拖慢整个搜索请求

    通常,协调节点会等待接收所有分片的回答。如果有一个节点遇到问题,它会拖慢整个搜索请求。

    timeout参数告诉协调节点最多等待多久,就可以放弃等待而将已有结果返回。返回部分结果总比什么都没有好。

    • routing (路由选择): 可限制搜索分片,用于设计大搜索系统。

    在路由值那节里,我们解释了如何在建立索引时提供一个自定义的routing参数来保证所有相关的document(如属于单个用户的document)被存放在一个单独的分片中。在搜索时,你可以指定一个或多个routing 值来限制只搜索那些分片而不是搜索index里的全部分片:GET /_search?routing=user_1,user2 这个技术在设计非常大的搜索系统时就会派上用场了。

    • search_type (搜索类型): 根据特定目的设定搜索类型。

    虽然query_then_fetch是默认的搜索类型,但也可以根据特定目的指定其它的搜索类型,例如:GET /_search?search_type=count

    • count(计数):count(计数)搜索类型只有一个query(查询)的阶段。当不需要搜索结果只需要知道满足查询的document的数量时,可以使用这个查询类型。

    • query_and_fetch(查询并且取回):query_and_fetch(查询并且取回)搜索类型将查询和取回阶段合并成一个步骤。这是一个内部优化选项,当搜索请求的目标只是一个分片时可以使用,例如指定了routing(路由选择)值时。虽然你可以手动选择使用这个搜索类型,但是这么做基本上不会有什么效果。

    • dfs_query_then_fetch 和 dfs_query_and_fetch:dfs搜索类型有一个预查询的阶段,它会从全部相关的分片里取回项目频数来计算全局的项目频数。

    • scan(扫描):scan(扫描)搜索类型是和scroll(滚屏)API连在一起使用的,可以高效地取回巨大数量的结果。它是通过禁用排序来实现的。

    4. 总结

    本文见解来源于调研,因为知识水平有限且ES的内容实在太多,只介绍了ES的基础、架构以及分布式方面的简单运行方式,对于数据、搜索、映射分析、排序、索引管理以及各种高级的搜索手段并未进行介绍。

    感兴趣的同学可以去看一下 Elasticsearch权威指南(中文版) ,内容很全面。

    参考文献

    • https://es.xiaoleilu.com/    《Elasticsearch权威指南》

    时间 2018-11-03 23:30:34
    主题: ElasticSearch 分布式系统
    原文地址:http://www.tuicool.com/articles/iAfiA3J