Статьи

Управление веб-сессиями

В предыдущей статье я настроил кластер из 2 экземпляров Tomcat, чтобы добиться балансировки нагрузки. Это также предложило возможность аварийного переключения. Однако при использовании этой функции сеанс пользователя был потерян при смене узла. В этой статье я покажу вам, как этого побочного эффекта можно избежать.

Напоминание: протокол HTTP по своей природе отключен (в отличие от подключенного FTP). В HTTP клиент отправляет запрос на сервер, получает ответ, и на этом все. Сервер не может изначально связать запрос с предыдущим запросом того же клиента. Чтобы использовать HTTP для приложений, нам нужно было сгруппировать такие запросы. Сессия является меткой для этой функции группировки.

Это делается с помощью токена, переданного клиенту по первому запросу. Токен передается с cookie, если это возможно, или добавляется к URL, если нет. Интересно, что это то же самое для PHP (токен с именем PHPSESSID) и для JEE (токен с именем JSESSIONID). В обоих случаях мы используем протокол без сохранения состояния и настраиваем его так, чтобы он выглядел с состоянием. Это псевдо-состояние делает возможными функции уровня приложения, такие как аутентификация / авторизация или корзина покупок.

Теперь давайте рассмотрим реальный вариант использования. Я просматриваю интернет-магазин. Я уже положил статью в корзину. Когда я решаю, наконец, перейти к оплате, я оказываюсь с пустой корзиной! Что произошло? Без моего ведома произошел сбой узла, в котором размещался мой сеанс, и я был прозрачно перенаправлен на рабочий узел без идентификатора сеанса и, следовательно, без содержимого моей корзины.

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

Сессии злые

Время от времени я натыкаюсь на статьи, обвиняющие сессии и обозначающие их как зло. Хотя это не универсальная истина, использование сессий в плохом смысле может иметь негативные побочные эффекты.

Наиболее представительное плохое использование сессии, если положить все в них. Я видел, как ленивые разработчики помещают коллекции в сессию, чтобы управлять подкачкой страниц. Вы передаете оператор один раз, помещаете результат в сеанс и управляете разбиением на страницы по сессионному результату. Поскольку размер коллекции не ограничен, такое использование плохо масштабируется с увеличением количества пользователей. В большинстве случаев все идет хорошо в процессе разработки, но вскоре вы ошеломлены необычным временем отклика или даже OutOfMemoryError в работе.

Если вы думаете, что сессии злые, некоторые решения:

  • Храните данные в базе данных: теперь ваши данные должны пройти от внешнего интерфейса до внутреннего, чтобы быть сохраненным, а затем снова вернуться для использования. Классическая реляционная база данных может оказаться не тем решением, которое вы ищете. Большая часть трафика с малым временем ответа использует маршрут NoSQL, хотя я не знаю, используют ли они его для хранения сессии
  • Храните данные на стороне клиента с помощью файлов cookie: вашим клиентам необходим браузер с поддержкой файлов cookie. Кроме того, данные будут отправляться с каждым запросом / ответом, поэтому не слишком злоупотребляйте

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

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

Репликация сессий сервера

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

Существует два ограничения, общих для репликации сеанса между всеми серверами:

  • Используйте тег <distribtable /> в файле web.xml.
  • Только помещайте в экземпляры сессий классов, которые являются java.lang.Serializable

ИМХО, эти правила должны применяться ко всем веб-приложениям, независимо от того, развернуты ли они в данный момент в кластере или нет, поскольку они не очень ограничительны. Таким образом, развертывание приложения в кластере приведет к отказу от операций.

Стратегии, доступные для репликации сеанса, зависят от сервера приложений. Однако они обычно основаны на следующих реализациях:

  • В репликации памяти: каждый сервер хранит данные сеанса всех серверов. При обновлении он передает всем узлам измененный сеанс (или дельту, основываясь на доступной / используемой стратегии). Эта реализация интенсивно использует сеть и память
  • Постоянство базы данных
  • Постоянство файла: используемая файловая система должна быть доступна для всех узлов кластера

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

Примечание: Tomcat 5.5 имеет конфигурацию кластера, прокомментированную в server.xml. Tomcat 6 нет. Вот конфигурация кластеризации по умолчанию:

<Cluster className="org.apache.catalina.cluster.tcp.SimpleTcpCluster"
  managerClassName="org.apache.catalina.cluster.session.DeltaManager"
  expireSessionsOnShutdown="false"
  useDirtyFlag="true"
  notifyListenersOnReplication="true">

  <Membership className="org.apache.catalina.cluster.mcast.McastService"
    mcastAddr="228.0.0.4"
    mcastPort="45564"
    mcastFrequency="500"
    mcastDropTime="3000"/>

  <Receiver className="org.apache.catalina.cluster.tcp.ReplicationListener"
    tcpListenAddress="auto"
    tcpListenPort="4001"
    tcpSelectorTimeout="100"
    tcpThreadCount="6"/>

  <Sender className="org.apache.catalina.cluster.tcp.ReplicationTransmitter"
    replicationMode="pooled"
    ackTimeout="15000"
    waitForAck="true"/>

  <Valve className="org.apache.catalina.cluster.tcp.ReplicationValve"
    filter=".*\.gif;.*\.js;.*\.jpg;.*\.png;.*\.htm;.*\.html;.*\.css;.*\.txt;"/>

  <Deployer className="org.apache.catalina.cluster.deploy.FarmWarDeployer"
    tempDir="/tmp/war-temp/"
    deployDir="/tmp/war-deploy/"
    watchDir="/tmp/war-listen/"
    watchEnabled="false"/>

  <ClusterListener className="org.apache.catalina.cluster.session.ClusterSessionListener"/>
</Cluster>

Сначала убедитесь, что mcastAddrand и mcastPort тега <Membership> совпадают. Это проверяется при запуске второго узла со следующим журналом в первом:

INFO: Replication member added:org.apache.catalina.cluster.mcast.McastMember[tcp
://10.138.97.43:4001,catalina,10.138.97.43,4001, alive=0]

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

Чтобы также направить идентификаторы сеанса, вам нужно указать два дополнительных тега в <Cluster>:

<Valve className="org.apache.catalina.cluster.session.JvmRouteBinderValve" enabled="true" sessionIdAttribute="takeoverSessionid"/>
<ClusterListener className="org.apache.catalina.cluster.session.JvmRouteSessionIDBinderListener" />

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

Примечание в последнюю минуту : предыдущая установка использует стандартный менеджер сеансов . Недавно я узнал о стороннем менеджере, который также обрабатывает куки-файлы сеанса при сбое узла, тем самым уменьшая трудности с настройкой. Такой продукт является Memcached Session Manager и основан на Memcached . Любые отзывы об использовании этого продукта приветствуются.

Репликация третьей стороны

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

Использование сторонних продуктов является средством для этого. Terracotta — это такой продукт: morevoer, предоставляя заданное количество узлов Terracotta, вы избегаете передачи изменений сеанса всем узлам сервера, как в предыдущем примере репликации Tomcat.

Далее сервер является сервером репликации Terracotta, а клиенты — экземплярами Tomcat. Для настройки терракоты необходимо выполнить два шага:

  • создать конфигурацию для сервера. Чтобы его использовать, назовите его tc-config.xml и поместите в каталог bin. В нашем случае это так:
    <tc:tc-config xmlns:tc="http://www.terracotta.org/config"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.terracotta.org/schema/terracotta-5.xsd">
    ...
        <modules>
          <module name="tim-tomcat-5.5"/>
        </modules>
    ...
          <web-applications>
    	    <!-- The webapp(s) to watch -->
            <web-application>servlets-examples</web-application>
    
          </web-applications>
    ...
    </tc:tc-config>

    Примечание : установленный по умолчанию модуль предназначен для Tomcat 6. В случае, если вам нужен модуль Tomcat 5.5, вы должны запустить Terracotta Integration Module Management (TIM) и использовать его для загрузки правильного модуля. Для пользователей, использующих прокси-сервер в Интернете, это стало возможным благодаря обновлению tim-get.properties следующими строками:
    org.terracotta.modules.tool.proxyUrl = …
    org.terracotta.modules.tool.proxyAuth = …

  • добавьте во все сценарии запуска клиента следующие строки:
    TC_INSTALL_DIR=
    
    TC_CONFIG_PATH=:
    
    . ${TC_INSTALL_DIR}/bin/dso-env.sh -q
    
    export JAVA_OPTS="$JAVA_OPTS $TC_JAVA_OPTS"

Теперь мы вернулись к репликации сеанса и отработке отказа, но конфигурация может использоваться на разных серверах приложений.

Чтобы увидеть, что хранится, вы также можете запустить Terracotta Developer Console.

Вывод

Существует три основных стратегии управления восстановлением сеанса на узлах кластера. Первый — вообще не использовать сессию: это имеет серьезные последствия для вашего времени разработки, поскольку ваше приложение должно делать это напрямую. Второй — посмотреть документацию к серверу и посмотреть, как это делается на конкретном сервере. Это связывает управление сессиями с одним продуктом. И последнее, но не менее важное: вы можете использовать сторонний продукт. Это позволяет переместить конфигурацию за пределы области действия вашего конкретного сервера, что позволяет вам с меньшими хлопотами переходить с одного сервера на другой и при этом пользоваться преимуществами отработки отказа сеанса. Если бы я был системным инженером, я бы рекомендовал это решение, поскольку оно наиболее гибкое.

Чтобы идти дальше: