сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Экосистема Rust

Веб-стек и базы в Rust

Веб-стек и базы

Прикладной блок для бэкенд-вакансий. Спрашивают про выбор между sqlx и diesel, зачем пул соединений, как устроены обработчики в axum и куда девать ошибки.

Стержень: доступ к базе асинхронный и через пул, а перевод доменных ошибок в HTTP живёт на границе транспорта.

// Формулировки: «sqlx или diesel?», «зачем пул?», «как отдать ошибку клиенту?»

Два подхода к базе

sqlx оставляет вам обычный SQL, но проверяет его на этапе компиляции: макрос query! сверяет запрос с реальной схемой, поэтому опечатка в имени столбца становится ошибкой сборки. Для CI без живой базы метаданные сохраняют в файл. Diesel идёт другим путём - типизированный конструктор запросов поверх схемы, где ошибку ловит система типов.

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

// Общее и важное: синхронный драйвер в асинхронном сервисе блокирует воркер. Если библиотека только синхронная, её вызовы заворачивают в spawn_blocking.

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

Пул и обработчики

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

В axum обработчик объявляет нужные части запроса прямо в аргументах: Json<Payload>, Path<u32>, Query<Params>, State<AppState>. Фреймворк разбирает их сам и возвращает ошибку, если что-то не сошлось, контракт эндпоинта оказывается выражен сигнатурой функции. Единственное ограничение: экстрактор, поглощающий тело, должен идти последним, потому что тело читается один раз.

async fn create(
    State(db): State<Pool>,
    Json(body): Json<NewUser>,
) -> Result<Json<User>, AppError> { .. }
экстрактор
аргумент обработчика, разбирающий часть запроса
State
разделяемое состояние приложения, например пул

Ошибки на границе

Доменная ошибка не должна знать про HTTP: она говорит, что произошло - NotFound, Conflict, Invalid. Перевод в статусы делает транспортный слой одной реализацией IntoResponse. После этого обработчики просто возвращают Result и ставят вопросительный знак.

Плюсов два. Логика перевода живёт в одном месте, а не ветвлениями по всем обработчикам. И внутренние детали не утекают: клиенту уходит безопасное сообщение, подробности пишутся в лог с идентификатором запроса.

// Тот же принцип позволяет переиспользовать домен в других транспортах - очереди, CLI, gRPC: меняется только слой перевода.

IntoResponse
трейт превращения значения или ошибки в HTTP-ответ
граница транспорта
слой, где домен переводится в протокол

Как отвечать: «Как отдавать доменные ошибки клиенту в axum?»

Реализую IntoResponse для своего типа ошибки - там варианты домена превращаются в статусы и тело ответа. После этого обработчики просто возвращают Result и используют вопросительный знак, а логика перевода живёт в одном месте, а не ветвлениями по каждому эндпоинту. Домен при этом ничего не знает про HTTP, поэтому его можно переиспользовать в очереди или CLI. И отдельно слежу, чтобы внутренние детали не утекали наружу: клиенту уходит короткое безопасное сообщение, а подробности с идентификатором запроса пишутся в лог.

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

На чём валятся

  • Ставят размер пула больше, чем разрешает база, и ловят отказы подключения.
  • Тянут синхронный драйвер в асинхронный сервис.
  • Возвращают клиенту текст внутренней ошибки вместе с деталями инфраструктуры.
  • Зашивают HTTP-статусы в доменный тип ошибки.
  • Ставят экстрактор тела не последним и получают ошибку компиляции.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_web_db1 / 5
    Зачем веб-сервису пул соединений с базой?
    A)Он распределяет запросы между репликами по нагрузке
    B)Он кэширует результаты запросов между обработчиками
    C)Установка соединения дорога, а их число ограничено
    D)Он делает драйвер базы потокобезопасным
    показать ответ и разбор
    +C)Установка соединения дорога, а их число ограничено

    // разбор: Соединение — это TCP-хендшейк, аутентификация и сессия на стороне сервера, а лимит подключений у базы конечен. Пул держит соединения открытыми и выдаёт их задачам на время запроса. Отсюда типовая настройка: размер пула соотносят с лимитом базы и числом инстансов сервиса, а на исчерпании — ждут с таймаутом, а не открывают новое.

  2. #rs_web_db2 / 5
    Что такое экстракторы в axum?
    A)Функции, извлекающие соединение из пула перед запросом
    B)Промежуточные слои, оборачивающие обработчик логированием и метриками
    C)Макросы, генерирующие маршруты по объявлениям структур
    D)Типы в аргументах обработчика, разбирающие части запроса
    показать ответ и разбор
    +D)Типы в аргументах обработчика, разбирающие части запроса

    // разбор: Обработчик объявляет Json<Payload>, Path<u32>, Query<Params> или State<AppState>, и фреймворк сам разбирает нужные части запроса, а при неудаче возвращает ошибку. Контракт эндпоинта оказывается выражен сигнатурой функции. Ограничение одно: экстрактор, поглощающий тело, должен стоять последним — тело читается один раз.

  3. #rs_web_db3 / 5
    Как обычно отдают доменную ошибку клиенту в axum?
    A)Реализуют IntoResponse для своего типа ошибки
    B)Возвращают строку с текстом ошибки и статусом 200
    C)Паникуют: middleware перехватит и вернёт 500
    D)Пишут ветвление по варианту ошибки в каждом обработчике
    показать ответ и разбор
    +A)Реализуют IntoResponse для своего типа ошибки

    // разбор: Одна реализация IntoResponse переводит варианты доменной ошибки в статусы и тело ответа — обработчики после этого просто возвращают Result и ставят вопросительный знак. Логика перевода живёт в одном месте, домен ничего не знает про HTTP, а внутренние причины не утекают наружу: клиенту уходит безопасное сообщение, подробности — в лог.

  4. #rs_web_db4 / 5
    Почему в асинхронном сервисе нельзя брать синхронный драйвер базы?
    A)Он не поддерживает пул соединений
    B)Он не компилируется внутри async-функции
    C)Он требует отдельного потока на каждое соединение
    D)Он блокирует воркер рантайма на время запроса
    показать ответ и разбор
    +D)Он блокирует воркер рантайма на время запроса

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

  5. #rs_web_db5 / 5
    Чем axum отличается от actix-web по устройству?
    A)axum асинхронный, actix-web синхронный
    B)axum обходится без рантайма, actix-web требует tokio
    C)axum собран на tower: middleware — переиспользуемые слои
    D)axum заточен под JSON, actix-web — под шаблоны и формы
    показать ответ и разбор
    +C)axum собран на tower: middleware — переиспользуемые слои

    // разбор: axum — тонкий слой поверх hyper и tower, поэтому таймауты, лимиты, трассировку и балансировку берут готовыми слоями и переносят между сервисами без переписывания. У actix-web своя система middleware и своя история с акторной моделью внутри, зато он дольше на рынке и оброс сторонними крейтами. Оба асинхронные и оба работают на tokio, так что выбор — про экосистему, а не про скорость.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.