- PVSM.RU - https://www.pvsm.ru -
Мы создаем новое поколение веб-приложения Netflix.com, использующего node.js. Вы можете узнать больше о нашем походе из презентации [1], которую мы представили на NodeConf.eu несколько месяцев назад. Сегодня я хочу поделиться накопленным опытом в настройке производительности нового стека нашего приложения.
Мы впервые столкнулись с проблемами, когда заметили, что задержка запроса в нашем node.js приложении со временем увеличивается. К тому же оно использовало больше ресурсов процессора, чем мы ожидали, и это коррелировало с временем задержки. Нам приходилось использовать перезагрузку как временное решение, пока мы искали причину с помощью новых инструментов и техник аналитики производительности в нашей Linux EC2 среде.
Мы заметили, что задержка запроса в нашем node.js приложении со временем увеличивается. Так, на некоторых из наших серверах задержка вырастала с 1 миллисекунды до 10 миллисекунд каждый час. Мы также видели зависимость увеличения использования ресурсов процессора.

Этот график демонстрирует длительность задержки запроса в миллисекундах относительно времени. Каждый цвет обозначает различный экземпляр AWS AZ. Вы можете увидеть, что задержка постоянно возрастает на 10 миллисекунд в час и достигает 60 миллисекунд перед перезагрузкой.
Изначально мы предположили, что это могут быть утечки памяти в наших собственных обработчиках запросов, которые, в свою очередь, вызывали задержки. Мы проверили это предположение, с помощью нагрузочного тестирования изолированного приложения добавив метрики для измерения задержек только на наших обработчиках запросов и общей длительности задержки запросов, также увеличив размер используемой памяти в node.js до 32 гигабайт.
Мы выяснили, что задержка в наших обработчиках остается постоянной и равняется 1 миллисекунде. Также мы установили, что размер затрачиваемой памяти процессом тоже остается неизменным, достигая примерно 1.2 гигабайта. Тем не менее общая задержка и использование процессора продолжало расти. Это означало, что наши обработчики тут ни при чем, а проблемы находятся глубже в стеке.
Что-то добавляло дополнительные 60 миллисекунд к обслуживанию запроса. Нам был нужен способ для профилирования использования процессора приложением и визуализация полученных данных. На помощь нам пришли Linux Perf Events [2] и flame graphs процессора.
Если вы не знакомы с flame graphs, то я советую вам прочитать отличную статью [3] Брендана Грегга, в которой он все подробно обьясняет. Вот её краткое содержание (прямо из статьи):
Ранее node.js flame graphs можно было использовать только в системах с DTrace совместно с jstack() [4] от Дейва Пачеко. Однако недавно команда Google V8 добавила поддержку perf_events в движок V8, которая позволяет профилировать JavaScript на Linux. В этой [5] статье Брендан описал использование новой возможности, которая появилась в node.js 0.11.13 для создания flame graphs в Linux.

