Сегодня доклад будет максимально простыми словами, будто сидим, пиво пьем, рыбку едим, потому что необычайно сложный контент. Но я хочу, чтобы вы выключили полностью мозги, расслабились, получили удовольствие и читали сердцем. Все это делал я, Александр Сербул, с небольшой командой. Моя задача — возбудить в вас интерес к Rust, высоким нагрузкам, асинхронщине, многопоточности и тому, как мы это используем.

Какие задачи мы решаем?
В Битрикс24 сотни тысяч коммерческих клиентов. Это много на самом деле. Они хранят файлы и базы данных в облаке. Сейчас это несколько петабайт данных и несколько миллиардов файлов, то есть Big Data. Эти данные нужно еженедельно бэкапить. Бывает так, что у нас бакет в одном регионе, мы бэкапим в другой, например, в России VK-бакет мы бэкапим в Яндекс-бакет, и наоборот.
Плюс, бэкап еженедельный, понятно, что за неделю может очень многое случиться. Поэтому лучше еще в реал-тайме делать репликацию синхронно этих s3-файлов, а дополнительно еще делать бэкап, чтобы максимально восстановить данных клиентов, не потерять их в случае какой-то катастрофы. Потом нужно эти файлы восстанавливать, но это не проблема.
Таким образом, нам нужно:
Делать еженедельный бэкап s3-бакетов
В реал-тайме синхронизировать события создания/удаления файлов в бакетах между основным и резервным
Восстанавливать файлы клиентов из резервного бакета в основной
Итого, у нас есть клиенты, в реал-тайм делаем их репликацию, потом еженедельный бэкап, а это миллиарды файлов, несколько петабайт данных, большая Big Data.
Хранение данных клиентов
Визуально это выглядит так.

У клиента в MySQL тысяча таблиц и от тысячи до десятков миллионов файлов, таких клиентов (коммерческих компаний) сотни тысяч. Нам нужно обеспечить бэкап MySQL и файлов. Сегодня про MySQL не рассказываю, привожу для полноты картины, понимания масштаба. Все это хранится в s3. Один клиент — это верхнеуровневая папка в s3.
Еженедельный перенос изменений
Сначала расскажу, как мы делаем перенос изменений еженедельно, потом как применяем технологии к этому, и как было и стало.
Бэкап файлов между основным и резервным бакетом s3
Как его сделать?
Сначала был кластер Hadoop из 5-10 машин – долго (неделя), дорого
Один файл — это одна джоба. Она туда летит, все гудит неделю, файлы копируются — красиво, функционально, но дорого и долго. Еще был Spark, но это слишком громоздкое решение.
-
Теперь «сверщик» метаданных (md5/длина):
Досылает десятки тысяч файлов в неделю, работает 1-2 дня
Собирает и сверяет списки файлов клиентов в памяти (s3 list v2)
Уместили все в памяти одной машины, загружаем списки названий нескольких миллиардов файлов попапочно, проверяем md5. В западном облачном провайдере есть команда get list s3 по префиксу, которая возвращает список. Сначала получаешь список папок наверху, их сотни тысяч у клиентов (коммерческих компаний). Потом заходишь в каждую папку, и сразу же этот list возвращает md5 и длину, потому что если делаешь multipart upload, md5 отличается слева и справа, хотя файлы идентичны. Поэтому тогда можно мерить по длине и по времени изменения.
У нас «сверщик» работает на сервере с большим количеством ОЗУ несколько дней, делает сверку и досылает изменения, то есть мы не делаем бэкап уже через Hadoop, а делаем таким «сверщиком». Есть риск, что крупный клиент когда-то не поместится в ОЗУ, но пока не мы упираемся в это. Мы заменили Hadoop полностью на машину, которая работает в памяти и сверяет.
Как это работает:

Сервер сверяет листинги попапочно и передает изменения раз в неделю соседний бакет.
Изначально было реализовано на Java.
Алгоритм достаточно простой:
Загрузить листинг слева в ConcurrentHashMap в ОЗУ по папкам верхнего уровня
Загрузить листинг справа в ConcurrentHashMap в ОЗУ по папкам верхнего уровня;
загрузить файлы в каждой папке (||), убирая на лету дубликаты по md5 или длине и времени модификацииОстатки скопировать
А вот остатки копировать интересно. Сначала мы копировали синхронно, потом файлов стало иногда много, и мы копируем асинхронно. В Java это делается через Netty.
Это был бэкап. Теперь расскажу, как мы делаем «реалтайм» репликацию.
Realtime-перенос изменений между бакетами s3
Изначально — NodeJS внутри Lambda AWS на события основного бакета s3.
У нас бакет был изначально в западный облачный провайдер. Хотя он там есть, но мы разделяемся. Там шлют в s3 события по изменению бакета, s3 перехватывается serverless Lambda функцией в Amazon. Мы написали ее на NodeJS. Потом переписали на Java, потому что NodeJS сыпался.
Текущее решение — Java (sync) копирование в Lambda-функции AWS «небольших файлов», отправка в SQS-заданий на копирование «больших файлов».
Когда имя файла получено, проверяем, если файл маленький, его копируем на лету. У нас одновременно запускается несколько тысяч воркеров процессов Java, которые в Lambda функции западного облачного провайдера копируют файл. Если файл большой, мы ставим задание в очередь, а очередь разгребают специальные «разгребатели», которые эти большие файлы потом копируют. Потому что если копировать большие файлы синхронно, огромные деньги уйдут на Lambda вычисления, на serverless.
Постановка задач через DynamoDB на «отложенное» удаление.
Не буду про это рассказывать, нам сейчас это неинтересно.
Отправка недоставленных событий SQS в DLQ-очередь для переотправки.
DLQ — это классика.
Несколько серверов обработки заданий из SQS на копирование «больших файлов» (Java/AWS SDK/async/mp-upload) и DLQ.
Получается, что большие файлы надо лить через отдельную инфраструктуру, через очередь, через «досылатели»-сервера, а маленькие файлы можно лить в реал-тайме.
Так это работало и сейчас работает у западного облачного провайдера : в s3 события перехватывают Lambda-функции. Запускаются тысячи функций, которые копируют мелкие файлы на лету. Если по размеру файла видно, что в s3 прилетел большой файл, проверяется размер, файл ставится в очередь, и там запускается флот «разгребателей», которые эти большие файлы льют, копируют и в итоге перемещают туда.

Мы с этой схемой несколько лет назад пришли в Россию, решили делать то же самое между VK и Яндексом.
Realtime-перенос изменений между бакетами s3: VK Cloud => Yandex Cloud
Поменяли архитектуру для событий (веб-хуки) в бакете s3 VK Cloud.
Мы столкнулись с тем, что в VK Cloud не было веб-хуков, Lambda, serverless, хотя[АС2] в Yandex Cloud они изначально были. VK нам предложил: «Мы будем веб-хуки дёргать несколько тысяч раз в секунду по событиям, можете их читать и выполнять свою операцию».
Обработчик веб-хуков (брокер) событий S3 VK Cloud на Java/AWS SDK/Netty, YC SQS, YC DynamoDB.
В итоге написали классический обработчик веб-хуков на одной машине — мощная машина, Java/Netty для асинхронщины, классический AWS-SDK. Мы используем классические AWS-SDK, и события по веб-хукам перехватываем и ставим в очередь Яндекса. Там у нас стоит флот «разгребатель», который эти файлы копирует и YC DynamoDB, который отлично много лет работает, куда мы кладем события об отложенном удалении.
Постановка (асинхронная) заданий в SQS-очередь Yandex Cloud на копирование всех файлов, запись заданий «отложенного удаления» в YC DD.
В итоге прилетает задача из VK, имя файла видим, ставим задачу в SQS-очередь Yandex Cloud на копирование файлов. В DynamoDB Yandex Cloud пишем, где отложено удаление.
Тюнинг обработчика на Java/Netty — увеличенные retries, тайм-ауты, увеличенный пул соединений AWS SDK Java.
Кажется, все логично и красиво — потюнили немножко AWS SDK Java, увеличили retries, потому что это в России теперь работает, timeout немножко увеличили. По сути, там встроен пул соединений HTTP, 100−200 соединений висит внутри. Казалось бы, все хорошо.
Выделенные серверы-копировщики файлов – Java/AWS SDK/async.
«Разгребатели» очереди у нас — это серверы-копировщики в Yandex Cloud, копируют асинхронно. Причем теперь у нас «разгребатели» льют все файлы, которые мы поставили в очередь в Yandex Cloud.
Копируем «через» disk, минусы и ... плюсы.
Все просто — скачиваем файлик, потом засылаем его. Минус в том, что если диск красный, то замедляется копирование. На это редко наступаем, но несколько машин таких «разгребателей» сейчас стоит.
Копируем большие файлы (>=5 GB) – multipart-upload.
Кусками досылаешь большой файл в API s3, потом говоришь: «Досылка завершена, пожалуйста, все объедини в один файл». Но при объединении разрушается md5 сумма, и непонятно, что файл скопировался правильно, потому что md5 слева и справа разные (md5 зависит от количества кусков слева и справа). Конечно, проблема в самом протоколе s3.
Мониторинг узких мест и отправка CloudWatch-метрик.
Никаких Grafana и Prometheus — мониторим через CloudWatch-метрики, и там смотрим по графикам.
Вот как это работает:

Из s3 VK Cloud события прилетают в брокер веб-хуков на Java/Netty, он ставит задачу в Yandex Cloud в очередь заданий на все файлы, а там тысячи событий в секунду прилетают. Дальше стоит флот «разгребателей» — разгребают, копируют через диск. Java/Netty, классический AWS SDK копируют файлы в Yandex Cloud.
Архитектура хорошая, прозрачная, всё хорошо работает. Мы взлетели, и что у нас произошло?
Брокер событий s3-бакета VK Cloud – развитие
Мы взлетели, и что у нас произошло:
Naïve Java/Netty/AWS SDK — отправка в лоб событий в Yandex Message Queue (SQS API) и Yandex Database (DynamoDB API)
Russian Clouds ...
Не знаю, обидно это или нет, но наши любимые, замечательные облака работают иначе. И началось!

Я просто сейчас учусь в автошколе, поэтому поставил предупреждающий знак.
Что началось?
· Резкие замедления Java/Netty при сборках мусора.
Внезапно у нас стал Java/Netty тормозить — все стоит колом при сборке мусора, не понимаем, что происходит.
· «Рассыпания» 500-ошибками без request-id облаков
Вдруг облака, не говорю, какие, потому что мы много лет с ними работаем (Yandex Cloud, VK Cloud и другие облачные партнеры) сыпятся. Ошибка 500 и строка, что это сервер nginx, написанный замечательным Игорем Сысоевым — и всё. А где request-id? Это же клиент западного облачного провайдера, 500-ошибка?!
· Внезапные замедления работы API облаков на порядки на часы
Вдруг API облако начинает тормозить на часы. П��ошёл час, два часа — у тебя ответ был 100 мс, стал 5 секунд. Ты пишешь в поддержку, они говорят: «Сейчас посмотрим. Да, проверьте сейчас» — «Блин, а мониторинг где?».
· Общение с техподдержками облаков – докажи, что проблема не у тебя, а у них
Нужно доказывать техподдержке, что проблема у тебя есть: «Request-id нет, проблемы нет!» — «Ну, как нет? 500 ошибка!» — и понеслось по кругу.
· Где взять детальную статистику по неотправленным сообщениям, ошибкам и очередям в брокере?
Надо статистику собирать ночью, когда проблема, а проблемы ночами бывают, и отправлять в поддержку.
· А если у облачного провайдера нет графиков с их ошибками?
· А если графики есть, но они «средне по больнице»?
Бывает, у облачного провайдера вообще нет графиков, или они есть, но на них все хорошо. У тебя проблема, 500 ошибки сыпятся — что делать? И не знаешь, это хорошо, что есть графики или нет. Так что графики — это не всегда решение.
Брокер событий s3-бакета VK Cloud – логирование и сетевая трассировка
Итого, что мы стали делать:
· Уровни логирования Java/Netty и паузы сборщика мусора/рост внутренних очередей.
Мы же Java-разработчики, я 20 лет на Java пишу — включаю в Netty отладку, debug и trace, сеть пишется в файл, и я вижу, что внутри происходит. Ага, Java сходит с ума.
· Тормоза сборщика мусора Java при росте внутренних очередей (миллионы) за часы аварий облачных сервисов.
Сборщик мусора сходит с ума, процессоры вылетают в полку, очередь растет, память потребляется, замедляется полностью наша инфраструктура, потому что когда включаешь отладку в Netty, все начинает на порядки больше тормозить.
· Резкое замедление при включении сетевой трассировки тела пакетов в Java/Netty.
Облачный провайдер не может понять, где проблема. Наши провайдеры активно, интенсивно развиваются, они быстро это фиксят, с ними приятно работать, но надо же сначала сказать, что не так, а ты сам не понимаешь, что не так. У тебя Java стала колом, начинаешь трассировать — у тебя стоит сервер колом.
· Мы начали экстренный поиск другой технологии
Еще раз я пройдусь, что может быть не так.
Java/Netty больше не помогает, как раньше...

Java 20 лет с нами, все хорошо. Но Java/Netty не тянет эти задачи. Какие выходы? Допустим, еще больше железа поднять. Если начнутся внезапные тормоза у облачных провайдеров, нам надо, чтобы сервера эту нагрузку вынесли, то есть, держать в запасе еще 10 серверов на случай, что облачный провайдер чем-то занят и не видит, что у нас проблема.
Истории «проблем» Java/Netty с облачными сервисами
Провайдер выкатил «битую реализацию» протокола SQS
· Бой стал колом
SQS не работает. Провайдер говорит: «Обновляйте», мы обновляем SQS, у нас бой становится таковым.
· Включение трассировки заголовков и тела пакетов AWS SDK Java в Netty замедлило пропускную способность брокера
Включаю трассировку и вижу в пакетах, что заголовок в SQS передается с маленькой буквой вместо большой. Открываю стандарт, спрашиваю: «Вы видели? Тестировали?» — «Да, тестировали. Все тестами не покроешь, понимаете, так бывает», быстро фиксят. В этом случае Java/ Netty не помогает. Включили трассировку, все стало колом.
· Растет очередь запросов в памяти
· Увеличилось время сборок мусора (секунды) и latency (секунды вместо мс)
Очередь стала расти в памяти, время от сборки мусора стало 20−30 секунд вместо секунд, latency увеличилась, в общем, все стало колом. Зато мы помогли провайдеру быстренько это пофиксить.
REST API провайдера резко увеличило latency на порядок до секунд
Вдруг у провайдера (это собирательный образ, такое бывает у разных провайдеров) у тебя latency s3, очередь, чего-то еще вырастает несколько раз, потому что виртуальная машина уехала с мощного железа на менее мощное, и никто не заметил это: «У нас же есть Docker, Kubernetes, виртуализация, DevOps наконец!» Latency выросла на порядок, а ответственных нет: «А, это Kubernetes перевел под куда-то!» — «Но у меня всё тормозит, клиенты возмущены» — «Kubernetes виноват».
В итоге:
· Резкое накапливание очереди запросов к s3 в памяти до 1-2 миллионов сообщений
· Общая деградация latency брокера
· Зависания сборщика мусора (паузы в секунды)
· Процессоры - в полку
Java очень требовательна к оперативной памяти, начинает сжирать десятки гигабайт. Latency падает, потому что сборщик мусора начинает все это убирать, и сервер просто тормозит, занимаясь сборкой мусора — пауза в секунды. Нам другой облачный провайдер звонит: «Что такое?» — «Не знаю, все работало, не понимаем, что делать».
· Как быстро найти причину проблемы?
А никак, включаешь, еще хуже становится при отладке.
Ночные потоки ошибок 50х (в т.ч. 500!) от REST API облачных провайдеров
Бывает что ночью каждые 5-10 секунд от провайдера летят пачки ошибок, в том числе, 500. Ты должен собрать эту информацию, задокументировать, собрать следователю папочку. А request-id нет, забыли. И надо прописать дату, время UTC, собрать логи, включить сетевую трассировку.
Что происходит?
· Активное переключение контекстов в потоках внутренних пулов AWS SDK Java
Становится колом AWS SDK, потому что он буферизирует события в памяти, у него определенное количество HTTP пулов, он не успевает это все переключать.
· Создание многочисленных таймеров для выполнения aws sdk retries
AWS SDK начинает делать retries, то есть, если Nginx отдал 500 что-то, он начинает рандомно делать retries с замедлением.
· Увеличилось latency брокера до секунд с мс
· Влияние сборки мусора на latency
В общем, приходим — все стоит колом. Оказалось, у провайдера просто ночью все стало работать медленно, под в Kubernetes куда-то переместился не туда. Написали, её вернули обратно, но через неделю под опять уехал[АС1] . И вот общаешься, а latency увеличивается.
· Теряются бизнес-события
Нас все не любят, потому что провайдер говорит, что у нас все работает медленно, а мы говорим, что нет, это вы, это Kubernetes — в общем, ищем виноватого. В итоге понятно, что происходит.
Нам нужна была технология, которая бы это все позволяла решить.
Нужно больше железа? Или ума?
Есть два варианта:
1. Поднять железо, нанять людей, в том числе, аварийную команду с папочками для следователей, что происходит ночами, почему 500 сыпятся.
2. Подумать и попытаться другую технологию использовать для того, чтобы все работало быстро.
А какую другую технологию?
Пока нагрузка стабильная – все хорошо. Но сейчас бывают проблемы:
· Замедление сети - растут внутренние очереди в памяти
· Накапливаются таймеры
· Активно переключаются контексты потоков
· Предсказуемости latency от Java/Netty больше нет
Нужно больше железа/людей для realtime. Все становится колом, надо что-то делать. А что делать?
Давайте порассуждаем, что делать. Java/Netty? Golang? Golang — это Java минус 90% функционала. Если коротко, в Golang есть горутинки, остальное то же самое будет, сборщик мусора такой же. C++? Надо 50 лет учиться C++, прямо с детства, или уже родиться с ним, чтобы даже предки знали C++, потому что в языке возможно неопределённое поведение, и статанализ не поможет. Нужно что-то другое. А что еще есть? Есть Rust.
Почему выбрали Rust для брокера событий s3-бакета VK Cloud
Что обещает Rust:
· Нет сборщика мусора
Ого, нам как раз это надо! Отлично.
· Сильно ниже аппетиты к памяти (порядки)
Аппетиты к памяти на порядки меньше (потом расскажу, почему).
· Деструкторы вставляются автоматически компилятором
То есть ты не думаешь об этом. Напоминает Python.
· Деструкторы выполняются заметно плавнее (незаметно)
Нет фаз сборщика мусора, и это удобно, потому что в итоге они выполняются незаметно.
· Zero-cost абстракции (как в C++) без сюрпризов
· Можно создавать объекты не только в heap, но и в stack
· Можно безопасно передавать ссылки на объекты – меньше копирования
· Можно избегать копирования (zero-copy) - важно для сети.
Вспоминаю, как я расстраивался — ноги на столе, мой язык в вашей баге, C++
. Это про фото Бьярна Стауструпа, автора C++, когда он сидит на кресле, ноги на столе и ниже надпись «Язык мой – баги ваши». У этих то же самое — Zero-cost абстракции без сюрпризов, потому что Rust гарантирует 100% безопасную работу с памятью — что хотите, делайте!
· Можно создавать объекты как в Java не только в heap, но и в stack
О, в stack можно создавать! А можно между потоками структуры в stack создать? Можно! В C++ можно, и в Rust можно. Отлично, нам это подходит.
· Можно безопасно передавать ссылки на объекты вместо самих объектов – меньше копирования
В Java это тоже есть, понятно, что для сети это хорошо.
· Можно избегать копирования (zero-copy) - важно для сети
То же самое — можно ссылки передавать, хорошо.
Самое интересное, почему мы взяли Rust:
· Встроенная асинхронность
Почему Golang взлетел? Потому что Golang выкинул 90% от Java и добавил встроенную асинхронность и горутины. Вот что такое Golang. В Rust тоже добавлена асинхронность прямо из коробки, она встроена в язык. А нам она нужна, потому что позволяет на одном потоке обрабатывать десятки тысяч сокетов.
· Асинхронные функции компилируются в корутины
· Корутины – stackless
По сути, примерно как в Golang, только с более строгой типизацией.
· Готовые runtimes для работы корутин (Tokio)
Если нужен какой-то runtime для того, чтобы запускать асинхронщ��ну в Rust, ты используешь один из движков. Их несколько, Tokio самый популярный. То есть ты как бы внутреннюю реализацию меняешь — будто взял Golang без сборщика мусора и можешь поменять внутренний runtime — классно.
· Строгая система типов (аффинные, sum, prod и др.)
· Автовывод (Hindley-Milner type inference)
· Pattern-matching по типам
Система типов на порядок более строгая. Самое страшное — это аффинные типы. Их не было вообще, я никогда с ними не сталкивался. Аффинные типы данных — это версия линейных типов. Расскажу немножко, этого вообще нигде нет.
Ребята[АС1] из Rust сделали sum-type и prod-type (sum-type — enumeration, prod-type — обычная структура), Pattern-matching и настоящий автовывод типов Hindley-Milner. Получается, можно писать как на Python без указания типов, при этом все строго типизировано, компилируется в бинарку, без сборщика мусора запускается, асинхронность безопасна — беру!
· Нет NULLs
В Golang и Java
NULLs до сих пор есть, говорят, не победили. А в Rust их убрали, это хорошо.
Звучит вкусно, давайте разбираться, так это или нет.
Нет NULLs в 2025, ну как же так? Почему же так долго?
В Rust, правда, NULLs нет, как в Kotlin, я зашел и проверил. А почему NULLs в Java до сих пор? Да потому что кто их будет менять? Это корпорация, деньги вложили — кто будет сейчас это делать? Ты как дурак, прошу прощения, вставляешь во все функции проверку с NULLs — зачем этим заниматься? Уже ChatGPT придумали, а NULLs до сих пор программируют.
Что еще взорвало нас на Rust
· JSON - это enum (sum-type)
· Рекурсия – через heap (ссылку)
· Никакой «кровавой императивщины» как в java sealed classes
Описание стандартным Rust обычного enum-типа слизали это с Haskell на самом деле. То есть это JSON, описанный в системе типов:

Лаконично, да? Я удивился после «кровавой императивщины» на Java, когда вы разбираете JSON и парсите. Все решается — а так можно было? Можно потому, что пришли математики в C++ и навели порядок. Haskell перелез в системное программирование.
Безопасный сетевой код, Sync, Send
Как работает система типов:
· 100% защита от гонок данных и (UB) от компилятора в safe-коде
· Уверенность через трейты Sync/Send/Pin
· Не нужно искать в документации Java слово «ThreadSafe»
· Потоко-небезопасный код - вообще не скомпилируется
В Java надо искать, что у тебя класс ThreadSafe или not ThreadSafe. Угадал, не угадал, если не угадал — чувак, извини! В интернете споры — ThreadSafe, not ThreadSafe. В Rust все определяет система типов[АС1] . Если трейт реализован Sync и Send, это ThreadSafe, если нет, просто не скомпилируется. То есть вы вообще не думаете об этом, собираете классы и, если не компилируется, значит класс не thread-safe. Люди подумали головой изначально и решили на уровне системы типов проблему многопоточного безопасного программирования.
Нет никакого UB (Undefined Behavior). Неопределенное поведение — это страшное дело вообще. Есть сотни правил возникновения UB, никто их не знает, никто их не учит — работает и работает.
· Горстка точек с «happens before»:
- Запуск потока
- Присоединение к потоку
- Захват блокировки мьютекса
- Ordering – в atomics

Чтобы понять последнюю строку, мне пришлось прочитать замечательную книгу «Rust: атомарности и блокировки». Мара Бос рассказывает, как работает в Rust модель памяти C++. Эта модель памяти очень сложная, но даже если вы сделаете не так, но у вас там только данные, вы ничего не потеряете, UB не будет, просто какие-то данные не синхронизируются, а для счетчиков это будет работать так. Это сложно на самом деле, сомневаюсь, что кто-то вообще это читал.
Сетевые примитивы в Rust
Send
Можно пересылать (move) между потоками.
С move переходит и владение через НЕПРИВЫЧНЫЕ аффинные типы
Как работает Send? В Rust нет наследования, наследование считается злом, есть трейты. Композиция считается лучше наследования, это вообще общий тренд. В Java так считается тоже. То есть OOP нет, есть отдельно структуры данных: ортогональная структура типов, ортогональная структура методов Есть структуры, enums, всякие там units. Короче, есть структуры данных, есть методы и есть трейты. Трейты — это интерфейсы. Если вы пишете трейт Send, просто говорите — это Send. Его можно посылать между потоками, все безопасно.
Sync
Можно ссылаться из потоков
Если у вас реализован трейт Sync для объекта, для класса, для типа, то можно на него ссылаться из потока.
Два реализованных трейта — очень грамотный подход к многопоточности.
Безопасные примитивы в Rust
Взял из «Rust in Action» book.

· String (обычная строка)
Неудобно работать. UTF поддерживается — ничего интересного.
· Box<T>
Здесь уже интересно. Вам надо какой-то тип в heap поместить, чтобы он там лежал, а не в стеке, делайте Box<T> — он у вас в heap теперь живет. Дальше объясню, как это элементарно композируется.
· Vec<T>
Нужно какое-то хранилище, делайте Vec<T>.
Одна проблема — в стандартной библиотеке Rust этих примитивов не так много. Есть сетевые примитивы и асинхронщина, нагрузочные есть, но их не так много. Если надо, например, как в Java, ConcurrentHashMap, «с бубнами и подтанцовками» — этого не будет. Это надо брать в Community crates.io и компилировать. Их тоже много, это все решено. В стандартной поставке Java уже будет ConcurrentHashMap. В Rust вам надо заниматься, чуть дальше покажу, чем.

· Cell<T>
Как поступил Rust, если нужна мутабельность внутри имутабельного типа по умолчанию? Функциональное программирование, все и имутабельно, да? Что сделал Rust? Они сказали, что надо минимизировать мутабельность. Вы можете иметь ссылку на объект — либо одну мутабельную, либо несколько иммутабельных. Таким образом они решили проблему, которой 50 лет в IT — это алиасинг. Вы передаете две ссылки в функцию, и она не понимает, это одна ссылка или нет, может, будет пересечение. Это называется алиасинг. В LLVM (llvm.org), если вы укажете, что это разные, не пересекающиеся ссылки, он дает большую оптимизацию. Rust это победил. Это очень смелый шаг.
Разработчик Rust Грэйдон Хор 3 года проработал и выгорел, потому что язык реализован в реально смелой концепции. Вы тоже можете выгореть, потому что язык сложный, но интересный.
· Rc<T>
Это счетчик ссылок. Можно захватывать память, считать ссылки. Если больше ссылок нет, память автоматически освобождается.
· Arc<T>
То же самое, только для ThreadSafe
Есть еще Mutex<T>, семафоры, каналы и т.п. — эта трехамудрия, которая тянется 50 лет, в Java и C++ вся есть. Но все это потоко-безопасно.
Как это собирать?
Безопасная композиция примитивов в Rust
Самое интересное — вы делаете матрешки. Если мне нужен ConcurrentHashMap, я умею ConcurrentHashMap.

Пожалуйста, HashSet оборачивается в Mutex, оборачивается в Arc — все, ConcurrentHashMap. Да, при нагрузках определенного рода это будет масштабироваться не очень хорошо. Но это случится в одной тысячной или сотой доле проектов, и когда у вас это случится, вы берете какой-то Community Crate, где он Java задачу ConcurrentHashMap решает. Он просто делает несколько Mutex на разных кусочках.
В Rust безопасная композиция примитивов так собирается. Вам даются примитивы, пожалуйста, Java/Netty собирается по принципу матрёшки. Если вы не в том порядке указали, элементарно просто не скомпилируется. По сути, вы играете в Лего на работе в Rust, собираете кубики и общаетесь с компилятором.

Забегу вперёд. Я считаю, что когда вы программируете с помощью Rust-компилятора, вы как будто с ChatGPT работаете. Но такой ChatGPT — это просто галлюцинирующая система, вы общаетесь с безумным существом. А Rust-компилятор — разумное существо, это математика и линейная теория типов, это Haskell — наука пришла спасать нас. Грэйдон Хор говорил, что язык Rust создан для того, чтобы защитить языки настоящего от самих себя, призвав языки прошлого им на помощь. Он призвал математику — пришла математика.
Асинхронный runtime в Rust – Tokio
Вы спрашиваете, а где асинхронщина? Асинхронщина — это просто типы. Есть тип, вы просто пишите корутины, они компилируются. Здесь то же самое. То есть ничего нового не надо придумывать — все так же, система типов вас защищает. Вы пишете обычный код, помещаете async, и он выполняется у вас. Точка останова вызывается, запускается. Вам нужен runtime.
Популярный runtime для Rust — Tokio, мы взяли его. Там есть асинхронщина всего:
· Асинхронный ввод-вывод сокетов сети, файлов, процессов, таймеры
· Асинхронные каналы, Mutex, семафоры, барьеры, асинхронные очереди
Мои разработчики любят, когда прихожу и говорю, что надо использовать сегодня новый асинхронный Mutex — красиво звучит! Вообще все, что можно, там заасинхронили, классно же!
· Отладка с Tokio console и подводные камни
Можно отладку делать с помощью только Tokio Console, это такой визуализатор. Честно сказать, мне не понравился, потому что зачем мне это надо, я и в логе все вижу.
· Логирование асинхронных операций с учетом контекста (span) – tracing
А вот логирование сделано хорошо. Когда вы работаете с асинхронщиной, в логе идет кровавое месиво, и непонятно откуда и какая корутина что написала. Нужно понять контекст. Span в логе — это как раз контекст: вы на таком-то уровне абстракции, на такой-то корутине, там-то зашли. Так логи читаются лучше, поэтому span — полезная вещь.
· AWS SDK for Rust уже официально и на базе Tokio
В Amazon Rust используют давно, наверное, с 2015 года, когда Rust только появился. У него инфраструктура и очень много кусков написано на Rust. Они говорят, что когда с Java переходят на Rust, в несколько раз счет за оборудованием падает. Чуть позже расскажу, что так и есть на самом деле. Вот. Не так давно у Amazon появился AWS SDK for Rust уже официально, можно брать и использовать официальную SDK на Rust.
Сбор асинхронной статистики в atomics
Как мы собираем статистику? Это самая сложная тема в Rust, по моему мнению. Это C++ часть, ordering, модель памяти.

