Categories
Development

Tomcat Redis

Nachdem ich mich vor ca. zwei Jahren mal mit Redis auseinander gesetzt hatte, hat es sich jetzt ergeben, ein Beispielsetup in der Praxis umzusetzen.

Eine Anwendung, die auf einem Tomcat Server läuft, soll vorbereitet werden skalierbar gemacht zu werden. Dazu wird im ersten Schritt Redis als Session Cache für den Tomcat eingebunden und ein zweiter Tomcat daneben gestellt, der ebenfalls auf den Redis Session Cache zugreift. Zur Lastverteilung wird ein Reverse Proxy vor die beiden Tomcats gestellt.

Die Server laufen alle in Docker Containern und werden über eine Docker-Compose Datei gesteuert.

Als Beispielanwendung für dieses Projekt kommt mal wieder Show Headers zum Einsatz.

Die Sourcen lege ich in ein GitHub-Repository.

Basis Setup

Basis Setup von einem Tomcat 9 und Show Headers, daneben ein Redis Server.

Das Show Headers ROOT.war liegt im tomcat Ordner.

Das docker-compose File:

version: "3.8"
services:
  tomcat:
    image: tomcat:9.0.83-jre21
    hostname: tomcat
    ports: 
      - "8888:8080"
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war

  redis:
    image: redis
    volumes:
      - redisvolume:/data

volumes:
  redisvolume: {}

networks:
  default:
    name: tomcatredis-network

Browser Output:

Tomcat einrichten

Für die Verbindung von Tomcat zu Redis wird Redisson verwendet.

Step 1

context.xml von einem Tomcat 9 in das tomcat Verzeichnis kopieren und den RedissonSessionManager einrichten:

<Context>
  
    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Redis Session Manager -->
    <!-- https://redisson.org/articles/redis-based-tomcat-session-management.html -->
    <Manager className="org.redisson.tomcat.RedissonSessionManager" 
            configPath="${catalina.base}/conf/redisson.yaml" 
            readMode="MEMORY" 
            updateMode="DEFAULT"/>

</Context>

Konfigurationsdatei anlegen:

singleServerConfig:
  address: "redis://${REDIS_HOST:-redis}:${REDIS_PORT:-6379}"

Step 2

Die beiden Redisson Dateien von Redisson herunterladen und ebenfalls in das tomcat Verzeichnis kopieren.

Step 3

Es muss ein neues Tomcat Image inklusive Redisson gebaut werden, dazu ein neues Dockerfile im tomcat Ordner anlegen:

# https://hub.docker.com/_/tomcat
FROM tomcat:9.0.83-jre21

# Add Redis session manager dependencies
COPY ./redisson-all-3.22.0.jar $CATALINA_HOME/lib/
COPY ./redisson-tomcat-9-3.22.0.jar $CATALINA_HOME/lib/

# Replace the default Tomcat context.xml with custom context.xml
COPY ./context.xml $CATALINA_HOME/conf/

# Add Redisson configuration
COPY ./redisson.yaml $CATALINA_HOME/conf/

# Expose the port Tomcat will run on
EXPOSE 8080

# Start Tomcat
CMD ["catalina.sh", "run"]

Anstelle des image Eintrags in der docker-compose den build Eintrag setzen: "build: ./tomcat"

Testen

Erneut starten:

docker-compose up --detach

Und es läuft immer noch im Browser:

Redis

So weit so gut, aber wird auch wirklich der Redis Cache verwendet?
Nein, denn bisher wurde noch gar keine Session erzeugt.
Holen wir das nach, indem wir ShowSession aufrufen:

Schauen wir in der Redis Datenbank nach, indem wir uns zuerst in den Container connecten:

docker exec -it tomcatredissample-redis-1 bash

Dort die redis-cli starten und die Keys aller Einträge zeigen lassen mittels "keys *":

Dort ist ein Eintrag mit der Session ID aus meinem Browser zu finden.
Es funktioniert!

Welche Daten stehen in der Session?
Um die Daten auslesen zu können, müssen wir erst den Datentyp mittels "TYPE" herausfinden, in diesem Fall ein "hash" und dann mit "HGETALL" anzeigen lassen:

Die seltsamen oder unlesbaren Informationen, die man sieht, wie z.B. "\t\xa6\xfa\xbd\xbe\x83c" für "session:thisAccessedTime", sind wahrscheinlich auf die Art und Weise zurückzuführen, wie Sitzungsdaten serialisiert werden, bevor sie in Redis gespeichert werden. Viele auf Java basierende Systeme, einschließlich solcher, die Tomcat für die Sitzungsverwaltung verwenden, serialisieren Objekte in ein binäres Format, bevor sie in einem Sitzungsspeicher wie Redis gespeichert werden. Diese binären Daten sind nicht direkt lesbar, wenn Sie sie mit Redis-Befehlen abrufen.

Um diese Daten zu interpretieren, müssen sie in ein lesbares Format deserialisiert werden.
Darauf gehe ich hier aber nicht weiter ein.

Reverse Proxy

Der Reverse Proxy basiert auf Apache HTTPD 2.4 und wird der docker-compose Datei hinzugefügt.

Die httpd.conf Datei aus dem Container wird in den reverseproxy Ordner kopiert und am Ende erweitert:

[...]

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so


ServerName reverseproxy

<VirtualHost *:80>
    ServerAdmin deringo@github.com
    DocumentRoot "/usr/local/apache2/htdocs"

    ## Tomcat
    ProxyPass        /          http://tomcat:8080/
    ProxyPassReverse /          http://tomcat:8080/

</VirtualHost>

Die Docker Compose Datei:

version: "3.8"
services:
  reverseproxy:
    image: httpd:2.4
    ports: 
      - "8888:80"
    volumes:
      - ./reverseproxy/httpd.conf:/usr/local/apache2/conf/httpd.conf

  tomcat:
    build: ./tomcat
    hostname: tomcat
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war

  redis:
    image: redis
    volumes:
      - redisvolume:/data

volumes:
  redisvolume: {}

networks:
  default:
    name: tomcatredis-network

Der anschließende Aufruf von http://localhost:8888/ShowSession funktioniert immer noch, Test bestanden.

Load Balancer

Im nächsten Schritt fügen wir einen Load Balancer hinzu, der erstmal auf genau den einen Tomcat "loadbalanced". Nach erfolgreichem Test wissen wir dann, dass der Load Balancer generell funktioniert und können dann weitere Server hinzufügen.
Die erweiterte Apache Konfiguration:

[...]
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so

LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
LoadModule watchdog_module modules/mod_watchdog.so


ServerName reverseproxy

<VirtualHost *:80>
    ServerAdmin deringo@github.com
    DocumentRoot "/usr/local/apache2/htdocs"

    <Proxy "balancer://tomcat">
        BalancerMember http://tomcat:8080
    </Proxy>

    ProxyPass        /          balancer://tomcat/
    ProxyPassReverse /          balancer://tomcat/

</VirtualHost>

Mehr Tomcat Server

Die einfachste Möglichkeit, mehrere Tomcat Server zu erzeugen, ist im Docker Compose weitere Replicas zu starten.

Docker Compose managed dann auch das Load Balancing, so dass alle Tomcat Instanzen über den Service Namen "tomcat" ansprechbar sind.

Wir haben damit ein doppeltes Load Balancing: Zuerst der Apache HTTPD der immer auf den "tomcat" loadbalanced und dann das wirkliche Load Balancing durch Docker auf die Replikas.

services:
  [...]
  tomcat:
    build: ./tomcat
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
    deploy:
      mode: replicated
      replicas: 4
  [...]

Genau zwei Server

Jetzt die Variante ohne Replikas und mit zwei dedizierten Tomcat Servern.
Die Zuteilung zum Server erfolgt beim Sessionaufbau sticky, aber wir können über Manipulation des Session Cookies den Server wechseln und so gezielt ansteuern.

In Docker Compose legen wir zwei Tomcat Server an:

services:
  [...]
  tomcat-1:
    build: ./tomcat
    hostname: tomcat-1
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
      - ./tomcat/server-1.xml:/usr/local/tomcat/conf/server.xml

  tomcat-2:
    build: ./tomcat
    hostname: tomcat-2
    volumes:
      - ./tomcat/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
      - ./tomcat/server-2.xml:/usr/local/tomcat/conf/server.xml
  [...]

Die server.xml ist eine Kopie der Tomcat 9 server.xml und lediglich an einer Stelle angepasst, für das Setzen der jeweiligen jvmRoute:

<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat-1">

In der Apache Konfiguration werden die beiden Server im Load Balancer eingetragen:

    <Proxy "balancer://tomcat">
        BalancerMember http://tomcat-1:8080 route=tomcat-1
        BalancerMember http://tomcat-2:8080 route=tomcat-2
    </Proxy>

Ggf. Session Cookies im Browser löschen, dann http://localhost:8888/ShowServer bzw. http://localhost:8888/ShowHeaders aufrufen. Man kann erkennen, dass bei jedem Aufruf der Server gewechselt wird.

Beim erstmaligen Aufruf von http://localhost:8888/ShowSession wird die Session erzeugt und man wird einem Server zugewiesen.

Man kann sehen, dass die Session ID ein Postfix ".tomcat-1" bzw. ".tomcat-2" hat.

Man kann im Browser den Session Cookie editieren und den Postfix auf den anderen Server ändern, zb von "SESSIONID.tomcat-1" auf "SESSIONID.tomcat-2". Dadurch kann man dann den Server auswählen, auf den man gelangen möchte. Eigentlich zumindest, denn leider hat es nicht funktioniert.

Entweder muss noch irgendwo irgendwas konfiguriert werden, oder es könnte auch ein Bug in Redisson sein: Der Postfix wird als Teil der Session ID durch Redisson in Redis als Key gespeichert.
Wenn man nun also lediglich den Postfix verändert, hat man eine ungültige Session ID und es wird eine neue Session generiert. Und so kann es irgendwie passieren, dass man wieder auf dem ursprünglichen Server landet, mit einer neuen Session.
Es könnte auch am Reverse Proxy liegen, dass dort der Postfix abgeschnitten werden muss, bei der Kommunikation RP zu Tomcat und lediglich auf der Strecke RP zum Browser gesetzt werden muss.

Vielleicht werde ich die Ursache des Problems und deren Behebung ermitteln können, dann gibt es hier ein Update. Allerdings werde ich nicht allzuviel Energie hineinstecken können, da andere Sachen wichtiger sind, zumal die Lösung mit den Replikas und dem durch Docker bereitgestellten Load Balancing durchaus ausreichend sein sollten.