Apache Auron 8.0.0-incubating 进阶实战详解

以五组实测实验剖析 Apache Auron 8.0.0-incubating 的进阶使用:join 三种连接计划的生成与替换(广播/SortMergeJoin/forceShuffledHashJoin)、原生窗口函数、跨机 jar 分发的 glibc 双向兼容边界、跳过原生引擎构建的产物差异与运行期行为、range 内联查询不转换的识别方法,并汇总全部本地计时数据与适用边界,命令回显均实测通过。

Apache AuronApache Auron2026/10/15

Apache Auron 8.0.0-incubating 进阶实战详解

目标:用五组可复现实验回答使用 Auron 时最容易踩坑的五个问题——join 计划怎么选与怎么被替换、窗口函数是否原生执行、构建产物能不能跨机器分发、快速构建产物能不能跑生产、哪些查询根本不会被转换;本文全部命令与回显均实测通过。

本文延续第 4 篇的构建与环境(Ubuntu 22.04 + JDK 17 + Spark 3.5.9 本地模式 + Auron 8.0.0-incubating fat jar),实验方法上统一使用 spark.log.level=INFO 观察每个 Stage 的 Auron convert result 段来确认原生节点替换,用 Time taken 与结果值双重核对。所有计时均为单机 local 模式单次运行,仅用于功能与行为验证,不代表集群性能结论。

一、环境信息

本节先固定可复现的基线环境,本文全部实验在此环境执行。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • JDK:OpenJDK 17.0.20.1
  • Spark:3.5.9(本地模式,/root/spark)
  • Auron:8.0.0-incubating(fat jar auron-spark-3.5_2.12-pre-linux-x86_64-8.0.0-incubating.jar,44.6 MB,内含 libauron.so)
  • 数据:t1(300 万行 Parquet 事实表,键 k 1000 个分组,v = id/7);t2(2000 行 Parquet 维表,键 k、标签 tag 10 个分组)
  • 对照系统(glibc 分发实验用):CentOS 8.4.2105(glibc 2.28)上构建的同版本组合 jar
ls -lh /root/spark/jars/ | grep auron
-rw-r--r-- 1 root root  43M Oct 15 07:27 auron-spark-3.5_2.12-pre-linux-x86_64-8.0.0-incubating.jar
-- t1 / t2 建表口径(全文,/root/sql/setup.sql 与 setup2.sql)
CREATE TABLE IF NOT EXISTS t1 (id BIGINT, k STRING, v DOUBLE) USING parquet;
INSERT OVERWRITE t1 SELECT id AS id, concat('k', CAST(id % 1000 AS STRING)) AS k, CAST(id AS DOUBLE)/7 AS v FROM range(0, 3000000);
CREATE TABLE IF NOT EXISTS t2 (k STRING, tag STRING) USING parquet;
INSERT OVERWRITE t2 SELECT concat('k', CAST(id % 1000 AS STRING)), concat('tag', CAST(id % 10 AS STRING)) FROM range(0, 2000);

二、实战一:join 的三种计划与原生替换

join 是 Auron 收益最典型的场景,也是配置交互最多的场景。同一条 join 聚合查询(/root/sql/qjoin.sql,全文):

SELECT t2.tag, count(*) AS c, ROUND(SUM(t1.v),1) AS s FROM t1 JOIN t2 ON t1.k = t2.k GROUP BY t2.tag ORDER BY t2.tag;

3M 行事实表 × 2 倍键放大 = 600 万连接结果,10 个 tag 分组。三种配置下的实测:

2.1 默认配置:广播哈希连接

t2 仅 2000 行,AQE 自动选择 BroadcastHashJoin,原生节点与结果(实测):

/root/spark/bin/spark-sql --conf spark.log.level=INFO -f /root/sql/qjoin.sql > /tmp/qjoin.out 2>/tmp/qjoin.err
head -2 /tmp/qjoin.out
grep -A6 'Auron convert result for current stage' /tmp/qjoin.err | grep -oE 'Native[A-Za-z]+' | sort -u
grep -E 'Time taken' /tmp/qjoin.err | tail -1
tag0    600000  1.28571E11
tag1    600000  1.285710857143E11
NativeBroadcastExchange
NativeBroadcastJoin
NativeFilter
NativeHashAggregate
NativeParquetScan
NativeProject
NativeShuffleExchange
NativeSort
Time taken: 37.884 seconds, Fetched 10 row(s)

