ElasticSearch架构原理

1.Apache Lucene

1.1 Lucene介绍

Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言).Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎.Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供.Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻.在Java开发环境里Lucene是一个成熟的免费开源工具.就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库.人们经常提到信息检索程序库,虽然与搜索引擎有关,但不应该将信息检索程序库与搜索引擎相混淆.

1.2 数据查询分类

数据的分类

  • 结构化数据: 指具有固定格式或有限长度的数据,如数据库,元数据等.
  • 非结构化数据: 指不定长或无固定格式的数据,如邮件,word文档等磁盘上的文件

数据查询方法

  • 结构化数据
    • 数据库搜索
  • 非结构化数据
    • 顺序扫描法(Serial Scanning): 所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件.
    • 全文检索(Full-text Search)
      • 将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的.这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引.
      • 这种先建立索引,再对索引进行搜索的过程就叫全文检索.

1.3 Lucene全文索引

索引和搜索流程图

  • 索引过程,对要搜索的原始内容进行索引构建一个索引库,索引过程包括:
    • 确定原始内容即要搜索的内容
    • 采集文档
    • 创建文档
    • 分析文档
    • 索引文档
  • 搜索过程,从索引库中搜索内容,搜索过程包括:
    • 用户通过搜索界面
    • 创建查询
    • 执行搜索
    • 从索引库搜索
    • 渲染搜索结果

1.4 Lucene结构

Lucene核心概念

  • Document: 它是在索引和搜索过程中数据的主要表现形式,或者称"载体",承载着我们索引和搜索的数据,它由一个或者多个域(Field)组成.
  • Field: 它是Document的组成部分,由两部分组成,名称(name)和值(value).
  • Term: 它是搜索的基本单位,其表现形式为文本中的一个词.
  • Token: 它是单个Term在所属Field中文本的呈现形式,包含了Term内容、Term类型、Term在文本中的起始及偏移位置.

2.ElasticSearch

ElasticSearch是构建在极少数的几个概念之上的.ElasticSearch的开发团队希望它能够快速上手,可扩展性强.而且这些核心特性体现在ElasticSearch的各个方面.从架构的角度来看,这些主要特性是:

  • 开箱即用.安装好ElasticSearch后,所有参数的默认值都自动进行了比较合理的设置,基本不需要额外的调整.包括内置的发现机制(比如Field类型的自动匹配)和自动化参数配置.
  • 天生集群.ElasticSearch默认工作在集群模式下.节点都将视为集群的一部分,而且在启动的过程中自动连接到集群中.
  • 自动容错.ElasticSearch通过P2P网络进行通信,这种工作方式消除了单点故障.节点自动连接到集群中的其它机器,自动进行数据交换及以节点之间相互监控.索引分片
  • 扩展性强.无论是处理能力和数据容量上都可以通过一种简单的方式实现扩展,即增添新的节点.
  • 近实时搜索和版本控制.由于ElasticSearch天生支持分布式,所以延迟和不同节点上数据的短暂性不一致无可避免.ElasticSearch通过版本控制(versioning)的机制尽量减少问题的出现.

2.1 Elastic架构

Elastic架构图

模块介绍

  • Plugins: 插件可以通过自定的方式扩展加强Elasticsearch的基本功能,比如可以自定义类型映射,分词器,本地脚本,自动发现等
  • Scripting: 使用脚本语言可以计算自定义表达式的值,比如计算自定义查询相关度评分 支持的脚本语言有groovy,js,mvel(1.3.0废弃),python等
  • Disovery: 该模块主要负责集群中节点的自动发现和Master节点的选举.节点之间使用p2p的方式进行直接通信,不存在单点故障的问题.Elasticsearch中,Master节点维护集群的全局状态,比如节点加入和离开时进行shard的重新分配
  • River: 代表es的一个数据源,也是其它存储方式(如:数据库)同步数据到es的一个方法.它是以插件方式存在的一个es服务,通过读取river中的数据并把它索引到es中
  • Gateway: 模块用于存储es集群的元数据信息
  • Zen Discovery: zen发现机制是elasticsearch默认的内建模块.它提供了多播和单播两种发现方式,能够很容易的扩展至云环境.zen发现机制是和其他模块集成的,例如所有节点间通讯必须用trasport模块来完成

与SOLR比对 | | solr | elasticsearch | | --- | --- | --- | | 功能 | 官方提供的功能更多 | 官方功能少,但是第三方插件很丰富,扩展能力更强 | | 建立索引和查询效率| 立索引的速度很和 ES 差不多,索引建立完成后的检索速度也很快,但是一边建立索引一边搜索会很慢(建立索引时会造成旧阻塞) | 建立索引速度和solr差不多,第一次检索会比solr慢,之后就会快了(缓存结果).一边建立素引,一边搜索的速度不影响(索引会先存在内存中,内存不足再写入磁盘,还有队列空闲时把索引写入硬盘) | | 支持的数据格式| Xml等多种格式 | json | | 分布式管理 | zookeeper | 自己维护 | | sharding | 没有自动 shard rebalancing 的功能 | shard 必须一次设置好,之后不能修改,如果要修改则需要重新建立索引 | | 高级查询 | 没有 Query DSL | 有Query DSL,能够支持更加高级和复杂的查询语法,而且还可以以此扩展实现类SQL语法的查询 | | 搜索 | 传统搜索应用 | 实时搜索应用(1 秒的延迟) | | 插件 | 不支持插件式开发 | 支持插件开发模式,提供丰富的开源插件库 |

2.2 核心概念

  • 集群(Cluster): ES集群是一个或多个节点的集合,它们共同存储了整个数据集,并提供了联合索引以及可跨所有节点的搜索能力.多节点组成的集群拥有冗余能力,它可以在一个或几个节点出现故障时保证服务的整体可用性.集群靠其独有的名称进行标识,默认名称为"elasticsearch".节点靠其集群名称来决定加入哪个ES集群,一个节点只能属一个集群.
  • 节点(node): 一个节点是一个逻辑上独立的服务,可以存储数据,并参与集群的索引和搜索功能, 一个节点也有唯一的名字,群集通过节点名称进行管理和通信.
  • 主节点: 主节点的主要职责是和集群操作相关的内容,如创建或删除索引,跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点.稳定的主节点对集群的健康是非常重要的.虽然主节点也可以协调节点,路由搜索和从客户端新增数据到数据节点,但最好不要使用这些专用的主节点.一个重要的原则是,尽可能做尽量少的工作.对于大型的生产集群来说,推荐使用一个专门的主节点来控制集群,该节点将不处理任何用户请求.
  • 数据节点: 持有数据和倒排索引.
  • 客户端节点: 它既不能保持数据也不能成为主节点,该节点可以响应用户的情况,把相关操作发送到其他节点;客户端节点会将客户端请求路由到集群中合适的分片上.对于读请求来说,协调节点每次会选择不同的分片处理请求,以实现负载均衡.
  • 部落节点: 部落节点可以跨越多个集群,它可以接收每个集群的状态,然后合并成一个全局集群的状态,它可以读写所有节点上的数据.
  • 索引(Index): ES将数据存储于一个或多个索引中,索引是具有类似特性的文档的集合.
    • 类比传统的关系型数据库领域来说,索引相当于SQL中的一个数据库,或者一个数据存储方案(schema)
    • 索引由其名称(必须为全小写字符)进行标识,并通过引用此名称完成文档的创建、搜索、更新及删除操作
    • 一个ES集群中可以按需创建任意数目的索引
  • 文档类型(Type):类型是索引内部的逻辑分区(category/partition),然而其意义完全取决于用户需求.因此,一个索引内部可定义一个或多个类型(type).一般来说,类型就是为那些拥有相同的域的文档做的预定义.例如,在索引中,可以定义一个用于存储用户数据的类型,一个存储日志数据的类型,以及一个存储评论数据的类型.类比传统的关系型数据库领域来说,类型相当于"表".
  • 文档(Document):
    • 文档是Lucene索引和搜索的原子单位,它是包含了一个或多个域的容器,基于JSON格式进行表示
    • 文档由一个或多个域组成,每个域拥有一个名字及一个或多个值,有多个值的域通常称为"多值域"
    • 每个文档可以存储不同的域集,但同一类型下的文档至应该有某种程度上的相似之处.相当于数据库的"记录"
  • Mapping: 相当于数据库中的schema,用来约束字段的类型,不过 Elasticsearch的mapping可以自动根据数据创建.ES中,所有的文档在存储之前都要首先进行分析.用户可根据需要定义如何将文本分割成token、哪些token应该被过滤掉,以及哪些文本需要进行额外处理等等.
  • 分片(shard):ES的"分片(shard)"机制可将一个索引内部的数据分布地存储于多个节点,它通过将一个索引切分为多个底层物理的Lucene索引完成索引数据的分割存储功能,这每一个物理的Lucene索引称为一个分片(shard).每个分片其内部都是一个全功能且独立的索引,因此可由集群中的任何主机存储.创建索引时,用户可指定其分片的数量,默认数量为5个.
    • Shard有两种类型:primaryreplica,即主shard及副本shard.
      • Primary shard用于文档存储,每个新的索引会自动创建5个Primary shard,当然此数量可在索引创建之前通过配置自行定义,不过,一旦创建完成,其Primary shard的数量将不可更改.
      • Replica shardPrimary Shard的副本,用于冗余数据及提高搜索性能.
    • 每个Primary shard默认配置了一个Replica shard,但也可以配置多个,且其数量可动态更改.ES会根据需要自动增加或减少这些Replica shard的数量.
    • ES集群可由多个节点组成,各Shard分布式地存储于这些节点上.
    • ES可自动在节点间按需要移动shard,例如增加节点或节点故障时.简而言之,分片实现了集群的分布式存储,而副本实现了其分布式处理及冗余功能.
  • Gateway: 在运行的过程中,ElasticSearch会收集集群的状态、索引的参数等信息.这些数据被存储在Gateway中.

3.文档操作

3.1 创建索引

创建索引

创建索引过程

  • 当分片所在的节点接收到来自协调节点的请求后,会将该请求写入translog,并将文档加入内存缓存.
  • 如果请求在主分片上成功处理,该请求会并行发送到该分片的副本上.
  • 当translog被同步到全部的主分片及其副本上后,客户端才会收到确认通知.

3.2 检索文档

检索文档

搜索相关性 相关性是由搜索结果中Elasticsearch打给每个文档的得分决定的.默认使用的排序算法是tf/idf(词频/逆文档频率).词频衡量了一个词项在文档中出现的次数 (频率越高 == 相关性越高),逆文档频率衡量了词项在全部索引中出现的频率,是一个索引中文档总数的百分比(频率越高 == 相关性越低).最后的得分是tf-idf得分与其他因子比如(短语查询中的)词项接近度、(模糊查询中的)词项相似度等的组合

3.3 更新删除索引

磁盘上的每个段都有一个相应的.del文件.当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除.该文档依然能匹配查询,但是会在结果中被过滤掉.当段合并(我们将在本系列接下来的文章中讲到)时,在.del文件中被标记为删除的文档将不会被写入新段.

在新的文档被创建时,Elasticsearch会为该文档指定一个版本号.当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段.旧版本的文档依然能匹配查询,但是会在结果中被过滤掉.

物理删除索引:当索引数据不断增长时,对应的segment也会不断的增多,查询性能可能就会下降.因此,Elasticsearch会触发segment合并的线程,把很多小的segment合并成更大的segment,然后删除小的segment,当这些标记为删除的segment不会被复制到新的索引段中.

4.文档操作分析配置

4.1 数据更新

Lucene的处理办法: 新收到的数据写到新的索引文件里.

Lucene把每次生成的倒排索引,叫做一个段(segment).然后另外使用一个commit文件,记录索引内所有的segment.而生成segment的数据来源,则是内存中的buffer.

4.1.1 ES中数据更新

  1. 当前索引有3个segment可用 索引初始状态

  2. 新接收的数据进入内存buffer 数据进入内存

  3. Elasticsearch在把数据写入到内存buffer的同时,其实还另外记录了一个translog日志 写入数据到Translog

  4. 内存buffer生成一个新的segment,刷到文件系统缓存中.刷新发生的时候,translog日志文件依然保持原样.

    NOTE:如果在这期间发生异常,Elasticsearch会从commit位置开始,恢复整个translog文件中的记录,保证数据一致性.

刷新数据前保存Translog

  1. 真正把segment刷到磁盘,且commit文件进行更新的时候,translog文件才清空.这一步,叫做flush. 清空Translog

  2. 内存buffer刷到磁盘,生成一个新的segment,commit文件同步更新,Lucene 即可检索这个新segment. 文件同步更新

总结 当分片所在的节点接收到来自协调节点的请求后,会将该请求写入translog,并将文档加入内存缓存.如果请求在主分片上成功处理,该请求会并行发送到该分片的副本上.当translog被同步到全部的主分片及其副本上后,客户端才会收到确认通知.

4.1.2 refresh