Если у вас счетчики, так сделайте, у вас счетчик будет работать. Об остальном не думайте, а читайте книгу Мары Бос. Это очень сложная книга, я читал 3 раза, с третьего раза понял. Но я вообще тупой, примерно 5 лет входил в Rust, много книг прочитал. В Haskell тоже входил несколько лет. Есть люди более умные, они это поймут, а мне было трудно. Поэтому пример привел, как счетчики наработать, UB у вас не будет.
Дальше начинается web, а что с web?
Axum – простой web-фреймворк для завершения пазла
С web есть фреймворк Axum на Rust, который позволяет писать асинхронный web. Что такое асинхронный web?
· Пишем асинхронные функции – обработчики URL’ов
· Парсим входной JSON через Serde
· Передаем контекст и мутабельный state между корутинами
· Ограничиваем нагрузку через асинхронные семафоры Tokio
· Работаем с разделяемыми структурами данных (hashmap)
· Логируем через tracing-subscriber
По сути, это Nginx плюс Lua, только строгая типизация и асинхронщина. Мы как куски Nginx стали писать на Rust в Axum, обработчик написан так. Парсим входной JSON. Дальше вызываем ветку и семафорами ограничиваем нагрузку. Понятно, семафоры всегда нужны, даже в Java. В памяти у нас структура типа данных, мы переносим Java, по сути, туда (hashmap), логируем это все.
В Java есть ощущение, что когда занимаешься асинхронностью и многопоточностью, делаешь уже что-то не то. Чуваки из 90-х, тот же Гослинг, о другом думали, когда это делали. Ты как бы не туда идешь и думаешь — пойти в «плюсы»? Но ты боишься «плюсов», потому что там куча, сотня UB. А Rust говорит: «Пожалуйста, чувак, вот тебе системное программирование, вот тебе асинхронщина».
Мы пошли, и оказалось, код, который был на Java, на Rust стал проще, открылись большие возможности поработать с асинхронщиной. Еще мы кусок Nginx написали на Axum на входе. Мы решили полностью все.
Утечки памяти, фрагментация heap – история
· Напоролись на «утечку памяти» в Tokio
Первое, с чем мы столкнулись, это утечка в памяти в Tokio, гигабайты за часы стали появляться. Оказалось, никакой утечки, мы просто деструктор не вызывали у запущенных корутин. Разобрались, это детская ошибка в Токио.
· Важен размер очередей при хранении строк в памяти
Если вы в памяти храните строки, у вас могут быть гигабайты и десятки гигабайт памяти, это нормально. Что мы сделали? Мы убрали весь SQS, который в облаках используем, в оперативную память, мы убрали очереди в оперативку. Дальше расскажу, что еще сделали.
· Фрагментация heap – мифы и реальность
· Стандартный memory allocator в Linux и его тюнинг
Вы знаете, в Rust память выделяется через malloc/free. У нас фрагментация — такое слово страшное, а давайте бороться с фрагментацией с помощью тюнинга memory allocator, давайте использовать jemalloc! Оказалось, никакой фрагментации у нас нет. Просто память растет, потому что данных много. Данные уходят, память постепенно возвращается.
· Jemalloc - нужен ли он нам и вам всегда?
Но с jemalloc память уходит быстрее, и Rust уже перестал поддерживать jemalloc[АС5] по умолчанию. В общем, аллокатором себе голову не забивайте. Память в Rust не течет, это все контролируется, поэтому забудьте.
· Режим unsafe в Rust – просто, с пивом и рыбкой
Что такое unsafe? Говорят, ваш Rust — это все фигня, там есть режим unsafe, и там можно заниматься всякими жуткими извращениями. Можно, да. А в PHP? Вы пишите модуль в PHP на C — PHP и кровавое месиво на C, то же самое. Python — пожалуйста, я пишу на Python, а модуль на чём для Python? На C, и ты переключаешься между двумя языками.
В Rust сказали — давайте так не делать, давайте и модуль к языку быстрый, и сам язык делать так же, писать в одних файлах, только блоки unsafe ставить, и все — в unsafe функции unsafe, так они сделали. В unsafe нельзя заниматься всякими жуткими извращениями. В unsafe можно работать с указателями — не с ссылками, которые всегда разыменовываются безопасно в Rust, а с указателями, их можно разыменовывать.
· Зачем тут valgrind и miri
Да, в unsafe можно попасть на неопределенное поведение, но в сообществе принято, что код unsafe прогоняется через miri. Это интерпретатор, который в одном потоке выполняет код и в LLVM определяет точки появления unsafe. В принципе, Rust базирован на LLVM, такое ощущение, что сообщество уходит от GCC (GNU, вся эта история). У них LLVM, miri для того, чтобы с UB бороться, и valgrind для борьбы с утечками памяти. Не думаю, что вы будете unsafe писать в Rust, это никогда не потребуется, потому что все, что можно, уже написали давно, библиотека большая. Но если вам надо, можете писать.
Как работает теперь наш брокер событий s3 VK Cloud на Rust
· Java/Netty vs Rust/Tokio/Axum: 1-1.5 vs 6-8 CPU, 50-500 MB vs 1-4 GB (+ gc)
· Можно и в http-эндпоинте переложить в channel и сразу вернуться
На Java у нас брокер ел 6-8 CPU и 1-4 GB, на Rust — одно ядро и 50-500 MB. То есть в разы уменьшились требования к железу.
Архитектура брокера событий s3 VK Cloud:

Захватываем семафор, ждем. Дальше берем корутину, запускаем. Она парсит то, что прилетело в JSON. Дальше пишет в Yandex Cloud события s3 (типа скопируй такой-то файл), обновляет статистику и отпускает семафор — элементарно, по сути, блок-схема. В Python так же выглядит, а зачем писать на Python, если можно на Rust с такой же скоростью писать? Зачем тогда Python, если получается как JavaScript, только на Rust? Это работает под нагрузкой тысячи web-хуков в секунду. Если Yandex Cloud по каким-то причинам отдает 500, или тормозит, работает в 10 раз медленнее, чем обычно, то просто в памяти растет эта очередь, а память на порядок меньше, чем Java занимает, наверное, раз в 20 — очень хорошо заметно.
Дальше мы написали «удалятор». Бизнес увидел, говорит: «А давайте файлы в Amazon удалим на Rust» — «Давайте». Написали скриптик, который ходит в один бакет, в другой бакет, проверяет и удаляет файлы. Запустили его на одном ядре — 4 тысячи операций в секунду удалений.

