Golang中x/net/http2实现反向代理与HTTP2连接池的探讨
Golang中x/net/http2实现反向代理与HTTP2连接池的探讨 我正在使用Go反向代理
客户端 (100个用户) ← http2 → Go代理 ← http2 → 服务器
代码
proxy := httputil.NewSingleHostReverseProxy(site)
tr := &http.Transport{
MaxIdleConns: 100000,
MaxIdleConnsPerHost: 100000,
IdleConnTimeout: time.Minute * time.Duration(1),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
err = http2.ConfigureTransport(tr)
if err != nil {
panic(err)
}
proxy.Transport = tr
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
})
err = http.ListenAndServeTLS(":12345", cert, key, handler)
if err != nil {
panic(err)
}
对于HTTP/2反向代理,只与服务器建立一个连接。 Go代理 ↔ 服务器之间只有1个连接,因此如果发生TCP重传,所有请求都会被阻塞。 通过检查wrk的结果,我们发现响应时间和带宽都在下降。
无论HTTP/2如何多路复用,单个TCP连接似乎都是一个瓶颈。 如果我使用HTTP/2反向代理,是否必须自己实现连接池? 有没有办法增加代理和服务器之间的连接数?
测试响应大小为9MB。
HTTP 1.1 ./wrk -c100 -d30s -t1 https://127.0.0.1:12345 Running 30s test @ https://127.0.0.1:12345 1 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 697.24ms 24.07ms 827.46ms 72.43% Req/Sec 271.15 261.07 820.00 56.94% 4218 requests in 30.06s, 37.73GB read Requests/sec: 140.33 Transfer/sec: 1.26GB
HTTP 2.0 ./wrk -c100 -d30s -t1 https://127.0.0.1:12345 Running 30s test @ https://127.0.0.1:12345 1 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.35s 372.40ms 2.00s 68.50% Req/Sec 66.23 58.17 525.00 92.96% 1823 requests in 30.01s, 16.45GB read Socket errors: connect 0, read 4, write 0, timeout 547 Requests/sec: 60.74 Transfer/sec: 561.32MB
更多关于Golang中x/net/http2实现反向代理与HTTP2连接池的探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang中x/net/http2实现反向代理与HTTP2连接池的探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在Go的HTTP/2反向代理场景中,确实存在代理与后端服务器之间默认只维持单个HTTP/2连接的问题。这是由于HTTP/2规范要求单个主机使用一个连接进行多路复用。对于大文件传输场景,这确实可能成为瓶颈。
以下是几种解决方案:
1. 使用HTTP/1.1连接池(简单方案)
将代理与服务器之间的连接降级为HTTP/1.1,利用现有的连接池机制:
package main
import (
"crypto/tls"
"net/http"
"net/http/httputil"
"net/url"
"time"
)
func main() {
target, _ := url.Parse("https://backend-server:443")
proxy := httputil.NewSingleHostReverseProxy(target)
// 使用HTTP/1.1 Transport,启用连接池
tr := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
// 强制使用HTTP/1.1
ForceAttemptHTTP2: false,
}
proxy.Transport = tr
// 启动代理服务器
http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", proxy)
}
2. 实现HTTP/2连接池(高级方案)
需要自定义Transport来管理多个HTTP/2连接:
package main
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"time"
"golang.org/x/net/http2"
)
type HTTP2ConnectionPool struct {
target *url.URL
tlsConfig *tls.Config
mu sync.RWMutex
transports []*http2.Transport
index int
maxConns int
}
func NewHTTP2ConnectionPool(target *url.URL, maxConns int) *HTTP2ConnectionPool {
pool := &HTTP2ConnectionPool{
target: target,
maxConns: maxConns,
tlsConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
// 初始化多个HTTP/2 Transport
for i := 0; i < maxConns; i++ {
tr := &http2.Transport{
TLSClientConfig: pool.tlsConfig,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
// 自定义Dialer,可以添加连接超时等配置
dialer := &tls.Dialer{
Config: cfg,
}
return dialer.DialContext(ctx, network, addr)
},
// 调整连接设置
MaxReadFrameSize: 1 << 20, // 1MB
IdleConnTimeout: 90 * time.Second,
PingTimeout: 15 * time.Second,
}
pool.transports = append(pool.transports, tr)
}
return pool
}
func (p *HTTP2ConnectionPool) RoundTrip(req *http.Request) (*http.Response, error) {
p.mu.Lock()
tr := p.transports[p.index]
p.index = (p.index + 1) % len(p.transports)
p.mu.Unlock()
// 克隆请求,避免修改原始请求
req = req.Clone(req.Context())
req.URL.Scheme = p.target.Scheme
req.URL.Host = p.target.Host
return tr.RoundTrip(req)
}
func main() {
target, _ := url.Parse("https://backend-server:443")
// 创建HTTP/2连接池,例如维护10个连接
pool := NewHTTP2ConnectionPool(target, 10)
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.Host = target.Host
},
Transport: pool,
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
fmt.Printf("Proxy error: %v\n", err)
http.Error(w, "Bad gateway", http.StatusBadGateway)
},
}
server := &http.Server{
Addr: ":8443",
Handler: proxy,
}
// 启动HTTP/2服务器
server.ListenAndServeTLS("cert.pem", "key.pem")
}
3. 使用多个后端主机(分布式方案)
如果可能,使用多个后端服务器地址:
package main
import (
"crypto/tls"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"time"
)
type MultiHostReverseProxy struct {
backends []*url.URL
proxies []*httputil.ReverseProxy
mu sync.RWMutex
}
func NewMultiHostReverseProxy(backendURLs []string) *MultiHostReverseProxy {
m := &MultiHostReverseProxy{}
for _, u := range backendURLs {
backend, _ := url.Parse(u)
m.backends = append(m.backends, backend)
proxy := httputil.NewSingleHostReverseProxy(backend)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 100,
}
m.proxies = append(m.proxies, proxy)
}
return m
}
func (m *MultiHostReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.mu.RLock()
index := rand.Intn(len(m.proxies))
proxy := m.proxies[index]
m.mu.RUnlock()
proxy.ServeHTTP(w, r)
}
func main() {
// 多个后端服务器地址(可以是同一服务器的不同端口)
backends := []string{
"https://backend-server:443",
"https://backend-server:444",
"https://backend-server:445",
}
proxy := NewMultiHostReverseProxy(backends)
// 初始化随机种子
rand.Seed(time.Now().UnixNano())
http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", proxy)
}
4. 调整HTTP/2传输参数
优化现有的HTTP/2 Transport配置:
package main
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
"golang.org/x/net/http2"
)
func main() {
target, _ := url.Parse("https://backend-server:443")
proxy := httputil.NewSingleHostReverseProxy(target)
// 自定义HTTP/2 Transport
tr := &http2.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
return tls.DialWithDialer(dialer, network, addr, cfg)
},
// 调整关键参数
MaxReadFrameSize: 1 << 20, // 1MB帧大小
IdleConnTimeout: 90 * time.Second,
PingTimeout: 15 * time.Second,
WriteBufferSize: 1 << 20, // 1MB写缓冲区
ReadBufferSize: 1 << 20, // 1MB读缓冲区
AllowHTTP: false,
DisableCompression: false,
}
proxy.Transport = tr
// 配置服务器端HTTP/2
server := &http.Server{
Addr: ":8443",
Handler: proxy,
}
// 启用服务器端HTTP/2
http2.ConfigureServer(server, &http2.Server{
MaxConcurrentStreams: 250,
MaxReadFrameSize: 1 << 20,
IdleTimeout: 120 * time.Second,
})
server.ListenAndServeTLS("cert.pem", "key.pem")
}
对于9MB大文件传输场景,建议优先考虑方案1(HTTP/1.1连接池),因为HTTP/1.1的连接池机制已经成熟稳定。如果必须使用HTTP/2,方案2的自定义连接池可以解决单连接瓶颈问题。

