Большинство материалов о компрессии в PostgreSQL отвечают на вопрос «во сколько раз удалось уменьшить базу». Мы предлагаем посмотреть на проблему с другой стороны — какой ценой достигается эта экономия? В статье разбираем архитектурные компромиссы различных подходов к компрессии страниц, объясняем, почему при разработке CSM в Tantor Postgres отказались от погони за максимальным коэффициентом сжатия, и показываем результаты нагрузочных испытаний на реальных базах 1С.

Когда речь заходит о компрессии данных в PostgreSQL, разговор почти всегда сводится к коэффициенту сжатия. Вендоры показывают красивые цифры: база становится меньше в три, пять или даже восемь раз, и подразумевается, что в целом в эти же 3–8 раз снизится нагрузка на файловую систему. Но это не всегда так, и стремление добиться максимального коэффициента компрессии само по себе может оказаться ложной целью. Более важными показателями могут явиться коэффициенты усиления записи (write amplification factor) и усиления чтения (read amplification factor). Далее мы покажем, что уменьшение размера базы в k раз не гарантирует уменьшения записи в k раз. Скорее, это идеальное значение, к которому всё только стремится.
Оглавление
Компрессия страниц переменного размера и фиксированного размера
Алгоритмы сжатия: по отдельности PGLZ, LZ4, ZSTD и их сравнение
-
Небольшая таблица (2,6 GB → 326 MB) и большая таблица (95 GB → 12 GB)
Объекты сжатия
Существующие технологии сжатия в PostgreSQL оперируют объектами двух типов — это либо значения типов данных переменной длины (varlena) либо страницы таблиц и индексов.
Сжатие объектов varlena имеет простую организацию, не нарушающую общую идеологию хранения значений. Заголовок varlena содержит специальные поля, указывающие на параметры сжатия за которыми следуют сжатые данные. Основной недостаток такого подхода — ограниченность применения, а именно только большие значения переменной длины (как, правило, TOAST).
У страничного сжатия есть существенные преимущества. Во‑первых, расширяется возможность применения — практически все данные в Postgres имеют страничную организацию. Во‑вторых, размер страницы обычно значительно больше размера varlena значения, а это повышает эффективность сжатия.
Второй вариант интереснее, поскольку именно он позволяет полноценно сжимать таблицы и индексы.

Сжатие выполняется на уровне менеджера хранения, то есть модуля PostgreSQL, ответственного за чтение и запись страниц. При чтении данных процесс считывает сжатую страницу с диска, распаковывает ее и только после этого помещает в shared_buffers. Все дальнейшие операции, использующие shared_buffers, работают со стандартной 8-килобайтной страницей и не требуют дополнительных действий. При изменении страницы ситуация аналогична. После выполнения UPDATE в памяти находится уже несжатая страница. Повторное сжатие выполняется только при записи данных на диск фоновыми процессами background writer или checkpointer, а в отдельных случаях — непосредственно backend‑процессом, если ему требуется вытеснить «грязную» страницу из буферного кэша.
Таким образом, дополнительная нагрузка на процессор возникает только в двух случаях:
при чтении страницы с диска — на этапе распаковки;
при записи страницы на диск — на этапе сжатия.
Подытоживая сказанное, компрессия применяется при хранении страниц на диске, а в оперативной памяти страницы всегда находятся в обычном несжатом виде размером 8 КБ.
Рассмотрим далее распространенные подходы к сжатию страниц и связанные с ними архитектурные особенности, а затем покажем, почему при разработке CSM (Compression Storage Manager) в Tantor Postgres был выбран именно такой вариант реализации и как он проявляет себя на реальных базах данных.
А что еще можно сжимать помимо таблиц и индексов?
Кроме основных данных можно сжимать, например, WAL журнал или трафик между клиентом и сервером. Работа с такими данными имеет свою специфику и во многом упрощается отсутствием необходимости вносить изменения в однажды записанную информацию.
Компрессия страниц переменного размера
Если рассматривать компрессию с точки зрения экономии дискового пространства, то наиболее привлекательным выглядит подход, при котором каждая страница после сжатия занимает ровно столько места, сколько удалось получить в результате компрессии. Если одна страница после компрессии занимает 1,2 КБ, а другая — 2,5 КБ, то столько они и будут занимать в файле. Дисковое пространство используется эффективно, выходит неплохой коэффициент сжатия, и на первый взгляд такое решение выглядит оптимальным.
Однако компрессия в этом случае затрагивает не только объем данных, но и сам механизм хранения страниц. В классическом PostgreSQL все страницы имеют фиксированный размер 8 КБ, благодаря чему их положение на диске определяется простым вычислением по номеру блока. После перехода к страницам переменного размера это свойство теряется, потому что для поиска страницы уже недостаточно знать ее номер, необходимо определить ее фактическое расположение в файле.
Одним из способов решения этой задачи является использование дополнительной структуры адресации, которая хранит информацию о положении каждой страницы. Именно такой принцип реализован в Compressed File System (CFS). Появление дополнительной структуры адресации само по себе не является каким‑либо недостатком, это естественная плата за возможность хранить страницы переменного размера. Однако вслед за этим возникают особенности, которые неизбежно начинают влиять на эксплуатационные характеристики системы.

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

Бороться с фрагментацией можно по‑разному. Например, можно периодически выполнять компактификацию данных, время от времени перекладывая страницы внутри файла. Или выделять место не произвольного размера, а блоками фиксированной длины, так вероятность повторного использования свободных областей будет выше. Оба варианта требуют дополнительных операций, которых нет в классическом механизме хранения PostgreSQL.
Есть и еще один эффект, менее очевидный, но более важный. Появление указателя на страницу крайне негативно влияет на коэффициент усиления записи SSD‑дисков.

Дело в том, что SSD пишет данные блоками по 4 КБ (у некоторых моделей значение может быть выше).

Здесь об этом чуть более углубленно
SSD‑диск разбит на страницы размером от 4 до 32 КБ в зависимости от модели, а страницы, в свою очередь, объединены в блоки (обычно по 128 страниц). Запись происходит поэтапно: стирается блок, а затем в него записываются страницы. То есть, если приложение пишет 1 байт, то фактически SSD перезаписывает 4КБ. Вывод очевиден: для SSD хорошо, если приложение пишет крупными блоками и делает это через append (этим требованиям, например, соответствует журнал WAL).
Можно даже прикинуть коэффициент усиления записи:

Значение в 1.5 раз хуже в сравнении с записью 8 КБ без сжатия. Можно пытаться оптимизировать и, например, за один раз записывать указатели сразу для нескольких страниц, но это не всегда возможно. Причём сжатые данные могут затрагивать две страницы SSD, в этом случае WAF = 2 (в два раза хуже записи 8KБ без сжатия).
Как вывод — подход позволяет получить максимальный коэффициент компрессии, но при этом в 1,5 — 2 раза увеличивается нагрузка на диск.
Компрессия страниц фиксированного размера
Если предыдущий подход стремится получить максимальный коэффициент компрессии, то при разработке CSM (Compression Storage Manager) в Tantor Postgres для нас было приоритетом улучшение всех существенных показателей менеджера хранения, а именно:
размер дискового пространства;
коэффициенты усиления записи и чтение;
использование CPU и ОЗУ.
Для этого мы сохранили основные свойства механизма хранения PostgreSQL. Идея заключается в том, что главный файл отношения (таблицы или индекса) по прежнему имеет фиксированный размер страницы, но меньшего размера 4 КБ, 2 КБ или 1 КБ. Размер страницы подбирается таким образом, что в большинстве случаев его было бы достаточно для хранения сжатых данных.

Размер страницы задается через параметр хранения compression_page...
CREATE TABLE foo (int id, varchar name) WITH (compression = lz4, compression_page = 4096);
.. и может быть впоследствии изменён:
ALTER TABLE foo SET ( compression_page = 2048);
Могут быть страницы, которые превысят ограничение, в этом случае не вместившиеся данные размещаются в специальной области хранения (overflow). Но таких случаев должно быть немного, и, как следствие, возникающие издержки будут незначительными.
В результате положение большинства страниц снова можно определить по их номеру без дополнительных структур адресации. Это позволяет сохранить простую организацию хранения данных и избежать большинства проблем, возникающих при использовании страниц переменного размера.
Теперь, если мы посмотрим на коэффициент усиления записи для страницы размером 4096 байт, то увидим, что в Tantor Postgres 18 значение WAF для сжатых отношений окажется в два раза лучше по сравнению с записью без сжатия.

Эти рассуждения подводят нас к формированию рекомендаций по выбору размера страницы и алгоритма сжатия:
Если выполняется активная запись в таблицу, то следует выбрать размер страницы 4 КБ и алгоритм сжатия LZ4, предполагающий минимальную нагрузку на процессор. Накладные расходы на сжатие будут минимальным, при этом мы получим двукратное уменьшение объёма и двукратное снижение нагрузки на диск, а также уменьшение использования оперативной памяти для кеширования файла операционной системой.
Если данные хорошо сжимаются и редко изменяются, следует использовать страницы 2 KБ. В этом случае при выборе алгоритма шифрования следует учитывать, какое влияние он оказывает на долю страниц, использующих overflow блоки. Если LZ4 справляется с задачей упаковки данных в 2K, то используем его, иначе смотрим в сторону алгоритма zstd.
Наконец, в редких случаях для хорошо сжимаемых данных следует использовать compression_page = 1024.
Алгоритмы сжатия
В настоящее время CSM поддерживает несколько алгоритмов сжатия, отличающихся как степенью компрессии, так и требованиями к вычислительным ресурсам.
PGLZ
PGLZ — исторический алгоритм PostgreSQL, используемый уже много лет. Основные его преимущества — простота реализации и минимальные требования к процессорным ресурсам. Степень компрессии PGLZ по современным меркам относительно невысока, и на большинстве реальных БД он заметно уступает более современным алгоритмам.
Преимущества:
низкая нагрузка на CPU;
встроенная поддержка PostgreSQL;
стабильное и предсказуемое поведение.
Недостатки:
сравнительно низкий коэффициент сжатия.
LZ4
LZ4 ориентирован на максимальную скорость работы, обеспечивает очень быстрое сжатие и распаковку при сохранении хорошего уровня компрессии. На практике именно LZ4 часто становится оптимальным выбором для высоконагруженных OLTP‑систем, где важны минимальные накладные расходы на обработку данных.
Преимущества:
высокая скорость сжатия;
очень высокая скорость распаковки;
низкая нагрузка на CPU;
хороший коэффициент компрессии.
Недостатки:
степень сжатия обычно ниже, чем у zstd.
zstd
zstd — самый современный алгоритм из поддерживаемых, он ориентирован на достижение максимального коэффициента компрессии. В большинстве случаев именно zstd позволяет получить наименьший размер БД, а вот платой за это становится более высокая нагрузка на процессор при выполнении операций сжатия и распаковки.
Преимущества:
высокий коэффициент компрессии;
эффективная работа с большими объемами данных.
Недостатки:
более высокие требования к процессорным ресурсам.
Сравнение алгоритмов
По степени компрессии выходит следующая последовательность: PGLZ < LZ4 < zstd. Если же сравнивать скорость работы, то получится, что LZ4 > PGLZ > zstd. Выбор алгоритма зависит от поставленной задачи. Если приоритетом является минимальная нагрузка на процессор, имеет смысл рассматривать PGLZ или LZ4, а если нужно сократить объем данных, предпочтительным выбором станет zstd.
В нашем инструменте роли распределены так:
LZ4 — настроен на максимальную скорость;
zstd — на максимальный уровень компрессии;
PGLZ — просто есть:)
В планах — реализация возможности указывать степень компрессии при анализе и сжатии. Это добавит гибкости в выборе алгоритма и, возможно, повлияет на картину сопоставления их эффективности.
Ниже мы сравним эти алгоритмы на реальных данных и посмотрим, насколько теоретические различия проявляются на практике.
Анализ потенциала сжатия
Исходя из архитектуры механизма сжатия CSM? логично предположить, что максимальная степень компрессии будет составлять 8. То есть, теоретически можно достигнуть уровень сжатия в 8 раз. Теоретические коэффициенты мы проверим на реальных базах данных. Проверять будем на базах 1С.
Прежде чем включать компрессию, полезно оценить, какой эффект она даст для конкретной таблицы или индекса. Для этого в расширении pg_csm реализована функция csm_compress_analysis, позволяющая проанализировать существующие страницы отношения и оценить потенциальную степень их сжатия.
Сам механизм сжатия реализован на уровне ядра, однако все аналитические и статистические функции вынесены в расширение pg_csm. Для их использования необходимо один раз подключить расширение к базе: create extension pg_csm
Пример вызова:
select * from csm_compress_analysis('test_table','all',0.01)
Функция принимает три параметра:
oid — oid / имя таблицы или индекса.
alg_name — алгоритм сжатия (all, zstd, lz4, pglz). Можно проанализировать только эффективность одного алгоритма, и это повысит скорость анализа. По умолчанию — 'all'.
sample — коэффициент количества страниц, выбираемых для анализа (>0, <=1). Чем меньше, тем быстрее, но менее точно. По умолчанию = 1.
Пример запроса, показанный чуть выше, анализирует таблицу test_table, используя все алгоритмы компрессии и выборку 1% страниц.
Результат:
relname |
reloid |
page_cnt |
alg_name |
avg_compress |
avg_page_sz |
page_over_1k |
page_over_2k |
page_over_4k |
compressed_relation_size_1k |
compressed_relation_size_2k |
compressed_relation_size_4k |
compress_ms |
test_table |
23 699 795 |
623 |
zstd |
0.2713623 |
2223 |
622 |
619 |
0 |
1 911 808 |
1 909 760 |
2 551 808 |
31.22 |
test_table |
23 699 795 |
623 |
pglz |
0.30786133 |
2522 |
622 |
622 |
0 |
1 911 808 |
1 912 832 |
2 551 808 |
52.74 |
test_table |
23 699 795 |
623 |
lz4 |
0.36462402 |
2987 |
622 |
622 |
0 |
2 101 248 |
2 102 272 |
2 551 808 |
7.86 |
На выходе:
relname — имя анализируемого отношения;
reloid — oid анализируемого отношения;
page_cnt — количество проанализированных страниц (в нашем случае это всего 1% от полного количества);
alg_name — алгоритм;
avg_compress — средний коэффициент сжатия;
avg_page_sz — средний размер сжатых данных;
pages_over_1k — количество страниц, не уместившихся в 1 КБ;
pages_over_2k — количество страниц, не уместившихся в 2 КБ;
pages_over_4k — количество страниц, не уместившихся в 4 КБ;
Добавлено, начиная с версии 18.4:
compressed_relation_size_1k — размер анализируемых данных в байтах, сжатых с использованием размера страницы 1024;
compressed_relation_size_2k — размер анализируемых данных в байтах, сжатых с использованием размера страницы 2048;
compressed_relation_size_4k — размер анализируемых данных в байтах, сжатых с использованием размера страницы 4096;
compress_ms — время в миллисекундах, затраченное процессором на сжатие страниц.
Очень важны поля pages_over_1k, pages_over_2k, pages_over_4k: они показывают, сколько страниц не смогут быть размещены в выбранном размере и будут использовать overflow.
Что такое overflow?
Если после компрессии страница вместе со служебной информацией не помещается в заданный размер (1, 2 или 4 КБ), избыточные данные сохраняются в отдельном файле *_ovr. Такая схема позволяет сохранить фиксированный размер основных страниц, однако обращение к overflow сопровождается небольшими дополнительными накладными расходами. Поэтому желательно, чтобы доля таких страниц оставалась минимальной.
Особенность работы функции csm_compress_analysis — повторяемость результата. При повторном выполнении запроса на тех же данных будут выбраны те же страницы для анализа и, следовательно, получены идентичные результаты.
Практические рекомендации по выбору параметров
Активно используемые данные. При выборе алгоритма и размера страницы нужно опираться на:
процент страниц, использующих overflow, должен быть минимален
время, затрачиваемое на компрессию / декомпрессию, должно быть минимально (обычно это протокол LZ4)
Минимальная нагрузка и сжатие 2х. Используйте страницы размером 4 КБ и алгоритм LZ4. Такой вариант обеспечивает минимальную нагрузку на процессор, практически не использует overflow и позволяет примерно вдвое уменьшить объем данных и нагрузку на дисковую подсистему.
Оптимальная компрессия. Если данные хорошо сжимаются, предпочтительным вариантом обычно становятся страницы 2 КБ. При выборе алгоритма стоит ориентироваться на долю страниц, использующих overflow. Если LZ4 обеспечивает достаточный коэффициент компрессии, предпочтителен именно он. Если overflow используется слишком часто, имеет смысл перейти на zstd, либо выбрать размер сжатой страницы 4 КБ.
Максимальная компрессия (актуально для архивных исторических данных). Страницы размером 1 КБ имеет смысл использовать только для данных с очень высоким коэффициентом компрессии, когда вероятность использования overflow остается небольшой. Алгоритм при этом используется с максимальным коэффициентом компрессии — zstd.
Скрипты анализа
Для анализа отдельной таблицы или индекса достаточно вызова функции:
select * from csm_compress_analysis('test_table', 'all', 0.01)
Однако на практике гораздо удобнее использовать запрос:
Расширенный анализ по таблице
select relname, pg_size_pretty(pg_relation_size(reloid)) AS full_size, page_cnt, pg_size_pretty(page_cnt::bigint 8 1024) AS page_cnt_size, alg_name, avg_page_sz, round(pages_over_4k::numeric / page_cnt * 100, 2) AS pages_over_4k_p, round(pages_over_2k::numeric / page_cnt * 100, 2) AS pages_over_2k_p, round(pages_over_1k::numeric / page_cnt * 100, 2) AS pages_over_1k_p, pg_size_pretty(compressed_relation_size_4k) AS compress_4k, pg_size_pretty(compressed_relation_size_2k) AS compress_2k, pg_size_pretty(compressed_relation_size_1k) AS compress_1k, compress_ms from csm_compress_analysis('test_table', 'all',0.01)
Результат для sample = 0.01 (1%):
relname |
full_size |
page_cnt |
page_cnt_size |
alg_name |
avg_page_sz |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
compress_4k |
compress_2k |
compress_1k |
compress_ms |
test_table |
15 GB |
19 219 |
150 MB |
zstd |
912 |
0.00 |
0.17 |
4.72 |
75 MB |
38 MB |
20 MB |
291.26 |
test_table |
15 GB |
19 219 |
150 MB |
pglz |
1171 |
0.00 |
0.21 |
99.99 |
75 MB |
38 MB |
38 MB |
858.89 |
test_table |
15 GB |
19 219 |
150 MB |
lz4 |
1259 |
0.00 |
0.23 |
99.99 |
75 MB |
38 MB |
38 MB |
55.55 |
Результат позволяет оценить, насколько эффективным будет выбор размера сжатой страницы.
Все вычисляемые значения относятся только к проанализированной выборке. Например, при sample = 0.01 анализируется лишь 1% страниц отношения, поэтому размеры compress_1k, compress_2k и compress_4k будут примерно в 100 раз меньше итоговых значений для всей таблицы. Для удобства в page_cnt_size представлен размер проанализированной выборки. Однако процент страниц, использующих overflow, уже являются репрезентативным в рамках выборки и может использоваться для оценки эффективности.
Дополненный список показателей:
relname — имя отношения;
full_size — полный размер отношения;
page_cnt — количество проанализированных страниц;
page_cnt_size — размер проанализированных страниц отношения;
alg_name — алгоритм компрессии;
avg_page_sz — средний размер сжатых данных;
pages_over_1k_p / pages_over_1k_p / pages_over_4k_p — процент страниц, которые не поместятся в выбранный размер и будут использовать overflow.
compress_1k / compress_2k / compress_4k — объем проанализированных сжатых данных при различных размерах сжатой страницы;
compress_ms — время в миллисекундах, затраченное процессором на сжатие страниц.
Результат анализа демонстрирует интересную особенность выбора алгоритма компрессии. Несмотря на то, что средний размер страницы после сжатия алгоритмом zstd отличается от PGLZ незначительно (1171 байт против 912 байт), именно эти дополнительные байты оказываются критичными.
В результате подавляющее большинство страниц, сжатых PGLZ, с добавленными служебными данными уже не помещается в лимит 1 КБ и часть данных переносится в overflow. Для zstd таких страниц менее 5%, тогда как для PGLZ — практически 100%. Фактически это означает, что при размере сжатой страницы 1 КБ использование PGLZ становится неэффективным, несмотря на близкий средний размер сжатых данных.
В результате выбора PGLZ и размера страницы 1024 будет получен объем сжатых данных как при размере страницы 2048 — очень большой процент страниц в overflow, и сниженная эффективность работы со сжатыми данными.
Именно поэтому при выборе алгоритма недостаточно ориентироваться только на коэффициент компрессии или средний размер страницы. Не менее важно учитывать распределение размеров страниц после сжатия и процент страниц, использующих overflow.
Для анализа всей базы данных можно воспользоваться расширенным запросом, который последовательно анализирует все таблицы и индексы, суммируя результаты по каждому алгоритму компрессии:
WITH c AS ( SELECT a.alg_name, c.relpages, a.page_cnt, a.page_cnt::bigint * 8196 AS analized_size, a.pages_over_1k, a.pages_over_2k, a.pages_over_4k, a.compressed_relation_size_4k * c.relpages / a.page_cnt sz4, a.compressed_relation_size_2k * c.relpages / a.page_cnt sz2, a.compressed_relation_size_1k * c.relpages / a.page_cnt sz1, COALESCE(pg_total_relation_size(c.reltoastrelid), 0) AS toast_size FROM pg_class c CROSS JOIN LATERAL csm_compress_analysis(c.oid, 'all',0.1) a WHERE c.relkind IN ('r','i') ) SELECT pg_size_pretty(pg_database_size(current_database())) AS full_database_size, pg_size_pretty(sum(toast_size)) AS toast_size, sum(page_cnt) AS analized_pages_cnt, pg_size_pretty(sum(analized_size)) AS analized_size, alg_name, pg_size_pretty(sum(sz4)) AS compress_4k, pg_size_pretty(sum(sz2)) AS compress_2k, pg_size_pretty(sum(sz1)) AS compress_1k, round(sum(pages_over_4k)::numeric / sum(page_cnt) * 100, 2) AS pages_over_4k_p, round(sum(pages_over_2k)::numeric / sum(page_cnt) * 100, 2) AS pages_over_2k_p, round(sum(pages_over_1k)::numeric / sum(page_cnt) * 100, 2) AS pages_over_1k_p FROM c GROUP BY alg_name;
Вывод скрипта:
full_database_size — полный размер базы данных
toast_size — полный размер toast данных, которые не будут сжаты (так как сжимаются отдельно)
analized_pages_cnt — количество проанализированных страниц
analized_size — размер проанализированных страниц
alg_name — алгоритм сжатия, используемый при анализе
compress_4k — полный размер сжатых данных при сжатии на страницу 4096 (вычисленный по обратной пропорции)
compress_2k — полный размер сжатых данных при сжатии на страницу 2048 (вычисленный по обратной пропорции)
compress_1k — полный размер сжатых данных при сжатии на страницу 1024 (вычисленный по обратной пропорции)
pages_over_4k_p — процент проанализированных страниц, в сжатом виде не уместившихся на страницу 4096 и потенциально размещаемых в overflow
pages_over_2k_p — процент проанализированных страниц, в сжатом виде не уместившихся на страницу 2048 и потенциально размещаемых в overflow
pages_over_1k_p — процент проанализированных страниц, в сжатом виде не уместившихся на страницу 1024 и потенциально размещаемых в overflow
Интересный эффект — возможность повторного анализа уже сжатой базы данных. В этом случае можно напрямую сравнить прогноз потенциального эффекта сжатия CSM с фактическими результатами и оценить точность анализа.
Примеры анализа различных баз (часть колонок убрана, чтобы упростить чтение таблицы):
ERP (full_database_size = 645 GB, toast_size = 59 GB):
sample = 1 (время выполнения — 2 ч 58 мин):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
76 938 139 |
587 GB |
zstd |
294 GB |
156 GB |
130 GB |
0.60 |
10.43 |
63.68 |
76 938 139 |
587 GB |
lz4 |
295 GB |
172 GB |
170 GB |
1.28 |
25.85 |
97.30 |
76 938 139 |
587 GB |
pglz |
296 GB |
167 GB |
164 GB |
1.55 |
19.14 |
95.39 |
sample = 0.1 (время выполнения — 39 мин 57 сек):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
7 747 202 |
59 GB |
zstd |
294 GB |
156 GB |
130 GB |
0.60 |
10.38 |
63.27 |
7 747 202 |
59 GB |
lz4 |
295 GB |
172 GB |
170 GB |
1.28 |
25.71 |
96.67 |
7 747 202 |
59 GB |
pglz |
296 GB |
167 GB |
164 GB |
1.54 |
19.04 |
94.78 |
sample = 0.01 (время выполнения — 3 мин 43 сек):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
830 416 |
6488 MB |
zstd |
294 GB |
156 GB |
130 GB |
0.56 |
9.86 |
59.50 |
830 416 |
6488 MB |
lz4 |
295 GB |
172 GB |
170 GB |
1.22 |
24.37 |
90.75 |
830 416 |
6488 MB |
pglz |
296 GB |
167 GB |
164 GB |
1.45 |
18.07 |
88.97 |
ЗУП (full_database_size = 51 GB, toast_size = 1982 MB):
sample = 1 (время выполнения — 11 мин 40 сек):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
6 325 661 |
48 GB |
zstd |
24 GB |
12 GB |
8285 MB |
0.05 |
1.13 |
32.82 |
6 325 661 |
48 GB |
lz4 |
24 GB |
13 GB |
12 GB |
0.22 |
18.18 |
83.26 |
6 325 661 |
48 GB |
pglz |
24 GB |
13 GB |
11 GB |
0.22 |
8.98 |
65.81 |
sample = 0.1 (время выполнения — 3 мин 11 сек):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
659 474 |
5155 MB |
zstd |
24 GB |
12 GB |
8287 MB |
0.04 |
1.10 |
31.61 |
659 474 |
5155 MB |
lz4 |
24 GB |
13 GB |
12 GB |
0.22 |
17.50 |
80.00 |
659 474 |
5155 MB |
pglz |
24 GB |
13 GB |
11 GB |
0.14 |
8.66 |
63.26 |
sample = 0.01 (время выполнения — 35 сек):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
93 537 |
731 MB |
zstd |
24 GB |
12 GB |
8283 MB |
0.03 |
0.93 |
23.10 |
93 537 |
731 MB |
lz4 |
24 GB |
13 GB |
12 GB |
0.18 |
13.03 |
57.57 |
93 537 |
731 MB |
pglz |
24 GB |
13 GB |
11 GB |
0.12 |
6.57 |
45.73 |
ДО (full_database_size = 328 GB, toast_size = 41 GB):
sample = 1 (время выполнения — 2 часа 4 мин):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
37 582 106 |
287 GB |
zstd |
143 GB |
74 GB |
66 GB |
0.02 |
6.88 |
77.77 |
37 582 106 |
287 GB |
lz4 |
143 GB |
89 GB |
88 GB |
0.31 |
45.68 |
95.92 |
37 582 106 |
287 GB |
pglz |
143 GB |
82 GB |
81 GB |
0.30 |
28.61 |
92.80 |
sample = 0.1 (время выполнения — 21 мин 36 сек):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
3 769 377 |
29 GB |
zstd |
143 GB |
74 GB |
66 GB |
0.02 |
6.85 |
77.56 |
3 769 377 |
29 GB |
lz4 |
143 GB |
89 GB |
88 GB |
0.31 |
45.55 |
95.66 |
3 769 377 |
29 GB |
pglz |
143 GB |
82 GB |
81 GB |
0.30 |
28.52 |
92.55 |
sample = 0.01 (время выполнения — 2 мин 16 сек):
analized_pages_cnt |
analized_size |
alg_name |
compress_4k |
compress_2k |
compress_1k |
pages_over_4k_p |
pages_over_2k_p |
pages_over_1k_p |
388 962 |
3039 MB |
zstd |
143 GB |
74 GB |
66 GB |
0.03 |
6.70 |
75.37 |
388 962 |
3039 MB |
lz4 |
143 GB |
89 GB |
88 GB |
0.31 |
44.30 |
92.95 |
388 962 |
3039 MB |
pglz |
143 GB |
82 GB |
81 GB |
0.30 |
27.80 |
89.92 |
Примерную зависимость времени анализа от размера выборки можно оценить по приведённым выше результатам. На другом оборудовании абсолютные значения могут отличаться, однако характер зависимости, скорее всего, сохранится. Рекомендуется сначала выполнить анализ с sample=0.01 (1%), чтобы оценить время выполнения и спрогнозировать, сколько займёт анализ при sample=0.1 (10%) и больших значениях sample.
На практике анализ баз данных объемом 50, 230 и 645 ГБ показал, что в большинстве случаев вполне достаточно использовать выборку до 10% страниц (sample = 0.1). Получаемые результаты практически не отличаются от полного анализа, а время выполнения сокращается в несколько раз. Для БД объемом несколько терабайт, вероятно, будет достаточно выборки до 5%, а при необходимости процесс анализа может быть дополнительно распараллелен.
Для получения оценки прогнозируемого полного размера базы в сжатом виде нужно будет к полученному при анализе размеру (compress_1k | compress_2k | compress_4k) прибавить размер toast.
Формула расчета размера базы после сжатия
Размер_базы_после_сжатия = Размер_сжатых_таблиц + toast_size
Как видно из результатов, максимальный эффект компрессии дает алгоритм zstd. В целом и не удивительно, поскольку он сам по себе реализован как алгоритм с высокой степенью компрессии, так еще и мы настроили его на максимальную степень компрессии. Но не нужно забывать, что помимо коэффициента степени сжатия, есть еще скорость сжатия, и есть скорость работы со сжатыми данными, и тут уже лидеры могут поменяться. Этот эффект мы подробно исследуем ниже.
Анализ позволил выявить интересную закономерность. Несмотря на то что, страницы размером 1024 байта обеспечивают наибольший теоретический коэффициент компрессии, именно для них наблюдается наибольший процент страниц, не помещающихся в заданный размер и уходящих в overflow.
В протестированных базах данных наиболее удачным компромиссом оказался размер страницы 2048 байт. Он обеспечивает несколько меньший коэффициент компрессии, чем страницы размером 1024 байта, но при этом значительно сокращает количество страниц, помещаемых в overflow. В результате суммарный объем хранения данных (сжатое отношение + overflow) оказывается меньше, а общая эффективность механизма — выше. Именно поэтому во всех дальнейших нагрузочных испытаниях использовался размер страницы 2048 байт.
Сжатие данных
В предыдущем разделе мы оценили потенциальную эффективность компрессии. Теперь посмотрим, как применить ее на практике. Компрессия включается отдельно для каждой таблицы и индекса командами:
ALTER TABLE <name> SET (compression = <alg_name>, compression_page = <compression_page>) ALTER INDEX <name> SET (compression = <alg_name>, compression_page = <compression_page>)
где:
name — relname или oid;
alg_name — алгоритм компрессии, одно из [zstd | pglz | lz4];
compression_page — размер сжатой страницы [1024 | 2048 | 4096].
Такой подход обеспечивает полный контроль над процессом компрессии и позволяет применять различные настройки для разных объектов базы данных.
Минимально достаточно последовательно выполнить эти команды для всех таблиц и индексов. Однако на практике такой способ оказывается довольно длительным. Например, для базы данных объемом около 50 ГБ последовательное сжатие может занимать несколько часов, а для многотерабайтных баз — уже десятки часов.
Автоматический подбор параметров компрессии
Наиболее удобным вариантом является использование скрипта alter_max_compress.sh, который автоматически анализирует каждое отношение, подбирает оптимальный размер сжатой страницы с учетом допустимого процента overflow и выполняет компрессию параллельно в несколько потоков.
Такой подход позволяет ускорить сжатие базы в разы, и 645 GB сжимается за 1.5 часа.
alter_opt_compress.sh
#!/bin/bash set -euo pipefail # === Параметры по умолчанию === DB_NAME="" DB_USER="postgres" DB_HOST="localhost" DB_PORT="5432" COMPRESSION="zstd" THREADS=20 SAMPLE=0.1 MAX_OVERFLOW_PROCENT=10 TABLES_FILE="tables_list.txt" INDEXES_FILE="indexes_list.txt" usage() { echo "Usage:" echo " $0 -d <database> [-a <compression_alg>] [-t <threads>] [-s <sample>] [-o <max_overflow_percent>]" echo echo "Options:" echo " -d Database name (required)" echo " -a Compression type (default: zstd)" echo " -t Parallel threads count (default: 20)" echo " -s Sample rows for csm_compress_analysis (default: 0.1)" echo " -o Max overflow percent (default: 10)" echo " -u DB user (default: postgres)" echo " -h DB host (default: localhost)" echo " -p DB port (default: 5432)" exit 1 } # === Разбор параметров === while getopts "d:a:t:s:o:u:h:p:" opt; do case $opt in d) DB_NAME="$OPTARG" ;; a) COMPRESSION="$OPTARG" ;; t) THREADS="$OPTARG" ;; s) SAMPLE="$OPTARG" ;; o) MAX_OVERFLOW_PROCENT="$OPTARG" ;; u) DB_USER="$OPTARG" ;; h) DB_HOST="$OPTARG" ;; p) DB_PORT="$OPTARG" ;; *) usage ;; esac done if [[ -z "$DB_NAME" ]]; then echo "ERROR: Database name is required" usage fi echo "==================================" echo "Database: $DB_NAME" echo "Compression: $COMPRESSION" echo "Threads: $THREADS" echo "Sample: $SAMPLE" echo "Max overflow percent: $MAX_OVERFLOW_PROCENT" echo "Host: $DB_HOST" echo "Port: $DB_PORT" echo "User: $DB_USER" echo "==================================" # === Получаем список таблиц === /opt/tantor/db/18/bin/psql \ -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" \ -At -c " SELECT n.nspname || '.' || c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind='r' AND n.nspname NOT IN ('pg_catalog','information_schema','pg_toast'); " > "$TABLES_FILE" # === Получаем список индексов === /opt/tantor/db/18/bin/psql \ -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" \ -At -c " SELECT n.nspname || '.' || c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind='i' AND n.nspname NOT IN ('pg_catalog','information_schema','pg_toast'); " > "$INDEXES_FILE" TOTAL_TABLES=$(wc -l < "$TABLES_FILE") TOTAL_INDEXES=$(wc -l < "$INDEXES_FILE") echo "Tables to process : $TOTAL_TABLES" echo "Indexes to process: $TOTAL_INDEXES" START_ALL=$(date +%s) ########################################################### # Определение рекомендуемой степени компрессии ########################################################### get_compression_page() { local name="$1" /opt/tantor/db/18/bin/psql \ -h "$DB_HOST" \ -p "$DB_PORT" \ -U "$DB_USER" \ -d "$DB_NAME" \ -At <<EOF SELECT CASE when (compressed_relation_size_1k < compressed_relation_size_2k) and round(a.pages_over_1k::numeric / a.page_cnt * 100,2) <= $MAX_OVERFLOW_PROCENT then '1024' when (compressed_relation_size_2k < compressed_relation_size_4k) and round(a.pages_over_2k::numeric / a.page_cnt * 100,2) <= $MAX_OVERFLOW_PROCENT then '2048' when round(a.pages_over_4k::numeric / a.page_cnt * 100,2) <= $MAX_OVERFLOW_PROCENT then '4096' else 'off' END END FROM csm_compress_analysis('$name','$COMPRESSION',$SAMPLE) a; EOF } ########################################################### # Таблица ########################################################### process_table() { local name="$1" local compression_page compression_page=$(get_compression_page "$name") if [[ -z "$compression_page" || "$compression_page" == "off" ]]; then echo "TABLE $name skipped (recommended compression: off)" return fi echo "Processing TABLE: $name. Recommended compression page: $compression_page" local start_obj start_obj=$(date +%s) SQL=" ALTER TABLE $name SET ( compression = $COMPRESSION, compression_page = $compression_page ); " /opt/tantor/db/18/bin/psql \ -h "$DB_HOST" \ -p "$DB_PORT" \ -U "$DB_USER" \ -d "$DB_NAME" \ -c "$SQL" 2>&1 | while IFS= read -r line do echo "[$name] $line" done local end_obj end_obj=$(date +%s) echo "TABLE $name done in $((end_obj-start_obj))s" } ########################################################### # Индекс ########################################################### process_index() { local name="$1" echo "Processing INDEX: $name" local compression_page compression_page=$(get_compression_page "$name") if [[ -z "$compression_page" || "$compression_page" == "off" ]]; then echo "INDEX $name skipped (recommended compression: off)" return fi echo "Recommended compression page: $compression_page" local start_obj start_obj=$(date +%s) SQL=" ALTER INDEX $name SET ( compression = $COMPRESSION, compression_page = $compression_page ); " /opt/tantor/db/18/bin/psql \ -h "$DB_HOST" \ -p "$DB_PORT" \ -U "$DB_USER" \ -d "$DB_NAME" \ -c "$SQL" 2>&1 | while IFS= read -r line do echo "[$name] $line" done local end_obj end_obj=$(date +%s) echo "INDEX $name done in $((end_obj-start_obj))s" } export -f get_compression_page export -f process_table export -f process_index export DB_NAME export DB_USER export DB_HOST export DB_PORT export COMPRESSION export SAMPLE export MAX_OVERFLOW_PROCENT ########################################################### # Таблицы ########################################################### echo echo "===== Processing TABLES =====" cat "$TABLES_FILE" | \ xargs -P "$THREADS" -I {} bash -c 'process_table "$@"' _ {} echo echo "===== Tables done =====" ########################################################### # Индексы ########################################################### echo echo "===== Processing INDEXES =====" cat "$INDEXES_FILE" | \ xargs -P "$THREADS" -I {} bash -c 'process_index "$@"' _ {} echo echo "===== Indexes done =====" END_ALL=$(date +%s) TOTAL_TIME=$((END_ALL-START_ALL)) echo echo "==================================" echo "Total execution time: ${TOTAL_TIME}s" echo "==================================" # rm -f "$TABLES_FILE" "$INDEXES_FILE"
Скрипт имеет следующие параметры:
‑d — имя базы данных (обязательный параметр);
‑a — алгоритм компрессии [zstd | pglz | lz4], по умолчанию zstd;
‑s — значение sample для csm_compress_analysis, по умолчанию 0.1;
‑o — максимально допустимый процент страниц overflow, по умолчанию 10;
‑t — количество параллельных потоков, по умолчанию 20;
‑u — пользователь PostgreSQL, по умолчанию postgres;
‑h — адрес сервера PostgreSQL, по умолчанию localhost;
‑p — порт PostgreSQL, по умолчанию 5432.
Пример запуска скрипта:
bash alter_opt_compress.sh -d 'zup' -a 'zstd' > alter_opt_compress_zup_zstd.log
Для большинства баз данных именно этот вариант является рекомендуемым, поскольку позволяет получить практически максимальный эффект от компрессии без ручного подбора параметров для каждой таблицы и индекса.
Упрощенный вариант сжатия
Если параметры компрессии уже известны и они одинаковы для каждого отношения, можно воспользоваться упрощенным скриптом alter_compress.sh. В отличие от предыдущего варианта, он не выполняет анализ отношений и применяет выбранные параметры ко всем объектам БД. Благодаря этому время подготовки еще немного сокращается (1 час 20 минут на 645 GB), однако оптимальный размер сжатой страницы для каждого отношения уже не подбирается и не учитывается статистика overflow. Этот вариант наиболее применим к базам 1С (вместе с параметрами на автоматическое сжатие таблиц).
alter_compress.sh
#!/bin/bash set -euo pipefail # === Параметры по умолчанию === DB_NAME="" DB_USER="postgres" DB_HOST="localhost" DB_PORT="5432" COMPRESSION="zstd" COMPRESSION_PAGE="2048" THREADS=20 TABLES_FILE="tables_list.txt" INDEXES_FILE="indexes_list.txt" usage() { echo "Usage:" echo " $0 -d <database> [-a <compression>] [-c <compression_page>] [-t <threads>]" echo echo "Options:" echo " -d Database name (required)" echo " -a Compression type (default: zstd)" echo " -c Compression page size (default: 2048)" echo " -t Parallel threads count (default: 20)" echo " -u DB user (default: postgres)" echo " -h DB host (default: localhost)" echo " -p DB port (default: 5432)" exit 1 } # === Разбор параметров === while getopts "d:a:c:t:u:h:p:" opt; do case $opt in d) DB_NAME="$OPTARG" ;; a) COMPRESSION="$OPTARG" ;; c) COMPRESSION_PAGE="$OPTARG" ;; t) THREADS="$OPTARG" ;; u) DB_USER="$OPTARG" ;; h) DB_HOST="$OPTARG" ;; p) DB_PORT="$OPTARG" ;; *) usage ;; esac done # Проверка обязательного параметра if [[ -z "$DB_NAME" ]]; then echo "ERROR: Database name is required" usage fi echo "==================================" echo "Database: $DB_NAME" echo "Compression: $COMPRESSION" echo "Compression page: $COMPRESSION_PAGE" echo "Threads: $THREADS" echo "Host: $DB_HOST" echo "Port: $DB_PORT" echo "User: $DB_USER" echo "==================================" # === Получаем список таблиц === /opt/tantor/db/18/bin/psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -At -c " SELECT n.nspname || '.' || c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ; " > "$TABLES_FILE" # === Получаем список индексов === /opt/tantor/db/18/bin/psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -At -c " SELECT n.nspname || '.' || c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'i' AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') ; " > "$INDEXES_FILE" TOTAL_TABLES=$(wc -l < "$TABLES_FILE") TOTAL_INDEXES=$(wc -l < "$INDEXES_FILE") echo "Tables to process: $TOTAL_TABLES" echo "Indexes to process: $TOTAL_INDEXES" START_ALL=$(date +%s) process_table() { local name="$1" echo "Processing TABLE: $name" local start_obj start_obj=$(date +%s) SQL="ALTER TABLE $name SET ( compression = $COMPRESSION, compression_page = $COMPRESSION_PAGE );" /opt/tantor/db/18/bin/psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "$SQL" 2>&1 | while IFS= read -r line; do echo "[$name] $line" done local end_obj end_obj=$(date +%s) echo "TABLE $name done in $((end_obj - start_obj))s" } process_index() { local name="$1" echo "Processing INDEX: $name" local start_obj start_obj=$(date +%s) SQL="ALTER INDEX $name SET ( compression = $COMPRESSION, compression_page = $COMPRESSION_PAGE );" /opt/tantor/db/18/bin/psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "$SQL" 2>&1 | while IFS= read -r line; do echo "[$name] $line" done local end_obj end_obj=$(date +%s) echo "INDEX $name done in $((end_obj - start_obj))s" } export -f process_table export -f process_index export DB_NAME export DB_USER export DB_HOST export DB_PORT export COMPRESSION export COMPRESSION_PAGE # === ЭТАП 1: Таблицы === echo "=== Processing TABLES ===" cat "$TABLES_FILE" | \ xargs -I {} -P "$THREADS" bash -c 'process_table "$@"' _ {} echo "=== Tables done ===" # === ЭТАП 2: Индексы === echo "=== Processing INDEXES ===" cat "$INDEXES_FILE" | \ xargs -I {} -P "$THREADS" bash -c 'process_index "$@"' _ {} echo "=== Indexes done ===" END_ALL=$(date +%s) TOTAL_TIME=$((END_ALL - START_ALL)) echo "==================================" echo "Total time: ${TOTAL_TIME}s" echo "==================================" # cleanup # rm -f "$TABLES_FILE" "$INDEXES_FILE"
Параметры:
‑d — имя базы данных (обязательный параметр);
‑a — алгоритм компрессии [zstd | pglz | lz4], по умолчанию zstd;
‑с — размер сжатой страницы [1024 | 2048 | 4096], по умолчанию 2048;
‑t — количество параллельных потоков, по умолчанию 20;
‑u — пользователь PostgreSQL, по умолчанию postgres;
‑h — адрес сервера PostgreSQL, по умолчанию localhost;
‑p — порт PostgreSQL, по умолчанию 5432.
Сжатие при создании таблиц
В этой части описан функционал, реализованный в еще не вышедшей на момент публикации версии СУБД Tantor Postgres (будет доступен с версии 18.6).
При использовании механизма сжатия с 1С есть одна особенность, которую необходимо учитывать. При реструктуризации первой версии (а ряде случаев и второй) платформа не изменяет существующие таблицы, а создаёт новые, переносит в них данные, удаляет старые таблицы и переименовывает новые на их место. Все эти операции выполняются последовательно в рамках одного процесса, поэтому окно для последующего выполнения ALTER TABLE или ALTER INDEX фактически отсутствует. Если компрессия не была задана в момент создания объекта, он будет создан без нее.
Для обеспечения полной совместимости CSM с механизмом реструктуризации 1С были реализованы глобальные параметры конфигурации (GUC). Их точное именование и использование будет отражено в документации. Эти параметры применяются только к командам CREATE TABLE и CREATE INDEX и позволяют автоматически задавать алгоритм компрессии и размер сжатой страницы для всех вновь создаваемых таблиц и индексов. Благодаря этому новые объекты создаются сразу с необходимыми параметрами компрессии, не требуя дополнительного выполнения ALTER TABLE или ALTER INDEX. Это обеспечивает прозрачную работу механизма сжатия при реструктуризации информационных баз 1С и индексирования полей через механизмы 1С. Тем самым полностью сохраняется совместимость с существующими процессами платформы.
Дополнительный эффект этих GUC — автоматическое сжатие базы при загрузке её из DT и при помощи ibcmd.
Практическая эффективность компрессии
В предыдущем разделе мы оценили потенциальную степень компрессии. Теперь сравним прогноз с фактическими результатами после сжатия баз данных.
Размер базы определялся стандартной функцией:
SELECT datname AS database_name, pg_size_pretty(pg_database_size(datname)) AS size FROM pg_database;
Добавим вручную размеры TOAST и прогнозируемые и реальные результаты сжатия из предыдущих таблиц:
database_name |
Full_size |
Compressed_size |
Full_compress_ratio |
toast |
max_compress_analyze |
Full_compress_analyze (+toast) |
erp |
645 GB |
219 GB |
2.94 |
59 GB |
167 GB |
226 GB |
do |
328 GB |
110 GB |
2.98 |
41 GB |
77 GB |
118 GB |
zup |
51 GB |
11 GB |
4.63 |
1982 MB |
9624 MB |
11 GB |
Расшифровка результатов:
database_name — имя базы;
Full_size — полный размер базы данных до компрессии;
Compressed_size — размер сжатой базы данных, полученный функцией pg_database_size;
Full_compress_ratio — фактический коэффициент компрессии;
toast — объем данных, исключаемых из компрессии CSM;
max_compress_analyze — прогнозируемый размер сжатых данных, полученный функцией csm_compress_analysis;
Full_compress_analyze (+toast) — прогнозируемый размер сжатой базы данных, полученный путем добавления toast, для сравнения с Compressed_size.
Как видно, фактический размер баз практически совпадает с результатами (и в даже фактически превосходит их), полученными при предварительном анализе. Это подтверждает высокую точность функции csm_compress_analysis и подтверждает эффективность её использования для прогнозирования эффекта компрессии еще до выполнения сжатия.
Стоит отметить еще одну особенность.
При тестировании альтернативных реализаций компрессии, основанных на страницах переменного размера, мы столкнулись с ситуацией, когда значение, возвращаемое функцией pg_database_size, существенно отличалось от фактически занимаемого места на диске. В отдельных случаях разница достигала 1,7 раза.
Например, для базы ERP после полного обслуживания (VACUUM и дефрагментации) функция pg_database_size показывала размер 151 ГБ, что соответствовало высокому коэффициенту компрессии 4,3. Однако фактический объем файлов базы данных на диске составлял 246 ГБ, то есть реальный коэффициент компрессии был лишь 2,6.
Поэтому для проверки результатов работы CSM дополнительно был выполнен контроль занимаемого пространства непосредственно на файловой системе:
database_name |
Full_size |
Compressed_size (pg_database_size) |
Compressed_size (FileSystem) |
Full_compress_ratio |
toast |
max_compress_analyze |
max_compress_analyze |
erp |
645 GB |
219 GB |
224 140 MB (219 GB) |
2.94 |
59 GB |
167 GB |
226 GB |
do |
328 GB |
110 GB |
112 836 MB (110 GB) |
2.98 |
41 GB |
77 GB |
118 GB |
zup |
51 GB |
11 GB |
11 268 MB (11 GB) |
4.63 |
1982 MB |
9624 MB |
11 GB |
Пояснения:
Compressed_size (pg_database_size) — размер сжатой базы данных, полученный функцией pg_database_size;
Compressed_size (FileSystem) — размер сжатой базы данных, исходя из данных файловой системы.
Как видно из результатов, размеры, определяемые функцией pg_database_size, полностью совпадают с объемом данных, занимаемым на диске. Можно сделать вывод, что механизм CSM обеспечивает не только высокий коэффициент компрессии, но и корректное отображение реального объема базы данных средствами СУБД.
Практический эффект компрессии полностью соответствует прогнозу, полученному на этапе анализа.
Нагрузочное тестирование 1С
Экономия дискового пространства сама по себе представляет интерес, однако важнее понять, как компрессия влияет на работу реальной информационной системы. Для этого были проведены нагрузочные испытания на базе ERP с использованием той же методики тестирования, которая применяется при проверке новых релизов СУБД Tantor Postgres и описана в одной из предыдущих статей. Перед каждым запуском выполнялась одинаковая последовательность действий:
Восстановление БД из резервной копии;
Компрессия БД;
Расчет статистики;
Запуск нагрузочного теста.
Для обеспечения сопоставимости результатов каждый тест выполнялся на идентичной конфигурации оборудования и программного обеспечения.
Параметры испытаний:
СУБД: Tantor Postgres 18.3;
конфигурация: 1С ERP;
размер исходной базы: 645 ГБ;
длительность теста: 1 час;
количество КО: 29 000 операций.
CPU: 16;
RAM: 128;
I/O: SSD.
Сначала был выполнен эталонный запуск без компрессии. После этого база была сжата алгоритмом ZSTD с размером страницы 2048 байт. Размер базы уменьшился до 220 ГБ, после чего тест повторили.
Результат в цифрах:
Без компрессии |
CSM (ZSTD, page 2048) |
|
Размер базы |
645 ГБ |
220 ГБ |
APDEX |
0,856 |
0,868 |
Среднее время отклика |
1,970 |
1,948 |
Графики утилизации cpu, mem, disk i/o:
ERP (Compression = off)