Крупный облачный провайдер прибегает, говорит: «Ребята, что вы там делаете? DDoS какой-то» — «Да ничего, на Rust скрипт написали, запустили» — «Ух ты, на одном ядре?» — «Да, на часах можно запустить, по сути». Думаешь, а зачем тогда запускать Hadoop? Один человек писал на Хабре, что когда на Rust стал программировать, начал разбирать все Docker-контейнеры обратно все. Зачем их писать? Бинарку собрал, у тебя все работает, тебе не нужен Docker -контейнер, у тебя жизнь упрощается.
То же самое — пролистали, получили файлик, асинхронно захватили, передали в другую корутину, она выполнилась, удалила файлик, обновилась статистика между бакетами, и работает.
История про «удалятор»
Мы запустили удаление.

Удалили за несколько дней несколько миллиардов файлов, несколько петабайт данных. Здесь показана ежедневная нагрузка на удалятор на одном ядре в Битрикс24. Вы понимаете — приходит чувак, на Rust пишет код, на одном ядре ноутбука запускает, у него нагрузка больше, чем на Битрикс24 вся на удаление. Это про то, что к железу язык очень экономично относится, это радует.
Упрощенная архитектура «копировщика» файлов в s3
Сейчас мы запускаем наш обработчик асинхронного массового копирования файлов s3 на Rust. Как он работает?

