构建有多慢,数据说了算:用Prometheus监控CI/CD流水线中Docker构建性能

构建有多慢,数据说了算:用Prometheus监控CI/CD流水线中Docker构建性能

你有没有遇到过这种情况:开发说"代码提交半小时了,镜像还没构建完",你登录CI Runner一看——CPU 100%、磁盘I/O打满、构建队列堵了二十个任务。但问题是,下一次发生的时候,你才知道。

可观测性的核心原则是:如果不能用数据衡量,就没法优化。今天聊聊怎么用Prometheus把Docker构建的每一步都量化出来。

一、Docker构建的可观测性痛点

Docker构建本身是一个黑盒——你只知道它"慢",但不知道慢在哪一步。是拉取基础镜像慢?是下载依赖慢?还是编译慢?

我们先暴露容器引擎的底层指标:

# docker-daemon.json — 开启容器引擎指标
{
  "metrics-addr": "0.0.0.0:9323",
  "experimental": true,
  "metrics": true
}

然后让Prometheus来采集:

# prometheus.yml — 添加Docker daemon指标采集
scrape_configs:
  - job_name: 'docker-daemon'
    metrics_path: /metrics
    static_configs:
      - targets:
        - 'runner-01:9323'
        - 'runner-02:9323'
        - 'runner-03:9323'
  - job_name: 'docker-containers'
    metrics_path: /metrics/containers
    static_configs:
      - targets:
        - 'runner-01:9323'
        - 'runner-02:9323'
        - 'runner-03:9323'

这样我们就获得了Docker引擎层面的基础指标:

# 构建容器的CPU使用率
rate(container_cpu_usage_seconds_total{container_label_org_label_schema_build="true"}[1m]) * 100

# 构建容器的内存使用
container_memory_working_set_bytes{container_label_org_label_schema_build="true"}

# 磁盘读写
rate(container_fs_writes_bytes_total{container_label_org_label_schema_build="true"}[1m])

二、精确到构建步骤的监控

Docker daemon指标只能看到容器级别的数据,不够精细。我们要的是:每一步用了多少时间

方案一:Docker Buildx Bake的JSON输出

Docker Buildx支持输出构建过程的JSON日志:

# 构建并输出JSON格式日志
docker buildx build --progress=plain --no-cache \
  -t app:latest . 2>&1 | jq -R 'fromjson? | select(. != null)' 

但这样不如直接instrument代码来得优雅。

方案二:Custom Exporter — 构建性能采集器

我们写了一个Python服务,作为Prometheus的Exporter,专门暴露构建指标:

# build_exporter.py — 自定义Exporter
import time
import subprocess
import json
import os
from prometheus_client import start_http_server, Gauge, Histogram, Counter
import re

# 定义指标
BUILD_DURATION = Histogram(
    'ci_docker_build_duration_seconds',
    'Docker build duration by stage',
    ['stage', 'project', 'branch'],
    buckets=(5, 10, 30, 60, 120, 300, 600, 900, 1800)
)

BUILD_STATUS = Counter(
    'ci_docker_build_status_total',
    'Docker build status count',
    ['project', 'branch', 'status']
)

CACHE_HIT = Gauge(
    'ci_docker_build_cache_hit_ratio',
    'Docker build cache hit ratio',
    ['project']
)

IMAGE_SIZE = Gauge(
    'ci_docker_build_image_size_bytes',
    'Built image size in bytes',
    ['project', 'tag']
)

