生产环境配置

代码写好了,要上线了。生产环境和开发环境差别很大,这一章聊聊 GORM 在生产环境的配置和注意事项。

连接配置

生产环境的数据库连接要考虑高可用、安全、性能。

连接字符串

不要把密码硬编码在代码里:

// 不好
dsn := "root:password123@tcp(localhost:3306)/mydb"

// 好:从环境变量读取
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
    log.Fatal("DATABASE_URL is required")
}

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

连接参数

MySQL 连接推荐参数:

dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s&readTimeout=10s&writeTimeout=10s",
    os.Getenv("DB_USER"),
    os.Getenv("DB_PASS"),
    os.Getenv("DB_HOST"),
    os.Getenv("DB_PORT"),
    os.Getenv("DB_NAME"),
)

关键参数:

  • charset=utf8mb4:支持完整的 UTF-8,包括 emoji
  • parseTime=True:自动解析时间类型
  • loc=Local:时区设置
  • timeout:连接超时
  • readTimeout/writeTimeout:读写超时

连接池配置

生产环境必须配置连接池:

sqlDB, err := db.DB()
if err != nil {
    log.Fatal(err)
}

// 根据实际负载调整
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)

配置原则:

  • MaxOpenConns 小于数据库的 max_connections
  • 多实例部署时,总连接数不超过数据库限制
  • ConnMaxLifetime 设置比数据库 wait_timeout 小

日志配置

生产环境不要打印所有 SQL:

newLogger := logger.New(
    log.New(os.Stdout, "\r\n", log.LstdFlags),
    logger.Config{
        SlowThreshold: 200 * time.Millisecond,
        LogLevel:      logger.Warn,  // 只记录慢查询和错误
        Colorful:      false,
    },
)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: newLogger,
})

集成项目的日志框架:

type GormLogger struct {
    *zap.SugaredLogger
}

func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
    if err != nil {
        l.Errorw("sql error", "error", err, "sql", fc)
    }
}

db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: &GormLogger{SugaredLogger: zap.L().Sugar()},
})

健康检查

提供数据库健康检查接口:

func HealthCheck(db *gorm.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        sqlDB, err := db.DB()
        if err != nil {
            c.JSON(500, gin.H{"status": "unhealthy", "error": err.Error()})
            return
        }
        
        ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
        defer cancel()
        
        if err := sqlDB.PingContext(ctx); err != nil {
            c.JSON(500, gin.H{"status": "unhealthy", "error": err.Error()})
            return
        }
        
        c.JSON(200, gin.H{"status": "healthy"})
    }
}

优雅关闭

应用退出时要正确关闭连接:

func main() {
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    sqlDB, _ := db.DB()
    
    // 注册关闭钩子
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    
    go func() {
        <-quit
        log.Println("shutting down...")
        
        // 给正在处理的请求一些时间
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
        
        // 关闭数据库连接
        if err := sqlDB.Close(); err != nil {
            log.Printf("error closing database: %v", err)
        }
        
        log.Println("database connections closed")
        os.Exit(0)
    }()
    
    // 启动服务...
}

错误处理

生产环境要有完善的错误处理:

func (s *UserService) GetUser(id uint) (*User, error) {
    var user User
    err := s.db.First(&user, id).Error
    
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, &AppError{Code: 404, Message: "用户不存在"}
        }
        
        // 记录错误日志
        log.Printf("database error: %v", err)
        return nil, &AppError{Code: 500, Message: "系统错误"}
    }
    
    return &user, nil
}

重试机制

数据库临时故障时可以重试:

func withRetry(db *gorm.DB, fn func(*gorm.DB) error, maxRetries int) error {
    var err error
    for i := 0; i < maxRetries; i++ {
        err = fn(db)
        if err == nil {
            return nil
        }
        
        // 判断是否可重试的错误
        if !isRetryableError(err) {
            return err
        }
        
        time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
    }
    return err
}

func isRetryableError(err error) bool {
    // 连接错误、超时等可以重试
    return strings.Contains(err.Error(), "connection") ||
           strings.Contains(err.Error(), "timeout")
}

主从配置

读写分离配置:

db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Replicas: []*gorm.DB{
        gorm.Open(mysql.Open(replicaDSN1), &gorm.Config{}),
        gorm.Open(mysql.Open(replicaDSN2), &gorm.Config{}),
    },
})

或者用 dbresolver 插件:

import "gorm.io/plugin/dbresolver"

db.Use(dbresolver.Register(dbresolver.Config{
    Replicas: []gorm.Dialector{
        mysql.Open(replicaDSN1),
        mysql.Open(replicaDSN2),
    },
    Policy: dbresolver.RandomPolicy{},
}))

// 读操作走从库
db.ReadDB().Find(&users)

// 写操作走主库
db.WriteDB().Create(&user)

监控指标

暴露数据库相关指标:

func collectDBMetrics(db *gorm.DB) {
    sqlDB, _ := db.DB()
    stats := sqlDB.Stats()
    
    metrics.SetGauge("db_open_connections", float64(stats.OpenConnections))
    metrics.SetGauge("db_in_use", float64(stats.InUse))
    metrics.SetGauge("db_idle", float64(stats.Idle))
    metrics.SetGauge("db_wait_count", float64(stats.WaitCount))
    metrics.SetGauge("db_wait_duration_ms", float64(stats.WaitDuration.Milliseconds()))
}

安全配置

最小权限原则

应用账号只给必要的权限:

CREATE USER 'app_user'@'%' IDENTIFIED BY 'secure_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';
-- 不要给 DROP, ALTER 等权限

敏感信息加密

敏感字段加密存储:

type User struct {
    ID       uint
    Name     string
    Phone    string `gorm:"type:varchar(255)"`  // 加密存储
}

func (u *User) BeforeCreate(tx *gorm.DB) error {
    u.Phone = encrypt(u.Phone)
    return nil
}

func (u *User) AfterFind(tx *gorm.DB) error {
    u.Phone = decrypt(u.Phone)
    return nil
}

小结

生产环境配置要点:

  1. 敏感信息从环境变量读取
  2. 合理配置连接池参数
  3. 只记录必要的日志
  4. 提供健康检查接口
  5. 优雅关闭数据库连接
  6. 完善的错误处理和重试机制
  7. 读写分离配置
  8. 监控数据库指标
  9. 安全配置

生产环境容不得马虎,每个细节都要考虑到。