Cómo diagnosticar y recuperar el clúster de Liferay

Check English version here

 

Liferay está diseñado para escalar horizontalmente soportando la adición de nuevos nodos. Estos nodos compartirán la misma información, pero también serán capaces de manejar diferentes peticiones simultáneamente. Más información se encuentra disponible en Liferay DXP Clustering.

La sincronización y comunicación del clúster de Liferay depende de JGroups, por lo que mucha de la información de la que se hablará también se puede encontrar en la documentación de  JGroups.  Por otro lado, la planificación depende de Quartz. Su conocimiento ayudará a trabajar con los jobs (o tareas) programados de Liferay.

Entender cómo ambos interactúan con Liferay puede ser útil para comprender algunos de los errores que se obtienen comúnmente una vez el clúster se encuentra arrancado. Normalmente cuando estos errores aparecen no hay más solución que reiniciar los nodos, pero a veces se pueden seguir otros enfoques. De ellos se hablará a lo largo de este blog. 

 

 

 

1. Introducción

Para arrancar el clúster basta con configurar la siguiente propiedad:

cluster.link.enabled=true

 

Una vez que arranque el servidor, con la configuración por defecto, se puede observar como Liferay crea un par de canales:

  • transport-0: Este canal maneja la comunicación necesaria para enviar los mensajes que permiten invalidar la caché de Liferay. Se pueden configurar hasta 10 canales de transporte.
  • control: El propósito de este canal es manejar la comunicación desde el punto de vista de la jerarquía del clúster, asegurando la sincronización de los jobs para que Liferay se encargue de saber qué nodo está ejecutando cada tipo de job.

Así que básicamente existen dos puntos a los que prestar atención:

  • Sincronización del contenido.
  • Gestión de los jobs programados de Liferay.

 

 

2. Tipos de Caché

La configuración de la caché de Liferay se apoya en Ehcache. Se trata de un framework independiente utilizado tanto por la capa de acceso a datos como por el motor de acceso a plantillas (más información en Introducción a la configuración de la caché). Gestiona dos pools:

  • Multi-VM: La caché se replica entre todos los nodos del clúster.
  • Single-VM: La caché no se replica entre los nodos del clúster, siendo gestionada de forma individual por cada máquina virtual. Se utiliza para objetos y referencias que no necesitan ser replicadas, coordinadas entre los nodos.

 

Cuando se menciona la replicación de caché la creencia popular es que Liferay replica los valores de la caché. Pero, ese no es el comportamiento por defecto, aunque se puede obtener configurando propiedades como replicatePutsViaCopy, replicateUpdatesViaCopy. Más información sobre esto en la API de clustering de Liferay.

Por tanto, por defecto, la sincronización se lleva a cabo por invalidación, no por valor. Esto significa que cuando se modifica el valor asociado a una clave en un nodo se envía una notificación al resto de nodos obligándoles a conciliar su información con la fuente de información común, es decir, la base de datos. Dicha sincronización se realiza apoyándose en los canales de transporte. Por ello se debe asegurar un canal estable que incluya a todos los nodos del clúster para garantizar una correcta sincronización del contenido.

A alto nivel, a modo de fotografía, se puede visualizar cada nodo con su propio espacio de caché gestionado por Ehcache y, en el caso de MultiVM, Liferay realizando tareas de sincronización en segundo plano para asegurar que ningún valor de la caché sea obsoleto.

 

SingleVM

 

