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

6 回复

2.1. 未修改的性能分析:

Screenshot 2023-11-15 at 2.56.12 PM

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


2.2 未修改的性能分析:

Screenshot 2023-11-15 at 2.56.27 PM

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

Screenshot 2023-11-15 at 2.52.37 PM

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

Screenshot 2023-11-15 at 3.00.53 PM

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

Screenshot 2023-11-15 at 2.53.18 PM

这是一个已知问题,当启用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相关的缓存分配会消失。

回到顶部