證異常:CredentialsProvider未注冊的排查與解決)
這個報錯我是在一個數(shù)據(jù)同步任務(wù)里第一次撞上的當(dāng)時跑的是Spark作業(yè)從對象存儲拉數(shù)據(jù)任務(wù)提交后還沒等出結(jié)果就拋了這句Authentication is required but no CredentialsProvider has been registered。翻遍日志認(rèn)證信息明明寫了但程序就是認(rèn)不出來。后來發(fā)現(xiàn)這個報錯在Hadoop生態(tài)、Spark、Flink、甚至一些Java客戶端里都會出現(xiàn)而且表面上是“缺憑證”實際是“沒找到取憑證的那個人”。這篇就把這個報錯的成因、排查鏈路和幾種可靠解法完整寫一遍幫大家少走彎路。1. 報錯的典型現(xiàn)場不是在登錄界面而是在分布式任務(wù)提交時這個報錯最容易出現(xiàn)的地方不是你在電腦前手動輸入用戶名密碼的交互界面而是任務(wù)已經(jīng)提交、分布式組件開始初始化連接的時候。它通常是這樣出現(xiàn)的你寫了一個Spark作業(yè)要訪問S3、OSS或者HDFS本地測試一切正常一旦打成Jar包丟到集群上或者用spark-submit提交程序就報這句錯緊接著是一大段堆棧。1.1 出現(xiàn)最多的三類場景第一類是對象存儲接入比如用s3a://協(xié)議訪問AWS S3、用oss://訪問阿里云OSS、用obs://訪問華為云對象存儲。這類服務(wù)要求攜帶AccessKey和SecretKey客戶端在建立連接前必須先拿到這套憑證而拿憑證的動作就是通過CredentialsProvider完成的。第二類是訪問開啟了Kerberos認(rèn)證的HDFS或WebHDFS。普通開發(fā)機(jī)連開發(fā)環(huán)境的HDFS通常用hadoop.user.name模擬一下就進(jìn)去了但到了生產(chǎn)集群NameNode強(qiáng)制走Kerberos這種情況下不光需要Principal和Keytab還需要在core-site.xml或者代碼里把對應(yīng)的認(rèn)證Provider注冊到Configuration里。第三類比較隱蔽是Hive Metastore的某些版本在通過Spark或Flink讀取外部表時如果底層存儲是S3、OSS、ADLS這類云存儲Metastore里的Location指向的是云存儲路徑那么在Spark解析表結(jié)構(gòu)并嘗試訪問底層文件時同樣會觸發(fā)認(rèn)證Provider查找。很多人在這個環(huán)節(jié)找不到頭緒因為報錯堆棧里既有Hive的類名又有Hadoop的類名混淆感特別強(qiáng)。1.2 這句報錯為什么有迷惑性乍一看這句話“Authentication is required but no CredentialsProvider has been registered”重點(diǎn)好像是“沒有提供認(rèn)證信息”。但如果你真的只往配置里塞AccessKey和SecretKey事情往往還不能解決。原因在于這句英文的原意是系統(tǒng)確實需要認(rèn)證但在它的注冊表里找不到任何“憑證提供者”。這里有個容易忽略的細(xì)節(jié)Hadoop配置系統(tǒng)里CredentialsProvider不是靠普通字符串key直接讀取的它是一類通過fs.s3a.aws.credentials.provider、hadoop.security.credential.provider.path這類配置項注冊進(jìn)去的類。系統(tǒng)在運(yùn)行時并不是自己去拿AccessKey而是去找一個實現(xiàn)了org.apache.hadoop.conf.Configuration.CredentialsProvider接口的類讓這個類去負(fù)責(zé)返回憑證。所以這個報錯的真正含義是系統(tǒng)找到了需要認(rèn)證的資源但沒找到“誰去取憑證”的入口。就好比你要進(jìn)一棟樓保安已經(jīng)站在門口了但物業(yè)沒有告訴他“這個人的門禁卡找誰核實”。你把門禁卡拿在手里沒有用得先讓物業(yè)把核驗流程配好。2. 機(jī)制拆解為什么CredentialProvider找不到又是哪一步丟的既然是圍繞“Provider沒注冊”報錯那就得把Hadoop這套認(rèn)證機(jī)制到底怎么組織說清楚不然排查只能靠瞎試。2.1 CredentialsProvider在Hadoop里是一個“取憑證的接口”在Hadoop的認(rèn)證體系里CredentialsProvider并不特指某一個類而是一類接口。它可以是從環(huán)境變量讀取AccessKey的實現(xiàn)也可以是從JVM系統(tǒng)屬性讀取的實現(xiàn)還可以是從本地文件、KMS、或者外部元數(shù)據(jù)服務(wù)讀取臨時憑證的實現(xiàn)。Hadoop內(nèi)置了幾種常見的Provider實現(xiàn)比如配置值實現(xiàn)類說明org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider匿名訪問不需憑證公開桶可用私有桶必報錯org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider從AccessKey/SecretKey靜態(tài)讀取最常用適合配置固定密鑰org.apache.hadoop.fs.s3a.EnvironmentVariableCredentialsProvider從環(huán)境變量AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY讀取適合容器內(nèi)注入org.apache.hadoop.fs.s3a.TemporaryAWSCredentialsProvider從session-token讀取臨時憑證適合STS臨時授權(quán)org.apache.hadoop.fs.s3a.InstanceProfileCredentialsProvider從EC2元數(shù)據(jù)服務(wù)讀取適合跑在云服務(wù)器上的作業(yè)這套設(shè)計的問題在于Provider必須通過配置項提前注冊進(jìn)Configuration對象或者通過Hadoop CredentialProvider API把加密后的憑證存到j(luò)ceks文件中。如果注冊這一步?jīng)]有做運(yùn)行時就只能拋“no CredentialsProvider has been registered”。2.2 調(diào)用鏈文件系統(tǒng)初始化到異常拋出的完整路徑我把它拆成幾步大家對照自己日志里的堆棧信息基本能對上客戶端代碼創(chuàng)建FileSystem實例或獲取FileSystem時傳入一個URI比如s3a://bucket/pathHadoop會根據(jù)URI的scheme找到對應(yīng)的文件系統(tǒng)實現(xiàn)類。文件系統(tǒng)實現(xiàn)類比如S3AFileSystem開始初始化進(jìn)入initialize()方法。初始化過程中調(diào)用配置里的fs.s3a.aws.credentials.provider項嘗試實例化一個AWSCredentialsProvider。但實例化失敗或配置項為空S3AFileSystem會走到兜底邏輯嘗試從其他渠道獲取默認(rèn)CredentialsProvider。如果所有渠道都未注冊最終拋出Authentication is required but no CredentialsProvider has been registered。還有一個很容易被忽略的細(xì)節(jié)Hadoop為了保證兼容性在加載配置時會合并多個來源包括默認(rèn)的core-default.xml、用戶自定義的core-site.xml、代碼里設(shè)置的Configuration、以及spark-submit --conf傳入的配置。問題恰恰出在合并順序上——如果代碼里new了一個全新的Configuration沒有調(diào)用addResource加載配置文件那么即使core-site.xml里寫了Provider配置這個新對象也看不到它。2.3 觸發(fā)這個報錯的三個前置條件反復(fù)排查下來這個報錯幾乎必然滿足以下三個條件中的兩個以上目標(biāo)資源的URI scheme所對應(yīng)的文件系統(tǒng)實現(xiàn)類被加載了但它的認(rèn)證模塊沒有初始化成功配置里沒有顯式指定CredentialsProvider實現(xiàn)類而Hadoop的默認(rèn)Provider又無法從本機(jī)環(huán)境取到憑證本地測試與集群環(huán)境的Configuration來源不一致本地IDE可能自動加載了用戶主目錄下的配置文件集群上卻只給了Jar包。明白了這套調(diào)用邏輯排查就有的放矢了。3. 完整排查鏈路三步定位認(rèn)證失敗的真正來源這個報錯的誤導(dǎo)性在于堆棧頂層信息很單一但根因可能五花八門。我見過有因為缺少hadoop-aws依賴導(dǎo)致的有因為core-site.xml配置寫錯了位置導(dǎo)致的還有因為使用了錯誤協(xié)議頭比如用s3a://訪問但配置寫給了s3n://導(dǎo)致的。3.1 第一步確認(rèn)到底連的是哪個服務(wù)打開報錯堆棧第一件事不是去搜這句英文而是往上翻幾行找到類似org.apache.hadoop.fs.s3a.S3AFileSystem或者org.apache.hadoop.hdfs.DistributedFileSystem這樣的類名。這能告訴你到底是哪個文件系統(tǒng)實現(xiàn)類在初始化時掛掉的。如果是S3AFileSystem基本就是對象存儲認(rèn)證問題如果是DistributedFileSystem多半和Kerberos票據(jù)有關(guān)如果是一堆org.apache.hadoop.mapreduce或者org.apache.spark.sql.execution.datasources的類夾雜中間說明是數(shù)據(jù)源掃描階段觸發(fā)的需要檢查Job或Session的默認(rèn)配置。隨手記錄下堆棧中文件系統(tǒng)實現(xiàn)類的完整類名這是后續(xù)在搜索引擎里找同類問題的最有效線索。直接搜“Authentication is required but no CredentialsProvider has been registered”經(jīng)常搜到大段大段的Spark討論但加上類名比如S3AFileSystem搜到的就是精準(zhǔn)答案了。3.2 第二步檢查Provider類是否被類加載器找到這一步是最容易被忽略的。配置里的Provider路徑齊全但類加載器根本加載不到對應(yīng)的類運(yùn)行時會報同樣的錯。拿S3來說S3AFileSystem本身在hadoop-aws這個Jar包里而它依賴的aws-java-sdk-bundle在另一個Jar包里。如果你在分布式環(huán)境里只把hadoop-aws打進(jìn)去了沒有把AWS SDK的bundle包打進(jìn)去那么初始化到CredentialsProvider實現(xiàn)類實例化那一步就會因為ClassNotFoundException或NoClassDefFoundError失敗最終被包裝成“Authentication is required”這個異常拋出來。區(qū)分方法也不復(fù)雜看堆棧里有沒有ClassNotFoundException或者看Caused by那段。如果前文提到“no CredentialsProvider has been registered”而根部是java.lang.NoClassDefFoundError那問題很有可能是Jar包不全而不是配置缺失。3.3 第三步檢查配置是寫給了誰同樣的代碼在本地IDE里跑通放到集群上就報這個錯絕大多數(shù)情況是配置的載體不一致。本地IDE里Hadoop的Configuration會自動加載HADOOP_CONF_DIR或HADOOP_HOME/conf目錄下的core-site.xml、hdfs-site.xml。如果你在本地寫了一個core-site.xml放在項目resources下IDE運(yùn)行時classpath里有它于是憑證Provider被正常注冊了。但打成Jar包之后如果你沒有在提交命令里顯式加上--files core-site.xml也沒有在SparkSession里通過.config()傳入對應(yīng)配置項那么運(yùn)行時的Configuration里就不會有這個Provider。這個點(diǎn)極其隱蔽我見過不少同事把配置寫在resources下在IDE里怎么跑都正常一打包就報錯。順帶說一句網(wǎng)上有些帖子建議直接修改$SPARK_HOME/conf/core-site.xml這能解決一部分問題但不優(yōu)雅一來污染公共環(huán)境二來在K8s或YARN動態(tài)容器里并不總是生效。4. 解決方案靜態(tài)配置、代碼注冊與自定義Provider根據(jù)上面三個排查方向給出對應(yīng)的解法。核心原則是讓運(yùn)行環(huán)境里實際生效的那份Configuration里存在一個可用的CredentialsProvider實現(xiàn)并且這個實現(xiàn)類能被類加載器加載到。4.1 方案一在core-site.xml集中注冊適合HDFS/YARN/Spark集群如果你的作業(yè)跑在相對固定的集群上最穩(wěn)妥的方式是在core-site.xml里配置Provider。以S3為例configuration property namefs.s3a.aws.credentials.provider/name valueorg.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider/value /property property namefs.s3a.access.key/name value你的AccessKey/value /property property namefs.s3a.secret.key/name value你的SecretKey/value /property /configuration然后把core-site.xml分發(fā)到所有需要運(yùn)行作業(yè)的節(jié)點(diǎn)放到$HADOOP_CONF_DIR下或者通過spark-submit --files /path/to/core-site.xml把文件帶到執(zhí)行端。這里注意fs.s3a.aws.credentials.provider的值可以填逗號分隔的多個類名Hadoop會按順序嘗試。前面某個Provider拋異常了它會繼續(xù)嘗試下一個直到有Provider返回憑證。4.2 方案二在SparkSession/Flink配置中動態(tài)傳入如果你的作業(yè)不依賴集群級別的core-site.xml而是在代碼里直接拼裝配置那么Spark場景下可以這樣val spark SparkSession.builder() .master(yarn) .appName(s3-access) .config(fs.s3a.aws.credentials.provider, org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider) .config(fs.s3a.access.key, accessKey) .config(fs.s3a.secret.key, secretKey) .config(spark.hadoop.fs.s3a.aws.credentials.provider, org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider) .getOrCreate()這里有個非常重要的前綴知識在Spark里凡是需要傳遞給Hadoop Configuration的項在spark-submit --conf或SparkSession.config()里最好都加上spark.hadoop.前綴。Spark會把帶這個前綴的配置項去掉前綴后放進(jìn)Hadoop的Configuration里。如果不加前綴Spark只會把它當(dāng)作Spark自有配置不一定會正確傳遞到Hadoop的文件系統(tǒng)初始化邏輯中。Flink同理需要在flink-conf.yaml或者代碼里通過org.apache.flink.configuration.Configuration設(shè)置fs.s3a.*相關(guān)項并且在提交時保證flink-shaded-hadoop-*或者對應(yīng)版本的hadoop-aws依賴在classpath中。4.3 方案三代碼里顯式注冊自定義CredentialsProvider如果憑證不是靜態(tài)的而是從內(nèi)部密鑰管理系統(tǒng)動態(tài)獲取那靜態(tài)配置就不夠用了。這種情況下可以實現(xiàn)自己的Provider類再把它注冊到Configuration中。自定義Provider的骨架如下import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; public class CustomKmsCredentialsProvider implements AWSCredentialsProvider { Override public AWSCredentials getCredentials() { // 從內(nèi)部KMS系統(tǒng)獲取AccessKey和SecretKey String accessKey MyKmsClient.getSecret(s3.access.key); String secretKey MyKmsClient.getSecret(s3.secret.key); return new BasicAWSCredentials(accessKey, secretKey); } Override public void refresh() { // 根據(jù)需要實現(xiàn)刷新邏輯 } }然后在初始化文件系統(tǒng)之前手動設(shè)置配置項Configuration conf new Configuration(); conf.set(fs.s3a.aws.credentials.provider, com.example.CustomKmsCredentialsProvider); conf.set(fs.s3a.impl, org.apache.hadoop.fs.s3a.S3AFileSystem); conf.set(fs.s3a.endpoint, https://oss-cn-hangzhou.aliyuncs.com); FileSystem fs FileSystem.get(URI.create(s3a://your-bucket/path), conf);注意FileSystem.get()會緩存FileSystem實例。如果你在同一個JVM里用不同Configuration獲取同一個URI的FileSystem第二次調(diào)用可能直接返回緩存實例導(dǎo)致新的Provider配置沒有生效。這時候需要調(diào)用FileSystem.closeAll()或者給URI加一個不同的fragment/查詢參數(shù)來繞過緩存。這是一個非常隱蔽的坑我在寫多租戶數(shù)據(jù)同步工具時踩過印象很深。4.4 配置優(yōu)先級和生效驗證接入云存儲后建議在代碼里打印或通過日志輸出最終的Configuration關(guān)鍵項確認(rèn)配置確實生效??梢栽赗DD/DataFrame讀取之前加一行臨時日志spark.sparkContext.hadoopConfiguration .get(fs.s3a.aws.credentials.provider)如果打印出來是null說明配置沒傳進(jìn)去如果打印出來是你設(shè)置的類名那說明Provider鏈已經(jīng)注冊成功此時再報錯就可以把注意力轉(zhuǎn)移到依賴Jar包和網(wǎng)絡(luò)連通性上。為了快速驗證配置是否正確可以先用一個極簡的S3訪問腳本做冒煙測試不跑完整業(yè)務(wù)邏輯只列一下桶內(nèi)文件hadoop fs -ls s3a://your-bucket/test-path這條命令能直接在終端驗證當(dāng)前環(huán)境指Hadoop客戶端所在的節(jié)點(diǎn)的配置是否可用。如果這個命令都能報出同樣的認(rèn)證錯誤那說明問題出在環(huán)境配置或依賴上而不是你的業(yè)務(wù)代碼。5. 連環(huán)雷區(qū)同類報錯的其他變體和規(guī)避技巧解決完一個場景不代表萬事大吉這個報錯在不同組件組合下會呈現(xiàn)一些微妙差異我挑三類高頻的展開說。5.1 依賴版本不匹配導(dǎo)致Provider類被“隱形刪除”Hadoop和AWS SDK的版本兼容性非常挑剔。hadoop-aws3.x系列要求配套的aws-java-sdk-bundle版本不能太低否則S3A初始化時會出現(xiàn)莫名其妙的NoSuchMethodError最終同樣包裝成認(rèn)證失敗。我踩過的典型版本組合是hadoop-aws-3.2.0配aws-java-sdk-s3-1.11.101結(jié)果在調(diào)用某個需要新版SDK API的代碼路徑時直接NoSuchMethodError。后續(xù)升級到aws-java-sdk-bundle-1.11.901才穩(wěn)定。建議直接用hadoop-aws對應(yīng)發(fā)布的依賴版本清單不要自己隨意配對。以hadoop-aws-3.3.4為例它的POM里會標(biāo)明依賴的aws-java-sdk-bundle版本。另外如果你的作業(yè)里還用了HiveHive本身攜帶了一套aws-java-sdk-*的舊版本Jar這些Jar也會進(jìn)入classpath導(dǎo)致沖突。此時應(yīng)該檢查依賴樹mvn dependency:tree -Dincludescom.amazonaws或者在Gradle里跑dependencies任務(wù)把沖突的舊版SDK排除掉。5.2 容器化和平臺化作業(yè)里配置不落盤在K8s、Airflow、以及各類自研數(shù)據(jù)平臺上跑作業(yè)時客戶端容器通常是一次性的core-site.xml根本來不及手動分發(fā)到容器里或者分發(fā)進(jìn)去了但路徑不在classpath中。這種情況下最省心的做法是不要依賴文件配置全部通過代碼或環(huán)境變量傳入。例如在K8s的Pod YAML里給Spark Driver/Executor容器注入環(huán)境變量AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY然后設(shè)置fs.s3a.aws.credentials.provider為org.apache.hadoop.fs.s3a.EnvironmentVariableCredentialsProvider。如果用的是臨時憑證還可以接入InstanceProfileCredentialsProvider讓每個容器自動從所在云資源元數(shù)據(jù)服務(wù)獲取臨時憑證。這樣既免去了密鑰硬編碼的管理麻煩也減少密鑰被寫入日志的風(fēng)險。5.3 一個典型的S3A 本地Spark案例復(fù)盤最后分享一個完整的踩坑復(fù)現(xiàn)這個例子基本涵蓋了上面所有要點(diǎn)。現(xiàn)象是本地Maven工程里跑Spark讀寫S3IDE里正常mvn package后執(zhí)行spark-submit --master local[*]立刻報標(biāo)題里的錯。排查第一步打印hadoopConfiguration里的Provider配置發(fā)現(xiàn)是null。第二步查classpath確認(rèn)hadoop-aws和aws-java-sdk-bundle都打進(jìn)了Jar。第三步看代碼發(fā)現(xiàn)SparkSession是通過.config(fs.s3a.access.key, accessKey)這種形式設(shè)置的而Spark在local模式下不會自動把非spark.hadoop.前綴的配置轉(zhuǎn)到Hadoop的Configuration里。解決方法是把配置項的key統(tǒng)一改成spark.hadoop.fs.s3a.access.key和spark.hadoop.fs.s3a.secret.key并顯式聲明spark.hadoop.fs.s3a.aws.credentials.provider。改完再跑問題消失。這個案例說明同一個報錯根因可能差得很遠(yuǎn)。但只要你把排查點(diǎn)鎖定在“誰在初始化時找Provider”“Provider類能不能被加載”“配置是否傳到了實際生效的Configuration”這三件事上即使碰到新變體也能按圖索驥。我個人在實際操作中的體會是把認(rèn)證鏈路當(dāng)成一個獨(dú)立模塊來測試不要每次等到業(yè)務(wù)作業(yè)跑掛才回頭看。先寫一個幾十行的最小連通性腳本把文件系統(tǒng)建立連接、拿到Provider、列出目標(biāo)路徑這三步跑通再往上疊加業(yè)務(wù)邏輯排查效率會高很多。