mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
扫描前自动探测网络环境(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+ 用例全通过。
546 lines
16 KiB
Go
546 lines
16 KiB
Go
package core
|
||
|
||
import (
|
||
"sync"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// =============================================================================
|
||
// 集成测试 1:探测 → 参数调整 → 线程池创建 完整链路
|
||
// 验证从 NetworkProfile 到 TuneConfig 到 AdaptivePool 的端到端数据流
|
||
// =============================================================================
|
||
|
||
func TestIntegration_ProbeToPool_LAN(t *testing.T) {
|
||
// 模拟内网探测结果
|
||
profile := classifyNetwork(
|
||
makeDurations([]int{1, 1, 2, 2, 2, 3, 3, 3, 4, 5}), // ms
|
||
0, 10,
|
||
)
|
||
|
||
if profile.Env != EnvLAN {
|
||
t.Fatalf("探测环境 = %v, want LAN", profile.Env)
|
||
}
|
||
|
||
// 构建 Config + TuneConfig
|
||
config := makeDefaultConfig()
|
||
session := makeTestSession(config)
|
||
sys := ProbeSystem()
|
||
|
||
ep := &EnvironmentProfile{Net: *profile, System: sys}
|
||
ep.TuneConfig(config, session)
|
||
|
||
// 验证参数被合理调整
|
||
if config.Timeout > 3*time.Second {
|
||
t.Errorf("内网 Timeout = %v, 不应 > 3s", config.Timeout)
|
||
}
|
||
if config.MaxRetries != 1 {
|
||
t.Errorf("内网零丢包 MaxRetries = %d, want 1", config.MaxRetries)
|
||
}
|
||
|
||
// 用调整后的参数创建线程池
|
||
target, ceiling := profile.RecommendConcurrency(config.ThreadNum, config.ThreadNumExplicit)
|
||
metrics := &ScanMetrics{}
|
||
pool, err := NewAdaptivePool(target, ceiling, func(interface{}) {}, metrics)
|
||
if err != nil {
|
||
t.Fatalf("创建池失败: %v", err)
|
||
}
|
||
defer pool.Release()
|
||
|
||
if pool.Cap() <= 0 {
|
||
t.Errorf("池容量 = %d, 应该 > 0", pool.Cap())
|
||
}
|
||
|
||
t.Logf("内网完整链路: Timeout=%v MT=%d Retry=%d ICMP=%.2f target=%d ceiling=%d poolCap=%d",
|
||
config.Timeout, config.ModuleThreadNum, config.MaxRetries,
|
||
config.Network.ICMPRate, target, ceiling, pool.Cap())
|
||
}
|
||
|
||
func TestIntegration_ProbeToPool_Internet(t *testing.T) {
|
||
profile := classifyNetwork(
|
||
makeDurations([]int{60, 70, 80, 90, 100, 110, 120, 130, 140, 150}),
|
||
0, 10,
|
||
)
|
||
|
||
if profile.Env != EnvInternet {
|
||
t.Fatalf("探测环境 = %v, want Internet", profile.Env)
|
||
}
|
||
|
||
config := makeDefaultConfig()
|
||
session := makeTestSession(config)
|
||
ep := &EnvironmentProfile{Net: *profile, System: SystemProfile{FDLimit: 4096, NumCPU: 4}}
|
||
ep.TuneConfig(config, session)
|
||
|
||
target, ceiling := profile.RecommendConcurrency(config.ThreadNum, config.ThreadNumExplicit)
|
||
metrics := &ScanMetrics{}
|
||
pool, err := NewAdaptivePool(target, ceiling, func(interface{}) {}, metrics)
|
||
if err != nil {
|
||
t.Fatalf("创建池失败: %v", err)
|
||
}
|
||
defer pool.Release()
|
||
|
||
// 公网并发应该明显低于默认 600
|
||
if target >= 600 {
|
||
t.Errorf("公网 target = %d, 应该 < 600", target)
|
||
}
|
||
|
||
t.Logf("公网完整链路: Timeout=%v MT=%d Retry=%d target=%d ceiling=%d poolCap=%d",
|
||
config.Timeout, config.ModuleThreadNum, config.MaxRetries, target, ceiling, pool.Cap())
|
||
}
|
||
|
||
// =============================================================================
|
||
// 集成测试 2:AdaptivePool + ScanMetrics 联动
|
||
// 验证:任务执行 → metrics 记录 → 池读取 metrics → 做出调整决策
|
||
// =============================================================================
|
||
|
||
func TestIntegration_PoolMetrics_HealthyTraffic(t *testing.T) {
|
||
metrics := &ScanMetrics{}
|
||
var taskCount atomic.Int64
|
||
|
||
pool, err := NewAdaptivePool(100, 100, func(i interface{}) {
|
||
taskCount.Add(1)
|
||
}, metrics)
|
||
if err != nil {
|
||
t.Fatalf("创建池失败: %v", err)
|
||
}
|
||
defer pool.Release()
|
||
|
||
pool.inSlowStart = false
|
||
pool.tune(100)
|
||
|
||
// 注入健康 metrics
|
||
for i := 0; i < 200; i++ {
|
||
metrics.RecordConnect(time.Millisecond)
|
||
}
|
||
|
||
// 运行任务
|
||
var wg sync.WaitGroup
|
||
for i := 0; i < 200; i++ {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
_ = pool.Invoke(nil)
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
pool.Wait()
|
||
|
||
// 触发调整
|
||
pool.lastCheck.Store(0)
|
||
pool.adjust()
|
||
|
||
if pool.Cap() < 90 {
|
||
t.Errorf("健康流量池容量不应大幅下降: cap = %d", pool.Cap())
|
||
}
|
||
|
||
t.Logf("健康流量: tasks=%d connects=%d cap=%d",
|
||
taskCount.Load(), metrics.Snapshot().Connects, pool.Cap())
|
||
}
|
||
|
||
func TestIntegration_PoolMetrics_ExhaustedTraffic(t *testing.T) {
|
||
metrics := &ScanMetrics{}
|
||
|
||
pool, err := NewAdaptivePool(100, 100, func(i interface{}) {}, metrics)
|
||
if err != nil {
|
||
t.Fatalf("创建池失败: %v", err)
|
||
}
|
||
defer pool.Release()
|
||
|
||
pool.inSlowStart = false
|
||
pool.tune(100)
|
||
|
||
// 直接向 metrics 注入大量资源耗尽事件(模拟扫描过程中的 fd 不足)
|
||
for i := 0; i < 200; i++ {
|
||
metrics.RecordExhausted()
|
||
}
|
||
|
||
// 手动触发调整(清除时间守卫)
|
||
pool.lastCheck.Store(0)
|
||
pool.adjust()
|
||
|
||
// 资源耗尽率 100% → 应该降速
|
||
if pool.Cap() >= 100 {
|
||
t.Errorf("资源耗尽后池应该降速: cap = %d", pool.Cap())
|
||
}
|
||
|
||
t.Logf("资源耗尽: exhausted=%d cap=%d", metrics.Snapshot().Exhausted, pool.Cap())
|
||
}
|
||
|
||
// =============================================================================
|
||
// 集成测试 3:慢启动 → 稳态 AIMD 过渡
|
||
// 验证慢启动阶段的翻倍行为和过渡到稳态的时机
|
||
// =============================================================================
|
||
|
||
func TestIntegration_SlowStartToSteady(t *testing.T) {
|
||
metrics := &ScanMetrics{}
|
||
|
||
pool, err := NewAdaptivePool(100, 100, func(i interface{}) {
|
||
metrics.RecordConnect(time.Millisecond)
|
||
}, metrics)
|
||
if err != nil {
|
||
t.Fatalf("创建池失败: %v", err)
|
||
}
|
||
defer pool.Release()
|
||
|
||
if !pool.inSlowStart {
|
||
t.Fatal("初始应该在慢启动状态")
|
||
}
|
||
|
||
initialCap := pool.Cap()
|
||
t.Logf("慢启动初始: cap=%d", initialCap)
|
||
|
||
// 喂入足够的健康 metrics
|
||
for i := 0; i < 100; i++ {
|
||
metrics.RecordConnect(time.Millisecond)
|
||
}
|
||
|
||
// 模拟多次调整周期
|
||
caps := []int{initialCap}
|
||
for i := 0; i < 10; i++ {
|
||
pool.lastCheck.Store(0) // 强制触发检查
|
||
pool.adjust()
|
||
caps = append(caps, pool.Cap())
|
||
}
|
||
|
||
// 验证:容量应该逐步增长
|
||
growing := false
|
||
for i := 1; i < len(caps); i++ {
|
||
if caps[i] > caps[i-1] {
|
||
growing = true
|
||
break
|
||
}
|
||
}
|
||
if !growing {
|
||
t.Errorf("慢启动期间容量没有增长: %v", caps)
|
||
}
|
||
|
||
// 最终应该退出慢启动
|
||
finalCap := pool.Cap()
|
||
if finalCap < initialCap {
|
||
t.Errorf("最终容量 %d < 初始 %d, 不合理", finalCap, initialCap)
|
||
}
|
||
|
||
t.Logf("慢启动过渡: %v, inSlowStart=%v", caps, pool.inSlowStart)
|
||
}
|
||
|
||
// =============================================================================
|
||
// 集成测试 4:拥塞 → 降速 → 恢复 完整周期
|
||
// =============================================================================
|
||
|
||
func TestIntegration_CongestionRecovery(t *testing.T) {
|
||
metrics := &ScanMetrics{}
|
||
|
||
pool, err := NewAdaptivePool(200, 200, func(i interface{}) {}, metrics)
|
||
if err != nil {
|
||
t.Fatalf("创建池失败: %v", err)
|
||
}
|
||
defer pool.Release()
|
||
|
||
// 直接到稳态,满容量
|
||
pool.inSlowStart = false
|
||
pool.tune(200)
|
||
|
||
// === 阶段 1: 正常运行 ===
|
||
for i := 0; i < 100; i++ {
|
||
metrics.RecordConnect(time.Millisecond)
|
||
}
|
||
pool.lastCheck.Store(0)
|
||
pool.adjust()
|
||
normalCap := pool.Cap()
|
||
t.Logf("正常阶段: cap=%d", normalCap)
|
||
|
||
// === 阶段 2: 突发拥塞(大量资源耗尽)===
|
||
for i := 0; i < 200; i++ {
|
||
metrics.RecordExhausted()
|
||
}
|
||
pool.lastCheck.Store(0)
|
||
pool.adjust()
|
||
congestedCap := pool.Cap()
|
||
|
||
if congestedCap >= normalCap {
|
||
t.Errorf("拥塞后应降速: normal=%d congested=%d", normalCap, congestedCap)
|
||
}
|
||
t.Logf("拥塞阶段: cap=%d (降幅 %d%%)", congestedCap, (normalCap-congestedCap)*100/normalCap)
|
||
|
||
// === 阶段 3: 恢复(大量成功连接)===
|
||
for i := 0; i < 500; i++ {
|
||
metrics.RecordConnect(time.Millisecond)
|
||
}
|
||
|
||
// 多次调整模拟恢复过程
|
||
for i := 0; i < 20; i++ {
|
||
pool.lastCheck.Store(0)
|
||
pool.adjust()
|
||
}
|
||
recoveredCap := pool.Cap()
|
||
|
||
if recoveredCap <= congestedCap {
|
||
t.Errorf("恢复后应提速: congested=%d recovered=%d", congestedCap, recoveredCap)
|
||
}
|
||
|
||
// 恢复后不应超过 ceiling
|
||
if recoveredCap > 200 {
|
||
t.Errorf("恢复后不应超过 ceiling: cap=%d ceiling=200", recoveredCap)
|
||
}
|
||
|
||
t.Logf("恢复阶段: cap=%d", recoveredCap)
|
||
}
|
||
|
||
// =============================================================================
|
||
// 集成测试 5:RTT 趋势检测 → 池调整
|
||
// 验证 ScanMetrics 的 RTT EMA 趋势信号能正确传导到池的健康判断
|
||
// =============================================================================
|
||
|
||
func TestIntegration_RTTTrend_DrivesPoolAdjustment(t *testing.T) {
|
||
metrics := &ScanMetrics{}
|
||
|
||
pool, err := NewAdaptivePool(100, 100, func(i interface{}) {}, metrics)
|
||
if err != nil {
|
||
t.Fatalf("创建池失败: %v", err)
|
||
}
|
||
defer pool.Release()
|
||
|
||
pool.inSlowStart = false
|
||
pool.tune(100)
|
||
|
||
// 建立基线:100 个 5ms RTT
|
||
for i := 0; i < 200; i++ {
|
||
metrics.RecordConnect(5 * time.Millisecond)
|
||
}
|
||
pool.lastCheck.Store(0)
|
||
pool.adjust()
|
||
baselineCap := pool.Cap()
|
||
|
||
// RTT 突增到 100ms(20 倍)
|
||
for i := 0; i < 100; i++ {
|
||
metrics.RecordConnect(100 * time.Millisecond)
|
||
}
|
||
|
||
ratio := metrics.RTTRatio()
|
||
if ratio <= 1.0 {
|
||
t.Logf("RTT ratio = %.2f, EMA 可能还没追上(正常)", ratio)
|
||
}
|
||
|
||
// 多次调整看池是否响应
|
||
for i := 0; i < 5; i++ {
|
||
pool.lastCheck.Store(0)
|
||
pool.adjust()
|
||
}
|
||
afterRTTSpike := pool.Cap()
|
||
|
||
t.Logf("RTT 趋势: baseline_cap=%d after_spike=%d rtt_ratio=%.2f",
|
||
baselineCap, afterRTTSpike, ratio)
|
||
|
||
// 如果 ratio 足够高,池应该降速
|
||
if ratio > 2.0 && afterRTTSpike >= baselineCap {
|
||
t.Errorf("RTT ratio=%.2f 但池没有降速: %d -> %d", ratio, baselineCap, afterRTTSpike)
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// 集成测试 6:不同网络环境下的参数一致性
|
||
// 验证同一组目标在不同环境下参数调整的合理递进关系
|
||
// =============================================================================
|
||
|
||
func TestIntegration_ParameterProgression(t *testing.T) {
|
||
environments := []struct {
|
||
name string
|
||
rtts []int // ms
|
||
loss int // failures out of 10
|
||
wantEnv NetworkEnv
|
||
}{
|
||
{"内网", []int{1, 1, 2, 2, 3, 3, 4, 4, 5, 5}, 0, EnvLAN},
|
||
{"局域网", []int{10, 15, 20, 25, 30, 35, 40, 45, 48, 49}, 0, EnvWAN},
|
||
{"公网", []int{60, 70, 80, 90, 100, 120, 140, 160, 180, 195}, 0, EnvInternet},
|
||
{"慢速", []int{200, 300, 400, 500, 600, 700, 800, 900, 1000, 1500}, 0, EnvSlow},
|
||
}
|
||
|
||
type params struct {
|
||
timeout time.Duration
|
||
mt int
|
||
retry int
|
||
icmpRate float64
|
||
}
|
||
|
||
var results []params
|
||
|
||
for _, env := range environments {
|
||
profile := classifyNetwork(makeDurations(env.rtts), env.loss, 10)
|
||
if profile.Env != env.wantEnv {
|
||
t.Errorf("%s: env = %v, want %v", env.name, profile.Env, env.wantEnv)
|
||
}
|
||
|
||
config := makeDefaultConfig()
|
||
session := makeTestSession(config)
|
||
ep := &EnvironmentProfile{
|
||
Net: *profile,
|
||
System: SystemProfile{FDLimit: 65536, NumCPU: 8},
|
||
}
|
||
ep.TuneConfig(config, session)
|
||
|
||
results = append(results, params{
|
||
timeout: config.Timeout,
|
||
mt: config.ModuleThreadNum,
|
||
retry: config.MaxRetries,
|
||
icmpRate: config.Network.ICMPRate,
|
||
})
|
||
|
||
t.Logf("%s: Timeout=%v MT=%d Retry=%d ICMP=%.2f",
|
||
env.name, config.Timeout, config.ModuleThreadNum, config.MaxRetries, config.Network.ICMPRate)
|
||
}
|
||
|
||
// 验证递进关系:从内网到慢速,Timeout 应递增
|
||
for i := 1; i < len(results); i++ {
|
||
if results[i].timeout < results[i-1].timeout {
|
||
t.Errorf("Timeout 不递增: %v (env[%d]) < %v (env[%d])",
|
||
results[i].timeout, i, results[i-1].timeout, i-1)
|
||
}
|
||
}
|
||
|
||
// ICMPRate 应递减(内网最高,慢速最低)
|
||
for i := 1; i < len(results); i++ {
|
||
if results[i].icmpRate > results[i-1].icmpRate {
|
||
t.Errorf("ICMPRate 不递减: %.2f (env[%d]) > %.2f (env[%d])",
|
||
results[i].icmpRate, i, results[i-1].icmpRate, i-1)
|
||
}
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// 集成测试 7:用户显式 -t + 网络探测 完整流程
|
||
// 验证用户指定值作为 ceiling 但探测仍然影响其他参数
|
||
// =============================================================================
|
||
|
||
func TestIntegration_ExplicitThreadNum_WithProbe(t *testing.T) {
|
||
profile := classifyNetwork(
|
||
makeDurations([]int{100, 120, 140, 160, 180, 200, 220, 240, 260, 300}),
|
||
2, 12, // 部分丢包
|
||
)
|
||
|
||
config := makeDefaultConfig()
|
||
config.ThreadNum = 200
|
||
config.ThreadNumExplicit = true
|
||
session := makeTestSession(config)
|
||
|
||
ep := &EnvironmentProfile{
|
||
Net: *profile,
|
||
System: SystemProfile{FDLimit: 4096, NumCPU: 4},
|
||
}
|
||
ep.TuneConfig(config, session)
|
||
|
||
// ThreadNum 不应被修改(fd limit 允许范围内)
|
||
// 但 Timeout、ModuleThreadNum 等应根据探测调整
|
||
if config.Timeout == 3*time.Second {
|
||
t.Error("即使 -t 显式,Timeout 仍应根据探测调整")
|
||
}
|
||
|
||
// 创建池
|
||
target, ceiling := profile.RecommendConcurrency(config.ThreadNum, config.ThreadNumExplicit)
|
||
if ceiling != 200 {
|
||
t.Errorf("显式 -t 200 的 ceiling = %d, want 200", ceiling)
|
||
}
|
||
if target > 200 {
|
||
t.Errorf("target = %d, 不应超过 ceiling 200", target)
|
||
}
|
||
|
||
metrics := &ScanMetrics{}
|
||
pool, err := NewAdaptivePool(target, ceiling, func(interface{}) {}, metrics)
|
||
if err != nil {
|
||
t.Fatalf("创建池失败: %v", err)
|
||
}
|
||
defer pool.Release()
|
||
|
||
t.Logf("显式 -t 200: Timeout=%v MT=%d Retry=%d target=%d ceiling=%d cap=%d",
|
||
config.Timeout, config.ModuleThreadNum, config.MaxRetries, target, ceiling, pool.Cap())
|
||
}
|
||
|
||
// =============================================================================
|
||
// 集成测试 8:AdaptiveTimeout + ScanMetrics 双 RTT 追踪
|
||
// 验证两个 RTT 追踪器独立工作不干扰
|
||
// =============================================================================
|
||
|
||
func TestIntegration_DualRTTTracking(t *testing.T) {
|
||
adaptiveTO := NewAdaptiveTimeout(3 * time.Second)
|
||
metrics := &ScanMetrics{}
|
||
|
||
// 喂入相同的 RTT 数据到两个追踪器
|
||
for i := 0; i < 50; i++ {
|
||
rtt := 10 * time.Millisecond
|
||
adaptiveTO.Record(rtt)
|
||
metrics.RecordConnect(rtt)
|
||
}
|
||
|
||
// AdaptiveTimeout 用于连接超时
|
||
toValue := adaptiveTO.Timeout()
|
||
// ScanMetrics 用于池健康判断
|
||
rttFast := metrics.RTTFast()
|
||
ratio := metrics.RTTRatio()
|
||
|
||
if toValue > 3*time.Second {
|
||
t.Errorf("AdaptiveTimeout 应该 < 初始值: %v", toValue)
|
||
}
|
||
if rttFast < 8*time.Millisecond || rttFast > 12*time.Millisecond {
|
||
t.Errorf("ScanMetrics RTTFast 应接近 10ms: %v", rttFast)
|
||
}
|
||
if ratio < 0.8 || ratio > 1.2 {
|
||
t.Errorf("稳定 RTT 的 ratio 应接近 1.0: %.2f", ratio)
|
||
}
|
||
|
||
t.Logf("双追踪: AdaptiveTO=%v, MetricsFast=%v, Ratio=%.2f", toValue, rttFast, ratio)
|
||
}
|
||
|
||
// =============================================================================
|
||
// 集成测试 9:丢包环境下 Retry + ModuleThreadNum 联动
|
||
// 验证高丢包同时影响重试和并发
|
||
// =============================================================================
|
||
|
||
func TestIntegration_LossyNetwork_RetryAndConcurrency(t *testing.T) {
|
||
lossRates := []float64{0.0, 0.05, 0.10, 0.20, 0.40}
|
||
|
||
type result struct {
|
||
loss float64
|
||
retry int
|
||
mt int
|
||
}
|
||
var results []result
|
||
|
||
for _, loss := range lossRates {
|
||
profile := &NetworkProfile{
|
||
Env: EnvInternet,
|
||
RTTMedian: 80 * time.Millisecond,
|
||
RTTStddev: 20 * time.Millisecond,
|
||
LossRate: loss,
|
||
Samples: 20,
|
||
}
|
||
|
||
config := makeDefaultConfig()
|
||
session := makeTestSession(config)
|
||
ep := &EnvironmentProfile{
|
||
Net: *profile,
|
||
System: SystemProfile{FDLimit: 65536, NumCPU: 8},
|
||
}
|
||
ep.TuneConfig(config, session)
|
||
|
||
results = append(results, result{loss, config.MaxRetries, config.ModuleThreadNum})
|
||
}
|
||
|
||
// 重试次数应随丢包率单调递增
|
||
for i := 1; i < len(results); i++ {
|
||
if results[i].retry < results[i-1].retry {
|
||
t.Errorf("Retry 不递增: loss=%.2f retry=%d < loss=%.2f retry=%d",
|
||
results[i].loss, results[i].retry, results[i-1].loss, results[i-1].retry)
|
||
}
|
||
}
|
||
|
||
// 高丢包时 ModuleThreadNum 应降低
|
||
if results[len(results)-1].mt >= results[0].mt {
|
||
t.Errorf("40%%丢包的 MT(%d) 应 < 0%%丢包的 MT(%d)",
|
||
results[len(results)-1].mt, results[0].mt)
|
||
}
|
||
|
||
for _, r := range results {
|
||
t.Logf("loss=%.0f%%: Retry=%d MT=%d", r.loss*100, r.retry, r.mt)
|
||
}
|
||
}
|