Чек-лист продакшена для Go-сервиса
Локально сервис работает, а в бою выясняется, что запрос без таймаута висит вечно, деплой рвёт пользовательские соединения, а при падении базы нельзя понять, где искать причину. Проблемы из этого списка не относятся к одному коммиту, поэтому на код-ревью их обычно не видно — их и собирают в отдельный чек-лист.
Что стоит за каждым пунктом
Список разбит на девять групп: таймауты и отмена, graceful shutdown, health-check, лимиты, логирование, метрики, конфигурация, безопасность и тесты. Порядок групп не случаен — он совпадает с порядком, в котором проблемы проявляются в жизни: сначала сервис зависает, потом не выключается, потом о нём невозможно понять, что происходит.
Каждый пункт — это не «хорошая практика вообще», а конкретная проверка с объяснением: почему пункт важен, что произойдёт без него и как это выглядит в коде. Там, где есть типовое решение, приведён пример.
Отметки сохраняются в localStorage, поэтому чек-лист можно проходить по частям и возвращаться к нему перед каждым релизом. Кнопка «Скопировать списком» превращает результат в текст, который можно вставить в задачу или в обсуждение с командой.
Кому это полезнее всего: автору первого сервиса, который уходит в продакшен, и ревьюеру, который хочет задать правильные вопросы на ревью архитектуры. Перечитывать чек-лист полезно и опытному разработчику: часть пунктов со временем меняет смысл. Например, лимит на размер тела запроса становится обязательным, как только за сервисом появляется публичный API, а health-check для готовности — после первого деплоя с несколькими репликами.
Прогресс
Отметки сохраняются в вашем браузере: можно пройти чек-лист частями и вернуться позже.
Таймауты и отмена
0 из 4Запрос без таймаута — это зависший воркер, съеденный connection pool и падение.
client := &http.Client{ Timeout: 10 * time.Second, }ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() if err := repo.Save(ctx, order); err != nil { return err }pool, err := pgxpool.New(ctx, dsn) pool.Config().MaxConnLifetime = 30 * time.Minute
Graceful shutdown
0 из 4Сервис должен уметь завершиться, не оборвав запросы на полуслове.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop()ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Error("shutdown", "err", err) }
Health-check
0 из 3Оркестратор должен понимать, жив ли процесс и готов ли он принимать трафик.
Лимиты и защита
0 из 4Ограничение тела запроса, числа соединений и частоты — базовая гигиена сервиса.
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 МБsrv := &http.Server{ ReadHeaderTimeout: 5 * time.Second, WriteTimeout: 30 * time.Second, }
Логирование
0 из 4Логи должны быть пригодны для чтения человеком и для агрегации машиной.
slog.Info("order created", "order_id", id, "amount", amount, )
Метрики
0 из 4Метрики отвечают на вопрос «что сломалось» до того, как об этом узнал пользователь.
Конфигурация
0 из 3Конфигурация проверяется при старте, а не в середине рабочего дня.
if cfg.Addr == "" { return errors.New("ADDR is required") }
Безопасность
0 из 4Секреты, TLS, ввод пользователя и заголовки ответов.
Тесты и CI
0 из 4Проверки, которые ловят проблемы до релиза, а не после.
Частые вопросы о готовности сервиса к продакшену
Зачем нужен чек-лист, если есть код-ревью?
Ревью смотрит на конкретное изменение, а чек-лист — на сервис целиком: есть ли в нём таймауты, health-check, лимиты и остановка. Проблемы из этого списка обычно не относятся к одному коммиту, поэтому на ревью их не видно. Чек-лист проходят один раз при выводе сервиса в бой, а потом — по пунктам при изменении архитектуры.
Как правильно настроить health-check в Kubernetes?
Разделяйте liveness и readiness. Liveness отвечает «процесс жив» и не должен ходить во внешние зависимости: иначе при кратковременной недоступности базы Kubernetes перезапустит здоровый под. Readiness отвечает «можно принимать трафик» и проверяет критичные зависимости: во время миграции или прогрева кэша readiness должен возвращать отказ, и новые поды не получат трафик. Оба эндпоинта — на отдельном порту, недоступном извне.
Нужен ли лимит на размер тела запроса, если за прокси стоит nginx?
Прокси обычно ограничивает размер тела, и это уже спасает от самых крупных запросов. Но nginx можно настроить неправильно или вовсе не ставить перед Go-сервисом. Лимит в самом приложении — последний рубеж: http.MaxBytesReader обрывает чтение тела и не даёт процессу съесть память на мусорном запросе.
Где хранить отметки и можно ли ими поделиться?
Отметки лежат в localStorage вашего браузера и никуда не отправляются. Чтобы поделиться результатом, нажмите «Скопировать списком» — получите текст с отметками, который можно вставить в задачу или обсуждение.