От любви до ненависти с process.send

Всем привет, я создатель https://vatsim-radar.com/ и сегодня я чуть не умер.

В общем, дело такое. Мы — карта виртуальных самолетиков. Недавно нас пропиарили на официальном уровне, и теперь мы обслуживаем тысячи человек ежедневно — в подвале при открытии сайта будет показано, сколько там сидит прямо сейчас («in Radar»).

Ранее мы обновлялись раз в 15 секунд с реальной задержкой от игры в примерно 30 секунд. Это всё дело кэшировалось на Cloudflare, и мы прекрасно жили — пока нам в какой-то момент не подарили апдейты на событиях практически без задержек.

Я решил использовать вебсокеты и обновлять данные сразу по мере поступления. Всё шло плюс минус гладко, пока не произошел тот самый пиар — и мы стали отдавать террабайты данных в сутки. Я сократил трафик до удобоваримых 300ГБ в сутки, но это все равно слишком много — в будущем мы планируем переезд на зарубежный хостинг, а у них у большинства лимиты по трафику, из-за чего мы будем платить больше за трафик, чем за сами серверы.

Я решил, что с этим надо что-то делать, и после безуспешной попытки переноса сокетов в CloudFlare Workers + Durable Objects (все еще слишком высокий прайс), я решил сдаться и отказаться от сокетов в пользу старого доброго кэша CF, но при этом продолжить гонять сильно урезанные данные с очень небольшим кэшем.

Результат говорит сам за себя:

См. что произошло после всплеска в 15 часов

См. что произошло после всплеска в 15 часов

Теперь мы в пике потребляем ~72ГБ в сутки, и с этим уже можно работать. Но, разумеется, отказ от сокетов увеличил задержку в обновлении.

Наш скрипт обработки данных для разгрузки основного потока вынесен в отдельный через fork, и при каждом обновлении отправляет данные родителю через process.send, а тот потом их раздает по апишке.

Скрипт обрабатывал данные 3 секунды, что, с учетом кэша, создавало задержку до 9 секунд, что меня не устраивало. Я решил оптимизировать скрипт, и по итогу добился обработки раз в секунду, что уже было лучше (комментаторам просьба принять погоню за задержкой и секундами как данность).

Задержка уменьшилась в 3 раза, но…

На продуктиве вылез баг. В какой то момент обновления… просто замедлялись. При первоначальном деплое всё было нормально, после чего пользователи видели задержку в минуту… две… десять… И на сервере со страшной силой росло ОЗУ воркера.

Думаю, ну понятно. Создал по пути утечку памяти. Просмотрел весь код, присобачил к воркеру дебагер — не нахожу ровным счетом ничего. Локально всё нормально, задержек нет, после деплоя есть. Стал копать кэш CF, лезть напрямую — то же самое. Бред.

Покрыл код логами, деплойнул на next-стенд. И к моему удивлению…

83edfc2973983833e91521d792625f54.png

Всё идеально. Работает как часы. Никаких задержек. При этом пользователи этих данных не получают, и становится хуже с каждой минутой.

В логах нет ошибок. Ни в ребенке, ни в родителе. У самого воркера память не растет. Процессы не растут. В коде ничего.

И тут до меня доходит.

Если воркер обновляется, а родитель нет, то о чем это говорит?

Что данные до родителя не поступают в принципе.

Работа process.send

Признаюсь честно, за всю свою карьеру я ни разу нормально не использовал спавн дочерних процессов ноды, не считая одноразовых. И уж тем более ни разу не общался через process.send.

Пошел гуглить — может быть, он не выполняется синхронно? Нахожу вот это

10969f105ac2bea1290d649f3884dfb1.png

Ишью тогда исправили, и добавили функциональность, которая позволяет сделать его «асинхронным». Думаю, странно, баг то вообще не про то, ну ладно. Открываю документацию.

183b84e8a684315a2e4e2c032442b427.png

Ничего про синхронность. Нет ни описания того, что делает sendHandle, ни того, нужен ли в 2024 году этому методу callback.

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

Решив сделать поправку на то, что в нашем облаке фиговые диски, плюс там крутится два инстанса на одной тачке, выкатил обертку с promise’ом для process.send на тот же next.

И — оп — утечки прекратились, как будто их и не было.

await new Promise((resolve, reject) => {
  process.send!(JSON.stringify(radarStorage.vatsim), undefined, undefined, err => {
    if (err) return reject(err);
    resolve();
  });
});

Как я умудрился это получить? Почему этого не было раньше?

  1. Отправляю слишком большие данные в process.send

  2. Миграция в облако, где более медленный диск

  3. Ускорение скрипта с 1 до 3 секунд, что перестало давать время на выполнение предыдущего process.send

К чему эта статья?

В процессе гугления я не нашел ничего про данную проблему с process.send.

Проблему весьма сложно создать, если общаться мелкими данными, и не очень часто. Однако, если она возникла, то её крайне тяжело нагуглить и догадаться о причине из документации Node.

Эта ошибка свела меня с ума: я много поменял в коде, и был уверен, что то ли я создаю утечку памяти, то ли что-то не так с новым хостингом, то ли Cloudflare делает что-то не то. Самым странным оказалось именно время последнего обновления: оно постоянно скакало и как будто оставалось в прошлом, при этом весь сервер работал так же, как и прежде.

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

Благодарю за внимание =)

© Habrahabr.ru