Node.js и нулевые серверы

Этот пост изначально был размещен в блоге Torii

В Torii мы используем Node.js + Serverless в производственной среде в течение последних 4 лет. Вот краткое изложение того, почему мы выбрали этот стек для нашего стартапа, и что мы узнали.

Выбор технологического стека

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

  • Совместимость с полным стеком. Кросс-платформенный язык, который позволит нашим разработчикам быть действительно эффективными.
  • Экосистема. Перспективный стек, который находится на подъеме и будет актуален как минимум в ближайшие несколько лет.
  • Производительность. Сведите к минимуму операции и позвольте нам сосредоточиться на продукте.

Имея это в виду, в первую очередь были выбраны JavaScript и Node.js. Система позволяет разработчикам стать экспертами по JS и работать над внутренним и внешним интерфейсом. Экосистема Node.js огромна, и на рынке доступны огромные таланты. Мы также выбрали React для интерфейса, но это уже для другой публикации.

Что касается развертывания, мы решили перепрыгнуть через пространство Docker / Kubernetes и сделали ставку на бессерверные вычисления. Я говорю ставку, потому что мы не слышали о многих компаниях, использующих бессерверные функции. Обещание - это высокодоступная, бесконечно масштабируемая и безопасная среда без хлопот, отнимающих много времени.

Мы выбрали AWS, поскольку он стал пионером в области FaaS (функция как услуга) и предоставляет все необходимые строительные блоки: функции (AWS Lambda), HTTP-шлюз (API Gateway ) и фоновое планирование (события Cloudwatch).

Вариант 1: Node.js + бессерверная версия для фоновых заданий

Наш первый вариант использования - выполнение фоновых заданий. Нам нужно запускать задания для отправки запланированных электронных писем, синхронизации с внешними API и анализа наших данных.

Для фоновых заданий лучше всего подходят Node.js и FaaS. Это позволяет нам определять функцию, которая запускается по расписанию или по запросу. Мы используем AWS и можем настроить любую функцию для работы по расписанию, подобному cron, инициированному AWS.

Возьмем пример: мы хотим отправлять клиентам электронные письма каждый день в 22:00 с дайджестом новостей в их аккаунтах. Мы определяем простую функцию:

const handler = (e, ctx) => {
  const { customer } = e
  const updates = await getUpdates({ customer })
  await mailer.sendDigest({ updates }}
  return { success: true }
}

Определите файл serverless.yml:

func:
  handler: index.handler
  event:
    daily 10pm

Вот и все. Наша функция будет запускаться каждый день в 22:00 после развертывания на AWS (простое бессерверное развертывание). Быстро и просто.

Вариант использования 2: Node.js + Serverless для серверов API (hapi-front)

Наш второй вариант использования - предоставление сервера API, обслуживающего клиентов веб-приложения.

Создание сервера API на бессерверной основе звучит менее интуитивно. Однако сервер API можно рассматривать как простую функцию. Он получает HTTP-запрос в качестве входных данных, обрабатывает его и в результате возвращает HTTP-ответ.

Что мы делаем, так это используем обычный сервер Node.js (мы используем фреймворк hapi.js, который похож на express, но мы считаем, что он лучше подходит для больших проектов) и упаковываем его в функцию Lambda.

Вместо того, чтобы запускать эту функцию по расписанию, мы запускаем ее каждый раз, когда на наш сервер отправляется HTTP-запрос. Мы используем шлюз AWS API Gateway для прослушивания HTTP-запросов и их пересылки на нашу функцию сервера API, которая обернута hapi-front, тонким слоем с открытым исходным кодом поверх hapi.js.

hapi-front - это тонкий слой, который преобразует события шлюза API в сетевые запросы hapi.js, https://github.com/toriihq/hapi-frontnd из ответов сети hapi.js на ответы шлюза API. em>

hapi.js предоставляет функцию для внедрения запросов, которая в основном используется для реализации модульных и интеграционных тестов. Мы подключились к этой функции и внедряем сетевой запрос всякий раз, когда API Gateway предоставляет событие. Это позволяет нам запускать сервер локально, как обычно, без каких-либо изменений, используя hapi-front при развертывании на AWS.

Node.js: бессерверные и реальные серверы

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

