Golang中X/net/http:自定义TLS配置并启用ForceAttemptHTTP2导致内存问题
Golang中X/net/http:自定义TLS配置并启用ForceAttemptHTTP2导致内存问题
在我的Go语言应用程序中,我们使用了带有自定义TLS配置的HTTP客户端。根据Go语言的x/net/http包,如果我们提供了自定义的TLS配置,那么对HTTP/2的支持将被禁用。因此,为了在使用自定义TLS配置时启用HTTP/2支持,我启用了ForceAttemptHTTP2标志,但随之而来的是,我的应用程序出现了200MB的内存峰值。
我在一个包含800个主机的设置上运行此应用程序。但截至目前,没有任何主机启用了HTTP/2支持,我仍然观察到了这个内存峰值。
我使用pprof对我的应用程序进行了性能分析,并在帖子中添加了分析详情。
对于为何出现如此高的内存使用率,有什么想法吗? 由于我启用了HTTP/2支持,在协议协商之后,由于没有任何主机支持HTTP/2,请求应该回退到HTTP/1.1,应用程序应该正常运作。 尽管请求确实回退到了HTTP/1.1,但应用程序的内存使用量却增加了250MB。
更多关于Golang中X/net/http:自定义TLS配置并启用ForceAttemptHTTP2导致内存问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
2.1. 未修改的性能分析:

更多关于Golang中X/net/http:自定义TLS配置并启用ForceAttemptHTTP2导致内存问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
2.2 未修改的性能分析:

1.1. 使用自定义TLS更改且ForceAttemptHTTP2为true的配置文件:

1.3. 配置文件包含自定义TLS更改并将ForceAttemptHTTP2设置为true:

1.2. 使用自定义TLS更改且ForceAttemptHTTP2为true的性能分析:

这是一个已知问题,当启用ForceAttemptHTTP2时,即使没有实际使用HTTP/2连接,也会为每个主机创建HTTP/2传输层缓存。以下是具体原因和解决方案:
问题根源
ForceAttemptHTTP2会在每个Transport中初始化HTTP/2缓存结构,即使最终没有使用HTTP/2。对于800个主机,这会创建大量缓存条目:
// 问题示例代码
package main
import (
"crypto/tls"
"net/http"
"time"
)
func main() {
// 自定义TLS配置
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
// 启用ForceAttemptHTTP2的Transport
transport := &http.Transport{
TLSClientConfig: tlsConfig,
ForceAttemptHTTP2: true, // 这里导致内存问题
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
// 每个主机都会创建HTTP/2缓存
_ = client
}
解决方案
方案1:禁用ForceAttemptHTTP2,手动处理HTTP/2
package main
import (
"crypto/tls"
"net/http"
"golang.org/x/net/http2"
"time"
)
func createClient() *http.Client {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
ForceAttemptHTTP2: false, // 禁用自动尝试
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
// 手动配置HTTP/2传输
http2Transport := &http2.Transport{
TLSClientConfig: tlsConfig,
}
// 创建支持回退的RoundTripper
client := &http.Client{
Transport: &fallbackTransport{
h2: http2Transport,
h11: transport,
},
Timeout: 30 * time.Second,
}
return client
}
type fallbackTransport struct {
h2 *http2.Transport
h11 *http.Transport
}
func (t *fallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// 先尝试HTTP/2
resp, err := t.h2.RoundTrip(req)
if err == nil {
return resp, nil
}
// 失败则回退到HTTP/1.1
return t.h11.RoundTrip(req)
}
方案2:使用连接池和共享Transport
package main
import (
"crypto/tls"
"net/http"
"sync"
"time"
)
var (
transportPool = make(map[string]*http.Transport)
poolMu sync.RWMutex
)
func getTransport(host string) *http.Transport {
poolMu.RLock()
if t, ok := transportPool[host]; ok {
poolMu.RUnlock()
return t
}
poolMu.RUnlock()
poolMu.Lock()
defer poolMu.Unlock()
// 检查是否已创建(双重检查)
if t, ok := transportPool[host]; ok {
return t
}
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
}
// 只为已知支持HTTP/2的主机启用ForceAttemptHTTP2
transport := &http.Transport{
TLSClientConfig: tlsConfig,
ForceAttemptHTTP2: false, // 默认禁用
MaxIdleConns: 10,
MaxIdleConnsPerHost: 2,
IdleConnTimeout: 30 * time.Second,
}
transportPool[host] = transport
return transport
}
func getClient(host string) *http.Client {
return &http.Client{
Transport: getTransport(host),
Timeout: 30 * time.Second,
}
}
方案3:监控并动态启用HTTP/2
package main
import (
"crypto/tls"
"net/http"
"sync"
"time"
)
type SmartTransport struct {
mu sync.RWMutex
transports map[string]*http.Transport
h2Enabled map[string]bool
baseConfig *tls.Config
}
func NewSmartTransport() *SmartTransport {
return &SmartTransport{
transports: make(map[string]*http.Transport),
h2Enabled: make(map[string]bool),
baseConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
}
func (st *SmartTransport) RoundTrip(req *http.Request) (*http.Response, error) {
host := req.URL.Host
st.mu.RLock()
transport, exists := st.transports[host]
st.mu.RUnlock()
if !exists {
transport = st.createTransport(host, false) // 初始禁用HTTP/2
st.mu.Lock()
st.transports[host] = transport
st.mu.Unlock()
}
// 尝试请求
resp, err := transport.RoundTrip(req)
// 如果响应表明支持HTTP/2,升级传输
if resp != nil && resp.ProtoMajor == 2 && !st.h2Enabled[host] {
st.upgradeToHTTP2(host)
}
return resp, err
}
func (st *SmartTransport) createTransport(host string, forceHTTP2 bool) *http.Transport {
config := st.baseConfig.Clone()
config.ServerName = host
return &http.Transport{
TLSClientConfig: config,
ForceAttemptHTTP2: forceHTTP2,
MaxIdleConns: 10,
MaxIdleConnsPerHost: 2,
IdleConnTimeout: 30 * time.Second,
}
}
func (st *SmartTransport) upgradeToHTTP2(host string) {
st.mu.Lock()
defer st.mu.Unlock()
if st.h2Enabled[host] {
return
}
// 创建新的支持HTTP/2的传输
newTransport := st.createTransport(host, true)
st.transports[host] = newTransport
st.h2Enabled[host] = true
}
验证解决方案
使用pprof验证内存使用情况:
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
// 启动pprof服务器
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// 使用优化后的客户端代码
client := createClient()
_ = client
// ... 应用程序逻辑
}
运行后访问 http://localhost:6060/debug/pprof/heap 查看内存分配情况。使用方案1或方案2后,内存使用应该显著降低,特别是ForceAttemptHTTP2相关的缓存分配会消失。

