Бесперебойный API на Golang



Книга Бесперебойный API на Golang

При запуске нового приложения важно, чтобы оно легко обновлялось и распространялось. А для некоторых приложений  —  еще и максимизация времени бесперебойной работы. Хороший пример таких приложений  —  API, используемый сразу несколькими клиентами, которые ожидают, что он всегда будет работать без сбоев.


Недавно у меня была задача: создать API с максимальным временем бесперебойной работы при обновлении бинарных файлов. Кратко изложу суть найденных мной решений. Наверняка есть и другие, просто эти два решения применяются чаще.


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


Решения должны работать не только на Linux, но и на любой платформе UNIX.


Знакомы с основами файловых дескрипторов, сигналов, а также различиями между потоками и процессами? Тогда начнем. Почти все примеры здесь на 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.



1007   0  

Comments

    Ничего не найдено.