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)


  1. erlyvideo
    16.12.2016 00:40
    +1

    Это очень хороший пример того, чем удобен эрланг. А удобен он именно в продакшне: не на тестах на ноутбуке, а в реальном продакшне.


    1. doom369
      16.12.2016 01:31

      И в чем же удобство?


    1. ImLiar
      16.12.2016 01:56

      Если речь про backpressure из коробки, то его сейчас каждый ленивый делает. Взять Akka стримы, например


      1. erlyvideo
        16.12.2016 10:30
        -2

        как уже обсуждали: джава не даст нормального fault tolerance. При любом эксепшне надо очень аккуратно следить за закрытием ресурсов.


        1. doom369
          16.12.2016 12:24

          как уже обсуждали: джава не даст нормального fault tolerance.

          Это что-то новенькое.

          При любом эксепшне надо очень аккуратно следить за закрытием ресурсов.


          try (Connection c : openConnection()) {
          }
          


          1. erlyvideo
            16.12.2016 16:19

            И?

            файл открыли, начали с ним работать, произошла ошибка и вылетели выше. Файл остался незакрытым.

            Положили ссылку в глобальный хеш, забыли её удалить.

            Так что тут ничего новенького. В джаве нет ничего такого специфичного по сравнению с C++, что помогло бы бороться с утечками ресурсов и их контролем.


            1. doom369
              16.12.2016 16:26

              Файл остался незакрытым.

              Код выше закрывает файл. Даже если Вы этого не увидели.


  1. keydon2
    16.12.2016 07:27

    Довольно странно менять дырявый и проприетарный скайп, который непонятно куда сливает мою переписку, на другой закрытый сервис.


    1. Desprit
      16.12.2016 08:12
      +2

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


    1. darthslider
      16.12.2016 18:31
      +1

      Попробуйте собраться в скайпе группой 20+ человек — будет тихий ужас. А в любом «игровом» голосовом чате это отлично работает. А дискорд еще и безумно удобный, например групповым чатом с оффлайн историей, удобной настройкой прав по каналам и тд. Для организации какой-то групповой деятельности (не обязательно ммо) он прекрасен.


  1. ajaxtelamonid
    16.12.2016 08:28

    Вчера один наш несколько дней назад созданный чатик потерял всю хистори на одном из каналов.
    Let it crash в действии? Ну, такое.


    1. am-amotion-city
      16.12.2016 09:19
      +1

      Вот не нужно проблемы разработчиков приписывать языку. Let it crash не подразумевает потерю данных, конечно же.


      Нормальный жизненный цикл выглядит так: старт ? заглянули в DETS / зачитали оттуда в ETS ? поработали ? синкнули ETS в DETS ? повторили. Вместо DETS может быть любой persistent cache, это не важно. Тогда let it crash позволит вам не думая просто рестартануть gen_server (точнее, даже отдать эту задачу соответствующему супервизору.)


      Если разработчики не выгружают данные из памяти в перситент иногда — тут не важно, какая идеология за этим, все равно рано или поздно все навернется и данные потеряются.


  1. am-amotion-city
    16.12.2016 09:25
    +6

    GenStage — это новый режим Elixir

    o_O


    В оригинале — behaviour, это ключевое слово языка. Оставьте как есть, или уж идите до конца: «УниЭтап — это новый режим Эликсира».


    1. erlyvideo
      16.12.2016 10:31

      униэтап — это пять. унидкм, унислуга.


  1. Ivan_83
    16.12.2016 12:01
    -6

    Хипстеры героически преодолевают трудности которые у нормальных людей не встречаются.

    1. 25к клиентов и 1м сообщений в МИНУТУ это было смешно даже для школьников сделавших ICQ в конце 90-х с их пентиум2.

    2. Идиотская архитектура. Очевидно же что оно рассчитано быть чатиком для пары калек но для пацанов стало открытием что оно тормозит когда внезапно набежал народ.
    Из наших источников тут интересно было бы послушать меилрушников с их опытом, у них то по вечерам только онлайн были миллионы клиентов (в жаркие 2006-8 года) а уж сообщений вообще не счесть.

    А по итогу у всех современных чатигов есть только одна фишка=достижение: они как то взлетели и набрали юзеров. Те то что сделали перцы может сделать любой школяр на своём домашнем игровом компе.
    Только всякие токсы, битмесажджи и прочие п2п пока считаются технологичными, такое с наскоку за вечер не накорябать.

    2 erlyvideo
    Если бы ерланг был хоть чуточку хорош то на нём бы чего нибудь писали, и это чего нибудь я бы поставил из портов себе и использовал, но нет, кроме флюсоника я ни о чём даже не слышал на эрланге.
    Прогеров на нём тоже днём с огнём не сыскать: на ха-ха была одна вакансия и та про твой продукт и с конской з/п.
    Сейчас го и раст пеарятся примерно тем же самым, хотя пока, к счастью, ничего полезного на них не родили.


    1. ymn
      16.12.2016 12:27

      и это чего нибудь я бы поставил из портов себе и использовал
      ejabberd, riak, rabbitmq прекрасно ставятся «из портов»

      Сейчас го и раст пеарятся примерно тем же самым, хотя пока, к счастью, ничего полезного на них не родили.
      Ага, позовите как только на них напишут аналог riak-core. Те же посоны из akka так ничего вразумительного пока не смогли придумать.


      1. am-amotion-city
        16.12.2016 12:36
        +3

        Ничего близкого к riak-core на jvm не добиться в принципе, из-за заточенной под другое архитектуры.


        Но людям из мира «если бы писали, я бы поставил себе из портов» этого никогда не понять. вотсап ведь «из портов» не поставишь, да и t-mobile дома не развернешь.


      1. Ivan_83
        17.12.2016 18:30
        -2

        Я тут погрепал по портам, если отбросить те чьи названия начинаются с erlang- набралось от силы 20 штук, ну это так, грубо считая, на самом деле это учитывая что элексир и какие то ошмётки к нему в разных портах и элексир такой не один.
        Те 20 штук из 25 ТЫСЯЧ портов, вот это бешеный успех эрланга.
        Что называется ПИШИЕСЧО!

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

        2 am-amotion-city
        Я про жабу вообще слова не сказал.
        Жабеных портов у меня тоже нет и не планируется.
        Я лично за то, чтобы больше писали на Си, без плюсов. Больше — не значит совсем всё.

        Воцап — очередная херня которая почему то взлетела, как и сотни других чатегов один -похожий на другой.
        т-мобиль дома нафик не нужен, есть дект, на худой случай тоже самое через вайфай.
        Это вообще к чему всё?

        Учитывая как у них там всё устроено то они рассчитывают продаться гуглу раньше, чем он их прижмёт лимитами.


        1. am-amotion-city
          19.12.2016 09:44
          +2

          t-mobile — это немецкий фактически монополист на рынке телефонии (https://www.telekom.com/en)


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


          1. Ivan_83
            19.12.2016 11:21
            -4

            Приводить в пример «западный» телеком — это сильно.
            Уж что-что а именно европейские и американские телекомы слоупоки относительно азиатов (япошек, корейцев) и даже стран бывшего СССР (россия, украина).
            А телефонисты это вообще антипаттерн во всём: пока они там делили доходы изобретая OSI пришли пацаны из тусовки греперов и налабали TCP/IP. :)

            Поэтому мне посрать что они там используют и как, они ни разу ни лидер в IT, скорее они сглотнули провал с OSI и пытаются приспособится, подбирая крохи от остального IT и патаясь всячески запретить месенгеры и пр, дабы совсем не превратится в тупую трубу по которой звонят через скайп/вайбер/ и которую с лёгкостью заменяет точка доступа от дяди Ляо.
            Насчёт монополизма — у них там так исторически сложилось, ровно поэтому там (евпропы/америки) до сих пор тянут и развивают ADSL и доксис как основное средство доступа интернета в хату абонента.

            Простому человеку достаточно посмотреть FreeRADIUS чтобы понять что телефония головного мозга тяжёлое деструктивное расстройство психики.
            Спасибо, теперь запишем туда же эрланг. )


            1. am-amotion-city
              19.12.2016 12:08
              +2

              Записывайте куда хотите и что хотите, законы физики это не изменит.


              Поэтому мне посрать что они там используют и как, они ни разу ни лидер в IT

              Ваше мнение очень ценно для нас, конечно же, держите нас в курсе.


              ровно поэтому там (евпропы/америки) до сих пор тянут и развивают ADSL
              и доксис как основное средство доступа интернета в хату абонента

              А, ясно. Инфа 100%, да? Поразительно, с каким апломбом иногда умудряются разговаривать второгодники.


              1. Ivan_83
                20.12.2016 11:30
                -2

                Когда нечего сказать — лучше жевать, чем переходить на личности, а тебе уже второе сообщение подряд сказать по существу во славу эрланга нечего.

                Моё мнение простое: эрланг не востребованный-умирающий язык на котором никто ничего не пишет.
                Доказательство: менее 20 живых прог в паблике из 25к обследованных.

                Всё по провайдерам и немного телефонистам есть на nag.ru

                Из крупных телеком в RFC за последние годы я видел кажется оранж или кого то из французов, они там придумали какие то костыли чтобы IPv6 запустить поверх существующей сети.

                Касательно тмобиля.
                Ты в курсе в чём нынче нагрузки измеряются?
                Телефонисты давно перешли на пакетную передачу и коммутацию пакетов — те то на чём работает интернет и о ужас теперь их нагрузки измеряются в тех же величинах что у обычных смертных и их жалких вебсайтах: пакеты/сек, мегабиты/сек, запросы сек, и тоже самое посуточно.

                Давай цифры от тмобиля, порази нас ужасными нагрузками которые только элранг смог осилить во славу телефонистов!


                1. am-amotion-city
                  20.12.2016 11:55
                  +1

                  я видел кажется [...] или кого то [...], они там придумали какие то костыли

                  Вот это по существу, это я понимаю.


          1. doom369
            20.12.2016 11:41

            потому что все остальное под такой нагрузкой сосет,


            Это было правдой лет 10-20 назад, когда эти телекомы создавались. Сейчас, например, Ерланг Mqtt брокер раза в 2 по перформансу уступает netty+mqtt на java. Проверял лично.


            1. am-amotion-city
              20.12.2016 12:06
              +1

              Мы же все, наверное, понимаем, что «проверял лично» — это хорошо, но мало. Что значит «по перформансу уступает»?


              Кроме того, вопрос же не только в наивном бенчмарке по перформансу, вопрос еще в том, как, например, связка netty+mqtt будет хот-релоадиться, и сколько вокруг придется приседать по этому поводу. Я знаю, что java подбирается, да, но насколько мне известно, далеко не все проблемы уже прямо решены.


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


              1. doom369
                20.12.2016 12:20

                Мы же все, наверное, понимаем, что «проверял лично» — это хорошо, но мало.

                Конечно, так же как и «все остальное под такой нагрузкой сосет».

                Что значит «по перформансу уступает»?

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

                связка netty+mqtt будет хот-релоадиться

                Никак. Так же как и Ерланг это может не всегда. Вопрос скорее — а нужно ли это?

                java ведь еще гораздо дороже с точки зрения саппорта

                ?


                1. am-amotion-city
                  20.12.2016 12:40
                  +1

                  так же как и «все остальное под такой нагрузкой сосет»

                  Согласен, да.


                  Так же как и Ерланг это может не всегда. Вопрос скорее — а нужно ли это?

                  В моем сценарии — нужно; есть куча сценариев, где это не нужно. Короче, нет серебряной пули, вот что :)


  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 тысяч обратных запросов в секунду тоже не очень-то просто.