Pgbouncer USE RED

Мы начали обновлять в нашем сервисе мониторинг для PgBouncer и решили все немного причесать. Чтобы сделать всё годно, мы притянули самые известные методологии перформанс мониторинга: USE (Utilization, Saturation, Errors) Брендана Грегга и RED (Requests, Errors, Durations) от Тома Уилки.


Под катом рассказ с графиками про то, как устроен pgbouncer, какие у него есть конфигурационные ручки и как используя USE/RED выбрать правильные метрики для его мониторинга.


Сначала про сами методы


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


USE


Для каждого ресурса следите за утилизацией, насыщением и ошибками.
Brendan Gregg

Тут ресурс это любой отдельный физический компонент — ЦПУ, диск, шина и т.п. Но не только — производительность некоторых программных ресурсов также может быть рассмотрена таким методом, в частности виртуальные ресурсы, типа контейнеров / cgroups с лимитами, тоже удобно так рассматривать.


U — Утилизация: либо процент времени (от интервала наблюдения), когда ресурс был занят полезной работой. Как, например, загрузка ЦПУ или disk utilization 90% означает, что 90% времени было занято чем-то полезным) либо, для таких ресурсов как память, это процент использованной памяти.


В любом случае 100% утилизация означает, что ресурс не может быть использован больше, чем сейчас. И либо работа будет застревать ожидая освобождения / отправляться в очередь, либо будут ошибки. Эти два сценария покрываются соответствующими двумя оставшимися метриками USE:


S — Сатурация, оно же насыщение: мера количества "отложенной" / поставленной в очередь работы.


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


RED


Том Уилки (сейчас работает в Grafana Labs) был фрустрирован методологией USE, а точнее ее плохой применимостью в каких-то случаях и несоответствием практике. Как, например, измерить сатурацию памяти? Или как на практике измерить ошибки системной шины?


Линукс, оказывается, реально фигово репортит счетчики ошибок.
Т. Уилки

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


R — Rate: количество запросов в секунду.
E — Errors: сколько запросов вернули ошибку.
D — Duration: время, затраченное на обработку запроса. Оно же latency, "латенция" (© Света Смирнова:), response time и т.д.


В целом USE больше подходит для мониторинга ресурсов, а RED — сервисов и их ворклоада / полезной нагрузки.


PgBouncer


будучи сервисом, он в тоже время имеет всякие внутренние лимиты и ресурсы. Тоже самое можно сказать и про Postgres, к которому через этот PgBouncer обращаются клиенты. Поэтому для полноценного мониторинга в такой ситуации нужны оба метода.


Чтобы разобраться с тем, как эти методы приложить к баунсеру, необходимо понимать детали его устройства. Недостаточно мониторить его как black-box — "жив ли процесс pgbouncer" или "открыт ли порт", т.к. в случае проблем это не даст понимания, что именно и как сломалось и что делать.


Что вообще делает, как выглядит PgBouncer с точки зрения клиента:


  1. клиент коннектится
  2. [ клиент делает запрос — получает ответ ] x сколько ему нужно раз

Вот я тут изобразиль диаграмму соответствующих стейтов клиента с точки зрения PgBoucer'а:


В процессе login'а авторизация может происходить как локально (файлы, сертификаты, и даже PAM и hba с новых версий), так и удаленно — т.е. в самой базе данных, к которой происходит попытка подключения. Таким образом состояние логина имеет дополнительное подсостояние. Назовем его Executing чтобы обозначить, что в это время выполняется auth_query в базе данных:


Но эти клиентские соединения на самом деле матчатся с соединеними к бекенд/апстрим базе, которые PgBouncer открывает в рамках пула и держит ограниченное количество. И выдают клиенту такое соединение только на время — на время сессии, транзакции или запроса, в зависимости от вида пулинга (определяется настройкой pool_mode). Чаще всего используется transaction pooling (его мы и будем в основном дальше обсуждать) — когда соединение выдается клиенту на одну транзакцию, а в остальное время клиент по факту к серверу не подключен. Таким образом "active" стейт клиента мало о чем нам говорит, и мы его разобьем на подстейты:


Каждый такой клиент попадает в свой пул соединений, которым будут выдаваться для использования настоящие соединения до Postgres. Это и есть основная задача PgBouncer'а — ограничивать количество соединений до Postgres.


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

Так как это может произойти и в случае, когда клиент только логинится и ему надо выполнить запрос для авторизации, то возникает еще и состояние CL_WAITING_LOGIN.


Если теперь посмотреть с обратной стороны — со стороны серверных коннекшенов, то они, соответственно, бывают в таких состояниях: когда происходит авторизация непосредственно после коннекта — SV_LOGIN, выдан и (возможно) используется клиентом — SV_ACTIVE, или свободно — SV_IDLE.


USE для PgBouncer


Таким образом мы приходим к (наивному варианту) Utilization конкретного пула:


Pool utiliz = использованные клиентами соединения / размер пула

У PgBouncer есть специальная служебная база данных pgbouncer, в которой есть команда SHOW POOLS, показывающая текущие состояния коннекшенов каждого пула:

Тут открыто 4 клиентских соединения и все они cl_active. Из 5 серверных соединений — 4 sv_active и одно в новом состоянии sv_used.


Что такое sv_used на самом деле и про разные настройки pgbouncer'а несвязанные с мониторингом

Так вот sv_used означает не "соединение используется", как вы могли подумать, а "соединение было когда-то использовано и давно не использовалось". Дело в том что PgBouncer по умолчанию использует серверные соединения в режиме LIFO — т.е. сначала используются только что освобожденные соединения, потом недавно использованные и т.д. постепенно переходя к давно использованным соединениям. Соответственно серверные соединения со дна такого стека могут "протухнуть". И их перед использованием надо бы проверить на живость, что делается с помощью server_check_query, пока они проверяются состояние будет sv_tested.


Документация гласит, что LIFO включено по умолчанию, т.к. тогда "малое количество коннекшенов получает наибольшую нагрузку. И это дает наилучшую производительность в случае, когда за pgbouncer находится один сервер обслуживающий базу данных", т.е. как бы в самом типичном случае. Я полагаю, что потенциальный буст перформанса происходит из-за экономии на переключении исполнения между несколькими backend процессами посгреса. Но достоверно это выяснить не получилось, т.к. эта деталь имплементации существует уже > 12 лет и выходит за пределы commit history на гитхабе и глубины моего интереса =)


Так вот, мне показалось странным и не соответствующим текущим реалиям, что дефолтовое значение настройки server_check_delay, которая определяет что сервер слишком давно не использовался и его надо бы проверить прежде чем отдавать клиенту, — 30 секунд. Это при том, что по дефолту одновременно включен tcp_keepalive с настройками по-умолчанию — начать проверять соединение keep alive пробами через 2 часа после его idle'инга.
Получается, что в ситуации burst'а / всплеска клиентских соединений, которые хотят что-то выполнять на сервере, вносится дополнительная задержка на server_check_query, который хоть и "SELECT 1; все равно может занимать ~100 микросекунд, а если вместо него поставить просто server_check_query = ';' то можно ~30 микросекунд сэкономить =)


Но и предположение, что выполнять работу всего в нескольких коннекшенах = на нескольких "основных" бекенд процессах постгреса будет эффективнее, мне кажется сомнительным. Постгрес воркер процесс кэширует (мета)информацию про каждую таблицу, к которой было обращение в этом соединении. Если у вас большое количество таблиц, то этот relcache может сильно вырасти и занять много памяти, вплоть до своппинга страниц процесса 0_о. Для обхода этого подойдет настройка server_lifetime (по умолчанию — 1 час), по которой серверное соединение будет закрыто для ротации. Но с другой стороны есть настройка server_round_robin, которая переключит режим использования соединений с LIFO на FIFO, размазав клиентские запросы по серверным соединениям более равномерно.


Наивно снимая метрики из SHOW POOLS (каким-нибудь prometheus exporter'ом) мы можем построить график этих состояний:



Но чтобы дойти до утилизации надо ответить на несколько вопросов:


  • Какой размер пула?
  • Как считать сколько соединений использованы? В шутках или по времени, в среднем или в пике?

Размер пула


Тут всё сложно, как в жизни. Всего в пгбаунсере есть аж пять настроек-лимитов!


  • pool_size можно задать для каждой базы. На каждую пару DB / user создается отдельный пул, т.е. от любого дополнительного пользователя, можно создать еще pool_size бекендов/воркеров Postgres. Т.к. если pool_size не задан, он фолбечится в default_pool_size, который по дефолту 20, то получается, что каждый пользователь имеющий права коннекта к базе (и работающий через pgbouncer) потенциально может создать 20 процессов Postgres, что вроде не много. Но если у вас много разных пользователей баз или самих баз, и пулы прописаны не с фиксированным пользователем, т.е. будут создаваться на лету (а потом по autodb_idle_timeout удаляться), то это может быть опасно =)
    Возможно стоит оставлять default_pool_size маленьким, на всякий пожарный.
  • max_db_connections — как раз нужен для того, чтобы ограничить суммарное количество коннектов к одной базе, т.к. иначе badly behaving клиенты могут насоздавать очень много бекендов/процессов постгреса. И по дефолту тут — unlimited ?_(?)_/?
    Возможно стоит поменять default'овый max_db_connections, например, можно ориентироваться на max_connections вашего Postgres (по дефолту 100). Но если у вас много PgBouncer'ов…
  • reserve_pool_size — собственно, если pool_size весь использован, то PgBouncer может открыть еще несколько коннекшенов до базы. Я так понимаю это сделано, чтобы справиться с всплеском нагрузки. Мы еще вернемся к этому.
  • max_user_connections — Это, наоборот, лимит коннекшенов от одного юзера ко всем базам, т.е. актуально если у вас несколько баз и в них ходят под одинаковыми юзерами.
  • max_client_conn — сколько клиентских соединений вообще суммарно PgBouncer будет принимать. Дефолт, как впрочем привычно, имеет очень странное значение — 100. Т.е. предполагается, что если вдруг ломятся больше 100 клиентов, то им надо просто практически молча на уровне TCP отдавать reset и всё (ну в логах, надо признать, при этом будет "no more connections allowed (max_client_conn)").
    Возможно стоит сделать max_client_conn >> SUM ( pool_size'ов ), например, в 10 раз больше.

Кроме SHOW POOLS служебная псевдо-база pgbouncer предоставляет еще и команду SHOW DATABASES, показывающую реально применяемые к конкретному пулу лимиты:


Серверные соединения


Еще раз — как измерять сколько соединений использованы?
В шутках в среднем / в пике / по времени?


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



Тут все изменения в нагрузке / использовании соединений просто фикция, артефакт рестартов сборщика статистики. Вот можно посмотреть на графики соединений в Postgres'е за это время и на файловые дескрипторы баунсера и PG — никаких изменений:



Вернемся к вопросу утилизации. Мы в нашем сервисе решили использовать комбинированный подход — мы сэмплим SHOW POOLS раз в секунду, а раз в минуту рендерим и среднее и максимальное количество соединений в каждом стейте:



А если поделить количество этих active state соединений на размер пула, получим среднюю и пиковую утилизацию данного пула и сможем алертить, если она близка к 100%.


Кроме того у PgBouncer есть команда SHOW STATS которая покажет статистику использования для каждой проксируемой базы:

Нас тут больше всего интересует колонка total_query_time — время, проведенное всеми соединениями в процессе выполнения запросов в postgres. А с версии 1.8 есть еще и метрика total_xact_time — время проведенное в транзакциях. Исходя из этих метрик мы можем построить утилизацию времени серверных соединений, этот показатель не подвержен, в отличие от рассчитанного из стейтов соединений, проблемам семплинга, т.к. эти total_..._time счетчики являются кумулятивными и ничего не пропускают:



Сравните

Видно, что семплинг не показывает все моменты высокой ~100% утилизации, а query_time — показывает.


Saturation и PgBouncer


Зачем вообще нужно следить за Saturation, ведь по высокой утилизации уже и так понятно, что все плохо?


Проблема в том, что как ни измеряй утилизацию, даже накопленные счетчики не могут показать локальное 100% использование ресурса, если оно происходит только на очень коротких интервалах. Например, у вас есть какие-нибудь кроны или другие синхронные процессы, которые могут одновременно по команде начать делать запросы в базу. Если эти запросы будут короткие, то утилизация, измеренная на масштабах минуты и даже секунды, может быть низкой, но при этом в какой-то момент эти запросы были вынуждены ждать очереди на исполнение. Это похоже с ситуацией не 100% CPU usage и высокого Load average — вроде процессорное время еще есть, а тем не менее много процессов ждут в очереди на исполнение.


Как можно отслеживать такую ситуацию — ну опять таки, мы можем просто считать количество клиентов в состоянии cl_waiting согласно SHOW POOLS. В нормальной ситуации таких — ноль, а больше нуля означает переполнение этого пула:



Тут остается проблема с тем, что SHOW POOLS можно только сэмплить, и в ситуации с синхронными кронами или чем-то таким, мы можем просто пропустить и не увидеть таких waiting клиентов.


Можно использовать такое ухищрение, pgbouncer сам умеет детектить 100% использование пула и открывать резервный пул. За это отвечают две настройки: reserve_pool_size — за его размер, как я уже говорил, и reserve_pool_timeout — сколько секунд должен какой-то клиент быть waiting прежде чем использовать резервный пул. Таким образом, если мы видим на графике серверных соединений, что количество открытых до Postgres коннекшенов больше чем pool_size, значит была сатурация пула, как вот тут:

Явно что-то типа кронов раз в час делают много запросов и полностью занимают пул. И даже несмотря на то, что мы не видим сам момент, когда active соединения превышают pool_size лимит, все равно pgbouncer был вынужден открывать дополнительные соединения.


Так же на этом графике хорошо видна работа настройки server_idle_timeout — через сколько переставать держать и закрывать соединения, которые не используются. По дефолту это 10 минут, что мы и видим на графике — после пиков active ровно в 5:00, в 6:00 и т.д. (согласно cron'у 0 * * * *), соединения висят idle + used еще 10 минут и закрываются.


Если же вы живете на острие прогресса и обновили PgBouncer за прошедшие 9 месяцев, то сможете найти в SHOW STATS колонку total_wait_time, которая лучше всего показывает сатурацию, т.к. кумулятивно считает
время проведенное клиентами в waiting состоянии. Например, тут — стейт waiting появился в 16:30:

А wait_time, сравнимый и явно влияющий на average query time, можно увидеть начиная с 15:15 и почти до 19:


Тем не менее мониторинг состояний клиентских соединений все равно очень полезен, т.к. позволяет узнать не только факт, что к такой-то базе данных все соединения истрачены и клиенты вынуждены ждать, но и благодаря тому, что SHOW POOLS разбито на отдельные пулы по пользователям, а SHOW STATS — нет, позволяет выяснить какие именно клиенты использовали все коннекты до заданной базы — по колонке sv_active соответствующего пула. Или по метрике


sum_by(user, database, metric(name="pgbouncer.clients.count", state="active-link")):


Мы в okmeter пошли даже дальше и добавили разбивку используемых соединений по IP адресам клиентов, которые их открыли и используют. Это позволяет понять, какие именно инстансы приложения ведут себя не так:

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


Errors


Тут ничего особо хитрого нет: pgbouncer пишет логи, в которых сообщает об ошибках, если достигнут лимит клиентских соединений, таймаут подключения к серверу и т.п. Мы пока до логов pgbouncer сами не добрались:(


RED для PgBouncer


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


Requests


Тут казалось бы все довольно просто для SQL базы и для прокси / connection пулера в такую базу ­— клиенты выполняют SQL statement'ы, которые и есть Requests. Из SHOW STATS берем total_requests и строим график его производной по времени


rate(metric(name="pgbouncer.total_requests", database: "*"))


Но на самом деле есть разные режимы пуллинга, и самый распространенный — transactions. Единица работы этого режима — транзакция, а не запрос. В соответствии с этим начиная с версии 1.8 Pgbouсner предоставляет уже две другие статистики — total_query_count, вместо total_requests, и total_xact_count — количество прошедших транзакций.


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


rate(metric(name="total_requests", database="*")) / rate(metric(name="total_xact", database="*"))


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


RED Errors


Понятно, что RED и USE пересекаются на мониторинге ошибок, но как мне кажется errors в USE в основном про ошибки обработки запроса из-за 100% утилизации, т.е. когда сервис отказывается принять больше работы. А errors для RED было бы лучше измерять ошибки именно с точки зрения клиента, клиентских запросов. То есть не только в ситуации когда пул в PgBouncer'е переполнен или сработал другой лимит, но так же когда сработали таймауты запросов, такие как "canceling statement due to statement timeout", cancel'ы и rollback'и транзакций самим клиентом, т.е. более высокоуровневые, более близкие к бизнес-логике типы ошибок.


Durations


Тут нам опять поможет SHOW STATS с кумулятивными счетчиками total_xact_time, total_query_time и total_wait_time, поделив которые на количество запросов и транзакций соответственно, получим среднее время запроса, среднее время транзакции, среднее время ожидания на одну транзакцию. Я уже показывал график про первое и третье:


Что тут можно еще классного получить? Известный антипаттерн в работе с базой и Postgres в частности, когда приложение открывает транзакцию, делает запрос, потом начинает (долго) обрабатывать его результаты или того хуже — ходит в какой-то другой сервис / базу и делает там запросы. Все это время транзакция "висит" в постгресе открытой, сервис потом возвращается и делает еще какие-то запросы, обновления в базе и только потом закрывает транзакцию. Для постгреса это бывает особенно неприятно, т.к. pg-воркеры — штука дорогая. Так вот мы можем мониторить, когда такое приложение пребывает idle in transaction в самом постгресе — по колонке state в pg_stat_activity, но там все те же описанные проблемы с семплингом, т.к. pg_stat_activity дает только текущую картину. В PgBouncer'е же мы можем вычесть время проведенное клиентами в запросах total_query_time из времени проведенного в транзакциях total_xact_time — это будет как раз время такого idling'а. Если результат еще поделить на total_xact_time, то оно получится отнормированным: значение 1 соответствует ситуации, когда клиенты 100% времени находятся idle in transaction. И с такой нормировкой дает возможность легко понять насколько все плохо:



Кроме того, возвращаясь к Duration, метрику total_xact_time - total_query_time можно поделить на количество транзакция, чтобы увидеть сколько в среднем приложение idle'ится на одну транзакцию.




На мой взгляд методы USE / RED полезны больше всего для структурирования того, какие метрики вы снимаете и зачем. Так как мы занимаемся мониторингом full-time и нам приходится делать мониторинг для самых разных компонентов инфраструктуры, эти методы помогают нам снимать правильные метрики, делать правильные графики и триггеры для наших клиентов.


Хороший мониторинг нельзя сделать сразу, это итеративный процесс. У нас в okmeter.io как раз continuous monitoring (много чего есть, но завтра будет лучше и детальнее:)

Комментарии (0)