Веб-стек и базы в 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, остальные разбираются в тренажёре.
- Зачем веб-сервису пул соединений с базой?A)Он распределяет запросы между репликами по нагрузкеB)Он кэширует результаты запросов между обработчикамиC)Установка соединения дорога, а их число ограниченоD)Он делает драйвер базы потокобезопасным
показать ответ и разбор
+C)Установка соединения дорога, а их число ограничено// разбор: Соединение — это TCP-хендшейк, аутентификация и сессия на стороне сервера, а лимит подключений у базы конечен. Пул держит соединения открытыми и выдаёт их задачам на время запроса. Отсюда типовая настройка: размер пула соотносят с лимитом базы и числом инстансов сервиса, а на исчерпании — ждут с таймаутом, а не открывают новое.
- Что такое экстракторы в axum?A)Функции, извлекающие соединение из пула перед запросомB)Промежуточные слои, оборачивающие обработчик логированием и метрикамиC)Макросы, генерирующие маршруты по объявлениям структурD)Типы в аргументах обработчика, разбирающие части запроса
показать ответ и разбор
+D)Типы в аргументах обработчика, разбирающие части запроса// разбор: Обработчик объявляет Json<Payload>, Path<u32>, Query<Params> или State<AppState>, и фреймворк сам разбирает нужные части запроса, а при неудаче возвращает ошибку. Контракт эндпоинта оказывается выражен сигнатурой функции. Ограничение одно: экстрактор, поглощающий тело, должен стоять последним — тело читается один раз.
- Как обычно отдают доменную ошибку клиенту в axum?A)Реализуют IntoResponse для своего типа ошибкиB)Возвращают строку с текстом ошибки и статусом 200C)Паникуют: middleware перехватит и вернёт 500D)Пишут ветвление по варианту ошибки в каждом обработчике
показать ответ и разбор
+A)Реализуют IntoResponse для своего типа ошибки// разбор: Одна реализация IntoResponse переводит варианты доменной ошибки в статусы и тело ответа — обработчики после этого просто возвращают Result и ставят вопросительный знак. Логика перевода живёт в одном месте, домен ничего не знает про HTTP, а внутренние причины не утекают наружу: клиенту уходит безопасное сообщение, подробности — в лог.
- Почему в асинхронном сервисе нельзя брать синхронный драйвер базы?A)Он не поддерживает пул соединенийB)Он не компилируется внутри async-функцииC)Он требует отдельного потока на каждое соединениеD)Он блокирует воркер рантайма на время запроса
показать ответ и разбор
+D)Он блокирует воркер рантайма на время запроса// разбор: Синхронный вызов не отдаёт управление: пока драйвер ждёт ответа базы, воркер стоит, и его задачи вместе с ним. На нагрузке это видно как разъехавшийся хвост задержек при живом сервисе. Если библиотека только синхронная, её вызовы оборачивают в spawn_blocking — тогда ожидание уходит в отдельный пул.
- Чем axum отличается от actix-web по устройству?A)axum асинхронный, actix-web синхронныйB)axum обходится без рантайма, actix-web требует tokioC)axum собран на tower: middleware — переиспользуемые слоиD)axum заточен под JSON, actix-web — под шаблоны и формы
показать ответ и разбор
+C)axum собран на tower: middleware — переиспользуемые слои// разбор: axum — тонкий слой поверх hyper и tower, поэтому таймауты, лимиты, трассировку и балансировку берут готовыми слоями и переносят между сервисами без переписывания. У actix-web своя система middleware и своя история с акторной моделью внутри, зато он дольше на рынке и оброс сторонними крейтами. Оба асинхронные и оба работают на tokio, так что выбор — про экосистему, а не про скорость.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.