一、为什么需要零停机迁移?

想象一下你正在给一栋大楼重新装修,但要求所有住户正常生活不受影响。Elasticsearch的索引重建也是类似的场景——业务不能停,数据不能丢,查询还得继续跑。传统的重建索引方式就像让所有住户搬出去装修,显然不现实。

常见痛点包括:

  • 直接删除旧索引会导致服务中断
  • 数据量大的情况下全量重建耗时可能超过维护窗口
  • 业务系统需要频繁修改连接配置
// 技术栈:Elasticsearch 7.x Java API
// 错误示范:直接删除旧索引
DeleteIndexRequest request = new DeleteIndexRequest("old_index");
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
// 这会导致所有正在使用该索引的查询立即失败

二、双写方案:新旧索引并行运作

这个方案的核心思想是"两条腿走路"。就像在旧楼旁边先盖好新楼,等所有住户都搬过去了再拆旧楼。具体实现分为三个阶段:

  1. 创建新索引并配置相同的mapping
  2. 所有写操作同时发往新旧索引
  3. 数据同步完成后切换查询到新索引
// 技术栈: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);

注意事项

  • 需要确保所有客户端都实现双写逻辑
  • 建议使用消息队列暂存写请求,防止某次写入失败
  • 监控两个索引的文档数量差异

三、别名切换:给索引戴上面具

别名就像给索引起的绰号,客户端永远访问这个绰号,我们只需要在后台更换绰号对应的真实索引。这是最优雅的方案之一,具体步骤:

  1. 给旧索引绑定业务别名(如products)
  2. 创建并预热新索引
  3. 原子操作将别名切换到新索引
// 技术栈: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);

优势

  • 客户端配置无需修改
  • 切换是原子操作,没有中间状态
  • 可以随时回滚到旧索引

四、滚动重建:化整为零的智慧

当索引特别大时,我们可以像吃披萨一样,一块一块来处理。具体实现方式:

  1. 使用reindex API分批次迁移数据
  2. 每次只处理部分文档
  3. 配合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
  • 集群资源有限
  • 可以容忍短暂的数据不一致

五、实战中的经验之谈

在实际操作中,我们还需要注意这些细节:

  1. 版本兼容性:确保新旧索引的mapping兼容,比如string类型在5.x之后分为text和keyword
  2. 性能调优:重建时可以临时关闭副本,完成后恢复
  3. 监控指标:重点关注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优先考虑滚动重建
  • 对一致性要求高的场景选择双写

七、避坑指南

这些是我们用血泪换来的经验:

  1. 不要在业务高峰期执行全量reindex
  2. 记得提前估算磁盘空间(新索引可能比旧索引大20%)
  3. 监控集群健康状态,特别是JVM内存使用情况
  4. 准备完善的回滚方案
// 技术栈: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版本更新,一些新特性可以简化这个过程:

  1. CCR(跨集群复制):适合跨数据中心迁移
  2. 冻结索引:减少重建过程对资源的占用
  3. 可搜索快照:直接从快照恢复可查询状态

无论采用哪种方案,核心原则都是:让数据流动的过程对业务透明,就像给飞行中的飞机更换引擎,既要保证安全,又要平稳过渡。