Para comprobar que no hay ninguna sincronización de caché cuando trabajamos con SingleVM se pueden seguir los siguientes pasos. Para simplificar solo se utilizarán un par de nodos, aunque el ejemplo puede extrapolarse a cualquier número de estos:

  1. Tomemos uno cualquiera de los nodos, al que denominaremos Nodo1 para diferenciar. Ejecutemos, a continuación el siguiente script de Groovy:

    import com.liferay.portal.kernel.cache.SingleVMPool;
    import com.liferay.registry.RegistryUtil;
    import com.liferay.registry.Registry;
    import com.liferay.portal.kernel.cache.PortalCache;
    import com.liferay.registry.ServiceReference;
    import com.liferay.portal.kernel.cluster.ClusterExecutorUtil;
    import com.liferay.portal.kernel.cluster.ClusterMasterExecutorUtil;
    
    private SingleVMPool getSingleVMPool() {
        Registry registry = RegistryUtil.getRegistry();
    
        ServiceReference serviceReference = registry.getServiceReference(SingleVMPool.class);
    
        return registry.getService(serviceReference);
    }
    
    out.println("=== Execution in: " + ClusterExecutorUtil.getLocalClusterNode());
    out.println("=== isMaster: "+ ClusterMasterExecutorUtil.isMaster());
    
    SingleVMPool singleVMPool = getSingleVMPool();
    PortalCache portalCache = singleVMPool.getPortalCache("CACHE-TEST");
    
    portalCache.put("testKey", "testValue1");
    out.println("=== Value for testKey: " + portalCache.get("testKey"));

    Este código simplemente asigna, en la caché CACHE-TEST, el valor testValue1 a la clave testKey. Después lo muestra por pantalla.

    Desde una perspectiva de bajo nivel no hay código de sincronización involucrado.
     
  2. Ejecutemos, en un nodo del clúster diferente (que denominaremos Nodo2 para diferenciar) cuya (falta de) sincronización contra el Nodo1 quiere ser probada, el siguiente script Groovy:

    import com.liferay.portal.kernel.cache.SingleVMPool;
    import com.liferay.registry.RegistryUtil;
    import com.liferay.registry.Registry;
    import com.liferay.portal.kernel.cache.PortalCache;
    import com.liferay.registry.ServiceReference;
    import com.liferay.portal.kernel.cluster.ClusterExecutorUtil;
    import com.liferay.portal.kernel.cluster.ClusterMasterExecutorUtil;
    
    private SingleVMPool getSingleVMPool() {
        Registry registry = RegistryUtil.getRegistry();
    
        ServiceReference serviceReference = registry.getServiceReference(SingleVMPool.class);
    
        return registry.getService(serviceReference);
    }
    
    out.println("=== Execution in: " + ClusterExecutorUtil.getLocalClusterNode());
    out.println("=== isMaster: "+ ClusterMasterExecutorUtil.isMaster());
    
    SingleVMPool singleVMPool = getSingleVMPool();
    PortalCache portalCache = singleVMPool.getPortalCache("CACHE-TEST");
    
    out.println("=== Value for testKey: " + portalCache.get("testKey"));
    portalCache.put("testKey", "testValue2");
    out.println("=== Value for testKey: " + portalCache.get("testKey"));
    

    El script comprueba si hay algún valor cacheado en el Nodo2, en la caché CACHE-TEST, para la clave testKey. No debería ser posible (de ahí que devuelva null) porque no se ha relizado un put antes.

    Después, el Nodo2 pone un nuevo valor para testKey. De nuevo, no hay código de sincronización involucrado.

     

  3. Ejecutemos el siguiente script, de nuevo sobre Nodo1:

    import com.liferay.portal.kernel.cache.SingleVMPool;
    import com.liferay.registry.RegistryUtil;
    import com.liferay.registry.Registry;
    import com.liferay.portal.kernel.cache.PortalCache;
    import com.liferay.registry.ServiceReference;
    import com.liferay.portal.kernel.cluster.ClusterExecutorUtil;
    import com.liferay.portal.kernel.cluster.ClusterMasterExecutorUtil;
    
    private SingleVMPool getSingleVMPool() {
        Registry registry = RegistryUtil.getRegistry();
    
        ServiceReference serviceReference = registry.getServiceReference(SingleVMPool.class);
    
        return registry.getService(serviceReference);
    }
    
    out.println("=== Execution in: " + ClusterExecutorUtil.getLocalClusterNode());
    out.println("=== isMaster: "+ ClusterMasterExecutorUtil.isMaster());
    
    SingleVMPool singleVMPool = getSingleVMPool();
    PortalCache portalCache = singleVMPool.getPortalCache("CACHE-TEST");
    
    out.println("=== Value for testKey: " + portalCache.get("testKey"));

    Comprueba que el valor para la clave testKey sigue siendo el mismo (testValue1) dado que no se ha producido ninguna sincronización con el Nodo2.

 

MultiVM

