[Перевод Rust] Почему Discord переходит с Go на Rust?

задняя часть Rust

Оригинальный адрес:blog.discord.com/почему-дискорд…

Оригинальный автор:medium.com/@Jesse_1122…

Опубликовано: 5 февраля 2020 г. - 10 минут чтения

image.png

Rust становится первоклассным языком в самых разных областях. В Discord мы видели успех Rust как на стороне клиента, так и на стороне сервера. Например, мы используем его на стороне клиента для конвейера кодирования видео Go Live и на стороне сервера дляElixir NIFs. Недавно мы значительно улучшили производительность сервиса, переключив его реализацию с Go на Rust. В этом посте объясняется, почему мы повторно реализовали сервис, как и в результате чего повысилась производительность.

служба чтения статуса

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

В реализации Go служба Read States не поддерживает требования продукта. Большую часть времени это было быстро, но каждые несколько минут мы наблюдали большие всплески задержки, которые плохо сказывались на пользовательском опыте. После расследования мы определили, что эти всплески были вызваны основными функциями Go: его моделью памяти и сборщиком мусора (GC).

Почему Go не соответствует нашим целям производительности

Чтобы объяснить, почему Go не достиг наших целей по производительности, нам сначала нужно обсудить структуру данных, масштаб, шаблоны доступа и архитектуру сервиса. Структура данных, которую мы используем для хранения информации о состоянии чтения, удобно называть «состоянием чтения». Discord имеет миллиарды состояний чтения. Каждый пользователь, каждый канал имеет статус чтения. Каждое состояние чтения имеет несколько счетчиков, которые необходимо обновлять атомарно и часто сбрасывать на 0. Например, один из счетчиков — это количество @упоминаний на канале.

Чтобы получать быстрые обновления атомарного счетчика, каждый сервер состояния чтения имеет кэш последнего использованного состояния чтения (LRU). В каждом кэше миллионы пользователей. В каждом кэше есть десятки миллионов «состояний чтения». В секунду происходят сотни тысяч обновлений кеша.

Для сохранения мы используем кластер базы данных Cassandra для поддержки кэширования. Когда ключ кеша удаляется, мы фиксируем ваше состояние чтения в базе данных. Мы также планируем фиксацию базы данных в течение следующих 30 секунд при каждом обновлении состояния чтения. Есть десятки тысяч операций записи в базу данных в секунду.

На изображении ниже вы можете увидеть время отклика и системный процессор для пикового периода выборки службы Go. ¹Как вы, возможно, заметили, примерно каждые 2 минуты возникают задержки и скачки загрузки ЦП.

image.png

Итак, почему существует 2-минутный всплеск?

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

Эти всплески задержки определенно влияют на производительность сборки мусора, но мы написали код Go очень эффективно, а количество аллокаций очень мало. Мы не производим много мусора.

Изучив исходный код Go, мы узнали, что Goминимум каждые 2 минутызаставит запустить сборку мусора. Другими словами, если сборка мусора не выполняется в течение 2 минут, Go все равно будет принудительно выполнять сборку мусора независимо от того, растет куча или нет.

Мы подумали, что могли бы настраивать сборщик мусора немного чаще, чтобы предотвратить большие всплески, поэтому мы реализовали конечную точку в службе, чтобы изменить процент GC сборщика мусора. К сожалению, независимо от того, как мы настроимGC процент, изменений нет. Как это возможно? Оказывается, это происходит из-за того, что мы недостаточно быстро выделяем память, чтобы заставить сборку мусора происходить чаще.

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

К сожалению, транзакция, которая уменьшила кэш LRU, привела к более высокой 99-й задержке. Это связано с тем, что если кеш становится меньше, состояние чтения пользователя не может быть в кеше. Если его нет в кеше, то мы должны выполнить загрузку базы данных.

После обширного нагрузочного тестирования с различными объемами кэш-памяти мы нашли настройку, которая выглядела хорошо. Не совсем доволен, но достаточно, и есть более важные дела, поэтому мы позволили сервису работать в таком режиме некоторое время.

За это время мы увидели растущий успех Rust в других частях Discord, и мы коллективно решили, что хотим создавать фреймворки и библиотеки для создания новых сервисов полностью на Rust. Этот сервис является идеальным кандидатом для переноса на Rust, потому что он небольшой и автономный, но мы хотели бы, чтобы Rust также устранял эти всплески задержки. Поэтому мы взялись за портирование состояний чтения на Rust, надеясь зарекомендовать себя как сервисный язык Rust и улучшить взаимодействие с пользователем.

Управление памятью в Rust

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

В Rust нет сборщика мусора, поэтому мы не думаем, что у него будут такие же всплески задержки, как у Go.

Rust использует относительно уникальный подход к управлению памятью, который включает концепцию «владения» памятью. По сути, Rust отслеживает, кто может читать и писать в память. Он знает, когда программа использует память, и освобождает ее, как только она больше не нужна. Он применяет правила памяти во время компиляции, делая практически невозможными ошибки памяти во время выполнения. Об этом позаботится компилятор.

Таким образом, в Rust-версии службы состояния чтения, когда пользовательское состояние чтения вытесняется из кеша LRU, оно немедленно освобождается из памяти. Память, которая считывает состояние, не ждет, пока ее соберет сборщик мусора. Rust знает, что он больше не используется, и немедленно выпускает его. Не существует процесса времени выполнения, чтобы решить, следует ли его освобождать.

Асинхронная ржавчина

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

К счастью, команда Rust усердно работает над тем, чтобы упростить асинхронное программирование, и оно также доступно на изменчивом ночном канале Rust.

Discord никогда не боялся использовать новые технологии, которые выглядят многообещающе. Например, мы одними из первых стали использовать Elixir, React, React Native и Scylla. Если технология многообещающая и дает нам преимущество, мы не возражаем против присущей передовой технологии сложности и нестабильности. Это один из способов быстро достичь более 250 миллионов пользователей с менее чем 50 инженерами.

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

Внедрение, нагрузочное тестирование и выпуск

Фактическое переписывание довольно прямолинейно. Сначала это был грубый перевод, а затем мы доработали его там, где это имело смысл. Например, в Rust отличная система типов с обширной поддержкой дженериков, поэтому мы можем выбросить код Go, существующий только из-за отсутствия дженериков. Кроме того, модель памяти Rust может рассуждать о безопасности памяти между потоками, поэтому мы смогли отказаться от некоторых ручных средств защиты памяти между потоками, необходимых в Go.

Когда мы начали нагрузочное тестирование, мы сразу же были довольны результатами. Задержка версии Rust такая же хорошая, как и версия Go, и никаких скачков задержки! Это делает нас очень счастливыми.

Стоит отметить, что при написании версии Rust мы учитывали только самые основные оптимизации. Даже с базовой оптимизацией Rust может превзойти настроенную вручную версию Go. Это огромная демонстрация того, насколько легко писать эффективные программы на Rust по сравнению с нашим глубоким погружением в Go.

Но нам недостаточно просто соответствовать производительности Go. После некоторого анализа и оптимизации производительности мы смогли превзойти Go по всем показателям производительности. Задержка, процессор и память лучше в версии Rust.

Оптимизация производительности Rust включает.

  1. Используйте BTreeMap вместо HashMap в кэше LRU, чтобы оптимизировать использование памяти.
  2. Замените исходную библиотеку метрик на ту, которая использует современный параллелизм Rust.
  3. Уменьшает количество копий памяти, которые мы делаем.

Удовлетворенные, мы решили запустить сервис.

Поскольку мы провели нагрузочное тестирование, развертывание прошло довольно гладко. Мы поместили его на один канареечный узел, нашли несколько недостающих пограничных случаев и исправили их. Вскоре после этого мы распространили его на весь флот.

Ниже приведены результаты.

Go — фиолетовый, Rust — синий.

image.png

Увеличить емкость кэша

После успешной работы службы в течение нескольких дней мы решили, что пришло время снова увеличить емкость кэша LRU. В версии Go, как упоминалось выше, повышение верхнего предела LRU-кэша приводило к более длительной сборке мусора. Нам больше не нужно заниматься сборкой мусора, поэтому мы подумали, что можем поднять верхний предел кеша для лучшей производительности. Мы увеличили объем памяти коробки, оптимизировали структуры данных, чтобы использовать меньше памяти (ради интереса), и увеличили емкость кеша до 8 миллионов состояний чтения.

Приведенные ниже результаты говорят сами за себя. Обратите внимание, что среднее время теперь указано в микросекундах, а максимальное @упомянутое время — в миллисекундах.

image.png

Развивающаяся экосистема

Наконец, еще одна замечательная особенность Rust — быстрорастущая экосистема. Недавно tokio (используемая нами асинхронная среда выполнения) выпустила версию 0.2. Мы сделали обновление, и оно дало нам преимущества процессора бесплатно. Ниже вы можете видеть, что процессор продолжает деградировать, начиная примерно с 16 числа.

image.png

идея окончания

На данный момент Discord использует Rust во многих местах своего программного стека. Мы используем его для игрового SDK, захвата и кодирования видео для Go Live,Elixir NIFs, несколько серверных сервисов и т. д.

При запуске нового проекта или программного компонента мы рассматриваем возможность использования Rust. Конечно, мы используем его только там, где это имеет смысл.

Помимо производительности, Rust имеет много преимуществ для инженерных команд. Например, безопасность типов и средство проверки кредитов упрощают рефакторинг кода при изменении требований к продукту или обнаружении новых языковых знаний. Кроме того, его экосистема и инструменты действительно хороши, и за этим стоит огромный импульс.

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

Также забавный факт: команда Rust использует Discord для координации. Есть даже очень полезный сервер сообщества Rust, где вы можете время от времени болтать с нами.нажмите здесь, чтобы посмотреть.


  1. Перейти на версию 1.9.2. Редактировать: график из 1.9.2. Мы пробовали версии 1.8, 1.9 и 1.10 без каких-либо улучшений. Первоначальный порт с Go на Rust был сделан в мае 2019 года.
  2. Грубо говоря, мы не думаем, что вы должны переписывать все в rust просто так.
  3. Цитата изwww.rust-lang.org/
  4. Если, конечно, вы не используете unsafe.
  5. areweasyncyet.rs/

www.deepl.comперевести