Архитектура JS Back-end: подводные камни, принципы работы, лайфхаки

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Я занимаюсь построением высоконагруженных сервисов и разработкой архитектурных решений. Когда выдается свободное время, иногда пишу о JS на Medium: medium.com/@alexanderbidiuk — делиться опытом не только интересно, но и полезно. Эта моя статья пригодится тем, кто не знает, какие технологии использовать в своем проекте. Она послужит поверхностным руководством при выбора стека и подходов к разработке продукта на JS.

Node JS был создан как легкийая для вхождения язык платформа, дополняющийая фронтендовый стек. Он был призван имплементировать событийную неблокирующую ввод-вывод модель. И до сих пор «нода» хороша для асинхронных операций.

Независимо от подхода к написанию кода, Node JS нельзя использовать для тяжелых вычислительных процессов. Лучше это отдать на исполнение микросервису на другом языке или написать модуль на C/C++. Как опциональное решение подойдут worker threads.

«Нода» отлично зарекомендовала себя при работе над легкими операциями input и output, когда нет большой нагрузки на процессор. Но если нам нужно обрабатывать не такие уж большие данные, и вы не хотите блокировать свою «ноду», потому что она должна ждать запроса от пользователей и продолжать работу, уже на неэкспериментальном этапе можно использовать worker threads.

О возможностях Node JS и архитектуре JS Backend, подводных камнях и лайфхаках при работе с ними — обо все, что мы рассмотрели на митапе Fullstack JS Beetup от Beetroot Ukraine, — читайте дальше.

Node JS и разные виды архитектуры

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

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

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

Использование BFF на монолите

Идеальный вариант для монолита — это разделение на бекенд с бизнес-логикой и BFF. Что такое BFF, в чем его суть? Это сервер, который занимается только логикой, необходимой для фронтенда. В нем нет крупной бизнес-логики, которая должна быть на бекенде.

Например, BFF может хранить секьюрные данные, например, ключи. И при запросах на third party сервисы использовать их. Это хорошее решение для платежных систем: фронтенд ничего не «знает» о секьюрности. Так же BFF может держать SSE-сессии. То есть у нас куча пользователей, они все подсоединились — зачем на основном бекенде хранить все эти сессии и заниматься их менеджментом? Также этот сервис возьмет на себя проксирование и фильтрацию потока данных, который потом пойдет в сессии.

Сервер BFF может также работать как API gateway. Например, можно сделать один универсальный эндпоинт для фронтенда, а уже внутри BFF он раскидывается на несколько запросов: на основной бекенд или сторонние сервисы, такие как Amazon S3, AWS Lambda и так далее.

Микросервисы

Когда нам нужны микросервисы? Идеальный вариант использования микросервисов — это когда поток данных может быть асинхронным. В таком случае можно будет использовать системы очередей для коммуникации, которые дают крутые возможности по управлению трафиком.

Бекенды микросервисов, по мере своих возможностей, когда у них освобождается thread, подхватывают эти данные. Такой подход дает независимость при разработке сервисов (микросервисы могут быть написаны на разных языках). К тому же каждый микросервис работает независимо от других частей системы.

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

Пример микросервисной архитектуры

На рисунке: сверху вниз — шкала по времени, слева направо — как идет флоу действий.

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

Какие недостатки у микросервисов? Микросервисную архитектуру тяжелее поддерживать. Нужно тщательно продумать, как вы ее будете логировать. Допустим, у вас была цепочка из трех микросервисов. Вы в один из них отправили объект, а он не пришел. Вы должны прологировать каждый микросервис, чтобы потом можно было найти, на каком этапе «потерялся» объект, где именно произошла ошибка. В этом и сложность: всегда нужно думать о логировании, а при возникновении ошибок придется проверять все. Еще один минус микросервисов: когда деплоите, это дороже, сложнее, требует больше ресурсов.

Организация кода

Для приложений среднего размера и более монолит — не лучшее решение: наличие одного большого программного обеспечения с множеством зависимостей просто трудно осмыслить и это часто приводит к слишком сложной кодовой базе.

Поэтому наиболее рациональное решение — это разделить весь проект на автономные модули, которые обмениваются файлами (сервисами) с другими. Каждый модуль должен содержать мало файлов, например: сервис, доступ к данным, контроллер и так далее. В таком случае будет очень легко вынести логический модуль в другой микросервис или в shared-компоненты.

Обратите внимание, что проект разбивается именно по логическим модулям: продуктов, отзывов и так далее. Это напоминает «утиную типизацию» или ducks-подход, но мы пляшем от доменной области. Например, есть доменная область «пользователи» — все, что связано с ними, мы пишем в одной папке. Между собой компоненты внутри папки могут быть сильно связаны, но наружу мы предоставляем минимальное API, чтобы между модулями сильной связи не было.

Хороший пример

Допустим, у нас есть юзер и продукт. Внутри продукта есть сервис и контроллер. А между собой юзер и продукт общаются через Public API. Это не классический http-запрос. Он подразумевает, что один модуль экспортирует определенный набор функций, а другой модуль знает, что у него в зависимостях должен быть такой модуль, который имплементирует заданные методы. При использовании Dependency Injection зависимость внедряется автоматически, и разработчику не нужно прописывать import и export. Вы просто регистрируете модуль и конфигурируете. Например, что модуль users должен получить модуль products. Это и есть публичное API, когда вы вообще не импортируете сторонние модули между собой.

Что это дает? Мы можем взять, например, модуль Users и использовать его в другом микросервисе. То есть мы можем этот модуль вообще «запихнуть» в private registery и установить как в одном микросервисе, так и в другом. Плюс, если мы используем TypeScript, мы четко знаем, какой интерфейс он имплементирует, какие методы он нам дает, и можем использовать модуль, где хотим.

Плохой пример

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

Классы vs. объявление функций как констант функциональный стиль

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

Допустим, вам нужно протестировать user-сервис. В данном случае это будет файл с двумя методами. Если два метода используют третий, его надо будет как-то замокать. Но как мы его можем замокать, если мы этот метод не экспортируем? В случае с классом, когда вы импортируете его полностью, вы можете сразу указать, что хотите у этого класса замокать определенный метод. И тогда, когда вы будете писать тесты на методы, вы можете через this указать, чтобы они юзали не изначальный метод, а замоканный.

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

DDD — Domain Driven Design

Возвращаясь к организации кода, давайте поговорим про Domain Driven Design. Его суть состоит в том, чтобы разделять логику, связанную с любым модулем системы, на слои. Основная задача DDD — снизить сложность внедрения нового функционала и модификации, существующего с ростом приложения.

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

Схожая с DDD практика — это гексагональная архитектура (Hexagonal Architecture). Принцип и подход схож с DDD, просто некоторые вещи имеют свое название. Мы разбиваем все на модули. Хороший пример: модуль статей работает с сервисом для статей через классы и методы, которые работают с базой данных. То есть легко вклеивать части системы в эту архитектуру.

Фреймворки

Чтобы не рассматривать все фреймворки, давайте остановимся на наиболее популярных и интересных:

  • Express.
  • Nest JS.
  • Koa JS.
  • Fastify.

Express

Начнем, наверное, с самого популярного фреймворка на планете. Это не диктующий правила минималистичный фреймворк. Он позволяет разработчикам выбирать любые инструменты и технологии, мидлвары и хуки, которые они хотят использовать. Разработчики структурируют свой код без какой-либо конкретной системы, навязанной фреймворком. Express не имеет конкретного механизма шаблонизации, парсера тела запроса, обработчика ошибок и так далее. Для начинающего девелопера это идеальный вариант.

Что за собой влекут особенности Express? Каждый разработчик начинает тянуть одеяло на себя и ставить свои любимые инструменты. Кому-то нравится шаблонизатор pug, а кому-то — ejs. Если в рамках одного проекта это все решается с помощью тимлида или заданных правил разработки (и демократии), то на проектах с той же микросервисной архитектурой — это колоссальная проблема. Разработчикам будет тяжело свитчнуться с проекта на проект, а также вам будет невозможно сделать какие-то shared-утилиты.

Плюсы:

  • Легковесность.
  • Свобода выбора инструментов.
  • Легкость вхождения.
  • Популярность.

Минусы:

  • Свобода выбора инструментов.
  • Скорость.
  • Легкость вхождения.
  • Не дает четкой структуры кода.

Nest JS

Nest следует парадигме дизайна «convention over configuration». Он настоятельно рекомендует разработчикам использовать определенные инструменты и кодить определенным образом. Под капотом он уже обертывает контроллер блоком try-catch, анализирует тело запроса, добавляет обработчик ошибок, мидлвары и т.д.

Таким образом, не нужно делать эти обычные вещи каждый раз при создании нового проекта. Nest с коробки дает нужную структуру папок, говорит, как и где писать нужные части кода. А еще Nest использует TypeScript, что очень хорошо для больших команд, а также при разработке микросервисов.

Плюсы:

  • Четкие паттерны разработки.
  • Легкость вхождения.
  • Легкая первоначальная настройка проекта.
  • Все типовые решения идут с коробки.
  • Возможность использования разного транспорта под капотом.

Минусы:

  • Тяжеловесность.
  • Не всегда уместное использование. (Например, если у вас есть сервер, на котором мало круда, но много обработки на микросервисы, Nest не подходит).
  • TypeScript. (Если проект простой, TypeScript будет только замедлять работу над ним).
  • Скорость. (Когда вы пишите свой код, он еще оборачивается функциями самого Nest, поэтому, каким бы движок под капотом ни был быстрым, за счет вот этих лишних вызовов функций он все равно будет чуть-чуть медленнее).

В Nest под капотом можно использовать разные фреймворки: тот же Express или Fastify. Нет готового решения. Когда вы получаете задачу, лучше выписать плюсы и минусы разных фреймворков и сопоставить их с вашим проектом.

Koa JS

Коа был сделан теми же разработчиками, которые создали Express. Он очень легкий: всего около 500 строк кода. Там нет ни одного встроенного мидлвара, также в нем нету встроенного роутера, как в Express. В последних версиях он очень быстрый.

Плюсы:

  • Быстрый (второй по скорости из всех фреймворков).
  • Удобно кастомизировать.
  • Легковесный и простой.

Минусы:

  • Свобода выбора инструментов.
  • Не дает четкой структуры кода.

Fastify

Самый быстрый фреймворк. По структуре и принципу написания кода он не похож на предыдущие фреймворки, у него интересный подход к организации модулей и скоупов. Предоставляет свой dependency injection.

Он обрабатывает почти 77 тысяч запросов в секунду. Но нужно учитывать, что когда вы работаете над проектом, и используете хотя бы одну мидлвару, он будет «тянуть» уже 20 тысяч запросов. Также у Fastify есть определенная часть статических проверок, когда он парсит запросы. Плюс он очень быстро валидирует JSON.

Плюсы:

  • Супербыстрый.
  • Удобно кастомизировать.
  • Легковесный и простой.

Минусы:

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

Например, у нас общение с сервером происходит по SSE-connection. Есть единственный модуль, единственная библиотека по SSE для Fastify,. Но она очень старая и не работает с текущий версией Fastify. Вы ее импортируете, подключаете, а она отказывается функционировать. В итоге может потребоваться самостоятельно писать плагин для SSE. Или, как вариант решения: можно заюзать Nest с Fastify в качестве движка. Но если круда на проекте почти нет, зачем нужен Nest с его большим оверхедом?

Хранилища и базы данных

Все слышали SQL и NoSQL. Давайте поговорим, что и когда нужно использовать. Напомню особенности обоих типов.

SQLNoSQL
Гарантирует консистентностьИмеет eventual consistency
Имеет жесткую структуру, которую нельзя быстро изменитьСтруктура может легко меняться
Каждая сущность может быть беспроблемно размазана по БДПредполагает, что сущность «локальная» (когда вся информация о сущности — внутри)
Требует хорошие знания для скейлингаЛегко скейлится (та же Mongo дает шардирование с коробки)
Дает не такой крутой перфомансХороша для интенсивных записей и чтений

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

SQL крут для данных с жесткой структурой и гарантирования консистентности. Можно спокойно использовать SQL, если вы знаете, что структура не меняется. Как ни крути, SQl мощнее, в нем сложнее серьезно напортачить, наделать багов.

Рассмотрим самые хайповые базы данных:

  • MongoDB — NoSQL.
  • PostgreSQL — SQL/NoSQL.
  • MySQL — SQL.
  • REDIS, Aerospike, Memcached — NoSQL.

Здесь не упомянуты графовые базы данных, но из них чаще всего встречается Neo4j. Для чего можно использовать разные базы данных?


  • Приложения по сбору статистики.
  • Приложения по сохранению событий (системных событий, событий пользователя, в конце концов, все данные, прилетающие асинхронно, — события).
  • Сохранения конфигураций.
  • Высоконагруженные записи,чтения по конкретным значениям.
MongoDB, REDIS, Aerospike, Memcached, PostgreSQL

  • Приложения, где вы должны гарантировать, что после ваших изменений все получат актуальную инфу.
PostgreSQL, MySQL

Почему PostgreSQL указан в обоих строках таблицы? Это очень интересная база данных, она поддерживает новые SQL-решения: можно сохранять новые SQL-данные. Мало того, что мы можем сохранять, записывать JSON, мы также можем делать запросы, индексировать. В некоторых случаях она работает даже быстрее, чем MongoDB.

Docker

