Как мы сократили время выполнения нашей самой долгой модели на 90 минут
Когда вы запускаете задачу, содержащую более 1700 моделей, как определить, что является "хорошим" временем выполнения? Если весь процесс занимает 3 часа, это замечательно или ужасно? Хотя существует множество возможных ответов в зависимости от размера набора данных, сложности моделирования и исторических времен выполнения, суть обычно заключается в вопросе "достигли ли вы своих SLA"? Однако в мире облачных вычислений, где счета выставляются на основе использования, вопрос на самом деле звучит так: "достигли ли вы своих SLA и остались в рамках бюджета?"
Здесь, в dbt Labs, мы использовали вкладку Model Timing в нашем внутреннем аналитическом проекте dbt, чтобы помочь нам выявить неэффективности в нашей инкрементальной задаче dbt Cloud, что в конечном итоге привело к значительной экономии средств и созданию пути для периодических проверок улучшений.
Ваш новый лучший друг: вкладка Model Timing
Внутренний проект dbt Labs — это настоящий монстр! Наша ежедневная инкрементальная задача dbt Cloud запускается 4 раза в день и вызывает более 1700 моделей. Мы любим просматривать нашу задачу dbt Cloud, используя вкладку Model Timing в dbt Cloud. Панель Model Timing
отображает состав моделей, порядок и время выполнения для каждой задачи в dbt Cloud (для командных и корпоративных планов). Топ 1% длительностей выполнения моделей автоматически выделяется, что облегчает поиск узких мест в наших запусках. Вы можете увидеть, что наша самая долго выполняющаяся модель выделяется как больной палец — вот пример нашей инкрементальной задачи до применения исправления:
Как вы можете видеть, легко определить модель, которая вызывает длительное время выполнения и задерживает другие модели. Модель fct_dbt_invocations
в среднем занимает 1,5 часа на выполнение. Это неудивительно, учитывая, что это относительно большой набор данных (~5 миллиардов записей) и что мы выполняем несколько интенсивных SQL-вычислений. Кроме того, эта модель вызывает эфемерную модель под названием dbt_model_summary
, которая также выполняет значительную работу. Тем не менее, мы решили исследовать возможность рефакторинга этой модели, чтобы сделать ее быстрее.
После рефакторинга этого кода мы заменили эфемерную модель dbt_model_summary
на инкрементальную модель, которая вынесла основную часть обработки из основной модели fct_dbt_invocations
. Вмес то того чтобы пересчитывать эту сложную логику при каждом запуске, мы извлекаем только новые данные и применяем эту логику к меньшему подмножеству этих записей. Общее время выполнения новой dbt_model_summary
и fct_dbt_invocations
теперь составляет ~15-20 минут, что позволяет сэкономить более часа на каждом запуске!
Определение проблемы
Этот проект работает на Snowflake, поэтому все приведенные ниже примеры показывают интерфейс Snowflake. Однако аналогичный анализ можно провести в любом хранилище данных.
Кроме того, этот блог-пост представляет собой довольно техническое погружение. Если все, что вы здесь прочитали, не сразу укладывается в голове, это нормально! Мы рекомендуем прочитать эту статью, а затем освежить знания о облачных хранилищах данных и оптимизации запросов, чтобы допол нить полученные здесь знания.
Разбор плана запроса
Нахождение этого долго выполняющегося запроса было первым шагом. Поскольку он был настолько доминирующим на вкладке Model Timing, было легко перейти прямо к проблемной модели и начать искать способы ее улучшения. Следующим шагом было изучение того, как выглядел план запроса Snowflake.
Существует несколько способов сделать это: либо найти выполненный запрос на вкладке History
в интерфейсе Snowflake, либо взять скомпилированный код из dbt и запустить его в рабочем листе. Пока он выполняется, вы можете нажать на ссылку Query ID
, чтобы увидеть план. Более подробная информация об этом процессе доступна в документации вашего провайдера (Snowflake, BigQuery, Redshift, Databricks).
Ниже вы можете увидеть план запроса для fct_dbt_invocations
, который включает логику из dbt_model_summary
:
Из профиля запроса было легко найти проблему. Существуют две оконные функции, которые составляют более 90% времени выполнения, когда мы учитываем сканирование таблицы, необходимое для извлечения данных. Кроме того, почти 1 ТБ данных выгружается в удаленное хранилище в рамках этого запроса. В Snowflake удаленное хранилище значительно медленнее как для записи, так и для чтения, поэтому любые данные, находящиеся на удаленном диске, действительно замедляют выполнение запроса. Мы нашли проблему!
Понимание данных
После того как мы выявили проблему, нам нужно было найти способ ее исправить.
Во-первых, полезно иметь общее представление о базовых данных для fct_dbt_invocations
. Каждый раз, когда вы выдаете команду dbt (run, test, build, snapshot и т.д.), мы отслеживаем определенные метаданные об этом запуске. Мы называем их "вызовами", и, как вы можете себе представить, dbt вызывается очень часто. Таблица, против которой выполняется этот запрос, отфильтрована, но все же содержит около 5 миллиардов строк. Соответствующие части данных, которые мы используем в этом запросе, включают идентификаторы проектов, идентификаторы моделей и анонимизированный хеш-ключ, представляющий необработанное содержимое модели, чтобы узнать, изменилась ли модель.
Если вам интересно, вот как выглядел запрос для dbt_model_summary
до внесения изменений:
{{config(materialized = 'ephemeral')}}
with model_execution as (
select * from {{ ref('stg_dbt_run_model_events') }}
),
diffed as (
select *,
row_number() over (
partition by project_id, model_id
order by dvce_created_tstamp
) = 1 as is_new,
/*
Оконная функция `mode` возвращает наиболее распространенный хеш содержимого
для данной модели в течение данного дня. Мы используем это как прокси для
'производственной' версии модели, работающей в развертывании. Когда выполняется
другой хеш, это, вероятно, отражает, что модель находится в стадии разработки.
*/
contents != mode(contents) over (
partition by project_id, model_id, dvce_created_tstamp::date
) as is_changed
from model_execution
),
final as (
select
invocation_id,
max(model_complexity) as model_complexity,
max(model_total) as count_models,
sum(case when is_new or is_changed then 1 else 0 end) as count_changed,
sum(case when skipped = true then 1 else 0 end) as count_skip,
sum(case when error is null or error = 'false' then 0 else 1 end) as count_error,
sum(case when (error is null or error = 'false') and skipped = false then 1 else 0 end) as count_succeed
from diffed
group by 1
)
select * from final
Оконные функции, упомянутые выше, отвечают на следующие вопросы:
row_number()
- Это первый раз, когда эта конкретная модель запускается в проекте?
- Примечание: это зерно на уровне проекта
mode()
- Это наиболее частая версия модели, которая запускалась сегодня (на основе хешированного содержимого)?
- Примечание: это зерно на уровне модели + даты запуска