Clustering Grails
Saturday, December 19th, 2009I did a talk on Grails
clustering at the Groovy & Grails eXchange 2009
in London last week and wanted to put up the slides and the sample application and clustering scripts. This was the third time I've given a version of this talk (first at a Boston Grails meetup
and again at SpringOne 2GX
) so I'm way overdue getting this up.
I created a simple Grails app and installed the Spring Security
plugin (to test web cluster login failover and to have domain classes for Hibernate 2nd level caching) and the Quartz
plugin to demonstrate support for multiple servers. The basic non-clustered version is here
so you can compare it to the version that has the required changes here
.
In no particular order, here are the relevant project files:
- There's a simple Quartz job in
grails-app/jobs/ClusterTestJobthat prints which instance it's running on to stdout every two seconds. grails-app/conf/QuartzConfig.groovyspecifiesjdbcStore=trueto have the plugin configure job storage in the database instead of in-memory.- Externalized configuration is enabled in
grails-app/conf/Config.groovyusinggrails.config.locations = [
"classpath:${appName}-config.groovy",
"file:./${appName}-config.groovy"]which allows local dev environment configuration using a file name
clustered-config.groovyin the root directory of the project, or prod configuration (e.g. to keep production database passwords out of source control) using aclustered-config.groovyin the classpath. Tomcat's lib directory is in its classpath, so that's a convenient place to put the file. - In addition note that the Log4j file loggers are configured using
ServerAwareFileAppender. Specifying the location of log files is tricky. If you leave the path out and make it relative, then files get created relative to the directory Tomcat is started from, which might not always be the same. But if you hard-code the path, it won't work cross-platform. So this class figures out if you're running in Tomcat or Jetty and if you're running in dev mode, and is also cluster-aware. Depending on how you're running it knows where the logs directory is and puts logs there. grails-app/conf/hibernate/hibernate.cfg.xmlloadsgrails-app/conf/hibernate/Quartz.mysql.innodb.hbm.xmlwhich has DDL for creating the tables needed for Quartz. Use the appropriate DDL for your database from one of the files in the Quartz plugin'ssrc/templates/sqldirectory.Role.groovyhas been customized to support the Hibernate 2nd-level cache:
- implementsSerializable
- read-only cache configured in mapping (along with disabling optimistic locking since it's not needed)
- customlist()andcount()methods that optionally use the cache or bypass itUser.groovyis also customized for the Hibernate 2nd-level cache:
- implements Serializable
- read-write cache configured in mapping-
grails-app/conf/spring/resources.groovyhas the fix for GRAILSPLUGINS-1207 src/java/ehcache.xmlhas caches configured for the domain classes. It's not distributed though since it'll be used in the development environment.scripts/_Events.groovyhas code to delete unused jars (unrelated to clustering, just a good idea) and delete the version ofehcache.jarthat comes with Grails since the app uses a newer version. It also replaces the non-distributedehcache.xmlwith the distributed version fromcluster_resources, and copiesquartz.propertiesin case you need to customize those beyond the defaults.-
grails-app/conf/BootStrap.groovyhas code to create the admin role and an admin user to test login
The files from Tomcat and the cluster scripts are available here
. Untar them somewhere on the server where you want to create a cluster:
cd cluster
You'll see five scripts:
- cleanup.sh
- createCluster.sh
- createInstance.sh
- deploy.sh
- run.sh
These are combinations of bash scripts and an Ant build file (clusterTasks.xml). All of the scripts need a cluster root directory. You can either specify it for each script invocation (run the script without parameters to see the usage) or you can set the CR environment variable
The first script to run is createCluster.sh:
This will create the directory structure and copy the Tomcat jars and other shared files.
There's an empty clustered-config.groovy that you can use to customize the production deployment. One common use for this is to externalize database passwords. This can be done with JNDI, but I prefer a self-contained war file that doesn't require container-specific configuration. This is copied to $CR/shared/lib when you run createCluster.sh since it's shared by all instances.
Next you need to run createInstance.sh once for each cluster node on this server. The syntax is
createInstance.sh [server number] [instance number] [cluster root dir]
and you need to ensure that the server number and instance number combination is unique throughout the cluster. This is simple if you number each server and choose a unique instance number for each instance on that server, e.g.
./createInstance.sh 1 2
./createInstance.sh 1 3
on server one,
./createInstance.sh 2 2
./createInstance.sh 2 3
on server two, etc. So go ahead and create at least two nodes on this server:
./createInstance.sh 1 2
Some files to note are $CR/shared/conf/server.xml and $CR/instance_X/conf/catalina.properties. Most values in server.xml have been replaced with ${} properties whose values are specified in catalina.properties. These property values are retrieved via System.getProperty() but Tomcat reads catalina.properties and sets system properties from there. Using this approach we can have one parameterized server.xml and allow per-instance customization of ports, etc. Another config file is $CR/instance_X/bin/setenv.sh which configures CATALINA_OPTS (and optionally other environment variables). Edit this file to change the heap or permgen memory for each instance.
Having created the instances, now you need to deploy a war. Download the test project
and unpack it:
cd clustered
and run grails compile to trigger installation of the project's plugins. As mentioned in the presentation
, there's a bug in the Quartz plugin that causes a problem in clusters since it tries to re-register jobs, so edit plugins/quartz-0.4.1/QuartzGrailsPlugin.groovy and change
scheduler.rescheduleJob(trigger.name, trigger.group, ctx.getBean("${key}Trigger"))
} else {
scheduler.scheduleJob(ctx.getBean("${key}Trigger"))
}
to
trigger.triggerAttributes.group)) {
scheduler.rescheduleJob(trigger.triggerAttributes.name,
trigger.triggerAttributes.group,
ctx.getBean("${key}Trigger"))
} else {
scheduler.scheduleJob(ctx.getBean("${key}Trigger"))
}
Now you can build the war:
and use deploy.sh to deploy it to the cluster:
The name of the war isn't important since deploy.sh deploys as the root context (in $CR/shared/webapps/ROOT). Note that you only deploy a war once per server since it's shared by all instances on that box.
Next create the database:
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 5.1.39-log MySQL Community Server (GPL)
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> create database clustered;
Query OK, 1 row affected (0.00 sec)
mysql> grant all on clustered.* to clustered@localhost identified by 'clustered';
Query OK, 0 rows affected (0.00 sec)
The project has a modified version of the SchemaExport script that fixes a bug reading hbm.xml files, so run
and choose option [2] to use the project version instead of the Grails version (this bug is fixed in Grails 1.2). This will create a file called ddl.sql that you can use to create the tables. The generated script is designed to be used by Hibernate's schema export tool which ignores errors trying to drop foreign keys on non-existent tables but this will fail from the commandline, so edit the file and remove all of the DROP TABLE ... and alter table ... drop foreign key ... statements and run
Now that we've deployed the war and created the database, we can now start the instances using run.sh. The syntax is
run.sh [start|stop|run] [instance number]
so you would run
to start instance #1. Once it has successfully started you can start the other instances:
You should see output indicating that the instances are discovering one another.
The server logs are in a few different directories. $CR/instance_X/logs will contain that instance's catalina.out and localhost_access_log, and the two rolling Log4j logs configured in Config.groovy, X_clusterdemo.log and X_sql.log, prefixed with the instance number.
$CR/shared/logs contains the remaining Tomcat logs; admin, catalina, host-manager, localhost, and manager all prefixed with the instance number. instance_X.pid files are here also, containing the process id for each instance to help with shutting down individual instances.
There is no load balancing configured yet; this can be configured using hardware load balancers, Apache, etc. $CR/shared/conf/server.xml configures the jvmRoute attribute of the <Engine> element (worker${cluster.server.number}_${cluster.instance.number}) for use with mod_jk.
So for now to test the app we'll go to specific instances. Open http://localhost:8091/admin/
and since that controller is secured, it'll prompt you to login. Use the username and password configured in BootStrap.groovy (admin/p4ssw0rd) and you should be allowed to view the page.
To test session replication, kill this instance. Find the pid for instance 1 in $CR/shared/logs/instance_1.pid and run kill -9 to ensure there's no orderly cleanup:
You'll see messages in the logs for instance 2 indicating that instance 1 has disappeared:
org.apache.catalina.tribes.group.interceptors.TcpFailureDetector memberDisappeared. You'll also see that if Quartz wasn't running in instance 2 it now is.
Open http://localhost:8092/admin/
and you should still be logged in since your Authentication is stored in the HTTP session and was replicated.
To shutdown an instance, use run.sh with the stop argument:
You can use cleanup.sh to delete log files and temp files in the temp and work directories. This isn't required but it's convenient.
Some further reading:
- Tomcat 6 clustering docs

-
Tomcat: The Definitive Guide
-
Quartz JDBC information
-
Ehcache xml documentation
Links and files:
- The PDF slides
from the London talk
- Watch the video
of the London talk
- The non-clustered application
- The clustered version
- The cluster files and scripts







