好的,我们来详细拆解一下从客户端发送数据到Elasticsearch(ES)并最终写入成功的完整过程。
这个过程可以分为两个主要阶段:
1. **数据写入阶段**:让数据变得“安全”和“持久化”,并向客户端确认写入成功。
2. **数据可搜索阶段**:让刚刚写入的数据能够被搜索到。
让我们一步一步来看。
### 整体流程概览
上图是一个简化的流程图,下面是详细的文字步骤分解。
---
### 第一阶段:数据写入与持久化 (Write & Persist)
这个阶段的目标是快速、安全地将数据写入,并向客户端返回成功响应。
#### 第1步:客户端发送请求
你(客户端)构造一个索引请求,通常是一个 HTTP `POST` 或 `PUT` 请求。
例如,向 `my-index` 索引中写入一个 ID 为 `1` 的文档:
```bash
POST /my-index/_doc/1
{
"user": "kimchy",
"post_date": "2024-01-01T12:00:00",
"message": "trying out Elasticsearch"
}
```
这个请求会被发送到 Elasticsearch 集群中的 **任意一个节点**。
#### 第2步:协调节点(Coordinating Node)接收请求
集群中接收到这个请求的节点被称为 **协调节点**(Coordinating Node)。任何节点都可以扮演这个角色。
协调节点的主要职责是:
1. **确定数据应该去哪里**:它需要计算出这个文档应该被存储到哪个**主分片(Primary Shard)**上。
2. **路由请求**:将请求转发给持有该主分片的节点。
**路由算法:**
协调节点通过以下公式来确定目标分片:
```
shard = hash(routing_value) % num_primary_shards
```
- `routing_value`:默认情况下是文档的 `_id`(在这个例子中是 `"1"`)。你也可以在写入时手动指定一个 routing 值。
- `num_primary_shards`:索引的主分片数量。
这个公式确保了具有相同 `_id`(或相同 routing 值)的文档总是被路由到同一个分片,这对于后续的读取和更新操作至关重要。
#### 第3步:请求被路由到主分片所在的节点
协调节点根据计算结果,在集群状态(Cluster State)中查找哪个节点上存有目标主分片(比如,分片0在Node-A上),然后将原始请求转发给那个节点(Node-A)。
#### 第4步:主分片(Primary Shard)处理写入操作
现在,请求到达了真正负责写入的节点。主分片会执行以下关键操作:
1. **验证文档**:检查文档结构是否符合索引的映射(Mapping)。如果字段类型不匹配,可能会报错。
2. **写入内存缓冲区(In-memory Buffer)**:将文档数据写入一个内存中的缓冲区。这个操作非常快,因为是在内存中完成的。
3. **写入事务日志(Transaction Log / Translog)**:**同时**,将这次写入操作追加(append)到事务日志(Translog)文件中。Translog 是一个持久化在磁盘上的文件,类似于数据库的 redo log。
**为什么需要这两步?**
- **内存缓冲区**:为了提高写入性能。批量写入内存远比每次都直接写磁盘文件快。
- **Translog**:为了保证数据安全。如果在数据从内存缓冲区刷新(flush)到磁盘之前,节点发生崩溃或断电,重启后ES可以通过重放(replay)Translog 中的操作来恢复数据,确保数据不丢失。
**此时,客户端的写入请求在主分片上已经“安全”了。**
#### 第5步:并行将请求复制到副本分片(Replica Shards)
主分片完成写入后,会**并行地**将相同的写入请求转发给它所有的副本分片(Replica Shards)。
#### 第6步:副本分片处理写入操作
每个副本分片会执行与主分片几乎相同的操作:
1. 验证文档(通常副本会信任主分片的验证结果)。
2. 将数据写入自己的**内存缓冲区**。
3. 将操作记录追加到自己的**Translog**。
4. 完成后,向主分片发送一个“成功”确认。
#### 第7步:主分片等待确认并响应协调节点
主分片会等待副本分片的确认。默认情况下,它需要收到**法定数量(Quorum)**的副本确认后,才认为写入操作是真正成功的。
这个数量由 `index.write.wait_for_active_shards` 参数控制:
- `1` (默认值): 只要主分片写入成功即可,不等待任何副本。
- `all`: 必须所有副本(主分片+所有副本分片)都写入成功。
- 一个具体的数字 `N`: 至少有 `N` 个分片(包括主分片)处于活动状态并成功写入。
当主分片收到了足够数量的确认后,它会向**协调节点**发送一个“成功”响应。
#### 第8步:协调节点响应客户端
协调节点收到来自主分片的成功响应后,最后向客户端返回一个 HTTP `200 OK` 或 `201 Created` 的成功消息。
**至此,从客户端的角度看,数据写入已经成功并完成了!**
---
### 第二阶段:数据可搜索 (Making Data Searchable)
虽然客户端收到了成功响应,并且数据已经通过 Translog 持久化,但此时数据还**不能被搜索到**。因为它还在内存缓冲区里,并没有被构建成可供检索引擎(Lucene)使用的**段(Segment)**。
这个过程由两个独立的后台机制完成:**Refresh** 和 **Flush**。
#### Refresh(刷新)
- **作用**:将内存缓冲区中的文档写入到一个新的**内存中的 Lucene 段(in-memory segment)**中,并使其可以被搜索。
- **过程**:
1. 内存缓冲区里的数据被清空。
2. 这些数据被用来构建一个新的、小的、不可变的段结构。
3. 这个新段被打开,可用于搜索。
- **触发时机**:
- 默认每隔 `1` 秒自动执行一次(由 `index.refresh_interval` 控制)。这就是为什么 Elasticsearch 被称为**近实时(Near Real-Time, NRT)**搜索的原因——从写入到可被搜索有秒级的延迟。
- 可以手动触发:`POST /my-index/_refresh`。
- **结果**:数据变得**可搜索**了,但这个新段仍在内存中,其持久化依赖于操作系统的文件系统缓存。
#### Flush(刷盘)
- **作用**:将内存中的段(Segments)真正地、物理地写入磁盘,并清空事务日志(Translog)。这是一个更重的操作,确保数据的完全持久化。
- **过程**:
1. 执行一次 Refresh,将内存缓冲区里的剩余数据也变成一个段。
2. 调用 `fsync`,将所有内存中的 Lucene 段(包括操作系统缓存中的)强制写入到磁盘。
3. 创建一个新的、空的 Translog,旧的 Translog 文件可以被安全删除了(因为里面的所有操作都已持久化到段文件中)。
- **触发时机**:
- 当 Translog 文件变得太大时(由 `index.translog.flush_threshold_size` 控制,默认512MB)。
- 定期执行(由 `index.translog.sync_interval` 控制,但默认不启用时间触发)。
- 手动触发:`POST /my-index/_flush`。
- **结果**:数据被永久地保存在磁盘的段文件中,Translog 被清空,节点重启恢复速度更快(因为不再需要重放大的 Translog)。
### 总结
1. **客户端 -> 协调节点**:请求发送到任意节点。
2. **协调节点 -> 主分片**:协调节点计算路由,将请求转发给正确的主分片。
3. **主分片写入**:数据写入**内存缓冲区**和**事务日志(Translog)**,保证速度和数据安全。
4. **主分片 -> 副本分片**:请求被并行复制到所有副本。
5. **副本分片写入**:副本执行同样操作,写入内存和 Translog。
6. **确认与响应**:副本向主分片确认,主分片等够数量后向协调节点确认,最终协调节点向客户端返回成功。
7. **Refresh (近实时搜索)**:后台进程(默认每秒)将内存缓冲区数据变为**可搜索的段**。
8. **Flush (永久持久化)**:后台进程(或按需)将内存中的段**写入磁盘**,并清空 Translog。