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 解析。
备注:内容仅供参考。