Para comprobar si la sincronización actual de la caché funciona como se espera (también para entender cómo funciona el comportamiento por defecto) se pueden seguir los siguientes pasos. Para simplificar solo se utilizarán un par de nodos, aunque el ejemplo puede extrapolarse a cualquier número de estos:

 

  1. Tomemos uno cualquiera de los nodos, al que denominaremos Nodo1 para diferenciar. Ejecutemos, a continuación el siguiente script de Groovy:

    import com.liferay.portal.kernel.cache.MultiVMPool;
    import com.liferay.registry.RegistryUtil;
    import com.liferay.registry.Registry;
    import com.liferay.portal.kernel.cache.PortalCache;
    import com.liferay.registry.ServiceReference;
    import com.liferay.portal.kernel.cluster.ClusterExecutorUtil;
    import com.liferay.portal.kernel.cluster.ClusterMasterExecutorUtil;
    
    private MultiVMPool getMultiVMPool() {
        Registry registry = RegistryUtil.getRegistry();
    
        ServiceReference serviceReference = registry.getServiceReference(MultiVMPool.class);
    
        return registry.getService(serviceReference);
    }
    
    out.println("=== Execution in: " + ClusterExecutorUtil.getLocalClusterNode());
    out.println("=== isMaster: "+ ClusterMasterExecutorUtil.isMaster());
    
    MultiVMPool multiVMPool = getMultiVMPool();
    PortalCache portalCache = multiVMPool.getPortalCache("CACHE-TEST");
    
    portalCache.put("testKey", "testValue1");
    out.println("=== Value for testKey: " + portalCache.get("testKey"));

    Este código simplemente asigna, en la caché CACHE-TEST, el valor testValue1 a la clave testKey. Después lo muestra por pantalla.

    Desde una perspectiva de bajo nivel lo que hace este código es añadir, en el espacio de caché del Nodo1, el valor testValue1 a la clave testKey en la caché CACHE-TEST. Mientras tanto se enviará un mensaje de invalidación al resto de nodos para que eliminen los valores asociados a testKey en la caché CACHE-TEST, en su propio espacio de caché individual.

     

  2. Ejecutemos, en un nodo del clúster diferente (que denominaremos Nodo2 para diferenciar) cuya sincronización contra el Nodo1 quiere ser probada, el siguiente script Groovy:

    import com.liferay.portal.kernel.cache.MultiVMPool;
    import com.liferay.registry.RegistryUtil;
    import com.liferay.registry.Registry;
    import com.liferay.portal.kernel.cache.PortalCache;
    import com.liferay.registry.ServiceReference;
    import com.liferay.portal.kernel.cluster.ClusterExecutorUtil;
    import com.liferay.portal.kernel.cluster.ClusterMasterExecutorUtil;
    
    private MultiVMPool getMultiVMPool() {
        Registry registry = RegistryUtil.getRegistry();
    
        ServiceReference serviceReference = registry.getServiceReference(MultiVMPool.class);
    
        return registry.getService(serviceReference);
    }
    
    out.println("=== Execution in: " + ClusterExecutorUtil.getLocalClusterNode());
    out.println("=== isMaster: "+ ClusterMasterExecutorUtil.isMaster());
    
    MultiVMPool multiVMPool = getMultiVMPool();
    PortalCache portalCache = multiVMPool.getPortalCache("CACHE-TEST");
    
    out.println("=== Value for testKey: " + portalCache.get("testKey"));
    portalCache.put("testKey", "testValue2");
    out.println("=== Value for testKey: " + portalCache.get("testKey"));

    El script comprueba si hay algún valor cacheado en el Nodo2, en la caché CACHE-TEST, para la clave testKey. No debería ser así (de ahí que devuelva null) porque se invalidó cuando se puso el valor en el primer paso (Nodo1).

    A continuación el Nodo2 asigna un nuevo valor a testKey. Esto repetirá el proceso de sincronización del paso anterior, lo que significa que el espacio de caché del Nodo2 debería tener testValue2 como valor para testKey y todos los nodos restantes, incluido el Nodo1, deberían haber eliminado su valor anterior para testKey.
     
  3. Ejecutemos el siguiente script, de nuevo sobre Nodo1:

    import com.liferay.portal.kernel.cache.MultiVMPool;
    import com.liferay.registry.RegistryUtil;
    import com.liferay.registry.Registry;
    import com.liferay.portal.kernel.cache.PortalCache;
    import com.liferay.registry.ServiceReference;
    import com.liferay.portal.kernel.cluster.ClusterExecutorUtil;
    import com.liferay.portal.kernel.cluster.ClusterMasterExecutorUtil;
    
    private MultiVMPool getMultiVMPool() {
        Registry registry = RegistryUtil.getRegistry();
    
        ServiceReference serviceReference = registry.getServiceReference(MultiVMPool.class);
    
        return registry.getService(serviceReference);
    }
    
    out.println("=== Execution in: " + ClusterExecutorUtil.getLocalClusterNode());
    out.println("=== isMaster: "+ ClusterMasterExecutorUtil.isMaster());
    
    MultiVMPool multiVMPool = getMultiVMPool();
    PortalCache portalCache = multiVMPool.getPortalCache("CACHE-TEST");
    
    out.println("=== Value for testKey: " + portalCache.get("testKey"));

    Comprueba que efectivamente ha llegado una invalidación al Nodo1 provocando la eliminación del valor asociado a la clave testKey.


     

 

