Сравнение кэшируемых и некэшируемых последовательностей в версиях Postgres 9.4 и 9.5

PostgreSQL Источник: Postgres Professional

Параметр cache при создании последовательности указывает, какое количество чисел будет выделено и размещено в памяти("закешировано") для более быстрого доступа к этим значениям. При использовании данного параметра нужно понимать следующее: если осуществляется конкурентный доступ к значению последовательности из нескольких сессий, то данное значение последовательности с точки зрения сессий будут возрастать не монотонно, а с возможными разрывами или скачками.

Таким образом, если мы укажем параметр cache равный, допустим, 10, то сессия 1 закеширует себе значения от 1 до 10. Далее, допустим, что сессия 1 выбрала не все значения из интервала 1..10 и завершает транзакцию После этого начинает работать сессия 2, которая следующим значением последовательности получит уже 11, а не число, следующее за последним использованным сессией 1. 

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

Далее, в документации Postgres указано, что использование параметра cache позволяет увеличить производительность при работе с последовательностями.

Для того, чтобы уверенно ответить на  вопрос, приведет ли использование параметра cache к увеличению производительности работы с последовательностями, было проведено сравнительное тестирование на версиях Postgres 9.4 и 9.5.

Тестирование производилось на следующем железе:

1. Процессор Intel(R) Xeon(R) CPU E7-8890 v3 @ 2.50GHz x 4. 72 ядра
2. Оперативная память =   3 ТB

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

Настройки Postgres были "по умолчанию", за исключением параметра checkpoint_segments = 300 для 9.4 и max_wal_size = 4GB для 9.5. 

Тестирование производилось программой pgbench следующим образом:

pgbench postgres -p 5495 -j cli -c cli -T TEST_TIME -n -f test_name

где cli - количество клиентов, TEST_TIME - время теста в секундах, test_name - имя скрипта

Последовательности myseq1 и myseq2 создавались следующим образом:

CREATE SEQUENCE myseq1 START 1;
CREATE SEQUENCE myseq2 START 1 cache 100;
 

Тестирование проводилось с использованием следующих двух видов транзакций:

Транзакция вида (1):
begin
   select nextval('имя_последовательности');
end;
 
 
Транзакция вида (2):
begin
   select seq_bulk('имя_последовательности');
end;
 

где функция seq_bulk имеет следующий вид:

CREATE OR REPLACE FUNCTION seq_bulk (seq_name text)  RETURNS bigint AS $body$
declare
      ind int;
      s bigint;
begin
      for ind in 1..1000000
      loop
            s:=nextval(seq_name);
      end loop;
      return s;
end;
$body$
LANGUAGE PLPGSQL
SECURITY DEFINER;
 
 

 Одновременно с работой pgbench снималась статистика использования ресурсов программой perf:

perf record -o имя_файла_статистики_perf pgbench postgres -p 5495 -j $cli -c $cli -T ${TEST_TIME} -n -f ${test_name}.sql

При каждом своем запуске pgbench  работал в течении 60 секунд.

 Результаты тестирования

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

 
Рисунок 1. График зависимости количества транзаций в секунду для транзакций вида (1)

 График показывает следующее: начиная с некоторого количества клиентов(~120 для версии 9.5) скорость работы с последовательностью снижается. Это связано с тем, что процессорные ресурсы, используемые pgbench (который в наших тестах был запущен на том же сервере, что и БД postgres), становятся равными ресурсам, которые используют процессы postgres, вследствие чего начинает снижаться производительность БД

 А вот как выглядит ситуация с использованием транзакций вида (2):


Рисунок 2. График зависимости количества транзаций в секунду для транзакций вида (2)

 Таким образом, использование параметра cache действительно может заметно повлиять на производительность, но лишь при достаточно предельных случаях, например:

  1. Работа более чем 40 сессий с последовательностью и на скоростях порядка 300 000 транзакций в секунду.
  2. Массовая загрузка в таблицы со столбцами, имеющими значение по умолчанию nextval().
  3. Массовое обновление содержимого таблицы со столбцами nextval().

 Объяснение результатов тестирования для транзакций вида (1)

Как было уже сказано,  в случае транзакций вида (1) на результатах производительности начинает заметно сказываться то, что программа pgbench была запущена на том же сервере, где располагалась тестируемая БД.

Однако можно заметить, что скорость работы линейно растет с увеличением количества сессий, работающих с последовательностью. Данный рост происходит до количества сессий ~40. Дальнейший рост производительности некеширующих последовательностей становится существенно нелинейным, достигая своего максимума при количестве сессий < 70 . С кеширующими же последовательностями, напротив, относительная линейность продолжается вплоть до значений порядка 90-120 сессий.

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

Последовательность
Количество сессий(9.4)
Количество сессий(9.5)
cache=16080
cache=10090120

Таким образом, можно сделать вполне однозначный вывод, что в случае транзакций вида (1) кешируемые последовательности работают не медленнее, чем некешируемые, вплоть до количества сессий меньше или равных количеству процессорных ядер в сервере. Начиная же с некоторого количества сессий(~40), работающих с последовательностью, кешируемые последовательности начинают опережать по производительности некешируемые.

К сожалению, из-за ограничения в вычислительных мощностях, предоставленных для тестирования, а именно отсутствии второго сервера ,по мощности сопоставимого с сервером, на котором работала тестируемая БД, нельзя сделать однозначный вывод о дальнейшем линейном росте скорости выборки nextval() значений при превышении количества сессий числа процессорных ядер для транзакций вида (1).

Объяснение результатов тестирования для транзакций вида (2)

Анализируя отчеты команды perf в отношении тестов транзакций вида (2), можно попытаться объяснить форму полученных графиков на рисунке 2.

Например, вот часть отчета perf для транзакций вида (2) в отношении последовательности с cache=1 для 72 сессий работающих с последовательностью одновременно:

# To display the perf.data header info, please use --header/--header-only options.
#
# Samples: 12M of event 'cycles'
# Event count (approx.): 2319157196125
#
# Children      Self         Command         Shared Object                                          Symbol
# ........  ........  ..............  ....................  ..............................................
#
    27.66%    27.54%        postgres  postgres              [.] s_lock                                    
                  |
                  --- s_lock
...
     9.14%     9.11%        postgres  postgres              [.] LWLockAcquire                             
                  |
                  --- LWLockAcquire
...
     4.46%     4.44%        postgres  postgres              [.] LWLockRelease                             
                  |
                  --- LWLockRelease
     4.00%     3.99%        postgres  postgres              [.] LWLockDequeueSelf                         
                  |
                  --- LWLockDequeueSelf

...

Видно, что большое количество времени процессы postgres проводят в ожидании освобождения ресурсов, требуемых для обновления состояния последовательности. Также видно, что активно используется функция s_lock(), которая ответственна за получение контроля над значением объекта "последовательность".

Вот, например, как выглядит зависимость времени, проведенного в s_lock() от количества клиентов:


Рисунок 3.

А вот как выглядит  отчет perf для транзакций вида (2) в отношении  последовательности с cache=100 для 72 сессий, работающих с последовательностью одновременно:

# To display the perf.data header info, please use --header/--header-only options.
#
# Samples: 23M of event 'cycles'
# Event count (approx.): 17062917032135
#
# Children      Self        Command         Shared Object                                          Symbol
# ........  ........  .............  ....................  ..............................................
#
    57.46%    57.29%       postgres  postgres              [.] LWLockAcquire                             
                 |
                 --- LWLockAcquire
    33.02%    32.95%       postgres  postgres              [.] LWLockRelease                             
                 |
                 --- LWLockRelease
...
    0.00%     0.00%       postgres  postgres               [.] s_lock                                    
                 |
                 --- s_lock

Видно, что, по сравнению с некешируемой последовательностью, фукция s_lock() используется гораздо реже. Исследуя исходные коды для функции nextval(), можно сказать, что такое поведение для кешируемой последовательности вытекает из самого алгоритма получения следующего значения. В случае с кешируемой последовательностью данный алгоритм предлагает получение этого значения без блокировки этой самой последовательности. Такое поведение мы и видим в отчетах perf.

Выводы

Кешируемые последовательности работают не медленнее, чем некешируемые. В некоторых случаях скорость их работы выше, чем у некешируемых. Однако в погоне за производительностью не стоит забывать, что приложение должно понимать, что работает с кешируемыми последовательностями, так как значение последовательности может расти скачками.

Также очевидно, что данное тестирование описывает предельные случаи при работе со значениями последовательности, однако не стоит забывать что вычислительная мощность компьютеров растет в экспоненциальной зависимости. Опять-таки, нужно помнить и о том, что с каждым годом растет и количество ядер на одном процессоре. Поэтому, если сегодня результаты данного теста слабо могут применяться на практике из-за экстремальных значений, полученных в его результате его проведения, то завтра, при наличии in-memory баз данных или же очень большого количества процессоров, данные результаты станут более актуальными.