生产环境配置

开发环境和生产环境有很大不同。生产环境需要考虑性能、安全、可观测性等多个方面。这一章我们讨论 Gin 应用在生产环境中的配置要点。

运行模式

Gin 有三种运行模式:

  • debug:开发模式,日志详细
  • release:生产模式,日志简洁
  • test:测试模式

设置方式:

// 代码设置
gin.SetMode(gin.ReleaseMode)

// 环境变量
// export GIN_MODE=release

生产环境必须使用 release 模式,避免输出敏感信息和影响性能。

日志配置

结构化日志

生产环境推荐使用结构化日志,便于检索和分析:

package main

import (
    "os"
    "time"
    
    "github.com/gin-gonic/gin"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    gin.SetMode(gin.ReleaseMode)
    
    // 配置 zerolog
    log.Logger = log.Output(zerolog.ConsoleWriter{
        Out:        os.Stdout,
        TimeFormat: time.RFC3339,
    }).With().Caller().Logger()
    
    router := gin.New()
    
    // 自定义日志中间件
    router.Use(func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        query := c.Request.URL.RawQuery
        
        c.Next()
        
        latency := time.Since(start)
        status := c.Writer.Status()
        
        log.Info().
            Str("method", c.Request.Method).
            Str("path", path).
            Str("query", query).
            Int("status", status).
            Dur("latency", latency).
            Str("client_ip", c.ClientIP()).
            Str("user_agent", c.Request.UserAgent()).
            Msg("Request")
    })
    
    router.Use(gin.Recovery())
    
    router.Run(":8080")
}

日志文件

使用 lumberjack 进行日志轮转:

import (
    "github.com/natefinch/lumberjack"
)

func setupLogger() {
    logFile := &lumberjack.Logger{
        Filename:   "/var/log/myapp/app.log",
        MaxSize:    100, // MB
        MaxBackups: 3,
        MaxAge:     28, // days
        Compress:   true,
    }
    
    gin.DefaultWriter = logFile
    gin.DefaultErrorWriter = logFile
}

配置管理

分层配置

package config

import (
    "os"
    "strconv"
)

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    Redis    RedisConfig
    Log      LogConfig
}

type ServerConfig struct {
    Port         int
    ReadTimeout  int
    WriteTimeout int
}

type DatabaseConfig struct {
    Host     string
    Port     int
    User     string
    Password string
    DBName   string
}

type RedisConfig struct {
    Addr     string
    Password string
    DB       int
}

type LogConfig struct {
    Level  string
    Output string
}

func Load() *Config {
    return &Config{
        Server: ServerConfig{
            Port:         getEnvInt("PORT", 8080),
            ReadTimeout:  getEnvInt("READ_TIMEOUT", 10),
            WriteTimeout: getEnvInt("WRITE_TIMEOUT", 10),
        },
        Database: DatabaseConfig{
            Host:     getEnv("DB_HOST", "localhost"),
            Port:     getEnvInt("DB_PORT", 5432),
            User:     getEnv("DB_USER", "postgres"),
            Password: getEnv("DB_PASSWORD", ""),
            DBName:   getEnv("DB_NAME", "myapp"),
        },
        Redis: RedisConfig{
            Addr:     getEnv("REDIS_ADDR", "localhost:6379"),
            Password: getEnv("REDIS_PASSWORD", ""),
            DB:       getEnvInt("REDIS_DB", 0),
        },
        Log: LogConfig{
            Level:  getEnv("LOG_LEVEL", "info"),
            Output: getEnv("LOG_OUTPUT", "stdout"),
        },
    }
}

func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

func getEnvInt(key string, defaultValue int) int {
    if value := os.Getenv(key); value != "" {
        if i, err := strconv.Atoi(value); err == nil {
            return i
        }
    }
    return defaultValue
}

敏感配置

敏感信息不要硬编码,使用环境变量或密钥管理服务:

# .env 文件(不要提交到版本控制)
DATABASE_URL=postgres://user:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-key
API_KEY=your-api-key

健康检查

生产环境必须有健康检查接口:

router.GET("/health", func(c *gin.Context) {
    // 检查数据库连接
    if err := db.Ping(); err != nil {
        c.JSON(http.StatusServiceUnavailable, gin.H{
            "status": "unhealthy",
            "error":  "database connection failed",
        })
        return
    }
    
    // 检查 Redis 连接
    if err := redis.Ping(context.Background()).Err(); err != nil {
        c.JSON(http.StatusServiceUnavailable, gin.H{
            "status": "unhealthy",
            "error":  "redis connection failed",
        })
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "status": "healthy",
    })
})

// 就绪检查
router.GET("/ready", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "ready": true,
    })
})

监控指标

Prometheus 指标

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )
    
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
        },
        []string{"method", "path"},
    )
)

func init() {
    prometheus.MustRegister(httpRequestsTotal)
    prometheus.MustRegister(httpRequestDuration)
}

func prometheusMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        
        c.Next()
        
        duration := time.Since(start).Seconds()
        status := strconv.Itoa(c.Writer.Status())
        
        httpRequestsTotal.WithLabelValues(c.Request.Method, c.FullPath(), status).Inc()
        httpRequestDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(duration)
    }
}

func main() {
    router := gin.New()
    router.Use(prometheusMiddleware())
    
    // 指标端点
    router.GET("/metrics", gin.WrapH(promhttp.Handler()))
    
    // ... 其他路由
}

安全配置

安全中间件

import "github.com/ulule/limiter/v3"

func securityMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 移除敏感头
        c.Header("X-Content-Type-Options", "nosniff")
        c.Header("X-Frame-Options", "DENY")
        c.Header("X-XSS-Protection", "1; mode=block")
        c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        
        c.Next()
    }
}

请求限流

import (
    "github.com/ulule/limiter/v3"
    mgin "github.com/ulule/limiter/v3/drivers/middleware/gin"
    "github.com/ulule/limiter/v3/drivers/store/memory"
)

func rateLimitMiddleware() gin.HandlerFunc {
    store := memory.NewStore()
    rate := limiter.Rate{
        Period: 1 * time.Minute,
        Limit:  100,
    }
    instance := limiter.New(store, rate)
    return mgin.NewMiddleware(instance)
}

CORS 配置

import "github.com/gin-contrib/cors"

func main() {
    router := gin.Default()
    
    router.Use(cors.New(cors.Config{
        AllowOrigins:     []string{"https://example.com"},
        AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
        AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
        ExposeHeaders:    []string{"Content-Length"},
        AllowCredentials: true,
        MaxAge:           12 * time.Hour,
    }))
    
    // ...
}

优雅关闭

func main() {
    router := gin.New()
    
    // ... 路由配置
    
    srv := &http.Server{
        Addr:         ":8080",
        Handler:      router,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }
    
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()
    
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    log.Println("Shutting down server...")
    
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    // 关闭数据库连接
    sqlDB, _ := db.DB()
    sqlDB.Close()
    
    // 关闭 Redis 连接
    redis.Close()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("Server shutdown error: %v", err)
    }
    
    log.Println("Server stopped")
}

错误处理

func recoveryMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                log.Error().
                    Str("path", c.Request.URL.Path).
                    Interface("error", err).
                    Msg("Panic recovered")
                
                c.JSON(http.StatusInternalServerError, gin.H{
                    "error": "Internal server error",
                })
                c.Abort()
            }
        }()
        c.Next()
    }
}

func notFoundHandler(c *gin.Context) {
    c.JSON(http.StatusNotFound, gin.H{
        "error": "Not found",
    })
}

func methodNotAllowedHandler(c *gin.Context) {
    c.JSON(http.StatusMethodNotAllowed, gin.H{
        "error": "Method not allowed",
    })
}

小结

生产环境配置的关键点:

  • 使用 release 模式运行
  • 配置结构化日志和日志轮转
  • 使用环境变量管理配置
  • 实现健康检查接口
  • 集成 Prometheus 监控指标
  • 配置安全相关的响应头
  • 实现请求限流
  • 优雅关闭服务

生产环境的配置需要在实际运行中不断调整优化,关键是建立完善的监控和告警机制,及时发现问题并处理。