Введение в Apache Kafka
Что такое Apache Kafka?
Apache Kafka — это распределённая платформа для обработки потоков данных в реальном времени, которая позволяет приложениям публиковать, хранить и обрабатывать данные в режиме потоков событий. Она обеспечивает высокую пропускную способность, масштабируемость и отказоустойчивость, что делает её популярной для создания событийно-ориентированных архитектур, аналитики в реальном времени и управления большими объёмами данных.
Подборка материалов по Kafka
- YouTube: Лучший Гайд по Kafka для Начинающих За 1 Час (Vlad Mishustin) — классный видос по Kafka
- YouTube: 5 Применений Kafka в Реальных Приложениях — дополнительное видео для понимания использования Kafka в приложениях
- YouTube: Kafka и RabbitMQ — БРОКЕРЫ СООБЩЕНИЙ Простым языком на понятном примере
- YouTube: Playlist Apache Kafka — большой курс по Kafka от JavaGuru
- YouTube: Про Kafka (основы)
- YouTube: Kafka со Слёрмом
- YouTube: Kafka в 2025 для дата-инженера: Полный разбор на практике с Python, S3 и ClickHouse
- Kafka Visualization — онлайн симулятор работы Kafka, можно сконфигурировать (ограничено) работу Kafka и посмотреть на «конвейер сообщений» между продюсерами и потребителями данных. Статья на habr «Симулятор брокера Apache Kafka: Kafka Visualization от компании SoftwareMill» про этот симулятор
- Статья на habr «Apache Kafka: обзор»
- Шпаргалка по Kafka.pdf
- YouTube: Типичные ошибки при работе с Apache Kafka — Виктор Корейша (Ozon)
- YouTube: HighLoadChannel «Kafka»
- YouTube: Алексей Кашин — Надежно отправляем события в Apache Kafka. От CDC до паттерна Transactional Outbox
- YouTube: Apache Kafka: погружение на 45 минут. Григорий Кошелев, Контур, ведущий разработчик
Основная логика работы Kafka
Kafka — это распределённая система журналов (distributed commit log). Она состоит из нескольких брокеров (серверов), которые вместе образуют кластер. Кластер хранит топики, которые разделены на партиции (partitions). Каждая партиция — это упорядоченный, неизменяемый лог событий.
Producer (Продюсер) соединяется с кластером и узнаёт, на каком брокере хранится нужный топик и его партиции. Для каждой записи продюсер выбирает партицию (по ключу, по хешу или случайно). Событие сериализуется (обычно Avro, JSON, Protobuf) и отправляется на брокер. Брокер записывает сообщение в конец партиции — фактически в лог-файл на диске. Каждому сообщению присваивается смещение (offset) — уникальный порядковый номер. Kafka не переписывает данные, а только добавляет новые в конец файла — поэтому она очень быстрая.
Consumer (Консюмер) подключается к брокеру через Consumer Group Coordinator. Консюмер читает сообщения по порядку offset-ов и может сохранять своё текущее смещение (commit offset) в Kafka (__consumer_offsets). Таким образом Kafka знает, до какого места консюмер дочитал поток.
Kafka хранит данные на диске — в виде логов, а не в памяти. Каждая партиция — это директория на диске брокера, содержащая:
|
1 2 3 |
00000000000000000000.log 00000000000000001000.log ... |
Каждый .log файл — это блок записей. Когда достигается лимит по размеру (segment.bytes) или времени (segment.ms), создаётся новый сегмент. Kafka не удаляет сообщения сразу после чтения.
Партиция сохраняется на диске как каталог сегментов. Сегменты — это физические фрагменты одной партиции, а не отдельные логические сущности.
Topic -> Log -> Segment:
Сообщения хранятся до истечения retention policy:
- по времени (retention.ms),
- по размеру (retention.bytes),
- или в режиме compaction — сохраняется только последнее сообщение для каждого ключа.
Глоссарий Apache Kafka
Основные определения Apache Kafka:
- Apache Kafka — Распределённая платформа потоковой передачи событий (event streaming platform), предназначенная для обработки и хранения потоков данных в реальном времени. Используется для построения real-time пайплайнов и стриминговых приложений.
- Event / Message / Record (Событие / Сообщение / Запись) — Представление факта изменения состояния системы — «что-то произошло». Включает ключ, значение, метку времени и необязательные заголовки. События в Kafka неизменяемы (immutable).
- Topic (Топик) — Логическая категория/канал данных, куда продюсеры публикуют события. Топики делятся на partitions (разделы) и могут быть реплицированы.
- Partition (Раздел) — Физическая часть топика, хранящая события в порядке записи. Базовая единица параллелизма Kafka. Каждая партиция — упорядоченная, неизменяемая последовательность событий, которая записывается в лог. События в партиции строго упорядочены и идентифицируются offset-ом.
- Broker (Брокер) — Сервер Kafka, который принимает события от продюсеров (producers), хранит их в партициях и выдаёт консюмерам (consumers). Обычно Kafka-кластер состоит из нескольких брокеров.
- Producer (Продюсер) — Приложение (клиент), которое публикует (отправляет) события в Kafka-топики. Продюсер может определять, в какую партицию топика записать сообщение.
- Consumer (Консюмер) — Приложение (клиент), которое подписывается на один или несколько топиков и обрабатывает поступающие события. Читает сообщения из партиций в порядке их offset-ов.
- Consumer Group (Группа Потребителей) — Набор консюмеров, совместно обрабатывающих события из одного или нескольких топиков. Каждая партиция назначается только одному консюмеру внутри группы → масштабирование без дублирования.
- Offset (Смещение) — Уникальный порядковый номер события в пределах партиции. Определяет позицию консюмера и обеспечивает контроль обработки сообщений.
- Replication (Репликация) — Механизм копирования партиций на несколько брокеров. Обеспечить отказоустойчивость и сохранность данных. Replication factor определяет количество копий каждой партиции.
- Leader (Лидер) — Основная реплика партиции, которая обрабатывает все операции чтения и записи.
- Follower (Фолловер) — Реплика, синхронизирующаяся с лидером путём копирования его журнала событий. При отказе лидера фолловер может быть выбран новым лидером.
- In-Sync Replica (ISR, Синхронная Реплика) — Набор реплик (включая лидера), которые полностью синхронизированы с лидером. Обеспечивает надёжную запись — продюсер может ждать подтверждений от всех ISR.
- ZooKeeper — распределённая система координации, традиционно использовавшаяся Kafka для хранения метаданных, выборов лидера и конфигурации. KRaft (Kafka Raft) — новый встроенный протокол консенсуса, заменяющий ZooKeeper в современных версиях Kafka.
- Kafka Connect — Фреймворк для интеграции Kafka с внешними системами (базы, очереди, хранилища и т. д.). Source Connectors (загрузка данных в Kafka) и Sink Connectors (выгрузка из Kafka).
- Kafka Streams — Клиентская библиотека, которая позволяет прямо из приложений читать данные из топиков, обрабатывать их (фильтровать, агрегировать, объединять) и писать результат обратно в другие топики. Это “потоковая логика поверх Kafka”.
Пример сценария использования Apache Kafka
Здесь разберем небольшой пример, чтобы на нем описать основную логику работы системы.
Допустим, у нас есть крупный интернет-магазин, где ежедневно создаются тысячи заказов. Каждый заказ запускает целую цепочку бизнес-процессов: уведомления клиенту, резервирование товара, доставка, обновление аналитики и многое другое.
Представим, что мы хотим построить эту систему так, чтобы она была масштабируемой, отказоустойчивой и позволяла добавлять новые микросервисы без необходимости переписывать старые. Именно здесь на сцену выходит Apache Kafka — платформа, которая превращает поток событий в единое, надёжное “сердце” всей e-commerce-инфраструктуры.
Шаг 1. Создание заказа
Всё начинается с того, что пользователь оформляет заказ на сайте. На уровне архитектуры это делает сервис, который мы назовём Order Service. Он отвечает за валидацию данных, проверку наличия товаров и создание записи в базе данных. Когда заказ успешно создан, сервис публикует событие OrderCreated в Kafka — в специальный топик под названием orders.
Выглядит это, как простое сообщение в формате JSON, например:
|
1 2 3 4 5 6 7 8 9 10 |
{ "order_id": 12345, "customer_email": "user1@ecom.ru", "total": 199.90, "items": [ {"sku": "AB-123", "qty": 2}, {"sku": "CD-456", "qty": 1} ], "created_at": "2025-10-22T10:30:00Z" } |
Продюсер (Order Service) обращается к брокеру Kafka и помещает событие в конец журнала (partition) топика orders. С этого момента Kafka становится источником истины: все сервисы, которым нужно знать о новых заказах, будут читать именно этот поток.
Шаг 2. Сервис уведомлений
После того, как событие появляется в Kafka, его получает первый потребитель — Notification Service. Это отдельное приложение, которое подписано на тот же топик orders, но принадлежит своей consumer group (например, notification_group). Kafka гарантирует, что каждый потребитель внутри группы получает уникальный набор партиций — то есть события распределяются равномерно, а не дублируются.
В данном случае сервис уведомлений получает все события, потому что он один в группе.
Получив сообщение OrderCreated, сервис выполняет простое действие: он берёт адрес электронной почты клиента, формирует письмо и отправляет уведомление:
|
1 |
“Ваш заказ №12345 успешно создан! Мы приступаем к обработке.” |
Kafka здесь выступает как надёжный посредник.
Если сервис уведомлений временно недоступен — ничего страшного. Kafka продолжает хранить сообщения в топике orders. Когда сервис вернётся в строй, он просто дочитает поток с того места, где остановился — по сохранённому offset-у. Таким образом, ни одно уведомление не потеряется, даже если в системе временно что-то пошло не так.
Шаг 3. Сервис логистики
Параллельно с Notification Service в системе работает другой потребитель — Logistics Service. Он также слушает топик orders, но уже со своей consumer group, например logistics_group. Это значит, что Kafka отдаёт ему тот же поток событий, что и сервису уведомлений, но независимо. Каждый сервис получает свои копии событий — и каждый может реагировать по-своему.
Когда Logistics Service получает сообщение OrderCreated, он делает совсем другие вещи: резервирует товар на складе, создаёт задачу для курьера, обновляет статус заказа в системе доставки. Таким образом, одно событие запускает два (и потенциально десятки) разных бизнес-процессов — и всё это асинхронно и безопасно.
Потоковая интеграция вместо хрупких связей через API
Если бы мы строили такую систему без Kafka, то Order Service должен был бы сам вызывать API уведомлений, API логистики и, возможно, ещё десяток других сервисов. Такой подход создаёт сильную связанность: ошибка одного из сервисов может замедлить или остановить весь процесс. Kafka решает эту проблему, превращая коммуникацию в поток событий, где каждый сервис просто подписывается на интересующие его данные.
Теперь Order Service не знает, кто именно реагирует на событие OrderCreated. Может, только логистика. Может, логистика и уведомления. А может, ещё и аналитика, CRM, биллинг — без разницы.
Он просто публикует факт: “Заказ создан.” И любой другой сервис может использовать эту информацию, не нарушая независимость архитектуры.
Надёжность и гибкость
Kafka гарантирует, что ни одно событие не потеряется:
- Все сообщения хранятся на диске и могут быть реплицированы на несколько брокеров.
- Каждый потребитель знает, до какого сообщения он дочитал (offset).
- Можно “перемотать” поток назад и перечитать историю заказов — например, если в сервисе логистики произошла ошибка и нужно пересоздать статусы.
Благодаря этому, система становится не просто асинхронной, а воспроизводимой: каждый бизнес-процесс можно “переиграть”, восстановить данные или проанализировать прошлые заказы.
Kafka — это не просто брокер сообщений, а фундаментальный слой событийной архитектуры, на котором можно построить всё: от аналитики и уведомлений до машинного обучения и мониторинга.
Партиционирование (Partitioning)
В контексте Apache Kafka партиционирование (partitioning) — это метод разделения топика на более мелкие, независимые сегменты, называемые разделами (partitions). Каждый раздел представляет собой лог, в котором сообщения хранятся в порядке их поступления. Партиционирование позволяет Kafka параллелизировать обработку данных, что даёт возможность нескольким потребителям (consumers) одновременно читать данные из разных разделов.
Данные в разделе хранятся последовательно (append-only log) на диске. Это позволяет эффективно писать и читать сообщения с высокой пропускной способностью.
Стратегия партиционирования Apache Kafka направлена на достижение нескольких целей: высокая доступность, устойчивость к сбоям, балансировка нагрузки и масштабируемость.
Kafka разбивает топики на разделы, и каждый раздел является независимой единицей данных, которую можно реплицировать между несколькими брокерами.
Партиционирование — это основа горизонтального масштабирования Kafka (единица масштабирования). При добавлении новых разделов система может обрабатывать больший объём данных и поддерживать более высокую параллельность потребителей (больше разделов — больше параллелизма потребителей).
Кроме того, сообщения внутри одного раздела всегда сохраняют порядок, что важно для приложений, где требуется строгая последовательность событий (offsets управляются для каждой consumer group, что даёт отказоустойчивое параллельное потребление).
ВАЖНО
Consumer group = логическая группа потребителей, которые совместно читают данные из одного или нескольких топиков, деля между собой партиции. Это значит, что каждая партиция топика обрабатывается только одним потребителем внутри группы. То есть, в одной consumer group обычно находятся все экземпляры одного сервиса, которые выполняют одну и ту же задачу. Kafka отслеживает смещения (offsets) отдельно для каждой группы, чтобы гарантировать, что одно сообщение не будет прочитано двумя потребителями из одной группы.
Тема разбивается на несколько разделов, что позволяет параллелить чтение/запись и распределять нагрузку по брокерам. Каждому разделу присваивается смещение (offset) для каждого сообщения.
Разделы могут быть реплицированы, один из реплик становится лидером (leader), остальные — followers. Это обеспечивает отказоустойчивость и доступность.
Описание схемы (схема демонстрирует базовый принцип устойчивости Kafka: запись — только через лидера, чтение — обычно из лидера, а синхронизация поддерживается через фолловеров):
- Производитель (Producer) всегда записывает сообщения в лидера раздела (partition).
- Фолловеры (Followers) автоматически реплицируют данные с лидера, чтобы поддерживать копии лога в актуальном состоянии.
- Потребители (Consumers) из группы потребителей (Consumer Group) обычно читают данные с лидера, чтобы получать подтверждённые сообщения.
Внизу показан список ISR (In-Sync Replicas) — это набор брокеров, чьи копии данных синхронизированы с лидером (в примере ISR = [101, 102, 103]).
Транзакции и целостность данных
Kafka поддерживает транзакции, что позволяет атомарно записывать сообщения в несколько разделов/тем и одновременно фиксировать смещения потребителя. Это важно для гарантии exactly-once (или ближе к этому) обработки.
При чтении можно выбирать уровень изоляции: «read_uncommitted» — читать все подряд или «read_committed» — читать только закоммиченные сообщения (т.е. получать только завершённые транзакции).
- Transaction Coordinator — модуль в брокере Kafka, который управляет транзакциями и отслеживает их состояние.
- Transaction Log — внутренний топик, куда записывается состояние транзакций (например: “начата”, “готова к коммиту”, “завершена”).
- Когда продюсер начинает новую транзакцию, он регистрирует свой
transactional.idу коорд. После этого он начинает отправлять сообщения обычным образом, но в рамках транзакции. Когда приходит время — либоcommitTransaction(), либоabortTransaction(). При коммите Kafka пишет “маркер” транзакции в каждую участвующую партицию и фиксирует, что сообщения этой транзакции видимы. - Потребитель, настроенный в режиме
isolation.level=read_committed, будет видеть только те записи, которые относятся к завершённым (committed) транзакциям, и игнорировать те, что от незавершённых или aborted.
2 модели обмена сообщениями (очередь сообщений и модель публикации-подписки)
При наличии только одной группы получателей Kafka функционирует как традиционная система очереди сообщений. Однако, если на тему подписано несколько групп получателей, Kafka ведёт себя как модель публикации/подписки, когда сообщения получают несколько получателей.
1. Очередь сообщений (Message Queue / Point-to-Point) — каждое сообщение обрабатывается ровно одним потребителем:
- Сообщения распределяются по разделам (partitions) темы.
- Внутри группы потребителей (Consumer Group) Kafka гарантирует, что каждый раздел назначен только одному потребителю.
- Таким образом, сообщения из одного раздела не дублируются между потребителями группы.
- Это обеспечивает горизонтальное масштабирование обработки — больше потребителей в группе — больше параллелизма.
Пример:
- Тема orders с 3 разделами.
- Группа потребителей order-service из 3 экземпляров.
Каждый экземпляр обрабатывает свой раздел — каждое сообщение читается только один раз в рамках группы.
2. Публикация-подписка (Publish-Subscribe) — одно сообщение может быть прочитано множеством независимых потребителей:
- Разные группы потребителей могут подписываться на одну и ту же тему.
- Каждая группа обрабатывает поток независимо от других — Kafka хранит смещения (offsets) для каждой группы.
- Это позволяет нескольким приложениям читать один и тот же поток событий параллельно, не мешая друг другу.
Пример: Тема user-activity.
Группы:
- analytics-service (для анализа поведения),
- monitoring-service (для алертов).
Обе группы читают одинаковые события, но Kafka ведёт отдельные смещения для каждой.
Schema Registry (Реестр схем)
Как только приложения начнут активно отправлять сообщения в Kafka и получать сообщения из него, произойдут два события.
- Во-первых, появятся новые потребители существующих топиков. Это будут совершенно новые приложения — возможно, написанные той же командой, которая создала исходный продюсер сообщений, а возможно, и другой командой, — и им потребуется понимать формат сообщений в топике.
- Во-вторых, формат этих сообщений будет меняться по мере развития бизнеса. Объекты заказов получат новое поле статуса, имена пользователей будут разделены на имя и фамилию вместо полного имени и так далее.
Схема наших объектов предметной области — это постоянно меняющаяся цель, и нам необходимо найти способ согласовать схему сообщений в любом топике.
Schema Registry предоставляет централизованный репозиторий для управления и проверки схем данных сообщений топиков, а также для сериализации и десериализации данных по сети. Производители и потребители топиков Kafka могут использовать схемы для обеспечения согласованности и совместимости данных по мере развития схем. Schema Registry — ключевой компонент управления данными, помогающий обеспечивать качество данных, соответствие стандартам, прозрачность происхождения данных, возможности аудита, совместную работу между командами, эффективные протоколы разработки приложений и производительность системы.
Schema Registry работает с:
- Avro (классика Kafka-мира, бинарный, компактный)
- Protobuf (Google Protocol Buffers)
- JSON Schema (читаемый, но больше размер сообщений)
Реестр схем не включен в Kafka, но существует несколько его вариантов с открытым исходным кодом. Например, Реестр Confluent Schema.
Гарантии доставки сообщений в Kafka
Под семантической гарантией понимается соглашение между продюсером, брокером и потребителем — как именно сообщения передаются и обрабатываются.
Kafka поддерживает три типа семантики доставки:
- At most once (не более одного раза) — Сообщения доставляются один раз, но при сбое системы часть сообщений может быть потеряна и не будет переотправлена. Минимальная задержка, но есть риск потерь.
- At least once (как минимум один раз) — Сообщения доставляются один или несколько раз. При сбое система гарантирует, что сообщение не потеряется, но возможны дубликаты. Без потерь, но может потребоваться обработка повторов.
- Exactly once (ровно один раз) — Каждый сообщение доставляется строго один раз. Оно не теряется и не читается повторно, даже если часть системы выходит из строя. Максимальная надёжность, но выше задержка и сложнее настройка.
Эти три подхода отражают компромисс между задержкой и надёжностью. Выбор зависит от требований вашего приложения.
Важно: многие системы заявляют о поддержке exactly-once, но на деле они не учитывают сбои компонентов за пределами самой системы (например, внешнего продюсера или потребителя).
Kafka же реализует эти гарантии на уровне журнала (log): как только сообщение записано и подтверждено, оно считается зафиксированным (committed). После этого оно не потеряется, пока хотя бы один брокер с репликой этого раздела остаётся «живым».
Доставка сообщений от продюсера
- At most once — Для минимальной задержки продюсер может отправлять сообщения асинхронно (“fire and forget”) — то есть не ожидая подтверждения от брокера. Можно также дождаться подтверждения от ведущего брокера (leader broker), чтобы снизить риск потери, но увеличить задержку.
В обоих случаях сообщения доставляются один раз, а при сбое — могут быть потеряны и не будут переотправлены. - At least once — В этом режиме, если продюсер не получил подтверждение, что сообщение было зафиксировано, он переотправит его. Это гарантирует доставку как минимум один раз, но при этом одно и то же сообщение может попасть в лог дважды, если первый запрос всё-таки был успешным.
Режим идемпотентного продюсера (idempotent producer) гарантирует, что повторная отправка не создаст дубликатов, а порядок сообщений в журнале сохранится. Для этого брокер присваивает продюсеру уникальный ID и использует порядковый номер (sequence number) для каждого сообщения, чтобы исключить повторную запись одного и того же события. - Exactly once — продюсеры могут использовать транзакционную доставку (transactional delivery). В этом режиме продюсер получает подтверждение, что сообщения приняты и реплицированы, при повторной отправке сообщение записывается идемпотентно — существующие данные перезаписываются, а не дублируются. Это добавляет задержку и требует больших ресурсов, но обеспечивает наивысший уровень надёжности.
Чтобы реализовать транзакционные гарантии “exactly once”, потребители также должны быть соответственно сконфигурированы (например, использоватьisolation.level=read_committed).
Получение сообщений потребителем (Consumer Receipt)
Каждое сообщение в разделе (partition) топика имеет последовательный идентификатор — offset.
Все реплики одного раздела содержат одинаковый журнал логов с теми же offset’ами, а потребитель сам управляет своей позицией в этом логе — то есть знает, с какого offset’а продолжать чтение.
Если потребитель выходит из строя, и его работу должен перенять другой потребитель, тот должен знать, с какого offset’а начать читать.
- At most once — «не более одного раза» — Потребитель читает группу сообщений. Сначала сохраняет своё положение (offset). Затем обрабатывает сообщения. Если потребитель завалится после сохранения offset, но до завершения обработки,
новый потребитель начнёт читать с сохранённого offset’а, и уже прочитанные, но не обработанные сообщения будут потеряны. Это семантика “at most once” — в случае сбоя часть сообщений может не обработаться вообще. - At least once — «как минимум один раз» — Потребитель сначала обрабатывает сообщения и только потом сохраняет offset. Если сбой произойдёт между обработкой и сохранением offset, новый потребитель, который возьмёт на себя задачу,
прочитает те же сообщения повторно. Таким образом, некоторые сообщения могут быть обработаны дважды, но ни одно не будет потеряно.
Чтобы избежать проблем от повторной обработки, можно использовать идемпотентную запись — например, задавать каждой записи первичный ключ (primary key), чтобы повторное получение просто перезаписало старую запись без дубликатов.
- Exactly once — «ровно один раз» — Когда Kafka используется для чтения из одного топика и записи в другой (например, в приложениях Kafka Streams), Kafka реализует exactly-once семантику с помощью транзакций.
Позиция потребителя (offset) сохраняется в виде сообщения в специальном топике Kafka. Эти данные об offset’ах записываются в одной транзакции вместе с результатами обработки, отправляемыми в выходные топики.
Если транзакция откатывается (aborted), то и offset возвращается к предыдущему значению. Таким образом, система возвращается в полностью согласованное состояние.Какие сообщения видны потребителям:isolation.level=read_uncommitted— потребитель видит все сообщения, включая из незавершённых транзакций.isolation.level=read_committed— потребитель читает только сообщения из завершённых транзакций (используется по умолчанию в режиме exactly-once).
Как работает механизм подтверждений (ACK) в Kafka?
ack (acknowledgement) в Kafka — это механизм, который позволяет производителю (producer) получать подтверждение от брокера о том, что сообщение было успешно отправлено и обработано. Этот параметр, настраиваемый в конфигурации продюсера, влияет на баланс между надежностью (риском потери данных) и производительностью.
Kafka предлагает три уровня подтверждений, каждый из которых балансирует между надежностью и скоростью. Рассмотрим каждый из них:
acks=0 (Производитель не ждет подтверждения)
- Fire-and-forget: отправил и забыл
- Молниеносная пропускная способность
- Сообщения могут потеряться
- Подходит для метрик или логов, где потеря нескольких данных не критична
acks=1 (Продюсер ждет подтверждения от лидера партиции/leader partition)
- Ожидает подтверждения от лидера
- Хорошая скорость при базовой надежности
- Есть риск потери сообщений, если лидер выйдет из строя
- Подходит для большинства повседневных сценариев использования
acks=-1 или all (Продюсер ждет подтверждения от всех реплик в синхронизации/In-Sync Replicas, ISR)
- Ожидает подтверждений от всех реплик
- Медленнее, но максимально надежно
- Максимальная устойчивость
- Идеально подходит для финансовых транзакций
Сравнительная таблица настроек ACKs
| Характеристика | acks=0 | acks=1 | acks=all |
|---|---|---|---|
| Надёжность сообщений | Низкая | Средняя | Высокая |
| Задержка отклика | Минимальная | Средняя | Наибольшая |
| Пропускная способность | Максимальная | Средняя | Минимальная |
| Риск потери сообщений | Возможна потеря сообщений | Потеря возможна только при сбое лидера | Сообщения не теряются |
| Сценарии использования | Метрики и логированиеМониторинг производительности | Регулярные обновленияПотоки аналитики | ПлатежиКритически важные данные |
| Нагрузка на CPU | Минимальная | Средняя | Наибольшая |
| Сетевые накладные расходы | Минимальные | Средние | Наибольшие |
Повторные попытки отправки сообщений (Retry Kafka)
Retry — это механизм повторных попыток отправки или обработки сообщений в Apache Kafka. Он помогает обеспечить надежность, повторно отправляя сообщения в случае временных сбоев, вместо того, чтобы терять их. Существуют два основных подхода: блокирующие (когда обработчик замирает в ожидании) и неблокирующие (когда сообщение перенаправляется в отдельный топик, чтобы освободить основной поток).
Может реализоваться 2 сценария, когда ретраи требуются:
Сбой сети:
|
1 2 3 4 5 |
Отправка сообщения → ✖ Сбой сети ↓ Ожидание 100 мс и попытка снова ↓ Успешная повторная попытка ✅ Сообщение доставлено |
Сбой в лидере партиции:
|
1 2 3 4 5 |
Отправка сообщения → ✖ Лидер недоступен ↓ Ожидание 100 мс (идёт выбор нового лидера) ↓ Повторная отправка ✅ Новый лидер на линии, сообщение доставлено |
Ключевые параметры конфигурации retry:
|
1 2 3 4 5 6 7 8 9 10 11 |
# Количество попыток отправки retries=3 # Повторить 3 раза # Интервал между повторными попытками retry.backoff.ms=100 # Базовый интервал 100 мс # Общий тайм-аут доставки сообщения delivery.timeout.ms=120000 # Ожидание до 2 минут # Включить идемпотентность, чтобы предотвратить дубликаты сообщений при retry enable.idempotence=true |
Анатомия сообщения Kafka
Сообщение Kafka состоит из следующих элементов:
Структура сообщения Kafka:
- Key (Ключ). Ключ является необязательным элементом в сообщении Kafka и может быть равен null. Ключ может быть строкой, числом или любым объектом, после чего он сериализуется в бинарный формат.
- Value (Значение). Значение представляет содержимое сообщения и также может быть
null. Формат значения произвольный и также сериализуется в бинарный формат. - Compression Type (Тип сжатия). Сообщения Kafka могут быть сжаты. Тип сжатия можно указать как часть сообщения. Доступные варианты:
none,gzip,lz4,snappyиzstd. - Headers (Заголовки). Может быть список необязательных заголовков сообщения Kafka в виде пар ключ-значение. Обычно заголовки добавляют для указания метаданных о сообщении, особенно для трассировки.
- Partition + Offset (Раздел + Смещение). После того как сообщение отправлено в топик Kafka, ему присваиваются номер раздела и идентификатор смещения (offset). Комбинация topic+partition+offset уникально идентифицирует сообщение.
- Timestamp (Временная метка). Временная метка добавляется либо пользователем, либо системой в сообщение.
API Kafka
Apache Kafka предоставляет пять основных API Java для управления кластерами и клиентами.
- Producer API — позволяет приложениям публиковать (записывать) потоки событий в одну или несколько тем Kafka, предоставляя настройки для подтверждений отправки и сжатия сообщений.
- Consumer API — позволяет приложениям подписываться на темы и читать поток событий, управляя положением чтения (offset) и количеством извлекаемых данных за один цикл.
- Admin Client API — предоставляет методы для программного управления кластером Kafka — создания, удаления, описания и изменения ресурсов вроде тем, брокеров и ACL.
- Connect API — служит фреймворком для встраивания источников и приёмников данных: позволяет перемещать потоки событий между Kafka и внешними системами (СУБД, хранилища, приложения).
- Kafka Streams API — библиотека для построения приложений и микросервисов, которые читают данные из тем Kafka, преобразуют их (фильтрация, агрегация, join) и записывают результаты обратно в темы.
GitHub проект «kafka-tutorial» — для понимания основ
Для статьи я создал отдельный тестовый проект «kafka-tutorial» на GitHub для локального развертывания Kafka и Zookeeper с примерами producer и consumer на Python. Предназначен для изучения и отладки взаимодействия с Kafka.
Краткое описание библиотеки kafka-python
Как работает обмен сообщениями между продьюсером и консюмером в библиотеке kafka‑python (клиент для Apache Kafka на Python).
- Продьюсер: настраиваем, сериализуем, буферизируем, отправляем через TCP к брокеру-лидеру.
- Консюмер: подключаемся, подписываемся/назначаем партиции, выполняем Fetch запросы, десериализуем, возвращаем сообщения.
- Всё взаимодействие реализовано через сетевой протокол Kafka (TCP), библиотека сама строит запросы (Metadata, Produce, Fetch и др), парсит ответы.
Отправка сообщения (Producer)
Создаётся объект:
|
1 2 3 4 |
from kafka import KafkaProducer producer = KafkaProducer(bootstrap_servers='localhost:9092', key_serializer=…, value_serializer=…) |
Здесь библиотека соединяется с брокерами Kafka, получает метаданные (какие топики, какие партиции, какие брокеры лидеры) через Metadata API.
Когда вызывается producer.send(topic, key=…, value=…) процесс выглядит следующим образом:
- Сериализуется ключ и/или значение через переданные
key_serializer/value_serializer. - Сообщение помещается в буфер (в память) для соответствующей партиции. Продусер внутренне ведёт накопитель (
RecordAccumulator) и фоновый поток I/O (Sender thread) которые группируют (batch) сообщения и отправляют партиями. - В зависимости от настроек (
linger_ms,batch_size) может быть задержка прежде чем буфер будет отправлен, чтобы накопить больше сообщений. - Когда пакет сообщений отправляется, используется бинарный протокол Kafka (через TCP) — библиотека вручную формирует запрос
ProduceRequest, посылает его брокеру-лидеру партиции. (Это скрыто в коде, но логика за этим есть:метаданные → выбор партиции → сетевой запрос)Ответ (ProduceResponse) возвращается, и еслиacksнастроено (напримерacks='all'), продьюсер может ждать подтверждения (или получить ошибку/ретрай) от брокера. - Вы можете получить
future = producer.send(...)и дальшеfuture.get(timeout=…)чтобы ждать результата. - Необходимо вызвать
producer.flush()перед завершением, чтобы дождаться отправки всех накопленных сообщений (иначе программа может завершиться раньше и сообщения не будут отправлены).
Резюме: Python-код → KafkaProducer.send() → буфер → сетевой запрос к брокеру Kafka → сообщение попадает в соответствующую тему/партицию.
Получение сообщения (Consumer)
Создаётся объект:
|
1 2 3 4 5 6 7 |
from kafka import KafkaConsumer consumer = KafkaConsumer('some_topic', bootstrap_servers='localhost:9092', group_id='my_group', key_deserializer=…, value_deserializer=…, auto_offset_reset='earliest') |
Библиотека подключается к брокерам, получает метаданные, подписывается на топик(и), либо явно назначает партиции (assign) или через subscribe получает назначение в составе группы потребителей.
После подписки (или назначения) потребитель выполняет цикл for msg in consumer: или вызывает consumer.poll() и получает сообщения:
- Он делает
FetchRequestк брокерам для каждой назначенной партиции, ожидая данные. Конфигурации вродеfetch_min_bytes,fetch_max_wait_msвлияют на задержку/объём. - Когда данные приходят, они десериализуются с помощью
key_deserializer/value_deserializer. - В сообщении вы получаете объект
ConsumerRecord, у которого естьtopic,partition,offset,key,value. - Если используется группа (
group_idзадана), то библиотека взаимодействует с координатором группы (GroupCoordinator) для ребалансировки, назначений партиций и фиксации смещений (offsets) либо вручную либо автоматически (еслиenable_auto_commit=True).
Например:
|
1 2 |
for msg in consumer: print(msg.topic, msg.partition, msg.offset, msg.key, msg.value) |
Резюме: Python-код → KafkaConsumer опрос брокеров → получает сообщения из Kafka → возвращает вам десериализованные объекты.
Обработка потоков (Stream processing) с Kafka Streams
Kafka Streams — это клиентская Java-библиотека, которая читает данные из Kafka топиков (источников), выполняет преобразования, агрегации, join’ы, оконные операции, и записывает результаты обратно в Kafka (или во внешние хранилища). И все это происходит в реальном времени.
Приложение на Kafka Streams — это просто обычный сервис (JVM-программа), без отдельного «кластера стримов». Kafka сама масштабирует обработку через партиции и consumer groups.
В Data Engineering Kafka Streams применяют для:
- очистки и обогащения данных «на лету» перед записью в DWH;
- real-time агрегаций (например, “средний чек за последние 10 минут”);
- вычислений метрик и алертов в реальном времени;
- построения CDC пайплайнов (Change Data Capture).
Подборка материалов по Kafka Streams
- Официальная документация на английском (видео) + небольшая документация на английском Core Concepts
- Статья «Введение в Kafka Streams» + GitHub demo-kafka-streams
- Статья Habr: «Потоковая обработка данных с помощью Kafka Streams: архитектура и ключевые концепции»
- YouTube: Kafka Streams (На примере OUTBOX pattern, Kafka Connect, Debezium)
- YouTube: Kafka Streams // Демо-занятие курса «Apache Kafka»
- YouTube: Kafka Streams: лекция 1 2022-10-10
- YouTube: Запись вебинара «Kafka Streams: для чего еще можно использовать Kafka
- YouTube: Создание потоковых приложений с использованием Kafka Streams // «Java Developer. Professional»
- YouTube ENG: PlayList «Kafka Streams Tutorials | Kafka Streams 101 (2023)» — и еще куча других видео на канале @Confluent/playlists
- YouTube ENG: PlayList «Kafka Streams: Zero to Hero»
- YouTube ENG: Build a Reactive Data Streaming App with Python and Apache Kafka | Coding In Motion
- YouTube: python kafka spark streaming
- Статья «Kafka Streams для начинающих. Потоковая обработка данных в мире Java»
- YouTube: Мощь Kafka Streams. Когда использовать? | Александр Кузнецов | Синимекс
- Статья Habr: Apache Kafka и потоковая обработка данных с помощью Spark Streaming
Что такое Kafka Streams: основные абстракции – KStream, KTable, топология
Kafka Streams — это библиотека потоковой обработки данных, встроенная в Apache Kafka. Она позволяет создавать приложения, которые обрабатывают данные в реальном времени непосредственно из Kafka-топиков и записывают результаты обратно в Kafka или во внешние системы. В отличие от фреймворков, требующих отдельного кластера (например, Apache Flink или Spark Streaming), Kafka Streams работает как часть обычного клиентского приложения и масштабируется за счёт механизма партиционирования Kafka.
Основной моделью данных в Kafka Streams является поток событий — непрерывная последовательность записей (пар «ключ–значение»), поступающих из Kafka-топика. Для работы с такими потоками библиотека предоставляет две ключевые абстракции: KStream и KTable.
KStream представляет собой неизменяемый поток событий, где каждая запись рассматривается как отдельное событие. Поток можно фильтровать, преобразовывать, объединять с другими потоками, группировать или агрегировать. Каждая новая запись в топике немедленно обрабатывается, что делает KStream подходящей моделью для событийных данных — например, логов, кликов, заказов или транзакций.
KTable, напротив, представляет собой табличное представление данных, отражающее их текущее состояние (последнее состояние для каждого ключа). Каждое новое сообщение с тем же ключом обновляет существующую запись, а не добавляет новую. Таким образом, KTable можно воспринимать как материализованное состояние, построенное из потока событий. Эта абстракция используется для агрегаций, подсчётов, хранения текущего состояния или выполнения операций объединения (join) между потоками и таблицами.
Связующим элементом между этими абстракциями является топология (Topology) — направленный граф, описывающий последовательность операций обработки данных.
Топология определяет, какие потоки данных читаются, какие преобразования применяются, где сохраняется промежуточное состояние и в какие топики отправляются результаты. Каждая вершина топологии соответствует операции (например, фильтрации, группировке или объединению), а каждое ребро представляет поток данных между этими операциями.
Kafka Streams, таким образом, объединяет концепции потоков и таблиц в единую модель обработки событий, где данные могут рассматриваться как непрерывный поток изменений или как текущее состояние системы. Это делает библиотеку удобным инструментом для построения реактивных, устойчивых и масштабируемых систем обработки данных в реальном времени.
Почему и когда использовать: трансформации, фильтрации, агрегаты, соединения потоков
Kafka Streams стоит рассматривать не просто как инструмент для обработки событий, а как логическое продолжение самой Kafka — средство, которое превращает поток данных в поток знаний.
Главный вопрос, который задаёт себе инженер данных, — зачем использовать Kafka Streams, если уже есть Kafka и, возможно, Spark? Ответ в архитектурной философии: Kafka Streams — это «ближе к данным», чем большинство других систем. Она позволяет обрабатывать события там же, где они рождаются, без тяжёлой инфраструктуры и внешних движков. Приложение на Kafka Streams становится умным клиентом Kafka, который не просто читает и пишет сообщения, а выполняет вычисления над ними в реальном времени, сохраняя при этом согласованность состояния и способность к восстановлению.
Использовать Kafka Streams стоит там, где нужны реактивные сценарии обработки: например, при построении витрин для real-time аналитики, обнаружении аномалий, расчёте метрик на лету, обработке транзакций, обновлении состояния пользователей или формировании рекомендаций. Она особенно эффективна, когда важна низкая задержка, надёжная обработка каждого события и возможность масштабирования через Kafka partitions, а не через сторонние кластеры.
Когда речь идёт о трансформациях, Kafka Streams превращается в конвейер данных. С помощью операций map, flatMap и selectKey можно изменять структуру, тип и ключ событий, формируя новые потоки. Это позволяет реализовывать бизнес-логику прямо в коде, а не на уровне ETL-инструментов. Каждое преобразование становится узлом в топологии, и поток данных, проходя через них, постепенно приобретает форму, нужную системе downstream.
Фильтрация (filter, filterNot) даёт возможность отсекать ненужные события ещё на раннем этапе. Это особенно важно при работе с большими объёмами данных, где стоимость дальнейшей обработки может быть высока. Потоки становятся чище, а вычисления — экономнее.
Агрегации делают Kafka Streams мощным инструментом для анализа событий во времени. С помощью операций groupByKey, aggregate, reduce и count можно собирать статистику в реальном времени, отслеживать тренды или поддерживать счётчики. Агрегации в Kafka Streams связаны с концепцией окон — временных диапазонов, внутри которых события группируются. Это позволяет, например, считать количество кликов за последние пять минут или среднюю сумму заказов за день. Такие операции сохраняют состояние в локальном сторе (RocksDB), что делает приложение самодостаточным и отказоустойчивым.
Одной из самых интересных возможностей Kafka Streams являются соединения (joins) — механизм, позволяющий объединять данные из разных потоков или таблиц. Сценарии могут быть разными: объединение двух KStream для корреляции событий из разных систем, соединение KStream и KTable для добавления справочной информации или join двух KTable для синхронизации состояний. При этом Kafka Streams обеспечивает согласованность и упорядоченность данных, что особенно критично для финансовых и аналитических систем.
Таким образом, Kafka Streams стоит использовать там, где данные должны не просто перемещаться, а жить — обновляться, объединяться, фильтроваться и агрегироваться в реальном времени. Это библиотека, которая позволяет думать о данных как о непрерывном процессе, а не о статичном снимке. И в этом её сила: она превращает поток событий в логическую модель, которую можно выразить кодом, а не инфраструктурой.
Как запустить приложение Streams (конфигурация, запуск, топология)
Когда речь заходит о запуске приложения Kafka Streams, всё начинается с понимания: мы не поднимаем кластер, мы пишем приложение, которое становится частью распределённой системы.
Kafka Streams — это не сервис, а библиотека, встроенная в ваше Java-приложение. В этом и заключается её философия — обработка данных должна быть как можно ближе к месту, где выполняется бизнес-логика.
Первым шагом в создании любого приложения Streams является конфигурация. Она определяет, как приложение будет взаимодействовать с Kafka и управлять своим состоянием. Ключевые параметры задаются через объект Properties: application.id, bootstrap.servers, default.key.serde, default.value.serde и другие. Параметр application.id служит не просто идентификатором, а точкой согласования состояния: Kafka Streams использует его для хранения метаданных, контрольных точек и топологического состояния в специальных служебных топиках. Поэтому выбор application.id должен быть осознанным — он определяет, сможет ли приложение продолжить работу после перезапуска без потери данных.
Следующим этапом идёт построение топологии — логического графа обработки данных. Для этого используется StreamsBuilder, объект, через который определяются источники (stream и table), преобразования (map, filter, join, aggregate) и выходные точки (to). Каждый вызов метода добавляет новый узел в топологию, формируя конвейер обработки. Эта топология в итоге становится «дорожной картой» данных: Kafka Streams компилирует её в набор задач (tasks), каждая из которых отвечает за обработку части данных из определённого раздела Kafka. Таким образом, масштабирование достигается естественным образом — за счёт распределения задач по инстансам приложения.
Когда топология определена, создаётся объект KafkaStreams. Именно он связывает логику обработки с реальной инфраструктурой Kafka. Запуск осуществляется вызовом метода start(), после чего приложение начинает читать сообщения из входных топиков, применять трансформации и записывать результаты в выходные топики. Под капотом Kafka Streams автоматически управляет состоянием: создаёт локальные сторы (например, RocksDB), периодически синхронизирует их с changelog-топиками и обрабатывает сбои с помощью механизма восстановления состояния.
Завершение работы должно быть таким же аккуратным, как и запуск. Метод close() позволяет корректно остановить приложение, завершить обработку текущих сообщений и синхронизировать состояние. В продакшене часто используется shutdown hook, чтобы при получении сигнала завершения (например, SIGTERM) приложение Streams успевало завершить все операции.
Таким образом, запуск Kafka Streams можно описать как соединение трёх слоёв: конфигурация, определяющая контекст выполнения; топология, описывающая бизнес-логику обработки; и исполнение, связывающее код с Kafka и обеспечивающее надёжность, масштабирование и отказоустойчивость.
Приложение Streams — это не просто потребитель и продюсер, а полноценный участник экосистемы Kafka, способный мыслить в терминах потоков, состояний и событий, превращая данные в реальном времени в управляемый и воспроизводимый процесс.
Состояние и state stores, оконная обработка (windowing), exactly-once семантика
Когда приложение Kafka Streams начинает работать с состоянием, оно выходит за рамки простой потоковой обработки и превращается в систему, способную помнить контекст. В этом и заключается фундаментальное отличие Kafka Streams от большинства других библиотек — она не просто реагирует на события, а хранит знание о прошлом, делая возможным агрегаты, join-операции и анализ во времени.
Состояние (state) — это локальные данные, которые приложение поддерживает между событиями. Когда выполняется агрегирование, подсчёт или обновление значения по ключу, Kafka Streams сохраняет промежуточный результат в специальном хранилище, называемом state store. Это может быть встроенная база RocksDB, in-memory store или кастомное решение. Каждый инстанс приложения хранит своё состояние локально, что позволяет ему работать автономно и с минимальной задержкой. Но, несмотря на локальность, надёжность обеспечивается через механизм changelog-топиков: каждое изменение состояния записывается в Kafka, что даёт возможность полностью восстановить state при сбое или перемещении задачи на другой узел.
Kafka Streams делает состояние «живым». Это не просто кеш, а часть потокового вычисления. Приложение может напрямую обращаться к локальному стору, использовать интерактивные запросы и даже предоставлять доступ к состоянию внешним системам. Такая архитектура позволяет строить event-driven микросервисы, которые не только реагируют на поток данных, но и опираются на накопленные знания.
Другим ключевым элементом является оконная обработка (windowing). В потоковом мире данные бесконечны, и чтобы их агрегировать, необходимо ограничить время наблюдения. Kafka Streams вводит окна — логические границы, разделяющие поток событий на временные сегменты. Окна бывают скользящие, фиксированные и сдвигаемые. Например, можно подсчитывать количество покупок за каждые 10 минут или находить среднее значение температуры за последние 5 секунд.
Важный момент — окно не просто отсекает время, оно управляет тем, какие события считаются «совместимыми». Каждое событие имеет временную метку, и Streams использует её для определения, к какому окну оно относится. При этом предусмотрена гибкость: можно задавать допустимые задержки (grace period), чтобы учесть события, пришедшие с опозданием, но всё ещё относящиеся к нужному окну.
Работа с состоянием и окнами невозможна без гарантии корректности обработки. Здесь вступает в силу exactly-once семантика — одна из важнейших возможностей Kafka Streams. Она обеспечивает, что каждое сообщение будет обработано строго один раз, даже в случае сбоев, перезапусков или дублированных сообщений.
Механизм exactly-once основан на транзакциях Kafka и согласованной записи в changelog-топики. Каждая операция, затрагивающая состояние и производящая выходные сообщения, выполняется в рамках атомарной транзакции. Если что-то идёт не так — транзакция откатывается, и состояние возвращается в согласованное состояние. Это гарантирует, что ни одно событие не будет потеряно и не будет обработано дважды.
Именно комбинация state stores, окон и exactly-once семантики превращает Kafka Streams в полноценную платформу для построения детерминированных потоковых приложений. Здесь поток не просто обрабатывается — он управляется, обогащается и осмысляется.
Kafka Streams делает возможным создание систем, где каждое событие не просто проходит сквозь поток, а оставляет след — формируя устойчивую, воспроизводимую и надёжную модель данных во времени.
Что такое ksqlDB?
YouTube: Курс по KsqlDB на английском «ksqlDB and Stream Processing Tutorials | ksqlDB 101»
ksqlDB — это эволюция идей Kafka Streams, превращённая в полноценную потоковую базу данных. Если Kafka Streams — это библиотека для разработчиков, то ksqlDB — это инструмент для инженеров и аналитиков, позволяющий описывать потоковую обработку не в коде, а с помощью знакомого SQL. Она создана, чтобы сделать работу с потоками данных такой же естественной, как запросы к реляционным таблицам.
В своей сути ksqlDB строится поверх Kafka и Kafka Streams. Каждый запрос, который вы пишете в виде SQL-команды, компилируется в топологию Streams, выполняемую под капотом. Это значит, что вся надёжность, отказоустойчивость и масштабируемость Kafka Streams автоматически становятся частью вашего SQL-приложения.
Главная идея ksqlDB — рассматривать потоки и таблицы как первоклассных граждан в SQL-мире. Потоки (STREAM) представляют собой последовательность событий, где каждое сообщение фиксирует факт: клик, транзакцию, лог, метрику. Таблицы (TABLE) отражают текущее состояние, агрегаты или материализованные результаты — именно как KTable в Streams. Взаимодействие между ними естественно: поток можно агрегировать в таблицу, а таблицу можно обновлять событиями из потока.
ksqlDB позволяет делать всё то же, что Kafka Streams, но декларативно: фильтровать данные (WHERE), преобразовывать (SELECT, CAST), агрегировать (GROUP BY, COUNT, SUM), соединять потоки (JOIN) и работать с окнами (WINDOW). При этом каждый запрос становится живым — он не возвращает статичный результат, а формирует непрерывный поток обновлений.
В отличие от традиционных баз данных, где запросы завершаются, в ksqlDB они живут во времени. Создавая поток или таблицу через SQL-запрос, вы фактически запускаете постоянное вычисление, которое обновляется с приходом новых данных в Kafka. Все результаты можно записывать обратно в топики, использовать для downstream-систем или даже делать запросы напрямую через REST API.
Кроме того, ksqlDB включает встроенное хранилище состояния. Это значит, что вы можете не только выполнять потоковые операции, но и сохранять результаты, а затем делать к ним запросы, словно к обычной базе данных. Таким образом, ksqlDB объединяет концепции потоковой обработки и транзакционного состояния в одном инструменте.
С точки зрения архитектуры, ksqlDB — это сервис, который подключается к вашему кластеру Kafka, управляет топологиями Streams и поддерживает API для работы с потоками данных. Вы можете запускать его как единый сервер или в распределённом режиме, масштабируя под нагрузку.
ksqlDB — это шаг вперёд в эволюции Kafka: она делает потоковую обработку доступной не только программистам, но и аналитикам, DevOps-инженерам и архитекторам данных. Это SQL-язык, который разговаривает с событиями, а не с таблицами — язык, в котором время становится таким же измерением данных, как строки и столбцы.
С помощью ksqlDB поток превращается в понятную, управляемую и интерактивную структуру, где события живут, изменяются и взаимодействуют — а данные текут так же естественно, как запросы к ним.





















Leave a Reply