Для чего мы используем Docker в приложениях?

  • Используется для гарантирования исполняемости кода на любой машине. Почти все, кто писал код на JS или Node, сталкивался с тем, что на «Маке» код собирается, а на Windows уже нет. Что делает Docker? Он гарантирует, что код исполняется на любой ОС.
  • Основа для CI/CD. Собирая проект, мы с помощью Docker создаем образ, и с этим образом уже могут работать различные CI/CD системы.
  • Помощь при разработке микросервисов (вместе с Docker Compose File). Вы сразу можете запустить одной командой весь environment, и он будет нормально функционировать: точно так же, как если бы его задеплоили на реальный продакшн.

Рассмотрим общие практики:

  • Используйте многоступенчатые сборки для более компактных и безопасных образов Docker. Сам Docker-образу будет супербольшим, поэтому мы билдим в одном образе, а потом запускает Node на другом, более легком образе, копируя все, что мы сбилдили, из первого образа.
  • Делайте запуск внутри Docker командой Node, а не npm. Когда вы раните ваш образ, npm не пробрасывает системные команды сигналы. Что это значит? Вы, например, подняли у себя Docker, образ Node, но что-то пошло не так, и вы решили остановить процесс. Но с npm вот этот сигнал остановки процесса не пробросится внутрь Node. Перед тем, как «убить» Node, вы должны дать команду отсоединиться от базы данных, от системы очередей, чтобы конекшн не висел. Когда вы выполняете команду Node внутри Docker-образа, вы гарантируете, что этот сигнал проскочит, и вы поймаете эту команду, сможете выполнить необходимые функции.
  • Всегда добавляйте .dockerignore, чтобы тесты и другие лишние данные не попали в ваш Docker-образ.
  • Всегда очищайте зависимости. Используйте в конце команду npm prune, которая почистит лишний dependence. И это сделает образ меньшим.
  • Используйте фичу кеширования. То есть копируйте уже все приложение после того, как скопировали package.json и сделали ему npm-инстал. Когда выполняется команда «copy», и она видит, что скопированные до этого файлы не изменились, она пропустит команду «npm ci», и ваш образ будет запускаться быстрее.

Выбирая стек приложения, нужно опираться на множество факторов: начиная от требований, заканчивая знание тех или иных технологий командой, которая будет заниматься разработкой. Мы не ставили себе целью дать однозначный ответ, какой фреймворк или БД выбрать, все очень ситуативно и зависит от многих нюансов. При проектировании продукта всегда стоит подумать на пару шагов вперед: «А вдруг захотят такой функционал?». Или «А вдруг захотят хранить в БД иные данные?». Тогда Вы сможете продумать и применить более универсальный стек, который покроет не только текущие требования и «хотелки», но и будущие.

👍НравитсяПонравилось11
В избранноеВ избранном8
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Категорически избегаю проектов, где-либо идёт упоминание ноды. Куча проблем и велосипедов, вечная борьба с ветряными мельницами, промайсами и распараллеливанием работы, благо есть джава, Го, руст где подобных проблем нет изкаропки при наличии мозгов

Як можна уникнути ноду, якщо вона всюди де є хоч якийсь натяк на фронтенд збирання?

Все таки содрать билд конфиг и вбить команду в консоль это как бы не использование ноды

Я може старомодний, але коли кажуть, що щось не використовується, то воно дійно не використовується ніде, навіть під капотом сторонніх утіліт
Або хоча б вказувати, що ми обмежено використовуємо щось і на проді його нема

закинуть json/yaml конфиг и запустить это не использование, уж точно не использование с точки зрения разработки. Я вот на вид не переношу питон (отступы, как часть семантики, неистово бесят :)), но бывает приходится юзать некоторые утилиты в сорсах, можно сказать что я юзаю питон? Вряд ли. Может как юзер максимум, хотя мне как юзеру абсолютно все ровно на чем но внутри оно написано.

можно сказать что я юзаю питон?

Якщо ви не ставите окрему версію, то я зі скрипом скажу, що ні, не використвоуєте
Наскільки я знаю, нода ще не ставиться по дефолту в лінукси

питон (отступы

О
Шанувальник робити візерунки з коду на перлі

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

І як ви джавою рендерите фігні для жс фреймворків?

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

у ноды проблемы с распараллеливанием работы и промисами... Так и запишем, что проблемы платформы в основном ее преимуществе- дешевых потоках и асинхронности.

В Го тоже куча дешевых потоков с распараллеливанием (а не асинхронностью), хоть миллион запускай, и не надо колдовства с цепочками промайсов городить

тока гоу не решение для продакшен систем, а решения для небольших утилит и сервисов)

