Подтвердить что ты не робот

Длительные соединения с Node.js, как уменьшить использование памяти и предотвратить утечку памяти? Также связано с V8 и webkit-devtools

Вот что я пытаюсь сделать: я разрабатываю http-сервер Node.js, который будет удерживать длинные соединения для продвижения цели (сотрудничать с redis) от десятков тысяч мобильных клиентов на одной машине.

Условия тестирования:

1.80GHz*2 CPU/2GB RAM/Unbuntu12.04/Node.js 0.8.16

В первый раз я использовал модуль "экспресс", с которым я мог достичь около 120 тыс. параллельных подключений до того, как будет использоваться swap, что означает, что ОЗУ недостаточно. Затем я переключился на собственный "http" модуль, я получил concurrency до около 160k. Но я понял, что по-прежнему существует слишком много функций, которые мне не нужны в родном модуле http, поэтому я переключил его на собственный "чистый" модуль (это значит, что мне нужно обрабатывать HTTP-протокол самостоятельно, но это нормально). теперь я могу достичь около 250 тыс. параллельных подключений на одну машину.

Вот основная структура моих кодов:

var net = require('net');
var redis = require('redis');

var pendingClients = {};

var redisClient = redis.createClient(26379, 'localhost');
redisClient.on('message', function (channel, message) {
    var client = pendingClients[channel];
    if (client) {
        client.res.write(message);
    }
});

var server = net.createServer(function (socket) {
    var buffer = '';
    socket.setEncoding('utf-8');
    socket.on('data', onData);

    function onData(chunk) {
        buffer += chunk;
        // Parse request data.
        // ...

        if ('I have got all I need') {
            socket.removeListener('data', onData);

            var req = {
                clientId: 'whatever'
            };
            var res = new ServerResponse(socket);
            server.emit('request', req, res);
        }  
    }
});

server.on('request', function (req, res) {
    if (res.socket.destroyed) {            
        return;
    }

    pendingClinets[req.clientId] = {
        res: res
    };

    redisClient.subscribe(req.clientId);

    res.socket.on('error', function (err) {
        console.log(err);
    });

    res.socket.on('close', function () {
        delete pendingClients[req.clientId];

        redisClient.unsubscribe(req.clientId);
    });
});

server.listen(3000);

function ServerResponse(socket) {
    this.socket = socket;
}
ServerResponse.prototype.write = function(data) {
    this.socket.write(data);
}

Наконец, вот мои вопросы:

  • Как уменьшить использование памяти, чтобы увеличить concurrency дальше?

  • Я действительно запутался в том, как рассчитать использование памяти процесса Node.js. Я знаю, что Node.js работает на Chrome V8, есть process.memoryUsage() api, и он возвращает три значения: rss/heapTotal/heapUsed, какая разница между ними, какую часть я должен касаться больше, и каков именно состав памяти, используемой процессом Node.js?

  • Я беспокоился об утечке памяти, хотя я провел несколько тестов и, похоже, не проблема. Есть ли какие-либо вопросы, которые я должен затронуть, или какие-либо рекомендации?

  • Я нашел документ о V8 скрытый класс, как он описал, означает ли это, когда я добавьте свойство с именем clientId к моему глобальному объекту pendingClients, как и мои коды выше, будет создан новый скрытый класс? Доза приведет к утечке памяти?

  • Я использовал webkit-devtools-agent для анализа кучи карты процесса Node.js. Я начал процесс и сделал снимок кучи, затем я отправил ему 10 тыс. Запросов и отключил их позже, после чего я снова сделал снимок кучи. Я использовал сравнение, чтобы увидеть разницу между этими двумя моментальными снимками. Вот что я получил: enter image description here Может ли кто-нибудь объяснить это? Количество и размер (массив)/(скомпилированный код)/(строка)/Command/Array значительно увеличились, что это значит?

ИЗМЕНИТЬ: Как я запустил тест загрузки? 1. Во-первых, я изменил некоторые параметры как на машине сервера, так и на клиентских машинах (для достижения более 60 тыс. concurrency требуется несколько клиентских машин, потому что одна машина имеет только порты 60k + (представлены 16 бит) не более)
1.1. Как сервер, так и клиентские машины, я изменил файловый дескриптор, используя эти команды в оболочке, где будет запущена тестовая программа:

ulimit -Hn 999999
ulimit -Sn 999999

1,2. На машине сервера я также изменил некоторые параметры ядра, связанные с сетью /tcp, наиболее важными из них являются:

net.ipv4.tcp_mem = 786432 1048576 26777216
net.ipv4.tcp_rmem = 4096 16384 33554432
net.ipv4.tcp_wmem = 4096 16384 33554432

1,3. Что касается клиентских машин:

net.ipv4.ip_local_port_range = 1024 65535

2. Во-вторых, я написал пользовательскую симулирующую клиентскую программу с помощью Node.js, так как большинство инструментов тестирования нагрузки, ab, siege и т.д. Предназначены для коротких соединений, но я использую длинные соединения и имею некоторые особые требования.
3. Затем я запустил серверную программу на одной машине и три клиентские программы на остальных трех разделенных машинах.

ИЗМЕНИТЬ: Я достиг 250k одновременных соединений на одной машине (2 ГБ ОЗУ), но оказалось, что это не очень значимо и практично. Потому что, когда соединение подключено, я просто разрешаю подключение, ничего другого. Когда я попытался отправить ответ на них, число concurrency упало до 150 тыс. Вокруг. Как я и вычислил, объем использования памяти на одно подключение составляет около 4 Кбайт, я полагаю, что он связан с net.ipv4.tcp_wmem, который я установил в 4096 16384 33554432, но даже я модифицировал он меньше, ничего не изменилось. Я не могу понять, почему.

ИЗМЕНИТЬ: На самом деле, теперь меня больше интересует, сколько памяти для подключения tcp используется и каков именно состав памяти, используемой одним соединением? Согласно моим данным теста:

150k concurrency потребляет около 1800M RAM (от free -m), а процесс Node.js - около 600M RSS

Тогда я предположил следующее:

  • (1800M - 600M)/150k = 8k, это использование памяти стека ядра TCP одного соединения, оно состоит из двух частей: буфер чтения (4KB) + буфер записи (4KB) (на самом деле это не соответствует моей настройке net.ipv4.tcp_rmem и net.ipv4.tcp_wmem, как система определяет, сколько памяти будет использоваться для этих буферов?)

  • 600M/150k = 4k, это использование памяти Node.js для одного соединения

Я прав? Как уменьшить использование памяти в обоих аспектах?

Если бы я нигде не описал, дайте мне знать, я уточню это! Любые объяснения или рекомендации будут оценены, спасибо!

4b9b3361

Ответ 1

  • Думаю, вам не стоит беспокоиться о дальнейшем уменьшении использования памяти. Из этого показания, которое вы включили, кажется, что вы довольно близки к минимально возможному (я интерпретирую его как находящийся в байтах, который является стандартным, когда единица не указана).

  • Это более глубокий вопрос, чем я могу ответить, но вот что RSS. Куча - это место, где динамически выделяемая память исходит из систем unix, насколько я понимаю. Таким образом, общее количество кучи похоже на то, что все это будет выделено в куче для вашего использования, тогда как используется куча, сколько из того, что вы использовали.

  • Использование вашей памяти неплохое, и, похоже, у вас нет утечки. Я бы не волновался. =]

  • Не знаю.

  • Этот снимок кажется разумным. Я ожидаю, что некоторые объекты, созданные из всплеска запросов, были собраны мусором, а другие - нет. Вы видите там ничего более 10k объектов, и большинство из этих объектов довольно малы. Я называю это хорошим.

Что еще более важно, интересно, как вы загружаете это тестирование. Я пытался сделать массовое нагрузочное тестирование, как это раньше, и большинство инструментов просто не могут сгенерировать такую ​​нагрузку на Linux, из-за ограничений на количество дескрипторов открытых файлов (как правило, около тысячи за процесс по умолчанию). Также, как только сокет используется, он не сразу доступен для использования снова. Как я помню, для того, чтобы снова использовать его, требуется значительная часть минуты. Между этим и тем фактом, что я обычно видел ограниченный системный ограничитель дескриптора файла, установленный где-то менее 100 тыс., Я не уверен, что можно получить такую ​​большую нагрузку на неизмененном поле или создать его на одном ящике. Поскольку вы не упомянули о каких-либо таких шагах, я думаю, вам также может потребоваться изучить ваши нагрузочные тесты, чтобы убедиться, что они делают то, что вы думаете.

Ответ 2

Всего несколько заметок:

Вам нужно обернуть res в объект {res: res}, вы можете просто назначить его напрямую

pendingClinets[req.clientId] = res;

ИЗМЕНИТЬ еще одну микро-оптимизацию, которая могла бы помочь

server.emit('request', req, res);

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

res['clientId'] = 'whatever';
server.emit('request', res);

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