Как стать автором
Обновить
15
0
Николай Андрейчук @Youkai

Пользователь

Отправить сообщение
Хотя второй воркер нам нужен не для поддержания надежности, а из-за других нюансов (не буду в это углубляться).

В большинстве случаев можно использовать более простые алгоритмы.
Если мы хотим гарантировать, что email не будет отправлен два раза (и при этом не сможем гарантировать, что он будет отправлен):
  1. Проверяем, что в БД нет записи об email из БД с переданным ключом идемпотентности (если есть — выбрасываем сообщение).
  2. Вставляем запись об email из БД со статусом «Новая».
  3. Коммитим транзакцию.
  4. Отправляем email.
  5. Изменяем статус записи об email на «Отправлено» (это просто атомарная операция без какой либо транзакции).
  6. Ack'аем сообщение.

Если мы хотим гарантировать, что email будет отправлен хотя бы один раз (и при этом не сможем гарантировать, что email не будет отправлен два раза):
  1. Проверяем, что в БД нет записи об email из БД с переданным ключом идемпотентности (если есть — выбрасываем сообщение).
  2. Вставляем запись об email из БД.
  3. Отправляем email.
  4. Коммитим транзакцию.
  5. Ack'аем сообщение.

Если использовать последний алгоритм, при падении коммита будут ситуации, когда письмо уже было отправлено, а записи в базе о нем еще нет (но позже она появится). Если мы отслеживаем открытие письма и изменяем при этом статус записи об email в БД, то лучше будет все-таки разделить отправку и выполнение SQL-транзакции по разным воркерам (это гарантирует нам, что email будет отправлен только после поялвения записи в БД).
1 и 2 не противоречит друг другу.
Если email остался в статусе «Отправляется», это может означать два варианта:
  1. Упала отправка email
  2. Упал update статуса

Т.е. если сообщение повисло в этом статусе мы не знаем наверняка было оно отправлено или нет. Эти сообщения мы автоматически не отправляем, поэтому гарантируется, что один email не будет отправлен два раза.

В вашем упрощенном подходе email-шлюз должен иметь доступ к БД системы-отправителя. Если вам не нужно выделять email-шлюз в отдельную систему, и вы считаете его частью CRM системы (и у них одна общая БД на двоих), то это вполне валидный подход.
100% уверенности, что отправление сообщения в MQ не упадет, у нас нет.

Если сохранять задачу на отправку, например, в SQL, а потом переотправлять, то список этих задач в итоге превратится в очередь. Проблемы с использованием SQL в качестве очереди я описал в конце статьи.

Подход с промежуточной очередью для отправки работает быстрее и при его использовании чаще всего можно обойтись без дополнительных таблиц, так что он даже проще, чем вариант со списком задач в БД.
Так как мы отсылаем сообщение в очередь до коммита, мы не можем гарантировать, что транзакция была завершена (коммит транзакции может упасть, хоть это и маловероятно). Поэтому нам нужна промежуточная очередь, при обработке сообщения из которой мы проверяем, что запись с идемпотентным ключом есть в БД (а значит транзакция была успешно закоммичена).

Если отправлять сообщение после коммита SQL транзакции, то может возникнуть ситуация, когда запись в БД об отправке есть, а сама отправка упала.
Немного устал повторять, но статья про другое)

Проблему с тем, что отправка email на почтовый сервер — не идемпотентная операция, мы решили немного усложнив логику работы второго воркера. Полностью она выглядит так:
  1. Получаем запись об email из БД (если ее там нет, значит транзакция первого воркера не была закомичена, и сообщение можно выбросить).
  2. Проверяем, что статус записи — «Новая» (если нет, значит сообщение уже обрабатывалось и скорее всего email был отправлен, поэтому сообщение выбрасывается).
  3. Изменяем статус записи об email на «Отправляется» и коммитим транзакцию
  4. Отправляем email.
  5. Изменяем статус записи об email на «Отправлено» (это просто атомарная операция без какой либо транзакции).
  6. Ack'аем сообщение.

