F.55. pg_throttle#

F.55. pg_throttle

F.55. pg_throttle #

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

F.55.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.55.2. Настройка #

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

# postgresql.conf
session_preload_libraries = 'pg_throttle'

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

CREATE EXTENSION pg_throttle;

Примечание

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

F.55.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.55.4. 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');
TABLE throttlings(
    username text,
    query_throttle int,
    XXX_throttle int
);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

F.55.6.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.55.6.7. Мертвые кортежи: #

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

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

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

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

F.55.6.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 (ограничено) из той же таблицы