结果核对:每个 tag 600,000 行(300 万 × 每 k 2 条 t2 记录 ÷ 10 tag),SUM(v) 总和 6.428569285714E11 × 2 ÷ 10 ≈ 1.28571E11,与 tag0 行完全一致。

2.2 关闭广播:原生 SortMergeJoin

/root/spark/bin/spark-sql --conf spark.log.level=INFO --conf spark.sql.autoBroadcastJoinThreshold=-1 -f /root/sql/qjoin.sql > /tmp/qjoin_smj.out 2>/tmp/qjoin_smj.err
grep -A6 'Auron convert result for current stage' /tmp/qjoin_smj.err | grep -oE 'Native[A-Za-z]+' | sort -u
grep -E 'Time taken' /tmp/qjoin_smj.err | tail -1
NativeFilter
NativeHashAggregate
NativeParquetScan
NativeProject
NativeShuffleExchange
NativeSort
NativeSortMergeJoin
Time taken: 19.506 seconds, Fetched 10 row(s)

2.3 强制洗牌哈希连接:forceShuffledHashJoin

/root/spark/bin/spark-sql --conf spark.log.level=INFO --conf spark.sql.autoBroadcastJoinThreshold=-1 --conf spark.auron.forceShuffledHashJoin=true -f /root/sql/qjoin.sql > /tmp/qjoin_shj.out 2>/tmp/qjoin_shj.err
grep -A6 'Auron convert result for current stage' /tmp/qjoin_shj.err | grep -oE 'Native[A-Za-z]+' | sort -u
grep -E 'Time taken' /tmp/qjoin_shj.err | tail -1
NativeFilter
NativeHashAggregate
NativeParquetScan
NativeProject
NativeShuffleExchange
NativeShuffledHashJoin
NativeSort
Time taken: 15.339 seconds, Fetched 10 row(s)

2.4 三种计划的判读

计划触发条件原生 join 节点本机实测
广播哈希连接默认(小表触发自动广播)NativeBroadcastJoin37.884 s
排序合并连接autoBroadcastJoinThreshold=-1NativeSortMergeJoin19.506 s
洗牌哈希连接同上 + spark.auron.forceShuffledHashJoin=trueNativeShuffledHashJoin15.339 s
  • 三种配置下结果逐行一致(10 组 tag 全对上);
  • forceShuffledHashJoin=true 的作用是把(原生)SMJ 换成(原生)SHJ,本例连接键基数低(1000 个 k),免排序的哈希路径更快;
  • 本例广播计划反而最慢的原因:600 万行 join 结果全量经广播重建 + INFO 级日志开销,单机 local 模式放大了广播物化成本;集群下广播通常是小维表场景首选。请勿拿本表数字直接外推,应视为「配置如何改变原生计划」的演示。

三、实战二:原生窗口函数

窗口函数是易被忽略的原生覆盖点。/root/sql/qwin.sql(全文):

SELECT k, v, ROW_NUMBER() OVER (PARTITION BY k ORDER BY v) AS rn FROM (SELECT id, k, v FROM t1 WHERE id < 20000) s LIMIT 10;
/root/spark/bin/spark-sql --conf spark.log.level=INFO -f /root/sql/qwin.sql > /tmp/qwin.out 2>/tmp/qwin.err
cat /tmp/qwin.out
grep -A6 'Auron convert result for current stage' /tmp/qwin.err | grep -oE 'Native[A-Za-z]+' | sort -u
grep -E 'Time taken' /tmp/qwin.err | tail -1
k0      0.0     1
k0      142.85714285714286      2
k0      285.7142857142857       3
k0      428.57142857142856      4
k0      571.4285714285714       5
k0      714.2857142857143       6
k0      857.1428571428571       7
k0      1000.0  8
k0      1142.857142857143       9
k0      1285.7142857142858      10
NativeCollectLimit
NativeFilter
NativeParquetScan
NativeProject
NativeShuffleExchange
NativeSort
NativeWindow
NativeWindow (convertible=false, strategy=Default)
Time taken: 10.606 seconds, Fetched 10 row(s)

验证点:结果为 k0 分组内 v 升序的行号 1–10,语义正确;NativeWindow 出现在转换结果中,说明窗口算子本身被原生执行(同 Stage 内过滤、扫描、投影、洗牌、排序也全部原生)。LIMIT 10 由 NativeCollectLimit 承接。

四、实战三:跨机 jar 分发的 glibc 边界(双向实测)

fat jar 里的 libauron.so 在首次使用时被解压到 /tmp/libauron-<随机串>.tmp 并由 SparkAuronAdaptor.loadAuronLib 加载,它对构建机的 glibc 有符号版本要求。本文用两台不同基线的「机器」做了双向分发实验(同组合:Spark 3.5 + Scala 2.12,JDK 17):

4.1 低版本构建 → 高版本部署:可行

把 CentOS 8(glibc 2.28)构建的 jar 放到 glibc 2.35 的 Ubuntu 22.04 Spark 上执行同一聚合查询:

k0      3000    6.426428571E8
k1      3000    6.426432857E8
...
k102    3000    6.426865714E8
Time taken: 5.713 seconds, Fetched 20 row(s)
NativeHashAggregate
NativeParquetScan
NativeProject
NativeShuffleExchange
NativeTakeOrdered

(输出节选:查询成功,五类原生节点全部出现,耗时与本机构建 jar 的同查询一致。)

4.2 高版本构建 → 低版本部署:失败

反向把 Ubuntu 22.04(glibc 2.35)构建的 jar 放到 CentOS 8(glibc 2.28)Spark 上,JVM 侧计划转换照常打印(convert result 段仍有 Native 节点),但任务运行期原生库加载失败:

java.lang.UnsatisfiedLinkError: /tmp/libauron-17827442579042209809.tmp: /lib64/libm.so.6: version `GLIBC_2.29' not found (required by /tmp/libauron-17827442579042209809.tmp)
        at org.apache.auron.jni.SparkAuronAdaptor.loadAuronLib(SparkAuronAdaptor.java:49)

4.3 实战结论

  1. 把构建放在集群里 glibc 最低的那类系统上(CentOS 8 glibc 2.28 是常见发行版里偏低的基线,其构建产物对高版本系统前向兼容——4.1 节实测通过);
  2. 排障时见到 UnsatisfiedLinkError ... /tmp/libauron-*.tmp,先查 glibc:ldd --version 对比构建机与目标机;
  3. 除 glibc 外还有 CPU 指令集约束(构建默认 -C target-cpu=native),跨代际分发要求目标机指令集不低于构建机;
  4. 完整的 CentOS 8 构建路径与源修复过程见第 2 篇《CentOS 8 安装 Apache Auron 指南》。

五、实战四:跳过原生引擎的构建取舍

改 Java/Scala 侧代码后需要快速编译回环时,-DskipBuildNative 可以跳过 Rust 段。实测(同版本、同参数,仅差 -DskipBuildNative,注意 8.0.0-incubating 需要用 -T1 -DskipBuildNative 变通写法,见第 4 篇 2.2 节):

对比项全量构建(方案 A)跳过原生构建
产物体积44.6 MB14 MB
jar 内 libauron.so有(约 106 MB 未压缩)无(条目数 0)
构建时长约 24 分钟(12 核首建)数分钟到十余分钟
运行加速查询正常失败

快速构建产物跑同一查询的实测结果(部署方式与第 1 篇完全一致):

java.lang.NoClassDefFoundError: Could not initialize class org.apache.auron.jni.AuronCallNativeWrapper

结论:快速构建产物不能投入运行,仅用于「编译过没过」的 CI 回环;一旦误部署,症状就是会话能起、查询在原生初始化处集中报 NoClassDefFoundError。可运行产物必须来自全量构建或受信分发源(glibc 边界见第四章)。

六、实战五:识别「根本不会转换」的查询

不是所有查询都会被原生接管。典型反例:range 内联数据源。/root/sql/qrange.sql(全文):

SELECT k, count(*) AS c, ROUND(SUM(v),1) AS s FROM (SELECT id AS id, concat('k', CAST(id % 1000 AS STRING)) AS k, CAST(id AS DOUBLE)/7 AS v FROM range(0, 3000000)) t GROUP BY k ORDER BY k LIMIT 20;
/root/spark/bin/spark-sql --conf spark.log.level=INFO -f /root/sql/qrange.sql > /tmp/qrange.out 2>/tmp/qrange.err
grep -E 'Time taken' /tmp/qrange.err | tail -1
grep -A1 'Auron convert result for current stage' /tmp/qrange.err | head -2
grep -A6 'Auron convert result for current stage' /tmp/qrange.err | grep -coE 'Native[A-Za-z]+'
26/10/15 08:31:45 INFO SparkSQLCLIDriver: Time taken: 5.301 seconds, Fetched 20 row(s)
26/10/15 08:31:42 INFO AuronSparkSessionExtension:  + AdaptiveSparkPlan (convertible=false, strategy=NeverConvert)
0

判读:查询正常返回(结果与 Parquet 版逐行一致),但整段 Stage 被标为 NeverConvert、Native* 节点统计为 0——range 内联数据源不支持原生扫描,连带整段回退 JVM。「日志里没有 convert result 或没有 Native 节点」本身就是排查入口:先查 Stage 内是否混有不支持的数据源(range/内联)、表达式或 UDF,再看算子开关(第 4 篇 4.2 节)。

七、实测数据汇总与边界声明

本文全部本地计时(单机 local[*] 或 local[4]、单次运行、JDK 17、Spark 3.5.9):

实验配置耗时关键原生节点
300 万行聚合开启 Auron(INFO 日志)5.569 sNativeParquetScan/NativeHashAggregate/NativeShuffleExchange/NativeTakeOrdered
300 万行聚合开启 Auron(WARN 日志)5.713 s同上
300 万行聚合spark.auron.enable=false7.696 s无(NativeHelper 出现 0 次)
300 万行聚合enable.scan.parquet=false10.143 s无(整段 NeverConvert)
600 万行 join默认广播37.884 sNativeBroadcastJoin
600 万行 join禁广播(SMJ)19.506 sNativeSortMergeJoin
600 万行 joinforceShuffledHashJoin15.339 sNativeShuffledHashJoin
窗口查询(2 万行子集)开启 Auron10.606 sNativeWindow
range 内联聚合开启 Auron5.301 s无(整段 NeverConvert)

边界声明(务必阅读):

  1. 所有计时为单机单次运行,且不同实验间日志级别不完全一致(INFO 会带来可观的日志开销),只用于确认「行为与节点替换符合预期」,不能作为性能基准;
  2. 本机观测到的收益(聚合场景约 26%~28%)来自向量化扫描与聚合的本地开销削减;Auron 的目标收益场景是分布式集群的网络洗牌与扫描 IO,集群基线请用真实负载另行建立;
  3. 聚合、扫描、投影、排序、TopN、窗口、三种 join 的原生覆盖均已实测确认;数据写出(spark.auron.enable.data.writing)默认关闭,本文未实测写路径。

八、常见问题(FAQ)

Q1:forceShuffledHashJoin 对广播连接有效吗? 无效。本实验中默认广播计划(NativeBroadcastJoin)不受该开关影响;它替换的是 SortMergeJoin(2.4 节对照)。

Q2:开了 Auron 为什么有的查询完全没变快? 先看日志有没有 Native 节点(第六章):range/内联数据源、不支持的表达式会把整个 Stage 打回 JVM;再看是不是单机小数据——本地模式下 JVM 起点很快,原生收益主要在大扫描/大聚合/网络洗牌上。

Q3:怎么确认手上的 jar 是全量构建产物? unzip -l auron-*.jar | grep libauron 有条目即可运行;再 unzip -p ... auron-build-info.properties 核对构建组合与时间戳。

Q4:UnsatisfiedLinkError ... GLIBC_2.29 not found 在目标机怎么自查? ldd --version 看目标机 glibc,与构建机的比较;低于构建机就需要换低 glibc 构建机重出 jar(第四章实测边界)。

Q5:窗口函数都支持吗? 本文实测了 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 的原生执行;窗口函数族很大,个别函数可能回退 JVM,判读方法同第六章(看 Stage 是否 NeverConvert)。

九、参考链接

  1. 官方 Configuration 文档(join/算子开关清单):https://auron.apache.org/documents/configuration.html
  2. 8.0.0-incubating 版本说明:https://auron.apache.org/archives/v8.0.0-incubating.html
  3. 第 1 篇《Ubuntu 22.04 安装 Apache Auron 指南》(构建产物来源)
  4. 第 2 篇《CentOS 8 安装 Apache Auron 指南》(低 glibc 构建机路径与 6.1 节分发实验)
  5. 第 4 篇《Apache Auron 8.0.0-incubating 常用配置与命令详解》(配置清单与开关行为)