У нас есть ситуация, когда мне приходится иметь дело с огромным потоком событий, поступающих на наш сервер, в среднем около 1000 событий в секунду (пик может составлять ~ 2000).
Проблема
Наша система размещена на Heroku и использует относительно дорогую базу данных Heroku Postgres , которая позволяет подключать до 500 БД. Мы используем пул соединений для подключения с сервера к БД.
События приходят быстрее, чем может обработать пул соединений с БД
Проблема в том, что события происходят быстрее, чем может обработать пул соединений. К тому времени, когда одно соединение завершило передачу по сети от сервера к БД, чтобы оно могло быть возвращено обратно в пул, n
приходит больше дополнительных событий.
В конечном итоге события складываются в ожидании сохранения и, поскольку в пуле нет доступных подключений, они истекают, и вся система становится неработоспособной.
Мы решили аварийную ситуацию, отправляя оскорбительные высокочастотные события медленнее от клиентов, но мы все еще хотим знать, как обращаться с этими сценариями в случае, если нам нужно обрабатывать эти высокочастотные события.
Ограничения
Другие клиенты могут захотеть читать события одновременно
Другие клиенты постоянно запрашивают чтение всех событий с определенным ключом, даже если они еще не сохранены в БД.
Клиент может запросить GET api/v1/events?clientId=1
и получить все события, отправленные клиентом 1, даже если эти события еще не сохранены в БД.
Есть ли "классные" примеры того, как с этим бороться?
Возможные решения
Поставьте в очередь события на нашем сервере
Мы можем поставить в очередь события на сервере (с максимальным параллелизмом в очереди, равным 400, чтобы пул соединений не заканчивался).
Это плохая идея, потому что:
- Это съест доступную память сервера. Сложенные в очередь события будут занимать огромное количество оперативной памяти.
- Наши серверы перезагружаются один раз каждые 24 часа . Это жесткий предел, наложенный Heroku. Сервер может перезапускаться, пока события ставятся в очередь, что приводит к потере событий в очереди.
- Он вводит состояние на сервере, что ухудшает масштабируемость. Если у нас настроена многосерверная система, и клиент хочет прочитать все события + в очереди + сохраненные, мы не будем знать, на каком сервере находятся события в очереди.
Используйте отдельную очередь сообщений
Я предполагаю, что мы могли бы использовать очередь сообщений (например, RabbitMQ ?), Где мы перекачиваем в нее сообщения, а на другом конце есть другой сервер, который занимается только сохранением событий в БД.
Я не уверен, позволяют ли очереди сообщений запрашивать события в очереди (которые еще не были сохранены), поэтому, если другой клиент хочет прочитать сообщения другого клиента, я могу просто получить сохраненные сообщения из БД и ожидающие сообщения из очереди и объединить их вместе, чтобы я мог отправить их обратно клиенту запроса на чтение.
Используйте несколько баз данных, каждая из которых сохраняет часть сообщений на центральном сервере БД-координатор для управления ими
Другое решение, которое у нас есть, - это использование нескольких баз данных с центральным «координатором БД / балансировщиком нагрузки». Получив событие, этот координатор выберет одну из баз данных для записи сообщения. Это должно позволить нам использовать несколько баз данных Heroku, тем самым увеличивая ограничение на соединение до 500 x количество баз данных.
По запросу на чтение этот координатор может выдавать SELECT
запросы к каждой базе данных, объединять все результаты и отправлять их обратно клиенту, который запросил чтение.
Это плохая идея, потому что:
- Эта идея звучит как ... хм .. чрезмерная инженерия? Было бы кошмаром управлять также (резервное копирование и т. Д.). Его сложно построить и поддерживать, и если это не является абсолютно необходимым, это звучит как нарушение KISS .
- Он жертвует последовательностью . Делать транзакции между несколькими БД не стоит, если мы пойдем с этой идеей.
источник
ANALYZE
запросы, и они не являются проблемой. Я также создал прототип для проверки гипотезы пула соединений и убедился, что это действительно проблема. База данных и сам сервер живут на разных машинах, следовательно, задержка. Кроме того, мы не хотим отказываться от Heroku, если в этом нет абсолютной необходимости, и для нас огромные плюсы - не беспокоиться о развертывании .select null
на 500 подключений. Бьюсь об заклад, вы обнаружите, что пул соединений не проблема там.Ответы:
Входной поток
Неясно, представляют ли ваши 1000 событий в секунду пики или это непрерывная загрузка:
Предложенное решение
Интуитивно понятно, что в обоих случаях я бы использовал поток событий, основанный на Кафке :
Это отлично масштабируется на всех уровнях:
Предлагая события, еще не записанные в БД для клиентов
Вы хотите, чтобы ваши клиенты могли также получить доступ к информации, которая еще находится в канале и еще не записана в БД. Это немного более деликатно.
Вариант 1. Использование кеша для дополнения запросов к базе данных.
Я не анализировал подробно, но первая идея, которая приходит мне в голову, - сделать процессор (ы) запросов потребителем (ями) тем kafka, но в другой группе потребителей kafka . Затем обработчик запросов получит все сообщения, которые получит писатель БД, но независимо. Затем он может хранить их в локальном кэше. Затем запросы будут выполняться в БД + кэш (+ устранение дубликатов).
Дизайн будет выглядеть так:
Масштабируемость этого уровня запросов может быть достигнута путем добавления большего числа обработчиков запросов (каждый в своей собственной группе потребителей).
Вариант 2: дизайн двойного API
ИМХО лучшим подходом было бы предложить двойной API (использовать механизм отдельной группы потребителей):
Преимущество в том, что вы позволяете клиенту решать, что интересно. Это может избежать систематического слияния данных БД с недавно кэшированными данными, когда клиент интересуется только новыми входящими событиями. Если деликатное слияние между свежими и заархивированными событиями действительно необходимо, клиент должен будет организовать это.
Варианты
Я предложил kafka, потому что он предназначен для очень больших объемов с постоянными сообщениями, чтобы вы могли перезапустить серверы, если это необходимо.
Вы можете построить аналогичную архитектуру с RabbitMQ. Однако, если вам нужны постоянные очереди, это может снизить производительность . Также, насколько мне известно, единственный способ достичь параллельного потребления одних и тех же сообщений несколькими читателями (например, Writer + cache) с RabbitMQ - это клонировать очереди . Таким образом, более высокая масштабируемость может быть выше.
источник
a distributed database (for example using a specialization of the server by group of keys)
? И почему Кафка вместо RabbitMQ? Есть ли особая причина для выбора одного над другим?Use multiple databases
идею, но вы говорите, что я не должен случайным образом (или циклически) распространять сообщения для каждой из баз данных. Правильно?Я думаю, что вам нужно более тщательно изучить подход, который вы отвергли
Мое предложение состояло бы в том, чтобы начать читать различные статьи, опубликованные об архитектуре LMAX . Им удалось заставить пакетирование больших объемов работать в своем случае, и возможно, что ваши компромиссы будут выглядеть как их.
Кроме того, вы можете захотеть узнать, сможете ли вы прочитать показания с пути - в идеале вы хотели бы иметь возможность масштабировать их независимо от записей. Это может означать изучение CQRS (разделение ответственности по командным запросам).
В распределенной системе, я думаю, вы можете быть уверены, что сообщения будут потеряны. Вы можете быть в состоянии смягчить некоторые из последствий этого, рассмотрев ваши барьеры последовательности (например, гарантируя, что запись в долговременное хранилище происходит до того, как событие будет передано за пределы системы).
Может быть - я бы с большей вероятностью посмотрел на границы вашего бизнеса, чтобы увидеть, есть ли естественные места, где можно хранить данные.
Ну, я полагаю, что может быть, но это не то, куда я шел. Дело в том, что дизайн должен был включать в себя надежность, необходимую для продвижения перед лицом потери сообщения.
То, на что это часто похоже, является моделью на основе извлечения с уведомлениями. Провайдер записывает сообщения в заказанный магазин длительного пользования. Потребитель извлекает сообщения из магазина, отслеживая собственную высокую отметку. Push-уведомления используются в качестве устройства, уменьшающего задержку, но если уведомление потеряно, сообщение по-прежнему выбирается (в конце концов), потому что потребитель выполняет регулярное расписание (разница в том, что если уведомление получено, получение происходит раньше). ).
См. Надежный обмен сообщениями без распределенных транзакций, Udi Dahan (на который уже ссылался Энди ) и Polyglot Data от Greg Young.
источник
In a distributed system, I think you can be pretty confident that messages are going to get lost
, В самом деле? Есть случаи, когда потеря данных является приемлемым компромиссом? У меня сложилось впечатление, что потеря данных = неудача.Если я правильно понимаю, текущий поток:
Если так, то я думаю, что первое изменение в дизайне состояло бы в том, чтобы прекратить обработку кода, возвращающего соединения с пулом, при каждом событии. Вместо этого создайте пул потоков / процессов вставки, который равен 1: 1 с количеством соединений с БД. Каждый из них будет содержать выделенное соединение с БД.
Используя некоторую параллельную очередь, эти потоки извлекают сообщения из параллельной очереди и вставляют их. Теоретически им никогда не нужно возвращать соединение с пулом или запрашивать новое, но вам может понадобиться встроить обработку в случае, если соединение разорвалось. Возможно, проще всего уничтожить поток / процесс и начать новый.
Это должно эффективно устранить накладные расходы пула соединений. Разумеется, вам нужно будет выполнять по крайней мере 1000 событий в секунду для каждого соединения. Возможно, вы захотите попробовать различное количество соединений, поскольку наличие 500 соединений, работающих с одними и теми же таблицами, может привести к конфликту на БД, но это совсем другой вопрос. Еще одна вещь, которую следует учитывать, - это использование пакетных вставок, то есть каждый поток извлекает несколько сообщений и вставляет их сразу. Кроме того, избегайте нескольких подключений, пытающихся обновить одни и те же строки.
источник
Предположения
Я собираюсь предположить, что нагрузка, которую вы описываете, постоянна, так как это более сложный сценарий для решения.
Я также собираюсь предположить, что у вас есть какой-то способ запуска запущенных, длительных рабочих нагрузок вне процесса вашего веб-приложения.
Решение
Предполагая, что вы правильно определили свое узкое место - задержку между вашим процессом и базой данных Postgres - это основная проблема, которую нужно решить. Решение должно учитывать ваши ограничения согласованности с другими клиентами, желающими прочитать события как можно скорее после их получения.
Чтобы решить проблему задержки, вам нужно работать так, чтобы свести к минимуму величину задержки, возникающей при сохранении события. Это ключевая вещь, которую вам нужно достичь, если вы не желаете или не можете менять оборудование . Поскольку вы работаете с сервисами PaaS и не имеете никакого контроля над оборудованием или сетью, единственный способ уменьшить задержку для каждого события будет заключаться в некоторой пакетной записи событий.
Вам нужно будет локально хранить очередь событий, которая периодически сбрасывается и записывается в вашу базу данных, как только она достигнет заданного размера, или по истечении определенного времени. Процесс должен будет отслеживать эту очередь, чтобы вызвать сброс в хранилище. Вокруг должно быть множество примеров того, как управлять параллельной очередью, которая периодически сбрасывается на выбранном вами языке. Вот пример на C # из периодического приемника пакетирования популярной библиотеки журналов Serilog.
Этот SO-ответ описывает самый быстрый способ сброса данных в Postgres - хотя для этого требуется, чтобы пакетное хранилище сохраняло очередь на диске, и, вероятно, там возникнет проблема, когда ваш диск исчезнет при перезагрузке в Heroku.
скованность
В другом ответе уже упоминалось CQRS , и это правильный подход для решения ограничения. Вы хотите гидрировать модели чтения при обработке каждого события - шаблон посредника может помочь инкапсулировать событие и распределить его по нескольким обработчикам в процессе работы. Таким образом, один обработчик может добавить событие в вашу модель чтения, находящуюся в памяти, которую клиенты могут запросить, а другой обработчик может быть ответственным за постановку в очередь события для его возможной пакетной записи.
Основным преимуществом CQRS является то, что вы отделяете свои концептуальные модели чтения и записи - это необычный способ сказать, что вы пишете в одну модель, а вы читаете из другой совершенно другой модели. Чтобы получить преимущества масштабируемости от CQRS, вы обычно хотите, чтобы каждая модель сохранялась отдельно таким образом, который оптимален для ее моделей использования. В этом случае мы можем использовать модель агрегированного чтения - например, кэш Redis или просто в памяти - чтобы обеспечить быстрое и согласованное чтение, в то время как мы все еще используем нашу транзакционную базу данных для записи наших данных.
источник
Это проблема, если каждому процессу требуется одно соединение с базой данных. Система должна быть спроектирована таким образом, чтобы у вас был пул работников, где каждому работнику нужно только одно соединение с базой данных, и каждый работник может обрабатывать несколько событий.
С этим дизайном может использоваться очередь сообщений, вам нужен производитель (и) сообщений, который отправляет события в очередь сообщений, а рабочие (потребители) обрабатывают сообщения из очереди.
Это ограничение возможно только в том случае, если события хранятся в базе данных без какой-либо обработки (необработанные события). Если события обрабатываются перед сохранением в базе данных, то единственный способ получить события из базы данных.
Если клиенты просто хотят запрашивать необработанные события, я бы предложил использовать поисковую систему, например Elastic Search. Вы даже получите API запроса / поиска бесплатно.
Учитывая, что для вас важно запрашивать события перед их сохранением в базе данных, простое решение, такое как Elastic Search, должно работать. Вы просто храните в нем все события и не дублируете одни и те же данные, копируя их в базу данных.
Масштабировать Elastic Search легко, но даже при базовой конфигурации он достаточно высокопроизводителен.
Когда вам нужна обработка, ваш процесс может получать события от ES, обрабатывать и сохранять их в базе данных. Я не знаю, какой уровень производительности вам нужен для этой обработки, но он будет полностью отделен от запроса событий от ES. В любом случае у вас не должно быть проблем с соединением, поскольку у вас может быть фиксированное количество рабочих, и у каждого будет одно соединение с базой данных.
источник
1 КБ или 2 КБ событий (5 КБ) в секунду не так много для базы данных, если она имеет соответствующую схему и механизм хранения. По предложению @eddyce мастер с одним или несколькими подчиненными может отделить запросы на чтение от совершения записей. Использование меньшего количества соединений с БД даст вам лучшую пропускную способность.
Для этих запросов им также нужно будет читать с главной базы данных, поскольку будет задержка репликации для ведомых устройств чтения.
Я использовал (Percona) MySQL с движком TokuDB для очень больших объемов записи. Есть также движок MyRocks, основанный на LSMtrees, который хорош для загрузки записей. Для обоих этих механизмов, а также, вероятно, для PostgreSQL, существуют параметры изоляции транзакций, а также поведения синхронизации при фиксации, которые могут значительно увеличить емкость записи. В прошлом мы принимали потерянные данные до 1 с, которые были переданы клиенту db как подтвержденные. В других случаях были SSD с батарейным питанием, чтобы избежать потерь.
Утверждается, что Amazon RDS Aurora в разновидности MySQL имеет 6-кратную пропускную способность записи с нулевой репликацией (сродни рабам, совместно использующим файловую систему с master). Аромат Aurora PostgreSQL также имеет другой продвинутый механизм репликации.
источник
Я бы отбросил героку все вместе, то есть отказался бы от централизованного подхода: множественные записи, которые достигают максимума подключения к пулу, являются одной из главных причин, по которым кластеры БД были изобретены, в основном потому, что вы не загружаете запись БД с запросами на чтение, которые могут выполняться другими БД в кластере, я бы попробовал с топологией ведущий-ведомый, более того - как уже упоминал кто-то другой, наличие ваших собственных установок БД позволило бы настроить целое система, чтобы убедиться, что время распространения запроса будет правильно обрабатываться.
Удачи
источник