Некоторые настройки Versola по умолчанию меняют гарантию на производительность или простоту. Они сделаны осознанно, видны в коде, и у каждой есть цена, которую нужно знать до запуска Versola в продакшене. В каждой записи ниже сказано, что мы сделали, зачем, что это стоит и что можно изменить. Чтобы прочитать запись, раскройте её.
Короткоживущие таблицы UNLOGGED состояние входа и защита от повторов не переживают сбой PostgreSQL
Что мы сделали
PostgreSQL записывает каждое изменение в журнал упреждающей записи (WAL) до того, как применит его, чтобы ничего не потерялось при сбое. Для таблиц, строки которых живут не дольше нескольких минут, Versola это отключает. Эти таблицы UNLOGGED:
| Схема | Таблицы | Что такое строка | Время жизни строки |
|---|---|---|---|
auth | auth_conversations | Вход в процессе (многошаговый вход: пароль, OTP, passkey, регистрация) | authConversationTtlSeconds тенанта, 15 минут, если не настроено иначе |
auth | authorization_codes | Код авторизации | 5 минут |
auth | pushed_authorization_requests | Pushed authorization request (PAR) | requestUriTtl, 60 секунд, если не настроено иначе |
auth | dpop_proofs_0 … dpop_proofs_11 | Уже встречавшийся DPoP proof (защита от повтора) | Окно iat, в пределах минуты |
auth | client_assertions_0 … client_assertions_11 | Уже встречавшийся client assertion private_key_jwt (защита от повтора) | До exp assertion (не больше времени жизни client assertion тенанта: по умолчанию 300 секунд, максимум 900) |
edge | edge_dpop_proofs_0 … edge_dpop_proofs_11 | DPoP proof, который видел edge (защита от повтора) | Окно iat |
Таблицы состояния входа стали UNLOGGED в 0.7.0. Таблицы защиты от повтора UNLOGGED с момента создания. Каждая из них — кольцо из 12 партиций, и каждая партиция UNLOGGED сама по себе.
Сессии, refresh-токены, пользователи, согласия и конфигурация лежат в обычных таблицах с журналом. В UNLOGGED-таблицах не хранятся ни учётные данные, ни сессии, ни токены, ни согласия.
Зачем
Журналировать данные, которые устаревают за минуту, значит платить за долговечность впустую, причём дорого. В одной из наших кампаний нагрузочного тестирования, на ступенчатой нагрузке в миллион пользователей, таблицы состояния входа давали около 63% байтов WAL и около 40% операторов, пишущих в WAL. База auth упиралась в ожидание коммитов WAL, а не во время выполнения операторов (versola#598). Таблицы защиты от повтора пишутся на каждый запрос токена (в auth) и на каждый проксируемый запрос (в edge), а строки в них через минуту бесполезны.
Что это стоит
| Событие | Что происходит с этими таблицами |
|---|---|
| Штатная остановка или перезапуск PostgreSQL | Строки сохраняются |
Восстановление после сбоя (kill -9 PostgreSQL, отказ хоста, пропадание питания) | Таблицы очищаются |
| Переключение на резервную реплику | Реплика не получает строки и видит таблицы пустыми |
| Восстановление из базовой резервной копии или point-in-time recovery | Таблицы возвращаются пустыми |
После события, очищающего таблицы:
- Пользователь посреди входа начинает заново с
/authorize. - Ещё не обменянный код авторизации или ещё не использованный pushed request отклоняется как недействительный, и клиент повторяет попытку.
- Уже обменянный код авторизации свою работу сделал: сессия и refresh-токен лежат в таблицах с журналом. Повторное предъявление такого кода после сбоя отклоняется как неизвестный код, а не отзывает заодно семейство refresh-токенов, выпущенное при первом обмене.
- Защита от повторов DPoP proof и client assertion начинается с пустого состояния. Уже использованный proof или assertion можно предъявить ещё раз, пока не пройдёт его окно: примерно
iatleeway (dpop.iat-leeway, по умолчанию 60 секунд) для DPoP proof и до времени жизни assertion для client assertion. Перезапуск сервиса несёт такой же риск.
Как вернуть журналирование
Делайте это, только если принимаете цену: вернётся нагрузка на WAL, а с ней и описанное выше ограничение. Это делается вручную, вне миграций и тестов Versola, и на ваш риск.
ALTER TABLE … SET LOGGED берёт блокировку ACCESS EXCLUSIVE и перезаписывает таблицу и её индексы, поэтому все операторы к таблице ждут. Таблицы небольшие, и это быстро, но выполняйте при низкой нагрузке. Перезапись один раз попадает в WAL как полная копия того, что в таблице на этот момент.
Таблицы состояния входа, в схеме, где живёт сервис auth (в настройках по умолчанию это auth):
ALTER TABLE auth.auth_conversations SET LOGGED;
ALTER TABLE auth.authorization_codes SET LOGGED;
ALTER TABLE auth.pushed_authorization_requests SET LOGGED;
Таблицы защиты от повтора секционированы, а PostgreSQL отказывает в SET LOGGED для секционированной родительской таблицы (This operation is not supported for partitioned tables). Меняйте 12 партиций каждой по одной:
DO $$
BEGIN
FOR i IN 0..11 LOOP
EXECUTE format('ALTER TABLE auth.dpop_proofs_%s SET LOGGED', i);
EXECUTE format('ALTER TABLE auth.client_assertions_%s SET LOGGED', i);
EXECUTE format('ALTER TABLE edge.edge_dpop_proofs_%s SET LOGGED', i);
END LOOP;
END $$;
Замените auth и edge на схемы вашего развёртывания. Если у edge своя база, выполните часть edge.edge_dpop_proofs_* в ней. Проверьте результат: p — с журналом, u — UNLOGGED.
SELECT n.nspname, c.relname, c.relpersistence
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND c.relname ~ '^(auth_conversations|authorization_codes|pushed_authorization_requests|dpop_proofs_[0-9]+|client_assertions_[0-9]+|edge_dpop_proofs_[0-9]+)$'
ORDER BY 1, 2;
Что учесть:
- Журналируемость — свойство таблицы, а не Flyway. Versola не проверяет её во время работы. Таблица, которую более поздний релиз создаст заново, снова будет
UNLOGGED, поэтому повторяйте проверку после обновления. До версии 1.0.0 Versola не гарантирует совместимость. - Партиция, добавленная позже без ключевого слова, журналируется. Тесты Versola следят за этим для таблиц защиты от повтора, но о вашем ручном изменении они не знают.
Чтобы вернуть всё назад, выполните те же операторы с SET UNLOGGED.
Refresh-токены не скользят пользователь входит заново, когда refresh-токен достигает фиксированного срока, даже если активен
Что мы сделали
Срок жизни refresh-токена фиксируется при выпуске. Это refreshTokenTtl клиента, по умолчанию 180 дней, и отсчитывается от входа. Использование refresh-токена его не продлевает, а токен, заменяющий ротированный, сохраняет исходный срок. До 0.7.0 срок был 90 дней и продлевался при использовании.
Зачем
Токен, который продлевается при каждом использовании, никогда не истекает у активного пользователя, поэтому один утёкший refresh-токен остаётся действительным, пока владелец пользуется приложением. Фиксированный срок ставит этому верхнюю границу.
Это ещё и экономит записи в базу, а значит, и WAL. Обновление токена, привязанного к ключу DPoP или к сертификату, без смены scope теперь только читает базу; раньше каждое обновление записывало новый срок. Обновление — самый частый запрос токена, поэтому одна запись меньше на каждое обновление заметна на больших объёмах.
Что это стоит
Пользователь входит заново, когда refresh-токен достигает срока жизни, даже если вчера был активен. Клиент должен обработать invalid_grant при обновлении и снова провести пользователя через поток авторизации.
Что можно изменить
Задайте refreshTokenTtl для клиента (в днях на странице клиента, Refresh Token TTL). Режима со скольжением нет.
Конфигурация доходит до auth и edge с задержкой изменение в консоли администратора видно не сразу
Что мы сделали
central — источник истины для конфигурации: клиентов, ресурсов, scope, ролей, разрешений, пресетов и ключей подписи. auth и edge держат копию в памяти и перезагружают её каждые configuration-cache-refresh-interval.
Зачем
Запросы обслуживаются из памяти и не ждут обращения к central.
Что это стоит
Изменение, сделанное в консоли администратора или через Central API, доходит до auth и edge не позже чем через один интервал. Значение по умолчанию при генерации — 5 минут для vps и Kubernetes и 1 минута локально. За это время auth и edge могут ещё применять прежнюю конфигурацию, например для только что зарегистрированного клиента или только что изменённого разрешения.
Что можно изменить
Задайте меньший configuration-cache-refresh-interval в конфигурации auth и edge. Чем короче интервал, тем больше запросов к central. См. Конфигурацию.
Отзыв распространяется через уведомления реплика, пропустившая уведомление, какое-то время принимает отозванный токен
Что мы сделали
Каждая реплика edge держит список отозванных токенов в памяти и проверяет по нему каждый запрос. Когда токен отзывают, реплики узнают об этом через LISTEN/NOTIFY PostgreSQL. На случай сбоя каждая реплика сверяет свой список с базой каждые revocation.reload-interval, по умолчанию 600 секунд, с джиттером.
Зачем
Проверка отзыва не должна стоить обращения к базе на каждый проксируемый запрос.
Что это стоит
- Реплика, пропустившая уведомление, например из-за обрыва слушающего соединения, принимает отозванный токен до следующей сверки: не дольше интервала перезагрузки.
LISTENне работает через пулер соединений в режиме transaction. Для уведомленийedgeнужно прямое соединение (postgres.notifications-url), и с пулером без негоedgeне запускается.
Что можно изменить
Задайте меньший revocation.reload-interval, чтобы сузить окно, ценой лишних обращений к базе. См. конфигурацию Edge.