一、为什么需要零停机迁移?
想象一下你正在给一栋大楼重新装修,但要求所有住户正常生活不受影响。Elasticsearch的索引重建也是类似的场景——业务不能停,数据不能丢,查询还得继续跑。传统的重建索引方式就像让所有住户搬出去装修,显然不现实。
常见痛点包括:
- 直接删除旧索引会导致服务中断
- 数据量大的情况下全量重建耗时可能超过维护窗口
- 业务系统需要频繁修改连接配置
// 技术栈:Elasticsearch 7.x Java API
// 错误示范:直接删除旧索引
DeleteIndexRequest request = new DeleteIndexRequest("old_index");
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
// 这会导致所有正在使用该索引的查询立即失败
二、双写方案:新旧索引并行运作
这个方案的核心思想是"两条腿走路"。就像在旧楼旁边先盖好新楼,等所有住户都搬过去了再拆旧楼。具体实现分为三个阶段:
- 创建新索引并配置相同的mapping
- 所有写操作同时发往新旧索引
- 数据同步完成后切换查询到新索引
// 技术栈:Elasticsearch 7.x Java API
// 创建结构相同的新索引
CreateIndexRequest createRequest = new CreateIndexRequest("new_index")
.mapping("{\"properties\":{\"title\":{\"type\":\"text\"}}}",
XContentType.JSON);
client.indices().create(createRequest, RequestOptions.DEFAULT);
// 双写示例
IndexRequest request1 = new IndexRequest("old_index")
.source(jsonMap, XContentType.JSON);
IndexRequest request2 = new IndexRequest("new_index")
.source(jsonMap, XContentType.JSON);
client.index(request1, RequestOptions.DEFAULT);
client.index(request2, RequestOptions.DEFAULT);
注意事项:
- 需要确保所有客户端都实现双写逻辑
- 建议使用消息队列暂存写请求,防止某次写入失败
- 监控两个索引的文档数量差异
三、别名切换:给索引戴上面具
别名就像给索引起的绰号,客户端永远访问这个绰号,我们只需要在后台更换绰号对应的真实索引。这是最优雅的方案之一,具体步骤:
- 给旧索引绑定业务别名(如products)
- 创建并预热新索引
- 原子操作将别名切换到新索引
// 技术栈:Elasticsearch 7.x Java API
// 初始设置别名
IndicesAliasesRequest aliasRequest = new IndicesAliasesRequest();
AliasActions aliasAction = new AliasActions(AliasActions.Type.ADD)
.index("old_index")
.alias("products");
aliasRequest.addAliasAction(aliasAction);
// 切换别名(原子操作)
IndicesAliasesRequest swapRequest = new IndicesAliasesRequest();
AliasActions removeAction = new AliasActions(AliasActions.Type.REMOVE)
.index("old_index")
.alias("products");
AliasActions addAction = new AliasActions(AliasActions.Type.ADD)
.index("new_index")
.alias("products");
swapRequest.addAliasAction(removeAction).addAliasAction(addAction);
client.indices().updateAliases(swapRequest, RequestOptions.DEFAULT);
优势:
- 客户端配置无需修改
- 切换是原子操作,没有中间状态
- 可以随时回滚到旧索引
四、滚动重建:化整为零的智慧
当索引特别大时,我们可以像吃披萨一样,一块一块来处理。具体实现方式:
- 使用reindex API分批次迁移数据
- 每次只处理部分文档
- 配合scroll API实现高效批量读取
// 技术栈:Elasticsearch 7.x Java API
// 分批reindex示例
ReindexRequest request = new ReindexRequest()
.setSourceIndices("old_index")
.setDestIndex("new_index")
.setSize(5000); // 每批5000条
request.setScroll(TimeValue.timeValueMinutes(10));
// 执行异步reindex
TaskSubmissionResponse response = client.submitReindexTask(
request, RequestOptions.DEFAULT);
String taskId = response.getTask();
适用场景:
- 索引数据量超过100GB
- 集群资源有限
- 可以容忍短暂的数据不一致
五、实战中的经验之谈
在实际操作中,我们还需要注意这些细节:
- 版本兼容性:确保新旧索引的mapping兼容,比如string类型在5.x之后分为text和keyword
- 性能调优:重建时可以临时关闭副本,完成后恢复
- 监控指标:重点关注refresh_interval和flush阈值
// 技术栈:Elasticsearch 7.x Java API
// 优化重建性能的设置
UpdateSettingsRequest settingsRequest = new UpdateSettingsRequest("new_index");
settingsRequest.settings(Settings.builder()
.put("index.number_of_replicas", 0) // 关闭副本
.put("index.refresh_interval", "30s") // 降低刷新频率
);
client.indices().putSettings(settingsRequest, RequestOptions.DEFAULT);
六、方案选型指南
根据不同的业务场景,可以这样选择:
| 方案 | 适用数据量 | 复杂度 | 风险 |
|---|---|---|---|
| 双写 | 中小 | 中 | 低 |
| 别名切换 | 任意 | 低 | 最低 |
| 滚动重建 | 超大 | 高 | 中 |
黄金法则:
- 能用别名解决的问题,就不要用其他方案
- 数据量超过1TB优先考虑滚动重建
- 对一致性要求高的场景选择双写
七、避坑指南
这些是我们用血泪换来的经验:
- 不要在业务高峰期执行全量reindex
- 记得提前估算磁盘空间(新索引可能比旧索引大20%)
- 监控集群健康状态,特别是JVM内存使用情况
- 准备完善的回滚方案
// 技术栈:Elasticsearch 7.x Java API
// 回滚到旧索引的示例
IndicesAliasesRequest rollbackRequest = new IndicesAliasesRequest();
AliasActions remove = new AliasActions(AliasActions.Type.REMOVE)
.index("new_index")
.alias("products");
AliasActions add = new AliasActions(AliasActions.Type.ADD)
.index("old_index")
.alias("products");
rollbackRequest.addAliasAction(remove).addAliasAction(add);
client.indices().updateAliases(rollbackRequest, RequestOptions.DEFAULT);
八、未来演进方向
随着Elasticsearch版本更新,一些新特性可以简化这个过程:
- CCR(跨集群复制):适合跨数据中心迁移
- 冻结索引:减少重建过程对资源的占用
- 可搜索快照:直接从快照恢复可查询状态
无论采用哪种方案,核心原则都是:让数据流动的过程对业务透明,就像给飞行中的飞机更换引擎,既要保证安全,又要平稳过渡。
评论