Решение проблемы «EMFILE, too many open files»

в 13:35, , рубрики: async, node.js, метки: ,

Добрый день.
Складывать в отдельный лог-файл сообщения об ошибках 404 — настолько обычное дело, что, казалось бы, и сложностей с этим быть не может. По крайней мере, так думал я, пока за одну секунду клиент не запросил полторы тысячи отсутствующих файлов.
Node.js-сервер выругался «EMFILE, too many open files» и отключился.
(В дебаг-режиме я специально не отлавливаю ошибки, попавшие в основной цикл)

Итак, что представляла собой ф-ия сохранения в файл:

        log: function (filename, text) {
// запись в конец файла filename строки now() + text
            var s = utils.digitime() + ' ' + text + 'n';
// utils.digitime() - возвращает текущую дату-время в виде строки в формате ДД.ММ.ГГГГ чч:мм:сс
            fs.open(LOG_PATH + filename, "a", 0x1a4, function (error, file_handle) {
                if (!error) {
                    fs.write(file_handle, s, null, 'utf8', function (err) {
                        if (err) {
                            console.log(ERR_UTILS_FILE_WRITE + filename + ' ' + err);
                        }
                        fs.closeSync(file_handle);
                    });
                }
                else {
                    console.log(ERR_UTILS_FILE_OPEN + filename + ' ' + error);
                }
            });
        }

Ну то есть всё прямо «в лоб» — открываем, записываем, если какие ошибки — выводим на консоль. Однако, как говорилось выше, если вызывать её слишком часто, файлы просто не успевают закрываться. В линуксе, например, упираемся в значение kern.maxfiles со всеми неприятными вытекающими.

Самое интересное

Для решения я выбрал библиотеку async, без которой уже не представляю жизни.
Саму функцию log перенёс в «private» область видимости модуля, переименовав в __log и слегка модифицировав: теперь у неё появился коллбек:

__log = function (filename, text) {
        return function (callback) {
            var s = utils.digitime() + ' ' + text + 'n';

            fs.open(LOG_PATH + filename, "a", 0x1a4, function (error, file_handle) {
                if (!error) {
                    fs.write(file_handle, s, null, 'utf8', function (err) {
                        if (err) {
                            console.log(ERR_UTILS_FILE_WRITE + filename + ' ' + err);
                        }
                        fs.closeSync(file_handle);
                        callback();
                    });
                }
                else {
                    console.log(ERR_UTILS_FILE_OPEN + filename + ' ' + error);
                    callback();
                }
            });
        };
    };

Самое главное: в private создаём переменную __writeQueue:

    __writeQueue = async.queue(function (task, callback) {
        task(callback);
    }, MAX_OPEN_FILES);

а в public-части модуля log выглядит теперь совсем просто:

        log: function (filename, text) {
            __writeQueue.push(__log(filename, text));
        },

И всё?!

Именно. Другие модули по-прежнему вызывают эту ф-ию примерно как

function errorNotFound (req, res) {
    utils.log(LOG_404, '' + req.method + 't' + req.url + 't(' + (accepts) + ')t requested from ' + utils.getClientAddress(req));
..

и при этом никаких ошибок.

Механизм прост: задаём константе MAX_OPEN_FILES разумное число, меньшее чем максимально допустимое количество открытых файловых дескрипторов (например 256). Далее все попытки записи будут распараллеливаться, но только до тех пор, пока их количество не достигнет указанного предела. Все свежеприбывающие будут становиться в очередь и запускаться только после завершения предыдущих попыток (помните, мы добавляли callback? Как раз для этого!).

Надеюсь, данная статья поможет решить эту проблему тем, кто с ней столкнулся. Либо — что ещё лучше — послужит превентивной мерой.
Удачи.

Автор: Keenest

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js