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).
|
||||
Работающего стенда пока нет: здесь только спека, исследование формата данных и
|
||||
контракт работы ([AGENTS.md](AGENTS.md)). Быстрый старт появится вместе с
|
||||
этапом «Каркас стенда».
|
||||
Сейчас готов первый элемент каркаса: кластер ClickHouse из двух шардов и
|
||||
отдельного 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