jueves, 30 de agosto de 2012

Cluster de Tomcats con balanceo y "session failover"

Al hilo del artículo de David González Múltiples Tomcats con Apache y conector HTTP o AJP y el problema mencionado de tener que reiniciar un Tomcat al actualizar aplicaciones y perder servicio, vamos a añadir este articulillo rápido para explicar como montar un cluster de Tomcats con varias aplicaciones intentando no perder servicio al reiniciar. En principio la idea es tener todas las aplicaciones en 1 Tomcat y hacer cluster para que podamos pararlo y re-arrancarlo sin perder servicio. También serviría, con ligeras modificaciones, para tener varios Tomcat, dividiendo las aplicaciones entre ellos, y luego "clusterizárlos". Con eso conseguiríamos las ventajas que menciona David, más las de este artículo.

El primer paso es tener tus aplicaciones montadas en un Tomcat normal y corriente. Si no sabes hacerlo, empieza por ahí ya que éste artículo te será muy complejo sin una buena base.

Vamos a suponer que las maquinas donde quieres desplegar las aplicaciones se llaman app_host1.com y app_host2.com (nombres no publicos pero accesibles desde el servidor donde se encuentra el Apache) y que en ambas escucha el Tomcat con el protocolo AJP por el puerto 8009 o con el protocolo HTTP por el puerto 8080. Podemos usar el protocolo que más nos convenga (a AJP se le supone mejor rendimiento pero cada uno...)

Una vez tenemos claro esto, para dividir la carga entre las dos máquinas y enviarla a los Tomcats, y con "sticky sessions", usaremos el mod_proxy_balancer de Apache, añadiendo este bloque a la configuración en el lugar adecuado (depende de lo que queramos hacer y la versión de Apache, por defecto httpd.conf):
 # Definición del cluster con 2 nodos, usando el protocolo AJP
 <Proxy balancer://app_ajp_cluster>
  BalancerMember ajp://app_hos1.com:8009 route=Tomcat-All.1  
  BalancerMember ajp://app_hos2.com:8009 route=Tomcat-All.2  
  ProxySet stickysession=JSESSIONID|jsessionid scolonpathdelim=On
 </Proxy> 
 # Definición del cluster con 2 nodos, usando el protocolo HTTP
 <Proxy balancer://app_http_cluster>
  BalancerMember http://app_hos1.com:8080 route=Tomcat-All.1  
  BalancerMember http://app_hos2.com:8080 route=Tomcat-All.2  
  ProxySet stickysession=JSESSIONID|jsessionid scolonpathdelim=On
 </Proxy> 
 # Descomenta la siguiente parte para administrar el cluster, si sabes lo que haces.
 # Esta parte sirve para poder consultar/gestionar el estado del cluster
 # ¡¡No debe dejarse abierto al público!!
 #<Location /balancer-manager>
 # SetHandler balancer-manager
 # Order Deny,Allow
 # Deny from all
 # Allow from W.X.Y.Z   #Host del administrator
 # #... otras medidas de proteccion
 #</Location>
 #...
 # Para enviar un contexto al cluster de Tomcats, podemos hacerlo así
 ProxyPass ^/my1app/ balancer://app_ajp_cluster/myapp/ [P,QSA,L]
 # o si queremos afinar más...
 RewriteRule ^/my2app/ balancer://app_ajp_cluster%{REQUEST_URI} [P,QSA,L]
 # Podemos hacer virguerías como deshabilitar acceso a aplicaciones si no es por SSL
 RewriteCond %{SERVER_PORT} =443
 RewriteRule ^/mysslapp/ balancer://app_ajp_cluster%{REQUEST_URI} [P,QSA,L]
 # Y otras magias que permite RewriteCond

* Conviene asegurarse que en el Apache que estemos utilizando están cargados los modulos proxy_module, proxy_ajp_module, proxy_balancer_module, proxy_http_module, rewrite_module, headers_module y status_module.
** NO hay que olvidarse de proteger el balancer-manager si lo abrimos. YOU HAVE BEEN WARNED!!!

Ahora necesitamos definir en la configuración del Tomcat los nombres de las rutas, para lo cual modificaremos el fichero server.xml (o equivalente) y en el nodo  Engine añadiremos el atributo jvmRoute con el valor adecuado en cada nodo. Así pues, en el host app_host1.com el nodo quedará como:

    <Engine
      name="..."
      defaultHost="localhost"
      jvmRoute="Tomcat-All.1"
    >

en el host app_host2.com el nodo quedará como:

    <Engine
      name="..."
      defaultHost="localhost"
      jvmRoute="Tomcat-All.2"
    >


Una vez hecho esto, tenemos un cluster con "sticky sessions", lo cual nos permite tener aplicaciones con sesión y donde todas las peticiones de la misma sesión van al mismo Tomcat. Sin embargo, ¿Qué ocurre si uno de los Tomcat se cae? Pues que las sesiones se perderán y aunque los usuarios podrán seguir accediendo de forma transparente, perderán lo que estaban haciendo. En ciertos casos, como aplicaciones públicas sin identificación, eso no nos importará y el servicio se mantendrá sin incidencias, pero en los casos donde, como mínimo, se guarda el usuario identificado como atributo de la sesión como prueba de login, el servicio se interrumpirá y "echará" a todos los usuarios obligándoles a identificarse de nuevo, perdiendo lo que tuvieran en la sesión.

Como ese resultado no es muy agradable para los usuarios, ¿cómo añadimos "session failover" a nuestra configuración para que eso no pase? Pues existen multitud de opciones (Terracota, Hazelcast...) para hacerlo, pero la más sencilla y barata (aunque no tan fiable) es usar el clustering que viene con el Tomcat.

Para usarlo, dentro del nodo Engine de la configuración del Tomcat (server.xml o equivalente) añadiremos el elemento Cluster, configurado adecuadamente. Antes de mostrar la configuración en sí, analicemos los datos que vamos a necesitar definir:
  • Necesitamos una dirección y un puerto de multicast, para que los nodos se encuentren (en el ejemplo definidas como ${tomcat.mcast.addr} y ${tomcat.mcast.port}). Deben ser los mismos valores para todos los nodos del cluster.
  • Necesitamos el nombre público del host particular para que los otros miembros del cluster se puedan comunicar (en el ejemplo ${tomcat.host}). Esto que parece una obviedad no lo es si añadimos DNS internos, firewalls, etc. Hay que definirlo por que si no, lo más normal es que la JVM asuma por defecto que nuestro host se llama... "localhost" y cuando otro miembro del cluster quiera comunicarse con "localhost"... podeis suponer lo que pasa. Así que hay que asegurarse que ese nombre es correcto y se le pasa correctamente al Tomcat, ya que es la fuente #1 de problemas de configuración del cluster. Avisados quedais.
  • * Hace falta además un puerto para las comunicaciones entre Tomcats, en el ejemplo definido como ${tomcat.nio.port}.
  • Si estos últimos campos son iguales o diferentes para todos los nodos, depende de si los nodos están en la misma máquina o no.
  • En el ejemplo los valores esán definidos como ${xxx} por que el script de arranque ha sido modificado para pasar los valores adecuados en cada nodo del cluster, pero el Tomcat por defecto no rellena estos valores, así que modifica los valores, o el script, o no te te funcionará.

Con todos estos datos, así es como queda el trozo de configuración:

<Engine
 ...
    >
      <Cluster
        className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
      >
        <Manager
          className="org.apache.catalina.ha.session.DeltaManager" />
        <Channel
          className="org.apache.catalina.tribes.group.GroupChannel"
        >
          <Membership
            className="org.apache.catalina.tribes.membership.McastService"
            address="${tomcat.mcast.addr}"
            port="${tomcat.mcast.port}" />
          <Sender
            className="org.apache.catalina.tribes.transport.ReplicationTransmitter"
          >
            <Transport
              className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
          </Sender>
          <Receiver
            className="org.apache.catalina.tribes.transport.nio.NioReceiver"
            address="${tomcat.host}"
            port="${tomcat.nio.port}" />
          <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>
        <ClusterListener
          className="org.apache.catalina.ha.session.ClusterSessionListener" />
      </Cluster>
      <Host
   ...
    </Engine>
* Nota: Esta es la configuración mínima que he conseguido que me funcione. Algunos atributos tienen los valores por defecto, según la documentación, y algunos nodos deberían estar incluidos por defecto, de nuevo según la documentación, pero o la documentación miente o el Tomcat se pasa por el forro de la boina los valores por defecto en algunos casos, escoge la que más te guste. De todas formas, esta no es la única configuración posible y lo único que puedo decir de ella es que se ajusta a los que nos interesa y nos funciona ;).

En caso de querer hacer pruebas para ver si funciona en una sola maquina, lo que habría que hacer es usar dos Tomcat cambiando los puertos y ajustando la configuración adecuadamente.

Se podrían contar más cosas, como el script que arranca los Tomcat pasando los valores adecuados, usar un solo binario para multiples Tomcats detrás de un único Apache... pero como dice un gurú... eso es otra historia.

Happy coding!
EJ.

Disclaimer: Este no es un tema sencillo y no es recomendable para los no-iniciados, así que tratarlo con prudencia ;).