Golang中net.Dialer调用DialContext时域名地址导致概率性连接超时问题
Golang中net.Dialer调用DialContext时域名地址导致概率性连接超时问题
Golang 版本:1.22.2
构建系统:AlmaLinux release 9.3
当 dialContext 传入的地址是域名时,对于域名解析出的多个 IP,我通过 iptables 的 drop 策略阻断其中一个 IP 的网络来模拟网络故障。
查阅文档得知,dialcontext 在传入域名时,会为解析出的多个 IP 平均分配设定的总超时时间。
我测试的 dialcontext 示例传入的地址是 www.baidu.com:443,超时时间设为 2 秒。在我的网络环境中,www.baidu.com 解析为 2 个 IP。理论上,每个 IP 的连接尝试超时时间应为 1 秒。
但在我的重复测试中,我发现了三种不同的结果:网络连接快速(可能第一个尝试连接的 IP 未被 iptables 阻断)和网络连接延迟(可能第一个尝试连接的 IP 被 iptables 阻断,但尝试连接第二个正常 IP 成功)。前两种情况在我的预期之内。第三种结果是 dialcontext 返回超时,这个结果超出了预期。
第三种情况的出现概率非常高。即使在添加了重试机制后,这个问题依然存在。
我使用 strace 追踪了网络调用。connect 调用的是 9 号端口,而不是 443 端口。我不明白原因。
在第三种情况下,我用 tcpdump 抓取 443 端口的包,没有看到系统发出的 TCP SYN 握手包。
connect(8, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("223.5.5.5")}, 16) = 0 <0.000027>
getsockname(8, {sa_family=AF_INET, sin_port=htons(60874), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000036>
getpeername(8, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("223.5.5.5")}, [112 => 16]) = 0 <0.000011>
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000027>
setsockopt(8, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000032>
connect(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.42")}, 16) = 0 <0.000021>
getsockname(8, {sa_family=AF_INET, sin_port=htons(46937), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000016>
getpeername(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.42")}, [112 => 16]) = 0 <0.000016>
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000017>
setsockopt(8, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000013>
connect(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.185")}, 16) = 0 <0.000013>
getsockname(8, {sa_family=AF_INET, sin_port=htons(60023), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000026>
getpeername(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.185")}, [112 => 16]) = 0 <0.000013>
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000031>
connect(8, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("183.2.172.42")}, 16) = -1 EINPROGRESS (Operation now in progress) <0.000043>
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000044>
setsockopt(8, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000062>
connect(8, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("223.5.5.5")}, 16) = 0 <0.000027>
getsockname(8, {sa_family=AF_INET, sin_port=htons(56527), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000011>
getpeername(8, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("223.5.5.5")}, [112 => 16]) = 0 <0.000008>
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000054>
setsockopt(8, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000049>
connect(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.42")}, 16) = 0 <0.000035>
getsockname(8, {sa_family=AF_INET, sin_port=htons(38185), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000012>
getpeername(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.42")}, [112 => 16]) = 0 <0.000028>
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000188>
setsockopt(8, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000026>
connect(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.185")}, 16) = 0 <0.000158>
getsockname(8, {sa_family=AF_INET, sin_port=htons(60410), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000055>
getpeername(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.185")}, [112 => 16]) = 0 <0.000019>
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000029>
connect(8, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("183.2.172.42")}, 16) = -1 EINPROGRESS (Operation now in progress) <0.000061>
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000041>
setsockopt(8, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000013>
connect(8, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("223.5.5.5")}, 16) = 0 <0.000052>
getsockname(8, {sa_family=AF_INET, sin_port=htons(49188), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000009>
getpeername(8, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("223.5.5.5")}, [112 => 16]) = 0 <0.000021>
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000026>
setsockopt(8, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000047>
connect(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.42")}, 16) = 0 <0.000028>
getsockname(8, {sa_family=AF_INET, sin_port=htons(37913), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000018>
getpeername(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.42")}, [112 => 16]) = 0 <0.000023>
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000016>
setsockopt(8, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0 <0.000018>
connect(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.185")}, 16) = 0 <0.000018>
getsockname(8, {sa_family=AF_INET, sin_port=htons(48424), sin_addr=inet_addr("172.20.0.13")}, [112 => 16]) = 0 <0.000018>
getpeername(8, {sa_family=AF_INET, sin_port=htons(9), sin_addr=inet_addr("183.2.172.185")}, [112 => 16]) = 0 <0.000012>
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 8 <0.000019>
connect(8, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("183.2.172.42")}, 16) = -1 EINPROGRESS (Operation now in progress) <0.000059>
--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} ---
更多关于Golang中net.Dialer调用DialContext时域名地址导致概率性连接超时问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
是的。一个IP的最小超时时间可能是2秒。
更多关于Golang中net.Dialer调用DialContext时域名地址导致概率性连接超时问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这没有问题,因为你设置的超时时间太短,可能在执行某些操作之前就已经超时了。
这个问题涉及Go的net.Dialer在域名解析和连接超时处理上的细节。从你的描述和strace输出看,关键点在于连接9号端口的行为和超时计算方式。
首先,连接9号端口(discard服务)是Go的happy eyeballs实现的一部分,用于快速检测IPv6/IPv4连通性。以下是相关代码示例:
package main
import (
"context"
"fmt"
"net"
"time"
)
func main() {
dialer := &net.Dialer{
Timeout: 2 * time.Second,
}
ctx := context.Background()
conn, err := dialer.DialContext(ctx, "tcp", "www.baidu.com:443")
if err != nil {
fmt.Printf("Dial error: %v\n", err)
return
}
defer conn.Close()
fmt.Println("Connected successfully")
}
关于超时分配,DialContext确实会将总超时时间分配给每个IP地址。但需要注意,这个分配包括DNS查询时间和连接建立时间。从你的strace可以看到多次DNS查询(端口53)和9号端口的连接尝试,这些都会消耗时间。
问题可能出现在以下几个方面:
-
超时计算包含DNS重试:当第一个IP被阻断时,系统可能会重试DNS查询,这会消耗额外时间。
-
happy eyeballs实现:对每个IP地址,Go会先尝试连接9号端口进行快速检测。从你的
strace可以看到多次connect到9号端口的调用,这些调用虽然快速失败,但仍会消耗时间。 -
连接尝试顺序:即使第二个IP正常,如果第一个IP的连接尝试占用了大部分超时时间,可能导致第二个IP的实际可用时间不足。
以下是更详细的调试示例,可以查看实际连接尝试:
package main
import (
"context"
"fmt"
"net"
"time"
"syscall"
)
type traceDialer struct {
net.Dialer
}
func (d *traceDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
start := time.Now()
fmt.Printf("DialContext called at %v\n", start)
conn, err := d.Dialer.DialContext(ctx, network, address)
elapsed := time.Since(start)
if err != nil {
fmt.Printf("Dial failed after %v: %v\n", elapsed, err)
} else {
fmt.Printf("Dial succeeded after %v\n", elapsed)
}
return conn, err
}
func main() {
dialer := &traceDialer{
Dialer: net.Dialer{
Timeout: 2 * time.Second,
Control: func(network, address string, c syscall.RawConn) error {
fmt.Printf("Creating connection to %s\n", address)
return nil
},
},
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
for i := 0; i < 5; i++ {
fmt.Printf("\nAttempt %d:\n", i+1)
conn, err := dialer.DialContext(ctx, "tcp", "www.baidu.com:443")
if err != nil {
fmt.Printf("Error: %v\n", err)
time.Sleep(500 * time.Millisecond)
continue
}
conn.Close()
}
}
从你的strace输出可以看到,系统在连接443端口之前,对两个IP地址(183.2.172.42和183.2.172.185)都进行了9号端口的连接尝试。这些尝试虽然快速失败,但在超时严格限制下,可能影响后续连接。
解决这个问题的直接方法是增加超时时间,或者实现自定义的Dialer来控制连接策略:
package main
import (
"context"
"fmt"
"net"
"time"
)
func customDial(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
// 手动解析DNS
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
fmt.Printf("Resolved %s to %v\n", host, ips)
// 为每个IP尝试连接,使用独立的超时
var firstErr error
for _, ip := range ips {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
address := net.JoinHostPort(ip.String(), port)
dialer := &net.Dialer{
Timeout: 1 * time.Second, // 每个IP单独的超时
}
conn, err := dialer.DialContext(ctx, network, address)
if err == nil {
return conn, nil
}
if firstErr == nil {
firstErr = err
}
fmt.Printf("Failed to connect to %s: %v\n", address, err)
}
if firstErr != nil {
return nil, firstErr
}
return nil, fmt.Errorf("no addresses to try")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
conn, err := customDial(ctx, "tcp", "www.baidu.com:443")
if err != nil {
fmt.Printf("Final error: %v\n", err)
return
}
defer conn.Close()
fmt.Println("Connected successfully")
}
这种自定义实现可以更精确地控制每个IP的连接超时,避免Go内置实现中可能的时间分配问题。