内存缓冲刷到文件系统缓存的步骤,在Elasticsearch中,是默认设置为1秒间隔的.Elasticsearch也提供了单独的/_refresh`接口,用户如果对 1 秒间隔还不满意的,可以主动调用该接口来保证搜索可见.

注: 5.0 中还提供了一个新的请求参数:?refresh=wait_for,可以在写入数据后不强制刷新但一直等到刷新才返回

对于Elastic Stack的日志场景来说,恰恰相反,我们并不需要如此高的实时性,而是需要更快的写入性能.所以,一般来说,我们反而会通过/_settings接口或者定制template的方式,加大refresh_interval参数:

# curl -XPOST http://127.0.0.1:9200/logstash-2015.06.21/_settings -d'
{ "refresh_interval": "10s" }'

如果是导入历史数据的场合,那甚至可以先完全关闭掉:

# curl -XPUT http://127.0.0.1:9200/logstash-2015.05.01 -d'
{
  "settings" : {
    "refresh_interval": "-1"
  }
}'

在导入完成以后,修改回来或者手动调用一次即可:

# curl -XPOST http://127.0.0.1:9200/logstash-2015.05.01/_refresh

4.1.3 flush

对于flush操作,Elasticsearch默认设置为:每30分钟主动进行一次flush,或者当translog文件大小大于512MB(老版本是200MB)时,主动进行一次flush.这两个行为,可以分别通过index.translog.flush_threshold_periodindex.translog.flush_threshold_size参数修改.

如果对这两种控制方式都不满意,Elasticsearch还可以通过index.translog.flush_threshold_ops参数,控制每收到多少条数据后flush一次.

4.1.4 translog 的一致性

索引数据的一致性通过 translog 保证.

默认情况下,Elasticsearch每5秒,或每次请求操作结束前,会强制刷新translog日志到磁盘上.后者是Elasticsearch 2.0新加入的特性.为了保证不丢数据,每次index、bulk、delete、update完成的时候,一定触发刷新translog到磁盘上,才给请求返回200OK.这个改变在提高数据安全性的同时当然也降低了一点性能.

如果你不在意这点可能性,还是希望性能优先,可以在index template里设置如下参数:

{
    "index.translog.durability": "async"
}

4.1.5 Elasticsearch 分布式索引

前面一段内容,一直写的是"Lucene索引".这个区别在于,Elasticsearch为了完成分布式系统,对一些名词概念作了变动.索引成为了整个集群级别的命名,而在单个主机上的Lucene索引,则被命名为分片(shard).

4.2 segment merge对写入性能的影响

Lucene 的设计思路就是"开新文件".从另一个方面看,开新文件也会给服务器带来负载压力.因为默认每1秒,都会有一个新文件产生,每个文件都需要有文件句柄,内存,CPU使用等各种资源.一天有86400秒,设想一下,每次请求要扫描一遍86400个文件,这个响应性能绝对好不了!

为了解决这个问题,ES会不断在后台运行任务,主动将这些零散的segment做数据归并,尽量让索引内只保有少量的,每个都比较大的,segment文件.

4.2.1 merge过程

归并过程中,索引状态如下,尚未完成的较大的segment是被排除在检索可见范围之外的:

segment merging

当归并完成,较大的这个segment刷到磁盘后,commit文件做出相应变更,删除之前几个小segment,改成新的大segment.等检索请求都从小segment转到大segment上以后,删除没用的小segment.这时候,索引里segment数量就下降了.

segments merged

4.2.2 归并线程配置

segment归并的过程,需要先读取segment,归并计算,再写一遍segment,最后还要保证刷到磁盘.可以说,这是一个非常消耗磁盘IO和CPU的任务.所以,ES提供了对归并线程的限速机制,确保这个任务不会过分影响到其他任务.

在5.0之前,归并线程的限速配置indices.store.throttle.max_bytes_per_sec是20MB.对于写入量较大,磁盘转速较高,甚至使用SSD盘的服务器来说,这个限速是明显过低的.对于Elastic Stack应用,社区广泛的建议是可以适当调大到100MB或者更高.

# curl -XPUT http://127.0.0.1:9200/_cluster/settings -d'
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "100mb"
    }
}'

5.0开始,ES对此作了大幅度改进,使用了LuceneCMS(ConcurrentMergeScheduler)auto throttle机制,正常情况下已经不再需要手动配置indices.store.throttle.max_bytes_per_sec了.官方文档中都已经删除了相关介绍,不过从源码中还是可以看到,这个值目前的默认设置是10240MB.

归并线程的数目,ES也是有所控制的.默认数目的计算公式是:Math.min(3,Runtime.getRuntime().availableProcessors()/2).即服务器CPU核数的一半大于3时,启动3个归并线程;否则启动跟CPU核数的一半相等的线程数.相信一般做Elastic Stack的服务器CPU合数都会在6个以上.所以一般来说就是3个归并线程.如果你确定自己磁盘性能跟不上,可以降低index.merge.scheduler.max_thread_count配置,免得IO情况更加恶化.

4.2.3 归并策略

归并线程是按照一定的运行策略来挑选 segment 进行归并的.主要有以下几条:

  • index.merge.policy.floor_segment: 默认2MB,小于这个大小的segment,优先被归并.
  • index.merge.policy.max_merge_at_once: 默认一次最多归并10个segment
  • index.merge.policy.max_merge_at_once_explicit: 默认forcemerge时一次最多归并30个segment.
  • index.merge.policy.max_merged_segment: 默认5GB,大于这个大小的segment,不用参与归并.forcemerge除外.

根据这段策略,其实我们也可以从另一个角度考虑如何减少segment归并的消耗以及提高响应的办法:加大flush间隔,尽量让每次新生成的segment本身大小就比较大.

4.2.4 forcemerge接口

既然默认的最大segment大小是5GB.那么一个比较庞大的数据索引,就必然会有为数不少的segment永远存在,这对文件句柄,内存等资源都是极大的浪费.但是由于归并任务太消耗资源,所以一般不太选择加大index.merge.policy.max_merged_segment配置,而是在负载较低的时间段,通过forcemerge接口,强制归并segment.

#curl -XPOST http://127.0.0.1:9200/logstash-2015-06.10/_forcemerge?max_num_segments=1

由于forcemerge线程对资源的消耗比普通的归并线程大得多,所以,绝对不建议对还在写入数据的热索引执行这个操作.这个问题对于Elastic Stack来说非常好办,一般索引都是按天分割的.

4.3 routing和replica的读写过程

当一个 ES 节点收到一条数据的写入请求时,它是如何确认这个数据应该存储在哪个节点的哪个分片上的?

4.3.1 路由计算

作为一个没有额外依赖的简单的分布式方案,ES 在这个问题上同样选择了一个非常简洁的处理方式,对任一条数据计算其对应分片的方式如下:

shard = hash(routing) % number_of_primary_shards

每个数据都有一个 routing 参数,默认情况下,就使用其 _id 值.将其 _id 值计算哈希后,对索引的主分片数取余,就是数据实际应该存储到的分片 ID.

由于取余这个计算,完全依赖于分母,所以导致 ES 索引有一个限制,索引的主分片数,不可以随意修改.因为一旦主分片数不一样,所以数据的存储位置计算结果都会发生改变,索引数据就完全不可读了.

4.3.2 副本一致性

作为分布式系统,数据副本可算是一个标配.ES 数据写入流程,自然也涉及到副本.在有副本配置的情况下,数据从发向 ES 节点,到接到ES节点响应返回.

  1. 正常有三个节点的集群状态 初始状态

  2. 创建,索引,删除文档

    • 1.客户端请求发送给Node1节点,注意图中Node1是Master节点,实际完全可以不是.
    • 2.Node1用数据的_id取余计算得到应该讲数据存储到shard0上.通过clusterstate信息发现shard0的主分片已经分配到了Node3上.Node1转发请求数据给Node3.
    • 3.Node3完成请求数据的索引过程,存入主分片0.然后并行转发数据给分配有shard0的副本分片的Node1Node2.当收到任一节点汇报副本分片数据写入成功,Node3即返回给初始的接收节点Node1,宣布数据写入成功.Node1返回成功响应给客户端.

索引

这个过程中,有几个参数可以用来控制或变更其行为:

  • wait_for_active_shards
    • 上面示例中,2 个副本分片只要有 1 个成功,就可以返回给客户端了.这点也是有配置项的.其默认值的计算来源如下:int( (primary + number_of_replicas) / 2 ) + 1.
    • 根据需要,也可以将参数设置为one,表示仅写完主分片就返回,等同于 async
    • 还可以设置为 all,表示等所有副本分片都写完才能返回.
  • timeout
    • 如果集群出现异常,有些分片当前不可用,ES 默认会等待 1分钟看分片能否恢复.可以使用 ?timeout=30s 参数来缩短这个等待时间.

副本配置和分片配置不一样,是可以随时调整的.有些较大的索引,甚至可以在做forcemerge前,先把副本全部取消掉,等optimize完后,再重新开启副本,节约单个segment的重复归并消耗.

# curl -XPUT http://127.0.0.1:9200/logstash-mweibo-2015.05.02/_settings -d '{
    "index": { "number_of_replicas" : 0 }
}'
  1. 取回单个文档 以下是从主分片或者副本分片检索文档的步骤顺序:

    • 1.客户端向Node1发送获取请求.
    • 2.节点使用文档的_id来确定文档属于分片0.分片0的副本分片存在于所有的三个节点上.在这种情况下,它将请求转发到Node2.
    • 3.Node2将文档返回给Node1,然后将文档返回给客户端 取回单个文档
  2. 局部更新文档

数据更新

以下是部分更新一个文档的步骤:

  • 1.客户端向Node1发送更新请求.
  • 2.它将请求转发到主分片所在的Node3.
  • 3.Node3从主分片检索文档,修改_source字段中的JSON,并且尝试重新索引主分片的文档.如果文档已经被另一个进程修改,它会重试步骤3,超过retry_on_conflict次后放弃.
  • 4.如果Node3成功地更新文档,它将新版本的文档并行转发到Node1Node2上的副本分片,重新建立索引.一旦所有副本分片都返回成功,Node3向协调节点也返回成功,协调节点向客户端返回成功.

4.3.2 多文档模式

使用mget取回多个文档 取回多个文档

以下是使用单个mget请求取回多个文档所需的步骤顺序:

  • 1.客户端向Node1发送mget请求
  • 2.Node1为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上.一旦收到所有答复,Node1构建响应并将其返回给客户端

使用bulk修改多个文档 修改多个文档

bulk API按如下步骤顺序执行:

  • 1.客户端向Node1发送bulk请求
  • 2.Node1为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机.
  • 3.主分片一个接一个按顺序执行每个操作.当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作.一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端

4.4 shard的allocate控制

4.4.1 shard分配策略

某个shard分配在哪个节点上,一般来说,是由ES自动决定的.以下几种情况会触发分配动作:

  • 新索引生成
  • 索引的删除
  • 新增副本分片
  • 节点增减引发的数据均衡

ES提供了一系列参数详细控制这部分逻辑:

  • cluster.routing.allocation.enable
    • 该参数用来控制允许分配哪种分片.默认是 all.可选项还包括 primariesnew_primaries.none 则彻底拒绝分片.该参数的作用,本书稍后集群升级章节会有说明.
  • cluster.routing.allocation.allow_rebalance
    • 该参数用来控制什么时候允许数据均衡.默认是 indices_all_active,即要求所有分片都正常启动成功以后,才可以进行数据均衡操作,否则的话,在集群重启阶段,会浪费太多流量了.
  • cluster.routing.allocation.cluster_concurrent_rebalance
    • 该参数用来控制集群内同时运行的数据均衡任务个数.默认是 2 个.如果有节点增减,且集群负载压力不高的时候,可以适当加大.
  • cluster.routing.allocation.node_initial_primaries_recoveries
    • 该参数用来控制节点重启时,允许同时恢复几个主分片.默认是 4 个.如果节点是多磁盘,且 IO 压力不大,可以适当加大.
  • cluster.routing.allocation.node_concurrent_recoveries
    • 该参数用来控制节点除了主分片重启恢复以外其他情况下,允许同时运行的数据恢复任务.默认是 2 个.所以,节点重启时,可以看到主分片迅速恢复完成,副本分片的恢复却很慢.除了副本分片本身数据要通过网络复制以外,并发线程本身也减少了一半.当然,这种设置也是有道理的——主分片一定是本地恢复,副本分片却需要走网络,带宽是有限的.从 ES 1.6 开始,冷索引的副本分片可以本地恢复,这个参数也就是可以适当加大了.
  • indices.recovery.concurrent_streams
    • 该参数用来控制节点从网络复制恢复副本分片时的数据流个数.默认是3个.可以配合上一条配置一起加大.
  • indices.recovery.max_bytes_per_sec
    • 该参数用来控制节点恢复时的速率.默认是 40MB.显然是比较小的,建议加大.

4.4.2 其他分片分配控制策略

ES 还有一些其他的分片分配控制策略.比如以 tagrack_id 作为区分等.一般来说,Elastic Stack 场景中使用不多.运维人员可能比较常见的策略有两种:

磁盘限额 为了保护节点数据安全,ES 会定时(cluster.info.update.interval,默认30秒)检查一下各节点的数据目录磁盘使用情况.在达到 cluster.routing.allocation.disk.watermark.low (默认 85%)的时候,新索引分片就不会再分配到这个节点上了.在达到 cluster.routing.allocation.disk.watermark.high (默认 90%)的时候,就会触发该节点现存分片的数据均衡,把数据挪到其他节点上去.这两个值不但可以写百分比,还可以写具体的字节数.有些公司可能出于成本考虑,对磁盘使用率有一定的要求,需要适当抬高这个配置:

# curl -XPUT localhost:9200/_cluster/settings -d '{
    "transient" : {
        "cluster.routing.allocation.disk.watermark.low" : "85%",
        "cluster.routing.allocation.disk.watermark.high" : "10gb",
        "cluster.info.update.interval" : "1m"
    }
}'

热索引分片不均 默认情况下,ES 集群的数据均衡策略是以各节点的分片总数(indices_all_active)作为基准的.

这对于搜索服务来说无疑是均衡搜索压力提高性能的好办法.但是对于Elastic Stack场景,一般压力集中在新索引的数据写入方面.正常运行的时候,也没有问题.但是当集群扩容时,新加入集群的节点,分片总数远远低于其他节点.这时候如果有新索引创建,ES 的默认策略会导致新索引的所有主分片几乎全分配在这台新节点上.整个集群的写入压力,压在一个节点上,结果很可能是这个节点直接被压死,集群出现异常.

所以,对于Elastic Stack场景,强烈建议大家预先计算好索引的分片数后,配置好单节点分片的限额.比如,一个 5 节点的集群,索引主分片 10 个,副本 1 份.则平均下来每个节点应该有 4 个分片,那么就配置:

# curl -s -XPUT http://127.0.0.1:9200/logstash-2015.05.08/_settings -d '{
    "index": { "routing.allocation.total_shards_per_node" : "5" }
}'

注意,这里配置的是 5 而不是 4.因为我们需要预防有机器故障,分片发生迁移的情况.如果写的是 4,那么分片迁移会失败.

4.4.3 其它影响分片分配的参数

Elasticsearch 中有一系列参数,相互影响,最终联合决定分片分配:

  • cluster.routing.allocation.balance.shard
    • 节点上分配分片的权重,默认为 0.45.数值越大越倾向于在节点层面均衡分片
  • cluster.routing.allocation.balance.index
    • 每个索引往单个节点上分配分片的权重,默认为 0.55.数值越大越倾向于在索引层面均衡分片
  • cluster.routing.allocation.balance.threshold
    • 大于阈值则触发均衡操作.默认为1

Elasticsearch 中的计算方法是:

(indexBalance * (node.numShards(index) – avgShardsPerNode(index)) + shardBalance * (node.numShards() – avgShardsPerNode)) <=> weightthreshold

所以,也可以采取加大 cluster.routing.allocation.balance.index,甚至设置 cluster.routing.allocation.balance.shard 为 0 来尽量采用索引内的节点均衡.

4.5 reroute接口

在必要的时候,还可以通过 ES 的reroute接口,手动完成对分片的分配选择的控制.

reroute 接口支持五种指令:

  • allocate_replica
  • allocate_stale_primary
  • allocate_empty_primary
  • move
  • cancel

常用的一般是allocatemove

4.5.1 allocate_* 指令

因为负载过高等原因,有时候个别分片可能长期处于 UNASSIGNED 状态,我们就可以手动分配分片到指定节点上.默认情况下只允许手动分配副本分片(即使用 allocate_replica),所以如果要分配主分片,需要单独加一个 accept_data_loss 选项:

curl -XPOST 127.0.0.1:9200/_cluster/reroute -d '{
  "commands" : [ {
        "allocate_stale_primary" :
            {
              "index" : "logstash-2015.05.27", "shard" : 61, "node" : "10.19.0.77", "accept_data_loss" : true
            }
        }
  ]
}'

注意,allocate_stale_primary 表示准备分配到的节点上可能有老版本的历史数据,运行时请提前确认一下是哪个节点上保留有这个分片的实际目录,且目录大小最大.然后手动分配到这个节点上.以此减少数据丢失.

4.5.2 move 指令

因为负载过高,磁盘利用率过高,服务器下线,更换磁盘等原因,可以会需要从节点上移走部分分片:

curl -XPOST 127.0.0.1:9200/_cluster/reroute -d '{
  "commands" : [ {
        "move" :
            {
              "index" : "logstash-2015.05.22", "shard" : 0, "from_node" : "10.19.0.81", "to_node" : "10.19.0.104"
            }
        }
  ]
}'

4.5.3 分配失败原因

如果是自己手工reroute失败,Elasticsearch返回的响应中会带上失败的原因.不过格式非常难看,一堆 YES,NO. 从 5.0 版本开始,Elasticsearch新增了一个allocation explain接口,专门用来解释指定分片的具体失败理由:

curl -XGET 'http://localhost:9200/_cluster/allocation/explain' -d'{
      "index": "logstash-2016.10.31",
      "shard": 0,
      "primary": false

}'

得到的响应如下:

{
    "shard" : {
        "index" : "myindex",
        "index_uuid" : "KnW0-zELRs6PK84l0r38ZA",
        "id" : 0,
        "primary" : false
    },
    "assigned" : false,
    "shard_state_fetch_pending": false,
    "unassigned_info" : {
        "reason" : "INDEX_CREATED",
        "at" : "2016-03-22T20:04:23.620Z"
    },
    "allocation_delay_ms" : 0,
    "remaining_delay_ms" : 0,
    "nodes" : {
        "V-Spi0AyRZ6ZvKbaI3691w" : {
            "node_name" : "H5dfFeA",
            "node_attributes" : {
                "bar" : "baz"
            },
            "store" : {
                "shard_copy" : "NONE"
            },
            "final_decision" : "NO",
            "final_explanation" : "the shard cannot be assigned because one or more allocation decider returns a 'NO' decision",
            "weight" : 0.06666675,
            "decisions" : [ {
                "decider" : "filter",
                "decision" : "NO",
                "explanation" : "node does not match index include filters [foo:\"bar\"]"
            }  ]
        },
        "Qc6VL8c5RWaw1qXZ0Rg57g" : {
            ...

这会是很长一串 JSON,把集群里所有的节点都列上来,挨个解释为什么不能分配到这个节点.

5.集群操作

5.1 集群自动发现

ES 是一个P2P类型(使用gossip协议)的分布式系统,除了集群状态管理以外,其他所有的请求都可以发送到集群内任意一台节点上,这个节点可以自己找到需要转发给哪些节点,并且直接跟这些节点通信.

所以,从网络架构及服务配置上来说,构建集群所需要的配置极其简单.在Elasticsearch 2.0之前,无阻碍的网络下,所有配置了相同 cluster.name的节点都自动归属到一个集群中.

2.0版本之后,基于安全的考虑,Elasticsearch稍作了调整,避免开发环境过于随便造成的麻烦.

unicast方式 ES 从2.0版本开始,默认的自动发现方式改为了单播(unicast)方式.配置里提供几台节点的地址,ES将其视作gossip router角色,借以完成集群的发现.由于这只是ES内一个很小的功能,所以gossip router角色并不需要单独配置,每个ES节点都可以担任.所以,采用单播方式的集群,各节点都配置相同的几个节点列表作为router即可.

此外,考虑到节点有时候因为高负载,慢GC等原因可能会有偶尔没及时响应ping包的可能,一般建议稍微加大Fault Detection的超时时间. 同样基于安全考虑做的变更还有监听的主机名.现在默认只监听本地io网卡上.所以正式环境上需要修改配置为监听具体的网卡.

network.host: "192.168.0.2"
discovery.zen.minimum_master_nodes: 3
discovery.zen.ping_timeout: 100s
discovery.zen.fd.ping_timeout: 100s
discovery.zen.ping.unicast.hosts: ["10.19.0.97","10.19.0.98","10.19.0.99","10.19.0.100"]

上面的配置中,两个timeout可能会让人有所迷惑.这里的fdfault detection的缩写.也就是说:

  • discovery.zen.ping_timeout 参数仅在加入或者选举master主节点的时候才起作用;
  • discovery.zen.fd.ping_timeout 参数则在稳定运行的集群中,master检测所有节点,以及节点检测master是否畅通时长期有用.

既然是长期有用,自然还有运行间隔和重试的配置,也可以根据实际情况调整:

discovery.zen.fd.ping_interval: 10s
discovery.zen.fd.ping_retries: 10

results matching ""

    No results matching ""