Найти тему
Nuances of programming

Тестирование клиент-серверов на Rust для IoT

Источник: Nuances of Programming

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

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

Меньший размер двоичного кода — это та цель, к которой всегда нужно стремиться, ведь известно, что HTTP клиент-сервер  —  это компонент, многократно переиспользуемый в приложениях, где задействована технология Интернета вещей. Однако после полной миграции общий размер двоичного кода у нас увеличился примерно на 3%.

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

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

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

При определении правил тестирования, мы руководствовались потребностями HTTP клиент-сервера в нашем Updatehub Agent, где запускалась периодическая задача — проверять и, возможно, устанавливать обновления. Важно также, чтобы Agent всегда выдавал быстрый отклик на запросы других приложений или самого пользователя. Через небольшой HTTP API Agent предлагает набор конечных точек, к которым локальные приложения могут получить доступ, либо запрашивая информацию о выполнении агента, либо для запуска каких-то действий, например отмены загрузки.

С учётом всего этого мы установили следующие требования:

  • Асинхронный API: за что мы так любим Rust? За текущее состояние поддержки экосистемы async/await. Язык предлагает очень хороший и простой способ реализации параллельного выполнения, обеспечивая всегда быстрый отклик Agent на запросы.
  • Общий доступ к внутреннему состоянию: в процессе обновления имеется несколько переменных, которые будут храниться в памяти и могут быть запрошены в любое время в Agent. Именно поэтому нам нужно, чтобы был протестирован и общий доступ к внутреннему состоянию между клиентом, используемым для отправки запросов в облако, и сервером, используемым для отправки ответов на локальный API.
  • Один маршрут на сервер и на запросы: мы решили ограничиться только одним маршрутом при тестировании. Надеемся, что этого будет достаточно, чтобы связать соответствующие функции библиотек с конечным двоичным кодом — тогда можно будет их сравнить.
  • Работа с OpenSSL при возможности: в Updatehub Agent мы уже используем OpenSSL для проверки цифровых подписей. Мы использовали OpenSSL по умолчанию всякий раз, когда это было возможно, поскольку в нашем сценарии применения remote link обычно использует протокол HTTPS.

Установив требования к приложениям, мы определились с участниками нашего тестирования:

Реализация Клиент Сервер

dummy - -
actix_full awc actix-web
actix_reqwest reqwest actix-web
gotham_reqwest reqwest gotham
warp_reqwest reqwest warp
hyper_reqwest reqwest hyper
hyper_full hyper hyper
tide_surf surf tide
warp_surf surf warp

При проведении анализа данных мы использовали cargo-bloat для оценки веса каждой зависимости в конечном двоичном коде. Он не даёт 100% точности, тем не менее, он проясняет «неизвестные» части в двоичном коде (обозначенные как крейт с именем «[Unknown]»). В наших сценариях использования их всегда было небольшое количество.

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

Параметр Значения
Оптимизация [0; 1; 2; 3; s; z]
Оптимизация во время компоновки [thin; fat]
Модули кодогенерации [1; 16]

Мы создали интерактивную информационную панель для отображения интерактивных графиков, используемых в приложении shinyapp. В этом приложении вы можете более детально просмотреть все собранные нами данные. Давайте посмотрим на самые важные.

Начинаем разбор результатов со сравнения между actix-reqwest и actix-full, первыми участниками нашего тестирования. Даже с усовершенствованной оптимизацией контекст, который мы настроили для тестирования размеров двоичного кода, всё равно показывает преимущество reqwest над awc.

Сравнение awc с reqwest (интерактивный график).
Сравнение awc с reqwest (интерактивный график).

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

Распределение крейтов по размерам во всех проектах (интерактивная диаграмма).
Распределение крейтов по размерам во всех проектах (интерактивная диаграмма).

Пока мы тестировали флаги оптимизации, никаких сюрпризов не возникло. Установка уровня оптимизации на z, оптимизации во время компоновки на fat и количества модулей кодогенерации на 1 привела к улучшению результатов у всех участников тестирования. На следующей диаграмме показана оптимизация у tide-surf:

Параметры оптимизации у tide-surf (интерактивная диаграмма).
Параметры оптимизации у tide-surf (интерактивная диаграмма).

async-std и tide-surf  —  наша новая пара HTTP клиент-сервер для встроенных приложений. Несмотря на наличие высокоуровневого API, у этой пары результаты оказались лучше, чем у более низкоуровневых API, таких как hyper_full.

Если бы мы выбирали пару, полностью основанную на tokio, ещё одной основной среде выполнения для асинхронной экосистемы rust, мы безусловно остановились бы на warp. Оказалось, что у этого крейта небольшой по сравнению с hyper расход вычислительных ресурсов в виде увеличения размера двоичного кода. Причём такой расход обусловлен созданием HTTP-серверов с гораздо более простым API.

Размер всех проектов (интерактивная диаграмма).
Размер всех проектов (интерактивная диаграмма).

Если хотите быть в курсе будущих обновлений нашего тестирования, обязательно следите за репозиторием на GitHub. Для отслеживания изменений результатов во времени будут использоваться теги версий. Более подробно ознакомиться с собранными нами данными можно, заглянув в интерактивное приложение shinny app.

Читайте также:

Читайте нас в телеграмме и vk

Перевод статьи Jonathas Conceição: Benchmarking HTTP Client-Server Binary Size in Rust