ES|QL|profile 和文本格式不能一起用了,以前是静默忽略

9.2 起 ES|QL 请求如果同时带 profile 和文本格式(format=txt / csv),会直接报错。以前不报错——但也不是真的给了你 profile 信息。从"悄悄不给"变成"明确告诉你不行",这个变化比看起来重要。

说明

实测环境:本地起的 Elasticsearch 9.4.2 单节点实例(官方 tar 包直接跑,堆内存 512MB,安全与机器学习关闭),curl 连本地 9200 端口。本文「实测」小节的命令和输出都出自这个实例。

来源和边界先交代:本文的依据出自 v9.4.2 标签下的官方文件(9.2.0.yml、EsqlMediaTypeParser.java),文中引用的原文均来自这些文件。示例取自官方示例;哪些是文档写明、哪些是我的推断,正文里逐处标注。

图 1:官方文件 9.2.0.yml(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 9.2.0.yml

官方条目

9.2 的 changelog 标为 feature(9.2.0.yml 第 66 行 @ v9.4.2),PR 128627:

Fail `profile` on text response formats

只有一行 summary。实质内容来自源码。

图 2:官方文件 EsqlMediaTypeParser.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 EsqlMediaTypeParser.java

profile 和响应格式

先说这两个东西。

profile:让 ES 返回查询的执行剖析信息——每个算子花了多少时间、处理了多少行、有没有走优化。用来排查"为什么这条查询慢"。

响应格式:ES|QL 可以返回多种格式:

  • json(结构化,能表达嵌套);
  • txt / csv(文本格式,扁平表格);
  • 还有一些其它格式。

问题就在这两个的组合上。

为什么不能一起用

profile 的输出是深度嵌套的树状结构:查询计划 → 各阶段 → 各算子 → 每个算子内部的计时和计数。

文本格式是扁平的表格——有行有列,没有嵌套。

把一棵树存成一张表,只能丢掉大部分信息。

所以源码的处理是:直接拒绝,而不是"尽力给你输出一个残缺版本"。

图 3:官方页面 PR #128627。来源:GitHub elastic/elasticsearch pull/128627

源码里的实现

校验在 EsqlMediaTypeParser.java 第 88–92 行(源码 @ v9.4.2):

private static void validateProfile(boolean profile, MediaType fromMediaType) {
    if (profile && fromMediaType instanceof TextFormat) {
        throw invalid("profile");
    }
}

逻辑非常直白:profile 开了,且格式是文本 → 抛异常。

报错文案在下面几行的 invalid() 里:

private static IllegalArgumentException invalid(String argument) {
    return new IllegalArgumentException(
        "Invalid use of [" + argument + "] argument: cannot be used in combination with "

(后面还有一段说明该用哪种格式,上面只引了开头——完整文案就在同一个方法里,遇到时能直接看到。)

同一处还有另一个参数

这是翻源码时的一个额外发现,changelog 里没提。

就在 validateProfile 上面几行(第 82–86 行):

private static void validateIncludeExecutionMetadata(Boolean includeExecutionMetadata, MediaType fromMediaType) {
    if (Boolean.TRUE.equals(includeExecutionMetadata) && fromMediaType instanceof TextFormat) {
        throw invalid("include_execution_metadata");
    }
}

include_execution_metadata 也有同样的限制——它要加的也是结构化元数据,同样塞不进扁平表格。

两个参数共用同一个 invalid() 方法,所以报错文案的措辞是一样的,只是中括号里的参数名不同。

(说明:这两个校验是否同一次改动引入的,未查证。只确认 9.4.2 里两者并存。)

为什么"静默"比"报错"更麻烦

上面那句"宁可报错"值得展开说,因为它是判断改动好坏的一个常用标准。

报错的成本:你立刻知道有问题,去查,改掉。成本是一次性的、可预期的。

静默的成本:你不知道有问题,拿着不完整的结果继续做事,直到某个下游环节出错才暴露——那时候排查范围已经扩大了很多倍。

举个本篇的具体例子:假设你在调优一条慢查询,用了 format=txt 加 profile。

  • 静默的情况:返回一个表格,你看到"没有 profile 信息",可能以为是这次查询没产生剖析数据,于是换个思路继续试——时间在错误的方向上消耗掉了;
  • 报错的情况:一句话告诉你"这个组合不行,换格式",十秒钟解决。

差别在于信息量:报错把原因直接给了你,静默什么都没说。

所以判断标准是——一条改动如果在"失败"和"静默"之间选了失败,通常是对的。_source mode 设置迁移、discovery-ec2 换 SDK v2 里批评过静默失效,理由就是同一个。

当然这条标准有边界:如果是正常使用路径因为一个无关紧要的事就报错,那是过度严格。本篇不属于这种情况——profile 和文本格式本来就是语义冲突的组合,不属于正常使用。

从"静默"到"报错"才是重点

标题里写了"以前是静默忽略"。这里说清楚,因为它决定了这条改动的价值。

以前:你请求 profile + format=txt,ES 不报错,返回一个表格——但里面没有 profile 信息(或者信息不完整)。

你会以为自己拿到了剖析数据,实际上没有。 基于该输出做的性能分析,可能是错的。

现在:直接报错,明确告诉你这个组合不行,让你换成 json。

这个变化属于"把静默问题变成明确失败"。 前面几篇里反复遇到这类区分:

篇改动属于哪类
_source mode 设置迁移_source.mode 搬家静默失效(坏)
discovery-ec2 换 SDK v2ec2 配置失效静默失效(坏)
本篇profile 与文本格式冲突静默 → 报错(好)

对这条的评价是正面的:它把一个可能误导人的静默行为,改成了明确的失败。结论是宁可报错,也不要给一个看起来正常但不完整的结果。

对使用者的影响

如果脚本同时用了这两者,9.2 之后会开始报错。

这听起来是坏消息,但要看清性质:以前它也没有给你 profile 数据。所以:

  • 如果你依赖了该输出做分析 → 那分析基础本来就有问题,报错是在帮你发现;
  • 如果你只是随手带上 profile 没真看 → 去掉这个参数就行。

真正的风险在没人注意的自动化脚本里。 比如某个巡检任务固定带 profile=true 和 format=csv,升级后突然开始失败。这类失败是好事(暴露了问题),但会造成告警——所以升级前最好扫一遍。

怎么改

要么去掉 profile,要么换成结构化格式。 取决于你要什么:

要看 profile → 用 json 格式:

curl -s 'localhost:9200/_query?format=json' -H 'Content-Type: application/json' -d '{
  "query": "FROM logs | STATS c = COUNT(*) BY host",
  "profile": true
}'

只要表格 → 去掉 profile:

curl -s 'localhost:9200/_query?format=csv' -H 'Content-Type: application/json' -d '{
  "query": "FROM logs | STATS c = COUNT(*) BY host"
}'

(说明:上面两条命令未实际执行,参数写法以你版本的文档为准。ES|QL 的请求入口和参数名在不同版本可能有差异。)

profile 输出大概长什么样

没见过 profile 输出的话,可能不好理解为啥它塞不进表格。这里说个大概结构(以下为示意,非实际输出的逐字拷贝)。

format=json 下,profile 部分大致是分层的:

{
  "profile": {
    "drivers": [
      {
        "description": "...",
        "operators": [
          { "operator": "...", "status": { "processed_rows": 1000 } }
        ]
      }
    ]
  }
}

重点看它的层次:profile → drivers(数组)→ 每个 driver 有 operators(又是数组)→ 每个 operator 有 status(又是对象)。

至少三层嵌套,中间还有数组。

而 csv 格式能表达的是:

host,count
a,10
b,20

行和列,没有嵌套。

把三层结构压成两列,只能:要么把整个 profile 放进一个单元格(那不如直接看 json),要么拆成几百行(那就失去了可读性)。两条路都不合理,所以拒绝是对的。

(说明:上面这段 JSON 是构造的示意,用来说明层次问题,不是 ES 实际返回的格式。真实结构请以你版本的输出为准。)

排查建议

因为这是"新增报错"而不是"行为改变",排查方式很直接:

一,看报错信息。 文案里会明确点出是哪个参数([profile] 还是 [include_execution_metadata]),一眼能定位。

二,搜代码和脚本里这两个参数的用法。 重点找把它们和文本格式一起用的地方:

# 在脚本里找同时出现 profile 和 txt/csv 的请求
grep -rn "profile.*\(txt\|csv\)\|\(txt\|csv\).*profile" /path/to/your/scripts/

三,升级前扫一遍比升级后救火便宜。 这条改动不会造成数据问题,但会让脚本失败——在批量任务里可能触发一连串告警。

和 ES|QL 其它参数限制放一起看

ES|QL 有不少"参数之间互斥"的限制,本篇是一个例子。把能想到的同类整理一下:

组合结果
profile + 文本格式报错(本篇)
include_execution_metadata + 文本格式报错(同源码处)
ccs_minimize_roundtrips + PIT报错(RestSearchAction.java:425)

共同点:这些组合是"实现了也说不清楚",不是"不能实现"。与其给个含糊的结果,不如拒绝。

这形成了一个可以类推的判断:在 ES 里遇到"某两个参数不能一起用",通常是因为它们的组合语义不清晰,而不是技术做不到。 顺着这个思路去理解,比死记这些限制要省力。

也意味着:这类限制可能随版本增加(以前没发现冲突,后来发现了)。所以升级后出现新的参数冲突报错,先理解它为什么冲突,再决定怎么绕——不要第一反应就是"以前能用怎么现在不行了"。

这条改动怎么看

这条改动很小,但类型值得记住:它是"新增一个明确失败"来替代"原有的静默不完整"。

按改动的分类方式:

维度判断
是否改变查询结果否(只影响 profile 信息)
是否阻塞升级否(除非脚本依赖这个组合)
排查成本低(报错文案直接点名参数)
修复成本低(二选一去掉一个)

所以它的处理优先级不高——不是升级前必须处理的类型。但如果自动化脚本用得多,值得在升级前后留意一下有没有新增失败。

改动前是不是静默忽略

changelog 只写了 "Fail",没有描述改动前的行为。 标题和正文里说"以前是静默忽略"——这是理解,不是官方说明。

支持这个理解的是:改动的措辞是 "Fail"(让某事失败),暗示它以前不失败。而"不失败"只有两种可能——要么正常返回 profile(那现在改成失败就说不通),要么静默不给。

但未实测 9.1 的行为来确认。 如果你需要确定的答案,在 9.1 上跑一次该组合看返回什么就行。

把这个不确定性写出来,是因为它影响对这条改动的定性:

  • 如果以前是静默 → 这条是改进(把隐患变成明确报错);
  • 如果以前其实给了某种简化版 profile → 这条是能力收缩(虽然可能性不大)。

按推断前者概率高得多,但"高概率"不等于"确认",所以用的时候可以自己验证一下。

在 9.4.2 上跑一遍

同一个查询,只换 format:

图 4:本地 9.4.2 单节点实例上的实测输出。上:默认 json 格式;下:format=txt

curl -s -X POST "http://localhost:9200/_query" \
  -H 'Content-Type: application/json' -d '{"query":"ROW x = 1","profile":true}' \
  | jq -c 'keys'

curl -s -X POST "http://localhost:9200/_query?format=txt" \
  -H 'Content-Type: application/json' -d '{"query":"ROW x = 1","profile":true}' \
  | jq -r '"HTTP \(.status): \(.error.root_cause[0].reason)"'

默认格式返回的 key 里有 profile;换成 txt 直接 400,报错是 Invalid use of [profile] argument: cannot be used in combination with [txt, csv, tsv] formats。

这句报错把可用的格式都列出来了,照着改就行。真正容易出事的是改之前:如果某个脚本一直用 format=txt 配 profile=true 跑性能分析,升级前它只是拿不到 profile 段,升级后整条查询都失败——从"少一块数据"变成"没有数据",监控上的表现完全不同。

结论是:profile 只在 json 响应里有意义,分析脚本要单独走一条路径;把 format=txt 和 profile 写在同一处的地方,升级前先搜一遍。

写在最后

很小的改动:一个参数组合从"悄悄不给"变成"明确报错"。

一,profile 和文本格式(txt/csv)不能同时用,用了会报错。 原因是 profile 输出是嵌套树,塞不进扁平表格——这个冲突是语义层面的,不是实现偷懒。

二,include_execution_metadata 有同样的限制。 这条 changelog 没提,是从源码里看到的——两个参数共用同一个 invalid() 方法,报错措辞一致,只有参数名不同。

三,这条改动是好的——它替代的是"给你一个看起来正常但不完整的输出"这种更麻烦的行为。不过"改动前是静默"这一点是理解,changelog 只写了 "Fail",9.1 的实际行为未实测。

另外诚实标注两处:两个校验是否同一次改动引入未查证(只确认 9.4.2 里并存);示例命令未执行。

参考

本文对照 Elasticsearch v9.4.2 标签的 changelog 与源码核对,引文与代码逐字取自原文。**"以前是静默忽略"**是对该改动性质的理解,changelog 只说 "Fail",未描述改动前行为;两个校验是否同一次改动引入未查证,仅确认 9.4.2 中并存;两条命令已在本地 9.4.2 单节点实例上执行,图 4 的报错为完整原文。

0
0
0
0
评论
未登录
暂无评论