El (los) canal(es) de transporte es el que interviene en el proceso de sincronización de la caché. Cualquier problema no esperado relacionado con la sincronización del contenido de la caché no debería atribuirse al canal de control.

 

3. Jobs (Tareas) programados

Existen tres tipos de jobs programados que se encuentran definidos en StorageType:

  • MEMORY: Se ejecutan individualmente en cada uno de los nodos. Su estado reside en memoria, lo que implica que después de un reinicio la información sobre su fecha/hora de ejecución previa o su siguiente momento de ejecución se resetea.
  • MEMORY_CLUSTERED: Se ejecutan únicamente en el nodo maestro. Su estado reside en memoria, pero esta información es compartida por todos los nodos del clúster. Hablaremos de ello más adelante.
  • PERSISTED: Destinado a mantener su estado disponible después de cada reinicio del servidor. Es el único tipo que persiste su información en base de datos, en concreto en las tablas con prefijo persisted.scheduler.org.quartz.jobStore.tablePrefix.

 

Para gestionar el tiempo de ejecución, cuándo un job debería ser lanzado, Liferay se apoya en Quartz. Esta librería actúa como un planificador encargado de notificar a Liferay cuándo un job debe ser ejecutado. A continuación se ofrece una breve introducción acerca de cómo Liferay se integtra con Quartz y las características definitorias de cada uno de los tipos.

 

MEMORY

Los jobs de este tipo se ejecutan en cada nodo. Cada vez que un job de tipo MEMORY es desplegado Liferay utiliza Quartz para planificarlo. Quartz no es responsable de la ejecución, únicamente gestiona la planificación.

¿Qué significa que Quartz se encarga de la planificación? Cuando Quartz detecta que un job necesita ser ejecutado notifica a Liferay. Esto origina que se añada un mensaje al bus de Liferay con la información de ejecución del job y con el planificador como nombre de destino que ha de escucharse en el bus. El listener configurado para el job leerá los mensajes de dicho nombre de destino y se encargará de ejecutar la lógica del job.

La información sobre la planificación del job reside únicamente en memoria, de tal manera que el estado acerca de missfires anteriores o ejecuciones exitosas se perderá una vez se detenga el servidor. No existe para este tipo sincronización entre nodos.

 

MEMORY_CLUSTERED

Solamente un nodo se encarga de la ejecución de este tipo de jobs. ¿Cuál? El nodo maestro. Pero como distintos nodos pueden ser maestro con el paso del tiempo es necesario que haya una sincronización entre los disferentes nodos.

Se trata del tipo más utilizado. Son útiles para tareas de mantenimiento sobre un recurso común como es la base de datos. Dado que muchas tareas pueden ejecutarse independientemente del nodo no es necesario sobrecargar todos ellos con procesos repetitivos, de ahí el elegir un solo nodo.

Para garantizar esta coordinación Liferay mantiene en memoria la fotografía actualizada de los jobs que el maestro ha planificado en Quartz. En caso de que el nodo maestro se pare un nodo esclavo asumirá su papel y transferirá desde la memoria a Quartz los trabajos almacenados.

La coordinación se implementa en la siguiente clase: https://github.com/liferay/liferay-portal/blob/master/modules/apps/portal-scheduler/portal-scheduler-multiple/src/main/java/com/liferay/portal/scheduler/multiple/internal/ClusterSchedulerEngine.java

Este es el flujo que se sigue cada vez que un job de tipo MEMORY_CLUSTERED se despliega:

  • Si el nodo es esclavo: El esclavo envía un mensaje al nodo maestro para comprobar si el job ya ha sido desplegado en el maestro, y solo en caso caso almacena el job en memoria. Esto asegura que todos los nodos del clúster contengan la misma información en cualquier momento dado.
  • Si el nodo es el maestro: El job se planifica en el nodo maestro utilizando Quartz. A continuación dicho nodo se encarga de notificar a los esclavos con una solicitud de que guarden el job en memoria.

 

Para servir como soporte a toda esta lógica de comunicación se encuentra el canal de control. Todos los mensajes de sincronización de jobs se envían a través de este canal.

 

