ElasticSearch技术应用及性能优化

 

 

 

前言:全文搜索引擎ElasticSearch

 

1.什么是全文搜索引擎?

什么是全文搜索引擎?

百度百科中的定义:

全文搜索引擎是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

从定义中可以大致了解全文检索的思路,为了更详细的说明,先从生活中的数据说起。

生活中的数据总体分为两种:结构化数据非结构化数据

  • 结构化数据: 指具有固定格式或有限长度的数据,如数据库,元数据等。

  • 非结构化数据: 非结构化数据又可称为全文数据,指不定长或无固定格式的数据,如邮件,word文档等。

当然有的地方还会有第三种:半结构化数据,如XML,HTML等,当根据需要可按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。

根据两种数据分类,搜索也相应的分为两种:结构化数据搜索非结构化数据搜索

对于结构化数据,我们一般都是可以通过关系型数据库(mysql,oracle等)的 table 的方式存储和搜索,也可以建立索引。

对于非结构化数据,也即对全文数据的搜索主要有两种方法:顺序扫描法全文检索

 

顺序扫描:即按照顺序扫描的方式查询特定的关键字。例如给你一张报纸,让你找到该报纸中“RNG”的文字在哪些地方出现过。你肯定需要从头到尾把报纸阅读扫描一遍然后标记出关键字在哪些版块出现过以及它的出现位置。这种方式无疑是最耗时的最低效的

全文搜索:将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这种方式就构成了全文检索的基本思路。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引

 

2.为什么要用全文内搜索引擎?

我们的所有数据在数据库里面都有,而且 MySQL、Oracle等数据库里也能提供查询检索或者聚类分析功能,直接通过数据库查询不就可以了?那为什么要用搜索引擎呢?确实,我们大部分的查询功能都可以通过数据库查询获得,如果查询效率低下,还可以通过建数据库索引,优化SQL等方式进行提升效率,甚至通过引入缓存来加快数据的返回速度。如果数据量更大,就可以分库分表来分担查询压力。

那为什么还要全文搜索引擎呢?我们主要从以下几个原因分析:

  • 数据类型

全文索引搜索支持非结构化数据的搜索,可以更好地快速搜索大量存在的任何单词或单词组的非结构化文本。

例如 Google,百度类的网站搜索,它们都是根据网页中的关键字生成索引,我们在搜索的时候输入关键字,它们会将该关键字即索引匹配到的所有网页返回;还有常见的项目中应用日志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。

  • 索引的维护

一般传统数据库,全文检索都实现的很鸡肋,因为一般也没人用数据库存文本字段。进行全文检索需要扫描整个表,如果数据量大的话即使对SQL的语法优化,也收效甚微。建立了索引,但是维护起来也很麻烦,对于 insert 和 update 操作都会重新构建索引。

 

什么时候使用全文搜索引擎:

  1. 搜索的数据对象是大量的非结构化的文本数据。

  2. 文件记录量达到数十万或数百万个甚至更多。

  3. 支持大量基于交互式文本的查询。

  4. 需求非常灵活的全文搜索查询。

  5. 对高度相关的搜索结果的有特殊需求,但是没有可用的关系数据库可以满足。

  6. 对不同记录类型、非文本数据操作或安全事务处理的需求相对较少的情况。

 

3.Lucene,Solr,ElasticSearch?

现在主流的搜索引擎有三种:Lucene,Solr,ElasticSearch。

它们的索引建立都是根据倒排索引的方式生成索引,那么什么是倒排索引?

百度百科中的定义:

倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。

 

a.Lucene

Lucene是一个Java全文搜索引擎,完全用Java编写。Lucene不是一个完整的应用程序,而是一个代码库和API,可以很容易地用于向应用程序添加搜索功能。

但是Lucene只是一个框架,要充分利用它的功能,需要使用JAVA,并且在程序中集成Lucene。需要很多的学习了解,才能明白它是如何运行的,熟练运用Lucene确实非常复杂。

Lucene通过简单的API提供强大的功能:

  • 可扩展的高性能索引

    • 在现代硬件上超过150GB /小时

    • 小RAM要求 - 只有1MB堆

    • 增量索引与批量索引一样快

    • 索引大小约为索引文本大小的20-30%

  • 强大,准确,高效的搜索算法

    • 排名搜索 - 首先返回最佳结果

    • 许多强大的查询类型:短语查询,通配符查询,邻近查询,范围查询等

    • 现场搜索(例如标题,作者,内容)

    • 按任何字段排序

    • 使用合并结果进行多索引搜索

    • 允许同时更新和搜索

    • 灵活的分面,突出显示,连接和结果分组

    • 快速,内存效率和错误容忍的建议

    • 可插拔排名模型,包括矢量空间模型和Okapi BM25

    • 可配置存储引擎(编解码器)

  • 跨平台解决方案

    • 作为Apache许可下的开源软件提供 ,允许您在商业和开源程序中使用Lucene

    • 100%-pure Java

    • 可用的其他编程语言中的实现是索引兼容的

 

b.Solr

Apache Solr是一个基于名为Lucene的Java库构建的开源搜索平台。它以用户友好的方式提供Apache Lucene的搜索功能。作为一个行业参与者近十年,它是一个成熟的产品,拥有强大而广泛的用户社区。它提供分布式索引,复制,负载平衡查询以及自动故障转移和恢复。如果它被正确部署然后管理得好,它就能够成为一个高度可靠,可扩展且容错的搜索引擎。很多互联网巨头,如Netflix,eBay,Instagram和亚马逊(CloudSearch)都使用Solr,因为它能够索引和搜索多个站点。

主要功能列表包括:

  • 全文搜索

  • 突出

  • 分面搜索

  • 实时索引

  • 动态集群

  • 数据库集成

  • NoSQL功能和丰富的文档处理(例如Word和PDF文件)

 

c.ElasticSearch

Elasticsearch是一个开源、基于Apache Lucene库构建的RESTful搜索引擎。

Elasticsearch是在Solr之后几年推出的。它提供了一个分布式,多租户能力的全文搜索引擎,具有HTTP Web界面(REST)和无架构JSON文档。Elasticsearch的官方客户端库提供Java,Groovy,PHP,Ruby,Perl,Python,.NET和Javascript。

分布式搜索引擎包括可以划分为分片的索引,并且每个分片可以具有多个副本。每个Elasticsearch节点都可以有一个或多个分片,其引擎也可以充当协调器,将操作委派给正确的分片。

Elasticsearch可通过近实时搜索进行扩展。其主要功能之一是多租户。

主要功能列表包括:

  • 分布式搜索

  • 多租户

  • 分析搜索

  • 分组和聚合

一、ElasticSearch简介

 

Elasticsearch是一个高度可扩展的、开源的、基于 Lucene 的全文搜索和分析引擎。它允许您快速,近实时地存储,搜索和分析大量数据,并支持多租户。

Elasticsearch也使用Java开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

不过,Elasticsearch 不仅仅是 Lucene 和全文搜索,我们还能这样去描述它:

  • 分布式的实时文件存储,每个字段都被索引并可被搜索

  • 分布式的实时分析搜索引擎

  • 可以扩展到上百台服务器,处理PB级结构化或非结构化数据

而且,所有的这些功能被集成到一个服务里面,你的应用可以通过简单的RESTful API、各种语言的客户端甚至命令行与之交互。

1.ElasticSearch基本概念

Elasticsearch是一个近乎实时(NRT)的搜索平台。这意味着从索引文档到可搜索文档的时间有一点延迟(通常是一秒)。通常有集群,节点,分片,副本等概念。

  • 集群(Cluster)

集群(cluster)是一组具有相同cluster.name的节点集合,他们协同工作,共享数据并提供故障转移和扩展功能,当然一个节点也可以组成一个集群。

集群由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为如果节点设置为按名称加入集群的话,则该节点只能是集群的一部分。

确保不同的环境中使用不同的集群名称,否则最终会导致节点加入错误的集群。

集群健康状态:

集群状态通过绿,黄,红来标识

  • 绿色 - 一切都很好(集群功能齐全)

  • 黄色 - 所有数据均可用,但尚未分配一些副本(集群功能齐全)

  • 红色 - 某些数据由于某种原因不可用(集群部分功能)

注意:当群集为红色时,它将继续提供来自可用分片的搜索请求,但您可能需要尽快修复它,因为存在未分配的分片。

 

如果要检查集群健康状况,我们可以在kibana控制台中运行以下命令:

GET /_cluster/health

得到如下信息:

{
  "cluster_name" : "elasticsearch",
  "status" : "green",
  "timed_out" : false,
  "number_of_nodes" : 3,
  "number_of_data_nodes" : 3,
  "active_primary_shards" : 80,
  "active_shards" : 160,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 0,
  "delayed_unassigned_shards" : 0,
  "number_of_pending_tasks" : 0,
  "number_of_in_flight_fetch" : 0,
  "task_max_waiting_in_queue_millis" : 0,
  "active_shards_percent_as_number" : 100.0
}
  • 节点(Node)

节点,一个运行的 ES 实例就是一个节点,节点存储数据并参与集群的索引和搜索功能。

就像集群一样,节点由名称标识,默认情况下,该名称是在启动时分配给节点的随机通用唯一标识符(UUID)。如果不需要默认值,可以定义所需的任何节点名称。此名称对于管理目的非常重要,您可以在其中识别网络中哪些服务器与 Elasticsearch 集群中的哪些节点相对应。

可以将节点配置为按集群名称加入特定集群。默认情况下,每个节点都设置为加入一个名为 cluster 的 elasticsearch 集群,这意味着如果您在网络上启动了许多节点并且假设它们可以相互发现 - 它们将自动形成并加入一个名为 elasticsearch 的集群。

  • 索引(Index)

索引是具有某些类似特征的文档集合。例如,您可以拥有店铺数据的索引,商品的一个索引以及订单数据的一个索引。索引由名称标识(必须全部小写),此名称用于在对其中的文档执行索引,搜索,更新和删除操作时引用索引。

Elasticsearch与关系型数据库的对应关系:

Elasticsearch关系型数据库
索引(index)数据库(Database)
文档类型(type)表(Table)
文档(document)一行数据(Row)
字段(field)一列数据(Column)
映射(mapping)数据库的组织和结构(Schema)
  • 类型(Type)

类型,曾经是索引的逻辑类别/分区,允许您在同一索引中存储不同类型的文档,例如,一种类型用于用户,另一种类型用于博客帖子。

在6.0.0中弃用,以后将不再可能在索引中创建多个类型,并且将在更高版本中删除类型的整个概念。

  • 文档(Document)

文档是可以建立索引的基本信息单元。例如,您可以为单个客户提供文档,为单个产品提供一个文档,为单个订单提供一个文档。该文档以JSON(JavaScript Object Notation)表示,JSON是一种普遍存在的互联网数据交换格式。

在索引/类型中,您可以根据需要存储任意数量的文档。请注意,尽管文档实际上驻留在索引中,但实际上必须将文档编入索引/分配给索引中的类型。

  • 分片(Shards)

索引可能存储大量可能超过单个节点的硬件限制的数据。例如,占用1TB磁盘空间的十亿个文档的单个索引可能不适合单个节点的磁盘,或者可能太慢而无法单独从单个节点提供搜索请求。

为了解决这个问题,Elasticsearch 提供了将索引细分为多个称为分片的功能。创建索引时,只需定义所需的分片数即可。每个分片本身都是一个功能齐全且独立的“索引”,可以托管在集群中的任何节点上。

设置分片的目的及原因主要是:

(1)允许水平拆分/缩放内容量

(2)允许跨分片(可能在多个节点上)分布和并行化操作,从而提高性能/吞吐量

  • 副本(Replicasedit)

副本,是对分片的复制。目的是为了当分片/节点发生故障时提供高可用性,它允许您扩展搜索量/吞吐量,因为可以在所有副本上并行执行搜索。

总而言之,每个索引可以拆分为多个分片。索引也可以复制为零次(表示没有副本)或更多次。复制之后,每个索引将具有主分片(从原始分片复制而来的)和复制分片(主分片的副本)。

可以在创建索引时为每个索引定义分片和副本的数量。创建索引后,可以随时动态更改副本数。可以使用_shrink_splitAPI 更改现有索引的分片数,但这不是一项轻松的任务,所以预先计划正确数量的分片是最佳方法。

默认情况下,Elasticsearch 中的每个索引都分配了5个主分片和1个副本,这意味着如果集群中至少有两个节点,则索引将包含5个主分片和另外5个副本分片(1个完整副本),总计为每个索引10个分片。

 

副本是乘法,越多越浪费,但也越保险。分片是除法,分片越多,单分片数据就越少也越分散。

「索引」含义的区分

  • 索引(名词):

    一个索引(index)就像是传统关系数据库中的数据库,它是相关文档存储的地方,index的复数是 indices 或 indexes。

  • 索引(动词):

    「索引一个文档」表示把一个文档存储到索引(名词)里,以便它可以被检索或者查询。这很像SQL中的INSERT关键字,差别是,如果文档已经存在,新的文档将覆盖旧的文档。

  • 倒排索引:

    传统数据库为特定列增加一个索引,例如B-Tree索引来加速检索。Elasticsearch和Lucene使用一种叫做倒排索引(inverted index)的数据结构来达到相同目的。

 

2.与ElasticSearch交互

目前与 elasticsearch 交互主要有两种方式:Client API 和 RESTful API。

a.Client API方式

Elasticsearch 为以下语言提供了官方客户端 --Groovy、JavaScript、.NET、 PHP、 Perl、 Python 和 Ruby--还有很多社区提供的客户端和插件,所有这些都可以在 Elasticsearch Clients 中找到

b.RESTful API with JSON over HTTP

所有其他语言可以使用 RESTful API 通过端口 9200 和 Elasticsearch 进行通信,你可以用你最喜爱的 web 客户端访问 Elasticsearch 。事实上,正如你所看到的,你甚至可以使用 curl 命令来和 Elasticsearch 交互。

一个 Elasticsearch 请求和任何 HTTP 请求一样由若干相同的部件组成:

curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'

 

 

3.数据格式

在应用程序中对象很少只是一个简单的键和值的列表。通常它们拥有更复杂的数据结构,可能包括日期、地理信息、其他对象或者数组等。

也许有一天你想把这些对象存储在数据库中。使用关系型数据库的行和列存储,这相当于是把一个表现力丰富的对象挤压到一个非常大的电子表格中:你必须将这个对象扁平化来适应表结构,通常一个字段对应一列,而且又不得不在每次查询时重新构造对象。

Elasticsearch 是面向文档的,意味着它存储整个对象或文档。Elasticsearch 不仅存储文档,而且每个文档的内容可以被检索。在 Elasticsearch 中,你对文档进行索引、检索、排序和过滤而不是对行列数据。这是一种完全不同的思考数据的方式,也是 Elasticsearch 能支持复杂全文检索的原因。

Elasticsearch 使用 JavaScript Object Notation 或者 JSON 作为文档的序列化格式。JSON 序列化被大多数编程语言所支持,并且已经成为 NoSQL 领域的标准格式。 它简单、简洁、易于阅读。几乎所有的语言都有相应的模块可以将任意的数据结构或对象 转化成 JSON 格式,只是细节各不相同。

4.索引的应用

a.创建索引

我们创建一个 NBA 球队的索引,索引名称需是小写。

PUT nba
{
  "settings":{
    "number_of_shards": 3,   
    "number_of_replicas": 1 
 },
  "mappings":{
    "nba":{
      "properties":{
        "name_cn":{ 
          "type":"text"
       },
        "name_en":{
          "type":"text"
       },
        "gymnasium":{
          "type":"text"
       },
        "topStar":{
          "type":"text"
       },
        "championship":{
          "type":"integer"
       },
        "date":{
          "type":"date",
          "format":"yyyy-MM-dd HH:mm:ss|| yyy-MM-dd||epoch_millis"
       }
     }
   }
 }
}

字段说明:

字段名称字段说明
nba索引
number_of_shards分片数
number_of_replicas副本数
name_cn球队中文名
name_en球队英文名
gymnasium球馆名称
championship总冠军次数
topStar当家球星
date加入NBA年份

如果格式书写正确,我们会得到如下返回信息,表示创建成功

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "nba"
}

 

b.新增索引数据

索引创建完成之后,我们往索引中加入球队数据,1,2,3 是我们指定的 ID,如果不写 ES 会默认ID。

其实我们可以不创建上面的索引 mapping 直接推送数据,但是这样 ES 会根据数据信息自动为我们设定字段类型,这会造成索引信息不准确的风险。

PUT /nba/nba/1
{
  "name_en":"San Antonio Spurs SAS",
  "name_cn":"圣安东尼安马刺",
  "gymnasium":"AT&T中心球馆",
  "championship": 5,
  "topStar":"蒂姆·邓肯",
  "date":"1995-04-12"
}

PUT /nba/nba/2
{
  "name_en":"Los Angeles Lakers",
  "name_cn":"洛杉矶湖人",
  "gymnasium":"斯台普斯中心球馆",
  "championship": 16,
  "topStar":"科比·布莱恩特",
  "date":"1947-05-12"
}

PUT /nba/nba/3
{
  "name_en":"Golden State Warriors",
  "name_cn":"金州勇士队",
  "gymnasium":"甲骨文球馆",
  "championship": 6,
  "topStar":"斯蒂芬·库里",
  "date":"1949-06-13"
}

索引数据 PUT 成功,会返回如下信息:

{
  "_index": "nba",
  "_type": "nba",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
 },
  "created": true
}

 

c.查询索引数据 :

Elasticsearch 提供丰富且灵活的查询语言叫做 DSL 查询 (Query DSL) ,它允许你构建更加复杂、强大的搜索。

1.匹配查询 match,match_all

# 查询全部球队的信息
GET /nba/nba/_search
{
    "query": {
        "match_all": {}
   }
}

# 得到查询结果如下:
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 3,
    "successful": 3,
    "skipped": 0,
    "failed": 0
 },
  "hits": {
    "total": 3,
    "max_score": 1,
    "hits": [
     {
        "_index": "nba",
        "_type": "nba",
        "_id": "2",
        "_score": 1,
        "_source": {
          "name_en": "Los Angeles Lakers",
          "name_cn": "洛杉矶湖人",
          "gymnasium": "斯台普斯中心球馆",
          "championship": 16,
          "topStar": "科比·布莱恩特",
          "date": "1947-05-12"
       }
     },
     {
        "_index": "nba",
        "_type": "nba",
        "_id": "1",
        "_score": 1,
        "_source": {
          "name_en": "San Antonio Spurs SAS",
          "name_cn": "圣安东尼安马刺",
          "gymnasium": "AT&T中心球馆",
          "championship": 5,
          "topStar": "蒂姆·邓肯",
          "date": "1995-04-12"
       }
     },
     {
        "_index": "nba",
        "_type": "nba",
        "_id": "3",
        "_score": 1,
        "_source": {
          "name_en": "Golden State Warriors",
          "name_cn": "金州勇士队",
          "gymnasium": "甲骨文球馆",
          "championship": 6,
          "topStar": "斯蒂芬·库里",
          "date": "1949-06-13"
       }
        ···
     }
   ]
 }
}

# 响应的数据结果分为两部分:
{
----------------first part  分片副本信息--------------------
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 3,
    "successful": 3,
    "skipped": 0,
    "failed": 0
 },
---------------second part  查询的数据集---------------------
  "hits": {
    "total": 0,
    "max_score": null,
    "hits": []
 }
}
# 查询英文名称为:"Golden State Warriors" 的球队信息
GET /nba/nba/_search
{
   "query": {
        "match": {
            "name_en": "Golden State Warriors"
       }
   }
}

# 可得到的查询结果为:
{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 3,
    "successful": 3,
    "skipped": 0,
    "failed": 0
 },
  "hits": {
    "total": 1,
    "max_score": 1.9646256,
    "hits": [
     {
        "_index": "nba",
        "_type": "nba",
        "_id": "3",
        "_score": 1.9646256,
        "_source": {
          "name_en": "Golden State Warriors",
          "name_cn": "金州勇士队",
          "gymnasium": "甲骨文球馆",
          "championship": 6,
          "topStar": "斯蒂芬·库里",
          "date": "1949-06-13"
       }
     }
   ]
 }
}

2.过滤查询Filter

我们让搜索变的复杂一些。我们想要找到当家球星是勒布朗·詹姆斯,但是我们只想得到总冠军多余1次的球队。我们的语句将做一些改变用来添加过滤器(filter),它允许我们有效的执行一个结构化搜索:

GET /nba/nba/_search
{
  "query": {
    "bool": {
      "filter": {
        "range": {
          "championship": {
            "gt": 1
         }
       }
     },
      "must": {
        "match": {
          "topStar": "勒布朗·詹姆斯"
       }
     }
   }
 }
}

每次查询,查询结果里面都有一个 _score字段,一般Elasticsearch根据相关评分排序,相关评分是根据文档与语句的匹配度来得出, _score值越高说明匹配度越高。

Elasticsearch如何进行全文字段搜索且首先返回相关性性最大的结果。相关性(relevance)概念在Elasticsearch中非常重要,而这也是它与传统关系型数据库中记录只有匹配和不匹配概念最大的不同。

 

二、索引映射Mapping

 

在 Elasticsearch中,创建索引的时候一般也需要指定索引的字段类型,这种方式成为映射(Mapping)。

1.字段类型

Elasticsearch支持文档字段的多种不同数据类型,根据官方文档的分类,可以划分为以下几个类别:

核心数据类型

  • 字符串类型:text 和 keyword

text 和 keyword的区别?

text 用于索引全文值的字段,例如电子邮件正文或产品说明。这些字段是analyzed,它们通过分词器传递 ,以在被索引之前将字符串转换为单个术语的列表。分析过程允许Elasticsearch搜索单个单词中 每个完整的文本字段。文本字段不用于排序,很少用于聚合(尽管 重要的文本聚合 是一个值得注意的例外)。

keyword 用于索引结构化内容的字段,例如电子邮件地址,主机名,状态代码,邮政编码或标签。它们通常用于过滤,排序,和聚合。keyword字段只能按其确切值进行搜索。如果需要索引电子邮件正文或产品说明等全文内容,则可能应该使用text字段。

有时候一个字段同时拥有全文类型(text)和关键字类型(keyword)是有用的:一个用于全文搜索,另一个用于聚合和排序。这可以通过多字段类型来实现。

  • 数字类型:long, integer, short, byte, double, float, half_float, scaled_float

  • 日期类型

  • 布尔类型

  • 二进制类型

  • 范围数据类型:integer_range, float_range, long_range, double_range, date_range

 

复杂数据类型

Geo(地理)数据类型

专用数据类型

多字段

有时候单纯的一个字段类型满足不了我们复杂的需求,为了不同的目的,以不同的方式索引同一个字段通常很有用。多字段也是ES的一种数据类型,只不过结合了更多的功能。

例如,对于字符串字段,我们既可以将它映射为text类型用于全文搜索,亦可以将它映射为keyword类型用于排序或聚合,或者,还可以使用标准分词器、英语分词器和其他语言分词器索引文本字段。

大多数数据类型都通过fields参数支持多字段。例如对于城市名称的多字段映射,可以这样写:

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "cityName": {
          "type": "text",
          "fields": {
            "raw": { 
              "type":  "keyword"
           }
         }
       }
     }
   }
 }
}

2.映射

映射是定义一个文档及其包含的字段如何存储和索引的过程。例如,使用映射来定义:

  • 应将哪些字符串字段视为全文字段。

  • 哪些字段包含数字,日期或地理位置。

  • 是否应将文档中所有字段的值索引到catch-all _all字段中。

  • 日期值的格式。

  • 自定义规则以控制动态添加字段的映射。

其实在 ElasticSearch中可以不需要事先定义映射(Mapping),文档写入ElasticSearch时,会根据文档字段自动识别类型,但是通过这种自动识别的字段不是很精确,对于一些复杂的需要分词的就不适合了。

根据是否自动识别映射类型,可以将映射分为动态映射静态映射

  • 动态映射,即不事先指定映射类型(Mapping),文档写入ElasticSearch时,ES会根据文档字段自动识别类型,这种机制称之为动态映射。

  • 静态映射,即人为事先定义好映射,包含文档的各个字段及其类型等,这种方式称之为静态映射,亦可称为显式映射。

3.动态映射

Elasticsearch最重要的功能之一是它试图摆脱你的方式,让你尽快开始探索你的数据。Elasticsearch试图让你成功安装环境之后就可以直接使用。要索引文档,您不必首先创建索引、定义映射类型和定义字段,其实您只需索引一个文档数据,然后索引、类型和字段将自动生效。

索引一个图书的文档:

PUT /library/book/1
{
  "bookId":1,
  "bookName":"Java核心技术 卷I",
  "publishDate":"2014-03-12"
}

返回结果如下,表示成功

{
  "_index": "library",
  "_type": "book",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
 },
  "_seq_no": 0,
  "_primary_term": 1
}

查看mapping映射信息 : GET library/_mapping

得到如下映射信息,重点关注mapping节点的内容

{
  "library": {
    "mappings": {
      "book": {
        "properties": {
          "bookId": {
            "type": "long"
         },
          "bookName": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
             }
           }
         },
          "publishDate": {
            "type": "date"
         }
       }
     }
   }
 }
}

假设启用了动态字段映射功能,则使用一些简单的规则来确定字段应具有的数据类型,其他的以外的字段必须要显式映射数据类型了:

JSON datatypeElasticsearch datatype
null没有字段添加
true or falseboolean
integerlong
objectobject
array依赖于数组中首个非空值
string可以是日期字段、double或long字段,也可以是带有关键字子字段的文本字段。

4.静态映射(显式映射)

静态映射与关系数据库中创建表语句类型,需要事先指定字段类似。相对于动态映射,静态映射可以添加更加详细字段类型、更精准的配置信息等。

三、索引别名Aliases

 

1.业务问题

业务需求是不断变化迭代的,也许我们之前写的某个业务逻辑在下个版本就变化了,我们可能需要修改原来的设计,例如数据库可能需要添加一个字段或删减一个字段,而在搜索中也会发生这件事,即使你认为现在的索引设计已经很完美了,在生产环境中,还是有可能需要做一些修改的,需要添加映射字段或者需要修改字段类型等等。

数据库中我们可以直接修改原来的表设计语句,前提是需要做好数据迁移。但是在 Elasticsearch 中就没那么简单了。尽管可以增加新的类型到索引中,或者增加新的字段到类型中,但是不能添加新的分析器或者对现有的字段做改动。如果你那么做的话,结果就是那些已经被索引的数据就不正确,搜索也不能正常工作。针对这个问题必须重新建立索引。

2.别名定义

重新建立索引的问题是必须更新应用中的索引名称,索引别名就是用来解决这个问题的!

假设我们有个学生的原始索引 student_index_v1,我们给它起个别名 student_index,程序中也是用别名 student_index 进行搜索,当我们的业务需求发生改变需要修改索引的时候,我们重新创建个索引 student_index_v2,同时将别名 student_index 指向新的索引 student_index_v2,同时将 student_index_v1 的数据迁移到新的 student_index_v2,这样我们就可以做到在零停机下从旧索引切换到新索引。

索引别名就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何一个需要索引名的API来使用,而且别名不能与索引同名

别名带给我们极大的灵活性,允许我们做下面这些:

  • 在运行的集群中可以无缝的从一个索引切换到另一个索引。

  • 给多个索引分组。

  • 给索引的一个子集创建视图。

3.别名管理

别名还可以映射到某个索引也可以映射到多个索引。别名还可以与筛选器关联,筛选器将在搜索和路由值时自动应用,别名不能与索引同名。

Elasticsearch 中有两种方式管理别名: _alias 用于单个操作, _aliases 用于执行多个原子级操作。

单个索引别名

POST /_aliases
{
    "actions" : [
       { "add" : { "index" : "test1", "alias" : "alias1" } }
   ]
}

删除别名

POST /_aliases
{
    "actions" : [
       { "remove" : { "index" : "test1", "alias" : "alias1" } }
   ]
}

重命名别名

POST /_aliases
{
    "actions" : [
       { "remove" : { "index" : "test1", "alias" : "alias1" } },
       { "add" : { "index" : "test2", "alias" : "alias1" } }
   ]
}

重命名别名是一个简单的删除然后指向新的索引。这个操作是原子性的,因此不需要担心短时间内的别名不指向一个索引。

将别名与多个索引关联

POST /_aliases
{
    "actions" : [
       { "add" : { "index" : "test1", "alias" : "alias1" } },
       { "add" : { "index" : "test2", "alias" : "alias1" } }
   ]
}

# 通过索引数组的方式来实现
POST /_aliases
{
    "actions" : [
       { "add" : { "indices" : ["test1", "test2"], "alias" : "alias1" } }
   ]
}

对于上面的示例,还可以使 glob pattern 将别名关联到拥有公共名称的多个索引:

POST /_aliases
{
    "actions" : [
       { "add" : { "index" : "test*", "alias" : "all_test_indices" } }
   ]
}

Filtered Aliases

过滤器别名提供了一个简单的方法对同一个索引来创建不同的“视图”。过滤器能够使用Query DSL来定义并且被应用到所有的搜索,统计,通过查询删除和其它类似的行为。

为了创建一个带过滤器的别名,首先需要确保映射的字段已经存在于mapping中。

PUT /test1
{
  "mappings": {
    "_doc": {
      "properties": {
        "user" : {
          "type": "keyword"
       }
     }
   }
 }
}
# 然后我们可以创建一个在user字段上带过滤器的别名。
POST /_aliases
{
    "actions" : [
       {
            "add" : {
                 "index" : "test1",
                 "alias" : "alias2",
                 "filter" : { "term" : { "user" : "kimchy" } }
           }
       }
   ]
}
# 成功则返回
{"acknowledged":true}

这样设置之后,我们通过 test1 这个 index 直接进行搜索可以看到索引的全部文档,但是通过 alias2 这个别名就只能看到符合过滤器过滤后的结果了,即只有一个 user 为 "kimchy" 的结果。

 

Routing

何为路由?

所有的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫做 routing 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档---例如所有属于同一个用户的文档都被存储到同一个分片中。

以下命令创建一个指向索引 test 的新别名 alias1。创建 alias1 后,所有具有此别名的操作将自动修改为使用值 1 进行路由:

POST /_aliases
{
    "actions" : [
       {
            "add" : {
                 "index" : "test",
                 "alias" : "alias1",
                 "routing" : "1"
           }
       }
   ]
}

还可以为搜索和索引操作指定不同的路由值

POST /_aliases
{
    "actions" : [
       {
            "add" : {
                 "index" : "test",
                 "alias" : "alias2",
                 "search_routing" : "1,2",
                 "index_routing" : "2"
           }
       }
   ]
}

如上例所示,搜索路由(search_routing)可能包含几个用逗号分隔的多个值,但是 索引路由(index_routing)就只能包含一个值。

如果使用路由别名的搜索操作也有路由参数,则使用搜索别名路由和参数中指定的路由的交集。例如,下面的命令将使用“2”作为路由值。因为搜索操作中有路由参数2,3,而搜索路由设置的是1,2,所以取交集即为2。

GET /alias2/_search?q=user:kimchy&routing=2,3

4.文档迁移

对于新旧索引的文档数据迁移,字段 _source 的一个优点是在Elasticsearch中已经有整个文档。不必从源数据中重建索引,而且那样通常比较慢。

为了有效的重新索引所有在旧的索引中的文档,用 scroll 从旧的索引检索批量文档 , 然后用 bulk API 把文档推送到新的索引中。

对现有数据的这类改变最简单的办法就是重新索引:用新的setting创建新的索引并把文档从旧的索引复制到新的索引。

在应用中最好的方式是使用别名而不是索引名。这样就可以在任何时候重建索引。别名的开销很小,应该广泛使用。

 

四、分布式工作原理

 

Elasticsearch 天生就是分布式的,并且在设计时屏蔽了分布式的复杂性。

Elasticsearch 尽可能地屏蔽了分布式系统的复杂性。这里列举了一些在后台自动执行的操作:

  • 分配文档到不同的容器 或 分片 中,文档可以储存在一个或多个节点中。

  • 按集群节点来均衡分配这些分片,从而对索引和搜索过程进行负载均衡。

  • 复制每个分片以支持数据冗余,从而防止硬件故障导致的数据丢失。

  • 将集群中任一节点的请求路由到存有相关数据的节点。

  • 集群扩容时无缝整合新节点,重新分配分片以便从离群节点恢复。

     

下里从以下几个部分来详细讲解 Elasticsearch 分布式的内部实现机制。

1.集群的原理

通常来讲,提升分布式性能可以通过购买性能更强大( 垂直扩容 ) 或者数量更多的服务器( 水平扩容 )来实现。对于Elasticsearch来说,受到硬件设备的技术和价格限制,垂直扩容是有极限的。真正的扩容能力是来自于水平扩容(为集群添加更多的节点,并且将负载压力和稳定性分散到这些节点中)

对于大多数的数据库而言,通常需要对应用程序进行非常大的改动,才能利用上横向扩容的新增资源。 与之相反的是,ElastiSearch天生就是 分布式的 ,它知道如何通过管理多节点来提高扩容性和可用性。 这也意味着你的应用无需关注这个问题。那么它是如何管理的呢?

a.主节点

启动一个 ES 实例就是一个节点,节点加入集群是通过配置文件中设置相同的 cluste.name 而实现的。所以集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。

与其他组件集群(mysql,redis)的 master-slave模式一样,ES集群中也会选举一个节点成为主节点,主节点它的职责是维护全局集群状态,在节点加入或离开集群的时候重新分配分片。

所有主要的文档级别API(索引,删除,搜索)都不与主节点通信,主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。如果集群中就只有一个节点,那么它同时也就是主节点。

所以如果我们使用 kibana 来作为视图操作工具的话,我们只需在kibana.yml的配置文件中,将elasticsearch.url: "http://localhost:9200"设置为主节点就可以了,通过主节点 ES 会自动关联查询所有节点和分片以及副本的信息。所以 kibana 一般都和主节点在同一台服务器上。

作为用户,我们可以将请求发送到 集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。

b.发现机制

ES 通过发现机制(discovery module)实现只需要配置相同的cluste.name就将节点加入同一集群。

发现机制 负责发现集群中的节点,以及选择主节点。每次集群状态发生更改时,集群中的其他节点都会知道状态(具体方式取决于使用的是哪一种发现机制)。

ES目前主要推荐的自动发现机制,有如下几种:

  1. Azure classic discovery 插件方式,多播

  2. EC2 discovery 插件方式,多播

  3. Google Compute Engine (GCE) discovery 插件方式,多播

  4. Zen discovery 默认实现,多播/单播

单播,多播,广播的区别:

  • 单播(unicast):网络节点之间的通信就好像是人们之间的对话一样。如果一个人对另外一个人说话,那么用网络技术的术语来描述就是“单播”,此时信息的接收和传递只在两个节点之间进行。例如,你在收发电子邮件、浏览网页时,必须与邮件服务器、Web服务器建立连接,此时使用的就是单播数据传输方式。

  • 多播(multicast):“多播”也可以称为“组播”,多播”可以理解为一个人向多个人(但不是在场的所有人)说话,这样能够提高通话的效率。因为如果采用单播方式,逐个节点传输,有多少个目标节点,就会有多少次传送过程,这种方式显然效率极低,是不可取的。如果你要通知特定的某些人同一件事情,但是又不想让其他人知道,使用电话一个一个地通知就非常麻烦。多播方式,既可以实现一次传送所有目标节点的数据,也可以达到只对特定对象传送数据的目的。多播在网络技术的应用并不是很多,网上视频会议、网上视频点播特别适合采用多播方式。

  • 广播(broadcast):可以理解为一个人通过广播喇叭对在场的全体说话,这样做的好处是通话效率高,信息一下子就可以传递到全体,广播是不区分目标、全部发送的方式,一次可以传送完数据,但是不区分特定数据接收对象。

上面列举的发现机制中, Zen Discovery 是 ES 默认内建发现机制。它提供单播多播的发现方式,并且可以扩展为通过插件支持云环境和其他形式的发现。所以我们接下来重点介绍下 Zen Discovery是如何在Elasticsearch中使用的。

集群是由相同cluster.name的节点组成的。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。

单播主机列表通过discovery.zen.ping.unicast.hosts来配置。这个配置在 elasticsearch.yml 文件中:

discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]

具体的值是一个主机数组或逗号分隔的字符串。每个值应采用host:porthost的形式(其中port默认为设置transport.profiles.default.port,如果未设置则返回transport.tcp.port)。请注意,必须将IPv6主机置于括号内。此设置的默认值为127.0.0.1,[:: 1]