Так и запишем — Kubernetes — не продакшн система, а небольшая утилита, т.к. написана на Go. Ну или github.com/...​iaMetrics/VictoriaMetrics — не полноценное решение для мониторинга продакшн-систем, а небольшой сервис, т.к. написан на Go.

В Го тоже куча дешевых потоков с распараллеливанием (а не асинхронностью), хоть миллион запускай, и не надо колдовства с цепочками промайсов городить

«Колдовство» это дописывание await перед вызовом функций? )) Да, это непосильная таска)) Уже упростили «асинхронность» js до примитивизма и еще некоторым не так)
Это ж надо :) И что, там придумали нечто другое чем синхронизация с блокировками системных потоков и прокидывания канала связи типа IPC? Или придумали даже эмуляцию потоков через асинхронность, которая в ноде требует писать await, а в Go ее аналог, небось, неявно используется? Там еще надо разобраться с накладными расходами на все это пиршество.
Как будто в node нет потоков и я не могу функцию выполнить в другом потоке, что в 99,99% времени нафик не нужно при наличии асинхронности, в то время как взаимодействие с потоком накладывает определённые расходы.
Практически нет никакого резона запускать нечто в другом потоке одного процесса, особенно если юзать ноду по прямому ее назначению- как бэкенд. Любая большая «синхронная» задача легко разбивается на простые рутины, которые можно протащить через eventloop, т.е выполнить асинхронно, при этом не ущемляя отзывчивость других «потоков». Добавить await с хелпером это не высшая математика. Да и вообще больших задач в принципе не найти, так что ваше масштабирование через потоки точно не лучше, а скорее хуже, масштабирования через асинхронность и процессы.

Уже даже топ левел авейт замутили для удобства, но некоторые люди все равно думаю о ноде как о express.js сервере на коллбеках с миддлвеа лапшой)))

Да и вообще больших задач в принципе не найти,

ШТА ????

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

практической большой/тяжелой вычислительной задачи для ноды

А, ну с такой уточненной формулировкой возражений нет

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

  • Одно ядро процессора может обслужить до 10к запросов в секунду
  • У сервера есть глобальный in-memory кэш, к которому идут обращения на чтение/запись при каждом запросе
  • Сервис должен работать на одном компе и потреблять минимальное количество cpu и ram

Жду решений этой задачи на node.js .

Задача из реальной жизни:

И это большая/тяжелая вычислительная задача в рамках одного запроса?

Сервис должен работать на одном компе и потреблять минимальное количество cpu и ram

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

Жду решений этой задачи на node.js .

Беру я такой Redis или другую in-memory key-value db и не занимаюсь изобретением велосипедов на том, для чего оно не предназначено. Может SQL DB будем пилить заодно на node или подгоним ТЗ к 1кб памяти микроконтроллера и напишем код не под C, а на Go? Или может напишем аппаратный файрволл с anti-DDoS на Go c потоком в сотни гигабит, вместо С? У каждого инструмента свои задачи и никто не пишет свои велосипеды in-memory db в веб. А если в 0.01% проектов встанет такой вопрос, что готовые продукты от гигантов не подошли, то там нужен будет максимально выгодный инструмент/ЯП, и вряд ли это будет Go.
Но если так охота, то беру я значит N процессов по количеству ядер, соответственно каждый инстанс имеет свой key-value хранилище в своем адресном пространстве и делаю балансировку по ключу mod N балансиром. В ТЗ никак не сказано о распределении и обрабатываются ли как то данные с кэша, кроме как выплюнуть назад в сокет, так что норм.

беру я значит N процессов по количеству ядер, соответственно каждый инстанс имеет свой key-value хранилище в своем адресном пространстве и делаю балансировку по ключу mod N балансиром

Так не пойдет, т.к. в задании указано, что кэш — глобальный, т.е. любой запрос может обратиться к любым записям в кэше. Шардировать запросы по процессам с индивидуальными кэшами не получится.

Шардировать запросы по процессам с индивидуальными кэшами не получится.

Тогда определенно берем redis/memcached/etc и не занимаемся фигней.
Сама постановка задачи ТЗ уже с приветами- ультимативные 1/10 запросов на одно ядро, т.е нужно обязательно 10 потоков. Спрашивается- нафига нам 10 потоков, если потоки ничего не делают кроме как ломятся в один участок памяти через системные блокировки? Как количество потоков влияют на IO? Никак. Тут искусственно подтасованное ТЗ для задачи, которая в принципе не должна так решаться на бэкенде.
Ну допустим мы еще как то обкатываем инфу с кэша перед выплевыванием ее в сокет, то выделяем один процесс под нужды key-value db и ломимся к нему по сокету/IPC и в итоге понимаем, что получаем аналог все того же однопоточного redis, который кстати умные люди почему то не сделали многопоточным.
Даже если удастся в этом конкретном сугубо теоретическом случае получить меньшую стоимость доступа к примитивному хранилищу, то на практике ее не будет видно абсолютно, а вот на архитектуру накладывает ограничение, ибо эта самопальная бд наглухо прикручена к процессу сервиса. На практике таких внутрипроцессорных кэшей больших не должно быть, чтобы пришлось экономить на озу, будь он выделенным у каждого процесса на ядро. Да и что такое стоимость получения доступа к банку любым способом на фоне задачи, для которой понадобились 10 ядер? Пыль да и все. Если мы такие мелочные, то надо писать все на С- жизнь боль же.

Тогда определенно берем redis/memcached/etc и не занимаемся фигней

Redis/memcached не подходят из-за больших накладных расходов на сериализацию и передачу данных между процессами.

Спрашивается- нафига нам 10 потоков, если потоки ничего не делают кроме как ломятся в один участок памяти через системные блокировки?

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

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

Еще раз повторяю — это задача из моей практики. Был сервис на node.js , обрабатывающий запросы на рекламу (aka ad server). Он хранил в памяти состояние по текущим рекламным кампаниям (тот самый кэш, про который
упомянуто выше). Все было ОК, пока нагрузка на него была низкой. При росте нагрузки уперлись в однопоточность node.js. Проблему пытались решить путем запуска параллельных процессов node.js. Вот только была небольшая проблемка — как расшарить между этими процессами состояние по рекламным кампаниям. Пытались применить всякие костыли вроде shared memory с изощренными межпроцессными блокировками. Затем за неделю переписали сервис на Go — и он стал автоматически масштабироваться на произвольное количество ядер CPU. При этом код стал проще в поддержке по сравнению с кодом на node.js, т.к. теперь там не было callback’ов, была понятная обработка ошибок и была статическая типизация.

Все было ОК, пока нагрузка на него была низкой. При росте нагрузки уперлись в однопоточность node.js.

Хм. То есть 2...N процессов было запустить не судьба?
Да клиенту ведь было очень важно, в каком процессе, сервере и с каким количеством ОЗУ был обработан его запрос и сколько мкс было потрачено при обращении к кешу, вынесенного в отдельный процесс в виде redis на фоне выполнения всего запроса и сетевых задержках, измеряемых в десятке-другом миллисекунд. Это технологический перфекционизм, а не практическая сторона вопроса.
Там о каких объёмах кеша то речь? Еще разобраться с инвалидацией и т.д на фоне экономии на спичках.
Даже, если без redis, то если 10 (а вероятно меньше) процессов промажут мимо своего кеша к БД, то это так уж критично? Конечно тут надо видеть что она за данные и как они используются.
Если хочется именно технологического перфекционизма с вылизанным до блеска решением по памяти и цп, то нужно писать проприетарное решение на С, а это так чувство удовлетворенности, т.к немного таки сэкономили, но по факту никто этого не заметит, что вам пришлось там лишнюю планку озу добавить или ползунок покрутить в облаке.
Иными словами да, в бинарном велосипеостроении наверняка Go будет лучше, а более низкоуровневые еще лучше, но суть в том что это такие велосипеды не область применения node. Да, нода сделает тоже самое немного дороже по железу, но практическая целесообразность для бизнеса более низкоуровневого проприетарного решения под вопросом. Это должны быть очень узкие кейсы.
Мы же не пишем nginx на ноде? Так само как и не пишем бек на С мейнстримом. 0.01% какого то заоптимизированного до безумия сервиса с желанием запустить его на одноплатнике это статистическая погрешность.

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

Т.е. по-вашему лучше ломать голову над правильной расстановкой async / await либо над правильной цепочкой асинхронных вызовов (aka callback hell) вместо того, чтобы писать простой и понятный код в синхронном стиле на Go?

Почитайте классику про async/await функции — journal.stuffwithstuff.com/...​t-color-is-your-function

Т.е. по-вашему лучше ломать голову

Пардоньте, но если расставить async/await перед декларацией функции и ее вызовом для кого то настолько тяжелая задача, что мозги у уходят в троттлинг, то всегда можно купить зеркалку и назваться фотографом :)

правильной расстановкой

Теряюсь в догадках, как можно неправильно расставить async/await?

синхронном стиле на Go?
чтобы писать простой и понятный код в синхронном стиле на Go?

