net/http: TLS handshake timeout 问题

759 阅读3分钟

最近系统偶现”net/http: TLS handshake timeout“,而且都集中在同一个机房,这个报错还是第一次见,产生的原因和解决的方案都比较有意思。

现场

报错的信息为:

Error sending request:%!(EXTRA *url.Error=Get "https://****//test_image.jpg?lk3s=50ccb0c5&x-expires=1733490979%3D": net/http: TLS handshake timeout)

报错的位置为resp, err := client.Do(req):

func GetImageContent(ctx context.Context, imageURL string) (string, oc_error.Error) {
	// Create an HTTP client
	client := &http.Client{}

	// Create a new request using http
	req, err := http.NewRequest("GET", imageURL, nil)
	if err != nil {
		logs.CtxError(ctx, "Error creating request:", err)
		return "", config.SystemError
	}

	// Send the request via a client
	resp, err := client.Do(req)
	if err != nil {
		logs.CtxError(ctx, "Error sending request:", err)
		return "", config.SystemError
	}
	defer resp.Body.Close()

	// Check if the request was successful
	if resp.StatusCode != http.StatusOK {
		logs.CtxError(ctx, "Error: Non-200 HTTP status code:", err)
		return "", config.SystemError
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		logs.CtxError(ctx, "ReadAll error %+v", err)
		return "", config.SystemError
	}
	return string(body), nil
}

线索

通过查看日志,发现从请求开始到报错,经历了10s。为什么是10s呢?net/http包的默认TLSHandshakeTimeout超时时间是10s。

// DefaultTransport is the default implementation of Transport and is
// used by DefaultClient. It establishes network connections as needed
// and caches them for reuse by subsequent calls. It uses HTTP proxies
// as directed by the environment variables HTTP_PROXY, HTTPS_PROXY
// and NO_PROXY (or the lowercase versions thereof).
var DefaultTransport RoundTripper = &Transport{
	Proxy: ProxyFromEnvironment,
	DialContext: defaultTransportDialContext(&net.Dialer{
		Timeout:   30 * time.Second,
		KeepAlive: 30 * time.Second,
	}),
	ForceAttemptHTTP2:     true,
	MaxIdleConns:          100,
	IdleConnTimeout:       90 * time.Second,
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: 1 * time.Second,
}

大家一般不配置,但如果想配置的话,可以这么写

c := &http.Client{  
    Transport: &Transport{
        Dial: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).Dial,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
}

TLS handshake过程

以前写过HTTPS连接过程,这里面详细描述了TLS的执行过程。TLS过程中,发送端和接收端都需要给双方提供一些信息,当然也需要用到证书。

在这里插入图片描述

所以产生的原因可能有三处,一个是运行我程序的容器有问题(可能性比较小),一个是网络(内部网络或外部网络)上有问题(可能性比较大)。

先去找了容器的同学,他们反馈最近没有更新,而且感觉真的是容器的问题,大概率是百分百出问题。

然后去找CDN的同学,初步判断是外部网络的问题。

追查

其实这个机房报错概率还是比较高的,在5%~10%之间。

找问题IP

为了完整复现,登录容器,执行

curl -v https://****

使用 curl --verbose或curl -v 命令可以详细显示 HTTP 请求和响应的过程

在这里插入图片描述

我们的域名对应多个IP地址,多次执行curl,发现部分IP地址确实会慢。

抓包

开两个窗口,一个窗口执行如下命令,使用tcpdump抓包,其中的ip是上面查到的有问题的ip,将抓包结果写入指定文件

tcpdump -i any host  ip1  or   host ip2  or  ip3   -w /home/if9.pcap

开另一个窗口,执行curl,一直执行到对应的ip确实出现握手失败的时候。

导出数据

把容器里的文件下载到本地,超复杂,但公司的同学真的是什么都经历过了,愣是走出了一条路。

需要使用item2,配置sz指令,可以参考 github.com/aikuyun/ite…

分析

下载好抓到的包,可以使用WireShark进行分析,你看,10s的位置就找到了。

在这里插入图片描述

解决

后续CDN的同学找了厂商,根据厂商反馈是厂商节点问题,对应的节点性能有点减弱,从服务链路进行剔除。针对全网是否还有类似的节点,厂商继续全网检查看看,有类似的及时处理掉。

资料

  1. golang net/http 超时机制完全手册