Elasticsearch 官方推荐我们使用 单播 代替 组播。而且 Elasticsearch 默认被配置为使用 单播 发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。

虽然 组播 仍然作为插件提供, 但它应该永远不被使用在生产环境了,否则你得到的结果就是一个节点意外的加入到了你的生产环境,仅仅是因为他们收到了一个错误的 组播 信号。对于 组播 本身并没有错,组播会导致一些愚蠢的问题,并且导致集群变的脆弱(比如,一个网络工程师正在捣鼓网络,而没有告诉你,你会发现所有的节点突然发现不了对方了)。

使用单播,你可以为 Elasticsearch 提供一些它应该去尝试连接的节点列表。当一个节点联系到单播列表中的成员时,它就会得到整个集群所有节点的状态,然后它会联系 master 节点,并加入集群。

这意味着你的单播列表不需要包含你的集群中的所有节点,它只是需要足够的节点,当一个新节点联系上其中一个并且说上话就可以了。如果你使用 master 候选节点作为单播列表,你只要列出三个就可以了。

2.应对故障

a.单节点的问题

如果我们启动了一个单独的节点,里面不包含任何的数据和索引,那我们的集群就是一个包含空内容节点的集群,简称空集群

当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。单点的最大问题是系统容错性不高,当单节点所在服务器发生故障后,整个 ES 服务就会停止工作。

所以,单节点在硬件故障时有丢失数据的风险。

b.水平扩容

既然单点是有问题的,那我们只需再启动几个节点并加入到当前集群中,这样就可以提高可用性并实现故障转移,这种方式即 水平扩容

当第二个节点加入到集群后,3个 副本分片 将会分配到这个节点上——每个主分片对应一个副本分片。 这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。

所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。

c.动态扩容

随着数据的不断增加,每个主分片和副本分片的数据不断累积,达到一定程度之后也会降低搜索性能。那么怎样为我们的正在增长中的应用程序按需扩容呢?答案是:继续增加节点。

为了分散负载,ES 会对分片进行重新分配。每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。

索引的主分片数这个值在索引创建后就不能修改了(默认值是 5),但是每个主分片的副本数(默认值是 1 )对于活动的索引库,这个值可以随时修改的。

如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 你需要增加更多的硬件资源来提升吞吐量。

3.处理并发冲突

通常当我们使用 索引 API 更新文档时 ,可以一次性读取原始文档,做我们的修改,然后重新索引 整个文档 。 最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在 Elasticsearch 中。如果其他人同时更改这个文档,他们的更改将丢失。

很多时候这是没有问题的。也许我们的主数据存储是一个关系型数据库,我们只是将数据复制到 Elasticsearch 中并使其可被搜索。也许两个人同时更改相同的文档的几率很小。或者对于我们的业务来说偶尔丢失更改并不是很严重的问题。

但有时丢失了一个变更就是非常严重的 。试想我们使用 Elasticsearch 存储我们网上商城商品库存的数量, 每次我们卖一个商品的时候,我们在 Elasticsearch 中将库存数量减少。

有一天,管理层决定做一次促销。突然地,我们一秒要卖好几个商品。 假设有两个 web 程序并行运行,每一个都同时处理所有商品的销售,那么会造成库存结果不一致的情况。

变更越频繁,读数据和更新数据的间隙越长,也就越可能丢失变更。

a.乐观并发控制 - 版本号

在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:

  • 悲观锁 这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。

  • 乐观锁 Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。

Elasticsearch 中对文档的 index , GET 和 delete 请求时,我们指出每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。

Elasticsearch 使用这个 _version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。

我们可以利用 _version 号来确保应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。

b.乐观并发控制 - 外部系统

版本号(version)只是其中一个实现方式,我们还可以借助外部系统使用版本控制,一个常见的设置是使用其它数据库作为主要的数据存储,使用 Elasticsearch 做数据检索, 这意味着主数据库的所有更改发生时都需要被复制到 Elasticsearch ,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。

如果你的主数据库已经有了版本号,或一个能作为版本号的字段值比如 timestamp,那么你就可以在 Elasticsearch 中通过增加 version_type=external到查询字符串的方式重用这些相同的版本号,版本号必须是大于零的整数, 且小于 9.2E+18(一个 Java 中 long 类型的正值)。

外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同, Elasticsearch 不是检查当前 _version 和请求中指定的版本号是否相同,而是检查当前_version 是否小于指定的版本号。如果请求成功,外部的版本号作为文档的新_version 进行存储。

4.文档存储原理

创建索引的时候我们只需要指定分片数和副本数,ES 就会自动将文档数据分发到对应的分片和副本中。那么文件究竟是如何分布到集群的,又是如何从集群中获取的呢?

a.文档是如何路由到分片中的

当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?

首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

在创建索引的时候合理的预分配分片数是很重要的。

所有的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫做 routing 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。

b.主分片和副本分片如何交互

我们可以发送请求到集群中的任一节点。每个节点都有能力处理任意请求。每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到 Node 1 ,我们将其称为 协调节点(coordinating node)

当发送请求的时候,为了扩展负载,更好的做法是轮询集群中所有的节点。

对文档的新建、索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。

以下是在主副分片和任何副本分片上面 成功新建,索引和删除文档所需要的步骤顺序:

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

  2. 节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。

  3. Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node1 和 Node2 的副本分片上。一旦所有的副本分片都报告成功,Node 3 将向协调节点报告成功,协调节点向客户端报告成功。

在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。

在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。

在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

 

五、自动发现机制 - Zen Discoveryedit

 

1.发现方式

Zen discovery是内建的、默认的、用于Elasticsearch的发现模块。它提供了单播和基于文件的发现,可以通过插件扩展到支持云环境和其他形式的发现。

Zen Discovery 是与其他模块集成的,例如,节点之间的所有通信都使用 transport 模块完成。某个节点通过 发现机制 找到其他节点是使用 Ping 的方式实现的。

Zen Discovery 使用种子节点(seed nodes)列表来开始发现过程。在启动时,或者在选举新主节点的时候,Elasticsearch 会尝试连接到其列表中的每个种子节点,并与他们进行类似'闲聊'的对话,以查找其他节点并构建集群的完整成员图。

默认情况下,有两种方法可用于配置种子节点列表:单播基于文件。建议种子节点列表主要由集群中那些 Master-eligible 的节点组成。

Master-eligible:node.master设置为 true(默认)的节点,使其有资格被选为控制集群的主节点。

2.选举主节点

作为 ping 过程的一部分,一个集群的主节点需要是被选举或者加入进来的(即选举主节点也会执行ping,其他的操作也会执行ping)。这个过程是自动执行的。通过配置discovery.zen.ping_timeout来控制节点加入某个集群或者开始选举的响应时间(默认3s)。

在这段时间内有3个 ping 会发出。如果超时,重新启动 ping 程序。在网络缓慢时,3秒时间可能不够,这种情况下,需要慎重增加超时时间,增加超时时间会减慢选举进程。

一旦节点决定加入一个存在的集群,它会发出一个加入请求给主节点,这个请求的超时时间由discovery.zen.join_time控制,默认是 ping 超时时间(discovery.zen.ping_timeout)的20倍。

当主节点停止或者出现问题,集群中的节点会重新 ping 并选举一个新节点。有时一个节点也许会错误的认为主节点已死,所以这种 ping 操作也可以作为部分网络故障的保护性措施。在这种情况下,节点将只从其他节点监听有关当前活动主节点的信息。

如果discovery.zen.master_election.ignore_non_master_pings设置为true时(默认值为false),node.masterfalse的节点不参加主节点的选举,同时选票也不包含这种节点。

通过设置node.masterfalse,可以将节点设置为非备选主节点,永远没有机会成为主节点。

discovery.zen.minimum_master_nodes设置了最少有多少个备选主节点参加选举,同时也设置了一个主节点需要控制最少多少个备选主节点才能继续保持主节点身份。如果控制的备选主节点少于discovery.zen.minimum_master_nodes个,那么当前主节点下台,重新开始选举。

discovery.zen.minimum_master_nodes必须设置一个恰当的备选主节点值(quonum,一般设置 为备选主节点数/2+1),尽量避免只有两个备选主节点,因为两个备选主节点quonum应该为2,那么如果一个节点出现问题,另一个节点的同意人数最多只能为1,永远也不能选举出新的主节点,这时就发生了脑裂现象。

3.集群故障检测

有两个故障检测进程在集群的生命周期中一直运行。一个是主节点的,ping集群中所有的其他节点,检查他们是否活着。另一种是每个节点都ping主节点,确认主节点是否仍在运行或者是否需要重新启动选举程序。

使用discovery.zen.fd前缀设置来控制故障检测过程,配置如下:

配置描述
discovery.zen.fd.ping_interval节点多久ping一次,默认1s
discovery.zen.fd.ping_timeout等待响应时间,默认30s
discovery.zen.fd.ping_retries失败或超时后重试的次数,默认3

4.集群状态更新

主节点是唯一一个能够更新集群状态的节点。主节点一次处理一个群集状态更新,应用所需的更改并将更新的群集状态发布到群集中的所有其他节点。当其他节点接收到状态时,先确认收到消息,但是不应用最新状态。如果主节点在规定时间(discovery.zen.commit_timeout ,默认30s)内没有收到大多数节点(discovery.zen.minimum_master_nodes)的确认,集群状态更新不被通过。

