Оптимизация производительности Node.js, о которой вы не знали

Node.js Апплет WeChat
Оптимизация производительности Node.js, о которой вы не знали
«Когда я впервые узнал, что собираюсь написать эту статью, я фактически отказался, потому что думал, что нельзя просить меня написать ее сразу, мне нужно сделать это правильно, написать несколько клише и добавить много трюков, кажется, производительность Node.js в Duang~ повысилась, тогда читатели определенно будут ругать меня, Node.js вообще не оптимизирует производительность, это все фальшивка». ------ Старк · Джеки Чан Ван

1. Используйте последнюю версию Node.js

Легко повысить производительность, просто обновив версию Node.js, потому что почти любая новая версия Node.js будет работать лучше старой. Почему?

Повышение производительности каждой версии Node.js в основном связано с двумя аспектами:

  • Обновление версии V8;
  • Обновление оптимизации внутреннего кода Node.js.

Например, в последней версии V8 7.1 был оптимизирован escape-анализ замыканий в некоторых случаях, а также улучшена производительность некоторых методов Array:

Внутренний код Node.js, с обновлением версии, тоже будет иметь явную оптимизацию.Например,следующая картинкаrequireПроизводительность меняется при обновлении версии Node.js:

Каждый PR, отправленный в Node.js, будет рассмотрен во время проверки, чтобы увидеть, не ухудшит ли он текущую производительность. Существует также специальная группа по тестированию для отслеживания изменений производительности, вы можете увидеть изменения производительности для каждой версии Node.js здесь:

Node.js Benchmarking

Таким образом, вы можете быть уверены в производительности новой версии Node.js, и если вы обнаружите какое-либо снижение производительности в новой версии, вы можете сообщить о проблеме.

Как выбрать версию Node.js?

Вот научно-популярная версия Node.js:

  • Версии Node.js в основном делятся на Current и LTS;
  • Текущая — это последняя версия Node.js, которая все еще находится в разработке;
  • LTS — это стабильная версия с долгосрочным обслуживанием;
  • Node.js будет выпускать обновление основной версии каждые шесть месяцев (в апреле и октябре каждого года), и основная версия будет содержать некоторые несовместимые обновления;
  • Версия, выпускаемая в апреле каждого года (номер версии — четное число, например, v10), является версией LTS, то есть версией с долгосрочной поддержкой.Сообщество будет продолжать поддерживать ее в течение 18 + 12 месяцев (активная + Maintenancece LTS) начиная с октября года выпуска;
  • Версии, выпускаемые каждый октябрь (версии с нечетными номерами, такие как v11 now), имеют только 8-месячный период обслуживания.

Например, прямо сейчас (ноябрь 2018 г.) версия Node.js Current — v11, а версии LTS — v10 и v8. Старая версия v6 находится на обслуживании LTS и больше не будет поддерживаться с апреля следующего года. Версия v9, выпущенная в октябре прошлого года, завершила техническое обслуживание в июне этого года.

Для производственных сред Node.js официально рекомендует использовать последнюю версию LTS, которая теперь является v10.13.0.


2. Используйтеfast-json-stringifyУскорить сериализацию JSON

В JavaScript очень удобно генерировать строки JSON:

const json = JSON.stringify(obj)

Но мало кто подумает, что здесь тоже есть место для оптимизации производительности, то есть использоватьJSON Schemaдля ускорения сериализации.

При сериализации JSON нам нужно определить большое количество типов полей, например, для строкового типа нам нужно добавить с обеих сторон", для типов массива нам нужно пройти по массиву, сериализовать каждый объект и использовать,разделены, затем добавлены с обеих сторон[а также], и так далее.

ноЕсли тип каждого поля заранее известен через схему, то нет необходимости проходить и идентифицировать тип поля., и соответствующие поля могут быть сериализованы напрямую, что значительно снижает вычислительные затраты, т.fast-json-stringfyпринцип.

В зависимости от бенчмарков в проекте, в некоторых случаях его можно даже сравниватьJSON.stringifyПочти в 10 раз быстрее!

Простой пример:

const fastJson = require('fast-json-stringify')
const stringify = fastJson({
    title: 'Example Schema',
    type: 'object',
    properties: {
        name: { type: 'string' },
        age: { type: 'integer' },
        books: {
            type: 'array',
            items: {
                type: 'string',
                uniqueItems: true
            }
        }
    }
})

console.log(stringify({
    name: 'Starkwang',
    age: 23,
    books: ['C++ Primier', '響け!ユーフォニアム~']
}))
//=> {"name":"Starkwang","age":23,"books":["C++ Primier","響け!ユーフォニアム~"]}

В промежуточном бизнесе Node.js обычно много данных в JSON, и структура этих JSON очень похожа (если вы используете TypeScript, тем более), этот сценарий очень подходит для использования JSON Schema для оптимизации .


3. Улучшить производительность промисов

Промисы — это панацея от ада вложенности коллбеков, тем более, что async/await стал полностью популярным, их комбинация, несомненно, стала окончательным решением для асинхронного программирования на JavaScript, и теперь большое количество проектов начали использовать этот паттерн.

Но за элегантным синтаксисом также стоит снижение производительности, мы можем использоватьСуществующий проект бенчмаркинга на githubСделайте тест, и вот результаты теста:

file                               time(ms)  memory(MB)
callbacks-baseline.js                   380       70.83
promises-bluebird.js                    554       97.23
promises-bluebird-generator.js          585       97.05
async-bluebird.js                       593      105.43
promises-es2015-util.promisify.js      1203      219.04
promises-es2015-native.js              1257      227.03
async-es2017-native.js                 1312      231.08
async-es2017-util.promisify.js         1550      228.74

Platform info:
Darwin 18.0.0 x64
Node.JS 11.1.0
V8 7.0.276.32-node.7
Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz × 4

Из результатов видно, что производительность нативного async/await + Promise намного хуже, чем callback, а использование памяти также намного выше. Для проектов промежуточного ПО с большим количеством асинхронной логики здесь нельзя игнорировать накладные расходы на производительность.

Для сравнения можно обнаружить, что потеря производительности в основном связана с реализацией самого объекта Promise.Промис, реализованный V8 изначально, намного медленнее, чем библиотека Promise, реализованная третьей стороной, такой как bluebird. А синтаксис async/await не сильно снижает производительность.

Таким образом, для большого количества асинхронной логики, легковесных проектов промежуточного программного обеспечения вы можетеЗамените глобальный промис реализацией bluebird в коде.:

global.Promise = require('bluebird');

4. Пишите асинхронный код правильно

После использования async/await асинхронный код проекта будет выглядеть очень хорошо:

const foo = await doSomethingAsync();
const bar = await doSomethingElseAsync();

Так что иногда мы также забываем о других способностях, которые нам подарили, например,Promise.all()Параллельная возможность:

// bad
async function getUserInfo(id) {
    const profile = await getUserProfile(id);
    const repo = await getUserRepo(id)
    return { profile, repo }
}

// good
async function getUserInfo(id) {
    const [profile, repo] = await Promise.all([
        getUserProfile(id),
        getUserRepo(id)
    ])
    return { profile, repo }
}

Также такие какPromise.any()(Этого метода нет в стандарте ES6 Promise, вы также можете использовать стандартныйPromise.race()вместо этого), мы можем легко добиться более надежного и быстрого вызова с его помощью:

async function getServiceIP(name) {
    // 从 DNS 和 ZooKeeper 获取服务 IP,哪个先成功返回用哪个
    // 与 Promise.race 不同的是,这里只有当两个调用都 reject 时,才会抛出错误
    return await Promise.any([
        getIPFromDNS(name),
        getIPFromZooKeeper(name)
    ])
}

5. Оптимизируйте сборщик мусора V8

Подобных статей о механизме сборки мусора в V8 было много, поэтому повторяться здесь не буду. Рекомендуется две статьи:

Интерпретация журнала сборщика мусора V8 (1): фон приложения Node.js и основы сборщика мусора Интерпретация журнала сборки мусора V8 (2): разделение памяти внутри и снаружи кучи и алгоритм сборки мусора

Когда мы ежедневно разрабатываем код, легче наступить на следующие ямы:

Яма 1: Использование больших объектов в качестве кеша приводит к более медленной сборке мусора в Старом пространстве

Пример:

const cache = {}
async function getUserInfo(id) {
    if (!cache[id]) {
        cache[id] = await getUserInfoFromDatabase(id)
    }
    return cache[id]
}

Здесь мы используем переменнуюcacheВ качестве кеша для ускорения запроса информации о пользователе, после многих запросов,cacheОбъекты войдут в старое поколение и станут чрезвычайно большими, а старое поколение GCed с использованием трехцветной маркировки + DFS.Большой объект напрямую приведет к увеличению времени, затрачиваемого на GC (а также есть риск утечек памяти ).

Решение:

  • Используйте внешний кеш, такой как Redis, на самом деле база данных в памяти, такая как Redis, идеально подходит для этого сценария;
  • Ограничьте размер локально кэшированных объектов, например, используя такие механизмы, как FIFO, TTL и т. д., для очистки кэша в объекте.

Яма 2: Недостаток места в новом поколении, что приводит к частому сбору мусора

Это отверстие будет более скрытым.

Память по умолчанию, выделенная Node.js для нового поколения, составляет 64 МБ (64-разрядная машина, то же самое ниже), но поскольку сборщик мусора нового поколения использует алгоритм очистки, фактическая память, которую можно использовать, составляет только половину, то есть 32 МБ.

Когда бизнес-код часто генерирует большое количество мелких объектов, это пространство может быть легко заполнено, вызывая сборку мусора. Хотя сборщик мусора молодого поколения намного быстрее, чем сборщик мусора старого поколения, частый сборщик мусора все же может сильно повлиять на производительность. В крайних случаях GC может занимать даже около 30% всего вычислительного времени.

Решение состоит в том, чтобы изменить лимит памяти нового поколения, чтобы уменьшить количество сборщиков мусора при запуске Node.js:

node --max-semi-space-size=128 app.js

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

По мере увеличения памяти количество GC уменьшается, но время, необходимое для каждого GC, также увеличивается, поэтому чем больше, тем лучше Конкретное значение необходимо протестировать под давлением в бизнес-профиле, чтобы определить, какой объем памяти нового поколения лучше всего выделять.

А вообще, по опыту,Разумно выделить 64 МБ или 128 МБ.


6. Используйте поток правильно

Поток — одна из самых основных концепций Node.js.Большинство связанных с вводом-выводом модулей в Node.js, таких как http, net, fs и repl, построены поверх различных потоков.

Следующий классический пример должен быть известен большинству людей.Для больших файлов нам не нужно полностью читать их в память, а использовать Stream для потоковой передачи:

const http = require('http');
const fs = require('fs');

// bad
http.createServer(function (req, res) {
    fs.readFile(__dirname + '/data.txt', function (err, data) {
        res.end(data);
    });
});

// good
http.createServer(function (req, res) {
    const stream = fs.createReadStream(__dirname + '/data.txt');
    stream.pipe(res);
});

Разумное использование Stream в бизнес-коде, конечно, может значительно повысить производительность, но мы, скорее всего, проигнорируем это в реальном бизнесе.Например, для проектов, использующих рендеринг на стороне сервера React, мы можем использоватьrenderToNodeStream:

const ReactDOMServer require('react-dom/server')
const http = require('http')
const fs = require('fs')
const app = require('./app')

// bad
const server = http.createServer((req, res) => {
    const body = ReactDOMServer.renderToString(app)
    res.end(body)
});

// good
const server = http.createServer(function (req, res) {
    const stream = ReactDOMServer.renderToNodeStream(app)
    stream.pipe(res)
})

server.listen(8000)

Управление потоками с помощью конвейеров

В старые времена Node.js работа с потоками была очень громоздкой, например:

source.pipe(a).pipe(b).pipe(c).pipe(dest)

Как только любой поток в источниках, a, b, c и dest выйдет из строя или закроется, весь конвейер остановится.В это время нам нужно вручную уничтожить все потоки, что очень проблематично на уровне кода.

Так появилось сообществоpumpТакая библиотека для автоматического управления уничтожением потоков. А Node.js v10.0 добавил новую функцию:stream.pipeline, можно заменить pump помогает нам лучше управлять потоками.

Официальный пример:

const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');

pipeline(
    fs.createReadStream('archive.tar'),
    zlib.createGzip(),
    fs.createWriteStream('archive.tar.gz'),
    (err) => {
        if (err) {
            console.error('Pipeline failed', err);
        } else {
            console.log('Pipeline succeeded');
        }
    }
);

Реализуйте свой собственный высокопроизводительный поток

В своем бизнесе вы также можете самостоятельно реализовать поток, доступный для чтения, записи или двунаправленный.Вы можете обратиться к документации:

Хотя Stream великолепен, самостоятельная реализация Stream также может иметь скрытые проблемы с производительностью, такие как:

class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            this.push(chunk);
        }
    }
}

когда мы звонимnew MyReadable().pipe(xxx), будуgetNextChunk()Полученные куски выталкиваются до конца чтения. Однако если скорость обработки следующего шага конвейера в это время низкая, это приведет к накоплению данных в памяти, что приведет к большему использованию памяти и снижению скорости GC.

Правильный подход должен быть, по мнениюthis.push()Возвращаемое значение выбирает правильное поведение, когда возвращаемое значениеfalse, что указывает на то, что накопленные чанки на данный момент заполнены, и чтение следует остановить.

class MyReadable extends Readable {
    _read(size) {
        while (null !== (chunk = getNextChunk())) {
            if (!this.push(chunk)) {
                return false  
            }
        }
    }
}

Эта проблема подробно описана в официальном стиле Node.js:

Backpressuring in Streams | Node.js

7. Обязательно ли расширение C++ быстрее, чем JavaScript?

Node.js очень подходит для приложений с интенсивным вводом-выводом, а для предприятий с интенсивными вычислениями многие люди подумают о написании дополнений C++ для оптимизации производительности. Но на самом деле расширения C++ не панацея, и производительность V8 не так плоха, как представлялось.

Например, в сентябре этого года я поставил Node.jsnet.isIPv6()Переход с реализаций C++ на JS привел к улучшению производительности в диапазоне от 10% до 250% для большинства тестовых случаев (Конкретный PR можно найти здесь).

JavaScript проходит быстрее, чем расширения C ++ на V8. Это происходит в основном в сценариях, связанных с строками и регулярными выражениями, поскольку регулярные экспрессии, используемые внутри V8,irregexp, этот движок регулярных выражений лучше, чемboostВстроенный двигатель (boost::regex) гораздо быстрее.

Также стоит отметить, что C++-расширение Node.js может потреблять много производительности при выполнении преобразования типов, и если не обращать внимания на детали кода C++, производительность будет сильно снижена.

Вот статья, сравнивающая производительность C++ и JS по одному и тому же алгоритму (требует преодоления стены):

How to get a performance boost using Node.js native addons

Примечательным выводом является то, что после того, как код C++ преобразует строку в параметре (String::Utf8ValueПеревести вstd::string), производительность и вполовину не уступает JS-реализации. Более высокая производительность, чем у JS, была достигнута только после использования переноса типов, предоставляемого NAN.

Другими словами, вопрос о том, является ли C++ более эффективным, чем JavaScript, необходимо анализировать в каждом конкретном случае, а в некоторых случаях расширения C++ не обязательно могут быть более эффективными, чем собственный JavaScript. Если вы не уверены в своем уровне C++, то рекомендуется реализовать его на JavaScript, потому что производительность V8 намного лучше, чем вы думаете.


8. Используйте node-clinic для быстрого обнаружения проблем с производительностью

Сказав все это, есть ли что-нибудь из коробки, которое работает за пять минут? Конечно, есть.

node-clinicдаNearFormИнструмент диагностики производительности Node.js с открытым исходным кодом, который может очень быстро обнаруживать проблемы с производительностью.

npm i -g clinic
npm i -g autocannon

При использовании сначала запустите сервисный процесс:

clinic doctor -- node server.js

Затем мы можем запустить стресс-тест с помощью любого инструмента для стресс-тестирования, например, используя тот же авторскийautocannon(Конечно, вы также можете использовать такие инструменты, как ab и curl, для проверки давления.):

autocannon http://localhost:3000

После завершения измерения давления мы можем ctrl+c закрыть процесс, открытый клиникой, и отчет будет сгенерирован автоматически. Например, ниже приведен отчет о производительности одной из наших служб промежуточного программного обеспечения:

Из кривой использования ЦП мы видим, что узким местом в производительности этой службы промежуточного программного обеспечения является не ее собственный внутренний расчет, а низкая скорость ввода-вывода. Clinic также сообщает нам выше, что была обнаружена потенциальная проблема ввода-вывода.

Ниже мы используемclinic bubbleprofЧтобы обнаружить проблемы ввода-вывода:

clinic bubbleprof -- node server.js

После повторного запуска стресс-теста мы получили новый отчет:

В этом отчете мы видим,http.ServerЗа все время работы программа 96% времени находится в состоянии pending.Нажав на нее мы обнаружим что в стеке вызовов большое количество пустых фреймов.То есть из-за ограничения сети I/O, CPU имеет много холостого хода, что очень часто встречается в бизнесе middleware, а также указывает на то, что направление оптимизации не внутри сервиса, а в соответствующей скорости шлюза сервера и зависимых сервисов.

интересно как читатьclinic bubbleprofСгенерированный отчет можно увидеть здесь:клиника JS.org/bubble Prof/…

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

Мы добавили «разрушающий» код с интенсивным использованием ЦП, например, 100 миллионов раз бездействия к некоторому промежуточному программному обеспечению:

function sleep() {
    let n = 0
    while (n++ < 10e7) {
        empty()
    }
}
function empty() { }

module.exports = (ctx, next) => {
    sleep()
    // ......
    return next()
}

затем используйтеclinic doctor, повторите описанные выше шаги, чтобы создать отчет о производительности:

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

Для таких приложений мы можем продолжать использоватьclinic flameчтобы точно определить, где происходят интенсивные вычисления:

clinic flame -- node app.js

После стресс-теста мы получили флейм-график (здесь количество холостых оборотов уменьшено до 1 миллиона, чтобы флейм-график не выглядел таким экстремальным):

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


Рекламная площадь

Мы являемся командой Tencent Cloud TCB и одной из немногих команд разработчиков в Tencent, использующих Node.js и промежуточное ПО Golang в качестве ядра. Текущие основные облачные продукты:

В настоящее время команда находится в периоде бурного развития, и бизнес быстро расширяется, некоторые должности необходимо набрать:

1. Инженер по разработке облачных сервисов малых программ (Шэньчжэнь)

Рабочие обязанности

  • Отвечает за внутреннюю разработку продуктов облачной разработки Mini Program;
  • Отвечает за разработку и поддержку архитектуры облачной системы разработки.

профессиональные требования

  • Степень бакалавра или выше, специальность компьютерная, опыт работы более 2-х лет;
  • Более 2-х лет опыта разработки на C/C++, Golang и Python под Unix/Linux;
  • Знаком с принципами и общими инструментами операционной системы Unix/Linux;
  • Всеобъемлющая и надежная структура знаний о программном обеспечении (операционные системы, программная инженерия, шаблоны проектирования, структуры данных, системы баз данных, сетевая безопасность);
  • Обладать хорошими аналитическими способностями и навыками решения проблем, способными самостоятельно решать задачи и иметь возможность систематически контролировать прогресс;
  • Прилежность, сильное чувство ответственности, внимательное и гибкое мышление, хорошие навыки внешней коммуникации и работы в команде; приветствуется опыт разработки крупных систем.

2. Планирование разработки облачных продуктов для небольших программ (Шэньчжэнь)

Рабочие обязанности

  • Отвечает за планирование связанных облачных продуктов, разработанных Tencent Mini Program Cloud;
  • Отвечает за дизайн продукта возможностей облачной платформы разработки апплетов, анализирует и исследует требования, а также заполняет документацию по продукту.

профессиональные требования

  • Степень бакалавра и выше, стаж работы более 3-х лет;
  • Сильные исполнительские способности, дизайн продукта и коммерческая упаковка, понимание продукта, сильная работа в команде и коммуникативные навыки, активное мышление, способность к обучению и сильное сопротивление давлению;
  • приветствуется опыт проектирования продуктов SaaS, PaaS;
  • Те, кто с небольшим разработкой программы или опытом в дизайне и эксплуатации продукта B / C являются предпочтительными.