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

微信公众号

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

相关推荐
INFINI Labs8 分钟前
Elasticsearch filter context 的使用原理
大数据·elasticsearch·jenkins·filter·querycache
chengpei1478 分钟前
Elasticsearch介绍及安装部署
elasticsearch·搜索引擎
it噩梦11 小时前
es 中 terms set 使用
大数据·elasticsearch
喝醉酒的小白13 小时前
Elasticsearch 配置文件
大数据·elasticsearch·搜索引擎
missay_nine17 小时前
Elasticsearch
大数据·elasticsearch·搜索引擎
it噩梦18 小时前
深度分析 es multi_match 中most_fields、best_fields、cross_fields区别
java·elasticsearch
喝醉酒的小白19 小时前
ES 集群 A 和 ES 集群 B 数据流通
大数据·elasticsearch·搜索引擎
炭烤玛卡巴卡19 小时前
初学elasticsearch
大数据·学习·elasticsearch·搜索引擎
it噩梦19 小时前
es 中使用update 、create 、index的区别
大数据·elasticsearch
Mitch31121 小时前
【漏洞复现】CVE-2015-3337 Arbitrary File Reading
elasticsearch·网络安全·docker·漏洞复现