Убрана kafka, переделан dag на работу с csv

This commit is contained in:
2025-10-14 22:43:20 +03:00
parent 50865db5e6
commit d17c1da542
8 changed files with 182 additions and 220 deletions
+3 -7
View File
@@ -16,10 +16,6 @@ GP_PORT=5432
GP_CONN_ID=greenplum_conn GP_CONN_ID=greenplum_conn
GP_USE_AIRFLOW_CONN=true GP_USE_AIRFLOW_CONN=true
# Kafka Configuration # CSV Pipeline
KAFKA_BOOTSTRAP=kafka:29092 CSV_DIR=/opt/airflow/data
KAFKA_TOPIC=orders CSV_ROWS=1000
KAFKA_BATCH_SIZE=500
KAFKA_POLL_TIMEOUT=10
KAFKA_MAX_EMPTY_POLLS=3
KAFKA_UI_CLUSTER_NAME=KafkaCluster
+1
View File
@@ -6,3 +6,4 @@
__pycache__/ __pycache__/
*/__pycache__/ */__pycache__/
*.pyc *.pyc
data/
+26 -23
View File
@@ -1,12 +1,13 @@
# DE Starter Kit — Greenplum + Kafka + Airflow # DE Starter Kit — Airflow + Greenplum + CSV
Добро пожаловать в учебный стенд для изучения основ Data Engineering! Этот проект поможет вам освоить ключевые инструменты современных data pipeline: **Airflow** для оркестрации, **Kafka** для потоковой передачи данных и **Greenplum** как аналитическую базу данных. Добро пожаловать в учебный стенд для изучения основ Data Engineering! Этот проект поможет вам освоить ключевые инструменты современных data pipeline: **Airflow** для оркестрации, **pandas/CSV** для подготовки данных и **Greenplum** как аналитическую базу данных.
## 🎯 Что вы узнаете ## 🎯 Что вы узнаете
- Как настроить локальный стек данных с помощью Docker - Как настроить локальный стек данных с помощью Docker
- Как Airflow управляет workflow и координирует задачи - Как Airflow управляет workflow и координирует задачи
- Как данные перемещаются из Kafka в аналитическую базу - Как генерировать датасеты через pandas и сохранять их в CSV
- Как загружать данные в Greenplum пакетами и избегать дублей
- Как проверять качество данных в автоматизированных pipeline - Как проверять качество данных в автоматизированных pipeline
- Основы проектирования ETL/ELT процессов - Основы проектирования ETL/ELT процессов
@@ -38,22 +39,18 @@ docker compose run --rm airflow-init
### Шаг 3: Первый запуск pipeline ### Шаг 3: Первый запуск pipeline
1. Откройте Airflow UI: **http://localhost:8080** (логин/пароль: admin/admin) 1. Откройте Airflow UI: **http://localhost:8080** (логин/пароль: admin/admin)
2. Найдите DAG с названием **kafka_to_greenplum** 2. Найдите DAG с названием **csv_to_greenplum**
3. Нажмите на переключатель слева от названия DAG, чтобы включить его 3. Нажмите на переключатель слева от названия DAG, чтобы включить его
4. Нажмите кнопку **Trigger** (значок воспроизведения ▶️) 4. Нажмите кнопку **Trigger** (значок воспроизведения ▶️)
🎉 **Поздравляем!** Вы только что запустили свой первый data pipeline: 🎉 **Поздравляем!** Вы только что запустили свой первый data pipeline:
- Система сгенерировала 1000 тестовых заказов - Система сгенерировала 1000 тестовых заказов при помощи pandas
- Данные отправились в Kafka (систему потоковой передачи сообщений) - Датасет сохранился в CSV-файл в каталоге `./data`
- Airflow прочитал данные из Kafka и загрузил их в Greenplum - Airflow загрузил данные из CSV в Greenplum без дублей по `order_id`
### Шаг 4: Проверка результатов ### Шаг 4: Проверка результатов
**Через веб-интерфейс:** **Проверка вручную:**
- Kafka UI: **http://localhost:8082** — посмотрите топик `orders` и сообщения
- Airflow UI: **http://localhost:8080** — отслеживайте выполнение задач
**Через командную строку:**
```bash ```bash
# Подключитесь к Greenplum и проверьте данные # Подключитесь к Greenplum и проверьте данные
docker compose exec greenplum bash -c "su - gpadmin -c 'psql -p 5432 -d gpadmin'" docker compose exec greenplum bash -c "su - gpadmin -c 'psql -p 5432 -d gpadmin'"
@@ -61,8 +58,13 @@ docker compose exec greenplum bash -c "su - gpadmin -c 'psql -p 5432 -d gpadmin'
# Внутри psql выполните: # Внутри psql выполните:
\dt # Показать таблицы \dt # Показать таблицы
SELECT count(*) FROM public.orders; # Посчитать записи SELECT count(*) FROM public.orders; # Посчитать записи
# Посмотреть несколько строк
SELECT * FROM public.orders LIMIT 5;
``` ```
CSV-файлы после выполнения DAG остаются в директории `./data`. Их можно открыть любым редактором или изучить через pandas.
--- ---
## 🛠️ Подробная настройка (для уверенных пользователей) ## 🛠️ Подробная настройка (для уверенных пользователей)
@@ -105,12 +107,12 @@ make gp-psql # Подключение к Greenplum
### Основные компоненты ### Основные компоненты
- **Greenplum** — аналитическая база данных для хранения и анализа данных - **Greenplum** — аналитическая база данных для хранения и анализа данных
- **Kafka** — система потоковой передачи данных (без Zookeeper)
- **Airflow** — оркестратор workflow и задач - **Airflow** — оркестратор workflow и задач
- **Postgres** — база метаданных для Airflow - **Postgres** — база метаданных для Airflow
- **pandas** — библиотека для генерации и анализа данных в формате CSV
### Готовые DAG (workflow) ### Готовые DAG (workflow)
- **kafka_to_greenplum** — базовый pipeline: генерация → Kafka → Greenplum - **csv_to_greenplum** — базовый pipeline: pandas → CSV → Greenplum
- **greenplum_data_quality** — проверки качества данных (наличие таблицы, схема, дубликаты) - **greenplum_data_quality** — проверки качества данных (наличие таблицы, схема, дубликаты)
### Полезные команды ### Полезные команды
@@ -138,9 +140,9 @@ make logs # Следить за логами Airflow
- `GP_DB` — база данных (по умолчанию: gpadmin) - `GP_DB` — база данных (по умолчанию: gpadmin)
- `GP_PORT` — порт (по умолчанию: 5432) - `GP_PORT` — порт (по умолчанию: 5432)
### Kafka ### CSV pipeline
- `KAFKA_TOPIC` — имя топика (по умолчанию: orders) - `CSV_DIR` — путь к каталогу с CSV внутри контейнеров Airflow (по умолчанию: `/opt/airflow/data`)
- `KAFKA_BATCH_SIZE` — размер пакета при загрузке (по умолчанию: 500) - `CSV_ROWS` — количество строк, генерируемых DAG (по умолчанию: 1000)
### Airflow ### Airflow
- `GP_CONN_ID` — ID подключения (по умолчанию: greenplum_conn) - `GP_CONN_ID` — ID подключения (по умолчанию: greenplum_conn)
@@ -151,10 +153,11 @@ make logs # Следить за логами Airflow
### Архитектура pipeline ### Архитектура pipeline
**Поток данных в DAG `kafka_to_greenplum`:** **Поток данных в DAG `csv_to_greenplum`:**
1. `create_table` — создает таблицу `public.orders` в Greenplum 1. `create_orders_table` — создаёт таблицу `public.orders` в Greenplum
2. `produce_messages` — генерирует и отправляет сообщения в Kafka 2. `generate_csv` — генерирует датасет при помощи pandas и сохраняет CSV в `CSV_DIR`
3. `consume_and_load` — читает из Kafka и загружает в Greenplum батчами 3. `preview_csv` — выводит предпросмотр и статистику по данным
4. `load_csv_to_greenplum` — загружает CSV во временную таблицу и переносит новые строки в `public.orders`
> 💡 **Безопасность повторного запуска:** Pipeline защищен от дубликатов, поэтому его можно запускать многократно. > 💡 **Безопасность повторного запуска:** Pipeline защищен от дубликатов, поэтому его можно запускать многократно.
@@ -180,7 +183,7 @@ make logs # Следить за логами Airflow
|----------|---------| |----------|---------|
| Airflow UI не открывается | Дождитесь сообщения `Listening at: http://0.0.0.0:8080` в логах (`make logs`) | | Airflow UI не открывается | Дождитесь сообщения `Listening at: http://0.0.0.0:8080` в логах (`make logs`) |
| Ошибка подключения к Greenplum | Убедитесь, что контейнер `greenplum` стал статусом `healthy` (проверьте `docker compose ps`) | | Ошибка подключения к Greenplum | Убедитесь, что контейнер `greenplum` стал статусом `healthy` (проверьте `docker compose ps`) |
| Нет топика `orders` в Kafka | Создайте топик через Kafka UI или дождитесь авто-создания при первом запуске DAG | | Нет файла в `./data` после запуска DAG | Проверьте логи задачи `generate_csv`, убедитесь, что `CSV_DIR` смонтирован в docker-compose |
| Команда `make` не найдена | Используйте полные команды `docker compose` или установите make | | Команда `make` не найдена | Используйте полные команды `docker compose` или установите make |
--- ---
@@ -193,7 +196,7 @@ make logs # Следить за логами Airflow
├── Makefile # Удобные команды для работы ├── Makefile # Удобные команды для работы
├── airflow/ ├── airflow/
│ └── dags/ # Файлы workflow (DAG) │ └── dags/ # Файлы workflow (DAG)
│ ├── kafka_to_greenplum.py │ ├── csv_to_greenplum.py
│ └── data_quality_greenplum.py │ └── data_quality_greenplum.py
└── sql/ └── sql/
└── ddl_gp.sql # Создание таблицы в Greenplum └── ddl_gp.sql # Создание таблицы в Greenplum
@@ -0,0 +1,146 @@
from __future__ import annotations
import logging
import os
import random
from datetime import datetime, timedelta
from pathlib import Path
from typing import List
import pandas as pd
from airflow import DAG
from airflow.operators.python import PythonOperator
from helpers.greenplum import get_gp_conn
CSV_DIR = Path(os.getenv("CSV_DIR", "/opt/airflow/data"))
CSV_ROWS = int(os.getenv("CSV_ROWS", "1000"))
def _create_table() -> None:
"""Создаёт таблицу public.orders, если она ещё не существует."""
ddl = """
CREATE TABLE IF NOT EXISTS public.orders (
order_id BIGINT,
order_ts TIMESTAMP NOT NULL,
customer_id BIGINT NOT NULL,
amount NUMERIC(12,2) NOT NULL
)
WITH (appendonly=true, orientation=row, compresstype=zlib, compresslevel=1)
DISTRIBUTED BY (order_id);
"""
with get_gp_conn() as conn, conn.cursor() as cur:
cur.execute(ddl)
conn.commit()
def _generate_csv(rows: int, csv_dir: Path) -> str:
"""Генерирует CSV c заказами с помощью pandas и сохраняет на диск."""
csv_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
csv_path = csv_dir / f"orders_{timestamp}.csv"
base_order_id = int(datetime.utcnow().timestamp())
order_ids: List[int] = list(range(base_order_id * 1_000, base_order_id * 1_000 + rows))
now = datetime.utcnow()
order_ts = [(now - timedelta(seconds=i)).isoformat() for i in range(rows)]
customer_ids = [random.randint(1, 1_000) for _ in range(rows)]
amounts = [round(random.uniform(10, 500), 2) for _ in range(rows)]
df = pd.DataFrame(
{
"order_id": order_ids,
"order_ts": order_ts,
"customer_id": customer_ids,
"amount": amounts,
}
)
df.to_csv(csv_path, index=False)
logging.info("CSV сохранён: %s (строк: %s)", csv_path, len(df))
return str(csv_path)
def _preview_csv(csv_path: str, sample_rows: int = 5) -> None:
"""Отображает предпросмотр CSV через pandas (head и describe)."""
df = pd.read_csv(csv_path)
df["order_ts"] = pd.to_datetime(df["order_ts"], errors="coerce")
logging.info("Первые %s строк:\n%s", sample_rows, df.head(sample_rows).to_string(index=False))
numeric_summary = df.describe(include="number")
logging.info("Числовая статистика:\n%s", numeric_summary.to_string())
if df["order_ts"].notna().any():
logging.info(
"Диапазон order_ts: %s%s",
df["order_ts"].min().isoformat(),
df["order_ts"].max().isoformat(),
)
def _load_csv(csv_path: str) -> None:
"""Загружает CSV в Greenplum через временную таблицу и anti-join."""
csv_file = Path(csv_path)
if not csv_file.exists():
raise FileNotFoundError(f"CSV не найден: {csv_file}")
with get_gp_conn() as conn, conn.cursor() as cur, csv_file.open("r", encoding="utf-8") as f:
cur.execute("CREATE TEMP TABLE tmp_orders (LIKE public.orders INCLUDING DEFAULTS) ON COMMIT DROP;")
cur.copy_expert(
"COPY tmp_orders (order_id, order_ts, customer_id, amount) FROM STDIN WITH CSV HEADER",
f,
)
cur.execute("SELECT COUNT(*) FROM tmp_orders")
tmp_rows = cur.fetchone()[0]
cur.execute(
"""
INSERT INTO public.orders(order_id, order_ts, customer_id, amount)
SELECT t.order_id, t.order_ts, t.customer_id, t.amount
FROM tmp_orders t
LEFT JOIN public.orders o ON o.order_id = t.order_id
WHERE o.order_id IS NULL
"""
)
inserted = cur.rowcount if cur.rowcount != -1 else 0
conn.commit()
logging.info("Загружено строк: %s (прочитано из CSV: %s)", inserted, tmp_rows)
default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(seconds=30)}
with DAG(
dag_id="csv_to_greenplum",
start_date=datetime(2024, 1, 1),
schedule=None,
catchup=False,
default_args=default_args,
tags=["demo", "greenplum", "csv"],
) as dag:
create_table = PythonOperator(
task_id="create_orders_table",
python_callable=_create_table,
)
generate_csv = PythonOperator(
task_id="generate_csv",
python_callable=_generate_csv,
op_kwargs={"rows": CSV_ROWS, "csv_dir": CSV_DIR},
)
preview_csv = PythonOperator(
task_id="preview_csv",
python_callable=_preview_csv,
op_kwargs={
"csv_path": "{{ ti.xcom_pull(task_ids='generate_csv') }}",
"sample_rows": 5,
},
)
load_csv = PythonOperator(
task_id="load_csv_to_greenplum",
python_callable=_load_csv,
op_kwargs={
"csv_path": "{{ ti.xcom_pull(task_ids='generate_csv') }}",
},
)
create_table >> generate_csv >> preview_csv >> load_csv
@@ -50,7 +50,7 @@ def assert_orders_table_exists(conn) -> None:
""" """
) )
if cur.fetchone() is None: if cur.fetchone() is None:
raise ValueError("Таблица public.orders не найдена; запусти DAG kafka_to_greenplum.") raise ValueError("Таблица public.orders не найдена; запусти DAG csv_to_greenplum.")
def fetch_orders_schema(conn) -> Sequence[Tuple[str, str]]: def fetch_orders_schema(conn) -> Sequence[Tuple[str, str]]:
@@ -82,7 +82,7 @@ def fetch_orders_count(conn) -> int:
def assert_orders_have_rows(conn) -> None: def assert_orders_have_rows(conn) -> None:
"""Проверяет, что таблица orders не пустая.""" """Проверяет, что таблица orders не пустая."""
if fetch_orders_count(conn) <= 0: if fetch_orders_count(conn) <= 0:
raise ValueError("Таблица public.orders пустая — запусти DAG kafka_to_greenplum перед проверкой.") raise ValueError("Таблица public.orders пустая — запусти DAG csv_to_greenplum перед проверкой.")
def fetch_orders_duplicates(conn) -> int: def fetch_orders_duplicates(conn) -> int:
@@ -1,146 +0,0 @@
from __future__ import annotations
import json
import os
import random
from datetime import datetime, timedelta
from typing import List, Tuple, Optional
from airflow import DAG
from airflow.operators.python import PythonOperator
from confluent_kafka import Consumer, KafkaException, Producer
from psycopg2.extras import execute_values
from helpers.greenplum import get_gp_conn
KAFKA_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP", "kafka:29092")
TOPIC = os.getenv("KAFKA_TOPIC", "orders")
BATCH_SIZE = int(os.getenv("KAFKA_BATCH_SIZE", "500"))
POLL_TIMEOUT_S = int(os.getenv("KAFKA_POLL_TIMEOUT", "10"))
MAX_EMPTY_POLLS = int(os.getenv("KAFKA_MAX_EMPTY_POLLS", "3"))
def _create_table():
ddl = """
CREATE TABLE IF NOT EXISTS public.orders (
order_id BIGINT,
order_ts TIMESTAMP NOT NULL,
customer_id BIGINT NOT NULL,
amount NUMERIC(12,2) NOT NULL
)
WITH (appendonly=true, orientation=column, compresstype=zlib)
DISTRIBUTED BY (order_id);
"""
with get_gp_conn() as conn, conn.cursor() as cur:
cur.execute(ddl)
conn.commit()
def _produce(n=1000):
producer = Producer({"bootstrap.servers": KAFKA_BOOTSTRAP})
for idx in range(n):
payload = {
"order_id": idx + 1,
"order_ts": datetime.utcnow().isoformat(),
"customer_id": random.randint(1, 100),
"amount": round(random.uniform(10, 500), 2),
}
producer.produce(TOPIC, json.dumps(payload).encode("utf-8"))
producer.flush()
def _flush_batch(cur, rows: List[Tuple]):
"""Insert deduplicated batch of rows into public.orders for GP6 (no PK support)."""
if not rows:
return
# Дедупликация внутри батча по первичному ключу (order_id)
by_id = {int(r[0]): r for r in rows}
unique_rows = list(by_id.values())
# Вставка через VALUES + anti-join для GP6 (без ON CONFLICT)
execute_values(
cur,
"""
INSERT INTO public.orders(order_id, order_ts, customer_id, amount)
SELECT v.order_id, v.order_ts, v.customer_id, v.amount
FROM (VALUES %s) AS v(order_id, order_ts, customer_id, amount)
LEFT JOIN public.orders o ON o.order_id = v.order_id
WHERE o.order_id IS NULL
""",
unique_rows,
template="(%s,%s,%s,%s)",
)
def _consume_and_load(max_messages=1000, timeout_s: Optional[int] = None):
if timeout_s is None:
timeout_s = POLL_TIMEOUT_S
consumer = Consumer(
{
"bootstrap.servers": KAFKA_BOOTSTRAP,
"group.id": "airflow-loader-gp",
"auto.offset.reset": "earliest",
"enable.auto.commit": False,
}
)
consumer.subscribe([TOPIC])
with get_gp_conn() as conn, conn.cursor() as cur:
batch: List[Tuple] = []
consumed = 0
empty_polls = 0
while consumed < max_messages and empty_polls < MAX_EMPTY_POLLS:
msg = consumer.poll(timeout_s)
if msg is None:
empty_polls += 1
continue
empty_polls = 0
if msg.error():
raise KafkaException(msg.error())
data = json.loads(msg.value().decode("utf-8"))
batch.append(
(
int(data["order_id"]),
datetime.fromisoformat(data["order_ts"]),
int(data["customer_id"]),
float(data["amount"]),
)
)
consumed += 1
if len(batch) >= BATCH_SIZE:
_flush_batch(cur, batch)
conn.commit()
batch.clear()
# Финальный сброс, если вышли по лимиту сообщений
if batch:
_flush_batch(cur, batch)
conn.commit()
# Фиксируем оффсеты после успешной загрузки
consumer.commit()
consumer.close()
default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(seconds=30)}
with DAG(
dag_id="kafka_to_greenplum",
start_date=datetime(2024, 1, 1),
schedule=None,
catchup=False,
default_args=default_args,
tags=["demo", "kafka", "greenplum"],
) as dag:
create_table = PythonOperator(task_id="create_table", python_callable=_create_table)
produce = PythonOperator(
task_id="produce_messages", python_callable=_produce, op_kwargs={"n": 1000}
)
consume_and_load = PythonOperator(
task_id="consume_and_load",
python_callable=_consume_and_load,
op_kwargs={"max_messages": 1000},
)
create_table >> produce >> consume_and_load
+1 -1
View File
@@ -1,2 +1,2 @@
confluent-kafka==2.3.0
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
pandas==2.1.4
+3 -41
View File
@@ -40,42 +40,6 @@ services:
timeout: 5s timeout: 5s
retries: 30 retries: 30
kafka:
image: apache/kafka:3.8.0
environment:
KAFKA_BROKER_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_NODE_ID: 1
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:29093
KAFKA_LISTENERS: PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LOG_DIRS: /tmp/kraft-combined-logs
# 22-символьный base64 идентификатор для KRaft-кластера
CLUSTER_ID: aGVsbG93b3JsZGtpdGNoZW4
ports:
- "9092:9092"
kafka-ui:
image: provectuslabs/kafka-ui:v0.7.2
env_file: .env
ports:
- 8082:8080
environment:
DYNAMIC_CONFIG_ENABLED: "true"
KAFKA_CLUSTERS_0_NAME: ${KAFKA_UI_CLUSTER_NAME:-KafkaCluster}
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: ${KAFKA_BOOTSTRAP:-kafka:29092}
volumes:
- kafka-ui-data:/app/data
depends_on:
- kafka
airflow-webserver: airflow-webserver:
image: apache/airflow:2.9.2 image: apache/airflow:2.9.2
container_name: gp_airflow_web container_name: gp_airflow_web
@@ -91,11 +55,10 @@ services:
volumes: volumes:
- ./airflow/dags:/opt/airflow/dags - ./airflow/dags:/opt/airflow/dags
- ./airflow/requirements.txt:/opt/airflow/requirements.txt - ./airflow/requirements.txt:/opt/airflow/requirements.txt
- ./data:/opt/airflow/data
depends_on: depends_on:
pgmeta: pgmeta:
condition: service_healthy condition: service_healthy
kafka:
condition: service_started
greenplum: greenplum:
condition: service_healthy condition: service_healthy
@@ -112,11 +75,10 @@ services:
volumes: volumes:
- ./airflow/dags:/opt/airflow/dags - ./airflow/dags:/opt/airflow/dags
- ./airflow/requirements.txt:/opt/airflow/requirements.txt - ./airflow/requirements.txt:/opt/airflow/requirements.txt
- ./data:/opt/airflow/data
depends_on: depends_on:
pgmeta: pgmeta:
condition: service_healthy condition: service_healthy
kafka:
condition: service_started
greenplum: greenplum:
condition: service_healthy condition: service_healthy
@@ -130,6 +92,7 @@ services:
volumes: volumes:
- ./airflow/dags:/opt/airflow/dags - ./airflow/dags:/opt/airflow/dags
- ./airflow/requirements.txt:/opt/airflow/requirements.txt - ./airflow/requirements.txt:/opt/airflow/requirements.txt
- ./data:/opt/airflow/data
command: > command: >
bash -lc " bash -lc "
set -e; set -e;
@@ -147,5 +110,4 @@ services:
volumes: volumes:
pgmeta: pgmeta:
kafka-ui-data:
greenplum_data: greenplum_data: