ARTICLE DETAIL

资讯详情

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

Docker容器秒崩循环(Restarting)排查思路与常见原因

Docker容器秒崩循环(Restarting)排查思路与常见原因 有段时间没更新这个系列了正好最近又碰到一个挺典型的容器故障顺手记录一下排查过程。情况是这样的开发同事反馈某个服务一直起不来docker ps一看状态列挂着Restarting (1) Less than a second ago重启间隔连一秒都不到。这种“秒崩循环”在 Docker 日常运维里非常常见但也特别容易让新手一头雾水因为日志往往还没来得及输出容器就已经被重启策略拉起来下一轮了。这篇文章就把这类问题的排查思路完整梳理一遍从状态字段解读、退出码分析到日志抓取手法和常见的坑争取让遇到同样问题的人能少走点弯路。1. 先看明白 Restarting 状态到底在告诉你什么很多人第一眼看到Restarting (1) Less than a second ago就开始慌其实这个状态本身已经带了不少信息。把状态拆开看(1)是重启次数说明容器已经被调度器重启了至少一次后面的Less than a second ago是距离上一次启动的时间换句话说这个容器当前正处在“启动了又挂掉、挂掉马上重启”的死循环里。出现这种状态说明你给容器配置了重启策略最常见的是--restartalways或者--restartunless-stopped。设计初衷是好的进程崩了自动拉起来但副作用就是如果你的程序本身启动就失败Docker 并不会帮你判断“这是不是一次有效启动”它只负责执行重启策略。结果就是容器以极高的频率反复创建、退出、再创建系统资源被白白消耗日志也可能被刷爆。这里有个细节值得注意重启间隔。Less than a second ago意味着这个容器基本上是“秒退”的连一个完整的进程生命周期都没撑过去。这种情况和那种跑几分钟才崩一次的容器故障原因往往完全不同。秒退的大概率是启动阶段的硬性错误——命令不存在、依赖缺失、权限不够、配置解析失败撑几分钟再挂的则更可能是运行时的资源耗尽、连接超时或者业务逻辑问题。排查之前先把这两类区分开能节省很多时间。还有一个小坑如果你用docker logs看不到任何输出不要急着怀疑日志驱动配置很可能是容器主进程在初始化阶段就失败还没来得及往 stdout/stderr 写东西。这种情况下需要换一条排查路径后面会详细讲。2. 排查第一步退出码是故障的第一把钥匙容器每次退出都会留下一个退出码。这个数字基本上就是程序给你的最后遗言千万别忽略。我一般习惯先把退出码拿到手再决定下一步往哪个方向查。docker inspect -f {{.State.ExitCode}} 容器名或ID拿到退出码之后对照常见情况来缩小范围退出码常见含义典型场景0正常退出进程主动结束但配合 Restarting 状态就很奇怪多半是启动脚本有逻辑分支直接 exit 01通用错误程序自身报错配置不对、连接失败、校验不过都有可能126权限问题或命令不可执行脚本没有执行权限或解释器无权限访问127命令不存在PATH 问题或启动命令里的可执行文件根本没打进镜像137被 SIGKILL 杀掉最常见的是 OOM也可能是有人手动 kill -9139段错误程序本身的内存访问越界通常是原生依赖编译问题143SIGTERM 终止进程收到终止信号配合重启策略看像是被系统或编排器清理2误用 shell 内建命令比如脚本里把exit写错位置或者命令语法错误130SIGINT 中断终端 CtrlC 或者容器收到中断信号光看退出码还不够退出码只是结果你得把“为什么退”挖出来。比如 137 这个退出码十次里有九次是 OOM但也不能排除有外部进程对容器发起了kill -9。再比如说 1这个最含糊Java 程序启动时抛异常退出是 1MySQL 初始化失败也是 1Python 脚本报错也是 1唯一的共性就是“程序自己不干了”。所以退出码用来定性真正定位还得靠日志和 inspect 信息。另外提醒一句docker inspect里除了ExitCode还有个Error字段有时候会直接告诉你容器启动失败的原因比如oci runtime error。很多人在这一步会忽略docker inspect -f {{.State.Error}} 容器名或ID这条命令在日志为空的时候特别救命它拿到的信息往往比日志更底层、更接近运行时的问题。3. 日志空空如也怎么办用 inspect 和 events 还原现场真正让人头疼的是那种docker logs什么都不输出的情况。我见过不少人卡在这一步就不知道怎么继续了其实还有三条路可以走。第一条路看docker inspect的完整输出。除了刚才提到的ExitCode和Error还可以查看State.FinishedAt、State.StartedAt、RestartCount这些字段把容器的“死亡时间线”拼出来。另外HostConfig.RestartPolicy能确认重启策略的配置Config.Cmd和Config.Entrypoint能帮你确认启动命令到底是怎么定义的。很多时候问题就出在启动命令上而 inspect 里正好能看到实际生效值。第二条路用docker events实时追踪容器状态变化。在容器反复重启的过程中后台跑一条命令docker events --filter container容器名 --filter eventdie --since 1mevents会打印出容器每次死亡时的事件信息里面往往带有退出码甚至错误描述。这比干等日志直观得多尤其适合现场复现时使用。第三条路临时覆盖重启策略和启动命令手动前台运行容器。这是最暴力也最有效的一招docker run --rm -it --entrypoint sh 镜像名如果能通过这个方式进入容器说明镜像本身是完整的问题大概率出在原本的启动命令或应用配置上。如果连这个都进不去说明镜像构建阶段就埋了雷比如基础镜像损坏或者动态库缺失。这个方式和之前的启动方式不一样不会触发重启策略反而能让你拿到第一手的报错信息。还有一个容易被忽视的细节有些容器秒退不是因为程序的问题而是因为入口脚本里用了错误的方式。比如 Dockerfile 里写的是CMD [/start.sh]但start.sh没有#!/bin/bash这行 shebang或者没有给执行权限容器启动时就会直接报exec format error或者 permission denied而且日志里可能只会出现一行报错非常容易漏看。4. 六个高频原因和对应解法基本上能覆盖九成问题排查思路有了下面整理一下我实际运维中遇到最多的几类原因。每一个都附上典型的报错特征和解决方式可以直接对照你的容器状态来判断。4.1 前台进程和后台进程的经典误区这是新手最容易踩的坑而且报错特别隐蔽。在 Dockerfile 里写CMD [/etc/init.d/nginx, start]或者写service mysql start表面上看没毛病但容器启动后几秒就退出。原因很简单容器里必须有一个前台运行的进程Docker 判断容器是否存活看的不是“你有没有启动过服务”而是“PID 1 进程是不是还活着”。service nginx start这类命令会把 nginx 放到后台运行然后命令本身很快就执行完了PID 1 直接退出容器自然也跟着停。然后重启策略一拉又起来一轮同样的事情再发生一遍于是你就看到了Restarting (1)。解决办法也很直接用前台方式运行服务。比如 nginx 镜像里的nginx -g daemon off;或者 supervisor 配置里把daemonize关掉。如果你自己写启动脚本脚本里最后一行一定要启动一个前台进程并且建议用exec替换掉当前进程让应用进程直接变成 PID 1。这样可以避免 shell 作为父进程带来的信号处理问题也方便 Docker 正确转发停止信号。4.2 启动命令本身有问题命令不存在、路径不对另一种常见情况是镜像里根本找不到启动命令里写的那个可执行文件。比如你在 Dockerfile 里把应用装在/app/bin但CMD里写的是run_server而没有写成绝对路径/app/bin/run_server。如果 PATH 环境变量没有包含/app/bin容器启动时就会报exec: run_server: executable file not found in $PATH退出码通常是 127。这种问题定位起来很快docker logs会直接给出报错。但有一种变体比较坑命令路径不对不是报找不到文件而是报no such file or directory。这个往往出现在动态链接库缺失的场景下——文件是存在的但加载器找不到依赖库系统给出的报错也是“找不到文件”容易被误导。解决方式分两层一是确认启动命令的路径是对的建议一律使用绝对路径二是确认执行权限特别是自定义脚本进入容器看一眼ls -l确认有-rwxr-xr-x这样的权限位。4.3 动态库和运行时环境不匹配说到动态库缺失这是从源码编译进容器的程序特别容易犯的毛病。经常听人问“我在本机编译好二进制丢进容器就跑不起来退出码是 127 或者 1。”原因通常是宿主机和新容器的基础镜像 libc 版本不一致程序依赖的 glibc 版本偏高而镜像里的版本偏低装的时候装不上。有一种更隐蔽的情况如果程序是用 musl 静态编译的但你对镜像做了奇怪的操作导致基础镜像本身不完整也会出现莫名其妙的启动问题。排查方式是用 ldd 检查依赖库ldd /path/to/your/binary如果有任何一个依赖显示not found就说明基础镜像的库环境不满足要求。解决方案有三种换更完整的基础镜像把缺失的库拷贝进去但要注意 glibc 版本兼容或者用静态编译的方式重新构建应用彻底摆脱动态库依赖。4.4 资源限制OOM 是 137 的头号来源退出码 137 基本等于被SIGKILL而SIGKILL在容器场景里最典型的原因就是内存超限。如果容器设置了--memory限制而应用实际占用的内存超过了这个上限内核的 OOM killer 会直接杀掉进程不留任何商量余地。怎么确认是不是 OOM先看退出码是不是 137然后看系统日志dmesg | grep -i oom如果要确认是不是某一个特定容器触发的可以结合docker inspect里的State.OOMKilled字段docker inspect -f {{.State.OOMKilled}} 容器名或ID如果输出true那就是内存不够用了。解决思路一般分三步第一分析应用真实内存占用是堆内存设置太高还是缓存机制把内存吃满第二适当放宽--memory限制但不要无脑放宽到不限制第三如果同时运行多个容器要检查宿主机整体内存是否够用swap 配置是否合理。还有一类容易忽略的资源问题磁盘空间和 inode 耗尽。容器写日志把磁盘写满或者镜像层太多导致 overlay 分区 inode 被占满都会让容器启动失败。这种报错在日志里可能只有一行no space left on device退出码是 1很容易被当成业务问题去排查绕一大圈才发现根因在宿主机。4.5 应用配置或依赖服务未就绪这一类的特点是容器本身能启动但启动后业务初始化时发现配置缺失、数据库连不上、依赖的 Redis 还没就绪于是直接报错退出。退出码可能是 1也可能是 0——有些启动脚本在检测失败后会显式exit 0这个最容易迷惑人。典型场景docker-compose 里一个应用依赖 MySQL但没有配置depends_on的健康检查条件MySQL 容器虽然起来了但初始化还需要几十秒应用这边已经尝试连接数据库并失败了。如果应用没有重试机制启动即崩溃然后被重启策略拉起来再崩如此往复。解决办法有几个层次最简单的在应用侧加启动重试和延迟稍微好一点的在 docker-compose 里用depends_on: condition: service_healthy配合健康检查再往上是引入 init 容器或编排平台级别的探活机制。另外应用本身要写好配置校验逻辑不要一缺配置就把进程打死至少要把缺什么配置打出来方便排查。4.6 镜像构建时埋下的隐性炸弹最后一类比较“阴间”问题不在应用而在镜像构建阶段。比如 Dockerfile 里用了ADD或COPY把宿主机的文件拷进去了但宿主机的文件和镜像架构不匹配再比如基础镜像本身是个奇怪的精简版缺一些基本工具和动态库还有构建时使用了错误的ENTRYPOINT写法导致每次启动都会去执行一个不存在的命令。有一个特别经典的案例Dockerfile 里ENTRYPOINT写成了 JSON 数组格式[/entrypoint.sh]但entrypoint.sh没有执行权限或者没有 shebang。这种情况下容器启动时直接报permission denied或者exec format error。关键是这类错误不会给应用层面任何机会去写日志docker logs输出几乎为空。更隐蔽的是环境变量问题有些应用启动时需要读取配置文件里的路径变量而 Dockerfile 里通过ENV设置的值如果在运行时被外部-e参数覆盖成了空值就会导致应用找不到路径。这种问题定位起来特别折磨人因为配置看起来是好的容器也启动了但应用就是起不来。排查时建议把docker inspect输出里的Env字段拉出来仔细对一遍确认运行时实际生效的环境变量是不是符合预期。这个时候还有个特别实用的工具在本地先把镜像跑起来覆盖入口进容器里手动执行启动命令一点点排除。只要能把启动命令在交互模式下跑通问题就基本定位在环境变量、目录挂载或者服务发现上和镜像本身关系不大了。5. 进阶一点的排查手法换个角度看容器状态上面这些常规手段搞不定的话可以试试下面几个进阶思路。这些手法不一定每次都用到但遇到难缠问题时往往能打开局面。先用一个冷门命令看进程在容器内的真实行为docker top 容器名如果容器恰好在一个“还活着”的窗口期这个命令能看到容器内的进程列表和资源占用帮你判断进程到底有没有起来、PID 1 是哪个、子进程又是什么状态。有时候你会发现主进程是起来了但它不断 fork 子进程然后失败退出这说明问题已经不在启动阶段而是运行逻辑的 bug排查方向就完全不一样了。再一个方法临时关掉重启策略让容器停在退出的状态所谓“死透”方便你观察docker update --restartno 容器名或ID docker stop 容器名或ID docker start 容器名或ID这样容器如果还是秒退就不会再被自动拉起了。你可以直接查它的退出码、日志和状态。这个操作不影响容器配置的持久化后面重新设置回--restartalways就行。还有一招用docker diff看容器退出后文件系统相比镜像新增或修改了哪些文件。有时候应用的报错信息不是写到 stdout而是落在某个日志文件里比如/var/log/app.log。容器退了但你还能看到文件系统快照docker diff 容器名配合docker cp把日志文件拽出来看有时候能拿到应用层面写下来的关键报错信息比到处猜要省事得多。值得一提的是如果你的容器是由 compose 编排的还可以用docker compose logs直接看整个服务的日志聚合并且docker compose ps能一次看所有服务的状态。多个服务一起出现类似问题时往往意味着宿主机层面有公共的东西出了问题比如端口被占、网桥异常、磁盘满等等。6. 写在最后的几个日常习惯能帮你少踩一半的坑排查归排查真有问题的项目提前养成几个小习惯能让你少走很多弯路。第一个习惯镜像里写启动脚本时务必用exec启动进程并且脚本开头加上set -e和set -x。前者保证任何一步出错就退出不会带着半死不活的状态硬撑后者会把执行过程打到日志里方便排查。第二个习惯启动容器时额外挂一个调试用的目录把应用日志落盘。很多容器镜像默认只把日志写到 stdout遇到秒退问题日志还没 flush 就被杀掉了。如果你把应用日志写到挂载出来的持久化目录里即使容器秒退日志也留得住。做法是在 docker-compose 里加一个volumes映射然后应用配置里把日志路径指到挂载目录。第三个习惯资源限制一定要尽早设。--memory和--cpus别等上线了再补开发阶段就加上。这样很多资源相关的问题在开发期就会暴露而不是等到生产环境才炸出来。而且早发现早调整应用的内存基线也更容易摸清楚。第四个习惯不要轻易把整个宿主机交给一堆--restartalways的容器。这个策略是方便但也会掩盖问题。最好针对核心服务单独配置且启动前必须有健康检查兜底。像数据库这类有状态服务重启策略更要谨慎避免反复重启把数据搞坏。我个人的经验是Restarting (1)这个状态本身并不可怕可怕的是它背后的信息没有被正确解读。把退出码、日志、inspect 信息、events 事件串起来分析绝大多数容器秒退的问题都能在十几分钟内定位出来。最后再分享一个小技巧每次排查完这类问题随手把退出码和对应的根因记到一个笔记里。积累半年之后你就会发现大部分容器启动问题都是有规律可循的而且你的排查速度会越来越快。
返回列表