言成言成啊 | Kit Chen's Blog

HTTP版本以及反向代理实现

发布于2024-11-13 22:00:36,更新于2025-03-22 21:58:47,标签:java nginx  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

最近对自己手撕的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/3QUIC,解决 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());
// sse中,\n\n表示一条消息的结束
emitter.send("\n\n");
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
}
emitter.complete();
}).start();
return emitter;
}
}

1.2 Chrome浏览器抓包HTTP版本

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

1.3 编写反向代理注意事项

在实现 HTTP 反向代理时,需要关注以下关键点,以确保请求和响应的正确转发:

  1. 移除逐跳标头参考):逐跳标头(Hop-by-Hop Headers)仅适用于当前连接,不应在 请求头响应头 中转发
    • Connection
    • TE
    • Transfer-Encoding
    • Keep-Alive
    • Proxy-Authorization
    • Proxy-Authentication
    • Trailer
    • Upgrade
  2. 判断请求体的存在参考):通过请求头判断是否包含请求体
    • Content-Length:指定请求体的长度,适用于小型请求。
    • Transfer-Encoding: chunked:采用分块传输,不需要预先计算请求体大小,适用于大文件等流式传输。
    • Content-LengthTransfer-Encoding 不能同时存在,否则请求格式无效。
  3. 不应直接转发 Host 请求头:代理服务器应根据请求的目标动态设置 Host 头,而不是直接转发客户端的 Host
  4. 记录原始客户端信息:代理服务器需要在请求头中添加以下标头,以保留原始访问者信息:
    • X-Forwarded-For:记录原始客户端的 IP 地址,以便后端服务器获取真实访问者信息。
    • X-Forwarded-Proto:指示原始请求使用的是 HTTP 还是 HTTPS。
  5. 重写 Location 响应头:当目标服务器返回 Location 头进行重定向时,代理服务器进行 URL 重写。
  6. 正确处理响应头 Content-LengthTransfer-Encoding
    • HTTP/1.1
      • 响应头必须包含 Content-LengthTransfer-Encoding 之一,否则客户端可能无法正确解析响应内容。
    • HTTP/2
      • 响应头 可以没有 Content-LengthTransfer-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() {
// 服务器keepalive为60s,客户端keepalive为300s。客户端发起一个请求,然后等待60秒后,再发起一个请求,客户端遇到了java.io.EOFException: \n not found;limit=0 content=
// 参考https://github.com/square/okhttp/issues/2738
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);
// 忽略HTTPS主机名验证
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(final String s, final SSLSession sslSession) {
return true;
}

@Override
public final String toString() {
return "NO_OP";
}
});
// 信任所有HTTPS证书,包括CA证书、自签名证书等
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 硬件配置

通用环境

  • java: 8
  • jmeter: 5.6.3

服务器环境

  • 客户端-Windows

    • 硬件: 12C-32G
    • IP: 10.0.0.1
    • 用途: jmeter
  • 网关-Linux

  • 后端-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

# 修改动态端口范围。start+num−1≤65535
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
# 修改用户进程可以打开的最大文件数 
# 请注意这个最大文件数不要大于单个进程可以打开的最大文件数fs.nr_open
root soft nofile 1010000 # 对应软限制, 可以通过ulimit -Sn查看
root hard nofile 1010000 # 对应硬限制, 可以通过ulimit -Hn查看

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 使用率
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

# 计算平均 CPU 使用率
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,经过代理后,情况如下。

  1. nginx从头到尾,cpu使用率平稳,vertx会出现最初100%,后续平稳。6000的突发请求量。nginx可以一秒拉起,而Vertx则需要数秒(线程预热)。
  2. 关闭日志输出,可以显著提升性能。
  3. 默认配置下,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) {
// 配置 Vertx 实例
// 设置事件循环线程池大小为 CPU 核心数的 倍数,适用于 I/O 密集型任务
VertxOptions options = new VertxOptions()
.setEventLoopPoolSize(Runtime.getRuntime().availableProcessors() * 2);
Vertx vertx = Vertx.vertx(options);

// 配置 HTTP 客户端
HttpClientOptions httpClientOptions = new HttpClientOptions()
.setTrustAll(true) // 信任所有 HTTPS 证书(适用于内网或调试环境)
.setVerifyHost(false) // 关闭 HTTPS 域名校验(适用于自签名证书)
.setKeepAlive(true) // 启用 keep-alive 连接,减少 TCP 连接建立的开销
.setPipelining(true) // 启用 HTTP 1.1 请求管道化,提高请求响应效率(仅 HTTP 1.1 生效)
.setMaxWaitQueueSize(20000); // 最大等待队列 10000,超出会拒绝请求
// 配置连接池选项
PoolOptions poolOptions = new PoolOptions()
.setHttp1MaxSize(15000) // HTTP 1.x 连接池最大连接数
.setHttp2MaxSize(2000); // HTTP/2 连接池最大连接数

// 配置 HTTP 服务器
HttpServerOptions httpServerOptions = new HttpServerOptions()
.setAcceptBacklog(20000) // TCP 监听队列(backlog)大小,防止连接被拒绝
.setTcpFastOpen(true) // 启用 TCP Fast Open,减少 TCP 三次握手的延迟
.setTcpNoDelay(true) // 禁用 Nagle 算法,降低 HTTP 响应延迟
.setReusePort(true); // 允许多个进程或线程绑定到同一端口,提高多核并发能力

// vertx提供的简易http反代
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);
}
}
发布:2024-11-13 22:00:36
修改:2025-03-22 21:58:47
链接:https://meethigher.top/blog/2024/http-proxy-boot/
标签:java nginx 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