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-12 09:46:02 +08:00
parent 683707fcd4
commit f883944b2b
21 changed files with 3287 additions and 251 deletions
+639
View File
@@ -0,0 +1,639 @@
package core
import (
"math"
"sync"
"testing"
"time"
)
// =============================================================================
// computeRetries 边界
// =============================================================================
func TestComputeRetries_EdgeCases(t *testing.T) {
tests := []struct {
lossRate float64
wantMin int
wantMax int
desc string
}{
{-0.5, 1, 1, "负数丢包率: 视为零"},
{-1.0, 1, 1, "负一: 视为零"},
{0.0, 1, 1, "精确零"},
{0.001, 1, 1, "精确边界 0.001"},
{0.0009, 1, 1, "低于 0.001 边界"},
{0.0011, 1, 6, "高于 0.001 边界"},
{0.95, 6, 6, "精确边界 0.95"},
{0.949, 1, 6, "低于 0.95 边界"},
{0.951, 6, 6, "高于 0.95 边界"},
{1.0, 6, 6, "精确 1.0"},
{1.5, 6, 6, "超过 1.0"},
{100.0, 6, 6, "极大值"},
{math.SmallestNonzeroFloat64, 1, 1, "最小正浮点数"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got := computeRetries(tt.lossRate)
if got < tt.wantMin || got > tt.wantMax {
t.Errorf("computeRetries(%v) = %d, want [%d, %d]",
tt.lossRate, got, tt.wantMin, tt.wantMax)
}
if got < 1 || got > 6 {
t.Errorf("computeRetries(%v) = %d, 超出 [1,6] 范围", tt.lossRate, got)
}
})
}
}
func TestComputeRetries_NaN_Inf(t *testing.T) {
// 确保不 panic
for _, v := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
got := computeRetries(v)
if got < 1 || got > 6 {
t.Errorf("computeRetries(%v) = %d, 超出 [1,6] 范围", v, got)
}
}
}
// =============================================================================
// computeICMPRate 边界
// =============================================================================
func TestComputeICMPRate_EdgeCases(t *testing.T) {
tests := []struct {
env NetworkEnv
fdLimit int
desc string
}{
{EnvLAN, 1, "fd=1: 极小"},
{EnvLAN, -1, "fd=负数: 应被忽略"},
{EnvLAN, 0, "fd=0: 未知"},
{EnvLAN, math.MaxInt32, "fd=极大"},
{NetworkEnv(99), 1024, "未知环境类型"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
net := &NetworkProfile{Env: tt.env}
sys := &SystemProfile{FDLimit: tt.fdLimit}
got := computeICMPRate(net, sys)
if got <= 0 || math.IsNaN(got) || math.IsInf(got, 0) {
t.Errorf("computeICMPRate(env=%v, fd=%d) = %v, 无效值", tt.env, tt.fdLimit, got)
}
})
}
}
// =============================================================================
// classifyEnv 精确边界值
// =============================================================================
func TestClassifyEnv_ExactBoundaries(t *testing.T) {
tests := []struct {
median time.Duration
lossRate float64
want NetworkEnv
desc string
}{
// RTT 边界
{4999 * time.Microsecond, 0.0, EnvLAN, "4.999ms → LAN"},
{5 * time.Millisecond, 0.0, EnvWAN, "精确 5ms → WAN"},
{49999 * time.Microsecond, 0.0, EnvWAN, "49.999ms → WAN"},
{50 * time.Millisecond, 0.0, EnvInternet, "精确 50ms → Internet"},
{199999 * time.Microsecond, 0.0, EnvInternet, "199.999ms → Internet"},
{200 * time.Millisecond, 0.0, EnvSlow, "精确 200ms → Slow"},
// 丢包率边界
{1 * time.Millisecond, 0.009, EnvLAN, "丢包 0.9% → LAN"},
{1 * time.Millisecond, 0.01, EnvWAN, "精确 1% → WAN (不满足 < 0.01)"},
{1 * time.Millisecond, 0.011, EnvWAN, "丢包 1.1% → WAN (超过 LAN 阈值)"},
{20 * time.Millisecond, 0.049, EnvWAN, "丢包 4.9% → WAN"},
{20 * time.Millisecond, 0.05, EnvInternet, "精确 5% → Internet (不满足 < 0.05)"},
{20 * time.Millisecond, 0.051, EnvInternet, "丢包 5.1% → Internet"},
{1 * time.Millisecond, 0.099, EnvInternet, "丢包 9.9% → Internet"},
{1 * time.Millisecond, 0.10, EnvInternet, "精确 10% → Internet (< 判断)"},
{1 * time.Millisecond, 0.101, EnvSlow, "丢包 10.1% → Slow"},
// 零值
{0, 0.0, EnvLAN, "零 RTT 零丢包 → LAN"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got := classifyEnv(tt.median, tt.lossRate)
if got != tt.want {
t.Errorf("classifyEnv(median=%v, loss=%.4f) = %v, want %v",
tt.median, tt.lossRate, got, tt.want)
}
})
}
}
// =============================================================================
// classifyNetwork 边界
// =============================================================================
func TestClassifyNetwork_EdgeCases(t *testing.T) {
t.Run("单个 RTT 样本", func(t *testing.T) {
p := classifyNetwork([]time.Duration{5 * time.Millisecond}, 0, 1)
if p.Samples != 1 {
t.Errorf("samples = %d, want 1", p.Samples)
}
// stddev 应该是 0
if p.RTTStddev != 0 {
t.Errorf("单样本 stddev = %v, want 0", p.RTTStddev)
}
})
t.Run("所有 RTT 相同", func(t *testing.T) {
rtts := make([]time.Duration, 50)
for i := range rtts {
rtts[i] = 10 * time.Millisecond
}
p := classifyNetwork(rtts, 0, 50)
if p.RTTStddev != 0 {
t.Errorf("全相同 RTT stddev = %v, want 0", p.RTTStddev)
}
if p.RTTMedian != 10*time.Millisecond {
t.Errorf("median = %v, want 10ms", p.RTTMedian)
}
})
t.Run("极大 RTT 值", func(t *testing.T) {
rtts := []time.Duration{time.Hour, time.Hour, time.Hour}
p := classifyNetwork(rtts, 0, 3)
if p.Env != EnvSlow {
t.Errorf("env = %v, want Slow", p.Env)
}
})
t.Run("混合极端值", func(t *testing.T) {
rtts := []time.Duration{time.Microsecond, time.Hour}
p := classifyNetwork(rtts, 0, 2)
// 不 panic 就行
if p.Samples != 2 {
t.Errorf("samples = %d, want 2", p.Samples)
}
})
t.Run("全部失败无响应", func(t *testing.T) {
p := classifyNetwork(nil, 100, 100)
if p.Env != EnvWAN {
t.Errorf("env = %v, want WAN (default)", p.Env)
}
})
t.Run("failures > total (异常输入)", func(t *testing.T) {
rtts := []time.Duration{time.Millisecond}
p := classifyNetwork(rtts, 10, 5) // failures > total
// lossRate = 1 - 1/5 = 0.8, 不应 panic
if p.LossRate < 0 {
t.Errorf("lossRate = %.2f, 不应为负", p.LossRate)
}
})
t.Run("total=0", func(t *testing.T) {
p := classifyNetwork(nil, 0, 0)
// 不 panic
if p.Samples != 0 {
t.Errorf("samples = %d, want 0", p.Samples)
}
})
}
// =============================================================================
// RecommendConcurrency 边界
// =============================================================================
func TestRecommendConcurrency_EdgeCases(t *testing.T) {
tests := []struct {
env NetworkEnv
loss float64
userT int
explicit bool
desc string
}{
{EnvLAN, 0.0, 0, false, "userThreadNum=0"},
{EnvLAN, 0.0, 1, false, "userThreadNum=1"},
{EnvLAN, 0.0, -1, false, "userThreadNum 负数"},
{EnvLAN, 0.0, math.MaxInt32, false, "userThreadNum 极大"},
{EnvLAN, 0.99, 600, false, "99% 丢包"},
{EnvLAN, 1.0, 600, false, "100% 丢包"},
{EnvSlow, 0.0, 1, true, "慢速+显式+1"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
p := &NetworkProfile{Env: tt.env, LossRate: tt.loss, Samples: 10}
target, ceiling := p.RecommendConcurrency(tt.userT, tt.explicit)
// 不 panic,且 target >= 1(clamp 保底 10 或 userT)
if target < 0 || ceiling < 0 {
t.Errorf("target=%d ceiling=%d, 不应为负", target, ceiling)
}
if tt.explicit && ceiling != tt.userT && tt.userT > 0 {
t.Errorf("显式模式 ceiling=%d, want %d", ceiling, tt.userT)
}
t.Logf("env=%v loss=%.2f userT=%d explicit=%v → target=%d ceiling=%d",
tt.env, tt.loss, tt.userT, tt.explicit, target, ceiling)
})
}
}
// =============================================================================
// ScanMetrics 边界
// =============================================================================
func TestScanMetrics_EdgeCases(t *testing.T) {
t.Run("RTT=0", func(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(0)
// 不 panic
if m.Total() != 1 {
t.Errorf("Total = %d, want 1", m.Total())
}
})
t.Run("负数 RTT", func(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(-time.Millisecond)
// 不 panic,负数 RTT 应被忽略
if m.rttSamples.Load() != 0 {
t.Errorf("负数 RTT 不应计入采样: got %d", m.rttSamples.Load())
}
})
t.Run("极大 RTT", func(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(time.Hour)
if m.RTTFast() != time.Hour {
t.Errorf("首个样本 RTTFast = %v, want 1h", m.RTTFast())
}
})
t.Run("EMA 首个样本初始化", func(t *testing.T) {
m := &ScanMetrics{}
m.RecordConnect(10 * time.Millisecond)
if m.rttFastNs.Load() != int64(10*time.Millisecond) {
t.Errorf("首个样本应直接设置 EMA: got %d", m.rttFastNs.Load())
}
})
t.Run("空 Snapshot", func(t *testing.T) {
m := &ScanMetrics{}
snap := m.Snapshot()
if snap.Total() != 0 {
t.Errorf("空 metrics Snapshot.Total = %d, want 0", snap.Total())
}
})
t.Run("RTTRatio 单侧为零", func(t *testing.T) {
m := &ScanMetrics{}
// 手动设置一个但不设另一个——不应该发生,但防御
m.rttFastNs.Store(1000)
m.rttSlowNs.Store(0)
m.rttSamples.Store(30)
ratio := m.RTTRatio()
if ratio != 1.0 {
t.Errorf("slow=0 时 ratio = %.2f, want 1.0", ratio)
}
})
t.Run("大量操作不溢出", func(t *testing.T) {
m := &ScanMetrics{}
for i := 0; i < 100000; i++ {
m.RecordConnect(time.Millisecond)
}
if m.Total() != 100000 {
t.Errorf("Total = %d, want 100000", m.Total())
}
ratio := m.RTTRatio()
if math.IsNaN(ratio) || math.IsInf(ratio, 0) {
t.Errorf("大量样本后 ratio = %v, 不应为 NaN/Inf", ratio)
}
})
}
// =============================================================================
// TuneConfig 边界
// =============================================================================
func TestTuneConfig_EdgeCases(t *testing.T) {
t.Run("RTTMedian=0 RTTStddev=0", func(t *testing.T) {
config := makeDefaultConfig()
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Env: EnvLAN, RTTMedian: 0, RTTStddev: 0, Samples: 10},
System: SystemProfile{FDLimit: 65536},
}
ep.TuneConfig(config, session)
// Timeout: median(0) + 4*stddev(0) = 0 → minTO = 0+200ms → clamp to 1s
if config.Timeout < time.Second {
t.Errorf("零 RTT Timeout = %v, 应该 >= 1s", config.Timeout)
}
})
t.Run("RTTStddev 远大于 RTTMedian", func(t *testing.T) {
config := makeDefaultConfig()
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Env: EnvInternet, RTTMedian: 10 * time.Millisecond, RTTStddev: 5 * time.Second, Samples: 10},
System: SystemProfile{FDLimit: 65536},
}
ep.TuneConfig(config, session)
// Timeout = 10ms + 4*5s = 20.01s → clamp to 10s
if config.Timeout != 10*time.Second {
t.Errorf("极大 stddev Timeout = %v, 应该被 clamp 到 10s", config.Timeout)
}
})
t.Run("ThreadNum=0", func(t *testing.T) {
config := makeDefaultConfig()
config.ThreadNum = 0
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Env: EnvLAN, RTTMedian: time.Millisecond, RTTStddev: time.Millisecond, Samples: 10},
System: SystemProfile{FDLimit: 65536},
}
ep.TuneConfig(config, session)
// ModuleThreadNum = 0/30 = 0 → clamp to 5
if config.ModuleThreadNum < 5 {
t.Errorf("ThreadNum=0 时 ModuleThreadNum = %d, 应该 >= 5", config.ModuleThreadNum)
}
})
t.Run("多次调用 TuneConfig", func(t *testing.T) {
config := makeDefaultConfig()
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Env: EnvLAN, RTTMedian: time.Millisecond, RTTStddev: time.Millisecond, LossRate: 0.0, Samples: 10},
System: SystemProfile{FDLimit: 65536},
}
ep.TuneConfig(config, session)
first := config.Timeout
// 第二次调用——已经调整过的值不等于默认值,应被视为"显式"
ep.TuneConfig(config, session)
second := config.Timeout
if first != second {
t.Errorf("多次调用 TuneConfig 不应重复调整: %v vs %v", first, second)
}
})
t.Run("fd limit = ThreadNum 精确值", func(t *testing.T) {
config := makeDefaultConfig()
config.ThreadNum = 600
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Samples: 0},
System: SystemProfile{FDLimit: 1000}, // 1000 * 0.6 = 600
}
ep.TuneConfig(config, session)
// ThreadNum(600) == maxConcurrency(600), 不应触发约束
if config.ThreadNum != 600 {
t.Errorf("fd=1000 时 ThreadNum = %d, 不应被约束", config.ThreadNum)
}
})
t.Run("fd limit 精确低于 ThreadNum", func(t *testing.T) {
config := makeDefaultConfig()
config.ThreadNum = 600
session := makeTestSession(config)
ep := &EnvironmentProfile{
Net: NetworkProfile{Samples: 0},
System: SystemProfile{FDLimit: 999}, // 999 * 0.6 = 599
}
ep.TuneConfig(config, session)
if config.ThreadNum > 599 {
t.Errorf("fd=999 时 ThreadNum = %d, 应该 <= 599", config.ThreadNum)
}
})
}
// =============================================================================
// AdaptivePool 边界
// =============================================================================
func TestAdaptivePool_EdgeCases(t *testing.T) {
t.Run("target=1", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(1, 1, func(interface{}) {}, metrics)
if err != nil {
t.Fatalf("创建失败: %v", err)
}
defer pool.Release()
// initial = max(1/4, 10) = 10 → 但 10 > target(1)... 看实现
// 实际上 initial = min(max(1/4, 10), 1) = 1... 不对
// initial = target/4 = 0, 但 < 10, 所以 initial = 10
// 但 initial > target(1)... initial = min(10, 1) = 1
// 看代码:if initial > target { initial = target }
if pool.Cap() != 1 {
t.Errorf("target=1 时 cap = %d, want 1", pool.Cap())
}
})
t.Run("target=0", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(0, 0, func(interface{}) {}, metrics)
// ants 可能拒绝 size=0
if err != nil {
t.Logf("target=0 正确返回错误: %v", err)
return
}
defer pool.Release()
t.Logf("target=0 cap = %d", pool.Cap())
})
t.Run("ceiling < target", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(100, 50, func(interface{}) {}, metrics)
if err != nil {
t.Fatalf("创建失败: %v", err)
}
defer pool.Release()
// initial = 100/4 = 25, 不超过 ceiling
if pool.Cap() > 50 {
t.Errorf("ceiling=50 但 cap = %d", pool.Cap())
}
})
t.Run("高频 Invoke 不 panic", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(10, 10, func(interface{}) {
time.Sleep(time.Millisecond)
}, metrics)
if err != nil {
t.Fatalf("创建失败: %v", err)
}
defer pool.Release()
pool.inSlowStart = false
pool.tune(10)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = pool.Invoke(nil)
}()
}
wg.Wait()
pool.Wait()
})
t.Run("assessHealth 零增量", func(t *testing.T) {
metrics := &ScanMetrics{}
pool, err := NewAdaptivePool(100, 100, func(interface{}) {}, metrics)
if err != nil {
t.Fatalf("创建失败: %v", err)
}
defer pool.Release()
// 初始化 prevSnapshot 后不产生新数据
pool.prevSnapshot = metrics.Snapshot()
health := pool.assessHealth()
if health != HealthUnknown {
t.Errorf("零增量应返回 HealthUnknown, got %v", health)
}
})
}
// =============================================================================
// pickSamples 边界
// =============================================================================
func TestPickSamples_EdgeCases(t *testing.T) {
t.Run("maxSamples=0", func(t *testing.T) {
s := pickSamples([]string{"a", "b"}, 0)
if len(s) != 0 {
t.Errorf("maxSamples=0 应返回空, got %d", len(s))
}
})
t.Run("maxSamples=1", func(t *testing.T) {
s := pickSamples([]string{"a", "b", "c"}, 1)
if len(s) != 1 {
t.Errorf("maxSamples=1 应返回 1 个, got %d", len(s))
}
})
t.Run("hosts 等于 maxSamples", func(t *testing.T) {
hosts := []string{"a", "b", "c"}
s := pickSamples(hosts, 3)
if len(s) != 3 {
t.Errorf("应返回全部, got %d", len(s))
}
})
}
// =============================================================================
// isTimeoutError / isConnectionRefused 边界
// =============================================================================
func TestIsTimeoutError_EdgeCases(t *testing.T) {
if isTimeoutError(nil) {
t.Error("nil 不应判为 timeout")
}
}
func TestIsConnectionRefused_EdgeCases(t *testing.T) {
if isConnectionRefused(nil) {
t.Error("nil 不应判为 refused")
}
}
// =============================================================================
// NetworkEnv.String 覆盖
// =============================================================================
func TestNetworkEnv_String(t *testing.T) {
for _, env := range []NetworkEnv{EnvLAN, EnvWAN, EnvInternet, EnvSlow} {
s := env.String()
if s == "" {
t.Errorf("NetworkEnv(%d).String() = 空", env)
}
}
// 未知值
s := NetworkEnv(99).String()
if s == "" {
t.Error("未知 NetworkEnv.String() = 空")
}
}
// =============================================================================
// clampInt / clampDuration 边界
// =============================================================================
func TestClampInt(t *testing.T) {
tests := []struct {
v, min, max, want int
}{
{5, 1, 10, 5},
{0, 1, 10, 1},
{15, 1, 10, 10},
{-5, -10, -1, -5},
{5, 5, 5, 5}, // min == max == v
{3, 5, 5, 5}, // v < min == max
{10, 5, 5, 5}, // v > min == max
}
for _, tt := range tests {
got := clampInt(tt.v, tt.min, tt.max)
if got != tt.want {
t.Errorf("clampInt(%d, %d, %d) = %d, want %d", tt.v, tt.min, tt.max, got, tt.want)
}
}
}
func TestClampDuration(t *testing.T) {
got := clampDuration(5*time.Second, time.Second, 10*time.Second)
if got != 5*time.Second {
t.Errorf("got %v, want 5s", got)
}
got = clampDuration(0, time.Second, 10*time.Second)
if got != time.Second {
t.Errorf("got %v, want 1s", got)
}
got = clampDuration(time.Hour, time.Second, 10*time.Second)
if got != 10*time.Second {
t.Errorf("got %v, want 10s", got)
}
}
// =============================================================================
// isExplicit 边界
// =============================================================================
func TestIsExplicit(t *testing.T) {
config := makeDefaultConfig()
// 默认值 → 非显式
if isExplicit(config, "time") {
t.Error("默认 Timeout 不应视为显式")
}
if isExplicit(config, "mt") {
t.Error("默认 ModuleThreadNum 不应视为显式")
}
if isExplicit(config, "retry") {
t.Error("默认 MaxRetries 不应视为显式")
}
if isExplicit(config, "icmp-rate") {
t.Error("默认 ICMPRate 不应视为显式")
}
if isExplicit(config, "num") {
t.Error("默认 PocNum 不应视为显式")
}
// 未知 flag
if isExplicit(config, "nonexistent") {
t.Error("未知 flag 不应视为显式")
}
// ThreadNumExplicit
config.ThreadNumExplicit = true
if !isExplicit(config, "t") {
t.Error("ThreadNumExplicit=true 应视为显式")
}
}