Node.js в огне

Original author: Yunong Xiao
  • Translation
Мы создаем новое поколение веб-приложения Netflix.com, использующего node.js. Вы можете узнать больше о нашем походе из презентации, которую мы представили на NodeConf.eu несколько месяцев назад. Сегодня я хочу поделиться накопленным опытом в настройке производительности нового стека нашего приложения.

Мы впервые столкнулись с проблемами, когда заметили, что задержка запроса в нашем node.js приложении со временем увеличивается. К тому же оно использовало больше ресурсов процессора, чем мы ожидали, и это коррелировало с временем задержки. Нам приходилось использовать перезагрузку как временное решение, пока мы искали причину с помощью новых инструментов и техник аналитики производительности в нашей Linux EC2 среде.



Огонь разгорается



Мы заметили, что задержка запроса в нашем node.js приложении со временем увеличивается. Так, на некоторых из наших серверах задержка вырастала с 1 миллисекунды до 10 миллисекунд каждый час. Мы также видели зависимость увеличения использования ресурсов процессора.

image

Этот график демонстрирует длительность задержки запроса в миллисекундах относительно времени. Каждый цвет обозначает различный экземпляр AWS AZ. Вы можете увидеть, что задержка постоянно возрастает на 10 миллисекунд в час и достигает 60 миллисекунд перед перезагрузкой.

Тушение огня



Изначально мы предположили, что это могут быть утечки памяти в наших собственных обработчиках запросов, которые, в свою очередь, вызывали задержки. Мы проверили это предположение, с помощью нагрузочного тестирования изолированного приложения добавив метрики для измерения задержек только на наших обработчиках запросов и общей длительности задержки запросов, также увеличив размер используемой памяти в node.js до 32 гигабайт.

Мы выяснили, что задержка в наших обработчиках остается постоянной и равняется 1 миллисекунде. Также мы установили, что размер затрачиваемой памяти процессом тоже остается неизменным, достигая примерно 1.2 гигабайта. Тем не менее общая задержка и использование процессора продолжало расти. Это означало, что наши обработчики тут ни при чем, а проблемы находятся глубже в стеке.

Что-то добавляло дополнительные 60 миллисекунд к обслуживанию запроса. Нам был нужен способ для профилирования использования процессора приложением и визуализация полученных данных. На помощь нам пришли Linux Perf Events и flame graphs процессора.

Если вы не знакомы с flame graphs, то я советую вам прочитать отличную статью Брендана Грегга, в которой он все подробно обьясняет. Вот её краткое содержание (прямо из статьи):
  • Каждый блок обозначает функцию в стеке (стек фрейм)
  • Ось Y обозначает глубину стека (количество фреймов в стеке). Верхний блок обозначает функцию, которая выполнялась процессором, все, что ниже – это стек её вызова.
  • Ось X обозначает количество вызовов функции. На ней не показывается количество затраченного функцией времени, как на большинстве графиков. Порядок расположения не имеет значения, блоки просто отсортированы в лексикографическом порядке.
  • Ширина блока показывает общее время выполнения функции процессором или часть времени выполнения вызвавшей её функции. Широкие блоки функций могут выполняться медленнее, чем узкие, а могут просто вызываться чаще.
  • Количество вызовов может превышать время, если функция выполнялась в несколько потоков.
  • Цвета не имеют особого значения и определяются в произвольном порядке из «теплых» тонов. Flame graphs [дословно «графики пламени»; прим. переводчика] называются так потому, что показывают самые «горячие» места в коде приложения.


Ранее node.js flame graphs можно было использовать только в системах с DTrace совместно с jstack() от Дейва Пачеко. Однако недавно команда Google V8 добавила поддержку perf_events в движок V8, которая позволяет профилировать JavaScript на Linux. В этой статье Брендан описал использование новой возможности, которая появилась в node.js 0.11.13 для создания flame graphs в Linux.

image

По этой ссылке вы можете посмотреть оригинальный интерактивный flame graph нашего приложения в SVG.

Сразу же можно отметить невероятно большие стеки в приложении (ось Y). Также очевидно, что на них приходится достаточно много времени (ось X). При ближайшем рассмотрении окажется, что эти стек фреймы полны ссылок на функции route.handle и route.handle.next из Express.

Мы нашли в исходном коде Express два интересных момента1:
  • Обработчики маршрутов для всех путей сохраняются в одном глобальном массиве
  • Express рекурсивно перебирает и вызывает все обработчики пока не найдет подходящий маршрут


Глобальный массив это не самая подходящая структура данных в этом случае, так как для того, чтобы найти маршрут, в среднем потребуется 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 миллисекунд в час. В самом деле, после того как мы устранили ошибку в нашем коде, постоянное возрастание времени задержки и увеличение использования процессорного времени прекратилось.

image

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

Когда дым рассеялся



Какой же опыт мы получили? Во-первых, мы должны полностью понимать, как устроены зависимости в нашем коде, прежде чем использовать его в production. Мы сделали неверное предположение о работе Express API без исследования его кода. Неправильное использование Express API является конечной причиной наших проблем с производительностью.

Во-вторых, при решении проблем с производительностью наглядность имеет первостепенное значение. Flame graphs дали нам огромное понимание того, где наше приложение тратит больше всего времени и ресурсов процессора. Я не могу себе представить, как бы мы могли решить эту проблему, будучи не в состоянии получить node.js стеки и визуализировать их с помощью flame graphs.

Желая улучшить наглядность, мы мигрируем на Restify, который позволит нам улучшить контроль над нашим приложением2. Это выходит за рамки данной статьи, поэтому читайте в нашем блоге о том, как мы используем node.js в Netflix.

Хотите решать подобные проблемы вместе с нами? Наша команда ищет инженера для работы с node.js стеком.

Автор: Юйнун Сяо @yunongx

Примечания:
  1. В частности этот фрагмент кода. Обратите внимание, что функция next() вызывается рекурсивно для перебора массива обработчиков.
  2. Restify предоставляет множество механизмов для получения лучшей наглядности работы нашего приложения, от поддержки DTrace до интеграции c node-bunyan.


Примечания переводчика:
  1. Я не имею никакого отношения к компании Netflix. Но ссылку на вакансию оставил намеренно, буду искренно рад, если кому-нибудь она пригодится.
  2. В комментариях к оригинальной статье объясняется, почему Express не использует хэш-таблицы в качестве структуры данных для хранения обработчиков. Причина кроется в том, что регулярное выражение, по которому выбирается необходимый обработчик, не может быть ключом в хэш-таблице. А если хранить его как строку, то сравнивать придется также все ключи из хэш-таблицы (хотя это все и не отменяет того, что при добавлении второго обработчика на маршрут, можно было бы выбрасывать хотя бы предупреждение).
  3. Также вы можете прочесть развернутый ответ Эрана Хаммера (одного из контрибьюторов hapi) и последовавшую за ним дискуссию.


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

UPD: внимательные читатели заметили, что переводчик неправильно сделал транскрипцию имени автора оригинальной статьи. Спасибо за подсказки domix32 и lany.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 64

  • UFO just landed and posted this here
      +2
      Цитата неверна — express перебирает все обработчики, не пока не найдёт подходящий, а пока не переберет все (ну или пока какой-нибудь обработчик явно не решит прекратить перебор, например. из-за ошибки).
      Это очень гибкая схема, которая позволяет делать pre- и post- обработчики на базе любых полей запроса.
      Ну, например, веб-api может отдавать данные в виде json, xml или отрендеренного для человека html, основываясь на заголовке запроса accept. При этом для json и xml достаточно по одному пост-обработчкику на всю систему (а не на каждый запрос) и всё будет работать «из коробки». Для html-версии скорее всего придётся подтягивать какие-то шаблонизаторы и т.п. уже более в ручную.
      • UFO just landed and posted this here
          +2
          Имеется в виду, что автор цитаты не разобрался, как работает роутинг в экспрессе.
          • UFO just landed and posted this here
      +12
      Какой же опыт мы получили? Во-первых, мы должны полностью понимать, как устроены зависимости в нашем коде, прежде чем использовать его в production.

      Сложилось ощущение, будто бы исходный пост опубликовала маленькая веб-студия, расположенная где-то в разорившемся Детройте
        –5
        Я начал это подзревать гораздо раньше, после
        Изначально мы предположили, что это могут быть утечки памяти в наших собственных обработчиках запросов, которые, в свою очередь, вызывали задержки. Мы проверили это предположение, с помощью нагрузочного тестирования изолированного приложения

        Т.е. они покатили в прод приложение без нагрузочного тестирования? о_О Колхоз какой-то.
        –4
        Не удивлюсь, если они использовали passport.js с использованием custom callback из офф документыции. Что старнно они постыдились опубликовать часть кода, которая отвечала за утечку middleware.

        app.get('/login', function(req, res, next) {
        passport.authenticate('local', function(err, user, info) {
        if (err) { return next(err); }
        if (!user) { return res.redirect('/login'); }
        req.logIn(user, function(err) {
        if (err) { return next(err); }
        return res.redirect('/users/' + user.username);
        });
        })(req, res, next);
        });

        Самый наглядный пример из офф документации как не нужно делать )
          –2
          Простите за ошибки, на iPad писал. Разве я не прав в такой реализации passportjs.org/guide/authenticate/, которая представлена в офф документации? И все её поголовно используют.
            –1
            Я просто оставлю этот здесь! gist.github.com/hueniverse/a3109f716bf25718ba0e
              0
              В конце статьи есть ссылка на этот пост.
              0
              Вместо возмущенных вопросов анонимусу, лучше бы описали почему вы считаете эту реализацию неправильной и в чем конкретно ее ошибки, это гораздо эффективнее помогло бы избежать минусов.
            +11
            «Node.js в огне» -> «Мы научились пользоваться expressjs»
              +5
              Угу. node-то в итоге оказался вообще ни при чём, да и express, в общем-то, тоже — казалось бы, ежу понятно, что 100500 разных route будут тормозить приложение независимо от стэка.
                0
                Ага.
                > Непонятно, почему разработчики Express решили не использовать постоянную струкутру данных, например, хэш-таблицу для хранения обработчиков.

                Как он себе это вообще предстовляет?!
                  –4
                  Упрощая, дать имена маршрутам и связать их с URL.
                  {
                     "Start": "/start"
                  }
                  routes['Start'] = function()
                  {
                  
                  }
                  


                  Т.е. внедрить еще 1 уровень гибкости.
                    +4
                    Советую узнать побольше и подумать, о чём идёт речь, перед тем как минусовать и писать. У запроса есть параметр еще — метод. ок, его можно добавить в начало ключа, но экспресс умеет матчить по префиксам и регуляркам. Как, собственно, и много других реализаций маршрутизаторов.
                +16
                Картинка в тему:

                Скрытый текст
                image

                Источник: твит @erinspice


                  0
                  Вау!
                  Очень крутой материал и очень крутой перевод! Спасибо огромное!
                  Ну и спасибо за наводку на блог и другие ссылки.
                    +2
                    Пожалуйста.
                    –2
                    > «Причина кроется в том, что регулярное выражение, по которому выбирается необходимый обработчик, не может быть ключом в хэш-таблице. А если хранить его как строку, то сравнивать придется также все ключи из хэш-таблицы (хотя это все и не отменяет того, что при добавлении второго обработчика на маршрут, можно было бы выбрасывать хотя бы предупреждение).»

                    Извините, может я туплю, но что мешает иметь хеш таблицу вида routes['/users/:id'] = [callback1, callback2,..]. И просто в цикле искать соответствие текущему роуту? Тут и остановится он сразу как найдёт и если надо можно несколько обработчиков повесить. А вообще, когда мы писали свой роутер для клиентского js, там обработчик был всегда один, а хуки типа before, after хранились отдельно. Причем были как глобальные хуки, так и отдельно для роутов. Вроде все довольно удобно, до сих пор его используем и проблем не возникало.
                      0
                      Ничего не мешает. Но, автор оригинальной статьи хотел использовать хэш-таблицу как раз для того, чтобы не было возможности повесить несколько обработчиков на один маршрут. С одной стороны предложение здравое, а с другой оно бы решало только текущую проблему Netflix с багом в обновлении обработчиков, при этом время поиска осталось бы прежним.
                        0
                        Я не об авторе статьи, а об авторах Express. В моём варианте время поиска было бы значительно меньше оригинального, ибо даже оставив возможность нескольких обработчиков, мы ищем в цикле и только для первого нахождения, они же перебираются весь массив и до кучи рекурсивно.
                        0
                        Может минусующие как-то обоснуют свою позицию? А то не понятно, в чем смысл минусовать.
                          0
                          Как скажете.
                          что мешает иметь хеш таблицу вида routes['/users/:id'] = [callback1, callback2,..]. И просто в цикле искать соответствие текущему роуту?
                          То, что если искать соответствие в цикле, то какой прок от хеш таблицы? Только вред — раньше структура была упорядочена, теперь непредсказуема. Если хотите список обработчиков для каждого патерна, достаточно сделать список обработчиков для каждого патерна.
                            +1
                            homm, спасибо за уточнение, но не соглашусь.

                            Как я понял из статьи, сейчас в Express роуты хранятся как просто массив:

                            [a, b, c, c, c, c, d, e, f, g, h]


                            Т.е. элемент массива может выглядеть так: routes[0] = {pattern: '/users/:id', callback: func};

                            Таким образом, при условии что на один и того же роут может быть повешено несколько обработчиков, приходится всегда перебирать весь массив. В случае с использование хеш таблицы — до первого вхождения, потому что в данном случае pattern = ключ в таблице. ИМХО профит очевиден.

                            То, что если искать соответствие в цикле, то какой прок от хеш таблицы?


                            Кстати, интересный вопрос. Если есть идеи как сравнить текущий path с ключами хеш таблицы без перебора в цикле (не теряя при этом возможность именованных параметров вроде :id), был бы вам очень признателен и вероятно оптимизировал бы свой роутер с вашей помощью.
                              0
                              Таким образом, при условии что на один и того же роут может быть повешено несколько обработчиков, приходится всегда перебирать весь массив.
                              Дак что вам мешает сделать просто список обработчиков. Без хеша. Просто список.

                              routes[0] = {pattern: '/users/:id', callbacks: [callback1, callback2,..]};
                                +1
                                Ничего не мешает, но похоже в Express (раз уж мы его обсуждаем), так не сделали и из-за этого у автора были проблемы. Вариант с просто списком по сравнению с хеш-таблицей ничем не лучше, однако хуже тем, что мы лишаемся возможности не обходить весь массив в некоторых случаях, например при удалении роута или проверки на существование или еще куча кейсов, связанных с администрированием роутинга. В случае с хеш-таблицей вы сделаете примерно так (внимание псевдо-код):

                                router.exist = function(route) {
                                	return route in this.routes;
                                };
                                router.remove = function(route) {
                                	if (this.exist(route))
                                		delete this.routes[route];
                                };
                                router.add = function(route, callback) {
                                	if ( ! this.exist(route))
                                		this.routes[route] = callback;
                                };
                                


                                А по списку вам придется каждый раз бегать.
                                  0
                                  Ничего не мешает, но похоже в Express (раз уж мы его обсуждаем), так не сделали и из-за этого у автора были проблемы.
                                  О, да? Спасибо, кэп. Тоже буду кепом — и вот вы предлагаете какой-то способ решения, а его минусуют. Вы спрашиваете почему минусуют, и я вам объясняю, почему ваш способ решения не очень хороший.

                                  Вариант с просто списком по сравнению с хеш-таблицей ничем не лучше
                                  Но ведь я уже написал, чем хэш хуже. Смотрите: «раньше структура была упорядочена, теперь непредсказуема». Хэш — неупорядоченная структура. Если вы будете «просто в цикле искать соответствие текущему роуту», вы будете искать их в случайном порядке.

                                  куча кейсов, связанных с администрированием роутинга
                                  Вот именно, администрированием, а не обработкой запросов. И это все равно будет не хуже текущей реализации.
                                    0
                                    О, да? Спасибо, кэп. Тоже буду кепом — и вот вы предлагаете какой-то способ решения, а его минусуют. Вы спрашиваете почему минусуют, и я вам объясняю, почему ваш способ решения не очень хороший.

                                    Ну так объясните чем он плох, хотя бы так, как я объяснил чем он хорош.

                                    Но ведь я уже написал, чем хэш хуже. Смотрите: «раньше структура была упорядочена, теперь непредсказуема». Хэш — неупорядоченная структура. Если вы будете «просто в цикле искать соответствие текущему роуту», вы будете искать их в случайном порядке.

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

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

                                    Если я чего-то не понимаю и вы уверены в своей правоте, тогда попрошу пример кода, который вы подразумеваете.

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

                                      В случае со списком
                                      В каком именно случае со списком, который есть или как я предлагаю? Давайте еще раз попробую.

                                      Как есть:
                                      routes = [
                                        ['patern1', 'callback1'],
                                        ['patern1', 'callback2'],
                                        ['patern2', 'callback3']
                                      ];
                                      

                                      Как предлагаете вы:
                                      routes = {
                                        'patern1': ['callback1', 'callback2'],
                                        'patern2': ['callback3'],
                                      };
                                      

                                      Вариант не хуже вашего, лишенный неоднозначности:
                                      routes = [
                                        ['patern1', ['callback1', 'callback2']],
                                        ['patern2', ['callback3']]
                                      ];
                                      

                                      Это вот и есть «список обработчиков для каждого патерна», как я с самого начала и сказал.
                                    0
                                    обсуждали же это уже выше — регэкспы нельзя ставить ключами объекта (см. примечание переводчика).
                                    Если бы не это, то можно было бы подумать про хэштаблицу. Но не в этом языке.
                                      0
                                      Извините, а где вы тут регэксп увидали?

                                      routes['/users/:id']
                                      


                                      Не зачем их в ключи ставить совершенно. Регэкспом проверять надо уже при поиске роута. А данном случае роут будет искаться в цикле до первого вхождения. В варианте, описанном в статье, всегда будет обход всего массива.

                                      Разве профит не очевиден? Или может вы видите еще какие-то проблемы, которые могут возникнуть с этим?

                                        0
                                        то есть, вы предлагаете дать роутам такое своеобразное имя — '/users/:id'. В принципе, неплохой подход, можно даже идти дальше, и дать им имена типа 'users' или 'users_find_by_id'.
                                        Но есть одна проблема — не все регэкспы имеют однозначное строковое обозначение. Разные регэкспы могут срабатывать на одну и ту же строку запроса.

                                        P.S. Хм, сейчас перепроверил. Я почему-то был уверен, что в регэксп можно добавлять функции. Получается, любой регэксп имеет однозначное отображение в строку, ведь все регэкспы записываются обычной строкой.
                                          0
                                          Из тех JS роутеров, которые я видел, 99% именно так и описывали роут, типа:

                                          router.add('/users/:id', function(id) {});
                                          


                                          Это довольно удобно. Регекспы из них потом создаются во время обхода списка роутов и теструются с помощью метода regex.test()
                                            0
                                            Да, я уже понял. Сейчас не вижу больших проблем с таким сохранением роутов, кроме той неопределённости при обходе, о которой говорит homm.

                                            >Регекспы из них потом создаются во время обхода списка роутов
                                            во время обхода списка роутов или во время добавления роута? всё-таки генерация регэкспа — не бесплатная процедура, и они постоянно перегенерируются?..
                          +1
                          Как получить Flame Graph: Profiling Node.js.
                          Получаем инфу с помощью dtrace. Визуализируем полученное с помощью stackvis.

                          А вообще очень надеюсь на подобные возможности в node-inspector (в Chrome DevTools уже можно строить такие профили).
                            0
                            Оказалось, это было вызвано периодическим (10 раз в час) обновлением обработчиков в нашем коде из внешнего источника. Мы реализовали это удалением старых обработчиков и добавлением новых к массиву.


                            Они обвноляют таблицу роутингов из внешнего источника раз в 10 секунд? Ежу понятно, что при таком подходе рано или поздно они бы наступили на грабли.
                              +2
                              Поправка: 10 раз в час.

                              Да, меня этот подход тоже смущает. Где-то в комментариях к исходной статье им посоветовали не делать «горячую» замену кода прямо на production. Вместо этого им предложили убивать процесс node.js и делать полный редеплой, на что автор статьи написал, что к сожалению они пока не могут отказаться от этого legacy подхода.
                              +1
                              А если хранить его как строку, то сравнивать придется также все ключи из хэш-таблицы

                              Зацепил этот момент. Все регулярки можно слепить в одну большую, которая выбирает наиболее подходящий хендлер. Я так делал парсер для подсветки синтаксиса.
                                +2
                                Наконец-то глаза открываются на экспресс, я еще 2 года назад говорил, что у meddleware есть три неприодолимые проблемы:
                                • в больших проектах обработчиков вешается на роутинг сотни и тысячи, и такой способ с массивом с проверкой каждого элемента отдельно годится только для малых задач и прототипирования;
                                • каждый обработчик может сделать res.end() или res.writeHead() или другие необратимые изменения, о которых не знают следующие обработчики, которые, моет быть, хотели бы добавить свой http заголовок или сделать что-то до res.end(), на финализацию соединения повеситься можно только переопределив в самом начале .end(), сохранив оригинальную ссылку к себе, в общем — совсем не красиво и не универсально;
                                • тяжелые обработчики, типа passport отрабатывают на каждом запросе, вместо того, чтобы быть вызванными только на нескольких URL-ах, которые связаны с их непосредственной задачей, например, для passport — с процессом аутентификации в соцсетях, это 2 URL: начало аутентификации и callback от соцсети;

                                Как я предлагаю их решать и почему я отклонился от политики партии и начал писать Impress
                                • держать в памяти дерево хешей и искать по ним, но хеши эти заполнять не вручную, а мапить на дерево файловой системы, таким образом, чтобы сделать обработчик на POST /api/method.json нужно сделать файл /api/method.json/post.js и положить в него
                                  module.exports = function(client, callback) {
                                      client.cache(30000); // закешировать и исполнять не чаще 30 сек
                                      dbImpress.users.find({ group: client.fields.group }).toArray(function(err, nodes) {
                                          callback(nodes); // т.к. у каталога расширение .json то ответ упакуется в JSON и добавятся HTTP заголовки
                                      });
                                  }
                                  
                                  Это вернет:
                                  [
                                      { "login": "Vasia Pupkin", "password": "whoami", "group": "users" },
                                      { "login": "Marcus Aurelius", "password": "tomyself", "group": "users" }
                                  ]
                                  

                                • еще перед обработчиками исполнить правила URL-реврайтинга при помощи regExp (максимально склеив регекспы и храня их в подготовленном виде в массивах)
                                • держать все обработчики в памяти и при изменении их на диске автоматом подгружать и заменять, дав возможность отработать уже запущенным
                                • такие модули как passport вызываются только на нужных URL, обычно тяжелые модули необходимы всего на нескольких URL
                                • считать плохим тоном делать .end() или .writeHead() и вообще заменить req и res на объединяющий их client, у которого есть целое свое API безопасных методов например client.cache(timeout); client.redirect(url); client.setCookie(name, value, host, httpOnly); и т.д.

                                Работа еще не завершена, но используя github.com/tshemsedinov/impress уже несколько десятков проектов показывают чудеса производительности по rps и кол-ву соединений.
                                  +1
                                  В целом выглядит куда интересней экспресса, но не нравится завязка на файловую систему.
                                  1. вынуждает все обработчики хранить в отдельной директории (api), вместо того, чтобы хранить их в соответсвующих модулях и подключать их только при подключении модуля.
                                  2. нельзя собрать однофайловый бандл.
                                  3. для каждого экшена должен быть отдельный файл, хотя во многих случаях код обработчика мог бы реиспользоваться.
                                  4. не позволяет делать такие выкрутасы: /my/app/index.js?my/autobuild — если файл не найден, то он будет сгенерирован и положен по этому пути. При разработке перегенерируется при изменении исходников…
                                    0
                                    1. Не обязательно все обработчики хранить в /api, они могут находиться в любом месте файловой структуры, можно разбить приложение на модули и у каждой будет свое API или разделить API на группы методов и задать для каждой права доступа через файл access.js. Чтобы создать метод в любом месте создаете папку с расширением .json, точно так же, для отдачи html можно создать папку с именем .ajax, для создания обработчика для вебсокетов папку с расширением .ws, для Server-sent events с расширением .sse, могут быть и другие типы обработчиков, их можно дописать самому;
                                    2. для исполнения приложения серверной стороны собирать все в 1 JavaScript файл нет ни какого смысла, в браузер можно отдавать 1 файл, который будет готовить сборщик, например, grunt или gulp и будет класть в папку со статикой, а в конфиге мы прописываем, что все из папок /js, /css и /images отдавать в виде статики /config/files.js/static = [ '/js/*', '/css/*', '/images/*' ];
                                    3. переиспользуемый код можно класть в библиотеку и использовать ее из всех обработчиков, например кладем /applications/example/init/myCommon.js и при запуске этот код подгружается, а потом доступен отовсюду;
                                    4. позволяет, для этого нужно сделать файл /my/app/index.js/get.js который будет получать все остальное как параметры и может изменять файлы на диске и даже создавать новые обработчики, которые будут подгружены в память при появлении;
                                      0
                                      1. может лучше вместо папки user.json в котором лежат файлы get.js, post.js, put.js, patch.js а рядом с которой лежит папка user.html с тем же get.js и тп сделать один файл user.impress.js который экспортирует реализацию интерфейсов get, post, put, patch и прочих?
                                      2. Есть, быстрее перезапуск, быстрее деплой, меньше мусора на сервере. Насчёт папок для статики — habrahabr.ru/post/236785/
                                      3. Всё-равно будет куча копипасты в каждом обработчике, который просто делегирует обработку «переимпользуемому коду». Например, я хочу сказать, что User — это rest-resource и чтобы у него автоматом появились реализации get, post, put, delete методов с проверкой прав и прочими плюшками. Автогенерировать пачку файлов — это довольно кривой путь.
                                      4. нет, всё не то :-) хочется один раз реализовать этот самый «my/autobuild» и использовать его для разных скриптов. Другой пример — /thumb/qwertyasdfgtredsvgtr.jpg?autosize=100 — nginx не находит по ссылке картинку, вызывает скрипт, который её генерирует в указанном размере из исходника и отдаёт. Последующие запросы к ноде даже не заходят — статика отдается напрямую nginx. Проще говоря, в данном случае путь — это параметр скрипта, а query-string — его идентификатор.
                                        0
                                        1. Такая структура как /user.json похожа на REST API для реализации CRUD над юзерами. Для этого полезно сделать папку /user.json и в ней файл /user.json/request.js, он будет вызываться при любом запросе get, post, put, delete… ну и файлы get.js, post.js, put.js, delete.js могут лежать рядом с ним и они будут вызовутся после него, а если request.js со всем справился, то их просто не нужно.
                                        2. В Impress деплой без перезапуска вообще, просто новые файлы выкладываете, хоть все целиком заливаете поверх и оно подхватывается без секунды простоя, более того, если старые обработчики в этот момент еще не успели окончиться, то они живут в памяти пока не отдадут последний запрос.
                                        3. Автогенерировать не нужно, есть другой способ, делаем папку /rest и в ней файл access.js в нем ставим virtual:true, теперь все запросы типа /rest/*, например /rest/user, /rest/dog, /rest/cat… будут приходить в /rest/request.js и в /rest/get.js и т.д. и могут обрабатываться просто как параметры. Вообще REST нужен очень редко, во всем приложении хватает иметь один такой обработчик, через который идут все запросы админки или любого универсального средства работы с БД. А все остальное API состоит из RPC методов.
                                        4. Части пути и query могут быть параметрами скрипта, nginx ноде не товарищ, он товарищ только expressу, а я держу всю статику в памяти и оттуда серваю ее не медленнее, ну и вот такой обработчик сделать не проблема вообще: весь он пишется в /thumb/get.js который ловит имя файла и размер и смотрит в каталоге /thumb/files соответствующие картинки, если есть — отдает, если нет — генерит, отдает и сохраняет сгенерированный вариант.
                                        • UFO just landed and posted this here
                                            +1
                                            Статика не так много занимает, а при 32-64 Gb памяти на современных серверах, почему бы ее не занять
                                            • UFO just landed and posted this here
                                                +1
                                                Статика все же отличается от пользовательского контента, храните их отдельно, в чем проблемы.
                                            0
                                            2. Это работает только в случае stateless приложения. Если есть состояние — горячая замена кода уже не такая тривиальная задача.
                                            3. Опять этот синтетический префикс. Лучше бы постфикс был.
                                            4. Замечательно, когда объем статики превысит объем памяти, что делаеть будем?
                                              0
                                              2. Это работает прекрасно, когда структуры памяти не меняются, а функциональность и визуализация изменяется. Если меняются структуры данных, то сервер все равно не перезапускается, он удаляет состояние и строит его заново.
                                              3. Какой префикс? (не уловил)
                                              4. Есть лимиты на размеры кеша и на размеры файлов, если файл 5Гб, то его же стримить нужно уже, это отдельное ПО, а у обычных приложений по 10-20 Мб статики — это почти ничто при нынешнем железе.
                                                0
                                                2. как он узнаёт меняется или нет?
                                                3. /rest/
                                                4. Один пользователь своими фоточками легко забьёт любую память.
                                                  +1
                                                  2. Флаг необходимости обновить структуры данных, это такое же свойство приложения, как версия и дата публикации;
                                                  3. Не обязательно называть /rest/, называйте как хотите /api/path/folder/ и на конце будет rest c одним обработчиков и все виртуальные пути вглубь на него приходят;
                                                  4. Пользовательский контент кладите в отдельную папку или на CDN, его кешировать не обязательно.
                                                    0
                                                    2. Я-то думал автоматически происходит :-)
                                                    3. Всё равно rest остаётся префиксом виртуальных путей, а хотелось бы постфикс
                                                    4. Кто будет превьюшки генерировать? тоже CDN?
                                                      +1
                                                      4. Кто будет превьюшки генерировать? тоже CDN?
                                                      Да. Простите, не смог удержаться, прочитав ваш вопрос.
                                                        0
                                                        Оно даже ватермарки ставить не умеет :-)
                                                          +1
                                                          Да, это частый запрос и фича из разряда show-stopper для многих. Скоро научимся.
                                                        0
                                                        2. Ну чудес не бывает, извините уж )
                                                        3. Постфиксов (расширений каталогов в реализации на Impress) сейчас есть несколько, для веб-сокетов *.ws, для SSE *.sse, для JSON *.json, для HTML блоков подгружаемых динамически *.ajax и можно расширять, согласен, что можно ввести *.rest или даже *.crud как специфицированные интерфейсы, захотел — в любом месте сделал /data.crud и в нем module.exports = impress.generateCRUD(db.myDatabase, ['entity1',… 'entityN']); и готово.
                                      • UFO just landed and posted this here
                                          0
                                          А что, есть какие-то предубеждения против файловой системы? Отображенная в оперативную память она работает быстро, а иерархическое наследование обработчиков каталогами делать проще, чем кодом.
                                        +3
                                        Когда Express запретил мне модифицировать какое-то поле (не помню уже) запроса/ответа просто так на ровном месте, никак его не используя, перешёл на Connect (и то, местами всё равно не понимал, зачем у Connect'а там так много кода), реализовав все middleware'и, которые были нужны за какой-то час/два.

                                        И расширение routing, в котором роутинг делится на статический и динамический. Если дают строку — значит статический, можно просто положить в хештабличку, если регексп — динамический, кладём в список.

                                        Говорим, что статические доминируют над динамическими by design, как приходит запрос — сначала смотрим по табличке, а затем уже матчим по всем регекспам подряд (было бы ещё дико круто написать/найти библиотеку, которая могла бы взять группу регекспов и по нюансам их работы сделала бы один-несколько, которые бы работали эффективнее).

                                        И всё это происходит в одном middleware'е, зачем обходить список рекурсивно и плодить их?

                                        Хотя я знатный велосипедостроитель, сейчас пишу своё асинхронное I/O на восьмой джаве, чтобы написать свой асинхронный HTTP сервер/клиент, подобный нодовскому, с хорошим API, а не этим безобразным сервлетовским наследием.
                                          +1
                                          Может вам будет интересно vertx.io
                                            0
                                            Можете подсмотреть реализацию слияния регулярок в следующих модулях: Lexer, Parser
                                            0
                                            Ось X обозначает количество вызовов функции. На ней не показывается количество затраченного функцией времени, как на большинстве графиков. Порядок расположения не имеет значения, блоки просто отсортированы в лексикографическом порядке.

                                            Смысл при переводе несколько потерялся, в оригинале:
                                            The x-axis spans the sample population. It does not show the passing of time from left to right, as most graphs do. The left to right ordering has no meaning (it's sorted alphabetically).

                                            — Это не количество вызовов функции, а количество сэмплов, что коррелирует с «затраченным функцией временем»
                                            — not show the passing of time" — не отображает течение времени

                                            Only users with full accounts can post comments. Log in, please.