ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Docker容器Restarting状态排查:从秒退原因到解决实践

Docker容器Restarting状态排查:从秒退原因到解决实践 搞容器排障最怕什么怕的是你盯着docker ps输出发现容器不是Up而是Restarting (1) Less than a second ago。注意那个“Less than a second ago”这意味着容器从启动到崩溃连一秒钟都没撑住docker 又在自动帮你重启。今天这篇我不讲复杂的编排就聚焦这个状态它到底在说什么为什么会出现以及最有效的排查路径是什么。写这篇文章的起因也简单。最近身边连续几个同事踩进同一个坑有人是因为直接抄了 Dockerfile 里的错误命令有人是挂载目录权限没给够还有人干脆是内存限制设得太小。这类问题看着各不相同但核心规律是一致的容器启动后进程立即退出触发 restart policy 无限拉起。这篇文章适合刚接触 docker 不久、一看到Restarting就慌的读者也适合那些把容器跑起来就算完、平时不看日志的老手。我会从状态解读、原因分类、排查流程、真实案例到预防手段把这条链路完整走一遍。1. 先搞清楚Restarting 状态到底在告诉你什么1.1 docker ps 输出的几个关键字段该怎么读先看一个典型的docker ps输出CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a12f2345cd67 myapp:1.0 /docker-entrypoint.… 15 minutes ago Restarting (1) 2 seconds ago web你第一眼应该关注STATUS列。正常情况下容器状态是Up 15 minutes这种代表进程健康运行。如果看到Restarting说明容器内部主进程已经退出了docker 守护进程正在按重启策略重新创建容器。括号里的(1)是已经被拉起的第 1 次“2 seconds ago”表示最后一次退出到现在只过了 2 秒。有些版本会显示成你标题里的写法Restarting (1) Less than a second ago一字不差就是这个场景。另外Exited (0)和Restarting (1)是两回事。前者是容器跑完就正常退出了后者是容器启动后立刻异常退出又立刻被拉起来形成一个“崩溃循环”。有时候docker ps里看不到这种容器是因为它处于重启间隔的瞬间窗口用docker ps -a才能稳定看到。1.2 谁在替你“反复拉起”容器重启策略容器崩溃后为什么会被自动拉起这得归功于restart policy。Docker 有几种值no、on-failure、always、unless-stopped。其中always和unless-stopped会在容器以任何状态退出时都尝试重启哪怕你手动docker stop了容器除非显式docker stop后重启否则反而可能被守护进程拉起来。开发阶段我见过不少人用了restart: always然后容器一崩日志疯狂刷新因为 docker 在极短周期内不断创建新容器。你看到的Restarting (1)、Restarting (2)括号里的数字会一直涨涨到 100 多也不奇怪。这里有个小陷阱on-failure只在退出码为非 0 时才重启如果进程以 0 退出容器就安静停在Exited (0)。而always不管退出码是多少都拉起来。所以排查问题前先看容器的重启策略搞清楚它为什么会被反复拉起再决定要不要先临时停掉自动重启免得日志被重启过程刷爆。2. 为什么容器总是秒退原因清单与判断方法2.1 入口命令或者启动脚本有问题最常见的原因就是主进程根本没法正常启动。可能是 Dockerfile 里ENTRYPOINT或CMD写错了路径比如镜像里根本没有这个脚本也可能是脚本没有执行权限还可能是脚本格式不对。我拿个生活类比来说容器就像一个只开放一个入口的房间docker 只会在你指定的那个入口推一下推完立刻关门。如果入口后面根本没有路或者门是锁的那房间里的人一步都迈不出去直接就“死亡”了。这个“死亡”就是主进程退出容器也就随之进入Restarting。比如日志里出现/usr/bin/env: ‘bash\r’: No such file or directory那就是典型的 Windows 换行符问题。下一条我会专门讲这个案例。再比如你写的启动脚本第一行是#!/bin/bash但脚本没有可执行权限docker 启动的时候也会报permission denied。这种情况在构建镜像时经常会漏掉RUN chmod x这一步。2.2 环境变量、挂载卷、端口冲突等配置问题主进程没崩但应用因为配置直接panic或返回错误退出的同样会出现Restarting。环境变量缺失是最常见的数据库连接串、密钥、配置模式任何一个为 null 都可能让应用启动时直接抛异常。挂载卷的问题我也见过不少。宿主机目录不存在时docker 会默认创建一个空目录挂进去应用读取目录里的配置就会失败。目录权限不对就更隐蔽尤其直接用官方镜像时容器内用户是mysql、postgres这类非 root 用户宿主机目录权限如果是 700 且属于 root容器内用户根本没写入权限一启动就报错退出。端口冲突也是老演员。宿主机 8080 端口已经被别的进程占用docker run 又指定-p 8080:8080容器启动时绑定端口失败直接退出。这类错误在日志里通常一眼能看出来。2.3 应用依赖的服务还没就绪这个原因常被忽略但杀伤力极大。你的应用启动时需要连数据库、连 Redis、调配置中心这些服务没起来应用连不上就 abort。问题在于 docker 不会帮你去判断依赖顺序它就是简单粗暴地同时启动这两个容器。举例来说你的 Java 服务启动时发现数据库连接池初始化失败直接 throw 异常进程退出。docker 看到进程退出就重启下一次数据库刚好起来了服务连上了一切恢复正常。所以有些时候Restarting只是一过性问题它会自己好但大多数时候依赖服务一直起不来容器就会一直重启。判断这点有个小技巧看日志里最后一条报错是什么。如果应用明确写着Connection refused或dial tcp 192.168.1.10:3306: connect: connection refused十有八九是依赖服务的问题需要调整启动顺序或者给应用加足够长的重试逻辑。2.4 资源限制导致 OOM 或被主动 kill容器启动后直接被系统杀掉的情况也不少见。主要分两类一类是内存超限被 OOM Killed一类是显式被docker stop或docker kill不过被你手动 kill 时状态一般不是Restarting。关键判断方法是看退出码。内存被杀时退出码通常是 137也就是SIGKILL128 9。如果你用docker inspect看State里的OOMKilled字段它会是true。举个真实例子一个 Java 服务JVM 默认堆内存会占物理内存的四分之一你宿主机 16G 内存容器限制 512MJVM 一启动就超限直接被杀容器疯狂重启。这类问题表面上和“容器启动失败”没关系但就是资源配额没规划好。3. 一套有效排查流程从日志到复现可直接照做3.1 第一步先看状态和重启次数别急着重启容器很多人一看到容器挂了的第一反应是docker restart这是典型的错误操作。重启不会解决问题只会让重启次数继续涨还容易把日志刷没。正确做法是先冷静下来执行docker ps -a关注STATUS列里的重启次数然后马上执行docker inspect container-name | grep -A 10 State重点看这几个字段ExitCode: 1, Error: , OOMKilled: false, RestartCount: 12ExitCode是容器最后一次退出的退出码OOMKilled是判断是否被内存杀掉的直接依据RestartCount告诉你它已经重启了多少次。如果退出码是 0 但还在Restarting说明重启策略是always问题可能更隐蔽需要进一步看日志。3.2 第二步docker logs 是定位问题的关键接下来看日志docker logs container-name --tail 200如果日志很多可以用docker logs container-name --since 10m这两种命令的差别我先说明白--tail只看最后 N 行适合日志很多、只关心最近报错的情况--since看最近 N 分钟内的日志适合容器重启了几十次、你想看最近一次启动时到底发生了什么的情况。有个细节容易被忽略看日志时不要只盯着末尾要找到启动初期打印的第一条错误。容器崩溃记录往往是2024-06-12 10:00:01 Starting application... 2024-06-12 10:00:01 Error: Failed to connect to database 2024-06-12 10:00:01 Aborting...如果你只看最后几行可能只看到Aborting...前面的原因被刷掉了。所以日志多的时候建议先把日志保存下来再按时间段去查docker logs container-name app.log 213.3 第三步用 docker inspect 查看完整配置日志看完了如果只是看日志还无法定位就看配置。执行docker inspect container-name输出是一个很长的 JSON重点关注这几个小节Config.Entrypoint和Config.Cmd容器实际执行的命令是什么。Config.Env有没有缺失的环境变量。Mounts挂载从宿主机哪里到哪里宿主机路径是否存在、权限对不对。HostConfig.RestartPolicy重启策略是啥。HostConfig.Memory内存限制是多少。比如你发现Cmd是[--config, /etc/myapp/config.yaml]但Mounts里根本没有这个配置文件那问题基本就锁定了。3.4 第四步覆盖入口进容器手动复现问题如果日志和配置看完还是没定位最有效的一招是覆盖容器的默认入口进容器手动执行启动命令。docker run -it --rm --entrypoint /bin/sh image-name进入容器后你可以手动执行镜像里定义的启动脚本/usr/local/bin/start.sh这样做的价值在于docker 自动启动时你看不到交互式终端但你自己手动跑一遍能清楚地看到报错发生在哪一步甚至可以边改配置边验证。如果容器里连/bin/sh都没有说明镜像底层是 distroless 之类的精简镜像这种情况可以用docker cp把脚本复制出来或者在宿主机用docker export导出文件系统再查看。4. 真实案例实录三个让我调了一晚上的 Restarting4.1 案例一Windows 下编辑的 shell 脚本一启动就格式报错有一次同事给我发了一个镜像说启动后一直Restarting (1)日志里看到的是standard_init_linux.go:228: exec user process caused: no such file or directory如果你在网上搜会看到很多文章说这是“文件不存在”但实际上问题可能出在换行符。他在 Windows 下用记事本或 VSCode 写了一个docker-entrypoint.sh保存的时候默认用了 CRLF\r\n结尾而 Linux 的 shell 只认 LF\n。结果就是脚本第一行#!/bin/bash\r系统去找名为/bin/bash\r的解释器当然找不到于是报错。解决办法也很简单sed -i s/\r$// docker-entrypoint.sh或者dos2unix docker-entrypoint.sh处理完重新构建镜像容器就正常启动了。经验所有在 Windows/macOS 里编辑过、准备放到 Linux 容器里运行的 shell 脚本构建前先检查一下格式。可以用file命令直接看file docker-entrypoint.sh如果输出里带with CRLF line terminators就跑一遍dos2unix。4.2 案例二MySQL 容器反复重启其实是数据目录权限另一个常见场景挂在官方镜像上。我在一个测试环境里跑 MySQL 8.0用docker run指定了-v /mydata/mysql:/var/lib/mysql结果容器启动后立刻退出状态一直是Restarting。看日志发现关键一行[ERROR] [MY-011087] [Server] Different lower_case_table_names settings for server (2) and data dictionary (0).这个报错其实是个误导真正的问题在早几行mysqld: Cant create/write to file /var/lib/mysql/ibtmp1 (Errcode: 13 - Permission denied)原因很简单宿主机/mydata/mysql是 root 用户创建的默认权限可能是755而容器内的 MySQL 进程是以mysql用户运行的无法写数据目录。解决chown -R 999:999 /mydata/mysql或者启动时用--user $(id -u):$(id -g)。如果你用 Docker Compose可以在服务里加一行user: 1000:1000排掉权限问题后MySQL 就正常起来了。经验官方镜像的数据目录通常有指定的 UID/GID不要想当然先查镜像文档再挂载目录。挂载目录的权限问题不会写在docker logs最前面要看完整日志。4.3 案例三Java 服务无响应秒退最后是内存不够 OOMKilled第三个案例是一个 Java Spring Boot 服务容器启动没一会儿就退出状态也从Up变成Restarting。docker logs看不到明显的 Java 异常堆栈只是进程消失有些版本甚至日志干干净净。此时我查了docker inspectdocker inspect container | grep OOMKilled输出是OOMKilled: true, ExitCode: 137这就明确了不是代码问题不是配置问题是内存不够被内核杀了。这个 Spring Boot 应用默认堆内存根据宿主机内存动态计算我宿主机内存 16G容器限制设成 512m应用启动时光 JVM 堆就想去拿 4G瞬间超限被 kill。解决方案也简单启动命令里显式限制 JVM 内存java -Xms256m -Xmx256m -jar app.jar同时调整容器内存限制docker run --memory512m ...经验Java 应用在容器里一定要手动指定-Xmx千万别依赖 JVM 自动识别。JVM 在 cgroup 限制下虽然新版会尝试识别配额但依然不够稳定手动指定最靠谱。5. 减少 Restarting 乱出现的日常预防措施5.1 开发阶段不要直接 restart: always我理解很多人为了方便在docker run或 compose 里直接写restart: always理由是“挂了它能自己拉起来”。但开发阶段你频繁改代码、改配置容器一启动就断restart: always会无限重启日志刷得飞快还会占用不必要的 CPU。开发阶段建议用restart: no或restart: on-failure:5后者表示最多重启 5 次第 6 次失败后就安静停下不再折腾。这样你既能看到Restarting现象又不会被“无限重启循环”干扰排查。生产环境再根据需求改成always或unless-stopped但前提是你已经用健康检查确认应用能正常启动。5.2 给容器加上 healthcheck 健康检查Restarting本质上是容器的“还魂术”但它的判断标准是进程是否存在而不是应用是否就绪。一个容器进程活着但应用处于不健康状态docker 不会自动重启它。这时候需要健康检查。在 Dockerfile 里加HEALTHCHECK --interval30s --timeout3s --start-period5s --retries3 \ CMD curl -f http://localhost:8080/health || exit 1或者docker run时指定docker run --health-cmdcurl -f http://localhost:8080/health --health-interval30s --health-retries3 --health-timeout3s myapp加了健康检查后docker ps的STATUS列会显示Up 2 minutes (healthy)或Up 2 minutes (unhealthy)。这样你就能区分“进程活着但不好”和“进程直接崩了”两种状态排查思路完全不一样。5.3 构建镜像时多留一手ENTRYPOINT 与 CMD 的搭配镜像构建阶段的习惯会影响启动阶段的稳定性。我强调一个ENTRYPOINT和CMD尽量用 exec form也就是 JSON 数组写法。推荐这样写ENTRYPOINT [docker-entrypoint.sh] CMD [nginx, -g, daemon off;]少用 shell formENTRYPOINT docker-entrypoint.sh区别在于exec form 会直接以子进程方式运行进程 PID 为 1能正确接收到 docker stop 传出的 SIGTERMshell form 实际是/bin/sh -c docker-entrypoint.shsh 是 PID 1而真正的应用是 sh 的子进程信号容易被吞掉导致优雅退出不生效进程被 SIGKILL 强杀从而出现非正常退出。启动脚本里还应该加上exec前缀#!/bin/bash exec java -jar app.jar用exec让脚本进程被应用进程替换这样信号能直接送到 Java 进程避免残留子进程。6. 常见问题速查表6.1 症状与原因速查症状常见原因排查入口启动后立刻退出日志无输出入口命令错误、脚本格式错误、权限不足docker inspect查看 Entrypoint/Cmd日志提示 Permission denied挂载目录权限不足或脚本无执行权限docker logsls -l检查宿主机目录日志提示 connection refused依赖服务未启动调整启动顺序或加依赖重试退出码 137OOMKilledtrue内存超限被杀手动指定应用内存参数加大容器内存限制退出码 1日志有堆栈应用本身启动异常看应用日志可能是配置或代码问题端口启动失败宿主机端口被占用netstat -tlnp查端口占用日志提示 no such file or directory入口文件不存在或 CRLF 换行符file entrypoint.sh检查格式6.2 退出码速查退出码含义常见场景0正常退出进程主动 exit(0)仍触发 always 重启策略时会变成 Restarting1通用错误应用抛异常、脚本执行出错126命令不可执行权限不足或格式不对127命令不存在路径写错、解释器不存在137被 SIGKILL 杀死常见 OOM或 docker kill 强杀139段错误可能是容器内死循环或有 C 扩展的崩溃143被 SIGTERM 杀死通常是 docker stop 后未处理信号7. 最后再分享一个小技巧排查Restarting这类问题我最常用的一个套路是在写任何 docker 编排、部署任何容器之前先在宿主机手动跑一遍原始启动命令。举个例子你是一个 Python 服务镜像里定义的是CMD [python, app.py]那在调试阶段我会用--entrypoint python启动容器进到容器里手动执行python /code/app.py看它能不能在前台正常跑起来。只要这一步能保证应用不秒退再回到 docker run 层面去解决入口配置问题就轻松多了。如果你发现容器还是不断重启不妨先把restart策略改成no让它彻底停下docker update --restartno container-name然后再用上面的方法慢慢排。这个技巧在线上救过我很多次——不用重启容器不用改编排一条命令先让场景冷静下来后面才有心思做完整排查。你下次遇到Restarting (1) Less than a second ago别慌按第 3 节的流程从头走一遍绝大多数问题都藏不住。
返回列表