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:
@@ -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:
|
||||
|
||||
@@ -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 и перезапустите контейнеры.
|
||||
|
||||
---
|
||||
|
||||
@@ -180,6 +180,8 @@ services:
|
||||
depends_on:
|
||||
postgres-metadata:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_started
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.53.4
|
||||
|
||||
@@ -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`.
|
||||
- Для демо по умолчанию использовать малый срез данных; полный прогон делать осознанно.
|
||||
|
||||
@@ -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,9 +212,13 @@ def import_datasets():
|
||||
" else:",
|
||||
f" print('Dataset {table_name} already exists')",
|
||||
" except Exception as e:",
|
||||
f" print(f'ERROR: failed to refresh {table_name}: {{e}}')",
|
||||
" db.session.rollback()",
|
||||
" errors += 1",
|
||||
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",
|
||||
"else:",
|
||||
" try:",
|
||||
(
|
||||
@@ -200,9 +237,14 @@ def import_datasets():
|
||||
f" print('Created dataset: {table_name}')",
|
||||
" imported += 1",
|
||||
" except Exception as e:",
|
||||
f" print(f'ERROR: failed to create {table_name}: {{e}}')",
|
||||
" db.session.rollback()",
|
||||
" errors += 1",
|
||||
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",
|
||||
])
|
||||
|
||||
script_lines.extend([
|
||||
|
||||
Reference in New Issue
Block a user