一旦足够的节点响应了更新的消息,新的集群状态(cluster state)被提交并且会发送一条消息给所有的节点。这些节点开始在内部应用新的集群状态。在继续处理队列中的下一个更新之前,主节点等待所有节点响应,直到超时(discovery.zen.publish_timeout,默认设置为30秒)。上述两个超时设置都可以通过集群更新设置api动态更改。

5.No master block

对于一个可以正常充分运作的集群来说,必须拥有一个活着的主节点和正常数量(discovery.zen.minimum_master_nodes个)活跃的备选主节点。discovery.zen.no_master_block设置了没有主节点时限制的操作。它又两个可选参数

  • all:所有操作均不可做,读写、包括集群状态的读写api,例如获得索引配置(index settings),putMapping,和集群状态(cluster state)api

  • write:默认为write,写操作被拒绝执行,基于最后一次已知的正常的集群状态可读,这也许会读取到已过时的数据。

discovery.zen.no_master_block,对于节点相关的基本api,这个参数是无效的,如集群统计信息(cluster stats),节点信息(node info),节点统计信息(node stats)。对这些api的请求不会被阻止,并且可以在任何可用节点上运行。

 

六、剖析ElasticSearch的索引原理

 

创建索引的时候,我们通过Mapping 映射定义好索引的基本结构信息,接下来我们肯定需要往 ES 里面新增业务文档数据了,例如用户,日志等业务数据。新增的业务数据,我们根据 Mapping 来生成对应的倒排索引信息 。

Elasticsearch是一个基于Apache Lucene 的开源搜索引擎。Elasticsearch的搜索高效的原因并不是像Redis那样重依赖内存的,而是通过建立特殊的索引数据结构--倒排索引实现的。由于它的使用场景:处理PB级结构化或非结构化数据,数据量大且需要持久化防止断电丢失,所以 Elasticsearch 的数据和索引存储是依赖于服务器的硬盘。这也是为什么我们在ES性能调优的时候可以将使用SSD硬盘存储作为其中一个优化项来考虑。

倒排索引的概念,我相信大家都已经知道了,这里就不在赘述,倒排索引可以说是Elasticsearch搜索高效和支持非结构化数据检索的主要原因了,但是倒排索引被写入磁盘后是不可改变 的:它永远不会修改

1.段和提交点

倒排索引的不可变性,这点主要是因为 Elasticsearch 的底层是基于 Lucene,而在 Lucene 中提出了按段搜索的概念,将一个索引文件拆分为多个子文件,则每个子文件叫作,每个段都是一个独立的可被搜索的数据集,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。

的概念提出主要是因为:在早期全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中。如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。

而且在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。说到这,你们可能会想到 ConcurrentHashMap 的分段锁 的概念,其实原理有点类似。

而且 Elasticsearch 中的倒排索引被设计成不可变的,有以下几个方面优势

  • 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。

  • 一旦索引被读入内核的文件系统缓存,便会留在哪里。由于其不变性,只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。

  • 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。

  • 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。

每一个本身都是一个倒排索引,但索引在 Lucene 中除表示所有段的集合外,还增加了提交点的概念。

为了提升写的性能,Lucene并没有每新增一条数据就增加一个段,而是采用延迟写的策略,每当有新增的数据时,就将其先写入内存中,然后批量写入磁盘中。若有一个段被写到硬盘,就会生成一个提交点,提交点就是一个列出了所有已知段和记录所有提交后的段信息的文件

2.写索引的流程

上面说过 ES 的索引的不变性,还有段和提交点的概念。那么它的具体实现细节和写入磁盘的过程是怎样的呢?

  • 用户创建了一个新文档,新文档被写入内存中。

  • 不时地, 缓存被提交,这时缓存中数据会以段的形式被先写入到文件缓存系统而不是直接被刷到磁盘。 这是因为,提交一个新的段到磁盘需要一个fsync 来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 fsync 操作代价很大;如果每次索引一个文档都去执行一次的话会造成很大的性能问题,但是这里新段会被先写入到文件系统缓存,这一步代价会比较低。

  • 新的段被写入到文件缓存系统,这时内存缓存被清空。在文件缓存系统会存在一个未提交的段。虽然新段未被提交(刷到磁盘),但是文件已经在缓存中了, 此时就可以像其它文件一样被打开和读取了。

  • 到目前为止索引的段还未被刷新到磁盘,如果没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。如上图所示,一个文档被索引之后,就会被添加到内存缓冲区,并且同时追加到了 translog。

  • 每隔一段时间,更多的文档被添加到内存缓冲区和追加到事务日志(translog),之后新段被不断从内存缓存区被写入到文件缓存系统,这时内存缓存被清空,但是事务日志不会。随着 translog 变得越来越大,达到一定程度后索引被刷新,在刷新(flush)之后,段被全量提交,一个提交点被写入硬盘,并且事务日志被清空。

从整个流程我们可以了解到以下几个问题:

  • 为什么说 ES 搜索是近实时的? 因为文档索引在从内存缓存被写入到文件缓存系统时,虽然还没有进行提交未被 flush 到磁盘,但是缓冲区的内容已经被写入一个段(segment6)中且新段可被搜索。这就是为什么我们说 Elasticsearch 是近实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。

  • Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据? 新索引文档被写入到内存缓存时,同时会记录一份到事务日志(translog)中,translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。 translog 也被用来提供实时 CRUD 。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。

a.段合并

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。

Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。

段合并的时候会将那些旧的已删除文档 从文件系统中清除。 被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。

3.如何更新索引

如果你需要让一个新的文档可被搜索,这就涉及到索引的更新了,索引不可被修改但又需要更新,这种看似矛盾的要求,我们需要怎么做呢?

ES 的解决方法就是:用更多的索引。就是原来的索引不变,我们对新的文档再创建一个索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到(从最早的开始),查询完后再对结果进行合并。

以正常逻辑来看,我们知道搜索的时候肯定以新的索引为标准,但是段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del文件,文件中会列出这些被删除文档的段信息。

当一个文档被 “删除” 时,它实际上只是在.del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

 

七、ElasticSearch的性能优化

 

1.硬件选择

Elasticsearch的基础是 Lucene,所有的索引和文档数据是存储在本地的磁盘中,具体的路径可在 ES 的配置文件../config/elasticsearch.yml中配置,如下:

# ----------------------------------- Paths ------------------------------------
#
# Path to directory where to store the data (separate multiple locations by comma):
#
path.data: /path/to/data
#
# Path to log files:
#
path.logs: /path/to/logs

磁盘在现代服务器上通常都是瓶颈。Elasticsearch 重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。这里有一些优化磁盘 I/O 的技巧:

  • 使用 SSD。就像其他地方提过的, 他们比机械磁盘优秀多了。

  • 使用 RAID 0。条带化 RAID 会提高磁盘 I/O,代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像或者奇偶校验 RAID 因为副本已经提供了这个功能。

  • 另外,使用多块硬盘,并允许 Elasticsearch 通过多个 path.data 目录配置把数据条带化分配到它们上面。

  • 不要使用远程挂载的存储,比如 NFS 或者 SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。

  • 如果你用的是 EC2,当心 EBS。即便是基于 SSD 的 EBS,通常也比本地实例的存储要慢。

 

2.内部压缩

对于一个分布式、可扩展、支持PB级别数据、实时的搜索与数据分析引擎,ES 本身对于索引数据和文档数据的存储方面内部做了很多优化,具体体现在对数据的压缩,那么是如何压缩的呢?介绍前先要说明下 Postings lists 的概念。

 

a.倒排列表 - postings list

搜索引擎一项很重要的工作就是高效的压缩和快速的解压缩一系列有序的整数列表。我们都知道,Elasticsearch 基于 Lucene,一个 Lucene 索引 我们在 Elasticsearch 称作 分片 , 并且引入了 按段搜索 的概念。

新的文档首先被添加到内存索引缓存中,然后写入到一个基于磁盘的段。在每个 segment 内文档都会有一个 0 到文档个数之间的标识符(最高值 2^31 -1),称之为 doc ID。这在概念上类似于数组中的索引:它本身不做存储,但足以识别每个item 数据。

Segments 按顺序存储有关文档的数据,在一个Segments 中 doc ID 是 文档的索引。因此,segment 中的第一个文档的 doc ID 为0,第二个为1,等等。直到最后一个文档,其 doc ID 等于 segment 中文档的总数减1。

那么这些 doc ID 有什么用呢?倒排索引需要将 terms 映射到包含该单词 (term) 的文档列表,这样的映射列表我们称之为:倒排列表(postings list)。具体某一条映射数据称之为:倒排索引项(Posting)

倒排列表 用来记录有哪些文档包含了某个单词(Term)。一般在文档集合里会有很多文档包含某个单词,每个文档会记录文档编号(doc ID),单词在这个文档中出现的次数(TF)及单词在文档中哪些位置出现过等信息,这样与一个文档相关的信息被称做 倒排索引项(Posting),包含这个单词的一系列倒排索引项形成了列表结构,这就是某个单词对应的 倒排列表

 

b.Frame Of Reference

针对倒排列表,Lucene 采用一种增量编码的方式将一系列 ID 进行压缩存储,即称为Frame Of Reference的压缩方式(FOR)

在实际的搜索引擎系统中,并不存储倒排索引项中的实际文档编号(Doc ID),而是代之以文档编号差值(D-Gap)。文档编号差值是倒排列表中相邻的两个倒排索引项文档编号的差值,一般在索引构建过程中,可以保证倒排列表中后面出现的文档编号大于之前出现的文档编号,所以文档编号差值总是大于0的整数。

之所以要对文档编号进行差值计算,主要原因是为了更好地对数据进行压缩,原始文档编号一般都是大数值,通过差值计算,就有效地将大数值转换为了小数值,而这有助于增加数据的压缩率。

比如一个词对应的文档ID 列表[73, 300, 302, 332,343, 372] ,ID列表首先要从小到大排好序;

  • 第一步: 增量编码就是从第二个数开始每个数存储与前一个id的差值,即300-73=227302-300=2,...,一直到最后一个数。

  • 第二步: 就是将这些差值放到不同的区块,Lucene使用256个区块,下面示例为了方便展示使用了3个区块,即每3个数一组。

  • 第三步: 位压缩,计算每组3个数中最大的那个数需要占用bit位数,比如30、11、29中最大数30最小需要5个bit位存储,这样11、29也用5个bit位存储,这样才占用15个bit,不到2个字节,压缩效果很好。

如下面原理图所示,这是一个区块大小为3的示例(实际上是256):

 

考虑到频繁出现的term(所谓low cardinality的值),比如gender里的男或者女。如果有1百万个文档,那么性别为男的 posting list 里就会有50万个int值。用 Frame of Reference 编码进行压缩可以极大减少磁盘占用。这个优化对于减少索引尺寸有非常重要的意义。

因为这个 FOR 的编码是有解压缩成本的。利用skip list(跳表),除了跳过了遍历的成本,也跳过了解压缩这些压缩过的block的过程,从而节省了cpu。

 

c.Roaring bitmaps (RBM)

在 elasticsearch 中使用filters 优化查询,filter查询只处理文档是否匹配与否,不涉及文档评分操作,查询的结果可以被缓存。具体的 Filter 和Query 的异同读者可以自行网上查阅资料。

对于filter 查询,elasticsearch 提供了Filter cache 这种特殊的缓存,filter cache 用来存储 filters 得到的结果集。缓存 filters 不需要太多的内存,它只保留一种信息,即哪些文档与filter相匹配。同时它可以由其它的查询复用,极大地提升了查询的性能。

Frame Of Reference 压缩算法对于倒排表来说效果很好,但对于需要存储在内存中的 Filter cache 等不太合适。

倒排表和Filter cache两者之间有很多不同之处:

  • 倒排表存储在磁盘,针对每个词都需要进行编码,而Filter等内存缓存只会存储那些经常使用的数据。

  • 针对Filter数据的缓存就是为了加速处理效率,对压缩算法要求更高。

这就产生了下面针对内存缓存数据可以进行高效压缩解压和逻辑运算的roaring bitmaps算法。

说到Roaring bitmaps,就必须先从bitmap说起。Bitmap是一种数据结构,假设有某个posting list:

[3,1,4,7,8]

对应的Bitmap就是:

[0,1,0,1,1,0,0,1,1]

非常直观,用0/1表示某个值是否存在,比如8这个值就对应第8位,对应的bit值是1,这样用一个字节就可以代表8个文档id(1B = 8bit),旧版本(5.0之前)的Lucene就是用这样的方式来压缩的。但这样的压缩方式仍然不够高效,Bitmap自身就有压缩的特点,其用一个byte就可以代表8个文档,所以100万个文档只需要12.5万个byte。但是考虑到文档可能有数十亿之多,在内存里保存Bitmap仍然是很奢侈的事情。而且对于个每一个filter都要消耗一个Bitmap,比如age=18缓存起来的话是一个Bitmap,18<=age<25是另外一个filter缓存起来也要一个Bitmap。

Bitmap的缺点是存储空间随着文档个数线性增长,所以秘诀就在于需要有一个数据结构打破这个魔咒,那么就一定要用到某些指数特性:

  • 可以很压缩地保存上亿个bit代表对应的文档是否匹配filter;

  • 这个压缩的Bitmap仍然可以很快地进行AND和 OR的逻辑操作。

Lucene使用的这个数据结构叫做 Roaring Bitmap,即位图压缩算法,简称BMP

 

其压缩的思路其实很简单。与其保存100个0,占用100个bit。还不如保存0一次,然后声明这个0重复了100遍。

 

3.分片策略

a.合理设置分片数

创建索引的时候,我们需要预分配 ES 集群的分片数和副本数,即使是单机情况下。如果没有在 mapping 文件中指定,那么索引在默认情况下会被分配5个主分片和每个主分片的1个副本。

分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,我们是不能重新修改分片数的。

例如某个创业公司初始用户的索引 t_user 分片数为2,但是随着业务的发展用户的数据量迅速增长,这时我们是不能重新将索引 t_user 的分片数增加为3或者更大的数。

可能有人会说,我不知道这个索引将来会变得多大,并且过后我也不能更改索引的大小,所以为了保险起见,还是给它设为 1000 个分片吧…

一个分片并不是没有代价的。需要了解:

  • 一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU 运转。

  • 每一个搜索请求都需要命中索引中的每一个分片,如果每一个分片都处于不同的节点还好, 但如果多个分片都需要在同一个节点上竞争使用相同的资源就有些糟糕了。

  • 用于计算相关度的词项统计信息是基于分片的。如果有许多分片,每一个都只有很少的数据会导致很低的相关度。

适当的预分配是好的。但上千个分片就有些糟糕。我们很难去定义分片是否过多了,这取决于它们的大小以及如何去使用它们。 一百个分片但很少使用还好,两个分片但非常频繁地使用有可能就有点多了。 监控你的节点保证它们留有足够的空闲资源来处理一些特殊情况。

一个业务索引具体需要分配多少分片可能需要架构师和技术人员对业务的增长有个预先的判断,横向扩展应当分阶段进行。为下一阶段准备好足够的资源。 只有当你进入到下一个阶段,你才有时间思考需要作出哪些改变来达到这个阶段。

一般来说,我们遵循一些原则:

  1. 控制每个分片占用的硬盘容量不超过ES的最大JVM的堆空间设置(一般设置不超过32G,参考下文的JVM设置原则),因此,如果索引的总容量在500G左右,那分片大小在16个左右即可;当然,最好同时考虑原则2。

  2. 考虑一下node数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了1个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以, 一般都设置分片数不超过节点数的3倍。

  3. 主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:

    节点数<=主分片数*(副本数+1)

创建索引的时候需要控制分片分配行为,合理分配分片,如果后期索引所对应的数据越来越多,我们还可以通过索引别名等其他方式解决。

 

b.调整分片分配器的类型

以上是在创建每个索引的时候需要考虑的优化方法,然而在索引已创建好的前提下,是否就是没有办法从分片的角度提高了性能了呢?当然不是,首先能做的是调整分片分配器的类型,具体是在 elasticsearch.yml 中设置cluster.routing.allocation.type 属性,共有两种分片器even_shardbalanced(默认)

even_shard 是尽量保证每个节点都具有相同数量的分片,balanced 是基于可控制的权重进行分配,相对于前一个分配器,它更暴漏了一些参数而引入调整分配过程的能力。

每次ES的分片调整都是在ES上的数据分布发生了变化的时候进行的,最有代表性的就是有新的数据节点加入了集群的时候。当然调整分片的时机并不是由某个阈值触发的,ES内置十一个裁决者来决定是否触发分片调整。另外,这些分配部署策略都是可以在运行时更新的。

 

c.推迟分片分配

对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。

通过修改参数 delayed_timeout ,可以延长再均衡的时间,可以全局设置也可以在索引级别进行修改:

PUT /_all/_settings 
{
  "settings": {
    "index.unassigned.node_left.delayed_timeout": "5m" 
 }
}

通过使用 _all 索引名,我们可以为集群里面的所有的索引使用这个参数,默认时间被延长成了 5 分钟。

这个配置是动态的,可以在运行时进行修改。如果你希望分片立即分配而不想等待,你可以设置参数: delayed_timeout: 0

延迟分配不会阻止副本被提拔为主分片。集群还是会进行必要的提拔来让集群回到 yellow 状态。缺失副本的重建是唯一被延迟的过程。

 

4.索引优化

a.Mapping建模

  1. 尽量避免使用nested或 parent/child,能不用就不用;

    nested query慢, parent/child query 更慢,比nested query慢上百倍;因此能在mapping设计阶段搞定的(大宽表设计或采用比较smart的数据结构),就不要用父子关系的mapping。

  2. 如果一定要使用nested fields,保证nested fields字段不能过多,目前ES默认限制是50。参考:

    index.mapping.nested_fields.limit :50

    因为针对1个document, 每一个nested field, 都会生成一个独立的document, 这将使Doc数量剧增,影响查询效率,尤其是Join的效率。

  3. 避免使用动态值作字段(key),动态递增的mapping,会导致集群崩溃;同样,也需要控制字段的数量,业务中不使用的字段,就不要索引。

    控制索引的字段数量、mapping深度、索引字段的类型,对于ES的性能优化是重中之重。以下是ES关于字段数、mapping深度的一些默认设置:

    index.mapping.nested_objects.limit :10000
    index.mapping.total_fields.limit:1000
    index.mapping.depth.limit: 20
  4. 不需要做模糊检索的字段使用 keyword类型代替 text 类型,这样可以避免在建立索引前对这些文本进行分词。

  5. 对于那些不需要聚合和排序的索引字段禁用Doc values。

    Doc Values 默认对所有字段启用,除了 analyzed strings。也就是说所有的数字、地理坐标、日期、IP 和不分析( not_analyzed )字符类型都会默认开启。

    因为 Doc Values 默认启用,也就是说ES对你数据集里面的大多数字段都可以进行聚合和排序操作。但是如果你知道你永远也不会对某些字段进行聚合、排序或是使用脚本操作, 尽管这并不常见,这时你可以通过禁用特定字段的 Doc Values 。这样不仅节省磁盘空间,也会提升索引的速度。

    要禁用 Doc Values ,在字段的映射(mapping)设置 doc_values: false 即可。

