先讲一个真实的夜班经历。凌晨一点半告警群里有人喊“订单服务挂了”我打开监控一看容器还处于Running状态端口也在监听但接口返回全部超时。Docker没挂、进程没退出、端口还活着这就是运维最怕遇到的状况系统不是宕机而是“假死”。我做了几年运维越来越觉得真正值钱的不是背过多少命令而是遇到问题时脑子里有没有一张排障地图。市面上写Docker、K8s、CI/CD的资料很多但大多按“知识点”组织看完能懂概念到了现场却不知道先敲哪条命令。这篇文章我不打算做百科全书而是把命令、Docker容器治理、Dockerfile构建、CI/CD流水线、脚本化、K8s、监控告警和故障排查这些点串成一条线所有内容都是我在服务器和集群上实际验证过的。如果你是刚转行做运维的新人或者正在用Docker部署自己的服务再或者被K8s和CI/CD搞得一头雾水的开发者这篇手册应该能给你一条清晰的落地路径。1. 你不是记不住命令是缺少一张排障地图我面试运维候选人时经常问一个问题某台服务器负载很高你第一步做什么不少人的答案是“先看top”这个回答没错但大多数人看了top之后就走不动了。原因很简单他们只是“会敲命令”但不知道每条命令输出背后的下一跳在哪里。1.1 负载高的排查思路先分清是CPU、内存还是IOLinux的负载值load average经常误导人。它统计的是处于运行态和不可中断睡眠态的进程数所以负载高未必是CPU不够可能是大量进程卡在IO等待上。我会这样走先执行uptime看负载趋势再执行vmstat 1 5看一列关键数字。命令关键列说明下一步动作uptimeload average负载有1/5/15分钟三个值看趋势是上升还是下降vmstat 1 5r、b、wa、si/sor是运行队列b是不可中断睡眠wa是IO等待si/so是换页判断瓶颈方向top%CPU、%MEM看哪个进程消耗资源记录PID深入分析iostat -x 1util、awaitutil接近100%说明磁盘饱和定位是哪个进程在读写free -havailable看可用内存不是总内存分析缓存与真实占用如果确认是CPU问题按这条线路继续top找到高CPU的PID然后ps -Lp PID列出该进程的所有线程找到CPU占用最高的线程号转成十六进制后用jstack PID | grep -A 20 nid0x十六进制线程号去抓Java线程栈。这个操作对Java应用尤其好用能直接告诉你代码卡在哪个方法。如果是容器环境还要注意你在宿主机上看到的PID是宿主机视角的需要先找到容器进程对应的宿主机PID再执行nsenter -t PID -n -m进入容器或直接针对进程分析。如果瓶颈在内存不要只盯free里的used字段。Linux会把空闲内存用作page cache所以更该看available。真正需要关注的是持续换页swappingvmstat里si/so长期不为0说明内存不够用了。如果瓶颈在磁盘iostat -x 1里util接近100%、await飙升基本可以确认是IO饱和。这种时候用lsof f -- PID或strace -p PID去看进程到底在操作哪些文件往往能抓到问题根源。1.2 磁盘满了但du找不到大文件的经典陷阱这类问题在日志服务里非常常见。现象是df -h显示根分区用了100%但你执行du -sh /var、du -sh /home怎么看都找不到那个把空间吃满的大文件。原因很简单某个进程打开了一个日志文件你用rm删掉了这个文件文件名从目录里消失了但进程的文件句柄还握着它磁盘空间也就一直不释放。等你用du顺着目录去数自然找不到。遇到这个情况直接在宿主机执行lsof | grep deleted系统会列出所有“已被删除但还被进程打开”的文件后面那列就会显示文件大小。解决办法是先确认是哪个业务进程然后重启进程或者用 /proc/PID/fd/文件描述符编号把这个文件清空。如果是对外提供服务的进程重启前记得走优雅下线别直接kill。1.3 网络排查别只知道ping和telnet很多新运维排查网络问题只会ping IP和telnet IP 端口这两个命令能测通不通但说不清为什么不通。我更常用ss -tnp它能看到当前机器的所有TCP连接状态、本地端口、对端地址以及对应的进程。遇到“端口起没起”的问题ss -lntp | grep 8080一条命令就能定位。如果发现大量TIME_WAIT连接先别急着调内核参数。TIME_WAIT多是短连接频繁建立关闭的结果sysctl里的net.ipv4.tcp_tw_reuse和tcp_fin_timeout可以缓解但真正的解法是在应用层用连接池让连接复用起来。调内核参数只是把症状往后推了推。还有一个容器场景要注意在容器里执行ss或nslookup经常提示命令不存在因为很多基础镜像为了控制体积不会装网络工具包。这种时候不用急着apt-get install在宿主机上执行nsenter -t 容器PID -n直接进入容器的网络命名空间宿主机的网络排查工具就都能用了。2. Docker安装与容器治理先把环境搞稳再说编排Docker本身不复杂但很多人还没开始用容器就卡在安装这一关。我见过最多的问题集中在两块服务起不来、容器跑一段时间后被OOM杀死。2.1 安装Docker最容易翻车的几个细节CentOS类系统安装Docker社区版我习惯这样操作sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install -y docker-ce docker-ce-cli containerd.io sudo systemctl enable --now dockerUbuntu类的系统官方仓库里的版本可能偏老我一般用官方安装脚本但要提醒一句执行curl -fsSL https://get.docker.com | sh这类脚本前务必要确认下载来源可信。装完以后用docker version验证如果Client和Server都显示出来了Docker服务才算真正起来了。有人在Windows上装Docker Desktop启动时报virtualization support not detected。这个报错本质上不是Docker的问题是Windows虚拟化层没准备好。解决办法分三步走第一进BIOS确认CPU虚拟化已经开启第二在“启用或关闭Windows功能”里勾选“虚拟机平台”和“Hyper-V”第三如果之前手动关过Hyper-V在管理员命令行里执行bcdedit /set hypervisorlaunchtype auto然后重启。如果公司电脑被安全策略锁死了这些选项那就要找负责终端管控的同事处理不要自己去改组策略。2.2 用Redis主从和MySQL 8.0把容器生命周期串起来我学习容器的建议是不要光跑一个docker run hello-world就觉得自己会了找一个有状态服务跑起来才能真正理解数据卷、网络和端口映射。以Redis主从为例。先建一个自定义网络让容器之间可以通过容器名互相访问docker network create redis-net docker run -d --name redis-master -p 6379:6379 --network redis-net redis:7然后启动一个从节点和主节点挂在同一个网络中并在启动参数里指定主从关系docker run -d --name redis-replica -p 6380:6379 --network redis-net redis:7 redis-server --replicaof redis-master 6379验证主从状态docker exec redis-master redis-cli info replication docker exec redis-replica redis-cli info replication这里有个细节Redis 5.0以上版本命令从--slaveof改成了--replicaof很多老教程还是旧写法在Redis 7容器里会直接报错。MySQL 8.0部署也值得完整跑一遍关键在于数据卷挂载和初始化参数docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDYourPassword \ -v /data/mysql:/var/lib/mysql \ mysql:8.0 \ --character-set-serverutf8mb4 \ --collation-serverutf8mb4_unicode_ci把数据目录挂载到宿主机的/data/mysql这一点必须养成习惯。容器的生命周期是短暂的今天可以docker stop明天可能因为宿主机故障整个容器被重建如果数据都写在容器可写层里删掉容器就等于删掉数据。我见过不止一次有人数据库容器运行了大半年某天手滑执行了docker rm然后来找我问能不能恢复数据只能告诉他们如果做了数据卷挂载还能救援没做的话基本无解。2.3 资源限制和日志治理是生产环境的分水岭很多人在本地跑一个docker run -d就把服务丢生产了这是非常危险的习惯。一个进程出现内存泄漏把宿主机拖挂的场景我在生产环境见过太多次。给容器加资源限制用这几个参数docker run -d \ --name app \ --memory2g \ --cpus2 \ --pids-limit200 \ app:latest--memory2g限制内存上限--cpus2限制CPU核数--pids-limit200限制进程数。加了内存限制后容器内存超限会触发OOM可以通过docker inspect -f {{.State.OOMKilled}} app快速判断容器是不是被OOM杀死的。日志治理更是我反复强调的点。默认情况下Docker会把容器日志以JSON格式存到宿主机单日志文件不断增长长期不清理会把磁盘撑爆。启动容器时加日志轮转参数docker run -d --log-opt max-size50m --log-opt max-file5 app:latest把单个日志文件限制在50MB、最多保留5个文件。另外我在Dockerfile里会刻意让应用把日志输出到标准输出而不是写文件这样日志路由统一交给Docker daemon管理排查问题也更方便。3. Dockerfile打磨每一条指令都在为镜像的“层”负责新手写Dockerfile最常见的状态是“能构建、能运行、跑几天出各种诡异问题”。其实Dockerfile的写法直接影响镜像体积、构建速度和运行安全性。3.1 先理解镜像层的缓存机制Docker镜像是一层一层堆叠的Dockerfile里的FROM、RUN、COPY这些指令每条会生成一个新层。构建时如果某一行指令和上次完全一致Docker会直接复用缓存不再重新执行。这个机制带来一条核心编写原则把不容易变化的指令写在前面把经常变动的指令写在后面。以Python项目为例FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . .很多人习惯直接COPY . .再执行pip install结果每次改一行代码所有依赖都要重新安装一遍。先拷贝requirements.txt并安装依赖再拷贝业务代码源码变更时只会重建最后几层构建速度会明显提升。3.2 多阶段构建让镜像既小又安全多阶段构建的精髓是在一个阶段里编译在另一个阶段里运行最终镜像里只保留运行所需的最小内容。以Java应用为例。如果没有多阶段构建你得找一个同时包含JDK和Maven的大镜像或者把打好的jar包在宿主机上单独构建再拷进去。多阶段构建这样写FROM maven:3.8-jdk-11 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:11-jre RUN useradd -r appuser WORKDIR /app COPY --frombuilder /build/target/app.jar app.jar USER appuser EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]第一阶段用Maven镜像完成依赖下载和打包第二阶段用JRE镜像运行再从第一阶段把编译好的jar包复制过来。最终镜像只有JRE和jar包不会有Maven、源码、编译中间文件攻击面也小了很多。用非root用户运行应用是必须养成的习惯否则容器拿到宿主机的写权限后一旦有漏洞被利用影响范围会很大。选择基础镜像时也要留个心眼。alpine系列体积最小但它是musl libc有些依赖了glibc特性的程序会跑不起来。涉及JDK、Node原生模块这类场景我更倾向于用debian:*-slim或ubuntu系列。3.3 构建失败排查的常见思路构建失败最常遇到的几类问题第一是容器内网络不通apt-get或pip install超时。解决思路是给构建过程配置一台能稳定访问的镜像源或者在系统级配置HTTP代理不要在Dockerfile里写死某个外部地址。第二是构建缓存导致代码没更新。明明改了代码重新构建出来的镜像还是旧版本。这种情况我在CI流水线里遇到过很多次原因就是某一行COPY的缓存被错误复用了。验证代码是否正确打进了镜像直接加--no-cache重新构建一次能复现问题就说明缓存命中机制出了问题。第三是COPY时把不该进镜像的文件带进去了比如.git目录、node_modules、target目录让构建上下文变得巨大。解法是在项目根目录维护一份.dockerignore把不需要的文件排除掉。第四是时区问题。容器默认时区是UTC运行出来的日志时间和本地时间差8小时。在Dockerfile里通过环境变量指定时区ENV TZAsia/Shanghai有些基础镜像不包含timezone数据还需要额外安装tzdata并软链时区文件这一点在写Dockerfile时就要考虑进去别等服务上线了才发现日志时间对不上。4. CI/CD流水线先把构建产物做对再谈自动化我见过很多团队折腾CI/CD最后做成了“为了自动化而自动化”。一套价值很低的流水线通常是这样的代码提交后跑一遍编译编译通过就算完后面所有部署还是靠人肉操作。这种流水线没有解决核心问题——发布过程不可控。4.1 流水线设计要从“最终产物”倒推不管是单体应用还是微服务CI/CD的核心是把“代码提交”变成“可部署产物”并且让部署过程可以随时重复、随时回滚。我建议你从最终一步倒推线上跑的是什么如果是基于Docker容器那么流水线必须产出镜像是够的需要处理镜像仓库、版本标签、部署更新。只有一台测试机的时候可以用最简单的方案GitLab CI构建镜像把镜像推到私有仓库然后SSH到目标服务器上执行docker pull和docker compose up -d来完成更新。等规模大了、服务多了再让流水线对接K8s用kubectl set image实现滚动更新。4.2 一套能直接落地的GitLab CI配置GitLab CI需要先有GitLab Runner。Runner装好以后执行gitlab-runner register把GitLab仓库地址和注册令牌填进去executor选择docker。选择docker executor后每次CI任务会从仓库拉取一个指定的Docker镜像在这个独立容器里执行任务环境隔离性会好很多。然后写.gitlab-ci.yml。这里给一个参考版本stages: - build - deploy variables: IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA build: stage: build image: docker:24.0 services: - docker:24.0-dind script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -t $IMAGE_NAME . - docker push $IMAGE_NAME only: - main deploy: stage: deploy image: bitnami/kubectl:latest script: - kubectl set image deployment/app app$IMAGE_NAME -n demo - kubectl rollout status deployment/app -n demo only: - main流程非常直接代码推到main分支后流水线先构建镜像并推送然后通过kubectl命令更新K8s里Deployment的镜像版本再rollout status等待滚动更新完成。如果你还在用docker-compose管理服务deploy阶段可以把image换成docker:24.0用SSH过去执行更新命令思路完全一样。4.3 若依这类前后端分离项目的流水线思路很多朋友照着若依开源的教程部署过单体或微服务版本部署过程中踩的坑大多集中在“前后端要怎么打成一个镜像”。前端是静态资源最终由Nginx托管后端是Spring Boot应用最终是一个JVM进程。这两个东西理应构建成两个镜像。后端的流水线以Maven打包为主生成jar包后用多阶段Dockerfile构建镜像前端的流水线执行npm install和npm run build生成dist目录再打包成Nginx镜像。Nginx镜像里把dist目录放到/usr/share/nginx/html同时挂载一份Nginx配置指向后端服务的入口地址。这里的关键思路是运行配置和镜像分离。数据库连接地址、Redis地址、JWT密钥这些配置不应该打进镜像里而是通过环境变量在运行时注入。这样同一份镜像可以在测试环境、预发环境、生产环境之间原样流转只是环境变量的值不同。这也是为什么K8s里会有ConfigMap和Secret这两个对象它们的价值就是把“可变配置”从“不可变镜像”中剥离出来。5. 脚本化运维把重复动作固化成工具但别过度设计命令是工具脚本是把工具组合成流水线。运维场景里大量操作是重复的健康检查、日志备份、磁盘清理、配置变更。这些工作写成脚本能显著提升效率但脚本化要有一个边界——能做常规检查和自动化恢复不要试图用脚本去解决所有复杂状态。5.1 先用Shell写一个健康检查脚本我写脚本有个基本要求执行后必须有清晰输出失败时要能定位到原因。一个最简单的HTTP服务健康检查可以这样写#!/bin/bash set -euo pipefail URLhttp://127.0.0.1:8080/health LOG/var/log/health_check.log STATUS$(curl -o /dev/null -s -w %{http_code} --connect-timeout 5 --max-time 10 $URL || true) if [ $STATUS 200 ]; then echo $(date %Y-%m-%d %H:%M:%S) health check ok $LOG exit 0 else echo $(date %Y-%m-%d %H:%M:%S) health check failed, status$STATUS $LOG # 这里可以发通知比如通过webhook调一个内部消息接口 exit 1 fiset -euo pipefail这三行不是仪式感而是让脚本在变量未定义、命令失败、管道中间环节出错时及时退出避免一个坏状态被后续命令掩盖。所有判断都要输出日志因为脚本一旦进入crontab调度你就得靠日志知道它到底跑没跑、跑得怎样。5.2 Python巡检脚本用psutil把系统状态汇总成报表Shell适合处理文本和命令但涉及复杂逻辑、数据汇总时Python的psutil库更好用。下面是一个简单的巡检脚本片段import psutil cpu psutil.cpu_percent(interval1) mem psutil.virtual_memory() disk psutil.disk_usage(/) print(fCPU: {cpu}%) print(f内存: 总 {mem.total / 1024**3:.1f}G, 可用 {mem.available / 1024**3:.1f}G, 使用率 {mem.percent}%) print(f根分区: 总 {disk.total / 1024**3:.1f}G, 使用率 {disk.percent}%)配合cron每天跑一次把输出结果推送或者生成报告这比每天手动登录每台机器敲free、df要高效得多。但要注意巡检脚本监控的是“瞬时状态”要发现趋势性变化必须有历史数据和可视化这部分应该交给监控系统而不是脚本本身。5.3 备份脚本别等数据丢了才想起它备份是运维最枯燥也最重要的活儿。MySQL备份我一般用mysqldump脚本里最关键的三件事备份文件命名带日期、保留最近N份、备份完成要校验文件非空。#!/bin/bash set -euo pipefail BACKUP_DIR/backup/mysql KEEP_DAYS7 mysqldump -h127.0.0.1 -uroot -p$DB_PASS --single-transaction --routines myapp $BACKUP_DIR/myapp_$(date %F).sql find $BACKUP_DIR -name myapp_*.sql -mtime $KEEP_DAYS -delete if [ -s $BACKUP_DIR/myapp_$(date %F).sql ]; then logger mysql backup ok else logger mysql backup failed: file empty exit 1 fi这个脚本再进阶一点把备份文件用gzip压缩、上传到对象存储或另一台服务器就形成了异地备份的雏形。防勒索病毒的一个有效手段就是备份系统与生产系统隔离。5.4 Node.js这种技术栈在运维场景里的定位有些人会问运维为什么会用Node.js这种后端语言我的看法是如果团队里前端工程师要兼顾运维工具开发Node.js是效率很高的选择。比如写一个内部运维小面板用Express起一个HTTP服务前端页面调用它提供的接口再通过SSH或K8s API去执行运维动作。相比PythonNode.js的优势在于前后端同构前后端同学都能上手劣势在于生态里的运维类库不如Python丰富。选型没有绝对标准团队里谁会、哪个生态适合当前场景这才是决定因素。脚本化运维要掌握一个度脚本做好“输入、判断、输出”就够了复杂的状态管理、重试策略、告警通知应该交给专业的监控和编排系统。6. K8s从能用到会用搭集群、看状态、管发布K8s是当前容器编排的事实标准但很多人第一次接触它就被一堆概念劝退。我的建议是不要从概念啃起先搭一个单节点集群把Pod、Deployment、Service这几个核心对象跑起来再回头理解原理会顺很多。6.1 单节点K8s搭建的关键步骤用kubeadm搭单节点集群几个关键步骤不能漏# 准备阶段需要关闭swap sudo swapoff -a # 加载内核模块 cat EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter # kubeadm init 初始化控制平面 sudo kubeadm init --apiserver-advertise-address192.168.1.10 --pod-network-cidr10.244.0.0/16初始化成功后kubeadm会输出一段kubeadm join命令用于后续添加节点。单节点集群默认不会让Pod调度到Master上但测试环境通常只有一个节点需要执行kubectl taint nodes --all node-role.kubernetes.io/control-plane-把控制平面节点的这个污点去掉Pod才能真正跑起来。CNI插件我一般用Calico它负责Pod之间的网络通信。这一步漏掉的话集群可能显示Ready但Pod的网络一直创建不出来。6.2 常用命令和“声明式”思维K8s和传统运维最大的区别是思维模式的转变。传统运维写命令“告诉系统做什么”K8s用户写YAML“描述期望状态”由控制器不断把实际状态调整到期望状态。核心对象和命令对应关系对象作用常用命令示例Pod最小调度单元kubectl get pods、kubectl logs、kubectl execDeployment声明Pod副本数并支持滚动更新kubectl apply -f app.yaml、kubectl rollout restartService提供稳定的访问入口kubectl get svc、kubectl port-forwardConfigMap保存非敏感配置kubectl create configmapSecret保存敏感配置kubectl create secret genericIngress从集群外访问服务kubectl get ingress排障最常组合的命令是kubectl get pods看到Pod状态异常然后kubectl describe pod pod名看Events事件再kubectl logs pod名看应用日志。讲个规律Pod一直是Pending通常是资源不足或调度器不满足条件Pod一直是CrashLoopBackOff大概率是启动命令报错、镜像不存在、启动就退出Pod状态正常但服务不通优先查Service和Endpoint是否对应得上。自愈机制是K8s最有价值的特性之一。Deployment控制ReplicaSetReplicaSet保证Pod副本数始终等于期望值。只要Pod异常退出控制器就会重建。想让这种自愈真正可靠必须在YAML里配置livenessProbe和readinessProbe。前者决定容器是否要重启后者决定Pod是否加入Service的负载均衡。这两个探针配错了服务会陷入无限重启或者流量被打到未就绪的Pod上。6.3 多Master高可用和原生管理页面单节点集群只能用于学习和小规模测试。生产环境至少要三个Master节点以保证控制平面高可用。搭建方案里kubekey对很多团队来说是更省心的选择它内置了etcd、负载均衡组件能一键创建高可用集群。核心设计是etcd奇数节点保证仲裁控制平面API Server通过负载均衡统一入口一个Master挂了请求会切到其他Master不会中断。想给团队提供一个可视化管理界面可以装Kuboard这一类开源面板也可以用K8s官方Dashboard。管理界面的意义在于团队里不是所有人都会用命令行把“看状态”这个高频操作交给UI能把运维人员从重复咨询中解放出来。7. 监控与告警先有指标再有告警后有自愈监控做得不好运维就只能当救火队员。我理解的监控体系分三层底层采集基础设施指标中间采集容器和集群指标上层采集业务应用指标。没有哪一层可以偏废。7.1 指标设计从“系统会不会挂”倒推搭建监控之前先想清楚要回答哪些问题宿主机CPU、内存、磁盘还剩多少能撑多久容器是否在反复重启重启频率如何K8s节点是否ReadyPod是否达到期望副本数应用接口的请求量、耗时、错误率有没有异常把这些维度梳理成指标后再选工具和采集目标。Prometheus是这个领域的事实标准配合Grafana做可视化再配Alertmanager做告警。7.2 Prometheus、Grafana和采集器一次跑起来我习惯用docker-compose把监控栈拉起来Prometheus的抓取目标配置如下scrape_configs: - job_name: node static_configs: - targets: [node-exporter:9100] - job_name: cadvisor static_configs: - targets: [cadvisor:8080] - job_name: kube-state-metrics static_configs: - targets: [kube-state-metrics:8080]node-exporter采集宿主机的CPU、内存、磁盘、网络指标cadvisor采集容器级别的指标kube-state-metrics采集K8s对象的状态指标。三个采集器覆盖了基础设施层、容器层和集群层。部署完成后在Grafana里添加Prometheus数据源导入官方Dashboard模板就能看到成体系的监控面板。很多团队到这里就停了觉得“有图就行”但图不会主动告诉你出事了必须配告警。7.3 告警规则怎么定才不吵人告警规则定得太松故障没人管定得太紧天天被无关告警轰炸最后人都麻木了。我比较常用的几条告警规则规则条件说明磁盘空间使用率大于80%持续10分钟预留处理时间不是等到满才告警节点状态Node变为NotReady持续1分钟属于严重告警Pod重启某Pod重启次数超过5次/小时可能是CrashLoop的前兆接口错误率错误率大于5%持续5分钟业务层告警Alertmanager配置里告警通过webhook推送到即时通讯工具值班人员能第一时间收到。但告警只是第一步更高级的目标是自愈比如监测到Pod多次重启自动触发kubectl rollout restart监测到磁盘紧张自动清理历史日志。自愈机制一定要加“熔断”限定执行次数和人工确认环节否则自动化操作本身也会成为新故障源。8. 一次容器服务假死的完整排查复盘最后放一个完整案例把前面所有章节串起来。那是周三下午客服反馈订单系统页面加载不出来持续了大概5分钟。当时监控面板上CPU不高、内存不高容器状态是Running端口也通但接口全部超时。第一反应是“应用卡住了”真正查下来才发现问题比想象中复杂。8.1 逐步缩小范围的过程我先登录宿主机执行docker stats看了一圈容器资源占用发现订单服务容器的内存使用率已经达到上限但进程没被OOM杀掉。再执行free -h发现宿主机物理内存还够但/var/lib/docker目录的磁盘占用率已经到了100%。这个时候我意识到“容器还Running只是假象”——容器里的进程还在但当它尝试写日志、写临时文件、更新状态时每一次磁盘写入都在超时重试导致大量请求被卡住。用df -h确认根分区已满用lsof | grep deleted查看是否存在占用文件句柄的进程再用docker system df查看Docker自身的空间分布最后发现是旧镜像和构建缓存占了大约40GB。8.2 处理过程与复盘结论处理动作分两步先执行docker system prune -a --volumes清理未使用的镜像、容器和缓存数据把磁盘占用量降下来服务在几分钟内自动恢复了随后再排查为什么磁盘会满发现是构建流水线在每次提交后都会产生新镜像但旧镜像没有清理策略日积月累把磁盘填满。这个案例很好地解释了监控的一个盲区容器状态是Running不代表服务健康磁盘使用率这种“最不起眼的基础指标”往往才是致命问题的源头。我后来给所有宿主机加了一条磁盘告警使用率超过80%就通知并把构建流水线接入了镜像清理任务。过了两周同一类问题再也没出现过。8.3 这类排障的通用路线图把这个案例拆解成方法通用排查路线是这样的先看监控确认故障范围和持续时间再登录宿主机看Docker资源和系统资源用docker inspect查看容器状态细节用df、lsof这类命令查宿主机层面的隐性问题最后回归业务日志确认恢复情况。整个过程的关键是不要被表面状态迷惑要把“容器Running”和“服务健康”完全区分开。我在实际工作中还有一个小习惯每次处理完一个线上问题都会把“现象、排查过程、根因、修复动作、后续防范”整理成一段文字放到团队的运维知识库里。时间长了这本手册就成了团队最值钱的知识资产。真正决定运维水平的不是记住多少现成答案而是面对一个陌生故障时能否有条不紊地把问题层层拆开。