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

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

Немного контекста

Производительность PostgreSQL напрямую влияет на всю систему. Медленные запросы и блокировки ухудшают пользовательский опыт, увеличивая задержки и вызывая ошибки. Неоптимальное использование CPU, памяти и дискового I/O перегружает сервер, замедляя другие процессы. При росте нагрузки недостаточная масштабируемость превращает СУБД в узкое место, требуя сложных решений вроде шардирования или репликации. Низкая производительность снижает надёжность системы в целом: возникают тайм-ауты, усложняется резервное копирование, нарушается работа микросервисов и очередей.

Любые просадки производительности PostgreSQL становятся поводом для вмешательства перформанс-инженера. Стандартный сценарий выглядит просто: сначала выполняется нагрузочное тестирование (бенчмарк), чтобы воспроизвести проблему, затем применяется профилирование и математическая статистика. Бенчмарк для PostgreSQL подразумевает нагрузку базы данных для измерения производительности и оценки количества транзакций в секунду (tps, transactions per second).

Для получения достоверного ��езультата одного теста недостаточно — приходится проводить множество запусков.

От контекста к проблематике

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

Ситуацию усложняет то, что по графику TPS во времени не всегда очевидно, действительно ли произошла просадка производительности. Казалось бы, можно применить стандартные агрегационные методы статистики — медиану или среднее. Однако они зачастую не дают объективной картины.

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

Поиск альтернативы

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

Например, мы можем взять выборку, отсортировать её на диапазоны (бины), а потом подсчитать количество значений в каждом диапазоне.

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

Таким образом можно перевести изначальный график в формат гистограммы.

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

Как и у любой другой функции, у нее тоже есть входные аргументы и параметры.

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

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

  • Сравнение. Допустим, мы запустили один и тот же сценарий сначала на PostgreSQL 16-й версии, потом — на 17-й, чтобы узнать, есть ли деградация в производительности. Определение типа распределения с параметрами способно в этом помочь.

Но как определить тип распределения? Теоретически это можно сделать вручную. 

Например, можно взять нашу гистограмму и попробовать переложить нормальное распределение на неё. При этом для функции нужны параметры. В качестве примера возьмём mean=9, std=4.

Результат неплохой, но его потенциально можно улучшить. Для этого можно уменьшить стандартное отклонение (mean=9, std=3).

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

Пока есть более значительная проблема — мультимодальность.

Немного о мультимодальности

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

Но когда речь заходит о целых СУБД (например, таких как PostgreSQL), одним системным вызовом не обойдётся:  каждая функция и каждый системный вызов дают определённые нюансы, которые отражаются на гистограмме. В связи с этим график распределения зачастую имеет не один пик (мод), а сразу несколько. 

Такое распределение с несколькими модами называется мультимодальным.

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

Наше решение для работы с мультимодальностью

Чтобы наглядно рассказать о работ�� с мультимодальностью, возможных «подводных камнях» и других издержках, рассмотрим реальный кейс из нашей практики.

К нам обратился клиент, который стал замечать просадку в tps во время поиска по таблице users. Сама таблица имела два формата:

  • в части строк имя и фамилия пользователей были разделены на две колонки (в одной — имя, в другой — фамилия);

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

Первый формат можно считать хорошими данными, второй — плохими.

Чтобы разобраться с проблемой, мы, в первую очередь, постарались воспроизвести её у себя. Для этого провели нагрузочное тестирование: 30 секунд циклично обращались к хорошим данным, 30 секунд — к плохим. В итоге получили следующий результат работы бенчмарков.

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

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

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

Дискретизация

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

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

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

Представление данных

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

Например, так выглядела наша исходная гистограмма.

Но она могла выглядеть и иначе. Например, начальное количество бинов могло бы быть в три раза меньше.

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

Решение, которое позволяет обойти проблему с бинами в качестве параметров, исключить случайность и потерю информации придумал Андрей Акиншин. 

Примечание. Подробнее с идеей и реализацией Андрея Акиншина можно ознакомиться здесь.

Его подход строится на основе методе квантильной оценки Харрелла—Дэвиса.

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

На этот график можно нанести квантиль — значение, которое делит исходное распределение на интервалы с одинаковыми вероятностями. Квантиль уровня 0,5, например, делит график пополам.

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

В случае процентиля выборка делится уже на 100 частей.

Классический подход к поиску квантиля достаточно грубый. Если в выборке из десяти значений нужно найти 25-й процентиль — число, ниже которого лежит 25% данных, — берут второе или третье значение из отсортированной выборки. Этот метод сильно зависит от конкретных значений и даёт неустойчивый результат.

Идея Харрелла—Дэвиса в том, что мы берём не одно конкретное значение, а всю выборку и даём всем значениям вес: чем ближе значение к искомому квантилю, тем больше его вес; чем значение дальше от него, тем меньше вес. В результате квантиль определяется как взвешенная сумма всех значений. 

Таким образом, гистограмма фактически отображает частоту встречаемости значений в выборке, а квантильная оценка Харрелла—Дэвиса учитывает не частоту, а вероятность, что каждое значение — это квантиль. Соответственно, получается более плавный переход между значениями и более реальное понимание представления данных. 

