利用别名和重新索引实现Elasticsearch零停机迁移
背景与挑战
在生产环境中使用Elasticsearch时,经常会遇到需要调整字段映射(mapping)的情况。例如,某个字段最初被定义为text类型,但后续业务需求要求其支持时间范围查询,这就需要将其更改为date类型。然而,Elasticsearch不允许直接修改已创建的字段类型,因此必须通过重建索引来完成变更。
如果直接重建索引会导致服务中断或数据不可用,影响线上应用。为此,我们需要一种能够在不中断服务的前提下完成索引结构升级的方案——即"零停机重新索引"。
解决方案:结合别名与_reindex API
核心思路是:
- 创建一个具有正确映射的新索引;
- 将旧索引的数据复制到新索引中;
- 通过索引别名平滑切换流量,确保读写操作不受影响;
- 最终删除旧索引以释放资源。
具体实施步骤
1. 创建原始索引(含错误映射)
PUT /articles_old
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"id": { "type": "keyword" },
"title": { "type": "text" },
"readCounts": { "type": "integer" },
"publish_time": { "type": "text" }
}
}
}
插入测试数据:
PUT /articles_old/_doc/1
{
"id": "1",
"title": "Elasticsearch入门指南",
"readCounts": 45,
"publish_time": "2023-07-15T10:00:00"
}
2. 定义新索引结构(修正字段类型)
将publish_time改为date类型以便支持时间查询:
PUT /articles_new
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"id": { "type": "keyword" },
"title": { "type": "text" },
"readCounts": { "type": "integer" },
"publish_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss" }
}
}
}
3. 设置共享别名
为两个索引添加统一别名blog_index,便于后续切换:
POST /_aliases
{
"actions": [
{ "add": { "index": "articles_old", "alias": "blog_index" } },
{ "add": { "index": "articles_new", "alias": "blog_index" } }
]
}
4. 执行数据迁移
使用_reindex API 将旧索引中的所有文档迁移到新索引:
POST /_reindex
{
"source": {
"index": "articles_old"
},
"dest": {
"index": "articles_new"
}
}
5. 验证迁移结果
检查新索引是否包含正确转换后的数据:
GET /articles_new/_doc/1
响应应显示publish_time已被成功解析为日期格式。
6. 切换别名指向新索引
将别名从旧索引移除,并仅指向新索引,实现无缝切换:
POST /_aliases
{
"actions": [
{ "remove": { "index": "articles_old", "alias": "blog_index" } },
{ "add": { "index": "articles_new", "alias": "blog_index" } }
]
}
此时,所有对blog_index的请求都将路由至articles_new,而应用程序无需任何代码改动。
7. 清理旧资源
确认系统稳定运行后,可安全删除旧索引:
DELETE /articles_old
优势总结
- 无停机更新:通过别名机制保证服务连续性;
- 灵活回滚:若新索引异常,可快速切回原索引;
- 兼容性强:适用于字段类型变更、分词器调整等多种场景;
- 操作简单:依赖Elasticsearch原生API,无需额外工具。