Он берет файлы, получает задания в s3, файлы копирует через память. В Rust можно копировать файлы через память, из сокета в сокет. Если есть очередь, то мы запускаем канал, асинхронную очередь tokio::sync::mpsc::bounded. То есть одна корутина забивает задание, вторая копирует. Попробовали — льёт. По сути, нам не нужна теперь Lambda AWS, потому что мы можем спокойно использовать эту штуку на одной машине. Там мы упираемся только в сетевой интерфейс по скорости.
Все задачи решаются на Rust легко, причём никаких unsafe блоков мы не пишем — стандартная библиотека. Главное, мы теперь можем помочь нашим замечательно облачным провайдерам. Если у них возникают какие-то проблемы, мы теперь отладку можем включить, посмотреть логи, собрать статистику.
Как мы тестируем асинхронные приложения на Rust/Tokio
· Роль и настройка localstack
- Активно помогает в отладке запросов к aws-сервисам
- Проверяем сложные сценарии со связкой сервисов
Чтобы тестировать сервисы Amazon (s3 и так далее), надо использовать mock. Им является localstack. Это проект на Python, который эмулирует API Amazon. Мы тестируем на нем, очень хорошая штука.
· Юнит-тесты – важно ли так покрытие?
- Бывает очень дорого
- Пример: SQLite и его баги
- TDD
В самой популярной по инсталляциям в мире базе данных SQLite 100% тестового покрытия по бренчам (Branch Coverage), тестового кода на порядки больше, чем обычного кода, а баги всё равно есть. Вопрос: а надо ли писать тесты в таком объеме? Конечно, надо, но надо всегда дружить с головой. Тесты надо писать, но мне кажется, чтобы багов было меньше, надо использовать строго типизированные, математические языки — такие, вот как Rust. Они позволят не писать столько тестов, которые надо писать на C, потому что в C слабая типизация. Там вообще компилятор не помогает. Хотя я очень люблю C, читал раз 50 книжку «Язык C» Кернигана и Ритчи.
· Интеграционные тесты – когда писать, как упрощать
- Часть интеграционных тестов мы не пишем, очень дорого
- Часть багов появляется только на «бою»
- Баги отечественных облаков
Интеграционные тесты пишем через localstack. То есть тесты мы пишем всегда, обязательно, 60−70% времени уходит на тесты, 30-40% на написание кода. В Rust встроенная инфраструктура тестирования, в язык встроены юнит-тесты и интеграционные тесты, это очень удобно.
· Тесты «асинхронщины» – встроенные возможности Tokio
- «Атрибуты» компилятора
- Макросы
В Rust есть хорошие макросы для мета-программирования, строго типизированные макросы, которые позволяют делать мета-программирование классов. Они позволяют встраивать тесты и асинхронщину. Тестировать асинхронщину в Rust также удобно, как и синхронщину, с этим проблем никаких нет.
· Тесты Axum, виды http-клиентов
- Локальный клиент, проверка эндпоинтов
Локальный клиент Axum тоже имеет тесты. Любая современная библиотека дает возможность себя протестировать. Например, Axum говорит: «Вот я, тестируй свои правила через меня локально в коде в интеграционном тесте». Axum — это кусочек Nginx, по сути, которы[АС4] й асинхронно писали. Его можно тоже потестировать. По сути, мы решили задачу — и Nginx написали кусочки, и облачный провайдер нам теперь частично не нужен, потому что SQS у нас уже внутри в памяти держится. Файлы копируем через память, а не через диск. Нам уже диски не нужны. Чек на оборудование у нас снижается в несколько раз, нам не нужно много серверов. Ощущение от технологии очень хорошее.
Как быстро прокачать команду на Rust/Tokio
· Почему Rust выглядит сложным?
- Он правда сложный, но не усложненный
- Много фукциональщины и идей из в т.ч. Haskell
Rust на самом деле не выглядит сложным, он на самом деле сложный. Он заставляет думать вас иначе. Математика пришла в системное программирование. Что делать? Надо просто практиковаться. Мне потребовалось несколько лет на это.
· Строки, slices
- Много «странных», но эффективных примитивов
- Box, Pin, Arc, Cell, OnceLock...
Например, как выучить Python? По дороге на собеседование ты выучил Python. Java — это где-то год, «Плюсы» — несколько лет. Чтобы изучить Rust, нужно несколько лет практиковаться. К сожалению, простого способа нет. Но скорость разработки на Rust как на Python, скорость работы, безопасный код, много логики как у C++.
· Как преодолевать сопротивление компилятора
- Наш путь – борьба!
- Вникать в его рекомендации
Честно скажу, самое страшное в Rust — борьба с компилятором, он очень умный. Например, аффинных типов данных нет ни в одном известном мне языке программирования. Но я считаю, что лучше бороться с компилятором Rust, чем программировать с ChatGPT. Все-таки компилятор Rust вас научит программировать правильно, у вас мозги начинают думать по-другому. Есть такая фраза, что когда ты программируешь на Rust, то ощущаешь себя гением. Да, у тебя что-то происходит в голове, когда программируешь на Rust, ты как будто на йогу пришел, у тебя мозги начинают работать, шевелиться, потому что язык очень выразительный. Похожие ощущения были от Haskell. Тебе уже скучно на Java, она простая, ну Kotlin чуть сложнее. По сути, они эти проблемы не решили, а Rust решил. Плюс, он качает вам еще дополнительно мозги, кроме того, что дает пользу бизнесу.
· Есть ли жизнь без ООП и наследования?
- Есть, и активная:
- Трейты, генерики, модули, функции, инкапсуляция
Наследование сейчас считается злом многими языками программирования. Поэтому все будет получаться, все будет нормально, не бойтесь Rust!
· Как быстро освоить дженерики а-ля type classes Haskell?
- Сильно проще и практичнее, чем в Java; HM type inf.
- «Программируя на Rust, я ощущаю себя гением» (С)
Просто практикуйтесь, есть масса источников, где можно практиковаться в браузере, например, Rust by Example, rustlings. Я стараюсь час-два в день писать код на Rust. У нас хорошо идет этот проект, есть команда, люди учатся, все получается. Думаю, у вас также все получится.
Что вызывает проблемы у команды?
· Использование выразительных типов языка на полную катушку
- Sum-, Prod-типы
Люди не привыкли работать с такими выразительными Sum-, Prod-типами, много enum. В Java enum по сравнению с enum в Rust — это будто 50 лет назад, Rust ушел далеко вперед.
- Pattern-matching
Pattern-matching позволяет не if’ами, а структурно, типами разбирать кусочки и их структуры. То есть вы определяете сначала структуру, а потом пишите методы и разбираете их, сопоставляя с образцом. Компьютер вам помогает меньше совершить ошибок. Конечно, будут логические ошибки, но меньше.
- Option, Result
NULLs нет. Всякий NULL заменен на Option. Exception нет, только Result. Это типа если все хорошо, то один тип, если плохо - другой очень удобный тип.
- Ссылки
Ссылки для того, чтобы не копировать. Компилятор позволяет, чтобы ссылки у вас всегда работали надежно.
· Типы стандартной библиотеки
Как не захлебнуться в типах стандартной библиотеки?
- Автовывод
Никогда не прописывайте типы. Они автоматически выводятся.
- Studio Code
Rust прекрасно поддерживает Visual Studio Code — пишите в консоли «code» и все работает.
- Интуиция, книги
- Практика, практика, практика
Читайте книги, практикуйтесь. Самое главное в стандартной библиотеке — читайте первое предложение, самое главное, а дальше можно не читать. Одно это предложение, и все понятно, как работает.
· Документация
Как читать документацию к тысячам трейтов сообщества?
- Документации пока мало
- Смотрим «/examples» и «/tests»
В хороших языках типа Rust документация не нужна, код читается, потому что инструменты типизированные, ты хорошо понимаешь разработчика.
Заключительные выводы и мысли
Сразу пишите код
Ежедневно боритесь с компилятором
Читайте книги «без воды»
Практикуйтесь!
Моя любимая фраза: «Программируя на Rust, я ощущаю себя гением».
Скрытый текст
А узнать больше по теме и пообщаться с профессионалами своего дела, чтобы обменяться опытом, вы сможете на предстоящей профессиональной конференции разработчиков высоконагруженных систем HighLoad++ 2025!
Комментарии (0)
V1tol
17.09.2025 09:14Форматирование немного плавает, местами тяжело читать. Насчёт аллокатора рекомендую глянуть mimalloc. Есть приложение на Rust, которое занимается разбором XML. Код написан в "питон" стиле безо всяких попыток оптимизировать - делался упор на читаемость кода для любого программиста. Шлёпнули mimalloc, буквально 2 строки поменять - получили х3 по RPS, по памяти может процентов 5 потеряли. И это на glibc, где аллокатор ещё не самый плохой. Если используете musl - в таком случае любой кастомный аллокатор просто необходим.
domix32
17.09.2025 09:14Господи, какой же это всё поток сознания. Нафига вам списки через строчку?
100% защита от гонок данных и (UB) от компилятора в safe-коде
пока не 100%, а 99. Трюк с манипуляцией временем жизни позволяет написать валидный 100% safe код для всяких разыменований нулевых указателей и использования памяти после освобождения. Гонки данных тоже можно написать, достаточно запутаться в очерёдности атомиков. Даже можно честный дедлок поймать, но это надо чтобы асинхронный код через третьи руки позвал рекурсивно себя же, взведя при этом какой-нибудь мьютекс. Всё из-за нюансов имплементации асинхронности в Rust.
Java/Netty vs Rust/Tokio/Axum: 1-1.5 vs 6-8 CPU, 50-500 MB vs 1-4 GB (+ gc)
то есть всё стало хуже? было 500 метров на полтора cpu в джаве, а стало 8 цпу на 4 гига памяти? "казнить нельзя помиловать" какой-то.
Никогда не прописывайте типы
иногда ты не можешь не написать, ибо компилятор не божество и иногда не может догадаться какой финальный тип нужен.
Бой стал колом
америкэн бой, видимо.
сервера эту нагрузку вынесли
обычно под "вынесли" подразумевается, что что-то было убито/уничтожено. Пошло от "вынести ногами вперёд". То есть ваши сервера были настолько хороши, что кого-то смогли положить? Сами сервера обычно нагрузку выдерживают, когда именно их начинают прессовать.
вот куда оно должно было сослаться?
Читайте книги «без воды»
ещё бы статью без воды иметь.
stas_dubich
Rust и правда хорош, после 15 лет в обнимку с Java, контраст лютый, на rust просто берешь и делаешь задачу, и оно работает, быстро и не прожорливо)