Постмортем: Сбой сервиса 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 часа (с периодическими восстановлениями)
Содержание
- Impact Assessment
- Timeline
- Root Cause Analysis
- What Went Wrong
- What Went Right
- Resolution
- Lessons Learned
- Action Items
- Appendix
Impact Assessment
Затронутые системы
| Компонент | Namespace | Cluster | Воздействие |
|---|---|---|---|
ordering-internal-host | mysport | yc-prod | 🔴 Полный отказ |
ordering-internal-host | witisi | yc-prod | 🔴 Полный отказ |
ordering-adminapi-host | mysport | yc-prod | 🟡 Частичная деградация |
ordering-clientapi-host | mysport | yc-prod | 🟡 Частичная деградация |
Метрики воздействия
Технические метрики:
- Error Rate: До 90% для
/api/v1/ordersendpoint - 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)
Усугубляющие факторы
-
Liveness probe не перезапускал под автоматически
- Health check timeout = 3 секунды (слишком мало)
- Иногда health check успевал открыть новое соединение → сброс счетчика failureThreshold
- Под оставался работать с мертвым connection pool
-
Высокий CommandTimeout = 300 секунд
- Запросы висели 5 минут вместо быстрого падения
- Пользователи ждали долго, затем получали ошибку
-
Малый MaxPoolSize = 10
- Быстрое насыщение пула мертвыми соединениями
- Меньше шансов на получение живого соединения
-
Отсутствие алертинг для health check failures
- Проблема обнаруживалась только через support tickets
- Задержка в реагировании на инцидент
What Went Wrong
🔴 Технические недостатки
-
Неполная реализация MentKit.Databases.PostgreSql
- Отсутствие
Connection Idle Lifetime - Отсутствие
Connection Pruning Interval - Отсутствие
MinPoolSize - Отсутствие явного
Timeout(использовался default 15 сек)
- Отсутствие
-
Неоптимальная конфигурация liveness probe
timeoutSeconds: 3- слишком мало для health check с проверкой БД- Не было явного
failureThreshold: 3(использовался default)
-
Неоптимальная конфигурация connection pool
MaxPoolSize: 10- мало для production нагрузкиCommandTimeout: 300- слишком долго для пользовательских запросов
-
Агрессивный режим PgBouncer
STATEMENTpooling без учета в приложении- Нет синхронизации lifecycle соединений между Npgsql и PgBouncer
🔴 Процессные недостатки
-
Отсутствие мониторинга
- Нет метрик Npgsql connection pool (busy/idle/failed connections)
- Нет алертов на health check failures
- Нет dashboard для БД метрик
-
Недостаточная документация
- Нет руководства по настройке PostgreSQL для production
- Нет примеров конфигурации для работы с PgBouncer
- Нет troubleshooting guide для проблем с БД
-
Отсутствие постмортемов
- Предыдущие проблемы не документировались
- Нет базы знаний для быстрого решения повторяющихся проблем
-
Ручное управление инцидентами
- Требовался ручной перезапуск подов
- Нет автоматических runbooks
- Зависимость от конкретных инженеров
What Went Right
✅ Сильные стороны
-
Быстрое временное восстановление
- Ручной перезапуск пода занимал ~5 минут
- Знание kubectl и навыки диагностики
-
Методичная диагностика
- Создан подробный файл с описанием проблемы
- Проверены все гипотезы (сеть, БД, конфигурация)
- Использован Five Whys метод для поиска root cause
-
Комплексное решение
- Не только "пластырь", но и системное исправление
- Изменения в код + конфигурация + документация
- План внедрения с фазами (dev → prod)
-
Документирование
- Создана подробная диагностика
- Создана документация MentKit
- Создан постмортем (этот документ)
-
Прозрачность
- Проблема не замалчивалась
- Готовность к отчетности перед бизнесом
- Обучение команды на ошибке
Resolution
Краткосрочное решение (Фаза 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.
Изменения:
-
Добавлены новые поля в
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; // ← КРИТИЧНО -
Обновлены builders для включения параметров в connection string:
NpgsqlDataSourceBuilder.csPostgresConnectionStringBuilder.cs
Результат: Connection pool будет автоматически очищаться от протухших соединений каждые 10 секунд.
Следующие шаги (Фаза 3) - 🔄 В ПРОЦЕССЕ
Требуется выполнить:
- ✅ Собрать NuGet пакет MentKit.Databases.PostgreSql (новая версия)
- ✅ Опубликовать в NuGet репозиторий
- ✅ Обновить зависимости в Ordering сервисе
- ✅ Собрать новый Docker образ Ordering
- ✅ Деплой в dev для тестирования (1-2 дня мониторинга)
- ✅ Деплой в 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.3 | Backend Team | 2026-01-21 | 🔄 В работе |
| 2 | Обновить Ordering с новой версией MentKit | Backend Team | 2026-01-21 | 🔄 В работе |
| 3 | Деплой в dev для тестирования | DevOps | 2026-01-21 | ⏳ Ожидает п.1-2 |
Краткосрочные действия (1-7 дней)
| # | Действие | Ответственный | Дедлайн | Статус |
|---|---|---|---|---|
| 4 | Мониторинг в dev окружении (1-2 дня) | Backend Team | 2026-01-23 | ⏳ Ожидает п.3 |
| 5 | Деплой в prod (mysport + witisi) | DevOps | 2026-01-24 | ⏳ Ожидает п.4 |
| 6 | Настроить Prometheus метрики для Npgsql | DevOps | 2026-01-25 | 📋 Запланировано |
| 7 | Создать Grafana dashboard для БД метрик | DevOps | 2026-01-26 | 📋 Запланировано |
| 8 | Настроить алерты на connection pool saturation | DevOps | 2026-01-27 | 📋 Запланировано |
| 9 | Настроить алерты на health check failures | DevOps | 2026-01-27 | 📋 Запланировано |
Среднесрочные действия (1-4 недели)
| # | Действие | Ответственный | Дедлайн | Статус |
|---|---|---|---|---|
| 10 | Обновить все сервисы на новую версию MentKit | Backend Team | 2026-02-07 | 📋 Запланировано |
| 11 | Создать production best practices документ | Backend Team | 2026-02-10 | 📋 Запланировано |
| 12 | Создать troubleshooting guide для БД проблем | Backend Team | 2026-02-14 | 📋 Запланировано |
| 13 | Провести code review всех PostgreSQL конфигураций | Backend Team | 2026-02-14 | 📋 Запланировано |
| 14 | Рассмотреть раздельные liveness/readiness endpoints | Backend Team | 2026-02-17 | 📋 Запланировано |
Долгосрочные действия (1-3 месяца)
| # | Действие | Ответственный | Дедлайн | Статус |
|---|---|---|---|---|
| 15 | Внедрить регулярные постмортемы в процесс | All Teams | 2026-02-28 | 📋 Запланировано |
| 16 | Создать базу знаний в Confluence/Wiki | All Teams | 2026-03-15 | 📋 Запланировано |
| 17 | Внедрить chaos engineering практики | DevOps | 2026-03-31 | 📋 Запланировано |
| 18 | Оценить переход на TRANSACTION pooling в PgBouncer | Backend + DevOps | 2026-04-15 | 📋 Запланировано |
| 19 | Внедрить Circuit Breaker паттерн для БД | Backend Team | 2026-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 >= MinPoolSizenpgsql_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).