一、为什么要把Gatling塞进容器里

做性能测试这事儿,最头疼的不是写脚本,而是"我这台机器能跑,你那台跑不动"。想象一下这个场景:开发同事在你工位上跑一遍Gatling压测,结果TPS是8000;到了你机器上同样脚本,TPS掉到5000,参数配置完全一样。你开始怀疑人生,反复检查GC参数、线程池大小、网络配置,折腾半天发现是操作系统内核版本不一样、DNS解析缓存策略不同。更别提测试环境、预发环境、生产环境各自为政,每次环境切换都是一次"玄学调试"。

Gatling本身是一款基于Scala的压测工具,跑在JVM上,对运行环境非常敏感。JDK版本、OS内核参数、文件描述符限制、网络栈配置,任何一个环节有差异都可能影响压测结果。如果每次都在本地裸跑,不同开发者之间的压测数据基本没法横向对比。

把Gatling打包进Docker容器,本质上就是把运行环境"冻结"住——不管你在哪台机器上跑,只要基础镜像一致,JDK版本一致、OS内核参数一致、网络配置一致,压测结果的可复现性就有了保障。这篇文章就来讲清楚:怎么把Gatling容器化,怎么用docker-compose管理多环境一致性,以及怎么和CI/CD流水线打通,让压测成为工程化的能力而不是某个人的"手工艺品"。

二、搭建基础环境

2.1 准备工作清单

在动手之前,我们需要以下几个组件:

  • Docker:容器运行时,版本20.10+
  • docker-compose:多容器编排工具,版本2.x
  • JDK:Gatling运行的JVM环境,建议使用JDK 11或17 LTS版本
  • Gatling:压测引擎,版本3.9+
  • Jenkins(可选):CI/CD流水线,用于自动化触发压测

下面以JDK 11 + Gatling 3.9.5为例展开。

2.2 拉取基础镜像

# =====================================================
# 技术栈:Docker + Gatling + docker-compose + Jenkins
# 基础镜像:Eclipse Temurin JDK 11(Alpine版,体积小)
# =====================================================

# 第一步:声明基础镜像,使用Alpine版JDK减小镜像体积
FROM eclipse-temurin:11-jdk-alpine

# 第二步:设置元数据标签,方便后续溯源
LABEL maintainer="performance-team@company.com" \
      description="Gatling性能测试容器镜像" \
      version="3.9.5" \
      java-version="11"

# 第三步:安装必要的系统依赖
# - curl: 用于下载Gatling发行包
# - ca-certificates: SSL证书,下载HTTPS资源需要
# - tzdata: 时区数据,压测报告时间戳依赖正确时区
RUN apk add --no-cache \
    curl \
    ca-certificates \
    tzdata && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

# 第四步:设置Gatling安装目录
ENV GATLING_HOME=/opt/gatling \
    GATLING_VERSION=3.9.5

# 第五步:创建Gatling目录结构
RUN mkdir -p ${GATLING_HOME}/results \
    && mkdir -p ${GATLING_HOME}/binaries \
    && mkdir -p ${GATLING_HOME}/simulation

# 第六步:下载Gatling发行包并解压
# 注意:这里假设构建环境可以访问外网
RUN curl -fsSL \
    https://github.com/gatling/gatling/releases/download/v${GATLING_VERSION}/gatling-${GATLING_VERSION}.zip \
    -o /tmp/gatling.zip && \
    unzip /tmp/gatling.zip -d ${GATLING_HOME} && \
    rm -f /tmp/gatling.zip

# 第七步:设置Gatling环境变量和工作目录
ENV PATH="${PATH}:${GATLING_HOME}/bin"
WORKDIR ${GATLING_HOME}

# 第八步:挂载点声明——压测结果和自定义脚本
VOLUME ["/opt/gatling/results", "/opt/gatling/user-projects"]

# 第九步:暴露Gatling HTTP运行器端口(分布式场景用)
EXPOSE 8333

# 第十步:默认启动命令
# 不指定脚本时,进入交互式环境便于调试
CMD ["gatling", "-simulationClass", "com.example.Simulation"]

这个Dockerfile的关键点在于:使用Alpine基础镜像可以显著减小镜像体积(相比Ubuntu基础镜像,大约能省150MB);时区设置放在时区数据安装之后,确保ln -sf命令能正确链接;下载Gatling后清理临时zip文件,减小构建层的体积。

三、构建和运行Gatling容器

3.1 镜像构建

# 在项目根目录执行构建
# --no-cache 确保每次都拉取最新的基础镜像和依赖
docker build -t gatling-runner:3.9.5 .

# 验证镜像是否构建成功
docker images | grep gatling-runner

3.2 编写Gatling模拟脚本

