Windows环境下用Staging S3A Committer写入S3时遇UnsatisfiedLinkError
我来帮你排查这个问题,这个错误在Windows本地跑Spark+S3+Parquet的场景里挺常见的,咱们一步步来分析解决:
问题根源分析
这个java.lang.UnsatisfiedLinkError: org.apache.hadoop.io.nativeio.NativeIO$POSIX.stat错误,本质是Hadoop的Native本地库在Windows环境下缺失或不兼容导致的:
- Hadoop的
NativeIO类依赖系统级的本地库(比如Windows下的.dll文件)来处理POSIX风格的文件操作,但Windows本身不支持POSIX接口; - Parquet的提交流程恰好触发了调用这个本地库的代码路径,而ORC/CSV的提交逻辑没走到这一步,所以没报错;
- 另外你用的
spark-hadoop-cloud版本和Spark2.4.5、Hadoop3.2.1不匹配,也加剧了兼容性冲突。
解决方案
方案1:快速绕开Native库依赖(临时解决)
直接强制Hadoop用纯Java实现代替本地Native库,在SparkSession配置里添加这两行:
.config("spark.hadoop.io.nativeio.posix.enabled", "false") .config("spark.hadoop.hadoop.native.lib", "false")
这样Hadoop就不会尝试调用POSIX本地方法,直接绕开链接错误,适合快速验证功能。
方案2:补充Hadoop Windows本地库(长期解决)
如果想彻底解决Native库依赖问题,需要下载适配Hadoop3.2.1的Windows本地库:
- 下载包含
hadoop.dll、winutils.exe等文件的Hadoop3.2.1 Windows Native包; - 把这些文件放到项目的本地目录,比如
src/main/resources/hadoop-native/; - 在Spark配置里指定本地库路径:
.config("spark.hadoop.hadoop.native.lib.path", "src/main/resources/hadoop-native/")
或者设置系统环境变量HADOOP_HOME指向存放这些文件的目录,并把%HADOOP_HOME%\bin加到系统PATH中。
方案3:修正SBT依赖版本冲突
你当前用的spark-hadoop-cloud版本(2.3.2.3.1.0.6-1)和Spark2.4.5、Hadoop3.2.1版本不兼容,建议调整:
修改SBT的libraryDependencies:
scalaVersion := "2.11.11" libraryDependencies ++= Seq( "org.apache.spark" %% "spark-sql" % "2.4.5", // 移除旧的不兼容版本,换成和Spark2.4.5匹配的版本 "org.apache.spark" %% "spark-hadoop-cloud" % "2.4.5", "org.apache.hadoop" % "hadoop-client" % "3.2.1", "org.apache.hadoop" % "hadoop-common" % "3.2.1", "org.apache.hadoop" % "hadoop-aws" % "3.2.1", "com.amazonaws" % "aws-java-sdk-bundle" % "1.11.704" ) // 其他dependencyOverrides和resolvers保持不变
如果不需要额外的cloud功能,甚至可以直接移除spark-hadoop-cloud依赖——Hadoop3.2.1自带的S3A Committer已经支持目录提交器功能。
方案4:简化Parquet提交器配置
你的配置里有些冗余项可能引发冲突,尝试简化为:
val spark = SparkSession.builder() .appName("test-run-s3a-commiters") .master("local[*]") .config("spark.hadoop.fs.s3a.impl", "org.apache.hadoop.fs.s3a.S3AFileSystem") .config("spark.hadoop.fs.s3a.endpoint", "s3.eu-central-1.amazonaws.com") .config("spark.hadoop.fs.s3a.aws.credentials.provider", "com.amazonaws.auth.profile.ProfileCredentialsProvider") .config("spark.hadoop.fs.s3a.connection.maximum", "100") .config("spark.hadoop.fs.s3a.committer.name", "directory") .config("spark.hadoop.fs.s3a.committer.magic.enabled", "false") .config("spark.hadoop.fs.s3a.committer.staging.conflict-mode", "append") .config("spark.hadoop.fs.s3a.committer.staging.unique-filenames", "true") .config("spark.hadoop.fs.s3a.committer.staging.abort.pending.uploads", "true") .config("spark.hadoop.fs.s3a.buffer.dir", "./tmp/") // 改成本地可写的明确路径,避免HDFS解析问题 .config("spark.hadoop.fs.s3a.committer.staging.tmp.path", "./tmp/s3a-staging/") .config("spark.hadoop.mapreduce.outputcommitter.factory.scheme.s3a", "org.apache.hadoop.fs.s3a.commit.S3ACommitterFactory") .config("spark.sql.sources.commitProtocolClass", "org.apache.spark.internal.io.cloud.PathOutputCommitProtocol") .config("spark.sql.parquet.output.committer.class", "org.apache.spark.internal.io.cloud.BindingParquetOutputCommitter") // 添加方案1的配置(如果选方案1的话) .config("spark.hadoop.io.nativeio.posix.enabled", "false") .config("spark.hadoop.hadoop.native.lib", "false") .getOrCreate()
重点是把临时路径改成本地可写的明确目录,移除可能冲突的MapReduce配置项。
验证
修改配置后重新运行你的测试代码,应该能正常将Parquet数据写入S3了。
内容的提问来源于stack exchange,提问作者Nementaarion

