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

1 回复

更多关于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的自定义连接池可以解决单连接瓶颈问题。

回到顶部