Соглашусь с этим комментарием. Понравилось, что в статье указаны достоинства и недостатки. Если у автора есть возможность рассказать о личном опыте - будет очень полезно. Особенно интересен и полезен формат "10 ошибок использования из практики" или "10 вариантов оптимизации на примере реальных задач". Любой рассказ практического опыта будет очень полезен.
Соглашусь с Вами, Кирилл. Автор Алесей как-то очень резко, болезненно и крайне токсично реагирует на любой, даже малейший намек, что он в чем-то некомпетентен. Автор сразу же переходит на личности, пишет длиннейшие субъективные, ничем не подкрепленные комментарии, переполненные токсичностью. А также банит в своей телеграм группе, подчищает все неудобные ему сообщения, банит "неудобный ему" контакт в Телеграм. Отчего у него такая крайне резкая реакция? Хотели ли бы заказчики/работодатели/ученики/коллеги работать с таким человеком? Каждый вправе решить это сам и отвечать за последствия.
Вместо этого ядро должно давать точную информацию о том, какие ожидания были у сеанса, сколько их было и сколько времени они заняли
То есть подробная статистика по сеансам, которая формируется в результате анализа событий, по аналогии с информацией о работе VACUUM. Причем анализировать нужно все сеансы и очень детально.
Кажется, что это достаточно большой и сложный модуль и возможно на его написание нет ресурсов.
Егор, спасибо за отличный цикл статей. Очень ждем следующего цикла :)
Вопрос скорее не про блокировки а в целом про экосистему PostgreSQL
К сожалению, единственная доступная информация об ожиданиях — информация на текущий момент. Никакой накопленной статистики не ведется. Единственный способ получить картину ожиданий во времени — семплирование состояния представления с определенным интервалом. Встроенных средств для этого не предусмотрено
Существует класс трудноулавливаемых и трудноизучаемых запросов — быстрых, короткоживущих. Например, где-то когда-то в цикле запрашивали строчки по одной, вместо того чтобы запросить их все сразу (типичный кейс ORM), а потом забыли об этом. Потом приложение выросло, запросы сами по себе легковесные и быстрые, но создают при этом большую нагрузку на систему, причину которой сложно отследить. Вот так я понимаю короткоживущие быстрые запросы, надеюсь, что правильно.
Я к тому, что для OLTP эта ситуация типична. Как так получилось, что в PostgreSQL до сих пор нет встроенных средств семплирования, которые позволят хоть как-то отловить подобные запросы? Либо все-таки подобные запросы не настолько сильно вредят Postgres, и поэтому семплирование неприоритетно. То есть блокировки, которые Вы предложили отловить семплированием — тоже редкий случай, не стоящий того, чтобы инструменты семплирования размещать в ядро.
Либо принципиально такие инструменты как семплирование — это не задача PostgreSQL и их некорректно встраивать. Было бы интересно услышать Ваше мнение по этому вопросу.
Немного смешалось 2 кейса, поэтому описание получилось расплывчатым. Изначально я забыл (или тогда еще не знал) о существовании частичного индекса и сделал с Redlock.
Затем появился кейс запрета параллельных действий пользователя. Приведенным Вами описанием выше это тоже можно решить, при условии, что каждое действие пишется в таблицу событий. А это так и есть. Спасибо. Кстати, еще один аргумент в пользу необходимости в проекте такой вот таблицы событий. Для rate limiters можно очень успешно использовать без необходимости REDIS
Изначально была сделана просто вставка событий id + JSONB. Среди этих событий было голосование за публикации. Конечно, в итоге была сделана отдельная таблица для событий голосования с подходящим уникальным индексом. Но до этого, в качестве быстрого решения была сделана распределенная блокировка на REDIS. Она впоследствии осталась для соблюдения условия «пользователь может сделать только одно социальное действие (голосование либо публикацию комментария и т.п.)», чтобы усложнить написания злоумышленниками скрипта, который будет от лица пользователя делать множество запросов (лайков, например) параллельно.
А можно было бы сделать это на рекомендательных блокировках? Например заблокировать id пользователя (или хеш от него)
В приведенном примере блокировка действует до конца сеанса, а не транзакции, как обычно.
У меня был случай, когда в силу недостатков архитектуры нельзя было явно поставить уникальность на вставку нового значения. Чтобы защититься от дубликатов, я реализовал REDIS lock. Перед тем как осуществить действие, происходит попытка захвата блокировки действия. После успешного завершения действия (или ошибки) — блокировка снимается. Или же она снимается по таймауту.
Получается, что вместо этого можно было бы использовать рекомендательные блокировки? Заблокировать условный ресурс и быть уверенным, что другой сеанс будет ждать освобождения ресурса? Разве что кажется, что есть недостаток. Если сеанс внезапно завершается (обрывается соединение по какой то причине, если я понимаю правильно, что такое сеанс) — то блокировка мгновенно опускается. С другой стороны, раз блокировки нет, то действие откатилось и вроде все хорошо.
Однако как быть, если сеанс зависнет и блокировка тоже застрянет? Тут тогда таймаут снятия блокировки будет равен таймауту завершения сеанса?
Егор, читаю (по 2 раза минимум, для глубокого понимания) все ваши статьи, но к сожалению, времени стало намного меньше, поэтому перестал задавать вопросы. Но по этой статье все таки задам :)
Раньше таблицы расширялись только на одну страницу за раз. Это вызывало проблемы при одновременной вставке строк несколькими процессами, поэтому в версии PostgreSQL 9.6 сделали так, чтобы к таблицам добавлялось сразу несколько страниц (пропорционально числу ожидающих блокировку процессов, но не более 512).
Интересен тогда кейс append-only:
* Пусть у нас идет интенсивная вставка значений в таблицу, допустим 1000 строк в секунду.
* Пусть так получилось, что это не batch — именно 1000 в секунду, причем это делают разные процессы. Извиняюсь за натянутость кейса, он специально преувеличен, чтобы понять суть.
* Пусть строки достаточно «тяжелые».
То есть вероятен описанный выше кейс, когда разные процессы начнут расширять таблицу на новую страницу. Пусть мы достигли предела и расширились на 512 страниц за раз из разных процессов.
А потом продолжили вставку новых значений (она не останавливалась).
Значит ли это все, что строки будут лежать в файлах сильно фрагментировано? То есть мы ожидаем append-only и вставку «строка за строкой, id за id». А из за такого вот расширения упорядоченность строк будет сильно нарушена. Что, в частности, помешает эффективному использованию индекса BRIN
Если я все правильно понял, контрольная точка не пишет на максимальной скорости в том числе и потому, чтобы не расходовать железные ресурсы. При необходимости ускорения железные ресурсы начнут больше расходоваться и можно «неожиданно» получить деградацию производительности. То есть цена ускорения это всегда потенциальная деградация?
Или алгоритм настолько умен, что даже рост нагрузки за счет ускорения можно прогнозировать? Вернее, есть параметр, ограничивающий его ресурсы, который я упустил, когда читал статью
Очень заинтересовал момент доступности страницы для чтения при pin :)
А если страница вытесняется на диск с целью заменить ее на новую с диска (все страницы буффера заняты, требуется вытеснение)? В этом случае «можно читать» вероятно, не работает, потому что содержимое страницы в какой-то момент полностью будет изменено.
В заключение все нежурналируемые таблицы перезаписываются с помощью образов в init-файлах.
имеются ввиду слои init, которые остались на диске на момент отказа системы? Могут ли они быть повреждены, ведь они могут быть в неконсистентном состоянии (на то и WAL для других операций)
Надо просто продолжить выполнение текущей, но ускориться.
Ускорение ведь возможно вроде бы только за счет увеличения расходуемых ресурсов железа и то, наверное, есть предел распараллеливания, обусловленный самим алгоритмом.
Думаю тут интереснее в первую очередь не внутренности алгоритма, а возможность деградации производительности системы в связи с неоптимальным процессом выполнения контрольных точек.
Правильно ли я понял, что процесс checkpointer это популярный кандидат (один из кандидатов) на оптимизацию, если вдруг система «ни с того ни с сего» начала работать медленнее?
Егор, спасибо за отличную статью. Вопрос, связанный с заморозкой.
В заголовке фрагмента WAL хранится:
номер транзакции, к которой относится запись;
полагаю, что этот номер для чего-то используется, например, при восстановлении.
Но как быть если номер транзакции заморожен, прошло уже много времени и в системе появилась уже другая транзакция, номер которой совпадает с замороженным номером?
Как в целом процесс заморозки дружит с WAL с точки зрения номеров транзакций?
Спасибо, а как происходит освобождение «уже ненужного» буфферного кольца. Буфферы, ранее выделенные на кольцо, полностью очищаются? Ведь насколько я понял трудно будет «убрать кольцо и сделать буфферы кольца доступными для всех», ведь буферы кольца узко специализированы были под конкретный массовый оператор. И врят ли будут полезны другим транзакциям.
Алгоритм clock-sweep перебирает по кругу все буферы (используя указатель на «следующую жертву»), уменьшая на единицу их счетчики обращений.
Вопрос 1 — Правильно ли я понял, что:
* Вытеснение начинается только когда нет свободных буферов, а в буферы надо поднять страницу
* Алгоритм уменьшает счетчики буферам, «которым не повезло оказаться до буфера с нулевым счетчиком». Потому что как только алгоритм находит буфер с нулевым счетчиком — он его вытесняет, а последующие буферы уже не трогает.
* То есть скорость пробегания полного круга существенно зависит от количества «уже нулевых счетчиков» на пути алгоритма.
Размышления:
На первый взгляд кажется, что было бы неплохо иметь хеш таблицу с указателем на буферы с нулевыми счетчиками, чтобы при необходимости вытеснения сначала сразу же вытеснять их и не трогать другие буферы, которым «не посчастливилось оказаться до нулевого буфера».
А алгоритм уменьшения счетчиков сделать отдельным, который лишь уменьшает счетчик и когда счетчик уменьшился до нуля — делает новую запись в хеш таблицу.
Тут конечно сразу же недостаток — при увеличении счетчика надо как то нулевые буферы из хеш таблицы убирать.
Вопрос 2 — Правильно ли я понял, что алгоритм вытеснения выполняет 2 задачи сразу
* Уменьшает счетчики
* Вытесняет нулевые буферы
Вопрос 3 — Фоновый процесс записи грязных страниц на диск. Использует ли он как-то информацию о счетчиках обращений?
А насколько переход на directIO способен увеличить производительность? Действительно ли если постгрес возьмет на себя часть функций ОС — это будет хорошим решением? Не идем ли мы таким образом в сторону, когда постгрес — это и есть операционная система, ведь она берет часть ее функций на себя.
И это может значительно замедлить скорость развития самой Постгрес.
UPD. На ум пока приходит выделять отдельный мощный сервер под постгрес таким образом, чтобы бОльшая часть RAM была занята файловым кешем и буфферным пулом. Тогда условно говоря файловый кеш ОС будет работать почти исключительно на нужды Постгрес.
При операциях, выполняющих массовое чтение или запись данных, есть опасность быстрого вытеснения полезных страниц из буферного кеша «одноразовыми» данными.
Имеются ввиду массовое чтение или запись данных в рамках одной транзакции? Или массовое чтение в рамках сессии, соединения? Или даже в рамках одного оператора внутри транзакции?
Чтобы этого не происходило, для таких операций используются так называемые буферные кольца (buffer ring) — для каждой операции выделяется небольшая часть буферного кеша. Вытеснение действует только в пределах кольца, поэтому остальные данные буферного кеша не страдают.
Расскажите, пожалуйста, поподробнее, для каких операций выделяются буфферные кольца. Интересная мысль возникла — если буфферное кольцо — это хорошая идея, то «хватит ли всем желающим» буфферных колец? Может ли такое получиться, что буфферные кольца займут весь буффер или значительную его часть, и остальным операциям «придется тесниться» на малом количестве оставшихся без колец буфферов?
Соглашусь с этим комментарием. Понравилось, что в статье указаны достоинства и недостатки. Если у автора есть возможность рассказать о личном опыте - будет очень полезно. Особенно интересен и полезен формат "10 ошибок использования из практики" или "10 вариантов оптимизации на примере реальных задач". Любой рассказ практического опыта будет очень полезен.
Соглашусь с Вами, Кирилл. Автор Алесей как-то очень резко, болезненно и крайне токсично реагирует на любой, даже малейший намек, что он в чем-то некомпетентен. Автор сразу же переходит на личности, пишет длиннейшие субъективные, ничем не подкрепленные комментарии, переполненные токсичностью. А также банит в своей телеграм группе, подчищает все неудобные ему сообщения, банит "неудобный ему" контакт в Телеграм. Отчего у него такая крайне резкая реакция? Хотели ли бы заказчики/работодатели/ученики/коллеги работать с таким человеком? Каждый вправе решить это сам и отвечать за последствия.
То есть подробная статистика по сеансам, которая формируется в результате анализа событий, по аналогии с информацией о работе VACUUM. Причем анализировать нужно все сеансы и очень детально.
Кажется, что это достаточно большой и сложный модуль и возможно на его написание нет ресурсов.
Вопрос скорее не про блокировки а в целом про экосистему PostgreSQL
Существует класс трудноулавливаемых и трудноизучаемых запросов — быстрых, короткоживущих. Например, где-то когда-то в цикле запрашивали строчки по одной, вместо того чтобы запросить их все сразу (типичный кейс ORM), а потом забыли об этом. Потом приложение выросло, запросы сами по себе легковесные и быстрые, но создают при этом большую нагрузку на систему, причину которой сложно отследить. Вот так я понимаю короткоживущие быстрые запросы, надеюсь, что правильно.
Я к тому, что для OLTP эта ситуация типична. Как так получилось, что в PostgreSQL до сих пор нет встроенных средств семплирования, которые позволят хоть как-то отловить подобные запросы? Либо все-таки подобные запросы не настолько сильно вредят Postgres, и поэтому семплирование неприоритетно. То есть блокировки, которые Вы предложили отловить семплированием — тоже редкий случай, не стоящий того, чтобы инструменты семплирования размещать в ядро.
Либо принципиально такие инструменты как семплирование — это не задача PostgreSQL и их некорректно встраивать. Было бы интересно услышать Ваше мнение по этому вопросу.
Затем появился кейс запрета параллельных действий пользователя. Приведенным Вами описанием выше это тоже можно решить, при условии, что каждое действие пишется в таблицу событий. А это так и есть. Спасибо. Кстати, еще один аргумент в пользу необходимости в проекте такой вот таблицы событий. Для rate limiters можно очень успешно использовать без необходимости REDIS
Изначально была сделана просто вставка событий id + JSONB. Среди этих событий было голосование за публикации. Конечно, в итоге была сделана отдельная таблица для событий голосования с подходящим уникальным индексом. Но до этого, в качестве быстрого решения была сделана распределенная блокировка на REDIS. Она впоследствии осталась для соблюдения условия «пользователь может сделать только одно социальное действие (голосование либо публикацию комментария и т.п.)», чтобы усложнить написания злоумышленниками скрипта, который будет от лица пользователя делать множество запросов (лайков, например) параллельно.
А можно было бы сделать это на рекомендательных блокировках? Например заблокировать id пользователя (или хеш от него)
У меня был случай, когда в силу недостатков архитектуры нельзя было явно поставить уникальность на вставку нового значения. Чтобы защититься от дубликатов, я реализовал REDIS lock. Перед тем как осуществить действие, происходит попытка захвата блокировки действия. После успешного завершения действия (или ошибки) — блокировка снимается. Или же она снимается по таймауту.
Получается, что вместо этого можно было бы использовать рекомендательные блокировки? Заблокировать условный ресурс и быть уверенным, что другой сеанс будет ждать освобождения ресурса? Разве что кажется, что есть недостаток. Если сеанс внезапно завершается (обрывается соединение по какой то причине, если я понимаю правильно, что такое сеанс) — то блокировка мгновенно опускается. С другой стороны, раз блокировки нет, то действие откатилось и вроде все хорошо.
Однако как быть, если сеанс зависнет и блокировка тоже застрянет? Тут тогда таймаут снятия блокировки будет равен таймауту завершения сеанса?
Интересен тогда кейс append-only:
* Пусть у нас идет интенсивная вставка значений в таблицу, допустим 1000 строк в секунду.
* Пусть так получилось, что это не batch — именно 1000 в секунду, причем это делают разные процессы. Извиняюсь за натянутость кейса, он специально преувеличен, чтобы понять суть.
* Пусть строки достаточно «тяжелые».
То есть вероятен описанный выше кейс, когда разные процессы начнут расширять таблицу на новую страницу. Пусть мы достигли предела и расширились на 512 страниц за раз из разных процессов.
А потом продолжили вставку новых значений (она не останавливалась).
Значит ли это все, что строки будут лежать в файлах сильно фрагментировано? То есть мы ожидаем append-only и вставку «строка за строкой, id за id». А из за такого вот расширения упорядоченность строк будет сильно нарушена. Что, в частности, помешает эффективному использованию индекса BRIN
Или алгоритм настолько умен, что даже рост нагрузки за счет ускорения можно прогнозировать? Вернее, есть параметр, ограничивающий его ресурсы, который я упустил, когда читал статью
А если страница вытесняется на диск с целью заменить ее на новую с диска (все страницы буффера заняты, требуется вытеснение)? В этом случае «можно читать» вероятно, не работает, потому что содержимое страницы в какой-то момент полностью будет изменено.
имеются ввиду слои init, которые остались на диске на момент отказа системы? Могут ли они быть повреждены, ведь они могут быть в неконсистентном состоянии (на то и WAL для других операций)
Ускорение ведь возможно вроде бы только за счет увеличения расходуемых ресурсов железа и то, наверное, есть предел распараллеливания, обусловленный самим алгоритмом.
Думаю тут интереснее в первую очередь не внутренности алгоритма, а возможность деградации производительности системы в связи с неоптимальным процессом выполнения контрольных точек.
Правильно ли я понял, что процесс checkpointer это популярный кандидат (один из кандидатов) на оптимизацию, если вдруг система «ни с того ни с сего» начала работать медленнее?
В заголовке фрагмента WAL хранится:
номер транзакции, к которой относится запись;
полагаю, что этот номер для чего-то используется, например, при восстановлении.
Но как быть если номер транзакции заморожен, прошло уже много времени и в системе появилась уже другая транзакция, номер которой совпадает с замороженным номером?
Как в целом процесс заморозки дружит с WAL с точки зрения номеров транзакций?
Вопрос 1 — Правильно ли я понял, что:
* Вытеснение начинается только когда нет свободных буферов, а в буферы надо поднять страницу
* Алгоритм уменьшает счетчики буферам, «которым не повезло оказаться до буфера с нулевым счетчиком». Потому что как только алгоритм находит буфер с нулевым счетчиком — он его вытесняет, а последующие буферы уже не трогает.
* То есть скорость пробегания полного круга существенно зависит от количества «уже нулевых счетчиков» на пути алгоритма.
Размышления:
На первый взгляд кажется, что было бы неплохо иметь хеш таблицу с указателем на буферы с нулевыми счетчиками, чтобы при необходимости вытеснения сначала сразу же вытеснять их и не трогать другие буферы, которым «не посчастливилось оказаться до нулевого буфера».
А алгоритм уменьшения счетчиков сделать отдельным, который лишь уменьшает счетчик и когда счетчик уменьшился до нуля — делает новую запись в хеш таблицу.
Тут конечно сразу же недостаток — при увеличении счетчика надо как то нулевые буферы из хеш таблицы убирать.
Вопрос 2 — Правильно ли я понял, что алгоритм вытеснения выполняет 2 задачи сразу
* Уменьшает счетчики
* Вытесняет нулевые буферы
Вопрос 3 — Фоновый процесс записи грязных страниц на диск. Использует ли он как-то информацию о счетчиках обращений?
И это может значительно замедлить скорость развития самой Постгрес.
UPD. На ум пока приходит выделять отдельный мощный сервер под постгрес таким образом, чтобы бОльшая часть RAM была занята файловым кешем и буфферным пулом. Тогда условно говоря файловый кеш ОС будет работать почти исключительно на нужды Постгрес.
Имеются ввиду массовое чтение или запись данных в рамках одной транзакции? Или массовое чтение в рамках сессии, соединения? Или даже в рамках одного оператора внутри транзакции?
Расскажите, пожалуйста, поподробнее, для каких операций выделяются буфферные кольца. Интересная мысль возникла — если буфферное кольцо — это хорошая идея, то «хватит ли всем желающим» буфферных колец? Может ли такое получиться, что буфферные кольца займут весь буффер или значительную его часть, и остальным операциям «придется тесниться» на малом количестве оставшихся без колец буфферов?