# 配置

# index template

:索引模板。用于在创建索引时进行初始化配置。

  • 创建索引时,会根据索引模式匹配索引模板。如果存在多个匹配的索引模板,则采用优先级最高的那个。

    • 创建索引时,如果加上了配置信息,则会覆盖索引模板中的对应配置。
    • 创建、修改索引模板时,只会影响之后新创建的索引,不会影响已创建的索引。
  • 相关 API :

    GET     /_template                            # 查询所有的索引模板
    GET     /_template/<template>                 # 查询指定的索引模板
    PUT     /_template/<template>  <request_body> # 创建索引模板
    DELETE  /_template/<template>                 # 删除索引模板
    
  • 例:创建一个索引模板

    PUT /_template/my_template_1
    {
      "index_patterns": ["mysql-log-*", "nginx-log-*"], # 索引模式,用于匹配一些索引
      "aliases": {
        "test-alias-1": {}
      },
      "settings": {...},
      "mappings": {...},
      # "version" : 1,                  # 声明该索引模板的版本
      # "_meta": {                      # 配置该索引模板的元数据,可以加入任意名称的参数,并不会被 ES 使用
      #   "description": "This is a log template."
      # }
    }
    
    • 至少要声明 index_patterns ,其它配置则可以省略。
  • ES v7.8 版本引入了可组合的索引模板(composable index template),API 为 _index_template 。

    • 优点:可组合使用,更灵活。
    • 可设置 "priority": 100 参数,表示该模板的优先级。
      • 默认为 0 ,即优先级最低。
      • 如果一个索引匹配多个索引模板,则采用优先级最高的那个。
      • 如果一个索引同时匹配 _template 和 _index_template ,则优先采用 _index_template 。

# index pattern

:索引模式。一个用于匹配任意个索引名的字符串。

  • 可以是多种格式:
    my-index-1                      # 一个索引名
    my-index-1,my-index-2           # 多个索引名,用逗号分隔
    my-index-*                      # 可以使用通配符 * 匹配多个索引名
    my-index-*,-my-index-2          # 可以在索引名之前加上 - ,从而排除它
    

# alias

  • 每个索引可以创建一个或多个别名(alias)。
    • ES 的大多数 API 都支持输入索引的别名,会自动解析到原名。
    • 别名与索引名共享一个命名空间,不允许重名。
  • 相关 API :
    GET   /_alias                     # 查询所有索引的所有别名
    GET   /_alias/<alias>             # 加上一个 alias 名称进行筛选,支持使用通配符 *
    GET   /<index>/_alias             # 查询指定索引的所有别名
    GET   /<index>/_alias/<alias>
    
    HEAD  /_alias/<alias>             # 检查索引是否存在
    HEAD  /<index>/_alias/<alias>
    
    PUT   /<index>/_alias/<alias>     # 给索引创建别名
    
    DELETE  /<index>/_alias/<alias>   # 删除索引的别名
    

# mappings

:映射,用于定义某个 index 中存储的文档的数据结构、字段类型。

  • 定义方式分为两种:

    • explicit mapping
      • :显式映射。是在 properties 区块,根据字段名定义一些字段。
      • index 在创建之后,不能修改显示映射中已有的字段,只能增加字段。也可以创建一个拥有新 mappings 的新 index ,然后将旧 index 的文档通过 reindex 拷贝过去。
    • dynamic mapping
      • :动态映射。是在 dynamic_templates 区块,根据至少一个匹配条件,为还没有显式映射的字段添加显式映射。
  • 例:

    PUT test_log
    {
      "mappings": {
        "_source": {
          "enabled": true                     # 是否存储 _source 字段,默认为 true
        },
        "dynamic_templates" : [               # 动态映射
          {
            "integer_fields": {
              # "match" : "*",                # 匹配字段名
              # "path_match" : "test",        # 匹配字段的 JSON 路径
              "match_mapping_type": "long",   # 匹配字段值的 JSON 数据类型
              "mapping": {                    # 为匹配的字段生成显式映射
                "type": "integer"             # 该效果为:如果新增的文档中,任意字段满足匹配条件,则存储为 integer 数据类型
              }
            }
          },
          {
            "string_fields" : {
              "match_mapping_type" : "string",
              "mapping" : {
                "type" : "text"
              }
            }
          }
        ],
        "properties": {             # 显式映射
          "@timestamp" : {
            "type" : "date"
          },
          "pid" : {
            "type" : "long"
          },
          "level" : {
            "type" : "text"
          },
          "message": {
            "type": "text"
            # "index": true,        # 是否对该字段建立倒排索引,从而允许搜索该字段,默认为 true
            # "doc_values": true,   # 是否存储 doc_values ,默认为 true
            # "store": false,       # 是否存储该字段的原始值(称为 store_values ),默认为 false
            # "fielddata": false,   # 是否允许 aggregations、sorting、scripting 操作,默认为 false
            # "norms": true,        # 是否启用 norms
            # "ignore_malformed": false, # 新增一个文档时,如果某个字段的数据类型不符合 mappings ,则整个文档会写入失败。启用该配置,会依然写入文档,只是不对该字段建立索引
          }
        }
      }
    }
    
    • 将字符串类型的值存储到 ES 时,建议同时写入一个 text 类型的字段、一个 keyword 类型的子字段。如下:
      "message" : {               # 一个名为 message 的字段,类型为 text
        "type" : "text",
        "fields" : {              # 定义子字段
          "keyword" : {           # 一个名为 keyword 的子字段,类型为 keyword
            "type" : "keyword",
            "ignore_above" : 256  # 限制字段取值的长度,默认为无限大。如果超过,则保存该文档时不对该字段建立索引,因此搜索该字段时不会找到该文档
          }
        }
      }
      
    • norms :为每个字段额外存储一个字节,记录多种有利于计算 score 的调节因子(normalization factors)。
      • text 类型的字段默认启用 norms ,keyword 类型的字段默认关闭 norms 。
  • 当用户新增一个文档时,ES 会解析文档中每个字段的值。如果当前 index mappings 定义了该字段的值应该是 T 数据类型,则尝试将字段的原始值转换成 T 数据类型(即数据类型转换)。

    • 例如 index mappings 中定义了 message 字段为 text 类型,则 _source 中可包含 "message": 1"message": "hello" ,但不能接受 "message": {} ,因为它的值属于 object 类型。
    • 如果一个字段在 index mappings 中未定义 properties ,则采用 dynamic_templates 。如果 dynamic_templates 也不匹配该字段,则 ES 会自动判断数据类型,通常转换为 number、text 类型。
    • 如果一个文档的所有字段都解析成功,才能将该文档写入 ES 。
    • 如果一个文档的任意字段解析失败,则 ES 默认会拒绝写入该文档,并报错 HTTP 400 ,例如:document_parsing_exception , failed to parse field [message] of type [text] in document

