最近对自己手撕的HTTP反向代理,进行了压测,并与Nginx进行对比,以便于排查其中的隐患问题。
一、HTTP基础
1.1 版本
HTTP 协议版本对比
1. HTTP/0.9(1991)
- 特点:仅支持 GET,无请求头,仅传输 HTML,连接后立即关闭。
- 缺点:功能极其有限。
2. HTTP/1.0(1996)
- 改进:支持多种方法、请求头、图片等资源,但仍使用短连接。
- 缺点:每次请求都重新建立 TCP 连接,性能低。
3. HTTP/1.1(1999)
- 改进:默认持久连接(Keep-Alive)、管道化、Host 头、多种优化,支持Server-Sent Events (SSE) 服务器推送(比如 chatgpt 的实时 eventstream 推送)。
- 缺点:队头阻塞(HOL Blocking),同一条 TCP 连接 只能按顺序发送请求和接收响应。如果第一个请求处理很慢,后面的请求都要等它完成,TCP 连接有限。
4. HTTP/2(2015)
- 改进:二进制分层、多路处理、头部压缩。
- 缺点:仍受 TCP 队头阻塞影响。
5. HTTP/3(2022)
- 改进:基于 QUIC(UDP),解决队头阻塞,0-RTT 连接(客户端在无需完整的握手过程的情况下,立即发送数据,从而减少延迟),内置 TLS 1.3。
- 缺点:部署成本较高,仍在推广阶段。
总结
版本 | 主要特点 | 主要问题 |
---|
HTTP/0.9 | 仅支持 GET、无请求头、仅 HTML | 仅适用于简单网页 |
HTTP/1.0 | 请求头、多方法、多资源 | 短连接,性能低 |
HTTP/1.1 | 持久连接、管道化、Host 头、SSE | 队头阻塞,TCP 连接受限 |
HTTP/2 | 二进制、处理多路、头部压缩 | 仍受 TCP 队头阻塞影响 |
HTTP/3 | QUIC,解决 TCP 阻塞 | 兼容性、部署成本高 |
HTTP/2 已广泛使用,HTTP/3 未来可能成为主流,但 HTTP/1.1 仍是基础。
下面提供使用springweb实现的sse示例。
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
| import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import javax.servlet.http.HttpServletResponse; import java.util.UUID;
@RestController public class SSEController {
@GetMapping("/api/sse") public ResponseBodyEmitter sse(HttpServletResponse response) { ResponseBodyEmitter emitter = new ResponseBodyEmitter(); response.setContentType("text/event-stream;charset=UTF-8"); final String id = UUID.randomUUID().toString(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { emitter.send("event: message"); emitter.send("\n"); emitter.send("id: " + id); emitter.send("\n"); emitter.send("data: 现在时间是" + System.currentTimeMillis()); emitter.send("\n\n"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } } emitter.complete(); }).start(); return emitter; } }
|

1.2 Chrome浏览器抓包HTTP版本
默认情况下,浏览器抓包是没有协议版本信息的。需要自己右键、勾选。如下图。