// =====================================================
// 文件:user-projects/src/main/scala/com/example/UserSimulation.scala
// 说明:一个典型的电商首页压测场景
// =====================================================

package com.example

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

// 继承Simulation类,这是所有Gatling压测脚本的基类
class UserSimulation extends Simulation {

  // -------------------------------------------------------
  // 1. HTTP协议配置:设置基础URL和超时时间
  // -------------------------------------------------------
  val httpProtocol = http
    .baseUrl("http://localhost:8080")  // 被测应用地址
    .acceptHeader("application/json")
    .acceptLanguageHeader("zh-CN,zh;q=0.9")
    .userAgentHeader("Gatling-Runner/3.9.5")
    .connectTimeout(5000)   // 连接超时:5秒
    .responseTimeout(10000)  // 响应超时:10秒

  // -------------------------------------------------------
  // 2. 用户场景定义:模拟单个用户的请求链路
  //    用户访问首页 → 获取商品详情 → 加入购物车 → 提交订单
  // -------------------------------------------------------
  val userScenario = scenario("用户购物链路")
    .exec(
      // 请求1:获取首页数据
      http("获取首页")
        .get("/api/home")
        .check(status.is(200))  // 断言状态码为200
    )
    .pause(2.seconds)  // 模拟用户思考时间:2秒

    .exec(
      // 请求2:获取商品详情(带参数化,模拟不同商品ID)
      http("获取商品详情")
        .get("/api/products/#{productId}")
        .check(status.is(200))
        .check(jsonPath("$.price").exists)  // 断言响应中有price字段
    )
    .pause(1, 3.seconds)  // 随机思考时间:1到3秒之间

    .exec(
      // 请求3:加入购物车
      http("加入购物车")
        .post("/api/cart")
        .body(StringBody("""{"productId":"#{productId}","quantity":1}"""))
        .contentType("application/json")
        .check(status.is(200))
    )
    .pause(3.seconds)

    .exec(
      // 请求4:提交订单(只有30%的用户会走到这一步)
      http("提交订单")
        .post("/api/orders")
        .body(StringBody("""{"cartId":"#{cartId}"}"""))
        .contentType("application/json")
        .check(status.is(200))
    )

  // -------------------------------------------------------
  // 3. 场景组合:定义多用户并发和比例
  //    - 70%用户完成完整的购物链路
    //    - 30%用户只浏览首页和商品详情(模拟流失用户)
  // -------------------------------------------------------
  val combinedScenario = scenario("混合用户场景")
    .forever {
      // 70%概率走完整链路
      exec(
        http("首页-浏览型")
          .get("/api/home")
          .check(status.is(200))
      )
      .pause(2, 5.seconds)
      .exec(
        http("商品详情-浏览型")
          .get("/api/products/#{productId}")
          .check(status.is(200))
      )
      .pause(3, 8.seconds)
      // 30%概率走完整购物链路(包含下单)
      .exec(
        http("下单")
          .post("/api/orders")
          .body(StringBody("""{"cartId":"#{cartId}"}"""))
          .check(status.is(200))
      )
    }

  // -------------------------------------------------------
  // 4. 负载模型配置:逐步加压
  //    先保持10用户跑30秒,然后每秒增加5个用户
  //    到100用户后保持2分钟,再阶梯式降到0
  // -------------------------------------------------------
  setUp(
    combinedScenario.inject(
      // 阶段1:基线测试——10个用户,持续30秒
      constantUsersPerSec(10) during(30.seconds),
      // 阶段2:逐步加压——每秒增加5个用户,目标100用户
      rampUsersPerSec(10 to 100) during(180.seconds),
      // 阶段3:峰值保持——维持100用户,持续2分钟
      constantUsersPerSec(100) during(120.seconds),
      // 阶段4:阶梯降载——每分钟减20用户,直到0
      reduceUsersPerSec(100 to 0).during(5.minutes)
    )
  ).protocols(httpProtocol)
    // 全局配置:并发线程数和超时
    .globalMaxThreads(500)     // 全局最大并发线程数
    .globalMaxPendingRequests(1000)  // 全局最大待处理请求数
    .maxActiveSimulations(5)   // 最大活跃模拟数
}

3.3 用docker-compose管理环境一致性

这是实现环境一致性的核心。我们把Gatling容器、被测应用容器、以及依赖服务(数据库、缓存)都放在一起管理:

# =====================================================
# 文件:docker-compose.yml
# 说明:压测环境编排配置,确保开发/测试/预发环境一致性
# =====================================================

version: "3.8"

