Envoy与Nginx正向代理压测方案

目标

  • 对比 Envoy 与 Nginx 在正向代理场景下的吞吐、延迟、失败率与连接行为(TIME_WAIT)。

环境

  • Envoy:监听 9094,管理口 9902,启用来源/目标 RBAC、动态前向代理(c-ares DNS)、访问日志与本地限流。
  • Nginx:监听 8080,启用 HTTP/1.1 长连接与 reuseport、CONNECT 支持,变量上游转发。
  • 系统(Linux 参考):ip_local_port_range=20000–65535tcp_tw_reuse=1tcp_timestamps=1tcp_max_tw_buckets=200000

指标采集

  • 吞吐与延迟:QPS、平均耗时、P50/P90/P99。
  • 失败率:HTTP 4xx/5xx 比例、连接错误(如 connect() failed)。
  • 连接行为:TIME_WAIT 总量与趋势。
  • 代理内部指标:
    • Envoy:curl http://localhost:9902/stats | head -n 200,关注 cluster.dynamic_forward_proxy_cluster.*dns_cache.*local_ratelimit.*http.*
    • Nginx:访问日志与错误日志,统计 5xx 与连接失败。

压测命令

  • HTTP(Envoy):
    • docker run --rm rakyll/hey -z 60s -c 200 -x http://host.docker.internal:9094 http://example.org/
  • HTTP(Nginx):
    • docker run --rm rakyll/hey -z 60s -c 200 -x http://host.docker.internal:8080 http://example.org/
  • HTTPS(CONNECT)功能验证(并发 curl):
    • docker run --rm curlimages/curl:8.5.0 -sS --proxy http://host.docker.internal:9094 https://ifconfig.io

TIME_WAIT 采样

  • macOS:watch -n 5 'netstat -an | grep TIME_WAIT | wc -l'
  • Linux:watch -n 5 'ss -tan state time-wait | wc -l'

输出记录模板

  • 工具命令、QPS、平均耗时、P95/P99、失败率、TIME_WAIT 初值/峰值/稳态值、Envoy/Nginx关键指标快照。

一键脚本

  • 路径:scripts/run_benchmark.sh
  • 使用:bash scripts/run_benchmark.sh
  • 可选参数(环境变量):
    • DURATION 压测时长秒(默认 60
    • CONCURRENCY 并发数(默认 200
    • TARGET HTTP目标(默认 http://example.org/
    • ENVOY_PROXY(默认 http://host.docker.internal:9094
    • NGINX_PROXY(默认 http://host.docker.internal:8080
    • CONNECT_TARGET(默认 https://ifconfig.io
    • CONNECT_COUNT(默认 50
    • CONNECT_PARALLEL(默认 10
  • 输出目录:docs/results/<timestamp>/,包含 hey 输出、TIME_WAIT 采样、Envoy stats 快照与 CONNECT 结果。

结论预期

  • Envoy:动态前向代理与连接合并带来更高复用率;在大量不同上游域名场景下,TIME_WAIT 峰值与稳态值更低,失败率更低,P99更稳。
  • Nginx:变量上游难以池化复用,可能导致频繁建连与大量 TIME_WAIT,高并发下更易受端口耗尽影响。

风险与注意

  • 白名单:测试目标域必须在 Envoy 白名单内,否则会被 RBAC 返回 403。
  • CONNECT:压测工具有限,建议以功能验证与失败率统计为主,配合 HTTP 压测综合判断。
  • 正则复杂度:避免单条 RE2 规则过大;当前已拆分并使用低复杂度通配(*.{region}.log.aliyuncs.com)。

可执行指引(一步步)

  • 前置条件

    • 安装 Docker(macOS 可用 Docker Desktop)。
    • 仓库已存在 Envoy 配置:envoy/envoy-forward-proxy.yaml
  • 启动 Envoy 容器

    • docker rm -f envoy-forward-proxy || true
    • docker run -d --name envoy-forward-proxy -p 9094:9094 -p 9902:9902 -v $(pwd)/envoy/envoy-forward-proxy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy:v1.30.2
    • 验证管理接口:curl -sS http://localhost:9902/stats | head -n 20
    • 验证白名单域:
      • HTTP:docker run --rm curlimages/curl:8.5.0 -v --proxy http://host.docker.internal:9094 http://example.org/
      • HTTPS:docker run --rm curlimages/curl:8.5.0 -v --proxy http://host.docker.internal:9094 https://ifconfig.io
      • 阿里云日志域(已通配允许):docker run --rm curlimages/curl:8.5.0 -v --proxy http://host.docker.internal:9094 http://sl-store-sdk-fjny1.ap-southeast-1.log.aliyuncs.com/
      • 非白名单示例(应返回 403):docker run --rm curlimages/curl:8.5.0 -v --proxy http://host.docker.internal:9094 http://httpbin.org/ip
  • 启动(或跳过)Nginx 正向代理

    • 若本机已运行 Nginx 正向代理(监听 8080,具备 ngx_http_proxy_connect_module):
      • 健康检查:curl -sS http://localhost:8081/health
      • HTTP 验证:docker run --rm curlimages/curl:8.5.0 -v --proxy http://host.docker.internal:8080 http://example.org/
    • 如果未安装带 CONNECT 模块的 Nginx,可暂时只对比 Envoy(HTTP/HTTPS),Nginx 对比仅进行 HTTP(如你已有其他 HTTP 代理)。
  • 运行压测脚本

    • 默认:bash scripts/run_benchmark.sh
    • 自定义时长/并发:DURATION=120 CONCURRENCY=300 bash scripts/run_benchmark.sh
    • 自定义目标与代理:TARGET=http://example.org/ ENVOY_PROXY=http://host.docker.internal:9094 NGINX_PROXY=http://host.docker.internal:8080 bash scripts/run_benchmark.sh
    • 脚本结束会打印输出目录,如:docs/results/20251128-081500
  • 查看结果

    • Envoy:envoy_http_hey.txtenvoy_timewait.csvenvoy_stats_after_http.txtenvoy_connect_results.txtenvoy_stats_after_connect.txt
    • Nginx:nginx_http_hey.txtnginx_timewait.csv
    • 观察对比:
      • QPS 与 P99 延迟(hey 输出)
      • TIME_WAIT 趋势(CSV,可导入 Excel/Numbers 画折线图)
      • Envoy 指标(DNS 缓存、连接池、限流)

常见故障与处理

  • 代理 403:目标域名不在白名单或命中敏感 IP 阻断(检查 envoy/envoy-forward-proxy.yaml:101–161)。
  • DNS 解析失败:检查本地网络、DNS 服务器(配置在 envoy/envoy-forward-proxy.yaml:176–183, 224–231)。
  • Nginx CONNECT 不工作:需使用包含 ngx_http_proxy_connect_module 的 Nginx 构建或镜像;否则仅能进行 HTTP 对比。

构建带 CONNECT 模块的 Nginx(Docker)

  • Dockerfile 路径:docker/nginx-connect/Dockerfile
  • 构建镜像:
    • docker build -t nginx-connect:1.24.0 -f docker/nginx-connect/Dockerfile .
  • 运行容器(挂载当前仓库 Nginx 配置):
    • docker rm -f nginx-forward-proxy || true
    • docker run -d --name nginx-forward-proxy -p 8080:8080 -p 8081:8081 \ -v $(pwd)/log/nginx.conf:/usr/local/nginx/conf/nginx.conf \ -v $(pwd)/log/nginx_deploy_download.conf:/usr/local/nginx/conf/nginx_deploy_download.conf \ nginx-connect:1.24.0
  • 验证:
    • 健康检查:curl -sS http://localhost:8081/health
    • HTTP:docker run --rm curlimages/curl:8.5.0 -v --proxy http://host.docker.internal:8080 http://example.org/
    • HTTPS(CONNECT):docker run --rm curlimages/curl:8.5.0 -v --proxy http://host.docker.internal:8080 https://ifconfig.io

Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FROM debian:bookworm-slim AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential wget git ca-certificates \
    libpcre3 libpcre3-dev zlib1g zlib1g-dev openssl libssl-dev \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /tmp
ARG NGINX_VERSION=1.24.0
RUN wget -q http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \
 && tar xzf nginx-${NGINX_VERSION}.tar.gz
RUN git clone --depth=1 https://github.com/chobits/ngx_http_proxy_connect_module.git

WORKDIR /tmp/nginx-${NGINX_VERSION}
RUN patch -p1 < ../ngx_http_proxy_connect_module/patch/unix/nginx-${NGINX_VERSION}.patch
RUN ./configure \
    --prefix=/usr/local/nginx \
    --with-http_ssl_module \
    --with-stream \
    --with-threads \
    --add-module=../ngx_http_proxy_connect_module
RUN make -j"$(nproc)" && make install

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
 && rm -rf /var/lib/apt/lists/*
COPY --from=build /usr/local/nginx /usr/local/nginx
ENV PATH=/usr/local/nginx/sbin:$PATH
EXPOSE 8080 8081
CMD ["nginx", "-g", "daemon off;"]

压测脚本(支持容器内TIME_WAIT采样与并发自动推导)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/env bash
set -euo pipefail

ts=$(date +%Y%m%d-%H%M%S)
out_dir="docs/results/$ts"
mkdir -p "$out_dir"

duration="${DURATION:-60}"
concurrency="${CONCURRENCY:-200}"
target="${TARGET:-http://example.org/}"
envoy_proxy="${ENVOY_PROXY:-http://host.docker.internal:9094}"
nginx_proxy="${NGINX_PROXY:-http://host.docker.internal:8080}"
connect_target="${CONNECT_TARGET:-https://ifconfig.io}"
connect_count="${CONNECT_COUNT:-50}"
connect_parallel="${CONNECT_PARALLEL:-10}"
auto_concurrency="${AUTO_CONCURRENCY:-false}"
nginx_status_url="${NGINX_STATUS_URL:-http://localhost:8081/nginx_status}"

count_timewait_in_container() {
  container="$1"
  docker run --rm --network="container:${container}" busybox sh -c '
    count_tcp() {
      if [ -f /proc/net/tcp ]; then awk "NR>1 && \$4==\"06\" {c++} END{print (c+0)}" /proc/net/tcp; else echo 0; fi
    }
    count_tcp6() {
      if [ -f /proc/net/tcp6 ]; then awk "NR>1 && \$4==\"06\" {c++} END{print (c+0)}" /proc/net/tcp6; else echo 0; fi
    }
    c4=$(count_tcp); c6=$(count_tcp6); echo $((c4 + c6))
  '
}

sample_timewait_container() {
  container="$1"
  interval="$2"
  samples="$3"
  file="$4"
  i=0
  while [ "$i" -lt "$samples" ]; do
    ts_now=$(date +%s)
    count=$(count_timewait_in_container "$container" || echo 0)
    echo "${ts_now},${count}" >> "$file"
    sleep "$interval"
    i=$((i+1))
  done
}

samples_envoy=$(( (duration / 5) + 2 ))
sample_timewait_container envoy-forward-proxy 5 "$samples_envoy" "$out_dir/envoy_timewait.csv" &
tw_pid_envoy=$!
docker run --rm rakyll/hey -z "${duration}s" -c "$concurrency" -x "$envoy_proxy" "$target" | tee "$out_dir/envoy_http_hey.txt"
curl -sS "http://localhost:9902/stats" > "$out_dir/envoy_stats_after_http.txt" || true
wait "$tw_pid_envoy"

seq 1 "$connect_count" | xargs -n1 -P "$connect_parallel" docker run --rm curlimages/curl:8.5.0 -sS -w 'HTTP:%{http_code}\n' -o /dev/null --proxy "$envoy_proxy" "$connect_target" > "$out_dir/envoy_connect_results.txt"
curl -sS "http://localhost:9902/stats" > "$out_dir/envoy_stats_after_connect.txt" || true

samples_nginx=$(( (duration / 5) + 2 ))
sample_timewait_container nginx-forward-proxy 5 "$samples_nginx" "$out_dir/nginx_timewait.csv" &
tw_pid_nginx=$!
docker run --rm rakyll/hey -z "${duration}s" -c "$concurrency" -x "$nginx_proxy" "$target" | tee "$out_dir/nginx_http_hey.txt"
wait "$tw_pid_nginx"

echo "$out_dir"

启用并发自动推导

1
AUTO_CONCURRENCY=true NGINX_STATUS_URL=http://localhost:8081/nginx_status bash scripts/run_benchmark.sh

脚本将:

  • 从 Nginx stub_status 读取 Reading/Writing/Active connections
  • 估算并发 derived = (Reading + Writing) + 20(低并发)或 + 20%(高并发),并写入 derive_concurrency.log
  • 然后以推导并发运行压测。
0%