PostgreSQL на многоядерных серверах Power 8

smagen 17 ноября 2015 в 02:19 17k

Аннотация


При помощи московского представительства компании IBM мы провели тестирование производительности последних версий СУБД PostgreSQL на серверах Power8, изучили масштабируемость зависимость производительности от количества одновременных запросов, нашли узкие места ограничивающие производительность, предложили новые технические решения и добились рекордной производительности.

Введение


В ряде задач практически неограниченного масштабирования по объему обрабатываемых транзакций можно достичь, используя распределённые системы, в которых тем или иным способом поток транзакций распределяется на большое количество серверов. Такое масштабирование часто называют “горизонтальным”. Однако, универсального распределенного решения не существует, кроме того, распределённость имеет свою цену. Архитектура системы должна заранее проектироваться как распределённая. Распределенные системы менее гибки, чем монолитные, к тому же они сложнее в эксплуатации и требуют более высокой квалификации персонала. Одни задачи легче поддаются распараллеливанию, другие — сложнее. Поэтому спрос на высокопроизводительные монолитные системы существует, и достижение возможно лучших результатов по производительности в рамках одного сервера было и остается важной задачей. Это часто называют “вертикальным масштабированием”.

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

Для решения таких проблем существуют механизмы управления доступом к ресурсам — использование блокировок, а также пригодные в некоторых случаях неблокирующие (lock-free) подходы. Рост производительности этих механизмов, а также детализация блокировок дает возможность снизить издержки, связанные с одновременным (конкурентным) доступом.

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

Исследования масштабирования PostgreSQL


В 2012 году на конференции PgCon Роберт Хаас из компании EnterpriseDB представил результаты исследований производительности (числа запросов на чтение в секунду) PostgreSQL в зависимости от количества одновременно действующих клиентских сессий (Рис.1). Измерения производились на платформе amd64 с помощью входящего в состав PostgreSQL средства pgbench.

image
Рис.1. График из доклада Р. Хааса на конференции PgCon, 2012 г., случай для 32 ядер

Прежде чем анализировать этот график, сделаем несколько важных замечаний. Архитектура PostgreSQL такова, что один клиент обслуживается одним однопоточным процессом на сервере, а работа с диском ведется через буфер, находящийся в разделяемой между всеми такими процессами оперативной памяти. Таким образом, с одной стороны, возможна одновременная независимая работа некоторого количества клиентских сессий, однако сами данные, находящиеся в разделяемой памяти (shared memory), являются общим ресурсом, к которому осуществляется конкурентный доступ. Общим ресурсом является и дисковая подсистема, но в данном исследовании работа с ней не рассматривалась. Все данные уже находились в буферах, поэтому проведенные измерения показывают максимально достижимую производительность.

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

Рис.1. показывает, что при переходе от версии 9.1 к 9.2 благодаря совершенствованию механизмов блокировки устойчивый рост производительности стал наблюдаться до как минимум 32 одновременно работающих с CPU клиентов (в тесте была использована 32-ядерная машина, поэтому при дальнейшем увеличении количества клиентов производительность больше не росла). Чуть позже Р. Хаасом были проведены аналогичные измерения на 64-х ядрах (Рис.2). Видно, что близкая к линейной вертикальная масштабируемость сохраняется и при этом количестве ядер.

image
Рис.2. График из доклада Р. Хааса на конференции PgCon, 2012 г., случай для 64 ядер

Эти графики и стоящие за ними исследования вдохновили нас на то, чтобы посмотреть, как поведет себя PostgreSQL современных версий на современных многопроцессорных серверах. Этим заинтересовалась компания IBM, поставляющая такие сервера на платформе Power (бизнес, связанный с x86-серверами, IBM уступила Lenovo в 2014 г. вслед за ПК). Вот что из этого вышло.

Наши исследования


