fix(superset): stabilize bootstrap after clean reset

- make superset-init run via dedicated init service\n- tolerate missing dm views during early metadata refresh\n- add clickhouse dependency for init service\n- document clean-reset behavior and re-init flow
This commit is contained in:
2026-02-11 09:48:56 +03:00
parent 7819af8688
commit ec6111d260
5 changed files with 58 additions and 9 deletions
+3 -1
View File
@@ -69,7 +69,9 @@ recover-monitoring:
# Инициализация Superset (подключение к ClickHouse + датасеты)
superset-init:
$(COMPOSE) exec -T superset bash -c "python /app/superset_init/init_superset.py"
$(COMPOSE) up -d postgres-metadata clickhouse
$(COMPOSE) up --abort-on-container-exit --exit-code-from superset-init superset-init
$(COMPOSE) up -d --no-deps superset
# Создание дашборда с чартами
superset-dashboard:
+3 -2
View File
@@ -75,7 +75,7 @@ docker compose exec -T clickhouse clickhouse-client --user=default --password=12
| Prometheus | http://localhost:9090 | Метрики | — |
| Grafana | http://localhost:3000 | Визуализация метрик | admin/admin |
Superset: после `make up` готовый дашборд доступен по адресу http://localhost:8088/superset/dashboard/1/
Superset: UI доступен после `make up` по адресу http://localhost:8088, дашборд — http://localhost:8088/superset/dashboard/1/
---
@@ -280,7 +280,7 @@ make superset-dashboard
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Superset UI │────▶│ PostgreSQL │───▶│ ClickHouse │
│ Superset UI │────▶│ PostgreSQL │───▶│ ClickHouse │
│ (localhost) │ │ (metadata) │ │ (данные) │
│ :8088 │ │ dashboards, │ │ dm.v_* VIEW │
└─────────────────┘ │ datasets, charts│ └─────────────────┘
@@ -324,6 +324,7 @@ make superset-dashboard
- Airflow (ClickHouseOperator) ходит в ClickHouse по native TCP (порт `9000` внутри сети Docker).
- Superset (clickhouse-connect) ходит по HTTP (порт `8123` внутри сети Docker).
- **Superset**: дашборд не появился сразу — подождите 30-60 секунд после `make up`, затем проверьте `curl http://localhost:8088/health`.
- **Superset**: после `make clean` витрины `dm.*` ещё не созданы, поэтому чарты могут быть пустыми до запуска `ddl_init -> kafka_load -> etl_pipeline`; после этого выполните `make superset-init`.
- **Superset**: при полном сбросе (`docker compose down -v`) метаданные Superset пропадут т.к. используется общая PostgreSQL. Для чистого перезапуска Superset удалите только БД `superset` в PostgreSQL и перезапустите контейнеры.
---
+2
View File
@@ -180,6 +180,8 @@ services:
depends_on:
postgres-metadata:
condition: service_healthy
clickhouse:
condition: service_started
prometheus:
image: prom/prometheus:v2.53.4
+2
View File
@@ -12,6 +12,7 @@
- `make ddl` (применяет SQL из `sql/ddl/00_databases.sql` и `sql/ddl/*/*.sql` в ClickHouse)
- `make data` (пересоздаёт топики и заливает небольшой срез данных в Kafka; полный режим — `FULL=1 make data`)
- `make transform` (запускает batch-процесс ODS -> DDS -> DM)
- `make superset-init` (повторная инициализация Superset: подключение к ClickHouse, датасеты, дашборд)
- `docker compose ps`
- `docker compose logs -f --tail=200 <service>`
- `docker compose down` (сохраняет named volumes, включая `clickhouse-data`)
@@ -302,4 +303,5 @@ curl -s -X POST -u admin:admin http://localhost:3000/api/admin/provisioning/aler
docker compose up -d --force-recreate superset-init superset
```
- После `docker compose down -v` нужно повторно прогнать: `ddl_init` -> `kafka_load` -> `etl_pipeline`.
- После `make clean`/`down -v` Superset стартует, но витрины `dm.*` ещё пустые или отсутствуют до прогона ETL; после `ddl_init` -> `kafka_load` -> `etl_pipeline` выполнить `make superset-init`.
- Для демо по умолчанию использовать малый срез данных; полный прогон делать осознанно.
+42
View File
@@ -29,6 +29,7 @@ import os
import logging
import subprocess
from urllib.parse import quote_plus
from sqlalchemy.exc import NoSuchTableError
# Настройка логирования
logging.basicConfig(
@@ -47,6 +48,27 @@ CLICKHOUSE_PORT = os.getenv('CLICKHOUSE_HTTP_PORT', '8123')
CLICKHOUSE_DATABASE = os.getenv('CLICKHOUSE_DATABASE', 'default')
def is_expected_missing_table_error(exc: Exception, schema_name: str, table_name: str) -> bool:
"""Проверяет, что ошибка связана с отсутствием таблицы/вьюхи в ClickHouse."""
if isinstance(exc, NoSuchTableError):
return True
text = str(exc).strip().lower()
full_table_name = f"{schema_name}.{table_name}".lower()
known_markers = (
"doesn't exist",
"does not exist",
"unknown table",
"no such table",
"code: 60",
)
if text == full_table_name:
return True
return full_table_name in text and any(marker in text for marker in known_markers)
def build_clickhouse_uri() -> str:
"""Собирает URI подключения к ClickHouse для Superset."""
user = quote_plus(CLICKHOUSE_USER)
@@ -143,6 +165,17 @@ def import_datasets():
"from superset.extensions import db",
"from superset.models.core import Database",
"from superset.connectors.sqla.models import SqlaTable",
"from sqlalchemy.exc import NoSuchTableError",
"",
"def is_expected_missing_table_error(exc, schema_name, table_name):",
" text = str(exc).strip().lower()",
" full_table_name = f'{schema_name}.{table_name}'.lower()",
" known_markers = ('doesn\\'t exist', 'does not exist', 'unknown table', 'no such table', 'code: 60')",
" if isinstance(exc, NoSuchTableError):",
" return True",
" if text == full_table_name:",
" return True",
" return full_table_name in text and any(marker in text for marker in known_markers)",
"",
"# Получаем базу данных",
"database = db.session.query(Database).filter_by(database_name='clickhouse_dwh').first()",
@@ -179,6 +212,10 @@ def import_datasets():
" else:",
f" print('Dataset {table_name} already exists')",
" except Exception as e:",
f" if is_expected_missing_table_error(e, {schema_name!r}, {table_name!r}):",
f" print('WARNING: metadata not refreshed for {table_name}: source table/view not found yet')",
" db.session.rollback()",
" else:",
f" print(f'ERROR: failed to refresh {table_name}: {{e}}')",
" db.session.rollback()",
" errors += 1",
@@ -200,6 +237,11 @@ def import_datasets():
f" print('Created dataset: {table_name}')",
" imported += 1",
" except Exception as e:",
f" if is_expected_missing_table_error(e, {schema_name!r}, {table_name!r}):",
" db.session.commit()",
f" print('WARNING: created dataset {table_name} without metadata: source table/view not found yet')",
" imported += 1",
" else:",
f" print(f'ERROR: failed to create {table_name}: {{e}}')",
" db.session.rollback()",
" errors += 1",