Compare commits

..
12 Commits
Author SHA1 Message Date
ddmitry 0bdce356e3 Merge pull request 'Запуск курса: JupyterLab и VS Code в браузере' (#5) from t3/browser-vscode-setup into main 2026-10-21 12:22:42 +03:00
ddadmin db8f275ea0 docs(course): make JupyterLab the primary notebook interface 2026-10-21 10:22:00 +02:00
ddadmin 863d658a5f fix(trino): bound memory for the learning stack 2026-10-19 10:47:40 +02:00
ddadmin 21f3570b89 docs: clarify learner workflow and resource requirements 2026-10-19 09:48:24 +02:00
ddadmin 78cf7b5fae fix(stack): use pinned MinIO image with registry fallback 2026-10-19 09:26:12 +02:00
ddadmin 7d451a7ce9 feat(ide): add browser VS Code for course notebooks 2026-10-18 22:39:39 +02:00
ddadmin bf3241124c fix(course): clarify history and fix snapshot expiration
Set an explicit expiration cutoff because retain_last alone preserves fresh snapshots under the default five-day age threshold. Add result checks, distinguish time travel from rollback, and explain schema metadata versions.

Validated changed cells with Spark 3.5.1 and Iceberg 1.10.0 in an isolated container: 10 to 2 snapshots, 1300 rows preserved, expired time travel rejected, repeat execution in UTC and America/New_York. Notebook validation, Python syntax, local links and diff checks pass.
2026-09-20 16:14:53 -04:00
ddmitry 872b876be1 Merge pull request 'feat(course): практика настройки SparkSession в модуле 3' (#4) from 2-spark-session-practice into main 2026-09-15 16:20:45 +03:00
ddadmin cc0cd3857d docs(course): mark module 3 work complete 2026-09-15 09:20:04 -04:00
ddadmin 9d5b0b716e docs(course): integrate Spark setup into the module flow 2026-09-15 09:12:28 -04:00
ddadmin 16f76d63fc docs(readme): remove module-specific practice details 2026-09-15 09:04:39 -04:00
ddadmin 7c5e976405 feat(course): add SparkSession setup practice to module 3 2026-09-15 09:01:41 -04:00
27 changed files with 1650 additions and 291 deletions

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
# Копировать в .env необязательно: это значения по умолчанию.
# Если .env уже есть, добавь нужную настройку в него, сохранив остальные.
# GHCR — основной источник MinIO. Резерв того же автора и digest: docker.io.
MINIO_REGISTRY=ghcr.io
# На Linux при необходимости укажи свой UID и GID (команды: id -u; id -g).
NB_UID=1000
NB_GID=100
+31 -11
View File
@@ -17,14 +17,15 @@
## Стек ## Стек
Всё работает локально в Docker. Никаких облаков, внешних зависимостей и регистраций. Всё работает локально в Docker. Для первой сборки и скачивания учебных данных
нужен интернет; облачный аккаунт не требуется.
```mermaid ```mermaid
graph TB graph TB
U[Student / engineer] U[Student / engineer]
subgraph Access subgraph Access
J[JupyterLab<br/>notebooks / PySpark] J[JupyterLab<br/>Notebooks / PySpark]
Q[Trino UI / CLI<br/>ad hoc SQL] Q[Trino UI / CLI<br/>ad hoc SQL]
end end
@@ -54,41 +55,60 @@ graph TB
| `PostgreSQL` | Catalog — метаданные JDBC-каталога `lakehouse` | | `PostgreSQL` | Catalog — метаданные JDBC-каталога `lakehouse` |
| `Spark` | Compute — ETL, запись и чтение Iceberg-таблиц | | `Spark` | Compute — ETL, запись и чтение Iceberg-таблиц |
| `Trino` | Compute — ad hoc SQL, чтение тех же таблиц | | `Trino` | Compute — ad hoc SQL, чтение тех же таблиц |
| `JupyterLab` | Точка входа — практические ноутбуки курса | | `JupyterLab` | Основной интерфейс — ноутбуки курса, Python и терминал внутри Docker |
| `code-server` | VS Code в браузере — альтернативный интерфейс для тех же ноутбуков |
Ноутбуки подключаются к Spark-кластеру с Ноутбуки подключаются к Spark-кластеру с
[готовыми настройками стенда](./docs/stack_reference.md#spark). [готовыми настройками стенда](./docs/stack_reference.md#spark).
## С чего начать ## С чего начать
1. Открой [START_HERE.md](./START_HERE.md) — запуск стенда, диагностика, первый ноутбук. 1. Открой [START_HERE.md](./START_HERE.md) — подготовка, запуск и первый ноутбук
2. Пройди `notebooks/01_environment_and_smoke_test.ipynb`. с ожидаемым результатом.
3. Дальше по порядку: [docs/course_program.md](./docs/course_program.md). 2. Продолжи по [маршруту из восьми модулей](./docs/course_program.md#маршрут-по-модулям).
3. Между занятиями используй [памятку учащегося](./docs/study_workflow.md):
остановка, продолжение работы и диагностика.
Краткий запуск из корня репозитория: Для одного учащегося рекомендуем компьютер с 16 ГБ RAM, не менее 4 CPU и
ориентировочно 8 ГБ памяти для Docker, а также от 20 ГБ свободного места перед сборкой.
Trino ограничен 2 ГиБ памяти контейнера и 1 ГиБ Java heap.
Подробности и границы проверки — в [системных требованиях](./START_HERE.md#системные-требования).
Краткий запуск из корня репозитория после подготовки по `START_HERE`:
```bash ```bash
docker compose pull minio minio-init postgres trino
docker compose build docker compose build
docker compose up -d docker compose up -d
docker compose ps docker compose ps -a
``` ```
Зафиксированные исторические образы MinIO загружаются из официального Quay registry; подробности — в [техническом reference](./docs/stack_reference.md#minio). MinIO скачивается готовым из GHCR, его версия и digest закреплены. Если какой-то
источник недоступен из твоей сети, начни с
[инструкции по доступу к репозиториям](./docs/network_access.md).
После старта: После старта:
| UI | Адрес | | UI | Адрес |
| --- | --- | | --- | --- |
| JupyterLab | `http://localhost:8888` | | JupyterLab | `http://localhost:8888` |
| VS Code в браузере (по желанию) | `http://localhost:8889` |
| Spark Master | `http://localhost:8080` | | Spark Master | `http://localhost:8080` |
| Trino | `http://localhost:8090` | | Trino | `http://localhost:8090` |
| MinIO Console | `http://localhost:9001` | | MinIO Console | `http://localhost:9001` |
JupyterLab опубликован только на loopback-интерфейсе хоста и рассчитан на локальный одно-пользовательский стенд. Подключение через VS Code Remote-SSH описано в [START_HERE.md](./START_HERE.md#как-работать-в-jupyter). Открой ноутбук в JupyterLab: Python работает внутри контейнера, локальный
Python и venv не нужны. JupyterLab доступен только с хоста стенда.
Первый запуск ноутбука описан в [START_HERE.md](./START_HERE.md#как-работать-с-ноутбуками).
VS Code запускается вместе со стендом. Его можно использовать по желанию;
он совместим не со всеми сочетаниями браузера, расширений и средств защиты.
[Выбор ядра и доступ к удалённому стенду](./docs/stack_reference.md#vs-code-в-браузере).
## Документы для прохождения ## Документы для прохождения
- [START_HERE.md](./START_HERE.md) — первый маршрут: prerequisites, запуск, диагностика. - [START_HERE.md](./START_HERE.md) — подготовка компьютера, запуск и первый результат.
- [docs/study_workflow.md](./docs/study_workflow.md) — памятка для каждого занятия и помощь при ошибках.
- [docs/course_program.md](./docs/course_program.md) — модульная структура и состав учебных материалов. - [docs/course_program.md](./docs/course_program.md) — модульная структура и состав учебных материалов.
- [docs/stack_reference.md](./docs/stack_reference.md) — порты, команды, конфиги, шпаргалка. - [docs/stack_reference.md](./docs/stack_reference.md) — порты, команды, конфиги, шпаргалка.
- [docs/glossary.md](./docs/glossary.md) — справочник терминов (storage, catalog, compute, snapshot и др.). - [docs/glossary.md](./docs/glossary.md) — справочник терминов (storage, catalog, compute, snapshot и др.).
+185 -92
View File
@@ -1,31 +1,88 @@
# START HERE # START HERE
Этот документ нужен для первого входа в курс: поднять стенд, проверить сервисы, открыть интерфейсы, выполнить базовый smoke test и подготовить локальный data bundle для следующих модулей. Здесь ты подготовишь окружение и выполнишь первое вычисление в Spark.
К концу запуска в ноутбуке появится `Spark cluster smoke test passed.`, а результат
сохранится в файле на твоем компьютере. Smoke test — короткая проверка, что
основные компоненты работают вместе.
Если стенд уже собран, открой [памятку для следующего занятия](./docs/study_workflow.md).
Она объясняет, как продолжить работу, что сохраняется и куда смотреть при ошибках.
## Маршрут прохождения ## Маршрут прохождения
1. Проверить prerequisites. 1. Подготовь Docker, ресурсы и локальную копию репозитория.
2. Собрать и поднять стенд. 2. Скачай образы, собери окружение и проверь запуск сервисов.
3. Убедиться, что контейнеры живы. 3. Открой первый ноутбук в JupyterLab и проверь результат вычисления.
4. Открыть основные UI. 4. Продолжи по [программе курса](./docs/course_program.md#маршрут-по-модулям).
5. Зайти в Jupyter и выполнить `notebooks/01_environment_and_smoke_test.ipynb`.
6. Перед Модулем 3 подготовить учебный датасет в `./data/nyc_taxi`.
7. При проблемах использовать логи и шаги диагностики из этого документа.
## Prerequisites Датасет Taxi понадобится перед Модулем 3, DBeaver необязателен и понадобится
только перед Модулем 6. Для первого ноутбука их готовить не нужно.
Нужно заранее установить: <a id="prerequisites"></a>
- Docker; ## Что подготовить
- Docker Compose;
- современный браузер для UI;
- свободные порты на хосте.
Рекомендуемые ресурсы хоста: - Docker с запущенным движком и Docker Compose 2.17+ с BuildKit.
- Современный браузер; основной интерфейс курса — JupyterLab на `localhost:8888`.
- Git для скачивания репозитория или его распакованную ZIP-копию.
- Свободные порты из таблицы ниже и доступ к источникам образов и зависимостей.
- не менее `4 vCPU`; Команды ниже рассчитаны на Bash: терминал Linux/macOS или терминал Linux
- не менее `10 GB RAM`, иначе `Spark`, `Trino` и `Jupyter` могут стартовать нестабильно; в WSL2 на Windows с включенной интеграцией Docker Desktop. Выполняй их на
- хотя бы `8-10 GB` свободного места под образы и контейнеры. машине, где работает Docker. Терминал внутри JupyterLab для запуска
`docker compose` не подходит.
### Системные требования
Ориентир для одного учащегося и основного набора Taxi за три месяца:
| Ресурс | Рекомендация | Что проверить |
| --- | --- | --- |
| Процессор | Не менее 4 CPU, доступных Docker | В Docker Desktop/WSL2 может стоять меньший лимит |
| Память Docker | Ориентир — 8 ГБ RAM; 10 ГБ дают дополнительный запас | Это память для среды Docker, а не общий объем памяти компьютера |
| Память компьютера | 16 ГБ RAM или больше | Оставь память ОС, браузеру и другим приложениям |
| Свободное место | Не менее 20 ГБ перед первой сборкой; 30 ГБ с запасом | На диске Docker и в папке репозитория: образы, кэш сборки, исходные данные и таблицы |
Это рекомендуемый бюджет для одного активного ноутбука, а не измеренный минимум.
На Windows с WSL2 по умолчанию доступно 50% RAM компьютера: при 16 ГБ это 8 ГБ
для всей WSL2, включая Docker. Лимит можно настроить, это не аппаратное
ограничение. См. [настройки WSL2](https://learn.microsoft.com/en-us/windows/wsl/wsl-config#main-wsl-settings)
и [ресурсы Docker Desktop](https://docs.docker.com/desktop/settings-and-maintenance/settings/#resources).
Trino ограничен 2 ГиБ памяти контейнера и 1 ГиБ Java heap. Проверка запросов
на данных курса описана в [справочнике](./docs/stack_reference.md#ресурсы-и-проверенная-среда).
Полный курс под общим жестким лимитом Docker 8 ГБ пока не проверен. В WSL2 часть
памяти также нужна самой Linux-среде; закрой другие тяжелые задачи на время урока.
Место внутри диска Docker и свободное место на диске компьютера — отдельные ограничения.
С добавлением VS Code образ окружения вырос примерно на 1,2 ГБ относительно
Jupyter; базовые слои у них общие. MinIO скачивается готовым, компилировать его
не нужно. [Основания оценок и границы проверки](./docs/stack_reference.md#ресурсы-и-проверенная-среда).
Python, Java, Jupyter и расширения VS Code устанавливаются при сборке образов.
На компьютере не нужно создавать venv или устанавливать Python-пакеты.
Для первой сборки нужен интернет. Если ты работаешь из РФ или сети с
ограничениями, при ошибке скачивания используй
[инструкцию по доступу к репозиториям](./docs/network_access.md). Она различает
проблемы скачивания образов и зависимостей внутри сборки.
### Проверка до сборки
Открой терминал в папке репозитория — там, где лежит `docker-compose.yml`:
```bash
docker version
docker compose version
docker info --format 'CPUs={{.NCPU}} MemoryBytes={{.MemTotal}}'
docker compose config --quiet
```
В `docker version` должны быть разделы Client и Server, версия Compose — не
ниже 2.17. `docker info` показывает ресурсы, которые видит Docker; память дана
в байтах (8 ГиБ — 8 589 934 592 байта). Последняя команда при успехе
ничего не печатает. Если Docker не отвечает или конфигурация не читается,
исправь эту ошибку до сборки.
## Порты стенда ## Порты стенда
@@ -38,19 +95,45 @@
| MinIO API | `http://localhost:9000` | S3-compatible endpoint | | MinIO API | `http://localhost:9000` | S3-compatible endpoint |
| MinIO Console | `http://localhost:9001` | Просмотр бакетов и файлов | | MinIO Console | `http://localhost:9001` | Просмотр бакетов и файлов |
| JupyterLab | `http://localhost:8888` | Основная точка входа в практику | | JupyterLab | `http://localhost:8888` | Основная точка входа в практику |
| VS Code в браузере | `http://localhost:8889` | Альтернативный интерфейс, по желанию |
| PostgreSQL | `localhost:5432` | JDBC-каталог Iceberg, нужен для диагностики | | PostgreSQL | `localhost:5432` | JDBC-каталог Iceberg, нужен для диагностики |
## Быстрый запуск ## Быстрый запуск
Все команды выполняются из корня репозитория. Все команды выполняются из корня репозитория.
### 1. Собрать образы На Linux и в WSL2 проверь владельца файлов:
```bash ```bash
id -u
id -g
```
Это UID и GID — числовые идентификаторы пользователя и группы. В контейнере по
умолчанию используются `1000` и `100`. Если значения отличаются, до сборки
создай `.env` в корне репозитория и укажи `NB_UID` и `NB_GID` со своими числами.
Например, для вывода `1001` и `1001`:
```dotenv
NB_UID=1001
NB_GID=1001
```
Если `.env` уже есть, измени только эти строки, сохранив остальные настройки.
Файл остается между занятиями. Образец — [`.env.example`](./.env.example).
На macOS с Docker Desktop обычно подходят значения по умолчанию.
### 1. Скачать готовые образы и собрать окружение
```bash
docker compose pull minio minio-init postgres trino
docker compose build docker compose build
``` ```
Зафиксированные образы MinIO загружаются из `quay.io`; их версии и назначение описаны в [техническом reference](./docs/stack_reference.md#minio). Первая команда проверяет загрузку готовых сервисов до сборки окружения.
MinIO уже собран; для Spark, Jupyter и IDE первая сборка скачивает зависимости
и зависит по времени от сети и компьютера. Дождись завершения без ошибок;
не переходи к запуску после неудачной сборки. Повторная сборка использует кэш Docker.
### 2. Поднять стенд ### 2. Поднять стенд
@@ -61,33 +144,36 @@ docker compose up -d
### 3. Проверить статус контейнеров ### 3. Проверить статус контейнеров
```bash ```bash
docker compose ps docker compose ps -a
``` ```
Ожидаемое состояние: Ожидаемое состояние:
- сервисы `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`, `trino`, `jupyter` находятся в состоянии `Up`; - `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`,
- однократные init-контейнеры вроде `minio-init` и `iceberg-catalog-init` могут завершиться после успешной инициализации. `trino`, `jupyter`, `ide` работают (`Up`); у `postgres-iceberg`, `trino` и `ide`
дождись `healthy`;
- `minio-init` и `iceberg-catalog-init` заканчивают работу со статусом `Exited (0)`.
Это успешная разовая подготовка хранилища и каталога. `Exited (1)` и другой
ненулевой код требуют проверки логов.
Флаг `-a` показывает и завершившиеся контейнеры. Сам по себе статус `Up` еще
не доказывает, что вычисления работают: дальше проверим их в ноутбуке.
## Куда заходить после старта ## Куда заходить после старта
Открой в браузере: | Что открыть | Что должно быть видно |
| --- | --- |
| [Spark Master](http://localhost:8080) | Два worker-а в состоянии `ALIVE` |
| [MinIO Console](http://localhost:9001) | После входа `minioadmin` / `minioadmin` виден бакет `lakehouse` — контейнер для файлов курса |
| [JupyterLab](http://localhost:8888) | Слева список ноутбуков `01_…` — `08_…` |
- `http://localhost:8080` для `Spark Master UI`; В [Trino UI](http://localhost:8090) для входа достаточно имени, например `student`;
- `http://localhost:8090` для `Trino UI`; пароль в стенде не задан. Сейчас достаточно проверить, что интерфейс открывается.
- `http://localhost:9001` для `MinIO Console`; Чтение общих таблиц через Trino разберем в Модуле 6.
- `http://localhost:8888` для `JupyterLab`.
Для входа в `MinIO Console` используй: Этот маршрут предполагает Docker на твоем компьютере: `localhost` относится
к машине с браузером. Доступ к JupyterLab на удаленном сервере описан отдельно
- логин: `minioadmin`; в [инструкции по SSH-туннелю](./docs/stack_reference.md#jupyterlab).
- пароль: `minioadmin`.
Если интерфейсы открываются, переходи в Jupyter и запускай:
```text
notebooks/01_environment_and_smoke_test.ipynb
```
## Роли сервисов в стенде ## Роли сервисов в стенде
@@ -97,68 +183,75 @@ notebooks/01_environment_and_smoke_test.ipynb
| `PostgreSQL` | `catalog`: хранит метаданные JDBC-каталога Iceberg | | `PostgreSQL` | `catalog`: хранит метаданные JDBC-каталога Iceberg |
| `Spark` | `compute`: выполняет PySpark и Spark SQL задания | | `Spark` | `compute`: выполняет PySpark и Spark SQL задания |
| `Trino` | `compute`: читает те же таблицы через общий каталог | | `Trino` | `compute`: читает те же таблицы через общий каталог |
| `Jupyter` | Точка входа в учебные ноутбуки | | `JupyterLab` | Интерфейс для учебных ноутбуков |
## Как работать в Jupyter ## Как работать с ноутбуками
- JupyterLab доступен без логина только с хоста, на котором запущен стенд; Ноутбук — файл с пояснениями, кодом и сохраненным выводом. Ядро — процесс Python,
- при работе через VS Code Remote-SSH выбери `Existing Jupyter Server` и укажи `http://127.0.0.1:8888/`; который исполняет код и хранит переменные до остановки.
- token в адрес добавлять не нужно: аутентификация и XSRF-проверка отключены для локального одно-пользовательского сценария;
- `./notebooks` смонтирован в контейнер как `/opt/work`;
- `./src` смонтирован read-only как `/opt/src`;
- `./data` смонтирован read-only как `/opt/data`;
- `PYTHONPATH=/opt/src`, поэтому helper-скрипты из `src/` доступны для импорта в ноутбуках;
- первый ноутбук курса: `01_environment_and_smoke_test.ipynb`;
- локальный учебный data bundle для Модуля 3 должен быть доступен внутри Jupyter по пути `/opt/data/nyc_taxi`.
Ноутбуки берут адрес кластера из 1. Открой `http://localhost:8888` в отдельной вкладке браузера.
[`spark/spark-defaults.conf`](./spark/spark-defaults.conf). Если изменил его, 2. Дважды нажми на `01_environment_and_smoke_test.ipynb` в списке файлов слева.
перезапусти ядро и заново создай Spark-сессию. Повторный запуск ячейки с 3. Если появится выбор ядра, выбери **Python 3 (ipykernel)**. Изменить ядро можно
`getOrCreate()` использует текущую сессию. При перезапуске ядра переменные Python через **Kernel → Change Kernel…**. Оно работает внутри контейнера `jupyter`,
сбрасываются, а учебные таблицы и файлы сохраняются. где уже есть зависимости курса. Дождись состояния **Idle** в нижней панели.
4. Выдели первую кодовую ячейку и запусти её через `Shift+Enter`, затем остальные
ячейки по порядку. Результат появится под ячейкой. Сохрани ноутбук через
**File → Save Notebook** (`Ctrl+S`; на macOS — `Cmd+S`).
Терминал JupyterLab тоже работает внутри контейнера. Команды `docker compose`
выполняй в терминале хоста, откуда запускал стенд.
### Как понять, что первый запуск прошел
В третьей кодовой ячейке первого ноутбука появится отчет, содержащий:
```text
Spark cluster smoke test passed.
master_url=spark://spark-master:7077
result_sum=499999500000
expected_sum=499999500000
```
В отчете есть и другие строки; значения выше должны совпасть. Затем выполни
самостоятельное задание: замени `TODO` в словаре `service_roles` ролями сервисов.
Сам факт запуска ячейки с `TODO` не означает, что задание выполнено.
В конце выполни ячейку `spark.stop()`, сохрани ноутбук и проверь, что его изменения
видны в `notebooks/01_environment_and_smoke_test.ipynb` на хосте. После повторного
открытия сохраненный вывод остается виден. Такой вывод — запись прошлого запуска;
он не означает, что ядро Python и Spark-сессия продолжают работать.
Правила запуска ячеек, перехода между модулями и сохранения данных собраны в
[памятке учащегося](./docs/study_workflow.md).
По желанию можно работать в [VS Code на `localhost:8889`](./docs/stack_reference.md#vs-code-в-браузере):
он запускается вместе со стендом. Некоторые сочетания браузера, расширений и
средств защиты мешают открытию ноутбуков в VS Code. В таком случае продолжи
курс в JupyterLab. Файлы общие, а ядра Python отдельные: перед сменой интерфейса
сохрани ноутбук, выполни `spark.stop()` и останови ядро. Не редактируй один файл
в двух интерфейсах одновременно.
## Базовая диагностика ## Базовая диагностика
### Посмотреть список контейнеров Начни с `docker compose ps -a` и логов проблемного сервиса, например:
```bash ```bash
docker compose ps docker compose logs --tail=100 jupyter
``` ```
### Посмотреть логи конкретного сервиса Выбери свой симптом в [таблице диагностики](./docs/study_workflow.md#если-что-то-не-работает).
При ошибке скачивания сразу открой [диагностику сети](./docs/network_access.md).
```bash
docker compose logs -f spark-master
docker compose logs -f trino
docker compose logs -f minio
docker compose logs -f jupyter
```
### Типовые первые проверки
- `Spark UI` не открывается: проверь `docker compose ps` и логи `spark-master`.
- `Trino UI` не открывается: проверь `docker compose ps` и логи `trino`.
- `JupyterLab` не открывается: проверь `docker compose ps` и логи `jupyter`.
- `MinIO Console` не открывается: проверь `docker compose ps` и логи `minio`.
- smoke test падает из ноутбука: сначала убедись, что `spark-master` и worker-ы видны в `Spark UI`.
## Restart и reset ## Restart и reset
### Мягкий перезапуск стенда Для обычной паузы используй `docker compose stop`, на следующем занятии —
`docker compose up -d`. Порядок действий и последствия удаления контейнеров
описаны в [памятке](./docs/study_workflow.md#закончить-занятие).
```bash Полный сброс удаляет учебные таблицы и каталог. Его команда вынесена в
docker compose down [отдельный раздел](./docs/study_workflow.md#начать-с-чистых-данных), чтобы не путать
docker compose up -d с окончанием занятия.
```
### Полный reset с удалением данных
```bash
docker compose down -v
docker compose up -d
```
Используй полный reset, если хочешь пройти практику заново с чистого состояния.
## Подготовка учебного датасета (перед Модулем 3) ## Подготовка учебного датасета (перед Модулем 3)
@@ -167,6 +260,7 @@ docker compose up -d
Скачай каноническое учебное подмножество `NYC TLC Yellow Taxi Trip Records`: Скачай каноническое учебное подмножество `NYC TLC Yellow Taxi Trip Records`:
```bash ```bash
mkdir -p data/nyc_taxi
curl -fLo data/nyc_taxi/yellow_tripdata_2024-01.parquet \ curl -fLo data/nyc_taxi/yellow_tripdata_2024-01.parquet \
https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet
curl -fLo data/nyc_taxi/yellow_tripdata_2024-02.parquet \ curl -fLo data/nyc_taxi/yellow_tripdata_2024-02.parquet \
@@ -197,13 +291,12 @@ ls -lh data/nyc_taxi/
- Зачем: в Модуле 6 можно работать с Trino через DBeaver параллельно с ноутбуком — привычный SQL-интерфейс. - Зачем: в Модуле 6 можно работать с Trino через DBeaver параллельно с ноутбуком — привычный SQL-интерфейс.
- Предусловие: DBeaver установлен (ссылка на [dbeaver.io/download](https://dbeaver.io/download/)). Необязателен — ноутбук работает без DBeaver. - Предусловие: DBeaver установлен (ссылка на [dbeaver.io/download](https://dbeaver.io/download/)). Необязателен — ноутбук работает без DBeaver.
- Шаги: New Database Connection -> Trino. Host: `localhost`. Port: `8090`. Database/Catalog: `lakehouse`. Username: любая строка (напр. `student`). Password: пусто. Test Connection -> Finish. - Шаги: New Database Connection -> Trino. Host: `localhost`. Port: `8090`. Database/Catalog: `lakehouse`. Username: любая строка (напр. `student`). Password: пусто. Test Connection -> Finish.
- Проверка: `SHOW SCHEMAS FROM lakehouse;`. Ожидаем: `bronze`, `default`, `information_schema`, `silver`. - Проверка: `SHOW SCHEMAS FROM lakehouse;`. После модулей 4–5 должны быть видны `bronze` и `silver`; до их прохождения этих схем может еще не быть.
- Troubleshooting: стенд поднят? контейнер `trino` Up? порт 8090 свободен? - Troubleshooting: стенд поднят? контейнер `trino` Up? порт 8090 свободен?
## Что делать дальше ## Что делать дальше
- пройти `notebooks/01_environment_and_smoke_test.ipynb`; После первого ноутбука открой [маршрут по модулям](./docs/course_program.md#маршрут-по-модулям).
- пройти `notebooks/02_lakehouse_mental_model.ipynb`; В нем есть ссылки на все восемь уроков и результат, который нужно получить
- подготовить data bundle по инструкции выше и пройти `notebooks/03_raw_ingest_and_first_read.ipynb`; перед переходом дальше. Для следующего занятия сохрани ссылку на
- свериться с программой курса в `docs/course_program.md`; [памятку учащегося](./docs/study_workflow.md).
- после прохождения первых модулей переходить к следующим учебным материалам.
+43 -4
View File
@@ -121,7 +121,8 @@ services:
- ./spark/spark-defaults.conf:/opt/spark/conf/spark-defaults.conf:ro - ./spark/spark-defaults.conf:/opt/spark/conf/spark-defaults.conf:ro
minio: minio:
image: quay.io/minio/minio:RELEASE.2024-10-02T17-50-41Z # Один и тот же образ опубликован автором в GHCR и Docker Hub.
image: &minio-image ${MINIO_REGISTRY:-ghcr.io}/coollabsio/minio:RELEASE.2025-04-22T22-12-26Z@sha256:a4938f37f1be1841b8e7b627ad0207b265345fd0d063e42d7410c78af0e63e68
container_name: minio container_name: minio
hostname: minio hostname: minio
networks: networks:
@@ -137,7 +138,7 @@ services:
- minio-data:/data - minio-data:/data
minio-init: minio-init:
image: quay.io/minio/mc:RELEASE.2024-06-24T19-40-33Z-cpuv1 image: *minio-image
container_name: minio-init container_name: minio-init
hostname: minio-init hostname: minio-init
networks: networks:
@@ -145,13 +146,13 @@ services:
depends_on: depends_on:
- minio - minio
entrypoint: > entrypoint: >
/bin/sh -c " /bin/sh -ec "
echo 'Waiting for MinIO...'; echo 'Waiting for MinIO...';
until mc alias set minio http://minio:9000 minioadmin minioadmin 2>/dev/null; do until mc alias set minio http://minio:9000 minioadmin minioadmin 2>/dev/null; do
sleep 1; sleep 1;
done; done;
echo 'MinIO is up, creating bucket...'; echo 'MinIO is up, creating bucket...';
mc mb -p minio/lakehouse || true; mc mb --ignore-existing minio/lakehouse;
echo 'Bucket lakehouse is ready'; echo 'Bucket lakehouse is ready';
exit 0; exit 0;
" "
@@ -160,6 +161,8 @@ services:
jupyter: jupyter:
build: build:
context: ./jupyter context: ./jupyter
additional_contexts:
spark-runtime: service:spark-master
args: args:
NB_UID: ${NB_UID:-1000} NB_UID: ${NB_UID:-1000}
NB_GID: ${NB_GID:-100} NB_GID: ${NB_GID:-100}
@@ -192,10 +195,45 @@ services:
- ./src:/opt/src:ro - ./src:/opt/src:ro
- ./data:/opt/data:ro - ./data:/opt/data:ro
ide:
build:
context: ./ide
additional_contexts:
jupyter-runtime: service:jupyter
container_name: ide
hostname: ide
depends_on:
- spark-master
- minio
- iceberg-catalog-init
networks:
- spark-net
environment:
- AWS_ACCESS_KEY_ID=minioadmin
- AWS_SECRET_ACCESS_KEY=minioadmin
- PYTHONPATH=/opt/src
ports:
- "127.0.0.1:8889:8889"
volumes:
- ./spark/spark-defaults.conf:/opt/spark/conf/spark-defaults.conf:ro
- ./notebooks:/opt/work
- ./src:/opt/src:ro
- ./data:/opt/data:ro
- .:/opt/course:ro
- ./notebooks:/opt/course/notebooks
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8889/healthz"]
interval: 10s
timeout: 5s
retries: 6
trino: trino:
image: trinodb/trino:478 image: trinodb/trino:478
container_name: trino container_name: trino
hostname: trino hostname: trino
# Heap JVM ограничен отдельно в trino/jvm.config; остальное — запас вне heap.
mem_limit: 2g
memswap_limit: 2g
networks: networks:
- spark-net - spark-net
depends_on: depends_on:
@@ -210,6 +248,7 @@ services:
- "8090:8080" # Web UI Trino - "8090:8080" # Web UI Trino
volumes: volumes:
- ./trino/catalog:/etc/trino/catalog:ro - ./trino/catalog:/etc/trino/catalog:ro
- ./trino/jvm.config:/etc/trino/jvm.config:ro
networks: networks:
spark-net: spark-net:
+7 -6
View File
@@ -50,7 +50,7 @@
1. Понимает различие между `storage`, `catalog` и `compute`, и может объяснить роль `MinIO`, `PostgreSQL`, `Spark` и `Trino` в этом стенде. 1. Понимает различие между `storage`, `catalog` и `compute`, и может объяснить роль `MinIO`, `PostgreSQL`, `Spark` и `Trino` в этом стенде.
2. Умеет поднять локальный стенд, проверить его базовую работоспособность и использовать логи/веб-интерфейсы для первичной диагностики проблем. 2. Умеет поднять локальный стенд, проверить его базовую работоспособность и использовать логи/веб-интерфейсы для первичной диагностики проблем.
3. Умеет загрузить raw-данные, прочитать их в `Spark`, проверить схему и подготовить данные к дальнейшей обработке. 3. Умеет настроить Spark-сессию для кластера, объяснить происхождение настроек и повторить подключение в новом ядре; загрузить raw-данные, прочитать их и проверить схему.
4. Умеет создать и заполнить `Iceberg`-таблицу как основной табличный формат курса. 4. Умеет создать и заполнить `Iceberg`-таблицу как основной табличный формат курса.
5. Умеет реализовать простой поток `raw -> bronze -> silver` с базовыми трансформациями и сохранением воспроизводимости шагов. 5. Умеет реализовать простой поток `raw -> bronze -> silver` с базовыми трансформациями и сохранением воспроизводимости шагов.
6. Умеет читать одну и ту же таблицу из `Spark` и `Trino` и понимает, почему это возможно без копирования данных. 6. Умеет читать одну и ту же таблицу из `Spark` и `Trino` и понимает, почему это возможно без копирования данных.
@@ -90,13 +90,13 @@
* один основной табличный формат: `Iceberg`; * один основной табличный формат: `Iceberg`;
* работа с raw-данными; * работа с raw-данными;
* построение слоев `bronze` и `silver`; * построение слоев `bronze` и `silver`;
* чтение и запись через `Spark`; * базовая настройка сессии, чтение и запись через `Spark`;
* чтение через `Trino`; * чтение через `Trino`;
* базовое schema evolution; * базовое schema evolution;
* snapshot/time travel на прикладном уровне; * snapshot/time travel на прикладном уровне;
* базовый `compaction/vacuum` с параллелями к `Greenplum AppendOnly`; * базовый `compaction/vacuum` с параллелями к `Greenplum AppendOnly`;
* набор практик в Jupyter Notebooks с обязательными заданиями и checkpoints; * набор практик в Jupyter Notebooks с обязательными заданиями и checkpoints;
* стартовая документация до первого ноутбука: prerequisites, запуск стенда, вход в Jupyter и базовый troubleshooting. * стартовая документация до первого ноутбука: prerequisites, запуск стенда, вход в JupyterLab и базовый troubleshooting.
Ориентировочная программа курса вынесена в отдельный документ: **[course_program.md](./course_program.md)**. Ориентировочная программа курса вынесена в отдельный документ: **[course_program.md](./course_program.md)**.
@@ -152,10 +152,10 @@
**Acceptance Criteria для учебных материалов и результата:** **Acceptance Criteria для учебных материалов и результата:**
1. В составе курса есть стартовая документация до первого ноутбука, включая prerequisites, запуск стенда, вход в `Jupyter` и получение учебного data bundle. 1. В составе курса есть стартовая документация до первого ноутбука, включая prerequisites, запуск стенда, вход в JupyterLab и получение учебного data bundle.
2. Студент проходит обязательные `checkpoints` по урокам. 2. Студент проходит обязательные `checkpoints` по урокам.
3. Студент самостоятельно выполняет набор практик на одном датасете, включая: 3. Студент самостоятельно выполняет набор практик на одном датасете, включая:
* загрузку raw-данных; * подключение к Spark и загрузку raw-данных;
* построение `bronze`; * построение `bronze`;
* построение `silver`; * построение `silver`;
* создание/чтение `Iceberg`-таблицы; * создание/чтение `Iceberg`-таблицы;
@@ -174,7 +174,8 @@
**В скоупе:** **В скоупе:**
* использование существующего `docker-compose.yml` (`Spark`, `Trino`, `MinIO`, `PostgreSQL`, `Jupyter`); * использование существующего `docker-compose.yml` (`Spark`, `Trino`, `MinIO`, `PostgreSQL`, `JupyterLab`, `code-server`);
* JupyterLab как основной интерфейс курса; браузерный VS Code доступен в том же стенде по желанию, с учётом совместимости браузера и средств защиты на компьютере;
* подготовка учебных датасетов и/или инструкций по их загрузке в репозиторий; * подготовка учебных датасетов и/или инструкций по их загрузке в репозиторий;
* наличие воспроизводимого способа получить учебный data bundle без ручного поиска по внешним сайтам; * наличие воспроизводимого способа получить учебный data bundle без ручного поиска по внешним сайтам;
* поддержка выполнения запросов к `Trino` из ноутбуков; * поддержка выполнения запросов к `Trino` из ноутбуков;
+53 -21
View File
@@ -1,6 +1,34 @@
# Программа учебного курса: Lakehouse без магии # Программа учебного курса: Lakehouse без магии
Этот документ заменяет предыдущий черновой план и выравнивает программу с PRD из [course_prd.md](./course_prd.md). Здесь собраны порядок модулей, ссылки на ноутбуки и результаты, по которым можно
проверить свою работу. Рамки курса и learning outcomes описаны в [course_prd.md](./course_prd.md).
## Маршрут по модулям
Перед первым уроком подготовь стенд по [START_HERE](../START_HERE.md).
Дальше проходи ноутбуки по порядку: файлы и таблицы из ранних модулей нужны в
следующих. Пауза между занятиями описана в [памятке учащегося](./study_workflow.md).
| Модуль и ноутбук | Что понадобится | Результат перед переходом дальше |
| --- | --- | --- |
| [1. Вход в стенд](../notebooks/01_environment_and_smoke_test.ipynb) | Работающий стенд | Smoke test вернул `499999500000`; ты можешь показать два worker-а и объяснить роли сервисов |
| [2. Модель Lakehouse](../notebooks/02_lakehouse_mental_model.ipynb) | Модуль 1 | Ты нашел файлы демо-таблицы в MinIO и запись каталога в PostgreSQL; выполнил очистку демо по ноутбуку |
| [3. Исходные данные](../notebooks/03_raw_ingest_and_first_read.ipynb) | [Четыре файла Taxi](../START_HERE.md#подготовка-учебного-датасета-перед-модулем-3) на хосте | Файлы загружены в raw-зону MinIO, Spark читает их; проверены схема и основные метрики |
| [4. Bronze и Iceberg](../notebooks/04_bronze_with_iceberg.ipynb) | Raw-файлы из Модуля 3 | Созданы bronze-таблица поездок и таблица зон из самостоятельного задания; видны их данные и metadata-файлы |
| [5. Silver](../notebooks/05_silver_layer.ipynb) | Обе bronze-таблицы | Создана silver-таблица, проверены правила очистки и качество результата |
| [6. Spark и Trino](../notebooks/06_spark_and_trino_on_same_table.ipynb) | Silver из Модуля 5; DBeaver по желанию | Одна таблица читается обоими движками; ты можешь объяснить, какие компоненты у них общие |
| [7. Изменения таблиц](../notebooks/07_safe_table_changes.ipynb) | Результаты предыдущих модулей | На демо-таблице проверены изменение схемы, чтение прошлого состояния и rollback; silver сохранена |
| [8. Обслуживание и финальная практика](../notebooks/08_maintenance_and_final_lab.ipynb) | Raw, bronze и silver | Проверено обслуживание таблиц; самостоятельно пройден поток от исходных файлов до чтения и проверок результата |
В каждом ноутбуке сначала прочитай объяснение, затем выполни демонстрацию и
самостоятельное задание. Checkpoint в конце урока — вопросы и проверки для
самопроверки. Сохраненный вывод помогает вернуться к результату, но не заменяет
собственный запуск и объяснение.
Основной интерфейс курса — JupyterLab. В том же стенде доступен
[VS Code в браузере](./stack_reference.md#vs-code-в-браузере), если тебе удобнее
работать в нём. Совместимость зависит от браузера, расширений и средств защиты
на компьютере; оба интерфейса открывают одни и те же ноутбуки.
## 1. Рамка курса ## 1. Рамка курса
@@ -27,7 +55,7 @@
## 3. Состав учебных материалов ## 3. Состав учебных материалов
Курс не должен состоять только из ноутбуков. Ноутбуки являются ядром практики, но вход в курс, запуск стенда и правила работы должны быть вынесены в обычную документацию, доступную до старта `Jupyter`. Курс не должен состоять только из ноутбуков. Ноутбуки являются ядром практики, но вход в курс, запуск стенда и правила работы должны быть вынесены в обычную документацию, доступную до открытия IDE.
### 3.1. Стартовая документация до первого ноутбука ### 3.1. Стартовая документация до первого ноутбука
@@ -38,15 +66,17 @@
* `START_HERE.md` или аналогичный стартовый документ с маршрутом прохождения курса; * `START_HERE.md` или аналогичный стартовый документ с маршрутом прохождения курса;
* инструкция по prerequisites: `Docker`, `Docker Compose`, свободные порты, базовые команды; * инструкция по prerequisites: `Docker`, `Docker Compose`, свободные порты, базовые команды;
* пошаговый гайд по запуску стенда: `build`, `up`, проверка сервисов, открытие UI; * пошаговый гайд по запуску стенда: `build`, `up`, проверка сервисов, открытие UI;
* краткое объяснение, как пользоваться `Jupyter`, где лежат ноутбуки и как читать структуру урока; * краткое объяснение, как запускать ноутбуки в JupyterLab, где лежат ноутбуки и как читать структуру урока;
* troubleshooting по типовым проблемам старта; * troubleshooting по типовым проблемам старта;
* инструкция по reset/restart стенда для повторного прохождения практик. * [памятка учащегося](./study_workflow.md): продолжение занятия, сохранение данных,
перезапуск ядра и диагностика;
* отдельная инструкция по сбросу учебных данных для повторного прохождения практик.
**Что должен закрывать этот слой:** **Что должен закрывать этот слой:**
* студент понимает, что нужно установить и проверить до начала курса; * студент понимает, что нужно установить и проверить до начала курса;
* студент может поднять стенд без ментора; * студент может поднять стенд без ментора;
* студент знает, как попасть в `Jupyter`, `MinIO`, `Trino UI`; * студент знает, как попасть в JupyterLab, `MinIO`, `Trino UI`;
* студент понимает, как устроен формат уроков и что от него ожидается. * студент понимает, как устроен формат уроков и что от него ожидается.
### 3.2. Практическое ядро курса ### 3.2. Практическое ядро курса
@@ -69,7 +99,7 @@
| --- | --- | --- | | --- | --- | --- |
| `LO1`. Понимание различий между `storage`, `catalog`, `compute` | 1, 2, 6 | checkpoints модулей 2 и 6 | | `LO1`. Понимание различий между `storage`, `catalog`, `compute` | 1, 2, 6 | checkpoints модулей 2 и 6 |
| `LO2`. Умение поднять и диагностировать стенд | 1 | checkpoint модуля 1 | | `LO2`. Умение поднять и диагностировать стенд | 1 | checkpoint модуля 1 |
| `LO3`. Умение загрузить raw-данные и проверить схему | 3 | checkpoint модуля 3 | | `LO3`. Умение подключиться к Spark, загрузить raw-данные и проверить схему | 3 | checkpoint модуля 3 |
| `LO4`. Умение создать и заполнить `Iceberg`-таблицу | 4 | checkpoint модуля 4 | | `LO4`. Умение создать и заполнить `Iceberg`-таблицу | 4 | checkpoint модуля 4 |
| `LO5`. Умение собрать поток `raw -> bronze -> silver` | 4, 5, 8 | checkpoints модулей 5 и 8 | | `LO5`. Умение собрать поток `raw -> bronze -> silver` | 4, 5, 8 | checkpoints модулей 5 и 8 |
| `LO6`. Умение читать одну таблицу из `Spark` и `Trino` | 6, 8 | checkpoint модуля 6 и финальная практика | | `LO6`. Умение читать одну таблицу из `Spark` и `Trino` | 6, 8 | checkpoint модуля 6 и финальная практика |
@@ -85,10 +115,10 @@
**Содержание:** **Содержание:**
* запуск `docker compose build` и `docker compose up -d`; * скачивание готовых образов, сборка окружения и запуск по `START_HERE.md`;
* проверка сервисов и веб-интерфейсов; * проверка сервисов и веб-интерфейсов;
* базовый smoke test для Spark; * базовый smoke test для Spark;
* первый обзор того, где в стенде `MinIO`, `PostgreSQL`, `Spark`, `Trino`, `Jupyter`. * первый обзор того, где в стенде `MinIO`, `PostgreSQL`, `Spark`, `Trino` и IDE.
**Практика студента:** **Практика студента:**
@@ -124,25 +154,27 @@
**Рабочее название ноутбука:** `03_raw_ingest_and_first_read.ipynb` **Рабочее название ноутбука:** `03_raw_ingest_and_first_read.ipynb`
**Зачем нужен:** показать, как raw-данные попадают в стенд и как с ними безопасно начать работать. **Зачем нужен:** пройти путь от подключения к Spark до первого чтения и проверки исходных файлов Taxi.
**Ориентир по времени:** около 2 часов, из них 20–30 минут на подготовку Spark-сессии.
**Содержание:** **Содержание:**
* работа с заранее подготовленным локальным data bundle из onboarding-документации; * подключение к Spark-кластеру: сессия, контекст, driver и executors; настройки из файла, параметров запуска и кода;
* явная загрузка исходных `PARQUET`-файлов в raw-зону; * повторное использование сессии, остановка и перезапуск ядра; изменение настроек во время работы;
* чтение raw-данных через Spark; * подготовленный data bundle и загрузка исходных файлов в raw-зону MinIO;
* проверка схемы, типов и базового качества данных; * чтение через Spark, проверка схемы и типов;
* обсуждение того, почему raw лучше не "чинить на месте" и почему датасет не должен скачиваться "из интернета из ноутбука". * базовые метрики, аномалии и причины сохранять raw без изменений.
**Практика студента:** **Практика студента:**
* проверить состав локально полученного набора `NYC Taxi`; * заполнить шаблон сессии с подготовленным каталогом Iceberg; проверить подключение на собственной временной таблице и удалить ее;
* загрузить его в raw-зону `MinIO`; * изменить и восстановить часовой пояс, сравнить повторный вызов builder-а с новым запуском приложения;
* прочитать несколько месяцев `NYC Taxi`; * проверить состав data bundle и загрузить несколько месяцев Taxi в MinIO;
* проверить схему и посчитать базовые метрики; * прочитать поездки и справочник зон, проверить схему и посчитать метрики;
* найти простые аномалии или проблемные значения. * найти проблемные значения и объяснить, почему их сохраняют в raw.
**Checkpoint:** студент умеет загрузить учебный набор в raw-зону, прочитать raw-данные, проверить схему и объяснить, почему raw-слой нужен как воспроизводимая точка входа. **Checkpoint:** студент создает сессию для кластера, объясняет источники настроек и место выполнения заданий, умеет повторить подключение в новом ядре. Загружает учебный набор в raw-зону, читает его через Spark, проверяет схему и объясняет, зачем сохранять исходные данные.
### Модуль 4. Первая рабочая Iceberg-таблица и слой bronze ### Модуль 4. Первая рабочая Iceberg-таблица и слой bronze
@@ -256,7 +288,7 @@
1. Поднимает локальный стенд и диагностирует типовые проблемы на старте. 1. Поднимает локальный стенд и диагностирует типовые проблемы на старте.
2. Понимает различие между `storage`, `catalog` и `compute`. 2. Понимает различие между `storage`, `catalog` и `compute`.
3. Умеет загрузить raw-данные и прочитать их в Spark. 3. Подключается к Spark-кластеру, загружает raw-данные и проверяет их после чтения.
4. Умеет создать и заполнить Iceberg-таблицу. 4. Умеет создать и заполнить Iceberg-таблицу.
5. Умеет построить простой поток `raw -> bronze -> silver`. 5. Умеет построить простой поток `raw -> bronze -> silver`.
6. Умеет читать одну и ту же таблицу из `Spark` и `Trino`. 6. Умеет читать одну и ту же таблицу из `Spark` и `Trino`.
+18 -2
View File
@@ -44,6 +44,22 @@
**Параллель с DWH:** в классическом DWH вычислитель один, поэтому проблема не возникает. **Параллель с DWH:** в классическом DWH вычислитель один, поэтому проблема не возникает.
**Где в курсе:** Модуль 6. **Где в курсе:** Модуль 6.
## Spark-сессия и выполнение
### SparkSession
Точка входа в DataFrame и Spark SQL: объект `spark` в ноутбуке. Builder задаёт параметры, а `getOrCreate()` создаёт сессию или возвращает уже работающую.
### SparkContext
Связывает Spark-приложение с вычислительными ресурсами. Через `spark.sparkContext` можно проверить фактические `master`, `appName` и `applicationId`. Повторный builder не заменяет работающий контекст.
### Driver и executors
Driver управляет приложением: выполняет код ноутбука, строит план и отправляет задания. Executors выполняют задания и читают данные. В нашем стенде driver работает в `jupyter`, executors запускаются на Spark worker-ах. Master выделяет приложению ресурсы worker-ов.
**Где в курсе:** Модуль 3.
## Структура Iceberg-таблицы ## Структура Iceberg-таблицы
### Namespace ### Namespace
@@ -112,9 +128,9 @@ Avro-файл со списком manifest-ов, составляющих оди
### Vacuum (expire_snapshots) ### Vacuum (expire_snapshots)
Удаление старых snapshot-ов и связанных с ними data files из хранилища. Освобождает место, но делает невозможным time travel к удалённым snapshot-ам. Необратимая операция. Удаление старых snapshot-ов и файлов, которые больше не нужны ни одному сохранённому snapshot-у. Может освободить место, но делает невозможным time travel к удалённым snapshot-ам. Необратимая операция.
**В этом курсе:** `CALL lakehouse.system.expire_snapshots(table => '...', retain_last => N)`. **В этом курсе:** `CALL lakehouse.system.expire_snapshots(table => '...', older_than => TIMESTAMP '<cutoff>', retain_last => N)`. `older_than` задаёт границу времени, а `retain_last` защищает последние N snapshot-ов в цепочке предков текущего состояния независимо от возраста. Без явной границы при стандартных настройках удаляются только snapshot-ы старше 5 дней; один `retain_last` не сокращает историю до N записей.
**Параллель с DWH:** `VACUUM` в PostgreSQL/Greenplum (удаление мёртвых строк). **Параллель с DWH:** `VACUUM` в PostgreSQL/Greenplum (удаление мёртвых строк).
**Где в курсе:** Модуль 8. **Где в курсе:** Модуль 8.
+4 -2
View File
@@ -17,7 +17,7 @@
| Зона | Где лежит | Назначение | | Зона | Где лежит | Назначение |
| --- | --- | --- | | --- | --- | --- |
| Runtime / stand | `docker-compose.yml`, `spark/`, `trino/`, `jupyter/` | Топология сервисов, образы, конфиги, порты | | Runtime / stand | `docker-compose.yml`, `spark/`, `trino/`, `jupyter/`, `ide/` | Топология сервисов, образы, конфиги, порты |
| Onboarding | `README.md`, `START_HERE.md` | Вход в репозиторий и первый пользовательский маршрут | | Onboarding | `README.md`, `START_HERE.md` | Вход в репозиторий и первый пользовательский маршрут |
| Runtime reference | `docs/stack_reference.md` | Технические детали стенда: порты, доступ, команды, smoke-тесты | | Runtime reference | `docs/stack_reference.md` | Технические детали стенда: порты, доступ, команды, smoke-тесты |
| Course definition | `docs/course_prd.md`, `docs/course_program.md` | Границы курса, learning outcomes, структура модулей | | Course definition | `docs/course_prd.md`, `docs/course_program.md` | Границы курса, learning outcomes, структура модулей |
@@ -31,9 +31,10 @@
| Файл или каталог | За что отвечает | Что не стоит туда складывать | | Файл или каталог | За что отвечает | Что не стоит туда складывать |
| --- | --- | --- | | --- | --- | --- |
| `docker-compose.yml` | Реальный состав сервисов, контейнеров, сетей, портов и зависимостей | Учебные пояснения, которые не нужны для запуска | | `docker-compose.yml` | Реальный состав сервисов, контейнеров, сетей, портов и зависимостей | Учебные пояснения, которые не нужны для запуска |
| `spark/`, `trino/`, `jupyter/` | Конкретные runtime-конфиги и образы | Описание программы курса | | `spark/`, `trino/`, `jupyter/`, `ide/` | Конкретные runtime-конфиги и образы | Описание программы курса |
| `README.md` | Верхнеуровневое объяснение репозитория, архитектуры и точек входа | Пошаговый student onboarding во всех деталях и low-level runtime reference | | `README.md` | Верхнеуровневое объяснение репозитория, архитектуры и точек входа | Пошаговый student onboarding во всех деталях и low-level runtime reference |
| `START_HERE.md` | Первый маршрут студента: prerequisites, запуск, первые UI, первый ноутбук, базовая диагностика | Полный PRD курса или подробный бэклог модулей | | `START_HERE.md` | Первый маршрут студента: prerequisites, запуск, первые UI, первый ноутбук, базовая диагностика | Полный PRD курса или подробный бэклог модулей |
| `docs/study_workflow.md` | Повседневный маршрут: продолжить и закончить занятие, сохранить результат, восстановиться после ошибки | Состав модулей, установка окружения с нуля |
| `docs/stack_reference.md` | Технический reference стенда: сервисы, порты, доступ, reset, smoke-тесты | Роль основного onboarding-документа или описание всей программы курса | | `docs/stack_reference.md` | Технический reference стенда: сервисы, порты, доступ, reset, smoke-тесты | Роль основного onboarding-документа или описание всей программы курса |
| `docs/course_prd.md` | Product scope, аудитория, learning outcomes, dataset strategy, out of scope | Технические мелочи запуска контейнеров | | `docs/course_prd.md` | Product scope, аудитория, learning outcomes, dataset strategy, out of scope | Технические мелочи запуска контейнеров |
| `docs/course_program.md` | Модульная структура курса, состав материалов, checkpoints | Подробные docker-команды и legacy-лабы | | `docs/course_program.md` | Модульная структура курса, состав материалов, checkpoints | Подробные docker-команды и legacy-лабы |
@@ -75,6 +76,7 @@
- `START_HERE.md`; - `START_HERE.md`;
- `docs/course_program.md`; - `docs/course_program.md`;
- `docs/study_workflow.md`;
- соответствующие `notebooks/`; - соответствующие `notebooks/`;
- `README.md`, если меняется верхнеуровневый вход в репозиторий. - `README.md`, если меняется верхнеуровневый вход в репозиторий.
+5 -4
View File
@@ -17,9 +17,9 @@
| Модуль | Тема | Самостоятельная работа | Обсуждение с ментором | Комментарий | | Модуль | Тема | Самостоятельная работа | Обсуждение с ментором | Комментарий |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| 1 | Вход в стенд | 30-40 мин | 10-15 мин | Если Docker знаком, идёт быстро | | 1 | Вход в стенд | 30-40 мин | 10-15 мин | Без учета первой сборки: ее время зависит от сети и компьютера |
| 2 | Ментальная модель | 40-60 мин | 15-20 мин | Ключевой модуль; убедись, что студент заходил в MinIO | | 2 | Ментальная модель | 40-60 мин | 15-20 мин | Ключевой модуль; убедись, что студент заходил в MinIO |
| 3 | Raw-данные | 40-50 мин | 10 мин | Зависит от скорости загрузки данных | | 3 | Raw-данные | Около 2 часов | 10 мин | Включая 20–30 минут на практику со Spark-сессией; датасет скачивается заранее |
| 4 | Bronze + Iceberg | 60-80 мин | 15-20 мин | Первая таблица — много новых концепций | | 4 | Bronze + Iceberg | 60-80 мин | 15-20 мин | Первая таблица — много новых концепций |
| 5 | Silver | 50-70 мин | 15 мин | Трансформации + проверки качества | | 5 | Silver | 50-70 мин | 15 мин | Трансформации + проверки качества |
| 6 | Spark + Trino | 40-60 мин | 15 мин | Если DBeaver настроен заранее — быстрее | | 6 | Spark + Trino | 40-60 мин | 15 мин | Если DBeaver настроен заранее — быстрее |
@@ -34,8 +34,9 @@
### Модуль 1. Вход в стенд ### Модуль 1. Вход в стенд
- **Docker не хватает ресурсов.** Стенд требует ~4-6 ГБ RAM. На машинах с 8 ГБ бывают проблемы. Решение: увеличить лимиты Docker Desktop или закрыть лишние приложения. - **Docker не хватает ресурсов.** Сверься с [системными требованиями](../START_HERE.md#системные-требования). Проверь отдельно память компьютера и лимит памяти Docker Desktop/WSL2: контейнерам может быть доступна лишь часть RAM хоста.
- **Порты заняты.** 8080, 8888, 9000 — популярные порты. `docker compose ps` покажет, какой сервис не стартовал. Решение: остановить конфликтующий процесс или (крайний вариант) поменять порт в `docker-compose.yml`. - **После паузы виден старый вывод, но переменные пропали.** Разбери разницу между файлом ноутбука и ядром Python по [памятке учащегося](./study_workflow.md). Передай студенту ее до второго занятия; для обращения за помощью там есть список нужных сведений.
- **Порты заняты.** Проверь 8080, 8888, 9000 и остальные [порты стенда](../START_HERE.md#порты-стенда). `docker compose ps -a` покажет, какой сервис не стартовал. Решение: остановить конфликтующий процесс или (крайний вариант) поменять порт в `docker-compose.yml`.
- **Студент не читает `START_HERE.md`.** Начинает с ноутбуков до поднятия стенда. Направь обратно к стартовому документу. - **Студент не читает `START_HERE.md`.** Начинает с ноутбуков до поднятия стенда. Направь обратно к стартовому документу.
### Модуль 2. Ментальная модель ### Модуль 2. Ментальная модель
+74
View File
@@ -0,0 +1,74 @@
# Если образы или зависимости не скачиваются
Доступность репозиториев зависит от сети и провайдера, в том числе в РФ.
GHCR выбран основным источником MinIO, чтобы его загрузка не зависела от Docker
Hub. Это не гарантия доступности без VPN у каждого провайдера: у стенда остаются
другие внешние зависимости. Для первой сборки и скачивания датасета нужен интернет.
## Сначала найди источник ошибки
Запускай команды в терминале хоста, из корня репозитория:
```bash
docker compose pull minio minio-init postgres trino
docker compose --progress plain build
```
Если первая команда завершилась с ошибкой, сначала реши проблему загрузки образа.
Если ошибка возникла во второй, найди имя сервиса и адрес, с которого скачивался
файл. Открывающийся в браузере сайт ещё не подтверждает доступ из Docker.
| Где возникла ошибка | Что скачивается | Что проверить |
| --- | --- | --- |
| `ghcr.io` при загрузке `minio` | Готовые MinIO и `mc` | Резервный источник ниже |
| `docker.io`, `registry-1.docker.io`, `auth.docker.io` | PostgreSQL, Trino, базовый Spark; резерв MinIO | Доступ Docker к Docker Hub и настройки прокси |
| Ubuntu archive, PyPI, Maven Central при сборке `spark` или `jupyter` | Системные пакеты, Python-пакеты и JAR | Доступ к зависимостям из сборки |
| GitHub Releases, Open VSX при сборке `ide` | code-server и расширения | Доступ к зависимостям из сборки |
## Резервный источник MinIO
Автор образа публикует его в двух местах: GHCR и Docker Hub. В обоих закреплён
один digest, поэтому смена registry не меняет версию или содержимое образа.
Ссылка на сборку и версия приведены в [техническом reference](./stack_reference.md#minio).
Если GHCR недоступен, создай файл `.env` в корне репозитория и добавь строку:
```dotenv
MINIO_REGISTRY=docker.io
```
Если файл уже существует, измени только эту настройку. Другие значения, например
`NB_UID` и `NB_GID`, сохрани. Затем повтори:
```bash
docker compose pull minio minio-init
```
Дальше используй обычные команды из [START_HERE](../START_HERE.md#быстрый-запуск).
Для возврата к основному источнику установи `MINIO_REGISTRY=ghcr.io`.
Оба источника публичные; регистрация для скачивания MinIO не требуется.
`.env.example` содержит пример настроек, а `.env` исключён из Git.
## Если недоступны другие источники
Переключение MinIO не меняет источник PostgreSQL, Trino, Spark или зависимостей
сборки. Если в твоей сети нужен VPN или HTTP-прокси, важно, чтобы им мог
пользоваться Docker, а не только браузер.
- **Docker Desktop:** настрой прокси в разделе **Settings → Resources → Proxies**.
См. [официальную инструкцию](https://docs.docker.com/desktop/settings-and-maintenance/settings/#proxies).
- **Docker Engine на Linux:** скачиванием образов занимается демон Docker.
Для него есть [отдельная настройка прокси](https://docs.docker.com/engine/daemon/proxy/).
- **Команды внутри сборки:** `apt`, `pip`, `curl` и установка расширений требуют
доступа из build-контейнера. Docker описывает
[настройки прокси для сборок](https://docs.docker.com/engine/cli/proxy/#build-with-a-proxy-configuration)
отдельно от настроек демона. Прокси на `localhost` хоста не обязательно
доступен как `localhost` внутри контейнера.
Ошибка `429` обычно означает лимит запросов, а `401`/`403` может быть связана
с доступом к registry. Сам по себе этот код не доказывает блокировку по стране.
Не меняй версии образов и не удаляй данные стенда для исправления сетевой ошибки.
Если нужна помощь, передай ментору ОС, вывод `docker compose version`, команду,
имя сервиса и фрагмент ошибки с адресом источника. Пароли и адреса прокси
с учётными данными в сообщение не добавляй.
+206 -24
View File
@@ -15,20 +15,27 @@
| MinIO API | `minio` | `9000` | S3-compatible endpoint | | MinIO API | `minio` | `9000` | S3-compatible endpoint |
| MinIO Console | `minio` | `9001` | веб-консоль бакетов и объектов | | MinIO Console | `minio` | `9001` | веб-консоль бакетов и объектов |
| PostgreSQL | `postgres-iceberg` | `5432` | JDBC-каталог Iceberg | | PostgreSQL | `postgres-iceberg` | `5432` | JDBC-каталог Iceberg |
| JupyterLab | `jupyter` | `8888` | практические ноутбуки | | JupyterLab | `jupyter` | `8888` | основной интерфейс для ноутбуков и терминала |
| VS Code в браузере (code-server) | `ide` | `8889` | альтернативный интерфейс для тех же ноутбуков |
## Основные команды ## Основные команды
Все команды выполняются из корня репозитория. Все команды выполняются в терминале хоста из корня репозитория.
Для первого запуска и настройки прав используй [START_HERE](../START_HERE.md),
для продолжения работы — [памятку учащегося](./study_workflow.md).
### Сборка и запуск ### Сборка и запуск
```bash ```bash
docker compose pull minio minio-init postgres trino
docker compose build docker compose build
docker compose up -d docker compose up -d
docker compose ps docker compose ps -a
``` ```
JupyterLab и VS Code собираются и запускаются вместе; выбирать интерфейс
можно после старта стенда.
### Логи ### Логи
```bash ```bash
@@ -36,16 +43,50 @@ docker compose logs -f spark-master
docker compose logs -f trino docker compose logs -f trino
docker compose logs -f minio docker compose logs -f minio
docker compose logs -f jupyter docker compose logs -f jupyter
docker compose logs -f ide
``` ```
### Остановка и reset ### Остановка и reset
```bash | Действие | Команда | Что происходит с данными |
docker compose down | --- | --- | --- |
docker compose down -v | Пауза до следующего занятия | `docker compose stop` | Контейнеры остановлены, файлы и volumes сохранены |
``` | Продолжение | `docker compose up -d` | Сервисы запускаются с прежними данными |
| Удаление контейнеров и сети | `docker compose down` | Volumes и файлы на хосте сохраняются, состояние внутри удаленных контейнеров теряется |
| Полный сброс учебных данных | См. [отдельную инструкцию](./study_workflow.md#начать-с-чистых-данных) | Удаляются MinIO volume и каталог PostgreSQL |
`down -v` удаляет volumes и возвращает стенд в чистое состояние. Переменные Python и живые Spark-сессии после остановки нужно создать заново.
## Ресурсы и проверенная среда
Актуальный бюджет для учащегося приведен в
[системных требованиях](../START_HERE.md#системные-требования).
Он учитывает одновременную работу двух Spark worker-ов, драйвера в Jupyter, Trino,
MinIO и PostgreSQL. У каждого worker-а в Compose указаны 2 CPU и 2 ГБ для
приложений Spark; это не предел потребления всей JVM или контейнера.
Потребление при простое не показывает пик во время урока.
Проверка 19 октября 2026 года: Linux x86_64, Docker Engine, 6 CPU и около
11,1 ГиБ памяти, видимых Docker. Выполнены первый ноутбук в Chromium и проверка
Spark → Iceberg → Trino. Затем Trino проверен с лимитом контейнера 2 ГиБ и heap
1 ГиБ на трех месяцах Taxi: 9 554 778 строк bronze, 9 418 184 строк silver
и 265 зон. Запросы Модуля 6 и дополнительный JOIN со справочником прошли;
агрегаты и JOIN сверены со Spark. Пик памяти контейнера Trino по cgroup составил
около 1,25 ГиБ, событий OOM и перезапусков не было.
Наблюдавшийся суммарный пик памяти контейнеров без неактивного файлового кэша
составил около 5,44 ГиБ. Это измерение выбранного сценария, а не проверка всей
среды под общим лимитом 8 ГБ: память Docker daemon, ядра и других процессов
Linux/WSL2 в сумму не входит. Прогона всех восьми модулей под таким лимитом
не было. Docker Desktop и ARM64 в этой проверке не запускались.
Методика и результаты сохранены в [отчете проверки](../plans/trino-memory-validation.md).
Размеры локально собранных образов на этой машине: Spark — около 3,37 ГБ,
Jupyter — 3,72 ГБ, IDE — 4,90 ГБ. Jupyter включает слои Spark, IDE включает слои
Jupyter: складывать эти три числа для оценки места неправильно. IDE добавляет
около 1,18 ГБ. IDE вместе с отдельными образами Trino, PostgreSQL и MinIO дает
около 8,1 ГБ до учета кэша сборки, данных и особенностей хранилища Docker.
Поэтому перед первой сборкой нужен запас места сверх размера готовых образов.
## Доступ к сервисам ## Доступ к сервисам
@@ -64,16 +105,41 @@ docker compose down -v
или настройки в файле, перезапусти ядро ноутбука и снова выполни ячейки или настройки в файле, перезапусти ядро ноутбука и снова выполни ячейки
с импортами и созданием сессии. с импортами и созданием сессии.
Фактический master и имя приложения доступны через `spark.sparkContext.master`
и `.appName`. Текущие SQL-настройки можно прочитать через `spark.conf.get(...)`.
В Spark 3.5.1 `spark.sql.session.timeZone` меняется через `spark.conf.set(...)`
без остановки сессии. Для нового имени приложения нужен новый SparkContext.
Пример запуска smoke-скрипта: Пример запуска smoke-скрипта:
```bash ```bash
docker compose exec spark-master \ docker compose exec jupyter python3 /opt/src/spark/cluster_smoke.py
/opt/spark/bin/spark-submit /opt/src/spark/cluster_smoke.py
``` ```
### Trino ### Trino
- Web UI: `http://localhost:8090` - Web UI: `http://localhost:8090`
- Контейнер ограничен 2 ГиБ RAM (`mem_limit: 2g`). `memswap_limit: 2g`
отключает swap для него: одинаковое значение задает общий предел RAM + swap.
- В [`trino/jvm.config`](../trino/jvm.config) heap Java начинается с 512 МиБ
и может вырасти до 1 ГиБ (`-Xms512M`, `-Xmx1G`). Остальная память контейнера
оставлена для потоков, служебных структур JVM и буферов вне heap.
Эти ограничения относятся к Trino; у всего Compose-проекта общего лимита
памяти нет. Если контейнер достигнет жесткого предела, он может быть завершен
системой. Увеличение только heap без изменения бюджета контейнера уменьшает
запас для памяти вне heap. См. [лимиты Compose](https://docs.docker.com/reference/compose-file/services/#mem_limit)
и [настройку JVM Trino](https://trino.io/docs/current/installation/deployment.html#jvm-config).
После обновления этих файлов примени настройки из терминала хоста:
```bash
docker compose up -d --no-deps --force-recreate trino
docker compose ps -a trino
```
Дождись `healthy`. Эта операция пересоздает только Trino и применяет в том числе
изменения содержимого `jvm.config`; таблицы остаются в MinIO и PostgreSQL.
CLI внутри контейнера: CLI внутри контейнера:
@@ -101,7 +167,20 @@ SHOW TABLES FROM lakehouse.default;
- логин: `minioadmin` - логин: `minioadmin`
- пароль: `minioadmin` - пароль: `minioadmin`
Стенд использует зафиксированные исторические образы MinIO Server и `mc` из официального namespace `quay.io/minio`. Эти версии подходят для локального курса, но больше не получают обновлений upstream и не предназначены для production. Стенд использует готовый образ `coollabsio/minio:RELEASE.2025-04-22T22-12-26Z`.
Это сторонняя [сборка из официальных исходников MinIO](https://github.com/coollabsio/minio),
которая содержит сервер и клиент `mc` версии `RELEASE.2025-08-13T08-35-41Z`.
Версия сервера предшествует [изменению встроенной консоли в мае 2025 года](https://github.com/minio/minio/releases/tag/RELEASE.2025-05-24T17-08-30Z).
Образ подходит для локального курса; это историческая
версия без текущих обновлений, не production-конфигурация.
В Compose закреплён digest индекса для AMD64 и ARM64:
`sha256:a4938f37f1be1841b8e7b627ad0207b265345fd0d063e42d7410c78af0e63e68`.
Сервисы `minio` и `minio-init` используют один образ. Компиляция MinIO и установка
Go не нужны. По умолчанию образ скачивается из GHCR без авторизации. Автор
публикует тот же digest в Docker Hub; резервный источник выбирается через
`MINIO_REGISTRY=docker.io` в `.env`. Подробности — в
[инструкции по доступу к репозиториям](./network_access.md).
Бакет `lakehouse` обычно создаётся автоматически контейнером `minio-init`. Бакет `lakehouse` обычно создаётся автоматически контейнером `minio-init`.
@@ -123,18 +202,85 @@ docker compose run --rm iceberg-catalog-init
### JupyterLab ### JupyterLab
- адрес: `http://localhost:8888` Основной интерфейс курса: `http://localhost:8888`. Открой ноутбук из списка
- порт опубликован только на `127.0.0.1` хоста; файлов слева; ядро **Python 3 (ipykernel)** выполняется внутри `jupyter`.
- аутентификация и XSRF-проверка отключены для локального одно-пользовательского стенда; Локальный Python или venv не нужен.
- через VS Code Remote-SSH используй `Existing Jupyter Server` с адресом `http://127.0.0.1:8888/` без token;
- `./notebooks` смонтирован как `/opt/work`
- `./src` смонтирован read-only как `/opt/src`
- `./data` смонтирован read-only как `/opt/data`
- `PYTHONPATH=/opt/src`
Первый ноутбук курса: `notebooks/01_environment_and_smoke_test.ipynb`. - Порт опубликован только на `127.0.0.1` хоста.
- Аутентификация и XSRF-проверка отключены для локального однопользовательского стенда.
- `./notebooks` смонтирован как `/opt/work` для чтения и записи.
- `./src` и `./data` доступны только для чтения как `/opt/src` и `/opt/data`.
- `PYTHONPATH=/opt/src`.
Для Модуля 3 локально скачанный data bundle должен лежать на хосте в `./data/nyc_taxi` и будет доступен внутри Jupyter по пути `/opt/data/nyc_taxi`. Для готового стенда на удалённой машине открой туннель в терминале своего компьютера:
```bash
ssh -N -L 8888:127.0.0.1:8888 user@host
```
Подставь свой SSH-адрес вместо `user@host`, оставь терминал открытым и перейди
на `http://localhost:8888`. Первый ноутбук:
`notebooks/01_environment_and_smoke_test.ipynb`. Перед Модулем 3 скачанный
на хост data bundle должен лежать в `./data/nyc_taxi`; в контейнере он доступен
как `/opt/data/nyc_taxi`.
### VS Code в браузере
Это альтернативный интерфейс, который запускается вместе с JupyterLab.
Используй его по желанию; основной маршрут курса описан для JupyterLab.
Открой `http://localhost:8889`. Сервис `ide` запускает code-server с установленными
расширениями Python и Jupyter. Python, ipykernel, Java, PySpark и зависимости курса
уже находятся в образе. Локальный Python или venv не нужен.
В рабочей области доступны ноутбуки, примеры из `src/` и документация. Репозиторий
смонтирован в `/opt/course` только для чтения, а его каталог `notebooks/` — для
чтения и записи. Так относительные ссылки между документами и ноутбуками работают
и внутри IDE. Пути `/opt/work`, `/opt/src`, `/opt/data` также доступны примерам курса.
Если сверху виден **Restricted Mode**, нажми **Manage → Trust** и закрой окно
Workspace Trust. Подтверди доверие только к своей копии учебного репозитория.
В папке **Ноутбуки** открой `01_environment_and_smoke_test.ipynb`, нажми
**Select Kernel → Python Environments** и выбери `/usr/bin/python3`. Это Python
внутри контейнера IDE. Расширение Jupyter запускает ядро там же; адрес отдельного
Jupyter Server вводить не требуется. Запускай ячейки через `Shift+Enter`, сохраняй
через **File → Save** (`Ctrl+S`; на macOS — `Cmd+S`). Терминал IDE тоже работает
внутри контейнера; команды `docker compose` выполняй на хосте стенда.
Совместимость зависит от браузера, его расширений и средств защиты на компьютере.
При проверке с Windows, Edge и Kaspersky сам редактор открывался, а ноутбук
блокировался дополнительной политикой безопасности страницы. Похожий случай
описан в [обсуждении code-server](https://github.com/coder/code-server/discussions/7457).
Если видишь `Error loading webview` или `Could not register service worker`
даже на `localhost`, продолжи в JupyterLab. Настраивать или отключать защиту
ради прохождения курса не требуется. Открытие окна VS Code само по себе
не подтверждает работу ноутбуков: проверь запуск ячейки и её вывод.
Порт IDE опубликован только на `127.0.0.1`, без пароля, для одного пользователя.
Открывай именно `localhost` или `127.0.0.1`: браузер считает их доверенным
контекстом даже по HTTP. Notebook webview использует service worker; при обращении
по обычному HTTP к IP сервера или имени в локальной сети этот механизм может
быть недоступен. Это ограничение secure context, отдельное от CORS и XSRF.
См. [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts)
и [webviews в code-server](https://coder.com/docs/code-server/FAQ#why-do-web-views-not-work).
Для стенда на удалённой машине используй SSH-туннель:
```bash
ssh -N -L 8889:127.0.0.1:8889 user@host
```
Затем открой `http://localhost:8889` на своём компьютере. Прямой доступ по
публичному адресу не входит в эту конфигурацию.
Образы собираются в порядке `spark-master → jupyter → ide` через Compose
`additional_contexts` (нужен Docker Compose 2.17+ и BuildKit). Версии code-server
и расширений зафиксированы в `ide/Dockerfile`; расширения скачиваются при сборке.
Автоматическое обновление расширений отключено, чтобы повторный запуск использовал
проверенные версии.
Файлы ноутбуков общие с JupyterLab, а ядра и переменные Python отдельные.
Перед сменой интерфейса сохрани ноутбук, выполни `spark.stop()` и останови ядро.
Не редактируй один файл в двух интерфейсах одновременно.
## Как связаны Spark, Trino, PostgreSQL и MinIO ## Как связаны Spark, Trino, PostgreSQL и MinIO
@@ -152,16 +298,45 @@ docker compose run --rm iceberg-catalog-init
| `docker-compose.yml` | состав сервисов, порты, volumes, init-контейнеры | | `docker-compose.yml` | состав сервисов, порты, volumes, init-контейнеры |
| `spark/spark-defaults.conf` | Spark master, catalog `lakehouse`, `s3a`, Iceberg extensions | | `spark/spark-defaults.conf` | Spark master, catalog `lakehouse`, `s3a`, Iceberg extensions |
| `trino/catalog/lakehouse.properties` | каталог Trino `lakehouse`, JDBC и S3-доступ | | `trino/catalog/lakehouse.properties` | каталог Trino `lakehouse`, JDBC и S3-доступ |
| `trino/jvm.config` | JVM Trino: heap до 1 ГиБ внутри контейнера с лимитом 2 ГиБ |
| `spark/Dockerfile` | образ Spark с Iceberg, S3A и Python-зависимостями | | `spark/Dockerfile` | образ Spark с Iceberg, S3A и Python-зависимостями |
| `jupyter/Dockerfile` | образ JupyterLab на базе Spark | | `jupyter/Dockerfile` | образ JupyterLab на базе Spark |
| `ide/Dockerfile` | code-server и расширения на базе образа Jupyter |
| `ide/lakehouse.code-workspace` | папки рабочей области и настройки Python |
| `docs/network_access.md` | источники образов и зависимостей, резервный registry и диагностика сети |
| `docs/study_workflow.md` | повседневная работа, сохранение состояния и диагностика ошибок |
## Smoke-тесты ## Smoke-тесты
### JupyterLab и первый ноутбук
Пройди [первый запуск ноутбука](../START_HERE.md#как-работать-с-ноутбуками):
открой Модуль 1, выбери **Python 3 (ipykernel)**, выполни ячейки и сверь вывод
Spark. После сохранения и повторного открытия вывод должен остаться в файле.
### Дополнительная IDE и Jupyter-ноутбук
На Linux с Docker Engine, после запуска стенда:
```bash
bash tests/browser_ide.sh
```
Проверка запускает Chromium в отдельном контейнере и проходит путь студента:
доверие рабочей области → открытие копии Модуля 1 → выбор Python → Run All →
вывод Spark в webview → сохранение → повторное открытие. Затем проверяет, что
все кодовые ячейки выполнились без ошибок и результат сохранился. Исходный
ноутбук не меняется. Скриншот остаётся в каталоге `/tmp/lakehouse-browser-check.*`,
путь печатается перед запуском. Docker Desktop этим скриптом не проверяется:
он использует Linux host networking.
Для проверки отдельного стенда можно задать `COMPOSE_FILE` и `IDE_URL`; по
умолчанию тест использует текущий Compose-проект и `http://localhost:8889`.
### 1. Проверка, что Spark-кластер жив ### 1. Проверка, что Spark-кластер жив
```bash ```bash
docker compose exec spark-master \ docker compose exec jupyter python3 /opt/src/spark/cluster_smoke.py
/opt/spark/bin/spark-submit /opt/src/spark/cluster_smoke.py
``` ```
### 2. Проверка Spark -> Trino через общий каталог ### 2. Проверка Spark -> Trino через общий каталог
@@ -216,7 +391,11 @@ SELECT * FROM <table>.files;
-- Обслуживание (Модуль 8) -- Обслуживание (Модуль 8)
CALL lakehouse.system.rewrite_data_files(table => '<namespace>.<table>'); CALL lakehouse.system.rewrite_data_files(table => '<namespace>.<table>');
CALL lakehouse.system.expire_snapshots(table => '<namespace>.<table>', retain_last => N); CALL lakehouse.system.expire_snapshots(
table => '<namespace>.<table>',
older_than => TIMESTAMP '<cutoff>',
retain_last => N
);
-- Rollback (Модуль 7) -- Rollback (Модуль 7)
CALL lakehouse.system.rollback_to_snapshot(table => '<namespace>.<table>', snapshot_id => <id>); CALL lakehouse.system.rollback_to_snapshot(table => '<namespace>.<table>', snapshot_id => <id>);
@@ -225,6 +404,8 @@ CALL lakehouse.system.rollback_to_snapshot(table => '<namespace>.<table>', snaps
SELECT * FROM <table> VERSION AS OF <snapshot_id>; SELECT * FROM <table> VERSION AS OF <snapshot_id>;
``` ```
Для `expire_snapshots` подставь в `<cutoff>` границу хранения истории в часовом поясе Spark-сессии. `retain_last` сохраняет минимум N последних snapshot-ов в цепочке предков текущего состояния, а не ограничивает всю историю числом N. Получение границы времени для учебного примера показано в [Модуле 8, Секции 4](../notebooks/08_maintenance_and_final_lab.ipynb).
### Часто используемые Trino SQL ### Часто используемые Trino SQL
```sql ```sql
@@ -248,6 +429,7 @@ SELECT * FROM <table> FOR VERSION AS OF <snapshot_id>;
- `README.md` — чтобы понять, что это за репозиторий и куда идти дальше. - `README.md` — чтобы понять, что это за репозиторий и куда идти дальше.
- `START_HERE.md` — чтобы впервые поднять стенд и пройти Модуль 1. - `START_HERE.md` — чтобы впервые поднять стенд и пройти Модуль 1.
- `study_workflow.md` — чтобы продолжить занятие, сохранить результат и разобраться с ошибкой.
- `stack_reference.md` — чтобы быстро вспомнить порты, команды, точки доступа, шпаргалку и smoke-тесты. - `stack_reference.md` — чтобы быстро вспомнить порты, команды, точки доступа, шпаргалку и smoke-тесты.
- `course_program.md` — чтобы понять учебную траекторию дальше первого модуля. - `course_program.md` — чтобы понять учебную траекторию дальше первого модуля.
- `glossary.md` — чтобы вернуться к определению термина (storage, catalog, compute, snapshot и др.). - `glossary.md` — чтобы вернуться к определению термина (storage, catalog, compute, snapshot и др.).
+160
View File
@@ -0,0 +1,160 @@
# Как заниматься со стендом
Для первой сборки используй [START_HERE](../START_HERE.md), для выбора урока —
[маршрут по модулям](./course_program.md#маршрут-по-модулям). Эта памятка нужна,
когда окружение уже готово.
## Начать следующее занятие
1. Запусти Docker и открой терминал хоста в той же папке репозитория.
2. Выполни:
```bash
docker compose up -d
docker compose ps -a
```
Дождись `healthy` у `ide`, `trino` и `postgres-iceberg`. У двух init-контейнеров
нормальное состояние — `Exited (0)`. Повторять сборку перед каждым занятием
не нужно; она понадобится после изменения образов или зависимостей.
3. Открой [JupyterLab](http://localhost:8888) и нужный ноутбук в списке файлов слева.
При запросе ядра выбери **Python 3 (ipykernel)**.
4. После остановки стенда переменные Python нужно создать заново: выполни
импорты, создание Spark-сессии и подключений в начале текущего ноутбука.
Сохраненный вывод ячеек может быть виден еще до их запуска.
5. Продолжи с места, на котором остановился. Перед повтором ячеек с записью
данных, `INSERT`, `CREATE OR REPLACE`, `DROP` или очисткой истории прочитай
пояснение: повторный запуск может изменить таблицу или создать новый snapshot.
Используй прежнюю папку репозитория: имя Compose-проекта определяет, какие
Docker volumes подключаются. После переименования папки или запуска с другим
`-p` можно получить новый пустой набор данных вместо предыдущего.
Вместе с JupyterLab доступен [VS Code в браузере](./stack_reference.md#vs-code-в-браузере).
Там же описаны выбор ядра и ограничения совместимости. Перед сменой интерфейса
сохрани файл, выполни `spark.stop()` и останови ядро. Файлы общие, переменные
Python — нет; не редактируй один ноутбук в двух интерфейсах одновременно.
## Как проходить ноутбук
Ноутбук состоит из пояснений и кодовых ячеек. Ядро Python выполняет код и хранит
переменные в памяти. Spark-сессия связывает этот Python-процесс с кластером.
- Прочитай пояснение, запусти ячейку через `Shift+Enter`, дождись окончания и
сравни результат с описанием. Следующая ячейка может использовать предыдущие
переменные, поэтому порядок важен.
- В самостоятельном задании заполни `TODO` и проверь результат. **Run → Run All Cells**
запускает код, но не выполняет задание за тебя.
- Checkpoint — вопросы и проверки в конце урока. Покажи результат в своем
стенде и объясни его своими словами перед переходом к следующему модулю.
- Сохраняй изменения через **File → Save Notebook** (`Ctrl+S`; на macOS — `Cmd+S`).
Файл ноутбука вместе с ответами и выводом записывается в `notebooks/` на хосте.
- Перед следующим модулем выполни завершающую ячейку `spark.stop()`, если она
есть; иначе вызови `spark.stop()` после окончания вычислений. Это освобождает
ресурсы Spark для следующего приложения. Закрытие вкладки браузера само по
себе не останавливает вычисления.
Держи одну активную Spark-сессию: если несколько ноутбуков одновременно занимают
все ядра worker-ов, новое задание может ждать ресурсы. Для своих SQL/Python-опытов
можно создать дополнительный ноутбук через **File → New → Notebook**. Он появится
в `notebooks/` на хосте. Примеры `src/` доступны Python по пути `/opt/src` только
для чтения; документацию и конфиги открывай в копии репозитория на хосте.
### Если нужно перезапустить ядро
Сначала сохрани ноутбук. Выбери **Kernel → Restart Kernel…** и подтверди перезапуск.
Затем заново выполни импорты и создание Spark-сессии. Переменные Python исчезнут;
файлы и записанные учебные таблицы сохранятся.
Перезапуск нужен, например, после изменения адреса кластера в
`spark/spark-defaults.conf` или после остановки Spark-сессии, с которой хочешь
продолжить работу. Повторный `getOrCreate()` при живой сессии использует ее;
подробнее это разбирается в Модуле 3.
## Закончить занятие
Сохрани ноутбук, дождись завершения вычислений и запиши для себя номер модуля
и следующую ячейку. Затем в терминале хоста выполни:
```bash
docker compose stop
```
Контейнеры остановятся и освободят занятую ими оперативную память. Файлы
ноутбуков, исходные данные и таблицы останутся. На следующем занятии используй
`docker compose up -d` из начала этой памятки.
## Что и где сохраняется
Docker volume — хранилище вне файловой системы контейнера. В этом стенде два
таких хранилища: `minio-data` с объектами и `postgres-data` с каталогом Iceberg.
Имена volumes в Docker обычно имеют префикс Compose-проекта.
| Что | Где находится | После `stop` или `down` без `-v` |
| --- | --- | --- |
| Код, ответы и сохраненный вывод ноутбуков | `notebooks/` на хосте; `/opt/work` в JupyterLab | Сохраняются |
| Скачанные Parquet и CSV | `data/nyc_taxi/` на хосте; `/opt/data/nyc_taxi` в контейнере | Сохраняются |
| Raw-файлы, данные и metadata-файлы Iceberg | Бакет `lakehouse`, volume `minio-data` | Сохраняются |
| Записи каталога Iceberg | PostgreSQL, volume `postgres-data` | Сохраняются |
| Переменные Python и живая Spark-сессия | Память процессов | Теряются; создай заново |
| Настройки интерфейса JupyterLab | Файловая система контейнера `jupyter` | При `stop` сохраняются; при удалении или пересоздании контейнера могут сброситься |
| Настройки VS Code, доверие рабочей области | Файловая система контейнера `ide` | При `stop` сохраняются; при удалении или пересоздании контейнера могут сброситься |
`docker compose down` удаляет контейнеры и сеть проекта. Именованные volumes
остаются, поэтому после `up -d` таблицы доступны снова. Для обычного окончания
занятия достаточно `stop`. Поведение команд описано также в документации Docker:
[stop](https://docs.docker.com/reference/cli/docker/compose/stop/),
[down](https://docs.docker.com/reference/cli/docker/compose/down/).
### Начать с чистых данных
Этот шаг нужен только для осознанного повторного прохождения с пустым
хранилищем. Он удалит оба volumes: raw-данные в MinIO, таблицы, их историю
и записи каталога PostgreSQL. Сохраненные ноутбуки и скачанные файлы в `data/`
на хосте останутся.
```bash
docker compose down -v
docker compose up -d
```
После сброса в ноутбуках еще может быть старый вывод. Заново создай raw в
Модуле 3, bronze в Модуле 4 и silver в Модуле 5, прежде чем продолжать модули,
которые используют эти данные. Сброс не является способом исправить сеть
или выбор ядра.
## Если что-то не работает
Все `docker compose` команды ниже выполняются в терминале хоста из корня
репозитория. Имя сервиса для логов берется из колонки `SERVICE` в `ps`:
например, `postgres`, хотя контейнер называется `postgres-iceberg`.
| Симптом | Первая проверка | Что сделать дальше |
| --- | --- | --- |
| Docker не отвечает | `docker version`: есть ли раздел Server? | Запусти Docker; при `permission denied` проверь доступ своего пользователя к Docker |
| Образ или зависимость не скачивается | Найди адрес источника и сервис в ошибке | Пройди [диагностику сети](./network_access.md) |
| `port is already allocated` / `address already in use` | Какой порт занят, не запущена ли другая копия стенда? | Освободи нужный порт; [список портов](../START_HERE.md#порты-стенда) |
| Сервис завершился, `unhealthy` или долго `starting` | `docker compose ps -a`, затем `docker compose logs --tail=100 <service>` | Для `postgres-iceberg` используй сервис `postgres`; проверь первую ошибку в логах |
| JupyterLab не открывается | `docker compose ps -a jupyter`, логи `jupyter` | Открой `http://localhost:8888`; для сервера используй [SSH-туннель](./stack_reference.md#jupyterlab) |
| VS Code не открывает ноутбук или сообщает об ошибке webview / service worker | Адрес `http://localhost:8889`, отдельная вкладка | Проверь [ограничения совместимости](./stack_reference.md#vs-code-в-браузере); продолжи в JupyterLab |
| Не выбирается ядро, нет `pyspark` | Выбрано **Python 3 (ipykernel)** в JupyterLab стенда? | Повтори [выбор ядра](../START_HERE.md#как-работать-с-ноутбуками); проверь логи `jupyter` |
| `NameError`, потерялась переменная | Ядро перезапускалось или выбран другой ноутбук? | Выполни импорты и подготовительные ячейки по порядку |
| Spark долго ждет, `Initial job has not accepted any resources` | В Spark UI два worker-а `ALIVE`? Есть другие приложения в `Running Applications`? | Заверши свою предыдущую сессию через `spark.stop()`; проверь логи worker-ов |
| Ядро умерло, контейнер убит, подозрение на память | `docker stats --no-stream`, лимит памяти Docker/WSL2 | Сверь [системные требования](../START_HERE.md#системные-требования), закрой другие вычисления; одного кода 137 недостаточно для вывода об OOM |
| Ноутбук не сохраняется (`Permission denied`) | Это файл из `notebooks/`? Совпадает UID? | Проверь `.env` и [настройку прав перед сборкой](../START_HERE.md#быстрый-запуск); после изменения UID/GID повтори `docker compose build`, затем `docker compose up -d` |
| Нет файлов Taxi или таблицы из прошлого модуля | Файлы в `data/nyc_taxi`, предыдущие модули, прежняя папка проекта | Для raw нужен Модуль 3, для bronze — 4, для silver — 5; после сброса восстанови их по порядку |
### Что передать ментору
Укажи ОС, модуль, ячейку или команду, ожидаемый результат и полный текст ошибки.
Приложи вывод:
```bash
docker compose version
docker compose ps -a
docker compose logs --tail=100 jupyter
```
В последней команде замени `jupyter` на проблемный сервис, если ошибка в другом компоненте.
Для ошибки ноутбука добавь имя выбранного ядра и вывод ячейки. Пароли, токены
и адреса прокси с учетными данными в сообщение не включай.
+35
View File
@@ -0,0 +1,35 @@
# Compose передаёт результат сборки jupyter через additional_contexts.
FROM jupyter-runtime
USER root
ARG CODE_SERVER_VERSION=4.141.0
ARG PYTHON_EXTENSION_VERSION=2026.4.0
ARG JUPYTER_EXTENSION_VERSION=2025.9.1
# Контрольные суммы .deb из выпуска code-server v4.141.0.
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
amd64) checksum=f384beb4f3ba207e0bca27d0dbd36dd825d5aa909d78c6d4baa8703eaf6b77ba ;; \
arm64) checksum=0bca33a9b01180ad88f0b8969a8e34ae1c76417df471697e7cd65c20ef5dd60d ;; \
*) echo "Unsupported architecture: $arch" >&2; exit 1 ;; \
esac; \
curl -fL --retry 3 \
"https://github.com/coder/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server_${CODE_SERVER_VERSION}_${arch}.deb" \
-o /tmp/code-server.deb; \
echo "$checksum /tmp/code-server.deb" | sha256sum -c -; \
apt-get update; \
apt-get install -y --no-install-recommends /tmp/code-server.deb; \
rm -rf /tmp/code-server.deb /var/lib/apt/lists/*
USER jovyan
ENV HOME=/home/jovyan
RUN code-server --install-extension "ms-python.python@${PYTHON_EXTENSION_VERSION}" \
&& code-server --install-extension "ms-toolsai.jupyter@${JUPYTER_EXTENSION_VERSION}"
COPY lakehouse.code-workspace /opt/lakehouse.code-workspace
COPY --chown=jovyan settings.json /home/jovyan/.local/share/code-server/User/settings.json
EXPOSE 8889
CMD ["code-server", "--bind-addr", "0.0.0.0:8889", "--auth", "none", \
"--disable-telemetry", "--disable-update-check", \
"/opt/lakehouse.code-workspace"]
+10
View File
@@ -0,0 +1,10 @@
{
"folders": [
{ "name": "Ноутбуки", "path": "/opt/course/notebooks" },
{ "name": "Примеры (только чтение)", "path": "/opt/course/src" },
{ "name": "Документация (только чтение)", "path": "/opt/course/docs" }
],
"settings": {
"python.defaultInterpreterPath": "/usr/bin/python3"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extensions.autoUpdate": false,
"extensions.autoCheckUpdates": false,
"telemetry.telemetryLevel": "off",
"workbench.startupEditor": "none",
"chat.disableAIFeatures": true,
"workbench.secondarySideBar.defaultVisibility": "hidden"
}
+1 -1
View File
@@ -1,5 +1,5 @@
# Берем за основу контейнер, приготовленный для spark в spark/Dockerfile # Берем за основу контейнер, приготовленный для spark в spark/Dockerfile
FROM de-spark-iceberg:3.5.1 FROM spark-runtime
USER root USER root
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
+21 -16
View File
@@ -12,10 +12,10 @@
"На выходе ты должен:\n", "На выходе ты должен:\n",
"\n", "\n",
"- понимать, что стенд уже поднят на хосте;\n", "- понимать, что стенд уже поднят на хосте;\n",
"- подключиться к Spark-кластеру из Jupyter;\n", "- подключиться к Spark-кластеру из ноутбука;\n",
"- выполнить базовый smoke test;\n", "- выполнить базовый smoke test;\n",
"- сопоставить сервисы стенда с их ролями;\n", "- сопоставить сервисы стенда с их ролями;\n",
"- знать первый маршрут диагностики через `docker compose ps`, логи и UI.\n" "- знать первый маршрут диагностики через `docker compose ps -a`, логи и UI.\n"
] ]
}, },
{ {
@@ -25,15 +25,16 @@
"source": [ "source": [
"## Перед стартом\n", "## Перед стартом\n",
"\n", "\n",
"Перед запуском этого ноутбука пройди шаги из `START_HERE.md`.\n", "Если стенд еще не готов, выполни подготовку и запуск по [START_HERE](../START_HERE.md#быстрый-запуск).\n",
"Если возвращаешься после паузы, используй [памятку учащегося](../docs/study_workflow.md).\n",
"\n", "\n",
"Важно: `docker compose build`, `docker compose up -d`, `docker compose ps` и просмотр логов выполняются на хосте, а не внутри Jupyter.\n", "Важно: `docker compose build`, `docker compose up -d`, `docker compose ps -a` и просмотр логов выполняются в терминале хоста; терминал JupyterLab работает внутри контейнера.\n",
"\n", "\n",
"Ожидаемое состояние перед началом:\n", "Ожидаемое состояние перед началом:\n",
"\n", "\n",
"- контейнеры `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`, `trino`, `jupyter` уже подняты;\n", "- контейнеры `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`, `trino`, `jupyter`, `ide` уже подняты;\n",
"- открываются `Spark UI`, `Trino UI`, `MinIO Console` и `JupyterLab`;\n", "- открываются `Spark UI`, `Trino UI`, `MinIO Console` и JupyterLab;\n",
"- ты работаешь в Jupyter внутри контейнера `jupyter`.\n" "- в JupyterLab выбрано ядро `Python 3 (ipykernel)` внутри контейнера `jupyter` (Kernel → Change Kernel…).\n"
] ]
}, },
{ {
@@ -49,7 +50,7 @@
"| `PostgreSQL` | `catalog`: хранит метаданные JDBC-каталога Iceberg |\n", "| `PostgreSQL` | `catalog`: хранит метаданные JDBC-каталога Iceberg |\n",
"| `Spark` | `compute`: выполняет PySpark и Spark SQL |\n", "| `Spark` | `compute`: выполняет PySpark и Spark SQL |\n",
"| `Trino` | `compute`: читает те же таблицы через общий каталог |\n", "| `Trino` | `compute`: читает те же таблицы через общий каталог |\n",
"| `Jupyter` | Точка входа в учебные ноутбуки |\n", "| `JupyterLab` | Точка входа в учебные ноутбуки |\n",
"\n", "\n",
"В этом модуле мы не строим пайплайн и не создаём Iceberg-таблицы. Задача только одна: убедиться, что среда рабочая и понятная.\n" "В этом модуле мы не строим пайплайн и не создаём Iceberg-таблицы. Задача только одна: убедиться, что среда рабочая и понятная.\n"
] ]
@@ -120,7 +121,7 @@
"\n", "\n",
"Если тест проходит, это означает минимум следующее:\n", "Если тест проходит, это означает минимум следующее:\n",
"\n", "\n",
"- Jupyter может создать SparkSession;\n", "- ядро ноутбука может создать SparkSession;\n",
"- Spark подключён к кластерному master;\n", "- Spark подключён к кластерному master;\n",
"- задание действительно выполняется, а не падает на первом действии.\n" "- задание действительно выполняется, а не падает на первом действии.\n"
] ]
@@ -146,10 +147,10 @@
"\n", "\n",
"Сделай руками и проверь себя:\n", "Сделай руками и проверь себя:\n",
"\n", "\n",
"1. Открой `Spark UI`, `Trino UI`, `MinIO Console` и `JupyterLab`.\n", "1. Открой `Spark UI`, `Trino UI`, `MinIO Console` и JupyterLab.\n",
"2. Сопоставь каждый сервис с одной из ролей: `storage`, `catalog`, `compute`, `entrypoint`.\n", "2. Сопоставь каждый сервис с одной из ролей: `storage`, `catalog`, `compute`, `entrypoint`.\n",
"3. Объясни, почему этот smoke test не должен работать в `local[*]` режиме.\n", "3. Объясни, почему этот smoke test не должен работать в `local[*]` режиме.\n",
"4. На хосте выполни `docker compose ps` и посмотри, какие контейнеры должны быть в состоянии `Up`.\n", "4. На хосте выполни `docker compose ps -a` и посмотри, какие контейнеры должны быть в состоянии `Up`.\n",
"5. На хосте выполни `docker compose logs -f trino` или `docker compose logs -f spark-master`, чтобы посмотреть, где начинается первичная диагностика.\n" "5. На хосте выполни `docker compose logs -f trino` или `docker compose logs -f spark-master`, чтобы посмотреть, где начинается первичная диагностика.\n"
] ]
}, },
@@ -166,7 +167,7 @@
" \"PostgreSQL\": \"TODO\",\n", " \"PostgreSQL\": \"TODO\",\n",
" \"Spark\": \"TODO\",\n", " \"Spark\": \"TODO\",\n",
" \"Trino\": \"TODO\",\n", " \"Trino\": \"TODO\",\n",
" \"Jupyter\": \"TODO\",\n", " \"IDE\": \"TODO\",\n",
"}\n", "}\n",
"service_roles\n" "service_roles\n"
] ]
@@ -181,10 +182,14 @@
"К концу модуля ты должен уметь подтвердить:\n", "К концу модуля ты должен уметь подтвердить:\n",
"\n", "\n",
"- стенд поднят и основные контейнеры живы;\n", "- стенд поднят и основные контейнеры живы;\n",
"- `Spark UI`, `MinIO Console`, `Trino UI` и `JupyterLab` открываются;\n", "- `Spark UI`, `MinIO Console`, `Trino UI` и JupyterLab открываются;\n",
"- smoke test завершился успешно и вернул ожидаемый результат;\n", "- smoke test напечатал `Spark cluster smoke test passed.` и `result_sum=499999500000`;\n",
"- ты понимаешь назначение `MinIO`, `PostgreSQL`, `Spark`, `Trino`, `Jupyter`;\n", "- ты понимаешь назначение `MinIO`, `PostgreSQL`, `Spark`, `Trino` и IDE;\n",
"- ты знаешь, что первый маршрут диагностики начинается с `docker compose ps`, `docker compose logs -f <service>` и проверки UI.\n" "- ты знаешь, что первый маршрут диагностики начинается с `docker compose ps -a`, `docker compose logs -f <service>` и проверки UI.\n",
"\n",
"После последней ячейки `spark.stop()` сохрани ноутбук. Дальше — [Модуль 2](./02_lakehouse_mental_model.ipynb)\n",
"или [завершение занятия](../docs/study_workflow.md#закончить-занятие).\n",
"Все уроки собраны в [маршруте по модулям](../docs/course_program.md#маршрут-по-модулям)."
] ]
}, },
{ {
+330 -48
View File
@@ -7,15 +7,9 @@
"source": [ "source": [
"# Модуль 3. Raw-данные и первое чтение в Spark\n", "# Модуль 3. Raw-данные и первое чтение в Spark\n",
"\n", "\n",
"В этом модуле мы разбираем полный первый цикл работы с учебным датасетом в Lakehouse:\n", "В прошлом модуле мы проследили, как Spark работает с каталогом и файлами в MinIO. Теперь загрузим настоящие данные: поездки NYC Taxi и справочник зон.\n",
"\n", "\n",
"- проверяем заранее скачанный локальный data bundle;\n", "Сначала подготовим Spark-сессию и проверим подключение. Затем перенесем заранее скачанные файлы в raw-зону MinIO, прочитаем их через Spark и посмотрим на схему, пропуски и аномалии. Эти файлы останутся источником для таблиц, которые мы построим в следующих модулях.\n"
"- загружаем исходные файлы в raw-зону `MinIO`;\n",
"- читаем raw-данные через `Spark` по `s3a://` пути;\n",
"- смотрим схему, типы, базовые метрики и простые аномалии;\n",
"- фиксируем, почему raw-слой нужен как воспроизводимая неизменяемая точка входа.\n",
"\n",
"После этого ноутбука ты должен уметь объяснить, почему raw-данные не надо «чинить на месте» и почему их не стоит скачивать прямо из ноутбука.\n"
] ]
}, },
{ {
@@ -25,20 +19,319 @@
"source": [ "source": [
"## 0. Перед стартом\n", "## 0. Перед стартом\n",
"\n", "\n",
"Перед выполнением ноутбука:\n", "- Подними стенд по `START_HERE.md` и пройди модули 1 и 2.\n",
"- Скачай data bundle в `./data/nyc_taxi` по инструкции из `START_HERE.md`.\n",
"- Открой этот ноутбук в новом ядре, чтобы начать без сессии от предыдущих опытов.\n"
]
},
{
"cell_type": "markdown",
"id": "session-intro",
"metadata": {},
"source": [
"## 1. Как настроен Spark в нашем стенде\n",
"\n", "\n",
"- подними стенд по `START_HERE.md`;\n", "До сих пор мы пользовались готовым объектом `spark`. Разберем, как его создать и куда он отправляет задания: это понадобится при чтении файлов Taxi.\n",
"- пройди `notebooks/01_environment_and_smoke_test.ipynb`;\n",
"- желательно пройти `notebooks/02_lakehouse_mental_model.ipynb`;\n",
"- заранее скачай data bundle в `./data/nyc_taxi` по инструкции из `START_HERE.md`.\n",
"\n", "\n",
"В классическом `PostgreSQL / Greenplum` данные часто уже лежат внутри системы. В Lakehouse путь более явный: сначала исходные файлы должны попасть в object storage, и только потом мы начинаем поверх них читать, профилировать и строить таблицы.\n", "`SparkSession` дает доступ к DataFrame и SQL. Его `SparkContext` связывает приложение с вычислительными ресурсами. В обычном ядре нашего ноутбука используется один работающий контекст.\n",
"\n", "\n",
"| | Классический DWH | Lakehouse |\n", "| Участник | Что делает в стенде |\n",
"| --- | --- | --- |\n", "| --- | --- |\n",
"| Где стартует работа | Данные уже внутри СУБД | Исходные файлы сначала попадают в storage |\n", "| Driver | Работает в `jupyter`: выполняет Python-код, строит план и раздает задания |\n",
"| Первая практическая задача | `SELECT` из готовой таблицы | Загрузить raw и прочитать raw |\n", "| Master | Принимает подключение по `spark://spark-master:7077` и выделяет ресурсы worker-ов |\n",
"| Что важно не сломать | Таблицу / схему БД | Неизменяемый raw-слой |\n" "| Executors | Работают на `spark-worker-1` и `spark-worker-2`, читают данные и выполняют задания |\n",
"\n",
"В режиме `local[*]` задания выполняются в потоках на машине driver-а. Нам нужен кластер, поэтому проверим фактический master вместе с результатом вычисления.\n",
"\n",
"Создадим сессию. `.appName(...)` задает имя приложения в Spark UI, `.getOrCreate()` создает сессию или возвращает уже работающую. Адрес кластера пока возьмем из общего файла настроек.\n"
]
},
{
"cell_type": "code",
"id": "session-demo",
"metadata": {},
"source": [
"from pyspark.sql import SparkSession\n",
"\n",
"spark = (\n",
" SparkSession.builder\n",
" .appName(\"module-03-session-demo\")\n",
" .getOrCreate()\n",
")\n",
"spark.sparkContext.setLogLevel(\"ERROR\")\n",
"\n",
"print(\"Spark:\", spark.version)\n",
"print(\"Master:\", spark.sparkContext.master)\n",
"print(\"Приложение:\", spark.sparkContext.appName)\n",
"assert spark.sparkContext.master == \"spark://spark-master:7077\", \"Нужен Spark-кластер стенда\"\n",
"print(\"Сумма 0…9:\", spark.range(10).groupBy().sum(\"id\").first()[0])"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "session-config-explanation",
"metadata": {},
"source": [
"### Откуда взялось подключение\n",
"\n",
"Master мы не указали в коде: Spark прочитал его из `spark/spark-defaults.conf`. Docker подключает этот файл к Jupyter как `/opt/spark/conf/spark-defaults.conf`. Там же подготовлены каталог Iceberg и доступ к MinIO.\n",
"\n",
"| Настройка в файле | Для чего нужна |\n",
"| --- | --- |\n",
"| `spark.sql.extensions` | Добавляет команды Iceberg к Spark SQL |\n",
"| `spark.sql.catalog.lakehouse` | Подключает Spark-каталог с именем `lakehouse` |\n",
"| `spark.sql.catalog.lakehouse.catalog-impl` и `.uri` | Выбирает JDBC-каталог в PostgreSQL, где зарегистрированы таблицы |\n",
"| `spark.sql.catalog.lakehouse.warehouse` | Задает место для файлов таблиц: `s3a://lakehouse/warehouse` |\n",
"| `spark.hadoop.fs.s3a.endpoint` и `.path.style.access` | Направляют S3A-запросы в MinIO внутри Docker-сети |\n",
"\n",
"При запуске через `spark-submit` настройки можно передать параметрами `--master`, `--name` и `--conf`. В коде для этого есть `.master(...)`, `.appName(...)` и `.config(...)`. Для нового приложения обычный приоритет: код -> параметры запуска -> файл defaults. Параметры запуска driver-а надо задать до его старта; уже работающий контекст новый builder не перестраивает.\n",
"\n",
"В нашем Jupyter дополнительные `--master` и `--conf` при запуске не передаются. Вне стенда потребуются также библиотеки Iceberg, S3A и JDBC: их состав и конфигурация описаны в [справочнике](../docs/stack_reference.md#spark).\n",
"\n",
"Посмотрим, какие значения получила сессия. В файле есть и учетные данные, поэтому выводим только выбранные несекретные настройки. Для SQL используем `spark.conf`; в Hadoop-настройках префикс `spark.hadoop.` уже снят.\n"
]
},
{
"cell_type": "code",
"id": "session-config",
"metadata": {},
"source": [
"for key in (\n",
" \"spark.sql.extensions\",\n",
" \"spark.sql.catalog.lakehouse\",\n",
" \"spark.sql.catalog.lakehouse.catalog-impl\",\n",
" \"spark.sql.catalog.lakehouse.uri\",\n",
" \"spark.sql.catalog.lakehouse.warehouse\",\n",
"):\n",
" print(f\"{key} = {spark.conf.get(key)}\")\n",
"\n",
"hadoop_conf = spark.sparkContext._jsc.hadoopConfiguration()\n",
"for key in (\"fs.s3a.endpoint\", \"fs.s3a.path.style.access\"):\n",
" print(f\"{key} = {hadoop_conf.get(key)}\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "session-live-explanation",
"metadata": {},
"source": [
"### Изменение настройки в работающей сессии\n",
"\n",
"Некоторые настройки можно менять, пока приложение работает. Например, `spark.sql.session.timeZone` определяет часовой пояс SQL-сессии. Посмотрим, как одна и та же секунда Unix-времени отображается в UTC и Москве.\n",
"\n",
"В Spark 3.5.1 эта настройка изменяется через `spark.conf.set(...)`. Сохраним исходное значение и вернем его в `finally`, в том числе при ошибке. Файлы данных этот опыт не меняет.\n"
]
},
{
"cell_type": "code",
"id": "session-live-config",
"metadata": {},
"source": [
"timezone_key = \"spark.sql.session.timeZone\"\n",
"original_timezone = spark.conf.get(timezone_key)\n",
"assert spark.conf.isModifiable(timezone_key)\n",
"try:\n",
" for timezone in (\"UTC\", \"Europe/Moscow\"):\n",
" spark.conf.set(timezone_key, timezone)\n",
" value = spark.sql(\"SELECT from_unixtime(0) AS displayed_time\").first()[0]\n",
" print(timezone, value)\n",
"finally:\n",
" spark.conf.set(timezone_key, original_timezone)\n",
"\n",
"assert spark.conf.get(timezone_key) == original_timezone\n",
"print(\"Исходный часовой пояс восстановлен:\", original_timezone)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "session-reuse-explanation",
"metadata": {},
"source": [
"### Повторный вызов builder-а\n",
"\n",
"Теперь попробуем сменить имя приложения на `module-03-raw-ingest-and-first-read`, с которым будем читать Taxi. Проверим имя у работающего контекста: `spark.sparkContext.appName`.\n"
]
},
{
"cell_type": "code",
"id": "session-reuse",
"metadata": {},
"source": [
"demo_context = spark.sparkContext\n",
"demo_name = demo_context.appName\n",
"same_spark = (\n",
" SparkSession.builder\n",
" .appName(\"module-03-raw-ingest-and-first-read\")\n",
" .getOrCreate()\n",
")\n",
"assert same_spark is spark\n",
"assert same_spark.sparkContext is demo_context\n",
"assert same_spark.sparkContext.appName == demo_name\n",
"print(\"То же приложение:\", same_spark.sparkContext.appName)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "session-stop-explanation",
"metadata": {},
"source": [
"Имя осталось прежним: `getOrCreate()` вернул ту же сессию с тем же контекстом. Повтор ячейки сохраняет и переменные Python, и работающее приложение.\n",
"\n",
"Чтобы создать приложение с новым именем, вызовем `spark.stop()`. Этот вызов останавливает сессию и ее контекст. Переменные Python останутся, но старые Spark DataFrame использовать уже нельзя. Файлы MinIO и таблицы каталога сохранятся.\n",
"\n",
"Для смены master или настроек в общем файле перезапускай ядро: одной остановки сессии недостаточно для всех параметров запуска. Перезапуск удалит переменные Python, и тогда придется повторить импорты, создание сессии и чтение DataFrame.\n"
]
},
{
"cell_type": "code",
"id": "session-stop",
"metadata": {},
"source": [
"spark.stop()"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "session-exercise-explanation",
"metadata": {},
"source": [
"## 2. Подключись к Spark самостоятельно\n",
"\n",
"Создай сессию, с которой продолжишь работу с Taxi. Заполни в следующей ячейке адрес кластера, имя `module-03-raw-ingest-and-first-read` и цепочку `.master(...)`, `.appName(...)`, `.getOrCreate()`.\n",
"\n",
"Настройки каталога возьми из общего файла. Адрес кластера в этот раз задай явно через `.master(...)`; остальные ноутбуки берут его из того же файла.\n",
"\n",
"Пока пропуски не заполнены, шаблон останавливается на `assert`. Выполни заполненный шаблон и проверку подключения дважды: имя и `applicationId` должны остаться теми же.\n"
]
},
{
"cell_type": "code",
"id": "session-exercise",
"metadata": {},
"source": [
"from pyspark.sql import SparkSession\n",
"\n",
"CLUSTER_MASTER = ...\n",
"APP_NAME = ...\n",
"assert CLUSTER_MASTER == \"spark://spark-master:7077\", \"Впиши адрес кластера\"\n",
"assert APP_NAME == \"module-03-raw-ingest-and-first-read\", \"Впиши имя приложения\"\n",
"\n",
"# Замени ... цепочкой вызовов SparkSession.builder.\n",
"spark = ...\n",
"assert isinstance(spark, SparkSession), \"Создай SparkSession через builder\"\n",
"spark.sparkContext.setLogLevel(\"ERROR\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "session-solution",
"metadata": {},
"source": [
"<details>\n",
"<summary>Контрольное решение (после своей попытки)</summary>\n",
"\n",
"```python\n",
"from pyspark.sql import SparkSession\n",
"\n",
"CLUSTER_MASTER = \"spark://spark-master:7077\"\n",
"APP_NAME = \"module-03-raw-ingest-and-first-read\"\n",
"spark = (\n",
" SparkSession.builder\n",
" .master(CLUSTER_MASTER)\n",
" .appName(APP_NAME)\n",
" .getOrCreate()\n",
")\n",
"spark.sparkContext.setLogLevel(\"ERROR\")\n",
"```\n",
"\n",
"</details>"
]
},
{
"cell_type": "code",
"id": "session-connection-check",
"metadata": {},
"source": [
"assert spark.sparkContext.master == CLUSTER_MASTER\n",
"assert spark.sparkContext.appName == APP_NAME\n",
"print(\"Master:\", spark.sparkContext.master)\n",
"print(\"Приложение:\", spark.sparkContext.appName)\n",
"print(\"ID приложения:\", spark.sparkContext.applicationId)"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "session-table-explanation",
"metadata": {},
"source": [
"### Запись и чтение через каталог\n",
"\n",
"Проверим, что сессия может работать с каталогом и MinIO. Следующая ячейка создаст собственную маленькую Iceberg-таблицу, прочитает из нее числа `0` и `1` и удалит ее в `finally`. Она работает и после удаления таблиц из модуля 2.\n",
"\n",
"У таблицы уникальное имя. `DROP TABLE ... PURGE` удаляет ее запись в каталоге, данные и метаданные; в MinIO могут остаться пустые маркеры `data/` и `metadata/`.\n",
"\n",
"Дождись завершения ячейки перед перезапуском ядра: при принудительной остановке процесса `finally` не выполнится. Если прервал ее, сохрани напечатанное имя, чтобы удалить эту таблицу после нового подключения.\n"
]
},
{
"cell_type": "code",
"id": "session-table-check",
"metadata": {},
"source": [
"from uuid import uuid4\n",
"\n",
"session_table = f\"lakehouse.default.session_practice_{uuid4().hex}\"\n",
"print(\"Временная таблица:\", session_table)\n",
"try:\n",
" spark.range(2).writeTo(session_table).using(\"iceberg\").create()\n",
" session_rows = [row.id for row in spark.table(session_table).orderBy(\"id\").collect()]\n",
" assert session_rows == [0, 1]\n",
" print(\"Прочитано через Iceberg:\", session_rows)\n",
"finally:\n",
" spark.sql(f\"DROP TABLE IF EXISTS {session_table} PURGE\")\n",
"\n",
"assert not spark.catalog.tableExists(session_table)\n",
"print(\"Временная таблица удалена\")"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"id": "session-checkpoint",
"metadata": {},
"source": [
"### Проверь запуск в новом ядре\n",
"\n",
"В выводе должны быть master `spark://spark-master:7077`, заданное имя приложения, строки `[0, 1]` и сообщение об удалении таблицы.\n",
"\n",
"Перезапусти ядро и повтори заполненный шаблон, проверку подключения и ячейку с таблицей. `applicationId` будет новым, остальные результаты должны совпасть. Импорт `SparkSession` уже есть в шаблоне, поэтому демонстрацию повторять не нужно.\n",
"\n",
"С этим подключением перейдем к файлам Taxi. Оставь сессию работать.\n"
]
},
{
"cell_type": "markdown",
"id": "raw-preparation",
"metadata": {},
"source": [
"## 3. Проверка data bundle\n",
"\n",
"Подготовим MinIO-клиент для загрузки файлов и пути к данным. Docker подключает каталог `data/` с хоста к Jupyter как `/opt/data/`.\n"
] ]
}, },
{ {
@@ -63,10 +356,6 @@
"RAW_URI = f\"s3a://{BUCKET_NAME}/{RAW_PREFIX}\"\n", "RAW_URI = f\"s3a://{BUCKET_NAME}/{RAW_PREFIX}\"\n",
"ZONE_LOOKUP_RAW_URI = f\"{RAW_URI}taxi_zone_lookup.csv\"\n", "ZONE_LOOKUP_RAW_URI = f\"{RAW_URI}taxi_zone_lookup.csv\"\n",
"\n", "\n",
"spark = SparkSession.builder .appName(\"module-03-raw-ingest-and-first-read\") .getOrCreate()\n",
"\n",
"spark.sparkContext.setLogLevel(\"ERROR\")\n",
"\n",
"s3 = boto3.client(\n", "s3 = boto3.client(\n",
" \"s3\",\n", " \"s3\",\n",
" endpoint_url=\"http://minio:9000\",\n", " endpoint_url=\"http://minio:9000\",\n",
@@ -127,7 +416,7 @@
"\n", "\n",
"\n", "\n",
"print(\"Spark и MinIO-клиент готовы\")\n", "print(\"Spark и MinIO-клиент готовы\")\n",
"print(f\"RAW URI: {RAW_URI}\")\n" "print(f\"RAW URI: {RAW_URI}\")"
] ]
}, },
{ {
@@ -135,15 +424,7 @@
"id": "d4d8f302", "id": "d4d8f302",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 1. Проверка data bundle\n", "Файлы должны быть скачаны заранее по `START_HERE.md`. Здесь проверим состав набора, прежде чем отправлять его в MinIO. Так во время урока нам не понадобится внешняя сеть, а при повторном прохождении мы будем читать те же исходные файлы.\n"
"\n",
"Сначала убеждаемся, что нужные файлы уже лежат внутри Jupyter-контейнера по пути `/opt/data/nyc_taxi`.\n",
"\n",
"Почему не скачиваем данные прямо здесь:\n",
"\n",
"- так модуль остаётся воспроизводимым и не зависит от сетевого доступа внутри ноутбука;\n",
"- onboarding и подготовка окружения остаются в `START_HERE.md`, а практика в ноутбуке занимается только Lakehouse-операциями;\n",
"- студент видит границу ответственности: сначала подготовить bundle, потом работать с raw-слоем.\n"
] ]
}, },
{ {
@@ -198,7 +479,7 @@
"id": "dabd90f2", "id": "dabd90f2",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 2. Загрузка в raw-зону MinIO\n", "## 4. Загрузка в raw-зону MinIO\n",
"\n", "\n",
"Raw-слой в Lakehouse похож на воспроизводимый входной `stg`-контур: мы сохраняем исходные файлы как есть, под оригинальными именами, не переписывая их содержимое.\n", "Raw-слой в Lakehouse похож на воспроизводимый входной `stg`-контур: мы сохраняем исходные файлы как есть, под оригинальными именами, не переписывая их содержимое.\n",
"\n", "\n",
@@ -296,7 +577,7 @@
"id": "696d6adc", "id": "696d6adc",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 3. Первое чтение raw PARQUET через Spark\n", "## 5. Первое чтение raw PARQUET через Spark\n",
"\n", "\n",
"Теперь читаем raw-файлы напрямую из `MinIO` по `s3a://` пути. Это и есть первая важная практика Lakehouse: вычислитель (`Spark`) работает поверх файлов в object storage, а не только поверх локальной файловой системы или таблиц внутри одной СУБД.\n", "Теперь читаем raw-файлы напрямую из `MinIO` по `s3a://` пути. Это и есть первая важная практика Lakehouse: вычислитель (`Spark`) работает поверх файлов в object storage, а не только поверх локальной файловой системы или таблиц внутри одной СУБД.\n",
"\n", "\n",
@@ -305,7 +586,7 @@
"- `spark.read.parquet(\"s3a://...\")` читает raw-файлы напрямую по физическому пути;\n", "- `spark.read.parquet(\"s3a://...\")` читает raw-файлы напрямую по физическому пути;\n",
"- `spark.table(\"lakehouse....\")` читает уже зарегистрированную управляемую таблицу через каталог.\n", "- `spark.table(\"lakehouse....\")` читает уже зарегистрированную управляемую таблицу через каталог.\n",
"\n", "\n",
"Сейчас мы ещё не создавали `Iceberg`-таблицу, поэтому работаем именно через `spark.read.parquet(...)`. К `spark.table(...)` вернёмся в Модуле 4.\n" "Для проверки подключения мы читали маленькую таблицу по имени через `spark.table(...)`. Файлы Taxi пока не зарегистрированы в каталоге: прочитаем их по пути через `spark.read.parquet(...)`. Таблицу поездок создадим в модуле 4."
] ]
}, },
{ {
@@ -372,7 +653,7 @@
"id": "9701728a", "id": "9701728a",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 4. Схема, типы и null-значения\n", "## 6. Схема, типы и null-значения\n",
"\n", "\n",
"Следующий шаг после первого чтения: понять, какие колонки пришли, какие у них типы и где уже на raw-слое встречаются `null`.\n", "Следующий шаг после первого чтения: понять, какие колонки пришли, какие у них типы и где уже на raw-слое встречаются `null`.\n",
"\n", "\n",
@@ -435,7 +716,7 @@
"id": "69e6b77e", "id": "69e6b77e",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 5. Базовые метрики и аномалии\n", "## 7. Базовые метрики и аномалии\n",
"\n", "\n",
"Теперь быстро профилируем данные и ищем несколько очевидных проблемных паттернов:\n", "Теперь быстро профилируем данные и ищем несколько очевидных проблемных паттернов:\n",
"\n", "\n",
@@ -554,7 +835,7 @@
"id": "095c9ac6", "id": "095c9ac6",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 6. Самостоятельное задание\n", "## 8. Самостоятельное задание\n",
"\n", "\n",
"Используй raw-данные, которые уже лежат в `MinIO`:\n", "Используй raw-данные, которые уже лежат в `MinIO`:\n",
"\n", "\n",
@@ -612,7 +893,7 @@
"id": "e23dfe47", "id": "e23dfe47",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 7. Почему raw-слой важен\n", "## 9. Почему raw-слой важен\n",
"\n", "\n",
"Raw-слой полезен не потому, что в нём удобно жить постоянно, а потому что он даёт устойчивую исходную точку. Если завтра поменяются правила очистки, дедупликации или нормализации, мы не обязаны заново скачивать источник и гадать, что именно уже успели «подправить». Мы просто перечитываем raw и перестраиваем downstream-слои.\n", "Raw-слой полезен не потому, что в нём удобно жить постоянно, а потому что он даёт устойчивую исходную точку. Если завтра поменяются правила очистки, дедупликации или нормализации, мы не обязаны заново скачивать источник и гадать, что именно уже успели «подправить». Мы просто перечитываем raw и перестраиваем downstream-слои.\n",
"\n", "\n",
@@ -628,16 +909,17 @@
"id": "8849c73f", "id": "8849c73f",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 8. Checkpoint\n", "## 10. Checkpoint\n",
"\n", "\n",
"Проверь себя:\n", "Проверь себя:\n",
"\n", "\n",
"1. Можешь ли ты показать файлы raw-зоны в `MinIO` через код или `MinIO Console`?\n", "1. К какому master подключена сессия? Какие настройки ты задал в коде, а какие она получила из общего файла?\n",
"2. Сколько колонок в схеме `Yellow Taxi` и какие типы там встречаются?\n", "2. Чем SparkSession отличается от SparkContext? Где работают driver и executors?\n",
"3. Какие колонки содержат `null` и почему это не обязательно баг загрузки?\n", "3. Почему часовой пояс изменился сразу, а для нового имени приложения понадобилось остановить сессию? Что придется повторить после перезапуска ядра?\n",
"4. Зачем нужен raw-слой, если дальше всё равно будут трансформации?\n", "4. Можешь ли ты показать загруженные raw-файлы в MinIO и прочитать их через Spark?\n",
"5. Почему мы не скачиваем датасет прямо из ноутбука?\n", "5. Сколько колонок в Yellow Taxi, какие у них типы и где встречаются `null`? Почему пропуски не обязательно означают ошибку загрузки?\n",
"6. Что произойдёт, если источник завтра поменяет формат файлов?\n" "6. Зачем сохранять raw-слой без изменений, если дальше будут трансформации?\n",
"7. Почему мы скачали датасет до запуска ноутбука? Что потребуется проверить, если источник изменит формат файлов?\n"
] ]
}, },
{ {
@@ -645,7 +927,7 @@
"id": "371c4c9d", "id": "371c4c9d",
"metadata": {}, "metadata": {},
"source": [ "source": [
"## 9. Завершение\n", "## 11. Завершение\n",
"\n", "\n",
"Мы намеренно не удаляем raw-данные из `MinIO`. В Модуле 4 они понадобятся для построения первого управляемого `bronze`-слоя на `Iceberg`.\n" "Мы намеренно не удаляем raw-данные из `MinIO`. В Модуле 4 они понадобятся для построения первого управляемого `bronze`-слоя на `Iceberg`.\n"
] ]
+10 -6
View File
@@ -14,11 +14,13 @@
"|---|---|---|\n", "|---|---|---|\n",
"| Добавить колонку | `ALTER TABLE ADD COLUMN` (мгновенно, NULL) | `ALTER TABLE ADD COLUMNS` (мгновенно, NULL) |\n", "| Добавить колонку | `ALTER TABLE ADD COLUMN` (мгновенно, NULL) | `ALTER TABLE ADD COLUMNS` (мгновенно, NULL) |\n",
"| Переименовать колонку | `ALTER TABLE RENAME COLUMN` | `ALTER TABLE RENAME COLUMN` |\n", "| Переименовать колонку | `ALTER TABLE RENAME COLUMN` | `ALTER TABLE RENAME COLUMN` |\n",
"| Откатить данные к вчерашнему состоянию | Восстановление из бэкапа (pg_dump / PITR) | `SELECT ... VERSION AS OF <snapshot_id>` |\n", "| Получить данные на момент времени в прошлом | Чтение из восстановленного бэкапа (pg_dump / PITR) | `SELECT * FROM <table> TIMESTAMP AS OF '<timestamp>'` |\n",
"| Посмотреть историю изменений таблицы | Нет встроенного механизма | `SELECT * FROM table.snapshots` |\n", "| Посмотреть историю изменений таблицы | Нет встроенного механизма | `SELECT * FROM table.snapshots` |\n",
"| Последствия ошибочной записи | Нужен бэкап или ручной откат | Rollback к предыдущему snapshot |\n", "| Восстановить таблицу после ошибочной записи | Нужен бэкап или ручной откат | `CALL lakehouse.system.rollback_to_snapshot(table => '<namespace>.<table>', snapshot_id => <snapshot_id>);` |\n",
"\n", "\n",
"**Ключевая идея:** Iceberg хранит полную историю изменений данных. Schema evolution (изменение структуры) и time travel (путешествие во времени) — встроенные инструменты формата, а не дополнительная инфраструктура.\n", "Time travel читает прошлое состояние, не меняя текущую таблицу. Rollback переключает текущее состояние таблицы на выбранный snapshot-предок.\n",
"\n",
"**Ключевая идея:** Iceberg хранит историю изменений данных, пока snapshot-ы не удалены при обслуживании. Schema evolution (изменение структуры) и time travel (путешествие во времени) — встроенные инструменты формата.\n",
"\n", "\n",
"Как устроен этот ноутбук:\n", "Как устроен этот ноутбук:\n",
"- Schema evolution демонстрируем на нашей рабочей таблице `silver` (это безопасная операция).\n", "- Schema evolution демонстрируем на нашей рабочей таблице `silver` (это безопасная операция).\n",
@@ -165,7 +167,9 @@
"\n", "\n",
"Добавляем колонку `loaded_at` к silver-таблице. В PostgreSQL `ALTER TABLE ADD COLUMN` — обычная операция. В Iceberg — аналогично, но с важным свойством: Iceberg хранит историю схем.\n", "Добавляем колонку `loaded_at` к silver-таблице. В PostgreSQL `ALTER TABLE ADD COLUMN` — обычная операция. В Iceberg — аналогично, но с важным свойством: Iceberg хранит историю схем.\n",
"\n", "\n",
"Важно: `ALTER TABLE ADD COLUMNS` **НЕ создаёт** новый data snapshot. Это изменение метаданных (schema), а не данных: data files не перезаписываются, существующие строки логически получают NULL. Snapshot-ы фиксируют только операции с данными (INSERT, DELETE, overwrite).\n" "`ALTER TABLE ADD COLUMNS` **не создаёт новый snapshot данных**: data files не перезаписываются, при чтении существующих строк новая колонка возвращает NULL.\n",
"\n",
"Изменение схемы записывается в новый файл `*.metadata.json` в MinIO. В списке `schemas` появляется новая версия схемы со своим `schema-id`, а `current-schema-id` указывает на неё как на текущую. JDBC-каталог в PostgreSQL переключает `metadata_location` на новый файл. Так схема получает новую версию, хотя текущий snapshot данных остаётся прежним.\n"
] ]
}, },
{ {
@@ -249,7 +253,7 @@
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "metadata": {},
"source": [ "source": [
"Trino увидел новую колонку мгновенно. Spark изменил метаданные в PostgreSQL (JDBC catalog), Trino прочитал обновлённые метаданные. Decoupled compute + общий каталог в действии.\n" "Trino увидел новую колонку. Spark записал новый metadata-файл в MinIO и обновил указатель на него в PostgreSQL (JDBC catalog). Trino прочитал схему из нового файла. Decoupled compute + общий каталог в действии.\n"
] ]
}, },
{ {
@@ -443,7 +447,7 @@
"source": [ "source": [
"#### 7c: Time travel по времени\n", "#### 7c: Time travel по времени\n",
"\n", "\n",
"Альтернативный способ — по timestamp. `TIMESTAMP AS OF` — Iceberg находит ближайший snapshot, существовавший на этот момент времени. Для точного отката лучше использовать `snapshot_id`.\n" "Альтернативный способ — по timestamp. `TIMESTAMP AS OF` читает snapshot, который был текущим в указанный момент времени. Чтобы прочитать конкретный snapshot по его ID, используй `VERSION AS OF`. Оба запроса оставляют текущее состояние таблицы без изменений.\n"
] ]
}, },
{ {
+38 -13
View File
@@ -315,9 +315,15 @@
"source": [ "source": [
"### Секция 4: expire_snapshots — очистка истории\n", "### Секция 4: expire_snapshots — очистка истории\n",
"\n", "\n",
"`expire_snapshots` удаляет старые snapshot-ы из метаданных. \n", "`expire_snapshots` удаляет старые snapshot-ы из метаданных и файлы, которые больше не нужны ни одному сохранённому snapshot-у. Очистка выполняется при вызове процедуры.\n",
"**Важный компромисс:** после expire_snapshots time travel к удалённым snapshot-ам невозможен. Это необратимо. \n", "\n",
"В production нужно решить: сколько истории хранить? Обычно 10–30 дней. Для демо оставим только 2 последних snapshot-а.\n" "Два параметра определяют, какие snapshot-ы будут удалены:\n",
"- `older_than` — граница времени: удалить можно snapshot-ы, созданные раньше неё. Без явной границы при стандартных настройках удаляются только snapshot-ы старше 5 дней.\n",
"- `retain_last` — сколько последних snapshot-ов в цепочке предков текущего состояния сохранить независимо от возраста. Это минимум, а не требование оставить ровно столько.\n",
"\n",
"Поэтому один `retain_last => 2` не удалит snapshot-ы, которые мы только что создали. Для демо возьмём текущее время Spark-сессии как `older_than` и сохраним два последних snapshot-а.\n",
"\n",
"**Важный компромисс:** time travel к удалённым snapshot-ам станет невозможен. Текущее время как границу используем только для одноразовой демо-таблицы; в production срок хранения выбирают с учётом требований к истории и восстановлению.\n"
] ]
}, },
{ {
@@ -328,12 +334,14 @@
"source": [ "source": [
"# Snapshot-история ДО expire\n", "# Snapshot-история ДО expire\n",
"snapshots_before = spark.sql(f\"SELECT snapshot_id, committed_at, operation FROM {DEMO_TABLE}.snapshots\")\n", "snapshots_before = spark.sql(f\"SELECT snapshot_id, committed_at, operation FROM {DEMO_TABLE}.snapshots\")\n",
"print(f\"Snapshot-ов до expire: {snapshots_before.count()}\")\n", "snapshot_count_before = snapshots_before.count()\n",
"print(f\"Snapshot-ов до expire: {snapshot_count_before}\")\n",
"snapshots_before.show(truncate=False)\n", "snapshots_before.show(truncate=False)\n",
"\n", "\n",
"# Сохраняем ID самого старого snapshot для проверки time travel после expire\n", "# Сохраняем ID самого старого snapshot для проверки time travel после expire\n",
"first_snapshot_id = snapshots_before.orderBy(\"committed_at\").first()[\"snapshot_id\"]\n", "first_snapshot_id = snapshots_before.orderBy(\"committed_at\").first()[\"snapshot_id\"]\n",
"print(f\"Самый старый snapshot: {first_snapshot_id}\")\n", "print(f\"Самый старый snapshot: {first_snapshot_id}\")\n",
"row_count_before_expire = spark.table(DEMO_TABLE).count()\n",
"\n" "\n"
] ]
}, },
@@ -343,10 +351,16 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Выполняем expire_snapshots (оставляем только 2 последних)\n", "# Получаем границу времени в часовом поясе Spark-сессии\n",
"cutoff = spark.sql(\"\"\"\n",
" SELECT date_format(current_timestamp(), 'yyyy-MM-dd HH:mm:ss.SSSSSS') AS cutoff\n",
"\"\"\").first()[\"cutoff\"]\n",
"\n",
"# Удаляем snapshot-ы до cutoff, сохраняя два последних\n",
"spark.sql(f\"\"\"\n", "spark.sql(f\"\"\"\n",
" CALL lakehouse.system.expire_snapshots(\n", " CALL lakehouse.system.expire_snapshots(\n",
" table => 'default.taxi_maintenance_demo',\n", " table => 'default.taxi_maintenance_demo',\n",
" older_than => TIMESTAMP '{cutoff}',\n",
" retain_last => 2\n", " retain_last => 2\n",
" )\n", " )\n",
"\"\"\")\n", "\"\"\")\n",
@@ -362,8 +376,16 @@
"source": [ "source": [
"# Snapshot-история ПОСЛЕ expire\n", "# Snapshot-история ПОСЛЕ expire\n",
"snapshots_after = spark.sql(f\"SELECT snapshot_id, committed_at, operation FROM {DEMO_TABLE}.snapshots\")\n", "snapshots_after = spark.sql(f\"SELECT snapshot_id, committed_at, operation FROM {DEMO_TABLE}.snapshots\")\n",
"print(f\"Snapshot-ов после expire: {snapshots_after.count()}\")\n", "remaining_snapshot_ids = [row[\"snapshot_id\"] for row in snapshots_after.collect()]\n",
"print(f\"Snapshot-ов: {snapshot_count_before} → {len(remaining_snapshot_ids)}\")\n",
"snapshots_after.show(truncate=False)\n", "snapshots_after.show(truncate=False)\n",
"assert len(remaining_snapshot_ids) == 2, \"Ожидалось ровно 2 snapshot-а после очистки.\"\n",
"assert first_snapshot_id not in remaining_snapshot_ids, \"Самый старый snapshot не удалён.\"\n",
"\n",
"# Очистка истории не меняет данные в текущем состоянии\n",
"row_count_after_expire = spark.table(DEMO_TABLE).count()\n",
"assert row_count_after_expire == row_count_before_expire, \"Число строк изменилось после очистки.\"\n",
"print(f\"Данные на месте: {row_count_after_expire} строк.\")\n",
"\n" "\n"
] ]
}, },
@@ -385,10 +407,11 @@
" SELECT count(*) FROM {DEMO_TABLE}\n", " SELECT count(*) FROM {DEMO_TABLE}\n",
" VERSION AS OF {first_snapshot_id}\n", " VERSION AS OF {first_snapshot_id}\n",
" \"\"\").show()\n", " \"\"\").show()\n",
" print(\"Time travel сработал (snapshot не был удалён).\")\n",
"except Exception as e:\n", "except Exception as e:\n",
" print(f\"Ожидаемая ошибка: {e}\")\n", " print(f\"Ожидаемая ошибка: {e}\")\n",
" print(\"Snapshot удалён — time travel невозможен.\")\n", " print(\"Snapshot удалён — time travel невозможен.\")\n",
"else:\n",
" raise AssertionError(\"Time travel сработал: старый snapshot не был удалён.\")\n",
"\n" "\n"
] ]
}, },
@@ -398,7 +421,9 @@
"source": [ "source": [
"**Вывод:** expire нужно запускать обдуманно. Слишком агрессивно (потеря страховки для rollback), слишком редко (рост метаданных и стоимости хранения).\n", "**Вывод:** expire нужно запускать обдуманно. Слишком агрессивно (потеря страховки для rollback), слишком редко (рост метаданных и стоимости хранения).\n",
"\n", "\n",
"*Примечание: физическое удаление файлов из MinIO зависит от конфигурации. Иногда требуется дополнительно запускать процедуру `remove_orphan_files`, которая физически чистит файлы без привязки к активным snapshot-ам.*\n" "В нашем демо сохранены snapshot после compaction и его предок до compaction. Предку ещё нужны старые мелкие файлы, поэтому `expire_snapshots` их не удаляет. Удаление snapshot-ов не обязательно освобождает data files: важны ссылки из оставшихся состояний.\n",
"\n",
"`remove_orphan_files` решает другую задачу: удаляет файлы, на которые вообще нет ссылок в метаданных таблицы, например остатки неудачной записи. Для файлов, нужных сохранённым snapshot-ам, она не подходит.\n"
] ]
}, },
{ {
@@ -414,11 +439,11 @@
"| 2 | `expire_snapshots` | Удаляет старые snapshot-ы | Метаданные и хранилище растут |\n", "| 2 | `expire_snapshots` | Удаляет старые snapshot-ы | Метаданные и хранилище растут |\n",
"\n", "\n",
"\n", "\n",
"Если сделать наоборот: expire удалит snapshot-ы, но мелкие файлы останутся (так как на них всё ещё ссылается текущий snapshot). Compaction затем создаст новые крупные файлы, а старые мелкие станут \"orphan\" (сиротами) и не удалятся автоматически.\n", "Если выполнить expire до compaction, мелкие файлы останутся: на них ссылается текущий snapshot. После compaction они будут нужны его предку. Удалить их сможет следующий вызов `expire_snapshots`, когда этот предок попадёт под условия очистки. До этого файлы не являются «сиротами».\n",
"\n", "\n",
"**Типичные ошибки:**\n", "**Типичные ошибки:**\n",
"1. **Никогда не запускать compaction:** таблица обрастает тысячами мелких файлов, чтение деградирует в десятки раз.\n", "1. **Никогда не запускать compaction:** таблица обрастает тысячами мелких файлов, чтение деградирует в десятки раз.\n",
"2. **`expire_snapshots` сразу после ошибки (с `retain_last => 1`):** если записали плохие данные и запустили expire, откатиться назад (rollback) уже не выйдет. Правило: перед expire убедись, что текущее состояние корректно.\n", "2. **Удалить исправный snapshot после ошибочной записи:** если `older_than` и `retain_last` допускают его удаление, после expire откатиться к нему уже не выйдет. Правило: перед expire убедись, что текущее состояние корректно.\n",
"3. **Обслуживание на каждый INSERT:** compaction — тяжёлая операция. В production его делают по расписанию (например, 1 раз в сутки ночью).\n" "3. **Обслуживание на каждый INSERT:** compaction — тяжёлая операция. В production его делают по расписанию (например, 1 раз в сутки ночью).\n"
] ]
}, },
@@ -455,8 +480,8 @@
"- Создай таблицу `lakehouse.default.taxi_compaction_practice` через CTAS (500 строк из `silver`). \n", "- Создай таблицу `lakehouse.default.taxi_compaction_practice` через CTAS (500 строк из `silver`). \n",
"- Выполни 5 INSERT INTO по 200 строк. \n", "- Выполни 5 INSERT INTO по 200 строк. \n",
"- Посмотри файловую статистику ДО.\n", "- Посмотри файловую статистику ДО.\n",
"- Выполни compaction и expire_snapshots (retain_last => 1). \n", "- Выполни compaction, затем получи свежий `cutoff` по образцу Секции 4 и вызови `expire_snapshots` с `older_than => TIMESTAMP '{cutoff}'` и `retain_last => 1`.\n",
"- Проверь результат ПОСЛЕ. \n", "- Проверь результат ПОСЛЕ: остался 1 snapshot, число строк не изменилось.\n",
"- Удали таблицу (`DROP TABLE`).\n" "- Удали таблицу (`DROP TABLE`).\n"
] ]
}, },
@@ -565,7 +590,7 @@
"\n", "\n",
"**Шаг 6 (snapshot-ы).** Выполни 3 `INSERT INTO trips_silver` (например, по 300 строк из `trips_bronze`). Каждый INSERT должен включать трансформации из шага 3, плюс `current_timestamp() AS processed_at`. *Подсказка: если не указать `processed_at`, Spark выдаст ошибку несовпадения числа колонок; альтернативный вариант — использовать явный список target-колонок в `INSERT INTO trips_silver (col1, col2, ...)`.* Посмотри файловую статистику и snapshot-ы.\n", "**Шаг 6 (snapshot-ы).** Выполни 3 `INSERT INTO trips_silver` (например, по 300 строк из `trips_bronze`). Каждый INSERT должен включать трансформации из шага 3, плюс `current_timestamp() AS processed_at`. *Подсказка: если не указать `processed_at`, Spark выдаст ошибку несовпадения числа колонок; альтернативный вариант — использовать явный список target-колонок в `INSERT INTO trips_silver (col1, col2, ...)`.* Посмотри файловую статистику и snapshot-ы.\n",
"\n", "\n",
"**Шаг 7 (обслуживание).** Выполни compaction (`rewrite_data_files`), затем `expire_snapshots` (retain_last => 1). Проверь, что файлов стало меньше, а snapshot-ов остался 1.\n", "**Шаг 7 (обслуживание).** Выполни compaction (`rewrite_data_files`). После него получи свежий `cutoff` по образцу Секции 4 и вызови `expire_snapshots` с `older_than => TIMESTAMP '{cutoff}'` и `retain_last => 1`. Проверь, что файлов стало меньше, snapshot-ов остался 1, а число строк не изменилось.\n",
"\n", "\n",
"**Шаг 8 (cleanup).** Удали `trips_silver`, `trips_bronze` и namespace `lakehouse.final`.\n", "**Шаг 8 (cleanup).** Удали `trips_silver`, `trips_bronze` и namespace `lakehouse.final`.\n",
"\n" "\n"
+5 -17
View File
@@ -1,6 +1,6 @@
# План развития курса «Lakehouse без магии» # План развития курса «Lakehouse без магии»
Статус: подключение Spark исправлено; следующая работа — практика настройки сессии. Статус: ближайшие доработки завершены; ожидается обратная связь по оставшимся модулям.
Последнее обновление: 2026-09-15. Последнее обновление: 2026-09-15.
Здесь собираем отзывы студентов, выбираем следующие улучшения и объясняем их порядок. Подробные задания, критерии приёмки и актуальный статус исполнения хранятся в [Gitea Issues](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues). Здесь собираем отзывы студентов, выбираем следующие улучшения и объясняем их порядок. Подробные задания, критерии приёмки и актуальный статус исполнения хранятся в [Gitea Issues](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues).
@@ -36,26 +36,13 @@ Iceberg-таблицы. Результаты проверок и их огран
### 2. Добавить практику «Как настроен Spark в нашем стенде» ### 2. Добавить практику «Как настроен Spark в нашем стенде»
**Статус:** запланировано; можно приступать после завершения #1. **Статус:** выполнено, [MR #4](https://git.dementev.space/ddmitry/mini-lakehouse-lab/pulls/4).
**Задача:** [#2 — практика базовой настройки SparkSession](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/2). **Задача:** [#2 — практика базовой настройки SparkSession](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/2).
Практику разместить в начале модуля 3. Ориентир — 20–30 минут; фактическую длительность проверить при прохождении. В модуле 3 студент готовит Spark-сессию, проверяет подключение и затем переходит к загрузке и чтению Taxi. Содержание и checkpoint описаны в [программе](../docs/course_program.md#модуль-3-raw-данные-и-первое-чтение-в-spark).
Студент должен научиться: Код проверен на стенде, включая перезапуск ядра и полный raw-ingest. Замеры и ограничения проверки сохранены в [отчете](issue-2-spark-session-validation.md).
- объяснять роли `SparkSession`, `SparkContext`, driver и executors на примере стенда;
- читать код с `master`, `appName` и `getOrCreate`;
- находить источник настройки: конфигурационный файл, параметры запуска или код;
- проверять фактический master и выбранные настройки каталога Iceberg и MinIO;
- различать настройки, меняемые во время работы, и настройки, требующие нового запуска;
- понимать, что повторное выполнение ячейки может использовать уже существующую сессию.
**Практика и checkpoint:** студент заполняет небольшой шаблон создания сессии, задаёт подключение к кластеру и имя приложения, использует подготовленные настройки каталога, проверяет фактическое подключение и читает небольшую учебную Iceberg-таблицу. Таблица должна создаваться самой практикой: демонстрационные таблицы модуля 2 могут быть удалены при его завершении. Студент объясняет, какие настройки он задал сам, какие предоставил стенд и что потребуется повторить в новом ядре.
При реализации обновить learning outcomes и scope в PRD, содержание и checkpoint модуля 3 в программе, ноутбук и связанные справочные материалы. Сохранить структуру из восьми модулей.
**Граница темы:** базовая настройка подключения и жизненный цикл сессии. Подробный подбор памяти, числа executors, shuffle и GC остаётся за рамками этой доработки.
### 3. Собрать замечания по оставшейся части курса ### 3. Собрать замечания по оставшейся части курса
@@ -81,3 +68,4 @@ Iceberg-таблицы. Результаты проверок и их огран
## Выпущенные улучшения ## Выпущенные улучшения
- [#1](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/1): все восемь ноутбуков подключаются к Spark-кластеру при создании сессии в новом процессе; настройки собраны в `spark/spark-defaults.conf`. - [#1](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/1): все восемь ноутбуков подключаются к Spark-кластеру при создании сессии в новом процессе; настройки собраны в `spark/spark-defaults.conf`.
- [#2](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/2): модуль 3 включает самостоятельное подключение к Spark перед загрузкой и чтением Taxi.
+95
View File
@@ -0,0 +1,95 @@
# Проверка практики SparkSession в модуле 3
Дата: 2026-09-15.
Задача: https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/2.
База: `00942c2`, результат закрытой [#1](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/1) прочитан вместе с комментариями.
## Что проверялось
Фактические ячейки `notebooks/03_raw_ingest_and_first_read.ipynb` выполнялись
через `jupyter_client.KernelManager` в контейнере `jupyter`, Spark 3.5.1.
Менеджер запускал настоящее IPython-ядро и перезапускал его через
`restart_kernel(now=True)`. Проверка не подменяла создание сессии общим helper-ом.
Порядок воспроизведения:
1. Поднять стенд и получить стандартный bundle по `START_HERE.md`.
2. В новом ядре выполнить демонстрацию, вывод настроек, опыт с часовым поясом,
повторный builder и остановку сессии.
3. Заполнить три пропуска шаблона, выполнить его дважды и сравнить контекст
и applicationId. Выполнить опубликованное контрольное решение
(при проверке извлекалось из блока `session-solution`).
4. Выполнить проверку master/appName и дважды создать, прочитать и удалить
временную таблицу.
5. Перезапустить ядро. Убедиться, что переменные `spark` и `CLUSTER_MASTER`
отсутствуют; выполнить контрольное решение, проверку подключения и таблицы.
6. Продолжить исходный raw-ingest до `spark.stop()`, заполнив самостоятельные
упражнения с lookup, top-5 локаций и ещё одной аномалией.
7. Отдельно проверить заполненный шаблон в новом ядре и повторную загрузку
уже существующих raw-файлов.
## Результаты
| Проверка | Результат |
| --- | --- |
| Демонстрационная сессия | Master `spark://spark-master:7077`, appName `module-03-session-demo`, сумма 0…9 = 45 |
| Настройки каталога | `SparkCatalog`, `JdbcCatalog`, URI PostgreSQL и warehouse соответствуют общему файлу; Hadoop endpoint `http://minio:9000`, path-style `true` |
| Изменяемая настройка | `spark.sql.session.timeZone` допускает изменение; Unix-время 0 отображается как `1970-01-01 00:00:00` в UTC и `1970-01-01 03:00:00` в Москве |
| Восстановление | В `finally` восстановлено исходное `Etc/UTC`, проверено через assert |
| Повторный builder с другим appName | Тот же объект сессии и контекста, фактическое имя остаётся `module-03-session-demo` |
| Создание после stop | Новое имя `module-03-raw-ingest-and-first-read`, master кластерный |
| Повторение шаблона и контрольного решения | Контекст и applicationId не меняются |
| Заполненный шаблон в отдельном новом ядре | Master и appName верны, распределённая сумма 0…9 = 45 |
| Перезапуск ядра | Переменные исчезли, сессия создана заново; ID изменился с `app-20260915125347-0001` на `app-20260915125511-0002` |
| Iceberg до и после перезапуска | Три независимые таблицы с уникальными именами; каждый раз прочитаны `[0, 1]`, после PURGE таблица отсутствует в каталоге |
| Raw-ingest | Январь: 2 964 624 строки; январь–март: 9 554 778 строк, 19 колонок |
| Профилирование | Схема, null-доли, describe и четыре проверки аномалий выполнены без ошибок |
| Самостоятельное задание raw | Lookup: 265 строк; подсчёт зон по Borough и top-5 pickup с названиями зон выполнены; найдено 180 поездок с отрицательной длительностью |
| Повторная загрузка raw в отдельном ядре | Все четыре файла пропущены с `skipped_same_size`; снимки всех объектов и каталога до/после совпали |
Код исходного raw-ingest после ячейки подготовки не изменён, что проверено
сравнением ячеек с базовым коммитом. Из подготовки убрано только создание сессии:
она теперь приходит из новой практики.
## Время и ограничения
- Машинный прогон блока настройки: **179,27 с**. Сюда вошли дополнительные
повторы шаблона и таблицы, а также настоящий перезапуск ядра.
- Исходная часть модуля, включая заполненные самостоятельные упражнения:
**70,41 с**. Полный основной прогон: **250,02 с** (около 4 минут 10 секунд).
- Запуск контейнеров и скачивание bundle в замер не входят. Это время выполнения
кода; чтение объяснений, самостоятельное заполнение и ответы студента не измерялись.
- Ориентир новой практики **20–30 минут**, модуля целиком **около 2 часов**.
Общую оценку **12–15 часов** оставили предварительной; машинного замера
недостаточно для её уточнения. Нужен самостоятельный проход студента.
- Кнопки Jupyter/VS Code, отображение раскрывающегося решения и UI сервисов
не проверялись. Перезапуск выполнен менеджером ядра через API, а не кнопкой.
- Остальные семь ноутбуков и Trino smoke не выполнялись: runtime-конфигурация
не менялась, проверялся модуль 3.
## Сохранность данных
До основного прогона в каталоге было **0 таблиц**, в bucket — **4 объекта**.
После прогона исходные объекты сохранили ETag и размер, состав каталога
не изменился. Это проверка на имевшемся состоянии стенда, а не на копии
заполненного студентом курса.
Добавлены четыре исходных файла Taxi в `raw/nyc_taxi/`, как предусмотрено уроком;
они оставлены для модуля 4. Локальный bundle сохранён в игнорируемом `data/`.
Файлы данных и метаданных трёх временных таблиц удалены через PURGE.
Hadoop S3A оставил шесть нулевых маркеров `data/` и `metadata/`;
при завершении проверки удалены только эти шесть зафиксированных объектов.
В трёх префиксах практики осталось 0 объектов. Volumes не сбрасывались.
Поднятый для проверки стенд остановлен через `docker compose down` без `-v`.
## Статические проверки
- `git diff --check`;
- `docker compose config --quiet`;
- проверка ноутбука через `nbformat.validate` и синтаксиса Python-ячеек через AST;
- относительные ссылки изменённых документов и ноутбука;
- сохранение восьми модулей и маршрута README → START_HERE → программа → ноутбук.
После редакторской правки по ревью повторно проверены ссылки, нумерация разделов
и синтаксис Python. Все исполняемые ячейки, их порядок и контрольное решение
совпадают с проверенной версией. Стенд повторно не запускался.
+44 -24
View File
@@ -1,16 +1,22 @@
# Модуль 3. Raw-данные и первое чтение в Spark # Модуль 3. Raw-данные и первое чтение в Spark
**Статус:** `Ready for validation` **Статус:** `Done`
**Последнее обновление:** `2026-03-07` **Последнее обновление:** `2026-09-15`
**Задача:** [#2](https://git.dementev.space/ddmitry/mini-lakehouse-lab/issues/2).
Результаты прогона и ограничения: [проверка #2](issue-2-spark-session-validation.md).
## Цель ## Цель
Научить студента полному циклу работы с raw-данными в Lakehouse: доставка заранее скачанного PARQUET-датасета в raw-зону MinIO, чтение raw-данных через Spark, проверка схемы, типов и базового качества. Студент должен понять, зачем нужен raw-слой и почему raw-данные не следует «чинить на месте». Научить студента подключаться к Spark-кластеру и работать с raw-данными: загружать заранее скачанный PARQUET-датасет в MinIO, читать его через Spark и проверять схему, типы и базовое качество. Студент должен понять, зачем нужен raw-слой и почему raw-данные не следует «чинить на месте».
## Результат для студента ## Результат для студента
После прохождения модуля студент: После прохождения модуля студент:
- создает сессию для кластера с подготовленным каталогом, проверяет настройки и понимает их источники;
- различает повторный вызов builder-а, остановку сессии и перезапуск ядра;
- понимает, как локальный data bundle попадает в raw-зону MinIO; - понимает, как локальный data bundle попадает в raw-зону MinIO;
- умеет загружать PARQUET-файлы в MinIO программно через `boto3`; - умеет загружать PARQUET-файлы в MinIO программно через `boto3`;
- умеет читать raw-данные из MinIO через Spark (`spark.read.parquet` по `s3a://` пути); - умеет читать raw-данные из MinIO через Spark (`spark.read.parquet` по `s3a://` пути);
@@ -81,61 +87,73 @@ volumes:
## Структура ноутбука ## Структура ноутбука
### Секция 0: Введение ### Секция 0: Перед стартом
- **[md]** Заголовок, цели модуля, prerequisite (data bundle из `START_HERE.md`). Таблица-сравнение: «Классический DWH: данные уже внутри СУБД» vs «Lakehouse: данные нужно явно доставить в storage». - **[md]** Связь с модулем 2, цели работы с Taxi и условия старта: работающий стенд, скачанный data bundle, новое ядро.
### Секция 1: Проверка data bundle ### Секция 1: Как настроен Spark в нашем стенде
- **[md]** SparkSession, SparkContext, driver и executors, local и кластер; источники настроек и подготовленный каталог.
- **[code]** Создание сессии, фактический master и несекретные настройки. Изменение часового пояса с восстановлением. Повторный builder сохраняет контекст; `stop()` позволяет создать приложение с новым именем.
### Секция 2: Подключись к Spark самостоятельно
- **[code]** Шаблон подключения и контрольное решение. Проверка master и appName, запись и чтение `[0, 1]` из собственной временной таблицы, удаление в `finally`.
- **[md]** После удаления таблицы перезапустить ядро, повторить заполненный шаблон и проверки. Оставить сессию для чтения Taxi.
### Секция 3: Проверка data bundle
- **[code]** Импорты, MinIO-клиент, пути и вспомогательные функции для работы с файлами.
- **[md]** Зачем проверяем; почему не скачиваем из интернета в ноутбуке (воспроизводимость, сетевые ограничения, разделение ответственности onboarding vs практика). - **[md]** Зачем проверяем; почему не скачиваем из интернета в ноутбуке (воспроизводимость, сетевые ограничения, разделение ответственности onboarding vs практика).
- **[code]** `os.listdir("/opt/data/nyc_taxi")`, вывод файлов с размерами, assert на наличие PARQUET-файлов и `taxi_zone_lookup.csv` — оба нужны для полного прохождения модуля (включая самостоятельное задание в Секции 6). Понятное сообщение с отсылкой к `START_HERE.md` при отсутствии любого из компонентов. - **[code]** `os.listdir("/opt/data/nyc_taxi")`, вывод файлов с размерами, assert на наличие PARQUET-файлов и `taxi_zone_lookup.csv` — оба нужны для полного прохождения модуля (включая самостоятельное задание в Секции 8). Понятное сообщение с отсылкой к `START_HERE.md` при отсутствии любого из компонентов.
### Секция 2: Загрузка в raw-зону MinIO ### Секция 4: Загрузка в raw-зону MinIO
- **[md]** Что такое raw-зона, параллель с stg-слоем в Greenplum, конвенция путей, почему raw не чинится «на месте». - **[md]** Что такое raw-зона, параллель с stg-слоем в Greenplum, конвенция путей, почему raw не чинится «на месте».
- **[code]** Демонстрация: перед загрузкой проверяем через `s3.head_object()`, существует ли уже объект с таким ключом. Три ветки: (1) объект не существует — загружаем через `boto3.upload_file()`; (2) объект существует и размер совпадает с локальным файлом — пропускаем с пометкой «already exists, skipping»; (3) объект существует, но размер отличается — **fail fast** с понятной ошибкой (raise), без перезаписи. Это обеспечивает идемпотентность и подкрепляет принцип «raw не перезаписывается». В markdown-пояснении перед ячейкой явно объяснить: повторный запуск безопасен, уже загруженные файлы не перетираются, а конфликт размеров сигнализирует о проблеме с данными. - **[code]** Демонстрация: перед загрузкой проверяем через `s3.head_object()`, существует ли уже объект с таким ключом. Три ветки: (1) объект не существует — загружаем через `boto3.upload_file()`; (2) объект существует и размер совпадает с локальным файлом — пропускаем с пометкой «already exists, skipping»; (3) объект существует, но размер отличается — **fail fast** с понятной ошибкой (raise), без перезаписи. Это обеспечивает идемпотентность и подкрепляет принцип «raw не перезаписывается». В markdown-пояснении перед ячейкой явно объяснить: повторный запуск безопасен, уже загруженные файлы не перетираются, а конфликт размеров сигнализирует о проблеме с данными.
- **[code]** Верификация: `s3.list_objects_v2()` — листинг raw-зоны. - **[code]** Верификация: `s3.list_objects_v2()` — листинг raw-зоны.
- **[md]** Данные теперь в storage. Spark может читать их напрямую по `s3a://` пути; для Trino потребуется зарегистрированная таблица (это будет в Модулях 4 и 6). - **[md]** Данные теперь в storage. Spark может читать их напрямую по `s3a://` пути; для Trino потребуется зарегистрированная таблица (это будет в Модулях 4 и 6).
### Секция 3: Первое чтение в Spark ### Секция 5: Первое чтение в Spark
- **[md]** Разница `spark.read.parquet("s3a://...")` (raw-файлы) vs `spark.table(...)` (управляемая Iceberg-таблица). Мы пока на этапе raw, Iceberg-таблица появится в Модуле 4. - **[md]** Разница `spark.read.parquet("s3a://...")` (raw-файлы) vs `spark.table(...)` (управляемая Iceberg-таблица). Проверочная таблица удалена; таблицу поездок создаем в Модуле 4.
- **[code]** Создание SparkSession (паттерн из Module 2 — адрес кластера из `spark-defaults.conf`, `setLogLevel("ERROR")`). - **[code]** Чтение через сессию, созданную в секции 2; повторная инициализация не нужна.
- **[code]** Чтение одного файла: `spark.read.parquet("s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_2024-01.parquet")`. - **[code]** Чтение одного файла: `spark.read.parquet("s3a://lakehouse/raw/nyc_taxi/yellow_tripdata_2024-01.parquet")`.
- **[code]** Чтение всех файлов wildcard: `yellow_tripdata_*.parquet` (без привязки к конкретному году в wildcard, чтобы код работал с любыми скачанными файлами). - **[code]** Чтение всех файлов wildcard: `yellow_tripdata_*.parquet` (без привязки к конкретному году в wildcard, чтобы код работал с любыми скачанными файлами).
### Секция 4: Проверка схемы ### Секция 6: Проверка схемы
- **[md]** Зачем проверять схему: в классическом DWH схема задана DDL, в raw — вычитывается из файлов. Что может пойти не так: разные типы в разных файлах, неожиданные nullable-поля. - **[md]** Зачем проверять схему: в классическом DWH схема задана DDL, в raw — вычитывается из файлов. Что может пойти не так: разные типы в разных файлах, неожиданные nullable-поля.
- **[code]** `.printSchema()`. - **[code]** `.printSchema()`.
- **[code]** Колонки и типы в удобном формате (`.dtypes`). - **[code]** Колонки и типы в удобном формате (`.dtypes`).
- **[code]** Null-анализ ключевых колонок (`VendorID`, `tpep_pickup_datetime`, `passenger_count`, `trip_distance`, `total_amount`) — количество и доля null в каждой. - **[code]** Null-анализ ключевых колонок (`VendorID`, `tpep_pickup_datetime`, `passenger_count`, `trip_distance`, `total_amount`) — количество и доля null в каждой.
- **[md]** Обсуждение: null в raw — норма, не исправляем на месте. Очистка — задача bronze/silver слоев (Модули 4-5). - **[md]** Обсуждение: null в raw — норма, не исправляем на месте. Очистка — задача bronze/silver слоев (Модули 4-5).
### Секция 5: Базовые метрики и аномалии ### Секция 7: Базовые метрики и аномалии
- **[md]** Цель: быстро понять диапазоны значений, выявить очевидные проблемы. - **[md]** Цель: быстро понять диапазоны значений, выявить очевидные проблемы.
- **[code]** `.describe()` на числовых колонках. - **[code]** `.describe()` на числовых колонках.
- **[code]** Поиск аномалий: отрицательные `fare_amount`, нулевая дистанция с ненулевой суммой, `total_amount > $1000`, pickup за пределами ожидаемого диапазона дат (определяется динамически из имён файлов, а не хардкодится). - **[code]** Поиск аномалий: отрицательные `fare_amount`, нулевая дистанция с ненулевой суммой, `total_amount > $1000`, pickup за пределами ожидаемого диапазона дат (определяется динамически из имён файлов, а не хардкодится).
- **[md]** Аномалии в реальных данных — норма, а не баг загрузки. Задача raw — сохранить как есть. - **[md]** Аномалии в реальных данных — норма, а не баг загрузки. Задача raw — сохранить как есть.
### Секция 6: Самостоятельное задание ### Секция 8: Самостоятельное задание
- **[md]** Инструкция: (1) прочитать `taxi_zone_lookup.csv` из raw-зоны через Spark, (2) вывести схему + 10 строк, (3) посчитать количество зон по borough, (4) найти топ-5 pickup locations и join с зонами, (5) найти ещё одну аномалию или интересный паттерн в данных. - **[md]** Инструкция: (1) прочитать `taxi_zone_lookup.csv` из raw-зоны через Spark, (2) вывести схему + 10 строк, (3) посчитать количество зон по borough, (4) найти топ-5 pickup locations и join с зонами, (5) найти ещё одну аномалию или интересный паттерн в данных.
- **[code]** 4 пустых ячейки с `# Ваш код: ...`. - **[code]** 4 пустых ячейки с `# Ваш код: ...`.
### Секция 7: Почему raw-слой важен ### Секция 9: Почему raw-слой важен
- **[md]** Мини-лекция (3-5 абзацев): raw = неизменяемый архив; параллель с stg/ods в Greenplum; почему не скачиваем в ноутбуке; почему не чиним на месте (можно перестроить bronze/silver заново); в Lakehouse raw-файлы лежат в storage отдельно от compute — Spark читает их напрямую, а для Trino потребуется оформить данные в таблицу. - **[md]** Мини-лекция (3-5 абзацев): raw = неизменяемый архив; параллель с stg/ods в Greenplum; почему не скачиваем в ноутбуке; почему не чиним на месте (можно перестроить bronze/silver заново); в Lakehouse raw-файлы лежат в storage отдельно от compute — Spark читает их напрямую, а для Trino потребуется оформить данные в таблицу.
### Секция 8: Checkpoint ### Секция 10: Checkpoint
- **[md]** Вопросы для самопроверки: (1) показать файлы в raw-зоне MinIO, (2) какая схема у Yellow Taxi — сколько колонок, (3) какие колонки содержат null — это баг или норма, (4) зачем raw-слой, если данные потом трансформируются, (5) почему не скачиваем из ноутбука, (6) что произойдёт, если NYC TLC изменит формат файла. - **[md]** Вопросы об источниках настроек, месте выполнения заданий и новом ядре; затем самопроверка по данным: (1) показать файлы в raw-зоне MinIO, (2) какая схема у Yellow Taxi — сколько колонок, (3) какие колонки содержат null — это баг или норма, (4) зачем raw-слой, если данные потом трансформируются, (5) почему не скачиваем из ноутбука, (6) что произойдёт, если NYC TLC изменит формат файла.
### Секция 9: Завершение ### Секция 11: Завершение
- **[md]** «Мы намеренно не удаляем raw-данные из MinIO. В Модуле 4 мы будем использовать их для создания bronze-слоя.» - **[md]** «Мы намеренно не удаляем raw-данные из MinIO. В Модуле 4 мы будем использовать их для создания bronze-слоя.»
- **[code]** `spark.stop()`. - **[code]** `spark.stop()`.
### Дизайн-решения по ноутбуку ### Дизайн-решения по ноутбуку
- **Helper-код:** весь код inline в ноутбуке. Отдельный модуль в `src/` не создаётся — студент должен видеть каждый шаг явно (`boto3.upload_file()`, `spark.read.parquet()`, `.printSchema()`). - **Helper-код:** весь код inline в ноутбуке. Отдельный модуль в `src/` не создаётся — студент должен видеть каждый шаг явно (`boto3.upload_file()`, `spark.read.parquet()`, `.printSchema()`).
- **Cleanup:** ноутбук НЕ удаляет raw-данные из MinIO — они нужны Модулю 4 (bronze). Spark-сессия останавливается. Явное пояснение в конце ноутбука. - **Cleanup:** практика настройки удаляет только свою временную таблицу. Ноутбук НЕ удаляет raw-данные из MinIO — они нужны Модулю 4 (bronze). Spark-сессия останавливается в конце.
## Checkpoint ## Checkpoint
Студент должен уметь: Студент должен уметь:
- создать сессию по заполненному шаблону, подтвердить master `spark://spark-master:7077` и заданное appName;
- прочитать `[0, 1]` из собственной Iceberg-таблицы и подтвердить её удаление;
- объяснить, какие настройки предоставил стенд, где выполняются задания и что повторить после перезапуска ядра;
- показать файлы в raw-зоне MinIO (через MinIO Console или через код); - показать файлы в raw-зоне MinIO (через MinIO Console или через код);
- прочитать raw PARQUET через Spark и вывести несколько строк; - прочитать raw PARQUET через Spark и вывести несколько строк;
- назвать схему Yellow Taxi (количество колонок, основные типы); - назвать схему Yellow Taxi (количество колонок, основные типы);
@@ -148,10 +166,11 @@ volumes:
- `docker-compose.yml` содержит volume mount `./data:/opt/data:ro` для контейнера `jupyter`, и `docker compose config` проходит без ошибок. - `docker-compose.yml` содержит volume mount `./data:/opt/data:ro` для контейнера `jupyter`, и `docker compose config` проходит без ошибок.
- `START_HERE.md` содержит воспроизводимую инструкцию по скачиванию NYC Taxi PARQUET (минимум 3 файла + taxi zone lookup) до запуска Модуля 3. - `START_HERE.md` содержит воспроизводимую инструкцию по скачиванию NYC Taxi PARQUET (минимум 3 файла + taxi zone lookup) до запуска Модуля 3.
- `.gitignore` содержит `/data/` для исключения датасета из git. - `.gitignore` содержит `/data/` для исключения датасета из git.
- `notebooks/03_raw_ingest_and_first_read.ipynb` можно выполнить сверху вниз в поднятом Jupyter-окружении при наличии скачанного data bundle. - `notebooks/03_raw_ingest_and_first_read.ipynb` можно пройти по порядку в поднятом Jupyter-окружении при наличии data bundle, заполнив шаблон и выполнив указанный перезапуск ядра.
- Ноутбук следует методике курса: `объяснение -> демонстрация -> самостоятельное повторение -> checkpoint`. - Ноутбук следует методике курса: `объяснение -> демонстрация -> самостоятельное повторение -> checkpoint`.
- Ноутбук использует паттерны из Модулей 1-2: `boto3` для MinIO, SparkSession с адресом кластера из `spark-defaults.conf`, `setLogLevel("ERROR")`. - Демонстрация берёт master из `spark-defaults.conf`; самостоятельное задание явно задаёт тот же адрес, сохраняя подготовленную конфигурацию каталога. Шаблон и контрольное решение проверяются в новых ядрах на Spark 3.5.1.
- Raw-данные остаются в MinIO после выполнения ноутбука (cleanup отсутствует намеренно). - Проверены повторные ячейки, перезапуск ядра, изменение и восстановление часового пояса, новое имя приложения после остановки контекста.
- Raw-данные остаются в MinIO после выполнения ноутбука; удаляется только временная таблица настройки.
- При отсутствии data bundle — понятный assert с отсылкой к `START_HERE.md`. - При отсутствии data bundle — понятный assert с отсылкой к `START_HERE.md`.
- `docs/stack_reference.md` обновлён: секция JupyterLab содержит `./data` mount. - `docs/stack_reference.md` обновлён: секция JupyterLab содержит `./data` mount.
- `plans/README.md` содержит строку Модуля 3 в оглавлении. - `plans/README.md` содержит строку Модуля 3 в оглавлении.
@@ -159,14 +178,15 @@ volumes:
## Риски ## Риски
- **Размер данных.** 3 месяца Yellow Taxi — примерно 120-150 MB (~40-50 MB на файл). Комфортно для стенда с 10 GB RAM. Extended (12 месяцев, ~600 MB) может замедлить Spark, но не должен вызывать падения. - **Размер данных.** 3 месяца Yellow Taxi — примерно 120-150 MB (~40-50 MB на файл). Комфортно для стенда с 10 GB RAM. Extended (12 месяцев, ~600 MB) может замедлить Spark, но не должен вызывать падения.
- **Студент забыл скачать данные.** Assert в Секции 1 с понятным сообщением и ссылкой на `START_HERE.md`. - **Студент забыл скачать данные.** Assert в Секции 3 с понятным сообщением и ссылкой на `START_HERE.md`.
- **NYC TLC изменит формат/URL.** Ноутбук использует wildcard `yellow_tripdata_*.parquet` без привязки к году. Инструкция в `START_HERE.md` фиксирует конкретные файлы 2024-01..03, но если URL станут недоступны — потребуется обновление инструкции. - **NYC TLC изменит формат/URL.** Ноутбук использует wildcard `yellow_tripdata_*.parquet` без привязки к году. Инструкция в `START_HERE.md` фиксирует конкретные файлы 2024-01..03, но если URL станут недоступны — потребуется обновление инструкции.
- **Директория `data/` не существует.** Docker volume mount создаст пустую директорию, assert в ноутбуке поймает отсутствие файлов. - **Директория `data/` не существует.** Docker volume mount создаст пустую директорию, assert в ноутбуке поймает отсутствие файлов.
- **Пересоздание контейнера.** Добавление volume в `docker-compose.yml` требует пересоздания контейнера `jupyter` (`docker compose up -d` достаточно — Docker пересоздаст только изменённый сервис). После этого bind mount уже работает: новые файлы в `./data` видны сразу без дополнительных перезапусков. Указать явно в инструкции. - **Пересоздание контейнера.** Добавление volume в `docker-compose.yml` требует пересоздания контейнера `jupyter` (`docker compose up -d` достаточно — Docker пересоздаст только изменённый сервис). После этого bind mount уже работает: новые файлы в `./data` видны сразу без дополнительных перезапусков. Указать явно в инструкции.
## Out of Scope ## Out of Scope
- Создание Iceberg-таблиц — отложено до Модуля 4. - Создание рабочих Iceberg-таблиц из raw — в Модуле 4. В Модуле 3 есть только маленькая временная таблица для проверки подключения.
- Подбор памяти, числа executors, shuffle, GC и эксперимент с числом партиций.
- Трансформации и очистка данных — отложено до Модулей 4-5. - Трансформации и очистка данных — отложено до Модулей 4-5.
- Чтение raw-данных через Trino (Trino не умеет читать произвольные файлы без таблицы) — отложено до Модуля 6. - Чтение raw-данных через Trino (Trino не умеет читать произвольные файлы без таблицы) — отложено до Модуля 6.
- Партиционирование, MERGE, streaming, gold-слой. - Партиционирование, MERGE, streaming, gold-слой.
+107
View File
@@ -0,0 +1,107 @@
# Проверка лимита памяти Trino
Дата: 19 октября 2026 года. Это отчет о проверке; актуальная конфигурация
находится в `docker-compose.yml` и `trino/jvm.config`, требования для учащегося —
в `START_HERE.md`.
## Что проверяли
Trino 478 в контейнере с пределом 2 ГиБ RAM, без swap, с Java heap от 512 МиБ
до 1 ГиБ. JVM сама подбирает размер регионов G1; вместо настройки образа
`G1HeapRegionSize=32M` для большого heap получен регион 1 МиБ.
Среда: Linux x86_64, Docker Engine, cgroup v2, 6 CPU и 11,14 ГиБ памяти,
видимых Docker. Остальные сервисы стенда работали одновременно. Общего лимита
8 ГБ для проекта или виртуальной машины в этом прогоне не было.
Исходные файлы основного маршрута курса:
| Файл | Размер, байт |
| --- | ---: |
| `yellow_tripdata_2024-01.parquet` | 49 961 641 |
| `yellow_tripdata_2024-02.parquet` | 50 349 284 |
| `yellow_tripdata_2024-03.parquet` | 60 078 280 |
| `taxi_zone_lookup.csv` | 12 331 |
Файлы взяты из уже скачанного учебного набора, без выборки или сокращения.
## Сценарий
1. Перед изменением проверены отсутствие активных Spark-приложений и запросов
Trino, чтение существующей таблицы `lakehouse.default.spark_trino_smoke`.
2. Пересоздан только Trino. Запуск до `SERVER STARTED` занял около 25 секунд,
healthcheck прошел.
3. Созданы отдельная временная схема `trino_memory_<id>` и собственный префикс
`validation/trino_memory_<id>/raw/` в MinIO. Таблицы курса не перезаписывались.
4. Spark подготовил bronze и справочник по SQL Модуля 4; фильтры, нормализации,
два JOIN со справочником и silver выполнены кодом Модуля 5. Проверки качества
этого модуля также прошли. Получены 9 554 778 строк bronze, 9 418 184 строк
silver и 265 строк справочника.
5. Через Python-клиент Trino выполнены 11 запросов из Модуля 6: просмотр схем,
таблиц и колонок, выборки, подсчет строк, общие метрики, агрегаты по районам
и топ-10 зон. Имена таблиц заменены на временные; SQL сохранен.
6. Дополнительно дважды выполнен JOIN всей bronze со справочником зон и
группировкой по району:
```sql
SELECT z.Borough, count(*) AS trips, avg(b.fare_amount) AS avg_fare
FROM <bronze> b
LEFT JOIN <zones> z ON b.PULocationID = z.LocationID
GROUP BY z.Borough
ORDER BY trips DESC;
```
7. Подсчет строк, общие метрики, агрегаты по районам, топ зон, чтение созданной
Spark таблицы итогов и оба JOIN сверены с результатами Spark. Для чисел с
плавающей точкой использован допуск `1e-8`. Ошибки Python-клиента Trino
пробрасывались наружу, без обработчика исключений из ноутбука.
8. Повторно прочитана прежняя smoke-таблица: результат `1 / from_spark` сохранен.
Всего завершились успешно 14 запросов. Временные таблицы удалены с `PURGE`,
затем удалены временная схема и собственный raw-префикс. После `PURGE`
отдельно удалены восемь пустых маркеров каталогов в собственном warehouse-префиксе.
Проверено отсутствие временных объектов; исходные Parquet/CSV на хосте сохранены.
## Измерения
| Показатель | Результат |
| --- | ---: |
| Лимит контейнера (`memory.max`) | 2 147 483 648 байт |
| Лимит swap (`memory.swap.max`) | 0 |
| Фактический `MaxHeapSize` через `jcmd 1 VM.flags` | 1 073 741 824 байта |
| Пик контейнера Trino (`memory.peak`) | 1 340 874 752 байта, около 1,25 ГиБ |
| События `oom`, `oom_kill`, `max` в `memory.events` | 0 |
| Перезапуски Trino | 0 |
| Пик суммы `memory.current` восьми контейнеров | около 5,89 ГиБ |
| Пик той же суммы за вычетом `inactive_file` | около 5,44 ГиБ |
Суммарная память снималась раз в секунду, получено 165 отсчетов во время
подготовки таблиц, запросов и очистки. Пик Trino взят из счетчика cgroup,
который учитывает и запуск контейнера. Вычитание `inactive_file` приближает
сумму к показателю памяти в `docker stats`; файловый кэш показан отдельно.
Агрегаты по silver обработали 9 418 184 строки каждый, что подтверждено
статистикой запросов Trino. `count(*)` по bronze использовал метаданные Iceberg;
проверкой чтения bronze служил также JOIN со справочником.
## Границы вывода
Бюджет Trino 2 ГиБ / heap 1 ГиБ достаточен для проверенных запросов на основном
наборе курса при одном учащемся. Измерение не подтверждает работу всех восьми
модулей под общим жестким лимитом 8 ГБ. В сумму контейнеров не входят Docker
daemon, ядро и другие процессы Linux/WSL2. Docker Desktop, Windows, ARM64,
параллельные занятия и расширенный набор Taxi за 12 месяцев не проверялись.
Для повторной проверки подними стенд с этой конфигурацией и пройди модули 3–6
на трех месяцах Taxi, добавив JOIN выше. Временная схема нужна, если важно
сохранить существующие учебные таблицы. Примененный лимит можно проверить
с хоста:
```bash
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}' trino
docker compose exec trino jcmd 1 VM.flags
docker compose exec trino cat /sys/fs/cgroup/memory.peak
docker compose exec trino cat /sys/fs/cgroup/memory.events
```
Последние два пути относятся к cgroup v2; счетчики накапливаются с запуска
контейнера. Числа нового прогона могут отличаться.
+100
View File
@@ -0,0 +1,100 @@
// Запуск через browser_ide.sh: настоящий notebook editor, локальное ядро и webview.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { chromium } = require('playwright');
const notebookName = 'lakehouse-browser-smoke.ipynb';
const notebook = JSON.parse(fs.readFileSync(
path.join(__dirname, '../notebooks/01_environment_and_smoke_test.ipynb'), 'utf8',
));
const smokeCell = notebook.cells.findIndex(cell =>
cell.source.join('').includes('smoke_report = run_cluster_smoke(spark)'),
);
async function openNotebook(page) {
await page.keyboard.press('Control+p');
await page.keyboard.type(`/tmp/${notebookName}`);
await page.getByRole('option').filter({ hasText: notebookName }).first().waitFor();
await page.keyboard.press('Enter');
await page.getByRole('list', { name: /Notebook Use/ }).waitFor();
}
async function checkOutput(page) {
// Notebook editor виртуализирует ячейки: сначала показываем нужную ячейку.
const list = page.getByRole('list', { name: /Notebook Use/ });
await list.press('Control+Home');
for (let i = 0; i < smokeCell; i++) await list.press('ArrowDown');
await page.getByRole('button', {
name: `Cell ${smokeCell + 1} of ${notebook.cells.length}`, exact: true,
}).waitFor();
for (let attempt = 0; attempt < 30; attempt++) {
for (const frame of page.frames()) {
const output = frame.getByText('result_sum=499999500000', { exact: false });
if (await output.count() && await output.first().isVisible()) return;
}
await page.waitForTimeout(500);
}
throw new Error('Notebook webview did not render the expected Spark result');
}
(async () => {
assert(smokeCell >= 0, 'Module 1 must contain the cluster smoke cell');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.setDefaultTimeout(30000);
const webviewErrors = [];
page.on('console', message => {
if (/Error loading webview|Could not register.*service.?worker/i.test(message.text())) {
webviewErrors.push(message.text());
}
});
try {
await page.goto(process.env.IDE_URL || 'http://localhost:8889', {
waitUntil: 'domcontentloaded',
});
assert(await page.evaluate(() => isSecureContext && 'serviceWorker' in navigator));
await page.getByRole('tree', { name: 'Files Explorer' }).waitFor();
// Баннер появляется позже Explorer; при повторном тесте доверие уже сохранено.
const restricted = page.getByRole('banner').filter({ hasText: 'Restricted Mode' });
await restricted.waitFor({ timeout: 10000 }).catch(error => {
if (error.name !== 'TimeoutError') throw error;
});
if (await restricted.count()) {
await restricted.getByRole('button', { name: 'Manage', exact: true }).click();
await page.getByRole('button', { name: 'Trust', exact: true }).click();
await page.getByRole('button', { name: 'Close Modal Editor (Escape)' }).click();
await restricted.waitFor({ state: 'hidden' });
}
console.log('PASS: workspace trust enabled');
await openNotebook(page);
await page.getByRole('button', { name: 'Select Kernel', exact: true }).click();
await page.getByRole('option', { name: 'Python Environments...' }).click();
await page.getByRole('option').filter({ hasText: '/usr/bin/python3' }).click();
console.log('PASS: notebook editor and local Python kernel selection');
await page.getByRole('button', { name: 'Run All', exact: true }).click();
await page.getByRole('button', { name: 'Interrupt', exact: true }).waitFor();
await page.getByRole('button', { name: 'Run All', exact: true }).waitFor({ timeout: 120000 });
await checkOutput(page);
await page.keyboard.press('Control+s');
console.log('PASS: Spark output rendered in notebook webview over HTTP localhost');
await page.getByRole('tab', { name: notebookName, exact: true })
.getByRole('button', { name: 'Close (Ctrl+W)' }).click();
await openNotebook(page);
await checkOutput(page);
assert.deepEqual(webviewErrors, []);
await page.screenshot({ path: '/artifacts/notebook.png' });
await page.getByRole('tab', { name: notebookName, exact: true })
.getByRole('button', { name: 'Close (Ctrl+W)' }).click();
console.log('PASS: saved notebook reopened with its output');
} catch (error) {
await page.screenshot({ path: '/artifacts/failure.png' }).catch(() => {});
throw error;
} finally {
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Проверка UI на Linux Docker Engine; зависимости браузера остаются в контейнере.
set -euo pipefail
cd "$(dirname "$0")/.."
artifact_dir=$(mktemp -d /tmp/lakehouse-browser-check.XXXXXX)
echo "Browser artifacts: $artifact_dir"
docker compose exec -T ide cp /opt/work/01_environment_and_smoke_test.ipynb \
/tmp/lakehouse-browser-smoke.ipynb
docker run --rm --network host --shm-size=1g \
-e IDE_URL="${IDE_URL:-http://localhost:8889}" \
-v "$PWD:/repo:ro" -v "$artifact_dir:/artifacts" \
mcr.microsoft.com/playwright:v1.56.1-noble \
bash -c 'npm install --prefix /tmp/browser-test --no-audit --no-fund playwright@1.56.1 >/dev/null &&
NODE_PATH=/tmp/browser-test/node_modules node /repo/tests/browser_ide.cjs'
docker compose exec -T ide python3 - <<'PY'
import json
from pathlib import Path
path = Path('/tmp/lakehouse-browser-smoke.ipynb')
notebook = json.loads(path.read_text())
cells = [cell for cell in notebook['cells'] if cell['cell_type'] == 'code']
assert all(cell['execution_count'] is not None for cell in cells)
assert not any(output['output_type'] == 'error'
for cell in cells for output in cell.get('outputs', []))
output = '\n'.join(''.join(item.get('text', []))
for cell in cells for item in cell.get('outputs', []))
assert 'master_url=spark://spark-master:7077' in output
assert 'result_sum=499999500000' in output
path.unlink()
print(f'PASS: all {len(cells)} code cells executed and saved without errors')
PY
+17
View File
@@ -0,0 +1,17 @@
# Параметры образа trinodb/trino:478 с бюджетом памяти для локального курса.
-server
-agentpath:/usr/lib/trino/bin/libjvmkill.so
-Xms512M
-Xmx1G
# Размер регионов G1 выбирает JVM с учетом небольшого heap.
-XX:+ExplicitGCInvokesConcurrent
-XX:+HeapDumpOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-XX:ReservedCodeCacheSize=256M
-XX:PerMethodRecompilationCutoff=10000
-XX:PerBytecodeRecompilationCutoff=10000
-Djdk.attach.allowAttachSelf=true
-Djdk.nio.maxCachedBufferSize=2000000
-Dfile.encoding=UTF-8
-XX:+EnableDynamicAgentLoading