services:
  # ---------------------------------------------------
  # 1. Gatling压测容器
  # ---------------------------------------------------
  gatling-runner:
    build:
      context: ./docker          # Dockerfile所在目录
      dockerfile: Dockerfile     # Dockerfile文件名
      args:
        GATLING_VERSION: "3.9.5"
    image: gatling-runner:3.9.5
    container_name: gatling-runner
    hostname: gatling-runner
    environment:
      - TZ=Asia/Shanghai                    # 统一时区
      - JAVA_OPTS=-Xms512m -Xmx2048m       # JVM堆内存配置
      - GATLING_ARGS=-Dgatling.home=/opt/gatling  # Gatling home路径
    volumes:
      # 挂载用户脚本目录——修改脚本无需重新构建镜像
      - ./user-projects:/opt/gatling/user-projects
      # 挂载结果输出目录——压测报告落盘
      - ./reports:/opt/gatling/results
      # 挂载配置文件——不同环境可覆盖配置
      - ./config/gatling.conf:/opt/gatling/gatling.conf:ro
    depends_on:
      # 确保被测应用和依赖服务先启动
      - app-server
      - redis-cache
      - mysql-db
    networks:
      - perf-test-network
    # 资源限制:防止压测容器抢占宿主机过多资源
    deploy:
      resources:
        limits:
          cpus: "4.0"     # 最多使用4个CPU核心
          memory: 4G      # 最多使用4GB内存
        reservations:
          cpus: "2.0"
          memory: 2G

  # ---------------------------------------------------
  # 2. 被测应用容器
  # ---------------------------------------------------
  app-server:
    image: my-app-server:2.3.1
    container_name: app-server
    hostname: app-server
    ports:
      - "8080:8080"       # 宿主机8080映射到容器8080
    environment:
      - TZ=Asia/Shanghai
      - JAVA_OPTS=-Xms256m -Xmx1024m
      - SPRING_PROFILES_ACTIVE=perf-test   # 激活压测专用配置
      - DB_HOST=mysql-db
      - DB_PORT=3306
      - REDIS_HOST=redis-cache
    volumes:
      - ./config/application-perf-test.yml:/app/config/application-perf-test.yml:ro
    depends_on:
      - mysql-db
      - redis-cache
    networks:
      - perf-test-network
    healthcheck:
      # 健康检查:确保应用启动完成后再让Gatling开始压测
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  # ---------------------------------------------------
  # 3. Redis缓存服务
  # ---------------------------------------------------
  redis-cache:
    image: redis:7.2-alpine
    container_name: redis-cache
    hostname: redis-cache
    command: ["redis-server", "--maxmemory", "512mb", "--maxmemory-policy", "allkeys-lru"]
    networks:
      - perf-test-network

  # ---------------------------------------------------
  # 4. MySQL数据库
  # ---------------------------------------------------
  mysql-db:
    image: mysql:8.0
    container_name: mysql-db
    hostname: mysql-db
    environment:
      - MYSQL_ROOT_PASSWORD=perf_test_root
      - MYSQL_DATABASE=perf_test_db
      - MYSQL_USER=perf_user
      - MYSQL_PASSWORD=perf_user_pass
      - TZ=Asia/Shanghai
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --innodb-buffer-pool-size=512M
      - --max-connections=200
    volumes:
      - mysql-data:/var/lib/mysql
      - ./config/mysql-init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - perf-test-network

  # ---------------------------------------------------
  # 5. 统一网络配置
  # ---------------------------------------------------
networks:
  perf-test-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16   # 自定义子网,避免与宿主机冲突
          gateway: 172.20.0.1

  # ---------------------------------------------------
  # 6. 数据卷声明(持久化存储)
  # ---------------------------------------------------
volumes:
  mysql-data:
    driver: local

这个docker-compose配置的几个关键点需要理解:

关于depends_on和healthcheck的配合:单纯用depends_on只能保证容器启动顺序,但不能保证服务真正就绪。比如MySQL容器启动了,但数据库还没完成初始化,这时候Gatling去连数据库会报错。通过给app-server加上healthcheck,depends_on就会等待健康检查通过后才允许后续容器启动。

关于资源限制deploy.resources:这个配置非常实用。压测时如果不限资源,Gatling容器可能把宿主机资源吃光,导致宿主机上的其他容器(比如监控、日志服务)被拖垮。限制CPU和内存后,即使压测挂了也不会影响其他服务。

关于网络ipam:自定义子网可以避免和宿主机Docker网络冲突。特别是当你在宿主机上运行多个docker-compose项目时,默认网络可能会重叠导致端口或IP冲突。

3.4 多环境配置管理

环境一致性的另一个关键点是:不同环境(开发、测试、预发、生产镜像)使用不同的配置参数。我们通过环境变量和配置文件挂载来实现:

# =====================================================
# 文件:docker-environment.yml
# 说明:不同环境的Gatling压测参数配置
# =====================================================

version: "3.8"

services:
  gatling-runner:
    # 通过override文件覆盖基础配置
    profiles:
      - dev
      - staging
      - production-mirror

# =====================================================
# 开发环境配置(docker-compose.override.yml - dev)
# 低并发、短持续时间,用于快速验证
# =====================================================
# x-dev-env:
#   environment:
#     - GATLING_SCENARIO=DevQuickTest
#     - GATLING_USERS=5
#     - GATLING_DURATION=60
#     - GATLING_ARGS=-Dgatling.home=/opt/gatling
#     - SPRING_PROFILES_ACTIVE=dev

# =====================================================
# 预发环境配置(docker-compose.override.yml - staging)
# 中等并发,模拟生产流量模式
# =====================================================
# x-staging-env:
#   environment:
#     - GATLING_SCENARIO=StagingFullTest
#     - GATLING_USERS=50
#     - GATLING_DURATION=1800
#     - SPRING_PROFILES_ACTIVE=staging

# =====================================================
# 生产镜像环境配置(docker-compose.override.yml - production-mirror)
# 高并发,复现生产环境配置
# =====================================================
# x-production-env:
#   environment:
#     - GATLING_SCENARIO=ProductionMirrorTest
#     - GATLING_USERS=200
#     - GATLING_DURATION=3600
#     - SPRING_PROFILES_ACTIVE=production

实际使用时,我们创建三个override文件,用--profile参数切换:

# =====================================================
# 启动开发环境压测(低并发、快速验证)
# =====================================================
docker compose --profile dev up -d

# =====================================================
# 启动预发环境压测(中等并发、全量验证)
# =====================================================
docker compose --profile staging up -d

# =====================================================
# 启动生产镜像环境压测(高并发、压测峰值)
# =====================================================
docker compose --profile production-mirror up -d

# =====================================================
# 停止所有容器并清理
# =====================================================
docker compose --profile dev down --volumes

四、Gatling容器镜像的优化技巧

4.1 多阶段构建减小镜像体积

前面直接下载的Gatling发行包其实包含了很多不需要的东西。用多阶段构建可以更精细地控制最终镜像内容:

# =====================================================
# 技术栈:Docker + Gatling + docker-compose + Jenkins
# 说明:多阶段构建,显著减小最终镜像体积
# =====================================================

# ---- 第一阶段:构建阶段 ----
# 使用完整JDK编译用户脚本(如果需要编译)
FROM eclipse-temurin:17-jdk-alpine AS builder

# 安装SBT(Scala构建工具,用于编译Gatling脚本)
RUN apk add --no-cache sbt

# 复制用户脚本并编译
WORKDIR /build
COPY user-projects/ ./
RUN sbt compile

# ---- 第二阶段:运行阶段 ----
# 使用轻量JRE即可运行编译好的脚本
FROM eclipse-temurin:11-jre-alpine

LABEL maintainer="performance-team@company.com"

RUN apk add --no-cache curl ca-certificates tzdata unzip && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

ENV GATLING_HOME=/opt/gatling \
    GATLING_VERSION=3.9.5

# 从第一阶段复制编译好的脚本
COPY --from=builder /build/target/scala-2.13/classes/ ${GATLING_HOME}/user-projects/target/

# 下载并安装Gatling
RUN mkdir -p ${GATLING_HOME}/{results,binaries,simulation} && \
    curl -fsSL \
    https://github.com/gatling/gatling/releases/download/v${GATLING_VERSION}/gatling-${GATLING_VERSION}.zip \
    -o /tmp/gatling.zip && \
    unzip /tmp/gatling.zip -d ${GATLING_HOME} && \
    rm -f /tmp/gatling.zip

ENV PATH="${PATH}:${GATLING_HOME}/bin"
WORKDIR ${GATLING_HOME}

VOLUME ["/opt/gatling/results"]
EXPOSE 8333

CMD ["gatling", "-simulationClass", "com.example.UserSimulation"]

4.2 使用Dockerfile多环境构建参数

# =====================================================
# 构建不同环境的Gatling镜像
# =====================================================

# 开发环境:包含调试日志
docker build \
  --build-arg GATLING_VERSION=3.9.5 \
  --build-arg JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" \
  -t gatling-runner:dev \
  -f docker/Dockerfile.dev \
  .

# 生产镜像环境:优化JVM参数,减少日志输出
docker build \
  --build-arg GATLING_VERSION=3.9.5 \
  --build-arg JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDateStamps" \
  -t gatling-runner:production-mirror \
  -f docker/Dockerfile.prod \
  .

五、与CI/CD流水线集成

5.1 Jenkinsfile配置

