Tomcat Cluster con JSF
0. Índice de contenidos.
- 1. Entorno
- 2. Introducción
- 3. Arquitectura
- 4. Aplicación
- 5. Despliegue
- 6. Prueba de balanceo
- 7. Problemas encontrados
- 8. Resultado
- 9. Conclusiones
1. Entorno
Este tutorial está desarrollado usando el siguiente entorno:
- Hardware: Portátil MacBook Pro 2Ghz Intel Core i7 (4 núcleos) 8Gb de RAM
- Sistema Operativo: Mac OS X 10.7.2 (Lion)
- Eclipse Indigo (Revisar tutorial de Alex para su instalación)
- Apache Tomcat 7
- Versión de java SDK 6 instalada en el sistema
- Maven3 instalado y configurado en el sistema
- Apache con mod_jk preconfigurado (se extenderá/modificará) (Consultar un ejemplo)
2. Introducción
A continuación veremos como realizar una configuración simple de un cluster tcp en Tomcat utilizando un Apache con mod_jk para el balanceo de peticiones.
3. Arquitectura
El objetivo de este tutorial será comprobar la escalabilidad y la réplica de sesión para una aplicación montada en JSF. La arquitectura de servidores será la siguiente:
En cada uno de los tomcat estará desplegada la misma aplicación con alguna variación en la vista para identificar el servidor que atiende a la llamada, como veremos en los siguientes
apartados. Cabe decir, que todos los servidores estarán en la misma red.
La configuración de mod_jk será la siguiente:
workers.properties
# Define la lista de workers que se usaran para mapear las peticiones worker.list=loadbalancer,status worker.maintain=2 # Configuración basica para todos los worker worker.basic.type=ajp13 worker.basic.socket_timeout=1 worker.basic.connect_timeout=100 worker.basic.ping_mode=A worker.basic.port=8009 worker.basic.lbfactor=1 # Definimos el Nodo1 worker.node1.reference=worker.basic worker.node1.host=192.168.1.2 # Definimos el Nodo2 worker.node2.reference=worker.basic worker.node2.host=192.168.1.3 #Definimos el balanceo de carga worker.loadbalancer.type=lb worker.loadbalancer.balance_workers=node1,node2 worker.loadbalancer.sticky_session=false worker.loadbalancer.retries=1 worker.status.type=status
uriworkermap.properties
# Contexto de nuestra aplicacion ejemplo /jsfcluster=loadbalancer /jsfcluster/*=loadbalancer
La configuración de cada uno de los nodos será la misma salvo por el identificador del worker en el atributo jvmRoute del componente «Engine»:
server.xml-nodo1
<?xml version='1.0' encoding='utf-8'?> <Server port="8005" shutdown="SHUTDOWN"> ... <Service name="Catalina"> ... <Engine name="Catalina" defaultHost="localhost" jvmRoute="node1"> <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8"> <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/> <Channel className="org.apache.catalina.tribes.group.GroupChannel"> <Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" port="45564" frequency="500" dropTime="3000"/> <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="5000" selectorTimeout="100" maxThreads="6"/> <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter"> <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/> </Sender> <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/> <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/> <Interceptor className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/> </Channel> <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt"/> <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/> </Cluster> ... </Engine> </Service> </Server>
server.xml-nodo2
<?xml version='1.0' encoding='utf-8'?> <Server port="8005" shutdown="SHUTDOWN"> ... <Service name="Catalina"> ... <Engine name="Catalina" defaultHost="localhost" jvmRoute="node2"> <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8"> <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/> <Channel className="org.apache.catalina.tribes.group.GroupChannel"> <Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" port="45564" frequency="500" dropTime="3000"/> <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="5000" selectorTimeout="100" maxThreads="6"/> <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter"> <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/> </Sender> <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/> <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/> <Interceptor className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/> </Channel> <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt"/> <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/> </Cluster> ... </Engine> </Service> </Server>
Una vez realizados estos pasos, ya tenemos preparada la infraestructura a nivel de servidores que dejaremos iniciados.
4. Aplicación
Crearemos un nuevo proyecto maven3 con la siguiente estructura:
Ahora crearemos el pom.xml, en el cual nuestro objetivo será crear un paquete WAR para desplegarlo en Tomcat, y definir las dependencias con las librerías necesarias para trabajar con JSF:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.autentia</groupId> <artifactId>jsfcluster</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>jsfcluster</name> <description>Pruebas de clustering con jsf</description> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>javax.faces</groupId> <artifactId>jsf-api</artifactId> <version>1.2_15</version> </dependency> <dependency> <groupId>javax.faces</groupId> <artifactId>jsf-impl</artifactId> <version>1.2_15</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.el</groupId> <artifactId>el-api</artifactId> <version>2.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> </dependencies> </project>
El siguiente paso es la configuración de jsf en el web.xml, para que atienda todas las peticiones .jsf:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>jsfcluster</display-name> <welcome-file-list> <welcome-file>home.jsf</welcome-file> </welcome-file-list> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.jsf</url-pattern> </servlet-mapping> <distributable/> </web-app>
Ahora definiremos un ManagedBean con el scope session:
package com.autentia.jsfcluster.beans; import java.io.Serializable; /** * @author cleon * */ public class Texto implements Serializable { private static final long serialVersionUID = -330458269625644065L; private String texto; public Texto() { } public String getTexto() { return texto; } public void setTexto(String texto) { this.texto = texto; } }
Y un listener para ver el ciclo de vida de la aplicación desde el que mostraremos el contenido de la sesión en cada petición:
package com.autentia.jsfcluster.listener; import java.util.Enumeration; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; import javax.servlet.http.HttpSession; import org.apache.commons.lang3.builder.ToStringBuilder; /** * @author cleon * */ public class ListenerPhase implements PhaseListener { private static final long serialVersionUID = 1L; @Override public void afterPhase(PhaseEvent pe) {} @Override public void beforePhase(PhaseEvent pe) { System.out.println("POST-FASE: " + pe.getPhaseId()); if (pe.getPhaseId() == PhaseId.RESTORE_VIEW){ HttpSession session = (HttpSession)pe.getFacesContext().getExternalContext().getSession(false); if (session!=null){ Enumeration<String> atributos = session.getAttributeNames(); String nombreAtributo = ""; System.out.println("Atributos en sesion: "); while (atributos.hasMoreElements()){ nombreAtributo = atributos.nextElement(); System.out.println("\tAtributo: " + nombreAtributo); Object atributo = session.getAttribute(nombreAtributo); System.out.println("\tValor: " + ToStringBuilder.reflectionToString(atributo)); } }else{ System.out.println("La session es nula: " + session); } } } @Override public PhaseId getPhaseId() { return PhaseId.ANY_PHASE; } }
Creamos la vista home.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%> <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%> <%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <f:view> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Pruebas con clustering jsf</title> </head> <body> <h3>Pruebas con clustering:</h3> <% out.println("NODOX<br/>"); System.out.println("NODOX"); if ( session.isNew() ) { %> Sesion nueva: <% out.println(session.getId()); } else { %> Sesion existente: <% out.println(session.getId()); }%> <br/> <h:form> <h:outputLabel value="Introduce un texto: " /><h:inputText label="texto" value="#{texto.texto}"></h:inputText><br/> <h:commandButton label="Submit" value="Submit"/><br/> El texto introducido es: ${texto.texto} </h:form> </body> </html> </f:view>
Por último la configuración de ambos en el faces-config.xml:
<?xml version="1.0" encoding="UTF-8"?> <faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"> <managed-bean> <managed-bean-name>texto</managed-bean-name> <managed-bean-class>com.autentia.jsfcluster.beans.Texto</managed-bean-class> <managed-bean-scope>session</managed-bean-scope> </managed-bean> <lifecycle> <phase-listener>com.autentia.jsfcluster.listener.ListenerPhase</phase-listener> </lifecycle> </faces-config>
Cuando tengamos el proyecto con todas sus partes tan solo tenemos que ejecutar un mvn clean package que nos generará el archivo jsfcluster.war en la carpeta target.
5. Despliegue
Una vez generado el war tan solo tenemos que copiarlo en la ruta <apache_home>/webapps.
Cuando esté desplegado en ambos servidores modificamos la vista en la ruta <apache_home>/webapps/jsfcluster/home.jsp para cambiar las líneas que identificarán el nodo que atiende la petición (modificar la X por 1 ó 2 en cada caso):
out.println("NODOX<br/>"); System.out.println("NODOX");
6. Prueba de balanceo
Si todo ha ido bien, debemos estar en condiciones de realizar una peticion al apache con la siguiente url http://localhost/jsfcluster/home.jsf
Ahora rellenamos el campo de texto y realizamos peticiones consecutivas sobre el boton, verificando que se va variando de nodo en cada petición:
Si revisamos los logs de ambos servidores, veremos que la sesion es la misma en cada caso:
Atributos en sesion: Atributo: com.sun.faces.logicalViewMap Valor: com.sun.faces.util.LRUMap@ccc621[maxCapacity=15,accessOrder=true,threshold=16,loadFactor=1.0] Atributo: texto Valor: com.autentia.jsfcluster.beans.Texto@1e3bfb6[texto=gfhfdgh] Atributo: javax.faces.request.charset Valor: java.lang.String@13f5a2f[value={U,T,F,-,8},offset=0,count=5,hash=81070450]
Donde se identifican los managed beans (en nuestro caso texto) y su valor, y el arbol de componentes jsf (que debe ser exactamente el mismo en los dos nodos).
7. Problemas encontrados
Esta prueba es totalmente funcional con JSF, la versión 1.2_15 concretamente. La misma prueba la hemos realizado sobre JSF2 2.1.4 (implementación Sun Mojarra) sin que se realice correctamente la replica de la sesión en los dos nodos.
Desconocemos la causa de porque no funciona en JSF2, salvo si llevamos el arbol de componentes al cliente, con la consecuente penalización en las comunicaciones.
Animamos a nuestros lectores a que muestren su opinion y/o experiencias con otros servidores de aplicaciones o con Tomcat mismo para ver si descubrimos la posible causa del fallo.
8. Resultado
Aunque no hemos podido completar el ejemplo en jsf2 este ejemplo de cluster es válido para cualquier aplicación que esté preparada para trabajar en cluster, en el caso de tomcat, con los siguientes requerimientos:
- Todos sus atributos de la sesión debe implementar el interfaz java.io.Serializable
- Si se han definido las válvulas de costumbre cluster, asegúrese de que tiene la ReplicationValve definido así en el elemento de clústeres en server.xml
- Si las instancias de Tomcat se está ejecutando en la misma máquina, asegúrese de que el tcpListenPort atributo es único para cada instancia, en la mayoría de los casos Tomcat es lo suficientemente inteligente como para resolver esto en su propia autodetección de puertos disponibles en el rango de 4000-4100
- Asegúrese de que su web.xml tiene el elemento <distributable/>
- Asegúrese de que todos los nodos tienen el mismo tiempo y la sincronización con el servicio NTP!
9. Conclusiones
Como comentaba en el anterior punto, este ejemplo es extrapolable a otro tipo de aplicaciones, ya que es la configuración básica de un tomcat en cluster con replicación de sesión.
Cualquier duda o sugerencia podeis comentarlo.
Saludos.
Buen dia, cuando dice replica de sesion se refiere a persistencia ? porque yo quisiera generar 3 WAR (A,B,C)cada uno colocarlo en un servidor y por ejemplo: uno sea el logeo(A) y dentro de el tener dos botones que me envie a cualquier war «B» o «C»y que al estar dentro de cualquiera de los servidores no me pierda la sesion y pueda moverme de uno a otro sin perder la sesion.
Muchas Gracias,.
Hola rsiscoq,
Lo que tu necesitas es un Single Sign On. Echa un vistazo a los siguientes tutoriales:
_http://adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=IntroduccionCAS
_http://adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=ImplementandoSSOCAS
Saludos,
Carlos León
Hola Carlos,
Estupendo tutorial. Estoy teniendo un problema muy raro que, para no pegártelo todo aquí te dejo un enlace a StackOverflow, donde he dejado explicado el problema.
¿Sabes por qué puede estar ocurriendo esto?
http://stackoverflow.com/questions/11258541/floadbundle-basename-non-serializable-attribute