摘要:最后一场实战把六层知识各就各位:多阶段构建的镜像、双网络拓扑、命名卷、cgroup 限额、Compose 编排、安全加固与观测,一次部署三层应用;随后给出"按层定位"的排错方法论与高频故障速查表。
任何应用容器化上生产前,值得用一张检查单快速自检,全部答"是"再动手:镜像是否用精确版本号或摘要锁定?是否以非 root 用户运行?是否配了健康检查?入口是否 exec 数组形式、能接收 SIGTERM?内存与 CPU 限制是否声明?数据是否落在命名卷并配了备份?端口是否只发布到需要的接口?日志是否走标准输出且有轮转?这八个问题分别对应第 2、3、5、6、7 章的一个知识点,答不上来的那一条,就是回去翻书的路标。下面用一个真实规模的例子把检查单走完。
目标系统:nginx 网关 + Flask 应用 + PostgreSQL,单机生产。逐层做决策。
镜像层。应用用多阶段构建(2.3 的王牌),锁定不可变 tag:
FROM python:3.12-slim AS deps WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt FROM python:3.12-slim RUN addgroup -S app && adduser -S app -G app WORKDIR /app COPY --from=deps /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages COPY --chown=app:app . . USER app HEALTHCHECK --interval=10s --timeout=3s --retries=5 \ CMD python -c "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8000/healthz')" CMD ["python", "-m", "gunicorn", "-b", "0.0.0.0:8000", "blog:app", "--workers", "2"]
细节全部有出处:slim 底座与缓存清理(2.3)、非 root 用户(7.1)、exec 数组 CMD(2.2)、健康检查(6.1)、监听 0.0.0.0(4.2)。
网络层。双网络分段(4.2 的拓扑):front-net 连网关与应用,back-net 连应用与数据库,只有网关发布 80 端口。
存储层。数据库数据进命名卷,拒绝匿名卷(5.2)。
运行时层。应用限内存 512m、CPU 1.0(3.2 起步值),数据库按峰值 1.5 倍给 1g。
治理层。日志驱动带轮转;部署后接入第 7.2 节的指标流水线。
汇总成编排文件:
services: db: image: postgres:16.4 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - blog-db:/var/lib/postgresql/data networks: [back] deploy: resources: limits: { memory: 1g } app: image: myrepo/blog:1.4.2 environment: DATABASE_URL: postgres://blog:${DB_PASSWORD}@db:5432/blog depends_on: db: { condition: service_healthy } networks: [front, back] deploy: resources: limits: { memory: 512m, cpus: "1.0" } gw: image: nginx:1.27-alpine ports: - "10.0.0.5:80:80" depends_on: [app] networks: [front] volumes: blog-db: networks: front: back:
发布到内网接口而非 0.0.0.0(4.2 的安全习惯),镜像用精确版本号(2.3 的不可变纪律)。部署一条命令:
docker compose -f compose.yml -f compose.prod.yml up -d docker compose ps
三个服务全绿(健康检查通过)即部署完成。验证观测面板有数据、日志正常滚动,一场生产化部署的骨架就齐了。
容器故障的第一反应不该是重启,而是定位它在哪一层。六层各有一组特征症状,判断顺序自下而上:
按"症状—层—处置"组织,都是生产上反复出现的剧目:
| 症状 | 层 | 处置 |
|---|---|---|
| 退出码 137,OOMKilled true | 运行时 | 查泄漏或提高内存 limit |
| 启动即退,日志 module not found | 镜像 | 依赖没进镜像,查构建缓存与 COPY 顺序 |
| 容器间按名访问不通 | 网络 | 确认同在自定义网络;目标监听 0.0.0.0 |
| 外部能连但日志里客户端 IP 全一样 | 网络 | DNAT 所致,用代理协议头取真实 IP |
| 容器删了数据没了 | 存储 | 当初写进了可写层;改卷并做备份 |
| stop 要等整整 10 秒 | 运行时 | 信号没到应用:exec 形式 + 信号处理器 + --init |
| 数据库初始化随机失败 | 编排 | depends_on 缺 condition: service_healthy |
| 磁盘满 | 存储/治理 | system df 盘点;查日志轮转配置 |
| CPU 满载但 throttled 为零 | 观测 | 应用真忙,扩容而非改配置 |
| 每次重启都被压垮又"复活" | 编排 | 崩溃循环,看日志栈回溯找根因 |
两个取证命令值得形成条件反射:
docker inspect <容器> --format '{{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' docker logs --tail 100 <容器>
第一条三秒定性(活没活、怎么死的、是不是被内核杀的),第二条看临终遗言。大多数排障在这两条之内就能收束。
回看全书,六层各自的"解决什么问题、底层怎么做"已经全部拆完:镜像层用 UnionFS 解决环境封装与分发;运行时层用 namespace 与 cgroup 解决隔离与配额;网络层用 NET namespace 与虚拟设备解决连接;存储层用挂载通道解决数据生存;编排层用声明式模型解决规模化运维;治理层用纵深防御与观测解决生产可信。以后遇到任何 Docker 问题,先问它在哪一层——这个问题会带着你找到答案。