PAYATHON 2026

PySpark 在 Docker 中访问 Moto 为何报 bucket is null/empty?

数据小许

先把 bucket 名称从 test_bucket 改为 test-bucket,同时修改创建 bucket 和生成 s3a:// 路径的代码。示例中的下划线 _ 会使 Java URI 无法将这段 authority 解析为 host。依赖 URI.getHost() 提取 bucket 的 S3A 实现因此得到 null,与当前报错一致。

现有日志指向文件系统初始化阶段的 URI 解析问题。改名后是否仍有连接或写入问题,需要重新运行测试确认。

为什么 bucket 明明存在,却被认为是空的?

失败发生在 fixture 的这一行:

df.write.mode("overwrite").format("parquet").save(filepath)

此时传入的路径是:

s3a://test_bucket/oo_forecast_training_meta_in/

路径字符串中有 test_bucket,但 Java URI 的 authority 和 host 是不同的组成部分。即使存在 authority,getHost() 也不一定能返回值:当 authority 无法解析为 server-based authority 时,host 可以是 null。下划线不符合这里的 host 解析要求。Java URI 文档

以 Apache Hadoop 3.3.6 的实现为例,S3AFileSystem 初始化时通过 name.getHost() 获取 bucket,随后 S3AUtils.propagateBucketOptions() 检查 bucket 是否为空,检查失败时抛出 bucket is null/empty。这个错误可以在发出 S3 请求之前发生,并不表示远端 bucket 不存在。这里用该版本源码说明机制,不能据此断定容器中的实际 Hadoop 版本。S3AFileSystem 源码、S3AUtils 源码

此外,test_bucket 也不符合当前普通 S3 bucket 的命名规则:允许的字符包括小写字母、数字、句点和连字符,不包括下划线。Amazon S3 bucket 命名规则

为什么 boto3 和 AWS CLI 能成功?

示例中 boto3 接收的是两个独立参数:

client = boto_session.client("s3", endpoint_url=moto_endpoint)
client.create_bucket(Bucket="test_bucket")

它没有经过 Hadoop S3A 解析 s3a://test_bucket/... 的过程。已有日志表明,这次 Moto 实例接受了该名称并返回成功,但这不能证明名称符合真实 S3 的要求,也不能证明 JVM 客户端能解析对应的 URI。

Moto 日志里只有创建 bucket 的 PUT /test_bucket 和 CLI 列举 bucket 的 GET /,这也符合 Spark 在发送请求前初始化失败的情况。

如何修改测试?

替换 filepath fixture,让创建 bucket 和返回路径始终使用同一个合法名称:

@pytest.fixture(scope="module")
def filepath(boto_session, moto_endpoint):
    bucket_name = "test-bucket"
    client = boto_session.client("s3", endpoint_url=moto_endpoint)
    client.create_bucket(Bucket=bucket_name)

    return f"s3a://{bucket_name}/oo_forecast_training_meta_in/"

oo_forecast_training_meta_in/ 是对象 key 的前缀,可以保留下划线。需要修改的是 s3a:// 后面、路径第一个 / 之前的 bucket 名称。

在 Moto 中创建使用新名称的 bucket,然后重新运行 pytest。如果只改返回路径,Spark 会指向另一个尚未创建的 bucket。

现有配置已显式设置 S3A endpoint、测试凭证和 path-style access,并禁用了 SSL。可以先保留这些设置,观察改名后的结果:

hc.set("fs.s3a.endpoint", "http://moto-server:5000")
hc.set("fs.s3a.access.key", "fake")
hc.set("fs.s3a.secret.key", "fake")
hc.set("fs.s3a.path.style.access", "true")
hc.set("fs.s3a.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem")
hc.set("fs.s3a.connection.ssl.enabled", "false")

fs.s3a.path.style.access=true 控制请求的寻址方式,不能修复 bucket 在 URI 中解析为 null 的问题。

如何在当前 JVM 中验证?

可以通过已有的 SparkSession 直接检查 Java URI 的解析结果,无需先连接 Moto:

jvm = spark_session.sparkContext._jvm

for path in (
    "s3a://test_bucket/oo_forecast_training_meta_in/",
    "s3a://test-bucket/oo_forecast_training_meta_in/",
):
    uri = jvm.java.net.URI.create(path)
    print(path)
    print("authority:", uri.getAuthority())
    print("host:", uri.getHost())

预期第一条 URI 的 authority 为 test_bucket,host 为 None,即 Java 的 null;第二条 URI 的 host 为 test-bucket。这段代码用于验证当前环境,上述预期并非已经执行过的测试结果。

如果改名后仍报同样的错误,应打印真正传入 .save() 的路径,并用上述方法检查它,确认测试没有继续引用旧名称。

改名后出现其他错误怎么办?

按新的异常继续定位:

  • 如果出现域名解析失败或连接被拒绝,检查 Glue 容器能否访问 http://moto-server:5000。宿主机 CLI 使用的 localhost:5000 与容器内的地址含义不同。
  • 如果出现 bucket 不存在,确认 boto3 和 S3A 指向同一个 Moto 实例,且新 bucket 已创建。
  • 如果出现凭证错误,检查 JVM 侧的 S3A 配置,不能仅凭 boto3 成功判断 Spark 凭证也正确。

Compose 中的 condition: service_started 只等待容器启动,不能保证 Moto 已经可以处理请求。如果后续出现间歇性连接失败,可以补充服务就绪检查。当前的 bucket is null/empty 应先检查 bucket 名称和 URI 解析。

备注:内容仅供参考。