В бессерверном режиме мы получаем один «сервер» на каждый клиентский запрос. Да, вы получаете полноценный сервер Node.js для обработки одного запроса. Когда вы выполняете асинхронные операции, такие как чтение из базы данных, цикл событий может быть пустым, и сервер больше не принимает запросы от клиента. Похоже, мы теряем преимущество Node, заключающееся в возможности обрабатывать несколько запросов на одном сервере.

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

Еще одно отличие состоит в том, что с постоянным «обычным» сервером мы можем сначала ответить на запрос, а затем обработать его:

res.send(200)
await doSomethingThatTakesTime(options)

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

await lambda.invoke('doSomethingThatTakesTime', options)
res.send(200)

Мифы о бессерверности

Бессерверные вычисления предназначены только для игры или «склеивания» кодовых путей. Хотя бессерверные вычисления отлично подходят для объединения работы других серверов, их определенно можно использовать в качестве основной инфраструктуры. Наш продукт обслуживает сотни клиентов без постоянного сервера. Бессерверное предложение AWS и других облачных провайдеров находится на переднем крае и постоянно улучшается.

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

Бессерверная версия стоит дороже. С одной стороны, управляемая инфраструктура, такая как бессерверная, стоит дороже, но с другой стороны, вы платите только за то, что используете. Если ваш сервис требует высокой доступности и возможности масштабирования, затраты могут быть равными. С учетом сокращенного времени DevOps бессерверное использование может быть дешевле, чем запуск и обслуживание серверов.

Бессерверная версия не может работать локально. Бессерверная функция - это функция, поэтому ее просто запустить локально. Мы просто запускаем функцию. По сути, это все, что нужно, вам даже не понадобится фреймворк, поскольку простой узел index.js эквивалентен запуску функции на AWS.

Ограничения: неочевидные преимущества

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

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

  • Лимит тайм-аута → Оптимизация времени. Функции ограничены 15 минутами (на данный момент) и будут остановлены по истечении тайм-аута. Когда мы видим, что функции достигают тайм-аута, мы решаем проверить и оптимизировать код. Мы даже видели, как разработчик сократил код с 10 минут до 5 секунд (!) После изучения неоптимизированной зависимости npm.
  • Предел тайм-аута → Продолжительность. Некоторым рабочим нагрузкам требуется больше времени для выполнения, чем позволяет функция. В этом случае мы сделали код «продолжаемым», где мы можем продолжить работу с того же места, где он остановился, снова запустив функцию. Это практически снимает любые ограничения на время выполнения и дает дополнительное преимущество, позволяя нам продолжать работу над ошибками, не теряя того, что уже было выполнено.
  • Ограничение памяти → Оптимизация ресурсов. При возникновении проблемы с памятью мы оптимизируем код вместо того, чтобы сразу же выбирать более крупные машины. Вместо того, чтобы загружать все данные в память для вычисления, мы загружаем необходимый рабочий набор и освобождаем то, что уже было вычислено.
  • Лимит данныхЛучшее использование ресурсов. Существует ограничение на объем данных, которые функции Lambda могут обрабатывать и на которые могут отвечать (на данный момент 10 МБ). Мы упираемся в кирпичную стену, когда пользователи пытались загружать или загружать большие файлы. Решение заключалось в том, чтобы пользователь работал напрямую со службой S3, которая хранит файлы. AWS позволяет делать это, сохраняя безопасность, подписывая запросы к S3. Это позволяет пользователю загружать и скачивать файлы быстрее и снимать ограничения.

Оптимизация для бессерверной версии

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

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

Для этого мы используем webpack. Помимо объединения нашего кода и возможности использовать новейшие функции JavaScript (с помощью babel), мы также получаем преимущество анализа веб-пакетов, в котором зависимости фактически импортируются в каждую функцию. Это позволяет нам упаковать каждую функцию индивидуально, и хотя на этапе сборки требуется больше времени, мы получаем преимущество более быстрой загрузки функций.

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

Будущее серверов API Node.js без серверов

Как бы выглядела серверная структура API, если бы она была построена полностью для бессерверной архитектуры. Фреймворк, ориентированный на будущее, мог бы иметь:

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

Заключение

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

Начать очень просто, а экосистема бессерверных инструментов помогает удовлетворить все общие потребности.