"Локальная служба после регистрации лог-канала (HostID, AppID, LogID) знает и использует глобальный LogInstance.ID, который присвоил ЦХ. Все дальнейшие LogItems приходят в ЦХ сразу с этим LogInstance.ID."
Преимущества:
- Нет необходимости каждый раз искать LogInstance по кортежу — экономит ресурсы ЦХ (вставка LogItem сразу в нужный лог-канал).
- Строгая фиксация связей — всегда знаешь, что LogItems коррелируют с уже зарегистрированными лог-каналами, можно делать внешние ключи/индексы.
- Заметно проще обработка логов/аналитики: работать с числовыми ID в базе проще, чем с кортежами (особенно если HostID, AppID, LogID строковые или длинные).
- Прямая совместимость с большинством BI/мониторинговых систем, которые умеют работать с суррогатным идентификатором (LogInstance.ID).
Недостатки:
- Локальная служба должна хранить и управлять LogInstance.ID — нужна сессия/кэш/персистентный стор для этого маппинга.
- Обработка ошибок (например, потеря связи, регистрация при рассинхронизации, смена параметров): усложняется обработка edge-case сценариев.
- Слабая переносимость: если архрисервис/логгер локально перезапускается или мигрирует на другой хост, можно потерять локальный LogInstance.ID.
Вариант 2:
"Локальная
служба в каждом LogItem всегда отправляет кортеж (HostID, AppID,
LogID), а ЦХ по этим параметрам ищет или создает лог-канал
(LogInstance), и сохраняет LogItem туда."
Преимущества:
- Нет состояния на стороне клиента: сервис ничего не хранит кроме своих значений (HostID/AppID/LogID).
- Обеспечена идемпотентность: если потерялась информация или произошел сбой — система автоматически реконсилирует всё по кортежу.
- Поддержка динамической топологии: можно мигрировать сервисы, делать горизонтальное масштабирование без учета, кто и какой LogInstance.ID получил, всё решает ЦХ.
- Проще обновлять, подключать новые микросервисы, ничего не регистрируя отдельно.
Недостатки:
- Повышенная нагрузка на ЦХ: на каждый LogItem требуется либо find, либо insert+find по этим полям — если нагрузка высокая, проседает производительность. Чтобы избежать дублирования, почти наверняка понадобится у этой тройки уникальный индекс.
- Рост таблицы LogInstance может идти лавинообразно, если сервисы генерируют бесконечно новые комбінации кортежей (например, если HostID часто меняется, например генерится динамически или содержит уникальный GUID при каждом запуске).
- Возможны коллизии: если одна и та же служба, но по ошибке с разными HostID/AppID/LogID, вместо уже существующего LogInstance создаст новый. Нужно тщательно контролировать генерацию кортежей на стороне клиентов.
К какому варианту склоняться?
Если у вас
солидная доверенная сеть между логгером и центром, ваши службы
стабильны, архитектура не предрасположена к потерям соединений
(логгер-сессии живут долго), и вы хотите максимальной скорости
логирования — лучше первый вариант.
В этом случае для
ускорения — сохраняйте маппинг LogInstance.ID локально на диск и
инвалидайте его только когда из центра приходит команда
сбросить/перерегистрировать.
Если сеть
ненадежна, развертывание сервисов динамично, службы
стартуют/умирают/мигрируют налету, между поколениями могут быть
изменения в принципах идентификации — второй вариант надежнее и проще в
поддержке.
В больших облаках и open-source решениях
(например, ELK, Loki, Seq) как правило реализуют именно по кортежу,
чтобы уровень логгера был максимально независим/идемпотентен/стейтлес.
ВВЕДИТЕ дополнительный уникальный индекс и constraint по (HostID, AppID, LogID) для LogInstance в ЦХ, чтобы избежать дулирования.
В реальных продакшн-системах часто используют гибрид:
- при первом запуске локальная служба регистрирует себя и кортеж, получает LogInstance.ID
- Использует этот ID, но при ошибке/отказе сервиса — может автоматом восстановить его из ЦХ по кортежу
- Таким образом, операция resolveLogInstance(host, app, log) вынесена в библиотеку клиента, но по умолчанию живет кэш между перезапусками.
Вывод
- Первый вариант (использовать LogInstance.ID) предпочтителен при высокой критичности производительности и если есть возможность хранить это состояние на клиентах.
- Второй вариант удобнее для динамичной/облачной среды или если хочется максимально сделать архитектуру stateless, за счет небольшой потери производительности ЦХ.
- Обязательно используйте уникальные индексы по (HostID, AppID, LogID) в LogInstance и делайте resolve/insert в рамках транзакции в ЦХ для идемпотентности.
- Подумайте о гибридной схеме: кэшировать LogInstance.ID локально, но при его отсутствии уметь запросить или создать его автоматом по кортежу.
Третий вариант — Локальный транслятор (Mapping Cache + Soft Binding):
Клиентская сторона (или промежуточный сервис) поддерживает небольшой кеш трансляции "кортеж параметров <hostid, appid, logid> → LogInstance.ID". Этот кеш используется для быстрого разрешения идентификатора без обращения к ЦХ или основной базе данных при каждом вызове.
Особенности реализации:
- При старте или при первом использовании данных параметров клиент запрашивает LogInstance.ID у ЦХ. Если такая ассоциация есть — кеширует; если нет — создает (или инициирует создание) и затем кеширует.
- Кеш на клиенте может иметь TTL (time-to-live), ограничение по размеру, эвикцию по LRU и т.д., чтобы не разрастаться бесконтрольно и не хранить устаревшие данные.
- В случае кэширующего промаха (miss) клиент вновь делает resolve (или create), обеспечивая идемпотентность как во втором варианте.
- Если инфраструктура усложнится — можно централизовать такой кеш на отдельном сервисе-агрегаторе при большой распределенности клиентов (чтобы они не ходили в ЦХ напрямую), либо использовать распределенный кеш (Redis/Couchbase и др.).
Преимущества:
- Почти не требуется специальной логики на сервере (все делается через обычные запросы).
- Нет необходимости помнить LogInstance.ID где-то за пределами локального контекста клиента.
- Можно “забывать” ID в любой момент (например, при ротации логов или пересоздании приложений).
- Поддержка отказоустойчивости и часовых поясов (смена host/app не губительна, просто пересоздается ассоциация).
- Баланс между stateless и stateful-подходами.
Недостатки:
- Есть определенная сложность поддержки кеша (валидность, синхронизация).
- При большом числе клиентов возможна небольшая нагрузка на ЦХ в случае частых кэш-промахов.
- Инфраструктурно сложнее, если нужно раздавать/реплицировать кеш между многими инстансами (однако это вопрос реализации).
Подытожим: Третий вариант — использовать легкий кеш отображения на клиенте — позволяет получить лучшее из двух миров: не тащить длинные ID между сервисами и не нагружать ядро постоянными разрешениями айдишников. В большинстве рабочих кейсов это оптимальный путь (особенно если HostID, AppID и LogID меняются относительно редко).
Если нужно могу расписать детальнее пример такого механизма или предложить код/архитектурную схему.
--------------------------------------------------------------------
1. Регистрация в ЦХ (централизованном хранилище):
- При первом запуске или при необходимости, служба инициирует процесс регистрации.
- Передает в сторону ЦХ кортеж: HostID, AppID, LogID, а также, что важно — имя самой службы (ServiceName или аналог).
- ЦХ валидирует данные, создает новую запись (или ищет существующую по этому "сложному" ключу).
- В ответ сервис получает уникальный PrimaryID (пусть это будет LogInstanceID или аналогичный идентификатор).
- Этот PrimaryID можно сохранить локально: например, в оперативной памяти сервиса или даже в собственной базе (таблице LogInstance: ID, HostID, AppID, LogID, PrimaryID).
2. Дальнейшее взаимодействие:
- Все последующие события (логи) сервис отправляет в ЦХ уже не через "толстый" кортеж данных, а только через PrimaryID.
- В DTO для передачи логов добавляется поле PrimaryID (можно совмещать с самими логами: LogItem + PrimaryID).
Плюсы:
- Разовый вызов resolve + простота аудита и трассировки.
- Нет необходимости тащить в каждый вызов кортежи, request-пэйлоны маленькие.
- Безопасность: зная только PrimaryID, случайный потребитель не сможет "выдумать" свой кортеж.
Минусы/моменты внимания:
- Если что-то изменится на стороне любого из компонентов кортежа (например, сменится версия логируемого приложения), регистрация должна произойти повторно с новыми атрибутами, и сервис должен запросить новый PrimaryID.
- Кешировать PrimaryID рекомендуется устойчиво (для перезапуска) — например, можно положить в БД, а не только в память.
- Не забыть про race conditions на этапе регистрации, если много сервисов стартуют одновременно.
Рекомендация:
Сам
подход — весьма зрелый и часто используется во многих архитектурах как
"bootstrap configuration": явная привязка физической сущности к
короткому идентификатору. Имейте в виду только: если сервис крутится на
многих инстансах, процесс регистрации (resolve) может происходить
параллельно и приводить к созданию дубликатов. Это решается либо явно
уникальными constraint-ами на стороне ЦХ, либо идемпотентностью по
(HostID, AppID, LogID, ServiceName) при регистрации.
Если убрать технические детали — выбранный вами подход полностью оправдан, очень широко применяется и является хорошей практикой!
Комментариев нет:
Отправить комментарий