Pull to refresh

Comments 29

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

Добрый день, знаю об этой в ситуации. Это не сыграет значимой роли в рамках нашего проекта без колоссальных нагрузок.

В ситуации, если нагрузка будет большой, описанную Вами проблему можно решить добавлением бóльшего числа коннектов, которое зависит от количества реплик sso.

Если есть предложения по улучшению решения, с удовольствием выслушаю.

Как можно улучшить? Ну это зависит от контекста (с) :)


1. Первое что приходит в голову - вернуть все же Redis :)


2. Можно изменения в словарях собственно отправлять в брокер какой-то вроде Kafka, RabbitMQ etc и массив сервисов будет уже его слушать


3. Ну и если хочестся ручной привод: можно выделить роль листенера ограниченному подмножеству инстансов с последующим броадкастом на обновления всем остальным (NATS тот же завести под капот, он очень легковесный).

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

Как так вышло, что на большом количестве микросервисов мы получили подключение к одной БД?

Имели ввиду реплики sso в кубере)

добрый день! а стоило ли реализовывать свое решение, когда на просторах гитхаба можно найти patrickmn/go-cache ?

Добрый, я бы сказал, что собственное решение в базовом случае простó в реализации и крайне гибко для дальнейшего улучшения и оптимизаций в будущим под нагрузку)

Лезть в БД диском на каждый чих

в pg (да и в любой более-менее развитой СУБД) есть буферный кеш в котором лежат часто используемые страницы. Так что вы не ходите на каждый чих на диск.

Гарантированную консистентность (кэш обновляется сразу после коммита транзакции в БД).

гарантированная консистентность у вас будет, если у вас notify "вдруг" станет синхронным, будет в нём механизм ретраев чтобы обрабатывать ошибки нотификации и т.д.

1. Про «диск»:
Вы правы, это упрощение. Речь не о физическом диске, а о накладных расходах сетевого взаимодействия и парсинга SQL. Даже из кеша буферов Postgres получение данных требует сетевого обмена и десериализации (десятые доли мс). Чтение же из локальной map в памяти Go — это наносекунды. Экономия именно на устранении межпроцессного взаимодействия.

2. Про «гарантированную консистентность»:
Абсолютно верно, здесь я переоценил формулировку. NOTIFY — асинхронный механизм, он не даёт гарантий доставки, как очередь. Это очень быстрая eventual consistency. В нашем случае (стабильное соединение, маленькая таблица) потеря сигнала маловероятна и быстро компенсируется, но строгой гарантии нет.

Видится мне что это просто чтобы поставить пункт в резюме….

В этом решение есть одна очень не приятная проблема, а именно, если кто-то подпишется на NOTIFY и не будет вычитывать данные, то данные не отправленные клиенты-подписчику будут накапливаться на сервере и когда объем данных в очереди превысит 8 Гб, то сервер остановится.

Вы правы, это серьёзное ограничение NOTIFY. В нашем сценарии (редкие события, мало данных) риск невелик, но в production обязательно нужны:

  1. Мониторинг состояния подключений в pg_stat_activity.

  2. Таймауты и health-checks в клиенте.

  3. План на случай сбоя: переподключение с принудительным Reload().

Немного смущает пара моментов:

  1. Если NOTIFY зовется FOR EACH ROW, то почему бы не передавать в нем payload с контентом записи, аккуратно порезав по 8KB, если надо.

  2. Если все равно хочется перечитывать таблицу целиком, что бы не сделать FOR EACH STATEMENT?

А мне нравится :) 🌟

Вот бы еще навайбкодить простое демо приложение для всех описанных тестов и положить в тот же докер для студентов очень наглядно.

Непонятен смысл делать путем тригера из БД ивентов. В чем смысл? Админ руками в БД лазит? Я бы по рукам дал. Интерфейс или АПИ и пущай себе конфигурит. А уже админка пусть информирует сервисы что подтянули конфиги путем пуша в брокер.

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

Спасибо за статью.

Таблица маленькая: Вряд ли там будет больше 100 строк (сервисов в экосистеме не тысячи).

