我在拿 NRUP 做一个简单代理转发测试时遇到一个稳定复现的问题:第一包能通,第二包
开始超时。后来定位到 buildSACKBitmap() 里调用了 smallPktSeen(),这个函数不
是只读检查,会直接把 seq 写进 seenSmall,导致 ACK 第一包时把后续 32 个 seq
都提前标记成“已收到”。
相关代码:
func (c *Conn) buildSACKBitmap(baseSeq uint32) uint32 {
var bitmap uint32
for i := uint32(1); i <= 32; i++ {
if c.smallPktSeen(baseSeq + i) {
bitmap |= 1 << (i - 1)
}
}
return bitmap
}
而 smallPktSeen() 里面会改状态:
if c.seenSmall[seq] {
return true
}
c.seenSmall[seq] = true
return false
所以流程会变成:
- 收到 seq=N 的小包
- 回 ACK 时调用 buildSACKBitmap(N)
- buildSACKBitmap() 调用 smallPktSeen(N+1 ... N+32)
- N+1 ... N+32 被提前写进 seenSmall
- 下一包 seq=N+1 真正到达时,被判断成 duplicate
- 包被丢掉,应用层表现为第二包开始 timeout
我这边是在 localhost loopback 和真实远端机器上都复现了,所以不是防火墙/公网丢
包导致的。
实测现象
测试环境:
CN2GIA 电信美西
- 测试方式:额外写了一个简单 NRUP 代理
- ToU:本地 TCP -> NRUP/UDP -> 远端 TCP target
- UoU:本地 UDP -> NRUP/UDP -> 远端 UDP target
- RTT 测试:echo 20 次
- 下载测试:本机相对远端回程下载
修复前:
| 场景 |
结果 |
| TCP echo over NRUP,localhost loopback |
第一包成功,第二包 timeout |
| UDP echo over NRUP,localhost loopback |
第一包成功,后续不稳定/timeout |
| TCP echo over NRUP,本机 -> 远端 |
timeout |
| UDP echo over NRUP,本机 -> 远端 |
timeout |
| iperf3 -R 回程下载 |
无法完成 |
本地日志里能看到第一包确实过了,但后续读不到返回:
pipe a->b done bytes=12 err=
pipe b->a done bytes=6 err=read udp ... use of closed network connection
修复后:
| 场景 |
结果 |
| TCP echo over NRUP,localhost loopback |
20/20 成功,avg 10.75 ms |
| UDP echo over NRUP,localhost loopback |
20/20 成功,avg 10.98 ms |
| ToU echo RTT,本机 -> 远端 |
avg 199.31 ms,p50 178.40 ms |
| UoU echo RTT,本机 -> 远端 |
avg 227.06 ms,p50 204.47 ms |
| ToU iperf3 -R 回程下载 |
24.23 Mbps |
| UoU 低频 UDP echo |
80/80 包,0% loss |
我本地验证过的修法
不要在 buildSACKBitmap() 里调用 smallPktSeen(),改成只读检查 seenSmall:
func (c *Conn) buildSACKBitmap(baseSeq uint32) uint32 {
var bitmap uint32
c.writeMu.Lock()
defer c.writeMu.Unlock()
for i := uint32(1); i <= 32; i++ {
if c.seenSmall != nil && c.seenSmall[baseSeq+i] {
bitmap |= 1 << (i - 1)
}
}
return bitmap
}
另外我还发现 Dial() 里创建客户端 Conn 时没有带上 cfg:
conn := &Conn{
dtls: dtlsConn,
...
}
这样客户端侧配置例如 SACKInterval、SmallPacketThreshold 实际上不会通过 c.cfg
生效。我本地也一起改成了:
conn := &Conn{
cfg: cfg,
dtls: dtlsConn,
...
}
这两个改完后,连续小包代理就能正常跑了。
我在拿 NRUP 做一个简单代理转发测试时遇到一个稳定复现的问题:第一包能通,第二包
开始超时。后来定位到
buildSACKBitmap()里调用了smallPktSeen(),这个函数不是只读检查,会直接把 seq 写进
seenSmall,导致 ACK 第一包时把后续 32 个 seq都提前标记成“已收到”。
相关代码:
所以流程会变成:
我这边是在 localhost loopback 和真实远端机器上都复现了,所以不是防火墙/公网丢
包导致的。
实测现象
测试环境:
修复前:
本地日志里能看到第一包确实过了,但后续读不到返回:
pipe a->b done bytes=12 err=
pipe b->a done bytes=6 err=read udp ... use of closed network connection
修复后:
我本地验证过的修法
不要在 buildSACKBitmap() 里调用 smallPktSeen(),改成只读检查 seenSmall:
另外我还发现 Dial() 里创建客户端 Conn 时没有带上 cfg:
这样客户端侧配置例如 SACKInterval、SmallPacketThreshold 实际上不会通过 c.cfg
生效。我本地也一起改成了:
这两个改完后,连续小包代理就能正常跑了。