境搭建實(shí)戰(zhàn))
1. 為什么這套組合在2020年之后依然值得折騰先把結(jié)論擺在前面Intellij idea 2020.1社區(qū)版 Maven 3.6.3 Scala 2.11.8 Spark這套組合放到今天看確實(shí)有點(diǎn)考古的味道但它依然是很多人入門大數(shù)據(jù)開發(fā)時(shí)繞不開的一條路。原因很直接——大量存量項(xiàng)目、教學(xué)資料、企業(yè)內(nèi)部的老系統(tǒng)仍然跑在Scala 2.11和Spark 2.x這條線上。你如果一上來就裝最新版的Scala 2.13配Spark 3.x遇到老項(xiàng)目直接編譯不過回頭還得降版本重來白白浪費(fèi)一整天。我自己第一次搭這套環(huán)境的時(shí)候踩的坑能寫滿兩頁紙。最典型的一個(gè)IDEA社區(qū)版新建項(xiàng)目時(shí)選了Scala SDK結(jié)果Maven編譯時(shí)報(bào)object apache is not a member of package org查了半天才發(fā)現(xiàn)是Scala版本和Spark依賴的二進(jìn)制版本對(duì)不上。這種問題在教程里往往一筆帶過但實(shí)際卡住新手的就是這些細(xì)節(jié)。所以這篇內(nèi)容我打算按真實(shí)搭建順序來講不是那種理想化的步驟羅列而是把每一步為什么這么做、哪里容易翻車、翻車了怎么救都攤開說清楚。適合的人群很明確剛接觸Spark、需要在Windows本地跑通第一個(gè)Scala項(xiàng)目的開發(fā)者或者手頭有老項(xiàng)目要維護(hù)、需要把環(huán)境對(duì)齊到Scala 2.11這條線的同學(xué)。整套流程走下來順利的話兩三個(gè)小時(shí)能跑通第一個(gè)WordCount不順利的話可能要在版本兼容上耗掉半天這篇就是幫你把后面那半天省下來。需要提前說明的是Windows平臺(tái)搭這套環(huán)境最大的敵人不是技術(shù)難度而是路徑、編碼、版本這三件事。下面每個(gè)章節(jié)我都會(huì)圍繞這三條主線展開。2. 動(dòng)手之前先把四個(gè)組件的版本關(guān)系理清楚很多人搭環(huán)境失敗根子不在操作而在于沒搞明白這幾個(gè)組件之間到底誰依賴誰。我見過太多人把JDK、Scala、Spark、Maven當(dāng)成四個(gè)獨(dú)立的東西分別裝最新版結(jié)果湊一起就炸。所以這一章先把關(guān)系講透后面操作才不會(huì)盲目。2.1 JDK、Scala、Spark三者的版本咬合關(guān)系先記住一條鐵律Spark的每個(gè)大版本都綁定了特定的Scala二進(jìn)制版本。Spark 2.4.x默認(rèn)編譯用的是Scala 2.11Spark 3.0之后才轉(zhuǎn)向Scala 2.12。你項(xiàng)目里寫的Scala代碼最終要被編譯成字節(jié)碼而這個(gè)字節(jié)碼必須和Spark運(yùn)行時(shí)依賴的Scala版本二進(jìn)制兼容。版本對(duì)不上輕則編譯警告重則運(yùn)行時(shí)拋NoSuchMethodError而且這種錯(cuò)誤往往在運(yùn)行到某個(gè)特定算子時(shí)才爆出來極難排查。JDK這邊Scala 2.11.8對(duì)JDK 8的支持是最穩(wěn)的。JDK 11雖然也能跑但Scala 2.11的編譯器在JDK 11下偶爾會(huì)有反射相關(guān)的警告而且Spark 2.x對(duì)JDK 11的支持并不完整。所以我的建議很明確JDK鎖死8別貪新。這不是保守是省事。組件推薦版本為什么是這個(gè)版本JDK1.88u系列Scala 2.11.8與Spark 2.x在JDK 8下兼容性最好Scala2.11.8與Spark 2.4.x的二進(jìn)制版本嚴(yán)格對(duì)應(yīng)Maven3.6.3穩(wěn)定且與IDEA 2020.1的集成成熟Spark2.4.x與Scala 2.11.8配套的最后一個(gè)穩(wěn)定大版本2.2 為什么Maven版本也不能隨便選Maven 3.6.3這個(gè)版本不是隨便定的。IDEA 2020.1內(nèi)置的Maven集成對(duì)3.6.x系列支持最順3.8之后Maven改了一些默認(rèn)行為比如默認(rèn)阻止HTTP倉(cāng)庫(kù)在老項(xiàng)目里反而容易出問題。而且3.6.3的依賴解析速度和對(duì)settings.xml鏡像配置的處理都比較成熟。另一個(gè)容易被忽略的點(diǎn)Maven的本地倉(cāng)庫(kù)路徑。默認(rèn)在C:\Users\你的用戶名\.m2\repository如果你的C盤空間緊張或者用戶名帶中文強(qiáng)烈建議在settings.xml里改到一個(gè)純英文、無空格的路徑。中文路徑在Windows下引發(fā)的詭異問題我后面會(huì)專門講。2.3 環(huán)境變量配置里最容易寫錯(cuò)的兩個(gè)地方環(huán)境變量這塊網(wǎng)上教程千篇一律但有兩個(gè)細(xì)節(jié)幾乎沒人強(qiáng)調(diào)第一JAVA_HOME指向的路徑不要帶\bin。很多人復(fù)制路徑時(shí)順手把bin也帶上了結(jié)果Maven和Scala都找不到JDK。正確寫法是JAVA_HOME C:\Java\jdk1.8.0_xxx然后Path里加%JAVA_HOME%\bin。第二SCALA_HOME和MAVEN_HOME同理都指向解壓后的根目錄不帶bin。配置完之后一定要新開一個(gè)命令行窗口驗(yàn)證因?yàn)榄h(huán)境變量的更新不會(huì)影響已經(jīng)打開的窗口。這個(gè)坑我踩過不止一次明明配好了卻一直報(bào)不是內(nèi)部或外部命令就是因?yàn)橛玫氖桥f窗口。驗(yàn)證命令很簡(jiǎn)單java -version scala -version mvn -version三條都能正常輸出版本號(hào)且java -version顯示的是1.8這一步才算過關(guān)。如果scala -version報(bào)錯(cuò)八成是Path沒配對(duì)如果mvn -version里顯示的Java版本不對(duì)說明JAVA_HOME有問題。3. 從零到跑通第一個(gè)Scala項(xiàng)目的完整操作鏈路這一章是重頭戲我按實(shí)際操作順序一步步來每一步都標(biāo)注了為什么這么做和這里容易出什么問題。你照著走基本不會(huì)偏。3.1 IDEA社區(qū)版的一個(gè)先天限制及應(yīng)對(duì)思路先說個(gè)扎心的事實(shí)Intellij idea社區(qū)版沒有內(nèi)置的Scala插件支持而旗艦版才有完整的Scala和Spark支持。但社區(qū)版不是不能用只是需要手動(dòng)裝Scala插件。這是很多人卡在第一步的原因——新建項(xiàng)目時(shí)找不到Scala選項(xiàng)以為社區(qū)版不支持其實(shí)只是插件沒裝。操作路徑File - Settings - Plugins在Marketplace里搜Scala安裝后重啟IDEA。注意2020.1這個(gè)版本對(duì)應(yīng)的Scala插件版本也要匹配如果裝最新版插件報(bào)兼容性錯(cuò)誤就去插件頁面找歷史版本選一個(gè)標(biāo)注支持2020.1的。裝完插件后新建項(xiàng)目時(shí)就能看到Scala選項(xiàng)了。但這里有個(gè)選擇是建Maven項(xiàng)目再手動(dòng)加Scala支持還是直接建Scala項(xiàng)目我的建議是建Maven項(xiàng)目因?yàn)镾park項(xiàng)目本質(zhì)上依賴管理靠Maven直接建Scala項(xiàng)目反而要額外配Maven繞遠(yuǎn)了。3.2 新建Maven項(xiàng)目時(shí)的坐標(biāo)填寫與骨架選擇新建項(xiàng)目選MavenJDK選你裝好的1.8。到了填GroupId和ArtifactId這一步隨便填個(gè)有意義的名字就行比如com.demo和spark-demo。關(guān)鍵是不要勾選Create from archetype也就是不要用模板骨架。原因很簡(jiǎn)單Scala項(xiàng)目的目錄結(jié)構(gòu)和Java不一樣用Java的archetype生成出來的目錄還得手動(dòng)改不如從空項(xiàng)目開始自己建目錄。項(xiàng)目建好后默認(rèn)目錄是src/main/java我們要手動(dòng)改成Scala的規(guī)范結(jié)構(gòu)src/main/scala src/main/resources src/test/scala在IDEA里右鍵src/main新建Directory輸入scala即可。然后關(guān)鍵一步把這個(gè)scala目錄標(biāo)記為Sources Root。右鍵目錄 -Mark Directory as-Sources Root目錄會(huì)變藍(lán)。這一步不做IDEA不會(huì)把里面的Scala文件當(dāng)源碼編譯后面會(huì)報(bào)找不到類。3.3 pom.xml里必須寫對(duì)的幾段配置pom.xml是整個(gè)項(xiàng)目的靈魂寫錯(cuò)了后面全是坑。我直接給一份經(jīng)過驗(yàn)證的配置然后逐段解釋。properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target scala.version2.11.8/scala.version spark.version2.4.5/spark.version encodingUTF-8/encoding /propertiesscala.version和spark.version這兩個(gè)屬性一定要顯式聲明后面依賴?yán)镉?{}引用改版本時(shí)只改一處避免遺漏。依賴部分dependencies dependency groupIdorg.scala-lang/groupId artifactIdscala-library/artifactId version${scala.version}/version /dependency dependency groupIdorg.apache.spark/groupId artifactIdspark-core_2.11/artifactId version${spark.version}/version /dependency dependency groupIdorg.apache.spark/groupId artifactIdspark-sql_2.11/artifactId version${spark.version}/version /dependency /dependencies注意spark-core_2.11這個(gè)后綴_2.11就是Scala的二進(jìn)制版本標(biāo)識(shí)。如果你寫成spark-core_2.12而你的Scala是2.11.8編譯能過但運(yùn)行必炸。這個(gè)后綴是Maven坐標(biāo)的一部分不是可有可無的裝飾。構(gòu)建插件部分需要scala-maven-pluginbuild plugins plugin groupIdnet.alchim31.maven/groupId artifactIdscala-maven-plugin/artifactId version3.4.6/version executions execution goals goalcompile/goal goaltestCompile/goal /goals /execution /executions /plugin /plugins /build這個(gè)插件的作用是讓Maven能編譯Scala代碼。沒有它Maven只認(rèn)Java你的.scala文件會(huì)被直接忽略編譯時(shí)一個(gè)類都找不到。3.4 第一個(gè)Scala文件的編寫與運(yùn)行驗(yàn)證在src/main/scala下新建一個(gè)Scala Object寫個(gè)最簡(jiǎn)單的WordCountimport org.apache.spark.{SparkConf, SparkContext} object WordCount { def main(args: Array[String]): Unit { val conf new SparkConf().setAppName(WordCount).setMaster(local[*]) val sc new SparkContext(conf) val data sc.parallelize(Seq(hello spark, hello scala, spark is fast)) val result data.flatMap(_.split( )).map((_, 1)).reduceByKey(_ _) result.collect().foreach(println) sc.stop() } }setMaster(local[*])表示用本地所有CPU核心跑這是本地開發(fā)的標(biāo)準(zhǔn)寫法。跑起來如果能看到每個(gè)單詞的計(jì)數(shù)輸出恭喜你環(huán)境通了。但這里有個(gè)Windows特有的坑運(yùn)行時(shí)會(huì)報(bào)Could not locate executable null\bin\winutils.exe。這個(gè)錯(cuò)誤不影響結(jié)果但看著煩。原因是Spark在Windows下需要Hadoop的winutils工具而Windows沒有。解決辦法是下載對(duì)應(yīng)Hadoop版本的winutils放到一個(gè)目錄然后配置HADOOP_HOME環(huán)境變量指向它?;蛘吒?jiǎn)單在代碼里加一行System.setProperty(hadoop.home.dir, 你的路徑)。這個(gè)坑幾乎每個(gè)Windows用戶都會(huì)遇到提前知道能省不少搜索時(shí)間。4. 那些讓新手抓狂的報(bào)錯(cuò)與排查思路環(huán)境搭建最耗時(shí)的從來不是順利的部分而是報(bào)錯(cuò)。這一章我把幾個(gè)高頻報(bào)錯(cuò)的完整排查鏈路寫出來你可以對(duì)照著復(fù)現(xiàn)排查思路而不是直接抄答案。4.1 依賴下載卡住或失敗的幾種成因Maven下載依賴慢是新手遇到的第一個(gè)勸退點(diǎn)。默認(rèn)從中央倉(cāng)庫(kù)拉國(guó)內(nèi)網(wǎng)絡(luò)環(huán)境下經(jīng)??ㄔ谀硞€(gè)jar上不動(dòng)。解決辦法是配置國(guó)內(nèi)鏡像。在settings.xml的mirrors里加一段鏡像配置指向國(guó)內(nèi)的Maven鏡像源。配置完之后記得在IDEA里指定這個(gè)settings.xmlSettings - Build Tools - Maven把User settings file指向你改過的那個(gè)文件。但鏡像也不是萬能的。有時(shí)候某個(gè)依賴在鏡像里沒有會(huì)報(bào)Could not find artifact。這時(shí)候先別急著換鏡像先確認(rèn)你的坐標(biāo)寫對(duì)了沒有——groupId、artifactId、version三個(gè)字段任何一個(gè)拼錯(cuò)都會(huì)導(dǎo)致找不到。我見過有人把spark-core寫成sparkcore找了半天。還有一種情況是依賴下載到一半斷了本地倉(cāng)庫(kù)里留了個(gè).lastUpdated文件導(dǎo)致Maven認(rèn)為這個(gè)依賴已經(jīng)嘗試過、不再重試。解決辦法是刪掉對(duì)應(yīng)目錄下的.lastUpdated文件或者直接刪掉整個(gè)依賴目錄重新拉。4.2 Scala版本不匹配引發(fā)的編譯錯(cuò)誤長(zhǎng)什么樣版本不匹配的報(bào)錯(cuò)有個(gè)典型特征編譯期可能不報(bào)錯(cuò)運(yùn)行期才炸。比如你Scala用2.12Spark依賴用_2.11編譯時(shí)scala-maven-plugin可能只是警告但運(yùn)行時(shí)一調(diào)用Spark的類就拋NoSuchMethodError或ClassNotFoundException。排查這類問題的思路是先看報(bào)錯(cuò)里提到的類屬于哪個(gè)包然后去Maven依賴樹里查這個(gè)包是哪個(gè)版本。IDEA右側(cè)的Maven面板有個(gè)Dependencies視圖能看到完整的依賴樹。如果發(fā)現(xiàn)同一個(gè)包有多個(gè)版本那就是版本沖突需要用exclusions排除掉多余的。更隱蔽的一種Scala SDK在IDEA里的配置和pom.xml里的版本不一致。IDEA的Project Structure - Global Libraries里可能掛著一個(gè)Scala 2.12的SDK而pom里寫的是2.11.8。這種情況下IDEA的代碼提示按2.12走M(jìn)aven編譯按2.11走兩邊打架。解決辦法是統(tǒng)一要么在IDEA里把Scala SDK換成2.11.8要么干脆不掛全局SDK完全靠Maven管理。4.3 中文亂碼與路徑空格的隱藏陷阱Windows下有兩個(gè)隱形殺手中文路徑和路徑空格。中文路徑的問題在于某些工具鏈在處理路徑時(shí)用的編碼不一致導(dǎo)致讀不到文件。比如你的項(xiàng)目放在D:\我的項(xiàng)目\spark-demoMaven或Scala編譯器可能就找不到源碼。解決辦法很簡(jiǎn)單所有和開發(fā)相關(guān)的路徑全部用純英文、無空格。項(xiàng)目路徑、Maven倉(cāng)庫(kù)路徑、JDK安裝路徑一個(gè)都別例外。路徑空格的問題類似。C:\Program Files\Java這種帶空格的路徑在某些腳本里會(huì)被截?cái)?。所以JDK、Scala、Maven這些工具我建議統(tǒng)一裝在C:\dev\這類簡(jiǎn)短無空格的目錄下。中文亂碼則主要出現(xiàn)在控制臺(tái)輸出。IDEA的控制臺(tái)默認(rèn)編碼可能和系統(tǒng)不一致導(dǎo)致Scala程序里的中文輸出變成亂碼。解決辦法是在IDEA的Settings - Editor - File Encodings里把Global Encoding、Project Encoding、Default encoding for properties files全部設(shè)為UTF-8并勾選Transparent native-to-ascii conversion。同時(shí)在pom.xml里加上project.build.sourceEncodingUTF-8/project.build.sourceEncoding。4.4 一個(gè)完整的排查案例復(fù)盤說個(gè)我實(shí)際遇到的案例。某次幫人看環(huán)境現(xiàn)象是代碼能編譯但一運(yùn)行就報(bào)java.lang.NoClassDefFoundError: scala/Product。這個(gè)報(bào)錯(cuò)的意思是運(yùn)行時(shí)找不到Scala的基礎(chǔ)類。排查鏈路是這樣的第一步確認(rèn)pom里有沒有scala-library依賴——有。第二步看依賴樹里scala-library的版本——發(fā)現(xiàn)是2.12但pom里明明寫的2.11.8。第三步追查為什么變成2.12——原來是spark-sql的某個(gè)傳遞依賴帶進(jìn)來了2.12的scala-library覆蓋了顯式聲明的版本。解決辦法是在spark-sql依賴?yán)锛觘xclusions把傳遞進(jìn)來的scala-library排除掉強(qiáng)制用我們聲明的2.11.8。這個(gè)案例說明依賴沖突不一定來自你直接寫的依賴更多時(shí)候來自傳遞依賴。學(xué)會(huì)看依賴樹是排查這類問題的基本功。5. 讓開發(fā)體驗(yàn)更順手的幾個(gè)配置調(diào)整環(huán)境跑通只是及格線真正提升效率的是那些順手的配置。這一章分享幾個(gè)我用了很久、確實(shí)能省時(shí)間的調(diào)整。5.1 IDEA里Scala編譯器的性能調(diào)優(yōu)Scala編譯慢是出了名的尤其是項(xiàng)目大了之后。IDEA里可以調(diào)整編譯器的堆內(nèi)存。在Settings - Build, Execution, Deployment - Compiler里把Build process heap size從默認(rèn)的700調(diào)到2048甚至更高。這個(gè)調(diào)整對(duì)編譯速度的提升是肉眼可見的尤其是依賴多的時(shí)候。另外Scala插件的Compile server選項(xiàng)建議開啟。它會(huì)讓編譯在一個(gè)獨(dú)立的進(jìn)程里跑避免每次編譯都重新加載第二次之后的編譯速度會(huì)快很多。開啟路徑在Settings - Languages Frameworks - Scala里。5.2 Maven的離線模式與依賴緩存策略依賴都下載完之后日常開發(fā)其實(shí)不需要每次都聯(lián)網(wǎng)檢查更新。IDEA的Maven面板里有個(gè)Toggle Offline Mode按鈕開啟后Maven只用本地倉(cāng)庫(kù)不再聯(lián)網(wǎng)。這在網(wǎng)絡(luò)不穩(wěn)定的時(shí)候特別有用能避免因?yàn)榫W(wǎng)絡(luò)問題導(dǎo)致的構(gòu)建失敗。但要注意離線模式下如果本地缺某個(gè)依賴會(huì)直接報(bào)錯(cuò)而不是去下載。所以開啟離線模式的前提是所有依賴都已經(jīng)完整下載到本地。我一般是在項(xiàng)目第一次完整構(gòu)建成功后再開離線模式。5.3 日志輸出太吵怎么辦Spark默認(rèn)會(huì)打一大堆INFO級(jí)別的日志控制臺(tái)刷得飛快真正有用的信息反而被淹沒。解決辦法是在src/main/resources下放一個(gè)log4j.properties文件把日志級(jí)別調(diào)到WARNlog4j.rootCategoryWARN, console log4j.appender.consoleorg.apache.log4j.ConsoleAppender log4j.appender.console.targetSystem.err log4j.appender.console.layoutorg.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern%d{yy/MM/dd HH:mm:ss} %p %c{1}: %m%n這樣控制臺(tái)就清爽多了只輸出警告和錯(cuò)誤以及你自己println的內(nèi)容。這個(gè)配置雖小但對(duì)調(diào)試體驗(yàn)的提升很大。5.4 打包提交時(shí)的注意事項(xiàng)本地跑通之后下一步往往是打包成jar提交到集群。這里有個(gè)關(guān)鍵點(diǎn)Spark相關(guān)的依賴不要打進(jìn)jar包。因?yàn)榧荷弦呀?jīng)有Spark環(huán)境了你打進(jìn)去反而會(huì)造成沖突。在pom里把spark-core、spark-sql這些依賴的scope設(shè)為provideddependency groupIdorg.apache.spark/groupId artifactIdspark-core_2.11/artifactId version${spark.version}/version scopeprovided/scope /dependencyprovided的意思是編譯時(shí)需要、打包時(shí)不帶。這樣打出來的jar體積小提交到集群也不會(huì)和集群自帶的Spark沖突。這個(gè)細(xì)節(jié)很多人第一次提交作業(yè)時(shí)都會(huì)忽略導(dǎo)致jar包幾百兆或者運(yùn)行時(shí)類沖突。6. 關(guān)于這套環(huán)境的一些個(gè)人體會(huì)搭這套環(huán)境的過程本質(zhì)上是在和版本兼容這四個(gè)字較勁。我自己的經(jīng)驗(yàn)是把版本關(guān)系當(dāng)成一張網(wǎng)來看而不是一條線。JDK、Scala、Spark、Maven任意兩個(gè)之間都有兼容性約束牽一發(fā)而動(dòng)全身。所以每次要升級(jí)其中任何一個(gè)都要先確認(rèn)其他三個(gè)是否還兼容。另一個(gè)體會(huì)是Windows平臺(tái)做大數(shù)據(jù)開發(fā)確實(shí)比Linux麻煩主要就麻煩在路徑、編碼、缺少原生工具這幾件事上。但本地開發(fā)用Windows、提交運(yùn)行用Linux這個(gè)組合其實(shí)挺常見把Windows這邊的坑趟平了日常開發(fā)效率并不低。最后分享一個(gè)小習(xí)慣每搭好一套環(huán)境就把pom.xml和settings.xml備份一份。下次換機(jī)器或者重裝系統(tǒng)直接拿過來用能省掉大量重復(fù)勞動(dòng)。環(huán)境搭建這種事做一次就夠了沒必要每次都從零開始。