- Зачем: - default должен остаться только учёткой локальных служебных вызовов. - Что: - пароль default подставлен из окружения на обеих нодах. - healthcheck и служебные скрипты передают его внутри контейнера. - Проверка: - make config-test, make up, make smoke, make check-clickhouse, make check-services.
257 lines
15 KiB
Bash
Executable File
257 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
readonly ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
readonly INVENTORY="${ROOT_DIR}/data/world-inventory.json"
|
||
readonly CLUSTER="clickstream_cluster"
|
||
readonly LOCAL_TABLE="smoke_replicated_local"
|
||
readonly DISTRIBUTED_TABLE="smoke_distributed"
|
||
|
||
compose() {
|
||
docker compose --project-directory "$ROOT_DIR" "$@"
|
||
}
|
||
|
||
# Ввод закрыт намеренно: у запроса INSERT clickhouse-client дочитывает данные
|
||
# из стандартного ввода и ждёт его конца. Если проверку запустили не из
|
||
# терминала, а из фонового процесса с открытым вводом, конца не наступает
|
||
# никогда, и команда висит без сообщений.
|
||
query() {
|
||
local service="$1"
|
||
local sql="$2"
|
||
compose exec -T "$service" sh -c '
|
||
clickhouse-client \
|
||
--user default \
|
||
--password "$CLICKHOUSE_DEFAULT_PASSWORD" \
|
||
--query "$1"
|
||
' _ "$sql" </dev/null
|
||
}
|
||
|
||
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" \
|
||
sh -c '
|
||
clickhouse-client \
|
||
--user default \
|
||
--password "$CLICKHOUSE_DEFAULT_PASSWORD" \
|
||
--query "$1"
|
||
' _ "$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 check-clickhouse.\n' >&2
|
||
exit "$status"
|
||
}
|
||
|
||
on_signal() {
|
||
trap - EXIT INT TERM
|
||
printf 'Проверка прервана пользователем: выполняется быстрая очистка временных таблиц...\n' >&2
|
||
cleanup_tables || printf 'ПРЕДУПРЕЖДЕНИЕ: очистка не завершена; после восстановления стенда повторите make check-clickhouse.\n' >&2
|
||
exit 130
|
||
}
|
||
|
||
# Единственный постоянный сторож цепочки Kafka → STG → ODS.
|
||
#
|
||
# Проверка спрашивает не «есть ли данные», а «те ли это данные»: подневный счёт
|
||
# в `ods.event` против описи мира, лежащей в git. Поэтому она умеет краснеть на
|
||
# сломанном разборе — уехали события в брак, счёт разошёлся, — и на потере по
|
||
# дороге, а не только на пустой таблице.
|
||
#
|
||
# **Счёт обрамлён датами стартового мира.** Мир растёт: менти переиграет день,
|
||
# этап 5 добавит следующий, — и голый счёт по таблице разойдётся с описью
|
||
# законно, без всякой поломки.
|
||
#
|
||
# **Счёт идёт через FINAL** — правило репозитория для ODS, довод в storage.md:
|
||
# голый `count()` по ReplacingMergeTree зависит от числа прошедших мержей.
|
||
#
|
||
# Таблица брака здесь не второе утверждение, а объяснение первого. Утверждай мы
|
||
# «брака нет», проверка краснела бы навсегда после первого же урока, где менти
|
||
# нарочно отправил в топик мусор, — и краснела бы не о том.
|
||
#
|
||
# По той же причине непустой брак сам по себе ничего не доказывает: модельного
|
||
# дня у брака нет, обрамить его нечем, и строки прежних уроков лежат в нём
|
||
# месяц. Поэтому объяснение не утверждает причину, а даёт признак, по которому
|
||
# её отличают: сошлась недостача с числом брака — разбор, не сошлась — доставка.
|
||
check_starting_world() {
|
||
local expected actual broken first_date last_date
|
||
|
||
expected="$(jq -r '.days[] | "\(.date)\t\(.events)"' "$INVENTORY")"
|
||
first_date="$(jq -r '.days | first | .date' "$INVENTORY")"
|
||
last_date="$(jq -r '.days | last | .date' "$INVENTORY")"
|
||
|
||
actual="$(query clickhouse-01 "
|
||
SELECT EventDate, count()
|
||
FROM ods.event_dist FINAL
|
||
WHERE EventDate BETWEEN '${first_date}' AND '${last_date}'
|
||
GROUP BY EventDate
|
||
ORDER BY EventDate
|
||
FORMAT TSV")"
|
||
|
||
if [[ "$actual" == "$expected" ]]; then
|
||
printf 'ЗЕЛЁНО: события всех дней стартового мира на месте, счёт сходится с описью.\n'
|
||
return
|
||
fi
|
||
|
||
broken="$(query clickhouse-01 "
|
||
SELECT error_class, count()
|
||
FROM ods.event_errors_dist
|
||
GROUP BY error_class
|
||
ORDER BY count() DESC
|
||
FORMAT TSV")"
|
||
printf 'Опись мира ожидает (дата, событий):\n%s\n' "$expected" >&2
|
||
printf 'В ods.event лежит:\n%s\n' "${actual:-— ничего —}" >&2
|
||
if [[ -z "$broken" ]]; then
|
||
fail 'счёт разошёлся с описью, а таблица брака пуста: события не доехали до ODS — начните с чтеца топика stg.hits_raw_kafka и матвью приёма stg.hits_raw_mv'
|
||
fi
|
||
printf 'В ods.event_errors по классам брака:\n%s\n' "$broken" >&2
|
||
fail 'счёт разошёлся с описью, и в таблице брака есть строки. Сойдётся недостача с их числом — сломан разбор, смотрите матвью ods.event_mv; не сойдётся — брак остался от прежних опытов, а события не доехали: смотрите чтеца топика stg.hits_raw_kafka'
|
||
}
|
||
|
||
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/9: описание кластера одинаково на обеих нодах...\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/9: у нод разные макросы 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/9: 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/9: 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/9: путь в 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/9: 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/9: в очереди распределённых DDL нет незавершённых заданий...\n'
|
||
assert_ddl_queue_completed clickhouse-01 'после CREATE'
|
||
assert_ddl_queue_completed clickhouse-02 'после CREATE'
|
||
printf 'ЗЕЛЁНО: очередь содержит задания CREATE, незавершённых среди них нет.\n'
|
||
|
||
printf 'Проверка 8/9: временные таблицы удаляются через 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 'Проверка 9/9: стартовый мир в ods.event сходится с описью...\n'
|
||
check_starting_world
|
||
|
||
printf 'ИТОГ: все 9 проверок кластера ClickHouse прошли.\n'
|