ElasticSearch作为一个近实时的分布式搜索系统,构建于全文搜素引擎Lucene基础之上,是一个风靡已久的开源项目。其中对于分布式架构设计有很多值得学习和借鉴的地方。本文将通过介绍ElasticSearch的CRUD入手,一窥内部的运作机理,最后简单介绍ElasticSearch在环信产品中的典型应用。

1.jpg

基本概念:

  节点(Node):集群中一个ElasticSearch的实例

  集群(Cluster):一个或多个拥有相同cluster name的节点组成,共同承担着数据和负载的压力

  索引(Index):用来保存相关数据的地方,这是个逻辑概念,实际上指向一个或者多个物理分片

  类型(Type):对索引数据的逻辑分区

  分片(Shard):索引可以被划分成多个分片,每个分片都保存着部分数据,并且每一个分片都是一个Lucene的实例。为了保证数据的可靠性和可用性,分片可以分为主分片和副本分片。一个节点可以保存多个分片,但是包含相同数据的主副分片会避免分配到同一节点上去

  文档(Document):是被索引的基本数据单元

  一、Create

  对于新增文档属于写操作,其完成流程如下图所示:

2.jpg

      1.首先请求会到达ElasticSearch其中一个节点上:node1,node1将作为协调节点(coordinating node)负责处理这次请求。协调节点默认会根据该文档的ID进行hash运算找到该文档应该存在于哪一个主分片上,具体的计算公式为:

  shard=hash(routing)%number_of_primary_shards

  其中,routing默认为文档ID,当然也可以对该字段自定义,从而决定对应文档存储于哪个分片之上。number_of_primary_shards是集群中主分片的个数,因此在建立索引的时候需要指定主分片的个数,且不能改变。协调节点node1就会根据计算结果将请求转发到节点node3的主分片P2

  2.当node3收到写操作的请求后,对将文档写入主分片P2中

  3.node3随后会将写请求转发到该分片的副本分片R2所在节点node2上,并待其完成并返回成功

  4.待到主分片P2和副本分片R2都写入成功后,node3会将写操作成功的结果返回给协调节点node1,随后返回给请求方

  二、Read

  其中读操作可以分为两种,一种是根据文档ID获取内容,另外一种是根据搜索相关的文档。下面先介绍下根据文档ID查询,其实现流程如下图所示:

3.jpg

      1.请求到达了node3节点,node3将作为本次操作的协调节点。已经知道文档ID,便可以直接hash得到该文档存储的分片信息

  2.node3会根据负载均衡算法来决定请求主分片和副本分片中的哪一个,上图node3将请求转发到副本分片R0所在的node1节点

  3.副本分片R0会找到该文档内容并返回给协调节点node3,最后node3将结果返回给请求方

  另外一种读操作是搜索,搜索可以划分为两个阶段,Query阶段和Fetch阶段

  Query阶段:

4.jpg

      1.搜索请求到达节点node2,作为协调节点node2会创建一个空的优先队列,大小为size

  2.node2会将搜索请求转发到该索引的所有分片(主分片或副分片)。搜索请求到达后,在对应的节点也会建立同样大小为size的优先队列,并将根据搜索文档的相关性分数在本地优先队列中进行排序,得到最满足搜索条件的数量为size的文档列表

  3.对应节点在完成搜索后将优先队列的结果返回给协调节点,其中结果内容有文档ID和相关性分数等信息。协调节点收到各个节点返回的结果,会在其本地优先进行一次全局的排序,得到最终满足条件的结果列表。


  Fetch阶段:

5.jpg

      1.协调节点node2在对搜集到的数据在本地优先队列根据相关性分数完成全局排序后,会根据队列里的文档ID去获取对应的文档数据

  2.每个分片会将对应的文档数据返回给协调节点

  3.当协调节点将优先队列里的文档数据收集完成后,就会将结果返回给请求方


  三、Update和Delete

  在ElasticSearch中,Segment在生成后是不能被修改的。每个Segment都会有一个.del的文件,删除后的文档信息都会保存于该文件中。需要删除指定的文档时,ElasticSearch将文档信息保存于.del文件。对Segment进行搜索时,依旧会搜到已经删除的文档数据,但是在返回搜索结果集前会根据.del中的文件信息进行过滤,移除已经删除的文档数据,然后再返回结果集。

  对于更新操作,ElasticSearch会在文档中有version字段,用于表示文档的新旧程度。在对文档进行更新后,旧文档的信息被被标记到.del文件中,新文档的数据则会被索引到新的Segment中,并且文档中的version字段+1。在进行搜索时,新旧文档数据都会被搜索到,但是旧文档数据会在返回结果前过滤。

  分片内部流程

  下面以文档写入主分片为例介绍内部实现流程:

6.jpg

在节点的主分片收到协调节点发过来的写请求,会将文档写入到内存的Buffer和Transaction Log中,之后会将写请求转发到对应的副本分片。

  Buffer中的文档数据会被生成为新的Segment,并且保留在内存中,默认每秒会生成一个Segment。至于ElasticSearch为什么将Segment保存于内存中,是因为在将数据写入磁盘时用到了fsync,该操作时较为耗时且消耗资源,不可能频繁的将数据写入磁盘,但是如果在间隔一段时间后写入,会造成写入的文档数据没有办法被搜索到。因此ElasticSearch默认会每秒将Buferr中数据生成新的Segment保存于内存中,然后每隔一段时间将Segment数据经过压缩后写入到磁盘中。另外,当搜索请求过来后,内存中Segment也会被搜索,实现了数据读写近实时性

  由于写入的文档数据在一定时间内是保存于内存当中,如果节点发生故障导致异常关闭,会丢失内存中的数据。为此,ElasticSearch引入了Transaction Log,在将文档数据写入到内存里的Transaction Log后,会被flush到磁盘中,保证不会因为机器故障造成数据的丢失。Transaction Log默认每30分钟或者当其保存数据过多时,数据会被清空并flush到磁盘。


  在环信客服系统的典型应用场景

  作为一个较为成熟的分布式搜索系统,并且其开源社区也比较活跃,因此ElasticSearch在环信产品中很多应用,下面介绍下ElasticSearch在环信客服系统中的典型应用场景—历史会话的存储。

  历史会话是环信客服系统中是非常重要的一个环节,历史会话信息会在各个端上的会话界面展示并且用户随时会查看、搜索、导出这些会话信息,并且后台在有延迟时用户可以立马感知到。如果使用传统的关系型数据库很难满足大数据量下的查询搜索的业务场景,而常见的分布式数据库不能提供随时搜索的功能。在这里ElasticSearch就展示出它真正的优势,对于历史会话而言,一般在产生之后就不会再去更改,而是会频繁的查询,而这些正是ElasticSearch作为一个分布式搜索引擎最擅长的。

  在环信客服系统中,负责历史会话的服务会监控Kafka的会话事件,并将其同步写入到ElasticSearch之中,其它服务或者用户就可以使用ElasticSearch中的历史会话数据满足相应的业务需要。

     具体的实现架构如下图所示:

7.jpg

本文通过介绍ElasticSearch的关于CRUD的实现过程,展示其作为一个分布式数据库和搜索引擎是如何运作。并且介绍了ElasticSearch在客服系统中历史会话的实际应用,该架构已经满足了绝大部分用户的使用场景,但是由于历史数据累积过多以及偶发的数据读写延迟等问题,这依然需要客服后台团队进行不断的优化,为用户提供更加满意的服务。