Golang中Context超时错误未返回deadline的问题
Golang中Context超时错误未返回deadline的问题 此问题位于 client.go 文件的 setRequestCancel 第 375 行。 我设置了 client.Timeout 为 3 秒,并调用 client.Do 发送 HTTP/2 请求。根据 net/http 包中 client.go 的源代码,虽然调用了带截止时间的上下文,但超时并未发生,请求进入了使用 FQDN 作为主机的往返传输(这只是我们代码中的问题)。 每次都是在 4 秒后返回错误,然后才调用取消。 根据我的观察,上下文在 3 秒后已完成,但并未采取任何操作来使用 rt.CancelRequest 退出正在进行的发送请求处理。 有人能帮忙理解为什么在 HTTP/2 请求中设置无效主机时,这个超时不会发生吗?
更多关于Golang中Context超时错误未返回deadline的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你需要提供一些示例代码。
更多关于Golang中Context超时错误未返回deadline的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这个问题有解决方案吗
上述代码可以运行,但在我的应用程序中,client.Do 存在延迟。
上述代码返回以下错误:
时间 2.001242093s 2024-04-08 10:18:27.139158269 +0000 UTC m=+2.001778206 Post “http://udr-subs.:3000/”: dial tcp: lookup udr-subs.: i/o timeout (Client.Timeout exceeded while awaiting headers)
我的应用程序在 4 秒后返回此错误: Post “http://udr-subs.:3000/”: dial tcp: lookup udr-subs. on 10.21.0.10:53(这可能是 DNS,问题可能出在这里): server misbehaving (Client.Timeout exceeded while awaiting headers)
以下是示例代码……nfDomain1.namespace1 在 Kubernetes 集群中并不存在……尽管我设置了 2 秒的客户端超时,但 client.Do 却花费了 4 秒……
url := http://nfDomain1.namespace1/initial
data := "some data"
postReq, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(data))
if err != nil {
return nil
}
client := &http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.DialTimeout(network, addr, (2000)*time.Millisecond)
},
PingTimeout: 2000 * time.Millisecond,
ReadIdleTimeout: 2000 * time.Millisecond,
},
Timeout: 2000 * time.Millisecond,
}
// 我已将客户端超时设置为 2 秒,但每次错误都在 4 秒后才收到
resp, err := client.Do(postReq)
if err != nil {
return nil
}
这是一个典型的HTTP/2连接复用场景下的上下文超时问题。在HTTP/2中,由于连接复用机制,client.Timeout设置的超时可能不会按预期工作,特别是当请求被阻塞在连接池等待可用连接时。
问题的核心在于http.Transport的dialConn和roundTrip方法中,对于HTTP/2请求,上下文超时的处理方式与HTTP/1.x不同。当使用无效主机名时,DNS解析或连接建立可能会被阻塞,而Transport的CancelRequest方法对HTTP/2请求的支持有限。
以下是问题重现的示例代码:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
// 创建自定义Transport,启用HTTP/2
tr := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
ForceAttemptHTTP2: true,
}
client := &http.Client{
Transport: tr,
Timeout: 3 * time.Second, // 设置3秒超时
}
// 使用无效的主机名
req, err := http.NewRequest("GET", "https://invalid-hostname-that-does-not-exist.example.com", nil)
if err != nil {
panic(err)
}
start := time.Now()
_, err = client.Do(req)
elapsed := time.Since(start)
fmt.Printf("请求耗时: %v\n", elapsed)
fmt.Printf("错误: %v\n", err)
// 验证上下文是否超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case <-time.After(4 * time.Second):
fmt.Println("4秒后超时")
case <-ctx.Done():
fmt.Println("上下文在3秒后完成")
}
}
问题的根本原因在于http.Transport的roundTrip方法中,对于HTTP/2请求,当连接池中没有可用连接时,会尝试创建新连接。在这个过程中,DNS解析可能会被阻塞,而client.Timeout的超时机制可能无法正确中断这个阻塞操作。
在client.go的setRequestCancel函数中,虽然创建了带截止时间的上下文,但Transport.CancelRequest对于HTTP/2请求的支持不完整。查看transport.go中的cancelRequest方法:
func (t *Transport) cancelRequest(key cancelKey) {
t.reqMu.Lock()
defer t.reqMu.Unlock()
c := t.reqCanceler[key]
if c != nil {
c()
delete(t.reqCanceler, key)
}
}
对于HTTP/2,取消请求是通过流级别的取消实现的,但这需要连接已经建立。如果连接尚未建立(比如在DNS解析阶段),取消可能无法正确传播。
解决方案是使用带有超时的上下文,并确保在请求的所有阶段都能正确传播取消信号:
func makeRequestWithContext() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET",
"https://invalid-hostname.example.com", nil)
if err != nil {
panic(err)
}
// 使用没有Timeout设置的Client,依赖上下文超时
client := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
},
}
start := time.Now()
_, err = client.Do(req)
elapsed := time.Since(start)
fmt.Printf("使用上下文的请求耗时: %v\n", elapsed)
fmt.Printf("错误: %v\n", err)
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("上下文超时生效")
}
}
另外,可以自定义Transport的DialContext函数,在DNS解析阶段就支持上下文取消:
func makeRequestWithCustomDialer() {
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
tr := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// 在DNS解析前检查上下文是否已取消
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
return dialer.DialContext(ctx, network, addr)
},
ForceAttemptHTTP2: true,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
}
client := &http.Client{
Transport: tr,
Timeout: 3 * time.Second,
}
req, _ := http.NewRequest("GET", "https://invalid-hostname.example.com", nil)
start := time.Now()
_, err := client.Do(req)
elapsed := time.Since(start)
fmt.Printf("自定义Dialer请求耗时: %v\n", elapsed)
fmt.Printf("错误: %v\n", err)
}
在HTTP/2场景下,当使用无效主机名时,超时机制失效的主要原因是:
- DNS解析阶段可能被阻塞,而标准超时机制无法正确中断
- HTTP/2的连接复用机制使得请求取消更加复杂
Transport.CancelRequest对HTTP/2的支持有限
建议使用http.NewRequestWithContext创建请求,并确保在自定义传输层中正确处理上下文取消,特别是在DNS解析和连接建立阶段。