# 数据类型

每个文档包含多个字段,字段名为字符串类型(区分大小写),而字段值的常见类型如下:

  • number :数字类型,细分为 integer、long、byte、double、float 等。
  • boolean :布尔值。
  • date :日期,可以是 UNIX 时间戳,或 ISO8601 格式的字符串。
  • array :数组,包含一组元素,且这些元素的数据类型相同。例如 [1, 2]["one", "two"]
  • 字符串 :分为两种:
    • text
      • 适合存储非结构化数据,比如一篇文章,然后进行模糊搜索。
      • 支持 search 操作,默认不支持 aggregations、sorting、scripting 操作,因为这需要构建 fielddata 缓存,占用大量 JVM 内存。
    • keyword
      • 适合存储结构化数据,比如文章标题、编号,然后进行精确搜索。
      • 支持 search、aggregations、sorting、scripting 操作。
  • object :一个 JSON 对象。
  • nested :用于嵌套一组文档。这会在当前文档之外,创建独立的 Lucene 文档。
  • join
    • :用于声明父子文档。
    • 父文档与子文档必须位于同一索引、同一 shard 。
    • 例:
      PUT test_index
      {
        "mappings": {
          "properties": {
            "test_join": {
              "type": "join",
              "relations": {
                "question": "answer"  # 定义一个 join 字段,声明 question 对 answer 的父子关系
              }
            }
          }
        }
      }
      
      PUT test_index/_doc/1
      {
        "text": "This is a question",
        "test_join": {
          "name": "question"          # 新增一个文档,join 名称为 question ,因此为父文档
        }
      }
      
      PUT test_index/_doc/2?routing=1 # 设置 routing 参数,将 HTTP 请求路由到 _id=1 的文档所在 shard
      {
        "text": "This is an answer",
        "test_join": {
          "name": "answer",           # 新增一个文档,join 名称为 answer ,因此为子文档
          "parent": "1"               # 父文档是 _id=1 的文档
        }
      }
      

# doc_values

  • 当用户新增一个文档时,ES 会存储以下内容到 Lucene 中:

    1. 默认会对文档的每个字段建立倒排索引,并存储,从而支持对任意字段执行 search 操作。

    2. 默认会将整个文档的原始 JSON 内容,作为内置字段 _source 的值存储。

      • _source 字段只是用于存储文档的原始内容。该字段没有建立索引,因此不支持搜索。
      • _source 在 Lucene 中存储为单个字段。因此读取 _source 时,只能读取一整个 _source 的内容,不能只读取 _source 的子字段。建议非必要不读取 _source 。
      • 如果 index mappings 中配置了 "_source": false ,不存储文档的 _source 字段,则可节省大量磁盘空间。此时可通过 doc_values、store_values 读取字段的原始值,例如:
        GET test_log/_search
        {
          "_source": false,
          "docvalue_fields": ["pid"]
        }
        
    3. 默认会将文档每个字段的原始值额外存储一份,采用列式存储,称为 doc_values ,从而支持对该字段执行 aggregations、sorting、scripting 操作。

      • text 类型的字段不支持存储 doc_values 。
      • number、boolean、date、keyword 等类型的字段默认会存储 doc_values ,因此可以不建立倒排索引,只存储 doc_values 用于查询,相当于从 ES 数据库变成了 Mongo 数据库。这样占用的磁盘存储空间减少,但是查询速度变慢。
    4. 如果启用了 store_values ,则将文档每个字段的原始值额外存储一份,采用行式存储。

      • 与 doc_values 相比,_source、store_values 只能用于读取字段的原始值,不支持 search、aggregations、sorting、scripting 操作。
      • 如果需要经常读取文档个别字段的原始值,则建议不读取整个 _source ,而是单独读取该字段的 doc_values 或 store_values 。
      • 如果需要经常读取文档多个字段的原始值,则建议读取整个 _source ,然后筛选出目标字段,这样效率更高。例如:
        GET test_log/_search
        {
          "_source": ["message"]
        }
        
  • 执行 aggregations、sorting、scripting 操作时,需要读取字段的原始值,因此用 doc_values 比倒排索引的效率更高。

    • 如果用倒排索引,则 ES 需要先从磁盘读取文档内容,然后在内存中建立从文档 _id 到 term 的正向索引,称为 fielddata 缓存。查询的文档越多,占用的 JVM 内存越多。
    • 如果用 doc_values ,则 ES 可直接从磁盘读取 doc_values 数据,此时使用文件系统的缓存,不占用 JVM 内存。

# 文本分析

  • 文本分析(Text analysis):ES 存储 text 类型的字段时,会先经过 analyzer 处理,再建立索引。从而方便模糊搜索。

    • 查询时,也先将查询字符串经过 analyzer 处理,再进行查询。
    • text 字段存储之后的字符内容、顺序可能变化,因此使用 term、regexp 查询时可能不匹配,应该用 match 查询。
  • 一个分析器(analyzer)包含多个组件:

    • character filter
      • :字符过滤器,用于添加、删除、转换某些字符。
    • tokenizer
      • :分词器,用于将字符串拆分成一个个单词,称为 token 。
      • 例如英语 "Hello World" 会拆分成 2 个单词,汉语 "你好啊" 会拆分成 3 个单词。
    • token filter
      • :用于添加、删除、转换某些 token 。
      • 例如 the、on、to 等单词通常不影响查询,可以删除。
  • ES 内置了多种 analyzer ,参考官方文档 (opens new window),例如:

    • standard :默认采用。根据大部分标点符号进行分词。
      • 还启用了 Lower Case Token Filter ,将 token 全部小写。
    • simple :根据非字母的的字符进行分词,还启用了 Lower Case Token Filter 。
    • whitespace :根据空白字符进行分词。
    • stop :与 simple 相似,但启用了 Stop token filter ,会删除 a、an、and、are、it、no、the 等冠词、介词、连词。
  • index 的 analyzer 配置示例:

    {
      "settings": {
        "analysis": {
          "analyzer": {
            "default": {            # 设置存储时的默认 analyzer
              "type": "standard"
            },
            "default_search": {     # 设置查询时的默认 analyzer
              "type": "standard"
            }
          }
        }
      },
      "mappings": {
        "properties": {
          "message": {
            "type":     "text",
            "analyzer": "standard"  # 设置指定字段的 analyzer
          }
        }
      }
    }
    
  • 可用 API /_analyze 测试分析一个字符串:

    POST /_analyze
    {
      "analyzer": "standard",
      "text": "Hello World! -_1"
    }
    

    结果如下:

    {
      "tokens": [
        {
          "token": "hello",
          "start_offset": 0,
          "end_offset": 5,
          "type": "<ALPHANUM>",
          "position": 0
        },
        {
          "token": "world",
          "start_offset": 6,
          "end_offset": 11,
          "type": "<ALPHANUM>",
          "position": 1
        },
        {
          "token": "_1",
          "start_offset": 14,
          "end_offset": 16,
          "type": "<NUM>",
          "position": 2
        }
      ]
    }
    

# settings

