1. Жизненный цикл записи: Роль Distributor и Ingester в обеспечении консистентности
Жизненный цикл записи: Роль Distributor и Ingester в обеспечении консистентности
Высоконагруженный production-кластер генерирует сотни тысяч строк логов в секунду. Если база данных попытается синхронно записывать каждую строку на диск, она неминуемо откажет из-за исчерпания лимитов операций ввода-вывода (IOPS). Чтобы переваривать такие объемы, система должна принимать данные максимально быстро, держать их в оперативной памяти и сбрасывать в постоянное хранилище крупными блоками, при этом гарантируя, что при внезапном отключении питания ни одна строка не потеряется. В Loki эта задача решается через микросервисную архитектуру пути записи (write path), где ключевые роли играют два компонента: Distributor и Ingester.
!Архитектура пути записи в Loki
Путь записи начинается, когда агент (например, Promtail) собирает пачку логов и отправляет HTTP POST запрос на эндпоинт /loki/api/v1/push. Этот запрос попадает на балансировщик нагрузки, за которым скрывается флот инстансов Distributor.
Distributor: Валидация и защита кластера
Distributor — это первый рубеж обороны кластера Loki. Это абсолютно stateless (не хранящий состояния) микросервис. Его главная задача — принять входящий трафик, проверить его на соответствие правилам, нарезать на правильные порции и перенаправить нужным Ingester-ам. Отсутствие состояния означает, что вы можете масштабировать Distributor горизонтально (добавлять новые поды или виртуальные машины) в любой момент, просто реагируя на рост входящего трафика.
При получении пакета логов Distributor выполняет несколько этапов валидации.
Во-первых, проверяются метки времени (timestamps). Система отбрасывает логи, которые пришли из слишком далекого прошлого или будущего. За это отвечают параметры конфигурации, определяющие границы окна приема. Исторически Loki требовал строгой хронологической последовательности логов внутри одного потока (stream). Если строка приходила с меткой времени старше, чем у уже записанной строки в том же потоке, она отбрасывалась с ошибкой entry out of order. В современных версиях Loki этот механизм смягчен: система умеет принимать логи вне порядка (out-of-order), но в пределах заданного временного окна, что критически важно для распределенных систем, где часы на разных серверах могут быть слегка рассинхронизированы.
Во-вторых, Distributor применяет лимиты (Rate Limiting). Поскольку Loki изначально проектировался как multi-tenant (многоарендная) система, где логи разных команд или клиентов хранятся в одном кластере, необходимо защищать ресурсы от «шумных соседей». Distributor проверяет размер входящего запроса (в байтах и строках) и сравнивает его с квотами конкретного тенанта. Если лимит превышен, Distributor немедленно возвращает ошибку HTTP 429 (Too Many Requests), заставляя агента сбора применить механизм экспоненциальной задержки (exponential backoff) и повторить попытку позже.
Маршрутизация и Hash Ring
После успешной валидации Distributor должен решить, куда отправить данные. В кластере работают десятки Ingester-ов, хранящих потоки в оперативной памяти. Крайне важно, чтобы все строки логов, принадлежащие одному потоку (имеющие идентичный набор лейблов), всегда попадали на одни и те же Ingester-ы. Если распределять логи одного потока случайным образом, при выполнении запроса системе придется опрашивать абсолютно все узлы кластера, что уничтожит производительность.
Для решения этой задачи Distributor использует механизм консистентного хеширования (Consistent Hashing) и структуру данных, называемую Hash Ring (хэш-кольцо).
!Перераспределение потоков в Hash Ring при добавлении узла
Алгоритм маршрутизации работает следующим образом:
{app="frontend", env="prod", cluster="us-east-1"}).Математически позиция потока на кольце вычисляется по формуле:
Где — позиция на кольце (целое число от 0 до ), — криптографическая хеш-функция (в Loki используется алгоритм fnv32a), — отсортированная строка, представляющая все лейблы потока, а — операция взятия остатка от деления, замыкающая пространство значений в кольцо.
Консистентное хеширование решает проблему масштабирования. Если мы добавим новый Ingester в кластер, он займет свое место на кольце и «заберет» на себя только часть потоков от соседнего узла. Большинство потоков останутся на своих старых местах. Это кардинально отличается от простого деления по модулю на количество серверов, при котором изменение числа серверов привело бы к перераспределению 100% данных.
Репликация и кворум записи
Хранение данных исключительно в оперативной памяти одного Ingester-а — это прямой путь к потере логов при сбое оборудования (например, при Kernel Panic или отключении питания сервера). Чтобы обеспечить отказоустойчивость, Loki использует концепцию фактора репликации (Replication Factor, RF), заимствованную из архитектуры Dynamo.
По умолчанию в production-инсталляциях фактор репликации равен 3 (). Это означает, что Distributor, определив по хэш-кольцу первый Ingester для потока, продолжает движение по кольцу и выбирает еще два следующих уникальных Ingester-а. Входящая порция логов отправляется на все три узла параллельно.
Однако Distributor не ждет ответа от всех трех узлов. Для подтверждения успешной записи агенту (Promtail) достаточно получить ответы от кворума узлов. Размер кворума записи вычисляется по формуле:
Где — минимальное количество успешных ответов от Ingester-ов (Write Quorum), — фактор репликации, а — математическая операция округления вниз до ближайшего целого числа.
Для стандартного :
!Динамика кворума записи: параллельная отправка и ожидание ответов
Это означает, что Distributor отправляет данные на три Ingester-а и ждет. Как только два из них ответят «Успешно» (HTTP 200 OK), Distributor немедленно возвращает успешный ответ (HTTP 204 No Content) агенту Promtail. Третий Ingester может ответить позже, или его ответ может потеряться в сети, или сам узел может быть в этот момент перезагружен — это не повлияет на успешность операции с точки зрения клиента.
Такой подход обеспечивает идеальный баланс между надежностью и задержкой (latency). Кластер с способен пережить полное падение одного любого Ingester-а без деградации приема данных.
Рассмотрим сценарии отказов при и :
positions.yaml и через некоторое время повторяет попытку отправки этой же пачки логов.Ingester: Прием и буферизация
Пока Distributor занимается маршрутизацией и кворумами, Ingester выполняет тяжелую физическую работу. В отличие от Distributor, Ingester — это stateful (хранящий состояние) компонент. Его состояние — это гигабайты логов, находящиеся в оперативной памяти и еще не записанные в постоянное объектное хранилище (например, AWS S3).
Когда Ingester получает запрос на запись от Distributor, внутри него происходит следующий процесс:
Здесь кроется важный архитектурный компромисс. Ingester подтверждает запись до того, как данные физически окажутся на жестком диске или в S3. Скорость работы памяти позволяет кластеру Loki принимать миллионы строк в секунду, но создает риск потери данных. Если Ingester внезапно будет обесточен, все активные чанки в его оперативной памяти исчезнут.
Именно поэтому кворум записи () критически важен. Если один Ingester сгорел вместе со своей оперативной памятью, те же самые строки логов гарантированно находятся в оперативной памяти как минимум одного другого Ingester-а (а в идеале — двух). Когда придет время выполнять поиск по логам, компонент Querier (отвечающий за чтение) опросит все Ingester-ы, соберет дублирующиеся данные, выполнит дедупликацию и вернет пользователю полный результат.
Согласованность данных и CAP-теорема
В контексте CAP-теоремы (Consistency, Availability, Partition tolerance), архитектура пути записи Loki делает явный перекос в сторону доступности (Availability) и устойчивости к разделению (Partition tolerance). Это классическая система с конечной согласованностью (Eventual Consistency).
В традиционных реляционных базах данных с транзакционной семантикой (ACID), запись считается успешной только тогда, когда данные надежно зафиксированы на диске (часто с использованием Write-Ahead Log и fsync). Это гарантирует строгую консистентность, но сильно ограничивает пропускную способность.
Loki жертвует строгой консистентностью в моменте ради колоссальной скорости приема. Согласованность достигается на уровне кластера за счет избыточности (репликации в памяти разных узлов) и умного механизма чтения, который умеет склеивать фрагментированные данные.
Жизненный цикл записи на этом этапе завершается с точки зрения клиента (агента сбора логов) — он получил подтверждение и перешел к чтению следующих файлов. Однако для самого кластера работа только начинается. Данные безопасно лежат в оперативной памяти распределенных Ingester-ов, но память не бесконечна. Потоки буферизируются, сжимаются в чанки, и при достижении определенных лимитов по времени или размеру запускается процесс фиксации (flushing). Кроме того, для дополнительной защиты от одновременного падения нескольких узлов, современные версии Loki используют механизм локального журналирования на диск внутри самого Ingester-а.