一直很好奇web后台如何启动Spark应用程序,查找Api后发现可以使用org.apache.spark.launcher.SparkLauncher来做到这一点。我想得动手测试一下,而且要做的体面一些,所以搞个简易的web工程吧,顺便学习熟悉一下使用springboot框架。在这里将整个折腾的过程记录下来,新手上路,有任何搞错的地方,或者走了弯路,还请大家不吝指出,帮我进步。
1. 搭建hadoop集群,我这边用的是两台主机的分布式集群
2. 安装Spark,测试能运行spark-submit即可,然后配置好HistoryServer
3. 安装Mysql,创建一个Spark应用信息表,只有mainClass和jarPath两个字段
4. 熟悉Springboot框架的基本使用
我设想主要有三个html页面:
1. 查询已经开发好的spark应用(应用信息提前入到数据库里)
2. 设置执行参数后提交(参数包括mainclass、jar包路径、driver内存、executor内存等)
3. 显示应用执行结果
1. 查询Spark应用,点击应用进入submit页面
2. 执行参数设置
3. 提交应用程序,正在执行中
4. 执行结束后跳转,查看执行结果。点击Tracking URL会跳转到Yarn的Application管理
页面,还能查看Spark应用的job信息。
搭建一个springboot项目,配置依赖DevTools + web + thymeleaf + mysql + mybatis
DevTools模块使Spring Boot应用支持热部署,提高开发者的开发效率,修改后无需手动重启Spring Boot应用。可以先不配,需要用的时候再说。
Spark应用信息表,只有三个字段:mainClass是应用程序的main方法,jarPath是jar包存放路径,note是应用说明
实体类
这里只用到两个实体类:Spark应用信息AppInfo和Spark应用执行参数SparkAppPara
public class AppInfo {
String mainClass;//应用程序的mainClass
String jarPath;//应用程序jar包的存放位置,可以是本地或HDFS
String note;//应用说明
//省略getter和setter
}
public class SparkAppPara {
String mainClass;
String jarPath;
String master;//可以是Yarn或StandAlone
String deployMode;//可以是Cluster或Client
String driverMemory ;//driver内存
String executorMemory;//executor内存
String executorInstances;//executor个数
String executorCores;//executor核数
String defaultParallelism;//参数spark.default.parallelism的值
//省略getter和setter
}
Controller
(1)访问应用信息页面
@RequestMapping("/appInfo")
public String appInfo(){
return "appInfo";
}
(2)查询Spark应用信息
@RequestMapping("/getAllAppInfo")
@ResponseBody
public String getAllAppInfo(){
return sparkAppInfoService.getAllAppInfo();
}
(3)点击某个应用,跳转到提交页面
@RequestMapping("/submitApp")
public ModelAndView submitApp(String mainClass,String jarPath){
ModelAndView mav = new ModelAndView();
mav.setViewName("submitApp");
mav.addObject("mainClass",mainClass);
mav.addObject("jarPath",jarPath);
return mav;
}
这里我希望跳转之后,自动填写mainClass和jarPath,我的做法是把这俩参数通过后台转给新页面。由于页面不是jsp,所以不能用el表达式获取model值。需要靠Thymeleaf的语法th:xxx=${…}来获取渲染数据。
<div class="icon">
<label class="cd-label" for="mainClass">mainClasslabel>
<input class="mainClass" type="text" name="mainClass" id="mainClass" th:value=${mainClass}>
div>
<div class="icon">
<label class="cd-label" for="jarPath">jarPathlabel>
<input class="jarPath" type="text" name="jarPath" id="jarPath" th:value=${jarPath}>
div>
(4)提交任务
@RequestMapping(value = "/submit")
@ResponseBody
public String Submit(@RequestBody SparkAppPara sparkAppPara) throws IOException, InterruptedException {
return submitService.submitApp(sparkAppPara);
}
(5)执行完后跳转到结果页面
在这里我希望拿到执行结果json之后,跳转到结果页面展示。我的做法是在Ajax请求成功后带参数跳转页面,我觉得肯定有更好的办法,在此抛砖引玉。
success: function(data)
{
window.location.href=host+'/result?resultJson='+ encodeURIComponent(data);
}
因为url请求里不能有大小括号等特殊字符,所以请求之前需要使用encodeURIComponent方法进行编码。
@RequestMapping("/result")
public ModelAndView toResult(String resultJson){
ModelAndView mav = new ModelAndView();
mav.setViewName("result");
mav.addObject("resultJson",resultJson);
return mav;
}
关于在结果页面的JS代码里获取resultJson:
第(3)步中,Thymeleaf直接把model值渲染到html标签中。而在结果页面中,我需要先拿到resultJson,进行一些处理后再渲染。在JS代码里,我们可以像下面这样来获取resultJson。
<script th:inline="javascript">
var resultJson = JSON.parse([[${resultJson}]]);
$("#trackingUrl").attr("href",yarnAppUrl+resultJson.id);
$("#applicationId").html(resultJson.id);
$("#applicationName").html(resultJson.name);
//次要代码省略
script>
这里需要注意的是,这部分JS代码只能内嵌在html页面中,外联JS中不会生效。
Service和Mapper
(1)获取Spark应用信息的Service和Mapper
@Service
public class SparkAppInfoService {
@Autowired
private AppInfoMapper appInfo;
public String getAllAppInfo(){
List<AppInfo> list = appInfo.getAllAppInfo();
return JSONObject.toJSONString(list);
}
}
@Component
public interface AppInfoMapper {
@Select("SELECT * FROM appinfo")
@Results({
@Result(property = "mainClass", column = "mainclass"),
@Result(property = "jarPath", column = "jarpath"),
@Result(property = "note", column = "note")
})
List<AppInfo> getAllAppInfo();
}
(2)提交Spark应用的Service
提交spark应用的API不止一种,我用的是org.apache.spark.launcher.SparkLauncher
<dependency>
<groupId>org.apache.sparkgroupId>
<artifactId>spark-launcher_2.12artifactId>
<version>2.4.0version>
dependency>
@Service
public class SparkSubmitService {
public String submitApp(SparkAppPara sparkAppPara) throws IOException, InterruptedException {
HashMap env = new HashMap();
//这两个属性必须设置
env.put("HADOOP_CONF_DIR", "/usr/local/hadoop/etc/hadoop");
env.put("JAVA_HOME", "/usr/lib/jdk/jdk1.8.0_191/");
CountDownLatch countDownLatch = new CountDownLatch(1);
SparkAppHandle handle = new SparkLauncher(env)
.setSparkHome("/usr/local/spark/")
.setAppResource(sparkAppPara.getJarPath())
.setMainClass(sparkAppPara.getMainClass())
.setMaster(sparkAppPara.getMaster())
.setDeployMode(sparkAppPara.getDeployMode())
.setConf("spark.driver.memory", sparkAppPara.getDriverMemory()+"g")
.setConf("spark.executor.memory", sparkAppPara.getExecutorMemory()+"g")
.setConf("spark.executor.instances", sparkAppPara.getExecutorInstances())
.setConf("spark.executor.cores", sparkAppPara.getExecutorCores())
.setConf("spark.default.parallelism", sparkAppPara.getDefaultParallelism())
.setVerbose(true).startApplication(new SparkAppHandle.Listener() {
@Override
public void stateChanged(SparkAppHandle sparkAppHandle) {
if (sparkAppHandle.getState().isFinal()) {
countDownLatch.countDown();
}
System.out.println("state:" + sparkAppHandle.getState().toString());
}
@Override
public void infoChanged(SparkAppHandle sparkAppHandle) {
System.out.println("Info:" + sparkAppHandle.getState().toString());
}
});
System.out.println("The task is executing, please wait ....");
//线程等待任务结束
countDownLatch.await();
System.out.println("The task is finished!");
//通过Spark原生的监测api获取执行结果信息
String restUrl = "http://master:18080/api/v1/applications/"+handle.getAppId();
String resultJson = RestUtil.httpGet(restUrl,null);
return resultJson;
}
}
Http请求工具
我们使用这个工具,发送rest请求,就可以获取Spark应用执行结果的json信息(我觉得有一个前提是需要配置好History Server服务并启动)。
public class RestUtil {
public static String httpGet(String urlStr, List<String> urlParam) throws IOException, InterruptedException {
// 实例一个URL资源
URL url = new URL(urlStr);
HttpURLConnection connet = null;
int i = 0;
while(connet==null || connet.getResponseCode() != 200 ){
connet = (HttpURLConnection) url.openConnection();
connet.setRequestMethod("GET");
connet.setRequestProperty("Charset", "UTF-8");
connet.setRequestProperty("Content-Type", "application/json");
connet.setConnectTimeout(15000);// 连接超时 单位毫秒
connet.setReadTimeout(15000);// 读取超时 单位毫秒
i++;
if (i==50)break;
Thread.sleep(500);
}
//将返回的值存入到String中
BufferedReader brd = new BufferedReader(new InputStreamReader(connet.getInputStream(),"UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while((line = brd.readLine()) != null){
sb.append(line);
}
brd.close();
connet.disconnect();
return sb.toString();
}
}
项目里引用的第三方模板和插件如下,如有侵权请联系我删除。
https://blog.csdn.net/sparkexpert/article/details/51045762
https://blog.csdn.net/u011244682/article/details/79170134
http://spark.apache.org/docs/latest/monitoring.html
https://github.com/zxxxc/sparksubmit