Servidores de aplicaciones
 

Rendimiento y alta disponibilidad

Introducción

Esta sesión se divide en dos partes diferenciadas: la primera en la que revisaremos puntos importantes que influyen en el rendimiento de un servidor WebLogic, y en la segunda hablaremos de clustering o dicho de otro modo, cómo configurar varios servidores para que funcionen como uno solo.

Rendimiento

Ajuste del tiempo de parada de los hilos de ejecución

Al procesar una petición, WebLogic crea un hilo y se lo asigna, y al terminar se libera. Pero ¿Qué ocurre si la petición no termina? WebLogic implementa un mecanismo tipo "sonda" que cada cierto tiempo monitoriza el estado de los hilos en ejecución. Por defecto, si un hilo excede los 600 segundos en ejecución, se considera como "colgado" y el servidor pasa a estado Warning en el que sigue operando, pero ya nos avisa de que algo va mal.

Si consideramos que nuestro servidor va a atender peticiones de larga duración, podemos modificar el comportamiento de esta sonda. En cada servidor en la pestaña de configuración->Ajuste tenemos un par de parámetros que configurar:

El primero de ellos determina el tiempo máximo que puede estar un hilo en ejecución, y el segundo cada cuánto tiempo WebLogic debe escanear el conjunto de hilos en ejecución

Aprovechar los Gestores de trabajo

Ya hemos hablado de que WebLogic mantiene un "pool" de hilos de ejecución que incrementa o reduce automáticamente en función de las peticiones que procese. Mediante los gestores de trabajo podemos añadir condiciones a esa gestión para que funcione según nuestras necesidades. Estas condiciones son de dos tipos:

  • Guías: Mediante el parámetro Clase de solicitud podemos asignar un "peso" a cada Gestor de trabajo en función de su importancia. Cuando el servidor, por cuestiones de carga de trabajo, se vea en la necesidad de crear nuevos hilos, los adjudicará en base a ese peso. De esta forma nos aseguramos de que en situación de stress se atiendan antes las tareas prioritarias.
  • Restricciones: En un gestor de trabajo podemos fijar el número máximo de hilos que podrán crearse, el número mínimo, y/o la capacidad de encolar peticiones (cuando no hay hilos disponibles).

Otra característica interesante es que podemos especificar que las peticiones que se atiendan aplicando nuestro gestor de trabajo se puedan ejecutar durante un tiempo indefinido, saltando el parámetro global definido a nivel de servidor.

Con estos controles podremos impedir el crecimiento desmedido de peticiones contra módulos de aplicación concretos (Servlets/JSP's, EJB's), para evitar la sobrecarga de dichos módulos e indirectamente del servidor completo.

Por último, no hay que olvidar que estas restricciones deben ser coherentes con las que podamos establecer en otras partes de la aplicación, como en los orígenes de datos, pool de objetos, etc. (Por ejemplo, de nada sirve definir un pool de 50 session beans cuando sólo el gestor de trabajo sólo permite 10 peticiones simultáneamente).

E/S Nativa

WebLogic utiliza unos componentes denominados muxers para comunicar cliente y servidor. En función del sistema operativo y de los "Performance Packs" que tengamos instalados en WebLogic estos módulos delegarán en el sistema operativo (Native I/O) o se utilizarán implementaciones independientes del S.O. en Java (Java I/O). Para un servidor en producción es muy importante asegurarnos que se utiliza la Native I/O, y ello lo podemos comprobar en la consola dentro de Ajustes de un servidor.

Aumentar el número de conexiones por usuario en Unix

Uno de los problemas que nos podemos encontrar en un servidor WebLogic, es que reciba un gran volumen de peticiones es que de repente empiecen a surgir excepciones del tipo:

java.io.IOException: Too many open files

Esto se debe a que a cada conexión concurrente, el sistema operativo le asigna un descriptor de archivo. Estos descriptores son un recurso limitado, y si nuestro servidor excede dicho límite dejará de dar servicio, además de tener problemas incluso para abrir ficheros.

La solución pasa por estudiar el volumen de conexiones concurrentes que debe soportar el sistema, y ajustar el número máximo de descriptores por usuario.

En el caso de Unix, por defecto el valor es de 1024 y lo podemos consultar con el comando ulimit -a:

				
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 20
file size               (blocks, -f) unlimited
pending signals                 (-i) 16382
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024		<---
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) unlimited
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
			

Se recomienda aumentar el valor en múltiplos de 1024, y se hace mediante el comando ulimit -n. Ejemplo:

ulimit -n 4096

Dimensionar orígenes de datos

Para dimensionar un pool de conexiones, en primer lugar debemos monitorizar el número máximo de conexiones concurrentes que consumen nuestras aplicaciones. Recordad que se puede obtener esta información desde la pestaña estadísticas.

Una vez determinado el valor máximo y con el fin de obtener un rendimiento óptimo, debemos fijar el número de conexiones inicial del pool a dicho valor, y escoger un valor máximo algo superior de modo que el pool pueda crecer en caso de que se superen nuestras previsiones de carga.

Una opción interesante en la configuración de los orígenes de datos es la de Probar conexiones en reserva. En ocasiones por mantenimientos planificados o averías WebLogic se interrumpe la comunicación entre WebLogic y las bases de datos.

En estas circunstancias, el servidor puede creer que las conexiones que gestiona son válidas cuando realmente no lo son y provocar un fallo en las aplicaciones cuando éstas intenten utilizarlas. La mejor forma de evitarlo es activar esta opción, que obliga al servidor a ejecutar consulta contra la base de datos (a modo de "ping") antes de entregar una conexión a las aplicaciones.

Ajustar la persistencia de las colas JMS

Por defecto, WebLogic garantiza la persistencia en disco a nivel de mensaje cuando configuramos un almacén persistente en disco. Esto tiene un coste en E/S importante que en función de los requerimientos de la aplicación podemos mejorar. Si miramos en la consola, se nos ofrece diferentes políticas a la hora de gestionar el archivo:

  • Desactivado: Una transacción JMS se da por finalizada cuando termina la operación en memoria, pudiendo retrasar la escritura en disco. Tiene le ventaja que puede ser hasta 100 veces más rápido que cualquier otra política de persistencia, pero la desventaja de que ante cualquier fallo del sistema podemos perder mensajes o recibir mensajes duplicados.
  • Vaciado de caché: Se trabaja con cachés en memoria que recogen todos las operaciones sobre la cola. Una vez termina una transacción, las distintas escrituras que la componen se vuelcan sobre el archivo, dándose por completada la transacción en ese momento.
  • Escritura directa: Cada operación sobre la cola se realiza directamente sobre el archivo de persistencia. El rendimiento de esta forma de trabajar es menor que el de vaciado del cache, no obstante se puede mejorar si se permite al sistema operativo que cachee el archivo.

La siguiente tabla compara el rendimiento de cada una de estas estrategias:

1-m Perf (1-1000 peticiones concurrentes)

m-m Perf (miles de peticiones concurrentes)

M-M Perf (millones de peticiones concurrentes)

Por supuesto, lo más rápido en términos absolutos es no utilizar persistencia pero recordad que si no especificamos un almacén, se utilizará un archivo común del servidor, por tanto lo que deberíamos hacer es especificar a nivel de mensaje o de factoría de mensajes que no queremos persistencia.

También es posible utilizar una BD como almacén persistente pero el rendimiento es inferior al de un archivo local en el servidor.

Paginación en las colas JMS

El flujo de mensajes en las colas JMS no suele constante, hay situaciones en las que se producen picos en los que el sistema puede recibir el doble o el triple de lo habitual. Bajo estas condiciones, WebLogic consumirá la memoria que sea necesaria para almacenar estos mensajes, independientemente de que sean persistentes o no. Para evitar situaciones de saturación de memoria WebLogic permite utilizar un mecanismo de paginación.

Este mecanismo se basa en fijar unas condiciones de alarma sobre el uso de memoria. Cuando estas condiciones se alcanzan, el sistema vuelca parte del contenido de la cola a disco, como si fuera una especie de swap de memoria.

De forma transparente, el sistema trabaja con los mensajes en memoria y los almacenados en disco hasta que la condición de saturación desaparezca, pasando nuevamente a trabajar sólo en memoria.

En resumen, es un mecanismo que permite reducir el consumo de memoria por parte de las colas JMS, a costa de una penalización en rendimiento por los accesos a disco.

Precompilar las aplicaciones antes de desplegarlas

Una utilidad interesante para utilizar antes de desplegar aplicaciones en WebLogic es el compilador appc. Sus funciones son las siguientes.

  • Genera y compila las clases necesarias para desplegar EJB's y JSP's en un servidor WebLogic.
  • Se asegura de que todas las clases e interfaces cumplan la especificación de EJB's.
  • Valida los descriptores de despliegue de una aplicación Web o Empresarial.
  • Como consecuencia de los puntos anteriores, reduce notablemente el tiempo de despliegue de una aplicación.

La llamada a este compilador es muy sencilla, basta con ejecutar:

java weblogic.appc [opciones] <ear, jar, war o directorio>

Además de desde la línea de comandos, podemos ejecutar este compilador como tarea de Ant, y en las últimas versiones de WebLogic, a través de su plugin para Maven.

Configurar la JVM

Un servidor WebLogic, como cualquier aplicación Java, se ejecuta mediante una Java Virtual Machine. La JVM es una aplicación nativa, dependiente del sistema operativo que constituye una especie de contenedor en el que se ejecuta el código compilado de Java.

La configuración de dicho contenedor define la forma en la que se utilizarán los recursos principales del ordenador, tales como procesadores o memoria. Por tanto, es importante conocer los aspectos más importantes de esa configuración para conseguir un buen rendimiento. De hecho, WebLogic tiene una configuración por defecto que no tiene porqué ser la más adecuada para el trabajo que vaya a desempeñar.

A la hora de ajustar estos parámetros hay que tener claro que no hay distinción entre servidor y aplicaciones desplegadas, todo es una única aplicación Java para la JVM por tanto los recursos se deben definir de forma global.

Cliente o Servidor

Un componente importante de la JVM es el compilador JIT, que es el encargado de traducir el bytecode de Java (resultado de compilar las clases Java) a instrucciones nativas de la máquina donde se va a ejecutar la aplicación.

Normalmente, en las JVM se incluye dos versiones distintas de este componente, una orientada a aplicaciones de escritorio/clientes y otra orientada a servidores:

