注:本人只安装过3.9和2.5,文章中也只会说明这两个版本
目录
一、准备工作
github上下载对应版本的源码,如果不是最新版则需要在README.md文件点击超链接跳转的历史版本下载页面
2.5编译用的是ant,3.9编译用的是gredle(基于Apache Ant和Apache Maven概念的项目自动化构建开源工具)
如果是版本是2.5则还需要再网上下载编译过后的安装包,因为我尝试了很多次都无法再本地编译,解决一个还会出现别的问题,windows和Linux都尝试了下,所以不想再浪费时间
https://codeload.github.com/azkaban/azkaban/zip/2.5.0
部署可参考:Azkaban2.5.0部署安装(含安装包) (icode9.com)
3.9的版本官方文档说的很详细,可以用浏览器的翻译或者有道词典的划词翻译和截图翻译来阅读
Getting Started — Azkaban documentation
二、邮件配置说明
邮件的配置只需要修改前端项目azkaban-web的azkaban.properties就可以
mail.sender=mhy_404@163.com
mail.host=smtp.163.com
mail.user=mhy_404@163.com
mail.password=PIWVMKXTPW......
这4个配置的都是发件人,azkaban会用该邮箱发送邮件,qq邮箱也是可以的,需要将mail.host设置为smtp.qq.com。密码并不是邮箱的密码,而是邮箱开启smtp的时候给的授权码,该授权码只会再开启的时候出现一次,如果忘记那就把smtp功能重新关闭后再开启
job.failure.email=
job.success.email=
成功或收件人的邮箱,但该配置azkaban的代码中没有用到,实际配置也没有什么用。我们可以修改源码,来把这两个配置项用起来。
三、修改邮件内容
邮件的内容是在 DefaultMailCreator中调整
邮箱最终效果
azkaban2.5的ExecutableFlow的对象中并没有项目名称的属性,我们可以修改源码进行添加
效果:
失败代码:
@Override
public boolean createErrorEmail(ExecutableFlow flow, EmailMessage message, String azkabanName, String clientHostname, String clientPortNumber, String... vars) {
ExecutionOptions option = flow.getExecutionOptions();
//获取失败后收件人日志
List<String> emailList = option.getFailureEmails();
int execId = flow.getExecutionId();
//获取job内容列表
Map<String, Object> executableNodeInfo = getExecutableNodeInfo(flow);
HashMap<String, Object> newNode = new HashMap<>(executableNodeInfo);
List<Map<String, Object>> nodes = (ArrayList<Map<String, Object>>) newNode.get("nodes");
int succeededNum = 0;
List<Map<String, String>> failedList = new ArrayList<>();
//获取所有job节点
for (Map<String, Object> map : nodes) {
String status = map.get("status").toString();
if ("SUCCEEDED".equals(status)) {
succeededNum++;
} else if ("FAILED".equals(status) || "FAILED_FINISHING".equals(status)) {
Map<String, String> failedMap = new HashMap<>();
String id = map.get("id").toString();
//获取后续影响节点
String outStr = getOut(map);
failedMap.put("id", id);
failedMap.put("out", outStr);
Set<String> impactAll = new HashSet<>();
//节点失败后后续影响节点分析
impactAnalysis(nodes, outStr, impactAll);
String impactAllStr = impactAll.toString();
failedMap.put("impactAll", impactAllStr.substring(1, impactAllStr.length() - 1));
failedList.add(failedMap);
}
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String newDate = simpleDateFormat.format(new Date(flow.getStartTime()));
String endTime = simpleDateFormat.format(new Date(flow.getEndTime()));
if (emailList != null && !emailList.isEmpty()) {
message.addAllToAddress(emailList);
message.setMimeType("text/html;charset=UTF-8");
message.setSubject("项目名称:" + flow.getProjectName() + ",execution '" + execId + "' 今日调度作业运行:失败");
message.println("<h3>作业开始时间:" + newDate + "</h3>");
message.println("<h3>作业结束时间:" + endTime + "</h3>");
message.println("<table width=\"400\" style=\"border: 1pt solid rgb(232, 242, 249); border-image: none;\" border=\"1\" cellpadding=\"0\">");
message.println("<tr style=\"background: rgb(66, 139, 202); padding: 0.75pt;\">" +
"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">作业状态</td>" +
"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">记录数</td>" +
"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">作业数据日期</td>" +
"</tr>");
message.println("<tr>" +
"<td>调度作业总数</td>" +
"<td>" + nodes.size() + "</td>" +
"<td>" + LocalDate.now().minusDays(1) + "</td>" +
"</tr>");
message.println("<tr>" +
"<td>当前成功执行</td>" +
"<td>" + succeededNum + "</td>" +
"<td>" + LocalDate.now().minusDays(1) + "</td>" +
"</tr>");
message.println("<tr style=\"background: crimson; padding: 0.75pt;\">" +
"<td style=\"color: white;\">当前运行失败</td>" +
"<td style=\"color: white;\">" + failedList.size() + "</td>" +
"<td style=\"color: white;\">" + LocalDate.now().minusDays(1) + "</td>" +
"</tr>");
message.println("</table>");
message.println("<h3>当前失败作业明细</h3>");
message.println("<table width=\"550\" style=\"border: 1pt solid rgb(232, 242, 249); border-image: none;\" border=\"1\" cellpadding=\"0\">");
message.println("<tr style=\"background: rgb(66, 139, 202); padding: 0.75pt;\">" +
"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">失败作业名称</td>" +
"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">后续影响第二级作业</td>" +
"<td align=\"center\" style=\"color: white; font-size: 13.5pt;\">后续影响所有的作业</td>" +
"</tr>");
for (Map<String, String> failedMap : failedList) {
message.println("<tr>" +
"<td>" + failedMap.get("id") + "</td>" +
"<td>" + failedMap.get("out") + "</td>" +
"<td>" + failedMap.get("impactAll") + "</td>" +
"</tr>");
}
message.println("</table>");
return true;
}
return false;
}
/**
* 获取后续影响节点
*/
private static String getOut(Map<String, Object> map) {
Object out = map.get("out");
String outStr = null;
if (out != null) {
outStr = out.toString();
outStr = outStr.substring(1, outStr.length() - 1);
}
return outStr;
}
/**
* 节点失败后,所有后续影响节点分析
*/
private void impactAnalysis(List<Map<String, Object>> nodes, String outStr, Set<String> impactAll) {
if (outStr != null) {
String[] sonNodeIds = outStr.split(",");
Set<String> collect = nodes.stream().filter(node -> {
String sonId = node.get("id").toString();
for (String sonNodeId : sonNodeIds) {
if (sonId.equals(sonNodeId.trim()) && node.get("out") != null) {
return true;
}
}
return false;
}).map(DefaultMailCreator::getOut).collect(Collectors.toSet());
for (String set : collect) {
impactAnalysis(nodes, set, impactAll);
}
impactAll.addAll(collect);
}
}
/**
* 获取job内容列表
*/
private Map<String, Object> getExecutableNodeInfo(ExecutableNode node) {
HashMap<String, Object> nodeObj = new HashMap<String, Object>();
nodeObj.put("id", node.getId());
nodeObj.put("status", node.getStatus());
nodeObj.put("startTime", node.getStartTime());
nodeObj.put("endTime", node.getEndTime());
//前置节点
if (node.getInNodes() != null && !node.getInNodes().isEmpty()) {
nodeObj.put("in", node.getInNodes());
}
//后置节点
if (node.getInNodes() != null && !node.getOutNodes().isEmpty()) {
nodeObj.put("out", node.getOutNodes());
}
if (node instanceof ExecutableFlowBase) {
ExecutableFlowBase base = (ExecutableFlowBase) node;
ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
for (ExecutableNode subNode : base.getExecutableNodes()) {
Map<String, Object> subNodeObj = getExecutableNodeInfo(subNode);
if (!subNodeObj.isEmpty()) {
nodeList.add(subNodeObj);
}
}
nodeObj.put("nodes", nodeList);
}
return nodeObj;
}
通过页面的url查看源码的示例:
上述代码中:getExecutableNodeInfo方法是根据ExecutorServlet.getExecutableNodeInfo改编
该方法对应历史中Job List的内容
该方法的请求地址为:https://192.168.189.130:8443/executor?execid=60&ajax=fetchexecflow
通过全文搜索可知该方法的地址在ExecutorServlet中
四、替换
3.9可正常编译,也可在idea中编译
编译后会生成build目录
instarll目录可以启动和关闭项目
如果不想执行命令编译,也可以通过下面方法替换lib中jar的内容
2.5我们只能通过替换jar包内容的方式进行修改
1、通过idea对代码进行编译,编译后的代码将会存放在azkaban-2.5.0\dist\classes\azkaban目录中
2、首先把网上下载的编译包中azkaban-2.5.0.jar复制出来。对了,github下载的2.5源码包中并没有引用相关jar包,可以直接用编译包中的jar,但需要注意把azkaban-2.5.0.jar删掉
3、拷贝azkaban-2.5.0\dist\classes\azkaban中所有修改过代码的classes文件拷贝到azkaban-2.5.0.jar中对应的位置,jar可以用压缩文件打开,直接拖着进来进行替换。
也可以完全直接替换所有的目录
4、停止服务器上的azkaban服务,azkaban-executor和azkaban-web都要停止。
5、将替换过后的jar上传到两个azkaban目录的lib中进行替换
五、2.5定时任务发送邮件
手动的邮件发送可以在页面上直接配置,而2.5的定时任务并没有配置的地方,如果想要定时任务也能发送邮件,那么就需要在上传的时候再最后一个job中添加一下配置
failure.emails
--> 任务失败时的邮件提醒设置,以逗号分隔多个邮箱success.emails
--> 任务成功时的邮件提醒设置,以逗号分隔多个邮箱notify.emails
--> 任务无论失败还是成功都邮件提醒设置,以逗号分隔多个邮箱
需要注意三点:
1、必须是最后一个job,否则可能配置的当前job执行完成后就会发送邮件。(没试过,猜测)
2、必须是通过页面上传的job中包含上面配置才可以,上传后通过页面修改无效。(亲测)
3、修改azkaban-executor-2.5.0/projects/中的job也不会生效。
2和3就算修改后重新也不会生效
可以看到填写邮箱的地方并没有邮箱,说明修改无效,并不会发送邮件。
当然修改命令什么的还是会生效的。
如果配置生效,那么打开执行页面后填写邮箱的地方就直接有邮箱,如:
查看本次执行的job文件内容
先看数据库的id为4,版本为6
进入项目目录,所有执行过后的目录都会有记录,没有执行过则不会有
可以看到notify.emails的配置并没有在文件中
第二种方法:
除了上面上传的时候最后一个job配置外,还可以同通过修改源码的方式
通过方法的调用链,我们可以发现是否发送邮件是ExecutorManager.finalizeFlows方法控制的
如果任务状态是失败或成功,且没有设置对应的邮箱那么将不会调用邮件发送的方法
解决方法: 我们可以在配置文件中添加收件人的地址,且在代码判断前把收件人加入代码中
上文提到这两个配置并没有被用到,我们可以利用一下,多个邮箱用逗号隔开
job.failure.email=mhy_404@163.com,xxx@163.com job.success.email=mhy_404@163.com,xxx@163.com
//添加邮箱
List<String> successEmails = options.getSuccessEmails();
if (successEmails.isEmpty() && !"".equals(successEmail)) {
String[] split = successEmail.split(",");
successEmails.addAll(Arrays.asList(split));
}
List<String> failureEmails = options.getFailureEmails();
if (failureEmails.isEmpty() && !"".equals(failureEmail)) {
String[] split = failureEmail.split(",");
failureEmails.addAll(Arrays.asList(split));
}
源码修改后可以安装步骤4的编译操作
注:此方法修改后将会影响到所有项目
六、定时发送邮件
很多时候我们往往需要某一时刻发送邮件,来查看当前正在运行的任务或者当前定时任务的运行汇总情况。这时候我们可以使用java自带的定时任务来实现,或者使用 while+sleep 方案,简单的说,就是定义一个线程,然后 while 循环,通过 sleep 延迟时间来达到周期性调度任务。
package azkaban.executor.mail;
import azkaban.alert.Alerter;
import azkaban.executor.ExecutableFlow;
import azkaban.project.ProjectManager;
import azkaban.utils.Props;
import azkaban.webapp.AzkabanWebServer;
import org.apache.log4j.Logger;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
/**
* 今日azkaban定时任务信息汇总
*/
public class InfoScheduleThread implements Runnable {
private static final Logger logger = Logger.getLogger(InfoScheduleThread.class);
private final Props azkabanSettings;
private final AzkabanWebServer application;
public InfoScheduleThread(Props azkabanSettings, AzkabanWebServer application) {
this.azkabanSettings = azkabanSettings;
this.application = application;
}
@Override
public void run() {
//定时时间 14:00
String scheduleTime = azkabanSettings.getString("schedule.mail.info.time");
//收件人
String recipients = azkabanSettings.getString("job.failure.email");
if (scheduleTime == null || recipients == null) {
return;
}
List<String> emailList = Arrays.asList(recipients.split(","));
logger.info("汇总任务每日定时邮件发送时间为:" + scheduleTime);
String[] split = scheduleTime.split(":");
String scheduleHour = split[0];
String scheduleMinute = split[1];
Alerter mailAlerter = application.getMailAlerter();
while (true) {
try {
LocalDateTime now = LocalDateTime.now();
//如果不设置withNano,程序会在1秒内执行多次
LocalDateTime executeTime = now.withHour(Integer.parseInt(scheduleHour))
.withMinute(Integer.parseInt(scheduleMinute)).withSecond(0).withNano(0);
//如果当前时间超过执行时间,则需要睡眠到明天的执行时间
if (now.compareTo(executeTime) > 0) {
executeTime = executeTime.plusDays(1);
}
logger.info("汇总任务下次定时邮件发送时间:" + executeTime);
//线程休眠时间
long millis = now.until(executeTime, ChronoUnit.MILLIS);
Thread.sleep(millis);
logger.info("汇总任务开始执行");
//设置今天0点
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
long todayZero = calendar.getTimeInMillis();
//获取今天执行的任务
List<ExecutableFlow> history = application.getExecutorManager().getExecutableFlows(
null, null, null, 0, todayZero, -1, 0, 16);
//设置任务的项目名
ProjectManager projectManager = application.getProjectManager();
for (ExecutableFlow executableFlow : history) {
String projectName = projectManager.getProject(executableFlow.getProjectId()).getName();
executableFlow.setProjectNam(projectName);
}
mailAlerter.alertOnInfo(history, emailList);
} catch (Exception e) {
e.printStackTrace();
logger.error(e);
}
}
}
}