Демон на Golang

Внезапно возникла ситуация, в которой нужно было выполнять долгие запросы из php и не задерживать ответ пользователю сайта, то есть выполнять их в фоне. Вариант с выполнением curl в консоли не очень красивый и было решено написать свой фоновый рассыльщик запросов. Исходный код как всегда выложен на GitHub.

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

Требования

Программа должна была:

  • Добавлять запросы в очередь через http.
  • Отправлять запросы с определенного сетевого интерфейса. Например, у хоста есть 2 публичных IP, один приватный, а на втором настроен фаерволл, который запрещает любые входящие подключения. Для безопасности, отправлять запросы нужно со второго адреса.
  • Должна уметь выполнять несколько запросов параллельно.
  • Работать быстро и с минимальным потреблением ресурсов.

В остальном ничего особенного не требовалось.

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

Web API

Так как это уже не первый проект, то для http сервера была взята библиотека fasthttp и на ней быстренько набросаны обработчики с роутером fasthttp-routing. Если совсем в общих чертах, то вышло примерно так:

Но суть не в этом, вообще не знаю зачем я это написал. Реализация, код, все остальное можете посмотреть в сорцах на гите. Здесь я хотел рассказать о проблемах.

Трудности написания

Для начала, так как Go для меня довольно новый язык, то нужно было хорошо разобраться как структуры передаются в функции, когда лучше юзать указатели и что в каких случаях будет работать быстрее. Хорошо помогла статья Effective Go от создателей языка. Тут главное понять что нельзя всегда юзать указатели или вообще никогда их не юзать, нужно использовать их в комплексе.

Вторая фича - выбор публичного IP

Для реализации второй нужной фичи - выбор локального (внешнего) IP для подключения к удаленному серверу - сначала я сделал довольно простой костыль, который устанавливал все подключения с ip заданного в конфиге:

conn потом используется для отправки http запроса удаленному хосту и все чисто и красиво - запросы отправляются с указанного нами IP, казалось бы, что может быть не так. Но все же что-то может быть не так.

Если нужно отправлять запросы не только в интернет, а еще и в локальную сеть, которая подключена через другую сетевую карту, то интернетовым IP достучаться туда не удастся. Если вы думаете что это очень редкая и неадекватная ситуация, то нет, это стандартная реализация VLAN (vRack у OVH) и с этим приходится мириться.
В результате страданий и мыслей было решено написать свой роутер, который бы определял для какой подсети с какого IP адреса нужно отправлять запросы. Формат сего роутера не сложный:

Эта запись означает что на для подсети 172.16.0.0/12 запросы будут отправляться с 172.16.0.1, а для всех остальных с 192.168.0.2. Таким образом с помощью своего роутера проблема была решена. Код роутера можете глянуть тут: bwp/iprouter/iprouter.go.

Пул воркеров

Это даже не проблема, но пришлось погуглить чтобы найти правильный подход к распределению задач на какое-то множество горутин (воркеров).

Решение нашлось в блоге одного из разработчиков Malwarebytes:

Там же и описана примерная реализация. Примерный смысл таков, в пуле есть freeWorkers chan *worker, в который добавляются все свободные воркеры и ждут новых задач. Как только кто-то кидает в пул задачу, пул забирает из канала свободного воркера и кидает ему задачу. После выполнения задачи воркер снова добавляется в канал свободных воркеров и ждет следующей задачи. Конечный код тоже можете посмотреть на гите: bwp/worker.

"Грациозный" рестарт

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

Для этой задачи ребята из Facebook постарались и выложили в паблик пакет как раз для такой перезагрузки.

Опять же, все было красиво и приятно, до тех пор, пока не оказалось, что после перезагрузки меняется pid процесса. Конечно, это очевидно, но не сразу. Чтобы хоть как-то следить за процессом было решено сохранять его pid в файлик сразу же при запуске. Получается что новая версия проги перезаписывала pid сразу и таким образом pid в файлике будет всегда актуальным. Осталось только подружить Supervisor с таким поведением проги, когда её pid меняется.

Единственным очевидным решением был баш скрипт, который бы в фоне запускал прогу и мониторил её pid на предмет живости. Еще нужно было перехватывать всякие сигналы типо SIGTERM, SIGINT, SIGUSR2. В этом и была самая большая проблема. Изначально я почему-то думал что нужно перехватывать все сигналы башем и отправлять в прогу только нужные, но оказалось что как ни старайся их перехватить, сигналы все равно приходят к дочерним процессам. Пару часов я потратил на способы решения этой проблемы, а потом оказалось что её и не нужно решать, никакие сигналы не нужно отменять, а Supervisor почему-то посылает сигналы только для баш скрипта, а на дочернюю прогу они не перекидывались, в отличии от kill -SIG pid, который слал сразу всех.

ыва

Красивые перезагрузки с сохранением stdout, stderr

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

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

Кстати, скрипт тоже можете глянуть где-то тут, на пастбине.

Оптимизации

Тут уже от скуки я начал читать, смотреть, экспериментировать со всем чем только можно было и оказалось что в Go есть охуительный пул для объектов sync.Pool. Чтобы не создавать каждый раз новый объект и не дергать и так слабенький GC, можно очищать ненужные структуры и складывать их в пул, а потом, когда понадобилась структура этого типа, достать её из пула, ну или создать, если в пуле ничего нет. Огромный плюс пула, что объекты в нем могут сами удаляться и не может произойти ситуации, когда в какой-то момент в него попали миллионы объектов и они там останутся висеть до закрытия проги. Триггер удаления там не написан, но логично предположить что при сборке мусора могут удаляться и неиспользуемые объекты из пула.


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