第 215 章:CI/CD 与监控
学习目标
- 掌握 GitHub Actions 工作流
- 学会 Jenkins Pipeline
- 部署 Prometheus + Grafana 监控
- 搭建 ELK 日志聚合
- 配置告警与值班
一、CI/CD 简介
CI/CD 是 持续集成 / 持续交付 / 持续部署 的实践,自动化构建、测试、发布流程。
| 缩写 | 含义 |
|---|---|
| CI | Continuous Integration(持续集成) |
| CD | Continuous Delivery / Deployment(持续交付 / 部署) |
二、GitHub Actions
2.1 基础概念
| 概念 | 说明 |
|---|---|
| Workflow | YAML 配置文件(.github/workflows/*.yml) |
| Job | 一组步骤的集合,跑在 Runner 上 |
| Step | 单个任务(shell 命令或 action) |
| Action | 可复用单元 |
| Runner | 执行环境(ubuntu / windows) |
2.2 Hello World
.github/workflows/ci.yml:
yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Maven
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2-
- name: Build
run: mvn clean package -DskipTests
- name: Test
run: mvn test2.3 构建 Docker 镜像并推送
yaml
name: Build & Push
on:
push:
tags: ['v*']
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build & Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
user/myapp:latest
user/myapp:${{ github.ref_name }}
cache-from: type=gha
cache-to: type=gha,mode=max2.4 部署到 K8s
yaml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v4
- name: Set Kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
- name: Apply manifests
run: |
kubectl set image deployment/myapp \
myapp=user/myapp:${{ github.sha }} \
-n production
- name: Wait for rollout
run: kubectl rollout status deployment/myapp -n production2.5 Matrix 矩阵
yaml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java: [11, 17, 21]
node: [18, 20]
steps:
- uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}2.6 复用 Workflow
yaml
# .github/workflows/reusable.yml
on:
workflow_call:
inputs:
environment:
required: true
type: string
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- run: echo "deploying to ${{ inputs.environment }}"调用方:
yaml
jobs:
call-workflow:
uses: ./.github/workflows/reusable.yml
with:
environment: production三、Jenkins
3.1 安装
bash
# Docker
docker run -d -p 8080:8080 \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/jenkins:lts
# 首次密码
cat /var/jenkins_home/secrets/initialAdminPassword3.2 Pipeline(Jenkinsfile)
groovy
pipeline {
agent any
tools {
maven 'Maven-3.9'
jdk 'JDK-17'
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'https://github.com/user/myapp.git',
credentialsId: 'github-token'
}
}
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Test') {
steps {
sh 'mvn test'
junit '**/target/surefire-reports/*.xml'
}
}
stage('Docker Build') {
steps {
script {
docker.build("user/myapp:${env.BUILD_ID}")
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'kubectl set image deployment/myapp myapp=user/myapp:${BUILD_ID}'
}
}
}
post {
success {
echo '构建成功'
mail to: 'team@example.com',
subject: "Build Success: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
}
failure {
mail to: 'team@example.com',
subject: "Build Failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
}
}
}3.3 声明式 vs 脚本式
groovy
// 脚本式 Pipeline
node {
stage('Build') {
sh 'mvn clean package'
}
}3.4 共享库
vars/sayHello.groovy:
groovy
def call(String name) {
echo "Hello ${name}"
}Jenkinsfile:
groovy
@Library('my-shared-lib') _
sayHello('World')四、Prometheus 监控
4.1 简介
4.2 安装 Prometheus
yaml
# docker-compose.yml
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prom_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.enable-lifecycle'
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
depends_on:
- prometheus
volumes:
prom_data:
grafana_data:4.3 prometheus.yml
yaml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Prometheus 自身
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# 应用 /metrics
- job_name: 'myapp'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['host.docker.internal:8080']
# node-exporter(主机监控)
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
# K8s 服务发现
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod4.4 Spring Boot 集成
pom.xml:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>application.yml:
yaml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}访问 http://localhost:8080/actuator/prometheus 即可看到指标。
4.5 自定义指标
java
@RestController
public class OrderController {
@Autowired
private MeterRegistry registry;
private final Counter orderCounter;
private final Timer orderTimer;
public OrderController(MeterRegistry registry) {
this.registry = registry;
this.orderCounter = Counter.builder("orders_total")
.tag("type", "success")
.register(registry);
this.orderTimer = Timer.builder("order_duration")
.register(registry);
}
@PostMapping("/order")
public Result create() {
Timer.Sample sample = Timer.start(registry);
try {
// 业务逻辑
orderCounter.increment();
return Result.ok();
} finally {
sample.stop(orderTimer);
}
}
}4.6 PromQL 查询
promql
# QPS(每分钟订单数)
rate(orders_total[1m])
# 99 分位响应时间
histogram_quantile(0.99, sum(rate(order_duration_seconds_bucket[5m])) by (le))
# CPU 使用率
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# 内存使用
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100五、Grafana 可视化
5.1 添加 Prometheus 数据源
Configuration → Data Sources → Prometheus
URL: http://prometheus:9090
5.2 JVM 仪表板
导入 4701(JVM Micrometer) 即可看到完整的 JVM 监控面板。
5.3 自定义面板
json
{
"panels": [
{
"title": "QPS",
"type": "graph",
"targets": [
{
"expr": "rate(orders_total[1m])"
}
]
},
{
"title": "P99 延迟",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.99, rate(order_duration_seconds_bucket[5m]))"
}
]
}
]
}六、AlertManager 告警
6.1 alertmanager.yml
yaml
global:
resolve_timeout: 5m
route:
receiver: 'default'
group_by: ['alertname', 'cluster']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- match:
severity: critical
receiver: 'critical'
receivers:
- name: 'default'
webhook_configs:
- url: 'http://dingtalk-webhook/alert'
- name: 'critical'
webhook_configs:
- url: 'http://dingtalk-webhook/critical'
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'cluster']6.2 告警规则
prometheus/rules/app.yml:
yaml
groups:
- name: app
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) /
sum(rate(http_requests_total[5m])) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "错误率过高"
description: "{{ $labels.instance }} 5xx 错误率 {{ $value }}"
- alert: HighResponseTime
expr: |
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "P99 响应时间过高"
description: "当前 {{ $value }}s"
- alert: HighCPUUsage
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 10m
labels:
severity: warning七、ELK 日志聚合
7.1 架构
7.2 docker-compose
yaml
version: '3.8'
services:
elasticsearch:
image: elasticsearch:8.11.0
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms512m -Xmx512m
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data
kibana:
image: kibana:8.11.0
ports:
- "5601:5601"
depends_on:
- elasticsearch
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
filebeat:
image: elastic/filebeat:8.11.0
user: root
volumes:
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
- elasticsearch
volumes:
es_data:7.3 filebeat.yml
yaml
filebeat.inputs:
- type: container
paths:
- /var/lib/docker/containers/*/*.log
processors:
- add_docker_metadata: ~
output.elasticsearch:
hosts: ["elasticsearch:9200"]
indices:
- index: "app-logs-%{+yyyy.MM.dd}"
setup.kibana:
host: "kibana:5601"
logging.level: info7.4 Spring Boot 日志输出 JSON
logback-spring.xml:
xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<fieldNames>
<timestamp>@timestamp</timestamp>
<message>message</message>
<thread>thread</thread>
<logger>logger</logger>
<level>level</level>
</fieldNames>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>依赖:
xml
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>7.5 Kibana 查询
status:500 AND app:myapp
# 错误日志 + 堆栈
level:ERROR AND stack_trace:*
# 慢请求
duration:>1000
# 按服务聚合
GET app-logs-*/_search
{
"aggs": {
"by_service": {
"terms": { "field": "service.keyword" }
}
}
}八、Loki(轻量级日志方案)
比 ELK 更轻量,适合 K8s 环境。
yaml
services:
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
promtail:
image: grafana/promtail:latest
volumes:
- /var/log:/var/log:ro
- ./promtail.yml:/etc/promtail/config.ymlpromtail.yml:
yaml
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: system
static_configs:
- targets: [localhost]
labels:
job: syslog
__path__: /var/log/*.log九、K8s 监控方案
9.1 kube-prometheus-stack
bash
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install kube-prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace包括:
- Prometheus Operator
- Prometheus + AlertManager
- Grafana(预置仪表板)
- node-exporter / kube-state-metrics
- ServiceMonitor 自定义资源
9.2 ServiceMonitor
yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: myapp-monitor
labels:
release: kube-prometheus
spec:
selector:
matchLabels:
app: myapp
endpoints:
- port: http
path: /actuator/prometheus
interval: 15s应用 Service:
yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
labels:
app: myapp
spec:
ports:
- name: http
port: 8080
selector:
app: myapp十、APM(应用性能监控)
10.1 SkyWalking
yaml
# agent 下载
wget https://archive.apache.org/dist/skywalking/...
# 启动参数
java -javaagent:/path/skywalking-agent.jar \
-Dskywalking.agent.service_name=myapp \
-Dskywalking.collector.backend_service=127.0.0.1:11800 \
-jar myapp.jar可追踪:
- 服务拓扑
- 慢 SQL
- 慢调用链
- JVM 实时指标
10.2 替代方案
- Pinpoint:韩国出品,功能强大
- Zipkin:Twitter 开源,轻量
- Jaeger:Uber 开源,云原生
十一、SLO 与值班
11.1 SLO 定义
yaml
# Service Level Objective
apiVersion: sloth.slok.dev/v1
kind: PrometheusServiceLevel
metadata:
name: myapp-slo
spec:
service: myapp
labels:
owner: team-backend
slos:
- name: availability
objective: 99.9
description: "99.9% 可用性"
events:
error_query: sum(rate(http_requests_total{status=~"5.."}[5m]))
total_query: sum(rate(http_requests_total[5m]))11.2 错误预算
SLO = 99.9%
30 天 = 30 × 24 × 60 = 43200 分钟
错误预算 = 0.1% × 43200 = 43.2 分钟11.3 值班 OnCall
| 级别 | 响应时间 |
|---|---|
| P0(宕机) | 5 分钟 |
| P1(严重) | 30 分钟 |
| P2(一般) | 2 小时 |
| P3(提示) | 下个工作日 |
工具: PagerDuty / OpsGenie / 自建
十二、本章小结
| 工具 | 用途 |
|---|---|
| GitHub Actions | 轻量 CI/CD |
| Jenkins | 传统 CI/CD |
| Prometheus | 指标采集与存储 |
| Grafana | 可视化仪表板 |
| AlertManager | 告警路由 |
| ELK / Loki | 日志聚合 |
| SkyWalking | 链路追踪 |
动手练习
- 编写 GitHub Actions 自动构建并推送镜像
- 用 docker-compose 部署 Prometheus + Grafana
- Spring Boot 暴露
/actuator/prometheus,在 Grafana 中配置仪表板 - 配置告警规则,触发后发送钉钉消息
- 用 Filebeat 收集容器日志,接入 ELK