В феврале 2015 года наиболее известные российские разработчики PostgreSQL основали компанию “Постгрес Профессиональный” (Postgres Professional), целью которой стало развитие СУБД PostgreSQL и оказание полного спектра связанных с ней услуг. Создание компании позволило в несколько раз увеличить объем работ по разработке PostgreSQL в России, дало возможность проводить прикладные исследования и вступать в партнерские отношения с производителями программного обеспечения и аппаратных средств. Компания Postgres Professional с самого начала проявила интерес к партнерству с IBM и вошла с партнерскую сеть IBM PartnerWorld. В мае 2015 г. Москву посетила Терри Вирниг (Terri Virnig), вице-президент IBM по экосистеме и стратегии Power, которая подтвердила высокую степень заинтересованности IBM в PostgreSQL и его адаптации для достижения максимальной производительности на платформе Power.

В рамках партнерских отношений, в августе 2015 г. в клиентском центре IBM в Пoкипсии (Poughkeepsie, NY) для тестирования PostgreSQL был выделен сервер IBM E880 в конфигурации с 2 узлами, каждый узел содержал по 4 восьмиядерных процессора с технологией SMT (simultaneous multithreading), каждое ядро позволяет получить до 8 потоков. Таким образом, эта конфигурация обеспечивает до 2*4*8*8=512 потоков исполнения (или в терминологии IBM “Logical CPU”, LCPU). Важно заметить, что реализация SMT в IBM Power 8 отличается от ее реализации в процессорах Intel (известной как Hyperthreading), и в отличие от последней дает заметный, до двукратного, прирост производительности на задачах СУБД PostgreSQL. Количеством потоков (LCPU) можно управлять, устанавливая их число равным 1, 2, 4 или 8 на ядро CPU. Техника тестирования была предварительно отработана на менее мощных серверах в Москве. Тесты, как и у Р. Хааса, проводились при помощи входящей в состав PostgreSQL утилиты pgbench.

Сценарий тестирования представлял собой запросы на поиск по B-tree индексу, целиком помещенному в память. Сервер PostgreSQL запускался на большом LPAR, и при помощи встроенных в ядро Linux средств в условиях NUMA-архитектуры закреплялся на определенном количестве numa-нод. Успешность теста заключалась в достижении как можно бо́льшего количества транзакций в секунду (TPS).

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

image
Рис.3. Производительность в зависимости от числа клиентов, для различного числа LCPU

Для того, чтобы понять, почему при добавлении LCPU производительность падала, нужно представить себе устройство сервера IBM E880. В нём есть две материнские платы, на каждой из которых расположено по 4 процессора. Между собой материнские платы связаны гиперконнектом. Более детально про внутреннее устройство серверов на IBM Power 8 можно почитать в цикле статей от IBM. Гиперконнект, хоть и очень быстрый, но оказался предсказуемо медленнее, чем взаимодействие процессоров, находящихся на одной материнской плате. И это будет заметно не только в PostgreSQL, но и в любой другой системе, где процессы взаимодействуют через общую память. Поэтому в дальнейших исследованиях, в узких местах, мы исключали влияние гиперконнекта и использовали один вычислительный узел, т.е. 256 LCPU при SMT = 8.

Вначале был испытан последний на момент тестирования официальный релиз PostgreSQL (версия 9.4.5), который показал линейное масштабирование только до 32 клиентов, затем — текущие версии из веток 9.5 и 9.6, на которых рост, близкий к линейному, продолжался до 64 клиентов.


Рис.4. Производительность в зависимости от числа потоков

В версии 9.5 видно улучшение по сравнению с версией 9.4, которое можно объяснить оптимизацией механизмов LWLock (будет пояснено ниже).

Итак, измерения были проведены. Но мы решили не останавливаться на этом, а продвинуться дальше — попытавшись понять, есть ли возможность, обнаружив узкое место в коде, устранить его и достичь лучшего результата. Для поиска узких мест была использована утилита perf, которая показала, что около 32% процессорного времени занимает выполнение функции s_lock (Рис.5).

32.10% postgres       [.] s_lock
7.77%  postgres       [.] GetSnapshotData
2.64%  postgres       [.] AllocSetAlloc
1.40%  postgres       [.] hash_search_with_hash_value
1.37%  postgres       [.] base_yyparse
1.36%  postgres       [.] SearchCatCache
1.32%  postgres       [.] PinBuffer
1.23%  postgres       [.] LWLockAcquire
1.05%  postgres       [.] palloc
1.01%  postgres       [.] ReadBuffer_common
0.99%  postgres       [.] LWLockRelease
0.94%  libc-2.17.so   [.] __memset_power7

Рис.5. Пример вывода perf top во время тестирования

Чтобы идентифицировать места в программном коде PostgreSQL, ответственные за вызов s_lock, был использован отладчик gdb (производительность однопоточного perf для построение графа зависимостей оказалась недостаточной). Выборочные подключения gdb показали, что в большинстве случаев вызов s_lock происходит из функций PinBuffer и UnpinBuffer, служащих для увеличения и уменьшения счетчика ссылок на буфер, в котором хранится определённый блок данных. Счётчик ссылок защищён спинлоком BufferDesc.buf_hdr_lock, и поэтому для работы с ним нужно предварительно захватить данный спинлок. Чтобы лучше понять это место, давайте рассмотрим, какие виды блокировок есть в PostgreSQL, и зачем они нужны.

Блокировки в PostgreSQL: какие и зачем столько разных?


В PostgreSQL реализовано несколько типов блокировок, каждый из которых решает свои задачи.
  1. Spinlock (спинлок) – самый простой тип блокировок, который имеет только два состояния “занято” и “свободно”. Процедура взятия спинлока состоит из последовательных попыток поменять сменить его состояние со “свободно” на “занято”. Для того, чтобы избежать гонок, спинлоки использую атомарную операцию Test-and-set (TAS). Процессы, желающие получить спинлок не выстраиваются в очередь. Со стороны СУБД не поддерживается ни мониторинг спинлоков, ни автоматическое определение deadlock’ов на спинлоках. Всем этим было пожертвовано ради высокой производительности на очень коротких операциях. PostgreSQL содержит ассемблерные реализации спинлоков под различные аппаратные платформы. В случае когда ассемблерная реализация спинлоков отсутствует, используется эмуляция спинлоков через UNIX семафоры, которые, однако, имеют куда меньшую производительность.
  2. Lightweight lock (LWLock) – более сложный тип блокировок, который имеет два уровня shared и exclusive. Одновременно может быть взято или неограниченное число shared блокировок, или только одна exclusive. Реализация LWLock претерпела существенные изменения между версиями PostgreSQL 9.4 и 9.5. В версиях 9.4 и ниже LWLock содержал в себе набор полей, защищённых спинлоком. Начиная с версии 9.5 большая часть операций с LWLock’ом осуществляется минуя спинлок, с помощью атомарных операций с “переменной состояния” LWLock’а. Процессы, желающие получить LWLock выстраиваются в очередь, благодаря чему при длительном ожидании LWLock’а ресурс шины не расходуется зря. Автоматическое определение deadlock’ов для LWLock’ов не поддерживается, алгоритмы СУБД выверены таким образом, чтобы deadlock’и никогда не возникали. Работу по поддержке мониторинга LWLock’ов ведёт сотрудник Postgres Professional, Ильдус Курбангалиев (aka ildus) в рамках более общей задачи мониторинга событий ожидания.
  3. Heavyweight lock (HWLock) – наиболее “продвинутый” тип блокировок. Имеет много уровней и нетривиальную матрицу конфликтов. На таблицы индексы и другие объекты БД, HWLock’и устанавливаются, как неявно теми SQL-командами, которые их используют, так и явно командой LOCK. Для мониторинга HWLock’ов предназначено системное представление pg_locks. Как следует из названия, HWLock’и – это довольно тяжеловесный механизм, поэтому для их получения DML-командами, предусмотрен специальный механизм fastpath locking. Кроме этого, поскольку порядок получения HWLock’ов зависит от действий пользователя, нельзя гарантировать отсутствие deadlock’ов, поэтому в PostgreSQL реализован механизм автоматического обнаружения deadlock’ов.


