zoukankan      html  css  js  c++  java
  • ElasticSearch分布式高级特性

      es支持集群模式,是一个分布式系统,其好处主要有两个:

    1. 增大系统容量,如内存,磁盘,使得es集群可以支持PB级的数据
    2. 提高系统可用性,即使部分节点停止服务,整个集群依然可以正常服务

      es集群由多个es实例组成,不同集群通过集群名字来区分,可通过cluster.name来进行修改,默认为elasticsearch。每个es实例本质上是一个JVM进程,且有自己的名字,可以通过node.name来进行修改

      运行如下命令可以快速启动一个es节点的实例( -d 后台隐式启动 无控制台日志输出)

    bin/elasticsearch -Ecluster.name=my_cluster -Epath.data=my_cluster_node1 -Enode.name=node1 -Ehttp.port=5100 -d

      es 集群相关的数据称为 cluster state,主要记录如下信息:节点信息,比如节点名称、链接地址等索引信息,比如索引名称、配置等  ,可以通过  http://localhost:9200/_cluster/state?pretty 进行查看

    Cluster Node:

      master 节点有以下几个特点:

    • 可以修改cluster state的节点称为master节点,一个集群只能有一个
    • cluster state存储在每个节点上,master维护最新版本并同步给其他节点
    • master 节点是通过集群中所有节点选取产生的,可以被选举的节点称为master-eligible节点

      相关配置:node.master:true,对于每一种可视化工具来说,对于各个节点的状态都有对应的标识方式,以 cerebro 来说:

      处理请求的节点称为coordinating节点,该节点为所有节点的默认角色,不能取消,路由请求到正确的节点处理,比如创建索引的请求到master节点

      存储数据的节点称为 data节点,默认节点都是data类型,相关配置如下:node.data:true

    新增节点:

      只要保证启动的时候 cluster.name 保持一致,就会自动加入该集群,若不在一个网络下则需要通过 discovery.zen.ping.unicast.hosts: ["127.0.0.1"]  集群的 IP 组,配置主节点 IP 即可。

    bin/elasticsearch -Ecluster.name=my_cluster -Epath.data=my_cluster_node2 -Enode.name=node2 -Ehttp.port=5200 -d

    副本与分片:

      通过可视化工具我们可以创建一个索引:

      这个时候可以通过控制台查看该索引的分片与副本:

    数据扩容:

      那么如何将数据分布到所有节点上?

    • 引入分片(Shard)解决问题
    • 分片是es支持PB级数据的基石,创建完后不允许改变,因为document路由到分片是通过hash取模的方式,如果可以任意改变,那么会导致数据迁移,性能下降。
    • 分片存储了部分数据,可以分布于任意节点上
    • 分片数在索引创建时指定且后续不允许再更改,默认为5个
    • 分片有主分片和副本分片之分,以实现数据的高可用
    • 副本分片的数据由主分片同步,可以有多个,从而提高读取的吞吐量

      接下去通过es的Rest API来创建索引,其中我们设置分片数为3、副本数为1

      如上图此时增加节点是否能够提高test_index的数据容量吗?答案是不能,因为只有3个分片,已经分布到3个节点上,新增的节点无法利用

      此时增加副本数是否能提高test_index的读取吞吐量呢?答案是不能,因为新增的副本是分布在3个节点上,还是利用了同样的资源,

      如果要增加吞吐量,还需要增加节点。增加数据容量对应增加副本数

      分片数的设定非常重要,需要提前规划好,分片数太少,导致后续无法通过增加节点实现水平扩容分片数过大,导致一个节点上分布多个分片,造成资源浪费,同时会影响查询性能

    倒排索引不可变更:

      ElasticSearch引擎把文档数据写入到倒排索引(Inverted Index)的数据结构中,倒排索引建立的是分词(Term)和文档(Document)之间的映射关系,在倒排索引中,数据是面向词(Term)而不是面向文档的。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表示例:

    倒排索引-查询过程

    • 查询包含“搜索引擎”的文档,通过倒排索引获得“搜索引擎”对应的文档id列表,有1,3
    • 通过正排索引查询1和3的完整内容
    • 返回最终结果

    倒排索引-组成

    • 单词词典(Term Dictionary)
    • 倒排列表(Posting List)

      单词词典(Term Dictionary)单词词典的实现一般用B+树,如上图所示。

    倒排索引一旦生成,不能更改,有如下好处:

    • 不用考虑并发写文件的问题,杜绝了锁机制带来的性能问题
    • 由于文件不再更改,可以充分利用文件系统缓存,只需要载入一次,只要内存足够,
    • 对该文件的读取都会从内存读取,性能高
    • 利于生成缓存数据
    • 利于对文件进行压缩存储,节省磁盘和内存存储空间

      坏处:

    • 写入新文档时,必须重新构建倒排索引文件,然后替换老文件后,新文档才能被检索,
    • 导致文档实时性受到影响

      解决方案:新文档直接生成新的倒排索引文件,查询的时候同时查询所有的倒排文件,然后对查询结果做汇总计算即可.

      Lucene采用了这种方案,它构建的单个倒排索引称为segment,合在一起称为Index,与ES中的Index概念不同。ES中的一个Shard对应一个Lucene Index。Lucene会有一个专门的文件来记录所有的segment信息,称为Commit Point

      segment写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将segment在缓存中创建并开放查询来进一步提升实时性,该过程在es中被称为refresh。在refresh之前文档会先存储在一个buffer中,refresh时将buffer中的所有文档清空并生成segmentes默认每1秒执行一次refresh,因此文档的实时性被提高到1秒,这也是es被称为近实时(Near Real Time)的真正原因

      如果在内存中的segment还没有写入磁盘前发生了宕机,那么内存中的文档就无法恢复了。那么如何解决这个问题呢?es引入translog机制。写入文档到buffer时,同时将该操作写入translog。translog 文件会即时写入磁盘(fsync),6.x默认每个请求都会落盘,可以修改为每5秒写一次,这样风险便是丢失5秒内的数据,相关配置为index.translog.*es重新启动时会自动检查translog文件,并从中恢复数据

      flush负责将内存中的segment写入磁盘,主要做如下的工作:

    • 将translog写入磁盘
    • 将index buffer清空,其中的文档生成一个新的segment,相当于一个refrsh操作
    • 更新commit point并写入磁盘
    • 执行fsync操作,将内存中的segment写入磁盘
    • 删除旧的translog文件

      refresh发生的时机主要有以下几种情况:

    • 间隔时间达到时,通过index.settings.refresh_interval来设定,默认是1秒
    • index.buffer占满时,其大小通过indices.memory.index_buffer_size设置,默认为jvm heap的10%,所有shard共享flush发生时也会发生refresh
    • 间隔时间达到时,默认是30分钟,5.x之前可以通过index.translog.flush_threshold_period修改之后发布的版本无法设置
    • translog占满时,其大小可以通过index.translog.flush_threshold_size控制,默认是512MB,每个index有自己的translog

    segment一旦生成就不能更改,那么如果要删除文档该如何操作?

      lucene会专门维护一个.del的文件,记录所有已经删除的文档,注意.del上记录的是文档在Lucene的内部id,在查询结果返回前会过滤掉.del中所有的文档

    更新文档如何进行呢?

      首先删除文档,然后再创建新的文档

      随着segment的增多,由于一次查询的segment数增多,查询速度会变慢,es会定时在后台进行segment merge的操作,减少segment的 数量,通过force_merge api可以手动强制做segment merge的操作

    集群状态:

      通过如下api可以查看集群健康状况,包括以下三种:

    • green:健康状态,指所有主副分片都正常分配
    • yellow:指所有主分片都正常分配,但是有副本分片未正常分配
    • red:有主分片未分配

      三种状态只是代表分片的工作状态,并不是代表整个es集群是否能够对外提供服务,可以通过 GET _cluster/health 获取集群状态:

    故障转移:

      集群由3个节点组成,如下所示,此时集群状态是green

      master 所在机器宕机导致服务终止,此时集群会如何处理?

    1. slave-1 和 slave-2 发现 master 无法响应一段时间后会发起master选举,比如这里选举slave-1为master节点,此时由于主分片1、4下线,集群状态变为red。
    2. slave-1(也就是现在的master)发现主分片 1、4未分配,将slave-2 上的1、4副本分片提升为主分片。此时由于所有主分片都正常分配,集群状态变为yellow。
    3. slave-1(也就是现在的master)发现主分片1、4生成新的副本,集群状态变为green。

    脑裂问题:

      脑裂问题,英文为split-brain,是分布式系统中的经典网络问题,如下图所示:

      此时master断开了,slave-1与slave-2会重新选举master,比如slave-1成为了新master,此时会更新cluster state。而此时此刻,master复活了。自己组成集群后,也会更新cluster state。同一个集群有两个master,而维护不同的cluster state,网络恢复后无法选择正确的master,这就是脑裂问题,一个身体有两个大脑,不知道听谁的了

      解决方案为仅在可选举master-eligible节点数据大于等于quorum时才可以进行master选举,quorum = master-eligible 节点数/2 + 1,例如 3个master-eligible节点时,quorum为2。

      解决:配置 discovery.zen.mininum_master_nodes为quorum即可避免脑裂。

    分布式存储:

      比如执行以下插入操作,这个doc会落到那个分片上呢?:

      假设doc/1最终存储在分片1上,那么是如何存储到分片1的呢?选择1的依据是什么呢?这里就需要文档到分片的映射算法

      目的:使得文档均匀分布在所有分片上,以充分利用资源

      es通过如下公司计算文档对应的分片:shard = hash(routing) % number_of_primary_shards

    1. hash算法保证可以将数据均匀地分散在分片中
    2. routing是一个关键参数,默认是文档id,也可以自行指定
    3. number_of_primary_shards 是主分片数

      该算法与主分片数相关,这也是分片数一旦确定后便不能更改的根本原因。看到这里是不是想到了 kafka Topic消息的路由规则呢?可以说是一模一样的效果。

    文档的创建流程:

    1. 当Clinet向一个节点比如node3发起创建文档的请求
    2. node3通过routing计算该文档应该存储在Shard1上,查询cluster state后确认主分片P1在node2上,然后转发创建文档的请求到node2
    3. P1接收并执行创建文档的请求后,将同样的请求发送到副本分片R1
    4. R1接收并执行创建文档请求后,通知P1成功的结果

    文档读取请求流程:

    1. Clinet向node3发起创建文档的请求
    2. node3通过routing计算该文档应该存储在Shard1上,查询cluster state后获取 Shard 1的主副分片列表,然后以轮询的机制获取一个shard,比如这里是R1,然后转发读取文档的请求到node1
    3. R1接收并执行创建文档的请求后,将结果返回给node3
    4. node3返回结果给Client

    Search的运行机制:

      Search执行的时候实际分两个步骤运行的

    1. Query阶段
    2. Fetch阶段

    Query阶段:

    1. node3在接收到用户的search请求后,先会进行Query阶段(此时Coordinating Node角色)
    2. node3在6个主副分片中随机选择3个分片,发送search request
    3. 被选中的3个分片会分别执行查询并排序,返回from+size个文档Id和排序值

    Fetch阶段:

      node3根据Query阶段获取到文档Id列表对应的shard上获取文档详情数据

    1. node3向相关的分片发送multi_get请求
    2. 3个分片返回文档详细数据
    3. node3拼接返回的结果并返回给用户

    相关性算分:

      相关性算分在shard与shard间是相互独立的,也就意味着同一个Tearm的IDF等值在不同shard上是不同的。文档的相关性算法和它所处的shard相关在文档数量不多时,会导致相关性算分严重不准的情况发生

      解决思路有两个:

    1. 设置分片数为1个,从根本上排除问题,在文档数量不多的时候可以考虑该方案,比如百万到千万级别的文档数量
    2. 采用DFS Query-then-Fetch的查询方式

      DFS Query-then-Fetch是拿到所有文档后再重新完整的计算一次相关性算法,耗费更多的cpu和内存,执行性能也比较低下,一般不建议使用。操作方式如下:

       如果不采用DFS Query-then-Fetch,则结果的顺序就不尽人意了:

    ElasticSearch 基本操作:

      创建非结构化的索引:

       创建结构化的索引,当然也可以通过 postman操作:

      这里需要注意的是在6.0.0或者更高版本中仅支持单Mapping。对于5.X版本中多个Mapping依旧可以使用。计划在7.0.0中完全移除Mapping【移除是符合情理的,你看现在就只支持单Mapping,跟直接访问index没啥区别了】

      插入数据,自动生成文档 ID 插入:

       指定文档 ID 插入

       修改文档:

       删除文档:

      基础查询,先创建一个索引,对应一个 type,然后3个属性:

       然后初始化一批数据:

       查询所有:book/_search,默认显示10条,可以通过 "from" : 0, "size" : 20,来修改

      条件查询: book/_search,然后编辑如下内容:查询书籍中包含 spring关键字,且按发版日期降序排序。

      聚合查询: book/_search,然后编辑如下内容:根据书籍价格和发版日期进行分组

    {
      "aggs": {
        "group_by_price": {
          "terms": {
            "field": "price"
          }
        },
        "group_by_publication_date": {
          "terms": {
            "field": "publication_date"
          }
        }
      }
    }

      结果如下,结果很长,我就截取了一部分:

      聚合统计:book/_search,然后如下内容:根据书籍的价格进行聚合统计。

    {
      "aggs": {
        "grades_price": {
          "stats": {
            "field": "price"
          }
        }
      }
    }

      结果如下,结果很长,我就截取了一部分:

      模糊匹配,book/_search,查询标题中包含”spri”和”j”关键字的书籍

      多字段匹配 查询价格和标题中都是”100”的书籍

      Query 语法查询,查询名称和价格中同时包含 100和 200,或者包含 300的书籍。

    {
      "query": {
        "query_string": {
          "query": "(100 AND 200) OR 300",
          "fields": [
            "name",
            "price"
          ]
        }
      }
    }

      查询价格在 120到 200之间的数据

      查询 2018-01-01 至今发版的所有书籍

       filter 条件:筛选出符合条件的数据

       复合查询:

  • 相关阅读:
    小记:xml画一个爱心。
    类似UC天气下拉和微信下拉眼睛头部弹入淡出UI交互效果(开源项目)。
    FloatingActionButton增强版,一个按钮跳出多个按钮--第三方开源--FloatingActionButton
    回调机制的实现。
    小记:使用SharedPreferences存储来设置程序第一次进入欢迎界面,以后不会再进入欢迎界面。
    小记:获取系统时间的long值,格式化成可读时间。
    写程序的欢迎界面(运用画图方法画圆球)。
    并发的HashMap为什么会引起死循环?
    zuul重试配置
    zuul超时问题
  • 原文地址:https://www.cnblogs.com/wuzhenzhao/p/12891210.html
Copyright © 2011-2022 走看看