Обслуживать 150+ миллионов пользователей — это не шутки, и к тому же недешево.

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

Чтобы не отставать от этого трафика, мы можем сделать две вещи:

  1. Горизонтальное масштабирование сервисов прогнозирования
  2. Оценка больше предметов в секунду или запрос

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

Второе обязательно, но как мы до него доберемся?

Наша ПС состоит из двух классов операций:

  1. Сетевые вызовы в магазин функций (неснижаемый)
  2. Циклы ЦП, затраченные на синтаксический анализ + ранжирование + постобработка

Чтобы решить для 2, я решил реализовать один из самых больших PS (который делает ~ 1,5 миллиона прогнозов в секунду с 20% трафика) на скомпилированном языке. После небольшого исследования я решил написать его на Rust. Почему? потому что:

  1. В этом тесте Actix оказался одним из лучших веб-фреймворков (https://www.techempower.com/benchmarks/).
  2. Rust безопасен для памяти, быстр, а управление пакетами в 100 раз лучше, чем C/C++.

(Почему бы не перейти? См. сноски[1])

Каково было портировать фреймворк прогнозирования Python на Rust?

  1. В первые несколько часов я сделал много ошибок, но компилятор был очень мил, вежливо подсказав мне, что мне нужно исправить. После этого ржавчину было не так сложно подобрать.
  2. Написание конечных точек в Actix было довольно простым благодаря богатой документации.
  3. Хотя Rust имеет очень сильную поддержку сообщества, у него не было реализованного клиента для вызова Vertex Feature Store. К счастью, в Rust есть отличные криптографические библиотеки, которые позволили мне реализовать клиент с поддержкой аутентификации с нуля за пару часов.
  4. Структура проекта Rust очень близка к проекту React. Добавить зависимость пакета было так же просто, как добавить строку в файл Cargo.toml
  5. Система инструментов Rust просто работает

Первоначальные результаты сравнительного анализа:

Наконец, пришло время протестировать службу прогнозирования. Я провел несколько стресс-тестов, и вот что я получил:

Запросов в секунду (RPS):

Ржавчина едва достигает 60 RPS :(

Задержки:

Это не имело смысла! После определенной нагрузки задержки модели начали экспоненциально расти. Обратите внимание, что Python PS смог легко сделать ~ 160 RPS.

Раст солгал мне!?

Я почесал голову в этот момент. Мне много обещали, и я подумал, что Раст мне солгал. Я думал.

Я провел пару дней, копаясь глубже, и нашел этот эпический блог от ScyllaDB об их опыте отладки с Rust. В моем арсенале появился новый блестящий инструмент: Флеймографы!

Что такое Flamegraphs? (цитируя ссылку выше)

«Графики пламени — это визуализация иерархических данных, созданная для визуализации трассировки стека профилированного программного обеспечения, чтобы можно было быстро и точно определить наиболее часто встречающиеся пути кода».

Как интерпретировать эти графики? (цитируя блог ScyllaDB):

«эмпирическое правило заключается в том, чтобы искать операции, которые занимают большую часть общей ширины графа — ширина указывает время, затраченное на выполнение конкретной операции».

Создание Flamegraph вашего сервиса ржавчины так же просто, как:

Вот Flamegraph сервиса Rust:

Ничего особенно подозрительного, но я вижу огромные куски OpenSSL, занимающие 27–30% циклов процессора. К счастью, я нашел rusttls альтернативу OpenSSL, которая была намного надежнее и проще в использовании. Вот график пламени после перехода с OpenSSL на rusttls:

Хорошо, круто, пламя выглядит красиво и заостренно, и нет процесса, перегружающего все ресурсы процессора. Тогда это должно решить это правильно?

Ну да, а на самом деле нет…

Хотя RPS немного вырос, это было далеко не так, RPS службы прогнозирования Python! Кроме того, задержки продолжали расти в геометрической прогрессии по мере увеличения нагрузки. Что-то все же было не так!

В кроличью нору отладки

Я решил проверить Flamegraph, когда задержки начали расти в геометрической прогрессии. Но произошло нечто неожиданное: я обнаружил, что при локальном стресс-тестировании службы Rust задержки не увеличились, и фактически пропускная способность также смогла пересечь пропускную способность службы Python. Если один и тот же код работает по-разному в двух разных средах (локальной и рабочей), проблема должна заключаться в образе докера.

Чтобы сделать образ докера легким, я использовал scratch в качестве базового образа докера. Кроме того, я генерировал свой двоичный файл для следующей цели:

Разница в том, что этот образ использует musl-libc, а моя локальная среда использует glibc.

При дальнейшем копании я нашел сообщение одного из авторов musl-libc, в котором говорилось о основных проблемах в реализации malloc.

Tl;dr: реализация malloc в musl-libc может работать очень медленно при высокой нагрузке [2].

Чтобы исправить это, мы можем просто использовать разные распределители памяти [3]. Но еще проще было не жадничать по поводу размера образа и использовать более разумный базовый образ докера, использующий glibc. 😐 И я не первая допустила эту ошибку.

Прохладный. Урок выучен. Мы используем слим-бастер. Продолжаем движение…

Наконец, после перехода на slim-buster вот метрики:

Эпично! Это удобно превышает 900+ RPS при более высокой нагрузке и с задержкой p99 ‹ 90 мс!

Обобщить:

Для обработки производственного трафика службе Rust потребуется максимум 4 ВМ, тогда как службе Python требуется минимум 20 ВМ.

Это довольно мило! Много $$$

Принесите Rust домой, чтобы познакомиться с родителями

Если вы испытываете искушение добавить Rust в свой инженерный стек, вам лучше привести веские доводы в его пользу. Учтите это, если вы планируете представить Rust в качестве языка для своей команды:

  1. Actix — это безумно быстрый веб-фреймворк, и если для вас важна производительность, не ищите дальше. Наличие службы с высокой пропускной способностью также позволит контролировать расходы на инфраструктуру. Хотя производство микросервиса Rust может иметь несколько предостережений, если вы новичок в этом.
  2. Rust недостаточно развит, чтобы поддерживать ML из коробки. Но в данном случае это сработало, потому что мы выполняем минимальную математику поверх предварительно вычисленных оценок. Если вы хотите обслуживать свою модель Xgboost или глубокого обучения, то Rust — не лучший выбор (но, надеюсь, когда-нибудь).
  3. Rust очень элегантно спроектирован и имеет мощный компилятор. Вам придется очень постараться, чтобы написать программу, которая ломается. Отказоустойчивость встроена.
  4. У Rust есть кривая обучения, но если вы знакомы с C/C++ или Java, вам вряд ли понадобится час, чтобы стать продуктивным.

Сноски:

  1. Почему бы не использовать Голанг? — Мне просто не хватило времени. Но Тряпки сделали, и это не менее эпично
  2. Musl-libc работает над гораздо более производительной реализацией malloc: https://github.com/richfelker/mallocng-draft
  3. Вот подробное сравнение производительности различных распределителей памяти: https://www.linkedin.com/pulse/testing-alternative-c-memory-allocators-pt-2-musl-mystery-gomes

Подпишитесь на меня в Твиттере, если хотите заставить свои компьютеры работать быстрее: https://twitter.com/shvbsle