Помимо вышеперечисленных блокировок, в PostgreSQL присутствуют row-level locks, predicate locks, advisory locks и другие, которые, в свою очередь, реализованы с использованием перечисленных выше трёх базовых типов блокировок.

Как мы ускорили PostgreSQL


Чтобы понять, получится ли повысить производительность в обнаруженном нами узком месте, мы, совместно с ведущим разработчиком PostgreSQL Андресом Фройндом (Andres Freund), разработали патч, заменяющий спинлок buf_hdr_lock на атомарные операции с “состоянием” буфера. Таким образом, разработанный патч реализует для буферов оптимизацию аналогичную той, что была сделана для LWLock’ов в версии 9.5: функция PinBuffer стала выполнять операцию Compare and Swap (CAS) в цикле, а функция UnpinBuffer – атомарный декремент. Это позволило продлить область роста, близкого к линейному, примерно в 2 раза и достичь производительности более 800 тысяч TPS (Рис.6).


Рис.6. Результаты измерения производительности разработанного патча.

Однако, на этом мы тоже не остановились. Если посмотреть на ассемблерную реализацию операции CAS под power8, то можно увидеть, что она представляет собой цикл (Рис. 7).

# Входные параметры:
#  r3 – старое значение, r4 – новое значение
#  r5 – адрес атомарной переменной
.L1: lwarx 9,0,5
     cmpw 0,9,3
     bne- 0,.L2
     stwcx. 4,0,5
     bne- 0,.L1
.L2: isync

Рис 7. Реализация атомарной операции CAS на ассемблере Power 8.

Таким образом, выполнение операции CAS в цикле – это цикл в цикле. При большой конкуренции за изменение значения, это может быть заметно медленнее, чем выполнение одинарного цикла.

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

# Входные параметры:
#  r3 – инкремент
#  r5 – адрес атомарной переменной
.L1: lwarx 9,0,5
     add 9,9,3
     stwcx. 9,0,5
     bne- 0,.L1
     isync

Рис 8. Реализация атомарной операции атомарного инкремента на ассемблере Power 8.

Очевидно, что между командами lwarx и stwcx могут располагаться и более сложные вычисления.

Исходя их этого родилось две идеи для дальнейших оптимизаций.
  1. Реализовать критичные по производительности функции, таких как PinBuffer и LWLockAttemptLock с помощью ассемблерных вставок. Минусом данного варианта является то, что мы выходим за уровень абстракции в виде атомарных операций, который предоставляет нам компилятор.
  2. Альтернативная идея состоит в том, чтобы использовать атомарный инкремент и реализовать так называемый “оптимистичный подход”. В PinBuffer и LWLockAttemptLock можно делать атомарный инкремент “переменной состояния”, а затем, уже по фактически установленному значению, проверить, имели мы право на такое изменение состояния или нет. В случае, если мы не имели право изменять состояние таким образом, отменить это изменение операции атомарного инкремента. Расчёт здесь делается на то, что отменять изменения придётся лишь в очень небольшой доле случаев, в этом и заключается “оптимизм” подхода. Разумеется, все остальные функции, которые работают с “переменной состояния”, должны также учитывать это.

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


Рис.9. Результаты измерения производительности различных патчей.

На графике (Рис. 9) показано сравнение производительности следующих патчей:
  1. pinunpin-cas – PinBuffer выполняется с помощью цикла CAS операций.
  2. pinunpin-increment – PinBuffer выполняется с помощью “оптимистичного” атомарного инкремента.
  3. pinunpin-cas-lwlock-increment – PinBuffer выполняется с помощью цикла CAS операций, LWLockAttemptLock выполняется с помощью “оптимистичного” атомарного инкремента.
  4. pinunpin-lwlock-increment – PinBuffer и LWLockAttemptLock выполняются с помощью “оптимистичного” атомарного инкремента.
  5. pinunpin-lwlock-asm – PinBuffer и LWLockAttemptLock реализованы на ассемблере.

