Путь к асинхронной стабильности

Переход с Rocket на Actix

Когда я начинал Bipa, я только начал изучать Rust за пару месяцев до этого на моей предыдущей работе. Мы создали простой API с использованием Rocket, который обслуживал результаты более сложного фрагмента кода. Этого небольшого взаимодействия, которое у меня было с языком и фреймворком Rocket, было достаточно, чтобы заставить меня использовать его для всего, что я мог. Итак, когда мне пришло время решить, что использовать, это было несложно: Rust / Rocket казались идеальным сочетанием.

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

Однако, как упоминалось ранее, у Rocket есть два основных недостатка: он полагается на ночную сборку, что может стать препятствием, если вы хотите ее обновить, поскольку все не на 100% гарантированно работает; Но также он еще не поддерживает async / await в Rust, что означает, что он упускает ряд улучшений производительности, которые дает вам сервер, который может обрабатывать намного больше соединений одновременно.

Тогда Rust только что стабилизировался async-await в версии 1.39.0. Для меня это означало, что было возможно, что многие библиотеки, которые мне понадобятся, по-прежнему не будут поддерживать новую асинхронную среду выполнения Tokio. Имело смысл игнорировать тот факт, что Rocket не поддерживает async-await, тем более что они уже начали работать над ним как часть выпуска 0.5, и в конечном итоге также сняли необходимость быть в ночной сборке, что означало, что сборка ракеты на стабильной ржавчине также будет происходить в том же выпуске.

Перенесемся в будущее: Tokio Runtime 1.0 выпущен, в сообществе есть широкая поддержка сборки с использованием новой среды выполнения. Фактически, новые версии большинства библиотек определенно будут использовать новую среду выполнения async-await, и я подозреваю, что поддержка предыдущей среды выполнения с блокировкой будет постепенно исчезать. Все это означает для нас то, что нам, вероятно, следует начать миграцию некоторых из наших серверов для использования новой среды выполнения, которая позволит нам использовать новые версии и функции библиотек и фреймворков, на которые мы полагаемся.

Хотя я бы очень хотел попробовать версию 0.5 Rocket и перенести наш сервер на использование поддержки async-await, на сегодняшний день (февраль 2021 г.) они еще не выпустили его. Есть еще некоторые функции, которые необходимо реализовать, прежде чем они смогут его выпустить. На данный момент нам придется использовать ветку master, чего мы не собираемся делать для такой большой и важной зависимости нашего программного обеспечения. Имейте в виду, я вообще не критикую команду Rocket, они проделали и делают отличную работу с Rocket, я большой поклонник их работы.

Миграция

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

Мы сделали это за пару дней, и впечатления (как я объясню позже) были просто потрясающими. Сервер был развернут без изменений в нашей инфраструктуре, Rust stable улучшил нашу скорость сборки, и результаты были потрясающими. Сервер ценообразования теперь использовал по крайней мере часть возможностей async-await (у нас все еще было много работы): просто загрузив приложение, я мог заметить, что оно загружается быстрее, чем раньше. Имейте в виду, что это очень предвзятый тест, поскольку мы не проводили серьезных тестов для их сравнения. Несмотря на то, что это был бы интересный обучающий эксперимент, в Интернете есть множество тестов на этих фреймворках, и вы можете ясно видеть, что не только actix действительно быстр, но и входит в число самых быстрых веб-фреймворков. там, проигрывая только drogon C ++.

Хотя Rocket по-прежнему очень быстр, он с большим отрывом проигрывает Actix. Обратите внимание, что эти тесты тестируют именно тот параллелизм, в котором так хорош Actix. Когда Rocket представит async-await поддержку, он определенно поднимется на многие позиции в этом списке.

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

Поскольку миграция pricing сервера прошла так хорошо, мы решили сделать еще больший шаг и перенести основной сервер Bipa на Actix, сервер, который имеет более 50 маршрутов, взаимодействует с множеством внешних служб и имеет множество зависимостей. Из-за того, что он был намного больше и взаимодействовал с гораздо большим количеством внешних сервисов, он требовал больше работы, чем сервер ценообразования.

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

База данных

В ящике Rocket rocket_contrib есть несколько API для создания диспетчера пула, что значительно упрощает интеграцию Postgres или других баз данных с фреймворком. Это позволило нам использовать макрос database для создания diesel_postgres_pool, как вы можете видеть на изображении ниже.

Затем все, что нам нужно было сделать, это добавить эту Conn структуру как fairing промежуточное ПО к rocket.

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

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

Теперь, как нам использовать Actix, чтобы реализовать то же самое? Вот где нам нужно было проделать немного больше работы, чем при интеграции Rocket. При интеграции базы данных с Actix нет никаких волшебных макросов, вам нужно будет создать тип диспетчера пула соединений самостоятельно, используя замечательную библиотеку r2d2.

Затем при запуске сервера вам нужно будет установить соединение, которое будет возвращать Db созданный вами тип и передавать этот клон соединения как data вложение в Actix's App.

В конечном итоге это будет означать, что actix теперь будет передавать тип Data<Db> каждой конечной точке, которая в нем нуждается, точно так же, как это сделала Rocket, но немного более подробно, поскольку наш тип заключен в Data.

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

Большие изменения

Инкрементального обновления не было, это нужно было делать за один раз. Для сервера ценообразования это не было проблемой, на сервере есть только несколько конечных точек и таблиц. Но когда дело дошло до главного сервера, нам потребовалось прикоснуться к каждому файлу проекта, связанному с Интернетом. На сервере с более чем 50 маршрутами это изменение может быть очень пугающим. Теперь помогает то, что большинство этих изменений были буквально незначительными изменениями синтаксиса, такими как изменение макроса route с rocket::get("/my-route") на actix_web::get("/my-route") и обновление ответа с Result<Json<MyType>, BadRequest<Json<MyError>> на actix_web::Result<Json<MyType>>.

Внешние службы

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

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

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

Мы обошли эту проблему, создавая новый std::thread каждый раз, когда нам нужен был блокирующий поток для выполнения операции, а затем мы использовали метод std::thread::JoinHandle.join, чтобы убедиться, что мы дождемся завершения потока, чтобы извлечь его результат. Как вы можете видеть на изображении ниже.

Будущая работа

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

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

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

РЕДАКТИРОВАТЬ: Если вам нравится то, что мы делаем, и вы хотели бы работать с нами. Напишите мне в Twitter или Linkedin.