← Назад на главную

Запуск и завершение работы веб-сервера на Go

Стандартная библиотека Go предоставляет отличную основу для создания веб-серверов, однако, как показывает практика, часто требуется вносить изменения и дополнения. В этой статье я хотел бы рассмотреть процесс запуска веб-сервера и примеры его завершения.

Запуск веб-сервера

Чтобы веб-сервер или любая другая программа автоматически запускались после перезагрузки сервера Linux, можно использовать юниты systemd. Вот пример файла bot.service, который я использую для своего бота:

[Unit]
Description=bot # Краткое описание сервиса
After=network.target # Гарантирует, что сервис будет запущен после запуска сети.
# Это полезно, если ваше приложение требует доступа к сети.

[Service]
Type=simple
User=root # Имя пользователя, от имени которого будет запускаться приложение.
Group=root # Имя группы.
ExecStart=/home/main # Путь к исполняемому файлу приложения.
Restart=always # Автоматически перезапускается в любом случае, когда он завершается, независимо от причины.

[Install]
WantedBy=multi-user.target # Показывает, что сервис должен быть включен в multi-user.target,
# который представляет собой многопользовательский режим работы системы.
# Это означает, что сервис будет автоматически запущен при загрузке системы.
Чтобы включить и запустить службу (юнит) в systemd, нужно скопировать файл, описывающий вашу службу, в каталог /etc/systemd/system/ и создать ссылку на этот файл в директории, которую systemd сканирует для активных служб.

sudo ln -s /etc/systemd/system/bot.service /etc/systemd/system/multi-user.target.wants/bot.service
Перезагружаем конфигурацию systemd:

systemctl daemon-reload
Запускаем сервис:

systemctl start bot
Проверим статус и убедимся что все работает:

systemctl status bot
Подробную информацию о параметрах systemd можно найти в документации: https://systemd.io/

Завершение работы веб-сервера

Рассмотрим два сценария для завершения работы сервера:

1. Обработчик URL-адреса для завершения работы

В процессе разработки часто используют простой, но не всегда оптимальный подход - реализация остановки сервера при переходе по определенному URL-адресу, например /kill или /shutdown. Следующий пример демонстрирует простую реализацию этого метода.

package main

import (
"fmt"
"net/http"
"os"
)

func main() {
http.HandleFunc("/shutdown", shutdown) // Регистрация специального пути для завершения работы сервера
http.HandleFunc("/", homePage)
http.ListenAndServe(":8080", nil)
}
func shutdown(res http.ResponseWriter, req *http.Request) {
os.Exit(0) // Немедленно завершает приложение
}
func homePage(res http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/" {
http.NotFound(res, req)
return
}
fmt.Fprint(res, "The homepage.")
}
Несмотря на простоту, этот подход имеет ряд недостатков:

  • При развертывании в production, URL-адрес должен быть заблокирован или удален. Использование одного и того же кода для разработки и эксплуатации может привести к ошибкам. Открытый URL-адрес для завершения работы сервера может позволить любому пользователю его отключить.
  • Обработчик запроса немедленно завершает работу сервера, прерывая обработку текущих запросов. Это может привести к потере данных или некорректному состоянию приложения.
  • Использование этого подхода обходит стандартные инструменты управления и развертывания. Рекомендуется использовать более надежные и управляемые способы остановки сервера.

2. Штатное завершение работы с помощью пакета manners

Для обеспечения корректного завершения работы сервера необходимо:

  • Прекратить прием новых запросов.
  • Сохранить данные на диск.
  • Завершить обработку открытых подключений.

Стандартный пакет http завершает работу немедленно и не предоставляет возможностей для выполнения этих действий. Это может привести к потере данных или некорректному состоянию приложения.

Для решения этой задачи можно использовать сторонний пакет, например github.com/braintree/manners. Пакет manners позволяет корректно завершить работу сервера, используя тот же интерфейс, что и функция ListenAndServe из стандартного пакета http.

package main

import (
"fmt"
"net/http"
"os"
"os/signal"

"github.com/braintree/manners"
)

func main() {
handler := newHandler() // Получение экземпляра обработчика
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
go listenForShutdown(ch)
manners.ListenAndServe(":8080", handler) // Запуск веб-сервера
}
func newHandler() *handler {
return &handler{}
}

type handler struct{}

func (h *handler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
query := req.URL.Query()
name := query.Get("name")
if name == "" {
name = "Artem"
}
fmt.Fprint(res, "Hello, my name is ", name)
}
func listenForShutdown(ch <-chan os.Signal) {
<-ch
manners.Close()
}
В этом примере используем пакет manners для запуска веб-сервера и перехватываем сигнал операционной системы (например, Interrupt) с помощью пакета signal.

При получении сигнала мы вызываем manners.Close(), который:

  • Прекращает прием новых подключений.
  • Дожидается завершения обработки текущих запросов.
  • Освобождает порт TCP, позволяя другим приложениям использовать его.

Преимущества:

  • Позволяет завершить текущие HTTP-запросы, не прерывая их обработку.
  • Останавливает прием новых запросов, освобождая порт TCP.
  • Обеспечивает более управляемый и предсказуемый процесс завершения работы сервера.

Ограничения:

  • Пакет manners работает только с HTTP-подключениями, а не с любыми TCP-соединениями.
  • В некоторых случаях может потребоваться реализовать свою логику для управления сопрограммами и ожидания их завершения перед выходом.

Этот подход обеспечивает более надежный и безопасный способ остановки сервера, минимизируя риск потери данных и обеспечивая корректную работу приложения.

Спасибо за ваше время и внимание! Ваша поддержка очень важна для меня! Если вам понравилась статья, пожалуйста, поставьте лайк этой статье на моем канале Дзен

Подпишитесь на мой Телеграм-канал, чтобы быть в курсе новых статей.

Удачи!