JENKINS · GITLAB · DOCKER · HARBOR

CI/CD 流水线
故障排查手册

THE OPS FIELD MANUAL
Build · Test · Push · Deploy
pipeline { stages { ... } }
构建失败 agent 离线 docker push 凭证管理 偶发性失败 部署回滚

CI/CD 故障的特点是"上一次能跑,这一次不行"——多数是环境/依赖/凭证变化引起,而非代码问题。诊断思路:找出和上次成功构建的差异。流水线越长,断点越难定位;最有效的方法是分阶段验证,在每个 stage 加足够的日志。

典型流水线阶段

每个阶段都可能失败,排错时先定位"哪一阶段失败",再深挖。日志输出习惯:每个阶段开头打印关键变量。

1
CHECKOUT
拉代码
2
BUILD
编译打包
3
TEST
单测/扫描
4
PACKAGE
build image
5
PUSH
推到仓库
6
DEPLOY
部署生效
01

Jenkins 构建失败 — 从日志定位

BUILD FAILURE DIAGNOSIS
高频入门
# 1. 看 Console Output 完整日志
#    定位第一个 ERROR / FAILED / Exception 关键字

# 2. 复现:在同一 agent 上手动执行同样的命令
ssh jenkins-agent
su - jenkins
cd /var/lib/jenkins/workspace/<job>
# 执行 Jenkinsfile 里的命令

# 3. 对比上次成功构建的环境差异
#    - 代码版本(diff git log)
#    - 依赖版本(npm/maven 版本)
#    - 环境变量
#    - Agent 节点
错误关键字原因解决
No space left on deviceworkspace 磁盘满清理旧构建
Permission denied文件权限 / docker.sockchmod / 加 docker 组
command not foundPATH 不对用绝对路径或加 tool 配置
npm/maven 拉包失败仓库不通 / 私库认证配 .npmrc / settings.xml
OutOfMemoryErrorJVM 堆不够JAVA_OPTS=-Xmx2g
Timeout构建超时调大 timeout 或拆 job
pipeline {
    agent any
    stages {
        stage('Diagnose') {
            steps {
                // 每个阶段开头打印环境
                sh '''
                    echo "=== Environment ==="
                    pwd
                    whoami
                    env | sort
                    df -h .
                    docker version 2>&1 || true
                '''
            }
        }
        stage('Build') {
            steps {
                sh 'set -ex; mvn clean package -B'
                // set -e 出错立即停止
                // set -x 每行命令打印,debug 友好
            }
        }
    }
    post {
        failure {
            archiveArtifacts artifacts: '**/target/*.log', allowEmptyArchive: true
        }
    }
}
Jenkins 三件套 · 失败必看:① Console Output 拉到最后看堆栈;② Build → Environment Variables 看实际环境;③ Pipeline Steps 看每步耗时(超时常是网络问题)。
02

Jenkins Agent 离线 / 连不上

AGENT CONNECTION FAILURE
高频必会
# 1. Jenkins UI → Manage Jenkins → Nodes 看状态

# 2. Agent 机器上看进程
ps -ef | grep jenkins
systemctl status jenkins-agent

# 3. Agent 日志
tail -f /var/log/jenkins/agent.log
类型原理常见问题
SSHMaster 主动 ssh 到 AgentSSH key 失效 / 防火墙
JNLP / InboundAgent 主动连 Master50000 端口不通
K8s Pod动态 Pod,JNLP 启动镜像问题 / 资源不足
# Master 测试能否 ssh 到 Agent
su jenkins
ssh -i /var/lib/jenkins/.ssh/id_rsa jenkins@agent-host

# Agent 上检查 jenkins 用户权限
groups jenkins
cat ~jenkins/.ssh/authorized_keys
# 默认 50000(可在 Configure Global Security 改)
telnet jenkins-master 50000

# 启动 inbound agent 标准命令
java -jar agent.jar \
  -jnlpUrl http://jenkins:8080/computer/agent1/jenkins-agent.jnlp \
  -secret <secret> \
  -workDir /home/jenkins/agent
pipeline {
    agent {
        kubernetes {
            yaml '''
                apiVersion: v1
                kind: Pod
                spec:
                  containers:
                  - name: maven
                    image: maven:3.8-jdk-11
                    command: ["cat"]
                    tty: true
                    resources:
                      requests:
                        memory: "2Gi"
                        cpu: "1"
            '''
        }
    }
    stages {
        stage('Build') {
            steps {
                container('maven') {
                    sh 'mvn clean package'
                }
            }
        }
    }
}
K8s Agent 起不来 · 常见原因:① 镜像拉不到(检查 imagePullSecrets) ② 资源不足(Pod Pending) ③ Service Account 没权限 ④ Jenkins URL 在 Pod 内不通。先 kubectl describe pod <agent-pod> 看 events。
03