По этой ссылке [6] вы можете посмотреть оригинальный интерактивный flame graph нашего приложения в SVG.
Сразу же можно отметить невероятно большие стеки в приложении (ось Y). Также очевидно, что на них приходится достаточно много времени (ось X). При ближайшем рассмотрении окажется, что эти стек фреймы полны ссылок на функции route.handle и route.handle.next из Express [7].
Мы нашли в исходном коде Express два интересных момента1 [8]:
Глобальный массив это не самая подходящая структура данных в этом случае, так как для того, чтобы найти маршрут, в среднем потребуется O(n) операций. Непонятно, почему разработчики Express решили не использовать постоянную струкутру данных, например, хэш-таблицу для хранения обработчиков. Усугубляет ситуацию и то, что массив обходится рекурсивно. Это обьясняет, почему мы видели такие высокие стеки в flame graphs. Интересно и то, что Express позволяет установить множество обработчиков для одного маршрута.
[a, b, c, c, c, c, d, e, f, g, h]
В данном случае поиск для маршрута c был бы прекращен при нахождении первого подходящего обработчика (позиция 2 в массиве). Однако для того, чтобы найти обработчик маршрута d (позиция 6 в массиве), необходимо было бы потратить лишнее время на вызов нескольких экземпляров c. Мы проверили это с помощью простого Express приложения:
var express = require('express');
var app = express();
app.get('/foo', function (req, res) {
res.send('hi');
});
// добавляем еще один обработчик на тот же маршрут
app.get('/foo', function (req, res) {
res.send('hi2');
});
console.log('stack', app._router.stack);
app.listen(3000);
После запуска приложение выводит эти обработчики:
stack [ { keys: [], regexp: /^/?(?=/|$)/i, handle: [Function: query] },
{ keys: [],
regexp: /^/?(?=/|$)/i,
handle: [Function: expressInit] },
{ keys: [],
regexp: /^/foo/?$/i,
handle: [Function],
route: { path: '/foo', stack: [Object], methods: [Object] } },
{ keys: [],
regexp: /^/foo/?$/i,
handle: [Function],
route: { path: '/foo', stack: [Object], methods: [Object] } } ]
Обратите внимание, что есть два одинаковых обработчика для маршрута /foo. Было бы неплохо, если Express выкидывал бы ошибку всякий раз, когда один маршрут имеет несколько обработчиков.
Теперь наше предположение заключалось в том, что задержки возникали из-за постоянного увеличения массива с обработчиками. Скорее всего где-то в нашем коде дублировались обработчики. Мы добавили дополнительное логгирование, которое выводило массив обработчиков запросов, и заметили, что он растет по 10 элементов в час. Эти обработчики были идентичны друг другу, как из примера выше.
Что-то добавляло в приложение по 10 одинаковых обработчиков для статических маршрутов в час. Далее мы выяснили, что при переборе этих обработчиков затраты на вызов каждого из них занимают около 1 миллисекунды. Это коррелирует с тем, что мы видели раньше, когда задержка отклика росла на 10 миллисекунд в час.
Оказалось, это было вызвано периодическим (10 раз в час) обновлением обработчиков в нашем коде из внешнего источника. Мы реализовали это удалением старых обработчиков и добавлением новых к массиву. К сожалению, также при этой операции мы всегда добавляли обработчик для статического маршрута. Так как Express позволяет добавлять несколько обработчиков для одного маршрута, все эти дубликаты добавлялись в массив. Хуже всего то, что все они добавлялись раньше остальных, а это означало, что прежде чем Express найдет обработчик API для нашего сервиса, он несколько раз вызовет обработчик для статического маршрута.
Это в полной мере объясняет, почему задержки запросов росли на 10 миллисекунд в час. В самом деле, после того как мы устранили ошибку в нашем коде, постоянное возрастание времени задержки и увеличение использования процессорного времени прекратилось.

На этом графике видно, что время задержки сократилось до одной миллисекунды после обновления кода.
Какой же опыт мы получили? Во-первых, мы должны полностью понимать, как устроены зависимости в нашем коде, прежде чем использовать его в production. Мы сделали неверное предположение о работе Express API без исследования его кода. Неправильное использование Express API является конечной причиной наших проблем с производительностью.
Во-вторых, при решении проблем с производительностью наглядность имеет первостепенное значение. Flame graphs дали нам огромное понимание того, где наше приложение тратит больше всего времени и ресурсов процессора. Я не могу себе представить, как бы мы могли решить эту проблему, будучи не в состоянии получить node.js стеки и визуализировать их с помощью flame graphs.
Желая улучшить наглядность, мы мигрируем на Restify [9], который позволит нам улучшить контроль над нашим приложением2 [10]. Это выходит за рамки данной статьи, поэтому читайте в нашем блоге о том, как мы используем node.js в Netflix.
Хотите решать подобные проблемы вместе с нами? Наша команда ищет инженера [11] для работы с node.js стеком.
Автор: Юнонг Ксиао @yunongx [12]
Примечания:
next() вызывается рекурсивно для перебора массива обработчиков.Примечания переводчика:
Комментарии и замечания, касающиеся перевода, приветствуются в личных сообщениях.
Автор: codefo
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/75255
Ссылки в тексте:
[1] презентации: https://www.youtube.com/watch?v=gtjzjiTI96c&list=PLfXiENmg6yyUpIVY9XVOkbdmBPx6PUm9_
[2] Perf Events: https://perf.wiki.kernel.org/index.php/Main_Page
[3] отличную статью: http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html
[4] jstack(): http://dtrace.org/blogs/dap/2012/04/25/profiling-node-js/
[5] этой: http://www.brendangregg.com/blog/2014-09-17/node-flame-graphs-on-linux.html
[6] ссылке: http://cdn.nflximg.com/ffe/siteui/blog/yunong/200mins.svg
[7] Express: http://expressjs.com/
[8] 1: #footnote-1
[9] Restify: http://mcavage.me/node-restify/
[10] 2: #footnote-2
[11] ищет инженера: http://jobs.netflix.com/jobs.php?id=NFX01717
[12] @yunongx: https://twitter.com/YunongX
[13] фрагмент кода: https://github.com/strongloop/express/blob/d40dc65/lib/router/route.js#L81-L102
[14] DTrace: http://mcavage.me/presentations/dtrace_conf_2012-04-03/
[15] node-bunyan: https://github.com/trentm/node-bunyan
[16] ответ: https://gist.github.com/hueniverse/a3109f716bf25718ba0e
[17] hapi: http://hapijs.com/
[18] Источник: http://habrahabr.ru/post/243945/
Нажмите здесь для печати.