Мы с коллегами посмотрели детальнее настройки сети меж нодами кластера. И вы верно предположили )
Наш сетевой менеджер для kubernetes установил правила для conntrack-а: что все пакеты с любых адресов, помеченные внутри самого conntrack как Invalid (те как раз пакеты с другим seq id, к примеру) должны дропаться.
При том мы еще раз с коллегами убедились, что если бы этих правил на conntrack не было, то пакеты долетели бы до нод и их системы отстрелились бы с RST. Тк таблица с mac-адресами персистентна в кластере между падениями и восстановлением нод.
Странно, что раньше не заметили и внимания не обратили. Скорее всего не думали в сторону сетевого менеджера.
При том, польза от этого правила и его выставление сетевым менеджером по-дефолту понятны: отсеивать кривые пакеты еще до их захода на уровень операционки, тем самым разгрузив ее.
Добавлю эту секцией UPD в статью. Спасибо за годный комментарий!
Согласен, мы в спокойном для себя темпе идем к этому. Предстоит еще много работы, что бы процесс таймаутов для запросов был введен и был прозрачен для разработчиков. Возможно об этом когда-нибудь напишем статью и поделимся :)
как-будто фиксить поведение нужно именно тут
Что Linkerd, что Envoy убирают из пула балансировки такие реплики. Здесь сыграло то, что соединения уже установлены и Linkerd, видимо, не может по своей инициативе разрывать соединения. Либо ждет какого-то другого ивента от k8s.
Так это все еще аппликейшны
Согласен - мысль свою выразил не так :) Под апликейшеном подразумевал, что linkerd может быть использован как protocol-aware прокси и для grpc реализует мультиплексирование соединений. Под простым tcp - подразумевал проксирование application протокола на уровне L4 без доп фичей самого протокола.
который гоняет ack’и в отдельном коннекте или переодически новые tcp сессии создает
Это хорошее решение, плюсую за него! По такому же алгоритму работает в Go пулинг соединений до БД database/sql/sql.go - соединения периодически обновляются.
Но в Go-реализации gRPC (да и в Rust) не предоставляется конфигурация менеджмента соединений, те такую логику по периодическому обновлению соединений не реализовать. При этом ее нет только со стороны клиента, с стороны сервера она есть keepalive/keepalive.go
Почему сделано так - без понятия ¯_(ツ)_/¯. Причин не нашел
В общем основной мой поинт в том
Согласен с вами! По возможности лучше обходится силами applicaiton-протоколов. Но в случае gRPC, у нас это не получилось - не хватает опций для управления соединений и частой отправки пингов со стороны соединений.
Могу предположить, что в вашем случае на воркере, или где-то в другом месте по пути был iptables рул
Спасибо за детальную инфу! Мы с коллегами изучим, может действительно что-то такое там заложено и вернусь с ответом, если что найдем :)
Как таковой цели проверить, что именно процесс приложения работает корректно - не было. В рамках статьи рассматривается только TCP-доступность хоста. Нужны другие инструменты, чтобы гарантировать, что само приложение работает корректно. К примеру, тут хватило бы классических liveness проб от k8s. Которые убьют зависшую реплику, не говоря уж о соединении (убив реплику). Так что, при всем уважении, вы тут немного про другое.
Но если в целом раскрыть тему про сравнение проверки доступности хоста по L4 или L7. Проверка на уровне TCP позволит гарантировать, что хост все еще доступен, для протоколов, которые требуют строгой последовательности. Протокол MySQL, к примеру. Пока выполняется SQL запрос по соединению нельзя выполнить ping.
его тоже можно понизить, если машина работает только в условиях ДЦ
Согласен с вами, но исходя из внутренних тестов мы сошлись на значении в 30 секунд. Возможно действительно можно было меньше, исходя из условий работы внутри ДЦ. Мы еще хотели перенести эту настройку в Linkerd, он уже может проксировать запросы в дикий интернет. Поэтому сошлись на 30 секундах.
Чаще всего 99% запросов в inter-service коммуникации подпадают под какую-то адекватную верхнюю границу
Согласен с вами, что большинство запросов имеют какую-то адекватную верхнюю границу, но я бы обратил внимание на критичность этих запросов. Если рассматривать введение таймаутов с точки зрения ущерба приложению, который они могут принести на этапе их обкатки - то они становятся очень дороги в примении. В этот 1% запросов, что можно на мониторинге и не разглядеть при определении верхней границы, могут входить какие-то редкие, но критичные пользовательские сценарии. В нашем случае это особенно критично, т.к. у нас есть монолит, который при обработке бОльших данных в более крупном аккаунте (кейс нашей LMS) потреблять больше времени на обработку, чем в остальных - т.к. в монолите случается крайне не оптимизированный код местами :) Мы отказались от таймаутов на стороне приложения именно из-за высокого риска иметь отстреливаемые бизнес-сценарии на продолжительном промежутке времени. Наверное, если бы не решили решения через TCP_USER_TIMEOUT, то пришли бы к таймаутам приложения.
следующий аппликативный ретрай уже пойдет на здоровый инстанс
Не в случае gRPC: следующий ретрай для него как раз таки не обязательно пойдет на здоровый инстанс. Об этом кейсе статья :)
В случае cloudflare’а подход с tcp_user_timeout и другие low-level тюнинги на уровне ядра/сети имеют смысл
В нашем случае они так же имеют смысл, ведь мы подключили TCP_USER_TIMEOUT не напрямую в сервисы, а в используемый нами ServiceMesh Linkerd. Мы доработали Linkerd, чтобы системно на уровне ниже от приложения решить проблему. Мейнтенерам Linkerd нужно решать все те же проблемы, что и cloudflare lb. Я сейчас про:
вовремя освобождать ресурсы на балансере не имея контроля над upstream/downstream
Тк Linkerd может быть использован не только как прокси для application протоколов, но и general tcp прокси, для соединений до баз данных к примеру. Так что TCP_USER_TIMEOUT и низкоуровневый fine-tuning необходимы :)
Тем более в самом простом кейсе, когда процесс убился по сегфолту
Было не совсем это - по сегфолту упала сама worker-нода. И при перезапуске ноды, самим подам действительно выданы были те же ip-адреса. Но при этом после запуска ноды и появления подов, зависшие соединения все еще наблюдались и при анализе с TCP dump и wireshark - RST-пакетов не было. Если бы они были, то проблемы и кейса не было бы и статьи тоже :)
все "зависшие" коннекшны получат RST
Может у вас какая-то дока или кейс чтобы почитать про такое поведение. Потому что при своем исследовании, я такого не нашел.
Согласен с вами. Только у нас наступала проблема не из-за значений таймаута, а впринципе при ping-ах клиента сервером чаще, чем дефолтные значения.
У нас не все сервисы могут иметь постоянный трафик, что хотя бы раз в 5 или 30 секунд есть запросы к ним. Таким образом такие пинги будут отстреливать обращения в такие сервисы. Так что нам такой вариант не подошел
В целом, контейнеризация сборки приносит два заметных торможения в сборку:
Запуск контейнера на каждый шаг - дополнительные +-500m в зависимости от системы
Как будто, процессы местами медленнее работают внутри докер-конейтнеров, которые запускает Buildkit(там он немного лайфхачит с монтированием файловой системы) - примерно на 10% работает медленнее
Это не такие драматичные тормоза, которые сильно затормозят локальную сборку. Как будто, в случае 20 мин CI - возможно стоит оптимизировать этот процесс или локально не гонять все этапы, требуются в CI/CD, т.к. это не всегда необходимо
Brewkit без проблем собирает на маке на arm под интел: под капотом у Docker Buidlkit используется qemu, который запустит x86 образы докера с встроеной эмуляцией. Будет медленно, да. Но только в случае отсуствия нативного образа под ARM. Если есть или пересобрать образ под arm, указать его platform-у, докер будет автоматом скачивать образ под нужную платформу и обходится без той же виртуализации. Даже в Dockerfile или brewit.jsonnet ничего указывать не нужно.
Так что Brewkit/Docker Buidlkit прекрасно собирают проекты на разных системах (из ARM под x86, из x86 под ARM) - просто есть свои нюансы и особенности :)
Транзакция не может обеспечить атомарность работы над агрегатом. Как я писал в статье, мы используем DDD и внутри нашей доменной модели лежит агрегат и наш агрегат имеет несколько листьев, без именованной блокировки при операциях над агрегатом мы можем привести модель в неконсистентное состояние при загрузке агрегата в память либо при сохранении.
чтобы тот кто выполняет запросы к микросервису явно начинал и завершал транзакцию.
В нашем случае инициатором запросов является фронтенд. Переносить на фронтенд ответственность за транзакцию странно. К тому же ваше предложение чем-то похоже на реализацию распределеннной транзакции или 2-ух фазный коммит, только в данном случае это не зачем. Мы работаем только в рамках одного сенрвиса и к другим не ходим.
Спасибо за статью. Есть несколько спорных моентов, которые я бы хотел отметить.
Действительно, транзакцию можно представить как некую бизнес-сущность — ведь бизнес логика может требовать атомарного и неконкурентного выполнения операции
Разве не лучше обеспечивать эту транзакционность коду на уровне Application? К тому же, в ответе на комментарии вы писали:
В гексагоне я выполняю транзакцию на слое приложения, а сами методы репо вызываю внутри слоя доменов
Хотя, сюдя по вашему коду, транзакция у вас стратует в методе Transactor.WithinTransaction , который вызывается прямо в домене. Поэтому не понятно о каком выполнении транзакции на слое приложения идёт речь.Моя мысль - убрать транзакцию из домена и пусть за неё отвечает код Application. Зачем вам операции над репозиториями в домене вне транзакции? Не очень вижу от этого плюсов, зато вижу переусложнение доменных методов вызовами Transactor.WithinTransaction . К тому же потенциальные ошибки гарантированы в таком случае: велика вероятность прочитать старые данные из базы и проводить операции над нами и потерять консистентность.
Если быть точнее, слой приложения гексагона управляет транзакцией, а внутри он вызывает методы доменного слоя. А уже домен внутри дергает репозитории (спрятанные за интерфейсами-портами), при этом не зная ничего про транзакцию.
Тут тоже наблюдается противоречие: слой домена по факту управляет транзакцией, ведь именно в нём вызывается метод Transactor.WithinTransaction.
Мне кажется, что то как организована передача транзакции в коде репозитория можно улучшить. Как предлагали в одном из комментариев.Передавать только один объект Conn , который присутствует в стандартной библиотеке database/sql. В этом случае не нужно приведений типов и можно передавать только один объект - соединение. Изначально выделить себе соединение и начать на нём транзакцию
Этот код выглядит оверхедным после предложения использовать объект Conn . К тому же этот подход более универсальный. Если вы захотите применять блокировки на уровне базы postgresql, то у вас не будет зависимости в очередности применения транзакции и блокировки. У нас произошла подобная проблема, в статье я описал что у нас произошло и как мы её решили.
Ещё спорным является решение положить в Context объект Transaction . Вы сами писали:
который позволяет передавать утилитарные данные
И тут я с вами полностью согласен. Такие вещи как RequestID, данные запроса для хендлера(как делает роутер gorilla/mux) или телеметрия идеально подходят для хранения в контексте.
С моей точки зрения информация о транзакции отлично подходит под определение "утилитарных данных"
Объект транзакции это скорее "сервис-абстракция", он предоставляет свои методы, в данном случае, для выполнения SQL-запрсов. И уж точно он(объект транзакции) не является "утилитарными данными" :) К тому же вы не контролируете кто владеет транзакцией. Так код, что находится по дереву выполнения ниже может закрыть транзакцию, а она доступна всем по дереву выполнения выше из контекста, сущности выше могут попытаться выполнить SQL-запрос на уже закрытой транзакции. Кейс странный, но это просто ещё одно для возникновения ошибок. В своей статье я описал, как мы используем шаринг соединения используя Context как ключ доступа к сущностям. За получение транзакции через Context отвечает одна сущность. Решаем ту же проблему не складывая в Context сервисы :) Так можно Context ненароком превратить ServiceProvider в рамках запроса :)
Последнее и очень спорное: Context в домене. Объект Context в своём поле Value переносит неизвестную домену сущность interface{} и получается домен начинает зависеть от неопределенной сущности, а значит нарушается целостность домена! К тому же, зачем домену как-то зависеть такой сущности как Context , которая ему ни к чему?
В GO у объекта базы данных есть метод BeginTx, который начинает тразакцию им вовзращает объект транзакции, через который можно выполнять SQL-запросы и они будут выполнятся на том же соединении, где начата транзакция. Нам нужно было именно отвязать компонент от объекта транзакции. По сути компоненту неважно выполняет он свои запросы в рамках уже начатой транзакции или открывает новое соединение. За обеспечение атомарности и консистентности у нас отвечает слой Application, переиспользовать подключение нужно сущностям инфраструктуры. Мы же через наш пул шарим объект соединения, на котором можем и транзакцию стартовать и несколько блокировок накатить. Сущности на уровне инфраструктуры будут просто использовать то же самое соединение, используя Context, который им передаётся по ходу выполнения кода. Простой пример, где нам это пригодилось, если не очень углубляться во внутреннюю кухню :) - это TransactionalOutbox при синхронной обработке доменого события, как я писал в комментарии выше
Не совсем) Наша модель работает по механизму ревизий(каждый запрос изменяет ревизию) нам нужна блокировка, чтобы одномоментно один запрос мог изменить модель и выставилась новая ревизия для модели - другие запросы со сторой ревизией просто подождут снятия блокировки и получат ошибку при внесении изменений в модель со старой ревизией :) При Repeatable Read такого бы не получилось :) Ещё необходимо было разработать универсальный механизм применимый в других наших микросервисах. А там мы в основном используем именованные блокировки, преимущественно для полной блокировки агрегата внутри доменной модели )
Так и не понял, зачем нужен отдельный коннекшн для лока?
Отдельный коннекшн для лока это была как раз проблема, которую мы решили.
И в чем смысл вашей uow?
Это скорее абстакция для уровня Application. Наша uow никакие изменения модели ненакапливает, за нас это делает механизм транзакций в базе.
такой подход позволяет, например, схлопнуть update entity + delete entity до delete entity
Это уже выглядит как задача фреймворка ORM, который отслеживает изменения модели. В GO не очень популярны ORM(всё извлекается и записывается прямыми SQL-запросами) и мы тоже решили не использовать. Механизма транзакций базы данных нам хватает)
В языке GO в стандартной библиотеке присутсвует пуллинг соединений к базе. При этом при обращении к этому пулу мы можем только получить новое соединение либо выполнить SQL-запрос на рандомном соединении из пула. У нас же была необходимость выполнять SQL-запросы на одном соединении в разных независищих частях сервиса, чтобы любой компонент мог иметь возможность работать уже раннее созданном соединении или открывать новое. К примеру, в рамках одного запроса мы можем выполнить запросы к базе в независимых частях сервиса: сохранение сущностей из домена в базу в репозитории и в синхронном обработчике доменных событий - везде мы должны выполнить запросы на одном соединении. Так. в обработчике доменного события мы хотим применить TransactionalOutbox и записать сериализованное доменное событие в базу на той же транзакции, что и сохраняли сущности в репохитории. Именно поэтому нам нужен собственный пул - переиспользовать соединения, чтобы компоненты, которым нужно соединение к базе, могли переиспользовать текущее соединение с базой по нашим правилам: предоставив контекст как ключ для доступа к соединению. Благодаря контекстам, которые живут в рамках запроса, и пулу - мы предоставляем доступ к текущему соединению на запросе
Добрый вечер!
Мы с коллегами посмотрели детальнее настройки сети меж нодами кластера. И вы верно предположили )
Наш сетевой менеджер для kubernetes установил правила для conntrack-а: что все пакеты с любых адресов, помеченные внутри самого conntrack как
Invalid
(те как раз пакеты с другим seq id, к примеру) должны дропаться.При том мы еще раз с коллегами убедились, что если бы этих правил на conntrack не было, то пакеты долетели бы до нод и их системы отстрелились бы с RST.
Тк таблица с mac-адресами персистентна в кластере между падениями и восстановлением нод.
Странно, что раньше не заметили и внимания не обратили. Скорее всего не думали в сторону сетевого менеджера.
При том, польза от этого правила и его выставление сетевым менеджером по-дефолту понятны: отсеивать кривые пакеты еще до их захода на уровень операционки, тем самым разгрузив ее.
Добавлю эту секцией UPD в статью.
Спасибо за годный комментарий!
Согласен, мы в спокойном для себя темпе идем к этому. Предстоит еще много работы, что бы процесс таймаутов для запросов был введен и был прозрачен для разработчиков. Возможно об этом когда-нибудь напишем статью и поделимся :)
Что Linkerd, что Envoy убирают из пула балансировки такие реплики. Здесь сыграло то, что соединения уже установлены и Linkerd, видимо, не может по своей инициативе разрывать соединения. Либо ждет какого-то другого ивента от k8s.
Согласен - мысль свою выразил не так :)
Под апликейшеном подразумевал, что linkerd может быть использован как protocol-aware прокси и для grpc реализует мультиплексирование соединений.
Под простым tcp - подразумевал проксирование application протокола на уровне L4 без доп фичей самого протокола.
Это хорошее решение, плюсую за него!
По такому же алгоритму работает в Go пулинг соединений до БД
database/sql/sql.go
- соединения периодически обновляются.Но в Go-реализации gRPC (да и в Rust) не предоставляется конфигурация менеджмента соединений, те такую логику по периодическому обновлению соединений не реализовать. При этом ее нет только со стороны клиента, с стороны сервера она есть
keepalive/keepalive.go
Почему сделано так - без понятия ¯_(ツ)_/¯. Причин не нашел
Согласен с вами! По возможности лучше обходится силами applicaiton-протоколов. Но в случае gRPC, у нас это не получилось - не хватает опций для управления соединений и частой отправки пингов со стороны соединений.
Спасибо за детальную инфу!
Мы с коллегами изучим, может действительно что-то такое там заложено и вернусь с ответом, если что найдем :)
Спасибо за комментарий!
Как таковой цели проверить, что именно процесс приложения работает корректно - не было. В рамках статьи рассматривается только TCP-доступность хоста. Нужны другие инструменты, чтобы гарантировать, что само приложение работает корректно. К примеру, тут хватило бы классических liveness проб от k8s. Которые убьют зависшую реплику, не говоря уж о соединении (убив реплику).
Так что, при всем уважении, вы тут немного про другое.
Но если в целом раскрыть тему про сравнение проверки доступности хоста по L4 или L7.
Проверка на уровне TCP позволит гарантировать, что хост все еще доступен, для протоколов, которые требуют строгой последовательности. Протокол MySQL, к примеру. Пока выполняется SQL запрос по соединению нельзя выполнить ping.
Согласен с вами, но исходя из внутренних тестов мы сошлись на значении в 30 секунд. Возможно действительно можно было меньше, исходя из условий работы внутри ДЦ.
Мы еще хотели перенести эту настройку в Linkerd, он уже может проксировать запросы в дикий интернет. Поэтому сошлись на 30 секундах.
Спасибо за столь развернутый комментарий! )
Согласен с вами, что большинство запросов имеют какую-то адекватную верхнюю границу, но я бы обратил внимание на критичность этих запросов. Если рассматривать введение таймаутов с точки зрения ущерба приложению, который они могут принести на этапе их обкатки - то они становятся очень дороги в примении. В этот 1% запросов, что можно на мониторинге и не разглядеть при определении верхней границы, могут входить какие-то редкие, но критичные пользовательские сценарии. В нашем случае это особенно критично, т.к. у нас есть монолит, который при обработке бОльших данных в более крупном аккаунте (кейс нашей LMS) потреблять больше времени на обработку, чем в остальных - т.к. в монолите случается крайне не оптимизированный код местами :)
Мы отказались от таймаутов на стороне приложения именно из-за высокого риска иметь отстреливаемые бизнес-сценарии на продолжительном промежутке времени.
Наверное, если бы не решили решения через TCP_USER_TIMEOUT, то пришли бы к таймаутам приложения.
Не в случае gRPC: следующий ретрай для него как раз таки не обязательно пойдет на здоровый инстанс. Об этом кейсе статья :)
В нашем случае они так же имеют смысл, ведь мы подключили TCP_USER_TIMEOUT не напрямую в сервисы, а в используемый нами ServiceMesh Linkerd. Мы доработали Linkerd, чтобы системно на уровне ниже от приложения решить проблему. Мейнтенерам Linkerd нужно решать все те же проблемы, что и cloudflare lb. Я сейчас про:
Тк Linkerd может быть использован не только как прокси для application протоколов, но и general tcp прокси, для соединений до баз данных к примеру. Так что TCP_USER_TIMEOUT и низкоуровневый fine-tuning необходимы :)
Было не совсем это - по сегфолту упала сама worker-нода. И при перезапуске ноды, самим подам действительно выданы были те же ip-адреса.
Но при этом после запуска ноды и появления подов, зависшие соединения все еще наблюдались и при анализе с TCP dump и wireshark - RST-пакетов не было. Если бы они были, то проблемы и кейса не было бы и статьи тоже :)
Может у вас какая-то дока или кейс чтобы почитать про такое поведение. Потому что при своем исследовании, я такого не нашел.
Согласен с вами.
Только у нас наступала проблема не из-за значений таймаута, а впринципе при ping-ах клиента сервером чаще, чем дефолтные значения.
У нас не все сервисы могут иметь постоянный трафик, что хотя бы раз в 5 или 30 секунд есть запросы к ним. Таким образом такие пинги будут отстреливать обращения в такие сервисы.
Так что нам такой вариант не подошел
Это налайфхачил мой коллега: мы создали модуль ядра linux на C и разыменовали там nil указатель :)
Исходник:
Опечатку поправил, спасибо :)
В целом, контейнеризация сборки приносит два заметных торможения в сборку:
Запуск контейнера на каждый шаг - дополнительные
+-500m
в зависимости от системыКак будто, процессы местами медленнее работают внутри докер-конейтнеров, которые запускает Buildkit(там он немного лайфхачит с монтированием файловой системы) - примерно на 10% работает медленнее
Это не такие драматичные тормоза, которые сильно затормозят локальную сборку.
Как будто, в случае 20 мин CI - возможно стоит оптимизировать этот процесс или локально не гонять все этапы, требуются в CI/CD, т.к. это не всегда необходимо
Brewkit без проблем собирает на маке на arm под интел: под капотом у Docker Buidlkit используется qemu, который запустит x86 образы докера с встроеной эмуляцией.
Будет медленно, да.
Но только в случае отсуствия нативного образа под ARM.
Если есть или пересобрать образ под arm, указать его platform-у, докер будет автоматом скачивать образ под нужную платформу и обходится без той же виртуализации. Даже в Dockerfile или brewit.jsonnet ничего указывать не нужно.
Так что Brewkit/Docker Buidlkit прекрасно собирают проекты на разных системах (из ARM под x86, из x86 под ARM) - просто есть свои нюансы и особенности :)
Записал демку будучи незалогиненным на https://asciinema.org/ и через 7 дней она пропала :)
Перезалил на залогиненный акк, так что больше не пропадёт
Транзакция не может обеспечить атомарность работы над агрегатом. Как я писал в статье, мы используем DDD и внутри нашей доменной модели лежит агрегат и наш агрегат имеет несколько листьев, без именованной блокировки при операциях над агрегатом мы можем привести модель в неконсистентное состояние при загрузке агрегата в память либо при сохранении.
В нашем случае инициатором запросов является фронтенд. Переносить на фронтенд ответственность за транзакцию странно. К тому же ваше предложение чем-то похоже на реализацию распределеннной транзакции или 2-ух фазный коммит, только в данном случае это не зачем. Мы работаем только в рамках одного сенрвиса и к другим не ходим.
Спасибо за статью. Есть несколько спорных моентов, которые я бы хотел отметить.
Разве не лучше обеспечивать эту транзакционность коду на уровне Application? К тому же, в ответе на комментарии вы писали:
Хотя, сюдя по вашему коду, транзакция у вас стратует в методе
Transactor.WithinTransaction
, который вызывается прямо в домене. Поэтому не понятно о каком выполнении транзакции на слое приложения идёт речь.Моя мысль - убрать транзакцию из домена и пусть за неё отвечает код Application. Зачем вам операции над репозиториями в домене вне транзакции? Не очень вижу от этого плюсов, зато вижу переусложнение доменных методов вызовамиTransactor.WithinTransaction
. К тому же потенциальные ошибки гарантированы в таком случае: велика вероятность прочитать старые данные из базы и проводить операции над нами и потерять консистентность.Тут тоже наблюдается противоречие: слой домена по факту управляет транзакцией, ведь именно в нём вызывается метод
Transactor.WithinTransaction
.Мне кажется, что то как организована передача транзакции в коде репозитория можно улучшить. Как предлагали в одном из комментариев.Передавать только один объект
Conn
, который присутствует в стандартной библиотекеdatabase/sql
. В этом случае не нужно приведений типов и можно передавать только один объект - соединение. Изначально выделить себе соединение и начать на нём транзакциюЭтот код выглядит оверхедным после предложения использовать объект
Conn
. К тому же этот подход более универсальный. Если вы захотите применять блокировки на уровне базы postgresql, то у вас не будет зависимости в очередности применения транзакции и блокировки. У нас произошла подобная проблема, в статье я описал что у нас произошло и как мы её решили.Ещё спорным является решение положить в
Context
объектTransaction
. Вы сами писали:И тут я с вами полностью согласен. Такие вещи как RequestID, данные запроса для хендлера(как делает роутер gorilla/mux) или телеметрия идеально подходят для хранения в контексте.
Объект транзакции это скорее "сервис-абстракция", он предоставляет свои методы, в данном случае, для выполнения SQL-запрсов. И уж точно он(объект транзакции) не является "утилитарными данными" :) К тому же вы не контролируете кто владеет транзакцией. Так код, что находится по дереву выполнения ниже может закрыть транзакцию, а она доступна всем по дереву выполнения выше из контекста, сущности выше могут попытаться выполнить SQL-запрос на уже закрытой транзакции. Кейс странный, но это просто ещё одно для возникновения ошибок. В своей статье я описал, как мы используем шаринг соединения используя
Context
как ключ доступа к сущностям. За получение транзакции черезContext
отвечает одна сущность. Решаем ту же проблему не складывая вContext
сервисы :) Так можноContext
ненароком превратитьServiceProvider
в рамках запроса :)Последнее и очень спорное:
Context
в домене. Объект Context в своём поле Value переносит неизвестную домену сущностьinterface{}
и получается домен начинает зависеть от неопределенной сущности, а значит нарушается целостность домена! К тому же, зачем домену как-то зависеть такой сущности какContext
, которая ему ни к чему?Прошу прощения, но немного не понял чем здесь могут помочь процедуры
В GO у объекта базы данных есть метод BeginTx, который начинает тразакцию им вовзращает объект транзакции, через который можно выполнять SQL-запросы и они будут выполнятся на том же соединении, где начата транзакция.
Нам нужно было именно отвязать компонент от объекта транзакции. По сути компоненту неважно выполняет он свои запросы в рамках уже начатой транзакции или открывает новое соединение. За обеспечение атомарности и консистентности у нас отвечает слой Application, переиспользовать подключение нужно сущностям инфраструктуры.
Мы же через наш пул шарим объект соединения, на котором можем и транзакцию стартовать и несколько блокировок накатить. Сущности на уровне инфраструктуры будут просто использовать то же самое соединение, используя Context, который им передаётся по ходу выполнения кода.
Простой пример, где нам это пригодилось, если не очень углубляться во внутреннюю кухню :) - это TransactionalOutbox при синхронной обработке доменого события, как я писал в комментарии выше
Не совсем) Наша модель работает по механизму ревизий(каждый запрос изменяет ревизию) нам нужна блокировка, чтобы одномоментно один запрос мог изменить модель и выставилась новая ревизия для модели - другие запросы со сторой ревизией просто подождут снятия блокировки и получат ошибку при внесении изменений в модель со старой ревизией :) При Repeatable Read такого бы не получилось :)
Ещё необходимо было разработать универсальный механизм применимый в других наших микросервисах. А там мы в основном используем именованные блокировки, преимущественно для полной блокировки агрегата внутри доменной модели )
Отдельный коннекшн для лока это была как раз проблема, которую мы решили.
Это скорее абстакция для уровня Application. Наша uow никакие изменения модели ненакапливает, за нас это делает механизм транзакций в базе.
Это уже выглядит как задача фреймворка ORM, который отслеживает изменения модели. В GO не очень популярны ORM(всё извлекается и записывается прямыми SQL-запросами) и мы тоже решили не использовать. Механизма транзакций базы данных нам хватает)
В языке GO в стандартной библиотеке присутсвует пуллинг соединений к базе. При этом при обращении к этому пулу мы можем только получить новое соединение либо выполнить SQL-запрос на рандомном соединении из пула.
У нас же была необходимость выполнять SQL-запросы на одном соединении в разных независищих частях сервиса, чтобы любой компонент мог иметь возможность работать уже раннее созданном соединении или открывать новое. К примеру, в рамках одного запроса мы можем выполнить запросы к базе в независимых частях сервиса: сохранение сущностей из домена в базу в репозитории и в синхронном обработчике доменных событий - везде мы должны выполнить запросы на одном соединении. Так. в обработчике доменного события мы хотим применить TransactionalOutbox и записать сериализованное доменное событие в базу на той же транзакции, что и сохраняли сущности в репохитории.
Именно поэтому нам нужен собственный пул - переиспользовать соединения, чтобы компоненты, которым нужно соединение к базе, могли переиспользовать текущее соединение с базой по нашим правилам: предоставив контекст как ключ для доступа к соединению.
Благодаря контекстам, которые живут в рамках запроса, и пулу - мы предоставляем доступ к текущему соединению на запросе