Docker 与容器 列表全部文章
Docker 与容器
Docker 与容器第 4 / 5 篇

Docker 笔记(二)容器篇:生命周期、资源限制、退出码与健康检查

容器不是一个小型虚拟机,它是被 namespace 隔离、被 cgroup 限制的一个进程。理解了这点,「为什么一启动就退出」「为什么 JVM 以为自己有 64G 内存」「为什么 exit 137」都能自己解释。这篇讲生命周期、run 参数、cgroup 限制与 JVM 的关系、退出码速查、优雅停机和健康检查。

更多

容器不是一个小型虚拟机,它是被 namespace 隔离、被 cgroup 限制的一个进程。理解了这点,下面这些问题都能自己解释:为什么容器一启动就退出、为什么 JVM 以为自己有 64G 内存、为什么 exit code 是 137、为什么 docker stop 之后请求还是被硬生生掐断。

一、容器的三个底座

机制作用具体隔离/限制了什么
namespace隔离"看得见什么"pid(进程树)、mnt(挂载点)、net(网卡/端口)、ipc、uts(主机名)、user(uid 映射)
cgroup限制"能用多少"CPU、内存、blkio、pids、设备
联合文件系统提供文件系统视图见镜像篇的只读层 + 容器层

关键推论:容器里 PID 1 那个进程退出,容器就退出。没有"后台守护"这回事。

二、生命周期与状态

docker create  →  created
docker start   →  running  ←→  paused(docker pause/unpause)
进程退出       →  exited
docker rm      →  deleted

其中 restarting 是重启策略介入时的临时状态。看状态用:

docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

三、docker run 常用参数

参数作用备注
-d后台运行不加就是前台,Ctrl+C 容器就停
-it分配交互终端docker run -it ubuntu bash 调试必备
--name指定名字不指定会随机生成,脚本里很难用
--rm退出自动删除临时容器专用,省得手动清理
-p 8080:80端口映射宿主机端口:容器端口
-v /data:/var/lib/mysql挂载卷见网络与存储篇
-e KEY=VALUE环境变量密码建议用 env_file 或 secrets
--network指定网络bridge / host / none / 自定义
--restart重启策略见第六节
--memory 1g内存上限见第四节
--cpus 2CPU 上限见第四节
--user 10001指定运行 uid生产别用 root
--read-only根文件系统只读配合 --tmpfs /tmp
--health-cmd健康检查命令见第七节
--init用 tini 做 PID 1解决僵尸进程与信号转发
--privileged放开所有限制几乎永远不该用

四、资源限制:不限制就是裸奔

docker run -d \
  --name app \
  --memory 1g --memory-swap 1g \
  --cpus 1.5 \
  --pids-limit 300 \
  --restart unless-stopped \
  myapp:1.0
参数含义说明
--memory(-m)物理内存硬上限超了就是 OOM Kill,exit 137
--memory-swap内存 + swap 总量等于 --memory 表示禁用 swap;设为 -1 表示不限 swap
--memory-reservation软限制宿主机紧张时才生效,用于超卖
--cpus可用 CPU 核数(可为小数)cgroup v2 下最直观
--cpu-shares相对权重只在 CPU 打满时按比例分配,不是硬限制
--cpuset-cpus绑定具体核减少上下文切换,也用于隔离
--pids-limit最大进程数防 fork 炸弹拖垮宿主机
--blkio-weight / --device-write-bps磁盘 IO 限制避免一个容器把磁盘打满

JVM 容器感知:为什么容器 2G,JVM 以为有 64G

这是容器化 Java 最经典的一个坑。早期 JVM 读的是宿主机的 /proc/meminfo,看不到 cgroup 限制,于是按宿主机内存算默认堆(物理内存的 1/4)。宿主机 256G,容器限 2G,JVM 就敢把堆开到 64G,起步就 OOM。

# 验证容器里看到的内存到底是谁的
docker run --memory 2g eclipse-temurin:21-jre java -XX:+PrintFlagsFinal -version | grep -i maxheapsize
free -m    # 在容器里执行,看到的往往是宿主机的数字

解法:

  • JDK 8u191+ / JDK 11+:-XX:+UseContainerSupport 默认开启,会按 cgroup 限制算堆;再用 -XX:MaxRAMPercentage=75.0 指定占比(别用 -Xmx 写死,那样容器扩容要改代码)。
  • JDK 8u131-8u190:-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap。
  • 更老的版本:只能显式 -Xmx,并且让它跟 --memory 对齐。
  • CPU 同理:-XX:ActiveProcessorCount 可以手动指定,避免 JVM 按宿主机核数开线程池。
java -XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0 -jar app.jar

完整的排查过程见:Java 搬进 Docker 就"水土不服":容器 2G 内存,JVM 却以为自己有 64G。

注意:堆只是内存的一部分。限了 -Xmx 之后还有元空间、线程栈、直接内存、GC 开销。堆外内存泄漏同样会被 cgroup 判死刑,这类案例见 堆明明没满,容器却被 OOMKilled。

五、退出码速查

退出码含义怎么查
0正常退出—
1应用自身报错docker logs
125docker 命令本身失败(参数错、镜像名非法)命令行输出
126命令存在但不可执行(权限问题)检查 entrypoint 文件权限
127命令不存在基础镜像里没这个命令(alpine 没有 bash)
137被 SIGKILL:OOM Kill 或 docker killdocker inspect 看 OOMKilled;dmesg | grep -i killed
139段错误(SIGSEGV)多为 native 库问题
143收到 SIGTERM 后退出(docker stop 的正常结果)—
docker inspect app --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
dmesg | grep -i -E 'killed|oom' | tail -20

六、优雅停机:docker stop 之后为什么请求还是断了

docker stop 的流程是:发 SIGTERM → 等 10 秒(--time 可调)→ 还不退就 SIGKILL。要优雅停机,得满足两个条件:

  1. 应用真的能收到 SIGTERM:ENTRYPOINT 必须是 exec 格式(见镜像篇)。shell 格式下 PID 1 是 /bin/sh,它不会把信号转给子进程。
  2. 应用会处理 SIGTERM:Spring Boot 要开优雅关闭。
# Spring Boot 配置
server:
  shutdown: graceful          # 等待在途请求处理完
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

还有一层:K8s / Compose 停止容器前,上游可能还在往里发请求。真正的无损下线要把从注册中心摘除排在 SIGTERM 之前(Spring Boot 的 graceful shutdown 配合 preStop hook)。

七、健康检查

HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD curl -fsS http://localhost:8080/actuator/health || exit 1
# 或者运行时指定
docker run -d --health-cmd='curl -fsS http://localhost:8080/health || exit 1' \
  --health-interval=30s --health-timeout=3s --health-retries=3 --health-start-period=40s \
  myapp:1.0

docker inspect --format '{{.State.Health.Status}}' app

三个状态:starting → healthy → unhealthy。--start-period 是给慢启动应用的宽限期,避免启动阶段就被判死。

健康检查端点要轻:只查进程活着 + 关键依赖能连,不要查全库、不要做重计算。健康检查把应用打死是真实发生过的事。

八、重启策略与"重启风暴"

策略行为适用
no不自动重启(默认)一次性任务
on-failure[:N]非 0 退出才重启,最多 N 次有 bug 会崩的服务,避免无限重启
always退出就重启,Docker 重启后也会拉起常驻服务,但要小心死循环
unless-stopped同 always,但手动 stop 过就不再拉起推荐,尊重人工操作

典型事故:内存限得太小 → 应用启动到一半 OOM(exit 137)→ always 立刻重启 → 再 OOM,容器每 40 秒重生一次,日志刷满、告警淹没。重启策略要配合资源限制和健康检查一起调,别只开一个。完整案例见 容器每 40 秒"重生"一次:Restarting、exit 137 与一场 restart always 引发的重启风暴。

九、命令清单

docker run -d --name app -p 8080:8080 myapp:1.0
docker ps -a                                  # 看状态
docker logs -f --tail 200 app                 # 实时日志
docker logs --since 10m app                   # 最近十分钟
docker exec -it app sh                        # 进容器(alpine 用 sh,没有 bash)
docker exec -it app env                       # 看环境变量
docker stats app                              # 实时资源占用
docker top app                                # 容器内进程
docker inspect app --format '{{.State.Health.Status}}'
docker update --memory 2g --cpus 2 app        # 运行中调整限制(无需重建)
docker stop --time 30 app                     # 给 30 秒优雅退出
docker kill -s SIGTERM app
docker rm -f app
docker events --since 1h                      # 看这一小时发生了什么
docker system df -v                           # 磁盘占用明细

备忘

  • 容器 = namespace + cgroup + 一个进程;PID 1 退出容器就退出。
  • Java 必须确认 UseContainerSupport 生效,用 MaxRAMPercentage 而不是写死 -Xmx。
  • exit 137 = 被 SIGKILL,八成是 OOM;139 是段错误;143 是正常 SIGTERM。
  • 优雅停机两要素:exec 格式 ENTRYPOINT + 应用处理 SIGTERM。
  • 重启策略别单独用 always,配合资源限制和健康检查。
  • 健康检查端点要轻量,配 --start-period 给慢启动留宽限。
— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人