Читается постоянно: Почти каждый запрос на аутентификацию требует проверки: "А есть ли такое приложение? А какие у него права?".

А чем не устроило собственное решение на sync.Map?

Из преимуществ вижу только отсутствие затрат на первичный прогрев кэша ввиду безопасного хранения данных (а насколько он нужен). Но в тоже время, дополнительный поднятый бекенд для psql, если не через пул работаем. А если через пул, то как будто бы все равно вечно поднятый Коннект (к тому же и постоянно занимающийся с базой, как понял из статьи), не то что бы способствует улучшению пропускной способности psql в целом.

Возможно, криво статью прочитал.

В итоге один и тот же кеш лежит в нескольких подах, т.е. расход памяти = X * Y. А потом еще рассинхрон, и так далее... Мы тоже сначала делали локальный кеш. И в итоге всё равно перешли на использование Redis, чтобы кеш был общий.

Когда у вас из 6ms времени выполнения запроса в 99.9 перцентиле у вас 5 ms кушает поход в Redis вы задумаетесь о том "а надо ли оно мне" :)
Конечно можно сказать - эй, тут 6 ms, не надо ничего оптимизировать. Но если это один из самых нагруженных сервисов... Ну вы понимаете :)
А проблемы когеренции кэшей решали задолго до микросервисной архитектуры - еще в мультипроцессорных средах.
А память - ну тут классический трейд-офф.

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

Не совсем понял что вы имели ввиду в первом пункте таблицы

Мгновенно (ns). Данные уже в куче Go в нужном формате.

Быстро (~0.5 - 1 ms). Требует сериализации/десериализации (JSON/Protobuf) и похода в сеть.

Т.е у вас бэкенд стоит на одном хосте с Постгресс а Редис на отдельном, и вы сравниваете два этих кейса?

Вы абсолютно правы, в статье сравнение идёт для случая, когда бэкенд и база данных развернуты в одном сегменте сети (например, в одном дата-центре). В такой конфигурации разница действительно измеряется микросекундами против миллисекунд.

Однако ключевой момент в том, что при использовании облачной инфраструктуры или микросервисной архитектуры, где Redis вынесен в отдельный сервис, разница становится ещё ощутимее. Добавляются дополнительные сетевые хопы, что увеличивает задержку (latency). И да, важный фактор — однопоточная модель Redis, которая при высокой конкурентной нагрузке может стать узким местом, в то время как локальная мапа в Go масштабируется на количество ядер.

Ну тогда надо и Редис поставить на тот же хост. И сравнивать. А то сова какая-то получается с глобусом:)

Думаю напишу статью, отдельно с тестированием по данной теме, там будет всё честно)

То что Редис однопоточный - это не значит что он не производительный. Как раз наоборот, это его фишка т.к ненужна синхронизация потоков со всеми вытекающими. Он умеет запускать по потоку на ядро и очень бодро обрабатывать большое количество запросов. Если же его вдруг стало мало то есть KeyDB - распределенный Редис

Спорить на основе предположений — неблагодарное дело. Конкретные цифры под конкретную нагрузку всегда важнее теоретических выкладок.

Я обязательно проведу честные тесты в ближайшее время, сравнив:

  1. In-memory кэш (по схеме из статьи).

  2. Redis на том же хосте (loopback).

  3. Redis в отдельном сетевом сервисе.

Замерю не только чистую latency при разном количестве потоков, но и влияние на общую пропускную способность (RPS) системы.

Привет! Вместо rw лока можно свапать atomic.Pointer на data, если данные обновляются целиком

Привет, классая идея, как-то забыл про этот продход:)
Я как раз планирую провести детальное тестирование разных подходов (теперь включу и этот) и сравнить их производительность в следующей статье. Спасибо! :)

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

Меняется редко: Новые сервисы добавляются не каждый день.

Если сайты добавляются редко, что мешает, сделать простое кеширование на уровне приложения, и вместо того, чтобы слушать события от посгри и пинговать его, просто раз в минуту актуализировать кеш?

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

Sign up to leave a comment.

Articles