// =====================================================
// 技术栈:Docker + Gatling + docker-compose + Jenkins
// 说明:Jenkins流水线,自动化执行Gatling压测
// =====================================================

pipeline {
    agent {
        // 使用Docker agent运行Jenkins任务
        docker {
            image 'gatling-runner:3.9.5'
            args '--privileged --memory=4g --cpus=4'
            // 挂载代码仓库和报告目录
            label 'perf-test-node'
        }
    }

    parameters {
        // 环境选择
        choice(
            name: 'TARGET_ENV',
            choices: ['dev', 'staging', 'production-mirror'],
            description: '选择压测目标环境'
        )
        // 并发用户数
        input(
            name: 'USER_COUNT',
            description: '并发用户数',
            defaultValue: '50',
            parameters: [
                [name: 'minValue', value: '5'],
                [name: 'maxValue', value: '500']
            ]
        )
        // 压测持续时间(秒)
        input(
            name: 'DURATION',
            description: '压测持续时间(秒)',
            defaultValue: '600'
        )
        // 是否清理旧数据
        booleanParam(
            name: 'CLEAN_DATA',
            description: '压测前清理测试数据',
            defaultValue: true
        )
    }

    environment {
        // 环境变量
        GATLING_HOME = '/opt/gatling'
        RESULTS_DIR = "${env.WORKSPACE}/reports"
    }

    options {
        // 超时设置:最长运行1小时
        timeout(time: 60, unit: 'MINUTES')
        // 禁止并发构建
        disableConcurrentBuilds()
        // 自动清理工作空间
        cleanWs()
    }

    stages {
        // ---------------------------------------------------
        // 阶段1:构建并启动依赖服务
        // ---------------------------------------------------
        stage('准备环境') {
            steps {
                sh '''
                    echo "=== 构建并启动依赖服务 ==="
                    # 构建镜像
                    docker compose build --no-cache
                    # 启动依赖服务(数据库、缓存、应用)
                    docker compose up -d app-server redis-cache mysql-db
                    # 等待应用健康检查通过
                    echo "等待应用启动..."
                    sleep 30
                    # 验证应用可用性
                    curl -f http://localhost:8080/actuator/health || exit 1
                    echo "依赖服务已就绪"
                '''
            }
        }

        // ---------------------------------------------------
        // 阶段2:执行Gatling压测
        // ---------------------------------------------------
        stage('执行压测') {
            steps {
                sh '''
                    echo "=== 开始执行Gatling压测 ==="
                    echo "环境: ${TARGET_ENV}"
                    echo "并发用户数: ${USER_COUNT}"
                    echo "持续时间: ${DURATION}秒"
                    
                    # 执行压测
                    gatling -simulationClass com.example.UserSimulation \
                        -Dusers=${USER_COUNT} \
                        -Dduration=${DURATION} \
                        -Dgatling.reporting.html.title="性能测试报告 - ${TARGET_ENV} - $(date +%Y%m%d)"
                    
                    echo "压测执行完成"
                '''
            }
            post {
                failure {
                    sh '''
                        echo "=== 压测失败,保留现场 ==="
                        # 保留容器日志用于问题排查
                        docker compose logs > "${WORKSPACE}/container-logs.txt"
                        # 保留容器状态
                        docker compose ps > "${WORKSPACE}/container-status.txt"
                    '''
                }
            }
        }

        // ---------------------------------------------------
        // 阶段3:收集压测结果
        // ---------------------------------------------------
        stage('收集报告') {
            steps {
                sh '''
                    echo "=== 收集压测报告 ==="
                    # 查找最新的报告目录
                    LATEST_REPORT=$(find ${RESULTS_DIR} -maxdepth 1 -type d -name '20*' | sort | tail -1)
                    
                    if [ -z "${LATEST_REPORT}" ]; then
                        echo "错误:未找到压测报告目录"
                        exit 1
                    fi
                    
                    echo "最新报告目录: ${LATEST_REPORT}"
                    # 复制报告到工作空间
                    cp -r ${LATEST_REPORT} ${RESULTS_DIR}/latest
                    echo "报告已收集"
                '''
                // 归档压测报告
                archiveArtifacts artifacts: 'reports/latest/**/*', allowEmptyArchive: false, fingerprint: true
            }
        }

        // ---------------------------------------------------
        // 阶段4:结果分析与告警
        // ---------------------------------------------------
        stage('结果分析') {
            steps {
                sh '''
                    echo "=== 分析压测结果 ==="
                    
                    # 解析Gatling JSON结果(如果配置了JSON导出)
                    RESULT_FILE="${RESULTS_DIR}/latest/results-${TARGET_ENV}.json"
                    
                    if [ -f "${RESULT_FILE}" ]; then
                        # 使用jq解析关键指标
                        FAILED_REQUESTS=$(jq '.statistics.all.count.failures' "${RESULT_FILE}")
                        AVG_RESPONSE_TIME=$(jq '.statistics.all.measures.average' "${RESULT_FILE}")
                        P95_RESPONSE_TIME=$(jq '.statistics.all.measures.percentile.95' "${RESULT_FILE}")
                        THROUGHPUT=$(jq '.statistics.all.measures.measuredThroughput' "${RESULT_FILE}")
                        
                        echo "失败请求数: ${FAILED_REQUESTS}"
                        echo "平均响应时间: ${AVG_RESPONSE_TIME}ms"
                        echo "P95响应时间: ${P95_RESPONSE_TIME}ms"
                        echo "吞吐量: ${THROUGHPUT} req/s"
                        
                        # 性能基线检查
                        if [ "${P95_RESPONSE_TIME}" -gt 2000 ]; then
                            echo "警告:P95响应时间超过2000ms阈值"
                            # 发送告警通知
                            curl -X POST \
                                "https://hooks.zabbix.com/sendEvent?action=notify" \
                                -d "Gatling压测P95响应时间超标: ${P95_RESPONSE_TIME}ms (环境: ${TARGET_ENV})"
                        fi
                    else
                        echo "未找到JSON结果文件,跳过分析"
                    fi
                '''
            }
        }
    }

    post {
        // ---------------------------------------------------
        // 无论成功失败都清理环境
        // ---------------------------------------------------
        always {
            sh '''
                echo "=== 清理环境 ==="
                docker compose down --volumes --remove-orphans
                echo "环境已清理"
            '''
        }
        // 成功时发送通知
        success {
            sh '''
                echo "压测成功完成,报告已生成"
            '''
        }
    }
}

