Привет, Хабр! Меня зовут Дмитрий Капралов, я работаю в команде консалтинга компании Arenadata. Одной из наших задач является проведение аудита кластеров СУБД Arenadata DB (ADB). Проектирование физической модели данных и настройка квотирования ресурсов кластера проводятся на этапе разработки хранилища. Однако при переходе системы в продуктивный режим профиль нагрузки кардинально меняется, и первоначальные настройки СУБД и архитектура данных нередко оказываются не готовы к такой нагрузке. В процессе эксплуатации могут возникнуть узкие места, замедляющие работу системы. В этой статье я хочу поделиться опытом нашей команды по выявлению и оптимизации подобных проблем.

Мы исследуем несколько ключевых направлений, анализ которых позволяет выявить определённые проблемы:

  • анализ, который даёт понимание, какой ресурс кластера является наиболее дефицитным;

  • анализ настроек квотирования ресурсов СУБД относительно реальной нагрузки кластера;

  • анализ модели данных.

Ресурсы кластера

Мы анализируем стандартные метрики: нагрузку на CPU, потребление оперативной памяти, производительность дисковой подсистемы, скорость и надёжность сетевого взаимодействия. Обязательно следует проверять каждую метрику на перекосы по нагрузке в разрезе сегмент-серверов. Их наличие зачастую свидетельствует либо о проблемах с инфраструктурой, либо о неоптимальном распределении данных в ADB. А как известно, MPP-СУБД, к которой относится и ADB, работает со скоростью самого медленного сегмента.

Утилизация ЦПУ

Просто наблюдать за метрикой утилизации CPU имеет мало смысла. Мы рекомендуем следить за показателем ldavg-1 — это средняя нагрузка на систему за одну минуту. Нагрузка отражает экспоненциальное среднее длины очереди процессов на CPU при интервале в одну минуту. Метрику используют для сопоставления с количеством CPU на соответствующем сегмент-сервере для получения общей оценки нагрузки на систему, понятной для человеческого восприятия.

Если средняя нагрузка меньше количества CPU (с учётом Hyper-Threading), то система имеет некоторый запас по наращиванию нагрузки. Если средняя нагрузка больше количества логических ядер, то система считается полностью насыщенной. Кратное превышение нагрузки приводит к снижению производительности процессов, которые получают меньше времени на CPU, создавая при этом более высокую конкурентную нагрузку на менее производительные элементы системы (дисковую систему) и CPU-зависимые системные процессы (сетевой стек). Это негативно сказывается на общей производительности кластера, приводит к отказам в обслуживании запросов пользователей, снижению надёжности и доступности кластера в целом.

К примеру, ниже график утилизации CPU кластера ADB:

В рабочие дни нагрузка по ЦПУ доходит до 90%, как и положено с учётом значения по умолчанию параметра gp_resource_group_cpu_limit, равным 0,9. С кластером ADB всё хорошо, он работает на полную мощность с точки зрения утилизации ЦПУ.

Теперь взглянем на нагрузку на кластере через метрику ldavg-1:

Пунктирной линией показано количество логических ядер на серверах — 56 ядер (с учётом Hyper-Threading). В таком разрезе видно, что система на самом деле сильно перегружена и кратно превышает возможности кластера. В данном случае причинами такой повышенной нагрузки стали: выбранный высокий уровень конкуренции запросов и большое количество логических сегментов на сегмент-сервер — 12 primary-сегментов на сервер.

Утилизация оперативной памяти

Оперативная память — ключевой ресурс для производительности запросов в ADB. Доступная память на сервере используется процессами операционной системы, приложениями, в том числе и Arenadata DB, файловым кэшем операционной системы. От грамотной настройки параметров, отвечающих за распределение памяти, будет зависеть производительность кластера ADB. При этом нужно помнить о двух моментах:

  • память может требоваться сторонним приложениям: PXF (JVM), агентам мониторинга и др.;

  • свободную память ОС linux эффективно использует для организации кэша дисковой системы, что повышает производительность системы в целом.

Ниже приведён пример утилизации памяти приложениями на кластере ADB:

Система сильно недоиспользует память: в пиковые моменты её объём не достигает даже 300 Гб, несмотря на общую доступную память в 768 Гб на сегмент-сервере и примерно 500 Гб, выделенных для Arenadata DB. Мы подробнее разберём проблему недоиспользования памяти в следующем разделе, посвящённом настройке ресурсных групп.

С другой стороны, свободная память полностью утилизируется файловым кэшем операционной системы, тем самым способствуя повышению производительности кластера ADB.

Утилизация дисковой подсистемы

Проблемы с дисковой подсистемой могут привести к серьёзной деградации производительности кластера ADB. Мы в первую очередь обращаем внимание на метрики await, среднее время ожидания дискового устройства и aqu-sz — среднюю длину очереди запросов к дисковому устройству.

Ниже пример работы дисковой системы на кластере ADB и значения метрики await на сегмент-серверах кластера:

В этом случаем видны одновременно две проблемы:

  • очень высокое время ожидания, особенно для использовавшихся в данном случае NVMe SSD дисков;

  • перекос нагрузки по устройствам.

Причин у этих проблем было несколько:

  • высокая нагрузка в целом — на устройствах одновременно располагался каталог СУБД и пространство для временных файлов (spill-файлов);

  • небольшое количество дисковых устройств (два NVMe SSD диска), не рассчитанное на такую высокую нагрузку;

  • большое количество primary-сегментов на кластере (12 сегментов);

  • использование программного рейд-контроллера.

В результате дисковая система не справлялась с нагрузкой и в итоге приводила даже к переключению primary-сегментов в кластере на mirror-сегменты.

Решением проблемы стало переконфигурирование табличного пространства для временных файлов (temp-tablespace) на массив дисков, использовавшихся для хранения данных. В результате нагрузка на NVMe диски снизилась, время ожидания опустилось до приемлемых значений, переключения primary-сегментов прекратились.

Утилизация сети

При старте проекта и подготовке инфраструктуры кластера ADB необходимо проверять соответствие значения максимального размера пакета (MTU) в interconnect-сети и размера пакетов в СУБД — параметр gp_max_packet_size, значение которого по умолчанию равно 8192. Если у вас используется сеть с MTU = 1500, то несоответствие будет приводить к фрагментации пакетов Arenadata DB и сказываться на производительности и надёжности кластера вплоть до отказа сегментов под высокой нагрузкой. В продуктивных кластерах мы крайне рекомендуем использовать MTU = 9000.

Наши замеры показывают, что использование MTU = 1500 и соответствующее изменение размера пакета gp_max_packet_size до 1450 в ADB приводит к снижению производительности кластера на 15–25%, а при использовании значения по умолчанию (8192) производительность снижается в несколько раз (замеры делались на базе бенчмарка TPC-DS):

Кол-во одновременных запросов

MTU 9000, gp_max_packet_size 8192

MTU 1500, gp_max_packet_size 1450

Прирост по времени

MTU 1500, gp_max_packet_size 8192

Прирост по времени

10

0:10:40

0:12:42

19%

0:31:45

198%

12

0:12:53

0:14:47

15%

0:41:49

225%

16

0:17:01

0:21:04

24%

1:03:03

271%

20

0:21:16

0:26:19

24%

1:29:20

320%

24

0:26:33

0:32:31

22%

2:08:37

384%

28

0:30:54

0:37:46

22%

2:33:52

398%

32

0:35:00

0:43:03

23%

3:08:09

438%

При продуктивной эксплуатации кластера нужно отслеживать наличие ошибок и потерь пакетов на сетевых интерфейсах, а также следить за перекосами в пропускной способности на серверах кластера.

Общие рекомендации:

  • Следить за нагрузкой на ЦПУ, в первую очередь по метрике ldavg-1.

    • Не использовать большое количество логических primary-сегментов на кластере. В большинстве случаев, в нагруженных кластерах мы рекомендуем использовать 4 primary-сегмента на сервер.

    • Не завышать уровень конкурирующих транзакций.

    • По возможности, равномернее распределять нагрузку на кластере в течение дня.

  • Следить за утилизацией оперативной памяти со стороны ADB, при необходимости проводить реорганизацию настроек ресурсных групп.

  • Следить за нагрузкой на дисковую подсистему:

    • Избегать использования программных raid-контроллеров.

    • Разносить табличные пространства под каталог и WAL, временные файлы, данные, если дисковая система не справляется с общей нагрузкой. 

  • Следить за сетью:

    • Устанавливать MTU 9000, если такой возможности нет, то обязательно снизить размер пакета в СУБД.

    • Сетевых ошибок быть не должно, иначе проверить сетевой тракт.

    • Потери пакетов должны быть редкими и такие случаи должны быть объяснимы, например, они могут быть связаны с высокой нагрузкой на сеть.

  • Следить за перекосами по каждой метрике и исследовать их.

Управление квотированием ресурсов СУБД

ADB имеет два механизма управления ресурсами: ресурсные очереди и ресурсные группы. Мы рекомендуем использовать именно ресурсные группы, и такой тип управления ресурсами в ADB включён по умолчанию. Группы позволяют гибко управлять распределением ЦПУ, памяти и количеством конкурентных транзакций.

От грамотной настройки РГ зависит производительность кластера в целом и работа запросов в конкретной группе в частности. Наиболее часто встречающиеся проблемы в настройках:

Одновременная недоутилизация памяти и высокий объём временных файлов

В примере выше на один логический сегмент выделено 43 Гб памяти (красная пунктирная линия), однако при этом реальное потребление памяти в среднем достигает 10 Гб с редкими пиками в 20–40 Гб. В то же время на кластере фиксируется большой объём spill-файлов — временные файлы, необходимые СУБД для выполнения запроса.

Пример выше — довольно распространённый случай неоптимального квотирования ресурсов в ADB. Попробовать исправить эту ситуацию можно несколькими способами, но для начала немного теории по механизму распределения памяти запросу в Arenadata DB.

Теория

В ADB 6 при использовании механизма ресурсных групп резервирование памяти запросу рассчитывается следующим способом:

  1. Определяем сколько памяти доступно в целом всем ресурсным группам на логическом сегменте.

  2. Определяем сколько памяти доступно запросу в рамках группы, в которой он работает.

Формула расчет памяти, доступной всем группам на логическом сегменте следующая:

rg_perseg_mem = ((RAM * (vm.overcommit_ratio / 100) + SWAP) * gp_resource_group_memory_limit) / num_active_primary_segments
  • RAM - физическая память на сегмент-сервере;

  • vm.overcommit_ratio - процент памяти, который операционная система отдает всем приложениям на сервере;

  • SWAP - объем памяти на жестком диске, используемый ОС при нехватке оперативной памяти;

  • gp_resource_group_memory_limit - процент памяти, предоставленный ADB для использования ресурсными группами;

  • num_active_primary_segments - количество primary сегментов на сервере.

Предположим, что на сегмент-сервере имеется 512 Гб оперативной памяти, а размер SWAP 16 Гб. Тогда, при значениях по умолчанию параметров vm.overcommit_ratio (0.93) и gp_resource_group_memory_limit (0.7) и 8 primary сегментах на сервере, значение rg_perseg_mem будет равно: 

((512 * 0.93) + 16) * 0.7) / 8 = 43 Гб

Получившийся на предыдущем шаге объем памяти распределяется уже среди всех ресурсных групп исходя из параметров настройки каждой группы. За распределение памяти отвечают следующие параметры:

  • memory_limit - доля памяти, определенная конкретной группе от общего объема rg_perseg_mem;

  • memory_shared_quota - доля общей памяти от memory_limit, доступная запросам в рамках конкретной группы;

  • global_memory_shared_quota - доля глобальной общей памяти, доступная запросам всех групп. Рассчитывается как нераспределенная доля памяти между всеми группами от общего объема rg_perseg_mem;

  • memory_spill_ratio - порог использования оперативной памяти для транзакций, при превышении которого создаются spill-файлы;

  • concurrency - максимальное количество одновременных транзакций, разрешенное в ресурсной группе.

В релизе ADB 6.21 добавлен новый параметр gp_resource_group_enable_recalculate_query_mem, который позволяет СУБД более точно рассчитывать объем памяти исходя из количества primary-сегментов на сегмент-хосте. По умолчанию параметр выключен (off) и размер доступной памяти вычисляется, исходя из количества primary-сегментов и объему оперативной памяти на мастер-хосте.

Мы рекомендуем для версий ADB 6.21+ устанавливать параметр в gp_resource_group_enable_recalculate_query_mem = on и пересчитывать параметр MEMORY_SPILL_RATIO.

Предположим, что настройки у ресурсной группы следующие:

CONCURRENCY

CPU_RATE_LIMIT, (%)

MEMORY_LIMIT, (%)

MEMORY_SHARED_QUOTA, (%)

MEMORY_SPILL_RATIO, (%)

50

60

70

90

50

В таком случае, гарантированный объем памяти доступный одному запросу будет равняться 62 Мб, а предел памяти, после исчерпания которого оптимизатор планирует использовать spill-файлы - 309 МБ. Эти значения будем использовать для разбора примера с неоптимальной утилизацией памяти.

В ADB настройку и мониторинг ресурсных групп можно осуществлять в системе мониторинга запросов Arenadata DB Control, подробнее об этом можно прочитать в документации.

Итак, теперь посмотрим, как можно попробовать исправить ситуацию с неоптимальной утилизацией памяти и большим объёмом spill-файлов для примера выше:

1. Проанализировать текущие настройки ресурсной группы.

Самый простой способ чуть улучшить ситуацию — пересмотр текущих настроек, отвечающих за распределение памяти. На примере настроек выше стоит сразу обратить внимание на значение memory_spill_ratio, равное 50%. Подняв, к примеру, его до 80% мы поднимаем порог до генерации spill-файлов до 494 Мб, тем самым чуть улучшив ситуацию с утилизацией памяти и уменьшив общий объём spill-файлов.

2. Проанализировать профили запросов в рамках ресурсной группы.

Хорошее правило при планировании и настройке ресурсных групп — разделение нагрузки по бизнес-профилю, например на группы ETL, BI, Ad-hoc, Admin и т. д. Но даже если запросы разделены по бизнес-профилю, внутри конкретной группы всё равно могут остаться запросы разного профиля по потреблению ресурсов. На графике выше основную нагрузку создавала ресурсная группа под ETL-запросы, параметры которой мы вычислили ранее.

В результате каждому запросу в этой группе гарантированно выделялась фиксированная память в размере 62 Мб — fixed memory. Далее, если конкретному запросу не хватало памяти, ему была доступна общая память в рамках его ресурсной группы — shared memory в размере 27 Гб и глобальная общая память, доступная всем группам, global shared memory в размере 4 Гб (сделаем допущение, что в данном примере среди всех групп было не распределено 10% памяти, предосталвенной ресурсным группам).

Показатель Initial memory  предел памяти, после исчерпания которого оптимизатор планирует использовать spill-файлы — равнялся 309 Мб. Concurrency, максимальное количество одновременных транзакций, разрешённое в ресурсной группе, было равно 50.

С одной стороны, это хороший вариант квотирования:

  • У нас есть предположение, что большинству запросов в данной группе не нужно много памяти. Отдав большую часть памяти в shared memory, мы предоставляем одному запросу не так много памяти от общего объёма — 62 Мб. Тем самым мы стараемся не растрачивать память на запросы, которым она не нужна.

  • Если же какому-то «тяжёлому» запросу потребуется дополнительная память, он сможет взять её из общего пула группы, shared memory, или из глобальной памяти, global shared memory.

  • При этом, достигнув потребления памяти в 309 Мб, то есть Initial memory, ADB начнёт генерировать spill-файлы.

При этом на графике выше мы видим, что память всё-таки недоутилизирована и генерируется большой объём spill-файлов. Если детальнее проанализировать статистику потребления памяти по запросам, мы увидим следующую картину:

Потребление памяти на сегменте, МБ

Кол-во запросов

Доля от всех запросов

Запросов со spill-файлами на сегментах

Доля запросов со spill-файлами

Средний размер spill-файлов на сегменте, МБ

Суммарный размер spill-файлов на кластере, МБ

< 25

15,069

65%

17

0.1%

3

8,160 

25-125

5,834

25%

87

1.5%

57

793,440 

125-300

946

4%

134

14.2%

372

7,975,680

300-500

735

3%

694

94.4%

763

84,723,520 

> 500

521

2%

504

96.7%

1,574

126,927,360

Как видно из статистики, большинству запросов (65%) достаточно 25 Мб памяти, при этом текущие настройки гарантированно резервируют запросу 62 Мб. Небольшое количество запросов (5%) генерирует довольно большой суммарный объем spill-файлов, таким запросам нужно 300 и более Мб памяти. 

Напрашивается более тонкое разделение etl-группы на "легкие" и "тяжелые" запросы, предоставив первым меньший объем фиксированной памяти, а вторым, наоборот, выдав больше фиксированной памяти и подняв порог генерации spill-файлов.

Однако бывает, что сделать это непросто, т. к. назначение ресурсной группы в ADB настраивается на уровне пользователя, выполняющего запрос, и не всегда удаётся разбить поток загрузки на отдельные «тяжёлые» и «лёгкие» блоки. Тем не менее мы рекомендуем следить за реальной статистикой потребления памяти в рамках групп и при необходимости и возможности дробить их на подгруппы в зависимости от профиля нагрузки.

3. Проанализировать реальную конкуренцию в рамках ресурсной группы.

Ещё один параметр, влияющий на итоговую утилизацию памяти, — это параметр concurrency, максимальное количество одновременных транзакций, разрешённое в ресурсной группе. Этот показатель влияет на то, сколько фиксированной памяти будет выделено одному запросу, и на расчёт порога генерации spill-файлов. Чем выше показатель concurrency, тем меньше предоставляется памяти и тем раньше начинается генерация spill-файлов.

На нашем примере выше у группы etl параметр concurrency равен 50. Исходя из этого значения fixed memory у нас получился равным 62 Мб, а Initial memory — 309 Мб. При этом, как мы видели выше из статистики с сегментацией по объёму потребления памяти, бо́льшая часть запросов требует сильно меньше — 25 и менее Мб, а небольшому количеству «тяжёлых» запросов требуется сильно больше — 300 и более Мб.

Если посмотреть на статистику, сколько же в среднем выполняется «тяжёлых» запросов, то за анализируемый период увидим следующее:

Количество "тяжелых" конкурентных запросов

Количество минут с такой конкуренцией

1

479

2

354

3

362

4

144

5

61

6+

16

Согласно данным в таблице выше, для «тяжёлых» запросов параметр concurrency можно было бы установить равным 5.

Таким образом, при разделении группы ETL на «лёгкие» и «тяжёлые» запросы для первой подгруппы можно оставить заявленный уровень конкуренции 50 и занизить долю зарезервированной на неё памяти — memory_limit. Для второй подгруппы, наоборот, занизить concurrency до 5, тем самым в разы увеличив объём фиксированной памяти и порог генерации spill-файлов. В результате утилизация памяти на кластере должна стать более сбалансированной.

Довольно часто мы наблюдаем на кластере ситуацию, когда заданное значение concurrency не соответствует реальной статистике. На примере ниже на одном из кластеров ADB показатель конкуренции был равен 200, при этом фактическое значение даже в пиках редко превышало 50:

При этом значение concurrency существенно влияло на выделяемый запросу объём памяти, фактически занижая его минимум в четыре раза относительно реальной статистики активных запросов.

Физическая модель данных

Ошибки, допущенные при проектировании физической архитектуры данных, со временем могут оказывать существенное влияние на производительность кластера, особенно при увеличении объёмов данных и повышении нагрузки на кластере. Далее опишу несколько моментов, на которые мы обращаем внимание наших клиентов при проектировании физической модели данных.

Партиционирование таблиц

Партиционирование — логическое разбиение таблицы на более мелкие части — партиции или секции. Партиционирование позволяет значительно повысить быстродействие в определённых сценариях:

1. В запросах с предикатами, являющимися ключами партиционирования. Оптимизатор исключает использование лишних партиций, не попадающих в условие фильтрации, за счёт чего кардинально сокращается чтение данных из файловой системы.

2. При загрузке, обновлении или удалении данных с помощью команд EXCHANGE PARTITION, TRUNCATE PARTITION, DROP PARTITION.

3. При использовании инкрементального резервирования с помощью утилиты gpbackup.

При этом сложно дать однозначную рекомендацию по стратегии партиционирования таблиц. Необходимо ориентироваться на объём таблицы, размер которой заранее оценить не всегда бывает возможным; на профиль использования: как часто обращаются к таблице, за какой период чаще всего отбираются данные; конфигурацию оборудования; топологию кластера и т. д. Тем не менее определённые неоптимальные решения в архитектуре можно определить.

Большие таблицы без партиций

Большие таблицы фактов (от сотен гигабайт до терабайтов) — первые кандидаты для партиционирования. В большинстве случаев логика обращения к данным в них подразумевает какую-либо фильтрацию по дате. Необходимо проанализировать профиль запросов и продумать логику партиционирования, разбив данные на горячий, тёплый и холодный слои с разной разбивкой по диапазонам и, возможно, типом и уровнем сжатия.

Мы неоднократно сталкивались с ситуациями, когда большие таблицы не имеют партиций. Обращения к таким таблицам приводили к сканированию всех строк, хотя запросы подразумевали фильтрацию данных по дате. А это, в свою очередь, вызывало повышенную нагрузку на дисковую систему.

Мелкая нарезка партиций

В то же время следует избегать очень мелкой нарезки партиций в таблицах. Большое количество партиций в небольших таблицах, особенно если таких таблиц много в базе данных, — серьёзная проблема.

Каждая партиция хранится в словаре данных и обрабатывается как отдельная таблица. Если речь идёт об Append Optimized таблицах, то для их обслуживания создаются вспомогательные системные heap-таблицы:

  • таблица состояния сегментных файлов;

  • таблица карты видимости;

  • таблица карты блоков, которая создаётся, если у АО-таблицы есть индекс.

Это кратно увеличивает размер словаря данных и замедляет обращения к системному словарю и операции обслуживания базы данных: VACUUM, ANALYSE, подсчёт размеров таблиц, резервирование, восстановление primary-сегментов после их переезда на mirror и т. д. Увеличивается нагрузка и на файловую систему.

Один из примеров плохого применения партиционирования, с которым мы сталкивались: колоночная таблица из 8 колонок имела размер 3 Гб и состояла из 830 непустых партиций, в кластере было 140 primary-сегментов. Таким образом, размер одной партиции на сегменте составлял всего 26 Кб и на каждом сегменте хранилось 6640 файлов средним размером всего 3,5 Кб! В данном случае подобная логика партиционирования была излишней и только добавляла накладных расходов на обслуживание таблицы.

Пустые партиции

Стоит обращать внимание и на пустые партиции. Мы нередко встречали случаи, когда 15–20% партиций у таблиц не содержали данных, создавая лишнюю нагрузку на системный каталог.

Пустые партиции обычно возникают в следующих сценариях:

  • генерация партиций на будущие периоды при загрузке данных, например, ETL-фреймворком;

  • охлаждение или удаление старых данных без удаления уже ненужных партиций;

  • непродуманное проектирование объектов в схемах бизнес-пользователей — «песочницах».

