ElasticSearch跨版本无停机升级实战

ElasticSearch跨版本无停机升级实战

导读:随着产品的不断迭代升级,往往需要更中间件和业务系统。采用停服升级的方法,可能会造成业务中断,从而影响用户体验。相反,不停服升级能够实现业务的无缝更新,确保用户可以连续地使用产品而不受影响。考虑到 ElasticSearch 作为主流的搜索服务解决方案,其社区活跃度高、版本更新频繁,因此需要一套高效稳定且可回滚的平滑升级方案,以保证业务的稳定性和用户体验的一致性。

背景

目前线上使用的 Elasticsearch 版本以 6.2.3 为主,而 ElasticSearch 最新版已经迭代到 8.13.2 (截止发稿日)。然而 ElasticSearch 8.x 版本主要向量和机器学习方向发力,这一块内容有专业的向量数据库 Milvus 支撑,经过深度调研后,我们决定将 ElasticSearch 版本从 6.2.3 升级至 7.x 中较新的 7.17.3。

相比 6.2.3 版本,7.17.3 主要带来了以下提升:

在升级准备过程中,我们需要考虑:ElasticSearch 版本兼容性、插件兼容性、索引迁移、业务迁移、迁移过程平滑无感、可回滚等内容。为了保证升级成功,需要制定详尽的策略和方针,将升级过程中可能出现的风险降低到最小。

ES对滚动升级的支持

官方文档对 Elasticsearch 的滚动升级特性做了解读:

该方案有三个问题:

  • 从版本6.2.3 升级到 7.17.3,需经历多次滚动升级,无法一次原地滚动至目标版本。
  • 一旦滚动升级完成,无法回滚至原版本。此时若业务内 Elasticsearch SDK 没有做好跨版本兼容,一旦出现问题将大面积故障,无法回滚。
  • 对于未加密的老集群,原地滚动升级后仍无法加密,因为加密节点无法加入到未加密集群内。

综上,ES 自身支持的原地滚动升级方案,并不适用我们的场景。

升级思路

1.版本不兼容

对比 Elasticsearch 6.2.3 和 Elasticsearch 7.17.3,变化不可谓不多。以至于原针对 Elasticsearch 6.2.3 版本的SDK不再兼容 Elasticsearch 7.17.3,直接升级将导致业务代码读写 Elasticsearch 7.17.3 直接报错!

经过实际测试+文档参考,我们梳理了以下影响实际业务运行的差异点。

1.1 DSL语义不兼容

在 Elasticsearch 6.x 中,当 Should 同级有 Must或 Filter 条件,且在 Filter 上下文中时,Should 条件必须至少满足一个。

而在 Elasticsearch 7.x 中,Should 的最少命中条件数。仅与同级是否有 Must 或 Filter 相关,与 Filter 上下文无关。虽然官方文档未说明,但是从源码和查询实际效果可以确认此事。

可能有些人对上述表述认知比较模糊,这里举几个例子。MinimumShouldMatch 即 Should 最少必须命中的条件数。

比较尴尬的地方在于:同一条查询语句在不同的集群版本下,过滤效果居然不一样。并且官方文档也没有对此说明,只能从源码提交记录窥见。对于这种多层嵌套的查询语句,从 SDK 侧强行兼容改写的成本过高,且存在一定风险。最终我们采取查询语句巡检的方式,对问题语句定位改造。我们从 ES 网关侧收集到查询语句后,首先采集语句指纹(简单理解为同一类查询,入参不同,但指纹相同),再对指纹做问题语句分析。在我们的生产场景中,只发现个别业务采用此类用法,逐个改造即可。

1.2 索引Type不兼容

在 Elasticsearch 7.x 中,默认不再支持除"_doc"以外的索引 Type,但仍可以使用"Include_type_name"参数来使用非"_doc"以外的索引 Type。

在 Elasticsearch 8.x,完全删除索引 Type 概念,且移除 Include_type_name 参数。

解决方案:非"_doc" Type 的索引,通过 Reindex 迁移数据至"_doc" Type 索引,短期利用搜索网关对读写请求 Type 做改写,以兼容新的索引 Type。长期推动各业务方修改代码内的索引 Type。

1.3 查询Total不兼容

1.3.1 Total出参结构变化

total 值由原先的数字类型,变成了 Json 类型。这将导致 SDK 再解析 Elasticsearch 7.x Total 值的时候出错。

Elasticsearch 6.x

yaml 复制代码
{
    "total": 2048
}

Elasticsearch 7.x

json 复制代码
{
    "total": {
        "value": 2048,
        "relation": "eq"
    }
}

解决方案: 我们在 SDK 中兼容了两种格式的 Total 出参。

1.3.2 Total值精确度变化

在 Elasticsearch 7.x 中,搜索请求返回的 Total 值默认不精确(不超过10000),如果实际符合条件的文档数量超过10000,此时查询返回的 Total 值也是10000。

此外,额外提供了参数 Track_total_hits = true 来指定获取精确的total值。

解决方案:SDK 对查询请求处理,默认都带上 Track_total_hits = true ,以获取精确的 Total 值。

1.4 日期类查询不兼容

在 Elasticsearch 6.2.3 版本中,日期类型字段支持用科学计数法的时间戳查询,但该特性在 Elasticsearch 6.6.0 中被标记过时,且在 Elasticsearch 7.0.0 中被正式移除。详情:Deprecate use of scientific notation in epoch time parsing

bash 复制代码
GET xxx_index/_search
{
  "query": {
    "range": {
      "foundDate": {
        "lte": "1.6178112E12"
      }
    }
  }
}

上述查询在 Elasticsearch 6.2.3 中是被支持的,且能正常过滤数据,但在 Elasticsearch 7.17.3 中将会直接报错。

json 复制代码
{
    "type": "date_time_parse_exception",
    "reason": "date_time_parse_exception: Failed to parse with all enclosed parsers"
}

解决方案:SDK 拦截科学计数法格式的数字类型数值,并还原成普通格式。

1.5 查询脚本语法不兼容

bash 复制代码
GET xxx_index/_search
{
  "size": 1,
  "script_fields": {
    "demoScriptField": {
      "script": {
        "source": "doc['listWords'].values.contains(params.query)",
        "lang": "painless",
        "params": {
          "query": "xxx"
        }
      }
    }
  }
}

上述查询在 Elasticsearch 6.2.3 中是被支持的,且能正常生成 Script_field,但在 Elasticsearch 7.17.3 中将会直接报错。

json 复制代码
{
    "type": "illegal_argument_exception",
    "reason": "Illegal list shortcut value [values]."
}

解决方案:现存此类用法的业务方有限,推动改造兼容。

1.6 插件兼容

当前业务集群使用的插件中仅一款不兼容:Hanlp分词插件。

由于 Elasticsearch 7.17.3 对插件的权限管控更加严格,Hanlp 分词插件所需授权的读写权限已被禁止。但经过实际测试,仍有办法将 Hanlp 分词插件安装至 Elasticsearch 7.x 版本的集群上,且功能完整。只是安装过程比较复杂(利用软连接绕过 Elasticsearch 对文件目录的权限管控),不利于后续集群维护,因此该插件被放弃兼容至 Elasticsearch 7.17.3。我们使用经过二开的 Ik 分词插件来替代 Hanlp 分词插件。我们在 Ik 分词插件上实现了:索引级别指定远程自定义词库。

2.无停机迁移

以上。终于,解决完了所有跨版本集群间的所有兼容性问题。下面我们开始数据迁移。

2.1 资源/组建支持

2.1.1ES搜索架构

这里介绍一下我们的 ES 网关架构:集鉴权、限流、路由、慢查监控、拦截、流量收集、回放、灾备降级、集群认证一体,业务的读写请求经过 ES 网关再路由到对应的集群。本次无停机升级,我们重点用到了 ES Gateway 的集群路由、流量收集回放、多版本支持,Skadi Admin 的数据 CCR 等功能。

集群路由:ES Gateway 可以根据固定规则(某种请求 path、某种请求头、某类请求签名等等),将请求路由至对应的集群,规则可动态变更。

流量收集回放: ES Gateway 可以在处理业务读写请求的同时,收集、回放这些请求,本案借此功能实现数据双写和增量数据迁移。

多版本支持: ES Gateway 兼容高低版本的 Elasticsearch 集群。

数据 CCR:由 Skadi Admin 统筹控制无停机迁移任务,

2.2 架构-兼容性

我们在高版本 zcy saerch sdk 及 zcy sql search sdk 中,兼容了针对不同版本集群的差异。因此,在集群升级之前需确保所有业务方均已升级至安全版本。

2.3 存量数据迁移

2.3.1 快照备份恢复

a. 利用 Elasticsearch Snapshot and restore APIs 打包 Elasticsearch 6.x 的索引快照数据

bash 复制代码
GET /_snapshot/xxx_oss_repository/xxx_snapshot?wait_for_completion=true
{
    "indices": "index name",
    "ignore_unavailable": true,
    "include_global_state": false,
    "metadata": {
        "taken_by": "tugen",
        "taken_because": "back up for es6 up to es7"
    }
}

b. 利用开源的 阿里云OSS备份恢复插件 将快照文件保存至 OSS,实际上 Elasticsearch 支持多种共享存储进行备份恢复,可按需取用

c. Elasticsearch 7.x 从 OSS 读取并恢复快照至索引

bash 复制代码
POST /_snapshot/xxx_oss_repository/xxx_snapshot/_restore
 {
  "indices": "index name",
  "ignore_unavailable": true,
  "include_global_state": true
}

这种方案的优点是,基于 Lucene 快照实现,本质是对 Lucene 物理文件的直接拷贝,备份恢复速度极快,最大化利用磁盘 IO。缺点也很明显:

  1. 备份和恢复有版本限制,如 1.x 版本创建的快照不可以恢复到 5.x 版本,2.x 版本创建的快照不可以恢复到 6.x 版本
  2. 不支持自定义索引非动态的 Mapping 和 Setting,源集群索引啥样,目标集群就只能啥样。当我们需要 Elasticsearch 6.x 和 Elasticsearch 7.x 的索引结构不一致时,快照恢复就不能满足我们的需求

例如:

在 Elasticsearch 6.x 创建一个 Min_gram 和 Max_gram 差值大于 1 的索引时,仅作了过时提示。

bash 复制代码
#! Deprecation: Deprecated big difference between max_gram and min_gram in NGram Tokenizer,expected difference 	must be less than or equal to: [1]

但在 Elasticsearch 7.x 中,被强制校验,无法创建索引。因此需要在新集群改变所有结构。

json 复制代码
{
	"type": "illegal_argument_exception",
	"reason": "The difference between max_gram and min_gram in NGram Tokenizer must be less than or equal to: 				[1] but was [2]. This limit can be set by changing the [index.max_ngram_diff] index level setting."
}

需要注意的是,Snapshot and restore APIs 在打包快照前,会先执行一次 Flush 操作,将内存中未持久化的数据落盘。这是因为 Elasticsearch 的快照机制是由 Lucene 实现而来的。Lucene 的快照过程其实是对 Lucene 物理文件的复制过程。因此,不必担心 Elasticsearch 快照会丢失未来得及持久化到硬盘的数据。

java 复制代码
    /**
     * Acquires the {@link IndexCommit} which should be included in a snapshot.
     */
    public Engine.IndexCommitRef acquireIndexCommitForSnapshot() throws EngineException {
        final IndexShardState state = this.state; // one time volatile read
        if (state == IndexShardState.STARTED) {
            return getEngine().acquireIndexCommitForSnapshot();
        } else {
            throw new IllegalIndexShardStateException(shardId, state, "snapshot is not allowed");
        }
    }

2.3.2 跨集群重建

a. 按需求在目标集群创建所需的索引,目标索引可以与源索引不完全相同

b. 跨集群重建

json 复制代码
POST _reindex
{
  "source": {
    "remote": {
      "host": "http://oldhost:port"
    },
    "index": "source_index"
  },
  "dest": {
    "index": "dest_index"
  }
}

该方案的优点是:

a. 基于 Scroll 实现,本质是循环从老集群拉取数据写入新集群,在写入过程中可以对写入内容、写入字段值等做灵活配置

b. 兼容性强,支持任何相邻大版本 Elasticsearch 集群之间的数据迁移

但在跨集群重建时,只能单线程重建,极大影响速度。实测千万级别的索引就需要重建数小时。作为对比,这种量级的数据用快照备份恢复的方式只需要几分钟。

2.4 增量数据迁移

上述迁移只能迁移存量数据,下面介绍如何迁移存量数据迁移期间产生的增量数据。

  1. 首先,在做存量数据迁移之前,先开启ES网关的流量收集,录制所有写流量并投递到一二级队列(MQ + Redis)中。
  2. 通过 跨集群重建 或 快照备份恢复 迁移存量数据至 Elasticsearch 7.x。
  3. 批量回放队列中的数据至 Elasticsearch 7.x,对回放消息做压缩、合并处理,提高写入吞吐及效率。
  4. 直到队列无堆积,至此成功完成数据平滑迁移。

3.数据双写

索引迁移完成后,为了确保必要时能够回滚(实际上我们在测试环境发现了数个因为集群版本导致的兼容性问题),需要保持一段时间数据双写,同时写入 Elasticsearch 6.x 和 Elasticsearch 7.x。

3.1 异步双写

基于之前做增量数据迁移的二级队列双写。这里啥都不用干,只需要保持增量数据迁移阶段的队列和流量收集回放继续工作即可。

由于流量收集回放是异步的,且我们在回放端做了 批量消费 + 数据合并 逻辑。因此流量收集回放对业务侧的感知比较小,性能损耗不大 。但缺点是异步消费导致两个集群之间数据存在一定延迟,虽然 Elasticsearch 自身就是准实时的,但耐不住部分业务方掐着 Refresh_interval 去读数据。此时异步双写带来的数据延迟对业务方来说是不可接受的。

3.2 同步双写

在 ES 网关侧同步写两个集群,两个集群都写入成功才返回 Success。保证了集群之间数据时间上的强一致性。

由于在增量数据迁移阶段,Elasticsearch 7.x 集群是异步写的,因此会有一个异步写转同步写的过程。我们通过检测队列堆积值来实现,当异步队列内的堆积消息为 0 时,表示此时集群之间的数据完全追平,此时可以切换为同步写。否则会存在同步流量和异步流量数据前后覆盖的问题。同步双写方案的缺点是双写带来了额外的写入失败率,并且写入的整体 rt 也会上涨。

4.读写切流

4.1 读流量切换

通过ES网关的路由能力,将业务读流量部分或全部切到 Elasticsearch 7.x。此时观察 Elasticsearch 7.x 的负载监控表现及线上错误日志告警等信息,如有异常,即刻关闭该路由规则,将读流量还原至 Elasticsearch 6.x。若一段时间后无线上异常,可操作写流量切换。

4.2 写流量切换

此时下掉 Elasticsearch 6.x 的写入流量,仅同步写 Elasticsearch 7.x 即可。到此,完成了整个无停机平滑切换流程。

4.3 异常回滚

新老集群之间数据时刻保持着一致性,因此再出现异常时,可以通过ES网关随时路由流量回滚到老集群上。

升级收益

ES集群从6.x升级到7.x后,几个主要的指标:慢查数、查询耗时、写入耗时、Refresh 耗时均有不同程度的性能提升。集群慢查数量大幅下降,读写耗时也更短,更平稳。

慢查数

Query Time

Index Time

Refresh Time

最后

几年前我们经历过从 Elasticsearch 2.x 升级到 Elasticsearch 6.x,那时接入 Elasticsearch 的业务方不过寥寥数个,且数据量也不多,搜索架构相对简单,业务上也允许我们停机升级。所以整体流程相对比较粗暴,采取业务停服、集群停机原地升级的策略,前期准备工作也不需要很复杂,但承担着不可回滚的风险。随着业务体量的增大,整体架构偏向复杂,牵扯业务方的数量增多,停机升级已不再现实,升级期间的数据安全可回滚也变得尤为重要,甚至不允许升级过程中出现抖动。

此次跨版本无停机升级将 ElasticSearch 从 6.2.3 成功升级到 7.17.3,又确保业务的连续性和用户体验的一致性。作为主流的搜索服务解决方案,Elasticsearch 频繁的版本更新要求我们制定一套高效稳定、可回滚的平滑升级方案。

首先,我们分析了升级的必要性和新版本的改进,包括安全性提升、硬件成本降低、稳定性提高、内存和存储资源占用减少,以及性能的显著提升。然后,我们系统地解决了跨版本升级中的各种兼容性问题,具体涉及 DSL 语义、索引类型、查询 Total、日期类查询、查询脚本语法及插件兼容性。

在无停机迁移方面,我们采用了详细的资源准备、存量数据迁移和增量数据迁移策略。存量数据迁移通过快照备份恢复和跨集群重建两种方式完成,增量数据迁移通过流量收集和回放机制实现。我们还实现了数据的异步双写和同步双写,以确保在升级过程中能够随时回滚。

最终,通过读写流量的逐步切换和实时监控,我们实现了业务的平滑过渡,成功完成了无停机升级。升级后,我们的集群在慢查数、查询耗时、写入耗时和刷新耗时等关键指标上均有显著改善,充分证明了升级方案的有效性和必要性。希望本文能为大家在实际操作中提供有价值的参考和借鉴。

推荐阅读

Kubernetes Informer基本原理

JDK17 与 JDK11 特性差异浅谈

业务分析师眼中的数据中台

政采云大数据权限系统设计和实现

JDK11 与 JDK8 特性差异浅谈

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊......如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

相关推荐
一次旅行2 小时前
【效率秘籍】Shell 函数+别名组合拳,让 80% 的重复命令变 1 个字母
大数据·elasticsearch·搜索引擎
码农学院3 小时前
Elasticsearch构建汽车零配件智能搜索与匹配系统
大数据·elasticsearch·汽车
Elastic 中国社区官方博客19 小时前
不到 5 分钟完成本地部署:Jina embedding 模型现已支持本地部署
大数据·人工智能·elasticsearch·搜索引擎·embedding·jina
二进制流水搬运工19 小时前
入职第一天拉了23个仓库,我写了个脚本解放双手
大数据·elasticsearch·搜索引擎
Elasticsearch20 小时前
在不到一小时内将 Datadog Kubernetes 仪表板迁移到 Elastic Observability
elasticsearch
.柒宇.21 小时前
Elasticsearch 核心概念与系统架构详解
elasticsearch·系统架构
风行無痕1 天前
单机Elasticsearch 8.19.18安全升级到9.4.3的过程
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客1 天前
使用重新设计的 AutoOps 更快地进行 Elasticsearch 问题排查
大数据·运维·前端·人工智能·elasticsearch·搜索引擎·全文检索
阿拉雷️1 天前
【搜索实战】Spring Boot 3.3 + AI Agent × Elasticsearch:让AI自动优化搜索策略,查询速度从2秒压到50毫秒
人工智能·spring boot·elasticsearch
阿里云大数据AI技术2 天前
阿里云 ES AI 引擎版:面向 Agent 场景,为亿级租户、千亿规模向量设计的搜索引擎
人工智能·elasticsearch·agent