F.63. pg_throttle#

F.63. pg_throttle

F.63. pg_throttle #

pg_throttle - это расширение Tantor SE, которое добавляет функцию ограничения запросов.

F.63.1. Функциональность #

Расширение позволяет установить верхний предел количества кортежей, отправляемых от узлов сканирования к верхним узлам. Например, узел SeqScan может передавать не более 100 кортежей в секунду к верхним узлам.

Ограничение устанавливается как количество кортежей, возвращаемых узлом сканирования в секунду. Поддерживаемые ScanNodes:

  • SeqScan

  • IndexScan/IndexOnlyScan

  • BitmapHeapScan/BitmapIndexScan

  • CustomScan

  • ForeignScan

  • ResultScan/ValuesScan

  • FunctionScan

Также поддерживаются другие операторы DML, которые могут изменять таблицу. Например, INSERT, UPDATE, DELETE, но только если указано предложение RETURNING.

Существуют 2 режима работы для ограничения скорости:

  • per-node - ограничение устанавливается для каждого узла независимо

    • большая скорость (по сравнению с режимом запроса)

    • более тонкая степень управления

  • query - состояние ограничения скорости разделяется между всеми узлами

    • более строгое ограничение выполнения запросов

Скорость ограничения измеряется как количество кортежей, возвращаемых соответствующим узлом за 1 секунду. Расширение отслеживает только количество кортежей, и если оно превышает лимит (менее чем за 1 секунду), оно просто спит оставшееся время. Например, время ограничения составляет 100 (100 кортежей в секунду):

  • 0.1s - 35 кортежей возвращено

  • 0.2s - возвращено 35 кортежей

  • 0.3s - возвращено 30 кортежей

  • 0.4-1s - сон

F.63.2. Настройка #

Расширение должно быть загружено с использованием session_preload_libraries. Во время загрузки оно обращается к своей таблице конфигурации (см. ниже) и устанавливает хуки, если необходимо. Добавьте расширение в postgresql.conf

# postgresql.conf
session_preload_libraries = 'pg_throttle'

Наконец, запустите БД и создайте расширение:

CREATE EXTENSION pg_throttle;

Примечание

Для расширения может быть указана пользовательская схема

F.63.3. Начало работы #

Установите расширение, как обсуждалось в разделе настройки.

После этого вы можете захотеть ограничить себя, чтобы протестировать расширение. Выполните это:

CREATE TABLE tbl(
    value int
);
INSERT INTO tbl SELECT * FROM generate_series(1, 200);
SELECT throttle_user_per_node_all(current_user, 100);

Теперь этот пользователь (которого вы используете для входа) будет ограничен. Выйдите, войдите снова и выполните этот запрос:

SELECT * FROM tbl;

Продолжительность запроса должна составлять около 2 секунд: 200 кортежей / 100 кортежей в секунду = 2 секунды задержки.

F.63.4. Поддержка Cgroup v2 #

Расширение позволяет назначать фоновые процессы в предварительно созданную Cgroup версии 2 (группа управления версии 2). Используя скрипт pg_throttle_setup_cgroup.sh, вы можете создать и настроить группу управления с указанными ограничениями. После установки этого расширения скрипт будет находиться в каталоге share/postgresql/extension/pg_throttle_setup_cgroup.sh.

Скрипт позволяет настроить следующие ограничения:

  • --cpu-max-percent N - предел использования процессора в процентах (0-100 или -1 для неограниченного)

  • --memory-quota SIZE - Ограничение памяти (например, 512M, 2G)

  • --cpuset CPUS - Разрешенные процессоры (например, 0-3,6)

  • --io-limit LIMIT - лимит ввода-вывода: (/path|device MAJ:MIN):rbps=1M:wbps=1M:riops=100:wiops=50

Вы можете узнать больше обо всех параметрах скрипта с помощью --help.

Чтобы создать контрольную группу без ограничений ttcg, выполните команду:

sudo ./pg_throttle_setup_cgroup.sh --group-name "ttcg"

Creating new cgroup 'ttcg'...
Group 'ttcg' configured with parameters:
 cpu.max      = max 100000
 cpu.weight   = 100
 memory.max   = max
 pids.max     = max
 cpuset.cpus  = 
 io.max       = 

Чтобы установить предел потребления ЦП на 50% для этой контрольной группы, выполните команду:

sudo ./pg_throttle_setup_cgroup.sh --group-name "ttcg" --update --cpu-max-percent 50