При таком подходе мы не гарантируем, что email будет отправлен в 100% случаев, но гарантируем, что один email не будет отправлен два раза. Если упадет отправка email, то запись об email останется в невалидном статусе «Отправляется» (хотя если запись в этом статусе — есть вероятность, что email все таки отправился и упало изменение статуса). У нас настроен мониторинг, который проверяет количество таких записей и шлет алерт если их много. Сейчас сообщения в таком статусе остаются крайне редко — только если возникают проблемы на уровни сети.
Еще раз поясню, вопрос (да и вся статья) был про транзакцию отправки сообщения в очередь. Вы же описываете транзакцию обработки сообщения из очереди.

Обработка сообщения у нас устроена примерно так как вы описали, но вы упустили два нюанса:
  1. Так как вы генерируете ключ идемпотентности (guid) при обработке сообщения, вы не сможете его использовать, если транзакция упадет — он просто нигде не сохранится. Ключ идемпотентности должен быть в исходном сообщении и генерировать его должна система-отправитель.
  2. Если транзакция упадет на 5-ом шаге, то она повторится через некоторое время и во второй очереди окажется 2 одинаковых сообщения. Чтобы этого избежать нужно использовать тот же подход, который мы используем при отправке сообщения в очередь — 2ой воркер должен проверить, что запись с guid есть в БД,
Мы хотим гарантированной доставки сообщений, поэтому используем Ack. Но Ack не гарантирует, что сообщение не будет обработано несколько раз, поэтому обработку сообщений нужно делать идемпотентной.

При отправке нам необходимо гарантировать, что сообщение будет добавлено в очередь только в случае успешного завершения SQL-транзакции. В зависимости от того в какой момент отправляется сообщение могут быть две разные проблемы:
  • SQL-транзакция выполнилась успешно, но потом отправка сообщения в очередь упала (если мы отправляем сообщение после завершения SQL-транзакции)
  • Сообщение успешно попало в очередь, но потом SQL-транзакция была откачена (если мы отправляем сообщение до завершение SQL-транзакции)

Как вы предлагаете использовать confrim для того, чтобы избежать обеих проблем?

Синхронная отправка email нас не устраивает (хотя бы потому, что при этом в случае downtime email-шлюза, не работают все системы, которым нужно отправлять email).
В течение SQL транзакции мы только добавляем сообщение в промежуточную очередь. Практически сразу после завершения транзакции, мы перекладываем это сообщение в очередь, которую обрабатывает система-получатель. Обработка сообщения (отправка email, если система-получатель — это email-шлюз) полностью асинхронна и система-отправитель не ждет, когда она будет завершена.
NServiceBus работает как надстройка над MSMQ, который умеет интегрироваться с MS DTC.
MSMQ нас не устраивает, а для RabbitMQ NServiceBus не может сделать распределенную транзакцию.
Ack актуален только при обработке сообщения, при отправке Ack вызвать нельзя. Если сохранять информацию об успешной обработке сообщения где-либо (в другой очереди или в SQL базе), нет гарантии, что после этого отработает Ack (хотя бы из-за недоступности сети, но в реальных условиях между завершением транзакции и Ack может выполнятся еще код, который также может упасть). В этом случае сообщение через некоторое время обработается еще раз, чего мы не хотим.

Что делать, если падает именно отправка сообщения, я подробно написал в статье.
Ack при обработке мы используем, конечно же, но проблема с транзакционностью, которую я описал, относится к транзакции отправки сообщения.

Но и при обработке сообщения одного только Ack'а не достаточно. Если после публикации данных в очередь с результатами упадет Ack, то это сообщение будет обработано два раза. Поэтому обработку сообщения всегда лучше делать идемпотентной.
Получается сложновато, не хотелось бы столько сложностей добавлять к и так не простой архитектуре кэширования.

У нас есть места, где кэшируются данные для отдельных потребителей. Этот кэш у нас сбрасывается простым вызовом Redis'а, так как даже если сбросить кэш не удастся — он сам устареет через 5 минут и нас это устраивает. В большинстве случаев это наиболее хороший подход к кэшированию.

Можно бы было кэш кампаний также сделать устаревающим раз в 5 минут, но тогда у нас было бы значительно больше перезагрузок кэша по сравнению с кэшированием в памяти процесса.
Может упасть timeout, могут быть проблемы с сетью, сервер кэша может не ответить из-за слишком большой нагрузки и т.д. Мы логируем все ошибки и случаи падения запросов к Redis'у имеются. Проблем со сбросом кэша может не быть только при использовании InMemory кэша. Если кэш хранится на другом сервере, полагаться на то, что запрос к нему обработается в 100% случаев, не стоит.
Сброс кэша мы не делали асинхронным. Его можно бы было сделать через очередь, чтобы кэш был сброшен даже в случае падения запроса к Redis (если не удалось сбросить кэш, повторяем пока не получится). В случае использовании очереди будет задержка, но в общем то совсем небольшая — максимум несколько миллисекунд, так что можно было про это и не упоминать, наверное.

Если мы просто дергаем сброс кэша после редактирования кампании, то в случае если запрос на сброс кэша упадет, у нас останется старая версия кампании до тех пор пока кэш не устареет, что не очень приятно. Причем хотелось бы, чтобы период устаревания кэша был побольше (в текущей реализации кэш вообще не устаревает со временем — он обновляется, только если кампания изменилась).
Необходимо, чтобы кэш гарантированно сбрасывался в случае успешного редактирования кампании. Для этого придется делать что-то вроде распределенной транзакции между Redis'ом и MS-SQL. В принципе можно редактирование кампании сделать так:
  1. считываем кампанию
  2. сбрасываем кэш (если не получилось, откатываем SQL транзакцию)
  3. изменяем кампанию и завершаем транзакцию

Так как и редактирование кампании и загрузка кэша происходит в транзакции с уровнем изоляции Serializable, то загрузка кэша будет ждать завершения редактировании кампании. Однако такой подход добавляет зависимость админки от кэша (кэш не работает — редактировании кампании не работает) и сервисов от админки (пока редактируется кампания — все запросы сервисов ждут).

Альтернативный вариант — по окончании редактирования кампании добавлять в очередь Rabbit сообщение с командой на сброс кэша. Затем в windows-сервисе, где у нас обрабатываются сообщения, будет гарантированно сброшен кэш, но с некоторой задержкой, что в принципе приемлемо. При этом надо еще гарантировать, что команда на сброс кэша будет добавлена в очередь (правда распределенные транзакции между SQL и Rabbit мы уже сделали — об этом в следующей статье напишем).
Контекст у нас свой на каждую транзакцию.
Тут проблема скорее не в том, что сущности никогда не покинут контекст, а в том, что по коду даже не видно, что они в контекст загружались.

Например, если выполнении следующего запроса
var activeActionTemplates = modelContext.Repositories.Get<ActionTemplateRepository>().Items
    .Where(at => at.EffectiveStartDateTimeUtc <= DateTime.UtcNow)
    .Where(at => at.EffectiveEndDateTimeUtc > DateTime.UtcNow)
    .ToArray();

все ActionTemplate'ы загрузятся в память, так как EffectiveStartDateTimeUtc и EffectiveEndDateTimeUtc не мапятся на SQL. Если таких сущностей в базе много, то может даже упасть OutOfMemory, хотя по коду кажется, что мы вычитываем только небольшой массив данных.
Ну в общем да.

Если точнее, то у нас есть мини-фреймворк для обработки периодических задач в windows-сервисе. Для каждой задачи есть запись в БД, в которой кроме прочего хранится время последнего выполнения задачи. Поток планировщика каждую минуту проверяет, есть ли задачи, которые пора запустить и запускает каждую в отдельном потоке (а если точнее, то в отдельном System.Threading.Tasks.Task).

Для каждого триггера мы создаем отдельную периодическую задачу со своим периодом обработки. Минимальный период, который можно выставить в настройках триггерах, — 5 минут. Если нужно, чтобы уведомление (или, например, выдача, баллов) происходило прям сразу и 5 минут — слишком долгий срок, то триггеры мы не используем. Для таких задач у нас есть своя система event'ов, которые могут выполняться как синхронно, так и асинхронно с использованием Rabbit-очередей. Но на event'ах нельзя сделать логику вроде «послать письмо тем, кто не заходил на сайт боле 2-ух недель». Триггеры в общем более гибкие.
2

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность