Java連接Spark Standalone集羣

軟件環境:

spark-1.6.3-bin-hadoop2.6、hadoop-2.6.4、jdk1.7.0_67、IDEA14.1.5 ;

Hadoop集羣採用僞分佈式安裝,運行過程中只啓動HDFS;Spark只啓動一個Worker;使用虛擬機搭建Hadoop、Spark集羣;Idea直接安裝在Win10上;192.168.128.128是虛擬機ip;本機ip是:192.168.0.183;

Java連接Spark集羣,如果採用YARN的方式,可以參考:Java Web提交任務到Spark ;寫此篇的初衷是,在使用的過程中發現使用YARN調用Spark集羣效率太低,所以嘗試使用Java直接連接Spark Standalone集羣。同時,需要說明一點,這裏使用的是一個節點,如果使用多個節點情況可能有所不同。


本次測試一共進行了5次實驗,最終達到一個既可以連接Spark Standalone集羣,同時可以監控該任務的目的。所有代碼可以在 https://github.com/fansy1990/JavaConnectSaprk01 下載。

任務1:設置master直接連接

1.1. 創建Scala工程

設置SDK、JDK以及spark-assembly的jar包到Classpath,創建好的工程如下:


1.2 創建示例程序

這裏使用的是單詞計數程序,代碼如下:
package demo

import org.apache.spark.{SparkContext, SparkConf}

/**
 * Created by fansy on 2017/7/5.
 */
object WordCount {
  def main(args: Array[String]) {

    val input = "hdfs://192.168.128.128:8020/user/root/magic"
    val output =""

    val appName = "word count"
    val master = "spark://192.168.128.128:7077"

    val conf = new SparkConf().setAppName(appName).setMaster(master)
    val sc = new SparkContext(conf)

    val line = sc.textFile(input)

    line.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).collect().foreach(println)

    sc.stop()
  }
}
這裏面直接設置spark集羣中的地址,然後連接運行任務,運行完成後,執行sc.stop,關閉SparkContext,但是運行後出現錯誤:
ClassNotFound錯誤,出現這個錯誤,肯定是類找不到了,爲什麼找不到呢?

1.3 問題分析

如果要分析這個問題,那麼需要連接Spark執行過程,看日誌:


從日誌中可以看出,程序運行後,會先去連接Master,連接上Master後,會在啓動程序的地方,也就是本機win10上面啓動Driver(其實可以理解爲啓動BlockManager),Driver程序控制整個APP的生命週期,同時SparkContext也在這個上面運行。接着,Master就會分配Worker的資源給這個App,供App使用運行自己的業務邏輯。所以實際運行任務的是Worker,Worker上面運行demo.WordCount的邏輯,但是並沒有把demo.WordCount發給Worker,所以導致Worker找不到demo.WordCount這個類,也就會出現ClassNotFound的錯誤了。

任務2:添加業務邏輯Jar路徑,連接Master

2.1 修改代碼

如果需要添加對應的jar路勁,只需在代碼中添加addJars即可,如下:
val jars =Array("C:\\Users\\fansy\\workspace_idea_tmp\\JavaConnectSaprk01\\out\\artifacts\\wordcount\\wordcount.jar")

    val conf = new SparkConf().setAppName(appName).setMaster(master).setJars(jars)

2.2 運行代碼,觀察結果

打包後,運行代碼,結果如下:


會發現,出現多了一行日誌,把該Jar包添加到了Driver所在機器的路徑上(可以理解爲聲明瞭一個公共的Classpath,或者理解爲Worker就可以訪問到了);
同時任務可以往下運行,繼續運行,可以得到結果:

可以看到打印的單詞統計的結果。

任務3 Driver運行在不同節點的嘗試

3.1 任務描述

在使用Spark On YARN的方式提交Spark任務的時候,可以讓Driver運行在集羣中,即Cluster模式。這樣,如果同時有多個客戶端提交任務,就不會佔用客戶端太多的資源(想象一下,如果一個Driver需要默認256M內存資源,那100個客戶端就是25G左右內存了),而是佔用集羣資源。所以是否可以通過設置,使得Driver不在win10上運行,而在集羣上運行?
注意:這裏如果要採用這種模式,那麼集羣只要要有額外資源供除了worker使用外,還需要給Driver預留一定資源。

3.2 嘗試修改driver.host參數

修改代碼,添加:
val conf = new SparkConf().setAppName(appName).setMaster(master).setJars(jars)
      .set("spark.eventLog.enabled","true")
    .set("spark.eventLog.dir","hdfs://node10:8020/eventLog")
    .set("spark.driver.host","192.168.128.128")
    .set("spark.driver.port","8993")
    val sc = new SparkContext(conf)
運行,發現提交作業都提交不了了,暫時沒有發現原因,好像把Driver設置到其他節點上面這種方式是有問題的(至少目前對於Standalone這種模式來說)。

任務4 Java線程運行Spark程序提交到Spark StandAlone集羣

4.1 任務實現思路

既然使用任務2可以提交任務到Spark Standalone集羣,並且能正確運行,那麼是否可以設置一個多線程用於調用這個APP,然後在主程序中查看這個多線程運行情況,根據線程任務返回值,判斷任務是否執行成功。

4.2 任務實現

線程類:
package demo03;


import java.util.concurrent.Callable;

/**
 * 線程任務
 * Created by fansy on 2017/7/5.
 */
public class RunTool implements Callable {

    private String input;
    private String output;
    private String appName;
    private String master;
    private String jars;
    private String logEnabled;
    private String logDir;

    public RunTool(){}
    public RunTool(String[] args){
        this.input = args[0];
        this.output = args[1];
        this.appName = args[2];
        this.master = args[3];
        this.jars = args[4];
        this.logEnabled = args[5];
        this.logDir = args[6];
    }

    @Override
    public Boolean call() throws Exception {
        return WordCount.run(new String[]{input,output,appName,master,jars,logEnabled,logDir});
    }
}
線程類採用實現Callable接口,有返回值,根據返回值在主類中進行判斷;
主類:
package demo03;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * Created by fansy on 2017/7/5.
 */
public class Driver {
    public static void main(String[] args) {
//        <input> <output> <appName> <master>"
//        " <jars> <logEnabled> <logDir>
        String[] arg = new String[]{
            "hdfs://node10:8020/user/root/magic",
                "",
                "wordcount" + System.currentTimeMillis(),
                "spark://node10:7077",
                "C:\\Users\\fansy\\workspace_idea_tmp\\JavaConnectSaprk01\\out\\artifacts\\wordcount\\wordcount.jar",
                "true",
                "hdfs://node10:8020/eventLog"
        };
        FutureTask<Boolean> future = new FutureTask<>(new RunTool(arg));
        new Thread(future).start();
        boolean flag = true;
        while(flag){
            try{
                Thread.sleep(2000);
                System.out.println("Job running ...");
                if(future.isDone()){
                    flag = false;
                    if(future.get().booleanValue()){
                        System.out.println("Job done with success state");
                    }else{
                        System.out.println("Job failed!");
                    }
                }
            }catch (InterruptedException|ExecutionException e){
                e.printStackTrace();
            }
        }
    }
}

主類中,每2秒刷新次,看下線程任務執行狀態,最後根據線程任務返回值,判斷任務是否執行成功;

任務5 加入多信息監控

5.1 任務描述

在任務4中已經可以實現相關任務調用、任務監控的功能,但是在任務監控這塊並沒有執行APP的一些信息,比如一共有多少個Job,每個job運行的狀態等等,這節就是加入這些信息。

5.2 實現思路

在任務4中的demo03.WordCount中發現,在初始化SparkContext後,就會有一個APPID了,所以這裏可以把初始化SparkContext和實際運行業務邏輯的代碼分開,而多線程的任務調度放在業務邏輯上。
在主類中,先初始化SparkContext,獲取sc;然後把此sc傳入業務邏輯中,供其使用,在使用完成後,在業務邏輯類中關閉sc。最後在主類中使用sc來監控App執行情況,這裏需要注意的是,使用sc來監控App的執行情況,只能監控到App裏面的Job的狀態,如成功或失敗。這裏需要注意的是Job的成功與失敗和最後任務的成功和失敗是有區別的,Job可能會fail,但是job fail的App,其最終的結果可能是成功執行的。所以這裏還需要加上總任務的執行情況,也就是使用任務4中的返回值來判斷。
這裏需要明確一點:SparkContext啓動後,可以運行多個job;而AppID對應一個SparkContext;但是Job的個數不是SparkContext可以預知的,也就是說業務邏輯代碼生成的Job可以是多個的,也就是說不能夠通過job運行情況來判斷整個任務運行的失敗與否。

5.3 具體實現

Driver類:
package demo04;


import org.apache.spark.SparkContext;
import org.apache.spark.SparkJobInfo;
import org.apache.spark.SparkStatusTracker;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * Created by fansy on 2017/7/5.
 */
public class Driver {
    public static void main(String[] args) throws InterruptedException {
        String master = "spark://node10:7077";
        String appName = "wordcount" + System.currentTimeMillis();
        String[] jars = "C:\\Users\\fansy\\workspace_idea_tmp\\JavaConnectSaprk01\\out\\artifacts\\wordcount\\wordcount.jar".split(",");
        String logEnabled = "true";
        String logDir = "hdfs://node10:8020/eventLog";

        String[] arg = new String[]{
                "hdfs://node10:8020/user/root/magic",
                ""
        };

        // 1.獲取SC
        SparkContext sc = Utils.getSc(master, appName, jars, logEnabled, logDir);

        // 2. 提交任務 線程
        FutureTask<Boolean> future = new FutureTask<>(new WordCount(sc, arg));
        new Thread(future).start();

        // 3. 監控
        String appId = sc.applicationId();
        System.out.println("AppId:"+appId);
        SparkStatusTracker sparkStatusTracker = null;
        int[] jobIds ;
        SparkJobInfo jobInfo;
        while (!sc.isStopped()) {// 如果sc沒有stop,則往下監控
            Thread.sleep(2000);
            // 獲取所有Job
            sparkStatusTracker = sc.statusTracker();
            jobIds = sparkStatusTracker.getJobIdsForGroup(null);
            for(int jobId :jobIds){
                jobInfo = sparkStatusTracker.getJobInfo(jobId).getOrElse(null);
                if(jobInfo == null){
                    System.out.println("JobId:"+jobId+",相關信息獲取不到!");
                }else{
                    System.out.println("JobId:" + jobId + ",任務狀態:" + jobInfo.status().name());
                }
            }
        }

        // 4. 檢查線程任務是否返回true
        boolean flag = true;
        while(flag){
            try{
                Thread.sleep(200);
                System.out.println("Job closing ...");
                if(future.isDone()){
                    flag = false;
                    if(future.get().booleanValue()){
                        System.out.println("Job "+appId+" done with success state");
                    }else{
                        System.out.println("Job "+appId+" failed!");
                    }
                }
            }catch (InterruptedException|ExecutionException e){
                e.printStackTrace();
            }

        }

    }
}

Utils工具類主要是獲取SparkContext,每次獲取都是一個新的SparkContext,如下:
package demo04

import org.apache.spark.{SparkContext,SparkConf}

/**
 * Created by fansy on 2017/7/6.
 */
object Utils {

  /**
   * 獲得sc
   * @param master
   * @param appName
   * @param jars
   * @return
   */
  def getSc(master:String,appName:String,jars:Array[String],logEnabled:String,logDir:String):SparkContext = {
    val conf = new SparkConf().setMaster(master).setAppName(appName).setJars(jars)
      .set("spark.eventLog.enabled",logEnabled)
      .set("spark.eventLog.dir",logDir)
    new SparkContext(conf)
  }

}

再次運行,即可監控到App裏面每個Job的具體信息了。

思考

1. 任務3的嘗試失敗了,但是是否有方法設置Driver運行的地方呢?如果所有Driver都在Client端運行,那麼Client需要較高配置才行;
2. 這裏使用的是Java程序直接連接的方式,如果是Java Web呢?是否需要做些環境配置?
3. 使用Java 直連Spark Standalone的方式確實可以提交效率,不過如果需要同時運行MR的程序,那麼使用YARN的方式會方便一點,至少不需要部署Spark集羣了。



分享,成長,快樂

腳踏實地,專注

轉載請註明blog地址:http://blog.csdn.net/fansy1990





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章