При запуске нового приложения важно, чтобы оно легко обновлялось и распространялось. А для некоторых приложений — еще и максимизация времени бесперебойной работы. Хороший пример таких приложений — API, используемый сразу несколькими клиентами, которые ожидают, что он всегда будет работать без сбоев.
Недавно у меня была задача: создать API с максимальным временем бесперебойной работы при обновлении бинарных файлов. Кратко изложу суть найденных мной решений. Наверняка есть и другие, просто эти два решения применяются чаще.
Пусть они послужат некой отправной точкой для будущих открытий: возвращаясь к этим решениям, вам будет отчего отталкиваться в дальнейшем.
Знакомы с основами файловых дескрипторов, сигналов, а также различиями между потоками и процессами? Тогда начнем. Почти все примеры здесь на Golang.
Решение 1. Параметры сокетов SO_REUSEPORT задаются для слушателя (см. пример кода) — и через один и тот же порт запускается несколько веб-серверов.
Это решение реализовать проще всего. Надо только убедить слушателя TCP применить параметры сокетов:
lc := net.ListenConfig{
Control: control,
}
...
func control(network, address string, c syscall.RawConn) error {
var err error
c.Control(func(fd uintptr) {
err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
if err != nil {
return
}
err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
if err != nil {
return
}
})
return err
}
Здесь применили SO_REUSEADDR и SO_REUSEPORT: разница между ними хорошо описана здесь на Stackoverflow.
Обычно нельзя слушать порт, уже используемый другим приложением. Но при указании этих параметров сокета — можно. Когда несколько приложений слушают один порт, ядро «случайным образом» распределяет входящие запросы между ними. Посмотрите на это в коде.
Решение 2. Запускается новый процесс с указанием наследуемых файловых дескрипторов. Открытый сокет используется совместно с дочерним процессом: он запускается там, где остановился родительский.
Второе решение сложнее, но и сценариев применения у него больше. И не только с сокетами — с любыми файловыми дескрипторами (каналами, файлами и т. д.).
В Golang много способов создать новый процесс. Процесс и поток здесь — разные понятия. Процесс — это как очередное запускаемое приложение. В отличие от потоков, процессы не делят оперативную память.
Взгляните на функцию StartProcess в пакете os. В ее параметре ProcAttr указываются открытые файловые дескрипторы для дочернего процесса, который не будет создавать нового слушателя — а просто заберет его у родительского процесса. В коде это легко увидеть.
Протестируем оба примера (размещены на GitHub). Код для первого — в socket-option-version. Скомпилировав его с помощью go build -o zero ./main.go
и запустив как ./zero
, получим вывод.
Проверим функциональность, отправив HTTP-запрос в конечную точку. Обновляем двоичные файлы с помощью сигнала Linux SIGUSR2: отправляем его с другого терминала, например с pkill -SIGUSR2 zero
. Приложение «закрыто», и терминал снова готов к использованию.
Отправляем еще один HTTP-запрос и получаем ответ. Лог выводится в терминал, ведь stdout
и stderr
используются совместно с дочерним процессом — любой его вывод будет отображаться в терминале:
Код для второго примера — в папке inherit-version. Скомпилировав его с помощью go build -o zero ./main.go
и запустив как ./zero
, получим те же результаты:
Весь код размещен на GitHub.
Читайте также:
- Обработка сигналов в операционных системах семейства Unix на Golang
- Полиморфизм с интерфейсами в Golang
- Конкурентность и параллелизм в Golang. Горутины.
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Daniel Sedlak: Zero downtime API in Golang