MassTransit и очередь в базе данных

d8c0bf45f8078525602e08d4b40fe329.png

MassTransit одна из самых популярных библиотек для построения асинхронного взаимодействия между сервисами в среде dotnet. Она активно развивается уже долгие годы, до сих пор остается под открытой лицензией и каждую версию добавляет новые фичи. Я построил уже не одно решение на ней и могу сказать, что по поддержке, качеству кодовой базы и «взрослости», это один из лучших примеров в нашем dotnet сообществе.

Недавно в новой версии MassTransit вышла относительно свежая фича, SQL Transport, которая позволяет использовать базу данных вместо традиционных брокеров сообщений, при этом оставляя доступными тонну фичей работающих на традиционных брокерах. Ретраи, ределивери, роутинг, все продолжает работать. Такая обратная совместимость в теории позволяет легко заменять несложные конфигурации RabbitMQ на SQL Transport и обратно.

Я попробовал эту фичу, посмотрел на нее с разных сторон и сегодня хочу поделиться своими впечатлениями.

Зачем

Зачем вообще может быть нужен SQL Transport в мире где RabbitMQ поднимается одной строчкой в докере? Приведу свой пример, у меня было несколько петпроектов, которые в какой-то момент переросли свою экспериментальную стадию и я стал их поставлять другим людям. Но их петпроектность толкала меня в прошлом на эксперименты: десктопное приложение состояло из нескольких сервисов, общающихся между собой через RabbitMQ. Разворачивать это все на клиентских машинах (далеко не у всех есть докер), писать скрипты для установки RabbitMQ то еще развлечение. С другой стороны, база данных у нас уже есть, так как она являлась наобходимым требованием к установке, так что замена RabbitMQ на SQL Transport позволяла избавиться от одной раздражающей инфраструктурной зависимости.

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

Как

Первым делом стоит почитать документацию, там подробно написано как все настроить. Расскажу про свой конфиг. Тестировалось все на проекте, где используется EF Core и postgres в качестве базы данных. До момента миграции использовался RabbitMQ как транспорт, было несколько событий и команды (суммарно штук 20 разных). Нейминг очередей был прописан вручную. В общем же случае простейшая конфигурация может выглядеть так:

services.AddMassTransit(x =>
{
    x.AddSqlMessageScheduler();
    
    x.UsingPostgres((context, cfg) =>
    {
        cfg.UseSqlMessageScheduler();
    
        cfg.ConfigureEndpoints(context);
    });
});

Проблемы

Переезд оказался не столь безболезненным, как казалось будет изначально. Под транспорт рекомендуется заводить отдельную базу данных, и, так как это postgres, а не какая-нибудь MongoDB, то необходимо сначала на нее накатить миграцию. И вроде как MassTransit это должен сделать самостоятельно, но его миграция падала с ошибками. Я начал разбираться с проблемами по одной:

  1. База данных должна называться с маленькой буквы, иначе ничего не поедет. Встроенная миграция не умеет в escape имен и только чистые «без кавычные» постгресовские названия работают.

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

  3. В postgres уже пару мажорных версий для dotnet активно рекомендуют использовать типы из NodaTime для обозначения дат и времен. Все отлично работает, но вот в SQL Transport внутренние модельки про это не знают, они используют DateTimeOffset. Казалось бы, ничего страшного, но дело в том, что нормально сконфигурировать драйвер postgres на разную работу в моем коде и в коде MassTransit нужно еще постараться. Т.е. Npgsql отлично работает с типами NodaTime ИЛИ работает со встроенными типами, но совместно — не работает. Тоже много потраченного времени. В итоге были найдены костыли Npgsql драйвера и удалось разделить регистрацию. В моем коде очень уж не хотелось отказываться от приятных NodaTime-овских типов.

  4. Многие знают, что в RabbitMQ можно публиковать сообщения не в очередь, а в exchange, при этом если имя очереди и exchange совпадает, то RabbitMQ автоматически настраивает между ними связь и все работает. В SQL Transport это к сожалению не так, а я этим активно пользовался. Пришлось переписывать несколько мест и писать в них в очереди. В RabbitMQ это было неудобно, потому что нужно было обязательно знать конфигурацию очереди (различные флаги свойств), здесь этого нет. Но все равно неприятно.

В итоге после преодоления вышеописанных проблем, через недельку свободных вечеров у меня все завелось и даже интеграционные тесты (очень пригодились кстати) стали зелеными. Но что дальше? Давайте для начала расскажу про неочевидности.

Неочевидности

Транзакционность. SQL Transport это по сути таблички. Таблички в той же базе, где у вас лежат и пользовательские бизнес данные. Казалось бы, вот она мечта любителя транзакционности, наконец-то все будет в одной транзакции, но нет. Тут все не так гладко, SQL Transport существует отдельно, у него свои транзакции и своя база данных. Это такая же внешняя для вашего бизнес кода система. Кстати, поэтому outbox паттерн актуален даже в случае использования той же базы в качестве транспорта.

Автомиграции на старте. На старте вашего сервиса (каждого, если их много), MassTransit проверяет наличие базы\схемы для SQL Transport и если их нет, то создает. Абсолютно не понятно, как это будет вести себя в мультисервисной среде, где вы, возможно, деплоите и запускаете по 50 сервисов одновременно. Но может это там учтено, мне это не известно.

Версионирование схемы. Еще один непонятный для меня момент, я нигде не нашел версии схемы или миграции. Изначально было непонятно, как в новых версиях, если эта схема поменяется, будут проходить миграции старой версии (текущей) на новую. B вот через месяцы мое опасение таки оправдалось. В нескольких новых версиях менялись функции по составу и реализации, и если у вас где-то уже была рабочая старая версия, то единственный способ обновиться это РУКАМИ удалить эти функции из постгреса. Это прям рекомендованный способ из issues масстранзита. Проблем с данными или табличками пока не было. Для тех кто все таки не хочет это делать руками, можно написать простенький декоратор над ISqlTransportDatabaseMigrator и реализовать в нем методы миграции по такому принципу:

public async Task CreateInfrastructure(SqlTransportOptions options, CancellationToken ct)
{
    try
    {
        // пытается просто вызвать стандартный мигратор
        await _innerMigrator.CreateInfrastructure(options, ct);
    }
    catch (Exception e)
    {
        using var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        
        // тут пишем код удаления views и functions
        
        // и пробуем еще раз
        await _innerMigrator.CreateInfrastructure(options, ct);
        transaction.Complete();
    }
}

Ну и последний момент, это эксклюзивность. Решение гвоздями прибито к dotnet и MassTransit. Если сообщения в RabbitMQ от MassTransit с горем попалам еще можно было слушать в других языках (хотя скорее всего пришлось бы руками разбирать его схему), то этот транспорт актуален только если у вас все клиенты используют MassTransit.

Плюсы

Что мне в итоге понравилось. В своем пет проекте я по итогу оставил этот транспорт. SQL Transport это минус одна точка деплоя, мониторинга, масштабирования и отказа.

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

Конкретно реализация с postgres не использует пуллинг и не нагружает сильно базу данных, там используются нативные postgres-овские нотификации, увеличивая тем самым производительность и уменьшая задержки.

Бесшовность. За исключением описанных выше проблем, большая часть моих конфигураций и настроек успешно завелась на новом транспорте и продолжила работать. Могу представить, что переезд на большой кодовой базе так же не займет много времени, и уж точно не придется переписывать все с нуля.

Выводы

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

Я тем временем жду новых фичей, в MassTransit часто происходят инновации, а я как активный пользователь люблю за этим наблюдать. Буду следить за обновлениями и дальше.

Bonus 1

Есть видео от Криса Паттерсона (основной автор MassTransit) с перечислением нескольких новых уникальных возможностей именно для SQL Transport. В том числе особые варианты работы с партиционированием и порядком сообщений. Тем, кто загорелся идеей попробовать, рекомендую ознакомиться.

Bonus 2

Недавно вышла библиотека https://github.com/filipbekic01/ResQueue, которая добавляет веб интерфейс над SQL Transport-ом. Его можно удобно запустить в докере, открыть в браузере и:

  • смотреть очереди и сообщения в них

  • смотреть ошибочные очереди и отправлять сообщения обратно в обычные

  • смотреть джобы масстранзита и управлять ими

Библиотека еще только в начале разработки, но уже закрывает большую часть потребностей, рекомендую к изучению.

© Habrahabr.ru