Хабр, привет! На связи Алексей Постригайло, старший партнер интегратора «Энсайн».
Если размышлять об идеальной разработке, я бы вообще хотел, чтобы ошибка не уходила дальше разработчика, потом она только дорожает. Код уже ушел в тестирование, разработчика надо возвращать в старую задачу, восстанавливать контекст, а после исправления заново запускать проверки.
Rust нам интересен тем, что убирает часть этой работы. И хотя в основной стек этот язык пока не входит, мы осваиваем его заранее. Нам важно проверить: Rust действительно снимает часть будущих проблем или же переносит стоимость разработки в начало проекта? Об этом и поговорим.
Как строгий компилятор помогает поддерживать надежность системы
Мы преимущественно работаем с системами, которым предстоит жить десять лет и больше. За такой срок меняются инфраструктура, несколько версий библиотек и состав команды; появляются новые требования и подрядчики.
Тесты и ревью проверяют бизнес‑логику и поведение системы. Но чем дольше живет проект и чем больше людей успевают с ним поработать, тем сложнее рассчитывать исключительно на внимательность разработчиков.
В этом помогает Rust: часть технических правил его компилятор проверяет еще до запуска. Эти правила потом не надо искать в документации или держать в голове.
Что Rust проверяет до запуска
Если оставить в стороне синтаксис, в Rust нас интересуют четыре механизма. Все они работают с разными ошибками, но по одному принципу: код не должен собраться, пока нарушено правило, которое компилятор умеет проверить.

Память без сборщика мусора
В Rust нет сборщика мусора, но разработчику не нужно освобождать память вручную. Так устроена модель владения. У каждого значения есть владелец, а при выходе из области видимости освобождается связанный с ним ресурс.
Компилятор проверяет другую часть этого механизма. Если значение передали новому владельцу, прежняя переменная теряет к нему доступ. В safe Rust это защищает от двойного освобождения памяти и обращения к ней после освобождения.
Во время работы отдельную «уборку» не запускают, поэтому нет связанных с ней пауз. Для наших систем это выигрыш в скорости.
Данные удалили, ссылку забыли
Значение можно передать новому владельцу или временно дать к нему доступ по ссылке. Во втором случае компилятор следит, чтобы сами данные все еще существовали. Если объект уже уничтожен, воспользоваться старой ссылкой не получится, обращение к нему будет ошибкой компиляции.
fn main() { let r; { let x = String::from("Привет"); r = &x; // Ошибка: `x` уничтожается в конце этого блока } // `x` освобождает память здесь println!("{}", r); // Попытка использовать освобожденную память! } –------- error[E0597]: `x` does not live long enough --> src/main.rs:6:13 | 5 | let x = String::from("Привет"); | - binding `x` declared here 6 | r = &x; // Ошибка: `x` уничтожается в конце этого блока | ^^ borrowed value does not live long enough 7 | } // `x` освобождает память здесь | - `x` dropped here while still borrowed 8 | 9 | println!("{}", r); // Попытка использовать освобожденную память! | - borrow later used here
В этом примере важен не синтаксис, а сама точка остановки. В другом стеке похожая ошибка могла бы проявиться под нагрузкой или после доработки. Здесь она остается на этапе сборки: ссылка не может пережить данные, на которые указывает.
Нам эта функция дает больше свободы при доработках: можно менять старый код и сразу видеть, где нарушились связи между данными.
Result и Option
Функция не всегда возвращает то, за чем мы к ней пришли. В Rust это сразу записано в типе. Option означает: значение есть или его нет. Result: операция завершилась успешно либо вернулась с ошибкой.
Во многих языках отсутствие значения обозначают через null. Решение простое, но коварное: разработчик может забыть о проверке, и ошибка обнаружится только во время работы программы. Тони Хоар, который ввел null‑ссылки в язык ALGOL W в 1965 году, позднее назвал это своей «ошибкой на миллиард долларов». По его словам, удобное решение привело к массе сбоев.
Здесь уже нельзя просто взять результат и пойти дальше. Разработчик обязан четко решить, что делать с отсутствием значения или ошибкой, обработать на месте или передать выше.
Конечно, можно написать unwrap(), то есть сказать: я уверен, что здесь все будет нормально. Если нет, программа упадет. Rust не страхует от плохих решений, но такое место хотя бы сразу видно в коде.
Нам такая возможность Rust упрощает ревью. По типу сразу видим, что функция может ничего не вернуть или завершиться ошибкой.
Гонки данных
Например, два потока работают с одним объектом. Пока оба читают данные, конфликта нет. А если один начинает что‑то менять, пока второй читает или тоже пишет, результат уже зависит от того, кто успел первым. Это гонка данных.
Важно разделять race condition вообще и data race как небезопасный одновременный доступ к общей памяти. Rust защищает именно от data races в safe‑коде. Более широкие ошибки параллельной логики он не отменяет.
Safe Rust в такой ситуации просто не даст расшарить изменяемый объект как попало. Сначала нужно ему показать, как мы собираемся согласовывать доступ: через блокировку, канал, атомарную операцию.
При этом компилятор проверяет доступ к данным, и только его. Он не поймет, что две блокировки ждут друг друга или что операции пошли в неверном для бизнеса порядке. Это уже остается разработчику.
Это удобно на ревью. Код, в котором два потока небезопасно делят память, до рабочей сборки не дойдет: его остановит компилятор. Наша задача — проверить выбранный способ синхронизации, не обменяли ли мы гонку данных на взаимную блокировку.
Перед заказчиком все равно отвечаем мы
У компилятора понятная зона работы: типы, память, ссылки, доступ к данным. Но ошибку в расчете скидки он не увидит, код может пройти сборку и при этом решать задачу неправильно.
Дальше уже наша работа — проверить продуктовую, архитектурную и эксплуатационную корректность:
Область | Что решает команда | Как проверяют |
Бизнес‑логика | Как должна работать система, какие сценарии допустимы, что делать в пограничных случаях | Автотесты, приемочные сценарии, проверка с бизнес‑заказчиком |
Архитектура | Как разделить систему на компоненты, связать их между собой и обрабатывать сбои | Архитектурное ревью, прототипы, нагрузочные и интеграционные тесты |
Права доступа | Кто может читать, изменять и удалять данные | Матрица ролей, тесты авторизации, проверка настроек |
Зависимости и инфраструктура | Какие версии использовать, как обновлять окружение и разворачивать приложение | CI/CD, тестовый контур, контроль зависимостей и конфигурации |
Отдельная зона — unsafe. В safe Rust правила работы с памятью проверяет компилятор. Unsafe мы оставляем для низкоуровневых операций, которые он не умеет проверить полностью. Такой код держим в небольших изолированных участках, рядом описываем условия его безопасной работы и отдельно разбираем на ревью.
Зачем мы наращиваем компетенции
Мы осваиваем Rust заранее, чтобы быть готовыми к проектам, где он действительно потребуется. Когда такой запрос приходит от заказчика, от компании уже ждут оценки сроков, стоимости и технических рисков. Ему не очень интересно, сколько недель мы будем разбираться со сборкой и выбирать библиотеки. Значит, эти вопросы нужно закрыть раньше.
Синтаксис Rust можно выучить довольно быстро. На это как раз уходит меньше всего времени. Потом берешь проект и смотришь, что у тебя вообще есть: библиотеки, SDK, драйверы. Когда они последний раз обновлялись, и не придется ли половину писать самим. Плюс сборка, CI, деплой, обновление зависимостей. Пока команда сама через это не прошла, нормально оценить сроки не можем.
Стоимость входа тоже лучше знать по собственному опыту. Rust может замедлить первую разработку: человек дольше разбирается с моделью данных, переделывает привычные конструкции, спорит с компилятором. Позже эта строгость помогает при изменениях и рефакторинге. Нам нужны свои цифры и наблюдения, чтобы нормально оценивать проект.
Rust мы прежде всего рассматриваем для сетевых и инфраструктурных компонентов: шлюзов, агентов, парсеров, обработчиков событий. Такой код работает постоянно, принимает данные извне, запускает много операций параллельно. Редкую ошибку с памятью или доступом к общим данным трудно повторить на тестах, а искать в работающей системе — долго и дорого. Rust часть этих ошибок ловит при сборке. Здесь его строгость уже окупает дополнительное время на доработку и тестирование.
Встраиваемые системы — отдельная история. Там технологию, бывает, выбирает за нас железо, когда памяти мало и привычной операционной системы нет. Во внутреннем сервисе такой необходимости обычно нет. Если его нужно быстро выпустить, а на знакомом стеке для этого уже все есть, Rust может быть невыгоден. Разработка займет больше времени, а этот код еще кому‑то поддерживать.

Новый компонент — новый язык
Стремление использовать Rust для всего подряд в IT‑сообществе породило ироничный слоган к языку:
rewrite everything in Rust
Или «Давайте перепишем на Rust абсолютно все». Это не наш план. Мы не видим смысла переписывать работающую систему только ради нового языка.
Возьмем сервис на Java, который работает пятый год. В нем наверняка есть условие, появившееся после давнего сбоя, или странный порядок вызовов, без которого отваливается интеграция. В документации об этом может не быть ни слова. При переписывании такие куски кажутся лишними, а потом выясняется, что на них все и держалось.
У нового компонента этого прошлого нет. Для него стек можно выбрать заново: посмотреть на нагрузку, окружение, срок жизни, требования к памяти, доступность библиотек и состав команды. Если по этим параметрам Rust дает преимущество, включаем его в оценку вместе с привычным стеком.
В больших системах Rust тоже сначала отдают новые компоненты, не трогая работающий код. В Linux, например, начиная с версии 6.1 на нем можно писать новые драйверы и модули ядра. Код на C никто из‑за этого не переносит.
В Android Rust используют для новых низкоуровневых компонентов. На нем написаны хранилище ключей Keystore2, стек сверхширокополосной связи, DNS over HTTP/3 и части системы виртуализации. Раньше такой код писали бы на C или C++.
В обоих проектах язык вошел через новую разработку. Мы используем тот же принцип.
Где Rust пригодится
Мы изучаем Rust, чтобы понимать границы его применимости. Сначала разбираемся с самой задачей, смотрим на несколько признаков и решаем, будет это Rust или привычный стек. Для себя собрали их в короткий чек‑лист:
На что смотрим | Где видим | Где не видим |
Тип задачи | Инфраструктурный компонент, сетевой сервис, обработчик событий, нагруженный модуль | Типовой сайт, простая админка, быстрый MVP |
Срок жизни | Систему будут годами обновлять и расширять | Проект нужен на короткий срок и почти не изменится после запуска |
Цена ошибки | Сбой остановит важный процесс, повредит данные или создаст угрозу безопасности | Компонент легко перезапустить, простой не принесет заметного ущерба |
Нагрузка | Много данных, параллельная обработка, важен расход CPU и памяти | Нагрузка умеренная, серверные ресурсы не ограничивают систему |
Команда | Есть несколько разработчиков, которые могут писать и ревьюить Rust‑код | Rust знает один человек или подготовленной команды нет |
Библиотеки и сроки | Для задачи есть нужные библиотеки и время на подготовку | Зрелый стек закрывает задачу быстрее и дешевле |
Если в левой колонке совпадают несколько условий, Rust включаем в варианты и считаем вместе с привычным стеком. Проверяем библиотеки, сроки и инфраструктуру. Если проект ближе к правой колонке, берем готовое решение и не усложняем.
И где Rust уже пригодился
Интересный проект получился с сайтом «Газпромнефть — смазочные материалы». Обычно после разработки сервер под нашим контролем. Мы сами ставим нужные версии языка и библиотек, обновляем окружение.
Здесь по‑другому. Архив приложения будут передавать зарубежным партнерам, и каждый развернет сайт у себя. Сколько получится установок, какие там будут серверы и кто займется их обслуживанием, мы не знаем.
Если бэкенд передавать на интерпретируемом языке, на каждом сервере придется отдельно готовить окружение, устанавливать нужную версию интерпретатора, расширения и системные библиотеки. Любое несовпадение обнаружится уже на стороне партнера, а дальше проблема пошла бы по цепочке от местного администратора к заказчику, а потом к нам. Проблема не в настройке одного сервера, с ней можно разобраться. Но когда установок много и они находятся в разных странах и не под нашим контролем, на сопровождение каждого окружения будет уходить слишком много времени.
Чтобы не играть в испорченный телефон, приложение написали сразу на Rust, компилируемом языке. Под нужную операционную систему и архитектуру процессора мы собираем готовый исполняемый файл. В него уже включены необходимые приложению зависимости.
Для нас это рабочая проверка подхода. Мы применили накопленные знания Rust на реальном проекте, а заказчик получил вариант поставки, который проще передавать партнерам и сопровождать после развертывания, а требования к окружению заранее понятны.
Что дальше с Rust
Чего мы точно не собираемся делать, так это переводить все проекты на Rust. В одной задаче язык даст заметную фору, в другой пока привычный стек сработает быстрее и дешевле. Нам сейчас важнее научиться различать эти случаи.
В изучение языка вкладываемся заранее, чтобы к следующему проекту прийти с готовой инженерной позицией и пониманием, кого ставить в команду, сколько времени займет разработка, с какими ограничениями мы столкнемся и во сколько выйдет дальнейшая поддержка.
Если у вас Rust уже работает в продакшене, поделитесь опытом, на чем он дал выигрыш: ресурсах или развертывании? И были ли задачи, где расходы на него не окупились?