Удаление лишних объектов позволит оптимизировать работу со словарём данных.

Общие рекомендации по партиционированию:

1. Партиционируйте только большие таблицы. Никогда не партиционируйте маленькие таблицы.

2. Если количество партиций в таблице превышает тысячу, стоит хорошо обдумать целесообразность такого решения.

3. Используйте партиционирование по полю, наиболее часто встречающемуся в условиях выборки. Обычно это поля с датами.

4. Гранулярность партиции рассматривать исходя из разумных требований к выборке данных: наиболее горячий период партиционировать по дням/неделям, более старые периоды объединять в месяцы и годы.

5. Для холодных партиций рассмотреть более сильное сжатие данных.

6. Следить за слишком мелкой нарезкой таблиц, например, по дням, там, где в этом нет необходимости. Часто подобным страдают пользовательские объекты («песочницы»).

7. Следить за пустыми партициями и удалять их, если они не нужны. Часто подобным страдают пользовательские объекты.

8. Следить за партициями небольшого объёма (менее 100 Мб) и по возможности применять к ним более крупную гранулярность.

9. Следить за таблицами без партиционирования размером более 100 Гб и рассмотреть вариант их партиционирования, где это возможно/необходимо (см. пп. 1–2).

10. Никогда не партиционируйте таблицы по полям распределения данных.

11. Избегайте многоуровневого партиционирования.

12. Не создавайте слишком много партиций в колоночно-ориентированных таблицах, т. к. общее количество физических файлов на сегментном хосте определяется произведением:

СегментовНаХосте × КоличествоКолонок × КоличествоПартиций.

Подробнее про партиционирование можно почитать в нашей документации.

Типы таблиц

В ADB имеется два типа таблиц:

  • Heap - тип таблицы по умолчанию и унаследованный от PostgreSQL, рекомендуется для OLTP нагрузок. OLTP в случае Arenadata DB нужно понимать условно и данный профиль нагрузки не подразумевает сотен или тысяч одновременных запросов к БД.

  • Append-optimized - специализированная структура таблиц в ADB предназначенная для OLAP профиля нагрузки и пакетной загрузки данных. Дополнительным важным свойством данного типа таблиц является возможность сжатия данных. Делится в свою очередь на два подтипа:

    • Row-oriented (AORO) - построчное хранение данных.

    • Column-oriented (AOCO) - поколоночное хранение данных.

Подробнее с типами таблиц в ADB и их возможностями можно ознакомиться в нашей документации.

В данном разделе я отражу неэффективные, на наш взгляд, примеры использования типов таблиц, с которыми мы сталкивались в нашей практике.

Использование Heap-таблиц c Unique-индексами

В хранилище данных был использован архитектурный подход с уникальными ключами для исключения дубликатов записей при вставке данных. Мы считаем такой подход к проектированию хранилища данных антипаттерном и рекомендуем проверять уникальность записей на уровне SQL-кода перед загрузкой данных в таблицу. В случае ADB такая архитектура усугубляется ещё и тем, что уникальные индексы можно использовать только в heap-таблицами.

В ADB 7 у АО-таблиц появится возможность использовать unique-индексы.

В результате, помимо снижения производительности запросов на heap-таблицах, большой проблемой стала нехватка места, ведь heap-таблицы не сжимаются. Перевод объектов в АО-формат позволил бы уменьшить объём данных в несколько раз, но сделать это уже на работающем в продуктиве кластере было не так просто.

В целом использование heap таблиц — в большинстве случаев антипаттерн для реализации хранилищ данных в ADB: heap-таблицы не сжимаются; в большинстве случаев показывают худшую производительность, особенно на OLAP-запросах.

Использование Heap-таблиц по умолчанию

Опции хранения таблицы, такие как тип таблицы, тип сжатия, уровень сжатия и др., в ADB могут быть заданы двумя способами:

  • в блоке WITH команды CREATE TABLE при создании таблицы;

  • значениями параметра gp_default_storage_options, если при создании таблицы параметры не были описаны в блоке WITH.

По умолчанию значение опции appendoptimized в параметре gp_default_storage_options равно FALSE. Соответственно, если таблица создаётся без указания этой опции в блоке WITH или вообще без этого блока, то будет создана heap-таблица.

И если в основных слоях хранилища данных обычно объекты создаются согласно какой-либо принятой методике, то в бизнес-схемах, «песочницах», пользователи зачастую не задумываются о логике выбора типа таблицы или могут вообще не знать об этой специфике ADB и создавать таблицы без блока WITH. В результате в кластере появляются сотни объектов, которые отъедают существенную часть полезного пространства, ведь heap-таблицы не сжимаются.

