Teil 3 - Grails in Produktion – mit Apache, Tomcat und MySQL

von Peter Soth

In diesem Teil  meiner Blog-Serie zum Thema Grails in Produktion möchte ich auf die Konfigurationsmöglichkeiten eines Tomcat-Clusters eingehen (hier die Links zu den anderen Blog-Posts dieser Serie Teil 1 Teil 2 Teil 4 und Teil 5).  Dieser Teil meiner Blog-Serie mag für manche vielleicht der interessanteste sein. Ich werde auch ab und zu einen Vergleich zum Oracle WebLogic Server herstellen, da ich selbst einmal als Systemberater für BEA (bevor BEA von Oracle gekauft wurde) tätig war. Bei einem Cluster stellt sich gleich folgende Frage:

In-Memory-Session-Replication oder „nur” Load-Balancing der HTTP-Requests?

Zunächst einmal möchte ich die Unterschiede beschreiben. Der Vorteil bei In-Memory-Session-Replication liegt darin, dass sich ein User – falls ein Tomcat abstürzt – sich nicht erneut einloggen muss, da die Informationen der HTTP-Session bereits auf den Backup-Server repliziert wurden. Beim einfachen Load-Balancing wird nur die Last (Anmerkung: auch beim In-Memory-Session-Replication wird die Last auf die Tomcat-Server eines Clusters verteilt) auf die unterschiedlichen Tomcat-Server eines Clusters verteilt.

Bei der In-Memory-Session-Replication wird von einem Tomcat-Server die Daten des HTTP-Session-Objekts vom Primär- zum Backup-Server über das Netzwerk repliziert (Anmerkung: es gibt auch noch die Möglichkeit, eine Datenbank zur Replikation zu benutzen, dieser Weg ist jedoch nicht sehr performant). Tomcat unterstützt erst seit Version 5 die Replikation von Primär- zu Backup-Server (dies haben sich die Tomcat Entwickler vom WebLogic Server abgeschaut). Davor replizierte ein Tomcat-Server seine HTTP-Sessions zu allen Tomcats innerhalb eines Clusters. Dies hatte enorme Auswirkungen auf die Netzwerklast und somit auf die Gesamtperformanz des Tomcat-Clusters. Um es gleich vorweg zu nehmen, ist In-Memory-Session-Replication die komplexere Systemkonstellation. Es gibt noch einen einfacheren Weg, in dem „nur“ die HTTP-Requests zwischen den Tomcat-Servern eines Clusters verteilt werden (Load-Balancing).

Was nimmt man nun am besten für eine Grails-Web-Applikation?

Wir von exensio benutzen eigentlich nur das einfache Load-Balancing, da für die In-Memory-Session-Replication bereits bei der Entwicklung einiges beachtet werden muss. Die meisten unserer Kunden sind nicht gewillt, diesen Mehrpreis zu zahlen, nur damit der User einen transparenten Fail-Over erhält, sprich sich nicht erneut einloggen muss. Folgendes muss beachtet werden:

  • Alle Objekte, die in der HTTP-Session gespeichert werden müssen serialisierbar (von java.io.Serializable erben) sein. Dies ist per se kein Problem, jedoch habe ich es schon öfters in Projekten erlebt, dass dann doch die eine oder andere Hashmap ein nicht serialisierbares Objekt enthält.
  • Die HTTP-Session darf nicht größer als 50-70 KByte sein.  Hier auch wieder, eigentlich kein Problem, aber auch hier habe ich schon einiges erlebt. Es war dann doch eine Hashmap im HTTP-Session-Objekt, die beispielsweise die Ergebnisse einer Datenbankabfrage enthielt.
  • Wie gewährleisten wir die Idempotenz, wenn unser Service auf dem Backup-Server erneut aufgerufen wird. Einfaches Beispiel, wir haben in einem Grails-Controller eine Action die zweimal eine save() Methode nacheinander auf unterschiedliche Grails-Domian-Klassen ausführt. Würde es nun zwischen diesen beiden save() Aufrufen zu einem Absturz eines Tomcat-Servers kommen, so würde die Grails-Controller-Action dann erneut auf dem Tomcat-Backup-Server ausgeführt werden. In diesem Fall würde die erste save() Methode erneut ausgeführt werden. Aus unserer Erfahrung heraus ist es gar nicht so einfach, Grails-Actions oder Grails-Services idempotent zu schreiben.