1.3 编写反向代理注意事项
在实现 HTTP 反向代理时,需要关注以下关键点,以确保请求和响应的正确转发:
- 移除逐跳标头(参考):逐跳标头(Hop-by-Hop Headers)仅适用于当前连接,不应在 请求头 和 响应头 中转发
- Connection
- TE
- Transfer-Encoding
- Keep-Alive
- Proxy-Authorization
- Proxy-Authentication
- Trailer
- Upgrade
- 判断请求体的存在(参考):通过请求头判断是否包含请求体
Content-Length
:指定请求体的长度,适用于小型请求。Transfer-Encoding: chunked
:采用分块传输,不需要预先计算请求体大小,适用于大文件等流式传输。Content-Length
和 Transfer-Encoding
不能同时存在,否则请求格式无效。
- 不应直接转发
Host
请求头:代理服务器应根据请求的目标动态设置 Host
头,而不是直接转发客户端的 Host
头 - 记录原始客户端信息:代理服务器需要在请求头中添加以下标头,以保留原始访问者信息:
X-Forwarded-For
:记录原始客户端的 IP 地址,以便后端服务器获取真实访问者信息。X-Forwarded-Proto
:指示原始请求使用的是 HTTP 还是 HTTPS。
- 重写
Location
响应头:当目标服务器返回 Location
头进行重定向时,代理服务器进行 URL 重写。 - 正确处理响应头
Content-Length
和 Transfer-Encoding
- HTTP/1.1
- 响应头必须包含
Content-Length
或 Transfer-Encoding
之一,否则客户端可能无法正确解析响应内容。
- HTTP/2
- 响应头 可以没有
Content-Length
和 Transfer-Encoding
,因为 HTTP/2 采用了 帧(frame) 机制进行数据传输,已经内置了流量控制和帧分块的能力
以上是我在实现 HTTP 反向代理时留意的关键点,更多细节可参考 What Proxies Must Do。
1.4 OkHttpClient示例
健壮的构建一个OkHttpClient,可以参考如下示例代码
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
| import okhttp3.ConnectionPool; import okhttp3.OkHttpClient;
import javax.net.ssl.*; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.concurrent.TimeUnit;
public OkHttpClient okHttpClient() { OkHttpClient.Builder builder = new OkHttpClient().newBuilder() .retryOnConnectionFailure(true) .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS); builder.followRedirects(false); builder.followSslRedirects(false); builder.hostnameVerifier(new HostnameVerifier() { @Override public boolean verify(final String s, final SSLSession sslSession) { return true; }
@Override public final String toString() { return "NO_OP"; } }); try { X509TrustManager x509TrustManager = new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }; TrustManager[] trustManagers = { x509TrustManager }; SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, trustManagers, new SecureRandom()); builder.sslSocketFactory(sslContext.getSocketFactory(), x509TrustManager); } catch (Exception ignore) {
} return builder.build(); }
|
二、性能比较
2.1 硬件配置
通用环境
服务器环境
客户端-Windows
- 硬件:
12C-32G
- IP:
10.0.0.1
- 用途: jmeter
网关-Linux
- 硬件:
2C-2G
- IP:
10.0.0.10
- 用途: gateway
后端-Linux
- 硬件:
2C-2G
- IP:
10.0.0.9
- 用途: backend
- backend1: 最大并发受硬件影响,业务10秒响应
- backend2: 最大并发受硬件影响,业务立即响应
获取局域网间带宽,可以使用iperf3这个工具。Linux通过yum -y install iperf3
进行安装,Windows直接去下载后安装。命令使用方式如下。
1 2 3 4
| iperf3 -s -p 80
iperf3 -c 10.0.0.10 -p 80
|
经测试,三台机器间的局域网带宽约为3Gbps。
2.1.1 Windows
Windows需要调整一下动态端口的范围。避免出现端口耗尽的情况。
1 2 3 4 5
| netsh int ipv4 show dynamicport tcp
netsh int ipv4 set dynamicport tcp start=1024 num=64512
|
2.1.2 Linux
Linux进行如下配置。
1.) 编辑/etc/sysctl.conf
,按照下述参数进行配置。修改后使用sysctl -p
生效, 使用sysctl -a
查看当前配置。
1 2 3 4 5 6
| fs.file-max=1100000
fs.nr_open=1050000
net.ipv4.ip_local_port_range = 1024 64512
|
2.) 编辑/etc/security/limits.conf
,按照下述参数进行配置。重新打开会话会生效。
1 2 3 4
|
root soft nofile 1010000 root hard nofile 1010000
|
Linux监控CPU/Memory使用率、并计算平均使用率的脚本如下。
该工具不够直观,建议使用Jmeter自带的服务器监测插件,参考JMeter压测HTTP接口示例 - 言成言成啊
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
| #!/bin/bash
TOTAL_TIME=119
INTERVAL=1
CPU_LOG="cpu_usage.log" MEM_LOG="mem_usage.log"
> $CPU_LOG > $MEM_LOG
END_TIME=$((SECONDS + TOTAL_TIME)) while [ $SECONDS -lt $END_TIME ]; do CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}') echo "CPU-$(date +"%Y-%m-%d %H:%M:%S"): $CPU_USAGE%" | tee -a $CPU_LOG
MEM_USAGE=$(free | awk '/Mem/{printf("%.2f%%", $3*100/$2 ) }') echo "Memory-$(date +"%Y-%m-%d %H:%M:%S"): $MEM_USAGE" | tee -a $MEM_LOG
echo "-------------------------------------------------------" sleep $INTERVAL done
AVG_CPU=$(awk -F': ' '{gsub(/%/, "", $2); sum+=$2} END {print sum/NR}' $CPU_LOG) echo "Average CPU Usage: $AVG_CPU%"
AVG_MEM=$(awk -F': ' '{gsub(/%/, "", $2); sum+=$2} END {print sum/NR}' $MEM_LOG) echo "Average Memory Usage: $AVG_MEM%"
|
2.2 压测方案
压测条件
- 并发:6000
- 持续:60秒
- 评测标准:网关的吞吐量越趋近于后端服务的吞吐量,网关性能越佳。
- 均开启keepalive

2.3 压测结果
性能方面,还是nginx更优!
此处直接记录结论了,毕竟也不是专业压测。单压backend,吞吐量可达2w,经过代理后,情况如下。
- nginx从头到尾,cpu使用率平稳,vertx会出现最初100%,后续平稳。6000的突发请求量。nginx可以一秒拉起,而Vertx则需要数秒(线程预热)。
- 关闭日志输出,可以显著提升性能。
- 默认配置下,nginx吞吐量在6500左右,vertx吞吐量在4500左右。针对keepalive进行配置优化后,nginx可达1.2w,vertx可达1.1w,但发现该情况下的配置,对于短连接又不友好。因此还是建议使用默认配置。
另外,vertx也提供了内置http反代,性能总体相比nginx,也略差。比我自己实现的要好点,因为我实现的内部有不少业务逻辑。
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
| import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.PoolOptions; import io.vertx.httpproxy.HttpProxy;
public class App {
public static void main(String[] args) { VertxOptions options = new VertxOptions() .setEventLoopPoolSize(Runtime.getRuntime().availableProcessors() * 2); Vertx vertx = Vertx.vertx(options);
HttpClientOptions httpClientOptions = new HttpClientOptions() .setTrustAll(true) .setVerifyHost(false) .setKeepAlive(true) .setPipelining(true) .setMaxWaitQueueSize(20000); PoolOptions poolOptions = new PoolOptions() .setHttp1MaxSize(15000) .setHttp2MaxSize(2000);
HttpServerOptions httpServerOptions = new HttpServerOptions() .setAcceptBacklog(20000) .setTcpFastOpen(true) .setTcpNoDelay(true) .setReusePort(true);
HttpClient httpClient = vertx.createHttpClient(httpClientOptions, poolOptions); HttpProxy httpProxy = HttpProxy.reverseProxy(httpClient); httpProxy.origin(8080, "10.0.0.9"); vertx.createHttpServer(httpServerOptions).requestHandler(httpProxy).listen(8080); } }
|