← Назад к вопросам

Как отследить ошибки соединения с БД?

1.7 Middle🔥 221 комментариев
#Observability#Базы данных

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отслеживание ошибок соединения с БД в Go

Отслеживание ошибок соединения с базой данных — критически важная задача для обеспечения стабильности приложения. В Go существует несколько подходов, которые можно комбинировать для создания надежной системы мониторинга.

Основные методы отслеживания

1. Непосредственная обработка ошибок при операциях

При каждом вызове метода, взаимодействующего с БД, нужно проверять возвращаемую ошибку:

func getUserByID(db *sql.DB, id int) (*User, error) {
    var user User
    err := db.QueryRow("SELECT id, name FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name)
    if err != nil {
        // Определяем тип ошибки
        if errors.Is(err, sql.ErrNoRows) {
            return nil, fmt.Errorf("user not found: %w", err)
        }
        
        // Проверяем ошибки соединения
        if isConnectionError(err) {
            log.Printf("Database connection error: %v", err)
            metrics.Increment("db_connection_errors")
        }
        
        return nil, fmt.Errorf("query failed: %w", err)
    }
    return &user, nil
}

func isConnectionError(err error) bool {
    // Проверяем различные типы ошибок соединения
    errStr := err.Error()
    return strings.Contains(errStr, "connection refused") ||
           strings.Contains(errStr, "network unreachable") ||
           strings.Contains(errStr, "timeout") ||
           strings.Contains(errStr, "reset by peer")
}

2. Использование Health Checks

Реализация периодических проверок здоровья соединения:

func checkDBHealth(db *sql.DB) bool {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    // Простой запрос для проверки соединения
    err := db.PingContext(ctx)
    if err != nil {
        log.Printf("Database health check failed: %v", err)
        return false
    }
    
    // Проверка более сложных сценариев
    var result int
    err = db.QueryRowContext(ctx, "SELECT 1").Scan(&result)
    return err == nil && result == ap
}

3. Настройка мониторинга через sql.DB

Объект sql.DB предоставляет встроенные метрики:

func setupDBMonitoring(db *sql.DB) {
    // Статистика пула соединений
    stats := db.Stats()
    
    log.Printf("DB Stats - OpenConnections: %d, InUse: %d, Idle: %d", 
        stats.OpenConnections, stats.InUse, stats.Idle)
    
    // Мониторим ключевые метрики
    if stats.OpenConnections == 0 {
        log.Warn("No database connections available")
    }
    
    if stats.WaitCount > 0 {
        log.Warn("Clients waiting for database connections")
    }
}

Расширенное отслеживание с контекстом

4. Интеграция с трейсингом и контекстом

func queryWithContext(ctx context.Context, db *sql.DB) error {
    // Добавляем таймаут для предотвращения зависаний
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    
    // Используем контекст в запросе
    rows, err := db.QueryContext(ctx, "SELECT * FROM large_table")
    if err != nil {
        // Проверяем, была ли ошибка из-за таймаута контекста
        if ctx.Err() == context.DeadlineExceeded {
            metrics.Increment("db_query_timeouts")
            return fmt.Errorf("query timeout: %w", err)
        }
        return fmt.Errorf("query failed: %w", err)
    }
    defer rows.Close()
    
    return nil
}

Архитектурные подходы

5. Использование Circuit Breaker

Реализация паттерна Circuit Breaker для защиты БД:

type DBCircuitBreaker struct {
    mu          sync.RWMutex
    failures    int
    threshold   int
    cooldown    time.Duration
    lastFailure time.Time
    isOpen      bool
}

func (cb *DBCircuitBreaker) Execute(db *sql.DB, query string) error {
    cb.mu.RLock()
    if cb.isOpen && time.Since(cb.lastFailure) < cb.cooldown {
        cb.mu.RUnlock()
        return fmt.Errorf("circuit breaker is open")
    }
    cb.mu.RUnlock()
    
    err := db.QueryRow(query).Scan()
    
    cb.mu.Lock()
    defer cb.mu.Unlock()
    
    if err != nil {
        cb.failures++
        cb.lastFailure = time.Now()
        
        if cb.failures >= cb.threshold {
            cb.isOpen = true
            log.Printf("Circuit breaker opened after %d failures", cb.failures)
        }
    } else {
        cb.failures = 0
        if cb.isOpen {
            cb.isOpen = false
            log.Println("Circuit breaker closed")
        }
    }
    
    return err
}

6. Интеграция с системами мониторинга

func setupDBMetrics(db *sql.DB, registry *prometheus.Registry) {
    // Метрики для Prometheus
    dbErrors := prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "db_errors_total",
            Help: "Total database errors",
        },
        []string{"error_type"},
    )
    
    queryDuration := prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "db_query_duration_seconds",
            Help:    "Database query duration",
            Buckets: prometheus.DefBuckets,
        },
        []string{"query_type"},
    )
    
    registry.MustRegister(dbErrors, queryDuration)
    
    // Обертка для отслеживания запросов
    wrappedDB := &monitoredDB{
        db: db,
        errors: dbErrors,
        duration: queryDuration,
    }
}

Практические рекомендации

  1. Логирование с достаточным контекстом — всегда включайте информацию о запросе, параметрах и времени возникновения ошибки
  2. Многоуровневое отслеживание — комбинируйте health checks, метрики и алертинг
  3. Автоматическое восстановление — реализуйте retry логику с экспоненциальной задержкой
  4. Зависимость от типа БД — учитывайте специфичные для вашей СУБД коды ошибок
  5. Тестирование сценариев — имитируйте сетевые проблемы и таймауты в тестах

Пример комплексного решения

type DBMonitor struct {
    db         *sql.DB
    logger     *zap.Logger
    metrics    MetricsCollector
    breaker    *CircuitBreaker
    healthTicker *time.Ticker
}

func NewDBMonitor(dsn string) (*DBMonitor, error) {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("open db: %w", err)
    }
    
    // Настройка пула соединений
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(10)
    db.SetConnMaxLifetime(5 * time.Minute)
    
    monitor := &DBMonitor{
        db:      db,
        logger:  zap.NewExample(),
        metrics: NewPrometheusMetrics(),
        breaker: NewCircuitBreaker(5, 30*time.Second),
    }
    
    // Запуск периодических проверок
    monitor.startHealthChecks(30 * time.Second)
    
    return monitor, nil
}

func (m *DBMonitor) startHealthChecks(interval time.Duration) {
    m.healthTicker = time.NewTicker(interval)
    
    go func() {
        for range m.healthTicker.C {
            if !m.checkHealth() {
                m.logger.Error("Database health check failed")
                m.metrics.IncHealthCheckFailure()
                
                // Попытка восстановления
                m.reconnect()
            }
        }
    }()
}

Эффективное отслеживание ошибок соединения с БД требует многоуровневого подхода, сочетающего непосредственную обработку ошибок, health checks, метрики и архитектурные паттерны. Это обеспечивает не только обнаружение проблем, но и возможности для автоматического восстановления и оперативного реагирования.

Как отследить ошибки соединения с БД? | PrepBro