а для джабаскриптеров, которые с js ним дольше 5 мин, он не простой и понятный, потому как у них перед декларацией функции async keyword, а в Go — go. Логично, на 2 буквы меньше набирать, и фиг там с этими синхронизациями потоков и затраты на них. Надеюсь, в Go там хоть пул потоков, а не каждый раз плодят и убивают их, а что там происходит когда потоков тысячи только остаётся догадывается.

Почитайте классику про async/await функции

Мне лень, читать очередные манифесты почему js неправильный, за 13 лет уже начитался, меняются только кошерные языки, к которым они в итоге подводят в концовке)

Мне лень, читать очередные манифесты почему js неправильный

Зря вы так — статья 2015 года. Тогда даже async / await еще не было, а статью про него уже написали :)

статья 2015 года. Тогда даже async / await еще не было

Да ну? :) Было. Спецификация появилась практически вместе с промисами и генераторами, абстракцией над которыми они и являются, в 2013-2014 году.

Не припомните, какие популярные среды исполнения js (браузеры, node.js) поддерживали async / await в 2015 году?

Не припомните, какие популярные среды исполнения js (браузеры, node.js) поддерживали async / await в 2015 году?

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

Только твой милион потоков так само исключительно для ио релейтед задач годится, то есть та же асинхронность.

Или ты думаешь что го тебе наколдует новые ядра?

В Go эта асинхронность скрыта от глаз программиста — там просто пишется линейный код без коллбэков, который проще понять, проще поддерживать, проще рефакторить и проще дебажить по сравнению с callback hell’ом в асинхронном коде.

который проще понять, проще поддерживать, проще рефакторить и проще дебажить

Абсолютно субъективное заявление. Проще для кого? Для разраба, который активно не пишет не js? Ну это как бы логично.

В Go эта асинхронность скрыта от глаз программиста

Да, настолько проще и скрыто от глаз, что «Multithreading» у Вас отдельным пунктом скиллах идет :D

Не юзайте колбеки и не будет никакого хелла. Программист принимает решение о том какие подходы использовать. Если он знает только херовые подходы то он просто джун или херовый программист.

Вы видели программу на node.js сложнее, чем hello world application, чтобы в ней не было каллбэков?

Там уже как бы все давным давно на async await, и callback никто не юзает

Node JS был создан как легкий для вхождения язык, дополняющий фронтендовый стек.

node.js це мова?

Поэтому наиболее рациональное решение — это разделить весь проект на автономные модули, которые обмениваются файлами (сервисами) с другими.

шта?

Классы vs. функциональный стиль

ru.wiktionary.org/wiki/функциональный

Когда вы раните ваш образ, npm не пробрасывает системные команды

Це ви так називаєте POSIX сигнали?

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

Навіщо говорити про різні стеки якщо автор вміє лише в node.js і все одно все буде писати на ноді? %)

При проектировании продукта всегда стоит подумать на пару шагов вперед: «А вдруг захотят такой функционал?». Или «А вдруг захотят хранить в БД иные данные?». Тогда Вы сможете продумать и применить более универсальный стек, который покроет не только текущие требования и «хотелки», но и будущие.

В чому проблема випитати в клієнта які в нього плани та які технічні вимоги до продукту?

Навіщо в статті про архітектуру огляд js фреймворків і розповіді про докер та різницю між SQL та NOSQL?

Коли побачив слово архітектура в заголовку, то очікував побачити щось цікавіше.

Монгадб ещё ставится сразу с нодой, чтоб сразу носкуль было без схемы для хипстеров на гироскутерах с вейпом?

Допустим, у вас была цепочка из трех микросервисов. Вы в один из них отправили объект, а он не пришел.

А сервисы под ноду в gRPC уже научились?

Даже голый protobuf. Так же рекомендую посмотреть на вот этот протокол: capnproto.org
На просторах гитхаба есть его реализация для ноды

Цікаво чи моніторите ви роботу своїх мікросервісів або монолітів за допомогою, наприклад, Prometheus + Grafana? Чи легко вам вдалось інтегрувати їх зі згаданими Node.js фреймворками?

Вот скажу за fastify, есть такое:
www.npmjs.com/package/fastify-metrics
работают „из коробки”.

Дякую. Ви користуєтесь цим модулем, є побажання щоб у ньому щось виправити? (Питаю, бо хочу написати відповідний модуль для свого фреймворка).

С метриками сложно угодить всем. Кому-то нужны стандартные метрики, кому-то на каждый чих (роут). Вот и выбираешь либо 100500 агрументов передать в конструктора и описывать каждый в доке, либо включить почти все и везде, ибо лишних метрик не бывает. В вышеупомянутой штуке сделано по второму принипу.

побажання щоб у ньому щось виправити?

Лично мне больше нравится вариант «универсальный», где метрики вешаются на хттп или тсп либу. А что там поверх за фреймворк уже пофиг. Как-то давно такое делал www.npmjs.com/...​ometheus-plugin-tcp-stats. Не лучший код, но в свое время с задачей справлялось отлично и работало со всем. Из минусов такого — адептам реста с идентификаторами ресурсов в урле метрики собрать тяжеловато :).

Мы юзаем StatsD клиент на ноде, он достаточно быстрый, потом Prometheus их парсит, и потом уже строим графики в Grafana. У нас много метрик, считаем количество успешных и неуспешных запросов HTTP, запросов в БД, сообщений с месседж брокеров.

Такі клієнти як StatsD мабуть не розрізняють, що запит прийшов із параметром в URL чи без. Тобто «/api/posts/1» і «/api/posts/2» це для нього два різні роути, так же?

Да, нужно будет вручную делать ключи для StatsD, например:
{имя приложения}.{имя метода который отработал}.{например айдишка взятая с url}.req_in.counter.
req_in.counter — эти две части могут быть любыми которые вы добавите в конфиг prometheus (mapping configuration), в моем примере это просто количество входящих запросов на какой-то конкретный эндпоинт с конкретными параметрами

Метрики из statsd можно отправлять напрямую в VictoriaMetrics без необходимости заводить промежуточный statsd_exporter — docs.victoriametrics.com/...​ble-agents-such-as-statsd

А оно в многопоточность умеет?

Ну можешь пару инстансов запустить как и в php, а так асинхронности хватает.

с конца 2019-го
worker_threads стабилизирован по моему еще в 12.11

Конечно есть, только большинству она не нужна, а тем кто думает что нужна то им надо что-то больше чем параллельный поток системы. Очень мало кейсов где она реально нужна.

В ноде еще и shared memory есть. И потоки могут обращаться к ней. Но придется реализовывать семафоры и мьютексы)

Ще пару років і джаваскріптери додумаються до рендерінгу HTML на сервері зразу, без реактів та гідрації

Рендерю на сервере чистыми функциями без всяких шаблонизаторов уже 2 года :) Кайф)

Це як? return `<span>${zalupa}</span>` ?

Типа такого + свои хелперы для лупов, safehtml.
Сначала может казаться не привычным, но:
1. Все естественным путем разбивается на логические компоненты (большие портянки кода банально не удобно будет писать через функции и оно само будет проситься делать нормально)
2. Нету ментального оверхеда и задрочки с реактами и всякими шаблонизаторами
3. реюзабельность компонентов, js tooling, дебагинг (!)
4. Конкретно в этом проекте рендер через ejs и mustache занимал около 15-30 ms и растет с ростом AST шаблонизатора., чистыми функциями около 0.3 ms
5. вся сила js (и с ним ответсвенность), не надо задрачиваться с хелперами шаблонизатора

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

*для подсветки и автокомплита в vscode юзаю плагин lit-html

Ну, в мене теж є один проект де я так само рендерінг роблю (маленький віджет який вбудовується на сайт), але це не те, чим мені хотілось би займатися :)

джаваскріптери додумаються до рендерінгу HTML на сервері зразу

Да это уже сто лет в обед как можно было делать на ноде... вопрос только — зачем?

Я пришёл в бэк-енд и Веб разработку в начале нулевых, когда других вариантов, кроме рендеринга HTML на сервере, попросту не было. И прекрасно помню выбор между полной угрёбищностью пользовательского опыта, если на каждый чих нужно перезагружать страницу, и болью разработки чего-то нетривиального с помощью javascript и jQuery.

Единственное более-вменяемое направление развития server-side rendering’а, которое я видел — это AJAX over Websockets. Реализовано, если не ошибаюсь, в фреймворке Phoenix под Elixir.

Хотя, технологически, на Ноде тоже вполне можно сделать такое. Другое дело, что мало кому нужно.

Так це і так було з самого початку ноди (всякі шаблонізатори типу handlebars, jade, etc).
А в cучасному світі можна і зі зручними фреймворками, наприклад Next.js вміє зрендерити усі сторінки з реакта в html під час білда і видавати ці закешовані html, а паралельно перевіряти чи оновилися дані і замінити закешовану версію на нову, таким чином сторінка відкривається швидше ніж із шаблонізаторами і при цьому працює динамічно

От картинок слезы выступают

Инженеры не плачут)
ПЫ.СЫ.: обновлю, спасибо за замечание

Подписаться на комментарии