Aus den oben aufgeführten Gründen setzen wir unsere Tomcat-Cluster immer nur mit einfachem Load-Balancing auf. Aus meiner Zeit bei BEA weiß ich noch, dass die meisten Kunden (nachdem das Geld nach der Euphorie des Jahrtausendwechsels wieder knapper wurde) auch nur das Load-Balancing benutzt haben. Mich würde hier natürlich interessieren, wie die Meinung anderer zum Thema Grails im Tomcat-Cluster aussieht.

Installation des Tomcat-Clusters

Hier gibt es verschiedene Möglichkeiten. Da bei unseren Kundenprojekten das Hosting meistens durch einen professionellen Hosting-Provider, mit dem der Kunde bereits zusammenarbeitet, übernommen wird, müssen wir uns nicht um die Installation kümmern. Es ist hier jedoch wichtig darauf hinzuweisen, dass jeder Tomcat in einem eigenen Java-Prozess laufen soll und nicht als virtueller Server  (verwaltet über die Tomcat-Management-Applikation host-manager). In diesem Fall teilen sich alle Tomcats (obwohl sie über einen eigenen Port etc. verfügen können) einen Java-Prozess. Eine fehlerhafte Grails-Web-Applikation würde dann den gesamten Tomcat herunterreißen. Wer sich um die Installation selber kümmern muss, dem können folgende URLs empfohlen werden [1][2][3].

Anmerkung: Die einfachste Form der Installation ist einen Tomcat für jeden Java-Prozess zu installieren. Der physikalische Server enthält dann zwei Tomcat-Verzeichnisse (z.B. srv1 und srv2). Die oben erwähnten Skripte hingegen teilen den Tomcat in einen generellen und serverabhängigen Bereich auf (z.B.  common, srv1 und srv2). Der Teil, der in das common-Verzeichnis kommt, kann sich von Version zu Version des Tomcats unterscheiden. Deshalb ist bei diesen Skripten immer zu überprüfen, ob sie mit der benutzten Tomcat-Version funktionieren.

JVM Settings

Wir benutzen meistens nur leicht modifizierte Standardeinstellungen der JVM für unsere Grails-Applikationen. Die Erfahrung hat gezeigt, dass es nicht zwingend besser ist die JVM extrem zu tunen. Mit folgenden Einstellungen haben wir bisher ganz gute Erfahrungen gemacht:

JAVA_OPTS="-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms1536m
-Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:+DisableExplicitGC -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC"

Anmerkung:  die beiden letzten Parameter dienen dazu, dass der PermGenSpace wieder freigegeben wird. Je nach Grails-Applikation mögen diese Parameter nicht nötig sein.

JDBC Connection Pool

Es ist sinnvoll, den JDBC Connection Pool des Tomcat-Servers zu benutzen. Dieser verfügt über bessere Pooling-Eigenschaften, als der standardmäßig in einer Grails-Applikation (Grails Commons DBCP Pool) benutzte.  Des Weiteren kann sehr einfach eine neue Datenbank konfiguriert werden, ohne Code an der Grails Applikation ändern zu müssen. In der Grails Applikation muss folgendes in der DataSource.groovy eingefügt werden.

// environment specific settings
environments {
  ...
    grails_production {
        dataSource {
            jndiName        = "java:comp/env/jdbc/grails_application"
            dialect         = org.hibernate.dialect.MySQL5InnoDBDialect
            dbCreate        = 'update'
        }
    }
  ...
}

Unten habe ich eine unserer Standardkonfigurationen (zu ändern in conf/context.xml) eingefügt, mit der wir bisher gute Erfahrungen gemacht haben. Diese können sich natürlich von Projekt zu Projekt ändern.

<context>
<watchedresource>WEB-INF/web.xml</watchedresource>
<manager pathname="">
<resource abandonwhenpercentagefull="50" auth="Container" driverclassname="com.mysql.jdbc.Driver" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" initialsize="10" logabandoned="true" maxactive="30" maxidle="20" minevictableidletimemillis="60000" minidle="10" name="jdbc/grails web application" password="xxx" removeabandoned="true" removeabandonedtimeout="60" testonborrow="true" timebetweenevictionrunsmillis="30000" type="javax.sql.DataSource" url="jdbc:mysql://xx.xx.xx.xx:9987/grails_web_application" username="xxx" validationinterval="30000" validationquery="SELECT 1">
</resource></manager>
</context>

Anmerkung: In unserer Konfiguration wird der Connection-Pool  zeitbasiert wieder verringert. Diese zeit-gesteuerte Anpassung der Pool-Größe kann zu Problemen führen, wenn die Connection im Tomcat schon weg ist, aber in MySQL aufgrund des eingestellten Timeouts noch aktiv ist. In diesem Fall wird eine neue Session auf dem MySQL-Server angelegt und dies kann unter Umständen dazu führen, dass die auf dem MySQL-Server eingestellten Connections (MySQL Parameter max_connections) überschritten wird. Aus diesem Grund sollte die maximale Anzahl der Connections über einen Last-Test ermittelt werden und zum Go-Live etwas höher sein. Nach dem Go-Live kann man über das Monitoring [4] eine genauere Größe ermitteln.

Der MySQL JDBC Driver (beispielsweise mysql-connector-java-5.1.10-bin.jar) muss nach tomcat/lib kopiert werden.

Konfiguration der Tomcat-Ports

Wichtig hierbei ist, dass jeder Tomcat über eigene Ports verfügt, da wir ja 1..n Tomcats auf einem physikalischen Rechner haben. Folgende Werte müssen in der server.xml gesetzt werden: 

AJP Connector Ports

<connector port="9008" protocol="AJP/1.3" redirectport="9448" uriencoding="UTF-8">
</connector>

Hierbei ist wichtig, dass auch das URIEncoding (Vorsicht: der Code-Formatter hat oben URIEncoding in Kleinbuchstaben gewandelt) gesetzt wird. Falls nicht werden deutsche Umlaute in einem Formular auf einer .gsp Seite nicht richtig übermittelt.

Server-Ports

<server debug="0" port="9018" shutdown="SHUTDOWN">
</server>

HTTP Connector Ports (für Admin-Konsole)

<connector connectiontimeout="20000" maxthreads="150" port="9088" protocol="HTTP/1.1" redirectport="9588" uriencoding="UTF-8">
</connector>

Jeder Tomcat Server braucht ein jvmRoute Setting, dieses wird an die SessionID angehängt und muss mit denen in der worker.properties (Apache WS) übereinstimmen.

<engine defaulthost="localhost" jvmroute="svr1" name="Catalina">
</engine>

Deployment der Grails-Web-Applikation

Generell stoppen wir hierzu den Tomcat, löschen die Grails-Web-Applikation im webapps-Verzeichnis (z.B. Grails_App.war und das Verzeichnis Grails_App/) kopieren die neue Version der Grails-Web-Applikation in das webapps-Verzeichnis und starten dann wieder den Tomcat. Dies geht natürlich auch alles ohne den Restart, aber mit dem oben beschrieben Weg haben wir die besseren Erfahrungen gemacht.

Monitoring

Das Monitoring in einer Produktions-Umgebung wird meistens durch Nagios abgedeckt. Für die Last-Tests setzen wir hingegen sehr gerne PSI-Probe [5] ein. Für ein passendes Monitoring-Tool mussten wir eine Weile suchen, da wir hier vom Oracle WebLogic-Server sehr verwöhnt waren, wir denken jedoch, dass PSI-Probe vergleichbar ist. PSI-Probe basiert auf Lambda-Probe, jedoch ist Lambda-Probe veraltet. Mit PSI-Probe kann man sehr viele interessante Werte, wie Anzahl HTTP-Session, JDBC-Sessions, Java-Heap, Java-PermGenSpace etc. analysieren. Unten habe ich noch ein paar Screen-Shots von PSI-Probe angehängt.

Links

[1] Clustering Grails - Burt Beckwith - http://burtbeckwith.com/blog/?p=244
[2] Basic Apache Tomcat clustering for Grails applications - Peter Ledbrook
[3] Tomcat Cluster Scripts - https://github.com/acreeger/grails-tomcat-cluster-scripts
[4] Nagios - http://www.nagios.org
[5] PSI-Probe - http://code.google.com/p/psi-probe

Kategorien: Apache TomcatGrailsMySQL

Zurück