La diferencia entre ambas versiones radica en las optimizaciones que se aplican a la hora de generar código. La versión cliente optimiza el tiempo de arranque y minimiza el consumo de recursos a la hora de ejecutar código. Por el contrario, en la versión de servidor, lo que prima es el rendimiento y la estabilidad, y se aplican optimizaciones "agresivas" a costa de los recursos consumidos y los tiempos de arranque.

La forma de especificar que se utilice el Jit de cliente o de servidor es añadir a la línea de comandos de Java, el parámetro -client o -server respectivamente.

Ya en el caso concreto de WebLogic, cuando un dominio se configura en modo desarrollo (pensado para ejecutarse en un puesto de desarrollo) automáticamente se especifica el parámetro -client. En el caso de un dominio en modo producción lógicamente se utilizará el parámetro -server.

El Heap

El heap es el espacio en memoria destinado a la ejecución de nuestra aplicación Java. En este espacio encontraremos objetos "vivos", creados en tiempo de ejecución, objetos que ya han sido liberados y espacio libre a disposición de nuestra aplicación.

En contraposición a las aplicaciones nativas, que suelen tener a su disposición la memoria libre que permita el sistema operativo, las aplicaciones Java suelen trabajar en un espacio de memoria delimitado en el arranque de la máquina virtual. Este espacio se configura mediante parámetros que se añaden a la línea de comandos de la máquina virtual.

A medida que se crean objetos, la memoria se va ocupando. Cuando la JVM detecta que se está quedando sin memoria disponible, inicia un proceso denominado garbage collector (GC) que elimina de la memoria aquellos objetos que ya no son necesarios para la aplicación. Durante este proceso se produce una "parada" en nuestra aplicación en la que se deja de atender peticiones. Una vez liberada la memoria, la ejecución continúa normalmente.

Áreas de Memoria del Heap

El heap como tal no es un espacio de memoria diáfano, se divide en diferentes compartimentos entre los que se distribuyen los objetos en función de su antigüedad. Esto se hace así porque las estrategias de análisis de memoria del GC se basan precisamente en analizar el uso y la antigüedad de los objetos.

Tomando como referencia la JVM HotSpot (Sun/Oracle) tenemos tres grandes áreas:

  • Young Generation, que es el espacio donde se mantienen los nuevos objetos. Esta área se divide en tres apartados: Eden que es la zona donde se crea un objeto por primera vez y Survivor 0 y Survivor 1 que son compartimentos por los cuales van pasando los objetos a medida de que se va ejecutando el GC y el objeto no se considera liberable (en caso contrario se destruye).
  • Old Generation/Tenured, está formada por los objetos que llegan de la zona Survivor 2 cuando se ejecuta el GC y se consideran objetos estables dentro de la ejecución de nuestra aplicación.
  • Perm área, es una zona de memoria destinada a la propia máquina virtual y al almacenamiento de código y metadatos.

Para dimensionar cada compartimento tenemos una serie de parámetros que añadiremos a la línea de comandos de nuestra aplicación:

JVM 32bit vs 64bit

A día de hoy casi cualquier plataforma servidora puede trabajar con procesos de 64 bit. Desde el punto de vista de la aplicación Java, la diferencia fundamental que vamos nos vamos a encontrar es que en la JVM de 32bit podremos utilizar un heap de 2-3Gb Máximo, dependiendo del sistema operativo, mientras que con la versión de 64 el único límite que vamos a tener es el hardware (durante bastantes años...). No es preciso hacer ningún tipo de adaptación en nuestra aplicación java para que funcione en modo 32 o 64 bit sin embargo nos encontraremos con diferencias en cuanto a rendimiento.

Por ejemplo, si fijamos un tamaño de heap de 3Gb en una máquina AIX, que ambas pueden direccionar, nos podremos observar que la JVM de 64bit tiene un rendimiento ligeramente inferior a la de 32bit por el coste adicional de trabajar con direcciones de memoria ampliadas.

Esto no siempre será así, ya que depende de la arquitectura hardware de la máquina, el sistema operativo y las JVM seleccionadas, pero si prevemos que no vamos a necesitar direccionar más de 3Gb de memoria en un proceso, conviene hacer algunas pruebas y escoger la solución que proporcione más rendimiento.

El tamaño importa

En función del tamaño del heap y la complejidad de nuestra aplicación se invocará más o menos veces al garbage collector (GC) con la consiguiente penalización en cuanto a productividad. Un heap muy pequeño provocará se llame al GC muchas veces y con muy poca efectividad (no podrá liberar apenas memoria), por tanto el consumo de CPU aumentará y la productividad caerá al dedicarse esa CPU a intentar obtener memoria.

En el caso opuesto, un tamaño de heap muy grande conllevará que el rendimiento de nuestra aplicación sea óptimo... un tiempo. Cuando se ocupe toda la memoria, ejecución del GC será notablemente más pesada lo cual puede producir paradas muy prolongadas.

Lo ideal es ajustar los parámetros para que el consumo de CPU destinado a GC no supere el 5% y que las paradas por GC no sean perceptibles de cara al usuario final. Una forma de saber si nuestros ajustes son los adecuados, es tomar como referencia mediciones del rendimiento de nuestra aplicación configurada con un tamaño de heap elevado, en un intervalo de tiempo en el que no se haya disparado el GC.

Cuidado
Si asignamos más memoria de la que tiene libre el sistema, el ordenador empezará a hacer swap con lo que se arruinará cualquier medición de rendimiento.

Algoritmo del Garbage collector

El GC ejecuta de forma periódica dos tipos de barrido del heap:

  • Minor Collection: Revisión rápida de la Minor Generation.
  • Major Collection: Revisión exhaustiva de toda la memoria, fundamentalmente la Old Generation.

Mientras tengamos memoria en el Heap, el GC se limitará a hacer barridos rápidos (partiendo de la base de que los objetos recién creados son los más susceptibles a ser eliminados), pero si se agota la memoria, se analizará todo el heap de forma exhaustiva para liberar la mayor cantidad de memoria posible, deteniendo la ejecución de nuestra aplicación como ya hemos comentado.

Desde la propia aplicación podemos solicitar a la JVM que se libere memoria mediante una llamada del tipo System.gc(), pero se trata de una instrucción asíncrona que la JVM ejecutará cuando considere. De hecho ejecutar un barrido de memoria bajo petición puede perjudicar a la heurística del GC por tanto esta práctica de desaconseja (Estas llamadas se pueden desactivar con el parámetro -XX:-DisableExplicitGC).

El algoritmo que emplea el GC en estos barridos de memoria es otro parámetro configurable en la JVM. Los presentes en Java 1.6 son:

  • The Serial Collector -XX:+UseSerialGC Algoritmo sencillo destinado a máquinas con una CPU y a aplicaciones de escritorio con un tamaño de heap reducido. Se analizan simultáneamente las Major y Minor Collection

  • The Parallel Collector -XX:+UseParallelGC Similar al anterior pero sacando partido a arquitecturas con varios procesadores. Es el algoritmo por defecto de la JVM HotSpot.

  • Concurrent-Mark-Sweep (CMS) -XX:+UseConcMarkSweepGC Este algoritmo está pensado para las aplicaciones de servidor donde no es tan importante el procesar el mayor número de instrucciones por segundo sino el procesar múltiples peticiones lo más rápido posible. Con los algoritmos anteriores, el heap se va llenando progresivamente hasta que salta la condición de Major Collection y se para la ejecución del programa. En el caso de tamaños de heap elevados, esta pausa puede ser muy perceptible. Para paliar este comportamiento surge el algoritmo CMS.

    La tarea e Minor collection se ejecuta de forma similar al resto de algoritmos, pero la Major collection se realiza en dos fases diferenciadas:

    o La fase de marcado (Mark) se encarga de buscar los objetos que no están en uso. En primer lugar se determinan los objetos vivos "raíz" inmediatamente accesibles fuera del Heap. Luego de forma concurrente se marcan todos los objetos accesibles transitivamente desde el conjunto de objetos raíz. Como la aplicación sigue en ejecución, es posible que no todos los objetos vivos se hayan marcado correctamente por lo que se introduce una pequeña pausa en la que se revisa el grafo de objetos vivos.

    o En la segunda fase (Sweep) se eliminan de memoria el resto de objetos no marcados. Nuevamente es un proceso que se ejecuta de forma concurrente.

El algoritmo CMS es el más adecuado para servidores ya que salvo dos pequeñas pausas se ejecuta de forma concurrente con la aplicación (si bien es cierto que al ejecutarse de forma concurrente resta CPU a la aplicación).

Desde Java 1.6 Update 14 y de forma oficial en Java 1.7 aparece el algoritmo G1 (Garbage first) como sustituto evolucionado del CMS.

Ajustando el Heap de WebLogic

La forma más sencilla es acceder a la carpeta bin del dominio y editar el archivo setDomainEnv.sh

Concretamente buscaremos línea:

# IF USER_MEM_ARGS ...

Y añadiremos justo a continuación los parámetros que creamos oportunos:

USER_MEM_ARGS="-Xms512m -Xmx512m ..."

De esta forma estamos cambiando la configuración por defecto de todos los servidores del dominio. Si queremos establecer una configuración distinta para los servidores gestionados, la podemos fijar desde la consola, en la pestaña Inicio del Servidor, si el dominio tiene un NodeManager activo, o bien creando un script de arranque específico en el que se le pasen los parámetros directamente al ejecutable de la JVM.

Una estrategia para configurar la JVM

Una vez revisados los distintos parámetros que influyen en el rendimiento de una aplicación Java, veamos qué ideas podemos aplicar para WebLogic.

1. Modelar mediante una aplicación de Stress (JMeter, por ejemplo) una carga de trabajo ficticia que se asemeje a la que la aplicación va a procesar en la realidad. Si con un equipo no es suficiente, se puede recurrir a varias máquinas de modo que generen carga en paralelo.

2. Configurar la JVM con un tamaño de Heap generoso, pero que no provoque que el sistema haga swap. Configuraremos la memoria mínima y máxima en un ratio de 1:1 o 1:1.5. Tened en cuenta que un servidor de WebLogic consume alrededor de 64Mb (aplicaciones y recursos aparte), por tanto si se reduce la memoria disponible en exceso puede que ni arranque.

-Xms1024m -Xmx1024m
Consejo
Utilizar un tamaño de heap fijo tiene sentido cuando la carga de trabajo del servidor tienda a mantenerse a lo largo del tiempo. En el caso de que suela fluctuar, conviene mantenerlo variable para permitir que la JVM lo dimensione automáticamente para un mejor rendimiento del GC. Eso si, los valores mínimo-máximo deben ser los adecuados o conseguiremos el efecto contrario.

3. Asegurarnos de que el dominio esté configurado en modo producción con lo que se utilizará el Jit de servidor (parámetro -server).

4. Seleccionar el algoritmo de GC más adecuado, normalmente CMS (o G1)

-XX:+UseConcMarkSweepGC

5. Monitorizar el GC bien mediante trazas del GC para mostrar por la salida estándar la actividad del GC:

-verbose:gc -Xloggc:gc.log

O más gráficamente mediante una herramienta que monitorice la JVM a través del API JMX, como por ejemplo el Dashboard, JConsole o Visual VM.

6. Lanzar la batería de pruebas y tomar mediciones del número de llamadas al GC, duración de las mismas y consumo de CPU de la máquina. Podemos simultanear la prueba de carga con un acceso "humano" a la aplicación para asegurarnos que el servidor responde a una velocidad razonable, al margen de las métricas de las herramientas de prueba.

7. En función del algoritmo de GC y la aplicación podemos encontrarnos con dos diferentes comportamientos del GC:

  • Si al ejecutarse nuestra aplicación la ocupación del heap fluctua en torno a un valor determinado, intentad reajustar el tamaño del heap especificando como valor máximo X+50%X y repetir las pruebas (aseguraos de que el consumo de CPU no se incrementa y el rendimiento no decrece).
  • Si el consumo de memoria crece sin parar hasta llegar a forzar el GC (típicos dientes de sierra), la estrategia será intentar reducir el tamaño del heap manteniendo el consumo de CPU por GC por debajo del 5%.

8. Si necesitamos el máximo rendimiento, se pueden obtener mediciones del ciclo de vida de los objetos que se crean en nuestro servidor y ajustar el tamaño de las distintas áreas que definen el heap (hay muchas estrategias), así como la configuración de grado fino del algoritmo del GC (Ej: CMS Incremental para reducir los periodos de concurrencia entre aplicación y GC).

Divide y vencerás
Es importante modificar los parámetros uno a uno y a continuación repetir las pruebas para no perder perspectiva de lo que estamos modificando.

Alta disponibilidad

¿Que es un clúster?

Un clúster es una agrupación de servidores trabajando de forma conjunta con un doble objetivo:

  • Escalabilidad (horizontal) Podemos aumentar (o reducir) dinámicamente la capacidad de las aplicaciones desplegadas en un clúster añadiendo nuevas instancias (servidores). Todo ello en caliente, sin interrumpir el servicio.
  • Alta disponibilidad En caso de caída de un servidor, otro componente del clúster retomará su trabajo de forma automática y transparente para el cliente. De cara al exterior un clúster funciona como un único servidor.

Un clúster de servidores es un miembro más de un dominio en WebLogic, puede coexistir con otros clústeres o servidores independientes, y de hecho actúa en muchos casos como un servidor "normal".

Características principales de un clúster

Para lograr los objetos de escalabilidad y alta disponibilidad, el clúster se apoya en las siguientes funcionalidades:

  • Tolerancia a fallos en las aplicaciones Esta característica persigue que si un componente de una aplicación está realizando un trabajo y por cualquier motivo falla, exista una copia de dicho componente en otra ubicación que sea capaz de retomar el trabajo y finalizarlo.
  • Servidores migrables WebLogic soporta la migración automática o manual de una instancia de clúster, desde una máquina a otra. Este mecanismo complementa al anterior, pues permite la alta disponibilidad para servicios que sólo se ejecuten en una instancia (también denominados como singleton). También puede ser de utilidad en tareas de mantenimiento.
  • Reparto de carga Es la distribución de tareas entre los distintos componentes de un clúster, según un criterio predefinido. Para que esta distribución se llevar a cabo deben replicarse en cada instancia los objetos necesarios para acometer esas tareas. Un objeto de estas características recibe el nombre de clusterizado.
  • Comunicación entre servidores Entre los servidores integrantes de un clúster se establece un canal de comunicación que permite monitorizar el funcionamiento de las instancias y el replicar el estado de los objetos clusterizados. Para ello se utilizan los protocolos IP Sockets, Unicast o Multicast.

A continuación vamos a recorrer los distintos objetos que en WebLogic pueden ser clusterizados:

Servlets y JSP's

Estos objetos se replicarán en los contenedores web de cada instancia, de modo que independientemente de a qué instancia ataquemos, veremos la misma aplicación web. Sin embargo, vamos a necesitar una pieza adicional, denominada balanceador de peticiones, que cubra las siguientes funciones:

  • Creación un punto único de entrada para todo el clúster. No es conveniente atacar a instancias concretas, pues en caso de caída de dicha instancia, no podríamos utilizar el resto de servidores.
  • Reparto de carga. Las peticiones que reciba el balanceador, serán repartidas en función de un algoritmo de carga configurado, y el estado de funcionamiento de las distintas instancias.

Más adelante hablaremos con más detalle de los balanceadores.

Otro servicio importante es la réplica del estado de la sesión entre instancias que se realiza de forma automática. De este modo cuando el balanceador nos transfiera de un servidor a otro, éste tendrá una copia de nuestra sesión y podremos seguir navegando con normalidad. El requisito para que funcione es que la sesión se almacene en memoria (WebLogic admite otras configuraciones como persistencia en disco o BD). Se debe añadir al descriptor weblogic.xml el siguiente parámetro:

			
<session-descriptor>	
      <persistent-store-type>replicated_if_clustered</persistent-store-type>
 </session-descriptor>

Tolerancia a fallos

Supongamos un clúster formado por tres servidores (A, B y C). Desde un navegador accedemos a la IP del balanceador el cual nos redirigirá a uno de los servidores, en este caso el "A".

En la primera conexión se creará una sesión en el servidor "A", pero también se creará una réplica en otro servidor del clúster, el "B". Mediante una cookie (o reescritura de URL's) el cliente mantendrá información de cuál es su servidor principal y su servidor de réplica, y el balanceador por afinidad seguirá encaminando las peticiones http que haga este cliente al servidor "A".

Supongamos que se produce una caída de "A". Cuando el balanceador intente reenviarle una petición y vea que está caído, automáticamente escogerá otro servidor del clúster como servidor principal, y le enviará la información de qué servidor es el servidor de réplica.

En este ejemplo se escoge como servidor principal "C", y recibe una copia de la sesión del servidor "B". Adicionalmente se actualiza la cookie del cliente para hacer constar la nueva situación.

EJB's

El tratamiento que da el clúster a los EJB's es algo parecido a lo que acabamos de ver para Servlets/JSP's pero nos permite jugar a dos niveles:

  • A nivel de stub: Cuando un cliente remoto intenta acceder a un EJB, lo primero que hace es obtener un contexto inicial y a partir de él un objeto de tipo stub que adopta la interfaz remota de dicho EJB. En el caso de un servidor clúster, el objeto devuelto recibe el nombre de cluster-aware-stub ya que tiene la información necesaria para acceder a cualquier instancia donde se haya desplegado ese EJB.

    Este tipo de stub funciona como balanceador de carga en tanto es capaz de repartir el trabajo entre todos los servidores y en caso de caída, es capaz de redirigir las peticiones a otra instancia diferente.

  • A nivel de objeto: Dentro de cada servidor no existe una única instancia del EJB sino que tenemos un pool de objetos. Nuevamente se producirá un reparto de carga entre los distintos objetos del pool.

Los EJB desplegados en un clúster, por defecto aplicarán reparto de carga a nivel de stub y de objeto sin necesidad de definir ninguna propiedad, salvo que queramos modificar el algoritmo empleado para repartir la carga.

Un detalle importante es que si el Bean al que estamos accediendo es de tipo stateful, todas las invocaciones de métodos que hagamos irán dirigidas a la misma instancia del EJB.

Tolerancia a fallos

Si estamos accediendo a un EJB alojado en un servidor, y éste cae, se nos encaminará automáticamente a otra instancia del objeto alojada en otro servidor. En el caso de un "Stateful Session Bean", la redirección se hará hacia un servidor que contenga una réplica del estado del Bean:

Aunque se actualice la referencia al EJB, es responsabilidad del programador decidir qué hacer ante una caída de un servidor. WebLogic no reintenta automáticamente la ejecución de un método, a no ser que sea catalogado como idempotente en el descriptor weblogic-ejb-jar.xml:

	
<idempotent-method>
	<method>
		<description>...</description>
		<ejb-name>...</ejb-name>
		<method-intf>...</method-intf>
		<method-name>...</method-name>
		<method-params>...</method-params>
	</method>
</idempotent-method>

Destinos JMS

Ya hemos visto como configurar una cola o tópico en un servidor de aplicaciones. En el contexto de varias instancias clusterizadas, es posible agrupar un conjunto de colas/tópicos desplegadas en el clúster bajo la apariencia de un único destino JMS distribuido, con las siguientes características:

  • Se accederá a él, mediante un único nombre JNDI.
  • Cada servidor del clúster tendrá un elemento del destino distribuido, asociado a un servidor JMS propio de cada servidor.

El uso de un destino distribuido nos permite beneficiarnos de las características de alta disponibilidad y escalabilidad de la que gozan estos recursos (se reparten los mensajes entre todos los destinos del clúster, y en caso de caída de uno de ellos, se redirige el mensaje a otro servidor).

Orígenes de datos

El protocolo JDBC es un protocolo fuertemente orientado a la sesión: nos conectamos a una base de datos, iniciamos una transacción, ejecutamos una serie de sentencias en un orden determinado, hacemos commit y cerramos la conexión.

Este comportamiento limita las posibilidades de aplicar estrategias de tolerancia a fallos. Si una conexión cae, el gestor de base de datos simplemente hará rollback y deshará los cambios. Será responsabilidad de la aplicación intentar ejecutar de nuevo la transacción.

Donde sí puede aportar algo WebLogic es en el caso de que un cliente externo demande una conexión al clúster. Como otros objetos clusterizados, cuando desplegamos un origen de datos sobre un clúster se creará una réplica en cada nodo del clúster. Si una aplicación trabaja con el origen de datos de un servidor, y éste cae, la próxima petición de una conexión se redirigirá a otro servidor de forma transparente.

Por último, WebLogic soporta el uso de bases de datos con múltiples instancias replicadas, mediante el objeto origen de datos múltiple. De esta forma nuestro clúster podrá continuar trabajando en el caso de que una de las bases de datos caiga.

Balanceador de peticiones

Básicamente son elementos que enrutan peticiones entre diferentes destinos en base a un algoritmo de reparto configurado y a la monitorización del estado de los destinos configurados.

Como algoritmos los principales son:

  • Asignación en rueda (Round Robin) Es el algoritmo por defecto en muchos balanceadores. Cada petición se va repartiendo a cada uno de los componentes del clúster de forma secuencial. Cuando se termina de recorrer el clúster, se vuelve a empezar por el primero.

  • Basado en importancia (Weight-Based) Es una variación del anterior en la que se le asigna un peso a cada uno de los componentes. El peso de cada componente se compara con los valores asignados al resto y el cociente determina la parte proporcional de la carga que deberá dirigirse a ese componente. Este algoritmo se emplea fundamentalmente cuando los destinos tienen diferente capacidad de proceso.

  • Aleatorio Este algoritmo distribuye peticiones de forma aleatoria y sólo se debe utilizar en el caso de sistemas homogéneos. Aunque a la larga sea un reparto de trabajo equitativo, no es tan preciso como el algoritmo round robin.

El comportamiento de estos algoritmos varía si se activa la denominada afinidad. La afinidad es una propiedad por la cual si un balanceador ha asociado un cliente externo con un servidor, las sucesivas peticiones del mismo tipo que haga el cliente se enrutarán automáticamente al mismo servidor. Este comportamiento reduce el número de conexiones que se deben establecer entre el servidor y los clientes y es requerido en ciertos casos como llamadas a Stateful Session Beans.

En cuanto a la monitorización del estado, si uno de los componentes del clúster cae, el balanceador debe detectar la nueva situación marcando servidor caído como no disponible. Cuando éste se recupere, el balanceador deberá habilitarlo de nuevo en la lista de distribución de peticiones.

Atendiendo a la naturaleza del balanceador, tenemos balanceadores software y hardware:

Balanceador software

Ya hemos visto algunos de estos elementos en los puntos anteriores:

  • Balanceo de EJB's a nivel de stub. Es el único mecanismo disponible para repartir llamadas a EJB's por RMI. El algoritmo de reparto se configura a nivel de clúster en la consola, o a nivel de EJB en el descriptor weblogic-ejb-jar.xml.
  • Balanceo de EJB's a nivel de objeto. Se configura en el descriptor weblogic-ejb-jar.xml.
  • Balanceo de peticiones JMS. Se configura en la consola (Es el mismo parámetro que define el balanceo de EJB's).
  • HttpClusterServlet. Es un recurso software que podemos utilizar para balancear las llamadas peticiones HTTP. Se trata de un tipo de servlet especial en el que configuraremos la lista de servidores que compone el clúster. Este servlet se empaquetará como una aplicación Web (WAR) y se desplegará en un servidor independiente que hará de punto de entrada único.
  • WebLogic Server Proxy plugin. Se trata de un plugin de Oracle que se añade a un servidor web para balancear peticiones HTTP. Actualmente soporta los servidores Netscape Enterprise Server, Apache y Microsoft IIS.

Balanceador hardware

Se utilizan fundamentalmente para conformar un punto único de entrada y repartir peticiones HTTP entre las instancias de un clúster, pero en función de su complejidad pueden realizar múltiples tareas y descargar de trabajo a los servidores (por ejemplo encargándose de la encriptación del canal).

Un balanceador hardware no es ni más ni menos que un ordenador especializado ejecutando una serie de programas. Sin embargo es mucho más fiable que cualquier balanceador software y está capacitado para gestionar una mayor carga de trabajo.

Otra particularidad es que contemplan más algoritmos de reparto de carga y si los que tienen no se adaptan a nuestras necesidades, permiten ampliarlos mediante programación. Por el contra, al no estar integrados con el clúster deben comunicarse con nuestros servidores a través de "sondas" que monitorizan el estado, bien definidas por el fabricante, o por nosotros mismos. Un ejemplo sería monitorizar el estado de un servidor mediante peticiones HTTP que deban devolver un OK (200).

Single point of faliure

Como ya hemos dicho, un balanceador supone un punto único de entrada para nuestro clúster, pero ¿qué pasa si este falla? Pues que estamos perdidos, salvo que sea un balanceador hardware con redundancia.

Estos componentes internamente son un grupo de balanceadores configurados para alta disponibilidad. Es una configuración cara, pero justificada en algunas instalaciones.

Crear un clúster en WebLogic

Como siempre, WebLogic nos da muchas opciones a la hora de hacer las cosas. Podemos definir un clúster desde el asistente de instalación, scripts WLST, JMX, pero lo habitual es hacerlo desde la consola. Seguiremos los pasos siguientes:

1.Declarar las máquinas que participarán en el clúster, en Entorno->Máquinas. En caso de trabajar con varias máquinas es importante rellenar la información referente al gestor de nodos de cada máquina, para poder administrar los servidores gestionados desde la consola (tanto si hay clúster como si no).

2. Crear los servidores que compondrán el clúster. Para cada servidor debemos indicar un nombre y un puerto de escucha libre. Una vez creado, modificaremos su configuración y le asignaremos la máquina que le corresponda. El resultado será algo parecido a esto:

3. En Entorno->Clusters, crear un nuevo clúster. Una vez creado pinchamos en Configuración->Servidores y le asignamos los servidores gestionados.

4. En la Configuración general, nos limitaremos a habilitar el marca de "Plugin de WebLogic Activado". Observad que el primer parámetro determina el algoritmo de balanceo por defecto en el clúster (EJB's, JMS). Hay otros parámetros como la dirección del clúster y el número de servidores que podemos dejar en blanco para que WebLogic los genere dinámicamente.

5. Según nuestras necesidades, podemos habilitar en cada servidor gestionado la opción de "Ignorar sesiones durante el cierre". Por defecto, un servidor WebLogic no se podrá apagar/suspender mientras tenga sesiones activas, salvo que activemos esta opción.

6. Iniciamos los nodos administrados mediante el script de arranque o desde la consola, lanzando previamente el Gestor de Nodos.

Despliegue de aplicaciones en un clúster

Una vez creado el clúster, ya podemos desplegar nuestras aplicaciones en todas sus instancias como si fueran un único servidor. La forma de desplegar aplicaciones es la misma de siempre, no obstante la pantalla para elegir destino cambia ligeramente:

Ahora se diferencia entre servidores independientes y clústeres, pero aún en el caso de un clúster se nos da la opción de desplegar nuestra aplicación en todo el clúster o sólo en los nodos seleccionados.

Configurar un sencillo balanceador de peticiones http

1. Desde NetBeans crear un nuevo proyecto de Aplicación Web basado en Maven (Maven->Web Application). Lo podemos denominar balanceador.

2. Añadimos un servlet nuevo con la siguiente configuración:

3. Añadimos el parámetro que identifica los componentes del clúster:

El resultado, en el web.xml debe ser algo similar a esto:


<servlet>
        <servlet-name>HttpClusterServlet</servlet-name>
        <servlet-class>weblogic.servlet.proxy.HttpClusterServlet</servlet-class>
        <init-param>
            <param-name>WebLogicCluster</param-name>
            <param-value>localhost:7002|localhost7003</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>HttpClusterServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
			

4. Especificad el contexto raíz a "/" en el descriptor weblogic.xml:

<context-root>/</context-root>

5. Compilad el proyecto y desplegadlo en un servidor independiente (para hacer pruebas podéis utilizar el admin). Jugando con los "servlet-mapping" y el contexto raíz podéis controlar que peticiones se deben redirigir a los servidores del clúster o procesar en el servidor independiente.

Acceder a un clúster desde un client externo

Si nuestro cliente es un navegador y tenemos algún configurado un balanceador, utilizaremos su nombre de máquina y puerto. En el caso concreto de utilizar el HttpClusterServlet, indicaremos el puerto de la instancia donde esté desplegado el "balanceador".

Si en lugar de un navegador utilizamos una aplicación cliente externa en Java, y pretendemos llamar a un EJB tenemos un problema de base: el balanceador de carga no sirve para llamadas RMI.

Entonces ¿Cómo acceder al clúster desde un cliente java?

Si recordáis la teoría, lo primero que se hace a la hora de acceder a un servidor de aplicaciones desde un cliente remoto es obtener un contexto inicial indicando la ubicación del servidor y puerto. WebLogic pone a disposición de los desarrolladores de unas librerías propias de cliente para la conexión con el servidor.

Con estas librerías, la primera aproximación para trabajar con un clúster sería referenciar en este contexto inicial a uno de los servidores. De esta forma en la primera conexión nos descargaríamos el replica-aware stub y las sucesivas peticiones se balancearían a través de este stub. Sin embargo, si cae este servidor inicial, no podremos iniciar nuestro cliente, sin cambiar la configuración para que apunte a otro servidor:

InitialContext ctx =  new InitialContext("t3://localhost7002");

Una versión algo más correcta, sería enumerar la lista de servidores que componen el clúster. Con esta información, el cliente de WebLogic es capaz de enviar peticiones a cualquier servidor. Si el primero falla, lo intentará con el siguiente:

InitialContext ctx =  new InitialContext("t3://10.0.10.10:7002,10.0.10.10:7003");

El problema es que estamos añadiendo una enumeración de servidores que en función de las necesidades del negocio, o por tareas de mantenimiento puede cambiar. Un cambio en la composición del clúster obligará a cambiar las aplicaciones cliente que lo utilicen.

La solución más adecuada para entornos de producción es utilizar un DNS. Asignaremos un nombre al clúster, y le asignaremos la lista completa de servidores. Desde el punto del cliente, la llamada al initialContex quedaría así:

InitialContext ctx = new InitialContext("t3://micluster:7002");

Si se añade un nuevo servidor, bastará con actualizar el DNS sin tener que tocar programas, pero tiene una limitación importante: el DNS no entiende de puertos, por tanto esta solución sólo vale si los servidores de nuestro clúster están repartidos cada uno en una máquina con el mismo puerto de escucha.

Referencias