class DockerBuildMonitor:
    def __init__(self):
        self.build_history = {}
        
    def parse_build_output(self, output, project, branch):
        """解析构建输出,提取各阶段耗时"""
        stages = {}
        current_stage = None
        
        for line in output.split('\n'):
            # 匹配Docker构建阶段输出
            stage_match = re.match(r'#\d+ (?:DONE|ERROR)\s+(\d+\.\d+)s\s+(.+)', line)
            if stage_match:
                duration = float(stage_match.group(1))
                stage_name = stage_match.group(2).strip()
                stages[stage_name] = duration
                
                # 记录到Prometheus
                BUILD_DURATION.labels(
                    stage=stage_name,
                    project=project,
                    branch=branch
                ).observe(duration)
        
        return stages
    
    def monitor_build(self, project, branch, dockerfile_path='.'):
        """监控一次Docker构建"""
        start_time = time.time()
        
        # 执行构建(使用BuildKit的JSON输出)
        cmd = [
            'docker', 'buildx', 'build',
            '--progress=plain',
            '-t', f'app:{project}-{branch}',
            dockerfile_path
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        total_duration = time.time() - start_time
        
        # 解析构建输出
        stages = self.parse_build_output(result.stdout, project, branch)
        
        # 记录构建状态
        status = 'success' if result.returncode == 0 else 'failure'
        BUILD_STATUS.labels(
            project=project,
            branch=branch,
            status=status
        ).inc()
        
        # 记录镜像大小
        if status == 'success':
            size_cmd = [
                'docker', 'image', 'inspect',
                f'app:{project}-{branch}',
                '--format', '{{.Size}}'
            ]
            size_result = subprocess.run(size_cmd, capture_output=True, text=True)
            if size_result.returncode == 0:
                IMAGE_SIZE.labels(
                    project=project,
                    tag=f'{project}-{branch}'
                ).set(int(size_result.stdout.strip()))
        
        return {
            'project': project,
            'branch': branch,
            'total_duration': total_duration,
            'stages': stages,
            'status': status
        }

if __name__ == '__main__':
    # 启动Prometheus metrics HTTP服务
    start_http_server(8000)
    monitor = DockerBuildMonitor()
    
    # 持续监控(实际使用时通过CI/CD trigger触发)
    while True:
        time.sleep(60)

方案三:GitLab CI集成

# .gitlab-ci.yml — CI集成监控
variables:
  DOCKER_BUILDKIT: "1"
  BUILDKIT_PROGRESS: "plain"

before_script:
  - apk add --no-cache curl
  - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

build:
  stage: build
  script:
    # 开始构建,记录开始时间
    - BUILD_START=$(date +%s)
    - |
      docker buildx build \
        --cache-from $CI_REGISTRY_IMAGE:cache \
        --cache-to type=registry,ref=$CI_REGISTRY_IMAGE:cache,mode=max \
        -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA \
        -t $CI_REGISTRY_IMAGE:latest \
        --progress=plain \
        . 2>&1 | tee build.log
    # 解析日志并发指标到Exporter
    - BUILD_DURATION=$(($(date +%s) - BUILD_START))
    - |
      curl -X POST http://prometheus-pushgateway:9091/metrics/job/ci-build \
        -d "# HELP ci_build_duration Build duration in seconds
             # TYPE ci_build_duration gauge
             ci_build_duration{project=\"$CI_PROJECT_NAME\",branch=\"$CI_COMMIT_BRANCH\"} $BUILD_DURATION"

三、Grafana看板:构建性能一目了然

有了数据之后,我们搭了一个Grafana看板,展示以下关键指标:

看板一:构建耗时趋势

# 最近30次构建的耗时趋势
histogram_quantile(0.95, 
  sum(rate(ci_docker_build_duration_seconds_bucket[7d])) by (le)
)

# 分项目的平均构建时间
avg(ci_docker_build_duration_seconds) by (project)

看板二:缓存命中率

# 缓存命中率(通过layer reuse情况估算)
ci_docker_build_cache_hit_ratio{project="payment-service"}

看板三:资源使用与构建并发

# Runner节点CPU使用率 vs 构建任务数
sum(rate(node_cpu_seconds_total{mode!="idle"}[5m])) by (instance)
# 结合构建队列长度
ci_docker_build_status_total{status="running"}

看板四:各阶段耗时明细

构建阶段耗时排名(Top 10):
1. RUN mvn clean package — 平均 245s
2. COPY target/*.jar — 平均 12s
3. FROM maven:3.8 — 平均 8s
4. RUN apk add — 平均 3s
...

这个排名能精准告诉我们:优化重点应该放在Maven编译阶段

四、基于监控数据的优化循环

有了数据驱动的闭环,我们的优化流程变成了:

监控采集 → 数据分析 → 定位瓶颈 → 实施优化 → 验证效果 → 持续监控

举个例子,通过看板我们发现payment-service的构建中,RUN go mod download耗时高达180秒。进一步分析发现,是因为没有启用cache mount:

# 优化前
FROM golang:1.21-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download  # 每次都要完整下载,耗时180s

# 优化后
# syntax=docker/dockerfile:1.4
FROM golang:1.21-alpine
WORKDIR /app
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=bind,source=go.mod,target=go.mod \
    --mount=type=bind,source=go.sum,target=go.sum \
    go mod download  # 命中缓存,耗时3s

优化后该阶段耗时从180秒降到3秒,降幅98%。

结语

可观测性不只是监控线上服务。CI/CD流水线的性能同样需要被量化、被监控、被优化。用Prometheus把Docker构建的每一步都变成可追踪的指标,你就能从一个"凭经验猜测"的运维,变成一个"用数据说话"的工程师。

Kubernetes有一句名言:"You can't fix what you can't see"。这句同样适用于CI/CD。

本文作者:侯万里(万里侯),云原生运维工程师,专注CI/CD流水线可观测性与性能优化

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