上个月帮朋友把一套ES 7.17.9集群迁到OpenSearch 3.4.0,整个过程最大的感受就是:这种跨版本、跨产品线的迁移,最怕的不是数据量大,而是你对兼容性边界心中无数。我们当时先在本机用Docker Desktop把两套集群跑起来,完整预演了一遍迁移链路,后面在正式环境里基本只花了半天就搞定了。这篇文章就把这条模拟路径完整拆开,从环境搭建、数据盘点、reindex方案设计,到ILM转ISM、Dashboards适配、一致性验收,全部按可复现的标准写清楚。打算迁移、或者还在观望的团队,照着这套流程走一遍,值不值得迁、迁移成本到底多高,心里就有底了。
1. 为什么选7.17.9到3.4.0这个组合做迁移预演
1.1 ES 7.17.9在迁移链路里的位置
Elasticsearch 7.17.9是7.x分支后期比较稳定的维护版本,也是很多团队“最后停留”的版本。当时不少项目因为这个版本保留了部分基础安全能力,又处于7.x比较成熟的特性集,所以生产环境大量停留在这一代。而OpenSearch从Elasticsearch 7.10.2 fork出来后,API层面尽量保持了兼容,后续版本一直在迭代,3.4.0已经属于比较新的稳定版本,对7.17.9的存量数据而言,跨版本迁移的代价相对可控。
但要注意,兼容不是说完全无感。7.17.9在7.10之后引入了一些新特性,比如部分字段类型、管道处理、数据流能力,在OpenSearch 3.4.0里不一定以完全相同的方式存在。所以迁移前必须搞清楚一条主线:你的业务到底用了哪些API、哪些索引类型、哪些治理策略,逐一拿到OpenSearch 3.4.0上去验证。这也是为什么我强烈建议用Docker Desktop先做一套端到端模拟,成本几乎为零,但能提前暴露出80%以上的坑。
1.2 Docker Desktop模拟的不可替代性
真正在服务器上迁移,往往要面对停机窗口、资源不足、回滚困难这些问题。而在本地用Docker Desktop做模拟,你可以在同一台机器上同时运行源集群和目标集群,任意折腾,比如故意制造脏数据、测试异常中断后的续跑、对比不同reindex参数的效果。恢复的时候把容器删掉重新创建就行,完全不心疼。
另一个好处是网络环境可控。Docker Desktop内部支持自定义bridge网络,两个容器之间可以通过容器名做互相访问,这恰恰是远程reindex的前提。你可以在本地先验证“从OpenSearch容器访问ES容器”这条链路是否通,端口映射怎么避免冲突,安全插件要不要关,然后再把这些结论原封不动搬到生产环境的网络配置里。我在实际操作中把这套流程跑熟之后,感觉正式环境唯一要额外处理的就是ES的reindex白名单和防火墙规则,其余环节几乎零差异。
2. Docker Desktop环境搭建:两套集群共存的操作细节
2.1 网络规划与端口映射策略
先规划网络。ES和OpenSearch内部默认都监听9200端口,如果直接在本机跑两个容器,宿主机端口必然冲突。最简单的做法是:ES映射到宿主机9200,OpenSearch映射到9201,Kibana/OpenSearch Dashboards统一走5601。两个容器放到同一个自定义网络里,容器名之间用9200互相访问,不受宿主机映射影响。
docker network create es-os-net后续在docker-compose里直接引用这个网络即可。强调一下,网络一定要显式创建,不要用compose默认的独立网络,否则两个容器之间无法通过服务名连通。
2.2 ES 7.17.9容器的配置文件
源集群我用的单节点模式,关闭了xpack安全,因为模拟环境不需要额外认证,减少干扰项。
services: es: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9 container_name: es-source environment: - discovery.type=single-node - xpack.security.enabled=false - ES_JAVA_OPTS=-Xms1g -Xmx1g - bootstrap.memory_lock=true ulimits: memlock: soft: -1 hard: -1 ports: - "9200:9200" - "9300:9300" networks: es-os-net: aliases: - es-source这里有几个细节。bootstrap.memory_lock=true是为了模拟生产环境下锁定内存的配置,如果本机内存不充裕,可以改成false。ES_JAVA_OPTS我给的是1g堆内存,本地模拟完全够用。端口9300是集群节点间通信端口,虽然单节点用不到,但建议一起映射出来,方便后续如果要扩成多节点测试,不用再改配置。
启动后验证一下:
curl http://localhost:9200正常会返回带cluster_name和version 7.17.9的JSON。
2.3 OpenSearch 3.4.0与Dashboards容器配置
目标集群我用的是OpenSearch 3.4.0官方镜像,同时把Dashboards一起跑起来。这里第一个坑是OpenSearch 3.x默认会启用安全插件,并且会在启动时自动生成admin密码,如果不处理,后面访问和reindex都会很麻烦。模拟环境直接关掉安全插件是最省事的。
services: opensearch: image: opensearchproject/opensearch:3.4.0 container_name: os-target environment: - discovery.type=single-node - DISABLE_SECURITY_PLUGIN=true - DISABLE_INSTALL_DEMO_CONFIG=true - DISABLE_PERFORMANCE_ANALYZER_AGENT=true - OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g ports: - "9201:9200" - "9600:9600" networks: es-os-net: aliases: - os-target dashboards: image: opensearchproject/opensearch-dashboards:3.4.0 container_name: os-dashboards environment: - OPENSEARCH_HOSTS=http://opensearch:9200 - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true ports: - "5601:5601" depends_on: - opensearch networks: - es-os-net这里要注意,Dashboards里的OPENSEARCH_HOSTS如果写http://localhost:9200,它其实指向的是宿主机,在容器内部访问不通。必须写OpenSearch的容器名http://opensearch:9200或者http://os-target:9200。这个错误我最初犯过,导致Dashboards一直报无法连接。
OpenSearch容器起来之后,访问http://localhost:9201就能看到版本信息。Dashboards访问http://localhost:5601。
2.4 验证两集群网络互通
从OpenSearch容器里访问ES容器,这一步很关键,后续reindex全靠它:
docker exec -it os-target bash curl http://es-source:9200如果返回ES的版本信息,说明两个容器在同一网络内可以互通。顺手把ES里的测试索引也建好,后面迁移演练才有数据可用。
模拟数据建议覆盖常见类型,比如keyword、text、nested、geo_point、date,最好再放一个带别名的数据流索引,这样能验证更完整的迁移场景。
3. 迁移前的数据盘点:索引、模板和ILM该怎么处理
3.1 先想清楚要搬哪些东西
很多人一上来就盯着索引数据,其实迁移不只是搬文档,还要搬“规则”。一个ES集群里的存量对象通常包括:
- 数据索引本体,以及它们的mapping、settings
- 索引模板,包含模板匹配规则、别名、settings
- ingest pipeline,用于写入时的预处理
- ILM生命周期策略,负责索引滚动和删除
- Kafka或业务写入时依赖的alias,别名一旦丢失,应用侧直接报错
我在盘点时习惯先列一张清单,根据业务属性给每个索引打标:核心数据、日志数据、临时数据、弃用数据。核心数据必须完整迁移;日志数据如果已经过期,比如保留90天,那90天之前的直接不搬;临时数据干脆删除。这样能显著缩减迁移时间。
3.2 元数据导出命令一览
盘点阶段需要一批只读命令,不会影响业务。我在ES源集群上依次执行:
curl -s localhost:9200/_cat/indices?v&s=index curl -s localhost:9200/_mapping?pretty > mapping_backup.json curl -s localhost:9200/_template?pretty > templates_backup.json curl -s localhost:9200/_ilm/policy?pretty > ilm_policies_backup.json curl -s localhost:9200/_ingest/pipeline?pretty > pipelines_backup.json curl -s localhost:9200/_alias?pretty > aliases_backup.json这些JSON在迁移前必须仔细过一遍。尤其是mapping,如果源索引里有OpenSearch不支持的字段类型,提前发现总比迁移到一半报错强。常见的差异点包括_all字段、join字段类型、percolator等,还有ES 7.17里的dense_vector在OpenSearch 3.4里的默认索引引擎参数可能跟旧版本不一致。
3.3 哪些元数据需要手工转换
模板和pipeline大体兼容,可以直接建到OpenSearch上,但ILM策略不能直接建,因为OpenSearch用的是ISM体系,API路径、策略写法都不同。别名和数据流在OpenSearch里有对应的等价物,大部分可以直接沿用。
所以盘点结果落到操作清单上是这样:
| 源集群对象 | 目标集群处理方式 | 说明 |
|---|---|---|
| 索引mapping | 手工创建或以create index with mapping方式写入 | 需要检查字段类型差异 |
| 索引模板 | PUT _template/xxx | API兼容,基本可直用 |
| ingest pipeline | PUT _ingest/pipeline/xxx | API兼容,处理器需逐个验证 |
| ILM策略 | 转换后创建ISM策略 | API路径从_ilm变成_plugins/_ism |
| alias | POST /_aliases | 兼容,注意数据流alias不能乱动 |
4. 数据迁移核心链路:用reindex做远程拉取
4.1 为什么不用快照恢复
ES的索引快照理论上可以恢复到OpenSearch,但有两个现实问题。第一,OpenSearch 3.4对ES 7.x快照的兼容性有明确边界,不是所有仓库和快照格式都能直接恢复,官方文档里写得很保守。第二,快照恢复会把源端的settings、mapping原样搬过去,有些在ES里合法、在OpenSearch里不识别或不推荐的配置,恢复后索引可能直接处于yellow甚至red状态,排查起来比reindex麻烦得多。
所以推荐主路径是reindex:通过OpenSearch的_reindexAPI,从ES源集群远程拉取文档,写入目标索引。这个方案的好处是迁移过程中可以做字段清洗、类型转换、按查询条件过滤数据,灵活性最高,而且如果写入失败,单条错误不会让整个任务直接崩掉,可以定位后重试。
4.2 先解决remote whitelist这个拦路虎
直接在OpenSearch上执行_reindex访问ES,会遇到一个连接限制:OpenSearch默认不允许reindex访问任意远程host,需要显式配置白名单。具体配置项叫reindex.remote.whitelist,写在opensearch.yml里。
模拟环境中我用的是挂载配置的方式,在宿主机建一个opensearch.yml文件,然后通过volume挂载进容器:
reindex.remote.whitelist: "es-source:9200"docker-compose里加上:
volumes: - ./opensearch.yml:/usr/share/opensearch/config/opensearch.yml配置完重启OpenSearch容器,然后验证:
docker exec -it os-target bash -c "curl -s http://es-source:9200"如果能返回ES的版本JSON,说明白名单和网络都通了。这一步经常会遇到容器内curl未安装的情况,可以用wget或者直接在宿主机用docker exec进去后先装curl,模拟环境无伤大雅。
4.3 执行远程reindex
先把目标索引建好,我一般直接从源索引的mapping复制一份,再做必要的类型修正。比如源索引里如果有_all字段或者include_in_all设置,在OpenSearch里要去掉,因为_all已经废弃了。
curl -X PUT http://localhost:9201/my_index -H 'Content-Type: application/json' -d @my_index_mapping.json然后执行reindex。远程reindex在OpenSearch侧的写法是:
curl -X POST "http://localhost:9201/_reindex?pretty" -H 'Content-Type: application/json' -d' { "source": { "remote": { "host": "http://es-source:9200" }, "index": "my_index", "query": { "match_all": {} } }, "dest": { "index": "my_index", "op_type": "create" } } '这里op_type: create意味着如果目标索引中已经存在相同_id的文档,直接报告版本冲突而不是覆盖。这个设计是用来保护数据的:如果迁移中途需要重跑,重复执行不会污染目标数据。如果你明确想覆盖,比如要做增量同步,可以改成index。
执行后观察返回结果里的total、created、failures字段。有failures不要慌,把每条错误信息拿过来看,通常都是文档内容里的日期格式、坐标格式问题,单独清洗即可。
4.4 提升reindex速度的几个参数
数据量大的时候,reindex默认单线程跑得确实慢。我常用的调优手段有:
- 给reindex加
slices: auto,让OpenSearch自动按主分片数量切分并行任务 - 临时把目标索引的副本数设为0,迁移结束后再调回正常值
- 关闭目标索引的
refresh_interval为-1,避免频繁刷新 - 设置
"wait_for_completion": false配合tasks API查看进度
举个例子,如果目标索引有4个主分片,配置"slices": 4后,任务会被拆成4个子任务并行执行,速度提升非常明显。注意slices的值不建议超过主分片数,否则会浪费线程资源。
对于批量迁移多个索引,我习惯写一个循环脚本,把索引名从清单文件里逐行读取,逐个执行reindex,并记录每个索引的状态和耗时。脚本里一定要做错误重试,比如网络抖动导致某个索引失败,跳过重试3次后再报告,避免中途人工盯。
5. 迁移中的高频坑:ILM转ISM、Dashboards替换和应用适配
5.1 ILM策略转换成ISM策略
这是整个迁移过程中最容易忽略、也最容易翻车的环节。ES的ILM策略结构是phases,OpenSearch的ISM策略结构是states加transitions。语义上有对应关系,但API完全不同。
源ES里一个典型的ILM策略长这样:
{ "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_size": "50gb", "max_age": "30d" } } }, "delete": { "min_age": "90d", "actions": { "delete": {} } } } } }转换到ISM策略后:
{ "policy": { "description": "migrated from ILM", "default_state": "hot", "states": [ { "name": "hot", "actions": [ { "rollover": { "min_size": "50gb", "min_index_age": "30d" } } ], "transitions": [ { "state_name": "delete", "conditions": { "min_index_age": "90d" } } ] }, { "name": "delete", "actions": [ { "delete": {} } ] } ] } }创建ISM策略的API是:
curl -X PUT "http://localhost:9201/_plugins/_ism/policies/my_policy" -H 'Content-Type: application/json' -d @ism_policy.json创建完策略后,还要在目标索引模板的settings里关联这个策略。ES那边ILM是在索引模板里通过index.lifecycle.name关联的,OpenSearch这边要改成:
{ "settings": { "plugins.index_state_management.policy_id": "my_policy", "plugins.index_state_management.rollover_alias": "my_alias" } }我最初迁移时没注意这个关联关系,结果策略建好了,索引却一直停在hot状态不滚动,后来排查发现是模板里少了rollover_alias配置。这个问题在日志类数据流场景下影响很大,时间一长,分片大小和数据量直接失控。
5.2 Kibana到OpenSearch Dashboards的可视化迁移
Kibana 7.17.9里保存的索引模式、可视化、Dashboard,可以通过Saved Objects的Export功能导出为NDJSON。理论上OpenSearch Dashboards也支持导入,但实际用下来,大部分复杂面板并不能无损迁移。原因是内部对象引用了大量index pattern的ID和字段聚合配置,两个产品对这些对象的schema不完全一致。
我的建议是分两步走。第一步,把Kibana里所有的索引模式导出,在OpenSearch Dashboards里手动重建同名索引模式。第二步,关键的Dashboard不要依赖一键导入,而是重新创建。模拟环境里如果只有几个核心看板,手工重建半小时内就能搞定。如果环境大、面板多,建议先用迁移工具试一遍,能导多少导多少,剩下的手工补。
实际操作开OpenSearch Dashboards时,创建索引模式的方式和Kibana基本一致,但要注意字段列表的加载速度,以及时间过滤器字段的选择。如果原来用的是@timestamp,目标索引里也要保持一致,否则时间范围组件的展示会异常。
5.3 应用侧客户端适配
迁移完成后,业务代码不能继续指向ES地址就算完事。如果你的应用用的是Elasticsearch官方Java High Level REST Client,版本7.17.9,对接OpenSearch 3.4.0的REST API时,大部分查询DSL能跑通,但OpenSearch官方推荐的做法是切换到OpenSearch Java Client,因为两者在底层连接配置、兼容性维护策略上已经分叉。
如果只是快速验证,可以在不改代码的情况下,把客户端里的host从ES地址换成OpenSearch地址,端口从9200换成9201,跑一轮冒烟测试。遇到不兼容的请求,再针对性地修改DSL。Python侧同理,elasticsearch-py换成opensearch-py后,连接参数基本平移,但包内部的序列化逻辑可能有细微差别。
这里要特别提醒,如果ES端开了安全认证,reindex的source里需要带用户名密码;如果OpenSearch端没有关闭安全插件,写入和查询也要在请求里带认证头。模拟环境为了省事关掉了,但正式迁移时两端的安全配置都必须加到请求里。
6. 验收清单:从文档计数到搜索行为对比
6.1 文档数与字段抽样对比
迁移完成不等于迁移成功,验收环节必须做。首先是最基本的文档数对比。对每个索引分别执行:
curl -s localhost:9200/my_index/_count curl -s localhost:9201/my_index/_count两边返回的count应该完全一致。但只对比总数远远不够,因为reindex过程可能把某个字段拆成null导致文档写入成功但内容失真。我习惯写一个简单的python脚本,从源ES用scroll拉数据,再从目标OpenSearch按_id逐条取_source,对比每个字段的值。
from opensearchpy import OpenSearch from elasticsearch import Elasticsearch es = Elasticsearch(["http://localhost:9200"]) os_client = OpenSearch(["http://localhost:9201"]) # 示例:随机抽样100条做字段级对比 for doc_id in sample_ids: src = es.get(index="my_index", id=doc_id)["_source"] dst = os_client.get(index="my_index", id=doc_id)["_source"] for key in src.keys(): if str(src.get(key)) != str(dst.get(key)): print(f"diff: {doc_id} {key}: {src.get(key)} != {dst.get(key)}")这段脚本逻辑很简单,但作用很大。我实际跑的时候抓到过日期类型字段在迁移后时区偏移的问题,还有嵌套对象字段值类型被转换的问题。
6.2 查询行为一致性验证
文档一样不代表查询结果一样。选一批业务上真正在用的查询,分别打到ES和OpenSearch上,对比返回的hits.total、排序顺序、聚合结果。比如对日志索引执行一个时间范围加关键字搜索的query,两边返回的文档ID序列应该一致。如果某个查询在OpenSearch上报错,优先检查字段类型是keyword还是text,以及是否存在OpenSearch不支持的查询参数。
有一种情况很隐蔽:ES 7.17默认在_source里存储原始文档,OpenSearch同样如此,但当文档里有数组且数组元素被动态映射成不同类型时,两边的映射结果可能不一致,导致查询返回的数量不同。所以对每个索引执行一次_mapping对比,比抽样查字段更保险。
6.3 容易被忽略的验证盲区
我自己踩过的验证盲区有三个。第一个是带别名的数据流索引,alias指向的写入索引名在迁移后是否保持一致,直接决定业务写入端是否报错。第二个是nested子文档的聚合结果,ES和OpenSearch在nested聚合的返回结构上虽然一致,但如果映射里没显式声明nested类型,迁移后聚合会直接失败。第三个是索引的_refresh状态,reindex写入后目标索引可能没有最新数据可见,必须等refresh周期或者手动POST /my_index/_refresh再查询,否则刚迁移完就查会少文档。
这几个盲区如果不在模拟环境提前验证,生产环境出问题时会非常被动。
6.4 模拟迁移带来的额外收获
跑完这一整套Docker Desktop模拟之后,你手里会有一份很完整的迁移文档:哪些索引需要转换mapping、哪些pipeline要改、ILM策略的转换模板、reindex的调优参数、验证脚本。这些资产在正式迁移时能直接复用。而且模拟环境还可以反复练习回滚方案,比如reindex跑到一半断了,怎么清理目标索引重新来,这类操作熟练之后,正式迁移的心里压力会小很多。
我个人的操作习惯是:把模拟环境的docker-compose和所有脚本放到一个git仓库里,每次迁移一个业务索引组,就在仓库里记录一次结果。最终生产迁移时,基本就是在模拟链路里换一下源集群地址和认证信息,流程完全一致。这种迁移方式虽然不是最快的,但一定是最稳的。真等到生产环境遇到问题时,你会庆幸当初花了这一个多小时做模拟预演。