feat(infra): поднят кластер ClickHouse из двух шардов и keeper
- Зачем:
- этап 1 спеки требует стенд, поднимаемый одной командой; кластер —
единственный режим, выключателя «без кластера» нет (issue #12).
- Что:
- compose.yaml: две ноды ClickHouse и отдельный clickhouse-keeper на
зафиксированном LTS-образе 26.3.17.56, порты только на 127.0.0.1.
- infra/clickhouse: общее описание кластера, подключение к keeper и
отдельные макросы shard и replica для каждой ноды.
- scripts/clickhouse-smoke.sh: восемь проверок ON CLUSTER от описания
кластера до удаления временных таблиц, вывод по-русски.
- tests/smoke-guards.sh: три проверки самой smoke-команды —
ограниченная аварийная очистка, обработка прерывания, окружение keeper.
- README.md: быстрый старт, роли нод, обоснование выбора версии.
- Проверка:
- make up && make smoke && make smoke-guards && make clean
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
COMPOSE_PROJECT_NAME=clickstream-data-platform
|
||||||
|
CLICKHOUSE_IMAGE=clickhouse/clickhouse-server:26.3.17.56
|
||||||
|
CLICKHOUSE_01_HTTP_PORT=28123
|
||||||
|
CLICKHOUSE_01_TCP_PORT=29000
|
||||||
|
CLICKHOUSE_02_HTTP_PORT=28124
|
||||||
|
CLICKHOUSE_02_TCP_PORT=29001
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
COMPOSE := docker compose
|
||||||
|
|
||||||
|
.PHONY: up down clean ps logs smoke smoke-guards
|
||||||
|
|
||||||
|
up:
|
||||||
|
$(COMPOSE) up --detach --wait --wait-timeout 180
|
||||||
|
|
||||||
|
down:
|
||||||
|
$(COMPOSE) down --remove-orphans
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(COMPOSE) down --volumes --remove-orphans
|
||||||
|
|
||||||
|
ps:
|
||||||
|
$(COMPOSE) ps
|
||||||
|
|
||||||
|
logs:
|
||||||
|
$(COMPOSE) logs --follow
|
||||||
|
|
||||||
|
smoke:
|
||||||
|
./scripts/clickhouse-smoke.sh
|
||||||
|
|
||||||
|
smoke-guards:
|
||||||
|
./tests/smoke-guards.sh
|
||||||
@@ -8,9 +8,64 @@ clickhouse-keeper), Airflow, Superset. Преемник учебного сте
|
|||||||
|
|
||||||
Репозиторий строится по спеке
|
Репозиторий строится по спеке
|
||||||
[«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md).
|
[«Боевой реализм стенда (v2)»](docs/specs/2026-07-30-stand-v2-realism.md).
|
||||||
Работающего стенда пока нет: здесь только спека, исследование формата данных и
|
Сейчас готов первый элемент каркаса: кластер ClickHouse из двух шардов и
|
||||||
контракт работы ([AGENTS.md](AGENTS.md)). Быстрый старт появится вместе с
|
отдельного clickhouse-keeper.
|
||||||
этапом «Каркас стенда».
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
Нужны Docker с Compose и свободные порты `28123`, `28124`, `29000`, `29001`.
|
||||||
|
Настройки портов и образа лежат в `.env` и не попадают в git:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
make up
|
||||||
|
make smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
`make up` поднимает единственный режим стенда: два шарда ClickHouse по одной
|
||||||
|
реплике и отдельный keeper. Команда ждёт здорового состояния всех трёх
|
||||||
|
контейнеров. Нода `clickhouse-01` служит инициатором DDL и позже будет точкой
|
||||||
|
подключения Airflow. К `clickhouse-02` позже подключится Superset.
|
||||||
|
|
||||||
|
`make smoke` одной командой проверяет описание кластера, макросы нод, связь с
|
||||||
|
keeper, создание `ReplicatedMergeTree` и `Distributed` через `ON CLUSTER`,
|
||||||
|
путь в keeper из макроса, вставку через первую ноду и чтение со второй,
|
||||||
|
очередь распределённых DDL и удаление временных таблиц. Скрипт печатает каждый
|
||||||
|
шаг и зелёный результат по-русски.
|
||||||
|
|
||||||
|
`make smoke-guards` проверяет саму smoke-команду: аварийную очистку, обработку
|
||||||
|
прерывания, подсказку о незапущенном стенде и окружение keeper.
|
||||||
|
|
||||||
|
Остановить контейнеры можно командой `make down`. Для холодного старта с
|
||||||
|
удалением данных используйте `make clean`.
|
||||||
|
|
||||||
|
HTTP-интерфейсы доступны только с локальной машины:
|
||||||
|
|
||||||
|
- нода 1 — `http://127.0.0.1:28123`;
|
||||||
|
- нода 2 — `http://127.0.0.1:28124`.
|
||||||
|
|
||||||
|
У локального учебного кластера нет пароля: ноды используют общего пользователя
|
||||||
|
`default` для запросов `Distributed`. Порты поэтому привязаны к `127.0.0.1` и
|
||||||
|
не открыты во внешнюю сеть.
|
||||||
|
|
||||||
|
В бою перед репликами ClickHouse обычно был бы балансировщик. Здесь в каждом
|
||||||
|
шарде одна реплика, поэтому балансировать нечего. Балансировщик и топология
|
||||||
|
2×2 намеренно не входят в стенд.
|
||||||
|
|
||||||
|
Конфигурация сверена 30 июля 2026 года с официальной документацией ClickHouse:
|
||||||
|
[настройками сервера](https://clickhouse.com/docs/operations/server-configuration-parameters/settings),
|
||||||
|
[Keeper](https://clickhouse.com/docs/guides/oss/deployment-and-scaling/keeper/),
|
||||||
|
[ReplicatedMergeTree](https://clickhouse.com/docs/engines/table-engines/mergetree-family/replication),
|
||||||
|
[ON CLUSTER](https://clickhouse.com/docs/sql-reference/distributed-ddl) и
|
||||||
|
[Distributed](https://clickhouse.com/docs/engines/table-engines/special/distributed).
|
||||||
|
Описание кластера задаётся через `remote_servers`, макросы — через `macros`,
|
||||||
|
подключение к keeper — через `zookeeper`; путь `ReplicatedMergeTree` содержит
|
||||||
|
`{shard}` и `{replica}`, а `Distributed` получает имя кластера, базу, локальную
|
||||||
|
таблицу и ключ шардирования. Макросы выбраны, чтобы один DDL через `ON CLUSTER`
|
||||||
|
создавал отдельный путь каждого шарда без вписанных вручную значений. Для
|
||||||
|
образа зафиксирован точный текущий
|
||||||
|
[LTS-выпуск 26.3.17.56](https://github.com/ClickHouse/ClickHouse/releases/tag/v26.3.17.56-lts);
|
||||||
|
серверы и keeper используют один образ.
|
||||||
|
|
||||||
## Что здесь будет
|
## Что здесь будет
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
name: ${COMPOSE_PROJECT_NAME:-clickstream-data-platform}
|
||||||
|
|
||||||
|
x-clickhouse-nofile: &clickhouse-nofile
|
||||||
|
nofile:
|
||||||
|
soft: 262144
|
||||||
|
hard: 262144
|
||||||
|
|
||||||
|
x-clickhouse-common: &clickhouse-common
|
||||||
|
image: ${CLICKHOUSE_IMAGE:-clickhouse/clickhouse-server:26.3.17.56}
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Стенд локальный: нодам нужен общий пользователь без пароля для Distributed.
|
||||||
|
CLICKHOUSE_SKIP_USER_SETUP: "1"
|
||||||
|
depends_on:
|
||||||
|
clickhouse-keeper:
|
||||||
|
condition: service_healthy
|
||||||
|
ulimits: *clickhouse-nofile
|
||||||
|
mem_limit: 1g
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "clickhouse-client --host 127.0.0.1 --query 'SELECT 1' >/dev/null 2>&1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 30
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
services:
|
||||||
|
clickhouse-keeper:
|
||||||
|
image: ${CLICKHOUSE_IMAGE:-clickhouse/clickhouse-server:26.3.17.56}
|
||||||
|
restart: unless-stopped
|
||||||
|
user: clickhouse
|
||||||
|
ulimits: *clickhouse-nofile
|
||||||
|
entrypoint:
|
||||||
|
- clickhouse
|
||||||
|
- keeper
|
||||||
|
- --config-file=/etc/clickhouse-keeper/keeper_config.xml
|
||||||
|
volumes:
|
||||||
|
- ./infra/clickhouse/keeper/keeper_config.xml:/etc/clickhouse-keeper/keeper_config.xml:ro
|
||||||
|
- clickhouse_keeper_data:/var/lib/clickhouse
|
||||||
|
mem_limit: 512m
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "clickhouse keeper-client --host 127.0.0.1 --port 9181 --query ruok | grep -q imok"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 30
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
# Инициатор DDL и будущее подключение Airflow.
|
||||||
|
clickhouse-01:
|
||||||
|
<<: *clickhouse-common
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${CLICKHOUSE_01_HTTP_PORT:-28123}:8123"
|
||||||
|
- "127.0.0.1:${CLICKHOUSE_01_TCP_PORT:-29000}:9000"
|
||||||
|
volumes:
|
||||||
|
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
|
||||||
|
- ./infra/clickhouse/config.d/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml:ro
|
||||||
|
- clickhouse_01_data:/var/lib/clickhouse
|
||||||
|
|
||||||
|
# Будущее подключение Superset.
|
||||||
|
clickhouse-02:
|
||||||
|
<<: *clickhouse-common
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${CLICKHOUSE_02_HTTP_PORT:-28124}:8123"
|
||||||
|
- "127.0.0.1:${CLICKHOUSE_02_TCP_PORT:-29001}:9000"
|
||||||
|
volumes:
|
||||||
|
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
|
||||||
|
- ./infra/clickhouse/config.d/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml:ro
|
||||||
|
- clickhouse_02_data:/var/lib/clickhouse
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
clickhouse_keeper_data:
|
||||||
|
clickhouse_01_data:
|
||||||
|
clickhouse_02_data:
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<clickhouse>
|
||||||
|
<!--
|
||||||
|
Схема проверена по документации ClickHouse 26.3: одинаковый
|
||||||
|
remote_servers на нодах, координатор в zookeeper, DDL через keeper.
|
||||||
|
-->
|
||||||
|
<remote_servers>
|
||||||
|
<clickstream_cluster>
|
||||||
|
<shard>
|
||||||
|
<internal_replication>true</internal_replication>
|
||||||
|
<replica>
|
||||||
|
<host>clickhouse-01</host>
|
||||||
|
<port>9000</port>
|
||||||
|
</replica>
|
||||||
|
</shard>
|
||||||
|
<shard>
|
||||||
|
<internal_replication>true</internal_replication>
|
||||||
|
<replica>
|
||||||
|
<host>clickhouse-02</host>
|
||||||
|
<port>9000</port>
|
||||||
|
</replica>
|
||||||
|
</shard>
|
||||||
|
</clickstream_cluster>
|
||||||
|
</remote_servers>
|
||||||
|
|
||||||
|
<zookeeper>
|
||||||
|
<node>
|
||||||
|
<host>clickhouse-keeper</host>
|
||||||
|
<port>9181</port>
|
||||||
|
</node>
|
||||||
|
</zookeeper>
|
||||||
|
|
||||||
|
<distributed_ddl>
|
||||||
|
<path>/clickhouse/task_queue/ddl</path>
|
||||||
|
</distributed_ddl>
|
||||||
|
</clickhouse>
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<clickhouse>
|
||||||
|
<macros>
|
||||||
|
<shard>01</shard>
|
||||||
|
<replica>clickhouse-01</replica>
|
||||||
|
</macros>
|
||||||
|
</clickhouse>
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<clickhouse>
|
||||||
|
<macros>
|
||||||
|
<shard>02</shard>
|
||||||
|
<replica>clickhouse-02</replica>
|
||||||
|
</macros>
|
||||||
|
</clickhouse>
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<clickhouse>
|
||||||
|
<!-- Отдельный keeper использует тот же выпуск ClickHouse, что и серверы. -->
|
||||||
|
<logger>
|
||||||
|
<level>information</level>
|
||||||
|
<console>true</console>
|
||||||
|
</logger>
|
||||||
|
<listen_host>0.0.0.0</listen_host>
|
||||||
|
|
||||||
|
<keeper_server>
|
||||||
|
<tcp_port>9181</tcp_port>
|
||||||
|
<server_id>1</server_id>
|
||||||
|
<log_storage_path>/var/lib/clickhouse/coordination/log</log_storage_path>
|
||||||
|
<snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>
|
||||||
|
<coordination_settings>
|
||||||
|
<operation_timeout_ms>10000</operation_timeout_ms>
|
||||||
|
<session_timeout_ms>30000</session_timeout_ms>
|
||||||
|
</coordination_settings>
|
||||||
|
<raft_configuration>
|
||||||
|
<server>
|
||||||
|
<id>1</id>
|
||||||
|
<hostname>clickhouse-keeper</hostname>
|
||||||
|
<port>9234</port>
|
||||||
|
</server>
|
||||||
|
</raft_configuration>
|
||||||
|
</keeper_server>
|
||||||
|
</clickhouse>
|
||||||
|
|
||||||
Executable
+178
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
readonly CLUSTER="clickstream_cluster"
|
||||||
|
readonly LOCAL_TABLE="smoke_replicated_local"
|
||||||
|
readonly DISTRIBUTED_TABLE="smoke_distributed"
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
docker compose --project-directory "$ROOT_DIR" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
query() {
|
||||||
|
local service="$1"
|
||||||
|
local sql="$2"
|
||||||
|
compose exec -T "$service" clickhouse-client --query "$sql"
|
||||||
|
}
|
||||||
|
|
||||||
|
query_with_timeout() {
|
||||||
|
local timeout_seconds="$1"
|
||||||
|
local service="$2"
|
||||||
|
local sql="$3"
|
||||||
|
timeout --foreground "${timeout_seconds}s" \
|
||||||
|
docker compose --project-directory "$ROOT_DIR" exec -T "$service" \
|
||||||
|
clickhouse-client --query "$sql"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'ОШИБКА: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal() {
|
||||||
|
local expected="$1"
|
||||||
|
local actual="$2"
|
||||||
|
local message="$3"
|
||||||
|
[[ "$actual" == "$expected" ]] || fail "$message: ожидалось «$expected», получено «$actual»"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_stand_running() {
|
||||||
|
local running_services
|
||||||
|
local service
|
||||||
|
running_services="$(compose ps --status running --services 2>/dev/null || true)"
|
||||||
|
for service in clickhouse-keeper clickhouse-01 clickhouse-02; do
|
||||||
|
if ! grep -qx "$service" <<<"$running_services"; then
|
||||||
|
fail "стенд запущен не полностью; выполните make up и дождитесь здорового состояния контейнеров"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_tables() {
|
||||||
|
local cleanup_failed=0
|
||||||
|
query_with_timeout 5 clickhouse-01 "DROP TABLE IF EXISTS default.${DISTRIBUTED_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null 2>&1 || cleanup_failed=1
|
||||||
|
query_with_timeout 5 clickhouse-01 "DROP TABLE IF EXISTS default.${LOCAL_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null 2>&1 || cleanup_failed=1
|
||||||
|
return "$cleanup_failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
on_exit() {
|
||||||
|
local status=$?
|
||||||
|
trap - EXIT
|
||||||
|
if [[ "$status" -ne 0 ]]; then
|
||||||
|
printf 'Сбой проверки: выполняется быстрая очистка временных таблиц...\n' >&2
|
||||||
|
fi
|
||||||
|
cleanup_tables || printf 'ПРЕДУПРЕЖДЕНИЕ: очистка не завершена; после восстановления стенда повторите make smoke.\n' >&2
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
on_signal() {
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
printf 'Проверка прервана пользователем: выполняется быстрая очистка временных таблиц...\n' >&2
|
||||||
|
cleanup_tables || printf 'ПРЕДУПРЕЖДЕНИЕ: очистка не завершена; после восстановления стенда повторите make smoke.\n' >&2
|
||||||
|
exit 130
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_ddl_queue_completed() {
|
||||||
|
local service="$1"
|
||||||
|
local phase="$2"
|
||||||
|
local queue_state
|
||||||
|
local total
|
||||||
|
local unfinished
|
||||||
|
queue_state="$(query "$service" "SELECT count(), countIf(status NOT IN ('Finished', 'Removing')) FROM system.distributed_ddl_queue FORMAT TSV")"
|
||||||
|
total="${queue_state%%$'\t'*}"
|
||||||
|
unfinished="${queue_state##*$'\t'}"
|
||||||
|
[[ "$total" -gt 0 ]] || fail "на ${service} очередь DDL пуста ${phase}"
|
||||||
|
assert_equal "0" "$unfinished" "на ${service} есть незавершённые DDL ${phase}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_stand_running
|
||||||
|
trap on_exit EXIT
|
||||||
|
trap on_signal INT TERM
|
||||||
|
|
||||||
|
printf 'Проверка 1/8: описание кластера одинаково на обеих нодах...\n'
|
||||||
|
cluster_sql="SELECT cluster, shard_num, replica_num, host_name, port FROM system.clusters WHERE cluster = '${CLUSTER}' ORDER BY shard_num, replica_num FORMAT TSV"
|
||||||
|
cluster_01="$(query clickhouse-01 "$cluster_sql")"
|
||||||
|
cluster_02="$(query clickhouse-02 "$cluster_sql")"
|
||||||
|
expected_cluster=$'clickstream_cluster\t1\t1\tclickhouse-01\t9000\nclickstream_cluster\t2\t1\tclickhouse-02\t9000'
|
||||||
|
assert_equal "$expected_cluster" "$cluster_01" "неверная топология на первой ноде"
|
||||||
|
assert_equal "$expected_cluster" "$cluster_02" "неверная топология на второй ноде"
|
||||||
|
printf 'ЗЕЛЁНО: обе ноды видят ожидаемые два шарда: clickhouse-01 и clickhouse-02.\n'
|
||||||
|
|
||||||
|
printf 'Проверка 2/8: у нод разные макросы shard и replica...\n'
|
||||||
|
macros_sql="SELECT macro, substitution FROM system.macros WHERE macro IN ('shard', 'replica') ORDER BY macro FORMAT TSV"
|
||||||
|
macros_01="$(query clickhouse-01 "$macros_sql")"
|
||||||
|
macros_02="$(query clickhouse-02 "$macros_sql")"
|
||||||
|
assert_equal $'replica\tclickhouse-01\nshard\t01' "$macros_01" "неверные макросы первой ноды"
|
||||||
|
assert_equal $'replica\tclickhouse-02\nshard\t02' "$macros_02" "неверные макросы второй ноды"
|
||||||
|
[[ "$macros_01" != "$macros_02" ]] || fail "макросы нод не должны совпадать"
|
||||||
|
printf 'ЗЕЛЁНО: clickhouse-01=(shard 01, replica clickhouse-01), clickhouse-02=(shard 02, replica clickhouse-02).\n'
|
||||||
|
|
||||||
|
printf 'Проверка 3/8: keeper отвечает обеим нодам...\n'
|
||||||
|
query clickhouse-01 "SELECT name FROM system.zookeeper WHERE path = '/' ORDER BY name FORMAT Null"
|
||||||
|
query clickhouse-02 "SELECT name FROM system.zookeeper WHERE path = '/' ORDER BY name FORMAT Null"
|
||||||
|
printf 'ЗЕЛЁНО: system.zookeeper доступна с обеих нод.\n'
|
||||||
|
|
||||||
|
cleanup_tables || fail "не удалось очистить объекты предыдущего запуска"
|
||||||
|
|
||||||
|
printf 'Проверка 4/8: ReplicatedMergeTree создаётся через ON CLUSTER...\n'
|
||||||
|
query clickhouse-01 "
|
||||||
|
CREATE TABLE default.${LOCAL_TABLE} ON CLUSTER ${CLUSTER}
|
||||||
|
(
|
||||||
|
ClientID UInt64,
|
||||||
|
value String
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/${LOCAL_TABLE}',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY ClientID
|
||||||
|
" >/dev/null
|
||||||
|
tables_sql="SELECT name, engine FROM system.tables WHERE database = 'default' AND name = '${LOCAL_TABLE}' FORMAT TSV"
|
||||||
|
assert_equal $'smoke_replicated_local\tReplicatedMergeTree' "$(query clickhouse-01 "$tables_sql")" "локальная таблица не создана на первой ноде"
|
||||||
|
assert_equal $'smoke_replicated_local\tReplicatedMergeTree' "$(query clickhouse-02 "$tables_sql")" "локальная таблица не создана на второй ноде"
|
||||||
|
printf 'ЗЕЛЁНО: ReplicatedMergeTree видна в system.tables на обеих нодах.\n'
|
||||||
|
|
||||||
|
printf 'Проверка 5/8: путь в keeper собран из макроса shard...\n'
|
||||||
|
path_sql="SELECT zookeeper_path, replica_name FROM system.replicas WHERE database = 'default' AND table = '${LOCAL_TABLE}' FORMAT TSV"
|
||||||
|
assert_equal "/clickhouse/tables/01/${LOCAL_TABLE}"$'\t'"clickhouse-01" "$(query clickhouse-01 "$path_sql")" "неверные путь или имя реплики на первой ноде"
|
||||||
|
assert_equal "/clickhouse/tables/02/${LOCAL_TABLE}"$'\t'"clickhouse-02" "$(query clickhouse-02 "$path_sql")" "неверные путь или имя реплики на второй ноде"
|
||||||
|
printf 'ЗЕЛЁНО: пути собраны из shard (/01/ и /02/), имя реплики собрано из макроса replica.\n'
|
||||||
|
|
||||||
|
printf 'Проверка 6/8: Distributed создаётся ON CLUSTER и передаёт данные между нодами...\n'
|
||||||
|
query clickhouse-01 "
|
||||||
|
CREATE TABLE default.${DISTRIBUTED_TABLE} ON CLUSTER ${CLUSTER}
|
||||||
|
AS default.${LOCAL_TABLE}
|
||||||
|
ENGINE = Distributed(
|
||||||
|
'${CLUSTER}',
|
||||||
|
'default',
|
||||||
|
'${LOCAL_TABLE}',
|
||||||
|
cityHash64(ClientID)
|
||||||
|
)
|
||||||
|
" >/dev/null
|
||||||
|
distributed_sql="SELECT engine FROM system.tables WHERE database = 'default' AND name = '${DISTRIBUTED_TABLE}' FORMAT TSVRaw"
|
||||||
|
assert_equal "Distributed" "$(query clickhouse-01 "$distributed_sql")" "Distributed-таблица не создана на первой ноде"
|
||||||
|
assert_equal "Distributed" "$(query clickhouse-02 "$distributed_sql")" "Distributed-таблица не создана на второй ноде"
|
||||||
|
query clickhouse-01 "INSERT INTO default.${LOCAL_TABLE} VALUES (42, 'из первой ноды')"
|
||||||
|
read_back="$(query clickhouse-02 "SELECT ClientID, value FROM default.${DISTRIBUTED_TABLE} WHERE ClientID = 42 FORMAT TSV")"
|
||||||
|
assert_equal $'42\tиз первой ноды' "$read_back" "вторая нода не прочитала вставленную строку"
|
||||||
|
query clickhouse-01 "INSERT INTO default.${DISTRIBUTED_TABLE} SETTINGS distributed_foreground_insert = 1 SELECT number + 1000, 'через Distributed' FROM numbers(16)"
|
||||||
|
sharding_sql="SELECT countIf(_shard_num != cityHash64(ClientID) % 2 + 1), uniqExact(_shard_num) FROM default.${DISTRIBUTED_TABLE} WHERE value = 'через Distributed' FORMAT TSV"
|
||||||
|
assert_equal $'0\t2' "$(query clickhouse-02 "$sharding_sql")" "Distributed использует неверный ключ шардирования"
|
||||||
|
printf 'ЗЕЛЁНО: локальная строка первой ноды читается со второй; ключ cityHash64(ClientID) разложил строки по двум шардам.\n'
|
||||||
|
|
||||||
|
printf 'Проверка 7/8: в очереди распределённых DDL нет незавершённых заданий...\n'
|
||||||
|
assert_ddl_queue_completed clickhouse-01 'после CREATE'
|
||||||
|
assert_ddl_queue_completed clickhouse-02 'после CREATE'
|
||||||
|
printf 'ЗЕЛЁНО: очередь содержит задания CREATE, незавершённых среди них нет.\n'
|
||||||
|
|
||||||
|
printf 'Проверка 8/8: временные таблицы удаляются через ON CLUSTER...\n'
|
||||||
|
query clickhouse-01 "DROP TABLE default.${DISTRIBUTED_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null
|
||||||
|
query clickhouse-01 "DROP TABLE default.${LOCAL_TABLE} ON CLUSTER ${CLUSTER} SYNC" >/dev/null
|
||||||
|
remaining_sql="SELECT count() FROM system.tables WHERE database = 'default' AND name IN ('${LOCAL_TABLE}', '${DISTRIBUTED_TABLE}') FORMAT TSVRaw"
|
||||||
|
assert_equal "0" "$(query clickhouse-01 "$remaining_sql")" "временные таблицы остались на первой ноде"
|
||||||
|
assert_equal "0" "$(query clickhouse-02 "$remaining_sql")" "временные таблицы остались на второй ноде"
|
||||||
|
assert_ddl_queue_completed clickhouse-01 'после DROP'
|
||||||
|
assert_ddl_queue_completed clickhouse-02 'после DROP'
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
printf 'ЗЕЛЁНО: временные таблицы удалены; проверены завершённые задания CREATE и DROP.\n'
|
||||||
|
printf 'ИТОГ: все 8 проверок кластера ClickHouse прошли.\n'
|
||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
readonly LOCAL_TABLE="smoke_replicated_local"
|
||||||
|
readonly DISTRIBUTED_TABLE="smoke_distributed"
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
docker compose --project-directory "$ROOT_DIR" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
query() {
|
||||||
|
local service="$1"
|
||||||
|
local sql="$2"
|
||||||
|
compose exec -T "$service" clickhouse-client --query "$sql"
|
||||||
|
}
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
passed=$((passed + 1))
|
||||||
|
printf 'ЗЕЛЁНО: %s.\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
failed=$((failed + 1))
|
||||||
|
printf 'ОШИБКА: %s.\n' "$1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_local_table() {
|
||||||
|
local attempt
|
||||||
|
for attempt in $(seq 1 100); do
|
||||||
|
if [[ "$(query clickhouse-01 "SELECT count() FROM system.tables WHERE database = 'default' AND name = '${LOCAL_TABLE}' FORMAT TSVRaw" 2>/dev/null || true)" == '1' ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_stand() {
|
||||||
|
make --no-print-directory -C "$ROOT_DIR" up >/dev/null
|
||||||
|
query clickhouse-01 "DROP TABLE IF EXISTS default.${DISTRIBUTED_TABLE} ON CLUSTER clickstream_cluster SYNC" >/dev/null 2>&1 || true
|
||||||
|
query clickhouse-01 "DROP TABLE IF EXISTS default.${LOCAL_TABLE} ON CLUSTER clickstream_cluster SYNC" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
check_failure_guards() {
|
||||||
|
local failure_log
|
||||||
|
local failure_pid
|
||||||
|
local failure_status
|
||||||
|
local failure_elapsed
|
||||||
|
local interrupt_log
|
||||||
|
local interrupt_pid
|
||||||
|
local interrupt_status
|
||||||
|
local remaining_tables
|
||||||
|
local started_at
|
||||||
|
local bounded_failure_ok=0
|
||||||
|
local interrupt_ok=0
|
||||||
|
interrupt_status=999
|
||||||
|
remaining_tables='не проверено'
|
||||||
|
|
||||||
|
failure_log="$(mktemp)"
|
||||||
|
started_at="$(date +%s)"
|
||||||
|
setsid make --no-print-directory -C "$ROOT_DIR" smoke >"$failure_log" 2>&1 &
|
||||||
|
failure_pid=$!
|
||||||
|
if wait_for_local_table; then
|
||||||
|
compose kill clickhouse-02 >/dev/null
|
||||||
|
wait "$failure_pid"
|
||||||
|
failure_status=$?
|
||||||
|
failure_elapsed=$(( $(date +%s) - started_at ))
|
||||||
|
if [[ "$failure_status" -eq 2 ]] && [[ "$failure_elapsed" -lt 20 ]]; then
|
||||||
|
bounded_failure_ok=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
kill -TERM -- "-$failure_pid" >/dev/null 2>&1 || true
|
||||||
|
wait "$failure_pid" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
rm -f "$failure_log"
|
||||||
|
|
||||||
|
restore_stand
|
||||||
|
|
||||||
|
interrupt_log="$(mktemp)"
|
||||||
|
setsid env --default-signal=INT,TERM "$ROOT_DIR/scripts/clickhouse-smoke.sh" >"$interrupt_log" 2>&1 &
|
||||||
|
interrupt_pid=$!
|
||||||
|
if wait_for_local_table; then
|
||||||
|
kill -INT -- "-$interrupt_pid"
|
||||||
|
wait "$interrupt_pid"
|
||||||
|
interrupt_status=$?
|
||||||
|
remaining_tables="$(query clickhouse-01 "SELECT count() FROM clusterAllReplicas('clickstream_cluster', system.tables) WHERE database = 'default' AND name IN ('${LOCAL_TABLE}', '${DISTRIBUTED_TABLE}') FORMAT TSVRaw")"
|
||||||
|
if [[ "$interrupt_status" -eq 130 ]] && [[ "$remaining_tables" == '0' ]]; then
|
||||||
|
interrupt_ok=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
kill -TERM -- "-$interrupt_pid" >/dev/null 2>&1 || true
|
||||||
|
wait "$interrupt_pid" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
rm -f "$interrupt_log"
|
||||||
|
|
||||||
|
if [[ "$bounded_failure_ok" -eq 1 ]] && [[ "$interrupt_ok" -eq 1 ]]; then
|
||||||
|
pass 'аварийная очистка ограничена по времени, SIGINT возвращает 130 и удаляет временные таблицы'
|
||||||
|
else
|
||||||
|
fail "нарушена аварийная семантика smoke: bounded=${bounded_failure_ok}, interrupt=${interrupt_ok}, status=${interrupt_status}, tables=${remaining_tables}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_keeper_runtime() {
|
||||||
|
local keeper_user
|
||||||
|
local keeper_nofile
|
||||||
|
local keeper_owner
|
||||||
|
keeper_user="$(compose exec -T clickhouse-keeper id -un)"
|
||||||
|
keeper_nofile="$(compose exec -T clickhouse-keeper awk '$1 == "Max" && $2 == "open" && $3 == "files" {print $4}' /proc/1/limits)"
|
||||||
|
keeper_owner="$(compose exec -T clickhouse-keeper stat -c '%U:%G' /var/lib/clickhouse/coordination)"
|
||||||
|
if [[ "$keeper_user" == 'clickhouse' ]] && [[ "$keeper_nofile" -ge 262144 ]] && [[ "$keeper_owner" == 'clickhouse:clickhouse' ]]; then
|
||||||
|
pass 'keeper работает от clickhouse с nofile 262144 и своим каталогом данных'
|
||||||
|
else
|
||||||
|
fail "неверное окружение keeper: user=${keeper_user}, nofile=${keeper_nofile}, owner=${keeper_owner}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_preflight_hint() {
|
||||||
|
local output
|
||||||
|
local status
|
||||||
|
compose kill clickhouse-02 >/dev/null
|
||||||
|
output="$("$ROOT_DIR/scripts/clickhouse-smoke.sh" 2>&1)"
|
||||||
|
status=$?
|
||||||
|
if [[ "$status" -ne 0 ]] && grep -q 'выполните make up' <<<"$output"; then
|
||||||
|
pass 'неполный стенд получает русскую подсказку выполнить make up'
|
||||||
|
else
|
||||||
|
fail 'нет русской подсказки для незапущенного стенда'
|
||||||
|
fi
|
||||||
|
make --no-print-directory -C "$ROOT_DIR" up >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
check_failure_guards
|
||||||
|
check_keeper_runtime
|
||||||
|
check_preflight_hint
|
||||||
|
|
||||||
|
printf 'ИТОГ: пройдено %d, ошибок %d\n' "$passed" "$failed"
|
||||||
|
[[ "$failed" -eq 0 ]]
|
||||||
Reference in New Issue
Block a user