Почему совместное использование WebAssembly и Rust улучшает производительность Node.js

Перевод | Автор оригинала: Josh Hannaford

Команда IBM добилась невероятных улучшений производительности с помощью WebAssembly и Rust.

Сталкивались ли вы или ваша команда когда-нибудь с ситуацией, когда скорость работы приложений и производительность разработчиков были невероятно важны, но требования бизнеса быстро менялись? Я работаю в группе IBM Garage Method, и моя команда оказалась именно в этой ситуации, когда мне было поручено найти решение и создать транспилятор для проверки концепции для файлов содержимого нашей команды.

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

Работа над этим инструментом все еще продолжается, и мы еще не полностью реализовали все, что является бизнес-требованиями.

Путешествие к Rust и WebAssembly

Бизнес-требования

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

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

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

Рекомендации по производительности

Одно из наших первых соображений по производительности было связано со скоростью: если мы ожидаем запускать это каждый раз, когда кто-то фиксирует свои изменения, процесс транспилирования должен быть быстрым и происходить в течение нескольких секунд. В одном из наших сценариев мы рассматривали возможность использования инструмента в качестве API, который преобразует контент и передает полученный результат обратно клиенту. Это решение значительно увеличивает производительность, поскольку некоторые из этих файлов сами по себе могут иметь размер более мегабайта. В этом случае файл будет отправлен, проанализирован, преобразован в новый формат, сжат, а затем отправлен клиенту через несколько секунд. Итак, скорость была главным соображением.

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

Наконец, нам нужен был язык, который можно было бы легко и эффективно интегрировать с нашей средой на основе Node.js. Наша команда оценила экосистему Node.js, и недавно отремонтированное приложение станет клиентом на основе Vue с серверной частью на основе Express, реализованной как REST API. Инструменты Node, сообщество с открытым исходным кодом и возможность писать наш клиент и сервер на одном языке (в нашем случае - Typescript) являются огромным преимуществом для гибкого графика поставки нашей команды. Мы знали, что должны сохранить эти удивительные преимущества для наших приложений, но мы также знали, что для получения необходимой производительности нам нужно искать в другом месте. Таким образом, отличная интеграция с нашими основными приложениями Node была абсолютной необходимостью.

После того, как мы поняли наши бизнес-требования, пришло время провести некоторые доказательства концепций.

Первое подтверждение концепции: Scala

Моим первоначальным мыслительным процессом был поиск языка со следующими свойствами:

Этот список требований изначально побудил меня взглянуть на Scala, поскольку он отмечал все перечисленные выше поля: хорошо зарекомендовавший себя язык поверх виртуальной машины Java (JVM), отличная поддержка функционального программирования, документация, которая показала, что он хорошо работает как язык синтаксического анализа и - со Scala.js - достойный способ работы с Node и JavaScript.

После первоначальной работы над модулем синтаксического анализа с помощью Scala я смог добиться приличного прогресса, даже в синтаксическом анализе основных правил нашего DSL (на основе Handlebars и Markdown). Однако, продолжая работу, я быстро обнаружил множество проблем с архитектурой.

К тому же разработка на Scala шла очень медленно. Тесты для разработки через тестирование каждый раз занимали более 5 минут, что значительно замедляло процесс. Он также терпел неудачу в категории производительности: базовые тесты синтаксического анализа занимали от 500–1000 мс в конфигурации разработки до 300–450 мс в конфигурации выпуска.

Примечание: я не являюсь в первую очередь разработчиком JVM, поэтому я уверен, что можно добиться лучшей производительности, но мы хотели что-то, что было бы производительным по умолчанию, а не взломать Scala или JVM, если бы нам не пришлось.

Второе подтверждение концепции: Rust

После того, как я обнаружил, что Scala не подходит для наших нужд, я начал исследовать другие альтернативы - и Rust был одним из первых в моем списке, потому что я работал с Rust в течение полутора лет. Rust установил все флажки, упомянутые в первом POC, и, что наиболее важно, имеет отличную поддержку WebAssembly. Сначала я прототипировал новую функциональность для парсера в Rust, а затем перевел ее на Scala одновременно с написанием тестов и тестов для обоих.

Я сразу заметил, что с Rust легче работать, чем со Scala, и можно быстро разрабатывать новые решения и правила синтаксического анализа. Я также обнаружил, что запуск модульных тестов и сам синтаксический анализ были намного быстрее. Я отправил образцы кода той же функциональности, написанные как на Scala, так и на Rust, некоторым членам моей команды и нашему руководителю разработки. Все они обнаружили, что код Rust намного легче понять, и именно тогда наш руководитель разработчиков одобрил перевод моих усилий на Rust в качестве второго POC.

Результаты для нашей команды были просто потрясающими! Для начала, я перенес всю нашу предыдущую работу с Scala на Rust всего за полторы недели. Спустя еще несколько недель я реализовал гораздо больше правил синтаксического анализа. На этом этапе мы хотели посмотреть, какое повышение производительности мы получили от Rust, и, опять же, результаты были ошеломляющими! Давайте предположим, какой процент увеличения производительности мы увидели в режиме выпуска? 200%? 400%? 800%? Нет, все догадки хорошие, но мы увидели невероятное увеличение нашей скорости на 1200-1500%! Мы перешли от 300-450 мсек в режиме выпуска с Scala с меньшим количеством реализованных правил синтаксического анализа до 25-30 мсек в Rust с большим количеством реализованных правил синтаксического анализа!

На этом этапе, благодаря способности Rust так легко интегрироваться с Node.js и нашей командой в целом, а также невероятному увеличению производительности, сделавшему возможными как интерфейс командной строки, так и возможный API, мы знали, что наконец-то получили выигрышную комбинацию.

Наше увеличение скорости на 1200-1500% может быть не нормой, но есть и другие примеры компаний, которые начали переходить на Rust, добившись 200% -ного прироста скорости по сравнению с Java и значительно снизив потребление памяти. Отличная статья и пример этого размещены на BitBucket и написаны разработчиком из Oviyum Technologies, которую можно найти здесь.

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

Введение в Rust

Итак, что же такое Rust? Rust - это относительно новый язык системного программирования, который был создан и до сих пор поддерживается Mozilla. Mozilla - это также компания, которая разрабатывает Firefox, и они используют Rust в производственном коде для своего браузера, что помогает им находить болевые точки в языке и повышать производительность и удобство использования Rust.

Несмотря на то, что Rust относительно новичок в области языков программирования (версия 1.0 была выпущена 15 мая 2015 года), разработчики и компании уже быстро приняли этот язык. Фактически, согласно ежегодному опросу Stack Overflow, Rust возглавляет список самых популярных языков каждый год с момента его выпуска 1.0. Это четыре года подряд! В опросе 2019 года Rust также поднялся на 6-е место в списке «Самые разыскиваемые», подчеркнув, что все больше разработчиков хотят, чтобы их компании приняли Rust и позволили им использовать его в своих проектах. Итак, что же предлагает Rust, что снискало ему столько любви и поддержки со стороны разработчиков?

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

  1. Производительность
  2. Безопасность
  3. Инструменты
  4. Сообщество

Давайте взглянем на некоторые особенности каждой из этих категорий, чтобы понять, почему Rust так популярен!

Производительность

Безопасность

Инструменты

Сообщество

Теперь, когда вы увидели, что делает Rust таким уникальным предложением, давайте посмотрим на WebAssembly и на то, как он меняет правила веб-разработки!

Введение в WebAssembly

WebAssembly стремится быть легко переносимым как в Интернете, так и за его пределами, и в то же время работать со скоростью, близкой к скорости собственных приложений или с такой скоростью. Эта цель включает в себя такие платформы, как мобильные устройства и Интернет вещей, которые в будущем откроют некоторые захватывающие возможности для совместного использования кода между платформами. Другая важная цель - хорошо интегрироваться с текущими веб-технологиями, а именно с JavaScript, а также с существующими инструментами разработки, такими как инструмент «Просмотр исходного кода» и инструменты отладки.

Эти цели указывают на то, что Wasm не пытается заменить JavaScript, а скорее дополняет его, предоставляя способ переноса более зрелых библиотек в Интернет и предоставляя простой способ выполнения дорогостоящих в вычислительном отношении операций в более быстрой среде. Группа Wasm ориентирована на рост как платформу, что означает, что она предлагает первоклассную поддержку для таких инструментов, как:

Из-за целей Wasm и их выполнения 5 декабря 2019 года W3C объявил, что Wasm станет четвертым официальным языком Интернета. Это невероятное развитие, поскольку теперь Wasm объединяет HTML, CSS и JavaScript в качестве официального веб-стандарта, который поддерживается всеми W3C-совместимыми браузерами. Поскольку W3C официально признает Wasm, это значительно снижает риск принятия этой новой технологии, особенно после того, как версия 1.0 Wasm была запущена в браузерах Firefox, Chrome, Safari и Edge.

Итак, при всем этом волнении и потрясающем потенциале, каковы реальные варианты использования Wasm? Ознакомьтесь с неполным списком Wasm, чтобы увидеть более широкое представление, но вот список меньшего размера, который я лично видел или с которым работал:

Если вам нужна дополнительная информация о самом Wasm, я настоятельно рекомендую просмотреть https://webassembly.org, так как там есть невероятное количество информации о проекте. А пока давайте посмотрим, как мы взаимодействуем с Wasm через Rust и как работает этот набор инструментов.

Объединение Rust и Wasm

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

Стабильный и простой инструмент для взаимодействия

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

Некоторые из тех инструментов, которые вы обычно будете использовать, включают следующее:

Обратите внимание, что существует только один инструмент, специфичный для Rust, а все остальные являются общими инструментами Wasm. Вы не поверите, но на самом деле это огромное преимущество! Этот единственный инструмент делает все, что вам нужно, в отличие от многих других языков, где вам нужно использовать несколько инструментов или ручные процессы.

Давайте подробнее рассмотрим, что делает wasm-pack. wasm-pack оптимизирует размер вашего двоичного файла, генерирует файлы определений TypeScript для облегчения взаимодействия и помощи редактору, а также создает файл package.json, который вы можете использовать в качестве модуля npm с самого начала! Каждый из этих шагов будет отдельным инструментом или даже ручным процессом с таким инструментом, как emscripten для C/C++. Такие языки, как Go, даже не имеют автоматизированного способа создания файлов определений TypeScript. Отсутствие файлов определений также значительно затрудняет обнаружение того, что экспортируется и доступно в двоичном пакете.

Эффективность

Еще одним важным преимуществом использования Rust с Wasm является скорость выполнения и размер двоичного файла. Оба они сопоставимы или даже немного лучше, чем другие низкоуровневые языки без сборки мусора, такие как C и C++. Напротив, даже относительно небольшой язык среды выполнения, такой как Go, имеет двоичный размер hello world 2 МБ после компиляции в .wasm.

Размер двоичного файла hello world в Rust составляет всего 1,46 КБ после компиляции в .wasm. Вы также увидите пропорциональную скорость выполнения нативным приложениям при компиляции в .wasm, поэтому хорошо написанный Rust / C/C++ по-прежнему будет превосходить хорошо написанный Java / Go / Python. Извините, что разрушаю любые представления о том, что Python становится до смешного быстрым!

Переносимость

Последний момент, который я хотел коснуться, - это то, насколько переносимым этот стек технологий может сделать ваш код Rust. Прекрасным примером является проект, над которым я работал для своей команды. Он начинался как инструмент командной строки, предназначенный для запуска на этапе сборки. Поскольку бизнес-требования изменились, нам понадобился способ перевести этот инструмент в формат API. Мы смогли легко справиться с этими меняющимися требованиями, зная, что мы можем написать весь наш API на Rust, или мы можем использовать только основные функции библиотеки и обернуть их в настройку на основе Node.js, чтобы помочь нашему Node.js Разработчики.

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

Резюме

Wasm не был разработан для полной замены JavaScript в Интернете ни на обычных страницах, ни на узлах, а скорее как средство дополнения, когда скорость JavaScript недостаточна. С такими инструментами, как wasm-pack, которые упрощают интеграцию Rust с Node, вы можете очень легко постепенно адаптировать Rust и Wasm для ваших наиболее критичных к производительности рабочих нагрузок, а затем вы можете решить, хотите ли вы перенести и другие области. Такая универсальность с постоянно меняющимися потребностями - именно поэтому мы выбрали Rust и Wasm для нашего проекта!

Я надеюсь, что эта статья вдохновила вас взглянуть, есть ли в вашей команде место для Rust или Wasm! Если в прошлом вы избегали Node.js, особенно из соображений производительности, теперь есть решение этой проблемы, и, к счастью, это очень бесшовная интеграция.

Если вам нужна дополнительная информация о Rust и Wasm, я настоятельно рекомендую ознакомиться с их официальной книгой, которая проведет вас через создание игры жизни Конвея на Rust и Wasm и покажет, как оптимизировать и профилировать ваше решение. Вы можете найти эту книгу здесь. Обязательно следите за IBM Developer, чтобы узнать о новых материалах по совместному использованию Rust и Wasm. В моей следующей статье мы рассмотрим, что нужно знать разработчикам Node, чтобы добиться успеха с Rust!