Перейти к основному содержимому

Постмортем: Сбой сервиса Ordering в Production

Статус: ✅ РЕШЕНО Дата инцидента: 2026-01-17 — 2026-01-20 Severity: 🔴 CRITICAL (P1) Затронутые сервисы: ordering-internal-host (mysport, witisi) Автор: DevOps/Backend Team Дата создания постмортема: 2026-01-20


Executive Summary

Что произошло: Сервис Ordering периодически терял возможность обрабатывать запросы в production окружении из-за накопления "мертвых" соединений с PostgreSQL в connection pool. Проблема проявлялась как полная потеря связи с базой данных, что приводило к ошибкам 500 для всех API запросов.

Бизнес-воздействие:

  • Клиенты не могли создавать новые заказы
  • Обработка существующих заказов была приостановлена
  • Требовался ручной перезапуск подов для восстановления работоспособности
  • Проблема возникала несколько раз за месяц
  • Репутационные риски из-за нестабильности сервиса

Корневая причина: Отсутствие автоматической очистки connection pool (параметры Connection Idle Lifetime и Connection Pruning Interval) в библиотеке MentKit.Databases.PostgreSql привело к накоплению протухших соединений при работе через PgBouncer с агрессивным режимом STATEMENT pooling.

Решение:

  • Добавлены критические параметры управления connection pool в MentKit.Databases.PostgreSql
  • Обновлена конфигурация liveness probe для автоматического перезапуска проблемных подов
  • Создана подробная документация для предотвращения повторных инцидентов

Время восстановления (MTTR): ~5 минут (ручной перезапуск пода) Частота проблемы: Каждые 5-15 дней Общее время в деградированном состоянии: ~72 часа (с периодическими восстановлениями)


Содержание

  1. Impact Assessment
  2. Timeline
  3. Root Cause Analysis
  4. What Went Wrong
  5. What Went Right
  6. Resolution
  7. Lessons Learned
  8. Action Items
  9. Appendix

Impact Assessment

Затронутые системы

КомпонентNamespaceClusterВоздействие
ordering-internal-hostmysportyc-prod🔴 Полный отказ
ordering-internal-hostwitisiyc-prod🔴 Полный отказ
ordering-adminapi-hostmysportyc-prod🟡 Частичная деградация
ordering-clientapi-hostmysportyc-prod🟡 Частичная деградация

Метрики воздействия

Технические метрики:

  • Error Rate: До 90% для /api/v1/orders endpoint
  • Latency P99: Увеличение с ~200ms до 5000ms (таймауты)
  • Health Check Failures: 6 отказов за 23 часа (не подряд)
  • Manual Interventions: ~12 ручных перезапусков за 3 дня

Бизнес-метрики (оценочные):

  • Потерянные заказы: ~150-200 потенциальных заказов
  • Affected Users: ~500-800 пользователей
  • Support Tickets: ~15-20 обращений
  • Revenue Impact: Средний (точная оценка требует анализа с бизнесом)

География:

  • Все регионы (основной кластер в Yandex Cloud)

Timeline

Все время указано в UTC+5 (Екатеринбург).

2026-01-17 (День 1) - Начало инцидента

ВремяСобытиеДействие
~10:00🔴 Первые признаки проблемы - увеличение latency в логах
~14:30🔴 Health check failures начинают появляться в Kubernetes events
~15:00🚨 Support сообщает о проблемах с созданием заказовНачата диагностика
15:20🔍 Проверка логов - обнаружены ошибки OperationCanceledException
15:30⚙️ Первый ручной перезапуск пода ordering-internal-host-77b58d98-dlqsq✅ Сервис восстановлен временно
~20:00🔴 Проблема вернулась - снова ошибки подключения к БД
20:15⚙️ Повторный перезапуск пода✅ Сервис восстановлен временно

2026-01-18 (День 2) - Диагностика

ВремяСобытиеДействие
09:00🔴 Проблема повторилась после ночного простояРучной перезапуск
10:00🔍 Начата глубокая диагностика - анализ конфигурации БД
11:30🔍 Проверка сетевой доступности PostgreSQL - сеть работает нормально❌ Сеть не причина
12:00🔍 Анализ конфигурации PgBouncer - обнаружен режим STATEMENT pooling
14:00🔍 Изучение параметров Npgsql - обнаружено отсутствие Connection Idle Lifetime🎯 Возможная корневая причина
15:00📝 Создан файл ordering-problem-prompt.md с описанием проблемы
16:00🔍 Детальный анализ кода MentKit.Databases.PostgreSql
17:00📋 Создана первая версия диагностики ordering-problem-diagnostic.md❌ Неверная гипотеза (таймаут liveness)
~19:00🔴 Очередной отказ сервисаРучной перезапуск

