ElasticSearch 写数据过程和正确创建索引

566 阅读8分钟

ElasticSearch 写数据过程

Elasticsearch 写入数据的过程涉及客户端请求、路由、节点缓冲区、分片处理和段文件管理。以下是详细的步骤及涉及的核心概念:

image.png


1. 客户端请求:发送数据写入

  1. 客户端通过 REST API 或 Elasticsearch 客户端发送数据写入请求。
  2. 写入请求会指定:
    • 索引名称:决定目标索引。
    • 文档 ID(可选):若不提供,ES 会自动生成。
    • 文档内容:需要存储的数据。

2. 路由与主分片定位

  1. 路由计算
    • Elasticsearch 根据文档的 ID 和索引的 number_of_shards 配置,计算目标主分片
    • 默认情况下,routing_key 是文档的 ID,但可以自定义。
    • 通过路由,ES 确定该文档应写入到哪个主分片。
  2. 主分片的节点定位
    • 主分片的位置由分片分配表决定,查询后可定位主分片所在节点。
    • 将写入请求发送到主分片所在节点。

3. 写入数据到缓冲区

  1. 主节点的处理
    • 主分片所在的节点接收数据,写入到 节点级别的内存缓冲区(Indexing Buffer) 中,同时记录到 事务日志(Translog) 以保证数据可靠性。
  2. 缓冲区与事务日志(Translog)
    • 缓冲区:用于暂存写入的数据,数据存储在内存中。
    • 事务日志:持久化在磁盘上,确保即使节点宕机,数据也不会丢失。
    • 缓冲区是节点级别的,多个分片共享;而 Translog 是分片级别的,每个分片有自己的日志文件。
  3. 数据同步到副本分片
    • 主分片将写入请求异步复制到其副本分片所在的节点。
    • 副本分片按照类似主分片的方式处理写入,确保数据一致性。

4. 刷新(Refresh):生成 Lucene 段

  1. 定期刷新
    • 默认每秒触发一次刷新(由配置参数 refresh_interval 控制),将缓冲区的数据写入磁盘,生成新的 Lucene 段(Segment)。
  2. 段的特点
    • 段是不可变的。每次刷新后,新增数据被写入到新的段文件中。
    • 刷新后,新的数据可以被搜索,但写入性能会受到刷新频率的影响。
  3. 更新索引结构
    • Lucene 段包含倒排索引等数据结构。刷新时,段文件被更新,允许查询实时访问最新数据。

5. 合并(Merge):优化段文件

  1. 段合并
    • 随着数据写入增多,段文件数量增加,Elasticsearch 会在后台触发段合并,将小段文件合并成更大的段。
    • 合并可以减少段数量,降低查询开销,但会占用 I/O 资源。
  2. 删除标记清理
    • 删除文档不会立即从磁盘中移除,而是通过标记的方式隐藏。
    • 合并时,会清理已标记为删除的文档。

创建索引

PUT atomcityproductbindheterogeneous
{
  "aliases": {
    "alias_atomcityproductbindheterogeneous": {},
    "alias_atomcityproductbindheterogeneous_write": {}
  },
  "mappings": {
    "_doc": {
      "dynamic": "strict",
      "properties": {
        "id": {
          "type": "long"
        },
        "productareas_id": {
          "type": "keyword"
        },
        "product_id": {
          "type": "keyword"
        },
        "code": {
          "type": "integer"
        },
        "name": {
          "type": "text",
          "analyzer": "standard"
        },
        "main_image": {
          "type": "keyword",
          "doc_values": false
        }
        "recommend_binding_city_product": {
          "type": "nested",
          "properties": {
            "id": {
              "type": "long"
            },
            "rivalcityproductnew_id": {
              "type": "long"
            },
            "product_matching_degree": {
              "type": "float"
            },
            "rival_type": {
              "type": "integer"
            },
            "rival_city_product_status_label": {
              "type": "integer"
            }
          }
        }
      }
    }
  },
  "settings": {
    "index": {
      "refresh_interval": "1s",
      "indexing": {
        "slowlog": {
          "threshold": {
            "index": {
              "warn": "5s",
              "trace": "1s",
              "debug": "2s",
              "info": "3s"
            }
          }
        }
      },
      "number_of_shards": "12",
      "number_of_routing_shards": "24",
"routing.allocation.require.box_type":"p2_rival",
      "translog": {
        "flush_threshold_size": "3gb",
        "durability": "async"
      },
      "merge": {
        "scheduler": {
          "max_thread_count": "1"
        }
      },
      "max_result_window": "1000000",
      "number_of_replicas": "1"
    }
  }
}

Settings参数说明

settings 配置了索引的运行参数,确保其性能、可靠性和适配特定需求:

索引刷新

  • refresh_interval: 1s:每秒刷新索引一次,使数据尽快可被搜索。

在每次刷新时,Elasticsearch 会生成新的 Lucene 段(segment),将缓冲区数据写入磁盘,并更新搜索引擎的索引结构,只有刷新后的数据才会对搜索操作可见。

慢日志配置

  • 定义了慢日志的阈值:
    • warn: 5sinfo: 3sdebug: 2strace: 1s
    • 这些日志阈值帮助运维人员定位性能瓶颈。