Pipeline 卡死不动

HANGING PIPELINE
中频进阶
  • 构建已经跑了几小时,没有任何输出
  • 看上去没失败,但也没进展
  • Stop 按钮按了也停不掉
  • 等待用户输入(input step 没人确认)
  • Agent 死了但 Master 没察觉
  • 子进程 hang 住(网络请求无超时)
  • 资源锁(lock 资源没释放)
  • Sleep/Wait 写错时间单位
# 1. 看正在执行的步骤
# Pipeline UI → Steps,看哪一步在 running

# 2. 强制终止
# 先点 Stop(发 SIGINT)
# 30 秒后点第二次 Stop(发 SIGKILL)
# 还不行:Script Console 执行
Jenkins.instance.getItemByFullName("my-job")
  .getBuildByNumber(123).doKill()
pipeline {
    agent any
    options {
        // 整个流水线 30 分钟超时
        timeout(time: 30, unit: 'MINUTES')
        // 失败重试
        retry(2)
    }
    stages {
        stage('Build') {
            options {
                // 单 stage 也加超时
                timeout(time: 10, unit: 'MINUTES')
            }
            steps {
                sh 'mvn clean package'
            }
        }
        stage('Manual Approve') {
            steps {
                // input 必须加超时,否则等死
                timeout(time: 5, unit: 'MINUTES') {
                    input message: 'Deploy to prod?'
                }
            }
        }
    }
}
所有外部调用都要超时 · curl 加 --max-time 30,wget 加 --timeout=30,sh 命令外包 timeout 命令。任何"等"的操作没超时,迟早卡死流水线。
04

Docker build 失败

IMAGE BUILD FAILURE
高频必会
错误原因解决
Cannot connect to docker daemondocker.sock 权限/服务挂启动 docker / 加用户到 docker 组
pull access denied基础镜像私有/不存在docker login 或换镜像
COPY failed: no such file构建上下文不对检查 .dockerignore 和路径
returned a non-zero code某条 RUN 命令失败看上一行报错
no space left on device构建机磁盘满docker system prune
network timeoutRUN 阶段拉包慢换镜像源 / 加代理
# 多阶段构建,最终镜像最小
FROM maven:3.8-jdk-11 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline      # 利用缓存,依赖单独一层
COPY src ./src
RUN mvn package -DskipTests

FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
# 错误:每次改代码全部重新装依赖
COPY . /app
RUN npm install

# 正确:依赖文件单独一层
COPY package*.json /app/
RUN npm install
COPY . /app                           # 代码变也不重装依赖
# APT 换源(Debian/Ubuntu 基础镜像)
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list

# pip 换源
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

# npm 换源
RUN npm config set registry https://registry.npmmirror.com

# Maven 换源(挂载 settings.xml)
COPY settings.xml /root/.m2/settings.xml
BuildKit 加速 · 开启 BuildKit 后(DOCKER_BUILDKIT=1),支持并行构建多阶段、更好的缓存、secret 挂载。Docker 20.10+ 默认启用。
05

Docker push 401 / 拒绝

REGISTRY AUTHENTICATION ERRORS
高频必会
错误原因
unauthorized: authentication required没 login 或 credential 过期
denied: requested access to the resource is denied账号没该项目推送权限
name unknown项目不存在
tag does not exist本地没这个 tag
EOF / connection reset镜像太大 / 网络中断
# 1. 登录
docker login harbor.example.com -u <user> -p <password>
# 凭证会存在 ~/.docker/config.json

# 2. 给镜像打标签(必须包含完整仓库地址)
docker tag myapp:v1 harbor.example.com/myproject/myapp:v1

# 3. push
docker push harbor.example.com/myproject/myapp:v1

# 4. 验证
docker manifest inspect harbor.example.com/myproject/myapp:v1
stage('Push') {
    steps {
        withCredentials([usernamePassword(
            credentialsId: 'harbor-cred',
            usernameVariable: 'USER',
            passwordVariable: 'PASS'
        )]) {
            sh '''
                echo "$PASS" | docker login harbor.example.com -u "$USER" --password-stdin
                docker tag myapp:${BUILD_NUMBER} harbor.example.com/proj/myapp:${BUILD_NUMBER}
                docker push harbor.example.com/proj/myapp:${BUILD_NUMBER}
                docker logout harbor.example.com
            '''
        }
    }
}
--password-stdin 必须用 · 直接 docker login -p $PASS 会让密码出现在进程列表和日志中。用 stdin 传递更安全。
06

Harbor 推送被拒

HARBOR-SPECIFIC ISSUES
中频进阶
  • 项目不存在:Harbor 必须先在 UI 创建项目
  • 项目配额满:存储/数量超限
  • tag 不可变:开启了 immutable tag,不能覆盖
  • 镜像扫描失败:开启了 prevent vulnerable images 策略
  • HTTPS 证书:自签证书未信任
# 错误:x509: certificate signed by unknown authority

# 方案 A:把 Harbor 证书加到 Docker 信任
mkdir -p /etc/docker/certs.d/harbor.example.com
cp harbor-ca.crt /etc/docker/certs.d/harbor.example.com/ca.crt
systemctl restart docker

# 方案 B:把 Harbor 加到 insecure-registries(开发环境)
cat > /etc/docker/daemon.json <<EOF
{
  "insecure-registries": ["harbor.example.com"]
}
EOF
systemctl restart docker

# containerd 配置(K8s 节点)
vim /etc/containerd/config.toml
# [plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.example.com".tls]
#   ca_file = "/etc/containerd/certs.d/harbor-ca.crt"
systemctl restart containerd
# 通过 Harbor API 看项目配额
curl -u admin:Harbor12345 \
  https://harbor.example.com/api/v2.0/quotas

# 清理老镜像(Harbor UI → Project → Repositories)
# 配置 retention policy 自动清理
Harbor 推送检查清单 · ① 项目在 Harbor 上已创建 ② 用户对项目有 Developer 以上角色 ③ tag 不是 immutable ④ Docker 客户端信任 Harbor 证书 ⑤ 网络可达
07

GitLab Runner 异常

GITLAB CI RUNNER ISSUES
中频进阶
# 1. 看 Runner 注册状态
gitlab-runner list
gitlab-runner status

# 2. 看 Runner 服务
systemctl status gitlab-runner
journalctl -u gitlab-runner -f

# 3. 验证 Runner 能连 GitLab
gitlab-runner verify

# 4. 测试单个 job(不真实跑,只验证配置)
gitlab-runner exec docker test_job
问题表现解决
Runner 没接到任务UI 显示 Runner online,但 pipeline 一直 pending检查 tags 是否匹配
job 启动慢等很久才开始调高 concurrent / 加 Runner
shell executor 权限job 用 gitlab-runner 用户跑,权限不足给 gitlab-runner 用户加权限
docker executor 镜像拉不到job 报 pull image 失败配 docker pull policy
cache 失效每次重装依赖检查 cache:key 配置
stages:
  - build
  - test
  - deploy

# 全局缓存
cache:
  key: "$CI_COMMIT_REF_SLUG"      # 每个分支独立缓存
  paths:
    - node_modules/
    - .m2/repository/

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""

build_job:
  stage: build
  image: maven:3.8-jdk-11
  tags:
    - linux                       # 匹配 Runner tag
  script:
    - mvn package -DskipTests
  artifacts:
    paths:
      - target/*.jar
    expire_in: 1 week
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Runner tags 必须匹配 · job 写了 tags: [docker],但所有 Runner 都没有 docker 这个 tag,job 永远 pending。Runner 注册时指定 tag,或在 UI 中修改。
08

流水线偶发性失败 — 最难调

FLAKY PIPELINE
高频高级
  • 同样的代码,有时成功有时失败
  • retry 一下又过了
  • 失败原因每次不一样
  • 网络抖动:拉依赖、push 镜像、调用外部 API
  • 资源不足:Agent 同时跑多个 job,内存/磁盘紧张
  • 测试不稳定:时间敏感、并发依赖、随机数据
  • 缓存竞态:多 job 同时读写同一缓存
  • 外部服务不稳:测试环境 DB/Redis 偶尔不可达
// 1. 关键 step 加 retry
stage('Pull Deps') {
    steps {
        retry(3) {
            sh 'mvn dependency:resolve'
        }
    }
}

// 2. waitForCondition 替代 sleep
stage('Wait for Service') {
    steps {
        timeout(time: 60, unit: 'SECONDS') {
            waitUntil {
                script {
                    def r = sh(
                        script: 'curl -fs http://service/health',
                        returnStatus: true
                    )
                    return r == 0
                }
            }
        }
    }
}

// 3. 测试失败也归档,方便分析
post {
    always {
        junit '**/target/surefire-reports/*.xml'
        archiveArtifacts artifacts: '**/target/screenshots/*', allowEmptyArchive: true
    }
}
# 1. 拉包加重试和超时
curl --max-time 30 --retry 3 --retry-delay 5 <url>
wget --timeout=30 --tries=3 <url>

# 2. docker pull 失败重试
for i in 1 2 3; do
  docker pull image:tag && break
  sleep 10
done

# 3. npm/maven 加 mirrors 兜底
# 把不稳定的测试隔离
test:stable:
  script:
    - mvn test -Dgroups="!flaky"     # 排除 flaky 组

test:flaky:
  script:
    - mvn test -Dgroups="flaky"
  retry: 2                        # 自动重试
  allow_failure: true              # 失败不阻塞流水线
偶发失败需要数据驱动 · 一次失败不算 bug,记录 → 复现 → 修复。给每次失败打 label("network", "test-data", "timeout"),按 label 统计,找出最痛的痛点优先解决。
09

凭证管理 — 别让密码进代码

SECRET MANAGEMENT BEST PRACTICES
高频必会
类型用途
Username with password登录数据库/仓库
SSH Username with private key远程登录/Git
Secret textAPI token / webhook
Secret filekubeconfig / 证书
CertificateSSL/TLS 证书
// 用户名密码
withCredentials([usernamePassword(
    credentialsId: 'harbor-cred',
    usernameVariable: 'USER',
    passwordVariable: 'PASS'
)]) {
    sh 'echo "$PASS" | docker login -u "$USER" --password-stdin'
}

// SSH key
withCredentials([sshUserPrivateKey(
    credentialsId: 'git-key',
    keyFileVariable: 'SSH_KEY'
)]) {
    sh 'GIT_SSH_COMMAND="ssh -i $SSH_KEY" git clone git@host:repo.git'
}

// kubeconfig 文件
withCredentials([file(
    credentialsId: 'kubeconfig-prod',
    variable: 'KUBECONFIG'
)]) {
    sh 'kubectl get pods'
}
# 在 GitLab Project → Settings → CI/CD → Variables 配置
# 关键选项:
# - Protected:只在保护分支可用
# - Masked:日志中遮蔽
# - Expand variable reference:是否展开 ${VAR}

deploy:
  script:
    - echo "Deploying..."
    - curl -H "Authorization: Bearer $DEPLOY_TOKEN" ...
  only:
    - main
反模式合集 · ① 密码硬编码在 Jenkinsfile ② echo 凭证到日志(即使 Masked,经过 base64 也会泄漏) ③ 把 credential 文件 commit 到 Git ④ 所有人共用 admin 账号 ⑤ 凭证永不过期。
更高阶方案 · 生产环境用 Vault / AWS Secrets Manager / K8s External Secrets,Jenkins 不存凭证,运行时按需拉取。配合 dynamic secret,数据库密码可一次性使用。
10

部署失败与回滚

DEPLOYMENT FAILURE AND ROLLBACK
高频致命
stage('Deploy') {
    steps {
        sh '''
            set -e

            # 1. 应用新版本
            kubectl set image deployment/myapp \\
              myapp=harbor.example.com/proj/myapp:${BUILD_NUMBER} \\
              -n production

            # 2. 等待 rollout 完成,超时则失败
            kubectl rollout status deployment/myapp -n production --timeout=5m

            # 3. 健康检查
            curl -f http://service/health || exit 1
        '''
    }
    post {
        failure {
            sh '''
                echo "Deploy failed, rolling back..."
                kubectl rollout undo deployment/myapp -n production
                kubectl rollout status deployment/myapp -n production
            '''
        }
    }
}
# 查看部署历史
kubectl rollout history deployment/myapp -n prod

# 看具体某个版本的详情
kubectl rollout history deployment/myapp -n prod --revision=5

# 回滚到上一版本
kubectl rollout undo deployment/myapp -n prod

# 回滚到指定版本
kubectl rollout undo deployment/myapp -n prod --to-revision=3

# 暂停 / 恢复 rollout
kubectl rollout pause deployment/myapp -n prod
kubectl rollout resume deployment/myapp -n prod
策略原理适用
RollingUpdate逐步替换 Pod默认,大多数业务
Recreate先删完旧的再起新的有状态服务,版本不兼容
Blue-Green切流量到新版本需要快速回滚
Canary少量新版本验证核心业务,需小流量验证
# 决策树
# 1. rollout status 显示 timeout?
kubectl get pod -l app=myapp -n prod
kubectl describe pod <pod> -n prod | tail -20
#   - ImagePullBackOff → 镜像问题
#   - CrashLoopBackOff → 应用启动失败
#   - Pending → 资源不足

# 2. 健康检查失败?
kubectl logs <pod> -n prod --tail=100
# 看应用日志找根因

# 3. 紧急回滚
kubectl rollout undo deployment/myapp -n prod
# 1 分钟内能回到上一个稳定版本
没有回滚就不要部署 · 上线前必须确认:① 知道当前版本号 ② 知道回滚命令 ③ 5 分钟内能完成回滚。数据库迁移要可逆,新代码要兼容老 schema(至少一个版本)。
更稳的方案 — Argo CD / Flux · GitOps 模式,部署声明在 Git 仓库,改 Git 自动同步到集群。优势:有审计、可回滚到任意历史 commit、多集群一致。