Я фулстек-разработчик, индивидуальный предприниматель. По моему опыту, один из самых востребованных классов проектов, за разработкой которых к нам обращаются — приложение для работы в режиме реального времени. Конечно, вам такие приложения известны: WhatsApp, Discord, Slack, т.д. При разработке приложений для работы в режиме реального времени следует учитывать различные факторы, в частности масштабируемость, отказоустойчивость, отзывчивость и распределённость. Это задача не из лёгких, в особенности, для небольшой команды или разработчика-одиночки.
Но что если бы я вам сказал… что можно создавать приложения для работы в режиме реального времени, которые можно масштабировать более чем на миллион пользователей силами всего нескольких разработчиков? К тому же, такие приложения можно было бы развёртывать почти без задержек и ценой минимальных затрат. Здесь я имею в виду, что для этого нужно освоить секретное оружие под названием «Виртуальная машина Erlang» или BEAM (Абстрактная машина Богдана/Бьёрна для языка Erlang).
В этой статье будет рассказано, как Erlang и его более современный аналог Elixir превратились в секретное оружие для разработки приложений реального времени, таких как WhatsApp и Discord. Мне особенно не терпится поделиться этими идеями, так как к настоящему моменту я пользуюсь Elixir уже более 3 лет и могу утверждать, что это одно из лучших решений, которые мне довелось принять на протяжении всей карьеры.
Что ж, давайте перейдём к сути.
Происхождение Erlang и Elixir
Язык Erlang был разработан в конце 1980-х шведской телекоммуникационной компанией «Ericsson». Компания стремилась создать язык программирования, который соответствовал бы требованиям широкомасштабной конкурентности и высокой доступности, действующим в телекоммуникационных системах. Erlang спроектирован так, что акцент в нём делается на легковесных процессах, обмене сообщениями и отказоустойчивости. Поэтому на Erlang максимально удобно создавать системы, рассчитанные на непрерывную эксплуатацию и требующие высокой доступности.
При помощи легковесных процессов Erlang можно одновременно выполнять в одной и той же системе тысячи и даже миллионы параллельных действий, и производительность из-за этого почти не страдает. Действующая в Erlang модель передачи сообщений обеспечивает эффективную межпроцессную коммуникацию без разделения памяти. Благодаря этому также снижается сложность и увеличивается отказоустойчивость. Философия «Let it crash» (Пусть сломается) стимулирует разработчиков писать такой код, который может изящно восстанавливаться после отказов, что способствует общей надёжности приложений, написанных на Erlang.
Язык Elixir, созданный в 2011 году Жозе Валимом, построен на прочном фундаменте виртуальной машины Erlang, но характеризуется более современным синтаксисом, ориентированным на удобство разработчика. В Elixir сохранены мощная конкурентность и отказоустойчивость, свойственные Erlang, но при этом добавлены такие усовершенствования как метапрограммирование и функциональное программирование. Синтаксис Elixir вдохновлён Ruby, так что именно разработчикам, знакомым с Ruby, особенно легко влиться в работу с Elixir.
В совокупности Erlang и Elixir образуют мощный стек, на котором можно выстраивать надёжные приложения, которые удобно масштабировать и поддерживать. Благодаря всем этим чертам эта пара языков отлично подходит таким компаниям, как WhatsApp и Discord, — ведь в этих приложениях приходится обрабатывать огромное количество конкурентных действий со стороны множества пользователей и обходиться при этом почти без периодов недоступности.
5 основных свойств, благодаря которым BEAM превращается в секретное оружие по разработке приложений реального времени
Когда в WhatsApp и Discord было принято решение создавать приложения реального времени с использованием стека BEAM, это было продиктовано несколькими критическими факторами: отказоустойчивостью, масштабируемостью, распределённостью, отзывчивостью и возможностью обновления кода в live-режиме.
Давайте исследуем, как эти черты согласуются с требованиями платформ, на которых реализуется коммуникация в режиме реального времени.
Отказоустойчивость
Надёжность — критически важное свойство для таких приложений как WhatsApp и Discord, рассчитанных на непрерывную эксплуатацию почти без простоев. Принятая в Erlang философия «пусть сломается» гарантирует, что отказы отдельных процессов не приводят к отказу всей системы. Напротив, процессы изолированы, а их супервизоры в состоянии перезапускать отказавшие процессы. Если система спроектирована именно так, то влияние аварийных отключений минимизируется, а стабильность всего приложения только повышается.
Масштабируемость
Для масштабирования системы требуется не только добиться, чтобы можно было горизонтально расширять её за счёт физических серверов, но и обеспечивать коммуникацию между этими серверами. Но коммуникация в распределённых системах всегда сопряжена со сложностями: условиями гонок, обнаружением узлов, нарушениями оркестрации и т.д. Такие проблемы возникают по множеству причин, в основном проистекающих из неверного управления сложностью.
В свою очередь, BEAM обеспечивает масштабируемость без иных экзотических, но при этом опасных инструментов, существующих для этих целей. BEAM обрабатывает межузловую коммуникацию как по мановению руки. Рассмотрим на простом примере, как происходит обмен информацией между двумя узлами в Elixir:
Первым делом нужно запустить два узла:
# Запускаем первый узел, на котором нет функции `Hello.world/0`
$ iex --sname node1@127.0.0.1 --cookie secret -S mix
# Запускаем второй узел, на котором есть функция `Hello.world/0`
$ iex --sname node2@127.0.0.1 --cookie secret -S mix
После этого можно соединить два узла. Подключаем `node1` к `node2`:
# В node1
iex(node1@127.0.0.1)1> Node.connect(:"node2@127.0.0.1")
true
# Теперь можно вызвать функцию `Hello.world/0` из `node2` в `node1`
iex(node1@127.0.0.1)2> Node.spawn(:"node2@127.0.0.1", fn -> Hello.world() end)
Hello, world!
Это был простой пример, но и на нём понятно, насколько легко обеспечить межузловую коммуникацию в Elixir. Без той мощной масштабируемости, которую обеспечивает BEAM, для достижения аналогичного результата пришлось бы пользоваться сложными распределёнными системами, например Apache Kafka, RabbitMQ, т.д.
Распределение
Для «глобальных» приложений, работающих в режиме реального времени, таких как WhatsApp и Discord, требуется распределённая архитектура, позволяющая работать с пользователями из разных регионов. Речь о системе, способной распределять рабочую нагрузку на множество узлов и обеспечивать между ними бесшовную коммуникацию. Планировщик BEAM может не только распределять процессы по множеству ядер на одной машине, но и по множеству машин в сети. Кроме того, со всеми процессами он обращается одинаково, независимо от того, где именно они выполняются: на той же машине, что и BEAM, либо где‑то ещё. Именно поэтому становится так легко выстраивать распределённые системы, особенно не беспокоясь о том, на какой инфраструктуре они развёрнуты.
Когда система так легко распределяется, она также и хорошо масштабируется. При необходимости укрупнить приложение, вы без труда добавляете в кластер новые узлы и распределяете по ним рабочую нагрузку. Нет нужды беспокоиться о сложных планировщиках нагрузок или о стратегиях шардирования — BEAM всё это сделает за вас.
Отзывчивость
Если платформа рассчитана на коммуникацию в режиме реального времени, то доставка сообщений пользователям и применение обновлений на ней должны работать быстро и надёжно. В Erlang задача выполнения множественных процессов решается на уровне языка. Для этого применяются выделенные планировщики, которые попеременно берутся за процессы Erlang по мере необходимости. Планировщик работает в вытесняющем режиме — предоставляет каждому процессу небольшое окно для выполнения, затем приостанавливает этот процесс и переходит к работе над другим.
Поскольку данное окно выполнения невелико, ни один долгоиграющий процесс не может заблокировать всю остальную систему. Более того, на внутрисистемном уровне операции ввода/вывода делегируются отдельным потокам. Может использоваться сервис опроса, если такой предусмотрен в ядре базовой операционной системы. Таким образом, любой процесс, ожидающий, пока завершится операция ввода/вывода, не будет блокировать выполнение других процессов.
Обновление кода в работающей системе
Когда требуется поддерживать приложения, работающие в режиме реального времени, нужно предусмотреть возможность развёртывать обновления, не нарушая при этом действий пользователя. Как правило, в контейнеризованных облачных средах для решения таких задач применяются сине-зелёные варианты развёртывания, канареечные версии, т.д. Но работая с BEAM, можно обновлять код прямо в действующей системе, не перезапуская приложение. Такая возможность называется «горячая замена кода», она критически важна при обращении с приложениями реального времени, для которых необходимо обеспечить бесперебойную эксплуатацию. Благодаря такому подходу разработчик может фиксить баги, добавлять фичи или оптимизировать производительность, не прерывая работу сервиса.
Разбор кейса #1: Как в WhatsApp при помощи Erlang масштабируется обмен сообщениями в режиме реального времени, учитывая, что приложение обслуживает более миллиарда активных пользователей ежедневно
В 2009 году Брайан Эктон и Ян Кум основали WhatsApp, задуманный как приложение-мессенджер для кроссплатформенной, быстрой, надёжной и безопасной работы в режиме реального времени. Они выбрали язык Erlang в качестве базового благодаря его отказоустойчивости, масштабируемости и возможностям распределённой работы. Суть архитектуры и базы кода для систем обмена сообщениями в WhatsApp вдохновлены ejabberd — это опенсорный проект, XMPP-сервер, написанный на Erlang.
Приложение WhatsApp развивалось стремительно, к 2018 году с ним работали более миллиарда пользователей ежедневно. Благодаря эффективности Erlang и тщательной доработке как ejabberd, так и XMPP, авторам удалось обрабатывать миллионы сообщений ежедневно почти без простоев системы, в то же время обходясь небольшой командой и небольшими затратами на аппаратное обеспечение и инженерию.
По мнению Антона Лаврика, инженера по серверам WhatsApp, одна из самых сильных сторон Erlang – это принятый здесь подход к конкурентности. По его словам, «можно попытаться достичь аналогичного уровня конкурентности и на других платформах силами других языков, но зачастую для этого требуется слишком много сил и ресурсов». Поскольку Erlang изначально создавался с прицелом на решение задач, касающихся конкурентности, на нём конкурентность доводится до высокого уровня значительно легче».
Разбор кейса #2: Как в Discord воспользовались Elixir и написали чат-приложение для работы в режиме реального времени, а потом смогли масштабировать его для одновременного обслуживания 11 миллионов пользователей
Приложение Discord основали в 2015 году Джейсон Цитрон и Стэн Вишневский. Это чат для переписки в режиме реального времени, исходно ориентированный на геймеров. Тогда авторы решили выстраивать инфраструктуру на основе двух ключевых языков: Python и Elixir. До сих пор база кода Python, интегрированная с Elixir, используется в качестве двигателя тех элементов, что касаются API, но большинство ключевых аспектов, в частности, чат для общения в режиме реального времени, завязаны на Elixir.
Популярные серверы Discord — в частности, обслуживающие игры Fortnite и Minecraft, могут поддерживать одновременное общение сотен тысяч пользователей. Следовательно, они должны очень хорошо (без каких-либо задержек) справляться с широковещательной передачей сообщений и обновлений пользовательского статуса в режиме реального времени. В Elixir действует настолько удобная модель конкурентности и предусмотрены механизмы, обеспечивающие отказоустойчивость, что именно этот язык сочли идеальным инструментом для создания системы чатов реального времени в Discord. Благодаря тому, что в Discord появилась возможность справляться с такой массовой конкурентностью, а также распределять нагрузки на множество узлов, чат-платформа Discord отлично масштабируется и при этом сохраняет высокую производительность и надёжность.
Затем, в 2019 году, в истории Discord была достигнута новая веха: потребовалось поддерживать настолько много одновременно работающих пользователей, как никогда ранее. На тот момент платформа располагала уже не только BEAM, но и возможностями новых «высокопроизводительных» языков. Так, на Rust написали новый список участников, в котором можно очень быстро обновлять статусы до 11 миллионов одновременно работающих пользователей. Причём, самое интересное, что удалось приспособить заранее скомпилированный модуль Rust к работе с базой кода Elixir, это было сделано при помощи возможности под названием NIF (нативно-реализованная функция). Такой подход позволяет опираться на производительность Rust, не отказываясь при этом от продуктивности Elixir.
Разбор кейса #3: (от автора) Как я написал сетевое приложение для работы в режиме реального времени, воспользовавшись Elixir и Phoenix
В прошлом году мне довелось разработать интересный проект «FoodFocus» – в этом приложении в режиме широковещания транслируется, как пользователь готовит какое-либо блюдо, а другие пользователи могут подключаться к этой трансляции и непосредственно общаться с кулинаром. Более того, к приложению можно было подключаться через умные часы, и в таком случае пункты рецепта можно было бы синхронизировать с таймером часов.
Приступая к разработке этого проекта, я подбирал для него наиболее экономичный и эффективный технологический стек. Вот каковы были наиболее важные черты этого стека:
– Легко разрабатываемые фичи для работы в режиме реального времени
– Предоставление готовых к использованию инструментов, чтобы их не пришлось привносить из многочисленных сторонних библиотек
– С приложением должно быть приятно работать — я забочусь об удобстве разработчика
Немного исследовав проблему, я решил писать серверную часть на Elixir и Phoenix, а для клиентской использовать Flutter. Хотя, это была моя первая попытка написать на Elixir приложение, «готовое для прода», оставалось только удивляться, насколько легко при помощи Phoenix создавать фичи, ориентированные на работу в режиме реального времени.
Перечислю некоторые из моих любимых фич и инструментов, которые очень пригодились мне при разработке проекта на Elixir:
Каналы Phoenix: отличная разработка. Они выстроены на основе WebSockets, предоставляют простой и лёгкий в использовании API для создания приложений, рассчитанных на работу в режиме реального времени.
ETS (хранилище термов Erlang): ETS — это встроенное в Erlang хранилище ключей и значений, очень быстрое и эффективное. Не приходится прибегать к Redis-подобным инструментам для кэширования или сохранения временных данных.
Инструменты, подкреплённые Postgres: в экосистеме Elixir есть множество инструментов, подкреплённых базой данных Postgres. Ecto предназначен для операций с базой данных, Oban — для выполнения фоновых заданий, а ElectricSQL — для выполнения запросов прежде всего на устройстве, а не в облаке (local-first) — для улучшения UX.
Благодаря всем этим возможностям мне удалось сдать проект вовремя и уложиться
в бюджет. Если бы я выбрал иной технологический стек, то, возможно, мне
потребовалось бы больше времени на конфигурирование сторонних библиотек и их
интеграцию, а также быстрее сосредоточиться на разработке конкретных фич
приложения.
Заключение
На примерах WhatsApp и Discord хорошо видно, как при помощи виртуальной машины Erlang и Elixir можно создавать легко масштабируемые приложения для работы в режиме реального времени. Но силу описанных здесь технологий успели оценить далеко не только эти компании. Другие выдающиеся игроки — Spotify, Pinterest, Pepsico, Financial Times и Heroku — также взяли на вооружение Erlang-подобные языки, и с их помощи смогли обрабатывать высокую конкурентность, добились отказоустойчивости и масштабируемости в своих предметных областях.
В заключение отмечу: если вы создаёте новую платформу для обмена сообщениями, сервис для игр в режиме реального времени или любое другое приложение, для которого нужно обеспечить сильную соединяемость, очень рекомендую вам присмотреться к Erlang VM и Elixir в качестве палочки-выручалочки. Уникальные возможности этих языков помогут вам заложить прочную основу для приложения и масштабироваться сколько угодно, не жертвуя ни производительностью, ни надёжностью.
meettya
Я, конечно, не специалист, но, кажется, формулировка "приложение для работы в режиме реального времени" не очень удачна.
Насколько мне известно, существуют RTOS (ака операционные системы реального времени), которые отличаются от общеиспользуемых иным алгоритмом планировщика задач.
Кажется более уместным было бы использование классификации "система мгновенного обмена сообщениями" или чего-то подобного. Например Discord в википедии назван так.