From 7e4d5815a39072c6e009576c34724f175db66be4 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Thu, 6 Aug 2026 14:53:34 +0300 Subject: [PATCH] =?UTF-8?q?feat(smoke):=20=D1=81=D0=BC=D0=BE=D1=83=D0=BA?= =?UTF-8?q?=20=D1=81=D0=BF=D1=80=D0=B0=D1=88=D0=B8=D0=B2=D0=B0=D0=B5=D1=82?= =?UTF-8?q?=20Kafka=20=D1=81=D0=BD=D0=B0=D1=80=D1=83=D0=B6=D0=B8=20?= =?UTF-8?q?=E2=80=94=20=D0=BF=D0=BE=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D0=BE=D0=BC=D1=83=20=D0=B0=D0=B4=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - деление целей оставило дыру: про Airflow, Superset, Prometheus и Grafana смоук стучится с машины в отображённый порт, а про Kafka после переезда check_kafka_from_host знал только «контейнер здоров». - вердикт этот приходит из healthcheck в compose.yaml, а тот спрашивает брокер изнутри и по внутреннему слушателю: объявленный наружу адрес может вести не туда, и Kafka всё равно останется здоровой. - поломка популярная и показательная: клиент подключается, получает метаданные и молча виснет на адресе, которого с его стороны нет. Менти узнаёт, что у брокера два слушателя и зачем нужен advertised.listeners. Генератор будет писать в Kafka именно с машины. - Что: - check_kafka_external_listener в make smoke: запрос списка топиков с машины через отображённый порт, ответ приходит только если объявленный адрес ведёт туда же. Комментарий у проверки объясняет, от чего она заведена. - ожидание ответа ограничено 15 секундами при замеренных 2,6 — впятеро больше, чем стоит зелёный прогон. - README и карта проверок: новая проверка названа, доводы записаны, цена смоука обновлена с 6 до 8 секунд. - Проверка: - make config-test, make smoke (20 проверок, 8 с), make check-clickhouse, make check-services — зелёные. - краснеет на своей поломке: брокеру объявлен адрес kafka-nowhere:29092 при целом внутреннем слушателе — проверка состояния контейнера осталась зелёной, смоук покраснел именно на этой строке. После проверки Kafka возвращена в исходное состояние, посторонних контейнеров не осталось. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 32 ++++++++++++++++++++------------ docs/architecture/testing.md | 31 +++++++++++++++++++++++-------- scripts/stand-smoke.sh | 25 +++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 66e468e..2243555 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,12 @@ make smoke Superset, создаёт администратора и импортирует подключение к `clickhouse-02`. `make smoke` за секунды спрашивает, собран ли стенд: зависимости машины, -здоровье контейнеров, устройство keeper, три цели Prometheus, источник Grafana, -компоненты Airflow и подготовленное подключение к `clickhouse-01`. В конце -проверка спрашивает у Docker, не убивало ли ядро что-нибудь в долгоживущих -контейнерах за нехватку памяти и не включалась ли политика перезапуска: убитый -контейнер Docker поднимает сам, и проверка состояния об этом промолчит. +здоровье контейнеров, устройство keeper, ответ Kafka с машины через отображённый +порт, три цели Prometheus, источник Grafana, компоненты Airflow и подготовленное +подключение к `clickhouse-01`. В конце проверка спрашивает у Docker, не убивало +ли ядро что-нибудь в долгоживущих контейнерах за нехватку памяти и не включалась +ли политика перезапуска: убитый контейнер Docker поднимает сам, и проверка +состояния об этом промолчит. Одиннадцать проверок здоровья сразу после `make up --wait` повторяют то, чего Compose уже дождался: у каждой долгоживущей службы есть своя `healthcheck`. @@ -85,13 +86,20 @@ Compose уже дождался: у каждой долгоживущей слу выглядит и без этого, поэтому смоук спрашивает у живого контейнера, дошли ли объявленные настройки до процесса. -`make check-services` проверяет то, ради чего приходится ждать службу: Kafka -через порт машины, ручной запуск пробников `test_clickhouse` и `test_kafka`, -вход в Superset, его метаданные и подключение к `clickhouse-02`. Первый пробник -создаёт таблицы на обеих нодах и читает через `Distributed` на ноде 2 строку из -локальной таблицы ноды 1. Второй пишет в Kafka и читает свой маркер. Временный -топик проверки с машины и запуски DAG удаляются; постоянный топик пробника -сохраняется, а старые записи чистит Kafka. +Kafka по той же причине спрашивают снаружи. Её `healthcheck` обращается к +брокеру изнутри контейнера и по внутреннему слушателю, поэтому здоровой Kafka +остаётся и тогда, когда объявленный наружу адрес ведёт не туда. Клиент с машины +в этом случае подключается, получает метаданные и молча виснет на адресе, +которого с его стороны не существует. Смоук идёт тем же путём, что и будущий +генератор: стучится в отображённый порт и просит список топиков. + +`make check-services` проверяет то, ради чего приходится ждать службу: работу с +топиком Kafka с машины — создан, найден, удалён, — ручной запуск пробников +`test_clickhouse` и `test_kafka`, вход в Superset, его метаданные и подключение +к `clickhouse-02`. Первый пробник создаёт таблицы на обеих нодах и читает через +`Distributed` на ноде 2 строку из локальной таблицы ноды 1. Второй пишет в Kafka +и читает свой маркер. Временный топик проверки с машины и запуски DAG удаляются; +постоянный топик пробника сохраняется, а старые записи чистит Kafka. `make check-clickhouse` запускает отдельную глубокую проверку ClickHouse: описание кластера, макросы, связь с keeper, `ReplicatedMergeTree`, diff --git a/docs/architecture/testing.md b/docs/architecture/testing.md index b391aee..bb632e4 100644 --- a/docs/architecture/testing.md +++ b/docs/architecture/testing.md @@ -38,7 +38,7 @@ ClickHouse: строки в `ods.event` считает сам сервер и о | `make typecheck` | Типы генератора сходятся (ty) | не нужен | 0,5 с | | `make test` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранное из неё [описание выгрузки](../formats/clickstream-event.md) — свежим | не нужен | 40 с | | `make config-test` | Compose разбирается, Bash и Python синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с | -| `make smoke` | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 6 с | +| `make smoke` | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 8 с | | `make check-clickhouse` | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL | нужен | 7 с | | `make check-services` | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 44 с | @@ -117,7 +117,7 @@ ClickHouse отвечает сразу. контейнер Docker поднимает сам, и проверка здоровья об этом промолчит (ADR 0004). Стенд нагружает `check-services`, а увидеть последствия нужно и тому, кто гонял один смоук. Это единственная проверка, которая считается - дважды: 19 у смоука плюс 7 у `check-services` — это 25 разных проверок. + дважды: 20 у смоука плюс 7 у `check-services` — это 26 разных проверок. - **Зависимости машины считает только смоук.** «На машине есть Docker, curl и jq» — вопрос к машине, а не к службам, и на оси он стоит рядом с «стенд собран». Для `check-services` это условие запуска: без них он не начнёт @@ -131,19 +131,34 @@ ClickHouse отвечает сразу. Замеры 6 августа 2026 года, стенд поднят заранее; время `make up` в цену целей не входит. Время взято по `time` и совпадает с тем, что цель печатает сама. Оно -плавает от прогона к прогону: смоук дал 5 и 6 секунд, `check-services` — 43 и -44. В таблице стоит большее из замеренных. +плавает от прогона к прогону: смоук дал 8 секунд дважды (без проверки Kafka, +до её появления, — 5 и 6), `check-services` — 43 и 44. В таблице стоит большее +из замеренных. До деления `scripts/stand-smoke.sh` шёл 48 секунд на 25 проверок, из них 42 секунды съедали шесть: Kafka с машины, два запуска пробников Airflow и три проверки Superset. После деления те же 25 проверок разошлись по двум целям: 19 в смоуке и 6 в `check-services`. Содержание ни одной из них не менялось. -Что обе разделённые цели умеют краснеть, проверено руками в тот же день: со -снятым `prometheus` смоук дал три ошибки и ненулевой код возврата; с +Двадцатая проверка смоука — единственная новая: Kafka спрашивают с машины через +отображённый порт. Деление оставило дыру, которой раньше не было. Про Airflow, +Superset, Prometheus и Grafana смоук стучится с машины в отображённый порт, а +про Kafka после переезда знал только «контейнер здоров» — а это вердикт +проверки состояния из `compose.yaml`, и та спрашивает брокер изнутри по +внутреннему слушателю. Внешняя дверь оставалась непроверенной до +`check-services` с его 44 секундами, хотя генератор пишет в Kafka именно с +машины. Стоит проверка 2,6 секунды, и почти всё это — старт JVM в разовом +контейнере; ожидание ответа ограничено пятнадцатью секундами, впятеро больше +замеренного. + +Что обе разделённые цели умеют краснеть, проверено руками в тот же день. Со +снятым `prometheus` смоук дал три ошибки и ненулевой код возврата. С подменённым ожидаемым UUID подключения Superset так же покраснел -`check-services`. Обе краснеют и когда на машине не хватает команды из списка -зависимостей. +`check-services`. Новую проверку Kafka проверили её собственной поломкой: +брокеру объявили адрес `kafka-nowhere:29092`, оставив внутренний слушатель +целым, — проверка состояния контейнера осталась зелёной, а смоук покраснел +именно на этой строке. Краснеют они и когда на машине не хватает команды из +списка зависимостей. Семантика счётчиков Docker `OOMKilled` и `RestartCount`, на которой держится `check_containers_survived`, снята отдельными контейнерами и записана в diff --git a/scripts/stand-smoke.sh b/scripts/stand-smoke.sh index 1570536..e6c9595 100755 --- a/scripts/stand-smoke.sh +++ b/scripts/stand-smoke.sh @@ -53,6 +53,30 @@ check_keeper_runtime() { fi } +# Самая известная поломка Kafka — объявленный адрес не совпадает с тем, по +# которому к брокеру стучатся снаружи. Выглядит она издевательски: клиент +# подключается, получает метаданные и виснет на адресе, которого с его стороны +# не существует. Проверка состояния контейнера этого не увидит — она спрашивает +# брокер изнутри и по внутреннему слушателю (compose.yaml, healthcheck kafka). +# Здесь запрос идёт с машины через отображённый порт, и списка топиков клиент не +# получит, не сходив вторым шагом по объявленному адресу. Отсюда и цена: почти +# вся она — старт JVM в разовом контейнере, а не разговор с брокером. +check_kafka_external_listener() { + local image + local port + + image="$(compose config --format json 2>/dev/null | jq -r '.services.kafka.image // empty')" + port="$(published_port kafka 29092)" + if [[ -n "$image" ]] && [[ -n "$port" ]] && \ + timeout 15s docker run --rm --network host "$image" \ + /opt/kafka/bin/kafka-topics.sh \ + --bootstrap-server "localhost:${port}" --list >/dev/null 2>&1; then + pass "Kafka отвечает с машины через localhost:${port}: объявленный адрес ведёт туда же, куда отображён порт" + else + fail "Kafka не ответила с машины через localhost:${port:-порт не найден}: закрыт внешний слушатель, не отображён порт или объявленный адрес ведёт не туда" + fi +} + check_prometheus_targets() { local port local response @@ -206,6 +230,7 @@ if require_host_dependencies; then check_container_health "$service" done check_keeper_runtime + check_kafka_external_listener check_prometheus_targets check_grafana_datasource check_airflow