Group 'ttcg' exists. Updating specified parameters...
Group 'ttcg' configured with parameters:
 cpu.max      = 800000 100000
 cpu.weight   = 100
 memory.max   = max
 pids.max     = max
 cpuset.cpus  = 
 io.max       = 

Вы можете аналогичным образом установить или обновить другие ограничения для контрольной группы.

Формат ограничений ввода-вывода предоставляет два варианта для указания:

  • /path/to/pgdata_or_tablespace_dir:rbps=1M:wbps=1M:riops=100:wiops=50

  • 259:0:rbps=1M:wbps=1M:riops=100:wiops=50

В первом случае вы можете указать путь к PGDATA каталогу или путь к tablespace каталогу, а затем значения ограничений ввода-вывода (параметры разделяются символом :).

Во втором случае вы можете указать основной и второстепенный номера блочного устройства, на котором находится директория PGDATA или директория tablespace, а затем значения ограничений ввода-вывода (параметры разделяются символом :)

При указании пути к директории PGDATA или директории tablespace, скрипт попытается автоматически определить основной и второстепенный номера блочного устройства, на котором расположены эти директории. API Cgroup требует указания основного и второстепенного номеров блочного устройства для установки ограничений на ввод-вывод, скрипт помогает упростить определение номеров блочного устройства, предоставляя возможность указать только путь к нужной директории.

Чтобы получить настройки Cgroup в формате параметров скрипта, выполните команду:

sudo ./pg_throttle_setup_cgroup.sh --group-name "ttcg" --show

Cgroup settings for group 'ttcg':
--cpu-max-percent -1
...

Чтобы сбросить все настройки Cgroup, выполните команду:

sudo ./pg_throttle_setup_cgroup.sh --group-name "ttcg" --show

Resetting all limits in group 'ttcg'...

Чтобы удалить Cgroup, выполните команду:

sudo ./pg_throttle_setup_cgroup.sh --group-name "ttcg" --delete

Group 'ttcg' deleted

Чтобы процессы бэкенда, выполняющие запросы конкретного пользователя, автоматически назначались Cgroup, вам нужно вызвать функцию throttle_user_cgroup, где вы указываете имя пользователя и имя Cgroup:

SELECT throttle_user_cgroup('analytics', 'ttcg');

Важно знать, что после перезапуска операционной системы требуемая Cgroup будет удалена. Чтобы автоматически создать требуемую Cgroup, вы можете создать unit systemd, который создаст требуемую Cgroup при запуске операционной системы.

Пример шаблона системной единицы systemd можно найти в pg-throttle-cgroup.service.

На основе шаблона unit-файла systemd, вам нужно создать ссылку на шаблон:

sudo systemctl link ./pg-throttle-cgroups@.service

Шаблон unit-файла systemd принимает параметр, который указывает имя Cgroup. Чтобы создать службу systemd, которая будет воссоздавать Cgroup ttsg при запуске операционной системы, необходимо выполнить команду:

sudo systemctl enable pg-throttle-cgroups@ttcg.service
sudo systemctl start pg-throttle-cgroups@ttcg.service

Проверьте результат запуска службы systemd:

sudo systemctl status pg-throttle-cgroups@ttcg.service

Если вам нужно удалить эту службу systemd и ссылку на шаблон единицы, вам нужно выполнить команду:

sudo systemctl disable pg-throttle-cgroup@ttcg.service
sudo systemctl daemon-reload

Остановка или удаление службы systemd не удаляет созданную Cgroup. Чтобы удалить Cgroup, вам нужно запустить скрипт pg_throttle_setup_cgroup.sh с параметром --delete.

Если точка монтирования файловой системы Cgroup cgroup2fs не расположена по адресу /sys/fs/cgroup, то расширение имеет GUC pg_throttle.cgroup_mount_point для указания пути к точке монтирования:

ALTER SYSTEM SET pg_throttle.cgroup_mount_point TO '/custom/mount/point/to/cgroup2fs';
SELECT pg_reload_conf();

Скрипт pg_throttle_setup_cgroup.sh также имеет параметр --cgroup-root для явного указания точки монтирования файловой системы Cgroups cgroup2fs.

F.63.5. Конфигурация #

pg_throttle.cgroup_mount_point - точка монтирования файловой системы Cgroup cgroup2fs (по умолчанию /sys/fs/cgroup).

F.63.6. API & примеры #

FUNCTION throttle_user_per_node(username text, 
                                seq_scan_max int
                                idx_scan_max int,
                                idx_only_scan_max int
                                bitmap_heap_scan_max int,
                                bitmap_index_scan_max int
                                custom_scan_max int,
                                foreign_scan_max int
                                tid_range_scan_max int,
                                modify_scan_max int,
                                values_scan_max int,
                                result_scan_max int,
                                func_scan_max int);

Это основная функция для установки ограничения пользователя. Однако, обычно вы будете использовать другие сокращенные функции ниже.

username - имя пользователя, для которого должно быть ограничение. Если указанный пользователь не существует, будет выброшено исключение.

XXX_max - другие аргументы указывают ограничение для соответствующего узла:

  • seq_scan_max - SeqScan

  • idx_scan_max - Индексное сканирование

  • idx_only_scan_max - Только индексное сканирование

  • bitmap_heap_scan_max - Битовая куча сканирования

  • bitmap_index_scan_max - Битовый индексный скан

  • custom_scan_max - Пользовательское сканирование

  • foreign_scan_max - Иностранное сканирование

  • tid_range_scan_max - Диапазонное сканирование TID

  • modify_scan_max - операторы модификации DML (INSERT, UPDATE и т.д.), только для RETURNING

  • values_scan_max - Значения Сканирования

  • result_scan_max - Результатное сканирование

  • func_scan_max - Функция Сканирования

Для аргументов XXX_max допустимое значение:

  • NULL - узел не будет ограничен. Для query_max = NULL - включает режим на узел

  • 0 < XXX_max - устанавливает XXX_max кортежей в секунду обработки в секунду для соответствующего узла

Примечание

Отрицательное или нулевое значение не допускается. Если вы хотите отключить ограничение - установите его NULL

Пример - ограничение пользователя ‘analytics’.

SELECT throttle_user_per_node('analytics',
                              100,          -- SeqScan
                              1000, 1000,   -- Index/IndexOnly Scan
                              1000, 1000,   -- Bitmap Heap/Index Scan
                              NULL,         -- Custom Scan (disable throttling)
                              10,           -- Foreign Scan
                              100,          -- TID Range Scan
                              NULL,         -- DML (disable throttling)
                              NULL, NULL,   -- Values/Result Scan (disable throttling)
                              1000);        -- Function Scan
FUNCTION throttle_user_per_node_all(username text, max int);

Это сокращенная функция для throttle_user_per_node, которая устанавливает все XXX_max в max.

Пример:

SELECT throttle_user_per_node_all('analytics', 100);
FUNCTION throttle_user_query(username text, max int);

Эта функция ограничивает все узлы Scan, используя общее состояние. Это означает, что все узлы имеют один и тот же счетчик ограничения. Счетчик применяется ко всем поддерживаемым узлам - вы не можете установить конкретные узлы

Пример:

SELECT throttle_user_query('analytics', 100);
FUNCTION unthrottle_user(username text);

Эта функция удаляет ограничение пропускной способности для указанного пользователя.

Пример:

SELECT unthrottle_user('analytics');
FUNCTION throttle_user_cgroup(username text, cgroup_name text);

Эта функция устанавливает для конкретного пользователя имя Cgroup, в которой будут запускаться фоновые процессы, обслуживающие запросы этого пользователя.

Пример:

SELECT throttle_user_cgroup('analytics', 'ttcg');
TABLE throttlings(
    username text,
    query_throttle int,
    XXX_throttle int
);

Это основная таблица конфигурации расширения - хранит конфигурацию ограничения для пользователей:

  • username - имя пользователя с ограничением скорости

  • query_throttle - ограничение для режима запроса (NULL, если режим на узел)

  • XXX_throttle - это значение ограничения для соответствующего узла

Эта таблица читается расширением при запуске бэкенда, во время инициализации расширения. Поэтому пользователь должен иметь к ней доступ, но это создает риски безопасности. Чтобы решить эту проблему, см. раздел безопасность ниже.

F.63.7. Детали реализации #

Расширение реализовано с использованием декораторов.

Когда ExecutorStart_hook срабатывает, мы ищем в его PlanState интересующие узлы (которые поддерживаются). Если такой узел найден, мы:

  1. Создаем наш регулирующий узел

  2. Копируем содержимое старого узла

  3. Сохраняем ExecProcNode старого узла (и другие вещи)

  4. Заменяем ExecProcNode в PlanState (он будет вызываться исполнителем)

Затем во время выполнения:

  1. Исполнитель вызывает PlanState->ExecProcNode

  2. Вызывается наша функция ограничения

  3. Вызываем сохраненный ExecProcNode - получаем кортеж

  4. Обрабатываем логику ограничения

  5. Возвращаем кортеж

Это относится ко всем поддерживаемым узлам, за исключением CustomScan и ForeignScan. Мы не можем сделать то же самое для них, потому что расширения создают свои собственные структуры поверх них (также декоратор). Поэтому, делая это снова - мы перезаписываем их данные. Чтобы преодолеть это, мы создаем такой декоратор для его интерфейса - CustomScanMethods и FdwRoutine.

F.63.8. Ограничения в использовании #

Существует множество предостережений, о которых вы должны знать

F.63.8.1. Декораторы #

Расширение использует декораторы для реализации этой функциональности. Если какое-либо другое расширение делает это, однажды они могут перезаписать свои данные, что приведет к непредсказуемому поведению.

Если вы обнаружите это, 1) пожалуйста, сообщите нам (чтобы предупредить других пользователей) и 2) вы должны выбрать только одно расширение.

F.63.8.2. Параллельные рабочие процессы #

Параллельные рабочие обрабатываются таким же образом - они также используют Scan узлы, и к ним применяется регулирование. Но у них отдельные адреса памяти, поэтому режим запроса не будет работать для них.

F.63.8.3. Пилообразная волна #

Расширение не распределяет нагрузку равномерно - оно просто засыпает, когда порог достигнут. Из-за этого вы можете увидеть графики в виде зубцов пилы.

F.63.8.4. Применение изменений #

Расширение кэширует конфигурацию ограничения в статических переменных. Оно считывает их при инициализации расширения и использует позже.

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

F.63.8.5. Пользователь, к которому это применяется #

Логика ограничения применяется к SESSION_USER. Если вы измените его, например, используя SET ROLE, логика ограничения будет применяться к SESSION_USER с его начальной конфигурацией.

F.63.8.6. Безопасность #

Таблица конфигурации throttlings читается во время запуска сессии (инициализация расширения) для получения параметров ограничения пользователя сессии. Предоставление доступа SELECT всем пользователям кажется утечкой безопасности, поэтому для таблицы включена встроенная RLS. Существует одно правило - CURRENT_USER = username, так что пользователи читают только ограничения для самих себя.

Но при запуске у пользователей по умолчанию нет доступа к этой таблице, и все запросы приведут к ошибке отказа в разрешении. Чтобы исправить это, предоставьте SELECT для public (или любой другой роли, которую вы хотите).

REVOKE ALL ON TABLE throttlings FROM public;
GRANT SELECT ON TABLE throttlings TO public;

Теперь только суперпользователи могут изменять таблицу конфигурации throttlings;

Примечание

throttlings уже имеет RLS POLICY, поэтому пользователь сессии может работать только со своими собственными записями.

F.63.8.7. Мертвые кортежи: #

Расширение обрабатывает только количество кортежей возвращенных узлом, но не обрабатывает общее количество обращений к страницам/буферам. Это приводит к ситуациям, когда общее количество затронутых страниц/буферов значительно превышает наложенное ограничение.

Случаи, когда это может сработать:

  • Ключ сканирования (фильтр) - если в узле есть фильтр, возвращаются только кортежи, удовлетворяющие фильтру - может быть обработано сотни кортежей, прежде чем какой-либо из них будет возвращен из этого узла.

  • Мертвые кортежи - мертвые кортежи также читаются со страниц, но они не будут возвращаться из узла - сотни мертвых кортежей в таблице могут повлиять на производительность

F.63.8.8. Управление скоростью DML #

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

Но есть случаи, когда это происходит:

  1. Эти операторы имеют RETURNING - в этом случае логика применяется правильно, потому что этот узел возвращает несколько запросов

  2. Ввод для этих узлов осуществляется из других поддерживаемых узлов сканирования - примеры этого включают:

    • DELETE FROM tbl WHERE value > 1 - ввод для DELETE это SeqScan (ограничено) из той же таблицы

    • INSERT INTO tbl SELECT * FROM tbl2 - ввод для INSERT - SeqScan (ограниченный) из другой таблицы

    • INSERT INTO tbl SELECT * FROM generate_series(1, 100) - ввод для INSERT является FunctionScan (ограничено)

    • UPDATE tbl SET value = value + 1 - ввод для UPDATE это SeqScan (ограничено) из той же таблицы