Вы когда-нибудь выдергивали шнур питания из компьютера в отчаянии? Хотя это может показаться быстрым решением, оно может привести к потере данных и нестабильности системы. В мире программного обеспечения существует похожая концепция: жесткое завершение работы. Такое резкое завершение работы может вызвать проблемы, как и его физический аналог. К счастью, есть лучший способ: плавное завершение работы. Интегрируя плавное отключение, мы предоставляем предварительное уведомление службе. Это позволяет ей завершать текущие запросы, потенциально сохранять информацию о состоянии на диске и в конечном итоге избегать повреждения данных во время отключения. В этом руководстве мы погрузимся в мир корректного завершения работы, уделив особое внимание его реализации в работающих в Kubernetes. приложениях Go, Сигналы в системах Unix Одним из ключевых инструментов для достижения изящного завершения работы в системах на базе Unix является концепция сигналов, которые, говоря простыми словами, являются простым способом передачи одной конкретной вещи процессу из другого процесса. Понимая, как работают сигналы, мы можем использовать их для реализации контролируемых процедур завершения работы в наших приложениях, обеспечивая плавный и безопасный для данных процесс завершения работы. Сигналов много, и вы можете найти их , но нас интересуют только сигналы выключения: здесь - отправляется процессу для запроса на его завершение. Используется чаще всего, и мы рассмотрим его позже. SIGTERM — «немедленно выйти», нельзя вмешиваться. SIGKILL - сигнал прерывания (например, Ctrl+C) SIGINT - сигнал выхода (например, Ctrl+D) SIGQUIT Эти сигналы могут быть отправлены пользователем (Ctrl+C / Ctrl+D), другой программой/процессом или самой системой (ядром/ОС). Например, , также известный как ошибка сегментации, отправляется ОС. SIGSEGV Наша служба поддержки морских свинок Чтобы исследовать мир изящных отключений в практической обстановке, давайте создадим простую службу, с которой мы сможем поэкспериментировать. Эта служба «подопытного кролика» будет иметь одну конечную точку, которая имитирует некоторую реальную работу (мы добавим небольшую задержку) путем вызова команды Redis. Мы также предоставим базовую конфигурацию Kubernetes для проверки того, как платформа обрабатывает сигналы завершения. INCR Конечная цель: гарантировать, что наш сервис изящно обрабатывает отключения без потери каких-либо запросов/данных. Сравнивая количество запросов, отправленных параллельно, с конечным значением счетчика в Redis, мы сможем проверить, успешно ли реализовано наше изящное отключение. Мы не будем вдаваться в подробности настройки кластера Kubernetes и Redis, но . полную настройку вы можете найти в нашем репозитории Github Процесс проверки следующий: Разверните приложение Redis и Go в Kubernetes. Используйте для отправки 1000 запросов (25/с в течение 40 секунд). vegeta Во время работы vegeta инициализируйте Kubernetes, обновив тег образа. скользящее обновление Подключитесь к Redis, чтобы проверить «счетчик», он должен быть равен 1000. Начнем с нашего базового Go HTTP-сервера. hard-shutdown/main.go package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main() { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_ADDR"), }) server := http.Server{ Addr: ":8080", } http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest(redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") } При запуске процедуры проверки с использованием этого кода мы увидим, что некоторые запросы завершаются ошибкой, а (число может меняться при каждом запуске). счетчик меньше 1000 Это явно означает, что мы потеряли часть данных во время обновления. 😢 Обработка сигналов в Go Go предоставляет пакет , который позволяет обрабатывать сигналы Unix. Важно отметить, что по умолчанию сигналы SIGINT и SIGTERM приводят к завершению работы программы Go. И для того, чтобы наше приложение Go не завершалось так внезапно, нам нужно обрабатывать входящие сигналы. сигналов Есть два варианта сделать это. Использование канала: c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM) Использование контекста (предпочтительный подход в настоящее время): ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() возвращает копию родительского контекста, помеченную как выполненную (его канал Done закрыт), когда поступает один из перечисленных сигналов, когда вызывается возвращенная функция или когда закрывается канал Done родительского контекста, в зависимости от того, что произойдет раньше. NotifyContext stop() В нашей текущей реализации HTTP-сервера есть несколько проблем: У нас есть медленная горутина processRequest, и поскольку мы не обрабатываем сигнал завершения, программа автоматически завершается, что означает, что все работающие горутины также завершаются. Программа не закрывает никаких соединений. Давайте перепишем это. graceful-shutdown/main.go package main // imports var wg sync.WaitGroup func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { wg.Add(1) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf("could not shutdown: %v\n", err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit(0) } func processRequest(redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") } Вот краткий обзор обновлений: Добавлен для прослушивания сигнала завершения SIGTERM. signal.NotifyContext Введена для отслеживания текущих запросов (goroutines processRequest). sync.WaitGroup Обернул сервер в горутину и использовал с контекстом, чтобы корректно прекратить прием новых подключений. server.Shutdown Использовал , чтобы убедиться, что все текущие запросы (goroutines processRequest) завершены перед продолжением. wg.Wait() Очистка ресурсов: добавлен для корректного закрытия соединения Redis перед выходом. redisdb.Close() Чистый выход: используется для указания успешного завершения. os.Exit(0) Теперь, если мы повторим наш процесс проверки, мы увидим, что все 1000 запросов обработаны правильно. 🎉 Веб-фреймворки / HTTP-библиотека Такие фреймворки, как Echo, Gin, Fiber и другие, будут создавать goroutine для каждого входящего запроса, давая ему контекст, а затем вызывать вашу функцию/обработчик в зависимости от выбранной вами маршрутизации. В нашем случае это будет анонимная функция, заданная HandleFunc для пути «/incr». Когда вы перехватываете сигналы и просите свой фреймворк корректно завершить работу, происходят 2 важные вещи (упрощая): SIGTERM Ваш фреймворк перестал принимать входящие запросы Он ждет завершения всех существующих входящих запросов (неявно ожидая завершения горутин). Примечание: Kubernetes также прекращает направлять входящий трафик от балансировщика нагрузки к вашему модулю после того, как он помечает его как Terminating. Необязательно: тайм-аут выключения Завершение процесса может быть сложным, особенно если задействовано много шагов, например, закрытие соединений. Чтобы все прошло гладко, можно установить тайм-аут. Этот тайм-аут действует как страховочная сетка, изящно завершая процесс, если он занимает больше времени, чем ожидалось. shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() go func() { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("could not shutdown: %v\n", err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln("timeout exceeded, forcing shutdown") } os.Exit(0) } Жизненный цикл завершения Kubernetes Поскольку мы использовали Kubernetes для развертывания нашего сервиса, давайте углубимся в то, как он завершает pod. Как только Kubernetes решает завершить pod, произойдут следующие события: Pod устанавливается в состояние «Завершение» и удаляется из списка конечных точек всех служб. Если определено, выполняется Hook. preStop Сигнал отправляется в pod. Но эй, теперь наше приложение знает, что делать! SIGTERM Kubernetes ждет в течение льготного периода ( ), который по умолчанию составляет 30 секунд. terminateGracePeriodSeconds Сигнал отправляется на модуль, и модуль удаляется. SIGKILL Как видите, если у вас длительный процесс завершения, может потребоваться увеличить параметр , предоставив приложению достаточно времени для корректного завершения работы. terminateGracePeriodSeconds Заключение Мягкие отключения защищают целостность данных, поддерживают бесперебойный пользовательский опыт и оптимизируют управление ресурсами. Благодаря своей богатой стандартной библиотеке и акценту на параллелизме Go позволяет разработчикам легко интегрировать методы мягкого отключения — необходимость для приложений, развернутых в контейнерных или оркестрованных средах, таких как Kubernetes. Код Go и манифесты Kubernetes можно найти в . нашем репозитории Github Ресурсы пакет os/signal Жизненный цикл модуля Kubernetes