PySpark向Ceph S3a存储写DataFrame时触发400 InvalidRequest错误
前置场景
- 团队正探索使用Ceph替代HDFS承载AI/ML工作负载,已通过Rook将自建Ceph集群接入Kubernetes环境
- 测试验证阶段,S3CMD CLI可正常访问Ceph存储,K8s上部署的Spark也能正常从Ceph读取业务数据,但将处理后的DataFrame写回Ceph时触发运行错误
- 所用组件版本:Spark 3.0.1、Hadoop 3.2
报错核心特征
报错触发于写入作业的初始化阶段,尚未开始实际数据传输
关键调用链路:
FileOutputCommitter.setupJob→S3AFileSystem.mkdirs→S3AFileSystem.createFakeDirectory
错误详情:S3A客户端尝试向output-data/_temporary/0路径发送0字节PUT请求时,被Ceph RGW拒绝,返回状态码400,错误码InvalidRequest
高频根因与对应解决配置
Ceph RGW对S3A空目录对象的兼容性问题
Hadoop 3.2.x的S3A实现默认通过创建0字节、key末尾带/的对象模拟目录结构,部分版本Ceph RGW(尤其是Nautilus及更早版本、Rook默认部署的RGW配置)未开启对这类空对象PUT的兼容开关,会直接判定请求非法。
对应调整配置:# 关闭S3A的Fake目录创建逻辑 .config("spark.hadoop.fs.s3a.directory.marker.retention", "keep") .config("spark.hadoop.fs.s3a.metadatastore.impl", "org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore") .config("spark.hadoop.fs.s3a.create.empty.directory", "false")fast.upload参数与Ceph RGW的已知冲突
现有配置中开启的spark.hadoop.fs.s3a.fast.upload=true,在Hadoop 3.2.0/3.2.1版本和Ceph RGW对接时存在兼容bug:fast upload模式下会默认发起未携带Content-Length头的PUT请求,被Ceph RGW判定为非法请求返回400。
对应调整配置:# 替换为兼容Ceph的分块上传配置 .config("spark.hadoop.fs.s3a.fast.upload", "false") .config("spark.hadoop.fs.s3a.multipart.size", "104857600") .config("spark.hadoop.fs.s3a.threads.max", "10")输出提交器的临时路径预创建逻辑问题
虽然已经配置了版本2的文件输出提交器,但S3A客户端在作业初始化时仍会预创建_temporary临时目录,触发Fake目录请求。
对应调整配置:# 指定专属的作业暂存目录,跳过默认路径的预创建 .config("spark.hadoop.mapreduce.fileoutputcommitter.stagingdir", "s3a://bucket/.spark-staging/") .config("spark.hadoop.mapreduce.fileoutputcommitter.cleanup-failures.ignored", "true")权限覆盖校验遗漏
虽然读操作和S3CMD访问正常,但需要确认当前使用的Access Key是否对目标写入路径有PutObject权限,是否存在桶策略禁止0字节对象写入的规则。
可通过S3CMD直接模拟验证:# 手动执行0字节PUT请求,复现报错 s3cmd put /dev/null s3://bucket/output-data/_temporary/0/
验证流程
- 调整配置后,先执行最小化写入测试:运行
spark.range(10).write.csv("s3a://bucket/test-output/"),确认无报错后再运行完整业务逻辑 - 若问题仍未解决,可开启S3A调试日志,定位被拒绝请求的具体Header信息,匹配Ceph RGW的日志定位拒绝原因:
.config("spark.hadoop.root.logger", "DEBUG,console")
内容的提问来源于stack exchange,提问作者Prateek Dubey

