feat: 自适应并发调度 — 网络探测 + AIMD + 参数智能推导

扫描前自动探测网络环境(RTT、丢包率、fd limit),基于探测数据
推导 6 个关键参数,替代硬编码默认值:

- Timeout: median_RTT + 4σ(覆盖 99.9% 正常连接)
- ModuleThreadNum: target_concurrency / 30
- MaxRetries: ceil(log(0.01)/log(loss_rate))(全失败概率 <1%)
- ICMPRate: 环境基准 × fd 系数
- PocNum: 跟随 ModuleThreadNum
- DisablePing: 已有 ICMP 权限降级机制

线程池从单信号(资源耗尽率)升级为 AIMD + 慢启动:
- 慢启动:target/4 起步,500ms 翻倍
- 稳态 AIMD:健康 +5%,拥塞 ×0.5
- 双信号:资源耗尽率 + RTT 趋势(双 EMA)

用户 -t 显式指定时作为 ceiling,探测仍调整其他参数。

测试:单元 + 边界 + 集成 + 真实网络,core 包 580+ 用例全通过。
This commit is contained in:
ZacharyZcR
2026-06-14 22:23:45 +08:00
parent e0468ecd35
commit 88c7e4f2be
21 changed files with 3287 additions and 251 deletions
+169
View File
@@ -0,0 +1,169 @@
package core
import (
"testing"
"time"
)
// =============================================================================
// 单元测试:classifyEnv — 网络环境分类
// =============================================================================
func TestClassifyEnv(t *testing.T) {
tests := []struct {
median time.Duration
lossRate float64
wantEnv NetworkEnv
desc string
}{
{1 * time.Millisecond, 0.0, EnvLAN, "1ms 零丢包 → 内网"},
{3 * time.Millisecond, 0.005, EnvLAN, "3ms 0.5%丢包 → 内网"},
{5 * time.Millisecond, 0.0, EnvWAN, "5ms 零丢包 → 局域网边界"},
{20 * time.Millisecond, 0.02, EnvWAN, "20ms 2%丢包 → 局域网"},
{50 * time.Millisecond, 0.03, EnvInternet, "50ms 3%丢包 → 公网边界"},
{100 * time.Millisecond, 0.05, EnvInternet, "100ms 5%丢包 → 公网"},
{300 * time.Millisecond, 0.05, EnvSlow, "300ms → 慢速"},
{50 * time.Millisecond, 0.15, EnvSlow, "50ms 15%丢包 → 高丢包归类慢速"},
{1 * time.Millisecond, 0.20, EnvSlow, "低延迟但高丢包 → 慢速"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got := classifyEnv(tt.median, tt.lossRate)
if got != tt.wantEnv {
t.Errorf("classifyEnv(median=%v, loss=%.2f) = %v, want %v",
tt.median, tt.lossRate, got, tt.wantEnv)
}
})
}
}
// =============================================================================
// 单元测试:classifyNetwork — 从 RTT 样本推导 profile
// =============================================================================
func TestClassifyNetwork(t *testing.T) {
t.Run("内网 RTT 分布", func(t *testing.T) {
rtts := makeDurations([]int{1, 1, 1, 2, 2, 2, 3, 3, 4, 5}) // ms
p := classifyNetwork(rtts, 0, 10)
if p.Env != EnvLAN {
t.Errorf("env = %v, want LAN", p.Env)
}
if p.RTTMedian > 5*time.Millisecond {
t.Errorf("median = %v, want < 5ms", p.RTTMedian)
}
if p.LossRate != 0 {
t.Errorf("lossRate = %.2f, want 0", p.LossRate)
}
})
t.Run("公网 RTT 分布(低丢包)", func(t *testing.T) {
rtts := makeDurations([]int{60, 70, 80, 90, 100, 110, 120, 150, 200, 300}) // ms
p := classifyNetwork(rtts, 0, 10) // 无丢包
if p.Env != EnvInternet {
t.Errorf("env = %v, want Internet", p.Env)
}
if p.LossRate != 0 {
t.Errorf("lossRate = %.2f, want 0", p.LossRate)
}
})
t.Run("高丢包归类为慢速", func(t *testing.T) {
rtts := makeDurations([]int{60, 70, 80, 90, 100}) // ms, 5 responded
p := classifyNetwork(rtts, 5, 10) // 50% loss
if p.Env != EnvSlow {
t.Errorf("env = %v, want Slow (高丢包)", p.Env)
}
})
t.Run("零样本降级", func(t *testing.T) {
p := classifyNetwork(nil, 5, 5)
if p.Env != EnvWAN {
t.Errorf("env = %v, want WAN (default)", p.Env)
}
if p.Samples != 0 {
t.Errorf("samples = %d, want 0", p.Samples)
}
})
}
// =============================================================================
// 单元测试:RecommendConcurrency
// =============================================================================
func TestRecommendConcurrency(t *testing.T) {
tests := []struct {
env NetworkEnv
lossRate float64
userT int
explicit bool
wantTMin int
wantTMax int
wantCeil int
desc string
}{
{EnvLAN, 0.0, 600, false, 800, 1000, -1, "内网自动: ×1.5"},
{EnvWAN, 0.0, 600, false, 550, 650, -1, "局域网自动: ×1.0"},
{EnvInternet, 0.0, 600, false, 200, 280, -1, "公网自动: ×0.4"},
{EnvSlow, 0.0, 600, false, 80, 100, -1, "慢速自动: ×0.15"},
{EnvInternet, 0.0, 200, true, 70, 100, 200, "公网显式: target<ceiling"},
{EnvLAN, 0.0, 100, true, 100, 160, 100, "内网显式: ceiling=用户值"},
{EnvInternet, 0.15, 600, false, 170, 240, -1, "公网高丢包: 进一步压缩"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
p := &NetworkProfile{Env: tt.env, LossRate: tt.lossRate, Samples: 10}
target, ceiling := p.RecommendConcurrency(tt.userT, tt.explicit)
if target < tt.wantTMin || target > tt.wantTMax {
t.Errorf("target = %d, want [%d, %d]", target, tt.wantTMin, tt.wantTMax)
}
if tt.explicit && ceiling != tt.wantCeil {
t.Errorf("ceiling = %d, want %d", ceiling, tt.wantCeil)
}
})
}
}
// =============================================================================
// 单元测试:pickSamples
// =============================================================================
func TestPickSamples(t *testing.T) {
hosts := make([]string, 100)
for i := range hosts {
hosts[i] = "host"
}
s := pickSamples(hosts, 10)
if len(s) != 10 {
t.Errorf("pickSamples(100, 10) = %d items, want 10", len(s))
}
s = pickSamples(hosts[:5], 10)
if len(s) != 5 {
t.Errorf("pickSamples(5, 10) = %d items, want 5", len(s))
}
s = pickSamples(nil, 10)
if len(s) != 0 {
t.Errorf("pickSamples(nil, 10) = %d items, want 0", len(s))
}
}
// =============================================================================
// 辅助
// =============================================================================
func makeDurations(ms []int) []time.Duration {
ds := make([]time.Duration, len(ms))
for i, m := range ms {
ds[i] = time.Duration(m) * time.Millisecond
}
return ds
}