Learning Platform
Глоссарий Troubleshooting
Урок 18.04 · 25 мин
Средний
dockerdata-engineeringclickhouseolapkafka

ClickHouse quickstart: OLAP в compose

ClickHouse — columnar OLAP-БД для аналитики. Junior DE встречает её в трёх контекстах: (1) как target для агрегатов из Spark/Airflow pipeline’а, (2) как backend для дашбордов (Grafana, Superset), (3) как streaming-sink из Kafka через Kafka Engine. Все три кейса требуют локального стенда — и для этого ClickHouse прекрасно живёт в Docker.

В этом уроке поднимем ClickHouse в compose, настроим init-схему, подключим clickhouse-client, и в конце покажем killer-feature: подписку на Kafka topic напрямую из ClickHouse без посредников.


Брокеры, Топики, Партиции — базовая архитектура Kafka

Минимальный compose

services:
  clickhouse:
    image: clickhouse/clickhouse-server:24.10
    container_name: clickhouse
    ports:
      - "8123:8123"   # HTTP
      - "9000:9000"   # native TCP
      - "9009:9009"   # interserver (для кластера)
    environment:
      CLICKHOUSE_USER: default
      CLICKHOUSE_PASSWORD: ''
      CLICKHOUSE_DB: analytics
      CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    volumes:
      - ch-data:/var/lib/clickhouse
      - ch-logs:/var/log/clickhouse-server
      - ./init:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - ch-net

volumes:
  ch-data:
  ch-logs:

networks:
  ch-net:

Что важно:

  • ulimits.nofile — это критично. ClickHouse открывает тысячи file descriptors (один на каждый парт таблицы). Дефолтный лимит Docker (1024) приведёт к ошибкам типа Too many open files. 262144 — рекомендация документации.
  • Три порта: 8123 (HTTP API, для curl/JDBC/HTTP-клиентов), 9000 (native TCP, для clickhouse-client и JDBC), 9009 (interserver replication — не нужен для single-node, но image слушает).
  • CLICKHOUSE_DB: analytics — создаёт БД analytics при первом старте.
  • ./init:/docker-entrypoint-initdb.d — bind mount. ClickHouse при первом запуске выполняет все *.sql файлы из этой директории. Это место для DDL твоей схемы.
WARNING

ВНИМАНИЕ: init-скрипты из /docker-entrypoint-initdb.d выполняются ТОЛЬКО при первой инициализации (когда volume пустой). Если ты добавил .sql после первого запуска — он не выполнится. Решения: (1) docker compose down -v и снова up (потеряешь данные), (2) clickhouse-client < init/02-new.sql напрямую (применить вручную), (3) использовать миграции (golang-migrate, dbt и т.д.).


Init schema

Создай файл ./init/01-schema.sql:

CREATE TABLE IF NOT EXISTS analytics.events
(
    event_time DateTime,
    user_id UInt64,
    event_type LowCardinality(String),
    properties String,
    ingested_at DateTime DEFAULT now()
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, user_id);

CREATE TABLE IF NOT EXISTS analytics.events_hourly
(
    hour DateTime,
    event_type LowCardinality(String),
    cnt UInt64,
    unique_users UInt64
)
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (hour, event_type);

Несколько ClickHouse-патернов, которые junior должен запомнить:

  • MergeTree() — основной engine. Колоночное хранение, сортировка по ORDER BY-ключу, партиции по PARTITION BY. Базовый строительный блок.
  • LowCardinality(String) — оптимизация для колонок с малой кардинальностью (типы событий, страны, статусы). ClickHouse строит словарь и хранит индексы — ускорение GROUP BY в десятки раз.
  • PARTITION BY toYYYYMM(event_time) — каждый месяц отдельная партиция. Облегчает retention (DROP PARTITION) и speed-up filter’ов.
  • SummingMergeTree — engine для агрегатов. Когда есть две строки с одинаковым ORDER BY-ключом, ClickHouse при merge суммирует UInt-колонки. Удобно для realtime-агрегации.
ClickHouse: HTTP API и native TCP
HTTP :8123curl, dbt, BIHTTP API. Удобно для скриптов, dbt, BI-tools (Grafana, Superset, Tableau). Каждый запрос -- независимый HTTP.
Native TCP :9000clickhouse-clientNative бинарный протокол. Используется clickhouse-client, ClickHouse JDBC, ODBC. Производительнее HTTP для bulk-операций (insert miljonov строк).
MergeTree tablesMergeTree -- основной engine. Сорт по ORDER BY, партиции -- директории на диске.
Materialized ViewsMaterialized View -- автоматически пересчитывается при INSERT в source table. Используется для realtime агрегатов.
Kafka EngineEngine = Kafka -- псевдо-таблица. Каждый SELECT забирает новые сообщения. Обычно используется через Materialized View.

Подключение клиента

Два способа:

1) clickhouse-client внутри контейнера

docker compose exec clickhouse clickhouse-client --host clickhouse -u default

# Внутри REPL:
clickhouse :) SHOW DATABASES;
clickhouse :) USE analytics;
clickhouse :) SHOW TABLES;
clickhouse :) DESCRIBE TABLE events;
clickhouse :) SELECT count() FROM events;
clickhouse :) exit;

--host clickhouse тут не обязательно (внутри контейнера localhost тоже работает), но это привычка для consistency: тот же флаг работает и снаружи.

2) HTTP API через curl с хоста

curl 'http://localhost:8123/' --data-binary "SELECT version()"
# 24.10.2.80

curl 'http://localhost:8123/?database=analytics' \
  --data-binary "INSERT INTO events VALUES (now(), 42, 'click', '{}', now())"

curl 'http://localhost:8123/?database=analytics' \
  --data-binary "SELECT * FROM events LIMIT 5 FORMAT JSONEachRow"

HTTP-API подходит для quick-scripts и BI-инструментов. Для bulk-insert (миллионы строк) используй native TCP — он эффективнее.


INSERT и базовый SELECT

-- Внутри clickhouse-client

INSERT INTO analytics.events (event_time, user_id, event_type, properties) VALUES
  (now() - INTERVAL 5 MINUTE, 1, 'click', '{"button": "buy"}'),
  (now() - INTERVAL 4 MINUTE, 1, 'view', '{"page": "/home"}'),
  (now() - INTERVAL 3 MINUTE, 2, 'click', '{"button": "subscribe"}'),
  (now() - INTERVAL 2 MINUTE, 3, 'view', '{"page": "/pricing"}'),
  (now() - INTERVAL 1 MINUTE, 2, 'click', '{"button": "buy"}');

SELECT event_type, count() AS cnt
FROM analytics.events
GROUP BY event_type
ORDER BY cnt DESC;

-- event_type  | cnt
-- click       |   3
-- view        |   2

ClickHouse оптимизирован для аналитических запросов: GROUP BY с millions/billions строк проходит в десятки раз быстрее, чем в Postgres. Но он плох для частых UPDATE и DELETE — это OLAP, а не OLTP.


Kafka Engine: streaming в ClickHouse

Killer-feature ClickHouse для DE — Kafka Engine: создаёшь “таблицу”, которая на самом деле является consumer’ом Kafka-topic’а. Каждый SELECT забирает новые сообщения, и через Materialized View ты автоматически переливаешь их в обычную MergeTree-таблицу.

Допустим, у тебя есть Kafka из урока 16-02 с topic events. Подключим ClickHouse:

-- 1) Псевдо-таблица: consumer Kafka topic
CREATE TABLE analytics.events_kafka
(
    event_time DateTime,
    user_id UInt64,
    event_type String,
    properties String
)
ENGINE = Kafka()
SETTINGS
    kafka_broker_list = 'kafka:9092',
    kafka_topic_list = 'events',
    kafka_group_name = 'clickhouse-events',
    kafka_format = 'JSONEachRow',
    kafka_num_consumers = 1;

-- 2) Materialized View: автоматически переливает из Kafka в обычную таблицу
CREATE MATERIALIZED VIEW analytics.events_mv TO analytics.events AS
SELECT
    event_time,
    user_id,
    event_type,
    properties
FROM analytics.events_kafka;

Что произошло:

  • events_kafka — это не таблица с данными, а консьюмер. ClickHouse подключается к Kafka на kafka:9092 (по DNS-имени в compose-сети) и читает с offset’а.
  • events_mv — Materialized View. Каждый раз, когда events_kafka забирает новую партию из Kafka, MV пересчитывает свой SELECT и пишет результат в events.
  • В итоге ты получаешь streaming-pipeline без Airflow, без Spark Streaming. Просто SQL.

Теперь любой producer (включая kafka-console-producer.sh из прошлого урока) пишет в events topic JSON-сообщения, и через несколько секунд они появляются в analytics.events.

TIP

Чтобы Kafka Engine работал, ClickHouse и Kafka должны быть в одной compose-сети. Объедини через external networks или подключи оба сервиса к общей de-net. См. урок 16-05 про networking-патерны.


Реальный DE-кейс: streaming user events

Допустим, твоё web-приложение шлёт события в Kafka. Ты хочешь:

  1. Сохранять все события “сырыми” в ClickHouse для долгого retention.
  2. Агрегировать в hourly-bucket’ах для дашборда.

Pipeline:

-- Сырые события из Kafka -> events
-- (уже сделали выше: events_kafka + events_mv)

-- Hourly агрегаты: ещё одна MaterializedView
CREATE MATERIALIZED VIEW analytics.events_hourly_mv
TO analytics.events_hourly AS
SELECT
    toStartOfHour(event_time) AS hour,
    event_type,
    count() AS cnt,
    uniqExact(user_id) AS unique_users
FROM analytics.events
GROUP BY hour, event_type;

Теперь у тебя:

  • events_kafka — псевдо-таблица, читает Kafka
  • events — MergeTree со всеми сырыми событиями
  • events_hourly — SummingMergeTree с агрегатами по часу

И всё это без единой строчки внешнего кода. Только SQL и три MV.


Подключение к Grafana / BI

ClickHouse HTTP API на 8123 — это всё, что нужно для Grafana. Источник данных:

URL: http://clickhouse:8123  (внутри compose)
URL: http://localhost:8123   (с хоста)
Database: analytics
User: default
Password: (пусто)

Запросы в Grafana — обычный ClickHouse SQL:

SELECT
    hour AS time,
    event_type,
    cnt
FROM analytics.events_hourly
WHERE hour > now() - INTERVAL 24 HOUR
ORDER BY hour;

Попробуй сам

# 1. Подготовь init/01-schema.sql (см. выше)
mkdir -p init
# Создай файл init/01-schema.sql вручную

# 2. Запусти
docker compose up -d

# 3. Проверь
docker compose exec clickhouse clickhouse-client \
  --query "SHOW TABLES FROM analytics"
# events
# events_hourly

# 4. INSERT тестовые данные
docker compose exec clickhouse clickhouse-client --query "
INSERT INTO analytics.events VALUES
  (now(), 1, 'click', '{}', now()),
  (now(), 2, 'view', '{}', now()),
  (now(), 1, 'view', '{}', now())"

# 5. SELECT через HTTP
curl 'http://localhost:8123/?database=analytics' \
  --data-binary "SELECT count() FROM events"

# 6. Bulk insert большого dataset'а (1M строк) для теста производительности
docker compose exec clickhouse clickhouse-client --query "
INSERT INTO analytics.events
SELECT
  now() - intDiv(number, 1000) * 60,
  number % 1000,
  ['click', 'view', 'scroll'][1 + number % 3],
  '{}',
  now()
FROM numbers(1000000)"

# 7. Тяжёлый аналитический запрос
docker compose exec clickhouse clickhouse-client --query "
SELECT event_type, count(), uniqExact(user_id)
FROM analytics.events
GROUP BY event_type"
# 0.05 секунды на 1M строк -- ClickHouse в действии

# 8. Cleanup
docker compose down -v

Что ещё знать

  • Cluster mode: для prod ClickHouse деплоится в кластере с replication через Zookeeper или Keeper. compose-файлы для кластера сложнее (3+ shards, replicas). В compose обычно одна нода.
  • MinIO как backend: ClickHouse умеет хранить парты в S3 (s3 disk type). Это снижает стоимость для холодных данных.
  • ProxySQL / chproxy: на проде перед ClickHouse часто ставят chproxy для query routing, rate limiting, user management.
  • Backup: clickhouse-backup — утилита для бэкапов парт-файлов в S3.

Но это всё за пределами junior-роли. На данном этапе достаточно: compose -> init.sql -> clickhouse-client -> SELECT.


Проверка знанийKnowledge check
Ты добавил новую таблицу в init/02-new-table.sql, перезапустил docker compose restart clickhouse, но таблица не создалась. SHOW TABLES не показывает её. Что произошло?
ОтветAnswer
Init-скрипты из /docker-entrypoint-initdb.d выполняются только при ПЕРВОЙ инициализации, когда volume ch-data пустой. После первого старта volume содержит существующую metadata, и ClickHouse пропускает выполнение скриптов. docker compose restart не пересоздаёт volume. Решения: (1) docker compose down -v и up -- удалит данные и переинициализирует; (2) выполнить SQL вручную: docker compose exec clickhouse clickhouse-client < init/02-new-table.sql; (3) использовать systemic migration tool (golang-migrate, atlas, dbt seed). Для prod вариант (3) обязателен -- init-скрипты не подходят для эволюции схемы.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Почему для ClickHouse в Docker критично установить ulimits.nofile с большим значением (например, 262144)?

Закончили урок?

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

Войдите чтобы оценить урок

Прогресс модуля
0 из 5