ClickHouse — бинарная вставка с внешней схемойClickHouse — binary ingestion with an external schema

Работа с ProtobufWorking with Protobuf

CLI и HTTP-интерфейс (см. предыдущую главу) покрывают текстовые форматы (CSV, JSON) без лишней настройки. Protobuf — другой случай: формат бинарный и типизированный внешней `.proto`-схемой, а не самоописывающийся, поэтому ClickHouse нужно явно сказать, где эта схема лежит и какое сообщение из неё использовать — источник большинства проблем на практике.The CLI and HTTP interface (see the previous chapter) handle text formats (CSV, JSON) with no extra setup. Protobuf is different: it's a binary format typed by an external `.proto` schema, not self-describing — so ClickHouse must be explicitly told where that schema lives and which message from it to use, the source of most practical problems.

частый вопросcommon interview question

Зачем Protobuf вместо CSV/JSONWhy Protobuf over CSV/JSON

Бинарная сериализация компактнее текстовых форматов (нет накладных расходов на разделители, кавычки, повторяющиеся имена полей в каждой строке JSON) и быстрее парсится — на потоке с высокой частотой событий (тики биржи, метрики, логи с тысяч источников) это напрямую снижает нагрузку на сеть и CPU обеих сторон. Строгая типизация схемой `.proto` даёт то, чего CSV не даёт вообще, а JSON даёт лишь частично: контракт между источником и ClickHouse проверяется при генерации кода, а не только при вставке.Binary serialization is more compact than text formats (no overhead from delimiters, quoting, repeated field names on every JSON row) and parses faster — on a high-frequency event stream (exchange ticks, metrics, logs from thousands of sources) that directly cuts network and CPU load on both ends. Strict typing via the `.proto` schema gives something CSV doesn't offer at all and JSON only partially: the contract between the source and ClickHouse is checked at code-generation time, not only at insert time.

Плата за это — схема существует отдельно от данных и должна быть синхронизирована на обеих сторонах: если `.proto`-файл на источнике и файл, который видит ClickHouse, разошлись (устаревшая копия, не тот путь), сериализация/десериализация либо упадёт с ошибкой, либо, что хуже, тихо даст неверные значения (см. грабли).The cost is that the schema lives separately from the data and must stay in sync on both sides: if the `.proto` file on the source and the one ClickHouse sees have diverged (a stale copy, the wrong path), (de)serialization either fails outright or, worse, silently produces wrong values (see gotchas).

Схема .proto и format_schemaThe .proto schema and format_schema

ClickHouse не генерирует схему сам — `.proto`-файл пишется вручную (или переиспользуется существующий из проекта-источника) и должен быть физически доступен серверу ClickHouse по пути, заданному настройкой `format_schema_path` (директория) в `config.d/`, плюс сама настройка вставки указывает конкретный файл и сообщение через `<путь_к_файлу>:<ИмяСообщения>`.ClickHouse doesn't generate the schema itself — the `.proto` file is written by hand (or reused from an existing source-side project) and must be physically reachable by the ClickHouse server at the path set via the `format_schema_path` setting (a directory) in `config.d/`, plus the insert itself specifies the exact file and message via `:`.

// trades.proto
syntax = "proto3";

message Trade {
    int64  ts_millis = 1;
    string symbol    = 2;
    double price     = 3;
    double qty       = 4;
}
<!-- config.d/format_schemas.xml -->
<format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>
# вставка через clickhouse-client, файл + сообщение через двоеточие# insert via clickhouse-client, file + message separated by a colon
cat trades.bin | clickhouse-client \
  --query="INSERT INTO trades FORMAT Protobuf SETTINGS format_schema='trades.proto:Trade'"

Соответствие имён полей `.proto`-сообщения и колонок таблицы — по имени, не по номеру поля; порядок объявления в `.proto` не обязан совпадать с `ORDER BY` или порядком колонок таблицы. Числовые тэги полей (`= 1`, `= 2`) важны только для самого протокола Protobuf (обратная совместимость схемы) — ClickHouse их не использует для маппинга на колонки.Matching between `.proto` message fields and table columns is by name, not by field number; declaration order in `.proto` doesn't need to match `ORDER BY` or the table's column order. The numeric field tags (`= 1`, `= 2`) matter only to the Protobuf wire format itself (schema backward compatibility) — ClickHouse doesn't use them for column mapping.

ProtobufList и интеграция с KafkaProtobufList and Kafka integration

`FORMAT Protobuf` ожидает поток последовательных length-delimited сообщений (стандартный способ упаковать несколько Protobuf-сообщений подряд в один поток байт) — подходит для батч-вставки через CLI/HTTP. `FORMAT ProtobufSingle` — вариант без префикса длины, для случая ровно одного сообщения за раз, что типично для интеграции с Kafka: движок таблицы `Kafka` в ClickHouse читает сообщения топика по одному, и каждое из них уже является отдельной единицей, не частью потока.`FORMAT Protobuf` expects a stream of sequential length-delimited messages (the standard way to pack multiple Protobuf messages back-to-back into one byte stream) — suited to batch insertion via CLI/HTTP. `FORMAT ProtobufSingle` is a variant without the length prefix, for exactly one message at a time, typical for Kafka integration: ClickHouse's `Kafka` table engine reads topic messages one by one, and each is already a standalone unit, not part of a stream.

CREATE TABLE trades_kafka (
    ts_millis Int64,
    symbol    String,
    price     Float64,
    qty       Float64
) ENGINE = Kafka
SETTINGS
    kafka_broker_list = 'kafka1:9092',
    kafka_topic_list = 'trades',
    kafka_group_name = 'clickhouse_trades',
    kafka_format = 'ProtobufSingle',
    format_schema = 'trades.proto:Trade';

Движок `Kafka` — сам по себе не хранилище (аналогично `Distributed`, см. настройку шардов), а источник потока: связка с целевой `MergeTree`-таблицей и `MATERIALIZED VIEW` между ними — стандартный паттерн (`kafka_engine → materialized view → MergeTree`), где именно материализованное представление перекладывает данные из очереди в постоянное хранилище пачками, соблюдая правило батчинга вставок (см. эксплуатацию).The `Kafka` engine isn't storage itself (similar to `Distributed`, see sharding) but a stream source: pairing it with a target `MergeTree` table and a `MATERIALIZED VIEW` in between is the standard pattern (`kafka_engine → materialized view → MergeTree`), where the materialized view is exactly what moves data from the queue into permanent storage in batches, honoring the insert-batching rule (see operations).

ГраблиGotchas

format_schema_path не настроен — тихая запись нулейnot configured — a silent write of zeros

Если путь к директории со схемами не задан или указан неверно, а коллектор всё равно шлёт вставку в формате Protobuf, ClickHouse в части конфигураций может принять запрос без явной ошибки, но десериализовать данные некорректно — поля получают нулевые/пустые значения вместо реальных. Сбой обнаруживается только на этапе чтения результата (числа в таблице неожиданно нули), а не в момент вставки — ровно тот же класс проблемы, что и с любым несовпадением бинарной схемы, только без диагностического сообщения в логе на стороне клиента. Явная проверка перед деплоем коллектора: тестовая вставка с последующим `SELECT`, а не доверие к отсутствию ошибки на INSERT.If the schema directory path isn't set or is wrong, and a collector still sends a Protobuf-formatted insert, ClickHouse in some configurations can accept the request without an explicit error but deserialize the data incorrectly — fields end up zero/empty instead of holding real values. The failure only surfaces when reading the result back (numbers in the table are unexpectedly zero), not at insert time — the same class of problem as any binary schema mismatch, just without a diagnostic message on the client side. A mandatory pre-deploy check for a collector: a test insert followed by a `SELECT`, not trusting the absence of an error on INSERT.

Расхождение .proto-версий на источнике и в ClickHouse.proto version drift between the source and ClickHouse

Protobuf спроектирован быть обратно совместимым по номерам полей, но переименование поля, смена типа (`int32` → `string`) или удаление номера без резервирования (`reserved`) на источнике при устаревшей копии `.proto`-файла на стороне ClickHouse даёт рассинхронизацию, которая проявляется как смещённые или неверно типизированные значения в колонках, а не как ошибка вставки. Схема должна быть единственным источником истины — распространяться из одного места (общий репозиторий/пакет), а не копироваться вручную между проектом-источником и конфигом ClickHouse.Protobuf is designed to be backward-compatible by field number, but renaming a field, changing its type (`int32` → `string`), or removing a number without `reserved`-ing it on the source side while ClickHouse holds a stale copy of the `.proto` file causes a desync that shows up as shifted or mistyped column values, not an insert error. The schema should be a single source of truth — distributed from one place (a shared repo/package), not manually copied between the source project and the ClickHouse config.

ProtobufSingle вместо Protobuf на пакете из нескольких сообщенийinstead of Protobuf on a batch of several messages

Использование `ProtobufSingle` (без длины-префикса) там, где на вход подаётся поток из нескольких сообщений подряд (типичная ошибка при переносе кода с Kafka-потребителя, где каждое сообщение действительно одно, на батч-вставку через CLI/HTTP), приводит к тому, что распознаётся только первое сообщение, а остаток потока трактуется как мусор или обрывает парсинг с ошибкой. `Protobuf` (с длиной) и `ProtobufSingle` (без) — не взаимозаменяемые форматы, выбор определяется тем, что именно на другом конце: поток сообщений или ровно одно.Using `ProtobufSingle` (no length prefix) where the input is actually a stream of several back-to-back messages (a typical mistake when porting code from a Kafka consumer, where each message really is singular, to a batch insert via CLI/HTTP) means only the first message gets parsed, and the rest of the stream is either treated as garbage or aborts parsing with an error. `Protobuf` (length-prefixed) and `ProtobufSingle` (not) aren't interchangeable formats — the choice depends on what's actually on the other end: a stream of messages, or exactly one.