В итоге, на метода Харрелла—Дэвиса мы смогли перейти к новому представлению данных.

Решение проблемы мультимодальности

После подготовки данных к дальнейшему анализу мы смогли приступить к решению главной проблемы на текущем этапе — разделению исходной выборки на моды.

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

Чтобы корректно определить все моды на нашем графике, мы воспользовались еще одной наработкой Андрея Акиншина.

Примечание. Подробнее с упомянутой наработкой Андрея Акиншина можно ознакомиться здесь.

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

  • моды — пики графиков;

  • горы — кривые графиков;

  • подземные воды — нижние части графиков; 

  • глубоководье — озёра, у которых площадь больше, чем площадь земли под ними. 

Наш случай довольно простой:

  • построили квантильную оценку Харрелла—Дэвиса;

  • нашли все локальные максимумы;

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

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

  • чувствительность;

  • точность.

Сначала остановимся на первом.

Чувствительность позволяет понять, какое озеро считать глубоким. Например, в нашем случае при значении чувствительности 0,5 озеро считается глубоким, если его площадь больше всей площади водной поверхности.

S глубокого водоёма > 0,5 × (S глубокого водоёма + S грунтовых вод)

Вот здесь возникает проблема.

Так, если установить чувствительность слишком низкой, то мы будем выявлять только глобальные структуры данных, зачастую — только одну моду.

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

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

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

Для начала находим самые значимые моды (в нашем случае это вся зеленая зона) и исключаем их влияние.

Далее работаем только с красной частью графика: запускаем алгоритм на ней и получаем результаты. 

Итоговым решением будет объединение этих итераций с двумя модами. При этом шумы не детектируются.

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

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

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

Теперь важно понять, какое распределение и с какими параметрами имеют наши унимодальные выборки.

Начнём с анализа второй выборки.

Упрощение

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

Для удобства сократим количество рассматриваемых распределений до четырёх наиболее распространённых:

  • логнормальное;

  • Гумбеля;

  • Вейбулла;

  • Фреше.

Но важно понимать, что форма каждого распределения зависит от определённого набора параметров. Для одного и того же распределения даже при незначительном отличии параметров форма может сильно меняться.

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

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

Важно это сделать так, чтобы параметры не пришлось перебирать вручную.

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

  • мы загружаем в функцию выборку и некоторое распределение;

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

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

Подобную функцию можно получить из функции правдоподобия.

Примечание. Функция правдоподобия вычисляет плотности распределения для неких чисел и функций. То есть функция правдоподобия — просто значение функции плотности распределения в заданной точке. При этом функция характеризует только вероятность появления какой-то величины.

Рассчитать подобную функцию довольно просто:

  • берём выборку, то есть результаты бенчмарков, которые формируют гистограмму, и принимаем их за Х;

  • считаем правдоподобие каждого элемента и просто перемножаем их.

В результате получаем функцию правдоподобия. 

x = {х1, х2, х3, …, хN}φ(Х) = f(x1)f(x2)*...*f(xN)

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

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

Теперь стоит задача получит�� функцию расстояния из функции правдоподобия. Тут всё просто, поскольку мы приняли два правила:

  • чем функция расстояния меньше, тем лучше;

  • чем функция правдоподобия больше, тем лучше.

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

Функция правдоподобия:

φ(Х) = f(x1)f(x2)*...*f(xN)ln(φ(Х)) = ln(f(x1) + ln(f(x2) + … + ln(f(xN) 

Результат минимизации функции расстояния:

-ln(φ(Х)) = -ln(f(x1) - ln(f(x2) - … - ln(f(xN) 

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

Дальше нам нужно найти параметры, наиболее оптимальные для нашего случая. Чтобы не делать это вручную, можно применить алгоритм минимизации, например, Covariance Matrix Adaptation Evolution Strategy (CMA-ES). 

Выбор в пользу CMA-ES обусловлен преимуществами алгоритма, который:

  • не требует градиента (важно, поскольку не для всех MD-оценок мы можем рассчитать градиент);

  • может быть выбирана из локальных минимумов;

  • поддерживает оптимизацию по большому числу параметров;

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

У алгоритма есть и недостатки; 

  • Для работы алгоритма нужна стартовая точка, то есть набор параметров, с которого начинается минимизация. Нюанс в том, что мы не можем взять случайную стартовую точку: если мы зададим её слишком далеко от минимума, который ищем, то алгоритм не найдёт его.

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

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

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

В нашем случае лучшим было именно логнормальное распределение. 

Через аналогичный пайплайн мы прогнали и первое унимодальное распределение — в первом случае оказалось распределение Гумбеля.

Итоговая гистограмма

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

В итоге мы реализовали инструмент, который действует по уже отработанной и проверенной нами схеме:

  • собираются результаты бенчмарка;

  • результаты прогоняются через джиттеринг;

  • строится квантильная оценка Харрелла-Дэвиса для детектора мультимодальности;

  • детектируются моды;

  • каждая мода параллельно анализируется: для каждой подбираются параметры, каждая прогоняется через критерий согласия;

  • полученные данные объединяются в итоговое распределение. 

Краткое саммари

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

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