Примеры реальных патчей в PostgreSQL: часть 2 из N

в 14:45, , рубрики: C, open source, postgresql, базы данных, Блог компании Postgres Professional, разработка

Примеры реальных патчей в PostgreSQL: часть 2 из N - 1

В предыдущих статьях мы рассмотрели процесс разработки 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

Источник

Поделиться новостью

* - обязательные к заполнению поля