硬核压测:经过百万次接口调用,我终于量化了 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. 最终结论:我们到底应该关心什么?

经过三套环境、多种模式、累计超过数百万次的接口调用,我们终于可以自信地回答最初的问题了。

  1. Docker 有性能损耗吗?

    有,但仅限于“Linux vs. Linux”的场景。 在专业的 Linux 服务器环境中,Docker 确实存在约 7-12% 的极限 CPU 性能开销。

  2. 我感觉这次测试都是在虚拟化环境中进行的,所以还算不上绝对严谨。我已经向公司申请了三台全新的3647服务器,配置是2颗铂金8175M处理器,48核96线程,128G内存和2TB固态,每台4000多块。近期就会采购,等到货后我会在真正的裸金属服务器上再跑一遍,到时再来更新最终的结论,各位观众老爷们敬请期待!

posted @ 2025-07-24 22:16  donyu006  阅读(269)  评论(0)    收藏  举报