2026-01-19 (День 3) - Углубленный анализ

ВремяСобытиеДействие
09:00🔴 Утренний отказ сервисаРучной перезапуск
10:00🔍 Пересмотр гипотезы - фокус на connection pool corruption
11:00🎯 Root cause найден: отсутствие параметров очистки connection pool
12:00📝 Обновлена диагностика (v2) с правильной корневой причиной
14:00📋 Разработан план решения (3 фазы)
15:00🔴 Проблема повторилась днемРучной перезапуск

2026-01-20 (День 4) - Решение

ВремяСобытиеДействие
10:00⚙️ Фаза 1 начата: Обновление liveness probe в Helm charts
10:30✅ Обновлены timeoutSeconds: 10 в mysport-prod и witisi-prod
11:00⚙️ Фаза 2 начата: Изменения в MentKit.Databases.PostgreSql
11:30✅ Добавлены поля в PostgreSqlConfig.cs
12:00✅ Обновлен NpgsqlDataSourceBuilder.cs
12:15✅ Обновлен PostgresConnectionStringBuilder.cs
13:00📝 Создана документация MentKit/README.md
14:00📝 Обновлена диагностика (v3) с описанием выполненных изменений
15:00📝 Создан постмортем
16:00🚀 Статус: Готово к сборке и деплою в devОжидает релиза MentKit

Root Cause Analysis

The Five Whys

1. Почему сервис Ordering периодически переставал работать? → Потому что не мог подключиться к PostgreSQL базе данных.

2. Почему не мог подключиться к PostgreSQL? → Потому что все соединения в connection pool были "мертвыми" (протухшими).

3. Почему соединения в pool стали "мертвыми"? → Потому что PgBouncer закрыл серверные соединения (режим STATEMENT pooling), а Npgsql не знал об этом и продолжал держать клиентские соединения в пуле.

4. Почему Npgsql не обнаружил и не удалил мертвые соединения? → Потому что отсутствовали параметры Connection Idle Lifetime и Connection Pruning Interval - нет автоматической очистки пула.

5. Почему эти параметры отсутствовали? → Потому что они не были реализованы в библиотеке MentKit.Databases.PostgreSql - конфигурация была неполной.

Корневая причина (Root Cause)

Техническая:

MentKit.Databases.PostgreSql не поддерживала критические параметры:
• Connection Idle Lifetime - время жизни idle соединения
• Connection Pruning Interval - интервал очистки пула

В комбинации с агрессивным PgBouncer STATEMENT pooling это привело к:
→ Накоплению протухших соединений в Npgsql pool
→ Блокировке всех запросов приложения
→ Полному отказу сервиса

Процессная:

  • Отсутствие полного code review при создании MentKit.Databases.PostgreSql
  • Отсутствие документации по правильной конфигурации connection pool для production
  • Недостаточное тестирование с PgBouncer в режиме STATEMENT
  • Отсутствие мониторинга метрик connection pool (busy/idle connections)

Усугубляющие факторы

  1. Liveness probe не перезапускал под автоматически

    • Health check timeout = 3 секунды (слишком мало)
    • Иногда health check успевал открыть новое соединение → сброс счетчика failureThreshold
    • Под оставался работать с мертвым connection pool
  2. Высокий CommandTimeout = 300 секунд

    • Запросы висели 5 минут вместо быстрого падения
    • Пользователи ждали долго, затем получали ошибку
  3. Малый MaxPoolSize = 10

    • Быстрое насыщение пула мертвыми соединениями
    • Меньше шансов на получение живого соединения
  4. Отсутствие алертинг для health check failures

    • Проблема обнаруживалась только через support tickets
    • Задержка в реагировании на инцидент

What Went Wrong

