Tomcat Cluster con JSF

3
13738

Tomcat Cluster con JSF

0. Índice de contenidos.

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:

arquitectura

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:

Estructura de proyecto maven

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

Primera petición

Ahora rellenamos el campo de texto y realizamos peticiones consecutivas sobre el boton, verificando que se va variando de nodo en cada petición:

Segunda 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.

3 COMENTARIOS

  1. 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,.

  2. Hola rsiscoq,

    Lo que tu necesitas es un Single Sign On. Echa un vistazo a los siguientes tutoriales:

    _http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=IntroduccionCAS
    _http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=ImplementandoSSOCAS

    Saludos,

    Carlos León

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad