本章目标:能写 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 各自的作用。
- 能列出至少三条容器上线的检查项,例如资源限制、密钥、持久化。