Покупатель вошёл в личный кабинет, открыл каталог, добавил товар. Первый запрос обслужил веб-узел А, второй попал на узел Б — и магазин ведёт себя так, будто человек видит его впервые. Кнопка «Войти» снова на месте, а в шапке пусто. Добавить сервер оказалось проще, чем разделить то, что до этого незаметно жило на одном диске.
Вопрос, который стоит за этой сценой, звучит скромно: что именно Битрикс должен хранить совместно, чтобы два веб-сервера обслуживали одного покупателя? Ответ не умещается в один флаг. Сессия — только первое, самое заметное состояние. Рядом лежат корзина, файлы, кеш, настройки узлов и поведение хранилища в тот момент, когда оно недоступно. Разберём их по очереди.
Почему запросы одного человека разъезжаются
Если в /bitrix/.settings.php нет секции session, ядро использует файловое хранение (документация по сессиям). Для одного сервера это нормально: файл сессии лежит на локальном диске, и каждый запрос находит его там. Для двух серверов картина меняется. Документация веб-кластера описывает именно этот эффект: один запрос авторизует пользователя на первом сервере, а следующий попадает на другой, где этих данных нет (сессии веб-кластера).
Важная деталь: балансировщик здесь не виноват. Даже идеально равномерное распределение запросов лишь выявляет проблему, которая была заложена в локальном хранилище. Поэтому «закрепить пользователя за узлом» — это обходной приём, а не устранение причины. Он прячет разделённое состояние, пока один из узлов не перезагрузят или не выведут из ротации: тогда сессии пропадут у всех, кто был на нём.
Есть и вторая тонкость. На странице модуля «Веб-кластер» централизованное хранение сессий включается в базе данных, это не интерфейс настройки Redis. Доступность страницы зависит от редакции продукта, а переключение режима там уничтожает данные сессий и сбрасывает авторизацию. Redis настраивается другим путём — в конфигурационном файле, и об этом ниже.
Сессия, корзина и всё остальное: три разных вещи
Частая ошибка рассуждения — считать Redis «хранилищем магазина». Это не так. Разберём, что где живёт.
- Сессия — данные, привязанные к
session_id: состояние авторизации, служебные значения ядра, всё, что код положил через объект сессии. Её место определяется секциейsession. - Корзина — в D7 sale это сущности в базе данных, идентифицируемые покупателем (FUSER) и сайтом. Незаказанная корзина загружается методом
Basket::loadItemsForFUser($fuser, $siteId), аBasket::getListобращается к базе и, например, фильтрует по FUSER_ID, сайту и отсутствию ORDER_ID (D7: Корзина). - Кеш — отдельная секция
cacheс собственными параметрами подключения (кеширование).
Отсюда практический вывод. Если после переноса сессий в Redis корзина «ведёт себя странно», сначала нужно понять, что именно связывает посетителя с его корзиной. Покупатель между запросами должен сохранять один и тот же идентификатор, а связь с FUSER — оставаться стабильной. Эти данные корзины лежат в базе, которую оба узла должны видеть одинаково. Но пользовательские компоненты и сторонние модули могут устроить хранение иначе, поэтому утверждение «корзина всегда в БД» опирается на D7 API и не распространяется на любую самописную реализацию.
Есть и обратная сторона. Раз сессия не равна корзине, потеря сессии не обязана означать потерю товаров, но может оборвать связь посетителя с ними. Именно поэтому в проверке на двух узлах корзину стоит тестировать отдельно от входа: это два разных механизма, которые могут сломаться независимо.
Заодно полезно проверить код проекта на привычку обращаться к суперглобальному массиву $_SESSION напрямую. Документация рекомендует работать через Application::getInstance()->getSession(). Сессионный кеш SessionLocalStorage привязан к session_id и идёт через настроенный кеш, так что даже здесь сессия и кеш пересекаются.
Redis как хранилище сессий: что именно прописывать
Структура в документации такова: в .settings.php секция session, внутри value с ключами mode и handlers; в handlers → general задаются type: redis, host и port. Приводится и lifetime — время жизни в секундах. Вот схематичный фрагмент в духе документации; значения здесь иллюстративные:
'session' => [
'value' => [
'mode' => 'default',
'lifetime' => 14400,
'handlers' => [
'general' => [
'type' => 'redis',
'host' => '10.0.0.5',
'port' => 6379,
'timeout' => 2,
'readTimeout' => 2,
],
],
'ignoreSessionStartErrors' => false,
],
],Адрес 10.0.0.5 — просто пример приватного адреса. В документации в примере стоит 127.0.0.1, и это ловушка именно для двух серверов: на каждом узле «локальный» Redis будет собственным, и вы получите ту же проблему разных хранилищ, только в другой упаковке. Оба веб-узла должны ходить на один и тот же Redis по сетевому адресу, который доступен обоим.
Теперь про таймауты. В актуальной документации по конфигурации подключений timeout означает таймаут подключения, а readTimeout — таймаут чтения; единица — секунды. Там же есть прямое предупреждение: обработчик сессий не использует read_timeout. Если вы нашли в старом уроке или чужой заметке пример с read_timeout, переносить его в конфигурацию не стоит: сверяйтесь с текущей страницей и с версией main на своём сайте. Логика у таймаутов простая. Без ограничения чтения зависший Redis может держать PHP-процесс долго, и очередь запросов на веб-узле растёт. С разумным значением запрос завершится ошибкой раньше, чем исчерпается пул процессов. Какое число считать разумным, показывает только измерение на вашем проекте.
Здесь же легко смешать два разных мира. В PHP есть собственный механизм сессий: session.save_handler и session.save_path в настройках php. Битрикс управляет хранилищем через собственный обработчик ядра, настраиваемый секцией session в .settings.php. Это не одно и то же. Если параметры Redis вы выставили в php.ini, а ядро по-прежнему читает .settings.php, результат будет не тем, который вы ожидали. Правило простое: источником истины для хранилища сессий считаем секцию ядра, а PHP-native настройки отдельно не подмешиваем.
Подключению нужно расширение PHP redis — на обоих узлах. Параметр password документирован как доступный начиная с main 25.800.0; на более старом ядре его наличие нельзя предполагать, поэтому защиту Redis сначала стоит решать сетевыми средствами (приватная сеть, правила файрвола), а не надеяться на параметр.
Режим default и separated
Поле mode принимает default или separated. В разделённом режиме данные ядра, включая аутентификацию и авторизацию (hot), отделяются от остальных (cold). Hot-данные хранятся в зашифрованных cookies в неблокирующем режиме, а cold используют обычную сессию, и обращение к ним запускает блокирующую часть; хранилищем может быть Redis, Memcache или база (сессия в разделённом режиме).
Для двух узлов у этого режима есть любопытное следствие: часть состояния путешествует вместе с браузером, а не лежит на сервере. Но это не значит, что Redis можно не настраивать: cold-данные всё равно нужно хранить совместно. Нельзя и сказать, что separated целиком переносит авторизацию в Redis, или что Redis всегда работает без блокировок: фактическое поведение зависит от типа и настроек хранилища. В режиме separated указывают mode, lifetime и handlers.kernel: encrypted_cookies. Шифрование cookies строится на секретах приложения, так что на обоих узлах нужно проверить, что используются согласованные настройки (сами значения в статье и тикетах лучше не обсуждать). Здесь же стоит помнить: настройки могут переопределяться другими файлами, например .settings_extra.php. Если на одном узле такой файл есть, а на другом нет, конфигурации разойдутся незаметно.
Когда Redis недоступен
Параметр ignoreSessionStartErrors по умолчанию равен false. При true фатальная ошибка старта сессии, например невозможность подключиться к хранилищу, игнорируется: запрос продолжается без сессии, а ошибка попадает в лог.
Соблазн понятен: включить флаг и считать, что сайт «пережил» отказ. Но посмотрите, что на самом деле происходит. Запрос без сессии — это запрос анонимного посетителя. Для страницы статьи в блоге разница невелика. Для оформления заказа, личного кабинета и всего, что требует авторизации, это уже другое: человека не узнают, форма может отказать, а действие, ради которого он пришёл, не выполнится. Автоматического перехода на файлы или базу флаг не делает, записи состояния тоже не гарантирует. И он не является резервированием: это решение о том, что страницы без сессии лучше, чем страницы с ошибкой.
Поэтому выбор значения — продуктовый, а не только технический. Полезно разделить сайт на зоны: что допустимо показывать анонимно, а что нет. Если большая часть трафика — просмотр каталога, терпимость к деградации выше. Если магазин живёт на авторизованных покупателях, тихая работа без сессии может оказаться хуже явной ошибки, потому что маскирует аварию. В любом случае лог ошибок должен читаться и мониториться: без этого флаг превращается в способ не замечать проблему.
Что делать с самим Redis, чтобы такие ситуации были редкими? Есть несколько независимых вопросов, и каждый из них решается отдельно.
- Память. Параметр
maxmemory-policyопределяет, какие ключи вытесняются при достиженииmaxmemory. Приnoevictionключи не вытесняются, но команды, добавляющие новые данные, могут вернуть ошибку; чтение существующих при этом продолжается. Политикиallkeysмогут вытеснять любые ключи, аvolatileвыбирают ключи с TTL, и наличие TTL от вытеснения не защищает (Redis: key eviction). Для сессий это значит: вытесненный ключ — это покупатель, которого внезапно «разлогинило». - Общий экземпляр для кеша и сессий. Документация Redis предлагает рассмотреть отдельные экземпляры при совместном хранении кеша и данных, которые терять нельзя. Кеш — вещь одноразовая, сессия — чуть менее. Если оба живут в одном Redis с вытеснением, кеш может выдавить сессии.
- Персистентность. RDB сохраняет снимки, AOF журналирует записи, можно сочетать их или отключить всё. При аварии RDB допускает потерю изменений после последнего снимка (Redis: persistence). Нужна ли вам эта сохранность для сессий — тоже решение, а не умолчание.
- Репликация. Она по умолчанию асинхронна: ведущий узел не ждёт, пока каждую запись обработают реплики. Даже при ограничении записи условием подключённых реплик окно потери не исчезает (Redis: replication). Для доступности существуют Sentinel и Redis Cluster, но это разные механизмы.
В документации конфигурации для кластера перечислены servers и failover. Но failover относится к политике RedisCluster и не обещает безошибочной работы приложения при любом отказе. Пример с Redis Cluster нельзя выдавать за готовую инструкцию для Sentinel: совместимость конкретной топологии с обработчиком сессий нужно проверять отдельно. Для магазина среднего размера честная отправная точка — один Redis на отдельной машине или отдельном VPS в приватной сети, с осознанным решением о памяти и персистентности. Разумно сначала измерить объём сессий, а потом выбирать ресурсы, а не наоборот.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Файлы, кеш и конфигурации узлов
Сессии — только часть общего состояния. Второй веб-узел должен отдавать те же файлы. В уроке BitrixVM прямо сказано: при разделении проекта на веб-узлы нужно решить синхронизацию файлов и балансировку нагрузки; а документированный сценарий кластеризации требует модуль «Веб-кластер», который есть не во всех редакциях (BitrixVM: управление веб-серверами). Операции создания и удаления веб-узла там выполняются отдельно; сама запись в session их не заменяет.
Если посмотреть на описанную в BitrixVM конфигурацию, файлы синхронизируются через lsyncd по SSH и rsync, у основного и дополнительного узлов различаются синхронизируемые каталоги. Кеш-каталоги (bitrix/cache, managed_cache, stack_cache, upload/resize_cache) исключены из синхронизации, а на дополнительных узлах исключаются ещё и конфигурационные файлы, включая bitrix/.settings.php (BitrixVM: веб-кластер, бэкапы, восстановление). Это и есть место для внимательности: если вы строите ручную схему, а не BitrixVM, перечень каталогов не обязан совпадать. Но урок подсвечивает вопрос, который стоит задать себе: какие файлы должны быть идентичны, а какие обязаны отличаться?
Вот почему .settings.php — отдельный предмет проверки. Если файл синхронизируется и на втором узле оказывается копия с адресом базы или Redis, подходящим для первого, ошибка будет тихой. Если файл не синхронизируется, его нужно привести в соответствие вручную: тип хранилища, адрес Redis, lifetime, режим сессии, ignoreSessionStartErrors. Расхождение в одном значении даст ту самую картину «то работает, то нет», с которой мы начали, только причина будет уже не в хранилище, а в двух разных конфигурациях.
Следующий вопрос — удаление. В описанной схеме удаление файла на одном узле распространяется на другой. Документация отделяет такую синхронизацию от резервного копирования: если файл удалён по ошибке, синхронизация быстро сделает ошибку общей. Резервный архив рекомендуется держать отдельно от серверов, работающих с файлами. Там же рассматриваются отказ дополнительного и основного сервера как разные ситуации, и рекомендуется мониторинг: второй веб-узел сам по себе не убирает единую точку входа.
Кеш не равен сессии
Часть команды может предложить «всё равно у нас Redis, давайте использовать его и для кеша». Это допустимо: для Redis-кеша описан CacheEngineRedis и расширение redis. Но конфигурация кеша находится в секции cache, у неё свои параметры подключения. Параметр sid идентифицирует кеш сайта: ошибка настройки смешивает кеш разных сайтов, а смена формата требует очистки старого кеша. При этом sid — параметр кеша, а не префикс ключей сессий; переносить на него рассуждения о сессионных ключах нельзя.
Практическое следствие касается очистки. Если сессии и кеш лежат в одном Redis, команда «очистить всё» (FLUSHALL и подобные) заодно разлогинит всех покупателей. Поэтому очищать кеш нужно средствами Битрикс или адресно, а не опустошением общего хранилища. Лучше вовсе разделить экземпляры или базы, если проект позволяет, и сознательно решить, у кого какая политика памяти.
Переключение хранилища: что произойдёт с вошедшими
Отдельно о моменте перехода. Документация по проактивной защите допускает БД, Redis или Memcache как хранилища сессий вне файловой системы и направляет в конфигурацию session; раздел о переключении предупреждает о потере авторизации пользователями (проактивная защита: хранение сессий). Смена идентификатора сессии (в том числе regenerateIdAfterLogin после входа) — самостоятельная настройка защиты, а не синоним смены хранилища.
Значит, не стоит обещать, что старые сессии переживут переход. Разумно выбрать окно с минимальной активностью, предупредить сотрудников, которые работают в админке, и заранее проверить, нет ли у покупателей незавершённых оформлений. Ничего драматичного: люди войдут заново. Но корзину и заказы лучше защитить отдельной проверкой после переключения.
Ещё одна деталь из той же документации по сессиям: при BX_SECURITY_SESSION_READONLY сессия не записывается в конце запроса, и изменённые в рамках запроса данные теряются. Если где-то в проекте эта константа определена (например, для «лёгких» страниц), убедитесь, что на них никто не пытается что-то сохранить в сессии. На одном сервере это можно было не заметить, на двух поведение останется тем же, но диагностировать станет сложнее.
Репетиция чередования: вход, корзина, выход
Предположим, оба узла настроены, расширение redis стоит, .settings.php совпадает по значимым ключам. Теперь нужно убедиться на практике, а не по ощущению. Главная идея — намеренно развести запросы одного пользователя по узлам, не полагаясь на случайность балансировщика. Сделать это можно по-разному: временно вывести один узел из ротации, обращаться к узлам напрямую по отдельным адресам (если такие настроены) или добавить заголовок, видимый в ответе, чтобы понимать, кто обслужил запрос. Любой вариант годится, если вы точно знаете, какой узел ответил.
Сценарий можно записать так:
- Открыть главную через узел А, войти в личный кабинет. Убедиться, что в шапке отображается пользователь.
- Повторить запрос страницы через узел Б. Пользователь должен остаться авторизованным без повторного ввода данных.
- На узле Б добавить товар в корзину. Затем открыть корзину через узел А: состав должен совпасть. Если расхождение есть, следует разобраться, где именно хранится состояние корзины в вашем проекте: стандартный D7 sale опирается на базу, а кастомные компоненты могут вести себя иначе.
- Изменить количество товара на А, проверить на Б. Затем удалить позицию на Б и проверить на А.
- Выйти через А. Открыть защищённую страницу через Б: гостя должно отбросить на форму входа. Это проверка того, что выход затронул общее состояние, а не локальное.
- Повторить цикл с гостем: добавить товар без входа, затем войти и убедиться, что гостевая корзина встретилась с профилем так, как задумано в проекте.
Отдельно стоит проверить поведение при недоступном Redis — но в тестовой среде, не на живом магазине. Остановите сервис или закройте порт и посмотрите, что видит посетитель при ignoreSessionStartErrors со значением false и true, и что пишется в лог. Конкретный HTTP-код при сбое может зависеть от окружения, поэтому не закладывайте его в критерии заранее: фиксируйте то, что увидели.
Наконец, понаблюдайте за самим Redis. Команда INFO показывает evicted_keys и expired_keys: вытеснение и истечение срока — разные причины исчезновения ключей, и путать их нельзя. Если покупателей «разлогинивает» раньше, чем истекает lifetime, evicted_keys — первое, куда стоит заглянуть.
Где разместить Redis и второй узел
Теперь ресурсы. Redis с сессиями — лёгкая по процессору служба, но чувствительная к памяти и задержке сети. Веб-узлы, наоборот, потребляют CPU и RAM под PHP и nginx или Apache. В этом смысле разумна схема, где Redis работает отдельно от веб-узлов, в приватной сети, чтобы его не затронула нагрузка на любой из них, а адрес был один для обоих. Подбор размеров зависит от числа одновременных сессий, их размера и политики памяти; это вопрос замера, а не таблицы.
VPS MAATRIX с NVMe, процессорами AMD EPYC и root-доступом подходят для такой раскладки как строительные блоки: root нужен, чтобы поставить расширение redis для PHP, настроить сервис Redis и правила доступа. Линейка тарифов начинается с Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) и доходит до Djed (16 vCPU, 32 ГБ, 320 ГБ); эти цифры описывают ресурсы, а не обещание ёмкости, поэтому сначала стоит проверить нагрузку на тестовой копии. Резервное копирование, управляемый Redis и отказоустойчивость при этом не подразумеваются автоматически: их нужно проектировать самостоятельно. Больше о выборе серверной основы под магазин — в материале VPS для 1С-Битрикс.
Одна покупка без шва
Представим покупательницу, которая выбирает чайник. Каталог открывает узел А. Войти она решает на странице, которую обслужил узел Б. Товар попадает в корзину через А, количество меняется через Б, оформление начинается снова на А. Сессия лежит в одном Redis, корзина — в одной базе, файлы и настройки идентичны. Она не знает, что серверов два, и не должна знать. Для неё смена обслуживающего узла — не событие, а просто очередной запрос.