前言
术语与缩写说明
为方便不同背景的读者快速理解本文内容,以下表格整理了文中频繁出现的专业术语和缩写及其含义:
| 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 错误。
排查过程:
解决方案:不要一次性安装大量插件,建议按需安装,并锁定插件版本。我最终使用的插件清单如下:
# 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。
排查过程:
解决方案:
# 方案一:关闭 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
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 仍然重新下载所有依赖。
排查过程:
解决方案:
# 优化后的 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>/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 ] && [ $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}' > ${ACTIVE_FILE}
nginx -s reload
echo '已回滚到 ${ROLLBACK_TARGET}'
" 2>&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>&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)关键指标:
| 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 关键指标:
| 活跃连接数 | 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 > 500ms)———-
– alert: HighLatencyP99
expr: |
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket[5m])) by (le, instance, uri)
) > 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 > 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]) > 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 等插件版本不兼容,部分插件下载不完整。 |
| 排查步骤 |
|
故障二:GitLab Runner 注册失败
| 问题现象 | 执行 gitlab-runner register 时报错 ERROR: Registering runner… failed runner=xxx status=401 Unauthorized。 |
| 可能原因 | GitLab 使用自签名证书,Runner 容器内未信任该 CA 证书,导致 SSL 握手失败。 |
| 排查步骤 |
|
故障三:Docker 构建缓存失效
| 问题现象 | 每次 Jenkins 构建都重新下载 Maven 依赖,构建时间从 3 分钟飙升到 15 分钟。 |
| 可能原因 | Jenkins 每次构建清理工作空间,导致 pom.xml 时间戳变化,Docker 缓存层失效。 |
| 排查步骤 |
|
故障四:npm ci 缓存失效
| 问题现象 | GitLab CI 中虽然配置了 cache 关键字,但每次 Pipeline 运行时 npm ci 仍然重新下载所有依赖。 |
| 可能原因 | 缓存 key 随分支变化导致缓存不命中;npm ci 会删除 node_modules 重新安装;node_modules/ 目录不适合直接缓存。 |
| 排查步骤 |
|
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 体系的稳定性不是一劳永逸的,需要持续投入维护精力,才能确保自动化交付体系长期可靠运行。



