Kafka tombstone и JSON null: когда событие удаляет запись
Consumer получает обновления профилей. У одного сообщения само value отсутствует, у другого value содержит JSON с полем email: null. Если обработчик проверяет только «пустое значение», оба случая могут превратиться в удаление профиля. Но это разные уровни данных.
В компактируемой теме Kafka запись с ключом и null value служит tombstone — отметкой удаления значения этого ключа. JSON-объект с отдельным null остаётся обычным непустым значением сообщения. Строка JSON null тоже представляет байты, а не отсутствие Kafka value.
Проверим транспорт до разбора JSON
В примере обработчик получает исходные key и value в байтах, как их отдаёт клиент до пользовательской десериализации. None означает отсутствующее value. Для обычного обновления контракт требует JSON-объект; запись с JSON null отклоняется.
import json
state = {}
def apply_record(key_bytes, value_bytes):
if key_bytes is None:
raise ValueError("Profile key required")
key = key_bytes.decode("utf-8")
if value_bytes is None:
state.pop(key, None)
return "deleted"
value = json.loads(value_bytes)
if not isinstance(value, dict):
raise ValueError("Profile must be a JSON object")
state[key] = value
return "updated"
assert apply_record(b"A", b'{"email":"a@example.test"}') == "updated"
assert apply_record(b"A", b'{"email":null}') == "updated"
assert state == {"A": {"email": None}}
print("after null field", state)
assert apply_record(b"A", None) == "deleted"
assert state == {}
try:
apply_record(b"B", b"null")
except ValueError:
print("JSON null rejected")
else:
raise AssertionError("JSON null was accepted")
print("after tombstone", state)
После обновления с email=null профиль A остаётся в состоянии с очищенным email. После настоящего tombstone профиль удаляется. Повтор удаления безопасен: pop с заданным значением по умолчанию не считает отсутствие ключа ошибкой.
Этот пример хранит состояние в памяти для проверки семантики. В рабочем consumer замените присваивание и удаление операциями целевого хранилища, затем фиксируйте offset после успешного результата. Не подтверждайте чтение только потому, что JSON удалось разобрать.
Не потеряйте различие в десериализаторе
Если слой клиента превращает и отсутствующее value, и JSON null в один None, бизнес-код уже не сможет понять исходный случай. Сохраните отдельный признак tombstone до разбора либо возвращайте разные типы результата. Для клиента confluent-kafka проверяйте message.value() до json.loads.
Пустые байты b'' тоже не являются tombstone: это значение нулевой длины, которое не подходит под наш JSON-контракт. Ошибка парсинга должна попасть в обработку некорректных событий, а не в удаление данных.
Компактация не заменяет обработчик удаления
Удаление старых версий выполняется фоновыми процессами Kafka, поэтому consumer может прочитать несколько обновлений одного ключа. Он должен применять их в порядке партиции и понимать tombstone сам. Срок хранения отметок удаления важен при восстановлении давно простаивавшего потребителя.
Проверьте четыре входа: обычный объект, объект с null, отсутствующее value и некорректный JSON. Для внешней базы добавьте сбой между записью и commit offset, затем повтор того же события. Так отдельно проверяются значение операции и устойчивость её выполнения.
В курсе Data Engineering Fast-Track вы разберёте темы, ключи и схемы Kafka, затем проведёте события через Spark в lakehouse. Откройте программу, чтобы научиться сохранять смысл обновления и удаления во всём конвейере.
Продолжить на практике
- Data Engineering Fast‑Track: Kafka → Airflow → Spark ↗
Практический fast-track по data engineering: за 4–6 недель собираем рабочий конвейер Kafka → Spark → Lakehouse под управлением Airflow. Разберём batch и streaming, окна и watermark, схемы и контракты. Мини‑проекты, Docker Compose, шаблоны DAG и пайплайнов. Курс подойдёт инженерам, аналитикам и тем, кто хочет освоить современные пайплайны данных с уверенным Python.