:用于控制索引的存储等操作。

  • settings 中的配置参数按修改类型分为两种:

    • dynamic :支持在运行的索引上配置。
    • static :只能在创建索引时配置,有的也支持在 closed 索引上配置。
      • index.number_of_replicas 属于动态配置,index.number_of_shards 属于静态配置。因此索引在创建之后,不能修改主分片数量。
  • 例:

    "settings": {
      "index": {
        "codec": "LZ4",                 # Lucene 存储 segments 时的压缩算法,默认为 LZ4 。设置为 best_compression 则会采用 DEFLATE 压缩算法,压缩率更高,但增加了读写文档的耗时
        "number_of_shards": 3,          # 主分片的数量,默认为 1 。该参数属于 static settings 。
        "number_of_replicas": 1,        # 每个主分片的副本数量,默认为 1 。设置为 0 则没有副本
        "auto_expand_replicas": "0-1",  # 根据 ES 节点数,自动调整 number_of_replicas ,最小为 0 ,最大为 1 。默认禁用该功能
        "refresh_interval" : "5s",      # 每隔一定时间自动刷新一次索引。默认为 1 s ,接近实时搜索。设置成 -1 则不自动刷新
        "max_result_window": 10000,     # 限制分页查询时 from + size 之和的最大值
        "mapping": {
          "total_fields.limit": 1000,       # 限制索引包含的字段数
          "depth.limit": 50,                # 限制字段的深度
          "field_name_length.limit": 1000,  # 限制字段名的长度,默认不限制
          "ignore_malformed": false         # 是否所有字段都允许写入错误的数据类型
        }
        "blocks": {                         # 用于禁止对索引的一些操作
          "read": true,                     # 禁止读文档
          "write": true,                    # 禁止写文档
          "metadata": true,                 # 禁止读写元数据
          "read_only": true,                # 禁止写文档、元数据,也不能删除索引
          "read_only_allow_delete": true,   # 在 read_only 的基础上,允许删除索引(但依然不允许删除文档,因为这样并不会释放磁盘空间)
        }
      }
    }
    
  • ES 默认未开启慢日志,可以给某个索引,或所有索引添加慢日志配置:

    PUT /_all/_settings
    {
      "index": {
        "search": {
          "slowlog": {
            "threshold": {
              "fetch": {
                "warn": "10s"     # 如果 search 请求在 fetch 阶段的耗时超过该阈值,则打印一条 warn 级别的日志到 stdout
                "info": "5s",
                "debug": "100ms",
                "trace": null,    # 赋值为 null 即可关闭该日志
              },
              "query": {
                "warn": "10s"
                "info": "5s",
                "debug": "100ms",
                "trace": null,
              }
            }
          }
        },
        "indexing": {
          "slowlog": {
            "threshold": {
              "index": {
                "warn": "10s"
                "info": "5s",
                "debug": "100ms",
                "trace": null,
              }
            }
          }
        }
      }
    }
    

# component template

:组件模板。可以被索引模板继承,从而实现更抽象化的模板。

  • 例:创建一个组件模板
    PUT /_component_template/my_component_template
    {
      "template": {
        "mappings": {
          "properties": {
            "@timestamp": {
              "type": "date"
            }
          }
        }
      }
    }
    

# shard

  • ES 的 shard 根据用途分为两种:

    • 主分片(primary shard)
      • :每个索引可以划分 1 个或多个主分片,用于分散存储该索引的所有文档。
    • 副分片(replica shard)
      • :每个主分片可以创建 0 个或多个数据副本,称为副分片。
      • 主分片能处理用户的读、写请求。而副分片只支持读请求,不支持写请求。
  • 关于 shard 数量。

    • 一个索引不应该划分太多主分片。因为:
      • 每个分片都要存储一份元数据到内存中。查询文档时,合并所有 shard 的查询结果也有耗时。
      • 如果主分片数大于节点数,则不能发挥并行查询的优势。
      • 建议给每个索引默认划分 1 个主分片。如果数据量大,则增加节点数、划分更多分片。
    • 单个分片的体积越大,会增加查询耗时、故障后恢复耗时。
      • 建议将单个分片的体积控制在 10G~50G 。
      • 如果要存储海量数据,比如日志,建议每天创建一个新索引用于存储数据,比如 nginx-log-2020.01.01 。
    • ES 默认配置了 cluster.max_shards_per_node=1000 ,表示每个数据节点最多打开的 shard 数量(包括主分片、副分片、unassigned_shards,不包括 closed 索引的分片)。
      • 整个 ES 集群最多打开的 shard 数量为 max_shards_per_node * data_node_count ,这也限制了 open 状态的 index 总数。

