1. 命令与脚本:运维的基本功
1.1 高频命令的真实用法
很多人把“Linux常用命令”当成面试题背,但实际上运维日常用到的命令就那几十条,关键在于用得够不够“透”。我见过不少刚转运维的朋友,遇到环境问题第一反应是去查监控面板,或者在网上搜图形化工具,结果绕了一大圈,不如一条命令在现场看得清楚。
以最基础的排查场景来说,端口占用、进程状态、磁盘空间、大文件定位,这四个问题几乎每周都会遇到。查端口占用不要再用netstat了,虽然它还在,但ss的输出更快、信息更全,一条ss -lntp能看到监听端口和对应进程。查进程用ps -ef配合grep,但更建议用pgrep -f按完整命令行匹配,避免进程名太短误杀。磁盘满了不要急着删,先df -h看挂载点,再用du -sh /目录/* | sort -rh | head逐层往下找,把“哪个目录吃了磁盘”这件事一次性定位清楚。
日志排查也有讲究。tail -f跟踪日志是最常用的,但如果是系统刚启动就异常,要看journalctl -u 服务名 --no-pager -n 200,比直接翻文件方便得多,尤其是使用systemd管理的服务。想知道某段时间内有没有报错,可以journalctl --since "1 hour ago" -p err,这比肉眼翻几百行日志靠谱得多。
另外还有一个容易被忽略的细节:用到管道的时候,要注意前后命令的退出码。比如grep没匹配到任何内容,默认返回非0,如果用在脚本里就会导致脚本提前退出。类似这种“命令没报错,但结果不对”的情况,恰恰是运维脚本里最常见的坑。
1.2 脚本化思维:把重复劳动交给定时任务
命令是砖,脚本才是墙。运维日常的备份、日志清理、健康检查、状态上报,几乎都可以脚本化。很多人写脚本是“一次性”的:临时解决问题,用完就丢,下次遇到再手敲一遍。这种习惯非常消耗精力,我个人的建议是:只要同一个操作用了两次以上,就值得整理成脚本放进工具库。
写脚本前先想清楚三件事:一是什么时候运行,二是在哪里运行,三是失败时怎么办。以日志清理为例,生产环境日志增长快,如果不及时清理,磁盘很容易被打满。一个简单的清理脚本,核心逻辑就是“按天统计、按保留天数删除”,但有几个容易踩的坑要注意:路径不能用相对路径,因为cron执行时的环境变量和手动执行不一样;删除操作前要提前做好目录校验,避免路径写错导致误删;脚本输出一定要落到日志文件里,这样出问题时能查证。
我这边常用的备份脚本结构比较简单,大概就是这样:
#!/bin/bash set -euo pipefail BACKUP_DIR="/data/backup" SOURCE_DIR="/var/lib/mysql" RETENTION_DAYS=7 LOG_FILE="/var/log/backup.log" if [ ! -d "$SOURCE_DIR" ]; then echo "$(date '+%F %T') ERROR: source dir not found" >> "$LOG_FILE" exit 1 fi tar czf "$BACKUP_DIR/backup_$(date +%F_%H%M%S).tar.gz" -C "$SOURCE_DIR" . find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete echo "$(date '+%F %T') backup finished" >> "$LOG_FILE"set -euo pipefail这三行加粗,如果你不熟悉一定要弄明白:-e表示遇到错误退出,-u表示变量未定义时退出,pipefail表示管道中任何一条命令失败都会让整条管道返回失败。这三条组合起来能避免很多低级失误。
真正干了几年级别之后,你会慢慢地开始形成一套自己的脚本模板,写出来的东西会越来越好用。脚本不用炫技,逻辑清晰、参数明确、失败时能给出有效提示才是关键。
2. Docker与Dockerfile:镜像与容器的日常
2.1 容器管理的基础操作与核心概念
Docker是目前运维绕不开的基础工具。它在工作场景中的价值不用多说——构建一次,到处运行,让环境一致性大幅度提升。但实际用起来,很多人卡在了概念上:镜像、容器、数据卷、网络,这四者关系是什么,命令怎么组合才合理,出了问题怎么定位。
我先用大白话把几个核心概念捋一遍。镜像是模板,相当于装系统时的ISO文件;容器是镜像运行后的实例,相当于装好系统并正在运行的机器;数据卷是独立的存储空间,容器删了它还在;网络则决定了容器之间能不能互相通信、能不能被外部访问。这个三角关系一旦清楚了,Docker的大部分命令就顺了。
日常运维中,容器的启停和日志是最常见的操作。启动一个容器时,无论用docker run还是docker compose up,都要提前考虑三个问题:数据是否要持久化,端口要怎么映射,容器退出后要不要自动重启。数据持久化用-v挂载,日志和配置文件建议都挂在宿主机上;端口映射用-p,但要注意避免端口冲突;自动重启策略用--restart=unless-stopped,这个策略比always更实用,因为手动停止的容器不会被强制拉起来。
容器网络那边,bridge网络下容器之间可以通过服务名互相访问,前提是它们在同一个docker network里。跨主机通信则推荐用overlay网络,搭配K8s或Docker Swarm一起用。如果只是单机小项目,桥接网络就足够了,千万别为了追求“所谓的高端方案”把网络搞得过于复杂,维护成本会成倍增加。
2.2 Dockerfile的正确打开方式
Dockerfile,表面上看是一个“写构建指令的文件”,但实际使用中它决定了镜像的体积、构建速度和运行安全性。我刚接触Dockerfile时,总觉得只要把环境配好、服务跑起来就行,后来发现这样写出来的镜像个个都像“老古董”,体积大、层级多、漏洞隐患也多。
先说Dockerfile的基本指令。FROM定基础镜像,WORKDIR定工作目录,COPY拷文件,RUN执行构建时的命令,EXPOSE声明端口,CMD或ENTRYPOINT指定容器启动时的进程。这些指令里,最容易被忽略的是ENTRYPOINT和CMD的区别:ENTRYPOINT定义的是容器的主程序,CMD提供默认参数,两者配合使用时,docker run后面追加的参数会覆盖CMD但不会覆盖ENTRYPOINT。按我的实践经验,大多数服务用ENTRYPOINT写启动命令、CMD留默认参数,是最不容易出错的组合。
举个例子,一个Java服务的Dockerfile可以这么写:
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 openjdk:11-jre-slim WORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]这段多阶段构建其实学到了精髓:第一段负责编译,第二段只保留运行环境,最终镜像里没有源码、没有Maven,体积能小一半以上。所有容器镜像的构建都应该朝着“只装运行所需、不装编译工具”的方向靠。
还有个关键文件叫.dockerignore。很多人没写它,导致COPY . /app的时候,把本地的.git、target、node_modules全打进去了,镜像体积瞬间膨胀,构建速度也慢得离谱。正确的做法是提前声明忽略项,像这样:
.git node_modules target logs *.md这样构建上下文会小很多,同时也能避免把宿主机上的本地配置或密钥文件带到镜像里。安全底线:镜像里永远不要出现私钥、API令牌、数据库密码这些敏感信息,这些应该通过环境变量、配置中心或者K8s Secret注入。
2.3 Compose编排与Docker Desktop安装中的经典坑
单容器解决不了微服务的问题,于是有了Docker Compose。它用YAML文件把多个容器的启动参数集中管理,一条docker-compose up -d就能把整套环境拉起来。Compose文件里最常用的配置是services、networks、volumes,这三个块可以对应到传统的启动参数。
以Redis主从为例,很多运维人员都在本地环境或测试环境搭过这套。用Compose写主从结构,会比直接敲三四条docker run命令清晰得多:
version: "3" services: redis-master: image: redis:7 container_name: redis-master restart: unless-stopped ports: - "6379:6379" command: redis-server --appendonly yes redis-slave: image: redis:7 container_name: redis-slave restart: unless-stopped depends_on: - redis-master ports: - "6380:6379" command: redis-server --slaveof redis-master 6379这个Compose文件里的depends_on要注意,它只控制启动顺序,不保证服务内部已经就绪。如果主库还没完全拉起,从库连接失败,可能在启动日志里出现重试报错。更稳妥的做法是在应用启动脚本里加等待和重试逻辑。
另外,很多人装Docker Desktop会遇到Virtualization support not detected的报错,在Windows上尤其多见。这个问题的本质是宿主机没有开启硬件虚拟化或Hyper-V功能。排查顺序建议是:先确认CPU虚拟化在BIOS里已经开启(Intel VT-x或AMD-V),再去“启用或关闭Windows功能”里把Hyper-V和Windows虚拟机监控程序平台勾选上,最后打开任务管理器性能页确认“虚拟化”显示为“已启用”。装了第三方虚拟化软件或者旧版Android模拟器的机器,还可能和Hyper-V冲突,建议先卸载或停用再重试。这个坑我见过好几个同事踩,主要是看着报错很唬人,其实按步骤检查下来,绝大多数都是BIOS里那一项没开。
3. CI/CD:从代码到发布的自动化通道
3.1 一套零成本起步的CI/CD架构
CI/CD,中文叫持续集成和持续交付,简单讲就是让每一次代码提交都能自动触发构建、测试、打包、部署的过程。对小团队来说,最务实的方案其实是GitLab加GitLab Runner,因为GitLab本身自带CI/CD能力,不需要额外引入Jenkins,少维护一个系统就少一份负担。
架构上,一条完整的流水线长这样:开发push代码到GitLab,GitLab Runner检测到分支变化,拉取代码,在一个专门的执行环境(比如Docker容器)里跑测试和构建,构建成功后生成镜像并推送到镜像仓库,最后在目标服务器上执行部署命令。整个过程不需要任何人去手动敲命令。
结合标题里提到的“GitLab Docker Engine CI/CD”,有一个经验值得分享:Runner的executor类型,建议直接用docker,这样每个流水线任务都会在一个全新的容器里运行,环境干净且隔离。但这会带来一个常见问题——Runner容器内部没办法直接调用宿主机的docker命令,这时候有两种解法:一是挂载宿主机的docker.sock给Runner容器,让容器内docker CLI直接和宿主机docker daemon通信,二是用Docker in Docker方式在Runner容器内再起一个daemon。前者配置简单但存在安全隐患,后者隔离更好但镜像层会有额外损耗。团队刚起步时选前者效率最高,规模大了再上DinD也不迟。
before_script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"实际跑流水线时,这个登录步骤几乎是必须的,因为推镜像、拉私有大镜像都要先认证。用户名密码建议用CI/CD变量注入,别硬编码在.gitlab-ci.yml文件里,否则相当于把仓库的访问凭证拱手送出。
3.2 流水线文件设计与实际踩坑
.gitlab-ci.yml是整个CI/CD活动的核心文件,它跟项目代码放在一起,随着仓库版本一起演进。初次写这个文件的人,最容易犯的错是“一个stage里堆了太多事情”,测试、构建、推送、部署全塞一起,结果某一步失败很难定位。正确做法是拆成多个stage,每个stage有一个明确职责,失败后能快速看到是哪一段出了问题,而且后续的stage可以复用前面产出的内容。
一个基础的Java后端流水线大概是这样的:
stages: - test - build - deploy variables: MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository" cache: paths: - .m2/repository/ maven-test: stage: test image: maven:3.8-jdk-11 script: - mvn test docker-build: stage: build image: docker:20 services: - docker:20-dind script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA deploy-test: stage: deploy script: - ssh user@test-server "cd /opt/app && docker compose pull && docker compose up -d" only: - main这个方案跑通了之后,代码从提交到部署到测试环境,一般在三分钟以内。这里面的cache配置值得多说一句:它会把Maven依赖目录缓存下来,避免每次流水线都重新下载依赖,否则构建时间会成倍增加。另一个细节是部署时用了docker compose pull && docker compose up -d,这种做法的好处是先把新镜像拉下来再重启,能减少正在运行的服务因拉镜像失败而中断的概率。
踩坑最多的点在三个地方:一是Runner服务器的资源不足,构建时OOM导致任务失败,需要给Runner加上内存限制或者在Runner配置里降低并发数;二是services: docker:dind需要特权模式,有些自建Runner环境默认没开,导致docker命令报权限错误;三是分支和tag的触发条件没写对,导致每次提交都触发全量部署,生产环境被频繁重启。这些问题都是“跑几天才暴露”的,建议在新环境上先小流量试跑几条流水线,观察资源配置和日志,再放开全量。
4. K8s与容器编排:从单机到集群的跃迁
4.1 K8s核心概念与入门路径
K8s全称Kubernetes,它是容器编排领域的事实标准。有人问K8s和Docker到底什么关系,最简单的理解是:Docker解决了“单个容器怎么跑”的问题,K8s解决的是“一堆容器怎么调度、怎么互相发现、怎么扩容缩容、怎么保证高可用”的问题。换句话说,Docker是发动机,K8s是整车。
学习K8s建议先抓住四条主线:Pod、Deployment、Service、Ingress。Pod是K8s里最小的调度单位,一个Pod可以包含一个或多个容器,共享网络和存储卷;Deployment负责管理无状态应用的副本数、滚动更新和回滚;Service为一组Pod提供稳定的访问入口,相当于给随时可能“去世”的Pod发了一张“永不换号码”的名片;Ingress负责把集群外的HTTP请求路由到Service上。这四个概念串起来,就是一个请求从域名到Pod的完整链路。
部署方式上,单节点测试环境推荐用KubeKey、kubeadm或者minikube,图省事就装个发行版自带的K8s套件。生产环境至少三节点起步,越重要的系统越要关注高可用,比如控制平面需要三台Master,etcd数据也要有备份。我最开始只用单节点实验,等到跑真实业务时发现节点一挂,整个集群直接瘫痪,才老老实实把多Master的架构搭起来。基础概念的牢固程度,直接决定了后续排查故障的速度。
4.2 单节点到高可用:Master节点怎么保证稳
标题里有个搜索词“k8s三台master怎么保证高可用kubekey”,这个问题很实际。K8s的高可用,本质上分为两个层面:一是控制平面的高可用,二是业务工作负载的高可用。控制平面里的关键组件是etcd,它保存了整个集群的状态,而且用的是Raft算法保证数据一致性。Raft要求多数派存活才能继续工作,三台Master意味着最多只能坏一台,如果坏了两台,etcd就选不出领导者了,整个集群会进入只读状态甚至不可用。
三台Master的架构通常长这样:每台Master上部署kube-apiserver、kube-controller-manager、kube-scheduler、etcd,然后在前面放一个负载均衡器(比如HAProxy、Nginx或者云上的SLB),把kube-apiserver的6443端口统一暴露给Worker节点和外部客户端。Work节点上的kubelet和kube-proxy只连接这个负载均衡器地址,而不是某个具体Master的IP。这样即使某台Master宕机了,流量也会自动切换到健康的Master上。
我在实际搭建时,用KubeKey在离线环境部署过一套三主多从的集群,整个过程相对友好,版本兼容问题被官方工具处理了一部分。但有几个细节还是要重点检查:一是所有节点的hostname不能重复,且不能有大写字母;二是节点之间要能通过主机名互相解析,建议直接写hosts文件;三是内核和网络参数要提前配好,主要是net.ipv4.ip_forward、bridge-nf-call-iptables这些。如果你省略这些步骤,大概率会在集群初始化或Pod网络插件安装那一步失败,回头排查起来非常费时间。
4.3 应用迁到K8s的实操思路
把已有应用迁到K8s,很多人一上来就写一堆YAML,结果部署到一半发现配置错了、探针有问题、PVC没绑定,最后回滚也很难受。迁移的本质不是“把docker run命令翻译成YAML文件”,而是要把应用的工作方式改造成“能随时被杀、能随时重建、能水平扩展”的模式。
以“若依微服务不停机迁移”为例,这类应用通常包含多个服务,前端、后端、认证、网关等模块。迁移之前先盘一下现状:哪些服务是有状态的,哪些是无状态的,配置是写在文件里的还是环境变量里的,日志是写到磁盘还是标准输出。把这些理清楚之后再动手,往往事半功倍。
无状态服务写一个Deployment加Service就能搞定。下面这个配置就是非常典型的模板:
apiVersion: apps/v1 kind: Deployment metadata: name: app-backend namespace: production spec: replicas: 3 selector: matchLabels: app: backend template: metadata: labels: app: backend spec: containers: - name: backend image: registry.example.com/app-backend:1.0.0 ports: - containerPort: 8080 env: - name: DB_HOST value: mysql-service readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 15 periodSeconds: 5 resources: requests: cpu: 500m memory: 512Mi limits: cpu: "1" memory: 1Gi --- apiVersion: v1 kind: Service metadata: name: app-backend-service namespace: production spec: selector: app: backend ports: - port: 8080 targetPort: 8080这里面的readinessProbe特别重要,它决定Pod是否被纳入Service的负载均衡列表。如果探针没配,启动很慢的应用可能在还没就绪时就被转发请求,直接导致大量5xx错误。迁移时我会建议先接流量观察,不要一次性把全部副本切到新集群,可以用Ingress或者网关按比例灰度,逐步放量,直到确认稳定后再把旧环境缩容。
“不停机、不丢数据”的迁移,策略上要分步走:先把数据层同步好,再将应用层分批切换,最后在低峰期做一次流量切换和验证。整个过程如果提前规划好回滚方案,心里就有底得多。
5. 监控与告警:给系统装上仪表盘
5.1 一套监控体系怎么搭起来
从零搭一套可用的监控体系,Prometheus + Grafana + Node Exporter + Alertmanager是目前最推荐的组合。原因很简单:开源、生态大、资料多,而且和K8s、Docker都能无缝配合。
先理解Prometheus的工作方式。它是一个按“时间序列”存储数据的监控系统,每隔一个采集周期从目标机器或者目标服务上拉取指标数据。数据源用exporter暴露HTTP接口供Prometheus拉取,而Grafana负责把数据从Prometheus查出来并画成图表,Alertmanager则根据Prometheus里的告警规则来决定往钉钉、邮件或企业微信发通知。
部署顺序建议是:先装Node Exporter采集机器基础指标,再装Prometheus存数据,之后装Grafana出图,最后配Alertmanager发告警。K8s集群里的监控,通常会用一个Prometheus Operator来管理,它对Kubernetes原生的服务发现支持得非常好,能自动发现集群里的Pod和Service并采集指标,这也是“k8s集群搭建prometheus”最常见的落地方式。
下面是一段极简的Prometheus配置示例,把node_exporter和kube-state-metrics都加入采集目标:
global: scrape_interval: 15s scrape_configs: - job_name: "node" static_configs: - targets: ["192.168.1.10:9100", "192.168.1.11:9100"] - job_name: "kubernetes-pods" kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: "true"这段配置里kubernetes_sd_configs是核心,它让Prometheus通过K8s API自动发现带“prometheus.io/scrape: true”注解的Pod,也就是说以后新部署一个服务,只要在Pod模板上打上注解,监控采集就自动生效,再也不用手动改配置、再reload了。
5.2 告警规则与一次磁盘告警的复盘
监控的价值在于“提前发现问题”,如果只采集指标不做告警,那本质上和没监控差不多。告警规则不建议一上来就配二三十条,先把最核心的几类配上:节点不可用、CPU持续高、内存剩余不足、磁盘使用率超阈值、Pod反复重启。
以磁盘告警为例,规则可以写成这样:
groups: - name: node-alerts rules: - alert: DiskUsageHigh expr: disk_used_percent > 85 for: 10m labels: severity: warning annotations: summary: "节点磁盘使用率超过85%" description: "Node {{ $labels.instance }} disk usage is {{ $value }}%"for: 10m这个字段很重要,它表示持续10分钟才触发告警,避免因瞬时峰值误报。设置告警阈值还要考虑业务的实际情况,如果磁盘本身就很小或者增长极快,85%可能太晚,需要更早介入。
我复盘过一起典型的磁盘告警事件:告警平台在凌晨两点发出磁盘使用率超过85%的通知,我当时没有立即处理,想着凌晨业务量低,早上一并清理。结果五点没到,Prometheus自己的TSDB把剩余空间吃光了,监控数据本身写不进去,告警也停了,整个系统进入了“黑屏”状态。从那次以后,我把Prometheus的存储目录单独挂一块大容量盘,并给监控主机设置了更高的告警优先级,确保监控系统本尊不会先于业务系统说再见。这类经验真不是从书里能学到的,没踩过坑的人很难有这种警觉。
5.3 监控系统落地后的日常维护
搭建完监控体系后,日常维护的主要工作是三块:第一是定期检查Exporter是否全部在线,发现挂掉的采集目标要及时恢复;第二是梳理指标和看板,把不常用的指标从采集配置里去掉,避免存储膨胀;第三是每隔一段时间就检视一下告警规则,因为业务在变,以前合理的阈值现在可能已经失效了。
还要强调一点:监控数据要保留一定的历史周期。Prometheus默认本地存储有保留期(--storage.tsdb.retention.time),如果留太短,出了问题时查不到过去的曲线,就很难做“故障前后对比”。我的建议是热数据保留15天,如果要跨月度对比,考虑接一个Thanos或者把数据长期归档到对象存储。
6. 故障排查:现场处置的方法论和方法
6.1 一套可复用的排查思路
故障排查是运维工作的最终战场。一套系统的排查顺序如果对了,五分钟内基本能定位八成问题;顺序乱了,很容易做着做着把自己绕进去。
我常用的排查顺序是:先确认影响范围,再看应用日志,接着看资源指标,最后看网络链路。影响范围决定了处置的紧急程度,是全挂还是部分挂,是新发版导致还是老问题复发,这些信息直接影响后续动作。应用日志是定位的第一手证据,很多问题其实看日志就能看出来。资源瓶颈则通过top、free、df、iostat这些命令去确认,比如CPU满载、内存不足、磁盘IO过高,都会让服务出现“假死”的状态。网络层排查主要看连通性、DNS解析、端口访问和防火墙规则。
还有一条重要的经验:不要贸然重启服务。重启确实能让很多“症状”消失,但也会把定位问题的证据一起带走。遇到问题,先抓现场,再判断是否重启。如果是内存泄漏,重启后再观察,可能需要等好几天才能复现。
6.2 真实案例一:Docker服务起不来
现象是systemctl start docker卡在启动中,CentOS系统上执行systemctl status docker后看到dockerd一直在等待。这个问题的排查思路是看dockerd的日志,执行journalctl -u docker -n 100 --no-pager,大概率能看到具体原因。
我遇到过的典型情况是:/etc/docker/daemon.json配置里的网段和宿主机现有网段冲突,或者数据目录权限不对,还有磁盘空间不足导致docker daemon无法写临时文件。解决方式分别是改配置、修复目录权限、清理磁盘。还有个隐藏问题:如果宿主机开了防火墙或SELinux,docker的端口映射可能怎么配都无效,这时候用getenforce看下SELinux状态,临时改成Permissive作为定位手段,确认后再决定要不要彻底关闭或配置放行规则。
Docker daemon起不来的另一个高频原因是storage driver和内核版本或文件系统不兼容。老系统上的overlayfs支持不完整,daemon启动日志里会出现“failed to mount overlay”之类的信息,这种情况下要么升级内核,要么在daemon.json里显式指定"storage-driver": "vfs"(性能差一些但稳定)。
6.3 真实案例二:CI/CD流水线报Docker Engine未就绪
这个案例很典型,在我的经验里发生过至少三次。现象是GitLab Runner在跑流水线时,执行到docker build这一步,直接报错,提示“Cannot connect to the Docker daemon”,或者是Docker未启动。第一次遇到这个问题,我第一反应是检查Runner所在主机的dockerd有没有启动,结果系统里docker命令正常,用docker ps也没有报错,也就是说Host的Docker Engine是好的。
真正的原因其实在Runner配置。如果你用的是Shell executor在主机上跑命令,那runner用户可能没有权限访问/var/run/docker.sock,解决方案是把runner用户加入docker组;如果你用的是Docker executor,Runner容器内部并没有独立的Docker daemon,需要挂载宿主机的docker.sock,或者加上docker:20-dind服务作为DinD后台。很多新手会卡在docker.sock权限这个地方,报错信息又不直白,所以排查方向很容易跑偏。
另外,就算能连上Docker daemon,docker build时如果没有使用--network=host,一些需要访问宿主机内部服务的构建过程也会失败。我的经验是:构建阶段尽量只在容器内做标准的事,像拉Maven依赖、npm install这类只需要外网的步骤,不用额外处理;但如果构建过程要访问内网私服,一定要配置好pip或者maven的私服地址,别让构建期间的动作依赖宿主机环境。
6.4 真实案例三:K8s Pod反复处于ImagePullBackOff
在K8s里部署新应用,Pod一直起不来,状态停在ImagePullBackOff,是每个入门者都会遇到的事。原因其实就那几种:镜像地址写错、镜像标签不存在、镜像仓库需要凭证但Pod没有配置imagePullSecrets、或者仓库地址访问不通。
排查流程很清晰:执行kubectl describe pod <pod-name>,看Events部分有没有Failed to pull image或者ImagePullBackOff的具体描述,再根据描述去做处理。如果是私有仓库需要认证,创建一个docker-registry类型的secret,然后在Deployment的spec.template.spec.imagePullSecrets里引用这个secret:
apiVersion: v1 kind: Secret metadata: name: registry-key type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: <base64编码的docker config json内容>把docker login生成的config.json内容用base64编码填入,然后在Deployment里加上:
spec: template: spec: imagePullSecrets: - name: registry-key小型集群经常遇到这类问题,尤其是从本地Docker直接搬到K8s的团队,很少一开始就考虑私有仓库的认证方式。所以如果准备迁移K8s,先把镜像仓库的认证打通,后面会省很多事。
6.5 常见问题速查表
日常运维中一些可快速对照的高频问题,我整理成了一个表格。排查顺序不是死的,但按这个表去定位,大多数场景都能在几分钟内找到方向。
| 问题现象 | 常见原因 | 快速排查命令/动作 |
|---|---|---|
| 服务端口无法访问 | 服务没启动、端口未监听、防火墙拦截 | ss -lntp查看监听,curl -v验证连通,firewall-cmd --list-all检查策略 |
| CPU 使用率持续100% | 应用死循环、高消耗任务、容器无限重启 | top定位进程,pidstat -p <pid> 1看线程状态,必要时抓线程dump |
| 磁盘写满但df显示空 | 被删除文件仍被进程占用 | lsof +L1定位已删除但仍占用的文件句柄,重启对应进程 |
| Docker 容器反复重启 | 启动命令退出、内存超限、健康检查失败 | docker logs <container>看退出原因,docker inspect查重启策略和OOM状态 |
| K8s 节点NotReady | kubelet异常、网络插件损坏、磁盘压力 | kubectl describe node、journalctl -u kubelet、检查容器运行时 |
| Prometheus 抓取不到指标 | Exporter未启动、网络不通、服务发现配置错误 | curl http://IP:端口/metrics本地验证,检查scrape_configs |
| CI/CD 流水线卡住 | Runner并发满、镜像拉取慢、资源不足 | 查看Runner日志,检查并发数和资源限制,必要时提升超时时间 |
这张速查表可以打印出来贴在工位上,或者放团队知识库里。随着你遇到的实际问题越多,这张表也会越长越全,最终形成自己的知识库体系。
7. 收尾:一点实在的体会
写了这么多,我其实发现运维这个岗位最值钱的东西不是某个工具用得熟,而是故障发生时的冷静和对系统全貌的理解。好多时候问题看起来千奇百怪,但拆开来看无非就那几个层面:资源够不够、配置对不对、网络通不通、代码有没有异常。只要把这些基础修炼扎实了,再借助Docker、K8s、CI/CD和监控体系把这些能力固化下来,很多东西自然会变得标准化,故障也会越来越少。
最后再分享一个小技巧:每次处理完一个疑难故障之后,都花十五分钟把整个过程写成一篇复盘笔记,记录现象、排查步骤、根因、解决方式和预防措施。积累几十篇之后,你会发现自己从一个“救火队员”慢慢变成了能预测问题、提前规避风险的运维老手。这可能是做运维最值得坚持的习惯了。