欢迎光临
我们一直在努力

中小公司 CI/CD 流水线搭建实战:从 Jenkins 到 GitLab CI 的踩坑记录

前言

术语与缩写说明

为方便不同背景的读者快速理解本文内容,以下表格整理了文中频繁出现的专业术语和缩写及其含义:

术语/缩写全称/解释首次出现章节
CI/CD 持续集成(Continuous Integration)/持续交付(Continuous Delivery),自动化构建、测试和部署的工程实践 前言
DevOps 开发(Development)与运维(Operations)的合并缩写,强调开发与运维协作的文化和实践 前言
Jenkins 开源的自动化服务器,支持 Pipeline as Code,用于构建、测试和部署 前言
GitLab CI GitLab 内置的持续集成服务,通过 .gitlab-ci.yml 配置文件定义流水线 前言
Runner GitLab CI 的执行器,负责运行流水线中的作业(Job) 前言
Pipeline as Code 将 CI/CD 流水线以代码形式定义(如 Jenkinsfile),实现版本管理和可重复性 1. 背景与选型
Docker in Docker(DinD) 在 Docker 容器内运行 Docker 守护进程的方式,存在安全隐患 2.1 环境准备
Docker outside of Docker(DooD) 通过挂载宿主机 Docker socket 使容器调用宿主机 Docker 守护进程的方式 2.1 环境准备
LTS 长期支持(Long Term Support)版本,提供更长的维护周期 2.1 环境准备
Blue Ocean Jenkins 的现代化 UI 插件,提供可视化流水线编辑和运行状态展示 2.2 插件安装踩坑
SSL/TLS 安全套接层(Secure Sockets Layer)/传输层安全(Transport Layer Security),用于加密网络通信的协议 3.2 Runner 注册失败
Maven Java 项目的构建和依赖管理工具 4.1 项目结构
SonarQube 开源的代码质量检测平台,支持静态代码分析和质量门禁 4.2 Jenkinsfile 编写
npm ci npm 的干净安装命令,根据 package-lock.json 精确安装依赖,速度比 npm install 更快 5.1 .gitlab-ci.yml 配置
蓝绿部署 一种部署策略,同时维护两套生产环境(蓝/绿),通过切换流量实现零停机发布 6.1 部署策略
Nginx 高性能的 HTTP 和反向代理服务器,本文用于实现蓝绿部署的流量切换 6.1 部署策略
Prometheus 开源的系统监控和告警工具,通过拉取(scrape)方式采集指标数据 6.2 监控告警配置
Grafana 开源的可视化仪表板工具,支持 Prometheus 等多种数据源 6.2 监控告警配置
Alertmanager Prometheus 生态的告警管理组件,负责告警分组、抑制和通知路由 6.2 监控告警配置
PromQL Prometheus Query Language,用于查询和聚合监控指标数据的查询语言 6.2.2 Grafana 仪表板关键查询语句
JVM Java 虚拟机(Java Virtual Machine),运行 Java 字节码的虚拟计算机 6.2.2 Grafana 仪表板关键查询语句
GC 垃圾回收(Garbage Collection),JVM 自动管理内存的机制 6.2.2 Grafana 仪表板关键查询语句
HPA 水平 Pod 自动扩缩容(Horizontal Pod Autoscaler),Kubernetes 的自动扩缩容机制 总结与展望
GitOps 以 Git 仓库作为声明式基础设施的唯一真相来源的运维模式 总结与展望
ArgoCD Kubernetes 的声明式 GitOps 持续交付工具 总结与展望
Harbor 企业级容器镜像仓库,支持漏洞扫描、镜像复制和访问控制 总结与展望
OpenTelemetry 开源的可观测性框架,统一了指标、日志和链路追踪的数据采集标准 总结与展望

本文核心价值:这是一篇面向中小型技术团队的 CI/CD 实战记录,基于真实踩坑经验写成。文章完整记录了从 Jenkins 和 GitLab CI 选型、搭建、配置到生产环境部署与监控的全过程,重点剖析了四个典型故障的排查思路与解决方案,帮助读者在搭建自动化交付体系时少走弯路。

目标读者:适合 20~200 人技术团队中的后端开发、前端开发、运维工程师以及兼职 DevOps 的技术负责人。如果你正在为团队选择 CI/CD 工具、遇到了插件冲突或缓存失效等问题,或者想了解蓝绿部署和监控告警的落地细节,这篇文章应该能提供有价值的参考。

主要内容概览:

  • Jenkins 搭建与插件版本冲突的排查与修复
  • GitLab CI Runner 注册失败及 SSL 证书问题的处理
  • Java 微服务 Jenkins Pipeline 实战与 Docker 构建缓存优化
  • 前端项目 GitLab CI 自动化与 npm 缓存失效的解决方案
  • 蓝绿部署策略(含自动回滚与观察期)的完整实现
  • Prometheus + Grafana + Alertmanager 监控告警体系搭建

1. 背景与选型

2024 年初,我所在的公司(约 80 人技术团队)决定从传统的手动部署转向自动化 CI/CD。团队规模不大,没有专职 DevOps,因此选型原则很明确:工具要成熟、社区活跃、学习曲线不能太陡。

我们对比了 Jenkins、GitLab CI、GitHub Actions 和 Drone CI。最终选择 Jenkins + GitLab CI 双轨并行:

  • Jenkins:负责 Java 微服务(Spring Boot)的构建与部署,因为 Jenkins 的 Pipeline as Code 和丰富的插件生态更适合复杂构建流程。
  • GitLab CI:负责前端项目(Vue/React)和 Python 脚本的自动化测试与部署,配置简单,与代码仓库深度集成。

这个选型并非一开始就确定,中间经历了多次试错,下面会详细展开。

2. Jenkins 搭建与第一个坑:插件版本冲突

2.1 环境准备

服务器配置:4C8G CentOS 7.9,Docker 24.0.7,Jenkins 使用官方 LTS 版本 2.440.3。

# 拉取 Jenkins 镜像
docker pull jenkins/jenkins:lts-jdk17
启动容器,挂载数据卷和 Docker socket
docker run -d
–name jenkins
-p 8080:8080
-p 50000:50000
-v jenkins_home:/var/jenkins_home
-v /var/run/docker.sock:/var/run/docker.sock
-v /usr/bin/docker:/usr/bin/docker
–restart=always
jenkins/jenkins:lts-jdk17

这里有一个关键点:挂载 Docker socket 是为了让 Jenkins 容器内能调用宿主机的 Docker 守护进程,实现 Docker in Docker 的效果。但这种方式有安全隐患,生产环境建议使用 Docker outside of Docker(DooD)或 Kaniko。

2.2 插件安装踩坑

Jenkins 启动后,通过 http://ip:8080 访问,输入初始密码完成解锁。接下来安装插件,我选择了「推荐插件」安装,结果遇到了第一个大坑:

问题:安装过程中,Pipeline 插件与 Blue Ocean 插件版本不兼容,导致安装失败,Jenkins 页面报 500 错误。

排查过程:

  • 查看 Jenkins 日志:docker logs jenkins –tail 100,发现 java.lang.NoSuchMethodError 异常,指向 org.jenkinsci.plugins.workflow 包。
  • 进入容器检查插件目录:docker exec -it jenkins bash,查看 /var/jenkins_home/plugins/,发现部分插件下载不完整。
  • 手动删除不完整的插件文件:rm -rf /var/jenkins_home/plugins/*.jpi.pinned。
  • 重启 Jenkins 后,通过「插件管理」页面逐个安装核心插件:Pipeline、Git、Docker Pipeline、Credentials Binding。
  • 解决方案:不要一次性安装大量插件,建议按需安装,并锁定插件版本。我最终使用的插件清单如下:

    # plugins.txt
    pipeline:2.442
    git:5.2.1
    docker-workflow:1.29
    credentials-binding:2.1.0
    blueocean:1.27.3
    sonar:2.15
    config-file-provider:3.11.1

    将 plugins.txt 挂载到 /usr/share/jenkins/ref/plugins.txt,Jenkins 启动时会自动安装指定版本。

    3. GitLab CI 配置与第二个坑:Runner 注册失败

    3.1 Runner 部署

    GitLab CI 的核心是 Runner,我们选择 Docker Runner,部署在另一台 4C8G 的服务器上。

    # 拉取 GitLab Runner 镜像
    docker pull gitlab/gitlab-runner:latest
    启动 Runner 容器
    docker run -d
    –name gitlab-runner
    –restart=always
    -v /srv/gitlab-runner/config:/etc/gitlab-runner
    -v /var/run/docker.sock:/var/run/docker.sock
    gitlab/gitlab-runner:latest

    3.2 Runner 注册失败

    问题:执行 gitlab-runner register 时,输入 GitLab URL 和 Registration Token 后,报错 ERROR: Registering runner… failed runner=xxx status=401 Unauthorized。

    排查过程:

  • 确认 GitLab 版本:我们使用的是 GitLab 16.8 社区版,Runner 注册需要管理员权限。
  • 检查 Token 是否正确:在 GitLab 项目设置 > CI/CD > Runner 中重新生成 Token。
  • 查看 Runner 日志:docker logs gitlab-runner,发现 SSL 证书验证失败。
  • 检查 GitLab 实例的证书:我们使用的是自签名证书,Runner 容器内没有信任该证书。
  • 解决方案:

    # 方案一:关闭 SSL 验证(仅测试环境)
    docker exec -it gitlab-runner gitlab-runner register \\
    –non-interactive \\
    –url "https://gitlab.example.com" \\
    –registration-token "YOUR_TOKEN" \\
    –executor "docker" \\
    –docker-image "alpine:latest" \\
    –tls-ca-file "" \\
    –tls-skip-verify true
    方案二:将自签名证书挂载到 Runner 容器
    将 GitLab 的 CA 证书复制到宿主机
    cp /etc/gitlab/ssl/gitlab.example.com.crt /srv/gitlab-runner/config/certs/
    重启 Runner
    docker restart gitlab-runner

    生产环境推荐方案二,确保通信安全。

    4. Jenkins Pipeline 实战:Java 微服务构建

    4.1 项目结构

    我们的 Java 微服务项目采用 Maven 多模块结构:

    order-service/
    ├── pom.xml
    ├── Dockerfile
    ├── src/
    │ ├── main/
    │ │ ├── java/
    │ │ └── resources/
    │ └── test/
    └── Jenkinsfile

    4.2 Jenkinsfile 编写

    pipeline {
    agent any
    environment {
    DOCKER_REGISTRY = 'registry.example.com'
    IMAGE_NAME = "${DOCKER_REGISTRY}/order-service"
    IMAGE_TAG = "${BUILD_NUMBER}-${env.GIT_COMMIT.take(8)}"
    }
    stages {
    stage('Checkout') {
    steps {
    checkout scm
    }
    }
    stage('Unit Test') {
    steps {
    sh 'mvn clean test -DskipITs=true'
    }
    post {
    success {
    junit '**/target/surefire-reports/*.xml'
    }
    }
    }
    stage('SonarQube Analysis') {
    steps {
    withSonarQubeEnv('SonarQube') {
    sh 'mvn sonar:sonar -Dsonar.projectKey=order-service'
    }
    }
    }
    stage('Build & Package') {
    steps {
    sh 'mvn package -DskipTests=true -Dmaven.test.skip=true'
    }
    }
    stage('Build Docker Image') {
    steps {
    script {
    docker.build("${IMAGE_NAME}:${IMAGE_TAG}")
    }
    }
    }
    stage('Push to Registry') {
    steps {
    script {
    docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-registry-credentials') {
    docker.image("${IMAGE_NAME}:${IMAGE_TAG}").push()
    docker.image("${IMAGE_NAME}:${IMAGE_TAG}").push('latest')
    }
    }
    }
    }
    stage('Deploy to Test') {
    steps {
    sh """
    ssh deploy@test-server "docker pull ${IMAGE_NAME}:${IMAGE_TAG} &&
    docker-compose -f /opt/order-service/docker-compose.yml up -d"
    """
    }
    }
    }
    post {
    failure {
    emailext(
    subject: "构建失败: ${env.JOB_NAME} – ${env.BUILD_NUMBER}",
    body: "请查看构建日志: ${env.BUILD_URL}",
    to: 'team@example.com'
    )
    }
    }
    }

    4.3 遇到的第三个坑:Docker 构建缓存失效

    问题:每次构建都重新下载 Maven 依赖,构建时间从 3 分钟飙升到 15 分钟。

    排查过程:

  • 检查 Dockerfile,发现 COPY pom.xml 和 RUN mvn dependency:resolve 的顺序正确,理论上应该能利用缓存。
  • 查看 Jenkins 构建日志,发现每次构建的 COPY 步骤都显示 Cache invalidated。
  • 检查 Jenkins 工作空间,发现每次构建都会清理工作空间,导致 pom.xml 的时间戳变化。
  • 解决方案:

    # 优化后的 Dockerfile
    FROM maven:3.9-eclipse-temurin-17 AS builder
    WORKDIR /app
    # 先复制 pom.xml,利用 Docker 缓存层
    COPY pom.xml .
    RUN mvn dependency:resolve -DskipTests=true
    # 再复制源码
    COPY src ./src
    RUN mvn package -DskipTests=true
    FROM eclipse-temurin:17-jre-alpine
    WORKDIR /app
    COPY –from=builder /app/target/*.jar app.jar
    EXPOSE 8080
    ENTRYPOINT ["java", "-jar", "app.jar"]

    同时,在 Jenkins Pipeline 中设置 options { disableConcurrentBuilds() } 避免并发构建互相干扰。

    5. GitLab CI 实战:前端项目自动化

    5.1 .gitlab-ci.yml 配置

    stages:
    – install
    – lint
    – test
    – build
    – deploy
    cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
    – node_modules/
    – .npm/
    install:
    stage: install
    image: node:20-alpine
    script:
    – npm ci –cache .npm –prefer-offline
    artifacts:
    paths:
    – node_modules/
    expire_in: 1 hour
    lint:
    stage: lint
    image: node:20-alpine
    script:
    – npm run lint
    needs:
    – install
    test:
    stage: test
    image: node:20-alpine
    script:
    – npm run test:ci
    coverage: '/All files[|]\\s*([\\d.]+)/'
    artifacts:
    reports:
    coverage_report:
    coverage_format: cobertura
    path: coverage/cobertura-coverage.xml
    needs:
    – install
    build:
    stage: build
    image: node:20-alpine
    script:
    – npm run build
    artifacts:
    paths:
    – dist/
    expire_in: 1 week
    needs:
    – install
    deploy:
    stage: deploy
    image: alpine:latest
    before_script:
    – apk add –no-cache rsync openssh-client
    – eval $(ssh-agent -s)
    – echo "$SSH_PRIVATE_KEY" | tr -d '\\r' | ssh-add –
    – mkdir -p ~/.ssh
    – chmod 700 ~/.ssh
    script:
    – rsync -avz –delete dist/ deploy@web-server:/var/www/project/
    only:
    – main
    needs:
    – build

    5.2 第四个坑:npm ci 缓存失效

    问题:虽然配置了 cache 关键字,但每次 Pipeline 运行时 npm ci 仍然重新下载所有依赖。

    排查过程:

  • 查看 GitLab Runner 日志,发现缓存文件确实被上传到了 S3(我们配置了 S3 作为缓存后端)。
  • 检查 cache: key 的生成规则,发现 ${CI_COMMIT_REF_SLUG} 在分支切换时会变化。
  • 进一步发现 npm ci 会删除 node_modules 并重新安装,即使缓存命中也会被清空。
  • 解决方案:

    # 优化后的 install 阶段
    install:
    stage: install
    image: node:20-alpine
    cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
    – .npm/
    policy: pull-push
    script:
    – npm ci –cache .npm –prefer-offline –no-audit –no-fund
    artifacts:
    paths:
    – node_modules/
    expire_in: 1 hour

    关键改动:

    • 将 node_modules/ 从缓存路径中移除,改为通过 artifacts 传递。
    • 缓存只保留 .npm/ 目录(npm 的全局缓存)。
    • 使用 –prefer-offline 优先使用本地缓存。
    • 添加 –no-audit –no-fund 减少不必要的网络请求。

    6. 生产环境部署与监控

    6.1 部署策略

    6.1 部署策略

    我们采用蓝绿部署策略,通过 Nginx 反向代理实现流量切换。下图将部署触发、环境判断、健康检查、观察期监控、自动回滚与告警通知整合为一个完整的闭环流程图:

    flowchart TD
    %% 样式定义
    classDef startEnd fill:#e1f5fe,stroke:#0288d1,stroke-width:2px,color:#01579b
    classDef process fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c
    classDef decision fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#bf360c
    classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1b5e20
    classDef failure fill:#ffebee,stroke:#c62828,stroke-width:2px,color:#b71c1c
    classDef alert fill:#fff8e1,stroke:#f57f17,stroke-width:2px,stroke-dasharray:5 5,color:#e65100
    %% 部署触发
    A([Jenkins Pipeline 触发]):::startEnd
    %% 构建与推送
    B[构建 Docker 镜像并打标签]:::process
    C[推送镜像到 Registry]:::process
    %% 环境判断
    D{判断当前活跃环境}:::decision
    %% 部署分支
    E[部署到绿环境 10.0.1.11]:::process
    F[部署到蓝环境 10.0.1.10]:::process
    %% 启动与健康检查
    G[启动新环境容器]:::process
    H[执行健康检查: curl /actuator/health]:::process
    I{健康检查通过?}:::decision
    %% 健康检查失败路径
    J[停止新环境容器]:::failure
    K[保留旧环境继续服务]:::success
    L[发送告警: 部署失败通知]:::alert
    %% 健康检查成功路径
    M[更新 Nginx upstream 配置指向新环境]:::process
    N[nginx -s reload 切换流量]:::process
    O[新环境正式接管全部流量]:::success
    %% 观察期
    P[观察期: 持续监控 5 分钟]:::process
    Q{观察期内错误率正常?}:::decision
    %% 观察期成功
    R[记录本次部署成功]:::success
    %% 观察期失败 – 自动回滚
    S[触发自动回滚]:::failure
    T[恢复 Nginx upstream 指向旧环境]:::failure
    U[nginx -s reload 切回旧环境]:::failure
    V[发送告警: 自动回滚通知]:::alert
    W[记录本次部署失败并归档日志]:::failure
    %% 结束节点
    X([部署流程结束]):::startEnd
    %% 连接关系
    A –> B
    B –> C
    C –> D
    D –>|蓝环境活跃| E
    D –>|绿环境活跃| F
    E –> G
    F –> G
    G –> H
    H –> I
    %% 健康检查失败分支
    I –>|否| J
    J –> K
    J –> L
    K –> X
    L –> X
    %% 健康检查成功分支
    I –>|是| M
    M –> N
    N –> O
    O –> P
    P –> Q
    %% 观察期成功分支
    Q –>|是| R
    R –> X
    %% 观察期失败 – 自动回滚分支
    Q –>|否| S
    S –> T
    T –> U
    S –> V
    U –> W
    V –> W
    W –> X

    流程图说明:上图将基础蓝绿部署与完整的回滚、告警链路整合为一个闭环。图中使用不同形状和颜色区分节点类型:圆角矩形(蓝色)为开始/结束节点,矩形(紫色)为处理节点,菱形(橙色)为判断节点,绿色矩形为成功路径节点,红色矩形为失败路径节点,黄色虚线矩形为告警节点。部署触发后,先判断当前活跃环境,将新版本部署到非活跃环境。健康检查失败时立即停止新环境容器并发送告警,保留旧环境继续服务。健康检查通过后,系统进入 5 分钟观察期持续监控错误率;若观察期内指标异常,则自动触发回滚——恢复 Nginx 指向旧环境并发送告警通知。整个流程确保任何异常都能被及时发现并自动恢复,最大限度降低故障影响。

    关键路径说明:

    • 成功路径(绿色):部署触发 → 构建镜像 → 推送 Registry → 环境判断 → 部署新环境 → 健康检查通过 → 更新 Nginx 配置 → 切换流量 → 观察期通过 → 记录部署成功。
    • 失败路径(红色):健康检查失败 → 停止新环境容器 → 保留旧环境继续服务;或观察期异常 → 触发自动回滚 → 恢复 Nginx 指向旧环境 → 切回旧环境 → 记录部署失败。
    • 告警节点(黄色虚线):健康检查失败时发送「部署失败通知」;观察期异常触发自动回滚时发送「自动回滚通知」,确保运维人员第一时间获知异常状态。

    这个合并后的流程图将部署可靠性从「单点健康检查」提升到「持续验证+自动兜底」的级别,显著降低了因部署引入故障对线上服务的影响。

    基础部署脚本通过 Jenkins Pipeline 自动切换 Nginx 的上游服务器:

    #!/bin/bash
    # deploy.sh – 基础版蓝绿部署脚本
    ACTIVE=$(ssh deploy@nginx-server "cat /etc/nginx/conf.d/active.txt")
    if [ "$ACTIVE" = "blue" ]; then
    TARGET="green"
    TARGET_IP="10.0.1.11"
    else
    TARGET="blue"
    TARGET_IP="10.0.1.10"
    fi
    部署到目标环境
    ssh deploy@$TARGET_IP "docker pull ${IMAGE_NAME}:${IMAGE_TAG} && docker-compose up -d"
    健康检查
    for i in {1..30}; do
    if curl -s http://$TARGET_IP:8080/actuator/health | grep -q "UP"; then
    break
    fi
    sleep 2
    done
    切换流量
    ssh deploy@nginx-server "echo '$TARGET' > /etc/nginx/conf.d/active.txt && nginx -s reload"
    echo "Deploy to $TARGET completed"

    下面给出一个完整的、健壮的部署脚本 deploy_robust.sh,包含参数校验与预检查、详细日志记录、健康检查失败自动回滚、观察期持续监控与错误率阈值判断、以及关键步骤的告警通知接口:

    #!/bin/bash
    # =============================================================================
    # deploy_robust.sh – 健壮版蓝绿部署脚本
    # 功能:参数校验 + 预检查 + 详细日志 + 健康检查 + 观察期监控 + 自动回滚 + 告警
    # =============================================================================
    set -euo pipefail
    ============================================================
    1. 配置区
    ============================================================
    NGINX_SERVER="deploy@nginx-server"
    ACTIVE_FILE="/etc/nginx/conf.d/active.txt"
    HEALTH_CHECK_URL="/actuator/health"
    HEALTH_CHECK_RETRIES=30
    HEALTH_CHECK_INTERVAL=2
    OBSERVATION_PERIOD=300 # 5 分钟观察期(秒)
    OBSERVATION_INTERVAL=10 # 观察期采样间隔(秒)
    ERROR_RATE_THRESHOLD=5 # 错误率阈值(百分比)
    ERROR_SAMPLE_WINDOW=60 # 错误率统计窗口(秒)
    LOG_FILE="/var/log/deploy_$(date +%Y%m%d_%H%M%S).log"
    TIMESTAMP_FORMAT="+%Y-%m-%d %H:%M:%S"
    ============================================================
    2. 日志记录函数
    ============================================================
    log_info() {
    echo "[$(date "$TIMESTAMP_FORMAT")] [INFO] $*" | tee -a "$LOG_FILE"
    }
    log_warn() {
    echo "[$(date "$TIMESTAMP_FORMAT")] [WARN] $*" | tee -a "$LOG_FILE"
    }
    log_error() {
    echo "[$(date "$TIMESTAMP_FORMAT")] [ERROR] $*" | tee -a "$LOG_FILE"
    }
    log_step() {
    echo "" | tee -a "$LOG_FILE"
    echo "========================================" | tee -a "$LOG_FILE"
    echo "[$(date "$TIMESTAMP_FORMAT")] [STEP] $*" | tee -a "$LOG_FILE"
    echo "========================================" | tee -a "$LOG_FILE"
    }
    ============================================================
    3. 告警通知函数(预留钉钉/邮件接口)
    ============================================================
    钉钉机器人通知(需配置 WEBHOOK_URL)
    send_dingtalk_alert() {
    local subject="$1"
    local message="$2"
    local webhook_url="${DINGTALK_WEBHOOK_URL:-}"
    if [ -z "$webhook_url" ]; then
    log_warn "未配置 DINGTALK_WEBHOOK_URL,跳过钉钉告警"
    return 0
    fi
    curl -s -X POST "$webhook_url"
    -H "Content-Type: application/json"
    -d "{
    "msgtype": "text",
    "text": {
    "content": "[CI/CD 告警] ${subject}\\n${message}\\n时间: $(date '$TIMESTAMP_FORMAT')"
    }
    }" >/dev/null 2>&1 || log_warn "钉钉告警发送失败"
    }
    邮件通知(需配置 SMTP 相关环境变量)
    send_email_alert() {
    local subject="$1"
    local message="$2"
    local mail_to="${ALERT_EMAIL_TO:-}"
    if [ -z "$mail_to" ]; then
    log_warn "未配置 ALERT_EMAIL_TO,跳过邮件告警"
    return 0
    fi
    echo "$message" | mail -s "[CI/CD] $subject" "$mail_to" 2>/dev/null || log_warn "邮件告警发送失败"
    }
    统一告警入口
    send_alert() {
    local level="$1" # INFO / WARN / ERROR
    local subject="$2"
    local message="$3"
    log_info "发送告警 [${level}]: ${subject}"
    send_dingtalk_alert "${subject}" "${message}"
    send_email_alert "${subject}" "${message}"
    }
    参数校验
    ============================================================
    log_step "参数校验"
    if [ $# -lt 2 ]; then
    log_error "用法: $0 <IMAGE_NAME> <IMAGE_TAG> [ENVIRONMENT]"
    log_error "示例: $0 registry.example.com/order-service v1.2.3-branch"
    log_error " $0 registry.example.com/order-service v1.2.3-branch green"
    send_alert "ERROR" "部署脚本参数错误" "缺少必要参数 IMAGE_NAME 或 IMAGE_TAG"
    exit 1
    fi
    IMAGE_NAME="$1"
    IMAGE_TAG="$2"
    FORCED_TARGET="${3:-}" # 可选:强制部署到指定环境(blue/green)
    校验镜像标签格式(允许字母、数字、下划线、连字符、点、斜杠和冒号)
    if ! echo "$IMAGE_NAME:$IMAGE_TAG" | grep -qE '^[a-zA-Z0-9./-]+:[a-zA-Z0-9.-]+$'; then
    log_error "镜像标签格式无效: ${IMAGE_NAME}:${IMAGE_TAG}"
    send_alert "ERROR" "镜像标签格式无效" "镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
    exit 1
    fi
    log_info "镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
    log_info "日志文件: ${LOG_FILE}"
    ============================================================
    部署前预检查
    ============================================================
    log_step "部署前预检查"
    5.1 检查 Nginx 服务器 SSH 连通性
    log_info "检查 Nginx 服务器连通性…"
    if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$NGINX_SERVER" "echo OK" >/dev/null 2>&1; then
    log_error "Nginx 服务器 ${NGINX_SERVER} 无法连接,请检查网络和 SSH 配置"
    send_alert "ERROR" "服务器连通性检查失败" "Nginx 服务器 ${NGINX_SERVER} 无法连接"
    exit 1
    fi
    log_info "Nginx 服务器连通性检查通过"
    5.2 检查 Nginx 服务器磁盘空间
    log_info "检查 Nginx 服务器磁盘空间…"
    DISK_USAGE=$(ssh "$NGINX_SERVER" "df -h / | tail -1 | awk '{print $5}' | sed 's/%//'")
    if [ "$DISK_USAGE" -gt 85 ]; then
    log_error "Nginx 服务器磁盘使用率已达 ${DISK_USAGE}%,超过 85% 阈值,中止部署"
    send_alert "ERROR" "磁盘空间不足" "Nginx 服务器磁盘使用率: ${DISK_USAGE}%"
    exit 1
    fi
    log_info "Nginx 服务器磁盘使用率: ${DISK_USAGE}%"
    5.3 检查镜像是否存在于 Registry
    log_info "检查镜像 ${IMAGE_NAME}:${IMAGE_TAG} 是否存在于 Registry…"
    if ! docker manifest inspect "${IMAGE_NAME}:${IMAGE_TAG}" >/dev/null 2>&1; then
    log_error "镜像 ${IMAGE_NAME}:${IMAGE_TAG} 不存在于 Registry,请先构建并推送镜像"
    send_alert "ERROR" "镜像不存在" "镜像 ${IMAGE_NAME}:${IMAGE_TAG} 未在 Registry 中找到"
    exit 1
    fi
    log_info "镜像存在性检查通过"
    ============================================================
    确定部署目标环境
    ============================================================
    log_step "确定部署目标环境"
    if [ -n "$FORCED_TARGET" ]; then
    TARGET="$FORCED_TARGET"
    if [ "$TARGET" = "blue" ]; then
    TARGET_IP="10.0.1.10"
    elif [ "$TARGET" = "green" ]; then
    TARGET_IP="10.0.1.11"
    else
    log_error "无效的环境参数: ${TARGET},仅支持 blue 或 green"
    exit 1
    fi
    log_info "强制部署到: ${TARGET} (${TARGET_IP})"
    else
    ACTIVE=$(ssh "$NGINX_SERVER" "cat ${ACTIVE_FILE}" 2>/dev/null || echo "blue")
    if [ "$ACTIVE" = "blue" ]; then
    TARGET="green"
    TARGET_IP="10.0.1.11"
    else
    TARGET="blue"
    TARGET_IP="10.0.1.10"
    fi
    log_info "当前活跃环境: ${ACTIVE},目标环境: ${TARGET} (${TARGET_IP})"
    fi
    ============================================================
    部署到目标环境
    ============================================================
    log_step "部署到目标环境 ${TARGET}"
    DEPLOY_START=$(date +%s)
    DEPLOY_RESULT=0
    DEPLOY_OUTPUT=$(ssh "deploy@${TARGET_IP}" "
    set -e
    echo '拉取镜像…'
    docker pull ${IMAGE_NAME}:${IMAGE_TAG}
    echo '停止旧容器…'
    docker-compose -f /opt/app/docker-compose.yml down 2>/dev/null || true
    echo '更新镜像标签…'
    export IMAGE_NAME=${IMAGE_NAME}
    export IMAGE_TAG=${IMAGE_TAG}
    echo '启动新容器…'
    docker-compose -f /opt/app/docker-compose.yml up -d
    echo '部署完成'
    " 2>&1) || DEPLOY_RESULT=$?
    if [ $DEPLOY_RESULT -ne 0 ]; then
    log_error "部署到 ${TARGET} 失败"
    log_error "输出: ${DEPLOY_OUTPUT}"
    send_alert "ERROR" "部署失败" "部署到 ${TARGET} 失败\\n输出: ${DEPLOY_OUTPUT}"
    exit 1
    fi
    log_info "部署输出:"
    echo "${DEPLOY_OUTPUT}" | while IFS= read -r line; do log_info " ${line}"; done
    DEPLOY_DURATION=$(( $(date +%s) – DEPLOY_START ))
    log_info "部署耗时: ${DEPLOY_DURATION} 秒"
    ============================================================
    健康检查
    ============================================================
    log_step "健康检查"
    HEALTH_CHECK_PASSED=false
    for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" –connect-timeout 3
    "http://${TARGET_IP}:8080${HEALTH_CHECK_URL}" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" = "200" ]; then
    HEALTH_BODY=$(curl -s "http://${TARGET_IP}:8080${HEALTH_CHECK_URL}" 2>/dev/null)
    if echo "$HEALTH_BODY" | grep -q '"status":"UP"'; then
    log_info "健康检查通过 (第 ${i} 次尝试)"
    HEALTH_CHECK_PASSED=true
    break
    fi
    fi
    log_info "健康检查等待中… (第 ${i}/${HEALTH_CHECK_RETRIES} 次, HTTP ${HTTP_CODE})"
    sleep $HEALTH_CHECK_INTERVAL
    done
    if [ "$HEALTH_CHECK_PASSED" != "true" ]; then
    log_error "健康检查失败,${TARGET} 环境服务未正常启动"
    log_step "执行回滚:停止新环境容器"
    ssh "deploy@${TARGET_IP}" "docker-compose -f /opt/app/docker-compose.yml down" 2>&1 || true
    send_alert "ERROR" "健康检查失败 – 已回滚"
    "环境: ${TARGET}\\n镜像: ${IMAGE_NAME}:${IMAGE_TAG}\\n已停止新环境容器,旧环境继续提供服务"
    exit 1
    fi
    切换 Nginx 流量
    ============================================================
    log_step "切换 Nginx 流量到 ${TARGET}"
    SWITCH_RESULT=0
    SWITCH_OUTPUT=$(ssh "$NGINX_SERVER" "
    echo '${TARGET}' > ${ACTIVE_FILE}
    nginx -s reload
    echo 'Nginx 配置已更新并重新加载'
    " 2>&1) || SWITCH_RESULT=$?
    if [ $SWITCH_RESULT -ne 0 ]; then
    log_error "Nginx 切换失败"
    log_error "输出: ${SWITCH_OUTPUT}"
    log_step "执行回滚:恢复 Nginx 配置到原环境"
    if [ "$TARGET" = "green" ]; then
    ROLLBACK_TARGET="blue"
    else
    ROLLBACK_TARGET="green"
    fi
    ssh "$NGINX_SERVER" "echo '${ROLLBACK_TARGET}' > ${ACTIVE_FILE} && nginx -s reload" 2>&1 || true
    log_info "停止新环境容器…"
    ssh "deploy@${TARGET_IP}" "docker-compose -f /opt/app/docker-compose.yml down" 2>&1 || true
    send_alert "ERROR" "Nginx 切换失败 – 已回滚"
    "目标环境: ${TARGET}\\n已恢复 Nginx 指向 ${ROLLBACK_TARGET}\\n新环境容器已停止"
    exit 1
    fi
    log_info "Nginx 流量已切换到 ${TARGET}"
    log_info "切换输出: ${SWITCH_OUTPUT}"
    观察期:持续监控与错误率阈值判断
    ============================================================
    log_step "观察期:持续监控 ${OBSERVATION_PERIOD} 秒(错误率阈值 ${ERROR_RATE_THRESHOLD}%)"
    OBSERVATION_START=$(date +%s)
    OBSERVATION_PASSED=true
    TOTAL_REQUESTS=0
    FAILED_REQUESTS=0
    while [ $(( $(date +%s) – OBSERVATION_START )) -lt $OBSERVATION_PERIOD ]; do
    ELAPSED=$(( $(date +%s) – OBSERVATION_START ))
    REMAINING=$(( OBSERVATION_PERIOD – ELAPSED ))
    执行健康检查请求,统计错误率
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" –connect-timeout 3
    "http://${TARGET_IP}:8080${HEALTH_CHECK_URL}" 2&gt;/dev/null || echo "000")
    TOTAL_REQUESTS=$(( TOTAL_REQUESTS + 1 ))
    if [ "$HTTP_CODE" != "200" ]; then
    FAILED_REQUESTS=$(( FAILED_REQUESTS + 1 ))
    log_warn "观察期内健康检查失败 (HTTP ${HTTP_CODE}),剩余 ${REMAINING} 秒"
    fi
    每 30 秒计算一次当前错误率
    if [ $(( ELAPSED % 30 )) -eq 0 ] &amp;&amp; [ $TOTAL_REQUESTS -gt 0 ]; then
    CURRENT_ERROR_RATE=$(( FAILED_REQUESTS * 100 / TOTAL_REQUESTS ))
    log_info "观察中… 已监控 ${ELAPSED} 秒,剩余 ${REMAINING} 秒 | "
    "请求: ${TOTAL_REQUESTS}, 失败: ${FAILED_REQUESTS}, 错误率: ${CURRENT_ERROR_RATE}%"
    错误率阈值判断:如果当前错误率超过阈值,触发自动回滚
    if [ "$CURRENT_ERROR_RATE" -ge "$ERROR_RATE_THRESHOLD" ]; then
    log_error "错误率 ${CURRENT_ERROR_RATE}% 超过阈值 ${ERROR_RATE_THRESHOLD}%,触发自动回滚"
    OBSERVATION_PASSED=false
    break
    fi
    fi
    sleep $OBSERVATION_INTERVAL
    done
    观察期结束后的最终错误率统计
    if [ $TOTAL_REQUESTS -gt 0 ]; then
    FINAL_ERROR_RATE=$(( FAILED_REQUESTS * 100 / TOTAL_REQUESTS ))
    else
    FINAL_ERROR_RATE=0
    fi
    log_info "观察期结束 – 总请求: ${TOTAL_REQUESTS}, 失败: ${FAILED_REQUESTS}, 最终错误率: ${FINAL_ERROR_RATE}%"
    if [ "$OBSERVATION_PASSED" != "true" ]; then
    log_error "观察期内检测到异常,触发自动回滚"
    log_step "执行自动回滚"
    恢复 Nginx 指向旧环境
    if [ "$TARGET" = "green" ]; then
    ROLLBACK_TARGET="blue"
    ROLLBACK_IP="10.0.1.10"
    else
    ROLLBACK_TARGET="green"
    ROLLBACK_IP="10.0.1.11"
    fi
    log_info "恢复 Nginx upstream 指向 ${ROLLBACK_TARGET} (${ROLLBACK_IP})…"
    ROLLBACK_RESULT=0
    ROLLBACK_OUTPUT=$(ssh "$NGINX_SERVER" "
    echo '${ROLLBACK_TARGET}' &gt; ${ACTIVE_FILE}
    nginx -s reload
    echo '已回滚到 ${ROLLBACK_TARGET}'
    " 2&gt;&amp;1) || ROLLBACK_RESULT=$?
    if [ $ROLLBACK_RESULT -eq 0 ]; then
    log_info "自动回滚成功:Nginx 已切回 ${ROLLBACK_TARGET}"
    else
    log_error "自动回滚失败:Nginx 切换异常"
    log_error "输出: ${ROLLBACK_OUTPUT}"
    fi
    停止新环境容器
    log_info "停止 ${TARGET} 环境容器…"
    ssh "deploy@${TARGET_IP}" "docker-compose -f /opt/app/docker-compose.yml down" 2&gt;&amp;1 || true
    发送自动回滚告警
    send_alert "ERROR" "自动回滚已触发"
    "环境: ${TARGET} → ${ROLLBACK_TARGET}\\n"
    "镜像: ${IMAGE_NAME}:${IMAGE_TAG}\\n"
    "原因: 观察期错误率 ${FINAL_ERROR_RATE}% 超过阈值 ${ERROR_RATE_THRESHOLD}%\\n"
    "总请求: ${TOTAL_REQUESTS}, 失败: ${FAILED_REQUESTS}"
    log_info "回滚完成,旧环境 ${ROLLBACK_TARGET} 继续提供服务"
    exit 1
    fi
    部署成功
    log_step "部署成功"
    TOTAL_DURATION=$(( $(date +%s) – DEPLOY_START ))
    log_info "部署总耗时: ${TOTAL_DURATION} 秒"
    log_info "当前活跃环境: ${TARGET}"
    log_info "镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
    log_info "日志文件: ${LOG_FILE}"
    发送部署成功通知
    send_alert "INFO" "部署成功"
    "环境: ${TARGET}\\n"
    "镜像: ${IMAGE_NAME}:${IMAGE_TAG}\\n"
    "耗时: ${TOTAL_DURATION} 秒\\n"
    "观察期错误率: ${FINAL_ERROR_RATE}%"
    echo ""
    log_info "========================================"
    log_info " 部署完成!"
    log_info " 环境: ${TARGET}"
    log_info " 镜像: ${IMAGE_NAME}:${IMAGE_TAG}"
    log_info " 耗时: ${TOTAL_DURATION} 秒"
    log_info " 观察期错误率: ${FINAL_ERROR_RATE}%"
    log_info "========================================"

    脚本关键逻辑说明:

    • 参数校验与预检查(第 4-5 节):校验 IMAGE_NAME 和 IMAGE_TAG 两个必选参数及镜像标签格式;检查 Nginx 服务器 SSH 连通性、磁盘使用率(超过 85% 中止部署)、以及镜像是否已存在于 Registry,避免部署中途失败。
    • 详细日志记录函数(第 2 节):提供 log_info、log_warn、log_error、log_step 四个级别的日志函数,所有输出同时写入终端和日志文件 /var/log/deploy_YYYYMMDD_HHMMSS.log,便于事后审计。
    • 健康检查失败自动回滚(第 8 节):最多 30 次(每次间隔 2 秒)检查 /actuator/health 端点,确保服务真正就绪后再切换流量;若健康检查失败,立即停止新环境容器并发送告警,保留旧环境继续服务。
    • 观察期持续监控与错误率阈值判断(第 10 节):切换流量后进入 5 分钟观察期,每 10 秒采样一次健康检查结果,每 30 秒计算一次当前错误率。若错误率超过 5% 阈值,立即触发自动回滚——恢复 Nginx 指向旧环境、停止新环境容器、发送告警通知。
    • 关键步骤告警通知(第 3 节):封装了 send_dingtalk_alert 和 send_email_alert 两个告警函数,通过环境变量 DINGTALK_WEBHOOK_URL 和 ALERT_EMAIL_TO 配置。部署失败、健康检查失败、Nginx 切换失败、观察期异常触发自动回滚、部署成功等关键节点均调用 send_alert 统一告警入口。

    6.2 监控告警配置

    部署自动化只是第一步,生产环境必须配套完善的监控告警体系。我们使用 Prometheus + Grafana + Alertmanager 构建了全栈监控方案,覆盖 Java 微服务(Spring Boot Actuator)和前端应用(Nginx 日志)两个维度。下面给出具体的配置示例。

    6.2.1 Prometheus 配置:scrape_configs

    Prometheus 通过拉取(scrape)方式采集指标。以下 prometheus.yml 配置了 Java 微服务和 Nginx 日志指标的抓取任务:

    # prometheus.yml – 全局配置与抓取任务
    global:
    scrape_interval: 15s # 默认抓取间隔
    evaluation_interval: 15s # 告警规则评估间隔
    scrape_timeout: 10s # 抓取超时
    Alertmanager 配置
    alerting:
    alertmanagers:
    – static_configs:
    – targets:
    – 'alertmanager:9093'
    告警规则文件
    rule_files:
    'alerts/*.yml'
    抓取配置
    scrape_configs:
    ———- Java 微服务(Spring Boot Actuator)———-
    job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    scrape_interval: 10s
    scrape_timeout: 5s
    static_configs:
    targets:
    'order-service:8080' # 订单服务
    'user-service:8080' # 用户服务
    'payment-service:8080' # 支付服务
    labels:
    application: 'java-microservice'
    team: 'backend'
    服务发现:通过 Consul 或 K8s 自动发现目标
    以下为 Consul 服务发现示例(需部署 consul_exporter)
    consul_sd_configs:
    – server: 'consul:8500'
    services: ['order-service', 'user-service', 'payment-service']
    ———- Nginx 日志指标(通过 nginx-prometheus-exporter)———-
    job_name: 'nginx-log-metrics'
    scrape_interval: 15s
    scrape_timeout: 10s
    static_configs:
    targets:
    'nginx-exporter:9113' # nginx-prometheus-exporter 暴露的指标端口
    labels:
    application: 'nginx'
    team: 'infra'
    如果使用 nginx-lua-prometheus 直接在 Nginx 内暴露指标
    metrics_path: '/metrics'
    static_configs:
    – targets:
    – 'nginx-server:80'
    ———- 节点基础指标(Node Exporter)———-
    job_name: 'node-exporter'
    scrape_interval: 30s
    static_configs:
    targets:
    'node-exporter:9100'
    labels:
    team: 'infra'

    配置说明:

    • Spring Boot 应用:需在 pom.xml 中添加 micrometer-registry-prometheus 依赖,并在 application.yml 中暴露 /actuator/prometheus 端点。
    • Nginx 指标:推荐使用 nginx-prometheus-exporter 容器,它会读取 Nginx 的 stub_status 模块数据并转换为 Prometheus 格式。也可使用 nginx-lua-prometheus 库直接在 Nginx 内嵌指标端点。
    • 服务发现:生产环境建议使用 Consul 或 Kubernetes 服务发现,避免手动维护目标列表。
    6.2.2 Grafana 仪表板关键查询语句

    以下是在 Grafana 中创建监控仪表板时常用的 PromQL 查询语句,覆盖 Java 微服务和 Nginx 两个维度。

    Java 微服务(Spring Boot)关键指标:

    监控维度PromQL 查询说明
    JVM 堆内存使用率 jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} * 100 按服务分组,超过 80% 需关注 GC 或扩容
    JVM 非堆内存使用率 jvm_memory_used_bytes{area="nonheap"} / jvm_memory_max_bytes{area="nonheap"} * 100 元空间/永久代使用率
    GC 暂停时间(最近 5 分钟) rate(jvm_gc_pause_seconds_sum[5m]) / rate(jvm_gc_pause_seconds_count[5m]) 平均 GC 暂停时间,超过 1s 需优化 GC 参数
    HTTP 请求速率 rate(http_server_requests_seconds_count[1m]) 按 URI 和状态码分组,观察流量变化
    HTTP 请求错误率 sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) / sum(rate(http_server_requests_seconds_count[5m])) * 100 5xx 错误占比,超过 1% 触发告警
    HTTP 请求 P99 延迟 histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri)) 按 URI 查看 P99 延迟,超过 500ms 需优化
    线程池活跃线程数 jvm_threads_live_threads 对比最大线程数,判断是否线程耗尽
    数据库连接池活跃连接 hikaricp_connections_active 接近最大连接数时需扩容或优化 SQL

    Nginx 关键指标:

    监控维度PromQL 查询说明
    活跃连接数 nginx_connections_active 当前活跃连接数,对比最大连接数判断是否过载
    连接速率 rate(nginx_connections_accepted[1m]) 每秒新建连接数,反映流量变化
    请求速率 rate(nginx_http_requests_total[1m]) 每秒请求数,按状态码分组观察
    4xx 错误率 sum(rate(nginx_http_requests_total{status=~"4.."}[5m])) / sum(rate(nginx_http_requests_total[5m])) * 100 客户端错误占比,异常升高可能表示前端配置问题
    5xx 错误率 sum(rate(nginx_http_requests_total{status=~"5.."}[5m])) / sum(rate(nginx_http_requests_total[5m])) * 100 服务端错误占比,超过 1% 需立即排查
    上游响应时间 nginx_upstream_response_msecs_avg 后端服务平均响应时间,超过 1s 需优化

    Grafana 仪表板配置建议:

    • 创建两个独立仪表板:「Java 微服务监控」和「Nginx 网关监控」,分别聚焦应用层和网关层。
    • 在 Java 仪表板中按服务名称(instance 标签)分组,使用变量下拉框切换服务。
    • 在 Nginx 仪表板中按状态码分组展示请求速率,叠加 4xx/5xx 错误率曲线。
    • 设置「全局时间范围」变量,默认最近 1 小时,支持快速切换到 6h/24h/7d。
    • 为每个关键指标添加「告警阈值线」(如 JVM 内存 80%、错误率 1%),便于直观判断。
    6.2.3 Alertmanager 告警规则配置

    以下告警规则文件 alerts/service_alerts.yml 覆盖了高错误率、高延迟、服务不可用等关键场景:

    # alerts/service_alerts.yml – 服务监控告警规则
    groups:
    – name: java_microservice_alerts
    interval: 30s
    rules:
    # ———- 高错误率告警 ———-
    – alert: HighErrorRate
    expr: |
    (sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
    /
    sum(rate(http_server_requests_seconds_count[5m]))) * 100 > 1
    for: 3m
    labels:
    severity: critical
    team: backend
    annotations:
    summary: "{{ $labels.instance }} 错误率过高"
    description: |
    服务 {{ $labels.instance }} 的 5xx 错误率已超过 1%(当前值: {{ $value | humanize }}%),
    持续超过 3 分钟。请立即排查服务健康状况。
    runbook_url: "https://wiki.example.com/runbooks/high-error-rate"
    # ———- 高延迟告警(P99 &gt; 500ms)———-
    – alert: HighLatencyP99
    expr: |
    histogram_quantile(0.99,
    sum(rate(http_server_requests_seconds_bucket[5m])) by (le, instance, uri)
    ) &gt; 0.5
    for: 5m
    labels:
    severity: warning
    team: backend
    annotations:
    summary: "{{ $labels.instance }} P99 延迟过高"
    description: |
    服务 {{ $labels.instance }} 的 URI {{ $labels.uri }} P99 延迟已超过 500ms
    (当前值: {{ $value | humanizeDuration }}),持续超过 5 分钟。
    runbook_url: "https://wiki.example.com/runbooks/high-latency"
    ———- 服务不可用告警 ———-
    alert: ServiceDown
    expr: up{job="spring-boot-apps"} == 0
    for: 1m
    labels:
    severity: critical
    team: backend
    annotations:
    summary: "{{ $labels.instance }} 服务不可用"
    description: |
    服务 {{ $labels.instance }} 已离线超过 1 分钟,
    请立即检查容器状态和网络连通性。
    runbook_url: "https://wiki.example.com/runbooks/service-down"
    ———- JVM 堆内存使用率过高 ———-
    alert: HighJVMMemoryUsage
    expr: |
    (jvm_memory_used_bytes{area="heap"}
    /
    jvm_memory_max_bytes{area="heap"}) * 100 &gt; 80
    for: 5m
    labels:
    severity: warning
    team: backend
    annotations:
    summary: "{{ $labels.instance }} JVM 堆内存使用率过高"
    description: |
    服务 {{ $labels.instance }} 的 JVM 堆内存使用率已超过 80%
    (当前值: {{ $value | humanize }}%),持续超过 5 分钟。
    建议检查 GC 日志或考虑扩容。
    ———- GC 暂停时间过长 ———-
    alert: LongGCPause
    expr: |
    rate(jvm_gc_pause_seconds_sum[5m])
    /
    rate(jvm_gc_pause_seconds_count[5m]) &gt; 1
    for: 5m
    labels:
    severity: warning
    team: backend
    annotations:
    summary: "{{ $labels.instance }} GC 暂停时间过长"
    description: |
    服务 {{ $labels.instance }} 的平均 GC 暂停时间已超过 1 秒
    (当前值: {{ $value | humanizeDuration }}),持续超过 5 分钟。
    建议优化 GC 参数或增加堆内存。
    name: nginx_alerts
    interval: 30s
    rules:
    ———- Nginx 5xx 错误率告警 ———-
    alert: NginxHigh5xxRate
    expr: |
    (sum(rate(nginx_http_requests_total{status=~"5.."}[5m]))
    /
    sum(rate(nginx_http_requests_total[5m]))) * 100 > 1
    for: 3m
    labels:
    severity: critical
    team: infra
    annotations:
    summary: "Nginx 5xx 错误率过高"
    description: |
    Nginx 的 5xx 错误率已超过 1%(当前值: {{ $value | humanize }}%),
    持续超过 3 分钟。请检查上游服务健康状况。
    runbook_url: "https://wiki.example.com/runbooks/nginx-5xx"
    ———- Nginx 活跃连接数过高 ———-
    alert: NginxHighActiveConnections
    expr: nginx_connections_active > 1000
    for: 5m
    labels:
    severity: warning
    team: infra
    annotations:
    summary: "Nginx 活跃连接数过高"
    description: |
    Nginx 当前活跃连接数已超过 1000(当前值: {{ $value }}),
    持续超过 5 分钟。建议检查 worker_connections 配置或扩容。
    ———- Nginx 上游响应时间过长 ———-
    alert: NginxUpstreamSlowResponse
    expr: nginx_upstream_response_msecs_avg > 1000
    for: 5m
    labels:
    severity: warning
    team: infra
    annotations:
    summary: "Nginx 上游响应时间过长"
    description: |
    Nginx 上游平均响应时间已超过 1000ms(当前值: {{ $value | humanizeDuration }}),
    持续超过 5 分钟。请检查后端服务性能。

    Alertmanager 配置说明:

    • 告警分组:按服务类型分为 java_microservice_alerts 和 nginx_alerts 两组,每组独立评估。
    • 严重级别:critical 表示需要立即响应(如服务不可用、高错误率),warning 表示需要关注(如内存使用率高、延迟偏高)。
    • 持续条件(for):告警条件需持续满足指定时间后才触发,避免因瞬时抖动导致误报。例如高错误率需持续 3 分钟才告警。
    • runbook_url:每个告警关联 Wiki 中的故障处理手册,方便运维人员快速定位和修复。
    6.2.3.1 JVM 内存使用率告警规则示例(prometheus.rules.yml)

    以下是一个独立的告警规则文件 prometheus.rules.yml,专门针对 Java 应用的 JVM 堆内存使用率进行监控和告警。该规则可与 6.2.3 节中的 alerts/service_alerts.yml 配合使用,或作为独立文件被 Prometheus 加载。

    # prometheus.rules.yml – JVM 堆内存使用率告警规则
    groups:
    – name: jvm_memory_alerts
    interval: 30s # 规则评估间隔,每 30 秒检查一次
    rules:
    # ———- 告警 1:JVM 堆内存使用率过高(Warning)———-
    – alert: JVMHeapMemoryUsageWarning
    # 计算当前堆内存使用率:已用堆内存 / 最大堆内存 * 100
    expr: |
    (jvm_memory_used_bytes{area="heap"}
    /
    jvm_memory_max_bytes{area="heap"}) * 100 > 80
    for: 5m # 条件需持续满足 5 分钟才触发告警,避免瞬时抖动误报
    labels:
    severity: warning
    team: backend
    annotations:
    summary: "{{ $labels.instance }} JVM 堆内存使用率超过 80%"
    description: |
    服务 {{ $labels.instance }} 的 JVM 堆内存使用率已达 {{ $value | humanize }}%,
    持续超过 5 分钟。建议检查 GC 日志、分析堆转储(Heap Dump)或考虑扩容。
    runbook_url: "https://wiki.example.com/runbooks/jvm-memory"
    # ———- 告警 2:JVM 堆内存使用率严重过高(Critical)———-
    – alert: JVMHeapMemoryUsageCritical
    expr: |
    (jvm_memory_used_bytes{area="heap"}
    /
    jvm_memory_max_bytes{area="heap"}) * 100 > 95
    for: 2m # 严重级别缩短持续条件,更快响应
    labels:
    severity: critical
    team: backend
    annotations:
    summary: "{{ $labels.instance }} JVM 堆内存使用率超过 95%"
    description: |
    服务 {{ $labels.instance }} 的 JVM 堆内存使用率已达 {{ $value | humanize }}%,
    持续超过 2 分钟。存在 OOM(OutOfMemoryError)风险,请立即介入!
    建议操作:1. 检查当前 GC 频率和暂停时间;2. 执行 jstack 和 jmap 采集现场;
    3. 考虑重启服务或临时扩容。
    runbook_url: "https://wiki.example.com/runbooks/jvm-oom"
    # ———- 告警 3:JVM 非堆内存(元空间)使用率过高 ———-
    – alert: JVMMetaspaceUsageHigh
    expr: |
    (jvm_memory_used_bytes{area="nonheap"}
    /
    jvm_memory_max_bytes{area="nonheap"}) * 100 > 85
    for: 5m
    labels:
    severity: warning
    team: backend
    annotations:
    summary: "{{ $labels.instance }} JVM 元空间使用率超过 85%"
    description: |
    服务 {{ $labels.instance }} 的 JVM 非堆内存(元空间)使用率已达 {{ $value | humanize }}%,
    持续超过 5 分钟。元空间泄漏通常由类加载器未正确回收引起,建议检查
    是否频繁创建动态代理、反射调用或热部署导致类加载器泄漏。

    规则关键字段说明:

    • groups.name:告警组名称,用于逻辑分组,Prometheus 按组独立评估规则。
    • interval:规则评估周期,建议与 Prometheus 全局 evaluation_interval 保持一致或更短。
    • alert:告警名称,在 Alertmanager 中用于路由匹配和分组。
    • expr:PromQL 表达式,计算 JVM 堆内存使用率(已用/最大 * 100)。area="heap" 筛选堆内存区域。
    • for:持续条件,防止因 GC 瞬间波动导致误报。Warning 级别设 5 分钟,Critical 级别设 2 分钟。
    • labels:附加标签,severity 区分严重级别,team 用于 Alertmanager 路由分发。
    • annotations:告警描述信息,{{ $value }} 和 {{ $labels.instance }} 为 Prometheus 模板变量,渲染实际值。
    • runbook_url:关联故障处理手册,方便运维人员快速定位和修复。

    使用方式:将上述 prometheus.rules.yml 文件放置在 Prometheus 配置中 rule_files 指定的目录下(如 /etc/prometheus/rules/),并在 prometheus.yml 中添加引用:

    # prometheus.yml 中引用告警规则文件
    rule_files:
    – 'alerts/*.yml' # 原有的告警规则
    – 'rules/prometheus.rules.yml' # 新增的 JVM 内存告警规则

    6.2.4 Alertmanager 通知配置

    以下 alertmanager.yml 配置将告警通过钉钉机器人和邮件发送给对应团队:

    # alertmanager.yml – 告警通知路由配置
    global:
    resolve_timeout: 5m
    smtp_smarthost: 'smtp.example.com:587'
    smtp_from: 'alertmanager@example.com'
    smtp_auth_username: 'alertmanager@example.com'
    smtp_auth_password: 'YOUR_SMTP_PASSWORD'
    smtp_require_tls: true
    route:
    receiver: 'default'
    group_by: ['alertname', 'team']
    group_wait: 30s
    group_interval: 5m
    repeat_interval: 4h
    routes:
    # 后端团队告警
    – match:
    team: backend
    receiver: 'backend-team'
    continue: true
    # 基础设施团队告警
    – match:
    team: infra
    receiver: 'infra-team'
    continue: true
    # critical 级别告警额外通知值班人员
    – match:
    severity: critical
    receiver: 'on-call'
    repeat_interval: 30m
    receivers:
    name: 'default'
    email_configs:
    to: 'devops@example.com'
    send_resolved: true
    name: 'backend-team'
    email_configs:
    to: 'backend@example.com'
    send_resolved: true
    webhook_configs:
    url: 'https://oapi.dingtalk.com/robot/send?access_token=YOUR_DINGTALK_TOKEN'
    send_resolved: true
    http_config:
    headers:
    Content-Type: 'application/json'
    name: 'infra-team'
    email_configs:
    to: 'infra@example.com'
    send_resolved: true
    webhook_configs:
    url: 'https://oapi.dingtalk.com/robot/send?access_token=YOUR_DINGTALK_TOKEN'
    send_resolved: true
    name: 'on-call'
    email_configs:
    to: 'oncall@example.com'
    send_resolved: true
    webhook_configs:
    url: 'https://oapi.dingtalk.com/robot/send?access_token=YOUR_ONCALL_TOKEN'
    send_resolved: true

    通知路由说明:

    • 分组策略:按告警名称和团队分组,相同告警在 5 分钟内合并发送,避免告警风暴。
    • 重复间隔:普通告警每 4 小时重复一次;critical 级别告警每 30 分钟重复一次,确保值班人员不会遗漏。
    • 多通道通知:同时通过邮件和钉钉机器人发送,邮件作为正式记录,钉钉用于即时响应。
    • send_resolved:告警恢复时也会发送通知,便于确认问题已解决。
    6.2.5 监控告警体系部署建议
    • 容器化部署:建议使用 Docker Compose 或 Kubernetes 部署 Prometheus Stack(Prometheus + Grafana + Alertmanager + Node Exporter),统一管理配置和升级。
    • 数据持久化:Prometheus 默认使用本地存储,建议配置 –storage.tsdb.retention.time=30d 保留 30 天数据。如需长期存储,可对接 Thanos 或 VictoriaMetrics。
    • 告警静默:在计划维护或已知故障期间,通过 Alertmanager 的 Silence 功能临时静默相关告警,避免干扰。
    • 定期演练:每月模拟一次服务故障,验证告警规则是否触发、通知是否送达、值班人员是否响应。演练结果记录到团队 Wiki。
    • 与部署流程联动:将监控告警配置纳入 CI/CD 流程,每次部署新服务时自动注册 Prometheus 抓取目标和告警规则,避免遗漏。

    总结与展望

    关键收获

    经过三个月的迭代,我们的 CI/CD 体系已经稳定运行,日均构建次数从最初的 5 次提升到 30 次,部署时间从 30 分钟缩短到 5 分钟。以下是本次实践的核心收获:

    • 选型要匹配团队规模:Jenkins + GitLab CI 双轨并行虽然不够优雅,但 Jenkins 的 Pipeline as Code 适合 Java 微服务的复杂构建流程,GitLab CI 的简洁配置适合前端和脚本项目,两者互补满足了不同团队的需求。
    • 缓存是性能的关键:无论是 Maven 依赖还是 npm 包,合理配置缓存能节省 70% 以上的构建时间。Docker 构建缓存失效和 npm ci 缓存失效这两个坑,本质上都是对缓存机制理解不够深入导致的。
    • 监控必须与自动化同步推进:自动化部署后问题暴露得更快,没有监控等于盲人摸象。Prometheus + Grafana + Alertmanager 的组合对中小公司足够用,关键是要把告警链路跑通,实现从指标异常到自动回滚的闭环。
    • 文档沉淀不可忽视:每次踩坑后及时更新 Wiki,避免团队重复踩坑。我们使用 GitLab Wiki 记录所有故障和解决方案,新成员上手时间从两周缩短到三天。

    典型问题回顾

    问题根因解决方案
    Jenkins 插件版本冲突 一次性安装大量插件导致版本不兼容 按需安装并锁定插件版本,使用 plugins.txt 管理
    GitLab Runner 注册失败 自签名证书未被 Runner 容器信任 将 CA 证书挂载到 Runner 容器或关闭 SSL 验证(测试环境)
    Docker 构建缓存失效 Jenkins 清理工作空间导致 pom.xml 时间戳变化 优化 Dockerfile 分层策略,设置 disableConcurrentBuilds()
    npm ci 缓存失效 缓存 key 随分支变化且 node_modules 被清空 缓存 .npm/ 目录,通过 artifacts 传递 node_modules/

    未来展望

    当前体系虽然稳定运行,但仍有一些值得优化的方向:

    • 引入 Kubernetes:当前蓝绿部署依赖 Nginx 手动切换上游服务器,随着微服务数量增加,维护成本会急剧上升。Kubernetes 的 Deployment + Service + Ingress 可以原生支持滚动更新和蓝绿部署,配合 HPA 实现自动扩缩容。我们计划在明年将核心服务迁移到 K8s 集群。
    • ArgoCD 实现 GitOps:当前部署流程仍然依赖 Jenkins Pipeline 执行 SSH 命令,配置和代码没有完全分离。ArgoCD 可以将 Git 仓库作为声明式基础设施的唯一真相来源,每次代码合并后自动同步到集群,实现真正的 GitOps 工作流。
    • 统一镜像仓库与安全扫描:目前镜像直接推送到私有 Registry,缺乏安全扫描环节。Harbor 企业级镜像仓库支持漏洞扫描和镜像复制,可以作为下一步的升级方向。
    • 全链路可观测性:当前监控主要覆盖应用层指标,未来可以引入 OpenTelemetry 实现分布式链路追踪,结合 Loki 日志聚合,构建完整的可观测性体系。

    CI/CD 的建设没有终点,随着业务发展和团队成长,工具链和流程也需要持续迭代。希望这篇文章的实战经验能对正在搭建或优化 CI/CD 体系的团队有所帮助。

    7. 总结与故障排查清单

    7.1 四个典型故障排查表

    以下表格针对文中遇到的四个典型故障,分别提炼了「问题现象」「可能原因」和「排查步骤」,方便读者在实际运维中快速定位问题。

    故障一:Jenkins 插件版本冲突
    维度内容
    问题现象 安装推荐插件时页面报 500 错误,日志显示 java.lang.NoSuchMethodError,指向 org.jenkinsci.plugins.workflow 包。
    可能原因 一次性安装大量插件导致 Pipeline 与 Blue Ocean 等插件版本不兼容,部分插件下载不完整。
    排查步骤
  • 查看 Jenkins 日志:docker logs jenkins –tail 100,确认异常类型。
  • 进入容器检查插件目录:docker exec -it jenkins bash,查看 /var/jenkins_home/plugins/ 下是否有不完整的 .jpi.pinned 文件。
  • 删除不完整插件文件:rm -rf /var/jenkins_home/plugins/*.jpi.pinned。
  • 重启 Jenkins,通过「插件管理」页面逐个安装核心插件,并使用 plugins.txt 锁定版本。
  • 故障二:GitLab Runner 注册失败
    维度内容
    问题现象 执行 gitlab-runner register 时报错 ERROR: Registering runner… failed runner=xxx status=401 Unauthorized。
    可能原因 GitLab 使用自签名证书,Runner 容器内未信任该 CA 证书,导致 SSL 握手失败。
    排查步骤
  • 确认 GitLab 版本及 Token 是否正确:在项目设置 > CI/CD > Runner 中重新生成 Token。
  • 查看 Runner 日志:docker logs gitlab-runner,检查是否有 SSL 相关错误。
  • 检查 GitLab 实例证书类型:若为自签名证书,将 CA 证书复制到 /srv/gitlab-runner/config/certs/ 目录下。
  • 重启 Runner 容器后重新注册。
  • 故障三:Docker 构建缓存失效
    维度内容
    问题现象 每次 Jenkins 构建都重新下载 Maven 依赖,构建时间从 3 分钟飙升到 15 分钟。
    可能原因 Jenkins 每次构建清理工作空间,导致 pom.xml 时间戳变化,Docker 缓存层失效。
    排查步骤
  • 检查 Dockerfile 中 COPY pom.xml 和 RUN mvn dependency:resolve 的顺序是否正确。
  • 查看 Jenkins 构建日志,确认 COPY 步骤是否显示 Cache invalidated。
  • 检查 Jenkins 工作空间是否被清理(如 disableConcurrentBuilds() 未设置)。
  • 优化 Dockerfile 分层:先复制 pom.xml 执行依赖解析,再复制源码打包。
  • 故障四:npm ci 缓存失效
    维度内容
    问题现象 GitLab CI 中虽然配置了 cache 关键字,但每次 Pipeline 运行时 npm ci 仍然重新下载所有依赖。
    可能原因 缓存 key 随分支变化导致缓存不命中;npm ci 会删除 node_modules 重新安装;node_modules/ 目录不适合直接缓存。
    排查步骤
  • 查看 GitLab Runner 日志,确认缓存文件是否被上传到后端(如 S3)。
  • 检查 cache: key 的生成规则,确认 ${CI_COMMIT_REF_SLUG} 在分支切换时是否变化。
  • 将 node_modules/ 从缓存路径中移除,改为通过 artifacts 传递。
  • 缓存 .npm/ 目录(npm 全局缓存),在 npm ci 命令中添加 –prefer-offline –no-audit –no-fund 参数。
  • 7.2 CI/CD 日常巡检与维护建议清单

    以下清单覆盖了 CI/CD 体系日常运维的关键检查项,建议每周或每次重大变更后执行一次巡检。

    类别检查项检查频率说明
    基础设施 磁盘空间检查 每周 检查 Jenkins、GitLab Runner、Nginx 等服务器的磁盘使用率,超过 80% 及时清理日志和构建产物。
    Docker 存储驱动健康 每周 执行 docker system df 查看镜像、容器、卷的占用空间,定期清理悬空镜像和停止的容器。
    构建与缓存 构建缓存命中率 每次构建 关注 Maven .m2、npm .npm、Docker 构建层缓存是否正常命中,若命中率持续下降需排查缓存策略。
    构建耗时趋势 每日 对比近一周的构建耗时,若出现异常增长(如从 3 分钟涨到 10 分钟),优先检查缓存和依赖下载环节。
    安全与权限 凭证轮换 每月 检查 Jenkins 凭据、GitLab Token、SSH 密钥等是否过期,及时轮换并更新到凭据管理工具中。
    镜像安全扫描 每次推送 若已集成 Harbor 或 Trivy,检查镜像是否存在高危漏洞,及时更新基础镜像版本。
    监控与告警 告警规则有效性 每月 检查 Prometheus 告警规则是否覆盖所有关键指标(如构建失败率、部署成功率、错误率),避免告警沉默。
    告警通知可达性 每月 模拟触发一次告警(如手动停止服务),确认钉钉/邮件/企业微信通知能正常送达。
    文档与流程 故障库更新 每次踩坑后 将新遇到的故障及排查过程记录到团队 Wiki 或故障库中,避免重复踩坑。
    部署回滚演练 每季度 在测试环境模拟一次部署失败,验证自动回滚流程是否正常,包括 Nginx 切换、容器停止、告警通知等环节。

    维护建议:建议将以上巡检项集成到自动化脚本中,每周定时执行并输出巡检报告。对于告警规则和回滚演练这类需要人工参与的检查项,可在团队周会上同步执行结果。CI/CD 体系的稳定性不是一劳永逸的,需要持续投入维护精力,才能确保自动化交付体系长期可靠运行。

    赞(0)
    未经允许不得转载:171主机测评 » 中小公司 CI/CD 流水线搭建实战:从 Jenkins 到 GitLab CI 的踩坑记录
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址