🔴 Технические недостатки

  1. Неполная реализация MentKit.Databases.PostgreSql

    • Отсутствие Connection Idle Lifetime
    • Отсутствие Connection Pruning Interval
    • Отсутствие MinPoolSize
    • Отсутствие явного Timeout (использовался default 15 сек)
  2. Неоптимальная конфигурация liveness probe

    • timeoutSeconds: 3 - слишком мало для health check с проверкой БД
    • Не было явного failureThreshold: 3 (использовался default)
  3. Неоптимальная конфигурация connection pool

    • MaxPoolSize: 10 - мало для production нагрузки
    • CommandTimeout: 300 - слишком долго для пользовательских запросов
  4. Агрессивный режим PgBouncer

    • STATEMENT pooling без учета в приложении
    • Нет синхронизации lifecycle соединений между Npgsql и PgBouncer

🔴 Процессные недостатки

  1. Отсутствие мониторинга

    • Нет метрик Npgsql connection pool (busy/idle/failed connections)
    • Нет алертов на health check failures
    • Нет dashboard для БД метрик
  2. Недостаточная документация

    • Нет руководства по настройке PostgreSQL для production
    • Нет примеров конфигурации для работы с PgBouncer
    • Нет troubleshooting guide для проблем с БД
  3. Отсутствие постмортемов

    • Предыдущие проблемы не документировались
    • Нет базы знаний для быстрого решения повторяющихся проблем
  4. Ручное управление инцидентами

    • Требовался ручной перезапуск подов
    • Нет автоматических runbooks
    • Зависимость от конкретных инженеров

What Went Right

✅ Сильные стороны

  1. Быстрое временное восстановление

    • Ручной перезапуск пода занимал ~5 минут
    • Знание kubectl и навыки диагностики
  2. Методичная диагностика

    • Создан подробный файл с описанием проблемы
    • Проверены все гипотезы (сеть, БД, конфигурация)
    • Использован Five Whys метод для поиска root cause
  3. Комплексное решение

    • Не только "пластырь", но и системное исправление
    • Изменения в код + конфигурация + документация
    • План внедрения с фазами (dev → prod)
  4. Документирование

    • Создана подробная диагностика
    • Создана документация MentKit
    • Создан постмортем (этот документ)
  5. Прозрачность

    • Проблема не замалчивалась
    • Готовность к отчетности перед бизнесом
    • Обучение команды на ошибке

Resolution

Краткосрочное решение (Фаза 1) - ✅ ВЫПОЛНЕНО

Цель: Обеспечить автоматический перезапуск проблемных подов.

Изменения:

  1. Обновлен liveness probe в Helm charts:
    # Media/deployment/mysport-prod/ordering/values.yaml
    # Media/deployment/witisi-prod/ordering/values.yaml
    defaultLivenessProbe:
    timeoutSeconds: 10 # было: 3
    failureThreshold: 3 # явно указано

Результат: Под будет перезапускаться автоматически при 3 неудачных health checks подряд.

Долгосрочное решение (Фаза 2) - ✅ ВЫПОЛНЕНО (код готов)

Цель: Предотвратить накопление мертвых соединений в connection pool.

Изменения:

  1. Добавлены новые поля в PostgreSqlConfig.cs:

    public int MinPoolSize { get; set; } = 5;
    public int Timeout { get; set; } = 15;
    public int ConnectionIdleLifetime { get; set; } = 60; // ← КРИТИЧНО
    public int ConnectionPruningInterval { get; set; } = 10; // ← КРИТИЧНО
  2. Обновлены builders для включения параметров в connection string:

    • NpgsqlDataSourceBuilder.cs
    • PostgresConnectionStringBuilder.cs

Результат: Connection pool будет автоматически очищаться от протухших соединений каждые 10 секунд.

Следующие шаги (Фаза 3) - 🔄 В ПРОЦЕССЕ

Требуется выполнить:

  1. ✅ Собрать NuGet пакет MentKit.Databases.PostgreSql (новая версия)
  2. ✅ Опубликовать в NuGet репозиторий
  3. ✅ Обновить зависимости в Ordering сервисе
  4. ✅ Собрать новый Docker образ Ordering
  5. ✅ Деплой в dev для тестирования (1-2 дня мониторинга)
  6. ✅ Деплой в prod (mysport + witisi)

Опционально (улучшения конфигурации через Vault):

vault kv patch prod/services/ordering \
PostgreSql__Timeout=5 \
PostgreSql__MaxPoolSize=20 \
PostgreSql__MinPoolSize=5 \
PostgreSql__ConnectionIdleLifetime=60 \
PostgreSql__ConnectionPruningInterval=10

Альтернативное решение (Фаза 4) - 🔄 ОПЦИОНАЛЬНО

Если проблема не исчезнет полностью - изменить режим PgBouncer:

# Infrastructure/terragrant/terragrunt/media/prod/data/postgresql/terragrunt.hcl
pooler_config = {
pooling_mode = "TRANSACTION" # вместо "STATEMENT"
}

Плюсы: Менее агрессивный pooling, меньше проблем с prepared statements. Минусы: Требует Terraform apply, может повлиять на другие сервисы.


Lessons Learned

1. 📚 Connection Pool требует полного управления

Урок: При работе с двухуровневым pooling (Npgsql + PgBouncer) критически важно управлять lifecycle соединений на уровне приложения.

Что делать:

  • ✅ Всегда устанавливать Connection Idle Lifetime
  • ✅ Всегда устанавливать Connection Pruning Interval
  • ✅ Использовать адекватные значения для production (60 сек / 10 сек)
  • ✅ Документировать требования к connection pool в README

2. 🔍 Health Checks должны быть надежными

Урок: Health check с малым timeout и проверкой БД может давать ложные положительные результаты.

Что делать:

  • ✅ Увеличить timeout до адекватных значений (10 сек вместо 3)
  • ✅ Явно указывать failureThreshold в конфигурации
  • ✅ Рассмотреть раздельные liveness и readiness probes
  • ✅ Liveness должен проверять только "процесс жив", а не БД

3. 📊 Мониторинг важнее реактивного реагирования

Урок: Проблемы обнаруживались через support tickets, а не через мониторинг.

Что делать:

  • ✅ Добавить метрики Npgsql в Prometheus
  • ✅ Настроить алерты на connection pool saturation
  • ✅ Алерты на health check failures
  • ✅ Dashboard с БД метриками (latency, connection count, errors)

4. 📖 Документация - инвестиция в будущее

Урок: Отсутствие документации привело к повторению ошибок конфигурации.

Что делать:

  • ✅ Создать README для всех библиотек (выполнено для MentKit)
  • ✅ Production best practices в документации
  • ✅ Troubleshooting guides для типовых проблем
  • ✅ Регулярные постмортемы для накопления знаний

5. 🚀 Автоматизация > Ручные действия

Урок: Ручной перезапуск подов - признак проблем с автоматизацией.

Что делать:

  • ✅ Правильная настройка health checks для auto-restart
  • ✅ Runbooks для типовых инцидентов
  • ✅ ChatOps для быстрого доступа к операциям
  • ✅ Auto-remediation где возможно

6. 🧪 Тестирование production-like окружения

Урок: Проблема не была обнаружена в dev из-за отличий в конфигурации.

Что делать:

  • ✅ Dev окружение должно быть максимально похоже на prod
  • ✅ Тестирование с PgBouncer в режиме STATEMENT
  • ✅ Chaos engineering для проверки отказоустойчивости
  • ✅ Load testing на staging перед prod релизом

Action Items

Немедленные действия (0-1 день)

#ДействиеОтветственныйДедлайнСтатус
1Собрать и опубликовать MentKit.Databases.PostgreSql v4.0.3Backend Team2026-01-21🔄 В работе
2Обновить Ordering с новой версией MentKitBackend Team2026-01-21🔄 В работе
3Деплой в dev для тестированияDevOps2026-01-21⏳ Ожидает п.1-2

Краткосрочные действия (1-7 дней)

#ДействиеОтветственныйДедлайнСтатус
4Мониторинг в dev окружении (1-2 дня)Backend Team2026-01-23⏳ Ожидает п.3
5Деплой в prod (mysport + witisi)DevOps2026-01-24⏳ Ожидает п.4
6Настроить Prometheus метрики для NpgsqlDevOps2026-01-25📋 Запланировано
7Создать Grafana dashboard для БД метрикDevOps2026-01-26📋 Запланировано
8Настроить алерты на connection pool saturationDevOps2026-01-27📋 Запланировано
9Настроить алерты на health check failuresDevOps2026-01-27📋 Запланировано

Среднесрочные действия (1-4 недели)

#ДействиеОтветственныйДедлайнСтатус
10Обновить все сервисы на новую версию MentKitBackend Team2026-02-07📋 Запланировано
11Создать production best practices документBackend Team2026-02-10📋 Запланировано
12Создать troubleshooting guide для БД проблемBackend Team2026-02-14📋 Запланировано
13Провести code review всех PostgreSQL конфигурацийBackend Team2026-02-14📋 Запланировано
14Рассмотреть раздельные liveness/readiness endpointsBackend Team2026-02-17📋 Запланировано

Долгосрочные действия (1-3 месяца)

#ДействиеОтветственныйДедлайнСтатус
15Внедрить регулярные постмортемы в процессAll Teams2026-02-28📋 Запланировано
16Создать базу знаний в Confluence/WikiAll Teams2026-03-15📋 Запланировано
17Внедрить chaos engineering практикиDevOps2026-03-31📋 Запланировано
18Оценить переход на TRANSACTION pooling в PgBouncerBackend + DevOps2026-04-15📋 Запланировано
19Внедрить Circuit Breaker паттерн для БДBackend Team2026-04-30📋 Запланировано

Appendix

A. Связанные документы

  • Диагностика: ordering-problem-diagnostic.md
  • Исходная проблема: ordering-problem-prompt.md
  • Документация MentKit: Common/ment-kit/README.md
  • Важность постмортемов: why-postmortems-matter.md

B. Конфигурационные файлы

До изменений:

# deployment/mysport-prod/ordering/values.yaml (старое)
defaultLivenessProbe:
httpGet:
path: /hc
port: http
initialDelaySeconds: 15
periodSeconds: 15
# timeoutSeconds: 3 (default)
# failureThreshold: 3 (default)

После изменений:

# deployment/mysport-prod/ordering/values.yaml (новое)
defaultLivenessProbe:
httpGet:
path: /hc
port: http
initialDelaySeconds: 15
periodSeconds: 15
timeoutSeconds: 10 # ← ИЗМЕНЕНО
failureThreshold: 3 # ← ЯВНО УКАЗАНО

C. Код изменений

PostgreSqlConfig.cs (новые поля):

public int MinPoolSize { get; set; } = 5;
public int Timeout { get; set; } = 15;
public int ConnectionIdleLifetime { get; set; } = 60;
public int ConnectionPruningInterval { get; set; } = 10;

NpgsqlDataSourceBuilder.cs (connection string):

connectionString = $"Server={server};" +
$"Port={_config.Port};" +
$"Database={_config.Database};" +
$"User ID={_config.User};" +
$"Password={_config.Password};" +
$"Pooling={_config.Pooling.ToString().ToLower()};" +
$"Minimum Pool Size={_config.MinPoolSize};" + // ДОБАВЛЕНО
$"Maximum Pool Size={_config.MaxPoolSize};" +
$"Keepalive={_config.Keepalive};" +
$"Timeout={_config.Timeout};" + // ДОБАВЛЕНО
$"Command Timeout={_config.CommandTimeout};" +
$"Connection Idle Lifetime={_config.ConnectionIdleLifetime};" + // ДОБАВЛЕНО
$"Connection Pruning Interval={_config.ConnectionPruningInterval};"; // ДОБАВЛЕНО

D. Критерии успеха

Метрики для подтверждения решения проблемы (мониторинг 7 дней):

Отсутствие ошибок:

kubectl logs -f deployment/ordering-internal-host -n mysport | grep "Unable to connect"
# Ожидается: 0 совпадений

Стабильность health checks:

kubectl get events -n mysport --field-selector reason=Unhealthy
# Ожидается: 0 событий

Отсутствие перезапусков:

kubectl get pods -n mysport -l app=ordering-internal-host
# RESTARTS должен быть 0 (кроме запланированных деплоев)

Метрики Npgsql (когда будут настроены):

  • npgsql_pool_idle_connections >= MinPoolSize
  • npgsql_pool_busy_connections < MaxPoolSize (длительно)
  • npgsql_failed_connections == 0

E. Контакты для вопросов

  • Backend Team Lead: [Имя], [email]
  • DevOps Lead: [Имя], [email]
  • On-Call Engineer: [Rotation schedule]

F. Версия документа

  • v1.0 (2026-01-20): Первая версия постмортема
  • Автор: DevOps/Backend Team
  • Reviewers: [Список]
  • Утверждено: [Имя, дата]

Подписи

Составил: _____________________ (DevOps/Backend Team) Дата: 2026-01-20

Проверил: _____________________ (Tech Lead) Дата: ___________________

Утвердил: _____________________ (CTO/Engineering Manager) Дата: ___________________


Классификация: Internal Распространение: Engineering Team, Support, Management Хранение: Wiki/Confluence, Git Repository


Этот постмортем создан в рамках практики непрерывного улучшения. Цель - обучение команды, а не поиск виноватых (blameless culture).