# 分配

  • ES 集群中,master 节点会决定将每个 shard 分配到哪个 ES 节点进行存储。

    • 如果一个索引划分了多个主分片,则会尽量存储到不同节点,从而分散 IO 负载、实现并发查询。
      • 如果主分片数大于节点数,则一些主分片会存储到同一个节点,它们会竞争 CPU、内存、磁盘 IO 等资源,不能发挥并行查询的优势。
    • 如果一个主分片创建了副分片,则必须存储到与主分片不同的节点,否则不能实现灾备,失去了副分片的存在价值。
      • 如果主分片之外没有其它可用节点,则副分片会一直处于未分配(unassigned)状态,导致该索引的状态为 yellow 。
  • 默认启用了自动分配。配置如下:

    PUT  /_cluster/settings
    {
      "transient": {
        "cluster.routing.allocation.enable": "all"
      }
    }
    
    • enable 可以取值为:
      • all :自动分配所有分片。
      • primaries :只分配主分片。
      • new_primaries :只分配新索引的主分片。
      • none :禁用自动分配。
    • 改变索引、分片、节点数量时,都可能触发 shard 的自动分配。
  • 默认启用了基于磁盘的分配规则。配置如下:

    "cluster.routing.allocation.disk.threshold_enabled": true       # 是否启用基于磁盘阈值的分配规则
    "cluster.routing.allocation.disk.watermark.low": "85%"          # 低阈值。超出时,不会将新的副分片分配到该节点
    "cluster.routing.allocation.disk.watermark.high": "90%"         # 高阈值。超出时,会尝试将该节点上的分片迁移到其它节点
    "cluster.routing.allocation.disk.watermark.flood_stage": "95%"  # 洪水位(危险阈值)。超出时,会找出该节点上存储的所有分片,将它们所属的 index 开启 read_only_allow_delete
    "cluster.info.update.interval": "30s"                           # 每隔多久检查一次各节点的磁盘使用率
    
    • 这里可以设置磁盘使用率的一组阈值。超出阈值时会自动开启限制,低于阈值时会自动解除限制。
    • 阈值可以设置为磁盘的剩余大小,比如 "100gb" 。
  • 可以添加基于过滤器的分配规则。配置如下:

    "cluster.routing.allocation.exclude._name": "node1,node2"       # 禁止分配分片到指定节点上,已有的分片也会被迁移走。可以指定 _ip、_name 等
    
  • 默认会自动均衡各个节点上存储的分片数量。配置如下:

    "cluster.routing.rebalance.enable": "all"                           # 对哪些类型的分片进行均衡。all 表示主分片+副分片
    "cluster.routing.allocation.allow_rebalance": "indices_all_active"  # 什么时候开始均衡。默认为主发片+副分片都已经被分配时
    
    • rebelance 均衡会遵守其它分配规则,因此不一定完全均衡。
    • 建议将一个索引的不同主分片分配到不同节点,从而分散 IO 负载,也有利于并发查询。
  • 可以手动迁移 shard 。示例:

    POST /_cluster/reroute
    {
      "commands": [
        {
          "move": {
            "index": "test1",
            "shard": 0,
            "from_node": "node1",
            "to_node": "node2"
          }
        }
      ]
    }
    
    • 上例是将 test1 索引的 0 号主分片,从 node1 迁移到 node2 。
    • 目标节点上不能存在该分片的副分片,否则不允许迁移。
    • 迁移时,建议先禁用自动分配、自动均衡:
      "cluster.routing.allocation.enable": "none"
      "cluster.routing.rebalance.enable": "none"
      

# _split

  • API 格式: POST /<index>/_split/<target-index>
  • 用途:将一个索引分割为拥有更多主分片的新索引
  • 例:
    POST /my-index-1/_split/my-index-2
    {
      "settings": {
        "index.number_of_shards": 2
      }
    }
    
  • 分割索引的前提条件:
    • 源索引的 health 为 green 。
    • 源索引改为只读模式。可执行以下请求:
      PUT /my-index-1/_settings
      {
        "settings": {
          "index.blocks.write": true    # 禁止写操作,但允许删除
        }
      }
      
    • 目标索引不存在。
    • 目标索引的主分片数,是源索引的主分片数,的整数倍,比如 1 倍、2 倍。使得源索引的每个主分片,都可以平均拆分成多个目标索引的主分片。
  • 分割索引的工作流程:
    1. 创建目标索引,继承源索引的配置,但主分片数更多。
    2. 将源索引的数据通过硬链接或拷贝,迁移到目标索引。
    3. 允许目标索引被客户端读写。

# _shrink

  • API 格式: POST /<index>/_shrink/<target-index>
  • 用途:将一个索引收缩为拥有更少主分片的新索引。
  • 例:
    POST /my-index-1/_shrink/my-index-2
    {
      "settings": {
        "index.number_of_shards": 1
      }
    }
    
  • 收缩索引的前提条件:
    • 源索引的 health 为 green 。
    • 源索引为只读,并且所有主分片位于同一个节点上。可使用以下配置:
      PUT /my-index-1/_settings
      {
        "settings": {
          "index.number_of_replicas": 0,                        # 将主分片的副分片数量改为 0 ,方便迁移
          "index.routing.allocation.require._name": "node-1",   # 将主分片全部移到一个节点上
          "index.blocks.write": true
        }
      }
      
    • 目标索引不存在。
    • 源索引的主分片数,是目标索引的主分片数,的整数倍。

# segment

