Часть 1 Веб-серверы и сервисы
Автор: Prabhu Eshwarla
Rust - отличный язык программирования, который в наши дни пользуется большой популярностью. Изначально он рекламировался как язык системного программирования, наряду с другими известными языками, такими как C или Go (lang). Действительно, он постепенно проникает в ядро Linux: в настоящее время он ограничен драйверами и модулями, но его неотъемлемые качества — в основном выразительность, безопасность памяти и производительность — несомненно, откроют двери для более важных частей операционной системы. Более медленными темпами Rust также проникает во все еще конфиденциальную область веб-сборки (WAS), в браузере или в бессерверном облаке.
Как и в случае с Go, разработчики-новаторы показали, что применимость Rust выходит за рамки системного программирования и что его можно использовать, например, для разработки эффективных серверных частей веб-приложений, поддерживаемых базами данных.
В этой первой части книги мы разработаем простое, но представительное веб-приложение с использованием веб-служб REST, поддерживаемое реляционной базой данных. Мы пока не будем рассматривать аспекты пользовательского интерфейса, они будут рассмотрены во второй части книги. В этой части книги мы создадим основы для нашего веб-приложения, думая масштабно, но начиная с малого. Затем мы рассмотрим более специализированные темы, такие как сохранение базы данных, обработка ошибок, обслуживание и рефакторинг API.
После завершения этой части вы сможете настраивать и разрабатывать надежные серверные части приложений, включая маршрутизацию и обработку ошибок, используя Rust и несколько проверенных в полевых условиях ящиков (crates). После этого вы будете готовы приступить ко второй части.
1. Почему Rust подходит для веб-приложений?
Подключенные веб-приложения, работающие через Интернет, формируют основу современного бизнеса и цифровой жизни людей. Как частные лица, мы используем ориентированные на потребителя приложения для социальных сетей и коммуникаций, для покупок в электронной коммерции, для бронирования поездок, для осуществления платежей и управления финансами, для получения образования и для развлечения - вот лишь некоторые из них. Аналогичным образом, бизнес-ориентированные приложения используются практически во всех функциях и процессах предприятия.
Современные веб-приложения представляют собой невероятно сложные распределенные системы. Пользователи этих приложений взаимодействуют через веб- или мобильные интерфейсы. Но пользователи редко видят сложную среду серверных служб и компонентов программной инфраструктуры, которые отвечают на запросы пользователей, поступающие через простые пользовательские интерфейсы приложений. Популярные потребительские приложения содержат тысячи внутренних сервисов и серверов, распределенных в центрах обработки данных по всему миру. Каждая функция приложения может быть запущена на другом сервере, реализована с использованием другого дизайна, написана на другом языке программирования и расположена в другом географическом месте. Благодаря удобному взаимодействию с пользователем в приложении все выглядит очень просто. Но разрабатывать современные веб-приложения совсем не просто.
Мы используем веб-приложения каждый раз, когда пишем в Твиттере, смотрим фильм на Netflix, слушаем песню на Spotify, бронируем поездку, заказываем еду, играем в онлайн-игру, вызываем такси или пользуемся любым из многочисленных онлайн-сервисов в повседневной жизни. Без распределенных веб-приложений бизнес и современное цифровое общество пришли бы к полной остановке.
Веб-сайты предоставляют информацию о вашем бизнесе.
Веб-приложения предоставляют услуги вашим клиентам.
Из этой книги вы узнаете о концепциях, методах и инструментах, которые понадобятся вам при использовании Rust для проектирования и разработки распределенных веб-сервисов и приложений, взаимодействующих по стандартным интернет-протоколам. Попутно вы увидите основные концепции Rust в действии на практических примерах.
Эта книга для вас, если вы являетесь инженером-программистом веб-серверной части, разработчиком приложений с полным стеком, облачным или корпоративным архитектором, техническим директором технологического продукта или просто любопытным слушателем, заинтересованным в создании распределенных веб-приложений, которые являются невероятно безопасными, эффективными, высокопроизводительными и выполняют следующие функции: не требует непомерных затрат на эксплуатацию и техническое обслуживание. Разрабатывая рабочий пример по ходу чтения этой книги, я покажу вам, как создавать веб-сервисы и традиционные интерфейсы веб-приложений в чистом виде в Rust.
Как вы увидите из последующих глав, Rust - это язык общего назначения, который эффективно поддерживает разработку множества различных типов приложений. В этой книге представлено одно приложение, но продемонстрированные методы применимы ко многим другим ситуациям с использованием тех же или других ящиков (crates) (библиотека в терминологии Rust называется ящиком (crate)).
В этой главе мы рассмотрим ключевые характеристики распределенных веб-приложений, поймем, как и где работает Rust, и опишем пример приложения, которое мы будем создавать вместе в этой книге.
1.1. Знакомство с современными веб-приложениями
Мы начнем с рассмотрения структуры современных распределенных веб-приложений. В распределенных системах есть компоненты, которые могут быть распределены по нескольким различным вычислительным процессорам, взаимодействовать по сети и одновременно выполнять рабочие нагрузки. Технически ваш домашний компьютер напоминает сетевую распределенную систему (учитывая современные многопроцессорные и многоядерные процессоры).
К популярным типам распределенных систем относятся:
Распределенные сети, такие как телекоммуникационные сети и Интернет.
Распределенные клиент-серверные приложения. (Большинство веб-приложений относятся к этой категории)
Распределенные P2P-приложения, такие как BitTorrent и Tor.
Системы управления в режиме реального времени, такие как управление воздушным движением и промышленностью.
Распределенные серверные инфраструктуры, такие как облачные, сетевые и другие формы научных вычислений.
Распределенные системы в широком смысле состоят из трех частей: распределенных приложений, сетевого стека и аппаратной инфраструктуры и операционной системы.
Распределенные приложения могут использовать широкий спектр сетевых протоколов для внутренней связи между своими компонентами. Однако сегодня HTTP является подавляющим выбором для веб-сервиса или веб-приложения, взаимодействующих с внешним миром, благодаря своей простоте и универсальности.
Веб-приложения - это программы, использующие HTTP в качестве протокола прикладного уровня и предоставляющие функциональность, доступную обычным пользователям через стандартные интернет-браузеры. Когда веб-приложения не являются монолитными, а состоят из десятков или сотен распределенных прикладных компонентов, которые взаимодействуют и обмениваются данными по сети, они называются распределенными веб-приложениями. Примерами крупномасштабных распределенных веб-приложений являются приложения для социальных сетей, такие как Facebook и X, сайты электронной коммерции, такие как Amazon и eBay, приложения для совместного использования, такие как Uber и Airbnb, развлекательные сайты, такие как Netflix, и даже удобные в использовании облачные приложения от таких поставщиков, как AWS, Google и Azure.
На рисунке 1.1 представлено логическое представление стека распределенных систем для современного веб-приложения. В реальном мире такие системы могут быть распределены по тысячам серверов, но на рисунке вы можете видеть три сервера, подключенных через сетевой стек. Все эти серверы могут находиться в одном центре обработки данных или быть географически распределены в облаке. На каждом сервере показано многоуровневое представление аппаратных и программных компонентов.
[]
Рисунок 1.1 Упрощенный стек распределенных систем для приложения социальных сетей
Аппаратные компоненты и инфраструктура операционной системы — это такие компоненты, как физические серверы (в центре обработки данных или облаке), операционная система и средства виртуализации или контейнерные среды выполнения. Такие устройства, как встроенные контроллеры, датчики и периферийные устройства, также могут быть отнесены к этому уровню (представьте себе футуристический случай, когда подписчикам сети супермаркетов в социальных сетях отправляются твиты, когда товары с RFID-маркировкой добавляются на полки супермаркетов или убираются с них).
Сетевой стек — Сетевой стек включает в себя четырехуровневый набор интернет-протоколов, который формирует коммуникационную магистраль для компонентов распределенной системы, позволяя им взаимодействовать друг с другом через физическое оборудование. Четырьмя сетевыми уровнями (упорядоченными от низшего к высшему уровню абстракции) являются
Уровень сетевых соединений/доступа
Уровень Интернета
Транспортный уровень
Прикладной уровень
Первые три уровня обычно реализуются на аппаратном уровне или на уровне операционной системы. Для большинства распределенных веб-приложений HTTP является основным используемым протоколом прикладного уровня. Популярные протоколы API, такие как REST, gRPC и GraphQL, используют HTTP. Более подробную информацию о Internet Protocol suite смотрите в документации по адресу https://tools.ietf.org/id/draft-baker-ietf-core-04.html.
Распределенные приложения — Распределенные приложения представляют собой подмножество распределенных систем. Современные многоуровневые распределенные приложения создаются как комбинация следующих компонентов:
Интерфейсы приложений — это могут быть мобильные приложения (работающие на iOS или Android) или веб-интерфейсы, работающие в интернет-браузере. Эти интерфейсы взаимодействуют с серверными службами приложений, расположенными на удаленных серверах (обычно в центре обработки данных или облачной платформе). Конечные пользователи взаимодействуют с интерфейсами приложений.
Серверные части приложений — они содержат бизнес-правила приложений, логику доступа к базе данных, сложные вычислительные процессы, такие как обработка изображений или видео, и другие интегрированные сервисы. Они развертываются как отдельные процессы (например, процессы systemd в Unix/Linux), работающие на физических или виртуальных машинах, или как микросервисы в контейнерных движках (таких как Docker), управляемых контейнерными средами оркестровки (такими как Kubernetes). В отличие от интерфейсов приложений, серверные части приложений предоставляют свои функциональные возможности через интерфейсы прикладного программирования (API). Интерфейсы приложений взаимодействуют с серверными службами приложений для выполнения задач от имени пользователей.
Распределенная программная инфраструктура — это компоненты, которые предоставляют вспомогательные услуги для серверных частей приложений. Примерами могут служить серверы протоколов, базы данных, хранилища ключей/значений, кэширование, обмен сообщениями, балансировщики нагрузки и прокси-серверы, платформы обнаружения служб и другие подобные компоненты инфраструктуры, используемые для связи, операций, обеспечения безопасности и мониторинга распределенных приложений. Серверные части приложений взаимодействуют с распределенной программной инфраструктурой для обнаружения сервисов, связи, поддержки жизненного цикла, обеспечения безопасности, мониторинга и так далее.
Теперь, когда вы ознакомились с распределенными веб-приложениями, давайте рассмотрим преимущества использования Rust для их создания.
1.2. Выбор Rust для веб-приложений
Rust можно использовать для создания всех трех уровней распределенных приложений: интерфейсов, серверных служб и компонентов программной инфраструктуры. Но каждый из этих уровней решает свой набор проблем и характеристик. Важно помнить об этом при обсуждении преимуществ Rust.
Например, клиентские интерфейсы занимаются такими аспектами, как дизайн пользовательского интерфейса, взаимодействие с пользователем, отслеживание изменений в состоянии приложения и отображение обновленных представлений на экране, а также построение и обновление объектной модели документа (DOM).
Серверным службам необходимы хорошо разработанные API для сокращения количества обращений туда и обратно, высокая пропускная способность (измеряемая в запросах в секунду), короткое время отклика при различных нагрузках, низкая и предсказуемая задержка для таких приложений, как потоковое видео и онлайн-игры, низкий объем памяти и ресурсов процессора, обнаружение сервисов и доступность.
Уровень программной инфраструктуры в первую очередь связан с чрезвычайно низкими задержками, низкоуровневым управлением сетевыми и другими ресурсами операционной системы, экономичным использованием центрального процессора и памяти, эффективными структурами данных и алгоритмами, встроенной защитой, коротким временем запуска и выключения, а также эргономичными API для внутренних служб приложений.
Как вы можете видеть, одно веб-приложение состоит из компонентов, обладающих как минимум тремя наборами характеристик и требований. Хотя каждый из них сам по себе мог бы стать темой отдельной книги, мы рассмотрим все более целостно и сосредоточимся на наборе общих характеристик, которые в целом приносят пользу всем трем уровням веб-приложения.
1.2.1. Характеристики веб-приложений
Веб-приложения могут быть разных типов:
Критически важные приложения, такие как автономное управление транспортными средствами и интеллектуальными сетями, промышленная автоматизация и высокоскоростные торговые приложения, в которых успешные сделки зависят от способности быстро и надежно реагировать на вводимые события
Инфраструктура для проведения крупных транзакций и обмена сообщениями, такая как платформы электронной коммерции, социальные сети и розничные платежные системы
Приложения, работающие практически в режиме реального времени, такие как серверы онлайн-игр, обработка видео или аудио, видеоконференции и инструменты для совместной работы в режиме реального времени
К этим приложениям предъявляется общий набор требований:
Должно быть безопасным и безотказным
Должно быть ресурсосберегающим
Должны минимизировать задержку
Должны поддерживать высокий уровень параллелизма
Кроме того, к таким сервисам предъявляются следующие требования:
Они должны обеспечивать быстрое время запуска и завершения работы
Должны быть просты в обслуживании и реорганизации
Должны обеспечивать производительность разработчика
Все эти требования могут быть выполнены как на уровне отдельных сервисов, так и на архитектурном уровне. Например, отдельный сервис может обеспечить высокий уровень параллелизма за счет использования многопоточности или асинхронного ввода-вывода. Аналогичным образом, высокий уровень параллелизма может быть достигнут на архитектурном уровне путем добавления нескольких экземпляров службы за балансировщиком нагрузки для обработки параллельных нагрузок. Когда мы говорим о преимуществах Rust в этой книге, мы рассматриваем индивидуальный уровень обслуживания, поскольку параметры архитектурного уровня являются общими для всех языков программирования.
1.2.2. Преимущества Rust для веб-приложений
Вы уже видели, что современные веб-приложения включают в себя веб-интерфейсы, серверные части и программную инфраструктуру. Преимущества Rust для разработки веб-интерфейсов, как для замены, так и для дополнения частей кода JavaScript, являются актуальной темой в наши дни. Однако мы не будем обсуждать их в этой книге, поскольку эта тема достаточно обширна для отдельной книги.
Здесь мы сосредоточимся в первую очередь на преимуществах Rust для серверной части приложений и служб программной инфраструктуры. Rust отвечает всем критическим требованиям, которые мы определили в предыдущем разделе для таких сервисов. Давайте посмотрим, как это сделать.
RUST БЕЗОПАСЕН!
Когда мы говорим о безопасности программ, необходимо учитывать три различных аспекта: безопасность типов, безопасность памяти и потокобезопасность.
Что касается безопасности типов, то Rust - это язык со статической типизацией. Проверка типов, которая проверяет и применяет ограничения на типы, выполняется во время компиляции, поэтому типы переменных должны определяться во время компиляции. Если вы не укажете тип переменной, компилятор попытается вычислить ее сам. Если он не сможет этого сделать или обнаружит конфликты, он сообщит вам об этом и не позволит продолжить. В этом контексте Rust похож на Java, Scala, C и C++. Компилятор очень строго следит за безопасностью типов в Rust, но при этом выдает полезные сообщения об ошибках. Это помогает устранить целый класс ошибок во время выполнения.
Безопасность памяти, пожалуй, является одним из самых уникальных аспектов языка программирования Rust. Чтобы отдать должное этой теме, давайте проанализируем ее более подробно.
Основные языки программирования можно разделить на две группы в зависимости от того, как они обеспечивают управление памятью. В первую группу входят языки с ручным управлением памятью, такие как C и Си++. Вторая группа включает языки со сборщиком мусора, такие как Java, C#, Python, Ruby и Go.
Поскольку разработчики несовершенны, ручное управление памятью означает принятие определенной степени риска и, следовательно, некорректность программы. Таким образом, для языков, где нет необходимости в низкоуровневом управлении памятью и максимальная производительность не является основной целью, сборка мусора стала основной функцией за последние 20-25 лет. Сборка мусора сделала программы более безопасными, чем ручное управление памятью, но она сопряжена с ограничениями в плане скорости выполнения, потребления дополнительных вычислительных ресурсов и возможной остановки выполнения программы. Кроме того, сборка мусора имеет дело только с памятью, а не с другими ресурсами, такими как сетевые сокеты и дескрипторы базы данных.
Rust — первый популярный язык, предложивший альтернативу - автоматическое управление памятью и обеспечение ее безопасности без сбора мусора. Как вы, вероятно, знаете, это достигается за счет уникальной модели владения. Rust позволяет разработчикам управлять расположением памяти в своих структурах данных и делает владение явным. Модель управления ресурсами в Rust основана на RAII (получение ресурсов — это инициализация) — концепции программирования на C++ - и интеллектуальных указателях, которые обеспечивают безопасное использование памяти.
В этой модели каждому значению, объявленному в программе Rust, присваивается владелец. Как только значение передается другому владельцу, оно больше не может использоваться первоначальным владельцем. Значение автоматически уничтожается (память освобождается), когда владелец значения выходит за пределы области видимости.
Rust также может предоставлять временный доступ к значению, другой переменной или функции. Это называется заимствованием. Компилятор Rust (в частности, средство проверки заимствования) гарантирует, что ссылка на значение не переживет заимствуемое значение. Для заимствования значения используется оператор & (называемый ссылкой). Ссылки бывают двух типов: неизменяемые ссылки, &T, которые допускают совместное использование, но не мутацию, и изменяемые ссылки, &mut T, которые допускают мутацию, но не совместное использование. Rust гарантирует, что всякий раз, когда происходит изменяемое заимствование объекта, другие заимствования этого объекта (как изменяемые, так и неизменяемые) исключаются. Все это применяется во время компиляции, что приводит к устранению целых классов ошибок, связанных с недопустимым доступом к памяти.
Подводя итог, можно сказать, что вы можете программировать в Rust, не опасаясь недопустимого доступа к памяти и без использования сборщика мусора. Rust предоставляет гарантии во время компиляции для предотвращения следующих категорий ошибок, связанных с безопасностью памяти:
Разыменование нулевого указателя приводит к сбою программы из-за того, что разыменованный указатель имеет значение null.
Ошибки сегментации, при которых программы пытаются получить доступ к ограниченной области памяти.
Висячие указатели, при которых значение, связанное с указателем, больше не существует.
Переполнение буфера происходит из-за того, что программы обращаются к элементам до начала или за пределами конца массива. Итераторы Rust не выходят за пределы допустимых значений.
В Rust безопасность памяти и потокобезопасность (которые кажутся двумя совершенно разными проблемами) решаются с использованием одного и того же основополагающего принципа владения. Что касается безопасности типов, Rust по умолчанию гарантирует отсутствие неопределенного поведения из-за скачков данных. В то время как некоторые языки веб-разработки могут предлагать аналогичные гарантии, Rust идет еще дальше и не позволяет вам совместно использовать объекты, которые не являются потокобезопасными, между потоками. Rust помечает некоторые типы данных как потокобезопасные и применяет их для вас. Большинство других языков не проводят такого различия между потокобезопасными и потокобезопасно небезопасными структурами данных. Компилятор Rust категорически предотвращает все виды скачков данных, что делает многопоточные программы намного безопаснее.
Вот несколько рекомендаций для более глубокого погружения в вопросы безопасности в Rust:
Функции отправки и синхронизации: http://mng.bz/Bmzl
Бесстрашный параллелизм с Rust: http://mng.bz/d1W1
В дополнение к тому, что мы уже обсуждали, есть несколько других функций Rust, которые повышают безопасность программ:
Все переменные в Rust по умолчанию неизменяемы, и перед изменением любой переменной требуется явное объявление. Это заставляет разработчиков продумывать, как и где изменяются данные и каков срок службы каждого объекта.
Модель владения Rust обеспечивает не только управление памятью, но и управление переменными, которым принадлежат другие ресурсы, такие как сетевые сокеты, дескрипторы баз данных и файлов, а также дескрипторы устройств.
Отсутствие сборщика мусора предотвращает недетерминированное поведение.
Предложения Match (которые эквивалентны операторам Switch в других языках) являются исчерпывающими, что означает, что компилятор заставляет разработчика обрабатывать все возможные варианты в операторе match. Это предотвращает непреднамеренное упущение разработчиками определенных путей обработки кода, что может привести к непредвиденному поведению во время выполнения.
Наличие алгебраических типов данных упрощает представление модели данных в сжатой и поддающейся проверке форме.
Статически типизированная система Rust, модель владения и заимствования, отсутствие сборщика мусора, неизменяемые значения по умолчанию и полное сопоставление с образцом, все это обеспечивается компилятором, дает Rust неоспоримые преимущества при разработке безопасных приложений.
RUST ЯВЛЯЕТСЯ РЕСУРСОСБЕРЕГАЮЩИМ
Системные ресурсы, такие как процессор, память и дисковое пространство, с годами постепенно дешевеют. Хотя это оказалось очень полезным при разработке и масштабировании распределенных приложений, у него также есть несколько недостатков. Во-первых, среди разработчиков программного обеспечения существует общая тенденция просто использовать больше аппаратного обеспечения для решения задач масштабируемости — больше процессора, больше памяти и больше дискового пространства. Это достигается либо за счет увеличения ресурсов процессора, памяти и диска на сервере (вертикальное масштабирование, также известное как масштабирование) или путем добавления большего количества компьютеров в сеть для распределения нагрузки (горизонтальное масштабирование, или уменьшение масштаба).
Одной из причин, по которой эти подходы стали популярными, является ограниченность современных основных языков веб-разработки. Языки веб-разработки высокого уровня, такие как JavaScript, Java, C#, Python и Ruby, не позволяют осуществлять детальный контроль памяти для ограничения ее использования. Многие языки программирования плохо используют многоядерные архитектуры современных процессоров. Динамические языки сценариев не обеспечивают эффективного распределения памяти, поскольку типы переменных известны только во время выполнения, поэтому оптимизация невозможна, в отличие от языков со статической типизацией.
Rust обладает следующими встроенными функциями, которые позволяют создавать ресурсосберегающие сервисы:
Из-за своей модели управления памятью Rust затрудняет (если не делает невозможным) написание кода, который приводит к утечке памяти или других ресурсов.
Rust позволяет разработчикам жестко контролировать расположение памяти в своих программах.
В Rust отсутствует сборщик мусора, который потребляет дополнительные ресурсы процессора и памяти. Код для сбора мусора обычно выполняется в отдельных потоках и потребляет ресурсы.
Rust не имеет большого и сложного времени выполнения. Это дает разработчикам огромную гибкость при запуске программ Rust даже на маломощных встраиваемых системах и микроконтроллерах, таких как бытовая техника и промышленные машины. Rust может работать практически без использования ядер.
Rust препятствует глубокому копированию выделенной в куче памяти и предоставляет различные типы интеллектуальных указателей для оптимизации объема памяти, занимаемой программами. Отсутствие среды выполнения в Rust делает его одним из немногих современных языков программирования, подходящих для сред с крайне низким потреблением ресурсов.
Rust сочетает в себе лучшее из статической типизации, детального управления памятью, эффективного использования многоядерных процессоров и встроенной семантики асинхронного ввода-вывода, что делает его очень ресурсоэффективным с точки зрения использования процессора и памяти. Все это приводит к снижению стоимости серверов и эксплуатационной нагрузки как для небольших, так и для крупных приложений.
RUST ИМЕЕТ НИЗКУЮ ЗАДЕРЖКУ
Задержка сетевого запроса и ответа на него зависит как от задержки в сети, так и от задержки в обслуживании. Задержка в сети зависит от многих факторов, таких как среда передачи, расстояние распространения, эффективность маршрутизатора и пропускная способность сети. Задержка обслуживания зависит от многих факторов, таких как задержки ввода-вывода при обработке запроса, наличие сборщика мусора, который приводит к недетерминированным задержкам, паузы гипервизора, количество переключений контекста (например, при многопоточности), затраты на сериализацию и десериализацию и т.д.
С точки зрения чисто языка программирования, Rust обеспечивает низкую задержку благодаря низкоуровневому аппаратному управлению в качестве системного языка программирования. В Rust нет сборщика мусора или среды выполнения, зато есть встроенная поддержка неблокирующего ввода-вывода, хорошая экосистема высокопроизводительных асинхронных (неблокирующих) библиотек ввода-вывода и сред выполнения, а также абстракции с нулевой стоимостью как фундаментальный принцип разработки языка. Кроме того, по умолчанию переменные Rust хранятся в стеке, что упрощает управление ими.
Несколько различных тестов показали сопоставимую производительность между идиоматическим Rust и идиоматическим C++ для аналогичных рабочих нагрузок, что выше результатов для основных языков веб-разработки.
RUST ОБЕСПЕЧИВАЕТ БЕЗБОЯЗНЕННЫЙ ПАРАЛЛЕЛИЗМ
Ранее мы рассматривали возможности параллелизма в Rust с точки зрения безопасности программ. Теперь давайте рассмотрим параллелизм в Rust с точки зрения повышения загрузки многоядерных процессоров, пропускной способности и производительности приложений и инфраструктурных служб.
Rust - это язык, поддерживающий параллелизм, который позволяет разработчикам использовать возможности многоядерных процессоров. Rust обеспечивает два типа параллелизма: классическую многопоточность и асинхронный ввод-вывод:
Многопоточность — традиционная поддержка многопоточности в Rust обеспечивает параллелизм как при использовании совместно используемой памяти, так и при передаче сообщений. Для совместного использования значений предусмотрены гарантии на уровне типов. Потоки могут заимствовать значения, принимать права владельца и передавать область действия значения новому потоку. Rust также обеспечивает безопасность при передаче данных, что предотвращает блокировку потоков, повышая производительность. Чтобы повысить эффективность использования памяти и избежать копирования данных, совместно используемых потоками, Rust предоставляет подсчет ссылок в качестве механизма отслеживания использования переменной другими процессами или потоками. Значение сбрасывается, когда счетчик достигает нуля, что обеспечивает безопасное управление памятью. Кроме того, в Rust доступны мьютексы для синхронизации данных между потоками. Для ссылок на неизменяемые данные необязательно использовать мьютекс.
Асинхронный ввод—вывод - Неблокирующие примитивы параллелизма ввода–вывода на основе асинхронного цикла обработки событий встроены в язык Rust с нулевыми затратами на будущее и асинхронным ожиданием. Неблокирующий ввод-вывод гарантирует, что код не зависнет в ожидании обработки данных.
Кроме того, правила неизменяемости Rust обеспечивают высокий уровень параллелизма данных.
RUST - ПРОДУКТИВНЫЙ ЯЗЫК
Несмотря на то, что Rust в первую очередь является системно-ориентированным языком программирования, он также добавляет функциональные возможности более высокого уровня. Вот несколько высокоуровневых абстракций в Rust, которые обеспечивают продуктивную и приятную работу разработчика:
Замыкания с помощью анонимных функций
Итераторы
Обобщенные элементы и макросы
Перечисления, такие как Option и Result
Полиморфизм по признакам
Динамическая диспетчеризация по объектам признаков
Rust не только позволяет разработчикам создавать эффективное, безопасное и производительное программное обеспечение, но и оптимизирует производительность разработчиков благодаря своей выразительности. Не случайно Rust является самым популярным языком программирования в опросе разработчиков Stack Overflow в течение 8 лет подряд: 2016-2024 (https://insights.stackover flow.com/survey/2024).
ПРИМЕЧАНИЕ. Чтобы узнать больше о том, почему старшие разработчики любят Rust, ознакомьтесь со статьей “Почему разработчики, использующие Rust, так любят его” в блоге Overflow: http://mng.bz/rWZj.
До сих пор вы видели, как Rust предлагает уникальное сочетание безопасности памяти, эффективности использования ресурсов, низкой задержки, высокого уровня параллелизма и производительности разработчиков. Они придают Rust характеристики низкоуровневого управления и скорости системного языка программирования, производительность разработки на языках более высокого уровня и уникальную модель памяти без сборщика мусора. Серверные части приложений и инфраструктурные службы напрямую выигрывают от этих характеристик, обеспечивая отклик с низкой задержкой при высоких нагрузках и при этом высокоэффективно используя системные ресурсы, такие как многоядерные процессоры и память. Теперь давайте рассмотрим некоторые ограничения Rust.
1.2.3. Чего нет в Rust?
Когда дело доходит до языков программирования, не существует универсального варианта - ни один язык не может быть признан подходящим для всех случаев использования. Кроме того, в силу особенностей языка программирования то, что может быть легко сделано на одном языке, может оказаться трудным на другом. Чтобы у вас было полное представление о том, стоит ли использовать Rust для работы в Интернете, вот несколько вещей, которые вам необходимо знать:
Rust отличается высокой степенью обучаемости. Это, безусловно, большой шаг вперед для новичков в программировании или людей, работающих с динамическим программированием или скриптовыми языками. Синтаксис иногда может быть трудным для восприятия даже опытными разработчиками.
Некоторые вещи сложнее программировать на Rust по сравнению с другими языками, например, списки с одной и двумя связями. Это связано с особенностями языка.
Компилятор Rust в настоящее время работает медленнее, чем компиляторы для многих других компилируемых языков. Однако за последние несколько лет скорость компиляции улучшилась, и работа над этим постоянно ведется.
Экосистема библиотек Rust и ее сообщество все еще находятся на стадии становления по сравнению с другими распространенными языками.
Разработчиков Rust сложнее найти и нанять в больших масштабах.
Внедрение Rust в крупных компаниях и на предприятиях еще только начинается. У Rust пока нет естественного источника для его развития, такого как Oracle для Java, Google для Golang или Microsoft для C#.
Теперь вы ознакомились с преимуществами и недостатками использования Rust для разработки внутренних сервисов приложений. В следующем разделе я представлю пример приложения, которое мы создадим в этой книге.
1.3. Визуализация примера приложения
В следующих главах мы будем использовать Rust для создания веб-серверов, веб-служб и веб-приложений и продемонстрируем концепции на подробном примере. Обратите внимание, что наша цель - не разработать полностью функциональное или архитектурно завершенное распределенное приложение, а научиться использовать Rust для веб-домена.
Это важно иметь в виду: мы рассмотрим только некоторые пути — очень ограниченное число из всех возможных путей, и мы полностью проигнорируем другие, которые могут быть столь же многообещающими и интересными. Это осознанный выбор, чтобы сохранить фокус нашего обсуждения. Например, будут разработаны только веб-сервисы REST, полностью исключив SOAP-сервисы. Я полностью осознаю, насколько произвольным это может показаться.
В этой книге также не будут рассмотрены некоторые важные аспекты современной разработки программного обеспечения, такие как непрерывная интеграция/непрерывная поставка (CI/CD). Это очень важные темы в современной практике, но в Rust не было ничего конкретного, что можно было бы объяснить, и мы предпочли не затрагивать эти аспекты в контексте этой книги.
С другой стороны, поскольку контейнеризация в настоящее время является основной тенденцией, и поскольку я счел интересным продемонстрировать развертывание распределенного приложения, разработанного в Rust в виде контейнеров, я покажу, насколько просто развернуть и запустить наше примерное приложение с помощью Docker и Docker Compose.
Аналогичным образом, в заключительных главах книги мы совершим небольшое путешествие в область одноранговых сетей (P2P), которые являются одним из наиболее ярких применений асинхронных возможностей. Однако эта часть книги будет немного отличаться от примера приложения, поскольку я не нашел убедительного варианта использования для интеграции с ним P2P. Поэтому использование P2P в нашем примере приложения оставлено в качестве упражнения, которое вы можете изучить.
Давайте теперь рассмотрим наш пример приложения.
1.3.1 Что мы будем создавать?
В этой книге мы создадим цифровую витрину для преподавателей под названием EzyTutors, где преподаватели смогут публиковать каталоги своих курсов онлайн. Преподавателями могут быть частные лица или обучающие компании. Цифровая витрина станет инструментом продаж для преподавателей, а не торговой площадкой.
Мы определили концепцию продукта. Теперь давайте поговорим о сфере применения, а затем о технологическом стеке.
На витрине магазина преподаватели смогут зарегистрироваться, а затем войти в систему. Они могут создать предложение курса и связать его с категорией курса. Для каждого преподавателя будет создана веб-страница со списком курсов, и они смогут поделиться им в социальных сетях со своей сетью. Также будет создан общедоступный веб-сайт, который позволит учащимся искать курсы, просматривать курсы по преподавателям и просматривать подробную информацию о курсе. На рисунке 1.2 показана логическая схема нашего примера приложения.
[]
Рисунок 1.2. Пример нашего приложения EzyTutors
Наш технический стек будет состоять из веб-сервиса и серверного веб-приложения, написанного на чистом Rust. Существует несколько очень популярных подходов, например, разработка графического интерфейса пользователя с использованием зрелых веб-фреймворков, таких как React, Vue или Angular, но, чтобы мы не отвлекались на Rust, мы не будем использовать эти подходы. На эту тему есть много других хороших книг.
Данные курса будут сохранены в реляционной базе данных. Мы будем использовать Actix Web для веб-платформы, Sql для подключения к базе данных и Postgres для базы данных. Важно отметить, что дизайн будет полностью асинхронным. Как Actix Web, так и SQLx поддерживают полный асинхронный ввод-вывод, что хорошо подходит для нашей рабочей нагрузки веб-приложений, которая больше связана с вводом-выводом, чем с вычислениями.
Сначала мы создадим веб-сервис, который предоставляет API RESTful, подключается к базе данных и обрабатывает ошибки и сбои в работе конкретного приложения. Затем мы смоделируем изменения в жизненном цикле приложения, улучшив модель данных и добавив дополнительные функциональные возможности, что потребует рефакторинга кода и миграции базы данных. Это упражнение продемонстрирует одну из ключевых сильных сторон Rust — способность безбоязненно проводить рефакторинг кода (и сокращать техническую задолженность) с помощью строго типизированной системы и строгого, но полезного компилятора, который нас поддерживает.
В дополнение к веб-сервису, в нашем примере будет продемонстрировано, как создать интерфейс в Rust; выбранным нами примером будет клиентское приложение, отображаемое сервером. Мы будем использовать движок template для отображения шаблонов и форм для веб-приложения, отображаемого сервером. Можно было бы также реализовать браузерное приложение на основе WebAssembly, но это выходит за рамки данной книги.
Наше веб-приложение может быть разработано и развернуто на любой платформе, поддерживаемой Rust: Linux, Windows или macOS. Это означает, что мы не будем использовать какую-либо внешнюю библиотеку, которая ограничивает приложение какой-либо конкретной вычислительной платформой. Наше приложение можно будет развернуть либо в традиционном серверном режиме, либо на любой облачной платформе, либо в виде традиционного двоичного файла, либо в контейнерной среде (такой как Docker или Kubernetes).
Выбранная проблемная область для нашего примера приложения представляет собой практический сценарий, но его нетрудно понять. Это позволит нам сосредоточиться на основной теме книги — как применить Rust к веб-домену. В качестве бонуса мы также углубим наше понимание Rust, увидев в действии такие концепции, как свойства, время жизни, результат и опции, структуры и перечисления, коллекции, интеллектуальные указатели, производные свойства, связанные функции и методы, модули и рабочие пространства, модульное тестирование, замыкания и функциональное программирование.
Эта книга посвящена изучению основ веб-разработки на Rust. В этой книге не рассматривается настройка и развертывание дополнительных инфраструктурных компонентов и инструментов, таких как обратные прокси-серверы, балансировщики нагрузки, брандмауэры, TLS/SSL, серверы мониторинга, серверы кэширования, инструменты DevOps, CDN и т.д., поскольку эти темы не относятся к Rust (хотя они необходимы для крупномасштабного развертывания производства).
В дополнение к созданию бизнес-функционала в Rust, наш пример приложения продемонстрирует передовые методы разработки, такие как автоматизированные тесты, структурирование кода для удобства сопровождения, отделение конфигурации от кода, создание документации и, конечно же, написание идиоматических Rust.
Готовы ли вы к практическому использованию Rust в Интернете?
1.3.2. Технические рекомендации для примера приложения
Эта книга не о системной архитектуре или теории разработки программного обеспечения. Тем не менее, я хотел бы перечислить несколько основополагающих принципов, которые я использовал в книге, которые помогут вам лучше понять, как я обосновываю выбор дизайна в примерах кода:
1. Структура проекта — Мы будем активно использовать модульную систему Rust для разделения различных функциональных возможностей и упорядочения работы. Мы будем использовать рабочие пространства Cargo для группировки связанных проектов, которые могут включать как двоичные файлы, так и библиотеки.
2. Принцип единой ответственности — каждая логически обособленная часть функциональности приложения должна находиться в отдельном модуле. Например, обработчики на веб-уровне должны заниматься только обработкой HTTP-сообщений. Бизнес-логика и логика доступа к базе данных должны находиться в отдельных модулях.
3. Удобство сопровождения — следующие рекомендации относятся к удобству сопровождения кода:
Имена переменных и функций должны быть понятны сами по себе.
При использовании Rustfmt форматирование кода будет единообразным.
Мы напишем автоматизированные тестовые примеры для обнаружения и предотвращения регрессий по мере того, как код будет развиваться итеративно.
Структура проекта и имена файлов должны быть интуитивно понятными.
4. Безопасность — В этой книге мы рассмотрим аутентификацию API с использованием веб-токенов JSON (JWT) и аутентификацию пользователя на основе пароля. Безопасность на уровне инфраструктуры и сети рассматриваться не будет. Однако важно помнить, что Rust по своей сути обеспечивает безопасность памяти без сборщика мусора и потокобезопасность, которая предотвращает условия гонки, предотвращая, таким образом, несколько классов труднодоступных и трудно исправляемых ошибок памяти, параллелизма и безопасности.
5. Конфигурация приложения — Разделение конфигурации и приложения - это принцип, принятый для примера проекта.
6. Использование внешних ящиков — мы сведем использование внешних ящиков к минимуму. Например, в этой книге пользовательские функции обработки ошибок создаются с нуля, а не с помощью внешних ящиков, которые упрощают и автоматизируют обработку ошибок. Это связано с тем, что использование коротких путей с использованием внешних библиотек иногда затрудняет процесс обучения и глубокого понимания.
7. Асинхронный ввод—вывод - Я сделал осознанный выбор в пользу использования библиотек, которые поддерживают полностью асинхронный ввод-вывод в примере приложения, как для сетевого взаимодействия, так и для доступа к базе данных.
Теперь, когда мы рассмотрели темы, которые будем обсуждать в книге, цели примера проекта и рекомендации, которые мы будем использовать при выборе дизайна, мы можем приступить к изучению веб-серверов и веб-служб в нашей следующей главе.
Резюме
Современные веб-приложения являются незаменимыми компонентами цифровой жизни и бизнеса, но их сложно создавать, развертывать и эксплуатировать.
Распределенные веб-приложения включают в себя интерфейсы приложений, серверные службы и распределенную программную инфраструктуру.
Серверные части приложений и программная инфраструктура состоят из слабо связанных, взаимодействующих сетевых сервисов. У них есть особые требования к времени выполнения, которые влияют на выбор инструментов и технологий, используемых для их создания.
Rust является наиболее подходящим языком для разработки распределенных веб-приложений благодаря своей безопасности, параллелизму, низкой задержке и низким затратам аппаратных ресурсов.
Эта книга предназначена для читателей, которые рассматривают Rust для разработки распределенных веб-приложений.
Мы рассмотрели пример приложения, которое будем создавать в этой книге, и рассмотрели ключевые технические рекомендации, принятые для примеров кода.