首页 / 资讯中心 / 文章详情

Azkaban单机任务调度实战:DAG依赖、Quartz定时与避坑

Azkaban单机任务调度实战:DAG依赖、Quartz定时与避坑 ★ FEATURED ARTICLE
简介Azkaban Solo Server 0.1.0-SNAPSHOT 单机版压缩包面向个人开发者、小型数据团队及刚接触任务调度的学习者用于快速搭建轻量级工作流调度环境免去源码编译环节。资源包约42.28MB上游未提供文件清单与类型明细解压后即可获得可运行的单服务器部署内容Web服务、工作流执行器与内嵌数据库均集中于同一节点适合本地实验、教学演示和小规模作业编排。已有260人学习关注。借助它可直观理解Azkaban的项目管理、作业依赖定义、定时调度与执行日志查看等核心机制并以此为基础验证Hadoop、Shell或Java任务的串联流程SNAPSHOT版本更偏向开发测试生产环境需谨慎评估稳定性与性能上限。1. 一个 tar.gz 顶掉三台机器的 crontabazkaban 任务调度单机版能用到什么程度三台机器散着二十多条 crontab谁依赖谁只有写的人清楚。某天上游表延迟半小时下游照样准点跑跑出来的报表全是 0事后翻日志翻了两个小时。这种场景下azkaban 任务调度的 solo server 版是最省事的一步一个 tar.gz 解压、一条启动脚本、一个浏览器页面作业依赖就从「人脑里的记忆」变成一张能点开看的 DAG。solo server 把 web 服务和 executor 塞进同一个 JVM元数据落本地 H2 库不用先装 MySQL、不用起 executor 集群适合个人、三五人的数据小组也适合在正式集群上线前把作业逻辑先调通。它的边界同样清楚单进程、单机、并发靠一个 JVM 顶着flow 数量上到几百条、或者需要多机隔离时就得考虑多 executor 部署。下面按「拆包 → 写作业 → 定时 → 踩坑 → 进阶」的顺序走一遍。2. 解包后先别启动目录、JDK 与 conf 里必须先确认的四项2.1 版本号里的 SNAPSHOT 是个信号包名是azkaban-solo-server-0.1.0-SNAPSHOT.tar.gzSNAPSHOT这个词值得先停下来看一眼它说明这份包是从源码构建出来的开发版本而不是一个稳定的发布版本号。这不影响它能跑但影响你怎么查问题——网上大部分教程对着的是发布版本的目录结构和脚本名你照着敲bin/azkaban-solo-start.sh很可能提示文件不存在然后开始怀疑包坏了。我一般的做法是先花二十秒把目录摸清再决定敲什么命令# 解包进到包根目录。后面所有命令都在这个目录下执行 tar -zxvf azkaban-solo-server-0.1.0-SNAPSHOT.tar.gz cd azkaban-solo-server-0.1.0-SNAPSHOT # 别背教程先看自己这份包里到底有什么 ls -1 ls -1 bin ls -1 confls -1逐行列出比ls的横向排列更容易核对。bin下的启动脚本名以实际结果为准常见是start-solo.sh和shutdown-solo.sh这一对也可能带azkaban-solo-前缀。确认完再动能省掉一次「脚本不存在」的自我怀疑。2.2 目录结构哪几个目录会带来问题solo server 的包体不大但每个目录都对应一个运行期依赖挪错了位置表现出的症状完全不一样。目录/文件作用挪动或缺失后的表现bin/启停脚本里面会拼 classpath换个目录执行相对路径全断conf/azkaban.properties主配置端口、时区、数据库都在这缺了直接起不来conf/azkaban-users.xml登录用户与角色起来了但登不进conf/global.properties全局变量所有 job 可见作业里引用的变量变成空lib/主程序 jar 与三方依赖启动即NoClassDefFoundErrorplugins/job 类型插件按 type 名加载typexxx报找不到 job 类型web/前端静态资源页面白屏、JS 404h2db/运行后生成的 H2 数据文件删除等于清空所有项目和执行历史projects/上传的项目 zip 落盘位置项目列表在、文件没了h2db和projects是首次启动后才出现的所以第一次ls看不到它们并不代表包不完整。2.3 JDK启动前对齐别等报错再猜solo server 启动失败最常见的原因不是配置写错而是 JVM 版本不对。构建产物在 JDK 8 上最稳机器上如果同时装了 8 和 11、17java -version出来的那个未必是脚本最终用的那个——启动脚本里可能写死了JAVA_HOME。# 1) 看当前 shell 用的是哪个 java java -version echo JAVA_HOME${JAVA_HOME} # 2) 看启动脚本里有没有硬编码 JDK 路径 grep -nE JAVA_HOME|JAVA_OPTS|-Xmx bin/*.sh # 3) 需要切换时在当前 shell 里临时改别去改系统配置 export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export PATH${JAVA_HOME}/bin:${PATH} java -version第 2 步是关键grep能把脚本里的JAVA_HOME赋值和-Xmx内存参数一起打出来。如果脚本里写死了别的路径export是不生效的只能改脚本或者直接把那行删掉让它回落到环境变量。-Xmx那行也顺手看一眼solo server 默认给的堆往往偏小作业一多就容易 OOM后面迁移到正式环境时这个值要重新算。2.4 四个必须先确认的配置端口、时区、用户、路径solo server 的配置散在一个azkaban.properties里键名在不同构建之间可能略有出入所以先grep再改比照着记忆改安全。# 把要动的那几类键先捞出来连同行号改的时候好定位 grep -nE jetty.port|executor.port|timezone|azkaban.webserver.url conf/azkaban.properties grep -nE database.type|h2|project.dir|execution.dir conf/azkaban.properties grep -nE user.manager|global.properties conf/azkaban.propertiesgrep -n带行号改完再用同样的命令回查一遍能确认自己没改错行。几个键的取舍如下jetty.port是页面访问端口常见默认 8081。被占时改这里同时把azkaban.webserver.url一起改成新端口否则页面上一些跳转会打到旧地址。executor.port是 executor 内部通信端口单机单实例时一般不用动但同一台机器要跑两个 solo 实例做对比测试时这个和jetty.port必须成对错开。default.timezone.id是影响最大、最容易被忽略的一项。默认值常是美洲时区cron 会按那个时区解析你在页面上填「每天 2 点」实际触发时间是北京时间下午或者半夜偏移几个小时的账全算在「调度不准」头上。改成Asia/Shanghai再重启。h2.path或database.typeh2配套的路径项决定数据文件落哪。默认是包内相对路径等于把数据和生产程序绑死在一个目录里。真要用一段时间建议改成绝对路径或者至少记住这个目录备份时整个拷走。用户配置在conf/azkaban-users.xml默认账号密码就写在里面通常是azkaban/azkaban。第一次登进去之后把自己的账号加上、把默认密码换掉这件事别拖。改完就可以起了# 启动脚本名以 ls bin 的结果为准 bash bin/start-solo.sh # 确认进程和端口都在 ps -ef | grep -i azkaban | grep -v grep ss -lntp | grep -E 8081|12321ps和ss两条命令一起用是因为「进程在但端口没起」和「端口被别的进程占了」表现都是页面打不开只有分开看才能区分。启动日志一般在控制台直接打出来第一次启动建议前台跑别急着nohup报错能第一时间看到。3. 写第一个能跑的 flow.job 语法、依赖与参数传递的六个细节3.1 type 怎么选command 是主力javaprocess 是补充Azkaban 的作业单元是一个.job文件本质是一份 properties靠type决定由哪个插件执行。实际项目里九成以上的作业是typecommand执行一条命令或者调一个脚本逻辑写在脚本里.job只负责声明依赖和参数。这样做的原因是脚本可以单独bash跑、可以进 Git、可以用 shellcheck 检查出问题能脱离调度器定位。typejava是把作业跑在 executor 的 JVM 里共享堆内存。看起来省事实际是个隐患作业里的内存泄漏、System.exit、线程池没关都会影响 executor 进程本身表现是「别的作业莫名其妙失败」。真要跑 Java 逻辑用typejavaprocess它会 fork 一个独立 JVM配Xmx单独限内存隔离性接近 command。3.2 最小三件套三个 .job 组成一条链先看一条能跑通的最小链路抽取、转换、加载后一个依赖前一个。# extract.job typecommand commandbash bin/extract.sh ${dt} retries2 retry.backoff60000# transform.job typecommand commandbash bin/transform.sh ${dt} dependenciesextract# load.job typecommand commandbash bin/load.sh ${dt} dependenciestransformtype和command是必填项command后面跟的就是最终要执行的字符串。dependencies填的是上游 job 名也就是文件名去掉.job后缀多个上游用逗号隔开写成dependenciesextract,dim_sync就表示两个都要成功才轮到它。retries2表示失败后重试两次retry.backoff60000是两次重试之间等 60 秒。这两个参数只对「抖动型失败」有意义——网络超时、下游库瞬时连接数满。如果是脚本里 SQL 写错了重试三次也只是把同一个错误打印三遍还会让你在日志里多翻几屏。我一般只在纯 IO 型作业上开重试且retries不超过 2。要跑 Java 的话把 job 换成这样# agg.job typejavaprocess java.classcom.example.AggMain classpathlib/agg.jar Xmx512M main.args--dt ${dt} dependenciestransformjava.class是入口类全名classpath是 jar 路径Xmx单独限这个子进程的堆main.args拼到 main 方法的入参后面。这里有个隐性问题这些键名是插件按名字读的写错一个字母不会报错只会静默变成空值然后你的 main 方法收到一个null。第一次跑的时候在 main 里把args全打一遍比事后猜快得多。3.3 打包与上传zip 里到底该有什么Azkaban 的项目是通过上传 zip 创建的zip 里的.job文件构成 flow。目录怎么组织直接影响「上传成功但找不到 job」。# 打包前的工作目录job 文件放在最外层 work/ ├── extract.job ├── transform.job ├── load.job └── bin/ ├── extract.sh ├── transform.sh └── load.sh# 进到 work 里打别把 work 这一层目录打进 zip cd work zip -r ../myflow.zip . -x *.DS_Store -x __MACOSX/* cd .. # 打完确认一眼层级这一步别省 unzip -l myflow.zipzip -r ../myflow.zip .里的那个.是关键它表示「当前目录的内容」而不是「当前目录本身」。写成zip -r ../myflow.zip workzip 里就会多一层work/上传后 job 在子目录里flow 列表可能是空的。unzip -l打印出来的清单能直接看出有没有多这一层加上-x排除掉 macOS 的元数据目录避免__MACOSX被当成 job 目录扫进去。上传后如果 flow 列表是空的先怀疑两件事zip 多套了一层目录或者.job文件后缀名不对.job.txt这种。3.4 参数怎么传flow param、内置时间变量、下游取值作业里写${dt}这个值从哪来主要三条路。第一条是执行时在页面上填的 flow param。点 Execute Flow 时有个参数输入区填dt20240101这次执行里所有 job 的${dt}都会被替换成这个值。这条路径适合手工补数、回放某一天。第二条是 Azkaban 内置的时间变量不用自己算日期# 用内置变量拼分区避免脚本里再写 date 命令 typecommand commandbash bin/extract.sh ${azkaban.flow.start.year}${azkaban.flow.start.month}${azkaban.flow.start.day}azkaban.flow.start.*这一组取的是本次 flow 的启动时间年月日时分秒都有还有一些带前导零的变体。它比自己写date %Y%m%d稳因为定时任务在跨零点执行时job 之间的date可能落在不同的日子上而 flow 启动时间是固定的。变量名记不全的话第一次先写个commandenv | sort的调试 job把注入的环境变量全打出来。第三条是上游作业往下游传值。command 类型的作业在运行时会拿到一个环境变量指向一个属性文件往里面写键值对下游就能用${上游job名:键名}取到#!/usr/bin/env bash # bin/extract.sh把本次产出的表名写给下游 set -euo pipefail dt${1:-} # 没传参数就退出别让 ${dt} 的字面量流进 SQL if [ -z ${dt} ] || [ ${dt} ${dt} ]; then echo dt is empty, abort 2 exit 1 fi echo extracting dt${dt} # ... 实际抽取逻辑 ... # 变量名在不同构建里可能是 JOB_OUTPUT_PROPERTIES 或 JOB_OUTPUT_PROP_FILE out${JOB_OUTPUT_PROPERTIES:-${JOB_OUTPUT_PROP_FILE:-}} if [ -n ${out} ]; then echo tableods_orders_${dt} ${out} fi这段脚本有三处值得说。set -euo pipefail让任何一步失败都立刻退出避免「中间报错但脚本以 0 退出」这种最难查的情况。对${dt}的判断是必须的flow param 没传的时候Azkaban 会把${dt}原样保留成字面量传给你的脚本$1就等于字符串${dt}如果不拦SQL 里就会出现where dt${dt}然后在数据库层报一个跟你毫无关系的语法错误。最后的属性文件写法做了兜底因为不同构建里这个环境变量的名字不完全一样两个都试一遍比较省事。下游取值直接写在 command 里# load.job 里引用上游产出的表名 typecommand commandbash bin/load.sh ${extract:table}${job名:键名}这个语法只对上游有效写错了或者上游没写这个键替换结果是空字符串不报错。所以第一次接这种链路时我会在 load 脚本开头把收到的参数 echo 一遍确认拿到的是表名而不是空。4. 定时与运维Quartz 六段 cron、并发策略与日志落点4.1 六段 cron 与时区是绑在一起的Azkaban 的调度用的是 Quartz 的 cron 表达式跟 Linux crontab 不是一套东西最直观的差别是它有秒# 秒 分 时 日 月 周 0 30 2 * * ? # 每天 02:30:00 0 0/15 * * * ? # 每 15 分钟 0 0 3 ? * MON-FRI # 周一到周五 03:00第一段是秒第二段是分所以「每天两点半」要写成0 30 2 * * ?写成30 2 * * *是五段字段数量对不上保存时会报错或者被解析成完全不同的意思。日和周这两段里Quartz 要求其中一个必须是?不指定写成* *虽然有些版本能容忍但语义上是有歧义的。比 cron 语法更容易翻车的是时区。cron 是按default.timezone.id解析的这个值不改你填的「2 点」就是那个时区的 2 点。所以配置那一章里我把它列成必改项。改完记得重启并且把已有的 schedule 删掉重建——旧 schedule 是在旧时区下建的指望它跟着新配置自动刷新风险不值得冒。4.2 并发、失败策略、重试三个开关各管一段执行 flow 的时候页面上有几个选项名字都挺像作用范围完全不同。Concurrent控制的是同一个 flow 能不能同时跑多份。默认不允许上一份还在跑的时候新的一次会排队或者被拒。这个默认值在定时场景下是对的一份跑三小时的日批如果允许并发上游延迟时会堆起一串同时在跑的任务把下游库压垮。只有做补数、回放的时候才临时打开。失败策略Failure Options控制的是一个 job 失败之后整条 flow 怎么收场常见三选一立刻停掉所有正在跑的 job、等当前跑完再停、或者尽量把不依赖失败节点的分支跑完。日批我一般选第二种让已经在跑的兄弟节点跑完避免产生半截数据纯计算型的 flow 选第一种早点失败早点修。retries/retry.backoff是 job 级别的只影响这个 job 自己。这三个开关叠在一起的效果要想清楚允许并发 每个 job 重试 3 次 高峰期可能有三份实例在同时重试压力是乘出来的。4.3 日志与历史出问题先看哪一层页面上的 History 能看到每次执行的完整状态点进去是 DAG 图每个节点有独立的日志stdout 和 stderr 是分开的。排查顺序我一般是这样先看失败节点的日志这是唯一能直接看到错误信息的地方按时间倒序翻到最后几屏。如果日志里只有「exit code 1」没有别的说明脚本自己把错误吞了回去给脚本加set -e和2。再看上游节点。很多「莫名其妙失败」其实是上游产出了一个空文件或者 0 行表下游读到就炸了错误信息指向下游根因在上游。最后看执行历史的时间线看这个 flow 是不是从某天开始变慢的。如果每次都卡在同一个节点问题在作业本身如果卡的位置飘忽多半是资源竞争或者外部依赖抖动。日志到底落盘还是落库不同构建不一样真实位置用配置反查# 找日志和执行记录的落点别靠猜 grep -nE execution.dir|log|dir conf/azkaban.properties | head -30 find . -maxdepth 2 -type d -name *exec* -o -maxdepth 2 -type d -name *log*4.4 备份与迁移solo 的数据其实就三堆东西单机版最容易被低估的就是数据。项目列表、flow 定义、执行历史、调度配置全在本地一次误删就全没了。迁移或备份时把服务停掉再动运行中的 H2 文件拷出来是不完整的。需要拷的就三处h2db元数据、projects项目 zip、conf配置和用户。换机器时把这三块放到同样的相对位置版本保持一致起起来就是原样。# 停服后打包别在运行中拷 H2 bash bin/shutdown-solo.sh sleep 5 tar -czf solo-backup-$(date %Y%m%d).tar.gz h2db projects confshutdown-solo.sh之后sleep 5是给进程收尾留时间然后用ps确认进程真的没了再打包。H2 在进程没正常退出时会留下锁文件带着锁文件拷过去新机器启动会直接报数据库被占用。5. 避坑排查启动、执行、调度三个阶段最容易翻车的五处5.1 启动阶段工作目录和文件锁现象一启动脚本一执行就报找不到conf/azkaban-users.xml或者服务起来了但页面白屏、静态资源全 404。原因是azkaban.properties里的路径大量使用相对路径conf/、web/、./h2db、projects都是相对当前工作目录解析的。启动脚本本身不会cd到包根目录所以你在/opt下执行/opt/azkaban-solo/bin/start-solo.sh工作目录是/opt所有相对路径全部落空。解决办法很土但有效永远先cd到包根目录再执行bash bin/start-solo.sh。要写 systemd 或者启动脚本的话把WorkingDirectory显式设成包根目录。我自己的习惯是在包根目录放一个一行的run.sh里面就两行cd $(dirname $0)和bash bin/start-solo.sh谁执行都不会错。现象二重启时报Database may be already in use或者Locked by another process。原因是上一个实例没有正常退出。用kill -9杀掉的进程不会走 H2 的关闭流程h2db目录下会残留锁文件另一种情况是你同时起了两个实例指向同一个数据目录。解决办法先ps -ef | grep azkaban确认没有残留进程端口也确认释放ss -lntp | grep 8081。确认干净之后再处理锁文件——把h2db目录下名字里带lock的文件删掉然后在包根目录重新启动。删锁文件这一步必须在进程完全停止的状态下做运行中删会直接把数据库搞坏。长期看更稳的做法是给每个实例配独立的h2.path。5.2 执行阶段shell 语义和子进程残留现象三command里写bash extract.sh /data/out/log.txt 21执行成功但目标文件根本没生成或者整个被当成参数传给了脚本。原因是 command 类型的作业最终是用ProcessBuilder拉起进程的参数按空格切分后直接执行中间不经过 shell。没有 shell 就没有重定向、没有管道、没有、没有通配符展开。只是被当成一个普通字符串参数脚本收到了它然后忽略掉。解决办法两种一是把整条命令包进bash -ccommandbash -c bash extract.sh /data/out/log.txt 21注意引号要成对切分逻辑才正确二是干脆不在 job 里写重定向把逻辑全放进脚本文件job 里只留commandbash bin/extract.sh ${dt}。第二种更好维护因为脚本能单独测。判断标准很简单只要 command 里出现了|、、、*、$()就必须走bash -c或者脚本。现象四在页面上点了 Killflow 状态变成 Killed但机器上那个脚本还在跑数据还在往库里写。原因是 Azkaban 杀进程时只杀它直接拉起的那个子进程脚本里再 fork 出去的python、java、mysql客户端属于孙子进程父进程被杀了它们会被 init 收养继续跑完。解决办法是在脚本里自己管生命周期。能用exec的地方用exec比如exec java -jar xxx.jar这样 java 进程会替换掉 bash 进程PID 不变杀的时候能杀到。需要在中间做清理的用trap捕获TERM和INT收到信号后杀掉自己起的子进程再退出。这两条加起来能覆盖绝大多数情况剩下的极端场景脚本里又套了 nohup只能靠事后核对进程。5.3 调度阶段时区把 cron 挪了八小时现象五页面上填的是每天 02:00实际执行时间是上午 10:00或者前一天傍晚差值是整数小时。原因是default.timezone.id还是默认的美洲时区cron 按那个时区解析。这不是调度器算错是配置没改。解决办法把default.timezone.id改成Asia/Shanghai重启服务然后把已有的 schedule 全部删掉重建。只改配置不重建 schedule有一部分构建会把旧的触发时间缓存下来表现是「改了没用」。重建之后找个最近的触发点验证一次比如设一个十分钟后的 cron看它准不准点触发确认无误再切成正式时间。这一步我吃过亏——时区改了、重启了但 schedule 没重建第二天报表还是晚八小时又排查了一轮。6. 把它当作业开发环境三个验证技巧和一份上线前检查表solo server 最大的价值其实不在生产而在于它是一个「能看见 DAG 的本地沙箱」。我现在的习惯是任何要上正式调度的作业先在 solo 上跑通一遍看明白依赖图和每个节点的日志长什么样再迁过去。第一个技巧是上传前先在 shell 里裸跑。.job里只有command一行所以这条命令完全可以手工执行bash bin/extract.sh 20240101。脚本在 shell 里能跑通本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站