Всем привет, я создатель https://vatsim-radar.com/ и сегодня я чуть не умер.
В общем, дело такое. Мы - карта виртуальных самолетиков. Недавно нас пропиарили на официальном уровне, и теперь мы обслуживаем тысячи человек ежедневно - в подвале при открытии сайта будет показано, сколько там сидит прямо сейчас ("in Radar").
Ранее мы обновлялись раз в 15 секунд с реальной задержкой от игры в примерно 30 секунд. Это всё дело кэшировалось на Cloudflare, и мы прекрасно жили - пока нам в какой-то момент не подарили апдейты на событиях практически без задержек.
Я решил использовать вебсокеты и обновлять данные сразу по мере поступления. Всё шло плюс минус гладко, пока не произошел тот самый пиар - и мы стали отдавать террабайты данных в сутки. Я сократил трафик до удобоваримых 300ГБ в сутки, но это все равно слишком много - в будущем мы планируем переезд на зарубежный хостинг, а у них у большинства лимиты по трафику, из-за чего мы будем платить больше за трафик, чем за сами серверы.
Я решил, что с этим надо что-то делать, и после безуспешной попытки переноса сокетов в CloudFlare Workers + Durable Objects (все еще слишком высокий прайс), я решил сдаться и отказаться от сокетов в пользу старого доброго кэша CF, но при этом продолжить гонять сильно урезанные данные с очень небольшим кэшем.
Результат говорит сам за себя:
Теперь мы в пике потребляем ~72ГБ в сутки, и с этим уже можно работать. Но, разумеется, отказ от сокетов увеличил задержку в обновлении.
Наш скрипт обработки данных для разгрузки основного потока вынесен в отдельный через fork, и при каждом обновлении отправляет данные родителю через process.send, а тот потом их раздает по апишке.
Скрипт обрабатывал данные 3 секунды, что, с учетом кэша, создавало задержку до 9 секунд, что меня не устраивало. Я решил оптимизировать скрипт, и по итогу добился обработки раз в секунду, что уже было лучше (комментаторам просьба принять погоню за задержкой и секундами как данность).
Задержка уменьшилась в 3 раза, но...
На продуктиве вылез баг. В какой то момент обновления... просто замедлялись. При первоначальном деплое всё было нормально, после чего пользователи видели задержку в минуту... две... десять... И на сервере со страшной силой росло ОЗУ воркера.
Думаю, ну понятно. Создал по пути утечку памяти. Просмотрел весь код, присобачил к воркеру дебагер - не нахожу ровным счетом ничего. Локально всё нормально, задержек нет, после деплоя есть. Стал копать кэш CF, лезть напрямую - то же самое. Бред.
Покрыл код логами, деплойнул на next-стенд. И к моему удивлению...
Всё идеально. Работает как часы. Никаких задержек. При этом пользователи этих данных не получают, и становится хуже с каждой минутой.
В логах нет ошибок. Ни в ребенке, ни в родителе. У самого воркера память не растет. Процессы не растут. В коде ничего.
И тут до меня доходит.
Если воркер обновляется, а родитель нет, то о чем это говорит?
Что данные до родителя не поступают в принципе.
Работа process.send
Признаюсь честно, за всю свою карьеру я ни разу нормально не использовал спавн дочерних процессов ноды, не считая одноразовых. И уж тем более ни разу не общался через process.send.
Пошел гуглить - может быть, он не выполняется синхронно? Нахожу вот это
Ишью тогда исправили, и добавили функциональность, которая позволяет сделать его "асинхронным". Думаю, странно, баг то вообще не про то, ну ладно. Открываю документацию.
Ничего про синхронность. Нет ни описания того, что делает sendHandle, ни того, нужен ли в 2024 году этому методу callback.
Решил проверить работу несчастного колбека. Смотрю локально, и действительно: скрипт завершается раньше, чем срабатывает callback, но всего на 10 миллисекунд.
Решив сделать поправку на то, что в нашем облаке фиговые диски, плюс там крутится два инстанса на одной тачке, выкатил обертку с promise'ом для process.send на тот же next.
И - оп - утечки прекратились, как будто их и не было.
await new Promise<void>((resolve, reject) => {
process.send!(JSON.stringify(radarStorage.vatsim), undefined, undefined, err => {
if (err) return reject(err);
resolve();
});
});
Как я умудрился это получить? Почему этого не было раньше?
Отправляю слишком большие данные в process.send
Миграция в облако, где более медленный диск
Ускорение скрипта с 1 до 3 секунд, что перестало давать время на выполнение предыдущего process.send
К чему эта статья?
В процессе гугления я не нашел ничего про данную проблему с process.send.
Проблему весьма сложно создать, если общаться мелкими данными, и не очень часто. Однако, если она возникла, то её крайне тяжело нагуглить и догадаться о причине из документации Node.
Эта ошибка свела меня с ума: я много поменял в коде, и был уверен, что то ли я создаю утечку памяти, то ли что-то не так с новым хостингом, то ли Cloudflare делает что-то не то. Самым странным оказалось именно время последнего обновления: оно постоянно скакало и как будто оставалось в прошлом, при этом весь сервер работал так же, как и прежде.
Извиняюсь за столь сумбурную статью про проблему всего лишь одного метода, и, надеюсь, она поможет кому-то, кто будет шерстить интернет в поисках его странной проблемы.
Благодарю за внимание =)
Комментарии (14)
Driver86
20.08.2024 22:02+1>сегодня я чуть не умер
Сначала подумал, что автор уничтожил БД на проде, с соответствующими последствиями от начальства.
daniluk4000 Автор
20.08.2024 22:02Это другая история :D
Тут, возможно, излишний драматизм, но я правда в некотором отчаянии сидел, глядя на растущую память, жалобы пользователей, и очень странные, непонятные симптомы проблемы
Vitalics
20.08.2024 22:02Насколько я помню, у cloudflare свой рантаим для js, он отличается от годы(хотя апи такое же). Проверьте, точно ли у вас года запускается, или это косяк cloudflare
daniluk4000 Автор
20.08.2024 22:02+1Там как минимум урезанное апи, но не подошло не поэтому. Я смог завести свои сокеты, но дешевое не стало(
anonymous
20.08.2024 22:02НЛО прилетело и опубликовало эту надпись здесь
daniluk4000 Автор
20.08.2024 22:02Вы о чем? Чем хабр не лучше?
В данный момент мы не используем внешних скриптов, вообще - не считая антибота Cloudflare
zoto_ff
20.08.2024 22:02Рекомендую пересесть с ноды на Bun. RPS станет больше, а потребление памяти меньше
daniluk4000 Автор
20.08.2024 22:02Манит попробовать уже пару месяцев...
zoto_ff
20.08.2024 22:02самое время - бан уже очень стабильный, спокойно держу на нём прод практически без происшествий
slonopotamus
20.08.2024 22:02Мы - карта виртуальных самолетиков.
Ранее мы обновлялись раз в 15 секунд с реальной задержкой от игры в примерно 30 секунд.
Даже прочитав всю статью два раза и сходив на ваш сайт я так и не смог понять о чём вообще всё это.
daniluk4000 Автор
20.08.2024 22:02Вы не поняли суть статьи или суть проекта? Эта информация просто о том, зачем нам понадобился этот воркер
Сервис показывает самолеты в виртуальной сети полетов VATSIM. В ней реальные люди собираются и летают, их обслуживают диспетчера, которые также люди, и это всё симулирует реальные процессы, принятые в мире.
Низкая задержка нужна в том числе для того, чтобы, если диспетчера онлайн нет, смотреть самолеты в округе и быть в курсе происходящего - но и с диспетчерами пользы много от низкой задержки.
slonopotamus
20.08.2024 22:02Сервис показывает самолеты в виртуальной сети полетов VATSIM. В ней реальные люди собираются и летают, их обслуживают диспетчера, которые также люди, и это всё симулирует реальные процессы, принятые в мире.
Во, теперь понял)
19Zb84
А почему worker_threads не используете ?
https://nodejs.org/api/worker_threads.html
daniluk4000 Автор
Мне нужно выполнять ровно один скриптик, эта штука кажется немного про другое