Итоговый проект: микросервис с Graceful Shutdown и БД

Мы подошли к финалу фундаментальной части обучения. Ранее мы разобрали, как упаковать приложение в Docker-контейнер и следовать принципам 12-факторных приложений. Теперь нам предстоит объединить все знания: работу с HTTP-сервером, подключение к PostgreSQL через pgx, использование контекста и горутин — чтобы создать отказоустойчивый микросервис.

Центральная тема этого этапа — Graceful Shutdown (плавное завершение работы). В React вы использовали функцию очистки (cleanup function) в useEffect, чтобы сбросить таймеры или отписаться от событий перед размонтированием компонента. В бэкенде на Go это критический процесс. Он гарантирует, что при обновлении сервиса запросы пользователей не оборвутся на середине, а соединения с базой данных закроются корректно.

Жизненный цикл процесса и системные сигналы

Когда микросервис запущен в контейнере, им управляет оркестратор (например, Kubernetes). Чтобы остановить сервис, операционная система посылает ему OS signals (системные сигналы).

Для нас важны два типа сигналов:

  1. SIGINT (Signal Interrupt) — отправляется при нажатии Ctrl+C в терминале.
  2. SIGTERM (Signal Termination) — стандартный сигнал для остановки процесса в Docker и Kubernetes.

Если приложение не «слушает» эти сигналы, ОС принудительно завершит процесс через SIGKILL. В итоге активные HTTP-ответы не дойдут до клиента, а транзакции в базе данных могут «повиснуть».

Как показано в Схеме 1, корректный алгоритм завершения позволяет сервису доработать текущие задачи перед выходом.

Реализация: от теории к коду

В Go 1.22+ стандарт обработки сигналов — функция signal.NotifyContext. Она создает контекст, который переходит в состояние «отменен», как только в приложение прилетает нужный сигнал.

Мы запускаем HTTP-сервер в отдельной горутине. Это нужно, чтобы основная функция main могла заблокироваться и ждать сигнала отмены, не мешая серверу обрабатывать запросы.

package main

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	// Создаем контекст, реагирующий на сигналы завершения
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	dbPool, err := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/dbname")
	if err != nil {
		logger.Error("failed to connect to db", "error", err)
		os.Exit(1)
	}
	defer dbPool.Close()

	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	srv := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	// Запускаем сервер в горутине
	go func() {
		logger.Info("starting server", "addr", srv.Addr)
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			logger.Error("listen and serve error", "error", err)
		}
	}()

	// Ждем сигнала SIGINT или SIGTERM
	<-ctx.Done()
	logger.Info("shutting down gracefully...")

	// Даем серверу 5 секунд на завершение текущих запросов
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := srv.Shutdown(shutdownCtx); err != nil {
		logger.Error("server forced to shutdown", "error", err)
	}

	logger.Info("server stopped")
}

Почему это важно для React-разработчика

Представьте: пользователь нажимает «Оплатить» в вашем интерфейсе. Бэкенд начинает транзакцию. В этот момент вы деплоите новую версию бэкенда.

  1. Без Graceful Shutdown: Процесс убивается мгновенно. Фронтенд получает Network Error. Деньги могут быть списаны, но заказ в базе не создастся, так как код не успел дойти до конца.
  2. С Graceful Shutdown: Сервер получает SIGTERM. Он перестает принимать новые соединения, но дожидается окончания обработки платежа и отправляет успешный ответ. Только после этого процесс завершается.

Использование os.Exit(0) сразу после получения сигнала. Это мгновенно убивает программу, игнорируя блоки defer и обрывая активные соединения.

Управление ресурсами базы данных

При завершении работы важно вызвать dbPool.Close(). Это закрывает пул соединений с PostgreSQL. Если этого не сделать, в базе останутся «мертвые» сессии, которые бесполезно потребляют память и лимиты подключений.

  1. Возьмите ваш текущий учебный проект.
  2. Реализуйте в нем signal.NotifyContext для обработки SIGTERM.
  3. Добавьте в один из эндпоинтов задержку time.Sleep(3 * time.Second).
  4. Запустите сервер, сделайте запрос к этому эндпоинту и сразу нажмите Ctrl+C. Проверьте по логам, что сервер дождался ответа и только потом закрылся.

Мы построили надежный скелет микросервиса. Это фундамент профессиональной разработки. Однако в крупных системах сервисам нужно не просто «уходить красиво», но и эффективно общаться друг с другом.

В следующей теме мы изучим gRPC для высокопроизводительного взаимодействия и разберем инструменты наблюдаемости (observability), чтобы видеть, что происходит внутри приложения.

Понравился урок?

Сохраните прогресс и получите персональный курс по любой теме — без форм и паролей

Продолжить в Telegram