分片配置

  • number_of_shards: 12:主分片数为 12。
  • number_of_routing_shards: 12:设置路由分片数,用于优化分片再平衡。
  • routing.allocation.require.box_type: p2_rival:指定分片只分配到具有标签 p2_rival 的节点。

translog 事务日志配置

  • flush_threshold_size: 3gb
    • 含义:当事务日志的大小达到 3GB 时,Elasticsearch 会触发一次 强制刷盘(Flush) 操作。
    • 原因:事务日志记录了索引的每次变更,过大可能导致恢复时间过长或内存占用问题。通过设置这个阈值,可以在日志变得过大前刷盘,将缓冲区的数据写入磁盘。
  • durability: async
    • 含义:定义事务日志的持久性策略。
      • async 表示异步持久化,写入数据后不等待事务日志同步到磁盘即可返回成功。
      • request 表示同步持久化,写入数据后必须等待事务日志写入磁盘成功才返回。
    • 影响
      • async 提升了写入性能,但存在少量数据在节点崩溃时丢失的风险。
      • request 更安全,但性能较低,适合对数据丢失敏感的场景。

合并策略scheduler.max_thread_count

限制合并线程数为 1,控制系统资源使用。段合并是 Elasticsearch 在后台优化索引存储的一种机制。它会将小段合并成更大的段,以减少磁盘 I/O 和搜索时的开销。

  • scheduler.max_thread_count: 1
    • 含义:段合并线程池的最大线程数。
    • 作用:限制段合并操作的并发度,降低段合并对集群性能的影响。

结果窗口

  • max_result_window: 1000000:Elasticsearch 的分页方式基于跳过的文档量计算(from 参数值),如果分页深度过大,会导致大量数据被跳过但依然占用内存和 CPU 资源。如果你在查询中使用了from和size参数,例如:
{
  "from": 999900,
  "size": 100
}
  • 如果设置 from=999900,Elasticsearch 需要扫描和跳过前 999,900 条数据后才返回结果,这对系统开销非常大。

副本配置

  • number_of_replicas: 1:每个主分片有 1 个副本,确保数据高可用。

如何合理设置分片?

Elasticsearch 官方建议单个分片尽量不要超过 50GB,否则查询性能可能下降。若预计数据总量为 1TB,单分片目标大小为 40GB,则需要 25 个分片。

主分片和路由分片区别?

路由分片数是一个高级配置(他只是一个数字,如果我配置的就是"number_of_routing_shards": "24"),控制数据的哈希路由机制。它是数据分配逻辑的一个基准值,影响索引在执行 split(分裂)shrink(收缩) 时的灵活性。路由分片提供了一个更细粒度的逻辑分片机制,使得查询在不同物理分片间分布更加均匀。路由分片必须是主分片数的倍数,那为什么一定是倍数呢?哈希路由的空间是固定的,当进行分片扩展或收缩时,新的分片必须能均匀地划分原始路由空间。比如:

初始配置:

  • 主分片数:number_of_shards = 4
  • 路由分片数:number_of_routing_shards = 12
  • 分裂(split)索引到 6 个分片
    • 12 个路由分片可均匀划分给 6 个目标分片(每个目标分片负责 2 个路由分片)。
  • 分裂(split)索引到 8 个分片
    • 12 个路由分片无法均匀划分给 8 个目标分片,因为 12 ÷ 8 不是整数。

什么是 Routing Key(路由键)?

路由键是用于确定文档最终存储在哪个分片的关键值。在写入数据时,可以通过 REST API 提供自定义的路由键:


POST my_index/_doc?routing=user123
{
  "name": "Example Document"
}

交互流程:

  1. 计算路由键的哈希值
    • Elasticsearch 使用哈希函数(如 MurmurHash)对 routing key 进行哈希运算,生成一个固定的整数值。
  2. 映射到逻辑路由分片(number_of_routing_shards)
    • 将哈希值映射到逻辑分片空间,通过 mod number_of_routing_shards 限制结果到逻辑分片范围。
  3. 映射到实际主分片(number_of_shards)
    • 将逻辑分片结果再次取模 number_of_shards,决定实际存储的主分片。

实际例子

场景 1:固定主分片数,没有路由分片

假设你有 3 个主分片和以下数据:

数据hash(routing_key)主分片 (hash % 3)
user111
user441
user660

数据 user1user4 都写入主分片 1,user6 写入主分片 0。

问题:如果主分片数增加到 6

新的路由公式会是 hash % 6

数据hash(routing_key)主分片 (hash % 6)
user111
user444
user660

此时,user4 从主分片 1 路由到了主分片 4,造成数据位置改变,必须重新迁移数据,开销很大。


场景 2:引入路由分片

假设你仍然有 3 个主分片,但设置了 路由分片数为 6,初始路由逻辑如下:

数据hash(routing_key)路由分片主分片 (routing_shard % 3)
user1111
user4441
user6600

此时,user1user4 仍然写入主分片 1。

主分片扩展到 6 后的逻辑:

  • 路由分片数不变,仍为 6。
  • 计算主分片的公式变为 routing_shard % 6
数据hash(routing_key)路由分片主分片 (routing_shard % 6)
user1111
user4444
user6600

路由行为:

  1. 数据 user1user4routing_shard 位置没有改变。
  2. 扩展后的主分片只需要分配与原路由分片相匹配的数据,大大减少了数据迁移。