Видно, что эффекта от “оптимистичного” атомарного инкремента в PinBuffer почти нет, зато применение того же подхода к LWLockAttemptLock даёт очень большой эффект. И примерно такой же эффект можно получить с помощью ассемблерный оптимизаций.

На Рис.10 показана изменившаяся картина perf top после применения патча. Видно, что узкое место переместилось в функцию GetSnapshotData, которую, возможно, тоже удастся ускорить. Но выделенное нам для тестирования время на этом закончилось и лучших результатов показать не удалось. Однако и достигнутые результаты имеют большую ценность, т.к. был достигнут существенный прирост в производительности.

13.75% postgres       [.] GetSnapshotData
4.88%  postgres       [.] AllocSetAlloc
2.47%  postgres       [.] LWLockAcquire
2.11%  postgres       [.] hash_search_with_hash_value
2.02%  postgres       [.] SearchCatCache
2.00%  postgres       [.] palloc
1.81%  postgres       [.] base_yyparse
1.69%  libc-2.17.so   [.] __memset_power7
1.63%  postgres       [.] LWLockRelease
1.56%  libc-2.17.so   [.] __memcpy_power7
1.33%  postgres       [.] _bt_compare
0.99%  postgres       [.] core_yylex
0.99%  postgres       [.] expression_tree_walker

Рис.10 Пример вывода perf top во время тестирования после применения экспериментального патча pinunpin-lwlock-asm.

Следует заметить, что не только наша команда занимается вертикальной масштабируемостью постгреса. Общее направление в сторону более экономичного использования блокировок существует, об этом, в частности, рассказывалось на октябрьской конференции pgconf.eu в докладе Андреса Фройнда). В том числе, как было уже упомянуто, в версию 9.5 вошли оптимизации LWLock’ов.

Выводы


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

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

OpenSource IBM Advanced Toolchain (которая по сути является версией gcc с оптимизированными под PPC runtime-библиотеками ) показал большую производительность, чем ванильный gcc и рекомендуется для сборки. XLC под linux имеет пока не решенные проблемы с PostgreSQL и не рекомендуется.

PostgreSQL показал уверенную работу в LPAR. Hardware partitioning при помощи PowerVM оказался мощной и удобной системой виртуализации, при помощи которой можно наращивать производительность отдельного LPAR и находящегося внутри него сервера PostgreSQL.

SMT в Power8 может дать более чем двукратный прирост производительности на задачах, аналогичных тем, на которой производилось наше тестирование.

Последний патч на asm позволил достичь суммарной производительности более 2 млн транзакций в секунду на 48 ядер Power8. Достигнуто это было на двух экземплярах PostgreSQL запущенных одновременно на разных вычислительных узлах.

На практике это означает, что вам не придется задумываться о том, насколько PostgreSQL масштабируется по ядрам до тех пор, пока вы не достигнете производительности в 1млн транзакций в секунду на топовом железе, или не уткнетесь в часто используемую страницу памяти (например, верхушку B-дерева). Как правило, наши клиенты с > 100 физических ядер не испытывают проблем с масштабированием PostgreSQL и описанных здесь эффектов не чувствуют.

Благодарности


Данное исследование было проведено в рамках деятельности российского вендора PostgreSQL — компании Postgres Professional, участника программы IBM Partner World. Авторы благодарят московское отделение IBM и лично Дениса Сосновцева и Александра Лунякова за проявленный интерес к исследованиям Postgres Professional и содействие в получении доступа к технике для тестирования, и Ивана Гончарова за ценные консультации по её использованию, а также вице-президента IBM Терри Вирниг за поддержку и персонал клиентского центра IBM в Пукипси и Кембриджского инновационного центра IBM за организацию тестирования.

Также мне хотелось бы поблагодарить моих соавторов — сотрудников Postgres Professional Дмитрия Васильева (aka vadv), который непосредственно прогонял тесты и написал значительную часть данной статьи, и Юрия Журавлёва (aka stalkerg), который разработал первоначальную версию патча для перевода PinBuffer/UnpinBuffer на атомарные операции.
Проголосовать:
+33
Сохранить: