Как сделать стрим в Postgres?

Kate

Administrator
Команда форума
На одной конференции мне задали вопрос (спасибо Александру!): как сделать стрим в PostgreSQL? Представьте, что имеется bytea и вы к нему хотите что-то дописать. Люди столкнулись с тем, что на это в PostgreSQL тратится гигантское время и растет WAL-трафик.

Расскажу, что с этим возможно сделать — это будет еще один пример оптимизации TOAST (о чем я недавно писал), на на этот раз — для быстрой записи потока бинарных данных. На самом деле мой коллега, Никита Глухов, за несколько часов сделал расширение, которое «вылечило» проблему, и мы даже успели рассказать про это на сессии блиц-докладов на PGConf.Online 2021.

feda2e9119c70eae1c5a0fcef4244a12.png

Appendable bytea: мотивационный пример​

Предположим, что у нас имеется 100 Мб bytea:

CREATE TABLE test (data bytea);
ALTER TABLE test ALTER COLUMN data SET STORAGE EXTERNAL;
INSERT INTO test SELECT repeat('a', 100000000)::bytea data;
Мы добавляем 1 байт:
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
UPDATE test SET data = data || 'x'::bytea;
И видим, что на это тратится больше секунды!

81fa4509b43265e2d4fa7bda8d63d649.jpeg

Это связано с тем, о чем я рассказывал про TOAST в первой части статьи — 100 Мб копируется в WAL и в хранение. На этом примере мы серьезно задумались, и у нас появился челлендж — как же сделать так, чтобы можно было стримить в Postgres?

Дело в том, что TOAST не оптимизирован под частичное обновление, все данные должны сначала собраться из чанков, потом разжаться для модифицикаций в памяти (это все называется deTOAST), после чего они должны обратно сжаться, «нарезаться» на чанки и записаться на диск с новым OID.

Поэтому мы решили сделать специальный формат данных (в PostgreSQL это называется datum — то, что передается функцией и т.д.), который состоит из TOAST pointer и inline буфера, используемый для TOAST. Оператор конкатенации || не делает deTOAST, а просто добавляет данные в этот inline буфер. Если размер inline данных превышает 2KB, то происходит TOAST, но только изменившихся данных, при этом неизменившиеся чанки используются для всех версий:

b1444ac8960236f4fecd2d582123983d.jpeg

И если вы хотите добавить что-то, это сохраняется это в inline tuple:

7144119da3c8f529a8bf93d2a9031387.jpeg

Видим, что у TOAST pointer размер 5,5 Кб, это размер трех чанков (TOAST Tuple1, TOAST Tuple2, TOAST Tuple3). Мы добавили маленькое хранение — то, чего не хватает до 2 Кб — просто для того, чтобы вы могли записать именно туда. И тогда все ускоряется. Когда размер становится больше, мы делаем новый чанк, а старые сохраняем на месте:

15bfa4b0b03dc9a182f2a2ce9afe89aa.jpg

В результате мы получаем выигрыш: на таком простом примере вместо 14 чанков мы работаем с 7 чанками — сокращение в 2 раза:

971c21b1bfde356e0f1fa4b294bcf2cc.jpeg

Но это примитивный пример. На том же реальном примере мы получаем ускорение в 2750 раз:

766ff986b65318868abdc505ee6db421.jpeg

То есть стрим занимает меньше миллисекунды! При этом размер таблицы остается тем же самым (примерно 100 Мб), а WAL сгенерился всего 143 байта вместо 100 Мб и нет никаких дополнительных ненужных чтений блоков. Это хороший мотивирующий пример, который показывает, как можно чуть-чуть изменить TOAST. Однако давайте посмотрим, как это работает в PostgreSQL сейчас.

Appendable bytea: что в PostgreSQL есть сейчас​

Посмотрим на график. По оси X — размер данных. Разные цвета — это количество append size, то есть, сколько вы добавляете к этому размеру: 10, 100 байт, и т.д. до 1 Мб (∝ — знак пропорциональности):

a0e29543e06738fd95f4f2bf8f293325.jpeg

Слева показано, как это происходит в PostgreSQL сейчас. Время выполнения запроса пропорционально размеру JSONB и добавленному кусочку, и зависит оно только от размера того, что вы добавляете. По-моему, очень красиво.

На следующем графике показан трафик WAL, который тоже стал значительно красивее.

072de5f247976d152904845bee6e850b.jpeg

А теперь давайте действительно сделаем стрим.

Appendable bytea: STREAM​

У нас строка имеет размер 0, давайте его добьем до 1 Мб маленькими апдейтами, например, по 10 байт:

UPDATE test SET data = data || repeat('a', append_size)::bytea WHERE id = 0; COMMIT;
Мы решили использовать pg_stat_statements, чтобы вытаскивать время, количество блоков и размер WAL. В старом PostgreSQL мы сразу уперлись в производительность. Дело в том, что 1 Мб мы выбрали неслучайно — когда вы начинаете апдейтить от 100 Кб и больше, то таблица растет очень круто и место на ноутбуке начинает вылетать. Если вы будете апдейтить 10-байтными кусочками, вам придется это сделать много тысяч раз. Таблица и WAL «распухнут» , поэтому мы ограничились только 1 МБ.

И вот справа вы можете увидеть, каким стал тот же стрим с Appendable bytea:

7ed5f3303ca17f1ec62cbd7781cfb125.jpeg

То есть это открывает большие перспективы. В PostgreSQL действительно вполне можно стримить, если использовать наши оптимизации.

Здесь показано, как растет размер WAL. Справа он зависит только от размера добавляемого куска, а слева — от самой строки.

9672f07975036b2090fa46a15944cae5.jpeg

Мы посчитали скорость (Мб/с) для оригинального постгреса (слева) и с оптимизацией. Слева мы ограничились случаем, когда мы добавляем 10-байтные кусочки, иначе всё будет слишком медленно. Мы видим, что скорость очень быстро падает с 1 Мб/с до 1 Кб/с, полоса совсем маленькая. Справа мы видим, что производительность дописывания не зависит от размера данных для любых размеров добавлений, и можно предсказать, что если мы хотим занять полосу 20 Мб/с, то надо апдейтить килобайтными чанками и деградации производительности не будет.

5330550bb0e8db09ecde03aea8500877.jpeg

Вместо Заключения​

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

Мы предложили серию патчей, реализующих API для TOAST (см. Pluggable TOAST), на основе которого можно разрабатывать TOAST, оптимизированный для определенного типа данных. Например, все описанные оптимизации для JSONB можно реализовать в виде расширения. Надеемся закоммитить все это для следующей версии PG15.

 
Сверху