1 概述
本文主要內容包含Spark SQL讀寫Oracle表數據的簡單案例,並針對案例中比較常見的幾個問題給出解決方法。
最後從常見的java.lang.ClassNotFoundException(無法找到驅動類)的異常問題出發,分析相關的幾種解決方法,以及各個解決方法之間的異同點。
2 案例中比較常見問題及其解決方法
2.1 啓動
首先查看Spark 官網給出的SparkSQL的編程指南部分(http://spark.apache.org/docs/latest/sql-programming-guide.html)的JDBC To Other Databases 內容。參考命令:
SPARK_CLASSPATH=postgresql-9.3-1102-jdbc41.jar bin/spark-shell |
對應寫出訪問 Oracle的命令,如下:
SPARK_CLASSPATH=$SPARK_HOME/ojdbc14.jar bin/spark-shell --master local |
其中,CLASSPATH相關內容會在後一章節給出詳細分析,在此僅針對其他一些常見問題給出解決方法。
啓動過程如下(部分字符串已經被替換,如:$SPARK_HOME):
[hdfs@masternode spark-1.5.2-bin-hadoop2.6]$ SPARK_CLASSPATH=$SPARK_HOME/lib/ojdbc14.jar bin/spark-shell --master local …… Welcome to ____ __ / __/__ ___ _____/ /__ _\ \/ _ \/ _ `/ __/ '_/ /___/ .__/\_,_/_/ /_/\_\ version 1.5.2 /_/
Using Scala version 2.10.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_71) Type in expressions to have them evaluated. Type :help for more information. 16/04/18 11:56:35 INFO spark.SparkContext: Running Spark version 1.5.2 16/04/18 11:56:35 WARN spark.SparkConf: SPARK_CLASSPATH was detected (set to '$SPARK_HOME/lib/ojdbc14.jar'). This is deprecated in Spark 1.0+.
Please instead use: - ./spark-submit with --driver-class-path to augment the driver classpath - spark.executor.extraClassPath to augment the executor classpath
16/04/18 11:56:35 WARN spark.SparkConf: Setting 'spark.executor.extraClassPath' to '$SPARK_HOME/lib/ojdbc14.jar' as a work-around. 16/04/18 11:56:35 WARN spark.SparkConf: Setting 'spark.driver.extraClassPath' to '$SPARK_HOME/lib/ojdbc14.jar' as a work-around. …… 16/04/18 11:56:51 INFO server.AbstractConnector: Started [email protected]:4040 16/04/18 11:56:51 INFO util.Utils: Successfully started service 'SparkUI' on port 4040. 16/04/18 11:56:51 INFO ui.SparkUI: Started SparkUI at http://192.168.149.86:4040 …… 16/04/18 11:56:57 INFO repl.SparkILoop: Created sql context (with Hive support).. SQL context available as sqlContext.
scala> |
下面給出簡單讀寫案例中的常見問題及其解決方法。
2.2 訪問表數據時報找不到合適的驅動
scala> val url = "jdbc:oracle:thin:@xx.xx.xx.xx:1521:dbsid" url: String = jdbc:oracle:thin:@xx.xx.xx.xx:1521:dbsid
scala> val jdbcDF = sqlContext.read.format("jdbc").options( Map( "url" -> url, "dbtable" -> "MyTable", "user" -> "username", "password" -> " password ")).load() java.sql.SQLException: No suitable driver found for jdbc:oracle:thin:@xx.xx.xx.xx:1521:dbsid at java.sql.DriverManager.getConnection(DriverManager.java:596) at java.sql.DriverManager.getConnection(DriverManager.java:187) at org.apache.spark.sql.execution.datasources.jdbc.JDBCRDD$$anonfun$getConnector$1.apply(JDBCRDD.scala:188) at org.apache.spark.sql.execution.datasources.jdbc.JDBCRDD$$anonfun$getConnector$1.apply(JDBCRDD.scala:181) at org.apache.spark.sql.execution.datasources.jdbc.JDBCRDD$.resolveTable(JDBCRDD.scala:121) at org.apache.spark.sql.execution.datasources.jdbc.JDBCRelation.<init>(JDBCRelation.scala:91) at org.apache.spark.sql.execution.datasources.jdbc.DefaultSource.createRelation(DefaultSource.scala:60) at org.apache.spark.sql.execution.datasources.ResolvedDataSource$.apply(ResolvedDataSource.scala:125) at org.apache.spark.sql.DataFrameReader.load(DataFrameReader.scala:114) …… |
報錯信息爲:
java.sql.SQLException: No suitable driver found for jdbc:oracle:thin:@xx.xx.xx.xx:1521:dbsid |
即報錯提示無法找到合適的驅動時,此時可以通過在options方法中指定”driver”屬性爲具體的驅動類來解決,如下所示:
scala> val jdbcDF = sqlContext.read.format("jdbc").options( Map( "url" -> url, "dbtable" -> "TEST_TABLE", "user" -> "userName", "password" -> "password", "driver" -> "oracle.jdbc.driver.OracleDriver")).load() jdbcDF: org.apache.spark.sql.DataFrame = [ID: decimal(0,-127), XX: string, XX: string, XX: string, XX: decimal(0,-127)] |
注意:在1.5版本中,當Oracle表字段爲Number時,對應DataType爲decimal,此時會由於scala的精度斷言拋出異常——可以在stackoverflow網站查找該異常——應該是個bug,在1.6中應該已經解決。有興趣可以試下下。——如果繼續show數據的話,會拋出該異常。
補充:手動加載驅動類也可以解決。
2.3 訪問表數據時報無效調用參數
另外,當屬性名寫錯時的錯誤信息:
——由於在通常寫jdbc的連接時,使用username標識用戶名,(比如在sqoop工具中),在SparkSQL中需要使用user,否則會報以下錯誤(可以在官網或API的描述中查找相關屬性名):
scala> val jdbcDF = sqlContext.read.format("jdbc").options( Map( "url" -> url, "dbtable" -> "TEST_TABLE", "username" -> "userName", "password" -> "password")).load() java.sql.SQLException: invalid arguments in call at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:112) at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:146) at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:208) at oracle.jdbc.driver.T4CConnection.logon(T4CConnection.java:236) |
當出現invalid arguments in call錯誤,表示調用時使用了無效參數,可以檢查options中的參數名是否寫錯,尤其是user是否寫成了username。
下面解析最常見的無法找到驅動類的問題及其解決方法。
3 無法找到無法找到驅動類的問題及其解決方法
擴展知識:JVM的類加載機制——該知識是解決這一類問題的根本方法。
下面的解決方法主要從JVM進程的classpath配置出發去分析。
在Spark框架中,爲JVM進程的classpath配置提供了以下幾種方式:
1. 使用環境變量SPARK_CLASSPATH:將jar包放置在類查找路徑,比如前面的SPARK_CLASSPATH,設置該變量後可以查看4040(默認應用監控端口)的環境設置,此時需要在driver和executor的classpath的路徑中放置所需的jar包。
由前面啓動過程中的日誌可知,這種方式已經廢棄,對應的方式是直接設置driver和executor的classpath相關的配置屬性。
2. 使用配置屬性分別設置driver和executor執行時加載jar包的路徑。
a) spark.executor.extraClassPath
b) spark.driver.extraClassPath
取代使用環境變量進行配置的方法。由於通常情況下,driver和executor的進程不在同一個節點上,因此分別給出各自的配置屬性。
和前面使用環境變量進行配置的方式一樣,在設置相應的classpath路徑之後,需要將對應的jar包部署到各個節點上的該classpath路徑下。
說明:上述兩種方法都是通過設置JVM進程的classpath屬性,爲進程提供jar包查找路徑的。而對應的jar包,需要人爲地部署到各個節點的對應路徑下。
補充:另外可以通過提交命令的命令行選項--driver-class-path來設置driver端進程的classpath,原理類似,因此不作爲單獨一種。
注意:--conf命令項方式設置時,--confPROP=VALUE的等號中間不要加空格….
3. 除了前面兩種方式外,Spark框架還提供了第三種方法,可以通過提交命令(spark-submit或spark-shell)的命令行選項--jars,自動上傳、下載所需jar包,並同時將下載的jar包放入JVM進程的classpath路徑中。
說明:通過該命令行選項設置的jar包,會通過http服務(注意,該服務在driver端啓動後纔會啓動)上傳jar包,並在executor端執行時下載該jar包,然後放入classpath。即,此時不需要手動部署到各個節點上,但每個提交的應用都會增加jar包上傳、下載的網絡IO、磁盤IO開銷。
下面針對提交時的--master選項、--deploymode的選項分別進行解析。
不同選項的簡單說明如下:
1. 當--master選項爲local時,對應爲in-process方式,此時僅一個進程(local-cluster時也對應一個進程,內部通過實例模擬),因此,對應的classpath實際上使用的都是driver短對應進程的classpath。即只需要配置driver的classpath即可。
2. 當--master選項爲集羣的MasterURL(本文主要基於Standalone模式的集羣)時,對應driver和executor是以不同的進程方式啓動,因此需要分別進行設置。並且,在不同的部署模式(--deploymode)下也會有細節上的差異(本質上還是根據JVM類加載機制):
a) --deploymode爲client時:driver進程在當前提交節點上啓動
b) --deploymode爲cluster時:driver進程提交到集羣中,由集羣調度Master負責分配節點,並在分配的節點上啓動driver進程。
說明:不同的部署模式和jar包的上傳、下載有關,即在使用--jars方式時會有所差異,其關鍵點在於,jar包的上傳、下載是通過driver進程啓動過程中啓動的http服務來完成的,當指定的jar包是以本地文件系統的路徑提供時,在另一個節點啓動的driver進程中的http服務根據該路徑上傳jar包時,也會根據本地文件系統指定的路徑去上傳,所以此時必須保證由Master節點分配給driver的節點,對應的該路徑上也存在需要上傳的jar包。
因此,建議在使用cluster部署模式提交應用程序時,所使用的路徑儘可能與節點無關,比如使用hdfs文件系統的路徑等。
3.1 測試類的設計
關於下面使用的SparkTest.jar中的com.TestClass測試類,分別在driver端和executor端同時訪問Oracle的表數據,即兩處都需要加載Oracle的驅動器類。
通過這種方式,方便分別測試driver端和executor端與各自的classpath配置及其jar包放置等相關的內容。
3.1.1 包含Driver與Executor執行邏輯的代碼
如下所示:
object TestJarwithOracle { // 硬編碼 val url = "jdbc:oracle:thin:@xx.xx.xx.xx:1521:dbsid" val user = "userName" val password = "password" val dbtable = "TABLE_TEST"
def main(args: Array[String]) { val conf = new SparkConf().setAppName(this.getClass.getSimpleName) val sc = new SparkContext(conf) val sqlContext = new SQLContext(sc)
val logRDD = sc.parallelize( List( ("name", "action", "date", 1), ("name", "action", "date", 2) )) // 處理邏輯:在Driver端 deleteRecodes("date")
// 處理邏輯:位於Executor端 logRDD.foreachPartition(insertInto) sc.stop() }
def deleteRecodes(date: String): Unit = { var conn: Connection = null var ps: PreparedStatement = null val sql = s"delete from $dbtable where log_date in ('$date')" println(sql) try { Class.forName("oracle.jdbc.driver.OracleDriver") conn = DriverManager.getConnection(url, user, password) ps = conn.prepareStatement(sql) ps.executeUpdate() } catch { case e: Exception => e.printStackTrace } finally { if (ps != null) { ps.close() } if (conn != null) { conn.close() } } }
def insertInto(iterator: Iterator[(String, String, String, Int)]): Unit = { var conn: Connection = null var ps: PreparedStatement = null val sql = s"insert into $dbtable( USER_ACTION, USER_NAME, LOG_DATE, ACTION_CNT) values (?, ?, ?, ?)"
try { //conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/spark", "root", "123456") Class.forName("oracle.jdbc.driver.OracleDriver") conn = DriverManager.getConnection(url, user, password) iterator.foreach(data => { ps = conn.prepareStatement(sql) ps.setString(1, data._1) ps.setString(2, data._2) ps.setString(3, data._3) ps.setInt(4, data._4) // ps.setInt(3, data._3) ps.executeUpdate() } ) } catch { case e: Exception => e.printStackTrace } finally { if (ps != null) { ps.close()
} if (conn != null) { conn.close() } } } } |
3.1.2 代碼說明
前面給出了從oracle(其他數據庫基本一樣)中讀取的簡單案例,對應insert的話,和普通oracle表的insert類似,需要注意的是連接的創建位置(可以參考Spark官網的流部分)。
大致原理簡單描述如下:
1. Driver端創建的對象需要序列化到Executor端才能使用,當特定對象(如數據庫連接)與具體節點綁定(如hostname綁定)時,即使序列化成功,在Executor端反序列化後對象也不能使用(比如反序列化時的初始化失敗或hostname不同導致連接等無法使用等)
2. 一個分區對應一個task,Executor上的執行單位爲task。
在一個執行單位中複用,即針對分區提供一個連接——可以複用連接池。
比如:rdd.foreachPartition(insertInto)
對應的function :insertInto,和普通的數據庫insert方式是一樣的(可以採用批量插入),針對每個分區Partition,然後獲取或構建一個數據庫連接,通過該連接將分區的Iterator數據插入到數據庫表。
3.2 master爲local時
首先查看下SparkSubmit提交應用程序時,一些不支持的組合形式,對應代碼如下所示:
private[deploy] def prepareSubmitEnvironment(args: SparkSubmitArguments) …… case (LOCAL, CLUSTER) => ,,,,,, |
即,在使用local與local-cluster這種local方式時,不支持以CLUSTER的部署模式提交應用程序。
因此以下對應都是在CLIENT的部署模式提交應用程序。
3.2.1 環境變量方式
1. 命令:
2. 說明:
指定SPARK_CLASSPATH時,相當於同時指定driver和executor的classpath,根據前面的分析,實際上local模式下只需要設置driver端的classpath即可,同時需要手動在該路徑下方式所需的jar包,否則會拋出驅動類無法找到的異常。
參考前面啓動章節的啓動日誌中的紅色斜體部分,表示的是SPARK_CLASSPATH已經被廢棄,建議使用斜體部分的對應配置屬性進行替換。
Please instead use: - ./spark-submit with --driver-class-path to augment the driver classpath - spark.executor.extraClassPath to augment the executor classpath
16/04/18 11:56:35 WARN spark.SparkConf: Setting 'spark.executor.extraClassPath' to '$SPARK_HOME/lib/ojdbc14.jar' as a work-around. 16/04/18 11:56:35 WARN spark.SparkConf: Setting 'spark.driver.extraClassPath' to '$SPARK_HOME/lib/ojdbc14.jar' as a work-around. |
3. 測試方式一:在環境變量的路徑下不存在所需jar包,driver和executor端加載類同時異常,執行命令及其異常日誌如下所示:
[hdfs@nodemaster spark-1.5.2-bin-hadoop2.6]$ SPARK_CLASSPATH=$SPARK_HOME/ojdbc14.jar $SPARK_HOME/bin/spark-submit --master local \ > --deploy-mode client \ > --driver-memory 2g \ > --driver-cores 1 \ > --total-executor-cores 2 \ > --executor-memory 4g \ > --conf "spark.ui.port"=4081 \ > --class com.mb.TestJarwithOracle \ > /tmp/test/Spark15.jar 16/04/26 10:31:39 INFO spark.SparkContext: Running Spark version 1.5.2 16/04/26 10:31:40 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable 16/04/26 10:31:40 WARN spark.SparkConf: SPARK_CLASSPATH was detected (set to '/ojdbc14.jar'). This is deprecated in Spark 1.0+.
Please instead use: - ./spark-submit with --driver-class-path to augment the driver classpath - spark.executor.extraClassPath to augment the executor classpath
16/04/26 10:31:40 WARN spark.SparkConf: Setting 'spark.executor.extraClassPath' to '/ojdbc14.jar' as a work-around. 16/04/26 10:31:40 WARN spark.SparkConf: Setting 'spark.driver.extraClassPath' to '/ojdbc14.jar' as a work-around. 16/04/26 10:31:40 INFO spark.SecurityManager: Changing view acls to: hdfs 16/04/26 10:31:40 INFO spark.SecurityManager: Changing modify acls to: hdfs 16/04/26 10:31:40 INFO spark.SecurityManager: SecurityManager: authentication disabled; ui acls disabled; users with view permissions: Set(hdfs); users with modify permissions: Set(hdfs) 16/04/26 10:31:41 INFO slf4j.Slf4jLogger: Slf4jLogger started 16/04/26 10:31:41 INFO Remoting: Starting remoting 16/04/26 10:31:42 INFO Remoting: Remoting started; listening on addresses :[akka.tcp://[email protected]:32898] 16/04/26 10:31:42 INFO util.Utils: Successfully started service 'sparkDriver' on port 32898. 16/04/26 10:31:42 INFO spark.SparkEnv: Registering MapOutputTracker 16/04/26 10:31:42 INFO spark.SparkEnv: Registering BlockManagerMaster 16/04/26 10:31:42 INFO storage.DiskBlockManager: Created local directory at /tmp/blockmgr-43894e1f-4546-477c-91f9-766179306112 16/04/26 10:31:42 INFO storage.MemoryStore: MemoryStore started with capacity 1060.3 MB 16/04/26 10:31:42 INFO spark.HttpFileServer: HTTP File server directory is /tmp/spark-0294727b-0b57-48ff-9f36-f441fa3604aa/httpd-cec03f6e-ca66-49b8-94b9-39427a86ed65 16/04/26 10:31:42 INFO spark.HttpServer: Starting HTTP Server 16/04/26 10:31:42 INFO server.Server: jetty-8.y.z-SNAPSHOT 16/04/26 10:31:42 INFO server.AbstractConnector: Started [email protected]:57115 16/04/26 10:31:42 INFO util.Utils: Successfully started service 'HTTP file server' on port 57115. 16/04/26 10:31:42 INFO spark.SparkEnv: Registering OutputCommitCoordinator 16/04/26 10:31:57 INFO server.Server: jetty-8.y.z-SNAPSHOT 16/04/26 10:31:57 INFO server.AbstractConnector: Started [email protected]:4081 16/04/26 10:31:57 INFO util.Utils: Successfully started service 'SparkUI' on port 4081. 16/04/26 10:31:57 INFO ui.SparkUI: Started SparkUI at http://192.168.149.86:4081 16/04/26 10:31:57 INFO spark.SparkContext: Added JAR file:/tmp/test/Spark15.jar at http://192.168.149.86:57115/jars/Spark15.jar with timestamp 1461637917682 16/04/26 10:31:57 WARN metrics.MetricsSystem: Using default name DAGScheduler for source because spark.app.id is not set. 16/04/26 10:31:57 INFO executor.Executor: Starting executor ID driver on host localhost 16/04/26 10:31:58 INFO util.Utils: Successfully started service 'org.apache.spark.network.netty.NettyBlockTransferService' on port 23561. 16/04/26 10:31:58 INFO netty.NettyBlockTransferService: Server created on 23561 16/04/26 10:31:58 INFO storage.BlockManagerMaster: Trying to register BlockManager 16/04/26 10:31:58 INFO storage.BlockManagerMasterEndpoint: Registering block manager localhost:23561 with 1060.3 MB RAM, BlockManagerId(driver, localhost, 23561) 16/04/26 10:31:58 INFO storage.BlockManagerMaster: Registered BlockManager 16/04/26 10:31:59 INFO scheduler.EventLoggingListener: Logging events to hdfs://nodemaster:8020/user/hdfs/sparklogs/local-1461637917735 delete from TEST_TABLE where log_date in ('date') java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:425) at java.lang.ClassLoader.loadClass(ClassLoader.java:358) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:191) at com.mb.TestJarwithOracle$.deleteRecodes(TestJarwithOracle.scala:38) at com.mb.TestJarwithOracle$.main(TestJarwithOracle.scala:27) at com.mb.TestJarwithOracle.main(TestJarwithOracle.scala) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.apache.spark.deploy.SparkSubmit$.org$apache$spark$deploy$SparkSubmit$$runMain(SparkSubmit.scala:674) at org.apache.spark.deploy.SparkSubmit$.doRunMain$1(SparkSubmit.scala:180) at org.apache.spark.deploy.SparkSubmit$.submit(SparkSubmit.scala:205) at org.apache.spark.deploy.SparkSubmit$.main(SparkSubmit.scala:120) at org.apache.spark.deploy.SparkSubmit.main(SparkSubmit.scala) 16/04/26 10:31:59 INFO spark.SparkContext: Starting job: foreachPartition at TestJarwithOracle.scala:28 16/04/26 10:31:59 INFO scheduler.DAGScheduler: Got job 0 (foreachPartition at TestJarwithOracle.scala:28) with 1 output partitions 16/04/26 10:31:59 INFO scheduler.DAGScheduler: Final stage: ResultStage 0(foreachPartition at TestJarwithOracle.scala:28) 16/04/26 10:31:59 INFO scheduler.DAGScheduler: Parents of final stage: List() 16/04/26 10:31:59 INFO scheduler.DAGScheduler: Missing parents: List() 16/04/26 10:32:00 INFO scheduler.DAGScheduler: Submitting ResultStage 0 (ParallelCollectionRDD[0] at parallelize at TestJarwithOracle.scala:26), which has no missing parents 16/04/26 10:32:00 INFO storage.MemoryStore: ensureFreeSpace(1200) called with curMem=0, maxMem=1111794647 16/04/26 10:32:00 INFO storage.MemoryStore: Block broadcast_0 stored as values in memory (estimated size 1200.0 B, free 1060.3 MB) 16/04/26 10:32:00 INFO storage.MemoryStore: ensureFreeSpace(851) called with curMem=1200, maxMem=1111794647 16/04/26 10:32:00 INFO storage.MemoryStore: Block broadcast_0_piece0 stored as bytes in memory (estimated size 851.0 B, free 1060.3 MB) 16/04/26 10:32:00 INFO storage.BlockManagerInfo: Added broadcast_0_piece0 in memory on localhost:23561 (size: 851.0 B, free: 1060.3 MB) 16/04/26 10:32:00 INFO spark.SparkContext: Created broadcast 0 from broadcast at DAGScheduler.scala:861 16/04/26 10:32:00 INFO scheduler.DAGScheduler: Submitting 1 missing tasks from ResultStage 0 (ParallelCollectionRDD[0] at parallelize at TestJarwithOracle.scala:26) 16/04/26 10:32:00 INFO scheduler.TaskSchedulerImpl: Adding task set 0.0 with 1 tasks 16/04/26 10:32:00 INFO scheduler.TaskSetManager: Starting task 0.0 in stage 0.0 (TID 0, localhost, PROCESS_LOCAL, 2310 bytes) 16/04/26 10:32:00 INFO executor.Executor: Running task 0.0 in stage 0.0 (TID 0) 16/04/26 10:32:00 INFO executor.Executor: Fetching http://192.168.149.86:57115/jars/Spark15.jar with timestamp 1461637917682 16/04/26 10:32:00 INFO util.Utils: Fetching http://192.168.149.86:57115/jars/Spark15.jar to /tmp/spark-0294727b-0b57-48ff-9f36-f441fa3604aa/userFiles-9b936a62-13aa-4ac4-8c26-caabe7bd4367/fetchFileTemp8857984169855770119.tmp 16/04/26 10:32:00 INFO executor.Executor: Adding file:/tmp/spark-0294727b-0b57-48ff-9f36-f441fa3604aa/userFiles-9b936a62-13aa-4ac4-8c26-caabe7bd4367/Spark15.jar to class loader java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:425) at java.lang.ClassLoader.loadClass(ClassLoader.java:358) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:191) at com.mb.TestJarwithOracle$.insertInto(TestJarwithOracle.scala:61) at com.mb.TestJarwithOracle$$anonfun$main$1.apply(TestJarwithOracle.scala:28) at com.mb.TestJarwithOracle$$anonfun$main$1.apply(TestJarwithOracle.scala:28) at org.apache.spark.rdd.RDD$$anonfun$foreachPartition$1$$anonfun$apply$29.apply(RDD.scala:902) at org.apache.spark.rdd.RDD$$anonfun$foreachPartition$1$$anonfun$apply$29.apply(RDD.scala:902) at org.apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:1850) at org.apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:1850) at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:66) at org.apache.spark.scheduler.Task.run(Task.scala:88) at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:214) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) 16/04/26 10:32:00 INFO executor.Executor: Finished task 0.0 in stage 0.0 (TID 0). 915 bytes result sent to driver 16/04/26 10:32:00 INFO scheduler.TaskSetManager: Finished task 0.0 in stage 0.0 (TID 0) in 235 ms on localhost (1/1) |
兩個異常分別對應driver端和executor端。
4. 測試方式二:在環境變量的路徑下存在所需jar包,driver和executor端加載類正常。
環境變量同時設置了driver端和executor端的classpath,只要該路徑下有jar包,驅動類即可加載。
下面通過配置屬性分別進行配置並測試。
3.2.2 配置屬性方式
1. 使用配置屬性提交應用程序的命令:
$SPARK_HOME/bin/spark-submit --master spark://masternode:7078 \[h1] --conf "spark.executor.extraClassPath"="$SPARK_HOME/lib/ojdbc14.jar" \ --conf "spark.driver.extraClassPath"="$SPARK_HOME/lib/ojdbc14.jar" \ --conf "spark.ui.port"=4061 \ --class com.TestClass \ /tmp/test/SparkTest.jar |
通過前面的分析,在local下,只"spark.driver.extraClassPath"有效。
2. 說明:
此時,配置屬性只是指定jar包在classpath指定的路徑下,但沒有手動將所需的jar包部署到該路徑下,因此加載驅動器類時會拋出java.lang.ClassNotFoundException:oracle.jdbc.driver.OracleDriver的異常。
16/04/14 14:23:18 INFO spark.MapOutputTrackerMaster: Size of output statuses for shuffle 0 is 154 bytes 16/04/14 14:23:19 WARN scheduler.TaskSetManager: Lost task 0.0 in stage 1.0 (TID 2, 192.168.149.98): java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:425) at java.lang.ClassLoader.loadClass(ClassLoader.java:358) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:191) at com.TestClass$.insertInto(WebLogExtractor.scala:48) at com.TestClass$$anonfun$main$1.apply(WebLogExtractor.scala:43) at com.TestClass$$anonfun$main$1.apply(WebLogExtractor.scala:43) at org.apache.spark.rdd.RDD$$anonfun$foreachPartition$1$$anonfun$apply$29.apply(RDD.scala:902) at org.apache.spark.rdd.RDD$$anonfun$foreachPartition$1$$anonfun$apply$29.apply(RDD.scala:902) at org.apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:1850) at org.apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:1850) at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:66) at org.apache.spark.scheduler.Task.run(Task.scala:88) at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:214) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) |
4. 解決方法:
將對應ojdbc14.jar拷貝到當前節點的"spark.driver.extraClassPath"路徑下即可。
5. 擴展:
可以嘗試"spark.executor.extraClassPath"與"spark.driver.extraClassPath"是否設置的幾種情況分別進行測試,以驗證前面針對local時,僅"spark.driver.extraClassPath"有效的分析。
再次總結:只要設置"spark.driver.extraClassPath",並在該路徑下放置了jar包即可。
3.2.3 自動上傳jar包方式
當部署模式爲CLIENT時,應用程序(對應Driver)會將childMainClass設置爲傳入的mainClass,然後啓動JVM進程,對應代碼如下所示:
if (deployMode == CLIENT) { childMainClass = args.mainClass if (isUserJar(args.primaryResource)) { childClasspath += args.primaryResource } if (args.jars != null) { childClasspath ++= args.jars.split(",") } if (args.childArgs != null) { childArgs ++= args.childArgs } } |
在client模式,直接啓動應用程序的主類,同時,將主類的jar包和添加的jars包(如果在參數中設置的話)都添加到運行時的classpath中。即當前的driver的classpath會自動包含--jars 設置的jar包。
同時,driver通過啓動的http服務上傳該jar包,executor在執行時下載該jar包,同時放置到executor進程的classpath路徑。
測試案例的構建:刪除前面的環境變量或兩個配置屬性的設置,直接用--jars命令行選項指定所需的第三方jar包(即這裏的驅動類jar包)即可。例如:
$SPARK_HOME/bin/spark-submit --master local \ --deploy-mode client \ --driver-memory 2g \ --driver-cores 1 \ --total-executor-cores 2 \ --executor-memory 4g \ --conf "spark.ui.port"=4081 \ --class com.mb.TestJarwithOracle \ --jars "$SPARK_HOME/thirdlib/ojdbc14.jar" \ /tmp/test/Spark15.jar |
此時會直--jars指定的jar包加入classpath路徑,因此可以成功加載驅動類。
當將應用程序提交到集羣中時,對應不同的部署模式(--deploy-mode)會有不同的情況,因此下面分別針對不同的部署模式進行分析。
3.3 master爲集羣的MasterURL+部署模式爲Client時
Client部署模式時,在提交點啓動應用程序,因此對應driver端也在提交節點。此時,"spark.driver.extraClassPath"路徑對應提交節點的路徑。Executor則由調度分配到其他執行節點,此時"spark.executor.extraClassPath"對應的路徑應是針對實際分配執行executor的節點(不是提交節點!)。
3.3.1 配置屬性方式
屬於:集羣中jar包部署+ 配置屬性的方式
通過前面的測試與分析,應該可以知道配置屬性的方式只是將所需jar包放入類加載時查找的路徑中,而對應的jar包需要人爲去部署。
對應在分佈式集羣環境下,相關的JVM進程可能在各個節點上啓動,包括driver和executor進程。因此當某個節點啓動某類進程時,需要保證已經手動在該節點上,對應於配置屬性所設置的路徑下,已經存在或部署了所需的jar包。
進一步地,通常由資源調度器負責分配節點,運行進程,因此爲了保證分配的節點上的進程能成功加載所需類,應該在集羣的所有節點上部署所需jar包。
優點:一次部署多次使用
缺點:jar包衝突
3.3.1.1 測試1
1. 啓動命令:
$SPARK_HOME/bin/spark-submit --master spark://masternode:7078 \ --conf "spark.executor.extraClassPath"="$SPARK_HOME/thirdlib/ojdbc14.jar" \ --conf "spark.driver.extraClassPath"="$SPARK_HOME/thirdlib/ojdbc14.jar" \ --conf "spark.ui.port"=4071 \ --class com.mb.TestJarwithOracle \ /tmp/test/SparkTest.jar |
2. 測試說明:
a) 通過--conf命令行選項,設置Driver與Executor端的classpath配置屬性。
b) 在Driver端的classpath路徑下放置所需的jar包。
c) 在Executor端的classpath路徑下刪除所需的jar包。
補充:這裏放置或刪除jar包,可以簡單通過修改對應配置屬性的路徑來模擬。
3. 測試結果:
a) Driver端與Oracle的操作:由於設置了classpath路徑,同時該路徑下放置了所需的jar包,因此操作成功,直接查看driver的終端輸出日誌。
b) Executor端與Oracle的操作:雖然設置了classpath路徑,但該路徑下沒有放置所需的jar包,因此操作失敗,錯誤信息如下所示(查看4040-默認端口-的executor頁面,找到對應的stderr日誌信息):
3.3.1.2 測試2
1. 啓動命令:
$SPARK_HOME/bin/spark-submit --master spark://masternode:7078 \ --conf "spark.executor.extraClassPath"="$SPARK_HOME/thirdlib/ojdbc14.jar" \ --conf "spark.driver.extraClassPath"="$SPARK_HOME/thirdlib/ojdbc14.jar" \ --conf "spark.ui.port"=4071 \ --class com.mb.TestJarwithOracle \ /tmp/test/SparkTest.jar |
2. 測試說明:
a) 通過--conf命令行選項,設置Driver與Executor端的classpath配置屬性。
b) 在Executor端的classpath路徑下放置所需的jar包。
c) 在Driver端的classpath路徑下刪除所需的jar包。
3. 測試結果:
a) Executor端與Oracle的操作:由於設置了classpath路徑,同時該路徑下放置了所需的jar包,因此操作成功。
b) Driver端與Oracle的操作:雖然設置了classpath路徑,但該路徑下沒有放置所需的jar包,因此操作失敗,直接查看終端的錯誤信息,如下所示:
java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:425) at java.lang.ClassLoader.loadClass(ClassLoader.java:358) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:191) at com.mb.TestJarwithOracle$.deleteRecodes(TestJarwithOracle.scala:38) at com.mb.TestJarwithOracle$.main(TestJarwithOracle.scala:27) at com.mb.TestJarwithOracle.main(TestJarwithOracle.scala) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.apache.spark.deploy.SparkSubmit$.org$apache$spark$deploy$SparkSubmit$$runMain(SparkSubmit.scala:674) at org.apache.spark.deploy.SparkSubmit$.doRunMain$1(SparkSubmit.scala:180) at org.apache.spark.deploy.SparkSubmit$.submit(SparkSubmit.scala:205) at org.apache.spark.deploy.SparkSubmit$.main(SparkSubmit.scala:120) at org.apache.spark.deploy.SparkSubmit.main(SparkSubmit.scala) |
補充說明:Driver端捕捉了異常,因此Executor可以繼續執行。
3.3.1.3 擴展1
在classpath對應的這兩個配置屬性中,使用不同路徑的結果是不同的,比如前面測試案例中的配置屬性對應路徑修改爲hdfs文件系統路徑時,driver端的驅動類加載會拋出異常,命令如下:
$SPARK_HOME/bin/spark-submit --master spark://nodemaster:7078 \ --deploy-mode client \ --driver-memory 2g \ --driver-cores 1 \ --total-executor-cores 2 \ --executor-memory 4g \ --conf "spark.ui.port"=4081 \ --conf "spark.executor.extraClassPath"="hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" \ --conf "spark.driver.extraClassPath"="hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" \ --class com.mb.TestJarwithOracle \ /tmp/test/Spark15.jar |
簡單理解:
1. Driver端classpath相關的配置:在啓動應用程序(Driver)時作爲JVM的Options使用,此時只能識別本地路徑,使用hdfs文件系統路徑的話,無法識別,因此類加載會失敗。
2. Executor端classpath相關的配置:會根據指定的路徑去下載jar包,hdfs等文件系統以及被封裝,因此可以下載到本地——對應默認在work路徑的app目錄中,然後添加到僅的classpath路徑下,因此可以識別hdfs等文件系統的路徑。
3.3.1.4 擴展2
由於之前有人提過幾次類似的問題, 再此順便給出簡單說明。
異常日誌如下所示:
16/04/26 11:35:58 ERROR util.SparkUncaughtExceptionHandler: Uncaught exception in thread Thread[appclient-registration-retry-thread,5,main] java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@6946dc9c rejected from java.util.concurrent.ThreadPoolExecutor@2b3dbbc3[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0] at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2048) at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:821) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1372) at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:110) at org.apache.spark.deploy.client.AppClient$ClientEndpoint$$anonfun$tryRegisterAllMasters$1.apply(AppClient.scala:96) at org.apache.spark.deploy.client.AppClient$ClientEndpoint$$anonfun$tryRegisterAllMasters$1.apply(AppClient.scala:95) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244) at scala.collection.IndexedSeqOptimized$class.foreach(IndexedSeqOptimized.scala:33) at scala.collection.mutable.ArrayOps$ofRef.foreach(ArrayOps.scala:108) at scala.collection.TraversableLike$class.map(TraversableLike.scala:244) at scala.collection.mutable.ArrayOps$ofRef.map(ArrayOps.scala:108) at org.apache.spark.deploy.client.AppClient$ClientEndpoint.tryRegisterAllMasters(AppClient.scala:95) at org.apache.spark.deploy.client.AppClient$ClientEndpoint.org$apache$spark$deploy$client$AppClient$ClientEndpoint$$registerWithMaster(AppClient.scala:121) at org.apache.spark.deploy.client.AppClient$ClientEndpoint$$anon$2$$anonfun$run$1.apply$mcV$sp(AppClient.scala:132) at org.apache.spark.util.Utils$.tryOrExit(Utils.scala:1119) at org.apache.spark.deploy.client.AppClient$ClientEndpoint$$anon$2.run(AppClient.scala:124) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:304) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:178) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) 16/04/26 11:35:58 INFO storage.DiskBlockManager: Shutdown hook called 16/04/26 11:35:58 INFO util.ShutdownHookManager: Shutdown hook called |
僅根據異常無法判斷具體錯誤信息,需要跟蹤其堆棧信息。根據堆棧信息,可以知道是AppClient(代表應用程序客戶端)中的ClientEndpoint(RPC通訊終端)嘗試註冊到Master時被拒絕 ——此時,檢查提交應用程序時使用的Master URL是否正確即可。
經測試驗證,當使用錯誤的Master URL時,會拋出以上異常信息。
3.3.2 自動上傳jar包方式
當部署模式爲CLIENT時,應用程序(對應Driver)會將childMainClass設置爲傳入的mainClass,然後啓動JVM進程,對應代碼如下所示:
if (deployMode == CLIENT) { childMainClass = args.mainClass if (isUserJar(args.primaryResource)) { childClasspath += args.primaryResource } if (args.jars != null) { childClasspath ++= args.jars.split(",") } if (args.childArgs != null) { childArgs ++= args.childArgs } } |
在client模式,直接啓動應用程序的主類,同時,將主類的jar包和添加的jars包(如果在參數中設置的話)都添加到運行時的classpath中。即當前的driver的classpath會自動包含--jars 設置的jar包。
同時,driver通過啓動的http 服務上傳該jar包,executor在執行時下載該jar包,同時放置到executor進程的classpath路徑。
因此,--jars相當於:
1. 通過"spark.driver.extraClassPath"配置driver 端。
2. 通過"spark.executor.extraClassPath"配置executor端,同時將指定的jar包上傳到http 服務,並下載到executor端的該配置路徑下。
3.4 master爲集羣的MasterURL+部署模式爲Cluster時
CLUSTER部署模式時,Driver在某個節點提交,但卻是在集羣調度分配的節點上運行。
此時,可以將Driver看成是特殊的Executor,同樣由分配的節點運行JVM進程,但對應進程的classpath配置信息(補充說明下,看配置屬性的extra名,應該可以知道是附加的,或新增的classpath內容,而不是全部)由各自對應的配置屬性進行設置。
CLUSTER部署模式在Drive分配到節點,並在節點上啓動,相關屬性配置及其作用等和CLIENT部署模式基本一致。這裏僅基於配置屬性方式針對Driver進行解析。
3.4.1 應用程序提交方式
下面給出兩種形式的提交命令:
1. 基於REST服務提交方式
$SPARK_HOME/bin/spark-submit --master spark://nodemaster:6066 \ --deploy-mode cluster \ --driver-memory 2g \ --driver-cores 1 \ --total-executor-cores 2 \ --executor-memory 4g \ --conf "spark.ui.port"=4081 \ --conf "spark.executor.extraClassPath"="hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" \ --conf "spark.driver.extraClassPath"="hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" \ --class com.mb.TestJarwithOracle \ hdfs://nodemaster:8020/tmp/sptest/Spark15.jar |
該方式採用REST服務作爲masterurl提交應用程序。對應的值參考8080(默認)監控界面,如下所示:
1.5.2 Spark Master at spark://nodemaster:7078
· URL: spark://nodemaster:7078
· RESTURL: spark://nodemaster:6066 (cluster mode)
· AliveWorkers: 7
· Coresin use: 84 Total, 0 Used
· Memoryin use: 420.0 GB Total, 0.0 B Used
· Applications: 0 Running, 106 Completed
· Drivers: 0 Running, 11 Completed
· Status: ALIVE
日誌:
Running Spark using the REST application submission protocol. 16/04/26 13:15:02 INFO rest.RestSubmissionClient: Submitting a request to launch an application in spark://nodemaster:7078. 16/04/26 13:15:03 WARN rest.RestSubmissionClient: Unable to connect to server spark://nodemaster:7078. Warning: Master endpoint spark://nodemaster:7078 was not a REST server. Falling back to legacy submission gateway instead. 16/04/26 13:15:04 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable |
當REST 服務方式提交嘗試失敗後,會退回到傳統方式進行提交。
2. 傳統提交方式
$SPARK_HOME/bin/spark-submit --master spark://nodemaster:7078 \ --deploy-mode cluster \ --driver-memory 2g \ --driver-cores 1 \ --total-executor-cores 2 \ --executor-memory 4g \ --conf "spark.ui.port"=4081 \ --conf "spark.executor.extraClassPath"="hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" \ --conf "spark.driver.extraClassPath"="hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" \ --class com.mb.TestJarwithOracle \ hdfs://nodemaster:8020/tmp/sptest/Spark15.jar |
日誌:
Running Spark using the REST application submission protocol. 16/04/26 13:20:58 INFO rest.RestSubmissionClient: Submitting a request to launch an application in spark://nodemaster:6066. 16/04/26 13:20:58 INFO rest.RestSubmissionClient: Submission successfully created as driver-20160426132058-0010. Polling submission state... 16/04/26 13:20:58 INFO rest.RestSubmissionClient: Submitting a request for the status of submission driver-20160426132058-0010 in spark://nodemaster:6066. 16/04/26 13:20:58 INFO rest.RestSubmissionClient: State of driver driver-20160426132058-0010 is now RUNNING. 16/04/26 13:20:58 INFO rest.RestSubmissionClient: Driver is running on worker worker-20160418110627-192.168.149.95-45661 at 192.168.149.95:45661. 16/04/26 13:20:58 INFO rest.RestSubmissionClient: Server responded with CreateSubmissionResponse: { "action" : "CreateSubmissionResponse", "message" : "Driver successfully submitted as driver-20160426132058-0010", "serverSparkVersion" : "1.5.2", "submissionId" : "driver-20160426132058-0010", "success" : true } |
3.4.2 提交應用之後Driver的分析
兩種方式都可以成功提交應用程序,對應在界面會分別增加一個Driver,同時Driver啓動後,會和之前Client部署模式的流程一樣,提交一個Application(這裏的概念對應Executor),對應界面(8080默認端口界面-最下面)有:
兩個driver提交的應用如下(8080默認端口界面):
對應下面的Application,配置及其影響和前面是一樣的,只是當前的Driver在分配的節點上運行(所有相關路徑等概念都改爲基於該執行節點)。因此下面僅分析Driver的相關內容。
信息獲取相關操作:
1. 點擊Driver行所在的Worker,跳轉到該Worker監控頁面,到最下面,查找與Driver的SubmissionID相同的Driver信息,界面如下所示:
2. 點擊對應的stderr日誌信息,可以看到Driver的啓動命令及其執行日誌
3. 查看Driver的啓動命令
a) 對應傳統提交方式的日誌如下所示:
Launch Command: "/usr/java/jdk1.7.0_71/bin/java" "-cp" "hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar:$SPARK_HOME/sbin/../conf/:$SPARK_HOME/lib/spark-assembly-1.5.2-hadoop2.6.0.jar:$SPARK_HOME/lib/datanucleus-api-jdo-3.2.6.jar:$SPARK_HOME/lib/datanucleus-core-3.2.10.jar:$SPARK_HOME/lib/datanucleus-rdbms-3.2.9.jar:/etc/hadoop/conf/" "-Xms2048M" "-Xmx2048M" "-Dspark.deploy.defaultCores=4" "-Dspark.eventLog.enabled=true" "-Dakka.loglevel=WARNING" "-Dspark.history.fs.cleaner.maxAge=7d" "-Dspark.submit.deployMode=cluster" "-Dspark.executor.memory=4g" "-Dspark.executor.extraClassPath=hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" "-Dspark.executor.extraJavaOptions=-XX:+PrintGCDetails" "-Dspark.jars=hdfs://nodemaster:8020/tmp/sptest/Spark15.jar" "-Dspark.history.fs.cleaner.enabled=true" "-Dspark.master=spark://nodemaster:7078" "-Dspark.driver.supervise=false" "-Dspark.driver.extraClassPath=hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" "-Dspark.app.name=com.mb.TestJarwithOracle" "-Dspark.history.fs.logDirectory=hdfs://nodemaster:8020/user/hdfs/sparklogs" "-Dspark.driver.memory=2g" "-Dspark.cores.max=2" "-Dspark.rpc.askTimeout=10" "-Dspark.eventLog.dir=hdfs://nodemaster:8020/user/hdfs/sparklogs" "-Dspark.ui.port=4081" "-Dspark.history.fs.cleaner.interval=1d" "-Dspark.driver.cores=1" "-XX:MaxPermSize=256m" "org.apache.spark.deploy.worker.DriverWrapper" "akka.tcp://[email protected]:45661/user/Worker" "$SPARK_HOME/work/driver-20160426131505-0009/Spark15.jar" "com.mb.TestJarwithOracle" |
b) 對應REST提交方式的日誌如下所示:
Launch Command: "/usr/java/jdk1.7.0_71/bin/java" "-cp" "hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar:$SPARK_HOME/sbin/../conf/:$SPARK_HOME/lib/spark-assembly-1.5.2-hadoop2.6.0.jar:$SPARK_HOME/lib/datanucleus-api-jdo-3.2.6.jar:$SPARK_HOME/lib/datanucleus-core-3.2.10.jar:$SPARK_HOME/lib/datanucleus-rdbms-3.2.9.jar" "-Xms2048M" "-Xmx2048M" "-Dspark.deploy.defaultCores=4" "-Dspark.eventLog.enabled=true" "-Dspark.history.fs.cleaner.maxAge=7d" "-Dspark.submit.deployMode=cluster" "-Dspark.executor.memory=4g" "-Dspark.executor.extraClassPath=hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" "-Dspark.executor.extraJavaOptions=-XX:+PrintGCDetails" "-Dspark.jars=hdfs://nodemaster:8020/tmp/sptest/Spark15.jar" "-Dspark.history.fs.cleaner.enabled=true" "-Dspark.master=spark://nodemaster:7078" "-Dspark.driver.supervise=false" "-Dspark.driver.extraClassPath=hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" "-Dspark.app.name=com.mb.TestJarwithOracle" "-Dspark.history.fs.logDirectory=hdfs://nodemaster:8020/user/hdfs/sparklogs" "-Dspark.driver.memory=2g" "-Dspark.cores.max=2" "-Dspark.eventLog.dir=hdfs://nodemaster:8020/user/hdfs/sparklogs" "-Dspark.ui.port=4081" "-Dspark.history.fs.cleaner.interval=1d" "-Dspark.driver.cores=1" "-XX:MaxPermSize=256m" "org.apache.spark.deploy.worker.DriverWrapper" "akka.tcp://[email protected]:45661/user/Worker" "$SPARK_HOME/work/driver-20160426132058-0010/Spark15.jar" "com.mb.TestJarwithOracle"
|
(暫時不考慮兩者的差異,僅關注Driver進程相關的內容。)
下面對其中比較重要的幾個部分進行分析:
1. 其中,啓動的JVM進程主類爲"org.apache.spark.deploy.worker.DriverWrapper",在"-cp"後加入了hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar,這裏對應的是提交命令中的--conf"spark.driver.extraClassPath"配置屬性(可通過修改設置的路徑進行驗證)。
在該啓動命令(提交命令的配置參數)中,ojdbc14.jar路徑爲hdfs,因此無法識別,在driver部分的邏輯代碼執行時會拋出異常。對應日誌在該LaunchCommand:命令後面。因此"spark.driver.extraClassPath"配置屬性中設置的路徑應該對應當前節點(由於是調度分配的,對應就意味着應該是在集羣中各個節點都進行部署)的路徑,並且路徑下有所需jar包。
2. 對應的spark.driver.extraClassPath、spark.executor.extraClassPath等會繼續作爲Driver的參數傳入(和Client部署模式下直接執行Driver一樣)
3. "-Dspark.jars=hdfs://nodemaster:8020/tmp/sptest/Spark15.jar":對應提交時的主資源。
4. "$SPARK_HOME/work/driver-20160426132058-0010/Spark15.jar":work是默認的工作目錄,driver-20160426132058-0010是當前Driver對應的Submission ID,在該目錄中會下載Driver執行所需的Jar包,這裏對應主資源Spark15.jar。
此時,需要注意的是,當CLUSTER部署模式時,如果使用的主資源是本地路徑,如以下命令:
$SPARK_HOME/bin/spark-submit --master spark://nodemaster:6066 \ --deploy-mode cluster \ --driver-memory 2g \ --driver-cores 1 \ --total-executor-cores 2 \ --executor-memory 4g \ --conf "spark.ui.port"=4081 \ --conf "spark.executor.extraClassPath"="hdfs://nodemaster:8020/tmp/sptest/ojdbc14.jar" \ --conf "spark.driver.extraClassPath"="$SPARK_HOME/thirdlib/ojdbc14.jar" \ --class com.mb.TestJarwithOracle \ /tmp/test/Spark15.jar |
其中紅色部分對應爲主資源jar包,採用本地文件系統的路徑。執行時,Driver端輸出的錯誤信息如下所示(對應界面的Driver狀態爲Error):
java.io.FileNotFoundException: /tmp/test/Spark15.jar (No such file or directory) java.io.FileInputStream.open(Native Method) java.io.FileInputStream.<init>(FileInputStream.java:146) org.spark-project.guava.io.Files$FileByteSource.openStream(Files.java:124) org.spark-project.guava.io.Files$FileByteSource.openStream(Files.java:114) org.spark-project.guava.io.ByteSource.copyTo(ByteSource.java:202) org.spark-project.guava.io.Files.copy(Files.java:436) org.apache.spark.util.Utils$.org$apache$spark$util$Utils$$copyRecursive(Utils.scala:514) org.apache.spark.util.Utils$.copyFile(Utils.scala:485) org.apache.spark.util.Utils$.doFetchFile(Utils.scala:562) org.apache.spark.util.Utils$.fetchFile(Utils.scala:369) org.apache.spark.deploy.worker.DriverRunner.org$apache$spark$deploy$worker$DriverRunner$$downloadUserJar(DriverRunner.scala:150) org.apache.spark.deploy.worker.DriverRunner$$anon$1.run(DriverRunner.scala:79) |
在DriverRunner啓動後,調用downloadUserJar,下載所需jar包,但此時使用本地文件系統的路徑,對應的就需要在Driver當前執行節點上的該路徑下存在該jar包(當前未部署),因此報異常:java.io.FileNotFoundException:/tmp/test/Spark15.jar。
因此,對應在CLUSTER部署模式時,需要注意提交應用程序對應的主資源的配置:
1. 將主資源類似於其他的第三方jar包(如Oracle驅動類庫的jar包)部署到集羣中;
2. 使用HDFS這類文件系統,可以下載到本地工作目錄。
4 總結
通過配置classpath,爲JVM加載類時提供搜索路徑。
在分佈式計算集羣中,需要注意JVM進程是在哪臺節點上啓動,對應節點上的classpath下是否部署了所需的jar包(針對jar包以本地路徑的形式,即與具體節點相關的路徑)。
因此總結起來有以下兩點:
1. 是否爲對應JVM進程指定了classpath;
2. 在各個進程的classpath路徑下是否放置了所需的jar包。放置的方式可以有兩種:
a) 一種是Spark框架提供的自動放置到classpath的方式;
b) 一種手動在集羣中部署的方式;
這裏指的路徑,表示的是該JVM進程能夠讀取的路徑,比如本地文件系統路徑、hdfs路徑,其中本地文件系統路徑是針對本節點的,這點在分佈式集羣中尤其要注意。