Один из крупных клиентов нашей компании попал в грустную ситуацию: базы данных подросли, потребности тоже, купили мощные NUMA-сервера, установили любимую файловую систему ZFS (ZFS — для краткости: формально это OpenZFS), а производительность PostgreSQL стала хуже, чем до покупки.
Базы нешуточные: две базы, в каждой по 180ТБ. В них сливаются данные из многих других, непостгресовых баз. А этими, огромными, напрямую пользуются аналитики компании, и эта деятельность критически важная. ZFS сжала эти базы в два раза — теперь каждая занимает на диске по 90 ТБ, железу бы вздохнуть с облегчением. А стало только хуже. Пригласили наших сотрудников из поддержи, они провели аудит. Случай нам показался интересным, и мы решили о нём рассказать. Заодно напомнив о средствах диагностики.
Для этого у нас используют обычно набор опенсорсных и своих утилит. Свой скрипт
Скрипт умеет получать:
Кроме того в базу устанавливается либо опенсорсная утилита pg_profile, либо её разновидность с расширенной функциональностью PWR (pgpro_pwr), когда это вписывается в политику клиента (Андрей Зубков — автор обеих версий). Она заглядывает в системные представления — pg_stat_statesments и во многие другие, умеет работать с pg_stat_kcache. Она делает снимки текущей статистики через настраиваемые промежутки времени (допустим, полчаса), агрегирует данные, которые выводятся в отчёты. В них много разделов.
Самые жуткие цифры были в разделе Vacuum-related statistics:
Ежедневно процесс autovacuum обрабатывает некоторые таблицы более 20 тысяч раз за сутки! (интервалы сбора данных задаются, в данном случае агрегировались данные за полдня). При этом наблюдается достижение пикового значения количества процессов автоматической очистки. Среди процессов автовакуума можно было увидеть немало процессов, помеченных
Настройки автовакуума были довольно необычны: параметр
Это очевидная патология. Но ужасы автовакуума не причина, а симптом. Надо было искать долгоиграющие транзакции, понять из-за чего они стали долгоиграющими — а там видно будет. Долгоиграющие незавершённые транзакции не дают автовакууму вычистить «старые» данные, поэтому ему приходится предпринимать попытки вновь и вновь, так как максимальное количество «мусора» для таблицы по-прежнему выше допустимого порога, который определяется несколькими параметрами, это подробно расписано в документации.
pg_profile даёт возможность увидеть среднее время выполнения запроса из секции Top SQL by execution time.
Кликнув ссылку в отчёте, можно увидеть полный текст запроса. Параметры, правда, мы не увидим. Например, WHERE x=1 и WHERE x=2 будут представлены как WHERE х=$1 — в таком виде они доступны в представлении
А вот как ищутся долгоиграющие транзакции не в среднем, а в конкретный момент:
Информация о них содержится в строках представления pg_stat_activity. В данном случае искали активные транзакции, то есть те, в которых запрос ещё обрабатывается. Иногда не спеша: оказалось, что некоторым, рекордным транзакциям исполнилось уже 3 дня.
По отчёту можно обнаружить и сессии с состоянием
Найти их можно аналогичным запросом:
Среди этих транзакций тоже нашлись висевшие часами. Теперь можно, например, проанализировать текст запроса, посмотреть какое приложение его инициировало, что-то подкорректировать, если ситуация позволяет.
Но и этим не ограничиваются возможности диагностики. Полезно понять, «где», на каком этапе, в какой функции, запрос проводит время.
Анализ с помощью
Функция
Тут началось, может, самое интересное. Выходило, что PostgreSQL не виновник, а жертва. Безобразно ведёт себя функция лежащего ниже слоя — ОС.
Запуск
Напомним, базы с 180ТБ (или 90ТБ на диске после сжатия) обслуживаются серверами архитектуры NUMA с 8 сокетами (процессорами) с суммарным числом ядер 8 x 24 = 192. Известно, что NUMA не всегда искренне дружит (или хотя бы масштабируется) с Postgres. О том, как подружить Postgres с NUMA была, например, статья Параллелизм в PostgreSQL: не сферический, не конь, не в вакууме. На этой машине все 8 процессоров на одной материнской плате, но доступ к чужой памяти всё равно на порядок медленней, чем к своей.
Чтобы понять, что происходит, наши коллеги изучили код этих функций. Вообще-то ZFS оптимизировали под NUMA-машины и придумали, грубо говоря, вот что: в помощь глобальному счётчику операций записи-чтения добавили на каждый процессор этакий вспомогательный счётчик-ведёрко (bucket), накапливающий локальные значения, а потом сливающий их глобальному начальнику. Идея, вроде бы, хорошая, но работала не слишком проворно. В результате требуются блокировки и на каждое ведёрко на местном CPU, и на глобальный счётчик. За глобальный счётчик сражаются процессы на разных CPU, и чем их больше, чем медленней каналы межпроцессорных коммуникаций, тем неторопливей и сами коммуникации, тем дольше стоять в очередях. Для любителей заглянуть в первоисточники: подробней это описано в комментариях к файлу aggsum.c. Процессоры работают вовсю, но вхолостую, а с точки зрения Postgres транзакции проводят время в праздности (
Но хорошо, когда за кто виноват следует что делать. Самый простой способ: взять и отказаться от ZFS вообще, раз он доставляет такие хлопоты, а сжимать как-то по-другому. В Postgres Pro Enterprise, например, есть собственное сжатие — на уровне базы данных. Но такие радикальные перемены не планировались. Айтишники заказчика не были готовы отказаться от преимуществ ZFS.
ZFS это больше, чем файловая система со сжатием. Она работает по принципу COW (Copy On Write): при чтении области данных используется общая копия, в случае изменения данных создается новая копия. Её можно откатывать к предыдущим состояниям, так что можно даже обходиться без бэкапов. А масса возможностей настройки и управления. Вот статья ZFS: архитектура, особенности и отличия от других файловых систем Георгия Меликова из Mail.ru Cloud Solutions — он контрибьютор и коммитер OpenZFS и ZFS на Linux. Остаётся разбираться с комбинацией ZFS-NUMA.
Не желающие рассасываться очереди объясняли и неадекватно-активную работу автовакуума: из-за больших очередей на получение ZFS-счётчиков время выполнения запросов было гораздо больше ожидаемого, таблицы и индексы разрастались. У индексов нет карты видимости, поэтому автовакууму приходится проходить каждый блок каждого индекса. Так можно чистить сутками.
В результате поисков по форумам и другим ресурсам удалось найти информацию: Александр Мотин из компании iXsystems (США) наблюдал такое поведение на 40-ядерной машине под FreeBSD и сделал патч, который убирает лишние операции со счётчиками. Этот коммит попал в основную ветку ZFS 2.0+.
У клиента же была версия 0.8. Это не значит, что она такая старая: когда приводили к единому виду нумерацию версий ZFS и OpenZFS, пропустили единичку, а рестартовали сразу с 2.0.
Итого: клиенту посоветовали перейти на свежие версии ZFS (2.1+). Клиент последовал совету — обновил и Linux, и ZFS. Сейчас всё работает нормально, жалоб нет. Хотя, конечно, оптимизировать и настраивать непростую систему из ОС+ZFS и Postgres, можно бесконечно. Так как нюансы неисчерпаемы.
Технические подробности этой статьи помогли изложить сотрудники Postgres Professional Михаил Жилин и Пётр Петров.
Базы нешуточные: две базы, в каждой по 180ТБ. В них сливаются данные из многих других, непостгресовых баз. А этими, огромными, напрямую пользуются аналитики компании, и эта деятельность критически важная. ZFS сжала эти базы в два раза — теперь каждая занимает на диске по 90 ТБ, железу бы вздохнуть с облегчением. А стало только хуже. Пригласили наших сотрудников из поддержи, они провели аудит. Случай нам показался интересным, и мы решили о нём рассказать. Заодно напомнив о средствах диагностики.
Набор инструментов
Для этого у нас используют обычно набор опенсорсных и своих утилит. Свой скрипт
pg_diagdump.sh
(по просьбе клиента его можно загрузить), выполняющий подкоманды perf для формирования файлов с данными семплирования. А на основе полученной информации мы формируем уже наглядную картинку в FlameGraph.Скрипт умеет получать:
- статистику производительности процессов СУБД PostgreSQL с помощью perf
- stacktrace процессов СУБД PostgreSQL и взятые ими блокировки
- core dump процессов СУБД PostgreSQL с помощью gcore
- информацию из pg_stat_statements, pg_stat_activity, pg_stat_all_tables
Кроме того в базу устанавливается либо опенсорсная утилита pg_profile, либо её разновидность с расширенной функциональностью PWR (pgpro_pwr), когда это вписывается в политику клиента (Андрей Зубков — автор обеих версий). Она заглядывает в системные представления — pg_stat_statesments и во многие другие, умеет работать с pg_stat_kcache. Она делает снимки текущей статистики через настраиваемые промежутки времени (допустим, полчаса), агрегирует данные, которые выводятся в отчёты. В них много разделов.
Масштабы бедствия
Самые жуткие цифры были в разделе Vacuum-related statistics:
Ежедневно процесс autovacuum обрабатывает некоторые таблицы более 20 тысяч раз за сутки! (интервалы сбора данных задаются, в данном случае агрегировались данные за полдня). При этом наблюдается достижение пикового значения количества процессов автоматической очистки. Среди процессов автовакуума можно было увидеть немало процессов, помеченных
to prevent wraparound
. Что за метка и с чем её едят — тут советуем начать с основательной статьи Егора Рогова Автоочистка (она же autovacuum); можно ещё почитать, скажем, здесь — где эта метка как раз упоминается. А в документации об этом тут.Настройки автовакуума были довольно необычны: параметр
autovacuum_max_workers
, который ограничивает параллельную работу процессов автовакуума, выставлен в 50. По умолчанию он равен 3. Его иногда докручивают до, скажем, 10, но 50 — такого моим коллегам встречать не приходилось. Но и эти 50 были израсходованы: в представлении pg_stat_progress_vacuum максимальное кол-во строк было 50. Кстати, в отчётах pg_profile есть раздел Cluster settings during the report interval, который показывает и значение параметра autovacuum_max_workers
.Это очевидная патология. Но ужасы автовакуума не причина, а симптом. Надо было искать долгоиграющие транзакции, понять из-за чего они стали долгоиграющими — а там видно будет. Долгоиграющие незавершённые транзакции не дают автовакууму вычистить «старые» данные, поэтому ему приходится предпринимать попытки вновь и вновь, так как максимальное количество «мусора» для таблицы по-прежнему выше допустимого порога, который определяется несколькими параметрами, это подробно расписано в документации.
В поисках долгоиграющих транзакций
pg_profile даёт возможность увидеть среднее время выполнения запроса из секции Top SQL by execution time.
Кликнув ссылку в отчёте, можно увидеть полный текст запроса. Параметры, правда, мы не увидим. Например, WHERE x=1 и WHERE x=2 будут представлены как WHERE х=$1 — в таком виде они доступны в представлении
pg_stat_statements
. И это к лучшему, иначе была бы каша из запросов.А вот как ищутся долгоиграющие транзакции не в среднем, а в конкретный момент:
SELECT *
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY now() – xact_start DESC NULLS LAST;
Информация о них содержится в строках представления pg_stat_activity. В данном случае искали активные транзакции, то есть те, в которых запрос ещё обрабатывается. Иногда не спеша: оказалось, что некоторым, рекордным транзакциям исполнилось уже 3 дня.
По отчёту можно обнаружить и сессии с состоянием
idle in transaction
, когда транзакция не завершилась и не откатила свои изменения, что не даёт автовакууму вычистить устаревшие строки в таблице и в соответствующем индексе.Найти их можно аналогичным запросом:
SELECT *
FROM pg_stat_activity
WHERE state LIKE '%idle%'
ORDER BY now() – xact_start DESC NULLS LAST;
Среди этих транзакций тоже нашлись висевшие часами. Теперь можно, например, проанализировать текст запроса, посмотреть какое приложение его инициировало, что-то подкорректировать, если ситуация позволяет.
Но и этим не ограничиваются возможности диагностики. Полезно понять, «где», на каком этапе, в какой функции, запрос проводит время.
Анализ с помощью
perf top
и perf
с визуализаций в flamegrahp
навёл на функцию ядра Linux osq_lock()
, которая сжирала почти 38% CPU — одна из основных причин большого потребления CPU System Time. Вот эта красивая картинка:Функция
osq_lock
фиолетового цвета. Как можно догадаться, эта функция связана с блокировками. osq
это optimistic spin queue
— в ядре Linux. Виновники и жертвы
Тут началось, может, самое интересное. Выходило, что PostgreSQL не виновник, а жертва. Безобразно ведёт себя функция лежащего ниже слоя — ОС.
Запуск
perf script
(top, script
и многие другие — подкоманды perf
) показал, что взятие блокировки osq_lock
происходило во время выполнения функции aggsum_add()
модуля ZFS. Семейство функций aggsum_*
увеличивают счётчики, когда происходит чтение или запись.Напомним, базы с 180ТБ (или 90ТБ на диске после сжатия) обслуживаются серверами архитектуры NUMA с 8 сокетами (процессорами) с суммарным числом ядер 8 x 24 = 192. Известно, что NUMA не всегда искренне дружит (или хотя бы масштабируется) с Postgres. О том, как подружить Postgres с NUMA была, например, статья Параллелизм в PostgreSQL: не сферический, не конь, не в вакууме. На этой машине все 8 процессоров на одной материнской плате, но доступ к чужой памяти всё равно на порядок медленней, чем к своей.
Чтобы понять, что происходит, наши коллеги изучили код этих функций. Вообще-то ZFS оптимизировали под NUMA-машины и придумали, грубо говоря, вот что: в помощь глобальному счётчику операций записи-чтения добавили на каждый процессор этакий вспомогательный счётчик-ведёрко (bucket), накапливающий локальные значения, а потом сливающий их глобальному начальнику. Идея, вроде бы, хорошая, но работала не слишком проворно. В результате требуются блокировки и на каждое ведёрко на местном CPU, и на глобальный счётчик. За глобальный счётчик сражаются процессы на разных CPU, и чем их больше, чем медленней каналы межпроцессорных коммуникаций, тем неторопливей и сами коммуникации, тем дольше стоять в очередях. Для любителей заглянуть в первоисточники: подробней это описано в комментариях к файлу aggsum.c. Процессоры работают вовсю, но вхолостую, а с точки зрения Postgres транзакции проводят время в праздности (
idle
), и автовакуумы штурмуют таблицы и индексы раз за разом.Но хорошо, когда за кто виноват следует что делать. Самый простой способ: взять и отказаться от ZFS вообще, раз он доставляет такие хлопоты, а сжимать как-то по-другому. В Postgres Pro Enterprise, например, есть собственное сжатие — на уровне базы данных. Но такие радикальные перемены не планировались. Айтишники заказчика не были готовы отказаться от преимуществ ZFS.
ZFS это больше, чем файловая система со сжатием. Она работает по принципу COW (Copy On Write): при чтении области данных используется общая копия, в случае изменения данных создается новая копия. Её можно откатывать к предыдущим состояниям, так что можно даже обходиться без бэкапов. А масса возможностей настройки и управления. Вот статья ZFS: архитектура, особенности и отличия от других файловых систем Георгия Меликова из Mail.ru Cloud Solutions — он контрибьютор и коммитер OpenZFS и ZFS на Linux. Остаётся разбираться с комбинацией ZFS-NUMA.
Не желающие рассасываться очереди объясняли и неадекватно-активную работу автовакуума: из-за больших очередей на получение ZFS-счётчиков время выполнения запросов было гораздо больше ожидаемого, таблицы и индексы разрастались. У индексов нет карты видимости, поэтому автовакууму приходится проходить каждый блок каждого индекса. Так можно чистить сутками.
Happy End
В результате поисков по форумам и другим ресурсам удалось найти информацию: Александр Мотин из компании iXsystems (США) наблюдал такое поведение на 40-ядерной машине под FreeBSD и сделал патч, который убирает лишние операции со счётчиками. Этот коммит попал в основную ветку ZFS 2.0+.
У клиента же была версия 0.8. Это не значит, что она такая старая: когда приводили к единому виду нумерацию версий ZFS и OpenZFS, пропустили единичку, а рестартовали сразу с 2.0.
Итого: клиенту посоветовали перейти на свежие версии ZFS (2.1+). Клиент последовал совету — обновил и Linux, и ZFS. Сейчас всё работает нормально, жалоб нет. Хотя, конечно, оптимизировать и настраивать непростую систему из ОС+ZFS и Postgres, можно бесконечно. Так как нюансы неисчерпаемы.
Технические подробности этой статьи помогли изложить сотрудники Postgres Professional Михаил Жилин и Пётр Петров.