Обзор Инженерии: Выпуск 2025 года
Everett Pompeii
При разработке новой технологии, такой как Bencher, существует фундаментальное напряжение между желанием выбирать скучные технологии и побеждать средние показатели. В данный момент может быть трудно точно определить, где именно ты находишься в этом противоборстве. Каждые три года язык программирования Rust выпускает новое издание Rust. Я считаю, что это хороший ритм. Достаточно долго, чтобы добиться реального прогресса, но достаточно коротко, чтобы не уйти слишком далеко в сторону. Поскольку этой весной Bencher исполняется 3 года, я подумал, что это отличное время остановиться и поразмышлять обо всех инженерных решениях, которые привели нас сюда.
В этом посте я собираюсь оглянуться назад и посмотреть, как Bencher использовал свои “токены инноваций” за последние три года. Bencher — это проект с открытым исходным кодом набор инструментов для непрерывного бенчмаркинга. Я начну с фронтенда архитектуры Bencher и буду двигаться по всей структуре вниз. На каждой остановке по пути я обсудю, как мы сюда попали и дам бинарное заключение о том, как каждое инженерное решение себя проявило.
Фронтенд
Библиотека для Frontend
Как разработчик, восстанавливающийся после работы с C++, я большой поклонник Rust. Если бы у меня был выбор, я бы написал Bencher на full-stack Rust. Загляните в глубины репозитория Bencher, и вы увидите мои попытки сделать это. Я экспериментировал с Yew, Seed и Sycamore. Несмотря на то что они могут отлично работать для некоторых проектов, для меня была одна основная загвоздка, через которую я не смог переступить: взаимодействие с JavaScript.
Хотя взаимодействие с JS возможно из WASM через Rust, это не было бы легкой задачей. Я знал, что хочу, чтобы Bencher имел высоко интерактивные графики. Это означало использование библиотеки вроде D3, а значит, взаимодействие с JS.
Итак, если мне все равно пришлось бы использовать JavaScript, какую библиотеку мне выбрать?
Возвращаясь к тем Rust-крейтам, которые я пробовал, Yew является аналогом Rust для React Hooks. Ранее я разрабатывал и развертывал фронтенд, используя React Hooks, поэтому я знал об этой структуре больше всего. Однако я нашел жизненный цикл React Hooks очень сложным и полным неожиданностей и странных пограничных случаев.
Мне действительно нравились основные принципы функционального реактивного программирования (FRP). Это привело меня к изучению как Elm, так и его аналога на Rust, Seed. К сожалению, использование Elm страдает от тех же проблем, что и использование Rust. Elm требует собственного взаимодействия с JavaScript. Я также нашел, что Архитектура Elm слишком ограничивает для моего вкуса.
Из всех Rust-фреймворков, которые я попробовал, мне больше всего понравился Sycamore. Sycamore был вдохновлен Solid. Чем больше я узнавал о Solid, тем больше он мне нравился. В отличие от React, Solid не использует виртуальный DOM. Вместо этого он компилируется в обычный JavaScript. Это делает его гораздо быстрее, меньше по размеру и проще в работе. Solid состоит всего из нескольких мощных примитивов, позволяющих осуществлять мелкозернистую реактивность. Когда что-то в UI обновляется, только код, который от него зависит, будет повторно выполнен. За последние три года я обнаружил, что работать с Solid — это удовольствие.
Технология Вердикт Yew ❌ Seed ❌ Sycamore ❌ Elm ❌ SolidJS ✅
Фреймворк для фронтенда
Solid сам по себе — это просто библиотека. Для создания современного фронтенда мне нужно было использовать полноценный фреймворк для веб-приложений. Желая упростить задачу, я решил полностью полагаться на Solid и изначально использовал SolidStart. В то время SolidStart поддерживал только одностраничные приложения (SPA).
SPA прекрасно подходило для начального этапа. Со временем, однако, мне нужно было задуматься о таких вещах, как SEO. Я начал писать гораздо больше документации Bencher. Также я планировал раздел “Изучение” сайта. Это означало, что мне нужны были как рендеринг на стороне клиента (CSR), так и генерация статических сайтов (SSG). SolidStart был очень молодым, и я не смог добиться от него удовлетворения всех моих потребностей.
После изучения Astro и его использования, я решил перенести весь фронтенд Bencher с SolidStart на Astro. Это имело несколько недостатков. Самым очевидным было усилие, затраченное на это. Честно говоря, это было не так плохо. Astro имеет свою архитектуру островов и первоклассную интеграцию с Solid. Я также смог использовать много нужной мне логики из Solid Router, и всё просто заработало.
Большой компромисс, который до сих пор актуален, заключается в том, что Bencher перешел от одностраничного к многопостраничному приложению. В большинстве мест, где вы кликаете в консоли, происходит полное перерисование страницы. Astro обещал поддержку переходов между видами, когда я впервые сделал этот переход. Я попробовал их, но они были багованными. Мне еще нужно вернуться к этому вопросу.
В это время, похоже, SolidStart догнал немного. Теперь они поддерживают как CSR, так и SSG. Хотя я не проверял, работают ли они оба на одном сайте, как мне нужно. Что было, то было.
Технология Вердикт SolidStart ❌ Astro ✅
Язык интерфейса
Astro имеет встроенную поддержку TypeScript.
В процессе перехода с SolidStart на Astro, я также начал переход с JavaScript на TypeScript.
Конфигурация TypeScript для Bencher установлена на самый строгий режим Astro.
Однако Astro не выполняет проверку типов во время сборки.
На момент написания у Bencher все еще есть 604
ошибок типов.
Эти ошибки типов используются скорее как подсказки при редактировании кода,
но они не блокируют сборку (пока нет).
Я также добавил Typeshare, чтобы синхронизировать структуры данных Rust в Bencher с интерфейсом TypeScript. Это было очень полезно для разработки Bencher Console. Кроме того, все валидаторы полей, таких как имена пользователей, электронные адреса и т. д., совместно используются между кодом на Rust и интерфейсом TypeScript через WASM. Было несколько хлопотно заставить WASM работать как в SolidStart, так и в Astro. Самый распространенный класс ошибок, который я видел в интерфейсе, был в местах, где вызывается функция WASM, но модуль WASM еще не загружен. Я разобрался, как это исправить, но все равно иногда забываю, и это снова появляется.
Наличие автоматически генерируемых общих типов и валидаторов из кода Rust значительно упростило взаимодействие с интерфейсом. Они оба проверяются в CI, так что никогда не рассинхронизируются. Все, что мне нужно сделать, это убедиться, что HTTP-запросы сформированы правильно, и все просто работает. Это делает невозможность использования full-stack Rust немного менее болезненной.
Технология Итог Rust ❌ JavaScript ❌ TypeScript ✅ Typeshare ✅ WASM ✅
Хостинг на стороне клиента
Моё первоначальное решение полностью перейти на Solid было во многом продиктовано тем, что Netlify наняли создателя Solid для работы над ним на полную ставку. Видите ли, крупнейший конкурент Netlify — это Vercel. Vercel создали и поддерживают Next.js. И я предположил, что Netlify хотят, чтобы Solid стал их Next.js. Поэтому я подумал, что нет лучшего места для размещения сайта SolidStart, чем Netlify.
По умолчанию Netlify пытается заставить вас использовать их систему сборки. Использование системы сборки Netlify делает невозможным атомарное развертывание. Netlify всё равно публиковал бы фронтенд, даже если бы пайплайн бекенда дал сбой. Очень плохо! Это привело к тому, что я перешёл к сборке фронтенда в той же среде CI/CD, что и бекенд, а затем просто загружал последнюю версию на Netlify с помощью их CLI. Когда я перешёл с SolidStart на Astro, я смог сохранить ту же настройку CI/CD. Astro имеет первоклассную интеграцию с Netlify.
Bencher смог оставаться на бесплатном тарифном плане Netlify довольно долгое время.
Однако с растущей популярностью Bencher мы начали превышать некоторые лимиты бесплатного тарифа.
Я задумался о переносе сайта Astro на sst
на AWS.
Тем не менее, экономия средств пока кажется не стоящей затраченных усилий.
Технология Вердикт Netlify Builds ❌ Netlify Deploys ✅
Бэкенд
Язык для серверной части
Rust.
Технология Вердикт Rust ✅
Фреймворк HTTP-сервера
Одним из важнейших факторов при выборе фреймворка HTTP-сервера на Rust для меня была поддержка встроенной спецификации OpenAPI. По тем же причинам, что я инвестировал в установку Typeshare и WASM на фронтенде, я хотел иметь возможность автоматически генерировать как документацию API, так и клиентов из этой спецификации. Для меня было важно, чтобы эта функциональность была встроенной, а не сторонним дополнением. Чтобы автоматизация действительно стоила того, она должна работать почти на 100% времени. Это означает, что бремя поддержки и совместимости должно лежать на самих инженерах основного фреймворка. В противном случае вы неизбежно окажетесь в адских граничных ситуациях.
Еще одним ключевым фактором было риск заброшенности. Существует несколько когда-то многообещающих фреймворков HTTP на Rust, которые теперь практически заброшены. Единственным фреймворком, который я нашел с поддержкой встроенной спецификации OpenAPI и на который я был готов поставить, был Dropshot. Dropshot был создан и поддерживается Oxide Computer.
Пока у меня была только одна серьезная проблема с Dropshot. Когда сервер API генерирует ошибку, на фронтенде происходит сбой CORS из-за отсутствия заголовков ответа. Это означает, что веб-фронтенд не может показывать пользователям очень полезные сообщения об ошибках. Вместо того чтобы заниматься исправлением на upstream, я сосредоточил свои усилия на том, чтобы сделать Bencher более простым и интуитивно понятным в использовании. Но, как оказалось, решение занимает менее 100 строк кода. Вот так повезло!
К слову, фреймворк axum
еще не был выпущен, когда я начал работать над Bencher. Если бы он был тогда доступен, возможно, я бы попытался совместить его с одним из множества сторонних дополнений OpenAPI, несмотря на мой здравый смысл. К счастью, axum
тогда еще не существовал, чтобы меня соблазнить. Dropshot оказался отличным выбором. См. раздел Клиент API для получения дополнительной информации по этому вопросу.
Технология Вывод Dropshot ✅
База данных
Я старался сделать Bencher максимально простым. Первая версия Bencher получала все данные, включая результаты тестов, через параметры запроса URL. Я быстро узнал, что все браузеры имеют ограничение на длину URL. Логично.
Затем я задумался о хранении результатов тестов в git
и просто генерировании статического HTML-файла с графиками и результатами.
Однако, у этого подхода есть два основных недостатка.
Во-первых, время git clone
в конечном счете становится невыносимым для активных пользователей.
Во-вторых, все исторические данные должны присутствовать в HTML-файле,
что приводит к очень долгому времени начальной загрузки для активных пользователей.
Инструмент для разработки должен заботиться о своих активных пользователях, а не наказывать их.
Оказывается, есть решение моей проблемы. Оно называется база данных.
Так почему бы просто не использовать Postgres и не успокоиться? Ну, я действительно хотел, чтобы люди могли самостоятельно размещать Bencher. Чем проще я мог сделать архитектуру, тем проще (и дешевле) было бы другим развертывать его у себя. Я уже собирался требовать два контейнера из-за разделения фронтенда и бэкенда. Могу ли я избежать третьего? Да!
До Bencher я использовал SQLite только в качестве тестовой базы данных. Опыт разработчика был фантастическим, но я никогда не думал о его использовании в производстве. Затем я наткнулся на Litestream. Litestream — это инструмент для восстановления после аварий для SQLite. Он работает в фоне и непрерывно реплицирует изменения на S3 или в любое другое хранилище по вашему выбору. Это делает его простым в использовании и невероятно экономичным в эксплуатации, поскольку S3 не взимает плату за запись. Думайте о центах в день для небольшого инстанса.
Когда я впервые наткнулся на Litestream, также обещалось, что скоро появятся живые реплики для чтения. Однако, этого так и не произошло. Предлагаемая альтернатива была проектом-преемником того же разработчика под названием LiteFS. Однако у LiteFS есть серьезные недостатки. Он не предлагает встроенного решения для восстановления после аварий, если все реплики выходят из строя. Чтобы иметь несколько реплик, вы должны внедрить в логику вашего приложения концепцию о том, является ли она читателем или писателем. И абсолютным препятствием было то, что требует экземпляра Consul, который должен работать постоянно для управления репликами. Вся суть использования SQLite заключалась в том, чтобы избежать ещё одной службы. К счастью, я не пытался использовать LiteFS с Bencher Cloud, поскольку LiteFS Cloud был закрыт через год после запуска, а LiteFS сейчас практически мёртв.
В настоящее время короткое время простоя между развертываниями обрабатывается Bencher CLI. В будущем я планирую перейти к развертываниям без простоя, используя Kamal. С учетом того, что Rails 8.0 по умолчанию будет использовать Kamal и SQLite, я чувствую довольно уверенно, что Kamal и Litestream хорошо сработаются вместе.
Технология Вердикт Параметры URL ❌ git + HTML ❌ SQLite ✅ Litestream ✅ LiteFS ❌
Драйвер базы данных
Чем ближе я подхожу к базе данных, тем более строго типизированные вещи я хочу использовать. На фронтенде можно немного расслабиться. Если я ошибаюсь, всё станет как надо после следующей загрузки на продакшен. Но если я поврежу базу данных, это гораздо более сложная задача для исправления. Учитывая это, я решил использовать Diesel.
Diesel — это строго типизированный объектно-реляционный маппер (ORM) и строитель запросов для Rust. Он проверяет все взаимодействия с базой данных на этапе компиляции, предотвращая ошибки времени выполнения. Эта проверка на этапе компиляции также делает Diesel абстракцией SQL без дополнительных затрат. За исключением небольшой ошибки с моей стороны при ускорении в 1200 раз с помощью настройки производительности, у меня не было ошибок SQL времени выполнения при работе с Diesel.
🐰 Интересный факт: Diesel использует Bencher для непрерывного бенчмаркинга!
Технология Вердикт Diesel ✅
Хостинг бэкенда
Так же, как я выбрал Netlify для хостинга фронтенда, потому что я использовал Solid, я выбрал Fly.io для хостинга бэкенда, потому что я использовал Litestream. Fly.io только что наняли создателя Litestream для работы над ним на полный рабочий день. Как упоминалось выше, эта работа над Litestream в конечном итоге была заменена LiteFS, и LiteFS теперь не существует. Так что это не сработало так, как я надеялся.
В будущем, когда я переключусь на Kamal, я также уйду с Fly.io. Fly.io испытал несколько крупных сбоев, которые каждый раз выводили Bencher из строя на полдня. Но самая большая проблема — это несоответствие, возникающее при использовании Litestream.
Каждый раз, когда я вхожу в панель управления Fly.io, я вижу следующее предупреждение:
ℹ Ваше приложение работает на одной машине
Масштабируйте и запускайте приложение на большем количестве машин, чтобы обеспечить высокую доступность, с одной командой:
Ознакомьтесь с документацией, чтобы получить более подробную информацию о масштабировании.
Но с Litestream вы все равно не можете иметь более одной машины! Вы так и не реализовали репликацию для чтения, как обещали!
Так что да, это все немного иронично и раздражает. В какой-то момент я изучил libSQL и Turso. Однако libSQL требует специальный сервер для репликации, что делает его несовместимым с Diesel. В любом случае, похоже, я избежал еще одной приостановки жизненного цикла. Мне очень интересно, что Turso сделает с Limbo, их перепиской SQLite на Rust. Но я не собираюсь переходить на это в ближайшее время. Следующая остановка — это хорошая, скучная и стабильная ВМ на Kamal.
Бэкенд AWS S3 для репликации Litestream работал безупречно. Даже с исчезновением Litestream и Fly.io, я все равно считаю, что сделал правильный выбор, используя Litestream с Bencher. Я начинаю сталкиваться с проблемами масштабирования с Bencher Cloud, но это хорошая проблема.
Технология Вердикт Fly.io ❌ AWS S3 ✅
Интерфейс командной строки (CLI)
CLI Библиотека
При создании CLI на Rust, Clap является своего рода стандартом де факто. Представьте мое удивление, когда я впервые публично демонстрировал Bencher, и сам создатель, Ed Page, был там! 🤩
Со временем я продолжаю находить все больше полезных вещей, которые может делать Clap. Это немного стыдно, но я только недавно обнаружил опцию default_value
. Все эти возможности действительно помогают сократить количество кода, который я должен поддерживать в bencher
CLI.
🐰 Интересный факт: Clap использует Bencher для отслеживания размера бинарных файлов!
Технология Вердикт Clap ✅
API клиент
Основным фактором при выборе Dropshot в качестве HTTP серверного фреймворка для Bencher была его встроенная возможность генерировать OpenAPI спецификацию. Я надеялся, что однажды смогу автоматически генерировать API клиент из этой спецификации. Примерно через год создатели Dropshot исполнили мою мечту: Progenitor.
Progenitor — это инь к яню Dropshot. Используя OpenAPI спецификацию от Dropshot, Progenitor может генерировать Rust API клиент в двух паттернах: позиционном:
или через builder:
Лично я предпочитаю второй метод, поэтому Bencher использует именно его.
Progenitor также может генерировать целый Clap CLI для взаимодействия с API.
Однако я его не использовал.
Мне нужна была более строгая контроль над процессом,
особенно для команд, таких как bencher run
.
Единственный заметный недостаток, который я обнаружил в сгенерированных типах, заключается в том,
что из-за ограничений JSON Schema невозможно просто использовать Option<Option<Item>>
,
когда нужно различать между отсутствующим ключом item
и ключом item
со значением, установленным в null
.
Это возможно с помощью чего-то вроде double_option
,
но на уровне JSON Schema это выглядит одинаково.
Использование уплощенной или непомеченной внутренней структуры перечисления
не сочетается с Dropshot.
Единственным решением, которое я нашел, было использование внешнего, непомеченного перечисления.
Однако в настоящее время в API есть только два таких поля, так что это не большая проблема.
Технология Оценка Progenitor ✅
Разработка
Среда Разработки
Когда я начал работать над Bencher, многие призывали к концу эпохи localhost. Я уже давно перерос необходимость в новом ноутбуке для разработки, поэтому решил попробовать облачную среду разработки. На тот момент GitHub Workspaces не был доступен в общем доступе (GA) для моего случая, поэтому я выбрал Gitpod.
Этот эксперимент длился около шести месяцев. Мой вывод: облачные среды разработки не подходят для побочных проектов. Вы хотите быстро сесть и выполнить пару минут работы? Нет! Вам придется сидеть и ждать, пока ваша среда разработки переинициализируется в тысячный раз. О, у вас есть целый день на выходные, чтобы действительно сделать много работы? Нет! Ваша среда разработки просто случайно перестанет работать, когда вы ею пользуетесь. Снова и снова.
Я сталкивался с этими проблемами как платный пользователь. За $25 в месяц, я мог бы приобрести новый MacBook Pro на базе M1 с гораздо лучшими характеристиками каждые пять лет. Когда Gitpod объявили, что меняют свою ценовую политику с фиксированной ставки на использование, я просто позволил им отменить мой план и отправился на apple.com.
Возможно, это все было проблемой решения Gitpod теперь уже заброшенного решения использовать Kubernetes.
Но я не спешу снова пробовать облачную среду разработки с Bencher.
В конце концов, я перенес конфигурацию Gitpod в контейнер для разработки,
чтобы упростить начало работы для участников.
А для меня я остаюсь с localhost
.
Технология Вердикт Gitpod ❌ M1 MacBook Pro ✅
Непрерывная интеграция
Bencher является проектом с открытым исходным кодом. Как современный проект с открытым исходным кодом, вы просто обязаны находиться на GitHub. Это означает, что путь наименьшего сопротивления для непрерывной интеграции (CI) — это GitHub Actions. Со временем я начал ненавидеть CI DSL на основе YAML. У каждого из них есть свои особенности, и когда речь идет о такой огромной компании, как GitHub, получение значка ⚠️ вместо значка ❌ может затянуться на годы.
Это подтолкнуло меня попробовать Dagger. На тот момент Dagger можно было использовать только через один экзотический язык под названием CUE. Я пытался. Я действительно пытался. Целый выходной. Может быть, если бы ChatGPT существовал тогда, я бы смог справиться. Но я был не единственным. В конечном итоге Dagger отказался от CUE в пользу более здравых SDK. Но тогда для меня было уже поздно.
Побежденный Dagger, я принял свою судьбу с YAML CI DSL,
и теперь Bencher использует GitHub Actions.
Черт возьми, я даже построил GitHub Action для Bencher CLI.
Будьте изменением проблемой, которую вы хотите видеть в мире.
Технология Вердикт Dagger ❌ GitHub Actions ⚠️
Заключение
Создание Bencher научило меня многому о компромиссах, которые сопровождают каждое инженерное решение. Есть некоторые выборы, которые я бы теперь сделал иначе, но это хорошо. Это означает, что я кое-чему научился в процессе. В целом, я очень доволен тем, где находится Bencher сегодня. Bencher превратился из эскиза в моей записной книжке в полноценный продукт с растущей базой пользователей, активным сообществом и платящими клиентами. Я с нетерпением жду, куда нас приведут следующие три года!
Стек Компонент Технология Вердикт Фронтенд Библиотека фронтенда Yew ❌ Seed ❌ Sycamore ❌ Elm ❌ SolidJS ✅ Язык фронтенда Rust ❌ JavaScript ❌ TypeScript ✅ Typeshare ✅ WASM ✅ Хостинг фронтенда Netlify Builds ❌ Netlify Deploys ✅ Бэкенд Язык бэкенда Rust ✅ Фреймворк HTTP-сервера Dropshot ✅ База данных URL Query Params ❌ git + HTML ❌ SQLite ✅ Litestream ✅ LiteFS ❌ Драйвер базы данных Diesel ✅ Хостинг бэкенда Fly.io ❌ AWS S3 ✅ CLI Библиотека CLI Clap ✅ API-клиент Progenitor ✅ Разработка Среда разработки Gitpod ❌ M1 MacBook Pro ✅ Непрерывная интеграция Dagger ❌ GitHub Actions ⚠️
Bencher: Непрерывное тестирование производительности
Bencher - это набор инструментов для непрерывного тестирования производительности. Когда-нибудь регрессия производительности влияла на ваших пользователей? Bencher мог бы предотвратить это. Bencher позволяет вам обнаруживать и предотвращать регрессии производительности до того, как они попадут в продакшн.
- Запустить: Запустите свои тесты производительности локально или в CI, используя ваши любимые инструменты для этого. CLI
bencher
просто оборачивает ваш существующий аппарат тестирования и сохраняет его результаты. - Отслеживать: Отслеживайте результаты ваших тестов производительности со временем. Мониторите, запрашивайте и строите графики результатов с помощью веб-консоли Bencher на основе ветки исходного кода, испытательного стенда и меры.
- Поймать: Отлавливайте регрессии производительности в CI. Bencher использует инструменты аналитики, работающие по последнему слову техники, чтобы обнаружить регрессии производительности, прежде чем они попадут в продакшн.
По тем же причинам, по которым модульные тесты запускаются в CI, чтобы предотвратить регрессии функций, тесты производительности должны быть запущены в CI с Bencher, чтобы предотвратить регрессии производительности. Ошибки производительности – это тоже ошибки!
Начните отлавливать регрессии производительности в CI — попробуйте Bencher Cloud бесплатно.