Teil 1 - Grails in Produktion – mit Apache, Tomcat und MySQL
Mit diesem Blog-Post möchte ich die Schritte, die nötig sind, um eine Grails Applikation produktiv zu stellen, beschreiben. Dieses Thema werde ich in aufeinanderfolgende Blog-Posts aufteilen. Die Aufteilung sieht folgendermaßen aus:
- Teil 1 – Allgemeines zur Systemarchitektur und Ergänzungen in Grails
- Teil 2 – Apache Web Server
- Teil 3 – Tomcat
- Teil 4 – MySQL
- Teil 5 – Load Tests
Systemarchitektur
Das Bild rechts beschreibt die gewählte Systemarchitektur. Diese besteht aus folgenden Komponenten:
- Load-Balancer – dieser kann sowohl auf Soft-, wie auch auf Hardware basieren. Meistens wir hier Hardware favorisiert. Wichtig ist, dass der Load-Balancer „Sticky Sessions“ unterstützt, sprich einen User-Request immer wieder an den ursprünglichen Apache WS weiterleitet.
- Apache Web Server – hier benutzen wir mod_jk zur Kommunikation zwischen Apache WS und Tomcat. Gründe hierfür sind: sehr ausgereift und wir benutzen meistens keine SSL Verschlüsselung zwischen Apache WS und Tomcat. Falls SSL notwendig ist, sollte eher mod_proxy_http benutzt werden [1].
- Tomcat – hier ist wichtig zu erwähnen, dass wir nur ein Load-Balancing für die Requests durchführen, also kein Cluster mit Session Replication. Dies reicht in den meisten Fällen vollkommen aus. Nachteilig hierbei ist, dass ein User sich erneut anmelden muss, falls eine Tomcat-Instanz abstürzt. Als Nachteile für ein Cluster können folgende Punkte aufgeführt werden:
- Performanz – jede Session wird auf einen Backup-Server (in-memory) über das Netzwerk repliziert. Bei sehr vielen Sessions (und vielen Tomcat-Instanzen im Cluster) kann dies eine sehr hohe Netzwerklast verursachen. Die Sessions können auch in einer Datenbank persistiert werden. Dies ist jedoch nicht performanter.
- Bei der Entwicklung muss beispielsweise beachtet werden, dass alle Objekte in der HTTP-Session serialisierbar sind. Auch darf die http-Session nicht zu groß sein (max. 50 – 70 KByte). Des Weiteren darf beispielsweise keine Hibernate-Session in einem HTTP-Objekt gespeichert werden. Obwohl wir wissen, dass dies Anfängerfehler sind, treten diese Fälle trotzdem sehr oft auf. Insbesondere in hektischen Projektphasen. Dies bedeutet schließlich, dass ein Cluster höhere Aufwände verursacht.
- MySQL – wir verwenden auch sehr gerne PostgreSQL. Jedoch ist MySQL bei den Hosting Providern unserer Kunden öfters vertreten. Deshalb wird hier auf MySQL eingegangen. Wichtig ist hier, sich über einen Fail-Over-Szenario im klaren zu sein. Es gibt mehrere Möglichkeiten, beispielsweise Linux-Cluster, VM-Ware-Cluster, Sequoia, etc. Die Hosting-Provider unserer Kunden setzen hier meistens ein Linux-Cluster ein.
Ergänzungen in Grails
Folgende Ergänzungen sollten meines Erachtens in Grails hinzugefügt werden:
- GSP-Seite mit folgenden Informationen:
- Information über die für den Build des WAR-Files benutzte Revision-Nummer des Sourcecode-Systems (Subversion in meinem Fall).
- Ausgabe von allen Cookies. Hier ist insbesondere die JSESSIONID von Interesse, da mod_jk den in der workers.properties gewählten Servernamen an die JSESSIONID anhängt.
- Konfiguration des Connection-Pools im Tomcat. Dieser wird dann als JNDI Connection-Pool in der Grails-Applikation benutzt. Dies hat den Vorteil, dass eine neue Datenbank mit anderen Settings, ohne ein erneutes Kompilieren der Grails-Applikation, benutzt werden kann.
- Ausgabe der Logfiles in ${CATALINA_HOME}/logs. Nun könnte jemand auf die Idee kommen, dies über einen JNDI Eintrag steuern zu wollen, wie in meinem Blog [3] beschrieben. Hierbei ist jedoch das Problem, dass die Config.groovy vor der resources.groovy (die den Zugriff auf JNDI durchführt) aufgerufen wird.
Revision aus Subversion während Grails War-File Build ermitteln
Der Code unten basiert auf einem Code-Snippet von StackOverflow [2]. Ich habe diesen modifiziert (Zeile 32), so dass die Revision-Nummer angezeigt wird. Dieser Code muss der Datei _Events.groovy hinzugefügt werden.
import org.tmatesoft.svn.core.SVNException
import org.tmatesoft.svn.core.wc.SVNRevision
import org.tmatesoft.svn.core.wc.SVNInfo
import org.tmatesoft.svn.core.wc.SVNWCClient
import org.tmatesoft.svn.core.wc.SVNClientManager
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory
// Create revision info in case of a WAR build
eventWarStart = { type ->
println "******************* eventWarStart *****************"
try {
// initialise SVNKit
DAVRepositoryFactory.setup();
SVNRepositoryFactoryImpl.setup();
FSRepositoryFactory.setup();
SVNClientManager clientManager = SVNClientManager.newInstance();
println "clientManager = " + clientManager.toString();
SVNWCClient wcClient = clientManager.getWCClient();
println "wcClient = " + wcClient.toString();
// the svnkit equivalent of "svn info"
File baseFile = new File(basedir);
println "baseFile = " + baseFile.toString();
SVNInfo svninfo = wcClient.doInfo(baseFile, SVNRevision.WORKING);
println "svninfo = " + svninfo.toString();
def version = svninfo.revision.number as String
println "Setting Version to: ${version}"
metadata.'app.version' = "${version}".toString()
metadata.persist()
}
catch (SVNException ex) {
//something went wrong
println "**************** SVN exception **************"
println ex.getMessage();
}
} // End eventWarStart()
Während des Builds wird die Revisions-Nummer in der Datei application.properties (siehe unten Zeile 6) geändert.
#Grails Metadata file
#Sat Jan 21 16:17:44 CET 2012
app.grails.version=1.3.7
app.name=MyGrailsProject
app.servlet.version=2.4
app.version=455
plugins.aop-reloading-fix=0.1
plugins.batch-launcher=0.5.6
plugins.csv=0.3.1
plugins.fixtures=1.1
plugins.hibernate=1.3.7
plugins.jquery=1.7.1
plugins.jquery-ui=1.8.15
plugins.mail=1.0-SNAPSHOT
plugins.resources=1.0.2
plugins.spring-security-core=1.2.4
plugins.tomcat=1.3.7
In der BuildConfig.groovy muss dann noch überprüft werden, ob das War-File nicht die Revisionsnummer angehängt bekommen hat. Sonst hätte der Name des War-Files folgenden Aufbau : MyProject-455.war, mit dem Nachteil, dass sich jedes Mal die Url der Grails-Applikation ändern würde.
grails.project.class.dir = "target/classes"
grails.project.test.class.dir = "target/test-classes"
grails.project.test.reports.dir = "target/test-reports"
grails.project.war.file = "target/${appName}.war"
grails.project.dependency.resolution = {
...
Das Ganze kann in einer GSP-Seite (inkl. Ausgabe der Cookies) folgendermaßen dargestellt werden:
<%--
Created by IntelliJ IDEA.
User: Peter Soth
Date: 11.01.12
Time: 11:49
To change this template use File | Settings | File Templates.
--%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main"/>
<title>Info:</title>
</head>
<body>
<h3>
The following revision has been used for the build of this WAR.</h3>
REV: ${grailsApplication.metadata.'app.version'}
<h3>
Show all cookies</h3>
<g:each in="${request.cookies}" var="cookie">
NAME: ${cookie.name} VALUE: ${cookie.value}
</g:each>
</body>
</html>
Tomcat JDBC Connection-Pool mittels JNDI einbinden
Hierzu wird in DataSource.groovy folgendes eingetragen:
// environment specific settings
environments {
production {
dataSource {
jndiName = "java:comp/env/jdbc/production"
dialect = org.hibernate.dialect.MySQL5InnoDBDialect
dbCreate = 'update'
}
}
...
Im Blog-Post zum Tomcat zeige ich dann, wie dies im Tomcat konfiguriert wird.
Logging in CATALINA_HOME festlegen
Mit folgendem Code-Snippet werden die Logs-Files der Grails-Applikation in das Log-Verzeichnis des Tomcat-Servers geschrieben. Hierzu muss die Variable logDirectory in der log4j-Konfiguration in Config.groovy benutzt werden.
// request parameters to mask when logging exceptions
grails.exceptionresolver.params.exclude = ['password']
// set per-environment serverURL stem for creating absolute links
environments {
...
production {
grails.serverURL = "http://www.changeme.com"
//tomcat based logging
def catalinaHome = System.getenv('CATALINA_HOME')
if (!catalinaHome) catalinaHome = '.' // falls nicht gesetzt
def String grailsLogDir = "${catalinaHome}/logs/"
...
}
}