На самом деле в редисе миллионы ключей и все горячие сообщения - тут в зависимости от нагрузки могут миллионы лежать, так как новые сообщения зашли, но пропускная способность внешнего сервиса допустим 30 запросов в секунду по rate limit. в итоге все будет по порядку постепенно обрабатываться. постоянно туда летит и воркеры после обработки вычищают.
Насчет AOF/RDB. Его можно настроить чтобы без потерь работал. Цена - пропускная способность. Каждый выбирает под себя. Можно настроить выгрузку раз в секунду, тогда перечитывать нужно будет не все, а только с момента инцидента - должно быть быстро.
Плюс еще замечание к Вашему решению. У нас порядка 200 млн+ ключей, какие-то могут постепенно пропадать, какие-то новые появляются.
тут 2 вариант 1. добавляем и чистим 2. держим всегда в базе
1. тогда колличество запросов при добавлении сообщения. добавить сообщение / добавить ключ upsert всегда / прочитать ключи / залочить ключ / прочитать сообщение с локом / удалить сообщеие / если сообщения нет удалить ключ 2 добавить сообщение / добавить ключ upsert всегда / прочитать ключи (причем с джойном сообщений и фильтром по наличию иначе будет получать один и тот же ключ в котором нет сообщений) / залочить ключ / прочитать сообщение с локом / удалить сообщеие
В 1 будет просадка из-за увеличения колличества запросов В 2 будет просадка из-за большой таблицы с ключами и джойном с фильтром.
В целом я точно не могу согласиться делать http запросы внутри транзакции. Если честно это вредный совет. Ну согласитесь же? Всем всегда советую этого избегать. Если предложите решение без транзакций и которое будет держать хотя бы 300 rps, то буду рад обсудить ))
Вы сделали ровно тоже самое) Восстановление в вашем решение будет работать хорошо. А защиты от сбоев нет... Проблема долгих транзакций никуда не делась. Вы предлагаете обрабатывать по одному сообщению, но это не решит ошибку. Если один сервис начнет тормозить то все запросы к нему будут долгими, все воркеры рано или поздно встанут на запросах именно к этому сервису. И вот тут мы уже никак проблему не решим и можем так неделю просидеть пока сервису не полегчает (ну я конечно преувеличиваю, что-нибудь можно придумать будет). То есть в вашем решении заложена бомба которая тригернет в рандомный момент.
На счет редис мы наверное друг друга не до конца поняли. При настройке AOF/RDB будут потеряны не все сообщения, а только те что не успели зафиксироваться В целом придумал легкое решение... Доработаю чтобы ключ idempotency жил указанное окно. Тогда в случае сбоя достаточно будет сдвинуть offset в кафке за тот период который хотим перечитать. Плюс добавлю команду корторая перечитает out и заполнит ключ idempotency если его нет за указанное окно времени. в таком решение. команда заполнения блокировки должна отработать за пару минут, перечитывания с offset тоже. Если реакция на инцидент быстрая, то у воркеров не будет болого лага. Решение на час. На выходных добавлю и можно закрывать тему))
Введение execute_after не решает задачу. если предлагаете фильтровать по нему, то тогда ломается порядок, первую строку отфильтруете и возьмете вторую. если предлагаете проверять на клиенте, то возвращаемся к кейсу когда гоняем одно сообщение туда сюда.
Система блокировок будет далеко не тривиальна. На редисе это делается просто. В postgresql - нет. И из-за доп таблиц, полей и т.д. чтобы заставить постгрес удовлетворять условиям его пропускная способность станет еще меньше.
я уже привёл что можно разделить таблицу самой очереди где по партициям можно хранить сами сообщения
Не совсем понял про отдельную таблицу статусов сообщений. Партицировать таблицу можно - согласен, но все равно это будет 1 сревер и его ресрусы.
А очередь будет очищаться дропом партиций.
Тоже не понятно как очередь будет очищаться дропом партиции если там помимо обработанных есть и живые сообщения которые только будут взяты в работу....
Тут голословно.
Нет. Тут я имел в виду ваше предложение с длинными транзакциями. Длинные транзакции всегда зло и однозначно будут негативные последствия стрелять. Причем в этой схеме проблемы будут когда внешняя система начнет тормозить, а это мы не можем контролировать. Они тормозят, у нас дольше транзакции, мы падаем.
Можем, но не теряем) Редис хорошо справляется со своей задачей. Если будут прициденты - это станет приоритетом. Я согласен что это важный вопрос, просто пока есть более приоритетные задачи.
Это только звучит хорошо) Вы предлагаете открыть транзакцию и делать внутри нее http запросы, которые могут работать вплоть до минуты. Длинные транзакции будут убивать базу - чистка мусора, bloating, autovacuum, локи. постгрес не масштабируется горизонтально и с ней точно упремся в cpu / io на одном сревере. Вы предполагаете только успешный сценарий когда всегда акаем и удаляем данные, но на практике много reject и блокировок ключей в ожидание. как следствие при вашем подходе 1 и тоже сообщение будет бесконечно крутиться воркерами - взяли, словили 429 вернули, взяли словили 429 вернули, а таких ключей много. в итоге воркеры начнут крутить только их и по факту вся система встанет, не сможет обрабатывать те ключи которые реально сейчас доступны. Нужно будет городить более сложную систему блокировок.
Судя по предложению удалять записи вы предполагаете что таблица будет стремиться к нулю, но нет. очень часто сейчас бывает что есть большой пулл собщений (700k) по 1 ключу, и они обрабатываются часами, есть отложенные сообщения которые лежат годами. База никогда не будет пуста и в ней будут миллионы сообщений, что сделает запросы медленными.
Частые удаления под нагрузкой тоже будут приводить к деградации. Тут выгоднее было бы менять статус на done и потом чистить периодически.
Я согласен что на постгресе можно сделать что-то подобное, но повторюсь - пропускная способность такой системы будет значительно ниже)
Такое решение, что предложили, хорошо подходит для быстрых задач, когда сразу же в транзакции что-то обновить, маленькой нагрузке, для http запросов, считаю, плохая идея)
Насчет latency. Тут зависит от того какие будут задержки. пара секунд - нормально, пара минут - плохо. Постгрес - не очередь и не сможет стабильно держать высокую пропускную способность для таких задач. в итоге система будет деградировать и все ключи будут отрабатывать медленнее. В случае редиса, остается большой запас прочности. 1 ключ никак не афектит другой, объем базы никак не влиет на перфоманс.
Все просто) postgres плохо подходит для реализации требований описанных в ТЗ к задаче. Для нас критически важно - не блокирующие партиции / гарантированный порядок отправки в рамках 1 партиции. В этом случае на постгрес пришлось бы делать логику по типу. Храним мапинг воркер - партиция. При запросах джойним их и фильтруем. Затем получить 1 сообщение / залочить / обработать / акнуть или реджекнуть. После того как воркер разгреб одну партицию перекидываем его на другую, причем это пришлось бы делать не только когда разгреб а при любых ошибках с лиимитами или ретраями. Пришлось бы писать координатор, который бы слал еще доп запросы)) 2500 rps это выполненных задач. При получении по 1 сообщению если все будет лежать в базе, то на 1 сообщение это как минимум 3 запроса, то есть уже 7500 обращений к базе в секунду. То есть это уже почти потолок. С редис есть простор для оптимизаций и увеличению пропускной способности. Но не важно. Выше написал что за 5 часов у нас где-то 5_400_000 сообщений, если горизон хранения несколько дней, то в базе будет условно 54 миллиона сообщений и запросы будут явно работать сильно медленнее чем в редисе и более чем уверен что пропускная способнасть была бы меньше в разы. причем 54 миллиона это только срочных, а отложенные могут лежать годами. Ну и самое важное что решение с редис уже давно на проде и показывает себя отлично и надежно) То что редис не дает таких же гарантий согласен, но это можно поправить через логику описанную выше - с перечитываним кафки. Только прийдется немного посидеть)
Асинхронный контракт обычно делают когда результат нужен не обязательно сразу. Иначе зачем вообщем вам асинхронный контракт?
Все верно. есть кейсы когда ответ не нужен вообще, а есть когда нужен. и обязательно все сообщения должны проходить через одну систему rate limit
Главная цель использования такого количества инструментов это 2500 rps?
В приложении используется очередь / база / in memory хранилище. Это обычно минимум для чуть менее сложной системы) Без postgresql невозможно было бы реализовать сообщения отсылаемые через год. Без редиса невозможно было бы реализвоать партицирование и гарантию порядка для ограниченного числа воркеров. Очередь must have для такой системы. Все инструменты обоснованы и описаны в статье, что за что отвечает. rps был как маркер - выдержим ли мы текущие нагрузки. Ключевые факторы - неблокирующие партиции и порядок доставки в рамках портиции. Сами требования к сервису описаны в начале статьи)
А что с консистентностью вашей очереди? Или можно и не выполнять запрос если редис приложет?
Вот тут хороший вопрос! Сервис проектирвоался как замена текущей системы - сейчас уже есть хранение в редис и в случае если упадет, то - да сообщения будут потеряны. На практике у нас редис кластер и такого еще ни разу не было, поэтому на первом этапе этот момент опустил, но действительно собирался этим заняться. Ключевая идея, что у на есть лог событий в кафка и хотел добавить команду в приложение которая в случае сбоя заново перечитает кафку (топик in) с первого offset который был не обработан (тут нужно будет слдеить за хвостом) Преечитав кафку (топик out) можно гарантирвоать что сообщения не будут обработаны джажды + потребуется вероятно на этот момент не стартовать воркеры.
А если в режиме консистентности то какой объём данных и сколько будет подниматься в случае сбоя?
Как выше написал, пока не реализовано, но если делать, то будет зависеть от того как быстро среагируем на инцидент. Сейчас средняя скорость 300 с / сек (в пиках 1700) Допустим среагировали через 10 часов. Т очто было ранее 5 часов отсеиваем как устаревшие - получается не смогли отправить. Сейчас у бизнеса такие условия. получается за 5 часов надо вкачать
при средней скорости 300 с / сек это 5 3600 * 300 = 5_400_000 сообщений которые нужно заново загрузить в систему. Скорость перекидывания в базу согласно бенчмаркам очереди https://habr.com/ru/articles/1018194/ могут достигать 100_000 c / сек, то есть в базу все зайдет за минуту, а разгребаться уже будет со скоростью 2500 rps примерно минут 40. Это худший случай, когда все сообщения надо переотпрравить, если часть будет дедублицирована из-за того что ранее обрабоатлась, то должно быть быстрее. Но это пока в теории, реальные цифра можно будет увидеть только после реализации
Зачем вообще скорость ретрая если задача асинхронная?
Не совсем понял вопрос, но если суть что можно было sleep поставить, то тогда воркеры будут тормозиться и перфоманс будет сильно проседать. Так что с возвратом в очередь получается намного эффективнее
Прикладная задача - рассылка сообщений. Сервис был написан как заммена действующей системе. И ключевой фактор чтобы 1 клиент не блочил другого - именно для этого нужен редис и разделение по партициям! Сейчас почему-то у многих пунктик на LLM - сервис проектировали инженеры)) Но статью да - LLM помогала написать, что скрывать. Без нее сильно дольше бы писал, но суть не в буквах а в идеи и вызовах с которыми столкнулись, и хотелось поделиться этим. В целом если формат зайдет есть идея пройтись по архитектуре и написать ряд уже не прикладных статей. Не часто на хабре видел статьи по проектированию где разбирались бы моменты выбора стека, движенеи данных - считаю, что может быть интересным)
Постараюсь ответить) В целом было несколько причин:
У нас нет практического опыта с NATS, но из того что читал, там партицирование устроено как в kafka, то есть нельзя создать миллион партиций, чтобы каждый новый ключ попадал в свою. Точнее вроде можно, но будет связано с просадкой перфоманса и ограничением ресурсов. Как следствие будет ситуация что сообщения с разными ключами будут попадать в одну партицию и есть риск что если сообщение с ключом `a` не может обработаться и застряло, то ключ `b` будет ждать.
Вот тут https://github.com/nats-io/nats-server/discussions/2798 было обсуждение, но как понял не реализовано и чтобы такого добиться нужно писать костыли. Также в этой цепочке не нашел возможность отложить сообщение на опредлеленное время, чтобы не блокировать воркеры и они могли в это время обрабатывать другие партиции.
К тому же у нас уже были 2 очереди и добавлять в проект 3 сильно не хотелось чтобы не повышать порог входа и сложность инфраструктуры. А redis как must have. Уже используется для rate limit и блокировок поэтому выбор пал на него.
Реализовал решение и добавил блок в статью.
Также в рамках доработок добавил возможность указывать приоритеты сообщениям
На самом деле в редисе миллионы ключей и все горячие сообщения - тут в зависимости от нагрузки могут миллионы лежать, так как новые сообщения зашли, но пропускная способность внешнего сервиса допустим 30 запросов в секунду по rate limit. в итоге все будет по порядку постепенно обрабатываться. постоянно туда летит и воркеры после обработки вычищают.
Насчет AOF/RDB. Его можно настроить чтобы без потерь работал. Цена - пропускная способность. Каждый выбирает под себя. Можно настроить выгрузку раз в секунду, тогда перечитывать нужно будет не все, а только с момента инцидента - должно быть быстро.
Плюс еще замечание к Вашему решению.
У нас порядка 200 млн+ ключей, какие-то могут постепенно пропадать, какие-то новые появляются.
тут 2 вариант
1. добавляем и чистим
2. держим всегда в базе
1. тогда колличество запросов при добавлении сообщения. добавить сообщение / добавить ключ upsert всегда / прочитать ключи / залочить ключ / прочитать сообщение с локом / удалить сообщеие / если сообщения нет удалить ключ
2 добавить сообщение / добавить ключ upsert всегда / прочитать ключи (причем с джойном сообщений и фильтром по наличию иначе будет получать один и тот же ключ в котором нет сообщений) / залочить ключ / прочитать сообщение с локом / удалить сообщеие
В 1 будет просадка из-за увеличения колличества запросов
В 2 будет просадка из-за большой таблицы с ключами и джойном с фильтром.
В целом я точно не могу согласиться делать http запросы внутри транзакции. Если честно это вредный совет. Ну согласитесь же? Всем всегда советую этого избегать. Если предложите решение без транзакций и которое будет держать хотя бы 300 rps, то буду рад обсудить ))
Вы сделали ровно тоже самое)
Восстановление в вашем решение будет работать хорошо. А защиты от сбоев нет...
Проблема долгих транзакций никуда не делась. Вы предлагаете обрабатывать по одному сообщению, но это не решит ошибку. Если один сервис начнет тормозить то все запросы к нему будут долгими, все воркеры рано или поздно встанут на запросах именно к этому сервису. И вот тут мы уже никак проблему не решим и можем так неделю просидеть пока сервису не полегчает (ну я конечно преувеличиваю, что-нибудь можно придумать будет).
То есть в вашем решении заложена бомба которая тригернет в рандомный момент.
На счет редис мы наверное друг друга не до конца поняли. При настройке AOF/RDB будут потеряны не все сообщения, а только те что не успели зафиксироваться
В целом придумал легкое решение... Доработаю чтобы ключ idempotency жил указанное окно. Тогда в случае сбоя достаточно будет сдвинуть offset в кафке за тот период который хотим перечитать.
Плюс добавлю команду корторая перечитает out и заполнит ключ idempotency если его нет за указанное окно времени. в таком решение. команда заполнения блокировки должна отработать за пару минут, перечитывания с offset тоже. Если реакция на инцидент быстрая, то у воркеров не будет болого лага.
Решение на час. На выходных добавлю и можно закрывать тему))
Введение execute_after не решает задачу.
если предлагаете фильтровать по нему, то тогда ломается порядок, первую строку отфильтруете и возьмете вторую.
если предлагаете проверять на клиенте, то возвращаемся к кейсу когда гоняем одно сообщение туда сюда.
Система блокировок будет далеко не тривиальна.
На редисе это делается просто. В postgresql - нет. И из-за доп таблиц, полей и т.д. чтобы заставить постгрес удовлетворять условиям его пропускная способность станет еще меньше.
Не совсем понял про отдельную таблицу статусов сообщений. Партицировать таблицу можно - согласен, но все равно это будет 1 сревер и его ресрусы.
Тоже не понятно как очередь будет очищаться дропом партиции если там помимо обработанных есть и живые сообщения которые только будут взяты в работу....
Нет. Тут я имел в виду ваше предложение с длинными транзакциями. Длинные транзакции всегда зло и однозначно будут негативные последствия стрелять. Причем в этой схеме проблемы будут когда внешняя система начнет тормозить, а это мы не можем контролировать. Они тормозят, у нас дольше транзакции, мы падаем.
Можем, но не теряем) Редис хорошо справляется со своей задачей. Если будут прициденты - это станет приоритетом. Я согласен что это важный вопрос, просто пока есть более приоритетные задачи.
Это только звучит хорошо) Вы предлагаете открыть транзакцию и делать внутри нее http запросы, которые могут работать вплоть до минуты. Длинные транзакции будут убивать базу - чистка мусора, bloating, autovacuum, локи.
постгрес не масштабируется горизонтально и с ней точно упремся в cpu / io на одном сревере.
Вы предполагаете только успешный сценарий когда всегда акаем и удаляем данные, но на практике много reject и блокировок ключей в ожидание.
как следствие при вашем подходе 1 и тоже сообщение будет бесконечно крутиться воркерами - взяли, словили 429 вернули, взяли словили 429 вернули, а таких ключей много. в итоге воркеры начнут крутить только их и по факту вся система встанет, не сможет обрабатывать те ключи которые реально сейчас доступны. Нужно будет городить более сложную систему блокировок.
Судя по предложению удалять записи вы предполагаете что таблица будет стремиться к нулю, но нет. очень часто сейчас бывает что есть большой пулл собщений (700k) по 1 ключу, и они обрабатываются часами, есть отложенные сообщения которые лежат годами.
База никогда не будет пуста и в ней будут миллионы сообщений, что сделает запросы медленными.
Частые удаления под нагрузкой тоже будут приводить к деградации. Тут выгоднее было бы менять статус на done и потом чистить периодически.
Я согласен что на постгресе можно сделать что-то подобное, но повторюсь - пропускная способность такой системы будет значительно ниже)
Такое решение, что предложили, хорошо подходит для быстрых задач, когда сразу же в транзакции что-то обновить, маленькой нагрузке, для http запросов, считаю, плохая идея)
Насчет latency. Тут зависит от того какие будут задержки. пара секунд - нормально, пара минут - плохо. Постгрес - не очередь и не сможет стабильно держать высокую пропускную способность для таких задач. в итоге система будет деградировать и все ключи будут отрабатывать медленнее. В случае редиса, остается большой запас прочности. 1 ключ никак не афектит другой, объем базы никак не влиет на перфоманс.
Все просто) postgres плохо подходит для реализации требований описанных в ТЗ к задаче.
Для нас критически важно - не блокирующие партиции / гарантированный порядок отправки в рамках 1 партиции. В этом случае на постгрес пришлось бы делать логику по типу. Храним мапинг воркер - партиция. При запросах джойним их и фильтруем. Затем получить 1 сообщение / залочить / обработать / акнуть или реджекнуть. После того как воркер разгреб одну партицию перекидываем его на другую, причем это пришлось бы делать не только когда разгреб а при любых ошибках с лиимитами или ретраями. Пришлось бы писать координатор, который бы слал еще доп запросы))
2500 rps это выполненных задач. При получении по 1 сообщению если все будет лежать в базе, то на 1 сообщение это как минимум 3 запроса, то есть уже 7500 обращений к базе в секунду. То есть это уже почти потолок. С редис есть простор для оптимизаций и увеличению пропускной способности. Но не важно. Выше написал что за 5 часов у нас где-то 5_400_000 сообщений, если горизон хранения несколько дней, то в базе будет условно 54 миллиона сообщений и запросы будут явно работать сильно медленнее чем в редисе и более чем уверен что пропускная способнасть была бы меньше в разы. причем 54 миллиона это только срочных, а отложенные могут лежать годами.
Ну и самое важное что решение с редис уже давно на проде и показывает себя отлично и надежно)
То что редис не дает таких же гарантий согласен, но это можно поправить через логику описанную выше - с перечитываним кафки. Только прийдется немного посидеть)
Все верно. есть кейсы когда ответ не нужен вообще, а есть когда нужен. и обязательно все сообщения должны проходить через одну систему rate limit
В приложении используется очередь / база / in memory хранилище. Это обычно минимум для чуть менее сложной системы) Без postgresql невозможно было бы реализовать сообщения отсылаемые через год. Без редиса невозможно было бы реализвоать партицирование и гарантию порядка для ограниченного числа воркеров. Очередь must have для такой системы.
Все инструменты обоснованы и описаны в статье, что за что отвечает. rps был как маркер - выдержим ли мы текущие нагрузки. Ключевые факторы - неблокирующие партиции и порядок доставки в рамках портиции. Сами требования к сервису описаны в начале статьи)
Вот тут хороший вопрос! Сервис проектирвоался как замена текущей системы - сейчас уже есть хранение в редис и в случае если упадет, то - да сообщения будут потеряны. На практике у нас редис кластер и такого еще ни разу не было, поэтому на первом этапе этот момент опустил, но действительно собирался этим заняться.
Ключевая идея, что у на есть лог событий в кафка и хотел добавить команду в приложение которая в случае сбоя заново перечитает кафку (топик in) с первого offset который был не обработан (тут нужно будет слдеить за хвостом)
Преечитав кафку (топик out) можно гарантирвоать что сообщения не будут обработаны джажды + потребуется вероятно на этот момент не стартовать воркеры.
Как выше написал, пока не реализовано, но если делать, то будет зависеть от того как быстро среагируем на инцидент. Сейчас средняя скорость 300 с / сек (в пиках 1700)
Допустим среагировали через 10 часов. Т очто было ранее 5 часов отсеиваем как устаревшие - получается не смогли отправить. Сейчас у бизнеса такие условия. получается за 5 часов надо вкачать
при средней скорости 300 с / сек это 5 3600 * 300 = 5_400_000 сообщений которые нужно заново загрузить в систему.
Скорость перекидывания в базу согласно бенчмаркам очереди https://habr.com/ru/articles/1018194/ могут достигать 100_000 c / сек, то есть в базу все зайдет за минуту, а разгребаться уже будет со скоростью 2500 rps примерно минут 40.
Это худший случай, когда все сообщения надо переотпрравить, если часть будет дедублицирована из-за того что ранее обрабоатлась, то должно быть быстрее. Но это пока в теории, реальные цифра можно будет увидеть только после реализации
Не совсем понял вопрос, но если суть что можно было sleep поставить, то тогда воркеры будут тормозиться и перфоманс будет сильно проседать. Так что с возвратом в очередь получается намного эффективнее
Прикладная задача - рассылка сообщений. Сервис был написан как заммена действующей системе. И ключевой фактор чтобы 1 клиент не блочил другого - именно для этого нужен редис и разделение по партициям! Сейчас почему-то у многих пунктик на LLM - сервис проектировали инженеры)) Но статью да - LLM помогала написать, что скрывать. Без нее сильно дольше бы писал, но суть не в буквах а в идеи и вызовах с которыми столкнулись, и хотелось поделиться этим. В целом если формат зайдет есть идея пройтись по архитектуре и написать ряд уже не прикладных статей. Не часто на хабре видел статьи по проектированию где разбирались бы моменты выбора стека, движенеи данных - считаю, что может быть интересным)
Постараюсь ответить) В целом было несколько причин:
У нас нет практического опыта с NATS, но из того что читал, там партицирование устроено как в kafka, то есть нельзя создать миллион партиций, чтобы каждый новый ключ попадал в свою. Точнее вроде можно, но будет связано с просадкой перфоманса и ограничением ресурсов. Как следствие будет ситуация что сообщения с разными ключами будут попадать в одну партицию и есть риск что если сообщение с ключом `a` не может обработаться и застряло, то ключ `b` будет ждать.
Вот тут https://github.com/nats-io/nats-server/discussions/2798 было обсуждение, но как понял не реализовано и чтобы такого добиться нужно писать костыли. Также в этой цепочке не нашел возможность отложить сообщение на опредлеленное время, чтобы не блокировать воркеры и они могли в это время обрабатывать другие партиции.
К тому же у нас уже были 2 очереди и добавлять в проект 3 сильно не хотелось чтобы не повышать порог входа и сложность инфраструктуры. А redis как must have. Уже используется для rate limit и блокировок поэтому выбор пал на него.