Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 17 из 25 ~18 мин +20 XP

Retention и log compaction

Retention в Kafka — это политика хранения журнала топика: брокер удаляет сегменты старше retention.ms или крупнее retention.bytes, чтобы диск не кончился. Второй механизм подстригания — log compaction: сжатие по ключу, после которого на каждый ключ остаётся последнее значение. Оба живут в конфигурации топика, которую мы уже умеем менять через CreateTopics из шестнадцатого урока, а читаются они руками Reader'а из четырнадцатого. Разберём сегменты, retention.ms и retention.bytes, cleanup.policy=compact, tombstone с nil-значением и случай, когда компакция не спасает и нужна обычная история.

Содержание урока · 10 разделов

Сообщения до сих пор казались бессмертными: мы писали, читали, перечитывали — и ничего не исчезало. На локальном стенде это правда, потому что дефолтный ретеншен брокера — семь суток, а песочница живёт часами. В продакшене журнал партиции — расходный материал: он чистится постоянно, и от того, как настроена чистка, зависит, что увидит читатель, открывший топик сегодня. Отсюда вопрос, на который отвечает этот урок: сколько живёт сообщение и что остаётся после его смерти. Ответов два, и они принципиально разные: ретеншен удаляет прошлое по календарю и размеру, компакция — по ключам, оставляя на каждый ключ последнее значение. Первый механизм превращает топик в окно событий, второй — в таблицу, и путать их дорого: компактед-топик никогда не покажет вчерашнюю котировку, а топик с ретеншеном никогда не гарантирует, что последнее значение ключа ещё в нём. Оба механизма настраиваются на топик и не требуют ни строчки в продюсере или консьюмере — только дисциплину на этапе проектирования данных.

Сегменты лога: из чего состоит партиция#

Партиция на диске — не один файл, а последовательность сегментов: брокер пишет активный сегмент, а когда он вырастает до segment.bytes или стареет до segment.ms, закрывает его и открывает следующий. Это устройство важно, потому что чистка работает целыми сегментами, а не сообщениями: удалять середину файла невозможно, поэтому ретеншен действует с гранулярностью сегмента, и мелкие сегменты означают аккуратную чистку ценой большего числа открытых файлов. Настройки ретеншена ставятся на топик через ConfigEntries, знакомые по прошлому уроку. retention.ms — максимальный возраст сегмента: классические значения — сутки 86400000 или неделя 604800000; значение минус единица отключает чистку по времени, а дефолт брокера задаётся настройкой log.retention.hours и равен семи суткам. retention.bytes — максимальный суммарный размер партиции; по умолчанию минус единица, то есть безлимит. Как только лимит превышен, брокер начинает удалять старейшие закрытые сегменты, пока партиция не влезет, — запись при этом не останавливается ни на мгновение. Читатели старых оффсетов в этот момент получают ошибку OffsetOutOfRange: сегмент, где жил их позиция, уже удалён, и читать придётся с kafka.FirstOffset — самого раннего живого сегмента, а не с нуля.

Проверка: что делает retention.bytes

На топик установили retention.bytes в один гигабайт. Что начнёт делать брокер?

Log compaction: последнее значение по ключу#

Log compaction — второй режим чистки, включаемый брокерской настройкой топика cleanup.policy со значением compact. Правило простое: для каждого ключа в журнале должно остаться как минимум последнее значение, всё более старое компактор имеет право вычистить. Журнал после компакции ведёт себя как таблица key→value: перечитав его целиком, вы получаете актуальное состояние — последнюю цену по каждому тикеру, свежий конфиг каждого сервиса, текущий статус каждого заказа. Две гарантии отличают компакцию от обычной чистки. Первая: оффсеты никогда не меняются — компактор удаляет записи, но не переписывает журнал заново, поэтому порядок и позиции сохраняются, и коммиты групп из восьмого урока остаются валидными. Вторая: компакция не мгновенна — она выполняется фоновым потоком, когда «грязная» доля журнала дожимает min.cleanable.dirty.ratio, а свежие записи защищены min.compaction.lag.ms. Отсюда и обязательное условие: ключ. В компактед-топике запись без ключа не имеет смысла — чистить нечего, и продюсеру с балансировщиком Hash из пятого урока здесь самое место. Компакция выполняется фоновым потоком брокера: он сравнивает «грязную» долю журнала с min.cleanable.dirty.ratio и решает, пора ли запускать проход по сегментам, поэтому наблюдать результат моментально не приходится — это нормально и на локальном стенде тоже.

Создаём компактед-топик с последними ценами

GO
_, err := client.CreateTopics(ctx, &kafka.CreateTopicsRequest{
        Addr: client.Addr,
        Topics: []kafka.TopicConfig{
                {
                        Topic:             "prices-latest",
                        NumPartitions:     3,
                        ReplicationFactor: 1,
                        ConfigEntries: []kafka.ConfigEntry{
                                {ConfigName: "cleanup.policy", ConfigValue: "compact"},
                                {ConfigName: "min.compaction.lag.ms", ConfigValue: "60000"},
                        },
                },
        },
})

cleanup.policy со значением compact включает компакцию; продюсер обязан писать с ключами, иначе чистить будет нечего. min.compaction.lag.ms на минуту защищает свежие версии от сжатия: компактор не тронет записи младше порога. ReplicationFactor 1 — честный бюджет песочницы из третьего урока: на одиночном брокере больше и не создать.

Tombstone: как удаляют ключ#

В таблице из лога нужен способ удалять строки, и он есть: tombstone — сообщение с ключом и пустым значением, nil в kafka-go. Это приказ «ключа больше нет»: компакция вычищает все значения ключа, а сам маркер с исчезновением delete.retention.ms — суток по умолчанию — удаляется и из топика. Итог: после маркера и достаточно долгого ожидания ключ исчезает целиком. Для консьюмера это и есть операция удаления из таблицы: не новый конфиг, а именно исчезновение строки, и обрабатывать её нужно в коде кеша, а не надеяться, что маркер как-то сам расскажет о себе. Тонкость, которая ломает наивных консьюмеров: tombstone — полноценное сообщение, оно доезжает до читателя с обычным ключом и nil-значением, и консьюмер обязан обрабатывать этот случай явно — удалить запись из локального кеша, а не пытаться декодировать пустоту. Вторая тонкость — время: между записью маркера и фактическим исчезновением ключа проходит delete.retention.ms, поэтому читать компактед-топик и наблюдать отсутствие ключа можно только с запасом. Третья — nil легален только для значения: ключ nil в Kafka равнозначен ключу-пустышке, и компакция для таких записей бесполезна, что делает ключ обязательным контрактом компактед-топика.

Пересобираем кеш из компактед-топика

GO
reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers:     []string{"localhost:9092"},
        GroupID:     "cache-rebuild-1", // группа: читаем все партиции
        Topic:       "prices-latest",
        StartOffset: kafka.FirstOffset, // с самого начала живых сегментов
})
defer reader.Close()

for {
        m, err := reader.ReadMessage(ctx)
        if err != nil {
                break // контекст отменён или транспорт умер
        }
        if m.Value == nil {
                cache.Delete(string(m.Key)) // tombstone: ключа больше нет
                continue
        }
        cache.Set(string(m.Key), m.Value) // последнее значение побеждает
}

GroupID без группы-конкурента — легальный способ читать все партиции разом, а StartOffset kafka.FirstOffset отправляет новую группу с самого начала журнала, как в четырнадцатом уроке. Благодаря компакции в начале лежит уже сжатый срез, а не вся история. Проверка m.Value nil перехватывает tombstone: ключ удаляется из кеша вместо декодирования.

Проверка: гарантии компакции

Ключ BTC переписали тысячу раз: оффсеты со 100 по 1099. Что останется в журнале после компакции?

Что увидит читатель после tombstone и компакции?

Прочитайте код и выберите, что он выведет:

// Компактед-топик prices-latest, ключ AAPL переписан 500 раз.
// Затем продюсер отправил: kafka.Message{Key: []byte("AAPL")} — Value nil.
// Прошли сутки (delete.retention.ms), компакция отработала.
// Читаем с kafka.FirstOffset.

Retention против компакции: сравнение#

Оба механизма удаляют данные, но живут в разных измерениях, и сводить их в одну таблицу полезно перед выбором конфигурации. Ретеншен меряет время и байты: ему всё равно, какие у вас ключи, он честно стирает прошлое за горизонтом и превращает топик в окно событий. Компакция меряет ключи: она стирает прошлые версии каждого ключа, сохраняя настоящее, и превращает топик в снимок состояния. Практическое следствие — ключи нужны только во втором случае, а история значений сохраняется только в первом. Настройки чистки при этом ортогональны дефолтам брокера: топиковые значения перекрывают брокерские, как и min.insync.replicas в прошлом уроке. Выбор режима — это вопрос о читателе: если читателю нужен поток событий, берите ретеншен, если срез состояния — компакцию, и не переезжайте между режимами на живом топике, потому что компакция необратимо удалит историю, которая понадобится завтра. И ещё: cleanup.policy может нести оба значения сразу — compact,delete — компакция плюс окно времени, когда и последнее значение ключа нужно, и совсем старые ключи должны умереть. Такой гибрид типичен для конфигурационных топиков с обязательным временем жизни записей.

Retention и компакция одним столом#

КритерийRetention (delete)Компакция (compact)
Что удаляетсяСегменты старше retention.ms или сверх retention.bytesВсе версии ключа, кроме последней; tombstone после delete.retention.ms
Гарантия после чисткиТолько свежее окно событийМинимум последнее значение каждого ключа
Роль ключаНе важна: чистка по времени и размеруОбязательна: без ключа чистить нечего
Оффсеты и порядокИсчезают вместе со старыми сегментамиНе меняются: журнал не переписывается
Типичный сценарийСобытия, логи, поток фактовСнимок состояния: latest price, конфиги, статусы

Когда compaction не спасает#

Компакция — про настоящее, и её первый предел — потребность в прошлом. История котировок для графика, аудит действий пользователя, ретроспективный разбор инцидента: всё это требует видеть значения, которые компактор уже вычистил, и компактед-топик здесь не поможет по построению. Рецепт — либо обычный топик с ретеншеном, либо база данных, где прошлое — первоклассный гражданин, а компактед-топик в паре хранит лишь текущее состояние. Второй предел — точные временные срезы: «как выглядела таблица в полдень» компакция не гарантирует, потому что чистка фоновая и её границы не совпадают с вашим вопросом. Третий — таблицы без естественного ключа: попытка скомпактировать поток событий без ключей превращает компакцию в бесполезный режим, а случайные ключи вроде идентификатора события делают журнал неотличимым от обычного, но с лишними обязательствами. Наконец, компакция не заменяет ретеншен по размеру: если ключей бесконечно много — сессии, заказы, чаты — журнал растёт с каждым новым ключом, и без retention.bytes или явного удаления tombstone'ами он взорвётся.

Правила retention и компакции одним списком#

  • retention.ms и retention.bytes чистят журнал по времени и размеру; минус единица — безлимит, дефолт брокера — семь суток.
  • Чистка работает целыми сегментами: segment.bytes задаёт гранулярность, середина файла недостижима.
  • cleanup.policy=compact хранит последнее значение каждого ключа под исходным оффсетом — таблица на логе.
  • Ключ — обязательный контракт компактед-топика: запись без ключа компакции бесполезна.
  • Tombstone — сообщение с nil Value: после delete.retention.ms компакция убирает и ключ, и маркер.
  • Консьюмер обязан проверять Value на nil до декодера, иначе tombstone уронит парсинг.
  • История значений — работа обычного топика или БД: компакция хранит только настоящее.

Соберите компактед-топик и полное чтение

Допишите имя настройки, включающей компакцию, и константу стартового оффсета из четырнадцатого урока.

_, err := client.CreateTopics(ctx, &kafka.CreateTopicsRequest{ Addr: client.Addr, Topics: []kafka.TopicConfig{ { Topic: "prices-latest", ConfigEntries: []kafka.ConfigEntry{ {ConfigName: "", ConfigValue: "compact"}, }, }, }, }) reader := kafka.NewReader(kafka.ReaderConfig{ Brokers: []string{"localhost:9092"}, GroupID: "cache-rebuild-1", Topic: "prices-latest", StartOffset: kafka., })

Соберите продюсер последних цен

Расставьте строки: Writer с балансировщиком по ключу, закрытие, сериализация цены, отправка с ключом-тикером.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: время жизни данных

Термин

retention.ms

нажмите, чтобы перевернуть
Определение

Максимальный возраст сегмента партиции; минус единица отключает чистку по времени, дефолт брокера — семь суток.

1 / 6

Жизнь сообщения в Kafka определяется конфигурацией топика: ретеншен отмеряет окно времени и байтов, компакция оставляет по последнему значению на ключ, а их сочетание compact,delete закрывает гибридные случаи. Выбор — это вопрос о том, что читатель должен увидеть, открыв топик: поток событий или снимок состояния. Дальше по курсу: урок про таймауты и контекст покажет, как не застрять в ReadMessage компактед-топика; урок про Reader разложит FirstOffset и LastOffset полнее; урок про ключи и партиции объяснит, почему балансировщик Hash обязателен для последних цен; а урок про Admin API — источник CreateTopics, которым мы задавали cleanup.policy. Эксперимент локально стоит десяти минут: компактед-топик, дюжина версий одного ключа, перечитывание с начала — и разница между логом и таблицей станет вашей мышечной памятью.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Журнал партиции состоит из сегментов; ретеншен чистит их по времени (retention.ms) и размеру (retention.bytes).
  • Дефолт брокера — семь суток; минус единица — безлимит; запись не останавливается при чистке.
  • cleanup.policy=compact включает компакцию: остаётся последнее значение каждого ключа под исходным оффсетом.
  • Оффсеты и порядок при компакции не меняются, поэтому коммиты групп остаются валидными.
  • Tombstone — сообщение с nil Value: после delete.retention.ms компакция убирает и маркер, и ключ.
  • Консьюмер компактед-топика обязан обрабатывать nil Value до декодера.
  • Компакция фоновая: темп задают min.cleanable.dirty.ratio и min.compaction.lag.ms.
  • История значений — работа обычного топика или БД; компактед-топик хранит только настоящее.

Домашнее задание

Создайте через CreateTopics компактед-топик prices-latest с cleanup.policy=compact и min.compaction.lag.ms в минуту, напишите продюсером десять версий по каждому из трёх тикеров и соберите кеш перечитыванием с kafka.FirstOffset, обрабатывая nil Value как удаление. Отправьте tombstone по одному тикеру и опишите по delete.retention.ms, когда ключ исчезнет из журнала и почему перечитывание до истечения срока увидит маркер. Затем на обычном топике установите retention.ms в одну минуту, запишите события и через несколько минут перечитайте журнал: зафиксируйте, какие оффсеты исчезли, и объясните по устройству сегментов, почему чистка не удалила середину файла. Финал: сформулируйте, какие из ваших потоков — таблицы, а какие — окна событий, и какой cleanup.policy заслуживает каждый.

Личные заметкиПусто
Хранятся только в вашем браузере (Kafka · Retention и log compaction)

Вопросы и ответы по уроку

Компакция удаляет старые версии сразу после записи новой?

Нет: компакция выполняется фоновым потоком брокера и запускается, когда «грязная» доля журнала дожимает min.cleanable.dirty.ratio. Свежие записи защищены min.compaction.lag.ms, чтобы все консьюмеры успели их прочитать. Поэтому между записью сотой версии ключа и исчезновением девяноста девяти прежних проходит неопределённое время — проектируйте так, как будто старые версии могут доезжать всегда, а «последнее значение» гарантируется только после компакции.

С какого оффсета читать компактед-топик: с нуля или с последнего?

С kafka.FirstOffset — самого раннего живого оффсета: после компакции в начале журнала лежит уже сжатый срез состояния, и вы соберёте актуальную таблицу за один проход. С kafka.LastOffset вы пропустите всё лежащее и увидите только будущие записи — для пересборки кеша это бесполезно. Конкретный нулевой оффсет тоже плох: старые сегменты могли уйти по ретеншену, и чтение упадёт на отсутствующей позиции, тогда как FirstOffset всегда резолвится в существующее начало.

Можно ли совместить компакцию и ретеншен на одном топике?

Да: cleanup.policy принимает оба значения — compact,delete. Брокер будет и компактировать журнал по ключам, и удалять сегменты по retention.ms и retention.bytes. Гибрид типичен для топиков конфигураций и статусов, где нужно и последнее значение каждого ключа, и обязательное время жизни совсем старых записей: например, сессии пользователей, которые должны умирать через сутки независимо от того, писал ли их ключ tombstone или нет.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Ebitengine25 уроков~12 чпрактика25 уроков уже доступны

Ebitengine: 2D-игры на Go

Курс Ebitengine для Go: игровой цикл Update/Draw, спрайты и анимации, ввод, коллизии, тайлмапы, камера, звук, сцены и сборка на платформы. 25 уроков.

  • Понимать игровой цикл Update/Draw и Layout
  • Рисовать спрайты, атласы и примитивы
  • Обрабатывать ввод: клавиатура, мышь, тач
  • Строить движение, коллизии и анимации