PAYATHON 2026

MacBook Pro运行PySpark时NumPy C-extensions导入失败

排障赵师傅

结论

这是进程架构不一致引起的动态库加载失败,通常与 SparkSession 配置、缺少 import 或 PYTHONPATH 无关。

错误信息:

have 'arm64', need 'x86_64'

表示当前 NumPy 扩展 _multiarray_umath...so 的架构是 arm64,而执行 spark-submit 时启动的 Python 进程运行在 x86_64 模式。x86_64 Python 进程无法加载 arm64 动态库。

这套环境中首先要排查 Intel 版 JDK。spark-submit 会先启动 JVM,再由 Spark 的 JVM 进程启动 Python。如果 JVM 是 x86_64,macOS 可能会让 universal Python 选择 x86_64 slice。直接从原生 Terminal 启动 Python 时,Python 使用 arm64 slice,因此可以正常导入 NumPy。

建议将整套软件统一为原生 arm64 架构:

arm64 JDK
  → arm64 Python
    → arm64 NumPy、pandas

不要单独替换 _multiarray_umath.cpython-310-darwin.so。NumPy 包含多个编译扩展和底层依赖,只复制一个 .so 文件很容易引发新的 ABI 或架构冲突。

确认实际运行架构

除了检查 Python 文件是否为 universal binary,还需要确认进程启动后实际选择了哪种架构。

检查 Terminal 中的 Python

python3 -c 'import platform, sys; print(sys.executable); print(platform.machine())'

在 Apple silicon 原生环境中,输出应包含:

arm64

检查 Java

echo "$JAVA_HOME"
file "$JAVA_HOME/bin/java"
java -XshowSettings:properties -version 2>&1 | grep os.arch

如果输出类似:

Mach-O 64-bit executable x86_64

或者:

os.arch = x86_64

说明 Spark 当前使用的是 Intel 版 Java。它可以通过 Rosetta 2 正常运行 Spark SQL,但后续启动的 universal Python 可能会选择 x86_64。

检查 spark-submit 中的 Python

新建一个最小测试脚本:

import os
import platform
import sys

print("executable:", sys.executable)
print("machine:", platform.machine())
print("PYSPARK_PYTHON:", os.environ.get("PYSPARK_PYTHON"))

import numpy as np

print("NumPy:", np.__version__)
print("NumPy path:", np.__file__)

运行:

spark-submit check_arch.py

如果普通 python3 check_arch.py 显示 arm64,而 spark-submit check_arch.py 显示 x86_64,或在导入 NumPy 时失败,就可以确认两种启动方式使用了不同架构。

推荐方案:换成 arm64 JDK

安装适用于 Apple silicon 的 JDK。Spark 3.2.1 需要使用该版本支持的 Java,并确认下载包明确标注为 macOS ARM64、AArch64 或 Apple silicon。版本号本身不能说明架构。

安装后列出系统识别到的 JDK:

/usr/libexec/java_home -V

将 JAVA_HOME 指向 arm64 JDK 的实际目录。例如:

export JAVA_HOME="/Library/Java/JavaVirtualMachines/<arm64-jdk>/Contents/Home"
export PATH="$JAVA_HOME/bin:$PATH"

将 <arm64-jdk> 替换为本机的实际安装目录。修改后重新打开 Terminal,或重新加载配置:

source ~/.bash_profile

再次验证:

file "$JAVA_HOME/bin/java"
java -XshowSettings:properties -version 2>&1 | grep os.arch

输出应为 arm64 或 aarch64,具体名称取决于 JDK。

显式指定 PySpark 使用的 Python

设置 PYSPARK_PYTHON=python3 时,系统会根据 PATH 查找解释器。如果机器上安装了多个 Python,建议直接填写绝对路径:

export PYSPARK_PYTHON="/Library/Frameworks/Python.framework/Versions/3.10/bin/python3"

在本地模式下,还可以让 driver 使用同一个解释器:

export PYSPARK_DRIVER_PYTHON="$PYSPARK_PYTHON"

相关配置可以整理为:

export JAVA_HOME="/Library/Java/JavaVirtualMachines/<arm64-jdk>/Contents/Home"
export SPARK_HOME="/Users/.../Spark2/spark-3.2.1-bin-hadoop2.7"

export PYSPARK_PYTHON="/Library/Frameworks/Python.framework/Versions/3.10/bin/python3"
export PYSPARK_DRIVER_PYTHON="$PYSPARK_PYTHON"

export PATH="$JAVA_HOME/bin:$SPARK_HOME/bin:$SPARK_HOME/sbin:/Library/Frameworks/Python.framework/Versions/3.10/bin:$PATH"
export PYTHONPATH="$SPARK_HOME/python:${PYTHONPATH:-}"

这里的配置用于排查本机 local 模式。使用 spark-submit 的集群部署模式时,不要随意设置 PYSPARK_DRIVER_PYTHON。

修改后检查各个程序的实际入口:

which java
which python3
which spark-submit

"$JAVA_HOME/bin/java" -XshowSettings:properties -version 2>&1 | grep os.arch
"$PYSPARK_PYTHON" -c 'import platform, sys; print(sys.executable); print(platform.machine())'

在同一个 Python 环境中重新安装依赖

先确认 pip 属于准备交给 PySpark 使用的解释器:

"$PYSPARK_PYTHON" -m pip --version
"$PYSPARK_PYTHON" -m pip show numpy pandas

不要直接使用 pip 或 pip3,它们可能对应另一个 Python 安装。

如果现有环境中混装过不同架构的软件包,建议创建一个干净的虚拟环境:

"$PYSPARK_PYTHON" -m venv "$HOME/venvs/pyspark-arm64"
source "$HOME/venvs/pyspark-arm64/bin/activate"

python -m pip install --upgrade pip
python -m pip install numpy pandas

然后让 PySpark 使用该虚拟环境:

export PYSPARK_PYTHON="$HOME/venvs/pyspark-arm64/bin/python"
export PYSPARK_DRIVER_PYTHON="$PYSPARK_PYTHON"

验证安装结果:

"$PYSPARK_PYTHON" -c 'import platform, numpy, pandas; print(platform.machine()); print(numpy.__version__); print(pandas.__version__)'

也可以检查 NumPy 扩展的架构:

file "$("$PYSPARK_PYTHON" -c 'import numpy.core._multiarray_umath as m; print(m.__file__)')"

最小验证程序

统一环境后,先运行一个不涉及 Hive 和数据表的脚本:

import platform
import sys

import numpy as np
import pandas as pd
from pyspark.ml.feature import StringIndexer
from pyspark.sql import SparkSession

print("Python:", sys.executable)
print("Architecture:", platform.machine())
print("NumPy:", np.__version__)
print("pandas:", pd.__version__)

spark = (
    SparkSession.builder
    .master("local[*]")
    .appName("import-test")
    .getOrCreate()
)

df = spark.createDataFrame(
    [("a",), ("b",), ("a",)],
    ["category"],
)

model = StringIndexer(
    inputCol="category",
    outputCol="category_index",
).fit(df)

model.transform(df).show()

spark.stop()

运行:

spark-submit import_test.py

这个程序可以单独验证架构和模块导入,避免 Hive、数据库目录或表创建等操作干扰排查。

为什么 pyspark.sql 正常,pyspark.ml 却失败

导入基本的 pyspark.sql 主要涉及 Python 和 JVM 代码,不一定会立即加载 NumPy 的原生扩展。

pyspark.ml 的部分模块会在导入时加载 NumPy,pandas 也依赖编译后的扩展。执行这些 import 后,macOS 动态加载器才会检查 Python 进程与 .so 文件的架构是否一致。

因此,问题不在 pyspark.ml 的 import 写法。它只是更早触发了原生扩展加载,从而暴露出混合架构环境。

如果必须保留 Intel 版 Java

也可以将整套 Python 环境统一为 x86_64:

x86_64 JDK
  → x86_64 Python
    → x86_64 NumPy、pandas

这通常需要在 Rosetta 环境中安装并运行 Intel 版 Python 及其依赖。x86_64 Python 无法加载 arm64 NumPy。

必须依赖 Intel 原生库的旧环境可以采用这套方案。不过在 M1 Max 上,使用原生 arm64 JDK、Python 和依赖通常更容易维护,也能避免混用 Rosetta 与原生组件。

注意事项

  • universal Python 虽然同时包含两种架构,但一次运行只能选择其中一种,也只能加载对应架构的动态库。
  • need 'x86_64' 表示当前 Python 进程需要 x86_64 库,并非 M1 Max 硬件要求使用 x86_64。
  • Python 3.10.1 与 3.10.2 的补丁版本差异无法解释这条明确的 Mach-O 架构错误。
  • PATH 只决定选用哪个可执行文件,不能让正在运行的 x86_64 进程加载 arm64 扩展。
  • PYTHONPATH 用于查找 Python 模块,无法解决二进制架构冲突。
  • 不要手动下载并覆盖单个 NumPy .so 文件。应通过目标解释器的 python -m pip 或环境管理器重新安装完整软件包。
  • 修改环境后,启动新的 Terminal,再检查 JAVA_HOME、Java 架构以及 spark-submit 中的 platform.machine(),以免继续使用旧的环境变量。

备注:内容仅供参考。