ElasticSearch 写数据过程
Elasticsearch 写入数据的过程涉及客户端请求、路由、节点缓冲区、分片处理和段文件管理。以下是详细的步骤及涉及的核心概念:
1. 客户端请求:发送数据写入
- 客户端通过 REST API 或 Elasticsearch 客户端发送数据写入请求。
- 写入请求会指定:
- 索引名称:决定目标索引。
- 文档 ID(可选):若不提供,ES 会自动生成。
- 文档内容:需要存储的数据。
2. 路由与主分片定位
- 路由计算:
- Elasticsearch 根据文档的 ID 和索引的
number_of_shards配置,计算目标主分片 - 默认情况下,
routing_key是文档的 ID,但可以自定义。 - 通过路由,ES 确定该文档应写入到哪个主分片。
- Elasticsearch 根据文档的 ID 和索引的
- 主分片的节点定位:
- 主分片的位置由分片分配表决定,查询后可定位主分片所在节点。
- 将写入请求发送到主分片所在节点。
3. 写入数据到缓冲区
- 主节点的处理:
- 主分片所在的节点接收数据,写入到 节点级别的内存缓冲区(Indexing Buffer) 中,同时记录到 事务日志(Translog) 以保证数据可靠性。
- 缓冲区与事务日志(Translog):
- 缓冲区:用于暂存写入的数据,数据存储在内存中。
- 事务日志:持久化在磁盘上,确保即使节点宕机,数据也不会丢失。
- 缓冲区是节点级别的,多个分片共享;而 Translog 是分片级别的,每个分片有自己的日志文件。
- 数据同步到副本分片:
- 主分片将写入请求异步复制到其副本分片所在的节点。
- 副本分片按照类似主分片的方式处理写入,确保数据一致性。
4. 刷新(Refresh):生成 Lucene 段
- 定期刷新:
- 默认每秒触发一次刷新(由配置参数
refresh_interval控制),将缓冲区的数据写入磁盘,生成新的 Lucene 段(Segment)。
- 默认每秒触发一次刷新(由配置参数
- 段的特点:
- 段是不可变的。每次刷新后,新增数据被写入到新的段文件中。
- 刷新后,新的数据可以被搜索,但写入性能会受到刷新频率的影响。
- 更新索引结构:
- Lucene 段包含倒排索引等数据结构。刷新时,段文件被更新,允许查询实时访问最新数据。
5. 合并(Merge):优化段文件
- 段合并:
- 随着数据写入增多,段文件数量增加,Elasticsearch 会在后台触发段合并,将小段文件合并成更大的段。
- 合并可以减少段数量,降低查询开销,但会占用 I/O 资源。
- 删除标记清理:
- 删除文档不会立即从磁盘中移除,而是通过标记的方式隐藏。
- 合并时,会清理已标记为删除的文档。
创建索引
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: 5s,info: 3s,debug: 2s,trace: 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"
}
交互流程:
- 计算路由键的哈希值:
- Elasticsearch 使用哈希函数(如 MurmurHash)对
routing key进行哈希运算,生成一个固定的整数值。
- Elasticsearch 使用哈希函数(如 MurmurHash)对
- 映射到逻辑路由分片(number_of_routing_shards):
- 将哈希值映射到逻辑分片空间,通过
mod number_of_routing_shards限制结果到逻辑分片范围。
- 将哈希值映射到逻辑分片空间,通过
- 映射到实际主分片(number_of_shards):
- 将逻辑分片结果再次取模
number_of_shards,决定实际存储的主分片。
- 将逻辑分片结果再次取模
实际例子
场景 1:固定主分片数,没有路由分片
假设你有 3 个主分片和以下数据:
| 数据 | hash(routing_key) | 主分片 (hash % 3) |
|---|---|---|
user1 | 1 | 1 |
user4 | 4 | 1 |
user6 | 6 | 0 |
数据 user1 和 user4 都写入主分片 1,user6 写入主分片 0。
问题:如果主分片数增加到 6
新的路由公式会是 hash % 6:
| 数据 | hash(routing_key) | 主分片 (hash % 6) |
|---|---|---|
user1 | 1 | 1 |
user4 | 4 | 4 |
user6 | 6 | 0 |
此时,user4 从主分片 1 路由到了主分片 4,造成数据位置改变,必须重新迁移数据,开销很大。
场景 2:引入路由分片
假设你仍然有 3 个主分片,但设置了 路由分片数为 6,初始路由逻辑如下:
| 数据 | hash(routing_key) | 路由分片 | 主分片 (routing_shard % 3) |
|---|---|---|---|
user1 | 1 | 1 | 1 |
user4 | 4 | 4 | 1 |
user6 | 6 | 0 | 0 |
此时,user1 和 user4 仍然写入主分片 1。
主分片扩展到 6 后的逻辑:
- 路由分片数不变,仍为 6。
- 计算主分片的公式变为
routing_shard % 6:
| 数据 | hash(routing_key) | 路由分片 | 主分片 (routing_shard % 6) |
|---|---|---|---|
user1 | 1 | 1 | 1 |
user4 | 4 | 4 | 4 |
user6 | 6 | 0 | 0 |
路由行为:
- 数据
user1和user4的routing_shard位置没有改变。 - 扩展后的主分片只需要分配与原路由分片相匹配的数据,大大减少了数据迁移。