5.2 命令行快速执行示例

如果不想走完整的CI/CD流水线,也可以用docker-compose直接手动执行:

# =====================================================
# 手动执行压测的完整流程
# =====================================================

# 第一步:启动所有依赖服务(后台运行)
docker compose up -d --build

# 第二步:等待应用完全就绪
echo "等待应用健康检查通过..."
sleep 40

# 第三步:验证依赖服务状态
docker compose ps

# 第四步:进入Gatling容器执行压测
docker compose exec gatling-runner \
  gatling \
  -simulationClass com.example.UserSimulation \
  -Dusers=50 \
  -Dduration=300 \
  -Dgatling.http.authentication.login=/api/auth/login \
  -DgatewayUrl=http://app-server:8080

# 第五步:查看压测报告
# 报告生成在 ./reports/ 目录下,用浏览器打开index.html
ls -la reports/

# 第六步:查看压测容器日志
docker compose logs gatling-runner --tail=100

# 第七步:清理环境
docker compose down --volumes --remove-orphans

六、Gatling配置优化

6.1 Gatling配置文件

# =====================================================
# 文件:config/gatling.conf
# 说明:Gatling全局配置,统一JVM和网络参数
# =====================================================

# -------------------------------------------------------
# 1. Gatling运行配置
# -------------------------------------------------------
# 模拟引擎配置
simulation {
  # 每个模拟的最大线程数
  max-active-simulations = 5
  # 每个模拟的最大线程数
  max-active-simulations-per-simulation = 200
}

# HTTP协议配置
http {
  # 连接池配置
  connection {
    # 单个host的最大连接数
    max-pool-size = 200
    # 单个host的默认连接数
    pool-size-per-host = 50
    # 连接超时时间
    timeout {
      connect = 5000ms
      # 请求之间的超时时间
      request = 10000ms
    }
    # 连接保持存活
    keep-alive = true
    # 连接空闲时间
    idle-timeout = 60000ms
  }
  
  # SSL/TLS配置
  ssl {
    # 使用系统默认信任库
    use-system-trust-store = false
    # 信任库文件路径
    trust-store {
      path = "/opt/gatling/config/certs/truststore.jks"
      password = "changeit"
    }
  }
}

# -------------------------------------------------------
# 2. 报告配置
# -------------------------------------------------------
report {
  # 报告导出格式
  export {
    # 启用HTML报告
    html = true
    # 启用CSV导出
    csv = true
    # 启用JSON导出(用于自动化分析)
    json = true
    # 启用静态HTML报告(无JS依赖)
    static-html = true
  }
  # 报告标题
  title = "Gatling Performance Test Report"
  # 报告中的公司/团队标识
  company = "Performance Engineering Team"
  # 报告生成时间格式
  timestamp-format = "yyyy-MM-dd HH:mm:ss"
}

