最近系统偶现”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指令,可以参考 https://github.com/aikuyun/iterm2-zmodem。
分析
下载好抓到的包,可以使用WireShark进行分析,你看,10s的位置就找到了。
解决
后续CDN的同学找了厂商,根据厂商反馈是厂商节点问题,对应的节点性能有点减弱,从服务链路进行剔除。针对全网是否还有类似的节点,厂商继续全网检查看看,有类似的及时处理掉。
资料
- golang net/http 超时机制完全手册