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 (подключение к ClickHouse + датасеты)
|
||||||
superset-init:
|
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:
|
superset-dashboard:
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ docker compose exec -T clickhouse clickhouse-client --user=default --password=12
|
|||||||
| Prometheus | http://localhost:9090 | Метрики | — |
|
| Prometheus | http://localhost:9090 | Метрики | — |
|
||||||
| Grafana | http://localhost:3000 | Визуализация метрик | admin/admin |
|
| 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) │ │ (данные) │
|
│ (localhost) │ │ (metadata) │ │ (данные) │
|
||||||
│ :8088 │ │ dashboards, │ │ dm.v_* VIEW │
|
│ :8088 │ │ dashboards, │ │ dm.v_* VIEW │
|
||||||
└─────────────────┘ │ datasets, charts│ └─────────────────┘
|
└─────────────────┘ │ datasets, charts│ └─────────────────┘
|
||||||
@@ -324,6 +324,7 @@ make superset-dashboard
|
|||||||
- Airflow (ClickHouseOperator) ходит в ClickHouse по native TCP (порт `9000` внутри сети Docker).
|
- Airflow (ClickHouseOperator) ходит в ClickHouse по native TCP (порт `9000` внутри сети Docker).
|
||||||
- Superset (clickhouse-connect) ходит по HTTP (порт `8123` внутри сети Docker).
|
- Superset (clickhouse-connect) ходит по HTTP (порт `8123` внутри сети Docker).
|
||||||
- **Superset**: дашборд не появился сразу — подождите 30-60 секунд после `make up`, затем проверьте `curl http://localhost:8088/health`.
|
- **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 и перезапустите контейнеры.
|
- **Superset**: при полном сбросе (`docker compose down -v`) метаданные Superset пропадут т.к. используется общая PostgreSQL. Для чистого перезапуска Superset удалите только БД `superset` в PostgreSQL и перезапустите контейнеры.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
postgres-metadata:
|
postgres-metadata:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
clickhouse:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
prometheus:
|
prometheus:
|
||||||
image: prom/prometheus:v2.53.4
|
image: prom/prometheus:v2.53.4
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
- `make ddl` (применяет SQL из `sql/ddl/00_databases.sql` и `sql/ddl/*/*.sql` в ClickHouse)
|
- `make ddl` (применяет SQL из `sql/ddl/00_databases.sql` и `sql/ddl/*/*.sql` в ClickHouse)
|
||||||
- `make data` (пересоздаёт топики и заливает небольшой срез данных в Kafka; полный режим — `FULL=1 make data`)
|
- `make data` (пересоздаёт топики и заливает небольшой срез данных в Kafka; полный режим — `FULL=1 make data`)
|
||||||
- `make transform` (запускает batch-процесс ODS -> DDS -> DM)
|
- `make transform` (запускает batch-процесс ODS -> DDS -> DM)
|
||||||
|
- `make superset-init` (повторная инициализация Superset: подключение к ClickHouse, датасеты, дашборд)
|
||||||
- `docker compose ps`
|
- `docker compose ps`
|
||||||
- `docker compose logs -f --tail=200 <service>`
|
- `docker compose logs -f --tail=200 <service>`
|
||||||
- `docker compose down` (сохраняет named volumes, включая `clickhouse-data`)
|
- `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 up -d --force-recreate superset-init superset
|
||||||
```
|
```
|
||||||
- После `docker compose down -v` нужно повторно прогнать: `ddl_init` -> `kafka_load` -> `etl_pipeline`.
|
- После `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 logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
from sqlalchemy.exc import NoSuchTableError
|
||||||
|
|
||||||
# Настройка логирования
|
# Настройка логирования
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -47,6 +48,27 @@ CLICKHOUSE_PORT = os.getenv('CLICKHOUSE_HTTP_PORT', '8123')
|
|||||||
CLICKHOUSE_DATABASE = os.getenv('CLICKHOUSE_DATABASE', 'default')
|
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:
|
def build_clickhouse_uri() -> str:
|
||||||
"""Собирает URI подключения к ClickHouse для Superset."""
|
"""Собирает URI подключения к ClickHouse для Superset."""
|
||||||
user = quote_plus(CLICKHOUSE_USER)
|
user = quote_plus(CLICKHOUSE_USER)
|
||||||
@@ -143,6 +165,17 @@ def import_datasets():
|
|||||||
"from superset.extensions import db",
|
"from superset.extensions import db",
|
||||||
"from superset.models.core import Database",
|
"from superset.models.core import Database",
|
||||||
"from superset.connectors.sqla.models import SqlaTable",
|
"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()",
|
"database = db.session.query(Database).filter_by(database_name='clickhouse_dwh').first()",
|
||||||
@@ -179,9 +212,13 @@ def import_datasets():
|
|||||||
" else:",
|
" else:",
|
||||||
f" print('Dataset {table_name} already exists')",
|
f" print('Dataset {table_name} already exists')",
|
||||||
" except Exception as e:",
|
" except Exception as e:",
|
||||||
f" print(f'ERROR: failed to refresh {table_name}: {{e}}')",
|
f" if is_expected_missing_table_error(e, {schema_name!r}, {table_name!r}):",
|
||||||
" db.session.rollback()",
|
f" print('WARNING: metadata not refreshed for {table_name}: source table/view not found yet')",
|
||||||
" errors += 1",
|
" db.session.rollback()",
|
||||||
|
" else:",
|
||||||
|
f" print(f'ERROR: failed to refresh {table_name}: {{e}}')",
|
||||||
|
" db.session.rollback()",
|
||||||
|
" errors += 1",
|
||||||
"else:",
|
"else:",
|
||||||
" try:",
|
" try:",
|
||||||
(
|
(
|
||||||
@@ -200,9 +237,14 @@ def import_datasets():
|
|||||||
f" print('Created dataset: {table_name}')",
|
f" print('Created dataset: {table_name}')",
|
||||||
" imported += 1",
|
" imported += 1",
|
||||||
" except Exception as e:",
|
" except Exception as e:",
|
||||||
f" print(f'ERROR: failed to create {table_name}: {{e}}')",
|
f" if is_expected_missing_table_error(e, {schema_name!r}, {table_name!r}):",
|
||||||
" db.session.rollback()",
|
" db.session.commit()",
|
||||||
" errors += 1",
|
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([
|
script_lines.extend([
|
||||||
|
|||||||
Reference in New Issue
Block a user