# -------------------------------------------------------
# 3. 数据导出配置(用于CI/CD结果解析)
# -------------------------------------------------------
export {
  # JSON格式导出,包含统计信息
  json {
    path = "results"
    # 是否包含详细数据
    detailed = true
  }
  # CSV格式导出,便于Excel分析
  csv {
    path = "results"
    # 包含请求级别和场景级别数据
    include-per-request = true
    include-per-scenario = true
  }
}

七、JVM参数调优

Gatling跑在JVM上,JVM参数对压测结果影响很大。以下是针对不同环境的推荐配置:

# =====================================================
# 不同环境的JVM参数配置
# =====================================================

# 开发环境:低资源消耗,方便调试
export JAVA_OPTS="-Xms256m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -verbose:gc -XX:+PrintGCDateStamps"

# 测试环境:均衡配置
export JAVA_OPTS="-Xms512m -Xmx2048m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication"

# 预发/生产镜像环境:优化吞吐,减少GC暂停
export JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1NewRatio=2 -XX:G1HeapRegionSize=32m -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+PrintGCDateStamps -Xlog:gc*:file=/opt/gatling/results/gc.log:time,uptime:filecount=5,filesize=10m"

其中几个关键参数的含义:

  • -Xms / -Xmx:JVM堆内存的初始值和最大值。压测场景下建议设为相等,避免动态扩容带来的GC压力。
  • -XX:+UseG1GC:使用G1垃圾回收器,适合大堆内存场景,停顿时间可预测。
  • -XX:MaxGCPauseMillis:目标GC暂停时间上限,GC会尽量在这个时间内完成回收。
  • -XX:+UseStringDeduplication:字符串去重,压测场景下大量重复字符串(如请求头)可以有效减少内存占用。
  • -XX:+AlwaysPreTouch:JVM启动时预先触碰所有内存页,避免压测过程中因缺页中断导致的性能抖动。

八、应用场景分析

8.1 适合容器化压测的场景

微服务架构的性能验证:微服务系统由多个独立部署的服务组成,每个服务可能有独立的配置和依赖。用docker-compose把整个微服务链路(网关、业务服务、数据库、缓存、消息队列)一次性拉起,Gatling从容器内发起请求,测试的就是真实的服务间调用链路,包括网络延迟、序列化开销、负载均衡等。

版本迭代回归压测:每次发版前,用同一套容器化压测环境跑相同的脚本,对比新旧版本的压测数据。因为环境完全一致,数据差异就能归因到代码变更,而不是环境因素。

容量规划与扩容评估:通过调整docker-compose中的容器副本数(比如app-server从1个扩到3个),观察Gatling压测指标的变化趋势,评估扩容策略是否有效。

8.2 不适合容器化压测的场景

极端并发场景(万级并发以上):容器共享宿主机的内核资源,在超高并发下,宿主机的文件描述符限制、网络连接表大小、内核调度策略等因素可能成为瓶颈。这种场景下建议直接在物理机上跑Gatling集群。

需要操作系统级参数调优的场景:比如需要修改net.core.somaxconn、vm.max_map_count、fs.file-max等内核参数。虽然docker可以挂载主机内核,但参数调整会影响宿主机上的所有容器,不够隔离。

涉及硬件特性的压测:比如GPU计算密集型压测、需要特定网卡驱动的场景,容器隔离会屏蔽掉部分硬件特性。

九、技术优缺点分析

9.1 优点

环境一致性保障:这是最核心的优势。同一个Dockerfile构建的镜像在任何机器上运行结果一致,彻底解决了"我这边没问题"的问题。不同开发者、不同CI节点、不同环境之间的压测数据可以直接横向对比。

一键式环境搭建:新人入职后,docker compose up一条命令就能把完整的压测环境(包括被测应用、数据库、缓存、Gatling)全部拉起来,不需要花几个小时手动安装配置。

资源隔离与限制:通过deploy.resources限制压测容器的CPU和内存,防止压测把开发机搞挂。压测完成后再清理,不留痕迹。

与CI/CD自然集成:容器化的Gatling可以直接嵌入Jenkins、GitLab CI、GitHub Actions等流水线,实现自动化压测触发和报告收集。

9.2 缺点

容器性能开销:相比裸机运行,容器有大约2%-5%的性能损耗。对于追求极致精度的压测场景(比如需要精确到毫秒级差异),这个开销需要额外校准。

调试难度增加:容器内的进程调试(比如JVM attach、strace追踪)需要额外的特权模式和挂载配置,不如裸机直接。

网络性能差异:容器网络(bridge网络)有额外的NAT和转发开销,延迟通常比裸机高1-2ms。如果需要精确测试网络延迟,建议使用host网络模式。

依赖管理复杂度:完整的微服务压测环境涉及多个容器,依赖关系复杂。容器间网络、数据库初始化顺序、配置管理等问题需要额外注意。

