问题背景
在进行 Elasticsearch 数据迁移时,多个索引出现 dump 失败问题。查看日志发现单次 dump 报错:
Error Emitted => {
"error": {
"root_cause": [{
"type": "illegal_argument_exception",
"reason": "Malformed action/metadata line [1], expected a simple value for field [timestamp] but found [START_ARRAY]"
}],
"type": "illegal_argument_exception",
"reason": "Malformed action/metadata line [1], expected a simple value for field [timestamp] but found [START_ARRAY]"
},
"status": 400
}
错误信息显示:期望 timestamp 字段是单个值,但实际收到了数组(START_ARRAY)。
排查过程
第一阶段:检查配置和编码问题
首先怀疑是配置或编码问题,但检查后发现:
- ✅ 配置文件格式正确
- ✅ 编码没有问题
- ✅ 网络连接正常
第二阶段:怀疑 timestamp 字段类型不匹配
错误信息指向 timestamp 字段,开始怀疑是字段类型不匹配问题。
发现的问题
- Mapping 配置 :
timestamp字段在 mapping 中定义为long类型 - 实际数据 :
_source中timestamp字段存在两种格式:- 字符串格式:
"timestamp": "1508112000" - 数字格式:
"timestamp": 1508112000
- 字符串格式:
Elasticsearch 存储机制深入理解
在这个过程中,深入理解了 Elasticsearch 的存储机制:
1. _source 字段与 Mapping 的关系
关键理解 :_source 字段存储的是原始 JSON 文档 ,完全不受 mapping 类型影响。
json
// 写入的原始文档
{
"timestamp": "1508112000", // 字符串
"pmid": "12345"
}
json
// ES 存储的 _source(完全一样)
{
"_source": {
"timestamp": "1508112000", // 仍然是字符串!
"pmid": "12345"
}
}
重要点:
- ✅
_source永远保持原始格式 - ✅ Mapping 类型不会改变
_source中的值 - ✅
_source用于:返回搜索结果、重新索引、更新文档
2. Mapping 类型的作用范围
Mapping 类型只影响索引层 (用于搜索、排序、聚合的字段),不影响 _source。
json
// Mapping 定义
{
"mappings": {
"properties": {
"timestamp": {
"type": "long" // 只影响索引层
}
}
}
}
行为:
- ES 会尝试将
_source.timestamp的值(如"1508112000")转换为long类型 - 转换后的值存储在索引层(不可见,用于搜索)
_source中的原始值保持不变
3. 类型转换机制
场景1: 字符串数字 → long(自动转换)
json
// _source(原始)
{
"timestamp": "1508112000" // 字符串
}
// Mapping
{
"timestamp": {
"type": "long",
"coerce": true // 默认开启
}
}
结果:
- ✅
_source.timestamp="1508112000"(字符串,保持不变) - ✅ 索引层
timestamp=1508112000(long,用于搜索) - ✅ 查询
timestamp: 1508112000可以找到这个文档
场景2: 无法转换的字符串
json
// _source(原始)
{
"timestamp": "-1.0" // 字符串,但包含小数
}
// Mapping
{
"timestamp": {
"type": "long" // long 不支持小数
}
}
结果:
- ❌ 转换失败(
"-1.0"无法转换为整数) - ✅ 如果设置了
ignore_malformed: true:- 文档可以写入
_source.timestamp="-1.0"(保持不变)- 索引层
timestamp=null(无法搜索)
结论
经过分析,发现:
- ✅ Mapping 是有效的:它控制索引层的行为
- ✅
_source保持原始格式是正常的:这是 ES 的设计 - ✅ 字符串数字可以自动转换:ES 会在查询时自动转换
- ❌ 但错误信息说的是数组,不是字符串或数字类型不匹配
第三阶段:尝试使用 Reindex API
由于 elasticdump 一直失败,尝试使用 ES 原生的 Reindex API:
json
POST _reindex
{
"source": {
"remote": {
"host": "https://source_host:9200"
},
"index": "source_index"
},
"dest": {
"index": "target_index"
}
}
遇到的问题:
- ❌ Reindex 需要配置白名单 (
reindex.remote.whitelist) - ❌ 白名单只能在 ES 配置文件中配置,无法动态修改
- ❌ 需要重启 ES 集群才能生效
- ❌ 由于无法修改 ES 配置,只能回到 elasticdump
第四阶段:关键发现
导出到本地文件检查
将 dump 改为先导出到本地 JSON 文件:
bash
NODE_TLS_REJECT_UNAUTHORIZED=0 elasticdump \
--input="https://user:pass@host:9200/index" \
--output="./dump_sample.json" \
--type=data \
--limit=10
发现同名字段
检查导出的 JSON 文件,发现同一行数据中有两个同名的 timestamp 字段:
json
{
"_source": {
"timestamp": "1508112000", // 第一个:正常值(字符串)
"timestamp": [1508112000] // 第二个:数组值!
// ... 其他字段
}
}
问题根源:Store: True
经过分析,发现问题根源是 store: true 配置:
Store: True 的行为
当字段设置了 store: true 时:
- ✅ 字段值会被单独存储 一份(除了
_source) - ✅ 可以使用
stored_fieldsAPI 获取 - ⚠️ 返回格式总是数组,即使只有一个值
为什么会出现数组?
Elasticsearch 的 stored_fields API 设计:
- 为了支持多值字段(multi-valued fields)
- 为了保持一致性,即使单值字段也返回数组格式
- 例如:
stored_fields.timestamp总是返回[1508112000]而不是1508112000
Elasticdump 的行为:
- 可能同时读取
_source和stored_fields - 合并数据时,将
stored_fields的数组值覆盖或合并到文档中 - 导致最终的
timestamp变成数组[1508112000]
写入目标索引时:
- 目标索引的 mapping 期望
timestamp是单个值(long类型) - 但实际收到的是数组
[1508112000] - Bulk API 在解析阶段就失败:
expected a simple value but found [START_ARRAY]
影响范围
所有设置了 store: true 的字段都可能遇到这个问题,包括:
timestamplat_lon- 所有动态模板中设置了
store: true的字段(如*_int,*_long,*_kwd等)
解决方案
方案1: 使用 searchBody 参数(推荐)
在 elasticdump 命令中添加 --searchBody 参数,明确指定只使用 _source:
bash
NODE_TLS_REJECT_UNAUTHORIZED=0 elasticdump \
--input="https://user:pass@host:9200/source_index" \
--output="https://user:pass@host:9200/target_index" \
--type=data \
--limit=100 \
--maxSockets=5 \
--timeout=600000 \
--retryAttempts=5 \
--scrollTime=10m \
--ignore-errors \
--quiet \
--searchBody='{"_source": true}'
优点:
- ✅ 一次性解决所有字段的问题
- ✅ 不需要修改 mapping
- ✅ 不需要重新创建索引
- ✅ 明确告诉 ES 只返回
_source,不返回任何stored_fields
工作原理:
- 明确指定只使用
_source - ES 不会返回
stored_fields - elasticdump 只读取
_source中的数据 - 所有字段(无论是否设置了
store: true)都从_source读取,格式正常
方案2: 移除 Store: True(不推荐)
如果移除所有 store: true:
- ❌ 需要修改 mapping
- ❌ 需要重新创建索引(已存在的索引无法直接修改
store属性) - ❌ 如果业务需要
stored_fields,会影响功能
方案3: 使用中间文件处理(备选)
如果 searchBody 不工作,可以先导出到文件,然后处理:
bash
# 1. 导出到文件
elasticdump --input="..." --output="./dump.json" --searchBody='{"_source": true}'
# 2. 使用脚本处理数组(如果需要)
python3 fix_timestamp.py dump.json dump_fixed.json
# 3. 导入到目标索引
elasticdump --input="./dump_fixed.json" --output="..." --type=data
技术要点总结
1. Elasticsearch 存储机制
_source字段:存储原始 JSON,不受 mapping 影响- 索引层:根据 mapping 类型索引,用于搜索
stored_fields:单独存储的字段值,返回格式为数组
2. Store: True 的影响
- ✅ 字段值被单独存储,可以通过
stored_fieldsAPI 获取 - ⚠️
stored_fieldsAPI 总是返回数组格式 - ⚠️ 在 dump 时可能导致字段数组化问题
3. Dump 工具的选择
- Elasticdump :灵活,支持各种参数,但需要注意
stored_fields问题 - Reindex API:原生支持,但需要配置白名单,不够灵活
4. 最佳实践
- ✅ 在 dump 时使用
--searchBody='{"_source": true}'明确指定只读取_source - ✅ 可以继续使用
store: true(如果业务需要),在 dump 时使用正确的参数即可 - ✅ 不需要为了 dump 而改变 mapping 设计
经验教训
- 深入理解 ES 存储机制 :
_source和索引层是分离的,mapping 不影响_source - 仔细分析错误信息:错误说"数组"而不是"类型不匹配",这是关键线索
- 导出到本地文件检查:这是定位问题的关键步骤
- 理解工具的行为 :elasticdump 可能同时读取
_source和stored_fields - 使用正确的参数 :
--searchBody参数可以解决所有store: true字段的问题