package main import ( "context" "database/sql" "fmt" "net" "time" _ "github.com/go-sql-driver/mysql" ) // FScanMySQLDiagnosis fscan MySQL连接问题诊断工具 type FScanMySQLDiagnosis struct { host string port int username string password string timeout time.Duration } // NewDiagnosis 创建诊断工具 func NewDiagnosis(host string, port int, username, password string, timeout time.Duration) *FScanMySQLDiagnosis { return &FScanMySQLDiagnosis{ host: host, port: port, username: username, password: password, timeout: timeout, } } func main() { fmt.Println("FScan MySQL连接问题诊断工具") fmt.Println("===========================") // 初始化诊断工具 diagnosis := NewDiagnosis("127.0.0.1", 3306, "root", "123456", 3*time.Second) // 执行完整诊断 diagnosis.RunFullDiagnosis() } // RunFullDiagnosis 运行完整诊断 func (d *FScanMySQLDiagnosis) RunFullDiagnosis() { fmt.Printf("目标: %s:%d\n", d.host, d.port) fmt.Printf("用户: %s\n", d.username) fmt.Printf("超时: %v\n", d.timeout) fmt.Println() // 步骤1: 网络连通性测试 fmt.Println("步骤1: 网络连通性测试") if !d.testNetworkConnection() { return } // 步骤2: 测试fscan当前的连接字符串格式 fmt.Println("\n步骤2: 测试fscan当前连接字符串格式") d.testFScanCurrentFormat() // 步骤3: 测试不同的超时配置 fmt.Println("\n步骤3: 测试不同超时配置") d.testTimeoutConfigurations() // 步骤4: 测试连接池配置的影响 fmt.Println("\n步骤4: 测试连接池配置") d.testConnectionPoolSettings() // 步骤5: 提供修复建议 fmt.Println("\n步骤5: 修复建议") d.provideFixSuggestions() } // testNetworkConnection 测试网络连接 func (d *FScanMySQLDiagnosis) testNetworkConnection() bool { fmt.Printf("正在测试TCP连接 %s:%d...\n", d.host, d.port) conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", d.host, d.port), d.timeout) if err != nil { fmt.Printf("❌ TCP连接失败: %v\n", err) fmt.Println("建议:") fmt.Println("- 检查MySQL服务是否启动") fmt.Println("- 检查端口3306是否开放") fmt.Println("- 检查防火墙设置") return false } defer conn.Close() fmt.Println("✅ TCP连接成功") return true } // testFScanCurrentFormat 测试fscan当前使用的连接字符串格式 func (d *FScanMySQLDiagnosis) testFScanCurrentFormat() { timeoutStr := d.timeout.String() // fscan当前使用的格式 connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr) fmt.Printf("连接字符串: %s\n", connStr) success, elapsed, err := d.testConnectionString(connStr, "fscan当前格式") if success { fmt.Printf("✅ fscan格式连接成功 (耗时: %v)\n", elapsed) } else { fmt.Printf("❌ fscan格式连接失败 (耗时: %v): %v\n", elapsed, err) // 尝试替代方案 fmt.Println("\n尝试替代连接字符串...") d.testAlternativeFormats() } } // testAlternativeFormats 测试替代连接字符串格式 func (d *FScanMySQLDiagnosis) testAlternativeFormats() { timeoutStr := d.timeout.String() alternatives := []struct { name string format string desc string }{ { name: "不指定数据库", format: fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr), desc: "不连接到特定数据库", }, { name: "无数据库路径", format: fmt.Sprintf("%s:%s@tcp(%s:%d)?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr), desc: "完全省略数据库路径", }, { name: "使用information_schema", format: fmt.Sprintf("%s:%s@tcp(%s:%d)/information_schema?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr), desc: "连接到系统数据库", }, { name: "UTF8MB4字符集", format: fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8mb4&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr), desc: "使用完整UTF-8字符集", }, } for _, alt := range alternatives { success, elapsed, err := d.testConnectionString(alt.format, alt.name) if success { fmt.Printf("✅ %s 成功 (耗时: %v)\n", alt.name, elapsed) fmt.Printf(" 建议使用: %s\n", alt.format) } else { fmt.Printf("❌ %s 失败 (耗时: %v): %v\n", alt.name, elapsed, err) } } } // testConnectionString 测试指定的连接字符串 func (d *FScanMySQLDiagnosis) testConnectionString(connStr, name string) (bool, time.Duration, error) { ctx, cancel := context.WithTimeout(context.Background(), d.timeout+time.Second) defer cancel() start := time.Now() db, err := sql.Open("mysql", connStr) if err != nil { return false, time.Since(start), err } defer db.Close() // 模拟fscan的连接池设置 db.SetConnMaxLifetime(d.timeout) db.SetConnMaxIdleTime(d.timeout) db.SetMaxIdleConns(1) db.SetMaxOpenConns(1) err = db.PingContext(ctx) elapsed := time.Since(start) return err == nil, elapsed, err } // testTimeoutConfigurations 测试不同超时配置 func (d *FScanMySQLDiagnosis) testTimeoutConfigurations() { timeouts := []time.Duration{ 1 * time.Second, 3 * time.Second, 5 * time.Second, 10 * time.Second, } baseDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8", d.username, d.password, d.host, d.port) for _, timeout := range timeouts { fmt.Printf("测试超时: %v\n", timeout) // DSN中设置timeout dsnWithTimeout := baseDSN + "&timeout=" + timeout.String() success, elapsed, err := d.testConnectionStringWithTimeout(dsnWithTimeout, timeout+time.Second) if success { fmt.Printf(" ✅ DSN超时%v 成功 (实际耗时: %v)\n", timeout, elapsed) } else { fmt.Printf(" ❌ DSN超时%v 失败 (实际耗时: %v): %v\n", timeout, elapsed, err) } } } // testConnectionStringWithTimeout 使用指定超时测试连接字符串 func (d *FScanMySQLDiagnosis) testConnectionStringWithTimeout(connStr string, contextTimeout time.Duration) (bool, time.Duration, error) { ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) defer cancel() start := time.Now() db, err := sql.Open("mysql", connStr) if err != nil { return false, time.Since(start), err } defer db.Close() err = db.PingContext(ctx) elapsed := time.Since(start) return err == nil, elapsed, err } // testConnectionPoolSettings 测试连接池设置的影响 func (d *FScanMySQLDiagnosis) testConnectionPoolSettings() { connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, d.timeout.String()) configs := []struct { name string configure func(*sql.DB) desc string }{ { name: "fscan默认设置", configure: func(db *sql.DB) { db.SetConnMaxLifetime(d.timeout) db.SetConnMaxIdleTime(d.timeout) db.SetMaxIdleConns(1) db.SetMaxOpenConns(1) }, desc: "模拟fscan当前的连接池配置", }, { name: "更长生命周期", configure: func(db *sql.DB) { db.SetConnMaxLifetime(30 * time.Second) db.SetConnMaxIdleTime(30 * time.Second) db.SetMaxIdleConns(1) db.SetMaxOpenConns(1) }, desc: "延长连接生命周期到30秒", }, { name: "无生命周期限制", configure: func(db *sql.DB) { db.SetMaxIdleConns(1) db.SetMaxOpenConns(1) }, desc: "不设置连接生命周期限制", }, } for _, config := range configs { fmt.Printf("测试: %s\n", config.name) fmt.Printf("描述: %s\n", config.desc) success, elapsed, err := d.testWithPoolConfig(connStr, config.configure) if success { fmt.Printf(" ✅ 成功 (耗时: %v)\n", elapsed) } else { fmt.Printf(" ❌ 失败 (耗时: %v): %v\n", elapsed, err) } } } // testWithPoolConfig 使用指定连接池配置测试 func (d *FScanMySQLDiagnosis) testWithPoolConfig(connStr string, configure func(*sql.DB)) (bool, time.Duration, error) { ctx, cancel := context.WithTimeout(context.Background(), d.timeout+2*time.Second) defer cancel() start := time.Now() db, err := sql.Open("mysql", connStr) if err != nil { return false, time.Since(start), err } defer db.Close() configure(db) err = db.PingContext(ctx) elapsed := time.Since(start) return err == nil, elapsed, err } // provideFixSuggestions 提供修复建议 func (d *FScanMySQLDiagnosis) provideFixSuggestions() { fmt.Println("基于诊断结果的修复建议:") fmt.Println() fmt.Println("1. 连接字符串优化:") fmt.Println(" 原始: user:pass@tcp(host:port)/mysql?charset=utf8&timeout=3s") fmt.Println(" 建议: user:pass@tcp(host:port)/?charset=utf8mb4&timeout=5s&readTimeout=3s&writeTimeout=3s") fmt.Println(" 原因: 不指定数据库减少权限要求,使用utf8mb4支持完整字符集") fmt.Println() fmt.Println("2. 超时配置优化:") fmt.Println(" - Context超时应该 > DSN timeout") fmt.Println(" - 建议Context超时 = DSN timeout + 2秒") fmt.Println(" - 网络环境差时适当增加超时时间") fmt.Println() fmt.Println("3. 连接池配置优化:") fmt.Println(" - SetConnMaxLifetime(10*time.Second) // 适当延长") fmt.Println(" - SetMaxOpenConns(1) // 扫描场景保持1个连接") fmt.Println(" - 确保每次使用后正确关闭连接") fmt.Println() fmt.Println("4. 错误处理改进:") fmt.Println(" - 区分网络超时和认证失败") fmt.Println(" - 实现重试机制") fmt.Println(" - 记录详细的错误信息") fmt.Println() fmt.Println("5. 针对fscan的具体修改建议:") d.provideFScanSpecificFixes() } // provideFScanSpecificFixes 提供fscan特定的修复建议 func (d *FScanMySQLDiagnosis) provideFScanSpecificFixes() { fmt.Println("针对fscan代码的修改建议:") fmt.Println() fmt.Println("修改 plugins/services/mysql/connector.go:") fmt.Println("```go") fmt.Println("// buildConnectionString 构建连接字符串 - 优化版本") fmt.Println("func (c *MySQLConnector) buildConnectionString(host string, port int, username, password string) string {") fmt.Println(" timeoutStr := c.timeout.String()") fmt.Println(" readTimeoutStr := (c.timeout - time.Second).String() // 读超时比总超时短1秒") fmt.Println(" ") fmt.Println(" var connStr string") fmt.Println(" if common.Socks5Proxy != \"\" {") fmt.Println(" // 代理连接 - 不指定具体数据库") fmt.Println(" connStr = fmt.Sprintf(\"%v:%v@tcp-proxy(%v:%v)/?charset=utf8mb4&timeout=%s&readTimeout=%s&writeTimeout=%s\",") fmt.Println(" username, password, host, port, timeoutStr, readTimeoutStr, readTimeoutStr)") fmt.Println(" } else {") fmt.Println(" // 标准连接 - 不指定具体数据库") fmt.Println(" connStr = fmt.Sprintf(\"%v:%v@tcp(%v:%v)/?charset=utf8mb4&timeout=%s&readTimeout=%s&writeTimeout=%s\",") fmt.Println(" username, password, host, port, timeoutStr, readTimeoutStr, readTimeoutStr)") fmt.Println(" }") fmt.Println(" ") fmt.Println(" return connStr") fmt.Println("}") fmt.Println("```") fmt.Println() fmt.Println("修改认证函数中的Context超时:") fmt.Println("```go") fmt.Println("// Authenticate 认证 - 优化版本") fmt.Println("func (c *MySQLConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error {") fmt.Println(" // 创建更长的context超时,避免冲突") fmt.Println(" authCtx, cancel := context.WithTimeout(ctx, c.timeout+2*time.Second)") fmt.Println(" defer cancel()") fmt.Println(" ") fmt.Println(" // ... 其余代码保持不变") fmt.Println(" err = db.PingContext(authCtx) // 使用新的context") fmt.Println(" // ...") fmt.Println("}") fmt.Println("```") fmt.Println() fmt.Println("这些修改应该能解决'context deadline exceeded'错误。") }