# 相关 API

  • 相关 API :

    POST  /<index>/_refresh       # 触发一次索引的 Refresh
    POST  /<index>/_flush         # 触发一次索引的 Flush
    
    POST  /_forcemerge                                    # 执行一次 segment 的强制合并
    POST  /<index>/_forcemerge?                           # 将指定的索引强制合并
                              only_expunge_deletes=true   # 只合并文档删除率超过 expunge_deletes_allowed 的 segment
                              max_num_segments=1          # 将每个 shard 中的 segment 合并到只剩几个,不管文档删除率
    GET   /_tasks?detailed=true&actions=*forcemerge       # 查看正在执行的 forcemerge 任务
    
    • 删除一个文档之后,如果没有 Refresh ,则依然可以查询到该文档,并且再次请求删除该文档时会报错:version conflict, current version [2] is different than the one provided [1]
    • 发出 forcemerge 请求时:
      • 会阻塞客户端,直到合并完成才返回响应。
      • 如果客户端断开连接,也会继续执行合并任务。
      • 如果服务器正在对该 index 执行 delete 任务或 forcemerge 任务,则会忽略客户端发出的合并请求。
      • 如果合并请求没有加上 URL 参数,则也是按照自动合并的算法,只处理适合合并的 segment 。
  • 请求 GET /_cat/segments/test_segments?v 的结果示例:

    index         shard prirep ip         segment   generation  docs.count  docs.deleted    size size.memory committed searchable version compound
    test_segments 0     p      10.0.0.1   _x                33        6665             0  15.4mb       74939 true      true       7.4.0   true
    test_segments 0     p      10.0.0.1   _12               38       13000             0    28mb       97976 true      true       7.4.0   true
    test_segments 0     p      10.0.0.1   _14               40       39416             0  78.6mb      178092 true      true       7.4.0   true
    

    表中每行描述一个 segment 的信息,各列的含义如下:

    index           # 该 segment 所属的索引
    shard           # 所属的分片
    prirep          # 分片类型,为 primary 或 replica
    ip              # 分片所在主机的 IP
    segment         # segment 在当前 shard 中的 36 进制编号,从 _0 开始递增。新增的文档会存储在编号最大的 segment 中,但合并之后,文档所在的 segment 可能变化
    generation      # generation 的十进制编号,从 0 开始递增
    
    docs.count      # 可见的文档数。不包括 deleted 文档、尚未 refresh 的文档、副分片的文档
    docs.deleted    # deleted 文档数
    size            # 占用的磁盘空间,单位 bytes
    size.memory     # 占用的内存空间,单位 bytes
    committed       # 该 segment 是否已经提交到磁盘。取值为 false 则说明正在使用 translog
    searchable      # 该 segment 是否允许搜索
    
    version         # 存储该 segment 的 Lucene 版本
    compound        # 该 segment 的所有文件是否已经合并为一个文件。这样能减少打开的文件描述符,但是会占用更多内存,适合体积小于 100M 的 segment
    
    • docs.count + docs.deleted 等于实际存储的文档总数。
    • docs.deleted / (docs.count + docs.deleted) 等于文档删除率。

# 配置

  • index 中可添加关于 Lucene segment 的配置:
    PUT /my-index-1
    {
      "settings": {
        "index": {
          "translog": {
            "durability": "request",            # 当客户端发出写请求时,要等到将 translog 通过 fsync 写入磁盘之后,才返回响应
            "sync_interval": "5s",              # 每隔多久执行一次 fsync (不考虑是否有客户端请求)
            "flush_threshold_size": "512mb",    # 当 translog 超过该大小时,就执行一次 Flush
          }
          "merge": {
            "policy": {
              "expunge_deletes_allowed": 10,    # 调用 expungeDeletes 时,只合并文档删除率超过该值的 segment ,默认为 10%
              "floor_segment": "2mb",           # 自动合并时,总是合并小于该体积的 segment ,不管文档删除率。这样能避免存在大量体积过小的 segment
              "max_merged_segment": "5gb",      # 自动合并时,限制产生的 segment 的最大大小
              "max_merge_at_once": 10,          # 自动合并时,每次最多同时合并多少个 segment
              "max_merge_at_once_explicit": 30, # forcemerge 或 expunge_delete 时,每次最多同时合并多少个 segment
              "segments_per_tier": 10,          # 每层最多存在的 segment 数,如果超过该值则触发一次自动合并
            }
          }
        }
      }
    }
    
  • 对于 max_merged_segment :
    • 如果 n 个 segment 预计的合并后体积低于 max_merged_segment ,则自动合并。否则,让 n-1 再预计合并后体积。
      • 即使 n=1 ,但如果存在 deleted 文档,也可能被自动合并。
    • 如果某个 segment 的体积超过 max_merged_segment ,则可能一直不会被自动合并,除非其中的文档删除率足够大。
      • 此时可以通过 forcemerge 强制合并,或者通过 reindex 重新创建该索引。

# data stream

  • 用 ES 存储大量按时间排序的文档时(比如日志),通常需要按天切分索引。为了方便管理这些索引,ES 引入了一种称为 data stream 的对象。

  • data stream 会根据每天的日期,自动创建后端索引(backing index),这些索引默认按以下格式命名:

    .ds-<data-stream>-<yyyy.MM.dd>-<generation_id>
    

    例如:

    .ds-test_log-2020.01.01-000001
    .ds-test_log-2020.01.01-000002
    
  • data stream 会反向代理它创建的所有后端索引。

    • 用户不必知道后端索引的名称,只需读写 data stream 。
    • 当用户新增文档到 data stream 时,总是会写入最新一个后端索引。
      • 每个写入 data stream 的文档,必须包含一个 @timestamp 字段,采用 date 或 date_nanos 数据类型。
    • 当用户查询 data stream 中的文档时,会同时查询多个后端索引。
    • 用户不能通过 _id 直接读写 data stream 中的文档,只能使用 _search、_update_by_query、_delete_by_query 这些 API 。
  • 例:写入文档到一个 data stream

    POST /my-data-stream/_doc/
    {
      "@timestamp": "2020-03-08T11:06:07.000Z",
      "message": "Hello"
    }
    
  • 例:查询 data stream 中的文档

    GET /my-data-stream/_search
    {
      "query": {
        "match": {
          "message": "Hello"
        }
      }
    }
    

# ILM

:索引生命周期管理(Index lifecycle Management),是一些自动管理索引的策略,比如减少副本的数量、超过多少天就删除索引。

# 线程池

  • ES 的每个节点创建了多个线程池(thread pool),分别处理 get、index 等不同类型的请求。

  • 可通过 GET /_nodes/stats 查询各个节点的线程池状态,例如:

    "thread_pool": {
      "get": {                # 处理 get 请求的线程池
        "threads": 16,        # 线程总数
        "queue": 0,           # 等待被处理的请求数
        "active": 0,          # 正在处理请求的线程数
        "rejected": 0,        # 已拒绝的请求数,ES 重启之后会重新计数
        "largest": 16,        # 最大的 active 数
        "completed": 1580335  # 已完成的请求数,ES 重启之后会重新计数
      },
      "search": {
        "threads": 25,
        "queue": 0,
        "active": 1,
        "rejected": 0,
        "largest": 25,
        "completed": 3340013239
      },
      ...}
    
    • 也可通过 GET /_cat/thread_pool?v&h=host,name,size,active,queue_size,queue,rejected,largest,completed 查询。
    • 如果 active 的线程数达到线程池上限,即没有空闲的线程,则新增的请求会被放到 queue 缓冲队列,等待处理。
    • 如果 queue 中的请求数达到队列上限,则新增的请求会被拒绝(rejected),对客户端报错 HTTP 429 。
    • ES 集群的 queue 总量等于 node_count*queue_size 。
  • 用户可在 elasticsearch.yml 文件中修改线程池的配置参数,例如:

    node.processors: 2      # 当前主机可用的 CPU 核数,默认为全部核数
    
    thread_pool:
      write:
        # type: fixed       # 线程池大小的类型,fixed 是固定数量,scaling 是自动调整数量
        # size: 10          # 线程池大小
        queue_size: 1000    # 队列大小,取值为 -1 则不限制
    
    • 如果 ES 经常发生 rejected 报错,建议采用默认的线程池 size ,只是增加 queue_size ,或者增加节点数量、增加 CPU 核数。
  • 几个线程池的默认容量:

    线程池 处理操作 size queue_size
    search count、search、suggest CPU_count*3/2+1 1000
    get get CPU_count 1000
    write index、delete、update、bulk CPU_count 1000
    force_merge force_merge 1 -1

# 相关 API

# _cat

  • ES 返回的响应内容默认采用 JSON 格式,方便程序处理。而使用 _cat API 会按列表形式返回一些统计信息,称为紧凑的文本信息(compact and aligned text),更适合供人阅读。
  • 例:
    GET  /_cat/nodes                # 查询节点的信息
    GET  /_cat/indices[/<index>]    # 查询索引的信息
    GET  /_cat/shards[/<index>]     # 查询分片的信息
    GET  /_cat/segments[/<index>]   # 查询索引段的信息
    GET  /_cat/templates            # 查询索引模板的信息
    
    • _cat 支持在 URL 中加入以下请求参数:
      ?v                      # verbose ,增加显示一行表头
      ?help                   # 显示所有可用的列名及其含义
      ?v&h=ip,disk*           # headers ,只显示指定的列。支持使用通配符
      ?sort=index,docs.count  # 按一个或多个字段升序排列
      ?sort=index:desc        # 降序排列
      

# _tasks

  • _tasks API 用于管理 ES 集群中执行的任务。
  • 例:
    GET   /_tasks                       # 查询各个节点上正在执行的任务
    GET   /_tasks?detailed=true         # 查询任务的详细信息(比如显示 _search 请求的 query 条件)
    GET   /_tasks?nodes=node1,node2     # 查询指定节点上执行的任务
    GET   /_tasks?wait_for_completion=true&timeout=10s  # 等待任务执行完,才返回响应
    
    POST  /_tasks/<task_name>/_cancel         # 取消任务
    POST  /_tasks/_cancel?nodes=node1,node2   # 取消任务
    

# _reindex

  • _reindex API 用于将源 index 中的文档拷贝到目标 index 中。

    • 源 index 可以是位于当前 ES ,也可以是位于其它 ES 。
    • 拷贝时,不会拷贝 settings、mappings ,因此需要实现创建 dest index 或 index template 。
  • 例:

    POST /_reindex
    {
      "source": {
        "index": "index_1",
        # "query": {            # 可以加上 query 子句,只拷贝部分文档
        #   "match": {
        #     "test": "data"
        #   }
        # },
        # "_source": ["user.id", "_doc"]  # 指定要拷贝的字段。默认为 true ,拷贝全部字段
        # "max_docs": 1000,     # 限制拷贝的文档数,默认不限制
      },
      "dest": {
        "index": "index_2",
        # "op_type": "create"
      },
      # "conflicts": "proceed"  # 拷贝时,如果遇到文档已存在的 version conflict 报错,默认会停止 reindex 。如果设置该参数,则会继续执行 reindex ,只是记录冲突的次数
    }
    
  • 例:从其它 ES 拷贝文档到当前 ES

    1. 在 elasticsearch.yml 中添加允许 reindex 的远程 ES 白名单:
      reindex.remote.whitelist: "10.0.0.2:9200,10.0.1.*:*"   # 可以填多个服务器地址,不需要加 http:// 前缀
      
    2. 调用 reindex API :
      POST /_reindex
      {
        "source": {
          "remote": {                       # 要连接的远程 ES
            "host": "http://10.0.0.2:9200", # ES 的地址必须加协议前缀,且声明端口号
            "username": "test",
            "password": "******"
          },
          "index": "index_1",               # 要拷贝的源 index 名称
        },
        "dest": {
          "index": "index_2"
        }
      }
      
    3. 查看 reindex 任务的进度:
      GET /_tasks?detailed=true&actions=*reindex
      

# _rollover

  • _rollover API 用于过渡,当满足指定条件时,将索引迁移到另一个索引。
  • 用法:
    POST /<rollover-target>/_rollover/<target-index>  <request_body>  # 给索引添加一个过渡规则
    
    • 过渡目标 target-index 可以是索引别名或数据流。如果省略该值,则将原索引名递增 1 ,作为目标名。
  • 例:
    POST /my-index-1/_rollover/my-index-2
    {
      "conditions": {                       # 这里指定了多个条件,只要满足其中之一即可
        "max_age":   "7d",                  # 索引创建之后的存在时长
        "max_docs":  1000,
        "max_size": "5gb"
      }
    }
    

# _snapshot

  • _snapshot API 用于将索引保存为快照文件,从而备份数据。

  • 用法:

    1. 在 elasticsearch.yml 中加入配置:
      path.repo: ["backups"]    # 声明存储备份仓库的目录
      
    2. 创建一个备份仓库:
      PUT _snapshot/backup_repo_1
      {
        "type": "fs",                             # fs 类型表示共享文件系统,需要挂载到所有 ES 节点
        "settings": {
          "location": "backup_repo_1",            # 指定该仓库的存储路径,这里使用相对路径,实际保存路径为 elasticsearch/backup/backup_repo_1
          # "max_snapshot_bytes_per_sec": "20mb", # 限制生成快照的速度
          #"max_restore_bytes_per_sec": "20mb"    # 限制导入快照的速度
        }
      }
      
    3. 创建快照:
      PUT /_snapshot/backup_repo_1/snapshot_1
      {
        "indices": "index_1,index_2"
      }
      
      • 此时会立即返回响应报文,然后在后台创建快照。
      • 如果已存在同名的 snapshot ,则会报错:Invalid snapshot name [snapshot_1], snapshot with the same name already exists
      • 生成的第一个快照文件是全量备份,而之后的快照文件是增量备份。
    4. 从快照中导入索引,从而恢复数据:
      PUT /_snapshot/backup_repo_1/snapshot_1
      {
        "indices": "index_1,index_2"
      }
      
  • 相关 API :

    PUT   /_snapshot/<repo>                     [request_body]  # 创建备份仓库
    PUT   /_snapshot/<repo>/<snapshot>          [request_body]  # 生成快照
    POST  /_snapshot/<repo>/<snapshot>/_restore [request_body]  # 导入快照
    
    POST    /_snapshot/<repo>/_cleanup          # 删除未被现有快照引用的过时数据
    DELETE  /_snapshot/<repo>/<snapshot>        # 删除快照
    
    GET   /_snapshot/_all                       # 查看所有备份仓库
    GET   /_snapshot/<repo>                     # 查看一个备份仓库的信息
    GET   /_snapshot/<repo>/_all                # 查看一个备份仓库下的所有快照
    GET   /_snapshot/<repo>/<snapshot>          # 查看指定快照的信息
    GET   /_snapshot/<repo>/<snapshot>/_status  # 增加显示快照的 size_in_bytes
    
    GET   /_recovery/                           # 查看所有创建、恢复快照的记录