b.索引设置

  1. 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 index.refresh_interval 改到 30s或者更大。 如果你是在做大批量导入,设置 refresh_interval 为-1,同时设置number_of_replicas 为0,通过关闭 refresh 间隔周期,同时不设置副本来提高写性能。

    文档在复制的时候,整个文档内容都被发往副本节点,然后逐字的把索引过程重复一遍。这意味着每个副本也会执行分析、索引以及可能的合并过程。

    相反,如果你的索引是零副本,然后在写入完成后再开启副本,恢复过程本质上只是一个字节到字节的网络传输。相比重复索引过程,这个算是相当高效的了。

  2. 修改 index_buffer_size 的设置,可以设置成百分数,也可设置成具体的大小,最多给512M,大于这个值会触发refresh。默认值是JVM的内存10%,但是是所有切片共享大小。可根据集群的规模做不同的设置测试。

    indices.memory.index_buffer_size:10%(默认)
    indices.memory.min_index_buffer_size: 48mb(默认)
    indices.memory.max_index_buffer_size
  3. 修改 translog 相关的设置:

  • a. 控制数据从内存到硬盘的操作频率,以减少硬盘IO。可将sync_interval 的时间设置大一些。

    index.translog.sync_interval:5s(默认)。
  • b. 控制 tranlog 数据块的大小,达到 threshold 大小时,才会 flush 到 lucene 索引文件。

    index.translog.flush_threshold_size:512mb(默认)
  1. id字段的使用,应尽可能避免自定义_id, 以避免针对ID的版本管理;建议使用ES的默认ID生成策略或使用数字类型ID做为主键,包括零填充序列 ID、UUID-1 和纳秒;这些 ID 都是有一致的,压缩良好的序列模式。相反的,像UID-4 这样的 ID,本质上是随机的,压缩比很低,会明显拖慢 Lucene。

  2. all字段及source 字段的使用,应该注意场景和需要,all字段包含了所有的索引字段,方便做全文检索,如果无此需求,可以禁用;source存储了原始的document内容,如果没有获取原始文档数据的需求,可通过设置includes、excludes 属性来定义放入_source的字段。

  3. 合理的配置使用index属性,analyzed 和not_analyzed,根据业务需求来控制字段是否分词或不分词。只有 groupby需求的字段,配置时就设置成not_analyzed, 以提高查询或聚类的效率。

5.查询效率

  1. 使用批量请求,批量索引的效率肯定比单条索引的效率要高。

  2. query_stringmulti_match 的查询字段越多, 查询越慢。可以在 mapping 阶段,利用 copy_to 属性将多字段的值索引到一个新字段,multi_match时,用新的字段查询。

  3. 日期字段的查询, 尤其是用now 的查询实际上是不存在缓存的,因此, 可以从业务的角度来考虑是否一定要用now, 毕竟利用 query cache 是能够大大提高查询效率的。

  4. 查询结果集的大小不能随意设置成大得离谱的值, 如query.setSize不能设置成 Integer.MAX_VALUE, 因为ES内部需要建立一个数据结构来放指定大小的结果集数据。

  5. 尽量避免使用 script,万不得已需要使用的话,选择painless & experssions 引擎。一旦使用 script 查询,一定要注意控制返回,千万不要有死循环(如下错误的例子),因为ES没有脚本运行的超时控制,只要当前的脚本没执行完,该查询会一直阻塞。如:

     {
       “script_fields”:{
           “test1”:{
               “lang”:“groovy”,
               “script”:“while(true){print 'don’t use script'}”
           }
       }
    }
  6. 避免层级过深的聚合查询, 层级过深的group by , 会导致内存、CPU消耗,建议在服务层通过程序来组装业务,也可以通过pipeline 的方式来优化。

  7. 复用预索引数据方式来提高 AGG 性能:

    如通过 terms aggregations 替代 range aggregations, 如要根据年龄来分组,分组目标是: 少年(14岁以下) 青年(14-28) 中年(29-50) 老年(51以上), 可以在索引的时候设置一个age_group字段,预先将数据进行分类。从而不用按age来做range aggregations, 通过age_group字段就可以了。

  8. Cache的设置及使用:

    a) QueryCache: ES查询的时候,使用filter查询会使用query cache, 如果业务场景中的过滤查询比较多,建议将querycache设置大一些,以提高查询速度。

    indices.queries.cache.size: 10%(默认),//可设置成百分比,也可设置成具体值,如256mb。

    当然也可以禁用查询缓存(默认是开启), 通过index.queries.cache.enabled:false设置。

    b) FieldDataCache: 在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情形下很有必要,可通过indices.fielddata.cache.size:30% 或具体值10GB来设置。但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的。

    c) ShardRequestCache: 查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node), 由协调节点将结果整合。

    如果有需求,可以设置开启; 通过设置index.requests.cache.enable: true来开启。

    不过,shard request cache 只缓存 hits.total, aggregations, suggestions 类型的数据,并不会缓存hits的内容。也可以通过设置indices.requests.cache.size: 1%(默认)来控制缓存空间大小。

6.ES的内存设置

由于ES构建基于lucene, 而lucene设计强大之处在于lucene能够很好的利用操作系统内存来缓存索引数据,以提供快速的查询性能。lucene的索引文件segements是存储在单文件中的,并且不可变,对于OS来说,能够很友好地将索引文件保持在cache中,以便快速访问;因此,我们很有必要将一半的物理内存留给lucene ; 另一半的物理内存留给ES(JVM heap )。所以, 在ES内存设置方面,可以遵循以下原则:

  1. 当机器内存小于64G时,遵循通用的原则,50%给ES,50%留给lucene。

  2. 当机器内存大于64G时,遵循以下原则:

    • a. 如果主要的使用场景是全文检索, 那么建议给ES Heap分配 4~32G的内存即可;其它内存留给操作系统, 供lucene使用(segments cache), 以提供更快的查询性能。

    • b. 如果主要的使用场景是聚合或排序, 并且大多数是numerics, dates, geo_points 以及not_analyzed的字符类型, 建议分配给ES Heap分配 4~32G的内存即可,其它内存留给操作系统,供lucene使用(doc values cache),提供快速的基于文档的聚类、排序性能。

    • c. 如果使用场景是聚合或排序,并且都是基于analyzed 字符数据,这时需要更多的 heap size, 建议机器上运行多ES实例,每个实例保持不超过50%的ES heap设置(但不超过32G,堆内存设置32G以下时,JVM使用对象指标压缩技巧节省空间),50%以上留给lucene。

  3. 禁止swap,一旦允许内存与磁盘的交换,会引起致命的性能问题。 通过: 在elasticsearch.yml 中 bootstrap.memory_lock: true, 以保持JVM锁定内存,保证ES的性能。

  4. GC设置原则:

    • a. 保持GC的现有设置,默认设置为:Concurrent-Mark and Sweep (CMS),别换成G1GC,因为目前G1还有很多BUG。

    • b. 保持线程池的现有设置,目前ES的线程池较1.X有了较多优化设置,保持现状即可;默认线程池大小等于CPU核心数。如果一定要改,按公式((CPU核心数* 3)/ 2)+ 1 设置;不能超过CPU核心数的2倍;但是不建议修改默认配置,否则会对CPU造成硬伤。

7.调整JVM设置

ES 是在 lucene 的基础上进行研发的,隐藏了 lucene 的复杂性,提供简单易用的 RESTful Api接口。ES 的分片相当于 lucene 的索引。由于 lucene 是 Java 语言开发的,是 Java 语言就涉及到 JVM,所以 ES 存在 JVM的调优问题。

  • 调整内存大小。当频繁出现full gc后考虑增加内存大小,但是堆内存和堆外内存不要超过32G。

  • 调整写入的线程数和队列大小。不过线程数最大不能超过33个(es控制死)。

  • ES非常依赖文件系统缓存,以便快速搜索。一般来说,应该至少确保物理上有一半的可用内存分配到文件系统缓存。

 

八、Golang 操作 elasticsearch

Golang操作ElasticSearch主要有以下两个SDK:

以上两个SDK都是在github上的开源项目,经过对两个项目wiki的阅读发现:

从以上两点来看,选择github.com/olivere/elastic 确实是更好的选择。主要原因有两点:

  • 支持版本多,文档丰富,版本更新及时

  • 使用频次高,出现问题易于排查解决

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

脑图附件:

ElasticSearch技术应用及性能优化.xmind

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值