PERSISTED

El estado de los jobs o la fecha/hora de ejecución de cada tipo (MEMORY, MEMORY_CLUSTERED y PERSISTED) es gestionado por Quartz. Esto es común para los tres tipos. Pero en el caso de los jobs de tipo PERSISTED la coordinación también se delega en Quartz. Y para ello Quartz utiliza la base de datos para almacenar esta información, concretamente en las tablas QUARTZ_*.

De esta manera Quartz no sólo decide para este tipo cuándo se debe ejecutar un job, sino también en qué nodo se ejecutará. No importa, además, si en el momento de la ejecución todos los nodos están parados puesto que la información sobre cuándo lanzar el siguiente job se mantendrá en la base de datos y se encontrará disponible para el siguiente nodo que se levante.

Out of the Box Liferay no proporciona ningún job PERSISTED, pero podrían ser útiles dependiendo de la importación del job y de si es necesario asegurar su ejecución, llegando a ejecutar los jobs pendientes tan pronto como un nodo se levante sin importar si todos los nodos estaban parados a la hora de ejecución prevista.

 

A modo de breve resumen, esta es la responsabilidad de cada uno de los nodos dependiendo de su rol:

  • Maestro:
    • Ejecuta, siempre, jobs de tipo MEMORY.
    • Ejecuta, siempre, jobs de tipo MEMORY_CLUSTERED.
    • Ejecuta jobs de tipo PERSISTED cuando Quartz lo dictamina (la división maestro/esclavo es totalmente transparente para Quartz).
  • Esclavo:
    • Ejecuta, siempre, jobs de tipo MEMORY.
    • Nunca ejecuta jobs de tipo MEMORY_CLUSTERED, pero los almacena en memoria.
    • Ejecuta jobs de tipo PERSISTED cuando Quartz lo dictamina.

 

 

4. Comienza la acción: Arranque del clúster

Antes de profundizar en el arranque es necesario recordar la siguiente recomendación: Cuando se necesita arrancar varios nodos el arranque debe realizarse de forma secuencial, no en paralelo, para evitar problemas de concurrencia.

En lo que respecta al arranque nos encontraremos con varios hitos donde se mostrará información relevante relativa a la creación del clúster:

 

4.1 Creación de canales

