В рамках запуска своего API на Frame.io мы недавно добавили поддержку веб-перехватчиков. Поскольку мы уже используем GenStage в качестве шины событий, транслирующей множество разнородных потребителей событий для всех мутаций в системе, веб-перехватчики в простейшем смысле были просто добавлением другого потребителя к шине плюс подписанный HTTP-запрос. Тем не менее, мы хотим поднять планку и выявили несколько возможных ошибок при реализации веб-перехватчиков:

  • Непредсказуемая задержка от клиентов. Потребителю веб-перехватчика может потребоваться несколько секунд, чтобы принять запрос. Это в некоторой степени управляемо с таймаутами.
  • Периодические сбои клиентов. Одно из возможных решений - повторные попытки.
  • Управление открытыми HTTP-соединениями. Опять же, это несколько смягчено, на этот раз с помощью GenStage ConsumerSupervisor (который ограничивает количество одновременно обрабатываемых веб-перехватчиков) + тайм-ауты.

Очевидно, второй вариант решить проще всего, и для 1 и 3 существует тривиальное решение. Большая проблема в том, что если вы просто используете GenStage для управления противодавлением, вы рискуете в конечном итоге избавиться от веб-перехватчиков, поскольку в конечном итоге вы просто успеете при отправке события производителю GenStage. Также пагубным побочным эффектом этого будет замедление работы совершенно не связанных между собой потребителей, потому что система веб-перехватчиков продолжает оказывать противодавление на шину, предотвращая трансляции, что следует считать неуместным. По нашей оценке, ни один из этих вариантов не был приемлемым.

Лучший способ

Наше решение заключалось в том, чтобы создать диспетчер с нуля на основе простого GenServer и максимально использовать неблокирующую функциональность http. Проще говоря, hackney предоставляет хороший потоковый API с событиями в любой форме {:hackney_response, stream_ref, event}. Вы можете объединить их в handle_info обратные вызовы на GenServer с внутренней очередью, чтобы обеспечить полностью асинхронный диспетчер веб-перехватчиков, который не будет оказывать обратного давления на остальную систему. Вот код для демонстрации:

GenServer предоставляет приведение для отправки веб-перехватчика. При каждой отправке веб-перехватчик ставится в очередь для отправки, do_dispatch/1 проверяет, есть ли какие-либо доступные потоки (на основе настроенного максимального параллелизма), и если да, то отправляет HTTP-запросы POST с использованием опции async: :once hackney для каждого. Это гарантирует наше открытое ограничение HTTP-соединения. Ссылка на поток сохраняется во внутренней карте для использования в будущем, и всякий раз, когда hackney получает сообщение по сети, он отправляет его обратно на GenServer и обрабатывается с помощью вызова handle_info. Это гарантирует, что обработка в самом диспетчере не будет заблокирована http (условие 1). Мы даже можем преждевременно прервать поток, когда получим то, что ищем: код ответа. (Я пока оставлю реализацию загрузки ответов в стороне, но это также возможно, если продолжит работу hackney stream).

Добавление повторных попыток

Это дает нам 66% пути к нашей цели, но обратите внимание, что мы пожертвовали возможностью повторить попытку. Это нетривиальная задача при использовании всех преимуществ асинхронного ввода-вывода, которых мы только что достигли. Если вы заблокируете цикл, когда genserver, например, постоянно повторяет попытку POST, ну, это больше не асинхронный. Ключевым моментом здесь является то, что вы действительно можете использовать Process.send_after/3 для обработки повторных попыток, ничего не блокируя в GenServer. Все, что нам нужно сделать, это изменить mark_event и добавить еще один handle_info заголовок функции, вот так (предыдущий код для краткости опущен):

Handle_info для {:retry, event}message может просто имитировать обычную отправку события webhook. Мы инкапсулируем все принятие решений о повторных попытках в модуле Events, чтобы диспетчер оставался немым, но предполагаем, что он возвращает кортежи, подобные {:retry, _} | {:success, _} | {:halt, _} | {:error, _} (они становятся еще более важными, если вы выполняете загрузку тела, поскольку вы хотите убить хакни-поток, если вы '' повторная попытка, но оставьте ее для загрузки, если она была успешной или попытки были исчерпаны). Наконец, send_after оказывается действительно полезным механизмом повтора, поскольку вам просто нужно вычислить тривиальную экспоненциальную задержку, а затем правильно сформировать сообщение.

Заключительные предостережения

Добавив повторные попытки, мы достигли трех целей нашей системы диспетчеризации веб-перехватчиков: постоянного использования HTTP-соединения, способности обрабатывать разное время отклика без значительного снижения производительности других частей системы и повторных попыток при определенных ошибках. Также была неявная цель - обеспечить 100% доставляемость для наших веб-перехватчиков. Обратите внимание, что текущая реализация использует очередь в памяти. Если диспетчер всегда истощает воду за очень короткое время, это не вызывает особого беспокойства, но если система начинает принимать большую нагрузку, использование такой системы, как RabbitMQ или Kafka, в качестве механизма организации очереди было бы разумным, чтобы исключить возможность смерти виртуальной машины, вызывающей сброшенные события. . Приятно то, что нам ничего не мешает использовать их в этом коде. Замените структуру данных :queue клиентом Rabbit или Kafka, а вызовы _13 _ / _ 14_ аналогичными вызовами этих систем, и все должно вести себя как раньше. С Rabbit вы также можете подтвердить сообщение только после того, как оно завершит все повторные попытки, гарантируя доставку ровно один раз, даже если по какой-либо причине что-то взорвется во время процесса повторной попытки.

Понравилось то, что вы прочитали? Мы нанимаем!

В Frame.io мы обеспечиваем будущее творческого сотрудничества. Более 500 000 профессионалов в области видео используют Frame.io для беспрепятственного обмена медиафайлами и сбора отзывов с отметками времени от членов команды и клиентов. Проще говоря, мы помогаем компаниям создавать лучшие видео вместе.

Мы часто используем AWS Lambda, Elixir, Swift, Go и React. Мы небольшая команда полиглотов, которая мыслит масштабно и сообща работает над решением самых серьезных проблем для наших клиентов, включая Vice, BuzzFeed, Turner и NASA.