Threads tuning: предел threads и warehouse concurrency
threads: N в profiles.yml — самый известный, и наиболее неправильно понимаемый параметр dbt. Junior часто думает: “поставлю 32 threads, и dbt будет быстрее в 32 раза”. Senior знает, что после определённого порога threads перестают помогать и начинают вредить.
Этот урок — про практику тюнинга threads с конкретными числами по warehouse’ам и архитектурными решениями.
Пять уровней concurrency в AirflowЧто такое threads в dbt
threads: N — это размер thread pool, который dbt использует для параллельного запуска независимых моделей. Когда DAG позволяет — например, 10 staging-моделей не зависят друг от друга — dbt отправляет до N штук одновременно на warehouse.
Ключевое: dbt не делает многопоточные вычисления локально. Локальный CPU занят почти ничем — только координацией. Вся работа происходит на warehouse. dbt — это оркестратор, который посылает SQL и ждёт результата.
Это значит: предел threads определяется не локальной машиной, а concurrency capacity warehouse’а.
Как dbt использует threads — DAG scheduler
dbt-core имеет компонент GraphRunnableTask (см. модуль 01), который запускает модели в топологическом порядке. Когда модель завершилась — освобождается thread, и берётся следующая готовая модель из очереди.
DAG-граф из 7 моделей, threads=4. Time slice показывает, какие модели бегут параллельно. Узкие места — модели с большим количеством зависимостей или одиночные на critical path.
Это иллюстрирует ключевую мысль: threads помогают только параллельным секциям DAG-а. Если у вас «narrow funnel» DAG (много staging -> один большой fact), threads сверх какого-то порога бесполезны.
DuckDB: single-process, single-writer
DuckDB — это embedded database, работающая в том же процессе, что и dbt. Файл .duckdb имеет одного writer’а (single-writer-per-file). Это сильно ограничивает threads:
| threads | Результат |
|---|---|
| 1 | Серийное выполнение — медленно на параллельных DAG sections |
| 4 | Sweet spot для DuckDB на локальной машине |
| 8 | Можно, но contention при INSERT/UPDATE — иногда даже медленнее |
| 16+ | Threads thrashing, queries serializeется внутри DuckDB |
Реальный тест: проект 50 моделей, M2 MacBook, DuckDB. Threads=4 -> 12 секунд. Threads=8 -> 14 секунд. Threads=16 -> 18 секунд. То есть, увеличение threads даёт деградацию из-за внутренней сериализации writes.
В контексте курса используем DuckDB как обучающий warehouse, но в production для concurrency-heavy workloads DuckDB не подходит. Если у вас > 100 параллельных моделей с heavy writes — нужна нормальная warehouse.
# profiles.yml для DuckDB
my_project:
target: dev
outputs:
dev:
type: duckdb
path: './my_project.duckdb'
threads: 4
На MotherDuck (cloud DuckDB) ситуация лучше — multi-writer поддержка, но всё равно threads рекомендуется 4-8 максимум. MotherDuck — это managed scenarios на DuckDB engine, и фундаментальное ограничение single-process остаётся.
Snowflake: warehouse concurrency
Snowflake имеет concept virtual warehouse (compute) и concurrent query limit. Каждый warehouse определённого размера может обслуживать N concurrent queries:
| Snowflake warehouse | Concurrent queries (default) | Sweet spot dbt threads |
|---|---|---|
| X-Small | 8 | 4-6 |
| Small | 8 | 8 |
| Medium | 8 | 16 |
| Large | 8 | 24-32 |
| X-Large | 8 | 32 |
| 2X-Large | 8 | 32+ (через multi-cluster) |
Концепция: default concurrent query limit на Snowflake — 8 для всех warehouse-размеров. То есть на одном warehouse одновременно бегут до 8 queries, остальные становятся в очередь. Если у вас threads=32 на Medium warehouse, 24 thread’а ждут в очереди — реального параллелизма нет.
Решения:
- Multi-cluster warehouses —
MIN_CLUSTER_COUNT=2, MAX_CLUSTER_COUNT=4. Snowflake автоматически масштабирует количество кластеров. На каждом кластере свой concurrent limit 8. MAX_CONCURRENCY_LEVEL— increase per warehouse:ALTER WAREHOUSE my_wh SET MAX_CONCURRENCY_LEVEL = 16. Но это деградирует производительность отдельных queries — Snowflake даёт меньше CPU/memory на query.
Реальный паттерн в production: вместо одного большого warehouse — отдельный warehouse для dbt (multi-cluster, MIN=1 MAX=4) с threads=16-24. Это даёт хороший баланс throughput и query performance.
# profiles.yml для Snowflake в production
analytics:
target: prod
outputs:
prod:
type: snowflake
account: my-org-my-account
user: dbt_runner
password: "{{ env_var('SNOWFLAKE_PWD') }}"
role: TRANSFORMER
warehouse: DBT_PROD_WH # отдельный warehouse для dbt
database: ANALYTICS
schema: PROD
threads: 24
BigQuery: slots model
BigQuery концепция совсем другая — slots (compute units), а не concurrent queries. Default project имеет on-demand pricing, и concurrency ограничена rate limits API:
- on-demand: до 100 concurrent queries per project, но slots выделяются динамически. threads=16-32 обычно ок.
- flat-rate (reservations): фиксированное количество slots (например, 2000 slots). threads=N зависит от размера queries — если каждая query использует 100 slots, max concurrent ≈ 20. Threads сверху — очередь.
Особенность BigQuery: API rate limit для DDL/DML — 5 concurrent DDL per table. Если 30 моделей пишут в одну таблицу через partitions — упретесь в этот limit, dbt получает quota errors.
# profiles.yml для BigQuery
analytics:
target: prod
outputs:
prod:
type: bigquery
project: my-gcp-project
dataset: analytics_prod
threads: 16
location: US
Sweet spot: 16 threads на средний проект, 32 на большой. Сверх 32 редко даёт выигрыш из-за rate limits.
Профилирование threads: какое значение реально оптимально
Не угадывайте — мерьте. Простой experiment:
# Запустите с разными threads, замерьте время
for T in 1 2 4 8 16 24 32; do
sed -i "" "s/threads: .*/threads: $T/" profiles.yml
echo "=== threads=$T ==="
time dbt run --select state:modified+ --state target/manifest.json
done
Отобразите результат как график (threads vs time). Типичная кривая:
- 1 -> baseline (например, 10 минут)
- 2 -> 6 минут (1.7x speedup)
- 4 -> 4 минуты (2.5x speedup) — diminishing returns начинаются
- 8 -> 3.2 минуты (3.1x speedup)
- 16 -> 3.0 минуты (3.3x speedup) — sweet spot
- 32 -> 3.1 минуты (3.2x speedup) — НЕТ выигрыша
- 64 -> 3.5 минуты — деградация!
Правило: ищите “колено” кривой. Если threads=16 даёт 3x speedup, а threads=32 — 3.2x, не имеет смысла платить за 2x больше threads ради 7% выигрыша.
Когда threads НЕ помогают
Конкретные сценарии, где увеличение threads ничего не даёт:
- Narrow DAG: один большой fact, который зависит от всех staging. Critical path — последовательный, threads не помогают.
- Большие модели: одна модель на 500GB, которая считается час. Threads не разделят её, она занимает один thread всё это время.
- Heavy I/O: модели с
external_location(S3 reads). Не CPU-bound, не помогает thread-параллелизм. - Warehouse contention: 30 моделей пишут в одну partition таблицу. Warehouse сериализует, threads ничего не делают.
- Connection limits: warehouse имеет limit 50 concurrent connections per user, у вас threads=100 — половина просто ждёт.
DuckDB на CI: спорный паттерн
Иногда команды запускают dbt с DuckDB на CI (быстро, не нужны cloud creds для PR). Тогда threads tuning особенно важен:
# profiles.yml для CI с DuckDB
ci_target:
type: duckdb
path: ':memory:' # in-memory для скорости
threads: 4
:memory: даёт максимальную скорость, но есть gotcha — после run база исчезает. Это окей для тестов, не для actual analytics.
Trick для CI: запускайте dbt build с малым sample data (sample CSV files), threads=4, на DuckDB in-memory. Получается быстрый smoke test модели и тестов за секунды. Production-данные при этом не нужны.
Архитектурные паттерны для high-throughput
Если у вас 1000+ моделей и хочется максимизировать concurrency:
Разделение workload по warehouses вместо одного большого. Каждый warehouse оптимизирован под свой паттерн.
Конфигурируется через config() в моделях:
-- staging/stg_orders.sql
{{ config(snowflake_warehouse='DBT_STAGING_WH') }}
select * from {{ source('app', 'orders') }}
-- marts/fct_user_metrics.sql
{{ config(snowflake_warehouse='DBT_HEAVY_WH') }}
-- большая модель с heavy aggregations
Это даёт fine-grained control — каждая модель попадает на оптимальный warehouse. dbt при этом коннектится через свой default warehouse, но через USE WAREHOUSE переключается per query.
Параллелизация tests
dbt test тоже использует threads. На больших проектах с 1000+ тестами это критично. Reality check:
| Tests count | threads=4 | threads=16 | threads=32 |
|---|---|---|---|
| 100 | 30s | 12s | 10s |
| 1000 | 5min | 90s | 60s |
| 10000 | 50min | 12min | 8min |
Тесты — это SELECT-ы для assertion. Они хорошо параллелизуются (нет writes), warehouse concurrency обычно не bottleneck. На тестах можно поставить threads выше, чем на runs.
# Custom config через --threads CLI override
dbt run --threads 16 # для runs
dbt test --threads 32 # для tests
Production checklist для threads tuning
- Запустите benchmark: threads = [1, 4, 8, 16, 24, 32] на real-life command (
dbt run --select state:modified+). - Найдите колено в кривой: threads vs time. Это ваш sweet spot.
- Не превышайте warehouse concurrency limit: threads > limit означает waiting.
- На Snowflake — рассмотрите multi-cluster warehouse для CI.
- На BigQuery — следите за DDL rate limits, особенно на partition-tables.
- Отдельный config для tests — обычно можно threads*2.
- Архитектурно: если threads уперлись в потолок, ищите DAG-bottlenecks (narrow funnels), не увеличивайте threads дальше.
Резюме
- threads — это thread pool для concurrent queries, не локальный параллелизм.
- DuckDB — sweet spot 4-8 threads (single-process limit).
- Snowflake — зависит от warehouse size и concurrent query limit (default 8). Multi-cluster для большего throughput.
- BigQuery — slots model, threads=16-32 разумно, watch для DDL rate limits.
- Профилируйте через benchmark с разными threads, ищите колено кривой.
- Архитектурно — разные модели на разные warehouses через
config(snowflake_warehouse=...). - Threads сверх потребности тормозят — connection overhead, очередь в warehouse, compute splitting.