Discord
Discord испытал небывалый рост. Чтобы справиться с ним, нашему отделу разработки досталась приятная проблема — искать способ масштабирования сервисов бэкенда.
В этом деле мы добились большого успеха с помощью одной технологии, которая называется Elixir GenStage.
Идеальный шторм: Overwatch и Pokemon GO
Этим летом наша система мобильных push-уведомлений стала скрипеть от нагрузки. Чат /r/Overwatch перевалил за 25 000 одновременных пользователей, а чат-группы Pokemon GO возникали повсеместно, так что внезапные всплески потока уведомлений стали серьёзной проблемой.
Всплески потока уведомлений тормозят всю систему push-уведомлений, а иногда кладут её. Push-уведомления или приходят поздно, или не приходят вовсе.
GenStage идёт на помощь
После небольшого расследования мы выяснили, что основным бутылочным горлышком была отправка push-уведомлений в сервис Google Firebase Cloud Messaging.
Мы поняли, что можем немедленно улучшить пропускную способность, если отправлять push-запросы к Firebase по XMPP, а не по HTTP.
Firebase XMPP слегка сложнее, чем HTTP. Firebase требует, чтобы у каждого XMPP-соединения в каждый момент времени было не более 100 запросов в очереди. Если от вас улетело 100 запросов, то следует подождать, пока Firebase подтвердит получение запроса, прежде чем отправить следующий.
Поскольку в очередь допускаются только 100 запросов в каждый момент времени, нам пришлось спроектировать новую систему, чтобы XMPP-соединения не переполнялись во время всплесков потока запросов.
На первый взгляд показалось, что GenStage будет идеальным решением проблемы.
GenStage
Что такое GenStage?
GenStage — это новый режим (behaviour) Elixir для обмена событиями под обратным давлением между процессами Elixir. [0]
Что это значит на самом деле? По существу, этот режим даёт вам необходимые инструменты, чтобы ни одна часть вашей системы не перегружалась.
На практике, система с режимами GenStage обычно имеет несколько этапов.
Этапы (stages) — это шаги вычислений, которые отправляют и/или получают данные от других этапов.
Когда этап отправляет данные, он выступает в качестве производителя. Когда получает данные, то в качестве потребителя. Этапы могут играть роли одновременно и производителя, и потребителя.
Кроме назначения ролей производителя и потребителя, этап можно назначить «источником» (source), если он только производит элементы, или назначить «стоком» (sink), если он их только потребляет. [1]
Подход
Мы разделили систему на два этапа GenStage. Один источник и один сток.
- Этап 1 — Push Collector. Это производитель, который получает push-запросы. Сейчас у нас один процесс Erlang для Push Collector на одну машину.
- Этап 2 — Pusher. Это потребитель, который требует push-запросы от Push Collector и отправлят их к Firebase. Он запрашивает только по 100 запросов за раз, чтобы не превысить лимит Firebase на количество одновременных запросов. Процессов типа Pusher (тоже на Erlang) много на каждой машине.
Обратное давление и сброс нагрузки с помощью GenStage
У GenStage есть две ключевые функции, которые помогают нам во время всплеска запросов: обратное давление (back-pressure) и сброс нагрузки (load-shedding).
Обратное давление
Pusher использует функциональность GenStage, чтобы запросить у Push Collector'а максимальное количество запросов, которые Pusher может обработать. Это гарантирует верхнюю границу по количеству push-запросов, которые находятся в ожидании. Когда Firebase подтверждает запрос, тогда Pusher требует ещё от Push Collector'а.
Pusher знает точное количество запросов, которое может выдержать соединение Firebase XMPP, и никогда не требует лишнего. А Push Collector никогда не высылает запрос в сторону Pusher, если тот не попросил.
Сброс нагрузки
Поскольку Pusher'ы оказывают обратное давление на Push Collector, то появляется потенциальное бутылочное горлышко в Push Collector. Супер-дупер мощные всплески могут его перегрузить.
В GenStage имеется другая встроенная функция для таких ситуаций: буферизованные события.
В Push Collector мы определяем, сколько push-запросов помещать в буфер. В нормальном состоянии буфер пустой, но один раз в месяц при наступлении катастрофических событий он приходится весьма кстати.
Если через систему проходит ну уж очень много событий и буфер заполняется, тогда Push Collector сбрасывает входящие push-запросы. Это происходит само собой просто за счёт указания опции
buffer_size
в функции init
Push Collector'а.С этими двумя функциями мы способны справляться со всплесками push-уведомлений.
Код (наконец, самая важная часть)
Ниже пример кода, как мы настроили этапы Pusher и Push Collector. Для простоты мы убрали много фрагментов, отвечающих за обработку отказов, когда теряется соединение, Firebase возвращает ошибки и т.д.
Вы можете пропустить код, если хотите посмотреть на результат.
Push Collector (производитель)
push_collector.ex
defmodule GCM.PushCollector do
use GenStage
# Client
def push(pid, push_requests) do
GenServer.cast(pid, {:push, push_requests})
end
# Server
def init(_args) do
# Run as producer and specify the max amount
# of push requests to buffer.
{:producer, :ok, buffer_size: @max_buffer_size}
end
def handle_cast({:push, push_requests}, state) do
# Dispatch the push_requests as events.
# These will be buffered if there are no consumers ready.
{:noreply, push_requests, state}
end
def handle_demand(_demand, state) do
# Do nothing. Events will be dispatched as-is.
{:noreply, [], state}
end
end
Pusher (потребитель)
pusher.ex
defmodule GCM.Pusher do
use GenStage
# The maximum number of requests Firebase allows at once per XMPP connection
@max_demand 100
defstruct [
:producer,
:producer_from,
:fcm_conn_pid,
:pending_requests,
]
def start_link(producer, fcm_conn_pid, opts \\ []) do
GenStage.start_link(__MODULE__, {producer, fcm_conn_pid}, opts)
end
def init({producer, fcm_conn_pid}) do
state = %__MODULE__{
next_id: 1,
pending_requests: Map.new,
producer: producer,
fcm_conn_pid: fcm_conn_pid,
}
send(self, :init)
# Run as consumer
{:consumer, state}
end
def handle_info(:init, %{producer: producer}=state) do
# Subscribe to the Push Collector
GenStage.async_subscribe(self, to: producer, cancel: :temporary)
{:noreply, [], state}
end
def handle_subscribe(:producer, _opts, from, state) do
# Start demanding requests now that we are subscribed
GenStage.ask(from, @max_demand)
{:manual, %{state | producer_from: from}}
end
def handle_events(push_requests, _from, state) do
# We got some push requests from the Push Collector.
# Let’s send them.
state = Enum.reduce(push_requests, state, &do_send/2)
{:noreply, [], state}
end
# Send the message to FCM, track as a pending request
defp do_send(%{fcm_conn_pid: fcm_conn_pid, pending_requests: pending_requests}=state, push_request) do
{message_id, state} = generate_id(state)
xml = PushRequest.to_xml(push_request, message_id)
:ok = FCM.Connection.send(fcm_conn_pid, xml)
pending_requests = Map.put(pending_requests, message_id, push_request)
%{state | pending_requests: pending_requests}
end
# FCM response handling
defp handle_response(%{message_id: message_id}=response, %{pending_requests: pending_requests, producer_from: producer_from}=state) do
{push_request, pending_requests} = Map.pop(pending_requests, message_id)
# Since we finished a request, ask the Push Collector for more.
GenStage.ask(producer_from, 1)
%{state | pending_requests: pending_requests}
end
defp generate_id(%{next_id: next_id}=state) do
{to_string(next_id), %{state | next_id: next_id + 1}}
end
end
Пример инцидента
Ниже показан реальный инцидент, с которым столкнулась система. На верхнем графике показано количество push-запросов в секунду, проходящих через систему. На нижнем графике — количество push-запросов, помещённых в буфер Push Collector.
Хроника событий:
- ~17:47:00? — Система работает в нормальном режиме.
- ~17:47:30 ?—? К нам начинает поступать поток сообщений. Push Collector немного задействовал буфер, ожидая реакции Pusher. Вскоре буфер чуть освободился.
- ~17:48:50? — Pusher'ы не могут отправлять сообщения в Firebase быстрее, чем они поступают, так что буфер Push Collector'а начинает заполняться.
- ~17:50:00? — Буфер Pusher Collector достигает пика и начинает сбрасывать некоторые запросы.
- ~17:50:50? — Буфер Pusher Collector начинает освобождаться и перестаёт сбрасывать запросы.
- ~17:51:30? — ?Наплыв запросов пошёл на спад.
- ~17:52:30? — Система полностью вернулась в норму.
Успех Elixir
Мы в Discord очень довольны использованием Elixir и Erlang как ключевой технологии на наших сервисах бэкенда. Приятно видеть расширения вроде GenStage, которые опираются на нерушимые технологии Erlang/OTP.
Мы ищем смелых духом, чтобы помочь в решении таких проблем, поскольку Discord продолжает расти. Если вы любите игры и такого рода задачи заставляют ваше сердце биться чаще, посмотрите наши вакансии.
Комментарии (28)
keydon2
16.12.2016 07:27Довольно странно менять дырявый и проприетарный скайп, который непонятно куда сливает мою переписку, на другой закрытый сервис.
Desprit
16.12.2016 08:12+2Ну, на счет дырявости — эти претензии можно предъявлять вообще кому угодно. Плюсы дискорда в другом. Он действительно потрясающе удобен и очень активно развивается.
darthslider
16.12.2016 18:31+1Попробуйте собраться в скайпе группой 20+ человек — будет тихий ужас. А в любом «игровом» голосовом чате это отлично работает. А дискорд еще и безумно удобный, например групповым чатом с оффлайн историей, удобной настройкой прав по каналам и тд. Для организации какой-то групповой деятельности (не обязательно ммо) он прекрасен.
ajaxtelamonid
16.12.2016 08:28Вчера один наш несколько дней назад созданный чатик потерял всю хистори на одном из каналов.
Let it crash в действии? Ну, такое.am-amotion-city
16.12.2016 09:19+1Вот не нужно проблемы разработчиков приписывать языку. Let it crash не подразумевает потерю данных, конечно же.
Нормальный жизненный цикл выглядит так: старт ? заглянули в DETS / зачитали оттуда в ETS ? поработали ? синкнули ETS в DETS ? повторили. Вместо DETS может быть любой persistent cache, это не важно. Тогда let it crash позволит вам не думая просто рестартануть
gen_server
(точнее, даже отдать эту задачу соответствующему супервизору.)
Если разработчики не выгружают данные из памяти в перситент иногда — тут не важно, какая идеология за этим, все равно рано или поздно все навернется и данные потеряются.
am-amotion-city
16.12.2016 09:25+6GenStage
— это новый режимElixir
o_O
В оригинале —
behaviour
, это ключевое слово языка. Оставьте как есть, или уж идите до конца: «УниЭтап — это новый режим Эликсира».
Ivan_83
16.12.2016 12:01-6Хипстеры героически преодолевают трудности которые у нормальных людей не встречаются.
1. 25к клиентов и 1м сообщений в МИНУТУ это было смешно даже для школьников сделавших ICQ в конце 90-х с их пентиум2.
2. Идиотская архитектура. Очевидно же что оно рассчитано быть чатиком для пары калек но для пацанов стало открытием что оно тормозит когда внезапно набежал народ.
Из наших источников тут интересно было бы послушать меилрушников с их опытом, у них то по вечерам только онлайн были миллионы клиентов (в жаркие 2006-8 года) а уж сообщений вообще не счесть.
А по итогу у всех современных чатигов есть только одна фишка=достижение: они как то взлетели и набрали юзеров. Те то что сделали перцы может сделать любой школяр на своём домашнем игровом компе.
Только всякие токсы, битмесажджи и прочие п2п пока считаются технологичными, такое с наскоку за вечер не накорябать.
2 erlyvideo
Если бы ерланг был хоть чуточку хорош то на нём бы чего нибудь писали, и это чего нибудь я бы поставил из портов себе и использовал, но нет, кроме флюсоника я ни о чём даже не слышал на эрланге.
Прогеров на нём тоже днём с огнём не сыскать: на ха-ха была одна вакансия и та про твой продукт и с конской з/п.
Сейчас го и раст пеарятся примерно тем же самым, хотя пока, к счастью, ничего полезного на них не родили.ymn
16.12.2016 12:27и это чего нибудь я бы поставил из портов себе и использовал
ejabberd, riak, rabbitmq прекрасно ставятся «из портов»
Сейчас го и раст пеарятся примерно тем же самым, хотя пока, к счастью, ничего полезного на них не родили.
Ага, позовите как только на них напишут аналог riak-core. Те же посоны из akka так ничего вразумительного пока не смогли придумать.am-amotion-city
16.12.2016 12:36+3Ничего близкого к
riak-core
наjvm
не добиться в принципе, из-за заточенной под другое архитектуры.
Но людям из мира «если бы писали, я бы поставил себе из портов» этого никогда не понять. вотсап ведь «из портов» не поставишь, да и t-mobile дома не развернешь.
Ivan_83
17.12.2016 18:30-2Я тут погрепал по портам, если отбросить те чьи названия начинаются с erlang- набралось от силы 20 штук, ну это так, грубо считая, на самом деле это учитывая что элексир и какие то ошмётки к нему в разных портах и элексир такой не один.
Те 20 штук из 25 ТЫСЯЧ портов, вот это бешеный успех эрланга.
Что называется ПИШИЕСЧО!
Мне плевать на реакк и каких то там пацанов, так же как и им на меня, у нас не пересекающиеся интересы.
Уверен только в одном — можно тоже самое лучше и без эрланга.
2 am-amotion-city
Я про жабу вообще слова не сказал.
Жабеных портов у меня тоже нет и не планируется.
Я лично за то, чтобы больше писали на Си, без плюсов. Больше — не значит совсем всё.
Воцап — очередная херня которая почему то взлетела, как и сотни других чатегов один -похожий на другой.
т-мобиль дома нафик не нужен, есть дект, на худой случай тоже самое через вайфай.
Это вообще к чему всё?
Учитывая как у них там всё устроено то они рассчитывают продаться гуглу раньше, чем он их прижмёт лимитами.am-amotion-city
19.12.2016 09:44+2t-mobile — это немецкий фактически монополист на рынке телефонии (https://www.telekom.com/en)
Взлетел ли, и почему взлетел вотсап — дело десятое, вы, к сожалению, неспособны даже контекст разговора уловить. Вы про наколенные поделки, которые грепаются в портах, а я про промышленность. Как только напишете «то же самое, только без эрланга» — этот разговор станет предметным, а пока ваша компетентность сводится к «погрепал по портам» — как-то даже смешно. Весь западный телеком использует эрланг (он, кстати, так и появился на свет), потому что все остальное под такой нагрузкой сосет, но вам-то, мистер греп-по-портам, конечно, виднее.
Ivan_83
19.12.2016 11:21-4Приводить в пример «западный» телеком — это сильно.
Уж что-что а именно европейские и американские телекомы слоупоки относительно азиатов (япошек, корейцев) и даже стран бывшего СССР (россия, украина).
А телефонисты это вообще антипаттерн во всём: пока они там делили доходы изобретая OSI пришли пацаны из тусовки греперов и налабали TCP/IP. :)
Поэтому мне посрать что они там используют и как, они ни разу ни лидер в IT, скорее они сглотнули провал с OSI и пытаются приспособится, подбирая крохи от остального IT и патаясь всячески запретить месенгеры и пр, дабы совсем не превратится в тупую трубу по которой звонят через скайп/вайбер/ и которую с лёгкостью заменяет точка доступа от дяди Ляо.
Насчёт монополизма — у них там так исторически сложилось, ровно поэтому там (евпропы/америки) до сих пор тянут и развивают ADSL и доксис как основное средство доступа интернета в хату абонента.
Простому человеку достаточно посмотреть FreeRADIUS чтобы понять что телефония головного мозга тяжёлое деструктивное расстройство психики.
Спасибо, теперь запишем туда же эрланг. )am-amotion-city
19.12.2016 12:08+2Записывайте куда хотите и что хотите, законы физики это не изменит.
Поэтому мне посрать что они там используют и как, они ни разу ни лидер в IT
Ваше мнение очень ценно для нас, конечно же, держите нас в курсе.
ровно поэтому там (евпропы/америки) до сих пор тянут и развивают ADSL
и доксис как основное средство доступа интернета в хату абонентаА, ясно. Инфа 100%, да? Поразительно, с каким апломбом иногда умудряются разговаривать второгодники.
Ivan_83
20.12.2016 11:30-2Когда нечего сказать — лучше жевать, чем переходить на личности, а тебе уже второе сообщение подряд сказать по существу во славу эрланга нечего.
Моё мнение простое: эрланг не востребованный-умирающий язык на котором никто ничего не пишет.
Доказательство: менее 20 живых прог в паблике из 25к обследованных.
Всё по провайдерам и немного телефонистам есть на nag.ru
Из крупных телеком в RFC за последние годы я видел кажется оранж или кого то из французов, они там придумали какие то костыли чтобы IPv6 запустить поверх существующей сети.
Касательно тмобиля.
Ты в курсе в чём нынче нагрузки измеряются?
Телефонисты давно перешли на пакетную передачу и коммутацию пакетов — те то на чём работает интернет и о ужас теперь их нагрузки измеряются в тех же величинах что у обычных смертных и их жалких вебсайтах: пакеты/сек, мегабиты/сек, запросы сек, и тоже самое посуточно.
Давай цифры от тмобиля, порази нас ужасными нагрузками которые только элранг смог осилить во славу телефонистов!am-amotion-city
20.12.2016 11:55+1я видел кажется [...] или кого то [...], они там придумали какие то костыли
Вот это по существу, это я понимаю.
doom369
20.12.2016 11:41потому что все остальное под такой нагрузкой сосет,
Это было правдой лет 10-20 назад, когда эти телекомы создавались. Сейчас, например, Ерланг Mqtt брокер раза в 2 по перформансу уступает netty+mqtt на java. Проверял лично.am-amotion-city
20.12.2016 12:06+1Мы же все, наверное, понимаем, что «проверял лично» — это хорошо, но мало. Что значит «по перформансу уступает»?
Кроме того, вопрос же не только в наивном бенчмарке по перформансу, вопрос еще в том, как, например, связка
netty
+mqtt
будет хот-релоадиться, и сколько вокруг придется приседать по этому поводу. Я знаю, что java подбирается, да, но насколько мне известно, далеко не все проблемы уже прямо решены.
Могу, впрочем, ошибаться: java ведь еще гораздо дороже с точки зрения саппорта, поэтому мне лично за глаза хватает эрланга и непонятно, зачем бы куда-то уходить.
doom369
20.12.2016 12:20Мы же все, наверное, понимаем, что «проверял лично» — это хорошо, но мало.
Конечно, так же как и «все остальное под такой нагрузкой сосет».
Что значит «по перформансу уступает»?
Это значит, что в моем сценарии Ерланг мктт брокер показывал рельзультаты хуже чем ява мктт брокер. Дело было 2 года назад. Детали уже, конечно, не вспомнить.
связка netty+mqtt будет хот-релоадиться
Никак. Так же как и Ерланг это может не всегда. Вопрос скорее — а нужно ли это?
java ведь еще гораздо дороже с точки зрения саппорта
?
am-amotion-city
20.12.2016 12:40+1так же как и «все остальное под такой нагрузкой сосет»
Согласен, да.
Так же как и Ерланг это может не всегда. Вопрос скорее — а нужно ли это?
В моем сценарии — нужно; есть куча сценариев, где это не нужно. Короче, нет серебряной пули, вот что :)
BupycNet
17.12.2016 02:27Отправка 1 миллиона пушей в минуту не кажется такой уж большой проблемой. Если слать в firebase под одним коннектом связанные уведомления (если брать например чат то там будет 25000 человек по сути всем надо отправить пуш с одним и тем же контентом) то вы можете слать по 1000 пушей в пакете. Скорость будет примерно по 3-5 таких пакетов в секунду. Даже если брать вперемешку с небольшими группами по 10-20 устройств или даже там где у вас по одному то выходит скорость около 10-20 в секунду на одном коннекте ssl.
Как итог — при средней скорости в 300 пушей в секунду вам хватит 50 воркеров на одном сервере которые берут сообщения из rabbit. Выйдет около миллиона пушей в минуту. Окей для надежности можно и 500 воркеров или 10 серверов по 50 воркеров :). Все равно сервер на 70% занят ожиданием ответа гугла.
К слову тут скорее больше проблем с обработкой ответов чем с отправкой. Вы ведете статистику отправки помечаете у себя прочитанные? Обработать 16 тысяч обратных запросов в секунду тоже не очень-то просто.
erlyvideo
Это очень хороший пример того, чем удобен эрланг. А удобен он именно в продакшне: не на тестах на ноутбуке, а в реальном продакшне.
doom369
И в чем же удобство?
ImLiar
Если речь про backpressure из коробки, то его сейчас каждый ленивый делает. Взять Akka стримы, например
erlyvideo
как уже обсуждали: джава не даст нормального fault tolerance. При любом эксепшне надо очень аккуратно следить за закрытием ресурсов.
doom369
Это что-то новенькое.
erlyvideo
И?
файл открыли, начали с ним работать, произошла ошибка и вылетели выше. Файл остался незакрытым.
Положили ссылку в глобальный хеш, забыли её удалить.
Так что тут ничего новенького. В джаве нет ничего такого специфичного по сравнению с C++, что помогло бы бороться с утечками ресурсов и их контролем.
doom369
Код выше закрывает файл. Даже если Вы этого не увидели.