硬核压测:经过百万次接口调用,我终于量化了 Docker 的真实性能损耗
1. 缘起:从“项目经理的担忧”到“百万次调用”的硬核探索
“Docker 会有性能损耗的,我们直接部署吧。”
在一次项目评审中,项目经理的一句话,开启了我这次硬核的性能探索之旅。在云原生和 DevOps 大行其道的今天,“容器化会带来性能开销”似乎是一个普遍存在的“常识”或“担忧”。但作为一名工程师,我更信赖数据而非感觉。
这个所谓的“损耗”到底有多大?它是否足以让我们放弃容器化带来的巨大工程优势?为了彻底终结这个话题的争论,我决定不计成本地进行一次大规模压测。
在三套典型的环境中,我设计了核心对比场景,并让自动化脚本执行了累计超过数百万次的API调用。现在,是时候让数据说话了。
2. 测试策略:在极限的 CPU 负载下,寻找性能的蛛丝马迹
我的目标是测量 Docker 本身(由 cgroups, namespaces 等技术构成)带来的开销。因此,我选择了最“残酷”的测试方式:一个纯粹的、极限的 CPU 密集型任务。
通过将 CPU 压榨到 100%,我们可以最大程度地放大任何由于额外抽象层而引入的计算开销。
3. 测试环境与工具
为了保证测试的可靠性,我准备了多套典型的虚拟化环境,并统一了测试方法。
3.1 测试应用: app_heavy.py
我使用 Python 和 Flask 编写了一个简单的 Web 应用。它的核心是一个高负载的质数计算函数,每次收到请求都会执行一次。
# app_heavy.py
import time
from flask import Flask
app = Flask(__name__)
# 一个计算量巨大的 CPU 密集型任务
def cpu_heavy_task():
limit = 10000 # 计算 10000 以内的所有质数
prime_count = 0
for number in range(2, limit + 1):
is_prime = True
if number <= 1:
continue
for i in range(2, int(number**0.5) + 1):
if number % i == 0:
is_prime = False
break
if is_prime:
prime_count += 1
return prime_count
@app.route('/')
def hello():
cpu_heavy_task()
return 'Heavy task complete!'
if __name__ == '__main__':
import os
port = int(os.environ.get("FLASK_PORT", 5000))
app.run(host='0.0.0.0', port=port)
3.2 Docker 镜像: Dockerfile
用于容器化部署的 Dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN pip install flask -i https://pypi.tuna.tsinghua.edu.cn/simple
COPY app_heavy.py .
CMD ["python", "app_heavy.py"]
3.3 测试方法
-
压测工具:
wrk,一款现代、高性能的 HTTP 压测工具。 -
测试命令:
wrk -t4 -c200 -d30s -T 1h http://<IP>:<Port>/-c200: 模拟 200 个并发连接,持续施加高压力。-d30s: 每次测试持续 30 秒。-T 1h: 设置极长的超时时间,确保不会因为应用处理慢而误判为超时,从而得到最真实的 QPS 数据。
-
测试次数: 每个场景连续运行 100 次,然后取所有结果的平均值,以消除单次运行的偶然性。
3.4 自动化压测脚本
为了确保上百次测试的准确执行和结果的自动统计,我编写了下面的 Shell 脚本。它不仅能自动完成压测,还会将每一次的详细结果和最终的平均值记录到日志文件中。
#!/bin/bash
# --- 自动化性能压测脚本 (带详细日志记录) ---
# --- 配置区 ---
TARGET_IP="192.168.20.22" # 请替换为被测服务器的实际IP
PORTS_TO_TEST=(5000 5001 5002) # 分别对应 原生, Host, Bridge
RUN_COUNT=100
WRK_THREADS=4
WRK_CONNECTIONS=200
WRK_DURATION="30s"
WRK_TIMEOUT="1h"
# --- 日志文件配置 ---
LOG_FILE="benchmark_results_$(date +'%Y-%m-%d_%H-%M-%S').log"
# --- 主函数 ---
run_benchmarks() {
echo "======================================================"
echo " 开始执行自动化性能压测"
echo " 所有输出将记录到: ${LOG_FILE}"
echo "======================================================"
for port in "${PORTS_TO_TEST[@]}"; do
scenario_name=""
if [ "$port" -eq 5000 ]; then scenario_name="原生 Linux (Native)";
elif [ "$port" -eq 5001 ]; then scenario_name="Docker Host 模式";
elif [ "$port" -eq 5002 ]; then scenario_name="Docker Bridge 模式";
else scenario_name="未知场景"; fi
echo ""
echo "######################################################"
echo "# 测试场景: ${scenario_name} (端口: ${port})"
echo "######################################################"
echo "将执行 ${RUN_COUNT} 次压测..."
total_qps=0
total_latency=0
printf "%-15s | %-15s | %-15s\n" "测试轮次" "QPS (req/s)" "延迟 (s)"
echo "------------------------------------------------------"
for i in $(seq 1 $RUN_COUNT); do
wrk_output=$(wrk -t${WRK_THREADS} -c${WRK_CONNECTIONS} -d${WRK_DURATION} -T${WRK_TIMEOUT} http://${TARGET_IP}:${port}/)
qps=$(echo "$wrk_output" | grep 'Requests/sec:' | awk '{print $2}')
latency_str=$(echo "$wrk_output" | grep 'Latency' | awk '{print $2}')
if [[ "$latency_str" == *ms ]]; then
latency_val=$(echo "$latency_str" | sed 's/ms//')
latency_val=$(echo "scale=4; $latency_val / 1000" | bc)
elif [[ "$latency_str" == *s ]]; then
latency_val=$(echo "$latency_str" | sed 's/s//')
else
latency_val=0
fi
printf " Run %-10s | %-15.2f | %-15.4f\n" "${i}/${RUN_COUNT}" "$qps" "$latency_val"
total_qps=$(echo "scale=4; $total_qps + $qps" | bc)
total_latency=$(echo "scale=4; $total_latency + $latency_val" | bc)
done
avg_qps=$(echo "scale=2; $total_qps / $RUN_COUNT" | bc)
avg_latency=$(echo "scale=4; $total_latency / $RUN_COUNT" | bc)
echo "------------------------------------------------------"
echo "场景 [${scenario_name}] 的 ${RUN_COUNT} 次测试最终汇总:"
echo ""
echo " >>> 平均 QPS (Requests/sec): ${avg_qps}"
echo " >>> 平均延迟 (Avg Latency): ${avg_latency} s"
echo "------------------------------------------------------"
done
echo ""
echo "======================================================"
echo " 所有测试已完成"
echo " 详细日志请查看: ${LOG_FILE}"
echo "======================================================"
}
# --- 脚本执行入口 ---
# 检查 bc 命令是否存在
if ! command -v bc &> /dev/null; then
echo "[错误] bc 命令未找到,请先安装它。 (例如: sudo apt-get install bc)"
exit 1
fi
run_benchmarks | tee -a "$LOG_FILE"
有了这个强大的工具,我得以在三套环境中自动执行了所有测试。下面就是激动人心的数据分析环节。
命令一:场景 A - 原生 Linux 运行 (5000 端口)
此命令直接在 Linux 命令行中运行 Python 脚本,应用将监听默认的 5000 端口。
# 确保在 app_heavy.py 所在的目录
# 安装必要的依赖
pip install Flask
# 启动原生应用
python app_heavy.py
命令二:场景 B - 启动 Docker Host 模式容器 (5001 端口)
此命令使用 --network host 让容器直接共享主机的网络,并通过 -e 环境变量将监听端口改为 5001。
# 首先,构建 Docker 镜像 (只需执行一次)
# 确保 Dockerfile 和 app_heavy.py 在同一目录
docker build -t heavy-app .
# 启动 Host 模式容器,并设置端口为 5001
docker run -d --rm --network host -e FLASK_PORT=5001 --name test-heavy-host heavy-app
(注: --rm 参数表示容器停止后自动删除,方便测试。 -d 表示后台运行。)
命令三:场景 C - 启动 Docker Bridge 模式容器 (5002 端口)
此命令使用 Docker 默认的 bridge 网络模式,并通过 -p 参数将容器内部的 5000 端口映射到主机的 5002 端口。
# 假设镜像 heavy-app 已经构建好
# 启动 Bridge 模式容器,并将主机 5002 端口映射到容器 5000 端口
docker run -d --rm -p 5002:5000 --name test-heavy-bridge heavy-app
4. 各种环境下的结果
我首先在两种不同的虚拟化环境中进行了对比测试。
4.1 测试环境一:VMware 虚拟化 (E5-2645)
这台服务器配置太老了,2010年的CPU,单个CPU才6核心12线程,双路也不过12核心24线程,就比我小几岁。SATA固态2T,真不知道当初为什么要花1999买个这么个老爹玩意儿。
- 物理机: E5-2645 CPU, 80GB DDR3 RAM, Windows 10 Host OS
- 虚拟机: Ubuntu 22.04 (8核, 16GB) on VMware
性能对比总表 (VMware on Windows 环境)
| 指标 (Metric) | 场景 A: 原生 Linux | 场景 B: Docker Host | 场景 C: Docker Bridge |
|---|---|---|---|
| 平均 QPS (越高越好) | 46.72 req/s | 43.25 req/s | 43.32 req/s |
| 平均延迟 (越低越好) | 2.8063 s | 3.0130 s | 3.0725 s |
数据分析: 在这套环境中,我们首次观察到了清晰的性能差异。与原生环境相比,两个 Docker 场景的性能都下降了约 7-8% 。
部分日志如下:
测试场景: 原生 Linux (Native) (端口: 5000)
######################################################
将执行 100 次压测...
测试轮次 | QPS (req/s) | 延迟 (s)
Run 1/100 | 49.84 | 2.6900
Run 2/100 | 47.31 | 2.6200
Run 3/100 | 48.58 | 2.6900
Run 4/100 | 46.96 | 2.7000
Run 5/100 | 47.33 | 2.7800
Run 6/100 | 48.15 | 2.6600
Run 7/100 | 47.85 | 2.4900
Run 8/100 | 46.22 | 2.7200
Run 9/100 | 42.93 | 2.8000
Run 10/100 | 47.51 | 2.8700
Run 11/100 | 43.87 | 2.9500
Run 12/100 | 47.10 | 2.6900
测试场景: Docker Host 模式 (端口: 5001)
######################################################
将执行 100 次压测...
测试轮次 | QPS (req/s) | 延迟 (s)
Run 1/100 | 47.04 | 2.8800
Run 2/100 | 44.57 | 2.8500
Run 3/100 | 43.27 | 3.2000
Run 4/100 | 42.85 | 2.8600
Run 5/100 | 43.01 | 3.0100
Run 6/100 | 42.96 | 3.0100
Run 7/100 | 42.39 | 3.0600
Run 8/100 | 44.08 | 3.0200
Run 9/100 | 42.47 | 3.1100
测试场景: Docker Bridge 模式 (端口: 5002)
######################################################
将执行 100 次压测...
测试轮次 | QPS (req/s) | 延迟 (s)
Run 1/100 | 48.11 | 2.9000
Run 2/100 | 42.45 | 3.1800
Run 3/100 | 43.77 | 3.0300
Run 4/100 | 43.45 | 2.8700
Run 5/100 | 45.07 | 3.0100
Run 6/100 | 44.34 | 2.9600
Run 7/100 | 43.69 | 3.3000
Run 8/100 | 43.89 | 3.0100
Run 9/100 | 44.92 | 3.0300
Run 10/100 | 43.47 | 3.0400
Run 11/100 | 45.28 | 2.9800
4.2 测试环境二:Proxmox VE 虚拟化 (E5-2660v4)
这台是一个单路的X99平台,主要用于跑我们的开发环境,配有32x4共计128GB的DDR4内存和三星的1TB M.2固态。
- 物理机: E5 2660V4 (12核/24线程), 128GB DDR4 RAM
- 虚拟机: Ubuntu 22.04 (8核, 16GB) on Proxmox VE
性能对比总表 (Proxmox VE 环境)
| 指标 (Metric) | 场景 A: 原生 Linux | 场景 B: Docker Host | 场景 C: Docker Bridge |
|---|---|---|---|
| 平均 QPS (越高越好) | 83.47 req/s | 74.14 req/s | 74.03 req/s |
| 平均延迟 (越低越好) | 1.7641 s | 1.8972 s | 1.9443 s |
数据分析: 在这套性能更强的环境中,Docker 带来的性能开销稳定在 11% 左右。同时,Host 模式与 Bridge 模式的性能差异依然可以忽略不计。
部分日志如下:
测试场景: 原生 Linux (Native) (端口: 5000)
######################################################
将执行 100 次压测...
测试轮次 | QPS (req/s) | 延迟 (s)
Run 1/100 | 87.42 | 1.6800
Run 2/100 | 82.66 | 1.9000
Run 3/100 | 87.34 | 1.7600
Run 4/100 | 77.79 | 1.7000
Run 5/100 | 84.21 | 1.8100
Run 6/100 | 84.87 | 1.9100
Run 7/100 | 84.43 | 1.5400
Run 8/100 | 81.74 | 1.8200
Run 9/100 | 82.25 | 1.7800
Run 10/100 | 84.07 | 1.6600
Run 11/100 | 83.94 | 1.9300
测试场景: Docker Host 模式 (端口: 5001)
######################################################
将执行 100 次压测...
测试轮次 | QPS (req/s) | 延迟 (s)
Run 1/100 | 78.75 | 2.0100
Run 2/100 | 74.01 | 1.8600
Run 3/100 | 74.83 | 1.8200
Run 4/100 | 75.12 | 1.7800
Run 5/100 | 71.09 | 2.0500
Run 6/100 | 74.56 | 1.8100
Run 7/100 | 75.22 | 1.8900
Run 8/100 | 73.90 | 1.9700
测试场景: Docker Bridge 模式 (端口: 5002)
######################################################
将执行 100 次压测...
测试轮次 | QPS (req/s) | 延迟 (s)
Run 1/100 | 77.77 | 2.1200
Run 2/100 | 74.25 | 1.9600
Run 3/100 | 74.47 | 2.0200
Run 4/100 | 72.43 | 2.0400
Run 5/100 | 74.09 | 2.0900
Run 6/100 | 71.83 | 2.0100
Run 7/100 | 74.55 | 2.0000
5. 测试环境三:开发者桌面环境下的“惊人反转”
接下来,我将测试环境切换到了许多开发者日常使用的场景:在 Windows 主机上通过 Docker Desktop (WSL2 后端) 运行容器。
- 物理机: Intel i5-12500H (小主机), 16GB DDR4 RAM
- 环境: Windows 10 + Docker Desktop (WSL2)
在这个里面,wsl2上运行wrt命令居然报错了,错误如下
unable to connect to 192.168.20.85:5000 Connection refused
Run 34/100 | 0.00 | 0.0000
(standard_in) 2: syntax error
unable to connect to 192.168.20.85:5000 Connection refused
性能对比总表 (Windows + WSL2 环境)
| 指标 (Metric) | 场景 A: 原生 Windows | 场景 B: Docker Bridge (WSL2) |
|---|---|---|
| 平均 QPS (越高越好) | ~80 req/s (在崩溃前) | 142.81 req/s (稳定) |
| 稳定性 | 差 (高压下崩溃) | 极高 |
出现这个情况我是挺意外的,感觉是WSL2的网络或者Windows原生服务的稳定性没有优化好。
部分日志如下:
测试场景: 原生 Linux (Native) (端口: 5000)
######################################################
将执行 100 次压测...
测试轮次 | QPS (req/s) | 延迟 (s)
Run 1/100 | 103.78 | 0.7199
Run 2/100 | 40.25 | 1.7200
Run 3/100 | 65.31 | 1.2300
Run 4/100 | 72.76 | 0.6955
Run 5/100 | 79.67 | 1.3700
Run 6/100 | 72.62 | 1.2200
Run 7/100 | 76.50 | 1.4200
Run 8/100 | 78.77 | 1.3500
Run 9/100 | 73.46 | 1.4700
Run 10/100 | 101.13 | 0.9953
Run 11/100 | 88.84 | 1.2100
Run 12/100 | 78.05 | 1.2900
Run 13/100 | 77.21 | 1.1100
Run 14/100 | 74.62 | 1.4400
Run 15/100 | 90.56 | 1.1400
Run 16/100 | 108.01 | 0.8908
Run 17/100 | 77.60 | 1.2600
Run 18/100 | 79.54 | 1.3700
Run 19/100 | 78.26 | 1.2800
Run 20/100 | 78.22 | 1.2100
Run 21/100 | 77.91 | 1.3500
Run 22/100 | 78.24 | 1.3800
Run 23/100 | 77.66 | 1.3500
Run 24/100 | 78.62 | 1.3700
Run 25/100 | 78.51 | 1.3300
Run 26/100 | 80.93 | 1.3400
Run 27/100 | 77.79 | 1.3600
Run 28/100 | 78.88 | 1.3600
Run 29/100 | 78.05 | 1.3600
Run 30/100 | 78.02 | 1.3900
Run 31/100 | 78.56 | 1.3400
Run 32/100 | 78.14 | 1.3200
Run 33/100 | 78.13 | 1.3800
unable to connect to 192.168.20.85:5000 Connection refused
Run 34/100 | 0.00 | 0.0000
(standard_in) 2: syntax error
unable to connect to 192.168.20.85:5000 Connection refused
测试场景: Docker Bridge 模式 (端口: 5002)
######################################################
将执行 100 次压测...
测试轮次 | QPS (req/s) | 延迟 (s)
Run 1/100 | 190.80 | 1.2100
Run 2/100 | 184.38 | 1.2100
Run 3/100 | 157.66 | 0.7078
Run 4/100 | 93.36 | 0.1334
Run 5/100 | 191.28 | 1.2100
Run 6/100 | 184.34 | 1.2400
Run 7/100 | 135.58 | 0.9972
Run 8/100 | 175.25 | 0.5245
Run 9/100 | 190.68 | 0.6084
Run 10/100 | 174.57 | 0.5322
Run 11/100 | 105.52 | 0.3540
Run 12/100 | 149.20 | 0.4952
5. 最终结论:我们到底应该关心什么?
经过三套环境、多种模式、累计超过数百万次的接口调用,我们终于可以自信地回答最初的问题了。
-
Docker 有性能损耗吗?
有,但仅限于“Linux vs. Linux”的场景。 在专业的 Linux 服务器环境中,Docker 确实存在约 7-12% 的极限 CPU 性能开销。
-
我感觉这次测试都是在虚拟化环境中进行的,所以还算不上绝对严谨。我已经向公司申请了三台全新的3647服务器,配置是2颗铂金8175M处理器,48核96线程,128G内存和2TB固态,每台4000多块。近期就会采购,等到货后我会在真正的裸金属服务器上再跑一遍,到时再来更新最终的结论,各位观众老爷们敬请期待!
本文来自博客园,作者:donyu006,转载请注明原文链接:https://www.cnblogs.com/lcc6/articles/19003682

浙公网安备 33010602011771号