Компромиссы — Versola Docs
VersolaVersola/docs
До 1.00.7.1versola.kzGitHub

Компромиссы

Инженерные компромиссы, где Versola отказывается от гарантии ради скорости или простоты, что стоит каждый из них и что можно изменить.

Некоторые настройки Versola по умолчанию меняют гарантию на производительность или простоту. Они сделаны осознанно, видны в коде, и у каждой есть цена, которую нужно знать до запуска Versola в продакшене. В каждой записи ниже сказано, что мы сделали, зачем, что это стоит и что можно изменить. Чтобы прочитать запись, раскройте её.

Короткоживущие таблицы UNLOGGED состояние входа и защита от повторов не переживают сбой PostgreSQL

Что мы сделали

PostgreSQL записывает каждое изменение в журнал упреждающей записи (WAL) до того, как применит его, чтобы ничего не потерялось при сбое. Для таблиц, строки которых живут не дольше нескольких минут, Versola это отключает. Эти таблицы UNLOGGED:

СхемаТаблицыЧто такое строкаВремя жизни строки
authauth_conversationsВход в процессе (многошаговый вход: пароль, OTP, passkey, регистрация)authConversationTtlSeconds тенанта, 15 минут, если не настроено иначе
authauthorization_codesКод авторизации5 минут
authpushed_authorization_requestsPushed authorization request (PAR)requestUriTtl, 60 секунд, если не настроено иначе
authdpop_proofs_0 … dpop_proofs_11Уже встречавшийся DPoP proof (защита от повтора)Окно iat, в пределах минуты
authclient_assertions_0 … client_assertions_11Уже встречавшийся client assertion private_key_jwt (защита от повтора)До exp assertion (не больше времени жизни client assertion тенанта: по умолчанию 300 секунд, максимум 900)
edgeedge_dpop_proofs_0 … edge_dpop_proofs_11DPoP 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 можно предъявить ещё раз, пока не пройдёт его окно: примерно iat leeway (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.