9.3 缓解措施

性能开销校准:在正式压测前,用裸机跑一次基准测试,再在容器里跑一次,记录差异系数。后续分析时用裸机结果 = 容器结果 × 校准系数来还原。

调试技巧:在docker-compose中给Gatling容器加上privileged: true和挂载/proc,可以正常使用JVM调试工具。

网络优化:对延迟敏感的压测场景,把Gatling容器和被测应用放在同一台宿主机上,使用network_mode: host绕过bridge网络开销。

十、注意事项

10.1 资源监控与防护

压测时务必同时监控宿主机资源:

# =====================================================
# 压测期间实时监控命令
# =====================================================

# 查看容器资源使用率
docker stats

# 查看宿主机CPU和内存
top

# 查看网络连接数
ss -s

# 查看宿主机文件描述符使用
cat /proc/sys/fs/file-nr

# 实时监控Gatling容器内存(每秒刷新一次)
watch -n 1 "docker stats --no-stream gatling-runner"

# 监控压测容器日志(实时查看压测进度)
docker compose logs -f gatling-runner

10.2 常见坑点

Docker网络DNS解析问题:docker-compose内部的服务发现依赖Docker内置DNS。如果压测脚本中硬编码了localhost,在容器内可能无法正确路由到被测应用。应该使用服务名(如app-server)来代替localhost。

挂载目录权限问题:Linux下挂载的目录权限默认是root,如果JDK用户没有写入权限,压测报告可能无法写入。解决方案是在Dockerfile中设置用户权限,或者在挂载时指定UID:

# 指定挂载用户UID为1000(常见Docker默认UID)
docker compose up --user 1000:1000

时区不一致导致报告时间混乱:基础镜像默认是UTC时区,如果不设置TZ=Asia/Shanghai,压测报告中的时间戳会比实际时间晚8小时,影响结果对比。

Gatling结果目录挂载为空:如果宿主机上./reports目录不存在,docker-compose会自动创建,但可能权限不对。建议在启动前手动创建:

# 创建报告目录并设置权限
mkdir -p reports
chmod 777 reports

10.3 结果持久化与历史对比

压测数据需要长期保存,便于趋势分析:

# =====================================================
# 压测结果归档脚本
# =====================================================

# 将最新的压测报告打包并带时间戳命名
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REPORT_DIR="./reports/${TIMESTAMP}"

# 查找最新的报告
LATEST=$(find ./reports -maxdepth 1 -type d -name '20*' | sort | tail -1)
if [ -n "${LATEST}" ]; then
    # 复制报告到带时间戳的目录
    cp -r "${LATEST}" "${REPORT_DIR}"
    
    # 打包归档
    tar -czf "./reports/archive/${TIMESTAMP}.tar.gz" "${REPORT_DIR}"
    
    # 清理7天前的旧报告,保留最近的
    find ./reports/archive -name "*.tar.gz" -mtime +7 -delete
    
    echo "报告已归档: ${REPORT_DIR}"
fi

10.4 安全注意事项

压测容器可能会执行不受限的代码(比如动态参数化脚本),存在安全风险。建议在CI环境中限制容器权限:

# docker-compose安全配置
services:
  gatling-runner:
    # 禁用特权模式(除非确实需要)
    privileged: false
    # 只读文件系统,防止修改系统文件
    read_only: false  # 需要写报告,所以不能完全只读
    # 限制capabilities
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # 如果需要绑定端口
    # 限制容器可以使用的内核功能
    security_opt:
      - no-new-privileges:true

十一、文章总结

把Gatling塞进容器里,表面上看只是给压测工具套了个壳,但本质上解决的是工程化中一个非常经典的问题:可复现性。没有环境一致性的压测数据是没有说服力的——你没法证明这次压测结果比上次好了10%,是因为代码优化了,还是因为换了台配置更好的机器。

docker-compose在环境一致性管理中的角色,不仅仅是编排多个容器,更重要的是它提供了一个声明式的环境定义。你的压测环境是什么,配置文件里写得很清楚:什么版本的JDK、什么版本的Gatling、什么版本的数据库、什么网络配置。任何人在任何机器上跑这个配置,得到的环境都是一样的。

实际落地中,建议从简单场景开始:先让Gatling容器跑起来,再逐步加入被测应用、数据库、缓存等依赖。不要一开始就追求完整的微服务链路压测,先把基础框架跑通,遇到具体问题再针对性解决。

最后分享一个经验:每次压测环境有变更时,更新docker-compose文件,并在文件头部加上注释说明变更内容和原因。三个月后回头看这份配置,你会感谢现在的自己——因为三个月后,你已经不记得当初为什么这么配置了。