Cuando se inicia un nodo, la creación de un canal de JGroups es el primer evento relevante del clúster al que prestar atención. Al menos un par de canales se crean por defecto:

 

  • Canal de Control:
    [JGroupsClusterChannel:112] Create a new JGroups channel {channelName: liferay-channel-control, localAddress: malvaro-ThinkPad-T450s-15069, properties: UDP(time_service_interval=500;thread_pool_max_threads=100;mcast_group_addr=239.255.0.1
  • Canal de transporte-0:
    [JGroupsClusterChannel:112] Create a new JGroups channel {channelName: liferay-channel-transport-0, localAddress: malvaro-ThinkPad-T450s-28377, properties: UDP(time_service_interval=500;thread_pool_max_threads=100;mcast_group_addr=239.255.0.2

 

Cada canal tiene, por defecto, su propia configuración.

Según arranca el nodo y se crean ambos canales se puede considerar que el nodo pertenece a un clúster de Liferay. Inicialmente, cuando solo uno está arrancado, solo habrá un nodo en el clúster, pero cuando uno nuevo, que comparta la misma configuración de clúster, se inicie, se unirá a cada canal y pasará a formar parte del mismo clúster de Liferay.

Cada evento de unión/abandono del clúster puede visualizarse escaneando los logs en busca de una traza como la siguiente:

 

[JGroupsReceiver:91] Accepted view [node2-23516|1] (2) [node2-23516, node1-18511]

 

En la lista separada por comas se observarán todos y cada uno de los nodos que forman parte del clúster concreto. El orden en que aparece es relevante dado que Liferay, por convención, considera el primer nodo como el nodo maestro.

 

4.2 Jobs schedule

Una vez los canales se han creado, y a medida que los nodos se van uniendo, cada nodo conocerá su rol (maestro/esclavo) dentro del clúster de Liferay.

El nodo maestro se encargará de planificar en Quartz todos los jobs desplegados de tipo MEMORY_CLUSTERED. En ese momento cada job se vuelve elegible para ser ejecutado tan pronto como su instante de ejecución llegue.

De esta forma, cada vez que un nuevo nodo pase a formar parte del clúster como esclavo sucederá lo siguiente:

  • Cada job de tipo MEMORY que haya sido desplegado en dicho nodo se planificará en Quartz.
  • La gestión de tareas de tipo PERSISTED se delegará en Quartz.
  • El esclavo solicitará al maestro su lista de jobs de tipo MEMORY_CLUSTERED para almacenar una copia de ellos en memoria.

Para validar la correcta comunicación relativa al último punto existe incluso una traza:

[ClusterSchedulerEngine:607] Load 16 memory clustered jobs from master

 

Llegados a este punto, en lo que respecta a los jobs de tipo MEMORY_CLUSTERED, deberían estar sincronizados. ¿Qué puede ocurrir a continuación? Un nuevo job puede ser desplegado (hay que asegurarse de que el desarrollo se despliega siempre en todos los nodos).

Al desplegar un nuevo job de tipo MEMORY_CLUSTERED se inicia un nuevo proceso de sincronización que afecta a todos los nodos. Para forzarlo el nodo maestro utiliza el canal de control enviando un mensaje en común a cada nodo solicitándoles que añadan el nuevo job a la memoria. El objetivo es asegurar que en cualquier momento el nodo maestro esté ejecutando todos los jobs de tipo MEMORY_CLUSTERED y que los nodos esclavos tengan esos jobs almacenados en memoria, estando preparados para planificarlos en el momento en que se erijan nodo maestro.

Repentinamente el nodo maestro podría detener su ejecución (ya sea después de una caída o después de una parada programada). En tal caso JGroups detectará que un nodo ha abandonado el clúster y notificará al listener de Liferay. Esto ocasionará un nuevo evento en Liferay que implicará la selección del primer nodo de la lista como nuevo maestro (aka coordinador). Después de ello:

  • El nuevo nodo maestro procederá a planificar en Quartz todos los jobs de tipo MEMORY_CLUSTERED que se encontraban almacenados en memoria, y posteriormente notificará a los nodos restantes que es necesaria una nueva sincronización.

  • Los nodos esclavos atenderán la notificación almacenando en memoria todos los jobs recibidos de parte del maestro.

 

 

5. El clúster ya ha arrancado: Detectando problemas con los jobs programados

Una vez el clúster ha arrancado pueden surgir diversos problemas con los jobs programados.

Esta sección cubre alguno de los mensajes de error/warning más comunes que se pueden dar en los logs de Liferay y cómo interpretarlos. También se proporcionará un script para chequear qué jobs se encuentran actualmente planificados.

Se asume, llegados a este punto, que la configuración del clúster es válida (ya sea por TCP o UDP) y que se trata de un clúster que ha funcionado en, al menos, una ocasión:

 

5.1 Log common traces analysis

 

5.1.1 Errores comunes que requieren de una atención inmediata

  • [ERROR] "Unable to load memory clustered jobs from master in XX seconds, you might need to increase value set to "clusterable.advice.call.master.timeout", will retry again":  Si no se puede obtener una sincronización completa entre el maestro y el esclavo se obtendrá este error.

    Dado que es un paso crucial con afectación a todos los jobs de tipo MEMORY_CLUSTERED, existe un mecanismo que reintenta la sincronización hasta lograrla. El reintento se lleva a cabo cada  clusterable.advice.call.master.timeout segundos, por lo que el mensaje se imprimirá, espaciado en el tiempo, mientras siga fallando.

    Si se detiene el nodo maestro mientras el reintento está en curso, se perderán todos los jobs MEMORY_CLUSTERED. Esto ocurre porque mientras sigue fallando no se guarda ningún registro en la memoria, por lo que no se puede planificar ningún registro posteriormente. En las siguientes secciones se explicará cómo recuperarse de las consecuencias de esta situación.

     

 

  • [ERROR] "Unable to get a response from master for memory clustered job XXXXXX": Cada vez    que un nuevo job de tipo MEMORY_CLUSTERED se despliega en un esclavo este envía un mensaje al nodo maestro para llevar a cabo la sincronización. Si durante este proceso hay algún error en la comunicación se mostrará este mensaje.

    Desafortunadamente para este caso no hay ningún mecanismo de reintento, por lo que en un momento dado podría darse el caso de que si no llega a producir esta sincronización el nodo esclavo afectado y el nodo maestro podrían acabar con diferentes configuraciones. En las siguientes secciones se ofrecerá una explicación sobre cómo forzar esta sincronización.

 

  • [WARN] "Property scheduler.enabled is disabled in the master node. To ensure consistent behavior, this property must have the same value in all cluster nodes. If scheduler needs to be enabled, please stop all nodes and restart them in an ordered way.": Se muestra cuando el nodo maestro ha sido iniciado con esta propiedad deshabilitada y se une al clúster un nodo esclavo con dicha propiedad habilitada.

    No hay forma de asegurar que todos los jobs de tipo MEMORY_CLUSTERED se ejecuten siempre si al menos uno de los nodos tiene esta propiedad deshabilitada. Esto significa que no existe la posibilidad de forzar que los jobs se ejecuten en un nodo concreto solamente.

    Para habilitar de nuevo la propiedad todos los nodos deben pararse y posteriormente realizar el cambio, porque de lo contrario podría haber información inconsistente en memoria.

 

  • [ERROR] "Unable to notify slave": Esta traza surge cuando el nodo maestro no puede notificar a cada esclavo de que un nuevo job ha sido planificado. En las siguientes secciones se ofrecerá una explicación acerca de cómo recuperarse de las consecuencias de esta situación.

 

5.1.2 Mensajes que al menos deberían ser analizados

  • [INFO] "Memory clustered job XXXXXX is not yet deployed on master": Se da en caso de que un nuevo job de tipo MEMORY_CLUSTERED se despliegue en un esclavo antes de ser desplegado en el maestro.

    No debería ser algo preocupante. Normalmente ocurre cuando un nuevo desarrollo se despliega por primera vez en todos los nodos simultáneamente. En caso de que sea el esclavo el que lo procese primero aparecería el mensaje, pero puede ser ignorado si tenemos evidencia de que después se acabará desplegando y procesando por el maestro.

    En cualquier caso siempre es recomendable verificar el proceso de despliegue. Cada desarrollo debe ser desplegado en todos y cada uno de los nodos.

 

  • [INFO] "Load XX memory clustered jobs from master": Indicativo del número de jobs de tipo MEMORY_CLUSTERED que se han recuperado del maestro y almacenado en memoria.

    Un número menor que el esperado implicaría que algunos jobs se han perdido en el maestro. En las siguientes secciones se ofrecerá una explicación acerca de cómo recuperarse de las consecuencias de esta situación.

 

  • [INFO] "XX MEMORY_CLUSTERED jobs started running on this node": Si el nodo actual se erige maestro el número de jobs que pasa a planificar se muestran en los logs. Si el número es menor que el esperado en las próximas secciones veremos cómo rectificarlo.

                           

  • [INFO] "MEMORY_CLUSTERED jobs stopped running on this node": Se imprime en caso de que el nodo actual deje de ser maestro. Si esto ocurre sin que haya habido una parada de algún nodo se trataría de un síntoma de un clúster partido (generando un maestro por subclúster) y unido de nuevo.

 

  • [INFO] "Accepted view MergeView": con el siguiente formato:

    [BaseReceiver:83] Accepted view MergeView::[node1-38143|3] [node1-38143, node2-42091], subgroups=[node2-42091|2] [node1-42091], [node2-38143|0] [node1-38143]

    Esta traza no es un fallo en sí misma, pero puede suponer un aviso sobre una situación previa alarmante. Signfica que el clúster se partió previamente en, al menos, un par de subgrupos y se ha vuelto a unir de nuevo. Durante el tiempo que estuvo partido se pueden generar diferencias entre el contenido de los nodos (especialmente en las cachés de tipo MultiVM) dado que la comunicación en los canales estaba rota.

    Normalmente esta traza es consecuencia de problemas de red (paquetes que se pierden) o de sobrecarga de nodos que hace que al menos uno de ellos no pueda responder a tiempo a los mensajes de heartbeat. Esto ocasiona, desde la perspectiva de JGroups, que uno de los nodos pase a ser considerado sospechoso de haber abandonado el clúster.

 

5.1.3 Mensajes que pueden ser ignorados

  • [INFO] "Receive notification from master, add memory clustered job XXXXXX": Información no preocupante sobre el hecho de que un job de tipo MEMORY_CLUSTERED ha sido desplegado en el maestro y el esclavo acaba de recibir la notificación.

 

  • [INFO] "Skip scheduling memory clustered job XXXXXX with a null trigger. It may have been unscheduled or already finished.": Información no relevante sobre un job desplanificado que, por tanto, no se va a almacenar en memoria.

 

  • [INFO] "Receive notification from master, reload memory clustered jobs": Cada vez que se erige un nuevo maestro el resto de nodos esclavo recibirá una notifcación (expresada en esta traza) para sincronizar los jobs que tenga en memoria.

 

5.2 Scripts de Groovy para diagnosticar el clúster

La salud del clúster puede ser chequeada en cualquier momento no solo mediante el análisis de las trazas, pero también mediante diversos scripts de Groovy. A continuación se muestran un par de ellos:

  • SchedulerDebugInformation.groovy: Puede ser ejecutando tanto desde el maestro como desde cualquier nodo esclavo.

    Debe devolver el mismo número de jobs de tipo MEMORY_CLUSTERED independientemente del nodo en el que se ejecute. Si no es así sería indicativo de que hay un problema de sincronización entre nodos. En las siguientes secciones se ofrecerá una explicación sobre cómo recuperarse de esta situación.

    • Si se ejecuta en el maestro mostrará todos los jobs planificados en Quartz.
    • Si se ejecuta en el esclavo mostrará todos los jobs de tipo MEMORY_CLUSTERED que se encuentran almacenados en memoria.

 

  • ClusterNodesDebugInformation.groovy: Puede ejecutarse tanto en el nodo maestro como en cualquier esclavo.

    Mostrará la información, relativa al clúster, del nodo sobre el que se ejecute. También mostrará todos los nodos que conforman el clúster desde las perspectiva del nodo actual. Estos nodos deben ser los mismos independientemente del nodo sobre el que se ejecute. Lo contrario implicaría que el clúster se encontraría partido por problemas de comunicación.

 

 

6. Jobs are missing. Now what?

La pérdida de jobs de tipo MEMORY_CLUSTERED no es una situación sencilla de reproducir porque normalmente es el resultado de un proceso consistente en dos pasos:

  1. El esclavo no sincroniza correctamente los jobs planificados en el maestro. Normalmente esto genera un fallo silencioso puesto que los jobs no se planifican en el esclavo y, por tanto, solo viven en memoria a modo de backup.
  2. El nodo maestro se detiene y el esclavo pasa a ser maestro. Los jobs que residían en memoria pasan a ser planificados en Quartz con lo que el error se vuelve visible en forma de ejecuciones de jobs que no se producen.

 

Para detectar esta situación al menos un par de alternativas pueden seguirse:

  • Revisar la ejecución de los jobs desde un punto de vista lógico: Podría ser que el job imprimiera trazas, o que tuviera que limpiar una tabla y no lo esté realizando, etc.
  • Utilizar alguno de los scripts previos de diagnóstico.

 

Si después del análisis se llega a la conclusón de que efectivamente se han perdido jobs se pueden seguir los siguientes pasos para solucionarlo:

  • Reiniciar el componente utilizando la consola Gogo: Si la consola Gogo es accesible y el job se ha definido como componente (como ejemplo CheckAssetEntryMessageListener.java) la manera más rápida y sencilla es reiniciar el componente en el nodo maestro. Esto genera que el job se registre de nuevo.

    Una vez que se detecte el job que falta el reinicio puede realizarse utilizando los siguientes comandos:

    scr:disable com.liferay.asset.publisher.web.internal.messaging.CheckAssetEntryMessageListener
    scr:enable com.liferay.asset.publisher.web.internal.messaging.CheckAssetEntryMessageListener 

 

  • Ejecutar el siguiente script de Groovy: Como último recurso, cuando múltiples jobs se pierden o cuando alguno de ellos ha sido migrado desde una versión de Liferay previa (y no sigue el patrón componente) puede ser de utilidad ejecutar el siguiente script en cada uno de los nodos (el propio script detectará si el nodo tiene el rol de maestro y solo en ese caso ejecutará su lógica).

    El script, que se encuentra en SchedulerJobsManager.groovy, se encargará de reconstruir el serviceTracker de SchedulerEventMessageListener replanificando todos los jobs.

 

 

 

* Me gustaría disculparme por la terminología maestro-esclavo, pero con el fin de hacer la explicación lo más sencilla y familiar posible, he recurrido a esta. Cualquier sugerencia más apropiada es bienvenida.

** Me gustaría agradecer a Álvaro Saugar López su apoyo y ayuda con la traducción de este blog al español.

 

Blogs

¡Muchas gracias por el artículo!

Me ha parecido muy instructivo.

En cuanto a sugerencias sobre alternativas para el uso de los términos "maestro" y "esclavo", tengo visto usar "primario" (o "principal") y "secundario".