Node.js process.exit() не завершается чисто, и опасности асинхронного файла fs.WriteFile

#javascript #node.js #file

#javascript #node.js #файл

Вопрос:

tl; dr:

Вызов асинхронного fs.writeFile из асинхронных событий (и, возможно, даже из простого старого цикла), а затем process.exit() успешный вызов открывает файлы, но не позволяет сбросить данные в файлы. Обратные вызовы, переданные для writeFile выполнения, не получают возможности для запуска до завершения процесса. Является ли это ожидаемым поведением?

Независимо от того process.exit() , не удается ли выполнить эту очистку, я ставлю под сомнение, должно ли быть обязанностью узла хотя бы попытаться выполнить запись файла в расписание, потому что вполне может быть так, что освобождение огромных буферов зависит от их записи на диск.

Подробные сведения

У меня есть концептуально базовая часть node.js код, который выполняет преобразование в большом файле данных. Это файл данных лидарного датчика, который не должен иметь отношения к делу. Это просто набор данных, который довольно велик из-за природы его существования. Он конструктивно прост. Датчик отправляет свои данные по сети. Моя задача для этого скрипта — создать отдельный файл для каждого вращающегося сканирования. Детали этой логики также не имеют значения.

Основная идея заключается в том, что я использую node_pcap для чтения огромного .pcap файла, используя метод, указанный для выполнения этой задачи node_pcap, который является «автономным режимом».

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

Итак, основная структура программы состоит из нескольких глобальных переменных состояния и одного обратного вызова сеанса pcap. Я инициализирую глобальные переменные, затем назначаю функцию обратного вызова сеансу pcap. Этот обратный packet вызов события выполняет всю работу.

Частью этой работы является запись большого массива файлов данных. Время от времени пакет будет указывать на какое-то условие, которое означает, что я должен перейти к записи в следующий файл данных. Я увеличиваю индекс имени файла данных и вызываю fs.writeFile() снова, чтобы начать запись нового файла. Поскольку я только пишу, кажется естественным позволить узлу решать, когда подходящее время для начала записи.

По сути, оба fs.writeFileSync и fs.writeFile должны в конечном итоге write() вызывать системный вызов ОС для своих соответствующих файлов асинхронным образом. Меня это не беспокоит, потому что я только пишу, поэтому асинхронный характер записи, который может повлиять на определенные шаблоны доступа, для меня не имеет значения, поскольку я не делаю никакого доступа. Единственное отличие заключается в том writeFileSync , что цикл событий узла блокируется до тех пор, пока write() не завершится системный вызов.

По мере выполнения программы, когда я использую writeFile (js-асинхронную версию), создаются сотни моих выходных файлов, но в них не записываются данные. Ни одного. Самый первый файл данных все еще открыт при создании сотого файла данных.

Это концептуально нормально. Причина в том, что узел занят обработкой новых данных и счастливо придерживается растущего числа файловых дескрипторов, к которым он в конечном итоге доберется, чтобы записать данные файлов. Между тем, он также должен хранить в памяти все возможное содержимое файлов. Это в конечном итоге закончится, но давайте на мгновение проигнорируем ограничение размера ОЗУ. Очевидно, что плохая вещь, которая может произойти здесь, — это исчерпание оперативной памяти и сбой программы. Надеюсь, узел будет умным и поймет, что ему просто нужно запланировать некоторые записи в файл, а затем он может освободить кучу буферов…

If I stick a statement in the middle of all this to call process.exit() , I would expect that node will clean up and flush the pending writeFile writes before exiting.

But node does not do this.

Changing to writeFileSync fixes the problem obviously.
Changing and truncating my input data such that process.exit() is not explicitly called also results in the files eventually getting written (and the completion callback given to writeFile to run) at the very end when the input events are done pumping.

This seems to indicate for me that the cleanup is being improperly performed by process.exit() .

Question: Is there some alternative to exiting the event loop cleanly in the middle? Note I had to manually truncate my large input file, because terminating with process.exit() caused all the file writes to not complete.

Это узел v0.10.26 , установленный некоторое время назад в OS X с помощью Homebrew.

Продолжая мой мыслительный процесс, поведение, которое я здесь наблюдаю, ставит под сомнение фундаментальную цель использования writeFile . Предполагается, что это улучшит ситуацию, чтобы иметь возможность гибко записывать мой файл всякий раз, когда узел сочтет это нужным. Однако, по-видимому, если цикл событий узла будет достаточно сильно загружен, то он в основном «отстает» от своей рабочей нагрузки.

Похоже, что в цикле событий есть входящие и исходящие. В этой аналогии поле исходящие представляет временные переменные, содержащие данные, которые я записываю в файлы. Предположение, которое хочет сделать такой ленивый продуктивный программист, как я, состоит в том, что входящие и исходящие — это интерфейсы, которые я могу использовать, и что они гибкие, и что система будет управлять за меня. Однако, если я отправлю входящие сообщения со слишком высокой скоростью, то узел фактически не сможет идти в ногу, и он просто начнет складывать данные в исходящие, не имея времени на их очистку, потому что по той или иной причине планирование таково, что все входящие события должны обрабатываться в первую очередь. Это, в свою очередь, откладывает сборку всего содержимого папки исходящих, и мы довольно быстро истощаем оперативную память системы. Это довольно легко найти труднодоступную ошибку, когда этот шаблон используется в сложной системе. Я рад, что выбрал модульный подход к этому проекту.

Я имею в виду, да, ясно, очевидно, вне всякого сомнения, ответ заключается в том, чтобы использовать writeFileSync , как я делаю, почти каждый раз, когда я записываю файлы с помощью node.

В чем же тогда ценность даже наличия writeFile ? На данный момент я обмениваю потенциальное небольшое увеличение параллельной обработки на повышенную вероятность того, что если (по какой-либо причине) производительность машины снизится (будь то регулирование температуры или планирование на уровне ОС, или я не оплачиваю свои счета IaaS вовремя или по любой другой причине), он может потенциально привести к снежному взрыву памяти?

Возможно, это становится ядром решения действительно довольно сложных проблем, присущих системам потоковой обработки данных, и я не могу реалистично ожидать, что эта модель обработки, основанная на событиях, ускорит и элегантно решит эти проблемы автоматически. Возможно, я должен быть удовлетворен тем, что это дает мне только половину пути к чему-то надежному. Возможно, я просто проецирую на него свои пожелания и что с моей стороны неразумно предполагать, что node нуждается в менее детерминированном «улучшении» планирования своего цикла событий.

Комментарии:

1. writeFile может быть очень полезным в большинстве случаев, даже если в данном случае кажется, что он сломан.

2. Я начинаю задаваться вопросом, чего же я хочу. Может быть, ему просто нужно иметь небольшое предупреждение в документации? Я имею в виду, кажется, что вся реализация writeFile is just setTimeout(function(){fs.writeFileSync(args.slice(-1))},0) справедлива. Я просто действительно не знаю, что делать дальше. С одной стороны, «ошибочное» поведение легко объяснимо. И «правильное» поведение может легко превратиться в непостижимую магию.

3. Что ж, каждый язык высокого уровня реализует промежуточный этап между вызовом .write и фактическим отображением данных на диске: внутренний буфер. Таким образом, поведение является ожидаемым имхо. Вы пробовали использовать .fsync (да, документация отстой, но погуглите)? Он предназначен для обеспечения отображения данных на диске. Если это не сработает, вы можете выполнить ручную очистку, зарегистрировав обработчик в process.on('exit', clb) .

4. То же самое не происходит при использовании fs.open then fs.write . Мне не удалось создать файл размером 0. Это какое-то странное несоответствие между ними.

Ответ №1:

Я не специалист по узлам, но, похоже, вашу проблему можно упростить с помощью потоков. Потоки позволяют приостанавливать и возобновлять работу, а также предоставляют другие удобные функции. Я предлагаю вам взглянуть на главу 9 книги Педро Тейшейры «Профессиональные узлы». Вы можете легко найти онлайн-копию для чтения. Он содержит очень подробные и хорошо объясненные примеры того, как использовать потоки для чтения и записи данных, а также предотвращать потенциальные утечки памяти и потерю данных.

Комментарии:

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