Pull to refresh

Comments 64

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

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

Т.е. они покатили в прод приложение без нагрузочного тестирования? о_О Колхоз какой-то.
Не удивлюсь, если они использовали 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);
});

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

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

}


Т.е. внедрить еще 1 уровень гибкости.
Советую узнать побольше и подумать, о чём идёт речь, перед тем как минусовать и писать. У запроса есть параметр еще — метод. ок, его можно добавить в начало ключа, но экспресс умеет матчить по префиксам и регуляркам. Как, собственно, и много других реализаций маршрутизаторов.
Вау!
Очень крутой материал и очень крутой перевод! Спасибо огромное!
Ну и спасибо за наводку на блог и другие ссылки.
> «Причина кроется в том, что регулярное выражение, по которому выбирается необходимый обработчик, не может быть ключом в хэш-таблице. А если хранить его как строку, то сравнивать придется также все ключи из хэш-таблицы (хотя это все и не отменяет того, что при добавлении второго обработчика на маршрут, можно было бы выбрасывать хотя бы предупреждение).»

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

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

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


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

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

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


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

routes[0] = {pattern: '/users/:id', callbacks: [callback1, callback2,..]};
Ничего не мешает, но похоже в 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;
};


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

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

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

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

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

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

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

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

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

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

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

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

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

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

routes['/users/:id']


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

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

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

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

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


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

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

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


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

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

Зацепил этот момент. Все регулярки можно слепить в одну большую, которая выбирает наиболее подходящий хендлер. Я так делал парсер для подсветки синтаксиса.
Наконец-то глаза открываются на экспресс, я еще 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. вынуждает все обработчики хранить в отдельной директории (api), вместо того, чтобы хранить их в соответсвующих модулях и подключать их только при подключении модуля.
2. нельзя собрать однофайловый бандл.
3. для каждого экшена должен быть отдельный файл, хотя во многих случаях код обработчика мог бы реиспользоваться.
4. не позволяет делать такие выкрутасы: /my/app/index.js?my/autobuild — если файл не найден, то он будет сгенерирован и положен по этому пути. При разработке перегенерируется при изменении исходников…
  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 который будет получать все остальное как параметры и может изменять файлы на диске и даже создавать новые обработчики, которые будут подгружены в память при появлении;
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 — его идентификатор.
  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
Статика не так много занимает, а при 32-64 Gb памяти на современных серверах, почему бы ее не занять
UFO just landed and posted this here
Статика все же отличается от пользовательского контента, храните их отдельно, в чем проблемы.
2. Это работает только в случае stateless приложения. Если есть состояние — горячая замена кода уже не такая тривиальная задача.
3. Опять этот синтетический префикс. Лучше бы постфикс был.
4. Замечательно, когда объем статики превысит объем памяти, что делаеть будем?
2. Это работает прекрасно, когда структуры памяти не меняются, а функциональность и визуализация изменяется. Если меняются структуры данных, то сервер все равно не перезапускается, он удаляет состояние и строит его заново.
3. Какой префикс? (не уловил)
4. Есть лимиты на размеры кеша и на размеры файлов, если файл 5Гб, то его же стримить нужно уже, это отдельное ПО, а у обычных приложений по 10-20 Мб статики — это почти ничто при нынешнем железе.
2. как он узнаёт меняется или нет?
3. /rest/
4. Один пользователь своими фоточками легко забьёт любую память.
2. Флаг необходимости обновить структуры данных, это такое же свойство приложения, как версия и дата публикации;
3. Не обязательно называть /rest/, называйте как хотите /api/path/folder/ и на конце будет rest c одним обработчиков и все виртуальные пути вглубь на него приходят;
4. Пользовательский контент кладите в отдельную папку или на CDN, его кешировать не обязательно.
2. Я-то думал автоматически происходит :-)
3. Всё равно rest остаётся префиксом виртуальных путей, а хотелось бы постфикс
4. Кто будет превьюшки генерировать? тоже CDN?
4. Кто будет превьюшки генерировать? тоже CDN?
Да. Простите, не смог удержаться, прочитав ваш вопрос.
Оно даже ватермарки ставить не умеет :-)
Да, это частый запрос и фича из разряда show-stopper для многих. Скоро научимся.
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
А что, есть какие-то предубеждения против файловой системы? Отображенная в оперативную память она работает быстро, а иерархическое наследование обработчиков каталогами делать проще, чем кодом.
Когда Express запретил мне модифицировать какое-то поле (не помню уже) запроса/ответа просто так на ровном месте, никак его не используя, перешёл на Connect (и то, местами всё равно не понимал, зачем у Connect'а там так много кода), реализовав все middleware'и, которые были нужны за какой-то час/два.

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

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

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

Хотя я знатный велосипедостроитель, сейчас пишу своё асинхронное I/O на восьмой джаве, чтобы написать свой асинхронный HTTP сервер/клиент, подобный нодовскому, с хорошим API, а не этим безобразным сервлетовским наследием.
Можете подсмотреть реализацию слияния регулярок в следующих модулях: Lexer, Parser
Ось 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" — не отображает течение времени
Sign up to leave a comment.

Articles