В предыдущих статьях мы рассмотрели процесс разработки PostgreSQL, а также примеры некоторых реальных патчей, принятых в эту РСУБД за последнее время. При этом рассмотренные патчи были, прямо скажем, какие-то «несерьезные» — исправление опечаток, исправление простейших косяков, найденных при помощи статического анализа, и прочее в таком духе.
Сегодня мы рассмотрим примеры уже более серьезных патчей, устраняющих узкие места в коде, исправляющих достаточно серьезные баги, относительно крупные рефакторинги, и так далее. Как и ранее, основная цель статьи — не столько осветить изменения, принятые в PostgreSQL 9.6, сколько показать, что разработка open source проектов, в частности PostgreSQL, это интересно и не так сложно, как вам это может казаться.
Если эта тема вам интересна, прошу под кат.
6. Ускорение ResourceOwner'а для большого количества ресурсов
ResourceOwner — это объект (насколько слово «объект» применимо для процедурного языка C), предназначенный для управления ресурсами в процессе выполнения SQL-запросов. На каждую транзакцию и субтранзакцию создается отдельный ResourceOwner. ResourceOwner имеет множество методов вроде RememberLock / ForgetLock, RememberFile / ForgetFile и подобных. Кроме того, ResourceOwner'ы можно выстраивать в иерархии. В случае отката транзакции в силу любой причины (пользователь сказал rollback, возникла исключительная ситуация, и т.д.) мы просто освобождаем ResourceOwner, а это освобождение в свою очередь приводит к освобождению всех занятых ресурсов как в данном ResourceOwner'е, так и в его «детях». Подробности можно найти в соответствующем файле README.
В 9.5 для хранения ресурсов ResourceOwner использовал массивы. При этом предполагалось, что ресурсы обычно освобождаются в порядке обратном тому, в котором они выделялись, поэтому методы Forget* искали ресурсы с конца массива. На практике, однако, оказалось, что этот подход не всегда хорошо работает. Так профайлинг показал, что при выполнении простейших SELECT-запросов к таблице с большим количеством партиций при таком подходе PostgreSQL проводит 30% всего времени в этих самых Forget* методах.
Устранить bottleneck удалось, заменив массивы на хэш-таблицы. При этом, если количество ресурсов в ResourceOwner'е невелико, то используются массивы, как и раньше:
/*
* ResourceArray is a common structure for storing all types of resource IDs.
*
* We manage small sets of resource IDs by keeping them in a simple array:
* itemsarr[k] holds an ID, for 0 <= k < nitems <= maxitems = capacity.
*
* If a set grows large, we switch over to using open-addressing hashing.
* Then, itemsarr[] is a hash table of "capacity" slots, with each
* slot holding either an ID or "invalidval". nitems is the number of valid
* items present; if it would exceed maxitems, we enlarge the array and
* re-hash. In this mode, maxitems should be rather less than capacity so
* that we don't waste too much time searching for empty slots.
*
* In either mode, lastidx remembers the location of the last item inserted
* or returned by GetAny; this speeds up searches in ResourceArrayRemove.
*/
typedef struct ResourceArray
{
Datum *itemsarr; /* buffer for storing values */
Datum invalidval; /* value that is considered invalid */
uint32 capacity; /* allocated length of itemsarr[] */
uint32 nitems; /* how many items are stored in items array */
uint32 maxitems; /* current limit on nitems before enlarging */
uint32 lastidx; /* index of last item returned by GetAny */
} ResourceArray;
Этот же патч включает в себя рефакторинг ResourceOwner. Раньше для каждого типа ресурсов использовался отдельный массив File'ов, HeapTuple'ов и так далее. Все эти типы являются либо указателями, либо целыми числами, и потому могут быть сохранены в Datum (местный аналог uintptr_t). Была введена новая сущность ResourceArray, позволяющая хранить любые ресурсы, что избавило от существенного количества дублированного кода.
Коммит: cc988fbb0bf60a83b628b5615e6bade5ae9ae6f4
Обсуждение: 20151204151504.5c7e4278@fujitsu
7. Партицирование freelist'а для разделяемого dynahash
Dynahash (см файл dynahash.c) — это местная реализация хэш-таблиц. Хэш-таблицы в PostgreSQL могут вести себя сильно по-разному в зависимости от флагов, с которыми они были созданы. Например, они могут жить как в локальной памяти процесса, так и в разделямой памяти. В случае использования последней разделяемая память отображается на одни и те же виртуальные адреса во всех процессах PostgreSQL. Выделяется разделяемая память один раз и количество этой памяти не может быть изменено в процессе работы РСУБД.
В силу этих причин для отслеживания свободной памяти в разделяемых хэш-таблицах используется так называемый freelist — список свободных кусков памяти небольшого размера. При освобождении памяти она добавляется во freelist. Когда нужно выделить память, она берется из freelist'а. Так как доступ к разделяемой хэш-таблице осуществляется сразу несколькими процессами, доступ к freelist синхронизируется с помощью спинлока. Выяснилось, что определенных нагрузках возникает lock contention за этот спинлок.
Принятый в итоге патч решает эту проблему следующим образом. Вместо одного freelist'а используется несколько (32), каждый со своим спинлоком.
Было:
struct HASHHDR
{
slock_t mutex; /* unused if not partitioned table */
long nentries; /* number of entries in hash table */
HASHELEMENT *freeList; /* linked list of free elements */
/* ... */
Стало:
#define NUM_FREELISTS 32
typedef struct
{
slock_t mutex; /* spinlock */
long nentries; /* number of entries */
HASHELEMENT *freeList; /* list of free elements */
} FreeListData;
struct HASHHDR
{
FreeListData freeList[NUM_FREELISTS];
/* ... */
По умолчанию для выделения памяти используется freelist, номер которого определяется по младшим битам хэш-значения от ключа:
#define FREELIST_IDX(hctl, hashcode)
(IS_PARTITIONED(hctl) ? hashcode % NUM_FREELISTS : 0)
Однако если память в «нашем» freelist'е закончилась, она «заимствуется» из других freelist'ов.
Помимо прочего, патч интересен тем, что перед его принятием мне пришлось написать около 15-и его версий, фактически перебрав все возможные стратегии шардирования freelist'ов, их количество, и прочие параметры, выбрав один вариант, показавший наилучшую производительность. Например, вместо 32-х спинлоков, используемых в окончательной реализации, можно было бы использовать один RWLock, захватываемый на чтение, если мы хотим взять память из «нашего» freelist'а, и на запись — если позаимствовать из других. Плюс спинлоки можно по-разному расположить в памяти, с выравниванием или без выравнивания по размеру кэшлайна, и так далее.
Коммит: 44ca4022f3f9297bab5cbffdd97973dbba1879ed
Обсуждение: 20151211170001.78ded9d7@fujitsu
8. Поддержка нескольких итераторов в RB-деревьях
Работая над очередной фичей, я заметил, что интерфейс итерации по красно-черным деревьям (на данный момент они используются исключительно в GIN-индексах) в PostgreSQL выглядит следующим образом:
void rb_begin_iterate(RBTree *rb, RBOrderControl ctrl);
RBNode *rb_iterate(RBTree *rb);
Можно заметить, что этот интерфейс не позволяет создавать больше одного итератора по дереву, что довольно неудобно. Более того, реализация была весьма странной. Например, она хранила состояние итерации в узлах дерева.
Подумав немного, я переписал все это хозяйство, после чего интерфейс получился следующим:
void rb_begin_iterate(RBTree *rb, RBOrderControl ctrl, RBTreeIterator *iter);
RBNode *rb_iterate(RBTreeIterator *iter);
Узнать больше о различных контейнерах, используемых в PostgreSQL, вы можете из статьи Не унылый пост о списках и деревьях поиска в языке C. Кроме того, вас может заинтересовать GitHub-репозиторий, созданный мной в процессе работы над этой задачей. В нем вы найдете реализацию одно- и двусвязных списков, красно-черных деревьев и хэш-таблиц на языке C. Библиотека обильно покрыта тестами и распространяется под лицензией MIT/BSD.
Коммит: 9f85784cae4d057f307b83b0d33edede33434f04
Обсуждение: 20160727172645.3180b2e0@fujitsu
9. Исправление валидации чексумм в pg_filedump для таблиц с несколькими сегментами
PostgreSQL хранит данные таблиц и индексов в так называемых страницах. Размер одной страницы по умолчанию равен 8 Кб. Страницы хранятся в файлах на диске, называемых сегментами. Размер одного сегмента по умолчанию равен 1 Гб. Нарезание отношений и индексов на сегменты позволяет PostgreSQL работать даже на файловой системе, не поддерживающей файлы размером более 1 Гб. При помощи страниц реализуется кэширование часто используемых данных в памяти так называемым buffer manager'ом, что существенно сокращает количество обращений к диску.
Утилита pg_filedump позволяет делать разные полезные вещи с сегментами и страницами. Например, она может проверить чексуммы всех страниц в сегменте. Чексуммы пишутся в страницы, если база данных была создана путем вызова initdb с флагом -k:
-k, --data-checksums use data page checksums
Интересно, что процедура pg_checksum_page, вычисляющая хэш-функцию страницы, зависит не только от содержимого страницы, но и от номера блока:
uint16 pg_checksum_page(char *page, BlockNumber blkno)
Это позволяет убедиться, что страница не только хранит правильные данные, но и записана по правильному смещению в сегменте.
Так вот, недавно в pg_filedump был обнаружен такой баг. Чексуммы правильно проверялись для нулевого сегмента, но для первого, второго и так далее сегментов чексуммы, считаемые pg_filedump, не сходились с теми, что посчитал сам PostgreSQL. Как выяснилось, для любого сегмента pg_filedump начинал считать номера блоков с нуля. Правильный же способ заключается в том, чтобы учитывать все предыдущие сегменты, и использовать для данного сегмента «абсолютные» номера блогов, а не «относительные».
В силу понятных причин, в этом же патче в pg_filedump была добавлена поддержка двух ранее отсутствовавших флагов:
-s Force segment size to [segsize]
-n Force segment number to [segnumber]
Коммит: 052ed0112967dd1e9b0e2cbe54821c04475f1a3a
Обсуждение: (исключительно offlist)
10. Проверка значения, возвращаемого процедурами malloc(), realloc() и прочими
Напоследок я решил оставить патч, написанный не мной, но для которого я выступал в качестве reviewer'а. В процессе code review мною было предложено немало улучшений для данного патча.
Michael Paquier обратил внимание на то, что в ряде мест PostgreSQL не проверяет коды возврата процедур malloc(), realloc() и strdup(). В ходе работы над патчем список процедур был дополнен calloc(), а также процедурами для работы с разделяемой памятью.
В результате там, где это возможно, вызовы были заменены на аналогичные безопасные PostgreSQL-аналоги — pg_strdup, pg_malloc и прочие:
- steps = malloc(sizeof(Step *) * nsteps);
+ steps = pg_malloc(sizeof(Step *) * nsteps);
В остальных местах были просто добавлены проверки:
new_environ = (char **) malloc((i + 1) * sizeof(char *));
+ if (!new_environ)
+ {
+ write_stderr("out of memoryn");
+ exit(1);
+ }
См также пост самого Michael — Postgres 10 highlight — ShmemAlloc and ShmemAllocNoError.
Коммиты: 052cc223, 6c03d981
Обсуждение: CAB7nPqRu07Ot6iht9i9KRfYLpDaF2ZuUv5y_+72uP23ZAGysRg@mail.gmail.com
Продолжение следует...
Конечно, при условии, что подобного рода посты представляют для кого-то интерес :) Возможно, мне также удастся уговорить кого-нибудь из коллег осветить патчи, над которыми они работали в последнее время. Ведь кто сможет рассказать о патче лучше самого разработчика этого патча?
Как всегда, я с нетерпением жду ваших вопросов, и буду рад ответить на них в комментариях. И вообще, не стесняйтесь оставлять любые комментарии и дополнения!
Автор: Postgres Professional