Иджун

вступление

В предыдущей главе используется один реальный случай стресс-тестирования, чтобы объяснить, как использовать файлы профиля ЦП, сгенерированные в Node.js Performance Platform, для реализации оптимизации производительности во время стресс-тестирования. По сравнению с проблемами ЦП проблемы с памятью из-за неправильного использования в приложениях Node.js являются катастрофическими. Кроме того, эти проблемы трудно воспроизвести с помощью локальных стресс-тестов, поскольку они обычно возникают в производственной среде. На самом деле, многие разработчики Node.js не достаточно смелы, чтобы использовать Node.js в серверной части, в основном из-за проблем с памятью.

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

Это руководство впервые опубликовано на GitHub по адресу https://github.com/aliyun-node/Node.js-Troubleshooting-Guide и будет одновременно обновляться для сообщества.

Примечание. На момент написания статьи продукт Node.js Performance Platform был доступен только для внутренних (материковый Китай) учетных записей.

Минимальное воспроизведение кода

Поскольку утечки памяти отличаются от проблемы с высокой загрузкой ЦП, может быть более интуитивно понятно объединить проблемный код и описание устранения неполадок. Поэтому начало этой главы содержит минимум фрагментов кода. Вы можете узнать больше, запустив код и объединив этапы анализа, описанные ниже. Образец основан на Egg.js:

'use strict'; 
const Controller = require('egg'). Controller;
const DEFAULT_OPTIONS = { logger: console };
class SomeClient {
  constructor(options) {
    this.options = options;
  }
  async fetchSomething() {
    return this.options.key;
  }
}
const clients = {};
function getClient(options) {
  if (! clients[options.key]) {
    clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options));
  }
  return clients[options.key];
}
class MemoryController extends Controller {
  async index() {
    const { ctx } = this;
    const options = { ctx, key: Math.random().toString(16).slice(2) };
    const data = await getClient(options).fetchSomething();
    ctx.body = data;
  }
}
module.exports = MemoryController;

Добавьте маршрутизатор запросов Post в app/router.js:

router.post('/memory', controller.memory.index);

Ниже приведена демонстрация проблемного почтового запроса:

'use strict';
const fs = require('fs');
const http = require('http');
const postData = JSON.stringify({
  // A relatively large string (around 2 MB) can be put in body.txt
  data: fs.readFileSync('./body.txt').toString()
});
function post() {
  const req = http.request({
    method: 'POST',
    host: 'localhost',
    port: '7001',
    path: '/memory',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(postData)
    }
  });
  req.write(postData);
  req.end();
  req.on('error', function (err) {
    console.log(12333, err);
  });
}
setInterval(post, 1000);

После запуска демонстрационного сервера с минимальным воспроизведением кода запустите этот запрос Post на клиенте и инициируйте запрос Post каждую секунду. В консоли платформы вы можете видеть, что использование динамической памяти продолжает увеличиваться.

Исправление проблем

Получив предупреждение о памяти процесса от Node.js Performance Platform, войдите в консоль, перейдите на домашнюю страницу приложения и на основе предупреждения найдите проблемный процесс в соответствующем экземпляре.

Информация по умолчанию в верхней части отчета объяснялась ранее и не будет описываться здесь снова. Теперь давайте посмотрим на некоторую информацию о подозрительном узле: результат показывает, что 18 объектов занимают 96,38% размера кучи. Очевидно, необходимо дополнительно проверить эти объекты. Нажав на Имя объекта, вы увидите подробную информацию об этих 18 system/Context объектах:

В этом примере мы получаем доступ к дереву доминаторов, в котором эти 18 объектов системы/контекста являются корневыми узлами. После расширения каждого объекта вы можете увидеть фактическое использование памяти для каждого объекта. На предыдущем рисунке проблема явно вызвана первым объектом. Давайте еще раз расширимся и посмотрим соответствующую информацию:

Очевидно, что на самом деле слишком много места в куче занимает 451 экземпляр SomeClient. На этом этапе вам нужно определить, действительно ли это является причиной исключений памяти с двух точек зрения:

  • Нужен ли одному процессу 451 экземпляр SomeClient, если текущее приложение Node.js имеет нормальную логику?
  • Если процессу требуется такое количество экземпляров SomeClient, является ли нормальным и разумным, чтобы каждый экземпляр использовал 1,98 МБ пространства?

Что касается первого вопроса, мы еще раз подтвердили логику кода в реальном производственном сценарии и обнаружили, что такое количество экземпляров Client действительно необходимо. Таким образом, основное внимание уделяется определению того, разумно ли, чтобы каждый экземпляр использовал 1,98 МБ пространства. Если это разумно, максимальный размер кучи по умолчанию 1,4 ГБ для одного процесса в приложении Node.js неприменим в этом сценарии. В этом случае необходимо включить флаг, чтобы увеличить максимальное пространство кучи.

Нажмите, чтобы еще больше развернуть эти экземпляры SomeClient и просмотреть информацию об объекте:

Сам SomeClient весит всего 1,98 МБ. Однако Object@428973 (свойство options) под ним занимает 1,98 МБ. После расширения этого объекта Object@428973 вы можете увидеть, что Object@428919 (свойство ctx) является причиной того, что экземпляр SomeClient занимает слишком много места в куче.

Далее вы можете увидеть, что это верно для любых других экземпляров SomeClient. На этом этапе вам нужно проверить код, чтобы определить, разумно ли монтировать свойство options.ctx к экземпляру SomeClient. Нажмите на этот проблемный объект:

Перейдите к графу отношений этого объекта:

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

Однако в этом примере исходная диаграмма памяти, начинающаяся с Object@428973, сама по себе не позволяет найти соответствующий код. В конце концов, и Object.ctx, и Object.key являются обычными объектами JavaScript. Поэтому попробуйте переключиться на вид Retainer:

Вы можете увидеть следующую информацию:

Retainer здесь такой же, как Retainer в Chrome DevTools. Retainer представляет исходную родительскую ссылочную связь в памяти кучи. Как и в этом примере, если подозрительного объекта и подробной информации, полученной после раскрытия этого объекта, недостаточно для поиска проблемного кода, вы можете развернуть представление Retainers для этого объекта и просмотреть путь к его родительскому узлу, чтобы легко найти проблемный код.

Используя родительскую ссылку этого проблемного объекта в представлении Retainers, вы можете легко найти код, создающий этот объект:

function getClient(options) {
  if (! clients[options.key]) {
    clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options));
  }
  return clients[options.key];
}

Комбинируя SomeClient, вы можете видеть, что только свойство key фактически используется в параметре options, который используется для инициализации. Другие свойства являются избыточной конфигурацией и не требуются.

Исправление кода и подтверждение

Относительно просто решить проблему, когда вы знаете, что на самом деле вызывает эту проблему. Вы можете отдельно сгенерировать параметр options для SomeClient и получить необходимые данные из входного параметра options, чтобы гарантировать отсутствие избыточных данных:

function getClient(options) {
  const someClientOptions = Object.assign({ key: options.key }, DEFAULT_OPTIONS);
  if (! clients[options.key]) {
    clients[options.key] = new SomeClient(someClientOptions);
  }
  return clients[options.key];
}

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

Вывод

В этой главе описывается, как устранять утечки памяти в онлайн-приложениях с помощью Node.js Performance Platform. Строго говоря, проблема, описанная в этой главе, не является настоящей утечкой памяти. Разработчики могут столкнуться с этой проблемой в большей или меньшей степени, если они напрямую принимают полное назначение при передаче конфигурации. Мы можем извлечь урок из этой проблемы: никогда не доверяйте входным параметрам от пользователей при написании модуля общедоступного компонента; сохраняйте и передавайте только те параметры, которые нам нужны, чтобы избежать многих проблем.

Оригинальный источник

https://www.alibabacloud.com/blog/node-js-application-troubleshooting-manual---oom-caused-by-passing-redundant-configurations_594967?spm=a2c41.13092489.0.0