一、为什么要把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文件,并在文件头部加上注释说明变更内容和原因。三个月后回头看这份配置,你会感谢现在的自己——因为三个月后,你已经不记得当初为什么这么配置了。
评论
围绕“Gatling集成容器化环境的实践,实现负载测试的环境一致性管理”参与讨论