feat(smoke): смоук спрашивает Kafka снаружи — по объявленному адресу

- Зачем:
  - деление целей оставило дыру: про 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) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 15:20:27 +03:00
co-authored by Claude Opus 5
parent bbbe17cfb0
commit 7e4d5815a3
3 changed files with 68 additions and 20 deletions
+20 -12
View File
@@ -70,11 +70,12 @@ make smoke
Superset, создаёт администратора и импортирует подключение к `clickhouse-02`. Superset, создаёт администратора и импортирует подключение к `clickhouse-02`.
`make smoke` за секунды спрашивает, собран ли стенд: зависимости машины, `make smoke` за секунды спрашивает, собран ли стенд: зависимости машины,
здоровье контейнеров, устройство keeper, три цели Prometheus, источник Grafana, здоровье контейнеров, устройство keeper, ответ Kafka с машины через отображённый
компоненты Airflow и подготовленное подключение к `clickhouse-01`. В конце порт, три цели Prometheus, источник Grafana, компоненты Airflow и подготовленное
проверка спрашивает у Docker, не убивало ли ядро что-нибудь в долгоживущих подключение к `clickhouse-01`. В конце проверка спрашивает у Docker, не убивало
контейнерах за нехватку памяти и не включалась ли политика перезапуска: убитый ли ядро что-нибудь в долгоживущих контейнерах за нехватку памяти и не включалась
контейнер Docker поднимает сам, и проверка состояния об этом промолчит. ли политика перезапуска: убитый контейнер Docker поднимает сам, и проверка
состояния об этом промолчит.
Одиннадцать проверок здоровья сразу после `make up --wait` повторяют то, чего Одиннадцать проверок здоровья сразу после `make up --wait` повторяют то, чего
Compose уже дождался: у каждой долгоживущей службы есть своя `healthcheck`. Compose уже дождался: у каждой долгоживущей службы есть своя `healthcheck`.
@@ -85,13 +86,20 @@ Compose уже дождался: у каждой долгоживущей слу
выглядит и без этого, поэтому смоук спрашивает у живого контейнера, дошли ли выглядит и без этого, поэтому смоук спрашивает у живого контейнера, дошли ли
объявленные настройки до процесса. объявленные настройки до процесса.
`make check-services` проверяет то, ради чего приходится ждать службу: Kafka Kafka по той же причине спрашивают снаружи. Её `healthcheck` обращается к
через порт машины, ручной запуск пробников `test_clickhouse` и `test_kafka`, брокеру изнутри контейнера и по внутреннему слушателю, поэтому здоровой Kafka
вход в Superset, его метаданные и подключение к `clickhouse-02`. Первый пробник остаётся и тогда, когда объявленный наружу адрес ведёт не туда. Клиент с машины
создаёт таблицы на обеих нодах и читает через `Distributed` на ноде 2 строку из в этом случае подключается, получает метаданные и молча виснет на адресе,
локальной таблицы ноды 1. Второй пишет в Kafka и читает свой маркер. Временный которого с его стороны не существует. Смоук идёт тем же путём, что и будущий
топик проверки с машины и запуски DAG удаляются; постоянный топик пробника генератор: стучится в отображённый порт и просит список топиков.
сохраняется, а старые записи чистит Kafka.
`make check-services` проверяет то, ради чего приходится ждать службу: работу с
топиком Kafka с машины — создан, найден, удалён, — ручной запуск пробников
`test_clickhouse` и `test_kafka`, вход в Superset, его метаданные и подключение
к `clickhouse-02`. Первый пробник создаёт таблицы на обеих нодах и читает через
`Distributed` на ноде 2 строку из локальной таблицы ноды 1. Второй пишет в Kafka
и читает свой маркер. Временный топик проверки с машины и запуски DAG удаляются;
постоянный топик пробника сохраняется, а старые записи чистит Kafka.
`make check-clickhouse` запускает отдельную глубокую проверку ClickHouse: `make check-clickhouse` запускает отдельную глубокую проверку ClickHouse:
описание кластера, макросы, связь с keeper, `ReplicatedMergeTree`, описание кластера, макросы, связь с keeper, `ReplicatedMergeTree`,
+23 -8
View File
@@ -38,7 +38,7 @@ ClickHouse: строки в `ods.event` считает сам сервер и о
| `make typecheck` | Типы генератора сходятся (ty) | не нужен | 0,5 с | | `make typecheck` | Типы генератора сходятся (ty) | не нужен | 0,5 с |
| `make test` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранное из неё [описание выгрузки](../formats/clickstream-event.md) — свежим | не нужен | 40 с | | `make test` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранное из неё [описание выгрузки](../formats/clickstream-event.md) — свежим | не нужен | 40 с |
| `make config-test` | Compose разбирается, Bash и Python синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с | | `make config-test` | Compose разбирается, Bash и Python синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с |
| `make smoke` | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 6 с | | `make smoke` | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 8 с |
| `make check-clickhouse` | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL | нужен | 7 с | | `make check-clickhouse` | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL | нужен | 7 с |
| `make check-services` | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 44 с | | `make check-services` | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 44 с |
@@ -117,7 +117,7 @@ ClickHouse отвечает сразу.
контейнер Docker поднимает сам, и проверка здоровья об этом промолчит контейнер Docker поднимает сам, и проверка здоровья об этом промолчит
(ADR 0004). Стенд нагружает `check-services`, а увидеть последствия нужно и (ADR 0004). Стенд нагружает `check-services`, а увидеть последствия нужно и
тому, кто гонял один смоук. Это единственная проверка, которая считается тому, кто гонял один смоук. Это единственная проверка, которая считается
дважды: 19 у смоука плюс 7 у `check-services` — это 25 разных проверок. дважды: 20 у смоука плюс 7 у `check-services` — это 26 разных проверок.
- **Зависимости машины считает только смоук.** «На машине есть Docker, curl и - **Зависимости машины считает только смоук.** «На машине есть Docker, curl и
jq» — вопрос к машине, а не к службам, и на оси он стоит рядом с «стенд jq» — вопрос к машине, а не к службам, и на оси он стоит рядом с «стенд
собран». Для `check-services` это условие запуска: без них он не начнёт собран». Для `check-services` это условие запуска: без них он не начнёт
@@ -131,19 +131,34 @@ ClickHouse отвечает сразу.
Замеры 6 августа 2026 года, стенд поднят заранее; время `make up` в цену целей Замеры 6 августа 2026 года, стенд поднят заранее; время `make up` в цену целей
не входит. Время взято по `time` и совпадает с тем, что цель печатает сама. Оно не входит. Время взято по `time` и совпадает с тем, что цель печатает сама. Оно
плавает от прогона к прогону: смоук дал 5 и 6 секунд, `check-services` — 43 и плавает от прогона к прогону: смоук дал 8 секунд дважды (без проверки Kafka,
44. В таблице стоит большее из замеренных. до её появления, — 5 и 6), `check-services` — 43 и 44. В таблице стоит большее
из замеренных.
До деления `scripts/stand-smoke.sh` шёл 48 секунд на 25 проверок, из них До деления `scripts/stand-smoke.sh` шёл 48 секунд на 25 проверок, из них
42 секунды съедали шесть: Kafka с машины, два запуска пробников Airflow и три 42 секунды съедали шесть: Kafka с машины, два запуска пробников Airflow и три
проверки Superset. После деления те же 25 проверок разошлись по двум целям: проверки Superset. После деления те же 25 проверок разошлись по двум целям:
19 в смоуке и 6 в `check-services`. Содержание ни одной из них не менялось. 19 в смоуке и 6 в `check-services`. Содержание ни одной из них не менялось.
Что обе разделённые цели умеют краснеть, проверено руками в тот же день: со Двадцатая проверка смоука — единственная новая: Kafka спрашивают с машины через
снятым `prometheus` смоук дал три ошибки и ненулевой код возврата; с отображённый порт. Деление оставило дыру, которой раньше не было. Про Airflow,
Superset, Prometheus и Grafana смоук стучится с машины в отображённый порт, а
про Kafka после переезда знал только «контейнер здоров» — а это вердикт
проверки состояния из `compose.yaml`, и та спрашивает брокер изнутри по
внутреннему слушателю. Внешняя дверь оставалась непроверенной до
`check-services` с его 44 секундами, хотя генератор пишет в Kafka именно с
машины. Стоит проверка 2,6 секунды, и почти всё это — старт JVM в разовом
контейнере; ожидание ответа ограничено пятнадцатью секундами, впятеро больше
замеренного.
Что обе разделённые цели умеют краснеть, проверено руками в тот же день. Со
снятым `prometheus` смоук дал три ошибки и ненулевой код возврата. С
подменённым ожидаемым UUID подключения Superset так же покраснел подменённым ожидаемым UUID подключения Superset так же покраснел
`check-services`. Обе краснеют и когда на машине не хватает команды из списка `check-services`. Новую проверку Kafka проверили её собственной поломкой:
зависимостей. брокеру объявили адрес `kafka-nowhere:29092`, оставив внутренний слушатель
целым, — проверка состояния контейнера осталась зелёной, а смоук покраснел
именно на этой строке. Краснеют они и когда на машине не хватает команды из
списка зависимостей.
Семантика счётчиков Docker `OOMKilled` и `RestartCount`, на которой держится Семантика счётчиков Docker `OOMKilled` и `RestartCount`, на которой держится
`check_containers_survived`, снята отдельными контейнерами и записана в `check_containers_survived`, снята отдельными контейнерами и записана в
+25
View File
@@ -53,6 +53,30 @@ check_keeper_runtime() {
fi 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() { check_prometheus_targets() {
local port local port
local response local response
@@ -206,6 +230,7 @@ if require_host_dependencies; then
check_container_health "$service" check_container_health "$service"
done done
check_keeper_runtime check_keeper_runtime
check_kafka_external_listener
check_prometheus_targets check_prometheus_targets
check_grafana_datasource check_grafana_datasource
check_airflow check_airflow