Всем привет! Меня зовут Антон Огородников, и с начала этого года я руковожу отделом онлайн-разработки в «Магните». Не успел я заонбордиться, как столкнулся с  настоящим коллапсом — массовым лог-аутом пользователей из приложения лояльности «Магнит: акции и скидки». Клиентов разлогинивало в самые неподходящие моменты: например, на кассе во время оплаты товаров. Оценка приложения в сторах упала до 2 баллов, капали негативные комментарии. В этой статье я расскажу, как мы искали причину разлогинов и как сумели побороть проблему.  Старая версия мобильного приложения лояльности «Магнита» появилось до моего прихода в компанию еще в 2020 году. Оно предоставляло клиентам скидки лояльности, но его проектировали без собственного бэкенда.   Интеграция телефона клиента происходила через CRM, при этом авторизация была односторонней — только клиентская.  При регистрации в приложении пользователь отражался в базе CRM, где ему присваивался логин и пароль. После авторизации он мог совершать действия в приложении, а компания видела регистрацию этих действий в CRM. Такая авторизация занимала время и не всегда корректно отрабатывала на вход, поэтому оценка приложения колебалась от 1 и до 2,5 баллов. Летом было решено провести апгрейд приложения. Архитектурно за интерфейсом в телефоне разработали бэкенд magnit-API, под ним собрали три элемента: Keycloak — open-source сервис авторизации, написанный на Java; Redis — хранилище или key value storage; CRM, название которой мы сохраним в тайне. Это платный enterprise-сервис для программ лояльности, хранения информации о клиентах и их операциях. Архитектуру бэкенда выстроили, но пользователей продолжало выкидывать из приложения. После множества тестов и проверок стало ясно, что причина крылась в двойной авторизации между API и CRM. Она оставалась как в связке между телефоном и magnit-API по логину и паролю на базе Keycloak, так и между телефоном и CRM на базе бэкенда по логину и паролю клиента. Таким образом, клиент заходил в приложение, вводил логин и пароль, которые падали на бэкенд. Оттуда они направлялись в CRM для создания токена авторизации, который хранился в Redis.  Итак, проблема была в неудачно построенной архитектуре. Стоит сказать, что на момент разработки приложения сделать по-другому было практически невозможно. API в CRM был только по логину и паролю. Можно задать логичный вопрос «А зачем вообще была нужна CRM?» Все просто — в компании ее использовали с 2018 года, и в ней хранили информацию о клиентах. Это покупки, баллы лояльности и транзакции. Поэтому проще было привязать авторизацию сразу к ней.   В конце 2020 года «Магнит» выпустил новую версию приложения, и проблема разлогинов временно исчезла.  Но уже через месяц все началось по новой — около 7 000 пользователей в день выбрасывало из приложения.  Я предположил, что проблема кроется в короткой жизни токенов в CRM. Тогда она составляла 30 дней без возможности пролонгирования. Не важно, как часто клиент пользовался приложением: он проходил авторизацию один раз и получал связку токенов на 30 дней. Спустя это время срок жизни токена подходил к концу, и клиента разлогинивало. CRM не предусматривала возможность пролонгирования токенов.  Тогда мы взялись за CRM. Проблему решил наш CIO Валентин. Он связался с представителями сервиса, объяснил наш запрос и контекст ситуации. Договорились, что разработчики CRM создадут для нас специальное обновление, продлевающее жизнь токенам. Казалось бы, загадку разгадали, проблему решили. Однако через месяц ситуация стала ещё хуже.  В начале марта показатель разлогинов вырос до 115 000 в сутки. Это был настоящий апокалипсис — выкидывало каждого четвертого пользователя. Это и недовольные клиенты, и финансовые затраты: например, на повторные sms-авторизации. Реагировать надо было срочно.   Я начал анализировать ситуацию: смотрел код на бэкенде и на мобилке, снифал трафик между ними, смотрел, как хранятся токены. Чтобы откопать причину проблемы, нужно было полностью воспроизвести ситуацию, в которой она появлялась. Затем надо было отдебажить решение, устранить в нем проблему и внедрить новую версию в прод. Озарение пришло в конце марта, когда я работал с хранилищем. Я удалял токен — и меня разлогинивало. Я портил код — и меня снова выкидывало. И тут я понял, что… Но давайте по порядку. Однажды я копался в админке Keycloak и заметил, что время жизни офлайн-токена составляет 20 дней. Если за этот период пользователь заходил в приложение, то токен автоматически пролонгировался. Мы поправили ситуацию: увеличили продолжительность жизни офлайн-токена до 90 дней. Решение казалось простым: пользователя не будет выкидывать на уровне Keycloak, если он хотя бы раз в 90 дней использует приложение.  Поскольку мы получали токены клиента из CRM и отправляли их на хранение в Redis, в теории они могли удаляться из него по TTL. Тогда мы подняли их TTL в Redis также до 90 дней. Схему построили так: клиент приходит и авторизуется; мы выдаем ему токен Keycloak на 90 дней; получаем токен в CRM на 90 дней; кладем токены в Redis на 90 дней. Казалось, что схема правильная, и всё должно заработать без проблем. Но что-то опять пошло не так. Следующее предположение было таким: мы неправильно интерпретируем  ошибки CRM. Мы обрабатывали любую неизвестную ошибку из предположения, что у клиента «протух» токен. В связке magnit-API — CRM мы получали новый токен на 90 дней, а точнее два — access на 2-3 дня и refresh на 90 дней. После  смерти access мы шли в CRM с живым refresh, чтобы получить новую связку. Проблема оказалась в самом подходе.  Мы начали внимательнее обрабатывать ошибки в CRM и сбрасывать токены только в том случае, если получали специальный код с конкретным телом.  Процесс авторизации в приложении был выстроен так: Клиент в первый раз заходит в приложение, логинится; Получает токены Keycloak и CRM — access / refresh; Мы записываем их в Redis. Когда access «протухает», мы идем из нашего бэкенда в Redis, забираем связку, с refresh идем в CRM; Запрашиваем новую связку access/refresh, получаем ее и кладем обратно в Redis. Но когда мы это делаем, мы не продлеваем время жизни токенов.   И тут меня обдало холодным потом - это была детская ошибка. Офлайн-токен жил 20 дней, и клиента стабильно выкидывало по истечении этого срока. И не важно, какие были ошибки — время жизни ключа access/refresh в Redis не обновлялось. Когда мы подняли время жизни офлайн-токена до 90 дней, ситуация наконец-то выровнялась.    Так, с 85 000 лог-аутов в день в конце марта мы вернулись к показателю в 5 000 уже в середине апреля. На этом мы не остановились — взялись за улучшение в CRM-системе. В июле разработчики CRM выкатили для нас новую авторизацию b2b. Теперь между клиентской базой и Magnit-API есть только один токен, по которому мы обращаемся к CRM. Мы можем совершать с его помощью любые действия по всем клиентам.  Также мы отказались от хранения в Redis. У нас есть своя авторизация в телефоне на базе Keycloak и отдельная авторизация между нашим бэкендом и CRM.  Количество пользователей нашего приложения выросло на 300 000 и достигло отметки в 800 000 (DAU).  В день только 0,001% от них теряет авторизацию. Эти разлогины происходят, если клиент заходит в приложение реже одного раза в 90 дней. По нашей бизнес-логике клиент может использовать приложение, если у него активна хотя бы одна карта лояльности. Примерно 1% пользователей продолжает сталкиваться с проблемой. Так происходит, когда у клиента нет ни одной активной виртуальной карты, либо она заблокирована.    Keycloak — это open-source продукт. В России мы не нашли коммьюнити, которое бы помогло нам решить вопрос с клиентскими авторизациями на долгий период времени с большим количеством пользователей. Большинство коллег по цеху используют Keycloak как внутренний корпоративный инструмент.   Я нашел только один русскоязычный Telegram-канал, где можно задать вопрос и обсудить проблемы с такими же разработчиками, как я сам. Еще есть платная поддержка от RedHat. Нам она не подошла, потому что они не дают поддержку «голого» Keycloak, а дополняют его разными фичами.  Сейчас у нас в Keycloak порядка 10 млн клиентов и 15 млн сессий. Нам понадобился не один месяц, чтобы настроить сервис авторизации под нашу нагрузку. Сейчас он потребляет 50 ГБ памяти, и этот объем растет каждую неделю.   В июле 2021 года, после релиза новой версии приложения, мы начали получать новые регистрации. Плюс это совпало с маркетинговой кампанией «Обратно в школу». Прирост пользователей приложения составил 15%, что привело к новым проблемам с Keycloak.   В июле примерно раз в неделю нам приходилось перезапускать Keycloak. В ночь с субботы на воскресенье за 40 минут он прогревал в памяти все токены. Постепенно данных становилось все больше, 40 минут растягивались на несколько часов, а перезапуски требовались уже каждый день. Мы начали выдавать ему дополнительные CPU, но он продолжал падать. В августе 2021 года Keycloak начал падать уже по несколько раз в день. Вместе с ним падали и авторизации.    Мы думали, что после старта съедался весь CPU. Выдавали ему еще, но Keycloak продолжал падать. Тогда мы дали ему больше процессорной памяти, но он ее просто не потреблял.   Здесь важно помнить, что Keycloak это все-таки java-машина, а она может ограничивать себя по потреблению памяти системы. Мы нашли настройку Xmx, которая отвечает за максимальное количество памяти для java-машины в Keycloak. Когда все пофиксили, то CPU упала, а память начала расти:  Борьба с фродом в программах лояльности «Магнита» — важная тема как для нас, так и для наших клиентов. Начать нужно с точного определения: что именно мы считаем недобросовестными действиями. Мы считаем мошенниками тех, кто методом перебора номеров карт или логинов и паролей пытается получить доступ к программаме лояльности, чтобы воспользоваться баллами, специальным предложениями. Чаще всего их крадут, чтобы перепродать. Причем тут мошенники, если мы уже нашли проблему в неудачно выстроенной архитектуре приложения? Фактор фрода есть всегда. Когда все возможные изменения настроек применены, а небольшое количество пользователей все еще выбрасывает из их аккаунтов, это сбивает разработчиков с толку. Мы не понимали, где реальная проблема с авторизацией, а где попытка фрода. Мошенники постоянно создавали нам шум, из-за чего решение проблемы оттягивалось. Для примера, у фродеров есть метод «password spraying». Это перебор паролей не отдельно для каждого логина, а сразу для нескольких сотен учетных записей. Так как многие пользователи используют пароли по типу «123…», а у приложения раньше стоял лимит в 5 попыток ввода пароля до блокировки аккаунта, то мошенники с легкостью воровали учетные записи. Соответственно, пользователей разлогинивало, а наш Keycloak грузился еще больше.  Конечно, фрод и методы борьбы с ним в нашем приложении заслуживают отдельной статьи. Скажу только, что этой задачей занималась и продолжает это делать команда информационной защиты «Магнита». Отделив проблемы с Keycloack от попыток воспользоваться данными со стороны мошенников, мы продвинулись в борьбе с лог-аутами вперед.  Итак, в нашей схеме есть телефон, magnit-API, Keycloak и CRM с b2b-токеном. Начиная с июля этого года авторизация производится по номеру телефона. Выдача логина разделена на два этапа: аутентификацию и авторизацию.   Аутентификация происходит на уровне Keycloak. Мы отправляем одноразовый код клиенту, выдаем ему токен access/refresh. Дальше авторизуем — проверяем, есть ли этот пользователь в CRM. Если не найден, то ведем его на регистрацию. Если есть, то запускаем его в приложение.   Redis из схемы мы исключили, так как теперь у CRM один серверный токен.  С января по март 2021 года мы перекопали весь код приложения, пофиксили все возможное и невозможное и перестроили архитектуру.  За это время мы выяснили, что информации о Keycloak в открытых источниках почти нет.  Поэтому я собрал несколько полезных выводов из собственного расследования проблемы лог-аутов: Не стоит использовать Keycloak в качестве инструмента SSO для b2c-продукта. Такие решения лучше писать самим с помощью готовых инструментов. Оптимальная схема: команда авторизации пишет свой сервис на основе библиотек, используя любой язык программирования и реляционку. А обычный jwt access/refresh токен хранит в базе данных и оптимизирует запрос под свои нужды.  Нужно разделять логин на аутентификацию и авторизацию с самого начала. Если аутентификация и авторизация объединены, проблему сложно идентифицировать. Именно поэтому мы не сразу поняли, в чем дело — это пользователь неправильно ввел код или у него проблемы с правами в его программе лояльности. До июля 2021 года в предыдущей версии приложения было две кнопки — для входа и для регистрации. Это приводило к тому, что пользователи не понимали, на какую кнопку нужно нажимать. Заставлять клиента напрягаться — точно не наш выбор. Авторизация, как и любая другая фича, должна покрываться метриками. Да и само приложение должно проектироваться с фокусом на мониторинг возможных проблем. Если бы мы видели, что у нас каждые 20 дней пропадают ключи из Redis, мы могли сэкономить множество времени. Нужно мониторить запросы и статусы ответов, четко понимать, какой запрос клиента приводит к разлогину, смотреть на состояние активных истекающих сессий, количество созданных сессий и другие параметры.  Кто еще сталкивался с проблемой разлогинов пользователей? Поделитесь своим опытом, расскажите, как настраивали память, CPU и авторизацию токен-exchange?