Использование AOCO-таблиц по умолчанию

Обратная ситуация: по умолчанию в gp_default_storage_options заданы опции gp_default_storage_options = TRUE и orientation = column. Логика, с одной стороны, понятна: АО-таблицы предназначены для OLAP-нагрузки и пакетной вставки; колоночное хранение обеспечивает лучшее сжатие, отлично подходит для аналитических запросов. Почему бы не применить данный тип ко всем объектам хранилища по умолчанию?

Но преимущество колоночной таблицы в то же время имеет и дополнительные накладные расходы в виде большого количества файлов, ведь каждая колонка хранится в отдельном файле. При этом надо помнить, что запрос на вставку данных в АО-таблицу записывает данные в конец строго одному сегментному файлу. Если параллельно возникает ещё одна транзакция на вставку, то она или находит следующий незанятый сегментный файл, или порождает такой сегментный файл самостоятельно. Всего таких «версий» файлов может быть 127. А в случае АОСО-таблицы:

127 × количество колонок в таблице.

Если сюда добавить описанную выше проблему про мелкую нарезку партиций, проблема станет ещё масштабнее, ведь все эти файлы будут созданы для каждой патриции.

Появление дополнительных файлов может быть вызвано не только параллельной вставкой (например, gpfdist), но и одновременным выполнением VACUUM совместно с другими операциями над таблицей, например операцией чтения данных. Во время vacuum AO-таблицы видимые строчки переезжают в новый файл. После переезда старый файл должен быть очищен, а для этого требуется его эксклюзивная блокировка. Если в этот момент кто-то читает файл, то эксклюзивную блокировку получить невозможно и файл не очищается, но помечается как заблокированный. Последующие операции уже не будут ни читать, ни дописывать в него данные. А этот файл будет очищен при следующем VACUUM — очищен, но не удалён.

При этом мы часто видим сценарии, когда АОСО таблицы используются в stage-слое, ODS-слое и ином слое, где данные в таблицу один раз вставляются и потом целиком вычитываются для дальнейшей трансформации по пайплайну загрузки данных. В таких слоях лучше использовать строчный формат хранения в виде AORO-таблиц.

Типы данных

При работе с большими объёмами данных и таблицами размером десятки терабайт выбор типа данных оказывает существенное влияние на эффективность хранения данных и производительность запросов. На этапе проектирования физической модели данных имеется несколько базовых рекомендации для любой СУБД:

1. Не использовать избыточные типы данных, например bigint вместо integer в случаях, где значение поля не может быть больше 2147483647.

2. Использовать одинаковый тип данных для ключей распределения таблиц, по которым будет производиться соединение в запросах. В таком случае большие таблицы фактов при соединении их по равенству ключей распределения будут соединяться локально без ресурсоёмкой операции Redistribute Motion.

Так вот, для второй рекомендации может быть неочевидным то, что в случае использования одинакового типа данных, но разной размеренности, например bigint и integer, в запросе будет всё равно происходить Redistribute Motion, при том, что данные в таблицах будут распределены одинаково.

Пример

Создадим 2 тестовые таблицы с одним полем id с типом int и int8, соответственно, и распределим данные по этому полю id:

create table tst_join_types_int (id int) 
with (appendonly=true , orientation=row, compresstype=zstd, compresslevel=1)
DISTRIBUTED BY (id);

create table tst_join_types_int8 (id int8) 
with (appendonly=true , orientation=row, compresstype=zstd, compresslevel=1)
DISTRIBUTED BY (id);

insert into tst_join_types_int
select gen
from pg_catalog.generate_series(1,1000000) gen;

insert into tst_join_types_int8
select gen
from pg_catalog.generate_series(1,1000000) gen;

Проверим, что данные распределились по сегментам одинаково:

select 'tst_join_types_int',gp_segment_id,count(1)
from tst_join_types_int
group by gp_segment_id
union all
select 'tst_join_types_int8',gp_segment_id,count(1)
from tst_join_types_int8
group by gp_segment_id
order by 1;

table

gp_segment_id

count

tst_join_types_int

0

249 933

tst_join_types_int8

0

249 933

tst_join_types_int

1

249 548

tst_join_types_int8

1

249 548

tst_join_types_int

2

250 659

tst_join_types_int8

2

250 659

tst_join_types_int

3

249 860

tst_join_types_int8

3

249 860

Посмотрим на план запроса:

explain analyse 
select *
from tst_join_types_int t1
inner join tst_join_types_int8 t2 using(id);

Gather Motion 4:1  (slice2; segments: 4)  (cost=0.00..1002.34 rows=1000000 width=8) (actual time=150.920..339.981 rows=1000000 loops=1)
  ->  Hash Join  (cost=0.00..975.58 rows=250000 width=8) (actual time=149.745..304.020 rows=250659 loops=1)
        Hash Cond: (tst_join_types_int8.id = (tst_join_types_int.id)::bigint)
        Extra Text: (seg2)   Hash chain length 1.3 avg, 6 max, using 199357 of 524288 buckets.
        ->  Seq Scan on tst_join_types_int8  (cost=0.00..434.02 rows=250000 width=8) (actual time=0.243..36.083 rows=250659 loops=1)
        ->  Hash  (cost=438.46..438.46 rows=250000 width=4) (actual time=149.033..149.033 rows=250659 loops=1)
              ->  Redistribute Motion 4:4  (slice1; segments: 4)  (cost=0.00..438.46 rows=250000 width=4) (actual time=1.700..67.532 rows=250659 loops=1)
                    Hash Key: (tst_join_types_int.id)::bigint
                    ->  Seq Scan on tst_join_types_int  (cost=0.00..433.48 rows=250000 width=4) (actual time=0.548..51.956 rows=250659 loops=1)
Planning time: 9.074 ms
  (slice0)    Executor memory: 151K bytes.
  (slice1)    Executor memory: 220K bytes avg x 4 workers, 220K bytes max (seg0).
  (slice2)    Executor memory: 20840K bytes avg x 4 workers, 20840K bytes max (seg0).  Work_mem: 5875K bytes max.
Memory used:  128000kB
Optimizer: Pivotal Optimizer (GPORCA)
Execution time: 399.750 ms

Как видим, первая таблица все равно перераспределилась по кластеру, не смотря на то, что данные и так были уже распределены одинаково со второй таблицей:

->  Redistribute Motion 4:4  (slice1; segments: 4)  (cost=0.00..438.46 rows=250000 width=4) (actual time=1.700..67.532 rows=250659 loops=1)

На больших таблицах фактов эта особенность может оказать крайне негативное влияние на производительность запросов, добавляя в план запроса лишний шаг с перераспределением большого объёма данных по сети.

Равномерное распределение

Равномерное распределение — один из критичных факторов для достижения максимальной производительности MPP системы, т. к. общее время работы запросов зависит от времени окончания работы наиболее загруженного сегмента.

Сильный перекос данных даже одной таблицы замедляет работу не только обрабатывающего её запроса, но и отрицательно влияет на работу остальных, «равномерных» запросов, т. к. на перегруженном сегменте они не получают достаточного объёма ресурсов.

Аналогично, но, возможно, не столь очевидно, неравномерное распределение данных создаёт проблемы на кластере и с точки зрения свободного места. Перекошенная таблица блокирует пространство на сегментах кластера для других равномерно распределённых таблиц:

С небольшими таблицами этим фактором можно пренебречь, но, например, перекошенная таблица размером 100 Гб на кластере с 10 сегментами может заблокировать уже 1 Тб полезного объёма. Таким образом, при оценке свободного полезного объёма на кластере следует исходить из свободного места на самом перекошенном сегмент-сервере. Иначе можно легко ошибиться с итоговой оценкой.

Заключение

Описанные в статье моменты лишь часть аспектов, которые следует учитывать при оптимизации работы кластера Arenadata DB. В процессе аудита также анализируются такие факторы, как влияние процессов VACUUM и ANALYSE, блокировки, а также ресурсоёмкие запросы, что может значительно повлиять на производительность и стабильность кластера.

Надеюсь, эта статья будет полезна для подготовки вашего кластера к повышенной нагрузке и поможет избежать ошибок, связанных с неоптимальными настройками квотирования ресурсов и проектированием модели данных хранилища на базе Arenadata DB.

Комментарии (6)


  1. KateRicko
    21.11.2024 14:32

    Добротная статья


  1. box-570
    21.11.2024 14:32

    Как часто рекомендуется проводить аудит кластеров Arenadata DB, чтобы поддерживать их в оптимальном состоянии?


    1. gumaka Автор
      21.11.2024 14:32

      Мы рекомендуем проводить аудит раз в год.


  1. BahamutFF
    21.11.2024 14:32

    Если в примере для таблиц tst_join_types_int и tst_join_types_int8 выбрать orientation=column, то при замере занимаемого места через pg_total_relation_size получаем, что таблица с типом колонки int занимает почти в два раза больше места, чем колонка с типом int8. Почему так происходит?


    1. gumaka Автор
      21.11.2024 14:32

      Это, похоже, проблема алгоритма сжатия zstd, причем для последовательных чисел. Вот статистика для данных без сжатия, алгоритмов zstd и zlib, а также для последовательного набора данных (как в примере выше), так и для рандомного набора:


      1. gumaka Автор
        21.11.2024 14:32

        В Clickhouse схожая история: