Airflow + Docker 实现任务资源隔离的完整实践 1. 为什么做任务资源隔离调度不失控资源才不打架先说个我踩过的真实场景。早几年我维护一套基于Airflow的数据管道日常几十个DAG高峰时有几百个task在跑。一开始所有任务直接跑在Airflow所在的物理机上用的是本地Executor。刚开始任务少不明显等任务量上来之后问题接踵而至某个重型的ETL任务一启动直接把宿主机CPU拉到100%其他轻量级任务全部变慢偶尔有人写了个内存泄漏的脚本整个节点直接OOM连调度器都跟着遭殃。那段时间我天天被同事问“任务怎么又挂了”最后排查来排查去根因全是资源互相抢占。这个问题的本质在于Airflow本身只是一个工作流调度器它管的是任务什么时候跑、按什么顺序跑、失败了怎么重试但任务跑起来之后用多少CPU、多少内存、能不能读写宿主机文件系统它几乎不管。在共享节点上跑多个不自包含的任务就像一群人合租一间屋子没人规定各自的隔间边界只要有人折腾所有人一起遭殃。所以就有了这个组合方案Ubuntu做宿主机Airflow负责任务编排Docker负责把每个任务装进独立的容器里执行再通过Docker的容器资源限制能力给每个任务划定明确的CPU、内存、磁盘和进程数上限。这套组合做完之后我一个重型任务跑满CPU也只会撞到它自己的容器上限其他任务照常运行调度器再也不背黑锅。这篇文章适合谁看如果你正在用Airflow做数据管道、批处理或机器学习任务编排并且任务都直接跑在宿主机上已经感觉到资源互相干扰的痛或者你想把团队里的多个项目、多个租户的任务放在同一套调度平台里但又不想让彼此影响再或者你只是想在Ubuntu上搭一套干净、可控、可扩展的任务执行环境。这篇文章都能给你一条能直接落地的路径。2. 资源隔离的基础认知Docker限制的是内核参数先别急着敲命令我们得把原理捋清楚。很多人以为Docker的资源限制是“虚拟出一块CPU”或“划出一段内存”其实不是这样。Docker在Linux上默认用的是内核的cgroups机制也就是control groups。它的工作方式不是把资源切出来专门给某个容器而是给每个容器设一个配额上限容器里的进程可以正常使用CPU和内存但当用量超过上限时内核会介入CPU被降频调度内存分配被拒绝进程可能被OOM Killer杀掉。这个机制在Airflow加Docker的组合里非常关键。因为Airflow的task通常会拉起一个Python进程或者调用外部程序它运行多长时间、吃多少资源调度器本身无从知晓。但Docker容器可以在task启动前就固定好这个进程的资源边界相当于给每个任务都发了一张“定量饭票”。这里有一个特别容易混淆的点容器资源限制和Executor的资源限制是两层。Airflow的Executor决定同时跑多少个task比如CeleryExecutor可以为每个task启动一个worker进程而Docker只是决定每个task容器能用到多少资源。两层要一起设计调度层管并发数量容器层管单任务上限。如果你只是把Docker塞进去但不设置资源参数那效果跟直接在宿主机上跑没什么区别容器之间依旧会抢资源。2.1 为什么选Docker而不是虚拟机或纯进程隔离有人可能会问做资源隔离用虚拟机不也行吗或者直接在宿主机上用setsid、cgroup命令手动分组甚至用systemd-run也行。但放在Airflow的场景里Docker确实是最省心的选项。虚拟机的问题是开销太大。每一个VM都要跑一个完整内核启动以分钟计资源占用固定不适合高频、短周期的任务调度。Airflow的task经常是几分钟甚至几十秒就结束了用虚拟机等于杀鸡用牛刀。纯进程级隔离的问题是隔离维度不够。你可以在宿主机上用cgroup手动给进程分组、限流但那需要写一堆脚本而且在Airflow这种动态创建task的情况下要保证每个task的进程组生命周期管理正确代码复杂度会直线上升。更重要的是进程级隔离无法解决依赖环境的问题。Docker的核心优势在于“镜像即环境”。任务脚本依赖什么版本的Python、什么版本的库直接打进镜像里。调度的机器上只需要有Docker运行时不需要为每个项目维护一套依赖环境。这种可移植性和资源限制能力结合起来恰好是Airflow任务执行最需要的两个特性。2.2 Ubuntu作为宿主机的配置考量Ubuntu适合做这个组合的宿主机原因有几个。首先是内核版本足够新对cgroups v2和Docker的兼容性最好。其次是Docker Engine在Ubuntu上的安装路径最成熟基本是一条命令装完。第三是系统资源占用干净跑起来没有多余的服务抢占资源。但我们需要注意一点在生产环境上不要用带图形界面的Ubuntu Desktop尽量用Ubuntu Server版。桌面版会多出一堆后台进程还会吃内存对资源敏感的任务执行环境来说完全没有必要。另外内核参数方面建议检查一下/etc/sysctl.conf里的vm.overcommit_memory和vm.swappiness。对于任务型节点我通常会把vm.swappiness设为10以内避免系统提前把内存往磁盘交换任务一跑起来就变卡。这个参数怎么调后面在实操部分细说。3. 搭建一套最小可用的Airflow Docker执行环境下面进入实操环节。整个搭建按步骤拆解我这里给出的是经过验证的一整套流程Ubuntu版本以22.04 LTS为例Airflow版本我们以2.8.x为例Docker Engine用最新的稳定版。3.1 第一步Ubuntu系统初始化安装完系统之后先做基础升级和工具安装sudo apt update sudo apt upgrade -y sudo apt install -y curl git vim net-tools然后设置时区Airflow的调度跟时间有关保持一致能省掉很多时间戳相关的排查问题sudo timedatectl set-timezone Asia/Shanghai这一步不是必须的但建议做。接着把swap调低sudo sysctl vm.swappiness10要让这个设置持久化写入/etc/sysctl.confecho vm.swappiness10 | sudo tee -a /etc/sysctl.conf为什么强调swapAirflow的调度器和Docker容器频繁启停如果系统频繁换页会出现一种很隐蔽的现象任务本身没跑多久但任务从启动到完成的总时间特别长因为大量时间花在内存交换上。把swap调低等内存真不够时让OOM机制介入反而比任务慢吞吞跑完更符合“快速失败”的原则。3.2 第二步安装Docker EngineDocker的安装用官方源不要用Ubuntu自带的老版本sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor --no-tty -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release echo $VERSION_CODENAME) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin安装完成后让当前用户能直接操作docker命令避免每条命令前面都加sudosudo usermod -aG docker $USER注意这一步需要重新登录终端才生效。然后启动Docker并设为开机自启sudo systemctl enable docker --now验证一下docker run --rm hello-world看到Hello from Docker就说明环境正常。3.3 第三步安装Airflow及依赖Airflow的安装方式有好几种系统级pip直接装虚拟环境装或者用官方Docker镜像全家桶。这里我们的目标是让Airflow调度宿主机上的Docker容器所以Airflow本身可以直接装在宿主机上不需要把它也容器化当然你想容器化也没问题只是那样的话Airflow的Worker需要能访问到Docker的socket会多一层权限配置。我推荐用系统级Python的虚拟环境来安装Airflow干净且好维护sudo apt install -y python3-pip python3-venv sudo mkdir -p /opt/airflow sudo chown $USER:$USER /opt/airflow python3 -m venv /opt/airflow/venv source /opt/airflow/venv/bin/activate export AIRFLOW_HOME/opt/airflow pip install --upgrade pip然后指定版本安装。Airflow的依赖非常多直接用pip安装需要认真对待版本冲突。这里建议用Apache Airflow的官方约束文件AIRFLOW_VERSION2.8.4 PYTHON_VERSION$(python3 -c import sys; print(f{sys.version_info.major}.{sys.version_info.minor})) CONSTRAINT_URLhttps://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt pip install apache-airflow${AIRFLOW_VERSION} --constraint ${CONSTRAINT_URL}安装完之后额外装我们后面要用到的Docker Provider它是Airflow官方提供的与Docker交互的组件pip install apache-airflow-providers-docker初始化Airflow数据库。默认情况下Airflow用SQLite作为元数据库单机测试没问题这里我们就先用它避免一开始引入数据库依赖。当然正式环境建议换MySQL或PostgreSQL后面再聊。airflow db migrate创建管理员用户airflow users create \ --username admin \ --firstname Admin \ --lastname User \ --role Admin \ --email adminexample.com执行后按提示设置密码。然后创建DAG目录mkdir -p /opt/airflow/dags启动调度器和Web服务。为了快速验证先直接前台启动airflow scheduler另开一个终端窗口启动Web Serverairflow webserver --port 8080浏览器打开http://服务器IP:8080能登录界面就说明Airflow本体已经跑起来了。3.4 第四步配置Docker Operator相关环境Airflow要启动Docker容器实际上是在Docker守护进程上创建容器。所以Airflow的Worker进程需要能访问Docker的socket。我们在前面安装了Docker又将当前用户加入了docker组那在终端里以该用户身份启动Airflow时worker进程就有权限操作Docker。如果你用systemd方式把Airflow的scheduler和worker配置成服务那就得额外给它们配置docker组的权限或者在service里加Groupdocker。另外注意Airflow的Docker Operator有两种执行方式一种是DockerOperator它直接在宿主机的Docker deamon上创建容器另一种是KubernetesExecutor它通过Kubernetes API创建Pod。这里我们只用DockerOperator因为我们讨论的是Docker组合方案不涉及K8s。DockerOperator需要的核心配置是镜像和命令。后面我会用一个具体示例来演示。4. 核心实践用DockerOperator跑带资源限制的任务现在到了这篇文章的重头戏。我们不仅要让Airflow能跑Docker容器还要让每个任务都带上CPU、内存和进程数限制。先看一个最简单的DAG示例然后再解释每个参数背后的意义。4.1 编写第一个带资源限制的DAG在/opt/airflow/dags/下新建文件docker_resource_demo.pyfrom datetime import datetime, timedelta from airflow import DAG from airflow.providers.docker.operators.docker import DockerOperator default_args { owner: data_team, retries: 1, retry_delay: timedelta(minutes5), } with DAG( dag_iddocker_resource_demo, default_argsdefault_args, descriptionDemo for Docker resource isolation, schedule_interval*/10 * * * *, start_datedatetime(2024, 1, 1), catchupFalse, tags[demo, docker], ) as dag: heavy_task DockerOperator( task_idheavy_computation, imagepython:3.10-slim, commandpython -c import time; time.sleep(10); print(\done\), docker_urlunix://var/run/docker.sock, network_modebridge, auto_removeTrue, mem_limit512m, memswap_limit512m, cpu_shares256, cpus1.0, pids_limit100, privilegedFalse, force_pullFalse, docker_conn_idNone, )这个DAG每10分钟跑一次任务很简单就是在容器里睡10秒然后打印done。注意我在这里设置了几个资源参数mem_limit限制容器最大内存512MB超过这个值容器内申请内存会被拒绝极端情况下进程会被内核OOM杀掉。memswap_limit设置内存加swap总和上限。如果不设置Docker默认允许swap达到内存限制的两倍。对任务型容器来说不希望在内存不够时悄悄用swap所以我把memswap_limit和mem_limit设为一致等同于关闭容器的swap扩展。cpus限制容器最多使用1个CPU核心。cpus参数在Docker内部会转换为cpu quota和period即一个调度周期内最多占用多少CPU时间。cpu_shares这个参数是一个相对权重默认是1024。256代表这个容器在CPU竞争时权重较低优先级低一点。对于轻量任务把它调低一点比较好。注意cpu_shares和cpus并不矛盾cpus是硬上限cpu_shares是竞争权重。pids_limit限制容器内的最大进程数。这个参数非常重要能防止任务脚本 fork 炸弹或者意外创建海量子进程把宿主机拖死。还有几个非资源参数值得说明auto_removeTrue容器运行结束后自动删除。不设置的话容器会变成Exited状态一直堆在系统里时间久了浪费磁盘。docker_conn_idNone表示直接使用默认的Docker环境即宿主机socket。如果有远程Docker环境可以配置一个连接。privilegedFalse尽量不要给容器特权模式除非你的任务真的需要操作内核设备。4.2 测试DAG运行效果把DAG文件放好之后在Airflow界面里刷新DAG列表如果没看到检查一下scheduler进程是否在运行因为DAG需要被调度器解析到。看到DAG后点开关运行一次然后观察task log。日志里会显示Docker命令执行的完整过程。正常情况下会看到类似这样的输出[2024-06-01 12:34:56,789] {docker.py:186} INFO - Starting docker container from image python:3.10-slim [2024-06-01 12:34:57,012] {docker.py:194} INFO - Container ID: abc123... [2024-06-01 12:35:07,812] {docker.py:199} INFO - done [2024-06-01 12:35:08,011] {docker.py:210} INFO - Container exited successfully.这个过程中如果你想亲眼确认资源限制是否生效可以另开一个终端用docker inspect查看容器状态docker ps | grep heavy docker inspect container_id | grep -A 5 Memory\|NanoCpus\|Pids输出里能看到Memory、NanoCpus、PidsLimit这些字段对应的值和你配置的一致说明限制已经绑到容器上了。4.3 资源参数选择背后的逻辑每个项目的任务类型不一样资源参数怎么定需要一些经验。对于纯IO密集的任务比如从远程数据库拉数据再写文件这种任务内存可能不高但网络和磁盘占用大。重点限制内存CPU可以给少一点同时要把网络带宽的竞争做在宿主机层面Docker本身不直接限制网络带宽这是Docker的一个短板后面我们在问题排查里讲。对于计算密集的任务比如跑模型推理、做特征工程CPU是主要瓶颈。这种任务可以把cpus设为一个区间内的值比如2或4同时设置cpu_shares为1024让它和其他计算任务公平竞争。对于内存有大量使用的任务比如加载大DataFramemem_limit一定要给足冗余。经验值是预估峰值的1.5倍因为Python的gc机制有滞后性内存峰值往往出现在进程快结束的时候。还有一个容易被忽略的参数是tmpfs或者挂载卷的大小。如果任务要写大量临时文件到/tmp默认的容器层会占用的宿主机磁盘空间。这种事情应该用tmpfs挂载到容器的/tmp并且限制大小tmpfs{/tmp: size2g}这样可以保证任务写临时文件不会撑爆宿主机磁盘。4.4 多任务并发下的资源总预算单任务限制做好了还有一个更重要的层面整个节点上所有任务加起来的资源不能超过物理资源。这就像每个房间都有门锁但你得知道整栋楼一共能住多少人。在Airflow里控制并发的核心参数有三个parallelism整个Airflow实例同时运行的task实例总数默认32。max_active_tasks_per_dag同一个DAG里最多同时运行的task数。max_active_runs_per_dag同一个DAG里最多同时运行的DAG实例数。在airflow.cfg里我建议根据节点资源做一次简单计算。假设你的虚拟机有8核16GB内存每个任务平均需要1核1GB内存那么你可以把parallelism设为6到7留出一点资源给Airflow自身和Docker守护进程。如果任务大小差异很大就不要只靠这些全局参数而是要用Airflow 2.0开始引入的池Pool机制。池机制把资源量化为“槽位”。每个task在运行前需要申请槽位槽位不足就排队。比如你创建一个名为heavy_compute的池槽位数为4然后所有重型任务指定poolheavy_compute。这样不管有多少个DAG在跑重型任务最多同时4个不会突破你的预算。在DAG里给任务指定池很简单heavy_task DockerOperator( ..., poolheavy_compute, )池可以在Airflow Web界面的Admin - Pools里配置。这种做法比全局parallelism更精细推荐在实际项目中使用。5. 让资源隔离更稳妥的进阶配置只有DockerOperator的资源参数并不能解决所有问题。本机Docker的socket权限、镜像管理、日志增长、任务超时等都会在实际运行中冒出来。这一节我把自己惯用的一套进阶配置完整分享出来。5.1 超时控制Airflow里一个task实例如果卡住了默认没有任何超时机制会一直等到外层节点崩溃。对DockerOperator来说即便容器卡死了DockerOperator本身可能还在等容器返回。所以必须设置超时。DockerOperator本身有一个参数timeout单位是秒。它控制的是与Docker API交互的超时不是容器执行超时。要控制容器执行超时需要在执行超时层面控制两个都配上from airflow.utils.timezone import datetime DockerOperator( task_idtask1, imagepython:3.10-slim, command..., execution_timeouttimedelta(minutes30), timeout60, )execution_timeout是Airflow层面的task执行超时超过之后Airflow会杀掉task并标记失败。timeout是Docker客户端等待API响应的超时防止Docker daemon挂起导致Airflow worker一起卡住。还有一个更暴力的兜底方案就是在容器启动时用ulimit和timeout命令包裹真正的任务commandbash -c timeout 3600 python /opt/scripts/run.py这样即使Airflow层面因某些原因失效容器内部也会在1小时后强制退出。5.2 镜像管理任务镜像不是越多越好。每次调度都从一个很大的镜像启动容器启动时间会拖慢整个任务流程。我的建议是基础镜像用python:3.10-slim这类精简镜像不要用python:3.10完整版减少几百MB体积。把任务依赖提前打进镜像里而不是每次都pip install。这样容器启动后直接跑命令几秒内就能开始干活。给镜像打上明确的版本标签不要都用latest。DAG里指定imagemyproject:2024.06.01排查问题时才知道当时跑的是哪份代码。构建镜像的Dockerfile示例FROM python:3.10-slim WORKDIR /opt/app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ /opt/app/src ENV PYTHONUNBUFFERED1 CMD [python, src/main.py]注意PYTHONUNBUFFERED1这能保证Python日志实时输出否则任务日志会积压在缓冲区里导致Airflow长时间看不到输出。5.3 限制Airflow自身的资源使用这里说一个很多人忽略的点。Airflow本身也有CPU和内存开销尤其是调度器进程默认会解析所有DAG文件还会定期处理心跳。如果DAG文件很多或者依赖很复杂调度器CPU使用率会居高不下。建议对Airflow主进程本身也做一次资源守护。可以在systemd服务里配置MemoryMax或CPUQuota。比如你用systemd托管Airflow scheduler[Service] Userairflow ExecStart/opt/airflow/venv/bin/airflow scheduler MemoryMax2G CPUQuota200%这样即使某个DAG解析有bug导致CPU暴涨也不会拖垮整机。这个属于宿主层面的兜底。5.4 日志处理与磁盘保护Airflow默认把task日志写到$AIRFLOW_HOME/logs每个task实例一个文件。跑久了之后这个目录会膨胀得很快。尤其是任务里print了大量内容时日志文件可以轻松上几百MB。在airflow.cfg里把日志清理机制打开[logging] # 每个日志文件大小上限 max_log_size_in_bytes 1048576 # 保留的历史日志文件数 max_log_backup_count 5另外对宿主机层面做定期清理Docker垃圾docker system prune -f --volumes但要注意这条命令会删除未被使用的容器、网络、镜像和挂载卷。如果线上有正在使用的容器卷会误伤所以这个命令只适合在确认安全时手动执行或者写一个只清理悬空镜像的脚本docker image prune -f日志和镜像清理建议都做成cron任务每周跑一次防患于未然。6. 常见问题与排查技巧实录这部分是我在实际使用中踩坑最多的地方按优先级从高到低写下来。6.1 Docker socket权限问题症状Airflow任务一运行就报Permission denied while trying to connect to the Docker daemon socket。原因Airflow worker进程运行的用户没有加入docker组或者系统启动了SELinux/AppArmor限制。排查ls -l /var/run/docker.sock看看socket所属组是不是docker然后确认运行Airflow的进程用户ps aux | grep airflow处理如果用systemd启动Airflow在service文件里加SupplementaryGroupsdocker改完后systemctl daemon-reload systemctl restart airflow-scheduler airflow-webserver。6.2 容器内任务成功但Airflow显示失败症状task log最后出现Container exit code 1但容器里打印的信息看起来是正常的。原因容器的最终退出码取决于最后一条命令的退出状态。你在DockerOperator的command里如果写了多段命令但没有做错误处理最后一个命令失败或者exit code非0都会导致失败。建议在command里用set -e显式声明任何命令失败都中止容器。或者在Python脚本里捕获异常并sys.exit(0)或sys.exit(1)按你想要的结果返回。6.3 Docker容器里连不上宿主机数据库或服务症状任务在容器里运行连接宿主机上的MySQL或Redis时提示connection refused。原因容器有独立的网络命名空间。如果使用默认的bridge模式容器内的localhost是容器自身不是宿主机。要访问宿主机服务需要把localhost换成宿主机IP。在DockerOperator里如果你用的是network_modebridge可以在命令里通过环境变量传入宿主机IPdocker_operator DockerOperator( ..., environment{ HOST_IP: 172.17.0.1, }, )其中172.17.0.1是docker0网桥的默认网关即宿主机在容器网络里的地址一般固定是这个。如果不对用ip addr show docker0确认。另外如果宿主机服务监听了127.0.0.1容器是访问不了的。宿主机服务要监听0.0.0.0或至少监听在docker0的IP上同时对访问来源做好防火墙管理。6.4 磁盘被镜像或容器日志占满这个我栽过一次。某次大批量任务短时间内跑完宿主机的根分区直接被容器日志撑满系统各种服务开始报错。后来才意识到任务里有一个循环打印debug日志的语句每个容器打印了几十GB。之后我在所有任务镜像里统一加了日志切割配置并把Airflow的日志轮转打开。还有一个很实用的习惯在写Dockerfile时把日志文件挂载到宿主机的一个独立分区或者直接在容器里放弃写日志文件全部输出到stdout让Docker的日志驱动和Airflow的日志系统接管。容器日志驱动默认是json-file可以限制单文件大小{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }写入/etc/docker/daemon.json后重启Docker生效sudo systemctl restart docker6.5 cpus参数和cpu_shares参数混用时的预期有读者可能疑惑设置了cpus1.0还不够吗为什么还要设cpu_shares这两个参数的作用域不同。cpus是硬限制保证这个容器最多占用1个核心cpu_shares是软优先级只在有竞争时生效。设计方法是对每个任务设置cpus作为上限保证任务不会无限抢CPU再根据任务的重要程度设置cpu_shares。重要任务设为1024或更高不重要的设为100或200。这样即使两个任务都触达了自己的cpus上限调度时也会优先让重要任务获得更多的CPU时间片。6.6 任务并发高时Docker daemon本身成为瓶颈Docker daemon是单守护进程容器启停非常频繁时daemon的API处理会变得很慢导致任务排队。这种现象在任务周期短几秒钟且并发高时尤其明显。排查方法看宿主机上Docker进程的CPU占用如果dockerd稳定在100%以上多半是API调用过于频繁。解决策略尽量让任务运行时间长一点减少启停频率。把多个小任务合并成一个容器任务而不是每个task各起一个容器。评估使用Docker的“复用容器”模式但要注意容器状态污染不推荐。调高Airflow的min_file_process_interval和dagbag_import_timeout避免调度器频繁解析DAG这也能间接降低任务启动频率。6.7 使用旧版Docker的compatibilities问题如果宿主机Linux内核版本过旧Docker的pids_limit等参数可能不生效而是静默忽略。所以建议Ubuntu至少20.04内核5.4以上这个版本对cgroups v2支持已经比较完善。另外在Docker 20.10之后cpus限制参数兼容性更好。安装完成后可以用docker info查看Cgroup版本如果显示cgroup v2说明现代特性都能用。7. 方案升级路径从单机到多机的资源隔离单机的Docker资源隔离做到这一步已经能解决大部分问题。但如果任务量继续增长单节点扛不住了这套思路怎么外延最简单的升级路径是把Airflow的Executor从默认的SequentialExecutor切换为CeleryExecutor然后部署多台worker节点每个worker节点上都装Docker。任务会被分发给不同的worker机器每台机器上的Docker继续做资源限制。这是中等规模团队最常见的架构。在这个架构下资源预算要从单机视角升级到集群视角。假设你有5台worker每台8核16GB那么你的重型池槽位可以设为5 * 4 20逻辑上每台机器最多同时跑4个重型任务。这个计算要在设计时做清楚否则会出现某些worker满载、另一些空转的情况。另外要关注任务在worker间的分布均衡问题。CeleryExecutor通过消息队列分发任务默认的worker数量是大于等于并发时Airflow会随机选一台。如果不希望重型任务落在弱机器上可以用标签和队列的方式。Airflow的task可以指定queue参数在worker启动时指定airflow celery worker -q heavy这样只有打了heavy标签的worker才会接收heavy队列的任务。配合Docker资源限制能做到非常精细的资源管理。如果再往上走任务规模到了上百并发Kubernetes会是更合适的选择。Airflow的KubernetesExecutor可以直接为每个task创建一个PodPod天然支持request和limit。但K8s的复杂度和管理成本比Docker高一个量级对于大部分中小团队Docker组合方案已经是性价比最优解。8. 最后再分享几个实操中的心得这套Airflow Docker组合我在多个项目里反复打磨过有几个体会特别深。内存限制一定要留余量。Python任务的内存峰值往往出现在你认为不可能的地方。比如pandas的groupby、numpy的广播、机器学习的predict这些操作的内存暴涨都是瞬间的事情。我见过一个任务平时只吃200MB某次数据量翻倍后内存瞬间飙到2GB。所以mem_limit千万不要卡着预估值设至少留出30%到50%的冗余。如果怕内存超限导致任务失败频繁可以结合Airflow的重试机制设置合理的指数退避。任务的优雅退出比强制杀掉更重要。容器里跑的任务最好自己处理信号Python代码里注册SIGTERM处理器当Docker停止容器时给进程一点时间做清理比如提交检查点或者写失败标记。如果任务在毫无防备的情况下被强杀数据状态可能处于中间状态下次重跑时很容易出幺蛾子。不要贪心把什么都塞进Docker。宿主机上的元数据库如果用的SQLite和Airflow日志目录最好不要放到容器里。容器的生命周期太短日志和数据容易丢。Airflow本身跑在宿主机上日志落在宿主机磁盘才是最稳妥的。排查问题时先看宿主机再看容器。我遇到任务异常时曾经花几个小时在容器日志里找原因最后发现是宿主机磁盘满了。所以当你被某个问题卡住时先执行free -h、df -h、uptime这三个命令确认宿主机资源正常再进容器里看。这能省下大量时间。任务资源隔离不是一次配置就永远不用管的事。任务代码在演进数据量在变化资源参数需要周期性review。我的习惯是每个月看一次DAG的运行时长和资源监控曲线发现某个任务持续逼近资源上限就及时调整参数或拆分任务。隔离方案是静态的但使用方式是动态的保持关注才能让它持续发挥作用。