ERP (Compression = zstd, Compression_page = 2048)



Выводы:
Реальный коэффициент компрессии — 2,9;
Показатели APDEX и среднего времени отклика практически не изменились;
Увеличение утилизации CPU на 2%;
Увеличение потребления памяти на 10% в пике;
Утилизация дисковой подсистемы практически не изменилась.
Подобные результаты вполне ожидаемы.
В реальной эксплуатации (и нагрузочном тестировании) производительность системы ограничивается не столько оборудованием, сколько эффективностью планировщика запросов PostgreSQL. Рабочая нагрузка не упиралась в пропускную способность дисковой подсистемы, поэтому уменьшение объема читаемых данных практически не повлияло на скорость выполнения операций.
Аналогичные испытания проводились и для других конфигураций 1С. Во всех случаях полученные результаты отличались незначительно, поэтому приводить их отдельно не имеет практического смысла. Главный вывод заключается в том, что использование CSM не приводит к заметному изменению производительности рабочих систем, сохраняя при этом существенную экономию дискового пространства.
Синтетические тесты
Нагрузочные испытания на реальной базе ERP показали отсутствие заметного влияния компрессии на производительность. Это вполне ожидаемый результат — тест не упирался в возможности оборудования, а скорость выполнения запросов в первую очередь определялась эффективностью планировщика запросов. Возникает закономерный вопрос: что произойдет, если узким местом станет дисковая подсистема? Часто можно встретить утверждение, что компрессия автоматически ускоряет работу PostgreSQL за счет уменьшения объема данных. На практике подобные заявления редко сопровождаются измерениями. Давайте проверим это экспериментально.
Скорость подъема данных в shared_buffers
В идеальном случае уменьшение объема данных должно приводить к сокращению количества операций ввода‑вывода и, следовательно, ускорять загрузку страниц в кэш. Но так ли это на самом деле?
Небольшая таблица (2,6 GB → 326 MB)
Для проверки была сгенерирована тестовая таблица объемом 2,6 ГБ, которая сжималась до 326 МБ. Таблица содержала 5 миллионов строк, а данные были подобраны таким образом, чтобы достигался максимальный коэффициент компрессии при размере страницы 1024 байта.
DROP TABLE IF EXISTS test_table; CREATE TABLE test_table (id SERIAL PRIMARY KEY,col1 INTEGER,txt varchar); INSERT INTO test_table (col1,txt) SELECT i::INT, repeat(md5(i::text), 15) FROM generate_series(1, 5000000) i; DROP TABLE IF EXISTS test_table_zstd; CREATE TABLE test_table_zstd (id SERIAL PRIMARY KEY,col1 INTEGER,txt varchar); INSERT INTO test_table_zstd (col1,txt) SELECT col1,txt FROM test_table; DROP TABLE IF EXISTS test_table_pglz; CREATE TABLE test_table_pglz (id SERIAL PRIMARY KEY,col1 INTEGER,txt varchar); INSERT INTO test_table_pglz (col1,txt) SELECT col1,txt FROM test_table; DROP TABLE IF EXISTS test_table_lz4; CREATE TABLE test_table_lz4 (id SERIAL PRIMARY KEY,col1 INTEGER,txt varchar); INSERT INTO test_table_lz4 (col1,txt) SELECT col1,txt FROM test_table; ALTER TABLE test_table_zstd SET (compression = zstd, compression_page = 1024); ALTER TABLE test_table_pglz SET (compression = pglz, compression_page = 1024); ALTER TABLE test_table_lz4 SET (compression = lz4, compression_page = 1024); ALTER TABLE test_table SET (compression = lz4, compression_page = 1024); ALTER TABLE test_table SET (compression = off, compression_page = 1024); --ставим в одинаковые условия, чтобы данные размещались примерно одинаково --для чистоты эксперимента перед каждым explain... выполнять рестарт инстанса и очищать файловый кэш ОС 'sync | echo 3 > /proc/sys/vm/drop_caches' --explain (analyze, buffers) SELECT count( * ) FROM test_table_zstd T1 WHERE (T1.txt <> 'не существующая строка') --explain (analyze, buffers) SELECT count( * ) FROM test_table_pglz T1 WHERE (T1.txt <> 'не существующая строка') --explain (analyze, buffers) SELECT count( * ) FROM test_table_lz4 T1 WHERE (T1.txt <> 'не существующая строка') --explain (analyze, buffers) SELECT count( * ) FROM test_table T1 WHERE (T1.txt <> 'не существующая строка')
Для каждого алгоритма создавалась отдельная таблица. В результате наполнения получили 4 таблицы с 5 000 000 строк и количеством страниц на диске 333 376. Перед каждым измерением выполнялся перезапуск инстанса и очистка файлового кэша операционной системы, что гарантировало отсутствие страниц как в shared_buffers, так и в файловом кэше ОС. Измерения проводились с помощью запроса:
explain (analyze, buffers) SELECT count( * ) FROM test_table T1 WHERE (T1.txt <> 'не существующая строка')
Контролировалось, что в плане присутствуют только shared read, а значение shared hit отсутствует. Фиксируем I/O Timings и Execution time. Затем инстанс перезапускался, и фиксировали результаты уже с подъемом данных из кэша ОС. Кроме того, эксперимент повторялся для различных значений io_method, чтобы оценить влияние современных методов ввода‑вывода.
Сразу стоит отметить важную особенность текущей реализации CSM. На данный момент механизм поддерживает только синхронный доступ. При выборе worker или io_uring чтение сжатых данных всё равно выполняется синхронно, поэтому сравнение различных методов ввода‑вывода корректно проводить только для несжатой таблицы.
Каждый тест выполнялся три раза, после чего рассчитывалось среднее значение. Время в таблице указано в миллисекундах.
Алгоритм / io_method |
off / sync |
off / worker |
off / io_uring |
lz4 |
pglz |
zstd |
||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
Execution time | I/O Timings |
Exec |
I/O |
Exec |
I/O |
Exec |
I/O |
Exec |
I/O |
Exec |
I/O |
Exec |
I/O |
Холодный запуск |
5553 |
4512 |
3302 |
19 |
1890 |
11 |
3782 |
2825 |
4509 |
3545 |
6111 |
5104 |
Кэш ОС |
3628 |
2657 |
1358 |
13 |
1841 |
10 |
3559 |
2607 |
4159 |
3202 |
5796 |
4828 |
Полученный результат оказался вполне ожидаемым. На таком объёме данных физическое чтение с диска ещё не является основным ограничением. Особенно это заметно при чтении из кэша ОС, когда данные уже находятся в памяти. Для несжатой таблицы последовательность действий выглядит достаточно просто:
Страница считывается из кэша ОС → Выполняется практически бесплатный memcpy → Страница помещается в shared_buffers
Для сжатой таблицы добавляется ещё один обязательный этап — распаковка каждой страницы перед её помещение в shared_buffers.
Сравним сжатые таблицы с базовым вариантом (off/sync):
Алгоритм |
Холодный запуск |
Кжш OC |
lz4 |
−32% |
−2% |
pglz |
−19% |
+15% |
zstd |
+10% |
+60% |
Здесь хорошо видны особенности алгоритмов.
LZ4 оказывается наиболее быстрым. Несмотря на затраты на распаковку, небольшой объём читаемых данных практически компенсирует эти расходы;
PGLZ немного уступает LZ4 как по степени сжатия, так и по скорости распаковки;
zstd обеспечивает максимальное сжатие, однако цена распаковки уже становится заметной и полностью перекрывает выигрыш от уменьшения объёма чтения.
Отдельного внимания заслуживают результаты для worker и io_uring на несжатой таблице. Полное время выполнения уменьшается почти в два раза по сравнению с sync, хотя значения I/O Timings становятся практически нулевыми. Это ожидаемое поведение: при асинхронных методах ввода‑вывода время ожидания диска перестаёт учитываться так же, как при синхронном чтении, поэтому напрямую сравнивать значения I/O Timings между различными io_method некорректно. Для анализа следует ориентироваться дополнительно на Execution Time, поскольку выполняется один и тот же запрос над одинаковым набором данных.
Однако при добавлении к сравнению результатов для worker и io_uring на несжатой таблице победитель меняется — современные асинхронные методы чтения дают значительно больший эффект, чем уменьшение объёма данных. Таблица слишком мала, чтобы выигрыш от сжатия смог компенсировать стоимость распаковки.
А пока продолжим исследование.
Большая таблица (95 GB → 12 GB)
Для следующего теста объем данных был увеличен почти в сорок раз. Тестовая таблица содержала уже 50 млн строк (размер строки увеличен в 3,5 раза), занимала 95 ГБ без компрессии и около 12 ГБ после сжатия. Количество страниц увеличилось до 12,5 млн. Проверяем снова скорость подъема (также среднее из трех итераций, разные методы доступа):
Алгоритм / io_method |
off / sync |
off / worker |
off / io_uring |
lz4 |
pglz |
zstd |
||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
Execution time | I/O Timings |
Exec |
I/O |
Exec |
I/O |
Exec |
I/O |
Exec |
I/O |
Exec |
I/O |
Exec |
I/O |
Холодный запуск |
168 157 |
140 659 |
103 104 |
490 |
49 090 |
359 |
58 286 |
38 548 |
67 459 |
47 250 |
120 998 |
98 787 |
Кэш ОС |
66 514 |
45 835 |
22 533 |
505 |
44 656 |
327 |
50 943 |
31 851 |
59 154 |
39 676 |
112 291 |
91 693 |
После увеличения объёма данных картина меняется принципиально. Теперь читается уже 12,5 млн страниц, и стоимость операций ввода‑вывода начинает доминировать над затратами процессора. Результаты становятся значительно интереснее.
Холодный запуск.
LZ4 сокращает время выполнения запроса почти в три раза. Особенно показательны значения I/O Timings.
Без сжатия: 140 659 мс.
После сжатия:
38 548 мс (LZ4)
47 250 мс (PGLZ)
98 787 мс (zstd)
То есть объём физических операций ввода‑вывода действительно серьезно уменьшается.
Кэш ОС
Даже когда все данные уже находятся в кэше операционной системы, сжатие остаётся выгодным.
Относительно off/sync:
Алгоритм |
Изменение времени |
LZ4 |
−23% |
PGLZ |
−11% |
zstd |
+69% |
На первый взгляд это может показаться неожиданным, ведь обращения к диску отсутствуют. Однако в случае сжатой таблицы кэш ОС также работает со значительно меньшим количеством данных. Вместо чтения 95 GB из памяти операционной системы фактически передаётся около 12 GB, после чего страницы распаковываются уже на стороне ядра. В данном случае стоимость распаковки оказывается значительно ниже стоимости копирования дополнительных десятков гигабайт данных через подсистему памяти, поэтому LZ4 продолжает выигрывать даже без физических операций чтения с диска. Стоимость декомпрессии протокола zstd всё еще остается выше стоимости остальных протоколов.
Влияние io_method
Если же включить в наше соревнование все поддерживаемые методы доступа — получается любопытное сравнение.
На небольшой таблице:
основную роль играет эффективность механизма ввода‑вывода;
выигрыш от уменьшения объёма данных практически отсутствует;
io_uring обеспечивает почти двукратное преимущество над любым вариантом со сжатием.
На большой таблице:
влияние стоимости ввода‑вывода резко возрастает;
уменьшение объёма данных начинает почти полностью компенсировать отсутствие асинхронного I/O;
LZ4, несмотря на работу только через sync, проигрывает лучшему варианту без сжатия (io_uring) всего 14–19%
CSM на текущий момент использует только синхронный ввод‑вывод. Когда в будущих версиях появится поддержка io_uring, можно ожидать существенного роста производительности за счёт сочетания уменьшенного объёма данных и асинхронного чтения.
Тесты с нагрузкой
В реальных «боевых» системах практически не бывает ситуаций, когда дисковая система не нагружена, и как раз более интересно, как поведет себя система при 100% нагрузке дисковой подсистемы. Для генерации нагрузки воспользуемся инструментом pg_bench. Предварительно сгенерируем таблицу, которая точно не поместится в дисковый кэш целиком.
CREATE TABLE test_io ( id bigint PRIMARY KEY, payload text ); INSERT INTO test_io SELECT i, repeat(md5(i::text), 50) FROM generate_series(1, 500000000) i; CREATE INDEX test_io_idx ON test_io(id);
Сгенерированная таблица‑нагрузчик получилась 127 GB. Нагружать будем таким скриптом:
\set id random(1,200000000) SELECT payload FROM test_io WHERE id=:id;
Запускаем наши нагрузчики:
pgbench -f test.sql -c 64 -j 16
Проверять эффективность сжатия будем на первом наборе, только для начала на 1 млн строк, потом повторим для набора с 5 млн строк. Используем io_method = worker. Нагрузка на диски контролируется iostat ‑x 1. Методика испытаний та же — запускаем по 3 раза и выводим среднее. Для полноты понимания добавим еще третий блок выполнения запросов — когда данные есть в shared_buffers.
Алгоритм |
off |
lz4 |
pglz |
zstd |
|---|---|---|---|---|
Холодный запуск |
7683 |
2254 |
2212 |
2588 |
Кэш ОС |
686 |
1133 |
1286 |
2043 |
Shared_buffers |
340 |
|||
И 5 млн строк:
Алгоритм |
off |
lz4 |
pglz |
zstd |
|---|---|---|---|---|
Холодный запуск |
31 601 |
11 166 |
10 534 |
13 645 |
Кэш ОС |
3447 |
5657 |
6000 |
9890 |
Shared_buffers |
2000 |
|||
При 100% нагрузке результаты показали практически обратный эффект. Сжатые таблицы быстрее поднимаются с диска. А еще есть один очень полезный эффект от сжатия — в кэше ОС помещается больше данных и там, где с несжатыми данными приходится уже работать с диском, со сжатыми таблицами — данные всё еще будут в кэше ОС. Таким образом, ваши 70 GB файлового кэша ОС превратятся в >200 GB.
Эффективность записи
Чтение — лишь одна сторона взаимодействия с дисковой подсистемой. Не менее важно понять, как компрессия влияет на скорость записи. Методика тестирования:
Отключаем checkpoint:
alter system set checkpoint_timeout to '24h' alter system set max_wal_size to '200GB'
Сбрасываем статистику bgwriter и фиксируем данные о чекпойнте (для последующего сравнения):
select pg_stat_reset_shared('bgwriter'); select * from pg_stat_bgwriter; SELECT * FROM pg_stat_checkpointer; select checkpoint_lsn from pg_control_checkpoint();
Обновляем все строки таблицы:
update test_table set txt = txt || '.'
Контролируем что данные bgwriter и checkpoint не изменились и запускаем checkpoint:
checkpoint;
Фиксируем время;
Повторяем для сжатых таблиц;
Повторяем для таблиц с количеством строк 50 млн.
Полученные результаты оказались весьма показательны.
Алгоритм/количество строк |
off |
LZ4 |
PGLZ |
zstd |
5 000 000 |
13 sec |
10 sec |
32 sec |
27 sec |
50 000 000 |
43 sec |
27 sec |
1 min 40 secs |
46 sec |
Протокол LZ4 показал эффективность выше чем без сжатия, то есть затраты процессора на сжатие страницы компенсируются экономией времени на запись на диск. А вот PGLZ оказался в аутсайдерах, он показал снижение эффективности около 3х раз. Плата за максимальную эффективность протокола zstd — предпоследнее место, однако с увеличением объема записываемых данных эффективность растет.
Сравнительная таблица
Соберем все результаты тестов в одну таблицу и проставим «плюсики» по эффективности.
Алгоритм |
off |
lz4 |
pglz |
zstd |
Эффективность сжатия |
- |
++ |
++ |
+++ |
Чтение без нагрузки |
‑/+++ |
+++ |
++ |
+ |
Чтение под нагрузкой 100% |
‑/+ |
+++ |
+++ |
++ |
Запись |
+ |
+++ |
- |
+ |
Заключение
В статье мы рассмотрели различные подходы к реализации компрессии страниц в PostgreSQL, показали связанные с ними архитектурные особенности и подробно разобрали механизм CSM, реализованный в Tantor Postgres. Проведенные эксперименты позволяют сделать несколько выводов:
практический эффект уменьшения размера базы данных полностью подтвержден. На реальных базах 1С коэффициент компрессии составил от 3 до 5 раз;
функция
csm_compress_analysisпозволяет с высокой точностью оценить потенциальный эффект компрессии еще до ее включения и подобрать оптимальный размер страницы для конкретной нагрузки;использование страниц фиксированного размера позволяет сохранить простую архитектуру хранения и избежать большинства накладных расходов, характерных для подходов с переменным размером страниц;
компрессия снижает объем операций чтения и записи, и по мере роста объема данных этот эффект становится все более заметным и может компенсировать дополнительные затраты процессора на сжатие и распаковку страниц;
нагрузочные испытания на базе 1С:ERP не выявили деградации производительности;
синтетические тесты показали, что на системах, где производительность ограничивается дисковой подсистемой, компрессия способна обеспечить заметный выигрыш.
Компрессию страниц имеет смысл оценивать не только по коэффициенту сжатия. Не менее важны архитектура механизма хранения, влияние на операции чтения и записи, write amplification и эксплуатационные характеристики системы в целом. Именно поэтому при разработке CSM основной целью стало не достижение максимально возможного коэффициента компрессии, а поиск сбалансированного решения, которое обеспечивает существенную экономию дискового пространства без усложнения архитектуры PostgreSQL.
Другие статьи по релизу СУБД Tantor Postgres 18:
Mike_2594
То чувство, когда КДПВ сама по себе тянет на статью.