ElasticSearch分布式集群

2,374 阅读18分钟

ElasticSearch核心概念

  • 集群(Cluster)
    • 一个Elasticsearch集群由多个节点(Node)组成,每个集群都有一个共同的集群名称作为标识
  • 节点(Node)
    • 一个Elasticsearch实例即一个Node,一台机器可以有多个实例,正常使用下每个实例都应该会部署在不同的机器上。Elasticsearch的配置文件中可以通过node.master、node.data来设置节点类型。
    • node.master:表示节点是否具有成为主节点的资格
      • true代表的是有资格竞选主节点
      • false代表的是没有资格竞选主节点
    • node.data:表示节点是否存储数据
  • Node节点组合
    • 主节点+数据节点(master+data) 默认
      • 节点既有成为主节点的资格,又存储数据

        node.master: true
        node.data: true

      • 数据节点(data)
        节点没有成为主节点的资格,不参与选举,只会存储数据

        node.master: false
        node.data: true

      • 客户端节点(client)
        不会成为主节点,也不会存储数据,主要是针对海量请求的时候可以进行负载均衡

        node.master: false
        node.data: false

  • 分片
    • 每个索引有1个或多个分片,每个分片存储不同的数据。分片可分为主分片(primary shard)和复制分片(replica shard),复制分片是主分片的拷贝。默认每个主分片有一个复制分片,每个索引的复制分片的数量可以动态地调整,复制分片从不与它的主分片在同一个 节点上
  • 副本
    这里指主分片的副本分片(主分片的拷贝)
    提高恢复能力:当主分片挂掉时,某个复制分片可以变成主分片;提高性能:get和search 请求既可以由主分片又可以由复制分片处理

Elasticsearch分布式架构

Elasticseasrch的架构遵循其基本概念:一个采用Restful API标准的高扩展性和高可用性的实时数据分析的全文搜索引擎。

特性

  • 扩展性:体现在Elasticsearch添加节点非常简单,新节点无需做复杂的配置,只要配置好集群信息将会被集群自动发现。
  • 高可用性:因为Elasticsearch是分布式的,每个节点都会有备份,所以宕机一两个节点也不会出现问题,集群会通过备份进行自动复盘。
  • 实时性:使用倒排索引来建立存储结构,搜索时常在百毫秒内就可完成。

分层

image.png

  • 第一层 —— Gateway
    Elasticsearch支持的索引快照的存储格式,es默认是先把索引存放到内存中,当内存满了之后再持久化到本地磁盘。gateway对索引快照进行存储,当Elasticsearch关闭再启动的时候,它就会从这个 gateway里面读取索引数据;支持的格式有:本地的Local FileSystem、分布式的Shared FileSystem、 Hadoop的文件系统HDFS、Amazon(亚马逊)的S3。
  • 第二层 —— Lucene框架:
    Elasticsearch基于Lucene(基于Java开发)框架。
  • 第三层 —— Elasticsearch数据的加工处理方式:
    Index Module(创建Index模块)、Search Module(搜索模块)、Mapping(映射)、River代表es的一个数据源(运行在Elasticsearch集群内部的一个插件,主要用来从外部获取获取异构数据,然后在Elasticsearch里创建索引;常见的插件有RabbitMQ River、Twitter River)。
  • 第四层 —— Elasticsearch发现机制、脚本:
    Discovery是Elasticsearch自动发现节点的机制的模块,Zen Discovery和 EC2 discovery。EC2:亚马逊弹性计算云EC2 discovery主要在亚马云平台中使用。Zen Discovery作用就相当于solrcloud中的zookeeper。zen Discovery 从功能上可以分为两部分,第一部分是集群刚启动时的选主,或者是新加入集群的节点发现当前集群的Master。第二部分是选主完成后,Master 和 Folower 的相互探活。
    Scripting 是脚本执行功能,有这个功能能很方便对查询出来的数据进行加工处理。
    3rd Plugins 表示Elasticsearch支持安装很多第三方的插件,例如elasticsearch-ik分词插件、 elasticsearch-sql sql插件。
  • 第五层 —— Elasticsearch的交互方式:
    有Thrift、Memcached、Http三种协议,默认的是用Http协议传输
  • 第六层 —— Elasticsearch的API支持模式:
    RESTFul Style API风格的API接口标准是当下十分流行的。Elasticsearch作为分布式集群,客户端到服务端,节点与节点间通信有TCP和Http通信协议,底层实现为Netty框架

解析Elasticsearch的分布式架构

分布式架构的透明隐藏特性

Elasticsearch是一个分布式系统,隐藏了复杂的处理机制分片机制:将文本数据切割成n个小份存储在不同的节点上,减少大文件存储在单个节点上对设备带来的压力。分片的副本:在集群中某个节点宕掉后,通过副本可以快速对缺失数据进行复盘。

  • 集群发现机制(cluster discovery):在当前启动了一个Elasticsearch进程,在启动第二个 Elasticsearch进程时,这个进程将作为一个node自动就发现了集群,并自动加入,前提是这些 node都必须配置一套集群信息。
  • Shard负载均衡:例如现在由10 shard(分片),集群中有三个节点,Elasticsearch会进行均衡的分配,以保持每个节点均衡的负载请求。
  • rebalance再均衡
    增加或减少节点时会自动负载

主节点

主节点的主要职则是和集群操作的相关内容,如创建或删除索引,跟踪哪些节点是集群的一部分,并决定哪些分片分配给相关的节点。稳定的主节点对集群的健康是非常重要的。

节点对等

每个节点都能接受请求,每个节点接受到请求后都能把该请求路由到有相关数据的其它节点上,接受原始请求的节点负责采集数据并返回给客户端。

image.png

集群规划

我们需要多大规模的集群

需要从以下两个方面考虑:

  1. 当前的数据量有多大?数据增长情况如何?
  2. 你的机器配置如何?cpu、多大内存、多大硬盘容量?
  • 推算的依据:
    • Elasticsearch JVM heap 最大可以设置32G 。30G heap大概能处理的数据量10T。如果内存很大如128G,可在一台机器上运行多个ES节点实例。
      备注:集群规划满足当前数据规模+适量增长规模即可,后续可按需扩展。
  • 两类应用场景:
    • A. 用于构建业务搜索功能模块,且多是垂直领域的搜索。数据量级几千万到数十亿级别。一般2-4台机器的规模。
    • B. 用于大规模数据的实时OLAP(联机处理分析),经典的如ELK Stack,数据规模可能达到千亿或更多。几十到上百节点的规模。

集群中的节点角色如何分配

  • 节点角色:
    • Master
      node.master: true 节点可以作为主节点
    • DataNode
      node.data: true 默认是数据节点
    • Coordinate node
      协调节点,一个节点只作为接收请求、转发请求到其他节点、汇总各个节点返回数据等功能的节点,就叫协调节点,如果仅担任协调节点,将上两个配置设为false。 说明:一个节点可以充当一个或多个角色,默认三个角色都有
  • 节点角色如何分配:
    • 小规模集群,不需严格区分。
    • 中大规模集群(十个以上节点),应考虑单独的角色充当。特别并发查询量大,查询的合并量大,可以增加独立的协调节点。角色分开的好处是分工分开,不互影响。如不会因协调角色负载过高而影响数据节点的能力。

如何避免脑裂问题

什么是脑裂?
一个集群中只有一个A主节点,A主节点因为需要处理的东西太多或者网络过于繁忙,从而导致其他从节点ping不通A主节点,这样其他从节点就会认为A主节点不可用了,就会重新选出一个新的主节点B。过 了一会A主节点恢复正常了,这样就出现了两个主节点,导致一部分数据来源于A主节点,另外一部分数 据来源于B主节点,出现数据不一致问题,这就是脑裂。

如何解决

  • 6.x和之前版本
    尽量避免脑裂,需要添加最小数量的主节点配置: discovery.zen.minimum_master_nodes: (有master资格节点数/2) + 1 这个参数控制的是,选举主节点时需要看到最少多少个具有master资格的活节点,才能进行选举。官方的推荐值是(N/2)+1,其中N是具有master资格的节点的数量。
  • 新版7.X之后
    对es的集群发现系统做了调整,不再有discovery.zen.minimum_master_nodes这个控制集群脑裂的配置,转而由集群自主控制,并且新版在启动一个新的集群的时候需要有 cluster.initial_master_nodes初始化集群列表。
  • 常用做法(中大规模集群):
    1. Master 和 dataNode 角色分开,配置奇数个master,如3
    2. 单播发现机制,配置master资格节点(5.0之前): discovery.zen.ping.multicast.enabled: false —— 关闭多播发现机制,默认是关闭的
    3. 延长ping master的等待时长 discovery.zen.ping_timeout: 30(默认值是3秒)——其他节点ping主节点多久时间没有响应就认 为主节点不可用了。es7中换成了 discovery.request_peers_timeout

索引应该设置多少个分片

说明:分片数指定后不可变,除非重建索引。
分片设置的可参考原则:
ElasticSearch推荐的最大JVM堆空间是30 ~ 32G, 所以把你的分片最大容量限制为30GB, 然后再对分片数量做合理估算. 例如, 你认为你的数据能达到200GB, 推荐你最多分配7到8个分片。在开始阶段, 一个好的方案是根据你的节点数量按照1.5 ~ 3倍的原则来创建分片. 例如,如果你有3个节点, 则推荐你创建的分片数最多不超过9(3x3)个。当性能下降时,增加节点,ES会平衡分片的放置。对于基于日期的索引需求, 并且对索引数据的搜索场景非常少. 也许这些索引量将达到成百上千, 但每个索引的数据量只有1GB甚至更小. 对于这种类似场景, 建议只需要为索引分配1个分片。如日志管理就是一个日期的索引需求,日期索引会很多,但每个索引存放的日志数据量就很少。
单台服务器存储分片数=每gb堆内存×20 &&<750。
每个节点建议的单个索引分片数<3:因为分片分布在同一个服务器上。请求开始竞争相同的硬件资源时, 性能便会逐步下降。

分片应该设置几个副本

副本设置基本原则:
为保证高可用,副本数设置为2即可。要求集群至少要有3个节点,来分开存放主分片、副本。如发现并发量大时,查询性能会下降,可增加副本数,来提升并发查询能力。
注意:新增副本时主节点会自动协调,然后拷贝数据到新增的副本节点,副本数是可以随时调整的!

调优策略

Index(写)调优

  1. 副本数置0 如果是集群首次灌入数据,可以将副本数设置为0,写入完毕再调整回去,这样副本分片只需要拷 贝,节省了索引过程。
PUT /my_temp_index/_settings
{
 "number_of_replicas": 0
}
  1. 自动生成doc ID
    通过Elasticsearch写入流程可以看出,如果写入doc时如果外部指定了id,则Elasticsearch会先尝试 读取原来doc的版本号,以判断是否需要更新。这会涉及一次读取磁盘的操作,通过自动生成doc ID可以避免这个环节。

  2. 合理设置mappings

    • 将不需要建立索引的字段index属性设置为not_analyzed或no。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词通常没有什么意义。
    • 减少字段内容长度,如果原始数据的大段内容无须全部建立 索引,则可以尽量减少不必要的内 容。
    • 使用不同的分析器(analyzer),不同的分析器在索引过程中运算复杂度也有较大的差异。
  3. 调整_source字段
    source字段用于存储doc原始数据,对于部分不需要存储的字段,可以通过includes excludes过滤,或者将source禁用,一般用于索引和数据分离,这样可以降低I/O的压力,不过实际场景中大多不会禁用_source。

  4. 对analyzed的字段禁用norms
    Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用:

    "title": {
       "type": "string",
       "norms": {
         "enabled": false
       }
     }
    
  5. 调整索引的刷新间隔(refresh_interval)
    该参数缺省是1s,强制ES每秒创建一个新segment,从而保证新写入的数据近实时的可见、可被搜索 到。比如该参数被调整为30s,降低了刷新的次数,把刷新操作消耗的系统资源释放出来给index操作使用。

     PUT /my_index/_settings
     {
       "index": {
         "refresh_interval": "30s"
       }
     }
    

    这种方案以牺牲可见性的方式,提高了index操作的性能。

  6. 批处理
    批处理把多个index操作请求合并到一个batch中去处理,和mysql的jdbc的bacth有类似之处。如图: image.png 比如每批1000个documents是一个性能比较好的size。每批中多少document条数合适,受很多因素影响而不同,如单个document的大小等。ES官网建议通过在单个node、单个shard做性能基准测试来确定这个参数的最优值。

  7. Document的路由处理
    当对一批documents进行index操作时,该批index操作所需的线程的个数由要写入的目的shard的个数决定。看下图: image.png

    上图中,有2批documents写入ES, 每批都需要写入4个shard,所以总共需要8个线程。如果能减少shard的个数,那么耗费的线程个数也会减少。例如下图,两批中每批的shard个数都只有2个,总共线程消耗个数4个,减少一半。 默认的routing就是id,也可以在发送请求的时候,手动指定一个routing value,比如说put /index/doc/id?routing=user_id

    image.png

    注意线程数虽然降低了,但是单批的处理耗时可能增加了。和提高刷新间隔方法类似,这有可 能会延长数据不见的时间。

Search(读)调优

  1. 数据分组
    很多人拿ES用来存储日志,日志的索引管理方式一般基于日期的,基于天、周、月、年建索引。如下图,基于天建索引: image.png 当搜索单天的数据,只需要查询一个索引的shards就可以。当需要查询多天的数据时,需要查询多个索引的shards。这种方案其实和数据库的分表、分库、分区查询方案相比,思路类似,小数据范围查询而不是大海捞针。 开始的方案是建一个index,当数据量增大的时候,就扩容增加index的shard的个数。当shards增大时,要搜索的shards个数也随之显著上升。基于数据分组的思路,可以基于client进行数据分组,每一个client只需依赖自己的index的数据shards进行搜索,而不是所有的数据shards,大大提高了搜索的性能,如下图:

    image.png

  2. 使用Filter替代Query
    在搜索时候使用Query,需要为Document的相关度打分。使用Filter,没有打分环节处理,做的事情更少,而且filter理论上更快一些。
    如果搜索不需要打分,可以直接使用filter查询。如果部分搜索需要打分,建议使用'bool'查询。这种方 式可以把打分的查询和不打分的查询组合在一起使用,如:

    GET /study_test/_search
     {
       "query": {
         "bool": {
           "must": {
             "term": {
               "name": "elastic"
             }
           },
           "filter": {
             "term": {
               "price": 88
             }
           }
         }
       }
     }
    
  3. 数值字段定义为keyword
    一般情况,如果数值字段不会被用作Range类型搜索字段,都可以定义成keyword类型。这是因为keyword会被优化,以便进行terms查询。Integers等数字类的mapping类型,会被优化来进行range类型搜索。
    将integers改成keyword类型之后,搜索性能大约能提升30%。

深分页问题优化

ES默认采用的分页方式是from+ size的形式,类似于mysql的分页limit。当请求数据量比较大时, Elasticsearch会对分页做出限制,因为此时性能消耗会很大。举个例子,一个索引分10 shards,然后,一个搜索请求,from=990,size=10,这时候,会带来严重的性能问题: CPU、内存、IO、网络带宽

image.png CPU、内存和IO消耗容易理解,网络带宽问题稍难理解一点。在query 阶段,每个shard需要返回1000条数据给coordinating node,而 coordinating node需要接收10*1000条数据,即使每条数据只有_doc _id和_score,这数据量也很大了,而且这才一个查询请求,那如果再乘以100呢?所以深分页会造成严重的性能损耗

es中有个设置 index.max_result_window ,默认是10000条数据,如果分页的数据超过第1万条,就拒绝返回结果了。如果你觉得自己的集群还算可以,可以适当的放大这个参数,比如100万。
我们意识到,有时这种深度分页的请求并不合理,因为我们是很少人为的看很后面的请求的,在很多的业务场景中,都直接限制分页,比如只能看前100页。
不过,这种深度分页确实存在,比如有1千万粉丝的微信大V,要给所有粉丝群发消息,或者给某省粉丝群发,这时候就需要取得所有符合条件的粉丝,而最容易想到的就是利用from + size来实现,但这是不现实的,我们需要使用下面的解决方案。

解决方案

  1. 利用scroll遍历方式
    scroll分为初始化和遍历两步,初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照,在遍历时,从这个快照里取数据,也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果。因此,scroll并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发。

    1. 初始化
    POST /book/_search?scroll=1m&size=2
     {
       "query": {
         "match_all": {}
       }
     }
    

    初始化时需要像普通search一样,指明index(当然,search 是可以不指明index),然后,加上参数scroll,表示暂存搜索结果的时间,其它就像一个普通的search请求一样。初始化返回一个scroll_id,scroll_id用来下次取数据用。

    1. 遍历
    GET /_search/scroll
     {
       "scroll": "1m",
       "scroll_id": "步骤1中查询出来的值"
     }
    

    这里的scroll_id即上一次遍历取回 _scroll_id或者是初始化返回的_scroll_id,同样的,需要带scroll参数。重复这一步骤,直到返回的数据为空,即遍历完成。注意,每次都要传参数 scroll,刷新搜索结果的缓存时间。另外,不需要指定index。设置scroll的时候,需要使搜索结果缓存到下一次遍历完成,同时也不能太长,毕竟空间有限。

  2. search after方式
    满足实时获取下一页的文档信息,search_after分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改,这些变更也会实时的反映到游标上,这种方式是在es-5.X之后才提供的。为了找到每一页最后一条数据,每个文档的排序字段必须有一个全局唯一值使用 _id就可以了。

    GET /book/_search
     {
       "query": {
         "match_all": {}
       },
       "size": 2,
       "sort": [
         {
           "_id": "desc"
         }
       ]
     }
     GET /book/_search
     {
       "query": {
         "match_all": {}
       },
       "size": 2,
       "search_after": [
         3
       ],
       "sort": [
         {
           "_id": "desc"
         }
       ]
     }
    

下一页的数据依赖上一页的最后一条的信息 所以不能跳页。
注意
8.x之后就不建议用_id做fielddata进行range agge了,需自己在body中设置id。 如果还是需要用_id字段,需要在yml文件配置:indices.id_field_data.enabled: true image.png

三种分页方式比较

分页方式性能优点缺点场景
from + size灵活性好,实现简单深度分页问题数据量比较小,能容忍深度分页问题
scroll解决了深度分页问题无法反映数据的实时性(快 照版本)维护成本高,需要维护一个scroll_id海量数据的导出需要查询海量结果集的数据
search_after性能最好不存在深度分页问题能够反映数据的实时变更实现连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果海量数据的分页