DD 数据开发学习站DATA ENGINEERING
模块 07 · 调度与工程

容器化部署

Dockerfile、Compose、K8s 最小对象与上线检查,让任务在任何机器上一致运行。

难度 · 进阶 约 120 分钟 选学 练习 6 项 计划内 刷题 0/3
本页目录 · 26 节

本章目标:能写 Dockerfile 和 Compose 文件把数据任务跑起来,看懂 K8s 的 Deployment、Service 和 ConfigMap,并知道上线前要检查什么。 前置知识:Python 工程化、调度、补数与幂等 预计用时:120 分钟

“在我电脑上能跑”是数据任务上线前最常见的问题。容器把代码、Python 版本、系统依赖打包在一起,让开发、测试、生产用同一份运行环境。

1. 为什么需要容器

没有容器时,部署一个 Python 任务要手动装对应版本的 Python、装 requirements.txt 依赖、处理系统库与时区编码差异、配环境变量和日志目录。换一台机器再来一遍,任何一步不一致都可能出问题。容器把“环境”变成镜像的一部分,部署时只拉镜像、给配置,不再手工装依赖。

2. 镜像、容器、卷与网络

概念 是什么 类比
镜像(Image) 只读模板,包含代码和运行环境 安装光盘
容器(Container) 镜像的运行实例 用光盘装出来的一台机器
卷(Volume) 持久化目录,容器删了数据还在 外接硬盘
网络(Network) 容器之间按服务名通信 局域网

关键区别:容器里的文件默认不持久。写日志、写结果到容器内部,容器重建就没了。数据要么写到挂载卷,要么写到外部数据库/对象存储。

3. 动手:把 ETL 任务打成镜像

假设已经按 Python 工程化 建好了 jobs/ 目录。

3.1 写 Dockerfile

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY jobs ./jobs

RUN useradd --create-home --uid 10001 app \
    && chown -R app:app /app
USER app

ENTRYPOINT ["python", "-m", "jobs.etl.cli"]

几个要点:python:3.12-slim 固定大版本,避免 latest 带来不可控变化;先复制 requirements.txt 再复制代码,依赖没变时可以复用缓存;用非 root 用户运行,降低容器被攻破后的影响;PYTHONUNBUFFERED=1 让日志实时输出。

3.2 写 .dockerignore

.git
.venv
__pycache__
*.pyc
data
docs
site.tar.gz

data 不进镜像:数据属于运行时输入,不属于代码。把大数据打进镜像会让镜像巨大、更新变慢。

3.3 构建和运行

docker build -t datadev-etl:0.1 .

docker run --rm \
  -v "$PWD/data:/app/data:ro" \
  datadev-etl:0.1 \
  --input data/sample/orders_demo.csv

输出和本地一致:北京 455.00、上海 405.00 等。-v 把本地 data 目录只读挂进容器;:ro 防止任务误删源数据。用 docker images 和 docker ps -a 查看镜像与容器。

3.4 用 Compose 组织多个服务

一个任务通常还要连数据库或消息队列。compose.yaml:

services:
  etl:
    build: .
    environment:
      ETL_LOG_LEVEL: INFO
      ETL_INPUT: /app/data/orders_demo.csv
    volumes:
      - ./data:/app/data:ro
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: datadev
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: datadev
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U datadev"]
      interval: 5s

volumes:
  pgdata:

启动和查看日志:

export POSTGRES_PASSWORD='change-me-locally'
docker compose up --build
docker compose logs -f etl
docker compose down

密码用环境变量传入,不要写进 compose.yaml 提交到 Git。

4. Kubernetes 最小对象

单机用 Compose 够了;多机、滚动发布、自动恢复才需要 K8s。先认识四个对象:

  • Deployment:声明“我要几个副本、用哪个镜像”。
  • Service:给 Pod 一个稳定访问入口。
  • ConfigMap:放非敏感配置。
  • Secret:放密码、密钥(注意:默认只是 base64,不是加密)。

4.1 ConfigMap

kubectl create configmap etl-config \
  --from-literal=ETL_LOG_LEVEL=INFO \
  --from-literal=ETL_INPUT=/app/data/orders_demo.csv

4.2 Deployment

apiVersion: apps/v1
kind: Deployment
metadata: {name: etl-job}
spec:
  replicas: 1
  selector: {matchLabels: {app: etl-job}}
  template:
    metadata: {labels: {app: etl-job}}
    spec:
      containers:
        - name: etl
          image: registry.example.com/datadev-etl:0.1
          args: ["--input", "/app/data/orders_demo.csv"]
          envFrom: [{configMapRef: {name: etl-config}}]
          resources:
            requests: {cpu: "250m", memory: "256Mi"}
            limits: {cpu: "1", memory: "1Gi"}

requests 是调度依据,limits 是上限。不写资源限制,一个任务可能把节点内存吃光。生产环境还要用 volumeMounts 挂载数据卷,或直接读对象存储。

4.3 Service

如果任务要提供 HTTP 接口:

apiVersion: v1
kind: Service
metadata:
  name: etl-api
spec:
  selector:
    app: etl-api
  ports:
    - port: 80
      targetPort: 8080

Service 按 label 找到 Pod,给它一个集群内稳定的名字和 IP。Pod 重建后 IP 会变,Service 不变。

4.4 常用命令

kubectl apply -f configmap.yaml
kubectl apply -f deployment.yaml
kubectl get pods
kubectl logs deploy/etl-job
kubectl describe pod <pod-name>
kubectl exec -it <pod-name> -- sh
kubectl rollout restart deploy/etl-job

5. 健康检查与发布

5.1 两种探针

  • livenessProbe:容器活着吗?失败会重启。
  • readinessProbe:能接流量吗?失败会从 Service 摘掉,但不重启。
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

批处理任务通常没有 HTTP 接口,可以用 exec 探针检查进程或文件,但不要给一次性任务配 liveness,否则任务正常结束也会被判定失败。

5.2 CI/CD 最小流程

提交代码 → 跑测试 → 构建镜像 → 推镜像仓库 → 更新 Deployment 镜像 tag → 观察日志和指标

镜像 tag 用版本号或 Git commit,不要用 latest。latest 无法知道线上到底跑的是哪一版,回滚也没有明确目标。

6. 常见坑

6.1 容器里写数据、用 root 运行

容器重建后,写在容器文件系统里的结果全丢,输出必须写挂载卷、对象存储或数据库。root 容器挂载宿主机目录后,误操作可以改宿主机文件;Dockerfile 里创建普通用户并 USER 切换。

6.2 把密钥打进镜像、镜像太大

ENV PASSWORD=...、COPY .env 都会留在镜像层里,删掉文件也不一定消失,要用 Secret 或运行时环境变量。把数据集、虚拟环境、缓存目录复制进镜像会让镜像变成几个 GB;用 .dockerignore 排除,依赖在构建时装。

6.3 不设资源限制

一个任务内存泄漏,可能拖垮整个节点。requests 和 limits 都要设,并按实际压测调整。

6.4 时区不一致

容器默认 UTC,业务按北京时间看报表会差 8 小时。显式设置 TZ=Asia/Shanghai,或者统一在应用里做时区转换,别靠宿主机默认值。

7. 在线练习

题目 考点
595. 大的国家 简单过滤,保持 SQL 手感
620. 有趣的电影 条件过滤与排序
1068. 产品销售分析 I 多表关联

8. 面试延伸

面试题:Docker 镜像和容器有什么区别?

是什么:镜像是只读模板,容器是镜像的运行实例;一个镜像可以启动多个容器。

解决什么问题:镜像固定环境,容器隔离运行,部署时不用手工装依赖。

我的实操边界:本章在本地构建和运行过镜像,未负责生产 K8s 集群运维。

面试题:K8s 的 Deployment 和 Service 分别做什么?

是什么:Deployment 管理 Pod 副本和发布;Service 给 Pod 提供稳定访问入口。

解决什么问题:Pod IP 会变,Service 让调用方不用关心具体 Pod。

我的实操边界:了解对象模型和常用命令,未做过大规模集群排障。

面试题:为什么镜像不要用 latest 标签?

是什么:latest 指向不明确,无法知道线上版本,也无法精确回滚。

解决什么问题:用版本号或 commit 标签,发布和回滚都有确定目标。

我的实操边界:本章示例使用 0.1 版本标签。

9. 本章验收

  • 能写出一个使用非 root 用户、固定 Python 版本的 Dockerfile。
  • 能用 docker build 和 docker run 跑通 ETL 任务,并通过挂载卷读取输入。
  • 能解释镜像、容器、卷、网络四者的区别。
  • 能写出一个包含应用和数据库的 Compose 文件,并用健康检查控制启动顺序。
  • 能说出 Deployment、Service、ConfigMap、Secret 各自的作用。
  • 能列出至少三条容器上线的检查项,例如资源限制、密钥、持久化。
本章任务0 / 6
本页目录