ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Docker的Azkaban Solo模式调度Spark任务实践指南

基于Docker的Azkaban Solo模式调度Spark任务实践指南 做数据开发的朋友应该都有同感本地想搭一套完整的“数据任务链路”最烦的往往不是写SQL和脚本而是怎么把零散的任务串成可定时、可重试、可追踪的工作流。我一直在用Docker做学习环境这一篇就围绕Azkaban Solo模式怎么配合Spark做调度展开属于整个系列的第四篇。前面几篇已经把Spark基础环境、基础数据处理跑通了这一篇的目标很明确在容器里装一个Azkaban用它的网页界面把Spark任务配置成可重复运行的调度流并且能从页面上看到日志、失败重试、定时执行这些完整能力。这套东西非常适合两类人一是刚开始学调度系统的小白想不碰物理机直接把Azkaban跑起来二是团队里需要快速验证某个Spark脚本能不能被自动化调度又不想申请集群权限的研发。它解决的核心问题是“怎么用最低成本体验调度系统”。1. 为什么把调度环境装进Docker设计思路与整体架构1.1 这个系列在做什么Azkaban在其中的位置我之前写这个系列时定的主线是用Docker在本地复现一套轻量级大数据处理环境。前面几篇里已经规划好了数据存储层和计算层Spark可以跑起来Hive表也建好了。但做到这一步你会发现所有任务都是人肉触发——手动敲spark-submit、手动看结果、失败了一脸懵。这在实验环境里还能忍可一旦想模拟一个真实的“每日定时数据处理流程”没有调度器就完全不对味。Azkaban在这里扮演的就是“任务编排和触发中枢”。它本身不做数据计算只管“什么时候、以什么顺序、用什么参数去执行某个任务”。你可以把Spark任务、Shell脚本、Python脚本都塞给它它会按你画好的依赖关系依次执行同时记录每一次执行的状态和日志。我选择在这一篇引入Azkaban而不是直接在命令行里写crontab原因也很简单crontab只能解决“定时”这一个纬度解决不了“任务A成功后才跑任务B”“失败了自动重试”“每个任务都有独立日志”这些真实生产里绕不开的问题。调度器不是锦上添花是数据任务从“脚本”走向“流程”的关键一步。1.2 调度器选型Azkaban Solo、Crontab、DolphinScheduler怎么选网上聊调度器动不动就搬出DolphinScheduler、Apache Airflow、甚至自研引擎理论上一大堆。但我给这套学习环境定了一个原则在保证功能块完整的前提下安装复杂度和资源占用越低越好。先看crontab。它确实是零依赖但缺点太直接没有执行记录、没有日志汇总、写复杂依赖靠shell自己拼失败重试基本靠重复执行。我自己的经验是crontab最多适合三五个互不相关的定时脚本再多就失控。再看DolphinScheduler等重型调度。它功能确实强但依赖MySQL、ZooKeeper本身还是一整套分布式架构配置项极多。我见过很多新手在DolphinScheduler上折腾一天最后卡在前置依赖上真正的调度逻辑却没学到。对于一台笔记本里的学习环境完全没必要。Azkaban刚好卡在中间。它的Solo模式把所有组件打包在一个进程里内嵌H2数据库不需要额外装MySQL或ZooKeeper。安装就是解压、改配置、启动三步。但它的调度能力一点不少支持job依赖、定时执行、失败重试、权限控制、Flow级日志已经覆盖了生产调度八成以上的日常场景。LinkedIn当初就是拿它在Hadoop生态里做工作流调度血统不用怀疑。1.3 整体架构与影响范围这一套Docker环境的架构用大白话来说就是一个容器里同时住着Spark和Azkaban Solo Server容器外面只暴露Azkaban的网页端口。Azkaban负责按时拉起Spark任务Spark负责跑真正的计算两者通过本地进程通信不需要跨网络。为什么这么设计因为Solo模式本身就是“单机全家桶”。它的Executor和Web Server在同一个JVM里任务直接在本地执行天然适合和学习环境绑定。如果硬要拆成两个容器反而需要在容器间配置主机名、注册executor徒增工作量。这套架构的影响范围其实比“自娱自乐”要广得多。我在实际工作中就用类似方式给团队搭了一套临时开发调度沙箱前端同事在Azkaban页面上申请执行任务后端同学把Spark脚本传进去几分钟就能把一条完整的数据流程跑起来。对于新同学的培训也不用担心他们误操作影响生产集群。容器坏了就直接删掉重建成本几乎为零。2. 环境准备镜像组合、目录规划与编排方式2.1 版本组合与镜像选型版本选择是这类环境里最容易被忽略、却最能决定成败的一步。我一开始图新鲜选了最新的Azkaban 4.x结果和JDK版本卡了半天。后来老老实实换成稳妥的组合一次通过。我的推荐组合是这样的基础镜像ubuntu:20.04或eclipse-temurin:8-jdk后者自带JDK少一层麻烦。JDK版本8或11。Azkaban 3.x到4.x主推JDK8Spark 3.x也完全兼容JDK8我为了省事统一用8。Spark版本3.3.x以上任意稳定版这里用3.5.1部署模式用local或standalone均可。Azkaban版本3.90.0或4.0.0。我用的是solo server打包版注意不要下成多个模块的源码包。为什么不用JDK17因为Azkaban在JDK17下会出现反射访问报错需要额外加JVM参数学习阶段完全没必要跟它拼。用JDK8所有组件都能在默认参数下跑起来这才是“最小阻力路径”。2.2 用Dockerfile组装一套“开箱即用”的基础镜像如果你从零开始可以直接写一个Dockerfile把Spark和Azkaban都装进去。这个镜像后续留下的不仅是执行环境还是一份完整的依赖清单。FROM ubuntu:20.04 ENV DEBIAN_FRONTENDnoninteractive ENV JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 ENV SPARK_HOME/opt/spark ENV PATH$PATH:$JAVA_HOME/bin:$SPARK_HOME/bin:$SPARK_HOME/sbin RUN apt-get update apt-get install -y --no-install-recommends \ openjdk-8-jdk \ wget \ python3 \ python3-pip \ vim \ net-tools \ rm -rf /var/lib/apt/lists/* RUN wget -q https://archive.apache.org/dist/spark/spark-3.5.1/spark-3.5.1-bin-hadoop3.tgz \ tar -xzf spark-3.5.1-bin-hadoop3.tgz \ mv spark-3.5.1-bin-hadoop3 /opt/spark \ rm spark-3.5.1-bin-hadoop3.tgz # 将 azkaban-solo-server-3.90.0.tar.gz 放在构建上下文同目录 COPY azkaban-solo-server-3.90.0.tar.gz /opt/ RUN tar -xzf /opt/azkaban-solo-server-3.90.0.tar.gz -C /opt/ \ mv /opt/azkaban-solo-server-3.90.0 /opt/azkaban \ rm /opt/azkaban-solo-server-3.90.0.tar.gz WORKDIR /opt CMD [bash]有几个点值得说明。第一wget下载Spark的地址用的是apache archive稳定可靠版本锁死后不会有“今天能下载明天就失效”的问题。第二Azkaban的tar包建议提前下载好放进构建目录别在Dockerfile里临时下载否则网络抖动一次整个镜像构建就重来。第三环境变量必须写进Dockerfile而不是在容器里手敲不然每次进入容器都要重新export。2.3 启动容器与端口规划镜像构建好后启动容器要规划好端口和目录。Azkaban Solo模式的Web端口默认是8081Spark如果开了Web UI默认是8080还有Spark Master默认是7077这些端口在启动时要决定哪些映射到宿主机。docker run -d --name azkaban-lab \ -p 8081:8081 \ -p 8080:8080 \ -v /opt/data:/opt/data \ -e JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 \ -e SPARK_HOME/opt/spark \ azkaban-spark-lab:latest \ sleep infinity这里有个容易踩的坑sleep infinity作为主进程可以保持容器存活但如果后面想用docker exec进容器操作记住所有环境变量都只在交互终端里有效Azkaban启动脚本读的是/etc/profile或azkaban-solo-server.sh里定义的JAVA_HOME不是docker run传进去的。所以我习惯在容器里把JAVA_HOME、SPARK_HOME追加到/etc/profile一劳永逸。端口不用全映射出来我实际只映射8081一个端口其他服务都在容器内部访问。这样宿主机只暴露一个入口不会被Spark自带的一堆端口弄得眼花缭乱。3. Azkaban Solo模式核心配置单个进程里也能跑出完整调度3.1 Solo模式与Two Server/Multi Executor的差异很多初学者第一次接触Azkaban会被“Solo”和“Two Server”这些名词绕晕。我换个说法Solo模式就是全家桶Web界面和干活的人住在同一个进程里Two Server模式是把管界面的和干活的拆成两个进程分居不同机器Multi Executor则是管界面的一个干活的可以好多台。Solo模式的好处是零外部依赖。它的数据库用的是内嵌的H2不需要单独装MySQLExecutor和Web是同一个进程不需要注册服务。坏处也很明显所有任务都在本机执行没法横向扩展而且Web进程重启会导致正在跑的任务跟着中断。但在学习环境里这些都是可以接受的。我当时选择Solo模式的另一个原因是它和Two Server模式的配置逻辑基本一致。以后你从Solo切到Two Server只需要把数据库改成MySQL、把Executor单独启动注册思路完全相通。学习阶段用Solo把概念打牢后面上生产换模式是顺理成章的事。3.2 azkaban.properties关键项逐项拆解Azkaban的核心配置在/opt/azkaban/conf/azkaban.properties。这个文件决定了端口、数据库、Executor端口、任务目录这些关键行为。我梳理了几个必须懂的核心项jetty.portAzkaban Web界面的HTTP端口默认8081外部访问靠的就是它。database.type数据库类型Solo模式用h2不需要动。h2.pathH2数据库文件存放路径。注意容器删除后这个目录会带走全部Flow执行记录想要持久化就挂载到宿主机目录。executor.portWeb和Executor内部的通信端口默认12321。Solo模式是内部通信不要映射到宿主机。azkaban.project.max.size上传zip包的大小上限默认10MB。如果Spark脚本依赖大量本地lib需要调大到100MB。azkaban.temp.dir临时文件目录如果容器重启后任务日志丢失一般是这里没挂载持久卷。还有一个比较容易忽略的配置项是use.multiple.executors。Solo模式必须保持为false否则会默认找外部Executor注册我见过有人把这个改成true结果界面一直显示“No active executors”排查半天。3.3 用户配置与启动验证Azkaban默认没有admin用户需要在conf/azkaban-users.xml里显式配置。打开这个文件在azkaban-users标签内增加user usernameadmin passwordadmin rolesadmin,metrics/ role nameadmin permissionsADMIN/这里建议一上来就给自己的账号授予ADMIN权限因为学习阶段的很多操作查看Flow日志、kill任务、管理project都需要管理员权限。如果只给普通用户权限后面可能遇到“write permission denied”之类的报错。启动命令就一条cd /opt/azkaban ./bin/start-solo.sh启动后会生成logs/azkaban-solo-server.log建议先看这个日志而不是急着重启。正常情况下会出现Server running on port 8081类似的输出。然后用浏览器访问http://localhost:8081输入admin/admin就能登录。如果页面访问不了优先检查容器端口映射然后看容器内部是否有进程在监听8081用docker exec azkaban-lab netstat -tlnp看一眼。不要一开始就怀疑Azkaban配置错了大部分访问不了的情况是端口没暴露出来。4. 手把手实操在Azkaban中调度一个真正的Spark任务4.1 创建工程并上传Job定义Azkaban调度的基本单位是Project一个Project下面可以有多个Flow一个Flow由多个Job组成。Job用.job后缀的纯文本文件定义里面是keyvalue格式的属性类似Java properties文件。登录Web界面后第一步是点击“Create Project”填一个项目名比如spark-demo。创建后点击项目进入详情页你会看到一个上传zip包的入口。这个zip包就是你的Job定义文件集合。这里有几个细节需要注意。第一zip包里的.job文件必须处于根目录Azkaban不支持扫描子目录里的job文件。第二.job文件编码必须是UTF-8否则脚本里有中文注释会解析报错。第三zip包不能包含__MACOSX这类系统目录否则Azkaban会警告。我之前在Mac上压缩zip没注意默认会塞进一些元数据上传后Flow解析直接失败。先上传一个最简单的zip里面只有一个hello.job# hello.job typecommand commandecho hello azkaban上传成功后点击项目名选中这个Flow再点击“Execute Flow”按钮就能看到执行记录的跳转。这一步跑通说明Azkaban环境本身没问题后面可以上真家伙了。4.2 用command Job驱动spark-submit这里要聊一个关键点Azkaban官方并没有内置一个叫spark的Job类型最稳的做法是用typecommand然后在command属性里写spark-submit命令。这样做的原因是Azkaban本身只负责进程级调度具体跑什么由你交给它的命令决定这种设计反而灵活。看一个真实的job定义# wordcount.job typecommand command${SPARK_HOME}/bin/spark-submit \ --master local[2] \ --class org.apache.spark.examples.JavaWordCount \ ${SPARK_HOME}/examples/jars/spark-examples_2.12-3.5.1.jar \ /opt/data/input.txt注意我在command里用了${SPARK_HOME}这个变量这是利用Azkaban对系统环境变量的读取能力。但有个前提启动Azkaban的进程必须能拿到SPARK_HOME。我建议在bin/start-solo.sh或/etc/profile里写死环境变量而不是指望Azkaban自己会去读。还有一个更稳的写法是直接用绝对路径command/opt/spark/bin/spark-submit --master local[2] --class ...我实际测试下来绝对路径最省心虽然看起来笨但不会因为环境变量丢失导致spark-submit: command not found。学习阶段追求的是稳定复现而不是炫技。4.3 依赖、参数与重试把流程调得像生产环境Azkaban最有价值的功能是Job之间的依赖关系。比如一个完整的数据处理流程是“清洗 - 聚合 - 结果入库”这三个步骤必须依次执行前一步失败后一步不能跑。在Azkaban里实现依赖很简单在job文件里写dependencies属性# clean.job typecommand commandsh /opt/scripts/clean.sh # aggregate.job typecommand dependenciesclean command${SPARK_HOME}/bin/spark-submit --class com.demo.Aggregate /opt/scripts/demo.jar # report.job typecommand dependenciesaggregate commandpython3 /opt/scripts/gen_report.py上传后你会在Flow页面看到三个节点之间有连线视觉效果非常直观。如果clean.job运行失败aggregate.job和report.job会自动变成“CANCELLED”状态不会傻乎乎地继续跑。参数传递是另一个重要技能。Azkaban内置了一批系统参数可以在job里直接引用比如azkaban.flow.start.timestamp代表Flow启动时间。我一般用这套参数给Spark任务传业务日期避免每天手工改参数。command${SPARK_HOME}/bin/spark-submit --class com.demo.DailyReport /opt/scripts/report.jar --dt ${azkaban.flow.start.year}${azkaban.flow.start.month}${azkaban.flow.start.day}失败重试也不能忘。在job里加两行retries2 retry.backoff5000意思是失败后自动重试2次每次间隔5秒。我强烈建议所有Spark任务都加上retries因为Spark在本地模式下的偶发OOM或端口冲突一次重试往往就能恢复。4.4 一个完整示例每日统计用户复购率用一个贴近业务的例子把这些串起来。假设我们要每天统计电商平台用户的复购率数据从Hive里取Spark负责计算结果写入MySQL或HDFS。整个Flow我设计成三个Jobextract.job拉取前一天的用户订单明细生成中间表。repeat_calc.job调用Spark任务读取订单明细按用户聚合判断是否复购输出统计结果。sync.job把统计结果同步到业务库并发送一条通知。job文件如下# extract.job typecommand commandsh /opt/scripts/extract.sh ${azkaban.flow.start.year}${azkaban.flow.start.month}${azkaban.flow.start.day} # repeat_calc.job typecommand dependenciesextract command${SPARK_HOME}/bin/spark-submit --master local[2] --class com.demo.RepeatPurchase /opt/scripts/repeat-purchase.jar --dt ${azkaban.flow.start.year}${azkaban.flow.start.month}${azkaban.flow.start.day} # sync.job typecommand dependenciesrepeat_calc commandpython3 /opt/scripts/sync_to_mysql.py把这个三个文件打成一个zip上传在Web界面选择“Schedule”设置每天凌晨2点执行一次就完成了一个真正的“定时Spark调度任务”。你可以看到每个Job对应的日志点进去都是完整版排查问题很舒服。5. 踩坑记录与排查技巧Azkaban Spark Docker组合的真实体验5.1 环境变量与路径问题这一类问题几乎每个新人都要撞一次。最典型的报错是任务执行后显示spark-submit: command not found。我一开始以为是Azkaban没读环境变量后来才明白Azkaban启动的进程继承的是它自己JVM的环境变量跟你在容器里用docker exec进去敲命令时的环境变量不是同一个。解决办法就是不用环境变量job命令里全部写绝对路径或者在start-solo.sh脚本里手动export。另外Spark任务如果依赖Hadoop的native库容器里可能报Unable to load native-hadoop library。这个警告其实一般不影响学习环境跑local模式我建议直接忽略。真要处理装一个libhadoop.so配套的native库但花的时间不值。5.2 资源限制与内存问题Docker默认会限制容器能用的内存尤其在Mac上用Docker Desktop默认内存可能只有2GB。跑Spark任务时如果出现ExecutorLostFailure或者Container killed by YARN之类的问题十有八九是容器内存不够。我建议给Docker Desktop至少分配4GB内存。如果还是不够就把Spark的执行内存调小在job里这样加参数command${SPARK_HOME}/bin/spark-submit --master local[2] --driver-memory 1g --executor-memory 1g --class ...这里我特别想强调学习环境里不要贪大。local[2]已经足够体验并行计算再往上加核数除了一堆进程内存溢出对理解原理没有帮助。先把内存控制在1GB以内跑通再去追求更大的数据量。5.3 文件规范、编码与时区这个是Azkaban特有的坑。.job文件如果以UTF-8 with BOM格式保存Azkaban解析时会多一个不可见字符导致key名异常。解决方法是统一用纯UTF-8无BOM格式用VS Code或Notepad保存时注意选“UTF-8”。时区问题则是定时任务最隐蔽的坑。Azkaban默认使用服务器本地时区容器里的系统时区默认UTC导致你以为每天凌晨2点执行实际却是北京时间上午10点跑。处理办法是在容器里设置时区ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime echo Asia/Shanghai /etc/timezone然后重启Azkaban。最好在Dockerfile里就加上这一步一劳永逸。5.4 问题排查速查表现象可能原因解决方法8081页面无法访问端口未映射或容器未启动docker ps查看容器状态检查-p 8081:8081参数上传zip后Flow解析失败zip含中文路径或Mac元数据重新压缩确认job文件在zip根目录任务一直READY不执行executor未注册确认use.multiple.executorsfalse重启Azkaban任务执行后报command not found环境变量缺失command里使用绝对路径Spark任务OOM容器内存上限太小调大Docker内存或调低Spark executor内存执行记录突然丢失H2数据库未持久化将h2.path挂载到宿主机目录排查的核心思路是先看Azkaban日志再看Job日志。Azkaban日志在logs/azkaban-solo-server.logJob日志在Web界面点进具体执行实例就能看到。不要一上来就重构配置先定位是哪个环节出的问题。这轮实测下来我最大的感受是调度系统的门槛并不高难的是把每个细节都串起来。Azkaban Solo模式给了我们一个极低成本的入口配合Docker更是把环境搭建压缩到了十几分钟。建议大家在跑通基础Demo之后把日常工作里的一个脚本哪怕是简单的Python清洗任务交给Azkaban去调度很快就能体会到工作流管理的价值。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进