Quantcast
Channel: Ari Handler Gamboa – adictosaltrabajo
Viewing all 16 articles
Browse latest View live

Visualización de rendimiento del Garbage Collector

$
0
0

En este tutorial se presentan una serie de ejemplos que demuestran el comportamiento del recolector de basura o Garbage Collector (GC) de Java en determinados escenarios.

0. Índice de contenidos

1. Introducción

En este tutorial se presentan una serie de ejemplos que demuestran el comportamiento del recolector de basura o Garbage Collector (GC) de Java en determinados escenarios que fuerzan al GC a limpiar el Heap de ejecución.

Además de los trozos de código sobre los que se ha trabajado, se invluyen capturas de pantalla de las salidas gráficas de las herramientas especializadas utilizadas durante el tutorial, como VisualVM con el plug-in VisualGC.

2. Entorno de desarrollo

  • MacBook Pro Intel Core 2 Duo 8GB RAM
  • SO: Yosemite
  • IDE: Eclipse Mars 4.5.0
  • Java JDK 1.7

Al IDE Eclipse se le ha instalado el plugin de VisualVM que permite la representación en tiempo real de espacio de memoria de la JVM, uso de procesador, carga del GC, etc. Conjunto a VisualVM, se ha añadido el plugin VisualGC que añade una serie de visualizaciones del recolector: espacio utilizado en Eden, en Survivor y en PermGen, entre otros.

Para más información sobre VisualVM, consúltese la página oficial.

3. Llamada explícita al GC

La forma más simple de forzar al colector a limpiar memoria es desreferenciando (haciendo apuntar a null) un objeto creado. Se ha sobreescrito el método Object#finalize para mostrar por consola que, efectivamente, se limpia cuando no hay referencias al objeto.

Es conveniente indicar que la llamada a Runtime#gc no garantiza al 100% que el GC haga una pasada de limpieza; es sólo una sugerencia del usuario a la JVM.

package es.autentia.gc_samples;

public class GCExplicito {

    public static void main(String[] args) {
        A a = new A("blanco");
        
        a = null;

        Runtime.getRuntime().gc();
    }

}

class A {
    private String color;

    public A(String color) {
        this.color = color;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(color + " eliminado");
    }
}

La salida que obtendremos por consola nos indicará que el objeto ha sido reciclado:

blanco eliminado

4. Referencias inalcanzables

Otro de los casos en los que el GC realiza una limpia del Heap es cuando existe una referencia circular entre objetos y no son alcanzables desde fuera.

En el siguiente caso se declaran tres objetos apuntándose entre ellos (mediante un campo de clase):

package es.autentia.gc_samples;

public class GCAmbito {

    GCAmbito t;
    int i;
    
    public GCAmbito(int i) {
        this.i = i;
    }
    
    public static void main(String[] args) {

        GCAmbito t1 = new GCAmbito(1);
        GCAmbito t2 = new GCAmbito(2);
        GCAmbito t3 = new GCAmbito(3);
        // Ningun objeto se debe reciclar
        
        t1.t = t2;
        t2.t = t3;
        t3.t = t1;
        // Ningun objeto se debe reciclar
        
        t1 = null;
        System.out.println("t1 = null");
        Runtime.getRuntime().gc();
        // No se debe reciclar: t3.t apunta aun a t1
        
        t2 = null;
        System.out.println("t2 = null");
        Runtime.getRuntime().gc();
        // No se debe reciclar: t3.t.t apunta aun a t2
        
        t3 = null;
        System.out.println("t3 = null");
        Runtime.getRuntime().gc();
        // Deben reciclarse los tres objetos: cada uno de ellos
        // apunta a otro de manera circular, sin ser referenciados
        // desde fuera
        
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Finalizado objeto " + i);
    }
}

Como se desprende de los comentarios en código, cada uno de los objetos son desreferenciados en orden, pero no son liberados por el GC hasta que la referencia circular es inalcanzable (t3 es desreferenciado).

La salida por consola nos demuestra éste comportamiento (nótese que el orden de reciclaje no es determinista y depende de muchos factores como la carga actual de la JVM):

t1 = null
t2 = null
t3 = null
Finalizado objeto 1
Finalizado objeto 3
Finalizado objeto 2

5. Estructura del Heap de Java – Hotspot Heap

Estructura Hotspot VM

Conviene recordar cómo se produce el envejecimiento de los objetos en Java mediante la denominada “Weak Generational Hypothesis”. Para más información sobre el funcionamiento y los tipos de GC de Java conviene echarle un vistazo al tutorial sobre Garbage Collector de Jose Luis Rodríguez.

En el tipo de GC por defecto de la JVM (Serial GC), la política de limpieza y envejecimiento sigue estos pasos:

  • Cualquier objeto nuevo es colocado en Eden.
  • Cuando el Eden se llena, se realiza un reciclaje menor (minor garbage collection).
  • Los objetos con referencias se mueven al primer espacio de supervivientes (S0). El resto serán eliminados en la siguiente limpieza de Eden.
  • En el siguiente reciclaje menor, Eden sufre el mismo proceso antes descrito. Los objetos no referenciados son eliminados de Eden; sin embargo, los objetos viejos tanto de Eden como de S0 son movidos al segundo espacio de supervivientes (S1). Una vez terminada la copia, tanto Eden como S0 son limpiados.
  • En el siguiente reciclaje menor, los espacios de supervivientes (S0 y S1) se intercambian los roles y vuelven a repetir el proceso.
  • Cuando algún objeto de cualquier de los espacios de supervivientes alcanza un determinado tiempo de vida, es promocionado a la Old Generation.
  • Eventualmente, se producirá un reciclaje amplio (major garbage collection) que limpiará y compactará este espacio de la Old Generation.

Con este proceso en mente, el siguiente ejemplo ilustra cómo se produce este llenado y consecuente vaciado de ambas generaciones del Heap:

package es.autentia.gc_samples;

import java.util.ArrayList;

public class GCDesreferencia {

    public static void main(String[] args) {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        ArrayList arrayStrings = new ArrayList();

        for (int j = 0; j < 100000000; j++) {
            for (int i = 0; i < 1000000; i++) {
                try {
                    Thread.sleep(1);

                    String s = i + j + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce"
                        + " viverra semper ipsum, in mollis sem pellentesque eu. Donec ferment"
                        + "um erat sed mattis facilisis. Praesent porttitor purus at libero ti"
                        + "ncidunt laoreet. Nam posuere dolor tellus, vel laoreet sem rutrum eu"
                        + ". Cras posuere dui sit amet lectus pellentesque accumsan. Nulla auctor cu"
                        + "rsus dolor a efficitur. Cras auctor ultrices fringilla. Mauris pretium ris"
                        + "us orci, vitae scelerisque lorem blandit at. Nunc semper pulvinar facilisis."
                        + " Etiam bibendum blandit velit. Mauris commodo sapien in velit pretium"
                        + ", nec varius nulla porttitor. Aliquam congue facilisis convallis. Pel"
                        + "lentesque enim orci, sagittis sed cursus porta, dictum eget massa. ";

                    arrayStrings.add(s);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }
}

A modo de resumen, la aplicación espera unos 10 segundos antes de comenzar la funcionalidad para dar tiempo a lanzar VisualVM empezar a monitorizar el GC. Dentro de los bucles se ha añadido la creación de una serie de Strings lo suficientemente grandes como para que el GC tenga que saltar en un tiempo de simulación razonable. Un pequeño retardo de 1 ms entre cada creación permite un espaciado temporal mayor y aumenta la legibilidad de las gráficas generadas por VisualGc que veremos más adelante.

La creación de un array que vaya almacenando los String generados permite que los objetos sean movidos a la Old Gen cuando se cumpla el criterio de envejecimiento.

Entre los muchos flags que podemos añadir a la ejecución del programa para visualizar el trabajo del GC, los básicos son los siguientes:

  • -XX:+PrintGCDetails: Obtenemos mensajes cada vez que actúa el GC.
  • -Xloggc:<fichero.log>: Redirige el resultado de los mensajes del GC a un fichero externo en vez de por salida estándar.

Si nos fijamos en el log generado, podemos diferenciar dos partes:

[...]

0.822: [GC [PSYoungGen: 38912K->5104K(72704K)] 66872K->63644K(159744K), 0.1651680 secs] [Times: user=0.20 sys=0.03, real=0.16 secs]
0.988: [Full GC [PSYoungGen: 5104K->0K(72704K)] [ParOldGen: 58540K->63275K(151040K)] 63644K->63275K(223744K) [PSPermGen: 2631K->2631K(21504K)], 1.4727490 secs] [Times: user=2.58 sys=0.01, real=1.47 secs]
3.172: [GC [PSYoungGen: 67584K->5120K(72704K)] 130859K->119483K(223744K), 0.2449040 secs] [Times: user=0.29 sys=0.05, real=0.24 secs]
3.418: [Full GC [PSYoungGen: 5120K->0K(72704K)] [ParOldGen: 114363K->116723K(265728K)] 119483K->116723K(338432K) [PSPermGen: 2631K->2631K(21504K)], 1.3292800 secs] [Times: user=2.42 sys=0.01, real=1.33 secs]
5.192: [GC [PSYoungGen: 67584K->5120K(91136K)] 184307K->176627K(356864K), 0.2886140 secs] [Times: user=0.31 sys=0.04, real=0.28 secs]

[...]

La primera parte detalla el estado de ocupación del Heap de forma periódica cada vez que actúa el GC, indicando los tres espacios en KB: la Young, la Old y la Permanent Generation, así como los tiempos en los que se han tomado las medidas. El indicador GC se imprime cuando se realiza un minor collection y Full GC cuando es un major collection.

[...]

Heap
 PSYoungGen      total 238592K, used 194302K [0x00000007d5500000, 0x00000007f3e00000, 0x0000000800000000)
  eden space 97280K, 54% used [0x00000007d5500000,0x00000007d88bf808,0x00000007db400000)
  from space 141312K, 100% used [0x00000007e6400000,0x00000007eee00000,0x00000007eee00000)
  to   space 180224K, 0% used [0x00000007db400000,0x00000007db400000,0x00000007e6400000)
 ParOldGen       total 450560K, used 352260K [0x0000000780000000, 0x000000079b800000, 0x00000007d5500000)
  object space 450560K, 78% used [0x0000000780000000,0x0000000795801030,0x000000079b800000)
 PSPermGen       total 21504K, used 2637K [0x000000077ae00000, 0x000000077c300000, 0x0000000780000000)
  object space 21504K, 12% used [0x000000077ae00000,0x000000077b093790,0x000000077c300000)

Al final del log se detalla el nivel de ocupación del Heap de manera estratificada al finalizar la ejecución. En la Young Generation o PSYoungGen, from space se refiere a S0 y to space a S1.

6. Tipos de GC y su rendimiento comparado

Mediante las herramientas VisualVM con el plugin VisualGC, podemos realizar una pequeña comparación de rendimiento y comportamiento de los diferentes tipos de GC al ejecutar el ejemplo anterior.

6.1. GC Serie

Flag de ejecución: -XX:+UseSerialGC

GC Serie

Este primer ejemplo usa el recolector en serie, que sigue el proceso descrito en el apartado del Hotspot de Java. En la zona resaltada de las gráficas se puede apreciar cómo cada vez que alguna de las zonas de supervivientes son vaciadas, o bien pasa a la siguiente zona, o, si se cumplen los criterios de envejecimiento, se copian a la Old Gen.

6.2. GC en Paralelo con 2 hilos

Flags de ejecución: -XX:+UseParallelGC -XX:ParallelGCThreads=2

GC Paralelo

En el caso del recolector en paralelo, aunque las pasadas del GC sigan siendo Stop-the-world, al aprovechar el uso de varias CPUs, los minor collections pueden ser mucho más frecuentes, resultando en tamaños de Eden y Survivor (Young Generation) sensiblemente mayores que el GC en serie. Como los minor collections son más frecuentes, es necesario un major collection antes que con el método anterior.

6.3. GC con Concurrent Mark Sweep (GCCMS)

Flags de ejecución: -XX:+UseConcMarkSweepGC

GC Concurrent Mark Sweep

Como los minor collection pueden ser intercalados entre los major no se producen incrementos en el tamaño de los espacios de Survivor. Esta concurrencia permite unos major collections mucho más frecuentes (4 limpiezas vs. 0-1 de los métodos anteriores).

6.4. Garbage First GC (GCG1)

Flags de ejecución: -XX:+UseG1GC

GC Garbage First

Ya que Garbage First GC (G1) mantiene siempre una serie de áreas del Heap libres y controladas, las pausas para reciclaje serán menores y se evitará por completo el llenado de cualquiera de las generaciones. Este fenómeno se hace evidente en las gráficas sacadas por VisualVM, en el que vemos que ni el espacio de Survivor (con G1 solo existe un espacio de este tipo) ni el de la Old Generation son limpiados en ningún momento.

7. Conclusión

Herramientas como los flags de ejecución de la JVM, como -verbosegc correspondientes al recolector de basura en conjunto con plugins de visualización de datos, como VisualVM + VisualGC, permiten recabar información sobre el rendimiento del GC en nuestras aplicaciones.

La elección del mejor tipo de GC para un determinado problema es una tarea compleja que debe ser sopesada tanto en términos de uso de memoria, como de frecuencia de dicho uso.


Monitorización y profiling con Java Mission Control y Java Flight Recorder

$
0
0

En este tutorial presentaremos un pequeño viaje por las funcionalidades más básicas de Java Mission Control y Java Flight Recorder como herramientas de monitorización y profiling de aplicaciones en Java

0. Índice de contenidos


1. Introducción

Java Mission Control (JCM a partir de ahora) es una herramienta de monitorización y gestión de aplicaciones Java propia de Oracle que minimiza el overhead habitual entre las herramientas de este tipo (menos del 1% del total de procesamiento).

Este bajo impacto en el rendimiento de las aplicaciones bajo monitorización se consigue gracias a que se alimenta de los datos que se generan de forma natural durante las optimizaciones dinámicas de la JVM. Aparte de minimizar este overhead de monitorización, este sistema de recolección de datos elimina el sesgo del observador, que se produce cuando las herramientas de este tipo alteran las características de ejecución de un sistema.

JMC está compuesto por la propia aplicación gráfica (JMC client) y una serie de plugins que se montan encima de la misma:

  • JVM Browser: Muestra las aplicaciones Java que se están ejecutando con sus respectivas JVMs. Cada una de las instancias de JVM se les denomina JVM Connection. Estas conexiones pueden ser locales por PID, o bien remotas por dirección de host.
  • JMX Console: Se conecta a una JVM en funcionamiento y recopila y presenta sus características en tiempo real, además de permitir la modificación de algunas de ellas en caliente gracias al uso de las Managed Beans. Desde esta consola es posible incluso definir reglas que se ejecuten cuando se cumpla cierta condición (por ejemplo, mandar un correo electrónico cuando el uso de CPU exceda el 80% durante más de 3 segundos.
  • Java Flight Recorder (JFR): Recopila y guarda información detallada del rendimiento de aplicaciones para un análisis futuro. Puede ser utilizado como una herramienta de monitorización de rendimiento independiente de JMC, pero cuando se usa en conjunción con esta presenta la información capturada de forma fácil en tablas, gráficas, etc.

Aunque pueda parecer “una herramienta más” de monitorización de JVM sumada a la ya extensa colección de dichas herramientas (jstat, jmx, VisualVM, HPJmeter, etc…), lo cierto es que resulta en la agrupación de las cosas buenas de cada una de ellas de forma clara y sencilla, con una interfaz gráfica muy amigable y con una capacidad de personalización tremenda.

Para simular carga de la máquina y obtener resultados más o menos “reales” se ha utilizado la suite de benchmarking del proyecto Opensource DaCapo Benchmark Suite.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro Intel Core 2 Duo 8GB RAM.
  • Sistema Operativo: Mac OS Yosemite 10.10.3
  • Entorno de desarrollo: Eclipse Mars 4.5.0
  • Java JDK 1.7
  • DaCapo Benchmark Suite 9.12

3. Ejecución de JCM y JFR

Ya que JMC viene dentro del JDK de Oracle desde la versión 7u40, podemos ejecutarlo directamente desde la terminal (si tenemos añadido al $PATH el directorio del JDK):

$ jmc &

Si queremos hacer uso de JFR para el profiling de aplicaciones, es necesario activar las siguientes dos opciones de la JVM:

-XX:+UnlockCommercialFeatures -XX:+FlightRecorder

Hay que tener que la JVM, por defecto, solo permite obtener trazas de ejecución en puntos seguros, por lo que podría darse el caso de que dichas trazas no sean correctas en ciertos casos. Es por ello que la documentación de JFR recomienda el uso de dos parámetros adicionales de la JVM, que no pueden ser modificados en caliente:

-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints

Es también posible arrancar directamente JFR al ejecutar la aplicación que queremos monitorizar desde la línea de comandos, lo que permite ahorrarnos técnicas para parar la ejecución hasta que tengamos toda la suite de monitorización lista (dormir el hilo principal de ejecución durante X segundos o realizar varias pasadas de calentamiento):

java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=record-miapp.jfr MiApp

Si lo que queremos es monitorizar procesos en máquinas remotas, será necesario añadir los siguientes flags (que son específicos de JMX):

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false


4. Consola JMX

Si hacemos click derecho sobre una de las conexiones en el JVM Browser podremos iniciar una nueva sesión de JMX Console:

Veamos cada una de las pestañas disponibles más interesantes án esta vista.


4.1. Overview

En esta pestaña se nos muestra un pequeño resumen de las métricas más comunes (uso de memoria y porcentaje de CPU). Nada especial aquí, sin embargo podremos añadir las métricas que JMX tenga disponible haciendo click en los signos de cada uno de los paneles.


4.2. MBean Browser

Desde aquí podremos acceder y visualizar todos los MBeans disponibles que nos proporciona JMX, con sus valores actuales y con la capacidad de poder añadirlos a las gr´ficas presentadas en la pantalla Overview.

Una funcionalidad que destaca de este panel es la capacidad de modificar ciertos parámetros de los MBeans en caliente haciendo doble click sobre los atributos con fondo amarillo (de esta manera JMC marca los que son modificables).

Al seleccionar un MBean, tendremos acceso a una serie de pestañas que nos permite ver información sobre sus atributos, operaciones (como llamar al garbage collector en el MBean “Memory”), notificaciones declaradas dado un comportamiento (lo veremos en el apartado de “Triggers”) y los metadatos del MBean.


4.3. Triggers

Una de las funcionalidades que, desde mi punto de vista, son más interesantes de la JMX Console de JMC es la capacidad de definir notificaciones de tal manera que cuando se cumpla una determinada condición sobre un MBean (o varios), se ejecute una determinada acción, entre las cuales se encuentran:

  • Popup de alerta.
  • Loggearlo en un archivo.
  • Enviar un correo electrónico con la información del evento.
  • Lanzar una sesión de Flight Recorder, tanto continua como limitada.
Además, en cada Trigger podremos definir entre qué intervalo de tiempos estará activa dicha condición, de tal manera que, por ejemplo, solo salte en días entre semana.

4.4. Threads

Desde aquí podremos ver todos los hilos de ejecución de nuestra aplicación, con información sobre su estado actual (ejecutándose, esperando, etc…), además de si se encuentran en una situación de deadlock (para lo cual hay que activar la detección de deadlocks en el checkbox superior), así como el total de memoria reservada para su contexto (que también deberemos de activar en la casilla “Allocation”).

Al seleccionar cualquiera de los hilos, podemos acceder a sus trazas de ejecución en el panel inferior, funcionalidad típica en herramientas de monitorización. Esta selección puede ser múltiple.


5. Java Flight Recorder

Como hemos visto en el apartado sobre ejecución de JMC y JFR es necesario añadir una serie de flags a la JVM de la aplicación que estemos monitorizando para que JFR pueda extraer estadísticas (si no lo hacemos, JMC nos avisará y podremos copiar estas opciones directamente del cuerpo de la ventana de error), recordémoslas:

-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
-XX:+UnlockDiagnosticVMOptions
-XX:+DebugNonSafepoints

Una vez ejecutada la aplicación, nos vamos a JMC y hacemos click derecho sobre la conexión correspondiente y seleccionamos “Start Flight Recording…”, lo que nos abrirá un wizard de configuración del JRF:

En este primer paso podremos dar un nombre al registro, así como elegir entre la recopilación en un determinado tiempo, que pueden ser segundos (unidad “s”), minutos (unidad “m”), horas (unidad “h”), días (unidad “d”) o semanas (unidad “w”). Una vez terminado este lapso de tiempo, el cliente de JMC se encargará automáticamente de guardar y abrir dicho registro.

La otra opción es hacer una recopilación continua (de la cual podremos definir un tamaño o tiempos máximos) y que no será guardado hasta que paremos la grabación. Este tipo de registro es necesario abrirlo explícitamente.

En el siguiente paso podremos personalizar una serie de preferencias sobre el muestreo de estadísticas, entre las que nos encontramos el comportamiento del garbage collector, el nivel de detalle del muestreo de los métodos, la frecuencia de los dumps de hilos o los umbrales de detección de actividad en operaciones de E/S (ficheros y sockets).

Además, podremos elegir si durante la recopilación queremos recoger estadísticas detalladas sobre el Heap, la carga de clases o la reserva de memoria dinámica.

Cuando abramos la grabación (que será un archivo con extensión *.jfr), nos encontraremos con la pantalla principal de JFR:

Desde aquí podremos acceder a las distintas partes principales del informe (a la izquierda, en forma de pestañas principales), además de las subsecciones en la parte inferior. En la parte superior se nos muestra la línea temporal de toda la grabación, en la cual, si arrastramos los bordes de la ventana, podremos definir un rango de tiempos que nos interese.

Es conveniente tener activada la opción “Synchronize Selection”, en la parte superior de la línea temporal, para que todas las pestañas del informe compartan el mismo instante de tiempo.

En esta primera sección podemos visualizar estadísiticas básicas sobre carga de CPU y uso de memoria en forma de gráfica la primera y en medidores de agujas. La subsección “JVM Information” nos hace un pequeño resumen sobre la configuración de la JVM y los flags con los que ha sido ejecutado.

Veamos lo que nos ofrece el resto de secciones:


5.1. Memory

Esta sección nos da información sobre todo lo que tenga que ver con la gestión de memoria que haya realizado la aplicación durante su ejecución con alto nivel de detalle.

En la primera subsección “Overview”, podemos visualizar la evolución del Heap con respecto al tiempo en forma de gráfica. En la parte superior podemos elegir qué métricas de tamaño queremos ver. Normalmente, como las aplicaciones no usan toda la memoria física de la máquina sobre la que está corriendo la JVM, será conveniente ocultar estas dos series de la gráfica para no aumentar en demasía el eje de ordenadas (en mi ejecución, el heap no crecía más de 600 MB).

Garbage Collections / GC Times

En estas subsecciones tenemos una información mucho más detallada sobre el comportamiento del GC, incluidas las duraciones de las pausas Stop-the-world (representadas como barras verticales rojas).

No sólo eso, si no que si seleccionamos uno de los ítems de la tabla “Garbage Collections”, se nos presentará una serie de métricas tomadas durante dicha pasada del GC:

  • Duración total de la pausa del GC.
  • Razón de la llamada al GC.
  • Tiempo empleado en cada una de las fases de la recolección.
  • Tipo de referencias de los objetos liberados.
  • Estado del Heap antes y después de la recolección.
Allocations

Esta pestaña es especialemnte útil (sobre todo “Allocation in New TLAB”) para comprobar qué clases son instanciadas más frecuentemente, cuánta memoria consumen y en qué partes del código son reservadas. Tenemos dos tipos distintos de análisis:

  • Reservas por clase.
  • Reservas por hilo.

En mi caso, casi el 90% de las reservas de memoria son del tipo char[] (cadenas de caracteres). Si hago click sobre este tipo de objetos, puedo ver que el 87% de dichas reservas se realizan en la clase StandardTokenizerImpl y que se concentran en los hilos “Query0” y “Query1”, si cambio a la vista “Allocation by Thread”.


5.2. Code

Esta sección es útil para realizar optimizaciones sobre el uso y tiempo de ejecución de CPU. En la subsección “Overview” tenemos dos vistas diferenciadas:

  • Hot Packages: Lista ordenada de los paquetes que están consumiendo más tiempo de CPU. Útil cuando estemos utilizando librerías de terceros y quieres tener resumenes separados de dichas librerías, de tu propio código y del propio JDK.
  • Hot Classes: Misma clasificación que la vista anterior, pero a nivel de clases.
Hot Methods / Call Tree

En estas vistas podemos ver una lista con los métodos que más utilizan más CPU, incluido el árbol de llamadas realizado hasta dichos métodos. Este ánalisis es el típico que podremos encontrar en cualquier herramienta de monitorización. Cuando queramos mejorar el rendimiento de nuestra aplicación, éste debería ser el primer sitio que visitemos en busca de cuellos de botella.

En ambas subsecciones tendremos la capacidad de filtrar los resultados por nombre de la traza, número total de llamadas o porcentaje de uso de CPU.

Exceptions

Aunque no sea clave en posibles mejoras de optimización de rendimiento debido a que, en principio, las excepciones deberían ser siempre usadas con sumo cuidado y solo en el caso de que sea estrictamente necesario. Sin embargo, ya que la gestión de excepciones suele ser una operación bastante costosa, si sospechamos que estamos manejando muchas excepciones, en esta vista podremos comprobar cuales son las más comunes e intentar llevarlas a un mínimo.

Compilations / Class Loading

Las últimas dos pestañas son bastante autoexplicativas y nos permiten comprobar los tiempos de compilación (tanto estándar cómo OSR) la primera y la carga y descarga de memoria de clases.


5.3. Threads

Esta sección detalla el comportamiento y rendimiento de los hilos de ejecución de la JVM. La principal ventaja de fijarse en estos resultados será la capacidad de detectar hilos bloqueantes en deadlock, latencias de espera y acceso a los dumps de trazas de ejecución de dichos hilos.


5.4. I/O

Esta sección nos permite inspeccionar la entrada/salida de nuestra aplicación tanto en fichero como por socket: número de lecturas y escrituras, tamaños y tiempos. Usualmente las métricas que nos interesará aquí son posibles operaciones sobre ficheros no esperados, múltiples aperturas/cierres del mismo fichero o que los tamaños sean muy pequeños.

Si se está haciendo uso de un disco SSD se recomienda poner el umbral de detección de eventos de lectura (“File Read Threshold”, en el wizard inicial de JFR) a 1ms, ya que, de lo contrario, podríamos perdernos muchos datos.

Las cuatro subsecciones son prácticamente idénticas en cuanto a visualizaciones, cada una encargándose de lectura/escritura de ficheros y sockets, por lo que solo nos detendremos en la primera de ellas.

Tenemos a nuestra disposición tres listas diferentes según el elemento: por fichero/socket (By File/Host), por hilo (By Thread) y por evento (By Event, que es una caombinación de las dos anteriores).

Si seleccionamos uno de los ítems, podremos inspeccionar una gráfica con las cantidades de bytes leídos/escritos por unidad de tiempo, lo que nos dará una idea de si es posible bajar, por ejemplo, los tiempos de lectura implementando una caché de datos.


5.5. Events

Esta sección nos permite visualizar todos, y repito TODOS, los eventos que se han producido durante la ejecución del programa y nos los muestre en porcentajes con respecto al tiempo total, ni por carga de CPU ni por uso de memoria.

Tenemos dos vistas separadas: por productores de carga arriba y por tipos de evento abajo. Si queremos añadir a las gráficas nuevos tipos de eventos, podremos hacerlo seleccionando uno de los disponibles en la lista de la izquierda.

Graph

La subsección más interesante desde mi punto de vista es esta, que nos permite ver en forma de línea temporal como se han comportado cada uno de los hilos del sistema; en qué momentos se encuentran ejecutándose, cuándo esperando y, sobre todo, nos permite ver de forma gráfica cuándo estamos viviendo una situación de deadlock u otros tipos de bloqueos entre hilos.

En el hilo principal también podemos ver las pausas que realiza JFR para la recopilación de estadísticas.


6. Conclusiones

Java Mission Control es una herramienta estupenda para monitorizar y hacer profiling de aplicaciones que puedan ser mejoradas desde el punto de vista del rendimiento. Se suma al ya extenso abanico de herramientas de este tipo, siendo sus puntos fuertes el que esté incluido en el JDK desde la versión 1.7.40 y la buena presentación de los resultados y facilidad de uso en operaciones complejas, así como el mínimo overhead producido por el efecto del observador.

Java Flight Recorder es un gran añadido a JMC que, aunque no introduzca muchas funcionalidades revolucionarias en el mundo del profiling de aplicaciones Java, su facilidad de uso, personalización y su aproximación a las métricas más complicadas de tal manera que sean medianamente fáciles de entender tanto a iniciados como a expertos en estos temas.


7. Referencias

La directiva static en Java

$
0
0

En este tutorial veremos para qué sirve la directiva static en Java, a qué elementos se puede aplicar, sus características, beneficios y desventajas que pueden tener.

0. Índice de contenidos


1. Introducción

En este tutorial veremos para qué sirve la directiva static en Java, a qué elementos se puede aplicar, sus características, beneficios y desventajas que pueden tener.

Se propondrán una serie de ejemplos y snippets de código para ilustrar cada uno de los puntos mencionados.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • MacBook Pro Intel Core 2 Duo 8GB RAM
  • SO: Yosemite
  • IDE: Eclipse Mars 4.5.0
  • Java JDK 1.7

3. ¿Qué es static en Java?

Una clase, método o campo declarado como estático puede ser accedido o invocado sin la necesidad de tener que instanciar un objeto de la clase. Uno de los ejemplos típicos de uso de métodos y variables estáticas es la clase java.lang.Math:

public class MathTest {

    public static void main(String[] args) {
        double floorOfPi = Math.floor(Math.PI);
        System.out.println(floorOfPi);
    }
}

Cuya salida será el suelo matemático del número Pi:

3.0

4. Campos estáticos

Los campos de una clase declarados como estáticos son inicializados en el momento en que se carga la clase en memoria, respetando el orden de declaración. Los campos estáticos no pueden ser accedidos desde un contexto no estático; este comportamiento resultará en un error en tiempo de ejecución.

En conjunción con la directiva final (que evita la modificación del campo), las constantes de clase son definidas de esta manera.

Gracias a que las variables estáticas son compartidas entre todos los objetos de una clase, es posible implementar el patrón Singleton, guardando una referencia de la instancia:

public class SingletonTest {

    private static SingletonTest instance = null;

    protected SingletonTest() {
    }

    public static SingletonTest getInstance() {
        if (instance == null) {
            instance = new SingletonTest();
        }

        return instance;
    }
}

En este caso, el constructor por defecto se oculta con el modificador protected (que podría ser también private) para evitar la instanciación explícita desde fuera del contexto de la clase. Sin embargo, si la construcción es costosa y la clase es poco utilizada, el rendimiento se verá degradado.

Hay que tener en cuenta que, al igual que las variables transient, los campos estáticos no son serializadas. De esta manera, en el proceso de deserialización, los campos estáticos tomarán su valor por defecto (0.0 para los float, null para objetos, etc…).


5. Métodos estáticos

Debido a que los métodos estáticos son enlazados en tiempo de compilación mediante static binding usando la información del tipo, no es posible realizar sobreescritura (override) de métodos.

class Padre {

    public static void metodoEstatico() {
        System.out.println("Invocación desde el padre.");
    }
}

class Hijo extends Padre {

    public static void metodoEstatico() {
        System.out.println("Invocación desde el hijo.");
    }
}

public class BindingTest {

    public static void main(String[] args) {
        Padre p = new Hijo();
        p.metodoEstatico();
        Hijo h = new Hijo();
        h.metodoEstatico();
    }
}
Salida:
Invocación desde el padre.
Invocación desde el hijo.

En la salida podemos comprobar como el método que se invocará vendrá determinado por el tipo desde donde se invoque, aunque los objetos sean del mismo tipo (de la clase Hijo en este caso).

Otra de las consecuencias del static binding es que los métodos estáticos no pueden ser declarados como abstract, por el mismo motivo que la imposibilidad de la sobrecarga de métodos.

Uno de las características que produce más dolores de cabeza al tratar con métodos y variables estáticas es que, debido a su naturaleza, no son seguras para la programación con hilos. Es necesario hacer uso de las directivas synchronized de los métodos y de las variaciones atómicas de los tipos nativos con los campos.


6. Clases y bloques estáticos

Las clases internas pueden ser declaradas estáticas, de tal manera que aumenten la cohesión de la clase que las engloba. Hay que tener en cuenta que, como el resto de clases internas, el compilador crea un fichero .class por cada una de estas clases. Un ejemplo típico de clases internas estáticas son los comparadores de objetos:

public class Objeto {

    private int campo1;
    private String campo2;

    public Objeto(int campo1, String campo2) {
        this.campo1 = campo1;
        this.campo2 = campo2;
    }

    private static class ObjetoComparator implements Comparator<Objeto> {

        @Override
        public int compare(Objeto o1, Objeto o2) {
            return o2.campo1 - o1.campo1;
        }

    }
}

Es posible declarar bloques de código como estáticos, de tal manera que sean ejecutados cuando se cargue la clase. Este tipo de bloques se conocen como bloques de inicialización estáticos (static initializer block). Si no se declara un bloque de este tipo de forma explícita, el compilador Just-in-Time combina todos los campos estáticos en un bloque y los ejecuta durante la carga de clases. Aunque estos bloques no puedan lanzar checked exceptions, las unchecked pueden ocurrir, resultando en un ExceptionInInitializerError.

public class Objeto {

    private static int campo1;

    static {
        campo1 = 10;
    }
}

Los bloques de inicialización pueden ser, además, sustituidos por métodos estáticos:

public class Objeto {

    private static int campo1 = inicializaCampo();

    private static int inicializaCampo() {
        return 10;
    }

}

Como se observa en los ejemplos anteriores, esta funcionalidad permite dotar de cierta lógica la inicialización de variables de clase estáticas sin necesidad de incluirlo en su constructor.


7. Imports estáticos

Una de las características incluidas en Java 5 fué la capacidad de importar los métodos y variables estáticas de un módulo y acceder a ellos como si hubieran sido declarados en la propia clase. Es especialmente útil, y mejora la legibilidad, cuando se están definiendo test unitarios, ya que la mayoría de los métodos de aserción de JUnit son estáticos.

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class ImportEstático {

    @Test
    public void test() {
        assertEquals(1, 1);
    }

}

8. Conclusiones

La directiva static permite el acceso a métodos y variables de clase sin la necesidad de instanciar un objeto de dicha clase, permitiendo la inicialización de forma cómoda y durante la carga de clase. Además, los import estáticos mejoran la legibilidad de nuestro código, así como las clases estáticas internas la cohesión.


9. Referencias

Clustering de Apache Tomcat

$
0
0

En este tutorial veremos cómo crear un clúster de instancias de Apache Tomcat, haciendo uso de la extensión mod_jk de Apache Web Server.

0. Índice de contenidos


1. Introducción

Una de las extensiones de Apache Web Server que nos permite implementar un cluster de servidores Tomcat y su correspondiente balanceador de carga es mod_jk, antes conocido como mod_jserv.

Durante este tutorial se explicará como realizar una instalación desde cero de un clúster de servidores Apache Tomcat, la motivación para implementar éste tipo de arquitectura y una pequeña prueba con una aplicación sencilla que nos permita observar el balanceador de carga en funcionamiento.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • MacBook Pro Intel Core 2 Duo 8GB RAM
  • SO: Yosemite 10.10.5
  • IDE: Eclipse Mars 4.5.0
  • JDK 1.7
  • Máquina Virtual: VMWare Fusion 7 corriendo Ubuntu Server 14.04

3. Motivación

Supongamos que tenemos un sistema en producción en una sóla máquina física. Si por cualquier motivo dicho sistema parase de dar servicio (fallo en el hardware, una aplicación bloquea el sistema operativo o hay una subida de tensión y la máquina se apaga), nadie podría acceder a nuestras aplicaciones./p>

¿Cuál podría ser la opción para aumentar la disponibilidad de nuestro sistema?

Una de las ideas básicas, y es la que tendremos en cuenta durante el tutorial, es la replicación de nuestro servidor de aplicaciones. De esta manera, si uno de estos servidores cae, otro tomará su lugar para seguir dando servicio a nuestro sistema.

A este concepto de tener una serie de componentes disjuntos que, desde el exterior, parezcan comportarse como si fueran uno único se le denomina clustering.

Sin embargo, ahora se nos presenta un nuevo problema: si tenemos n instancias de nuestro servidor replicado, ¿cómo sabemos a que dirección o puerto debemos mandar la petición? ¿Están todos los miembros o nodos del clúster disponibles? ¿De verdad debemos nosotros saber todas éstas cosas?

Otro de los componentes de un sistema de clustering es el denominado balanceador de carga o load balancer. Este componente es el que se expondrá a los posibles clientes del sistema, con una única dirección IP y puerto sobre el que escuchar. Él será el encargado de mantener la cuenta de los nodos sanos y los no disponibles, redirigiendo el tráfico a unos u otros.

En este tutorial, utilizaremos Apache Tomcat como servidor de aplicaciones (en realidad se trata de un servidor de servlets), una aplicación JSP de prueba y para el balanceo de carga usaremos Apache Web Server junto con la extensión mod_jk.


4. Instalación de Apache Tomcat, Apache Web Server (httpd) y mod_jk

En Ubuntu Server 14.04, tenemos disponible el gestor de paquetes apt-get, por lo que lo usaremos para hacer la instalación de casi todos los componentes, por su facilidad de uso y delegación de responsabilidades que nos ofrece. En determinados casos realizaremos la descarga manual del software por temas de claridad.


Apache Tomcat

Vamos a la página de descarga de Apache Tomcat 8 y nos descargamos el fichero comprimido con el core del servidor.

Abrimos una terminal y descomprimimos y copiamos el contenido del fichero comprimido a donde deseemos, en nuestro caso en /usr/share/tomcat:

tar zxvf apache-tomcat-<version>.tar.gz
mv apache-tomcat-<version> /usr/share/tomcat

Vamos a arrancar el servidor Tomcat para probar que todo ha salido bien:

cd /usr/share/tomcat
bin/startup.sh

Deberíamos ver la siguiente salida por consola:

Using CATALINA_BASE:   /usr/share/tomcat
Using CATALINA_HOME:   /usr/share/tomcat
Using CATALINA_TMPDIR: /usr/share/tomcat/temp
Using JRE_HOME:        /usr
Using CLASSPATH:       /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar
Tomcat started.

No solo eso, si desde un navegador accedemos a

localhost:8080
veremos como Tomcat nos sirve su gestor.

new-tomcat

Paremos ahora la ejecución del servidor, ya que realizaremos una serie de cambios en su configuración:

bin/shutdown.sh

Apache Web Server (httpd)

La forma más sencilla de instalar Apache en Ubuntu Server es hacer uso del gestor de paquetes APT:

apt-get install apache2

Iniciemos el servidor Apache:

/etc/init.d/apache2 start

Si ahora accedemos a

localhost:80
, veremos la página de bienvenida de Apache HTTPD.

new-apache

Paremos el servidor:

/etc/init.d/apache2 stop

Conexión mod_jk

Para poder realizar el balanceo de carga, el servidor Apache necesita una conexión específica. Entre las opciones disponibles tenemos las extensiones mod_jk y mod_proxy.

En este tutorial usaremos mod_jk, ya que aunque su uso sea ligeramente más complicado (mod_proxy viene dentro del paquete de Apache HTTP desde la versión 2.2) nos permite tener un control mucho más amplio del balanceo de carga.

La instalación es muy sencilla:

apt-get install libapache2-mod-jk

Comprobamos que se ha instalado correctamente:

ls /usr/lib/apache2/modules/mod_jk.so

Al instalar la extensión, se nos generarn una serie de ficheros que serán los que debemos modificar (lo veremos en su apartado correspondiente):

  • /etc/apache2/mods-enabled/jk.conf: Fichero de configuración de la extensión donde podremos definir los parámetros generales de comportamiento.
  • /etc/apache2/mods-enabled/jk.load: Apunta al servicio instalado para cargarlo cuando se arranca el Apache.
  • /etc/libapache2-mod-jk/workers.properties: Especificación de los nodos del clúster, comportamiento y rutas permitidas.
  • /var/log/apache2/mod_jk.log: Log de ejecución de los eventos de mod_jk.

5. Múltiples instancias de Tomcat

Debido al diseño modular y segregado de la implementación de Apache Tomcat, es posible tener en una misma máquina física múltiples instancias con su propio espacio de aplicaciones y configuraciones.

En el directorio donde tengamos instalado el Tomcat (en el caso de este tutorial, y como hemos visto, en /usr/share/tomcat) tendremos las siguientes carpetas:

  • bin: ejecutables y scripts del Tomcat.
  • conf: ficheros de configuración como los nombres de servidor, puertos para cada tipo de servicio, etc.
  • logs: logs de ejecución del servidor.
  • temp: los ficheros temporales que necesite el Tomcat se crearán aquí.
  • webapps: aquí irán todas las aplicaciones desplegadas en el servidor.
  • work: Tomcat escribirá aquí todo lo que necesite durante la ejecución, como el código generado de los servlets JSP o las sesiones serializadas durante el reinicio del servidor.

Vamos a crear un directorio donde descansarán los ficheros necesarios para cada instancia de Tomcat:

mkdir /usr/tmp/tomcat-instances
cd /usr/tmp/tomcat-instances

Los directorios necesarios para ejecutar una instancia independiente son conf, temp, webapps y logs. Los copiaremos del directorio base de Tomcat a una nueva carpeta que denominaremos tomcat-instance1:

mkdir tomcat-instance1
cp -R /usr/share/tomcat/conf /usr/share/tomcat/temp /usr/share/tomcat/webapps /usr/share/tomcat/logs tomcat-instance1

En el archivo de configuración conf/server.xml se especifican los puertos de cada conexión que usará la instancia. Cada una de las instancias de Tomcat debe tener los siguientes puertos diferentes a todos los demás, como mínimo, para poder funcionar:

  • Shutdown port: Puerto sobre el que se quedar esperando para la señal de apagado.
  • HTTP port: Puerto que entiende HTTP. Es el que se expone al cliente.
  • AJP port: Puerto sobre el que se realizará la comunicación entre componentes para mod_jk mediante el protocolo AJP.

Así, si abrimos el fichero de configuración del servidor de la primera instancia, los puertos que debemos modificar son los siguientes:

tomcat-instance1/conf/server.xml
[...]
<Server port="8005" shutdown="SHUTDOWN">
[...]
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />
[...]
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
[...]

Con una herramienta como vim o nano modificaremos estos valores para que no entre en conflictos con otras instancias:

  • Shutdown: 6005
  • HTTP: 6080
  • AJP: 6009

Con esto ya tendríamos preparada la primera instancia de independiente de Tomcat. Repliquemos este árbol de ficheros en una nueva instancia tomcat-instance2:

cp -R tomcat-instance1 tomcat-instance2

A esta nueva instancia le asignaremos los siguientes puertos:

  • Shutdown: 7005
  • HTTP: 7080
  • AJP: 7009

Lo único que nos queda por hacer para tener la configuración preparada de las instancias de Tomcat son los scripts de arranque y apagado de cada una de ellas. Tomcat utiliza 5 variables de entorno a la hora de arrancar: CATALINA_HOME, CATALINA_BASE, CATALINA_TMPDIR, JRE_HOME/JAVA_HOME y CLASSPATH.

De todas ellas, las que tendremos que tener en cuenta para el clustering son:

  • CATALINA_HOME: Apunta a la ruta donde tengamos instalado el Tomcat. Desde aquí se puede acceder a las carpetas bin y lib. Se le da valor de forma automática.
  • CATALINA_BASE: Resulta un complemento de CATALINA_HOME que apunta a las carpetas conf y webapps. Ésta variable es la que deberemos modificar dinámicamente para iniciar una u otra instancia.

Para hacernos las cosas un poco más fciles crearemos una serie de scripts de arranque y apagado de las instancias en bash. Éstos scripts deberan residir en la carpeta base tomcat-instances.

Empezemos con el script de arranque:

startup-instance.sh
#!/bin/bash

NUM_OF_TOMCATS=2

# cambio de directorio al nativo de Tomcat
cd /usr/share/tomcat/bin

if [ $# -eq 0 ]; then
    echo "No args"
else
    if [ $1 = "all" ]; then
        for i in `seq 1 $NUM_OF_TOMCATS`; do
            export CATALINA_BASE=/usr/tmp/tomcat-instances/tomcat-instance$i
            ./startup.sh
        done
    else
        export CATALINA_BASE=/usr/tmp/tomcat-instances/tomcat-instance$1
        ./startup.sh
    fi
fi

Como se puede observar, recibe un único argumento de entrada (el resto, si los hubiera, son ignorados) que determina el número de instancia que queremos levantar. Podemos utilizar la cadena “all” para levantar todas las instancias.

Hay que acordarse siempre de darle permisos de ejecución al script. Si todo ha ido bien, al ejecutar este script, tendremos dos instancias separadas del Tomcat, una sirviendo en el puerto 6080 y otra en el 7080:

chmod +x startup-instance.sh
./startup-instance.sh all

multiple-tomcats

El script de apagado es análogo al de arranque:

shutdown-instance.sh
#!/bin/bash

NUM_OF_TOMCATS=2

# cambio de directorio al nativo de Tomcat
cd /usr/share/tomcat/bin

if [ $# -eq 0 ]; then
        echo "No args"
else
    if [ $1 = "all" ]; then
        for i in `seq 1 $NUM_OF_TOMCATS`; do
            export CATALINA_BASE=/usr/tmp/tomcat-instances/tomcat-instance$i
            ./shutdown.sh
        done
    else
        export CATALINA_BASE=/usr/tmp/tomcat-instances/tomcat-instance$1
        ./shutdown.sh
    fi
fi
chmod +x shutdown-instance.sh

Como comodidad adicional, podemos crear un script de reinicio de instancias que llame a los dos anteriores:

restart-instance.sh
#!/bin/bash

if [ $# -eq 0 ]; then
    echo "No args"
else
    ./shutdown-instance.sh $1
    ./startup-instance.sh $1
fi
chmod +x restart-instance.sh

Si ejecutamos este script de reinicio y no tenemos instancias que apagar nos saltará una excepción del Tomcat, pero proseguirá su ejecución sin problemas. Por ejemplo:

Using CATALINA_BASE:   /usr/tmp/tomcat-instances/tomcat-instance2
Using CATALINA_HOME:   /usr/share/tomcat
Using CATALINA_TMPDIR: /usr/tmp/tomcat-instances/tomcat-instance2/temp
Using JRE_HOME:        /usr
Using CLASSPATH:       /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar
ago 21, 2015 10:19:50 PM org.apache.catalina.startup.Catalina stopServer
GRAVE: Could not contact localhost:7005. Tomcat may not be running.
ago 21, 2015 10:19:50 PM org.apache.catalina.startup.Catalina stopServer
GRAVE: Catalina.stop: 
java.net.ConnectException: Conexión rehusada
        at java.net.PlainSocketImpl.socketConnect(Native Method)
        at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339)
        at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
        at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
        at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
        at java.net.Socket.connect(Socket.java:579)
        at java.net.Socket.connect(Socket.java:528)
        at java.net.Socket.<init>(Socket.java:425)
        at java.net.Socket.<init>(Socket.java:208)
        at org.apache.catalina.startup.Catalina.stopServer(Catalina.java:450)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:606)
        at org.apache.catalina.startup.Bootstrap.stopServer(Bootstrap.java:400)
        at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:487)

6. Balanceo de carga con mod_jk

El conector mod_jk permite la comunicación entre servidores Tomcat y Apache haciendo uso del protocolo propio Apache JServ Protocol (AJP).

Dependiendo de la instalación que hayamos hecho tanto de Apache como de mod_jk, los archivos de configuración del balanceador variarán en localización, pero no en contenido. Nosotros usaremos el árbol de directorios que nos crea automáticamente APT al instalar ambos.

La configuración más sencilla de un balanceador está compuesta de los siguientes elementos:

  • workers.properties: fichero de especificación de cada nodo trabajador del clúster, así como el comportamiento del balanceador (si es definido).
  • jk.conf: configuración del propio conector mod_jk (localización de los ficheros de workers, logs, intetrvalo de polling, etc…)

Empezemos por configurar el fichero /etc/libapache2-mod-jk/workers.properties:

workers.properties
worker.list=balancer,stat

worker.tomcat1.type=ajp13
worker.tomcat1.port=6009
worker.tomcat1.host=localhost

worker.tomcat2.type=ajp13
worker.tomcat2.port=7009
worker.tomcat2.host=localhost

worker.balancer.type=lb
worker.balancer.balance_workers=tomcat1,tomcat2

worker.stat.type=status

Hemos definido dos nodos denominados tomcat1 y tomcat2, uno por cada instancia del Tomcat. La información de cada nodo es la que definimos en su momento en cada uno de los ficheros server.xml de las instancias en el apartado del conector AJP, por ello es que el tipo sea “ajp13” o protocolo AJP v1.3.

Definimos, además, dos trabajadores del mod_jk, uno que se encargará de proporcionarnos una consola de monitorización de la extensión (trabajador “stat”) y otro que nos sirva del propio balanceador (trabajador “balancer”). A este último es al que le hemos añadido los dos nodos Tomcat como unidades de proceso.

El siguiente paso sería definir la configuración del Apache para que cargue la extensión mod_jk y sepa donde encontrar el fichero de workers, los configure según la ruta de entrada, etc. Ambos ficheros se encuentran en /etc/apache2/mods-enabled si lo hemos instalado mediante APT o en $APACHE_HOME/httpd.conf si lo hemos hecho de forma manual.

jk.load
# Carga el módulo mod_jk y lo denomina jk_module
LoadModule jk_module /usr/lib/apache2/modules/mod_jk.so
jk.conf
<IfModule jk_module>
    
    # Localización del fichero de trabajadores
    JkWorkersFile /etc/libapache2-mod-jk/workers.properties
    
    # Localización donde escribir los logs de mod_jk
    JkLogFile /var/log/apache2/mod_jk.log
    
    # Nivel de criticidad de los logs (trace,debug,info,warn,error)
    JkLogLevel debug
    
    # Fichero de memoria compartida
    JkShmFile /var/log/apache2/jk-runtime-status
    
    # Cada intervalo realiza tareas internas como
    # prueba de conexiones ociosas, redimensionamiento de
    # la reserva de conexiones, etc...
    JkWatchdogInterval 60
    
    # Definimos un nuevo virtual host en el puerto 80
    # (el de HTTP de Apache) especificando la ruta de la que
    # se encargar el mod_jk
    <VirtualHost *:80>
        ServerName  localhost
        ServerAdmin localhost
        JkMount /status stat
        JkMount /* balancer
    </VirtualHost>

</IfModule>

Guardamos los cambios en ambos ficheros y reiniciamos el servidor Apache para que sean efectivos:

apache2ctl restart

Si todo ha ido bien, podemos acceder al fichero /var/log/apache2/mod_jk.log para comprobar la creación y registro del balanceador y sus nodos, ya que hemos definido el nivel de detalle de dicho log a “debug”.

Si accedemos ahora a

localhost:80/status
nos encontramos con la herramienta de monitorización del balanceador de carga. Desde aquí podremos, además de comprobar el funcionamiento de mod_jk en tiempo real, modificar ciertos valores en caliente, como deshabilitar/eliminar nodos, añadir peso a un nodo o cambiar el número de reintentos antes de dar por caído uno de ellos, por ejemplo.

jk-status


7. Despliegue de la aplicación de prueba

Si ahora accedemos a

localhost:80/
será el balanceador el encargado de redirigirnos a uno de los Tomcat levantados:

jk-manager

Para que sea un poco más visual, vamos a instalar una pequeña aplicación web de prueba que nos muestre de forma sencilla cual de las instancias de Tomcat nos está sirviendo el balanceador. En este enlance se puede descargar el WAR ya empaquetado.

La aplicación es muy sencilla, con un único fichero JPA que muestra un mensaje de bienvenida y la ruta donde se encuentra:

<html>
<body>

    <h2>Hello World! from</h2>
    <h3><%=getServletContext().getRealPath("/")%></h3>
    
</body>
</html>

El despligue de la aplicación es terriblemente sencilla. Lo único que deberemos hacer es copiar pruebatomcat.war en la carpeta webapps de cada una de las instancias de Tomcat y, por si acaso, reiniciar ambas.

Ahora, si con un navegador accedemos a

localhost:80/pruebatomcat
observaremos como mod_jk nos muestra una de las dos instancias de Tomcat. Si refrescamos varias veces la página, veremos como dichos nodos se alternan, ya que, por defecto, la política que sigue el balanceador es Round Robin.

jk-balancer-1

jk-balancer-2


8. Conclusiones

La extensión mod_jk para la intercomunicación entre servidores Apache y Tomcat nos permite una forma sencilla de montar arquitecturas de disponibilidad no demasiado complicadas. Además, tiene la potencia de hacer sistemas mucho más complejos, debido a su capacidad de configuración.


9. Referencias

Introducción a Apache Spark – Batch y Streaming

$
0
0

Vamos a dar un pequeño paseo por el framework de computación paralela más popular del momento, Apache Spark.

Índice de contenidos


1. Introducción

Con todo el revuelo que se está montando alrededor del mundo del Big Data y con la asistencia al evento técnico Big Data Spain 2015 del equipo de Autentia (del que ya hicimos dos entradas sobre nuestras impresiones: primer día y segundo día), nos ha parecido una buena idea investigar y cacharrear con las herramientas que más sonaron durante las conferencias.

Comenzamos con el ya rey de la computación paralela, Apache Spark, proyecto de la fundación Apache que más interés está suscitando en el mundo del Big Data.


2. Apache Spark y la ciencia de datos

Apache Spark es un framework computación en clúster de código abierto que se diferencia con Hadoop (el otro gran proyecto de este tipo) en el uso de operaciones en memoria divididas en varias fases de procesamiento. Hadoop, como es bien conocido, hace uso del clásico modelo de programación publicado por Google MapReduce.

Debido al cambio de contexto que está sufriendo la ciencia de datos, el nuevo enfoque de Apache Spark pasando del procesamiento en batch al streaming, permitiendo la exploración de datos ad hoc, así como la aplicación más sencilla de algoritmos de machine learning es el factor de diferenciación básico de este proyecto.

No es el objetivo de este tutorial ahondar mucho en el funcionamiento interno del framework, sin embargo cabe destacar que, en su versión out-of-the-box, lleva incluidas librerías específicas para tratar datos estructurados (Spark SQL), capacidades para el streaming de datos (Spark Streaming), machine learning (MLib) y computación sobre grafos (GraphX).

intro_spark_imagen1


2.1. Arquitectura de la computación en clúster

Las aplicaciones para Spark se ejecutan como un grupo independiente de procesos en clústeres, coordinados por el objeto SparkContext. Más específicamente,

SparkContext
puede conectarse a gestores de clúster que son los encargados de asignar recursos en el sistema. Una vez conectados, Spark puede encargar que se creen ejecutores o executors encargados de la computación en los nodos del clúster. Los trozos de código propio de los que se encargan estos ejecutores son denominados tasks o tareas.

intro_spark_imagen2


2.1. Conceptos clave

Repasemos a continuación los conceptos básico sobre los que se apoya Apache Spark para realizar su magia.

SparkContext

Se trata del context básico de Spark, desde donde se crean el resto de variables que maneja el framework. Sólo un

SparkContext
puede estar activo por JVM.

En la shell interactiva de Spark es creada automáticamente bajo el nombre

sc
, mientras que en otros entornos es necesario instanciarlo explícitamente. La configuración de
SparkContext
puede ser definida mediante un bundle específico llamado SparkConf. RDDs

Viene de las siglas en inglés de Resilient Distributed Datasets o, en castellano, “conjuntos distribuidos y flexibles de datos”. Según la propia documentación de Spark, representan una colección inmutable y particionada de elementos sobre los que se puede operar de forma paralela.

Dependiendo del origen de los datos que contiene, diferenciamos dos tipos de RDDs:

  • Colecciones paralelizadas basadas en colecciones de Scala.
  • Datasets de Hadoop creados a partir de ficheros almacenados en HDFS.

Son posibles dos operaciones bien diferenciadas sobre un RDD, dependiendo del resultado final:

  • Transformaciones, que crean nuevos conjuntos de datos, como puede ser la operación
    map()
    que pasa cada elemento por una determinada función y devuelve un nuevo RDD con el resultado.
  • Acciones, que devuelven un valor al driver del clúster después de llevar a cabo una computación sobre el conjunto de datos. Un ejemplo de este tipo es la función
    reduce()
    , que agrega todos los elementos de un RDD mediante una función y devuelve el resultado.

3. Instalación del entorno Scala + Apache Spark


3.1. Scala

Nosotros vamos a hacer esta introducción a Spark con Scala (también soporta Python, R y Java), por lo que deberemos descargarlo de la página oficial y moverlo a una localización de nuestra elección:

wget http://downloads.typesafe.com/scala/2.11.7/scala-2.11.7.tgz
tar zxvf scala-2.11.7
sudo mv scala-2.11.7 /usr/local/scala-2.11.7
sudo ln -snf /usr/local/scala-2.11.7 /usr/local/scala
echo "export SCALA_HOME=/usr/local/scala" >> ~/.bash_profile

El directorio

$SCALA_HOME/bin
debería ser añadido también al PATH del sistema para poder acceder a los binarios desde cualquier localización.

Comprobemos que la instalación se ha realizado correctamente:

$> $SCALA_HOME/bin/scala -version
Scala code runner version 2.11.7 -- Copyright 2002-2013, LAMP/EPFL

3.2. Apache Spark

Nosotros vamos a bajarnos directamente el framework de Spark, aunque es posible (y preferible en entornos más serios) incluir el artefacto Maven en nuestro proyecto:

<dependency>
	<groupId>org.apache.spark</groupId>
	<artifactId>spark-core_2.10</artifactId>
	<version>1.5.1</version>
</dependency>

Vamos a la página oficial de descarga, eligiendo la última release. Aquí se nos presentan varias opciones:

  • Código fuente listo para compilar por nosotros mismos que permite el uso de la versión de Hadoop que deseemos. Nosotros elegiremos esta opción por su flexibilidad.
  • Pre-compilado a falta de la dependencia de Hadoop, que la debemos suministrar nosotros.
  • Pre-compilado con diferentes versiones de Hadoop integradas.

Descargamos el fichero y lo descomprimimos:

wget http://d3kbcqa49mib13.cloudfront.net/spark-1.5.1.tgz
tar zxvf spark-1.5.1.tgz

Lo movemos a la localización que deseemos como hicimos con Scala:

sudo mv spark-1.5.1 /usr/local/spark-1.5.1
sudo ln -snf /usr/local/spark-1.5.1 /usr/local/spark

Es hora de compilar el framework como nos dice en su fichero README.md:

sudo mvn -DskipTests clean package

Dependiendo del entorno en el que trabajemos, ésta compilación será más o menos costosa. En mi caso fueron unos 25 minutos. Por último, es conveniente declarar la variable de entorno

$SPARK_HOME
y añadir
$SPARK_HOME/bin
al
$PATH
del sistema, como hicimos en la instalación de Scala.

Para comprobar que todo se ha instalado de forma correcta, abramos una sesión en el REPL de Spark, que es con lo que trabajaremos durante todo el tutorial:

$> spark-shell
Using Spark's repl log4j profile: org/apache/spark/log4j-defaults-repl.properties
To adjust logging level use sc.setLogLevel("INFO")
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 1.5.1
      /_/

Using Scala version 2.10.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_79)
Type in expressions to have them evaluated.
Type :help for more information.

Spark context available as sc.
SQL context available as sqlContext.

scala>

4. Jugando con Datasets y Spark-shell


4.1. Procesado en batch

Algo que buscamos todos cuando se empieza a buscar información sobre Big Data son Datasets interesantes y grandes para jugar con ellos. En ésta pagina podéis encontrar una colección de enlaces a páginas con montones de datasets públicos listos para usarse.

Nosotros hemos elegido el de salarios de los empleados públicos de la ciudad de Chicago, en EEUU. Un vistazo rápido para saber con qué estamos trabajando (se ha formateado el fichero de CSV a TSV):

Name	Position	Title	Department	Employee	Annual Salary
AARON ELVIA J	WATER RATE TAKER	WATER MGMNT	88968.00
AARON JEFFERY M	POLICE OFFICER	POLICE	80778.00
AARON KARINA	POLICE OFFICER	POLICE	80778.00
AARON KIMBERLEI R	CHIEF CONTRACT EXPEDITER	GENERAL SERVICES	84780.00

Abramos una sesión del REPL de Spark, si no la tenemos abierta:

spark-shell

Lo primero que debemos hacer siempre es cargar el dataset en el contexto de Spark. Recordemos que

SparkContext
se instancia automáticamente bajo el objeto
sc
:
scala> val rawData = sc.textFile("Current_Employee_Names__Salaries__and_Position_Titles_formatted.csv")
rawData: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at textFile at <console>:21

Definiremos ahora una clase de Scala que represente un empleado:

scala> case class Employee(name: String, positionTitle: String, department: String, annualSalary: Float)
defined class Employee

Ahora sí, carguemos un RDD con el dataset:

scala> val employees = rawData.map(_.split("\t")).map(r => Employee(r(0), r(1), r(2), r(3).toFloat))
employees: org.apache.spark.rdd.RDD[Employee] = MapPartitionsRDD[3] at map at <console>:25

Es fácil entender ésta última operación: con todos los datos crudos, dividimos mediante tabulaciones y mapeamos cada columna a su atributo correspondiente de la clase

Employee
.

Si estás siguiendo este tutorial a la vez que escribes en la spark-shell, comprobarás que ninguna operación está siendo llevada a cabo, si no que éstas órdenes se están añadiendo a un buffer que se ejecutará en cuanto sea necesario una computación sobre los objetos.

Contemos el número de registros en el RDD:

scala> employees.count()
res0: Long = 32181

Es en las acciones cuando todas las operaciones dependientes son evaluadas.

count()
es una acción, así como
reduce()
, como hemos explicado en los conceptos clave.

Podemos jugar un poco más con este RDD. Por ejemplo, vamos a obtener el número de empleados que trabajan en un departamento con la palabra “FIRE” en el nombre:

scala> employees.filter(em => em.department.contains("FIRE")).count()
res1: Long = 4875

También podemos mostrar estos registros con la función

collect()
:
scala> employees.filter(em => em.department.contains("FIRE")).collect()
res2: Array[Employee] = Array(Employee(ABBATEMARCO JAMES J,FIRE ENGINEER,FIRE,90456.0), Employee(ABDELLATIF AREF R,FIREFIGHTER (PER ARBITRATORS AWARD)-PARAMEDIC,FIRE,98244.0), Employee(ABDOLLAHZADEH ALI,FIREFIGHTER/PARAMEDIC,FIRE,87720.0), Employee(ABDULLAH DANIEL N,FIREFIGHTER-EMT,FIRE,91764.0), ....

4.2. Spark Streaming

Como hemos comentado antes, el core de Apache Spark viene con una serie de librerías para diferentes áreas de Big Data. Entre ellas se encuentra Spark Streaming, evidentemente para el manejo de streaming de datos.

Vamos a ilustrar un pequeño ejemplo de uso de ésta librería con el HelloWorld del mundo del Big Data: WordCount o conteo de palabras en un texto.

Como servidor de streaming, vamos a utilizar el binario de UNIX netcat en modo contínuo y emitiendo en el puerto 9999:

nc -lk 9999

En nuestra

spark-shell
en otra consola diferente del netcat escribiremos lo siguiente:
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._

val ssc = new StreamingContext(sc, Seconds(1))

val lines = ssc.socketTextStream("localhost", 9999)

Como se puede ver, hemos importado la librería de streaming. El contexto que necesitamos usar ahora es StreamingContext, con un ratio de muestreo de 1 segundo. A la instancia de este contexto es necesario pasarle un

SparkContext
; como estamos en la shell, será
sc
.

Por último, abrimos un socket de texto en localhost en el puerto 9999, que es donde se encuentra sirviendo el netcat.

Implementemos ahora el algoritmo de WordCount:

// Dividimos las palabras por espacios
val words = lines.flatMap(_.split(" "))

// Creamos pares clave-valor con cada palabra
val pairs = words.map(word => (word, 1))
// Agregamos todas las palabras coincidentes
val wordCounts = pairs.reduceByKey(_ + _)

wordCounts.print()

Lo único que nos falta por hacer es comenzar el servicio de streaming y dejarlo a la espera:

ssc.start()
ssc.awaitTermination()

Ahora podemos escribir lo que queramos en la consola del netcat y veremos el conteo funcionando en la spark-shell:

// En netcat
lorem ipsum dolor sit dolor lorem amet

// En spark-shell
(sit,1)
(ipsum,1)
(dolor,2)
(amet,1)
(lorem,2)

5. Conclusiones

Hemos visto una introducción muy básica de cómo trabajar con Apache Spark, escribiendo nuestros aplicaciones en Scala, tanto en el batch processing más tradicional, así como en streaming. El siguiente paso podría ser crear un proyecto en Scala con Maven y conectarnos a una HDFS gestionada por Hadoop, o utilizar la API de streaming de Yahoo! Finance para análisis en tiempo real.


6. Referencias

Introducción a Apache Flink

$
0
0

En este tutorial haremos una pequeña introducción a la motivación por usar Apache Flink, sus ideas principales y un ejemplo de proyecto Maven haciendo uso del framework.

0. Índice de contenidos.

flink_logo


1. ¿Qué es Apache Flink?

Se trata de un motor de procesamiento de streams o flujos de datos que proporciona capacidades de distribución de datos, comunicaciones y, muy importante, tolerancia a fallos a las computaciones.

Cuando hablamos de computación distribuida, lo primero que se nos viene a la cabeza es el celebérrimo Apache Hadoop, del cual tenéis muchos tutoriales en este mismo portal. Lo que hizo muy popular a este framework fue el uso del modelo de programación ideado por Google, MapReduce. El modelo seguido por Apache Flink fue pensado desde el principio como alternativa a MapReduce, lo que no significa que no pueda acceder y hacer uso tanto de HDFS (Hadoop Distributed File System) como de YARN (MapReduce v2).


1.1. Componentes fundamentales

flink_components

El ecosistema de Flink se sustenta sobre el núcleo de Flink o Flink Core en el que se encuentran todas las APIs y librerías de utilidades básicas, entre ellas el optimizador de programas Flink o el constructor de streams.

Las dos APIs principales sobre

flink-core
que determinan el comportamiento, así como el entorno de trabajo de los programas ejecutados son:
  • DataSet API: Entorno de ejecución en el que las transformaciones se ejecutan sobre conjuntos de datos tomados de fuentes más estáticas (como ficheros o bases de datos locales).
  • DataStream API: Muy parecido a la API de DataSet, pero con la gran diferencia de que los datos son tomados o recolectados desde fuentes dinámicas, como sockets o colas de mensajes.

Las últimas releases de Apache Flink vienen con una serie de librerías adicionales que forman las capacidades más fronterizas del Big Data:

  • FlinkML como librería de machine learning en continuo crecimiento en número de algoritmos implmentados.
  • Gelly como API de creación y análisis de grafos.
  • Table API, en sus versiones tanto por lotes como de flujos, que permiten utilizar expresiones con una sintaxis SQL.

1.2. Arquitectura básica

A la hora de ejecutar un proceso, el sistema de Flink levanta un

JobManager
, que hace las veces de coordinador de todo el sistema, y uno o más
TaskManager
, encargados de ejecutar partes del programa en paralelo.

Cuando se envía un programa al sistema es el optimizador el encargado de transformarlo en un DataFlow, ejecutable de forma paralela por los

TaskManager
, coordinados por el
JobManager
.

El siguiente esquema muestra esta arquitectura básica:

flink_architecture

2. Apache Flink y Apache Spark

La comparación de Flink con el rey actual de la computación distribuida, Apache Spark (del cual podéis encontrar una guía de primeros pasos con él en éste tutorial), se antoja necesaria.

En la superficie, Flink es muy parecido a Spark, sin embargo, internamente se trata de un modelo de programación enteramente centrado en el procesado de flujos o stream processing, a diferencia de Spark que consigue esta funcionalidad gracias a un enfoque de pequeños procesos por lotes procesados rápidamente y de forma ligera, lo que los desarrolladores llaman micro-batching. En éste enlace podéis encontran más información sobre éste tema.

Otras diferencias fundamentales entre ambos modelos son la abstracción con la que trabaja cada uno de ellos: Spark procesa RDDs o bloques de datos, mientras que Flink realiza un procesado registro a registro en tiempo real.

El hecho de que soporte técnicas de machine learning o procesado de grafos de forma nativa sobre su motor de streaming y que sea más fácil para entornos ya implantados el paso de streaming a batch que al revés, hacen de Apache Flink lo que parece el futuro en éste campo.


2.1. Pero, ¿qué es streaming?

Estamos hablando en estos párrafos continuamente de procesamiento tanto de flujos o streams como de lotes o batchs, pero, ¿qué quieren decir exactamente estos dos conceptos?

El streaming viene a ser una nueva forma de pensar en la infraestructura de datos. Muchas fuentes de datos representan series de eventos que se producen de forma continua (por citar casos, logs, transacciones, sensores, etc.). Tradicionalmente, estos eventos se agrupaban de forma periódica en batchs o series, salvándose para luego ser procesadas en conjunto.

Esta forma de proceder introducía latencias innecesarias entre la generación de los datos y la obtención de resultados, así como asumía de forma implícita que los datos deben estar “completos” en algún momento y, por lo tanto, pueden ser usados en ese instante para realizar predicciones precisas.

A partir de esta forma de procesar por lotes, el siguiente paso lógico en analíticas es aprovechar la naturaleza continua de la generación de datos para procesarlos de la misma manera: como flujos de datos o streams.


3. Nuestra primera aplicación con Flink

Lo primero de todo que necesitamos para comenzar a desarrollar con Flink es el framework completo que nos proporciona la página oficial. Lo podemos mover a la localización que queramos; en todo éste tutorial nos referiremos a dicho path como

$FLINK_HOME
. Todo este proceso de descarga, relocalización y creación de variables de entorno lo dejamos como ejercicio para el lector.

Nosotros trabajaremos con tres scripts principales dentro de

$FLINK_HOME/bin
para arrancar servicios de Flink, aunque existen muchos más:
  • $FLINK_HOME/bin/start-local.sh
    que nos permite arrancar una instancia local de la arquitectura de Apache Flink y que será la encargada de ejecutar nuestras aplicaciones. Se levanta una página web en nuestra máquina local en la dirección localhost:8081.
  • $FLINK_HOME/bin/start-webclient.sh
    que nos levanta una página web que nos permite desplegar y visualizar los grafos de funcionamiento de las aplicaciones Flink en localhost:8080.

Existen scripts análogos para parar dichos servicios,

$FLINK_HOME/bin/stop-local.sh
y
$FLINK_HOME/bin/stop-webclient.sh
, respectivamente.

3.1. Generación del proyecto Maven

Podemos generar el arquetipo ya existente de ‘Primeros Pasos con Flink en Java’ que nos proporciona el propio Apache. Es posible utilizar el modo interactivo de generación de arquetipos de Maven para tener más control en el proceso. La siguiente orden es más directa:

mvn archetype:generate /
    -DarchetypeGroupId=org.apache.flink/
    -DarchetypeArtifactId=flink-quickstart-java /
    -DarchetypeVersion=0.10.0

Sin embargo, vamos a crear nosotros mismos el proyecto Maven con el fin de que nos quede un poco más claro qué es lo que estamos haciendo.

Nosotros trabajamos con el IDE Eclipse, por lo que crearemos un nuevo proyecto Maven sin arquetipo. El contenido interesante del

pom.xml
que utilizaremos será el siguiente:
pom.xml
[...]
<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<flink.version>0.10.0</flink.version>
	<jdk.version>1.7</jdk.version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.apache.flink</groupId>
		<artifactId>flink-java</artifactId>
		<version>${flink.version}</version>
	</dependency>
	<dependency>
		<groupId>org.apache.flink</groupId>
		<artifactId>flink-streaming-java</artifactId>
		<version>${flink.version}</version>
	</dependency>
	<dependency>
		<groupId>org.apache.flink</groupId>
		<artifactId>flink-clients</artifactId>
		<version>${flink.version}</version>
	</dependency>
</dependencies>

<build>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-jar-plugin</artifactId>
			<version>2.5</version>
			<configuration>
				<archive>
					<manifestEntries>
						<Main-Class>com.autentia.tutoriales.flink.flink_intro.StreamingWordCount</Main-Class>
					</manifestEntries>
				</archive>
			</configuration>
		</plugin>

		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<version>3.1</version>
			<configuration>
				<source>${jdk.version}</source>
				<target>${jdk.version}</target>
			</configuration>
		</plugin>
	</plugins>
</build>
[...]

3.2. WordCount en streaming

Dentro de Big Data y originalmente en el mundo de data mining, WordCount o conteo de palabras dentro de un texto es el equivalente al HelloWorld en la programación clásica. Con esto jugaremos en ésta introducción a Apache Flink.

Según la documentación de Flink, los pasos básicos a la hora de desarrollar una aplicación con el framework son los siguientes:

  • Instanciar un entorno de ejecución, que puede ser para procesamiento batch con
    ExecutionEnvironment.getExecutionEnvironment()
    o para streaming con
    StreamExecutionEnvironment.getExecutionEnvironment()
    .
  • Obtener el conjunto de datos con el que trabajaremos, en forma de
    DataSet
    para batching o
    DataStream
    para streaming.
  • Realizar transformaciones sobre el conjunto de datos como
    filter()
    ,
    flatMap()
    o
    join()
    .

Nuestra implementación de WordCount con Flink en Java es la siguiente:

[...]
public class StreamingWordCount {

    public static void main(String[] args) throws Exception {

        if (args.length != 3) {
            System.err.println("USO:\nbatStreamingWordCount <host> <puerto> <ruta_salida>");
            return;
        }

        String host = args[0];
        Integer puerto = Integer.parseInt(args[1]);
        String rutaSalida = args[2];

        // instanciamos el entorno de ejecución
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // obtenemos el stream de datos del host provisto
        DataStream<String> texto = env.socketTextStream(hostName, port);

        DataStream<Tuple2<String, Integer>> conteo =
        // realizamos un mapeo de las palabras a tuplas de la forma (palabra, 1)
        text.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {

            public void flatMap(String valor, Collector<Tuple2<String, Integer>> salida) throws Exception {
                String[] palabras = valor.toLowerCase().split("\\W+");

                for (String palabra : palabras) {
                    if (palabra.isEmpty() == false) salida.collect(new Tuple2<String, Integer>(palabra, 1));
                }
            }
        })
        // utilizamos la palabra (posición 0) como clave, y sumamos las frecuencias (posición 1)
        .keyBy(0).sum(1);

        conteo.print();

        // execute program
        env.execute("WordCount en Streaming");
    }

}

Como hicimos en el tutorial de Apache Spark, vamos a utilizar el binario

netcat
para montar un servidor en local que “stremee” por el puerto 9999:
nc -lk 9999

Para esta primera prueba arrancamos la aplicación directamente desde Eclipse, que nos monta un mini-clúster independiente. En los argumentos del programa especificaremos “localhost” como host y “9999” como puerto de escucha. La salida inicial:

11/19/2015 09:27:12	Job execution switched to status RUNNING.
11/19/2015 09:27:12	Source: Socket Stream(1/1) switched to SCHEDULED
11/19/2015 09:27:12	Source: Socket Stream(1/1) switched to DEPLOYING
11/19/2015 09:27:12	Flat Map(1/2) switched to SCHEDULED
11/19/2015 09:27:12	Flat Map(1/2) switched to DEPLOYING
11/19/2015 09:27:12	Flat Map(2/2) switched to SCHEDULED
11/19/2015 09:27:12	Flat Map(2/2) switched to DEPLOYING
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(1/2) switched to SCHEDULED
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(1/2) switched to DEPLOYING
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(2/2) switched to SCHEDULED
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(2/2) switched to DEPLOYING
11/19/2015 09:27:12	Flat Map(2/2) switched to RUNNING
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(1/2) switched to RUNNING
11/19/2015 09:27:12	Flat Map(1/2) switched to RUNNING
11/19/2015 09:27:12	Source: Socket Stream(1/1) switched to RUNNING
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(2/2) switched to RUNNING

En el anterior log podemos observar como el

JobManager
se encarga de levantar dos instancias de
TaskManager
e ir planificando y desplegando cada una de las operaciones (
flatMap
o
Keyed Aggregation
, p.e.) en estos trabajadores.

Si en

netcat
escribimos, por ejemplo, los primeros párrafos del “Don Quijote” de Cervantes, obtendremos la siguiente salida de nuestra aplicación.
[...]
1> (acordarme,1)
1> (mucho,1)
1> (galgo,1)
1> (corredor,1)
1> (una,1)
1> (olla,1)
1> (duelos,1)
1> (quebrantos,1)
1> (los,2)
1> (lentejas,1)
1> (los,3)
1> (domingos,1)
1> (las,1)
1> (tres,1)
1> (adarga,1)
1> (roc,1)
1> (n,1)
2> (antigua,1)
1> (ca,1)
2> (flaco,1)
1> (carnero,1)
2> (que,2)
1> (salpic,1)
[...]

Es conveniente observar como, por ejemplo, la palabra “los” se repite dos veces con diferentes frecuencias. Este comportamiento es debido a la división del trabajo entre los

TaskManager
, así como a la frecuencia de muestreo en la lectura del socket.

3.3. Haciendo uso del cliente web de Flink

Vamos a repetir la misma prueba de concepto del WordCount en streaming, pero desplegando la aplicación a través del cliente web que nos proporciona el propio framework.

Como la salida de la aplicación ya no podrá ser por consola al estar siendo ejecutado de forma interna por el cliente de Flink, hemos reemplazado la impresión por pantalla al fichero en la ruta que se nos indique en los argumentos:

// conteo.print();
conteo.writeAsCsv(rutaSalida, WriteMode.OVERWRITE);

Lo primero que debemos hacer es arrancar tanto el framework en standalone como el cliente web:

$FLINK_HOME/bin/start-local.sh
$FLINK_HOME/bin/start-webclient.sh

Dirijámonos ahora a localhost:8080, donde se encuentra levantado el cliente web:

flink_client_start

Subimos nuestro .jar, especificamos host, puerto y ruta al resultado:

flink_client_arguments

Una de las características que hace a este cliente web especial es que es capaz de mostrarnos el grafo DataFlow optimizado de nuestra aplicación:

flink_client_dataflow

Una vez ejecutemos la aplicación, se nos redirige al gestor de trabajos de Apache Flink, donde podemos ver el estado de cada ejecución:

flink_manager

La salida ahora es por el fichero que específicamos en los argumentos:

[...]
por,2
dar,1
una,1
mano,1
de,11
coces,1
al,1
traidor,1
de,12
galal,1
n,5
al,2
ama,1
que,7
ten,2
a,10
y,7
aun,1
a,11
su,3
sobrina,1
de,13
a,12
adidura,1
[...]

No nos olvidemos de parar las instancias levantadas de Flink cuando acabemos de operar con ellas:

$> $FLINK_HOME/bin/stop-local.sh
$> $FLINK_HOME/bin/stop-webclient.sh

4. Conclusiones

Apache Flink está recibiendo muchísima atención desde sus fases más tempranas de desarrollo gracias a esa filosofía de cambio radical en la forma de pensar en el procesado y analíticas en el mundo del Big Data, por lo que conviene estar atentos a su evolución en los próximos tiempos.


5. Referencias

Introducción a Vaadin

$
0
0

En este tutorial vamos a ver una introducción a Vaadin, que es un framework de la capa de vista en Java

Índice de contenidos


1. Introducción

Vaadin es un framework de desarrollo de SPA que permite escribir el código de dichas aplicaciones en Java o en cualquier otro lenguaje soportado por la JVM 1.6+. Esto permite la programación de la interfaz gráfica en lenguajes como Java 8, Scala o Groovy, por ejemplo.

Uno de las características diferenciadores de Vaadin es que, contrario a las librerías y frameworks de JavaScript típicas, presenta una arquitectura centrada en el servidor, lo que implica que la mayoría de la lógica es ejecutada en los servidores remotos. Del lado del cliente, Vaadin está construido encima de Google Web Toolkit, con el que puede extenderse.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro Intel Core 2 Duo 8GB RAM.
  • Sistema Operativo: Mac OS Yosemite 10.10
  • Entorno de desarrollo: Eclipse Mars 4.5
  • JRE 1.8.0_51
  • Apache Maven 3.3.3
  • Apache Tomcat 8.0.24

3. Creación de un proyecto Vaadin

Para empezar a desarrollar aplicaciones en Vaadin se nos presentan dos alternativas muy sencillas:

  • Utilizar los plugins específicos para IDEs como NetBeans, Eclipse o IntelliJ (utilizan como gestor de dependencias Ivy)
  • Crear directamente un proyecto con Maven haciendo uso del arquetipo de aplicación con Vaadin.

En este tutorial se seguirá la segunda alternativa, ya que tenemos un mejor control de lo que estamos haciendo, reduciendos los niveles de “magia” considerablemente.

Empezamos creando el proyecto Vaadin en una terminal con Maven. Para ello utilizamos la última versión del arquetipo de aplicación de este tipo:

$ mvn archetype:generate \
    -DarchetypeGroupId=com.vaadin \
    -DarchetypeArtifactId=vaadin-archetype-application \
    -DarchetypeVersion=7.5.6

Iremos suministrando al asistente de Maven los datos de grupo y nombre de artefacto de nuestro proyecto. En mi caso:

  • Grupo: com.autentia.vaadin.
  • Artefacto: tutorial_vaadin.
  • Version: la por defecto, 1.0-SNAPSHOT.

Si todo ha ido bien, podremos cambiar al directorio creado y realizar una instalación de la aplicación Vaadin:

$ cd tutorial_vaadin
$ mvn clean install

Dependiendo de si ya tenemos las dependencias en nuestro repositorio local de Maven, ésta última operación será más o menos larga. Lo que se llevará la mayoría de tiempo es la compilación de los módulos de Widgets de Vaadin personalizados (esqueleto

MyAppWidgetSet
auto-generado por el arquetipo).

Como alternativa, podemos hacer uso del objetivo verify, que construirá y pasarás las pruebas a nuestro proyecto:

$ mvn verify

Vamos a arrancar la aplicación con el servidor Jetty empotrado en el arquetipo de Vaadin:

Si ahora abrimos un navegador y accedemos a

http://localhost:8080/
, veremos la aplicación de muestra de Vaadin ejecutándose:
$ mvn jetty:run

intro_vaadin_1


3.1. Importación a un proyecto Eclipse

Importemos ahora el proyecto en Eclipse mediante “Import Maven Project”.

NOTA: Si al realizar la importación del proyecto, Eclipse nos muestra un mensaje de error parecido a “Plugin execution not covered by lifecycle configuration […] update-theme” lo que me ha funcionado a mí es añadir el objetivo en conflicto (en este caso, update-theme) dentro de lifecycle-mapping de org.eclipse.me2:

pom.xml
<plugin>
    <groupId>org.eclipse.m2e</groupId>
    <artifactId>lifecycle-mapping</artifactId>
    [...]
    <goals>
        <goal>resources</goal>
        <goal>update-widgetset</goal>
        <goal>compile</goal>
        <goal>compile-theme</goal>
        <goal>update-theme</goal>
    </goals>
    [...]

4. Fundamentos de la clase UI

Entre los ficheros fuente que nos ha generado el arquetipo Maven de Vaadin, el más interesante es

MyUI.java
:
MyUI.java
@Theme("mytheme")
@Widgetset("com.autentia.vaadin.MyAppWidgetset")
public class MyUI extends UI {

    @Override
    protected void init(VaadinRequest vaadinRequest) {
        final VerticalLayout layout = new VerticalLayout();
        layout.setMargin(true);
        setContent(layout);

        Button button = new Button("Click Me");
        button.addClickListener(new Button.ClickListener() {
            @Override
            public void buttonClick(ClickEvent event) {
                layout.addComponent(new Label("Thank you for clicking"));
            }
        });
        layout.addComponent(button);

    }

    @WebServlet(urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true)
    @VaadinServletConfiguration(ui = MyUI.class, productionMode = false)
    public static class MyUIServlet extends VaadinServlet {
    }
}

Repasemos las partes más interesantes de este código:

  • Líneas 1-2: Mediante anotaciones, le decimos a Vaadin qué temas y conjunto de Widgets vamos a hacer uso. También podremos decirle el título de la página con
    @Title
    , por ejemplo.
  • Línea 3: La superclase
    UI
    representa una página que se mostrará en el cliente.
  • Línea 6: El método
    init()
    es el punto de entrada de toda subclase de UI. Su objetivo básico es la inicialización y configuración de la interfaz gráfica visible al usuario. Es ejecutada en cada refresco de página.
  • Línea 7: Como muchos frameworks de este tipo, la organización de la interfaz viene dada por la definición de layouts.
  • Líneas 11-12: Fácil definición de funciones de callback para la comunicación asíncrona con el servidor.
  • Líneas 22-24: Ya que hace uso de la especificación 3.0+ de Servlets, no existe necesidad de un archivo web.xml, si no que mediante anotaciones podemos exponer nuestra clase con una URI personalizada.

5. Layouts programáticos vs declarativos

A la hora de definir layouts, Vaadin nos permite elegir entre hacerlo mediante código Java (muy parecido a la definición con Swing) o haciendo uso de ficheros HTML específicos.

Por ejemplo, la creación de un layout de un formulario, en su versión programática sería algo parecido a lo siguiente:

SampleForm.java
public class SampleForm extends FormLayout {
    Button button1 = new Button("Click me", this::button1Action);
    Button button2 = new Button("DO NOT Click me", this::button2Action);
    TextField textField1 = new TextField("I am a TextField, nothing more");
    [...]
    public SampleForm() {
        configureComponents();
        buildLayout();
    }
    [...]
    private void configureComponents() {
        button1.setStyleName(ValoTheme.BUTTON_PRIMARY);
        button1.setClickShortcut(ShortcutAction.KeyCode.ENTER);
        setVisible(false);
    }

    private void buildLayout() {
        setSizeUndefined();
        setMargin(true);

        HorizontalLayout actions = new HorizontalLayout(button1, button2);
        actions.setSpacing(true);

        addComponents(actions, textField1);
    }
    [...]
}

La versión declarativa de este snippet constaría de un fichero HTML de definición de los espacios donde reside cada componente, la clase Java que lee dicho fichero y por último, el propio código de lógica del formulario.

SampleFormLayout.html
<v-form-layout size-auto margin>
    <v-horizontal-layout spacing>
        <v-button style-name="primary" _id="button1">Click me</v-button>
        <v-button _id="button2">DO NOT Click me</v-button>
    </v-horizontal-layout>
    <v-text-field caption="I am a TextField, nothing more" _id="textField1"></v-text-field>
</v-form-layout>
SampleFormLayout.java
@DesignRoot
public class SampleFormLayout extends FormLayout {
    protected Button button1;
    protected Button button2;

    public SampleFormLayout() {
        Design.read(this);
    }
}
SampleForm.java
public class SampleForm extends SampleFormLayout {
    [...]
    public SampleForm() {
        setVisible(false);
        button1.setClickShortcut(ShortcutAction.KeyCode.ENTER);

        button1.addClickListener(this::button1Action);
        button2.addClickListener(this::button2Action);
    }
[...]

El resultado de ambos es el mismo:

intro_vaadin_2


6. Conclusiones

Vaadin presenta una alternativa interesante a toda la fiebre de librerías y frameworks del lado del cliente con JavaScript para la creación de aplicaciones de una sola página, rápidas e interactivas, volviendo a la arquitectura del lado servidor. Bien vale que se le eche un ojo, ya que su comunidad no hace más que crecer.


7. Referencias

Referencias a objetos en Java: Soft, Weak, Phantom

$
0
0

En este tutorial veremos los tipos de referencia que mantiene la JVM a los objetos creados y cómo ayuda esto al Garbage Collector en su trabajo de limpieza.

Índice de contenidos


1. Introducción

El concepto de referencia está íntimamente ligado con el comportamiento del recolector de basura. Dependiendo de las referencias que tenga un determinado objeto, este podrá ser elegido para ser limpiado de memoria (referencia débil) o, por el contrario, puede seguir residiendo en el Heap (referencia fuerte).

Cada una de estas referencias denominadas débiles tienen una clase asociada dentro del paquete java.lang.Reference y existen desde la versión 1.2 del JDK.


2. Accesibilidad o Reachability

La propia documentación del paquete java.lang.ref define, en el apartado Reachability, los grados de accesibilidad de más a menos fuertes:

  • Un objeto es accesible de forma fuerte (strongly reachable) si se puede llegar a él sin la necesidad de atravesar ningún objeto de referencia. Un objeto recién creado es accesible de forma fuerte por el hilo que lo creó.
  • Un objeto es accesible de forma soft (softly reachable) si no es accesible de forma fuerte, pero sí a través de una cadena de
    SoftReferences
    .
  • Un objeto es accesible de forma débil (weakly reachable) si no es accesible de forma ni fuerte ni soft, pero sí a través de una cadena de
    WeakReferences
    . Cuando todas las referencias débiles a un objeto accesible de este tipo desaparecen, este objeto puede ser elegido para su finalización.
  • Un objeto es accesible de forma phantom (phantom reachable) si no es accesible de forma fuerte, soft o débil, ha sido finalizado y existe algún objeto de la clase
    PhantomReference
    apuntando al objeto.
  • Por último, un objeto es inaccesible (unreachable) y, por tanto, apto para ser liberado si no puede ser accesible de ninguna de las formas antes descritas.

3. Referencias Fuertes

Las referencias fuertes son las que nos podemos encontrar de forma natural en nuestro código, p.e.:

StringBuilder builder = new StringBuilder();

Este tipo de referencias hace que el objeto no sea elegible para liberarse o, dicho de otra manera: siempre que un objeto sea referenciado por una cadena de referencias fuertes, no puede ser limpiado del Heap.


4. Referencias Soft

De forma análoga a las referencias fuertes, se puede decir que el Garbage Collector puede liberar un objeto si este está referenciado únicamente por referencias débiles.

El grado más fuerte de este tipo de referencias son las SoftReferences. Los objetos con este tipo de referencias serán liberados únicamente cuando la JVM necesite desesperadamente más memoria.

Las

SoftReferences
se usan principalmente para implementar cachés simples sensibles al consumo de memoria mediante instancias directas de
SoftReference<T>
.

Dado el hecho de que mientras el objeto referenciado por una

SoftReference
sea accesible de forma fuerte esta referencia no será liberada, es posible implementar sistemas de cachés más complejas que, por ejemplo, impidan la finalización de las entradas que se más se han utilizado en los últimos accesos manteniendo referencias fuertes a dichas entradas y dejando a discreción del GC la liberación del resto.

4.1. SoftReferences y el Garbage Collector

Una de las preguntas que suelen surgir cuando uno busca información sobre objetos de referencia en Java y en particular sobre las SoftReference es: ¿cómo y cuándo decide el GC limpiar este tipo de referencias?

Para empezar, la JVM mantiene una variable de tiempo que guarda el momento (en milisegundos desde el inicio de la aplicación) de la última pasada del GC. A su vez, cada

SoftReference
tiene un campo de timestamp que indica cuándo ella o el objeto referenciado fueron accedidos por última vez. Esto permite una ordenación grosso modo de las
SoftReference
existentes: el tiempo transcurrido desde el último GC y cada acceso a las referencias.

Cuando un evento del GC se dispara, la decisión de limpiar una

SoftReference
se basa en dos factores:
  • Hace cuánto se produjo el último acceso a la referencia.
  • Cuánta memoria disponible queda.

Tomando los siguientes parámetros:

  • free_heap
    es la cantidad de espacio libre en el Heap en MB.
  • interval
    es el tiempo transcurrido entre la última recolección y el timestamp de la referencia.
  • ms_per_mb
    es un número constante de milisegundos que determina el tiempo a mantener una
    SoftReference
    por cada MB libre en el Heap.

Así, la decisión para limpiar una referencia viene dada por la condición:

interval <= free_heap * ms_per_mb

Por ejemplo, teniendo una

SoftReference
con un
timestamp
de 2000ms, la última pasada del GC fué hace 5000ms, el
ms_per_mb
es 1000 y el espacio libre en el Heap es 1MB:
5000 - 2000 <= 1 * 1000

Ya que 3000 > 1000, la referencia será limpiada.

Una última cosa a tener en cuenta es que las

SoftReferences
se mantienen siempre durante al menos una pasada del GC después de haber sido accedidas. Esto se debe a que para el cálculo de
interval
se usa el tiempo de la última recolección, no de la que se esté realizando en ese momento.

5. Referencias Débiles

Como hemos visto antes, un objeto que sólo tenga referencias débiles será liberado por el GC en la siguiente pasada de limpieza. Este tipo puede ser útil, por ejemplo, para almacenar metadatos.

Veamos un pequeño ejemplo para determinar la diferencia entre

SoftReference
y
WeakReference
(la referencia débil más fuerte).
Integer significado = new Integer("42");
WeakReference<Integer> referencia = new WeakReference<Integer>(significado);
significado = null;

Una vez que hemos desreferenciado la variable

significado
, ésta se convierte en apta para ser liberada ya que la única referencia a él es
referencia
, del tipo
WeakReference
.

Si ésta variable

referencia
fuera del tipo
SoftReference
, como hemos mostrado antes, la siguiente pasada del GC no finalizaría
significado
.

El ejemplo más conocido de uso de las referencias

WeakReference
es la clase WeakHashMap. Ésta clase tiene la particularidad de mantener el conjunto de claves mediante
WeakReferences
, lo que permite la liberación de entradas de forma automática cuando sus claves no se encuentran en uso.

5.1. Referencias Phantom

En el nivel más bajo desde el punto de las referencias débiles se encuentran las PhantomReferences. El significado y la diferencia de este tipo con el resto de las referencias débiles lo veremos en el siguiente apartado.


6. ReferenceQueues – Notificación del grado de accesibilidad

Es posible de manera programática ser avisado de cuando un objeto cambia su accesibilidad, registrando una referencia válida mediante las denominadas colas de referencias o ReferenceQueue en el momento en el que se crea dicha referencia.

En este ámbito es cuándo las PhantomReferences muestran su utilidad. Mientras que el resto de las referencias débiles (

WeakReference
y
SoftReference
) son liberadas antes de ser añadidas a una cola de referencias, las
PhantomReferences
deben ser sacadas de la cola en la que se ha registrado para marcarlas para ser liberadas.

7. Ciclo de vida de un objeto

El siguiente diagrama muestra el ciclo de vida básico por el que pasa un objeto, incluidas las transiciones entre referencias:

referencias-imagen1


8. Conclusión

Conocer la existencia y las diferencias entre los tipos de referencias que mantiene la JVM nos permite idear nuevas formas de trabajar con objetos que no tenemos porque estar manipulando constantemente, de tal manera que la huella que dejamos en memoria en nuestros programas sea lo más eficiente y ligera posible.


9. Fuentes


Visualización de rendimiento del Garbage Collector

$
0
0

En este tutorial se presentan una serie de ejemplos que demuestran el comportamiento del recolector de basura o Garbage Collector (GC) de Java en determinados escenarios.

0. Índice de contenidos

1. Introducción

En este tutorial se presentan una serie de ejemplos que demuestran el comportamiento del recolector de basura o Garbage Collector (GC) de Java en determinados escenarios que fuerzan al GC a limpiar el Heap de ejecución.

Además de los trozos de código sobre los que se ha trabajado, se invluyen capturas de pantalla de las salidas gráficas de las herramientas especializadas utilizadas durante el tutorial, como VisualVM con el plug-in VisualGC.

2. Entorno de desarrollo

  • MacBook Pro Intel Core 2 Duo 8GB RAM
  • SO: Yosemite
  • IDE: Eclipse Mars 4.5.0
  • Java JDK 1.7

Al IDE Eclipse se le ha instalado el plugin de VisualVM que permite la representación en tiempo real de espacio de memoria de la JVM, uso de procesador, carga del GC, etc. Conjunto a VisualVM, se ha añadido el plugin VisualGC que añade una serie de visualizaciones del recolector: espacio utilizado en Eden, en Survivor y en PermGen, entre otros.

Para más información sobre VisualVM, consúltese la página oficial.

3. Llamada explícita al GC

La forma más simple de forzar al colector a limpiar memoria es desreferenciando (haciendo apuntar a null) un objeto creado. Se ha sobreescrito el método Object#finalize para mostrar por consola que, efectivamente, se limpia cuando no hay referencias al objeto.

Es conveniente indicar que la llamada a Runtime#gc no garantiza al 100% que el GC haga una pasada de limpieza; es sólo una sugerencia del usuario a la JVM.

package es.autentia.gc_samples;

public class GCExplicito {

    public static void main(String[] args) {
        A a = new A("blanco");

        a = null;

        Runtime.getRuntime().gc();
    }

}

class A {
    private String color;

    public A(String color) {
        this.color = color;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(color + " eliminado");
    }
}

La salida que obtendremos por consola nos indicará que el objeto ha sido reciclado:

blanco eliminado

4. Referencias inalcanzables

Otro de los casos en los que el GC realiza una limpia del Heap es cuando existe una referencia circular entre objetos y no son alcanzables desde fuera.

En el siguiente caso se declaran tres objetos apuntándose entre ellos (mediante un campo de clase):

package es.autentia.gc_samples;

public class GCAmbito {

    GCAmbito t;
    int i;

    public GCAmbito(int i) {
        this.i = i;
    }

    public static void main(String[] args) {

        GCAmbito t1 = new GCAmbito(1);
        GCAmbito t2 = new GCAmbito(2);
        GCAmbito t3 = new GCAmbito(3);
        // Ningun objeto se debe reciclar

        t1.t = t2;
        t2.t = t3;
        t3.t = t1;
        // Ningun objeto se debe reciclar

        t1 = null;
        System.out.println("t1 = null");
        Runtime.getRuntime().gc();
        // No se debe reciclar: t3.t apunta aun a t1

        t2 = null;
        System.out.println("t2 = null");
        Runtime.getRuntime().gc();
        // No se debe reciclar: t3.t.t apunta aun a t2

        t3 = null;
        System.out.println("t3 = null");
        Runtime.getRuntime().gc();
        // Deben reciclarse los tres objetos: cada uno de ellos
        // apunta a otro de manera circular, sin ser referenciados
        // desde fuera

    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Finalizado objeto " + i);
    }
}

Como se desprende de los comentarios en código, cada uno de los objetos son desreferenciados en orden, pero no son liberados por el GC hasta que la referencia circular es inalcanzable (t3 es desreferenciado).

La salida por consola nos demuestra éste comportamiento (nótese que el orden de reciclaje no es determinista y depende de muchos factores como la carga actual de la JVM):

t1 = null
t2 = null
t3 = null
Finalizado objeto 1
Finalizado objeto 3
Finalizado objeto 2

5. Estructura del Heap de Java – Hotspot Heap

Estructura Hotspot VM

Conviene recordar cómo se produce el envejecimiento de los objetos en Java mediante la denominada “Weak Generational Hypothesis”. Para más información sobre el funcionamiento y los tipos de GC de Java conviene echarle un vistazo al tutorial sobre Garbage Collector de Jose Luis Rodríguez.

En el tipo de GC por defecto de la JVM (Serial GC), la política de limpieza y envejecimiento sigue estos pasos:

  • Cualquier objeto nuevo es colocado en Eden.
  • Cuando el Eden se llena, se realiza un reciclaje menor (minor garbage collection).
  • Los objetos con referencias se mueven al primer espacio de supervivientes (S0). El resto serán eliminados en la siguiente limpieza de Eden.
  • En el siguiente reciclaje menor, Eden sufre el mismo proceso antes descrito. Los objetos no referenciados son eliminados de Eden; sin embargo, los objetos viejos tanto de Eden como de S0 son movidos al segundo espacio de supervivientes (S1). Una vez terminada la copia, tanto Eden como S0 son limpiados.
  • En el siguiente reciclaje menor, los espacios de supervivientes (S0 y S1) se intercambian los roles y vuelven a repetir el proceso.
  • Cuando algún objeto de cualquier de los espacios de supervivientes alcanza un determinado tiempo de vida, es promocionado a la Old Generation.
  • Eventualmente, se producirá un reciclaje amplio (major garbage collection) que limpiará y compactará este espacio de la Old Generation.

Con este proceso en mente, el siguiente ejemplo ilustra cómo se produce este llenado y consecuente vaciado de ambas generaciones del Heap:

package es.autentia.gc_samples;

import java.util.ArrayList;

public class GCDesreferencia {

    public static void main(String[] args) {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        ArrayList arrayStrings = new ArrayList();

        for (int j = 0; j < 100000000; j++) {
            for (int i = 0; i < 1000000; i++) {
                try {
                    Thread.sleep(1);

                    String s = i + j + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce"
                        + " viverra semper ipsum, in mollis sem pellentesque eu. Donec ferment"
                        + "um erat sed mattis facilisis. Praesent porttitor purus at libero ti"
                        + "ncidunt laoreet. Nam posuere dolor tellus, vel laoreet sem rutrum eu"
                        + ". Cras posuere dui sit amet lectus pellentesque accumsan. Nulla auctor cu"
                        + "rsus dolor a efficitur. Cras auctor ultrices fringilla. Mauris pretium ris"
                        + "us orci, vitae scelerisque lorem blandit at. Nunc semper pulvinar facilisis."
                        + " Etiam bibendum blandit velit. Mauris commodo sapien in velit pretium"
                        + ", nec varius nulla porttitor. Aliquam congue facilisis convallis. Pel"
                        + "lentesque enim orci, sagittis sed cursus porta, dictum eget massa. ";

                    arrayStrings.add(s);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }
}

A modo de resumen, la aplicación espera unos 10 segundos antes de comenzar la funcionalidad para dar tiempo a lanzar VisualVM empezar a monitorizar el GC. Dentro de los bucles se ha añadido la creación de una serie de Strings lo suficientemente grandes como para que el GC tenga que saltar en un tiempo de simulación razonable. Un pequeño retardo de 1 ms entre cada creación permite un espaciado temporal mayor y aumenta la legibilidad de las gráficas generadas por VisualGc que veremos más adelante.

La creación de un array que vaya almacenando los String generados permite que los objetos sean movidos a la Old Gen cuando se cumpla el criterio de envejecimiento.

Entre los muchos flags que podemos añadir a la ejecución del programa para visualizar el trabajo del GC, los básicos son los siguientes:

  • -XX:+PrintGCDetails: Obtenemos mensajes cada vez que actúa el GC.
  • -Xloggc:<fichero.log>: Redirige el resultado de los mensajes del GC a un fichero externo en vez de por salida estándar.

Si nos fijamos en el log generado, podemos diferenciar dos partes:

[...]

0.822: [GC [PSYoungGen: 38912K->5104K(72704K)] 66872K->63644K(159744K), 0.1651680 secs] [Times: user=0.20 sys=0.03, real=0.16 secs]
0.988: [Full GC [PSYoungGen: 5104K->0K(72704K)] [ParOldGen: 58540K->63275K(151040K)] 63644K->63275K(223744K) [PSPermGen: 2631K->2631K(21504K)], 1.4727490 secs] [Times: user=2.58 sys=0.01, real=1.47 secs]
3.172: [GC [PSYoungGen: 67584K->5120K(72704K)] 130859K->119483K(223744K), 0.2449040 secs] [Times: user=0.29 sys=0.05, real=0.24 secs]
3.418: [Full GC [PSYoungGen: 5120K->0K(72704K)] [ParOldGen: 114363K->116723K(265728K)] 119483K->116723K(338432K) [PSPermGen: 2631K->2631K(21504K)], 1.3292800 secs] [Times: user=2.42 sys=0.01, real=1.33 secs]
5.192: [GC [PSYoungGen: 67584K->5120K(91136K)] 184307K->176627K(356864K), 0.2886140 secs] [Times: user=0.31 sys=0.04, real=0.28 secs]

[...]

La primera parte detalla el estado de ocupación del Heap de forma periódica cada vez que actúa el GC, indicando los tres espacios en KB: la Young, la Old y la Permanent Generation, así como los tiempos en los que se han tomado las medidas. El indicador GC se imprime cuando se realiza un minor collection y Full GC cuando es un major collection.

[...]

Heap
 PSYoungGen      total 238592K, used 194302K [0x00000007d5500000, 0x00000007f3e00000, 0x0000000800000000)
  eden space 97280K, 54% used [0x00000007d5500000,0x00000007d88bf808,0x00000007db400000)
  from space 141312K, 100% used [0x00000007e6400000,0x00000007eee00000,0x00000007eee00000)
  to   space 180224K, 0% used [0x00000007db400000,0x00000007db400000,0x00000007e6400000)
 ParOldGen       total 450560K, used 352260K [0x0000000780000000, 0x000000079b800000, 0x00000007d5500000)
  object space 450560K, 78% used [0x0000000780000000,0x0000000795801030,0x000000079b800000)
 PSPermGen       total 21504K, used 2637K [0x000000077ae00000, 0x000000077c300000, 0x0000000780000000)
  object space 21504K, 12% used [0x000000077ae00000,0x000000077b093790,0x000000077c300000)

Al final del log se detalla el nivel de ocupación del Heap de manera estratificada al finalizar la ejecución. En la Young Generation o PSYoungGen, from space se refiere a S0 y to space a S1.

6. Tipos de GC y su rendimiento comparado

Mediante las herramientas VisualVM con el plugin VisualGC, podemos realizar una pequeña comparación de rendimiento y comportamiento de los diferentes tipos de GC al ejecutar el ejemplo anterior.

6.1. GC Serie

Flag de ejecución: -XX:+UseSerialGC

GC Serie

Este primer ejemplo usa el recolector en serie, que sigue el proceso descrito en el apartado del Hotspot de Java. En la zona resaltada de las gráficas se puede apreciar cómo cada vez que alguna de las zonas de supervivientes son vaciadas, o bien pasa a la siguiente zona, o, si se cumplen los criterios de envejecimiento, se copian a la Old Gen.

6.2. GC en Paralelo con 2 hilos

Flags de ejecución: -XX:+UseParallelGC -XX:ParallelGCThreads=2

GC Paralelo

En el caso del recolector en paralelo, aunque las pasadas del GC sigan siendo Stop-the-world, al aprovechar el uso de varias CPUs, los minor collections pueden ser mucho más frecuentes, resultando en tamaños de Eden y Survivor (Young Generation) sensiblemente mayores que el GC en serie. Como los minor collections son más frecuentes, es necesario un major collection antes que con el método anterior.

6.3. GC con Concurrent Mark Sweep (GCCMS)

Flags de ejecución: -XX:+UseConcMarkSweepGC

GC Concurrent Mark Sweep

Como los minor collection pueden ser intercalados entre los major no se producen incrementos en el tamaño de los espacios de Survivor. Esta concurrencia permite unos major collections mucho más frecuentes (4 limpiezas vs. 0-1 de los métodos anteriores).

6.4. Garbage First GC (GCG1)

Flags de ejecución: -XX:+UseG1GC

GC Garbage First

Ya que Garbage First GC (G1) mantiene siempre una serie de áreas del Heap libres y controladas, las pausas para reciclaje serán menores y se evitará por completo el llenado de cualquiera de las generaciones. Este fenómeno se hace evidente en las gráficas sacadas por VisualVM, en el que vemos que ni el espacio de Survivor (con G1 solo existe un espacio de este tipo) ni el de la Old Generation son limpiados en ningún momento.

7. Conclusión

Herramientas como los flags de ejecución de la JVM, como -verbosegc correspondientes al recolector de basura en conjunto con plugins de visualización de datos, como VisualVM + VisualGC, permiten recabar información sobre el rendimiento del GC en nuestras aplicaciones.

La elección del mejor tipo de GC para un determinado problema es una tarea compleja que debe ser sopesada tanto en términos de uso de memoria, como de frecuencia de dicho uso.

Monitorización y profiling con Java Mission Control y Java Flight Recorder

$
0
0

En este tutorial presentaremos un pequeño viaje por las funcionalidades más básicas de Java Mission Control y Java Flight Recorder como herramientas de monitorización y profiling de aplicaciones en Java

0. Índice de contenidos


1. Introducción

Java Mission Control (JCM a partir de ahora) es una herramienta de monitorización y gestión de aplicaciones Java propia de Oracle que minimiza el overhead habitual entre las herramientas de este tipo (menos del 1% del total de procesamiento).

Este bajo impacto en el rendimiento de las aplicaciones bajo monitorización se consigue gracias a que se alimenta de los datos que se generan de forma natural durante las optimizaciones dinámicas de la JVM. Aparte de minimizar este overhead de monitorización, este sistema de recolección de datos elimina el sesgo del observador, que se produce cuando las herramientas de este tipo alteran las características de ejecución de un sistema.

JMC está compuesto por la propia aplicación gráfica (JMC client) y una serie de plugins que se montan encima de la misma:

  • JVM Browser: Muestra las aplicaciones Java que se están ejecutando con sus respectivas JVMs. Cada una de las instancias de JVM se les denomina JVM Connection. Estas conexiones pueden ser locales por PID, o bien remotas por dirección de host.
  • JMX Console: Se conecta a una JVM en funcionamiento y recopila y presenta sus características en tiempo real, además de permitir la modificación de algunas de ellas en caliente gracias al uso de las Managed Beans. Desde esta consola es posible incluso definir reglas que se ejecuten cuando se cumpla cierta condición (por ejemplo, mandar un correo electrónico cuando el uso de CPU exceda el 80% durante más de 3 segundos.
  • Java Flight Recorder (JFR): Recopila y guarda información detallada del rendimiento de aplicaciones para un análisis futuro. Puede ser utilizado como una herramienta de monitorización de rendimiento independiente de JMC, pero cuando se usa en conjunción con esta presenta la información capturada de forma fácil en tablas, gráficas, etc.

Aunque pueda parecer “una herramienta más” de monitorización de JVM sumada a la ya extensa colección de dichas herramientas (jstat, jmx, VisualVM, HPJmeter, etc…), lo cierto es que resulta en la agrupación de las cosas buenas de cada una de ellas de forma clara y sencilla, con una interfaz gráfica muy amigable y con una capacidad de personalización tremenda.

Para simular carga de la máquina y obtener resultados más o menos “reales” se ha utilizado la suite de benchmarking del proyecto Opensource DaCapo Benchmark Suite.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro Intel Core 2 Duo 8GB RAM.
  • Sistema Operativo: Mac OS Yosemite 10.10.3
  • Entorno de desarrollo: Eclipse Mars 4.5.0
  • Java JDK 1.7
  • DaCapo Benchmark Suite 9.12

3. Ejecución de JCM y JFR

Ya que JMC viene dentro del JDK de Oracle desde la versión 7u40, podemos ejecutarlo directamente desde la terminal (si tenemos añadido al $PATH el directorio del JDK):

$ jmc &

Si queremos hacer uso de JFR para el profiling de aplicaciones, es necesario activar las siguientes dos opciones de la JVM:

-XX:+UnlockCommercialFeatures -XX:+FlightRecorder

Hay que tener que la JVM, por defecto, solo permite obtener trazas de ejecución en puntos seguros, por lo que podría darse el caso de que dichas trazas no sean correctas en ciertos casos. Es por ello que la documentación de JFR recomienda el uso de dos parámetros adicionales de la JVM, que no pueden ser modificados en caliente:

-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints

Es también posible arrancar directamente JFR al ejecutar la aplicación que queremos monitorizar desde la línea de comandos, lo que permite ahorrarnos técnicas para parar la ejecución hasta que tengamos toda la suite de monitorización lista (dormir el hilo principal de ejecución durante X segundos o realizar varias pasadas de calentamiento):

java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=record-miapp.jfr MiApp

Si lo que queremos es monitorizar procesos en máquinas remotas, será necesario añadir los siguientes flags (que son específicos de JMX):

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false


4. Consola JMX

Si hacemos click derecho sobre una de las conexiones en el JVM Browser podremos iniciar una nueva sesión de JMX Console:

Veamos cada una de las pestañas disponibles más interesantes án esta vista.


4.1. Overview

En esta pestaña se nos muestra un pequeño resumen de las métricas más comunes (uso de memoria y porcentaje de CPU). Nada especial aquí, sin embargo podremos añadir las métricas que JMX tenga disponible haciendo click en los signos de cada uno de los paneles.


4.2. MBean Browser

Desde aquí podremos acceder y visualizar todos los MBeans disponibles que nos proporciona JMX, con sus valores actuales y con la capacidad de poder añadirlos a las gr´ficas presentadas en la pantalla Overview.

Una funcionalidad que destaca de este panel es la capacidad de modificar ciertos parámetros de los MBeans en caliente haciendo doble click sobre los atributos con fondo amarillo (de esta manera JMC marca los que son modificables).

Al seleccionar un MBean, tendremos acceso a una serie de pestañas que nos permite ver información sobre sus atributos, operaciones (como llamar al garbage collector en el MBean “Memory”), notificaciones declaradas dado un comportamiento (lo veremos en el apartado de “Triggers”) y los metadatos del MBean.


4.3. Triggers

Una de las funcionalidades que, desde mi punto de vista, son más interesantes de la JMX Console de JMC es la capacidad de definir notificaciones de tal manera que cuando se cumpla una determinada condición sobre un MBean (o varios), se ejecute una determinada acción, entre las cuales se encuentran:

  • Popup de alerta.
  • Loggearlo en un archivo.
  • Enviar un correo electrónico con la información del evento.
  • Lanzar una sesión de Flight Recorder, tanto continua como limitada.
Además, en cada Trigger podremos definir entre qué intervalo de tiempos estará activa dicha condición, de tal manera que, por ejemplo, solo salte en días entre semana.

4.4. Threads

Desde aquí podremos ver todos los hilos de ejecución de nuestra aplicación, con información sobre su estado actual (ejecutándose, esperando, etc…), además de si se encuentran en una situación de deadlock (para lo cual hay que activar la detección de deadlocks en el checkbox superior), así como el total de memoria reservada para su contexto (que también deberemos de activar en la casilla “Allocation”).

Al seleccionar cualquiera de los hilos, podemos acceder a sus trazas de ejecución en el panel inferior, funcionalidad típica en herramientas de monitorización. Esta selección puede ser múltiple.


5. Java Flight Recorder

Como hemos visto en el apartado sobre ejecución de JMC y JFR es necesario añadir una serie de flags a la JVM de la aplicación que estemos monitorizando para que JFR pueda extraer estadísticas (si no lo hacemos, JMC nos avisará y podremos copiar estas opciones directamente del cuerpo de la ventana de error), recordémoslas:

-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
-XX:+UnlockDiagnosticVMOptions
-XX:+DebugNonSafepoints

Una vez ejecutada la aplicación, nos vamos a JMC y hacemos click derecho sobre la conexión correspondiente y seleccionamos “Start Flight Recording…”, lo que nos abrirá un wizard de configuración del JRF:

En este primer paso podremos dar un nombre al registro, así como elegir entre la recopilación en un determinado tiempo, que pueden ser segundos (unidad “s”), minutos (unidad “m”), horas (unidad “h”), días (unidad “d”) o semanas (unidad “w”). Una vez terminado este lapso de tiempo, el cliente de JMC se encargará automáticamente de guardar y abrir dicho registro.

La otra opción es hacer una recopilación continua (de la cual podremos definir un tamaño o tiempos máximos) y que no será guardado hasta que paremos la grabación. Este tipo de registro es necesario abrirlo explícitamente.

En el siguiente paso podremos personalizar una serie de preferencias sobre el muestreo de estadísticas, entre las que nos encontramos el comportamiento del garbage collector, el nivel de detalle del muestreo de los métodos, la frecuencia de los dumps de hilos o los umbrales de detección de actividad en operaciones de E/S (ficheros y sockets).

Además, podremos elegir si durante la recopilación queremos recoger estadísticas detalladas sobre el Heap, la carga de clases o la reserva de memoria dinámica.

Cuando abramos la grabación (que será un archivo con extensión *.jfr), nos encontraremos con la pantalla principal de JFR:

Desde aquí podremos acceder a las distintas partes principales del informe (a la izquierda, en forma de pestañas principales), además de las subsecciones en la parte inferior. En la parte superior se nos muestra la línea temporal de toda la grabación, en la cual, si arrastramos los bordes de la ventana, podremos definir un rango de tiempos que nos interese.

Es conveniente tener activada la opción “Synchronize Selection”, en la parte superior de la línea temporal, para que todas las pestañas del informe compartan el mismo instante de tiempo.

En esta primera sección podemos visualizar estadísiticas básicas sobre carga de CPU y uso de memoria en forma de gráfica la primera y en medidores de agujas. La subsección “JVM Information” nos hace un pequeño resumen sobre la configuración de la JVM y los flags con los que ha sido ejecutado.

Veamos lo que nos ofrece el resto de secciones:


5.1. Memory

Esta sección nos da información sobre todo lo que tenga que ver con la gestión de memoria que haya realizado la aplicación durante su ejecución con alto nivel de detalle.

En la primera subsección “Overview”, podemos visualizar la evolución del Heap con respecto al tiempo en forma de gráfica. En la parte superior podemos elegir qué métricas de tamaño queremos ver. Normalmente, como las aplicaciones no usan toda la memoria física de la máquina sobre la que está corriendo la JVM, será conveniente ocultar estas dos series de la gráfica para no aumentar en demasía el eje de ordenadas (en mi ejecución, el heap no crecía más de 600 MB).

Garbage Collections / GC Times

En estas subsecciones tenemos una información mucho más detallada sobre el comportamiento del GC, incluidas las duraciones de las pausas Stop-the-world (representadas como barras verticales rojas).

No sólo eso, si no que si seleccionamos uno de los ítems de la tabla “Garbage Collections”, se nos presentará una serie de métricas tomadas durante dicha pasada del GC:

  • Duración total de la pausa del GC.
  • Razón de la llamada al GC.
  • Tiempo empleado en cada una de las fases de la recolección.
  • Tipo de referencias de los objetos liberados.
  • Estado del Heap antes y después de la recolección.
Allocations

Esta pestaña es especialemnte útil (sobre todo “Allocation in New TLAB”) para comprobar qué clases son instanciadas más frecuentemente, cuánta memoria consumen y en qué partes del código son reservadas. Tenemos dos tipos distintos de análisis:

  • Reservas por clase.
  • Reservas por hilo.

En mi caso, casi el 90% de las reservas de memoria son del tipo char[] (cadenas de caracteres). Si hago click sobre este tipo de objetos, puedo ver que el 87% de dichas reservas se realizan en la clase StandardTokenizerImpl y que se concentran en los hilos “Query0” y “Query1”, si cambio a la vista “Allocation by Thread”.


5.2. Code

Esta sección es útil para realizar optimizaciones sobre el uso y tiempo de ejecución de CPU. En la subsección “Overview” tenemos dos vistas diferenciadas:

  • Hot Packages: Lista ordenada de los paquetes que están consumiendo más tiempo de CPU. Útil cuando estemos utilizando librerías de terceros y quieres tener resumenes separados de dichas librerías, de tu propio código y del propio JDK.
  • Hot Classes: Misma clasificación que la vista anterior, pero a nivel de clases.
Hot Methods / Call Tree

En estas vistas podemos ver una lista con los métodos que más utilizan más CPU, incluido el árbol de llamadas realizado hasta dichos métodos. Este ánalisis es el típico que podremos encontrar en cualquier herramienta de monitorización. Cuando queramos mejorar el rendimiento de nuestra aplicación, éste debería ser el primer sitio que visitemos en busca de cuellos de botella.

En ambas subsecciones tendremos la capacidad de filtrar los resultados por nombre de la traza, número total de llamadas o porcentaje de uso de CPU.

Exceptions

Aunque no sea clave en posibles mejoras de optimización de rendimiento debido a que, en principio, las excepciones deberían ser siempre usadas con sumo cuidado y solo en el caso de que sea estrictamente necesario. Sin embargo, ya que la gestión de excepciones suele ser una operación bastante costosa, si sospechamos que estamos manejando muchas excepciones, en esta vista podremos comprobar cuales son las más comunes e intentar llevarlas a un mínimo.

Compilations / Class Loading

Las últimas dos pestañas son bastante autoexplicativas y nos permiten comprobar los tiempos de compilación (tanto estándar cómo OSR) la primera y la carga y descarga de memoria de clases.


5.3. Threads

Esta sección detalla el comportamiento y rendimiento de los hilos de ejecución de la JVM. La principal ventaja de fijarse en estos resultados será la capacidad de detectar hilos bloqueantes en deadlock, latencias de espera y acceso a los dumps de trazas de ejecución de dichos hilos.


5.4. I/O

Esta sección nos permite inspeccionar la entrada/salida de nuestra aplicación tanto en fichero como por socket: número de lecturas y escrituras, tamaños y tiempos. Usualmente las métricas que nos interesará aquí son posibles operaciones sobre ficheros no esperados, múltiples aperturas/cierres del mismo fichero o que los tamaños sean muy pequeños.

Si se está haciendo uso de un disco SSD se recomienda poner el umbral de detección de eventos de lectura (“File Read Threshold”, en el wizard inicial de JFR) a 1ms, ya que, de lo contrario, podríamos perdernos muchos datos.

Las cuatro subsecciones son prácticamente idénticas en cuanto a visualizaciones, cada una encargándose de lectura/escritura de ficheros y sockets, por lo que solo nos detendremos en la primera de ellas.

Tenemos a nuestra disposición tres listas diferentes según el elemento: por fichero/socket (By File/Host), por hilo (By Thread) y por evento (By Event, que es una caombinación de las dos anteriores).

Si seleccionamos uno de los ítems, podremos inspeccionar una gráfica con las cantidades de bytes leídos/escritos por unidad de tiempo, lo que nos dará una idea de si es posible bajar, por ejemplo, los tiempos de lectura implementando una caché de datos.


5.5. Events

Esta sección nos permite visualizar todos, y repito TODOS, los eventos que se han producido durante la ejecución del programa y nos los muestre en porcentajes con respecto al tiempo total, ni por carga de CPU ni por uso de memoria.

Tenemos dos vistas separadas: por productores de carga arriba y por tipos de evento abajo. Si queremos añadir a las gráficas nuevos tipos de eventos, podremos hacerlo seleccionando uno de los disponibles en la lista de la izquierda.

Graph

La subsección más interesante desde mi punto de vista es esta, que nos permite ver en forma de línea temporal como se han comportado cada uno de los hilos del sistema; en qué momentos se encuentran ejecutándose, cuándo esperando y, sobre todo, nos permite ver de forma gráfica cuándo estamos viviendo una situación de deadlock u otros tipos de bloqueos entre hilos.

En el hilo principal también podemos ver las pausas que realiza JFR para la recopilación de estadísticas.


6. Conclusiones

Java Mission Control es una herramienta estupenda para monitorizar y hacer profiling de aplicaciones que puedan ser mejoradas desde el punto de vista del rendimiento. Se suma al ya extenso abanico de herramientas de este tipo, siendo sus puntos fuertes el que esté incluido en el JDK desde la versión 1.7.40 y la buena presentación de los resultados y facilidad de uso en operaciones complejas, así como el mínimo overhead producido por el efecto del observador.

Java Flight Recorder es un gran añadido a JMC que, aunque no introduzca muchas funcionalidades revolucionarias en el mundo del profiling de aplicaciones Java, su facilidad de uso, personalización y su aproximación a las métricas más complicadas de tal manera que sean medianamente fáciles de entender tanto a iniciados como a expertos en estos temas.


7. Referencias

La directiva static en Java

$
0
0

En este tutorial veremos para qué sirve la directiva static en Java, a qué elementos se puede aplicar, sus características, beneficios y desventajas que pueden tener.

0. Índice de contenidos


1. Introducción

En este tutorial veremos para qué sirve la directiva static en Java, a qué elementos se puede aplicar, sus características, beneficios y desventajas que pueden tener.

Se propondrán una serie de ejemplos y snippets de código para ilustrar cada uno de los puntos mencionados.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • MacBook Pro Intel Core 2 Duo 8GB RAM
  • SO: Yosemite
  • IDE: Eclipse Mars 4.5.0
  • Java JDK 1.7

3. ¿Qué es static en Java?

Una clase, método o campo declarado como estático puede ser accedido o invocado sin la necesidad de tener que instanciar un objeto de la clase. Uno de los ejemplos típicos de uso de métodos y variables estáticas es la clase java.lang.Math:

public class MathTest {

    public static void main(String[] args) {
        double floorOfPi = Math.floor(Math.PI);
        System.out.println(floorOfPi);
    }
}

Cuya salida será el suelo matemático del número Pi:

3.0

4. Campos estáticos

Los campos de una clase declarados como estáticos son inicializados en el momento en que se carga la clase en memoria, respetando el orden de declaración. Los campos estáticos no pueden ser accedidos desde un contexto no estático; este comportamiento resultará en un error en tiempo de ejecución.

En conjunción con la directiva final (que evita la modificación del campo), las constantes de clase son definidas de esta manera.

Gracias a que las variables estáticas son compartidas entre todos los objetos de una clase, es posible implementar el patrón Singleton, guardando una referencia de la instancia:

public class SingletonTest {

    private static SingletonTest instance = null;

    protected SingletonTest() {
    }

    public static SingletonTest getInstance() {
        if (instance == null) {
            instance = new SingletonTest();
        }

        return instance;
    }
}

En este caso, el constructor por defecto se oculta con el modificador protected (que podría ser también private) para evitar la instanciación explícita desde fuera del contexto de la clase. Sin embargo, si la construcción es costosa y la clase es poco utilizada, el rendimiento se verá degradado.

Hay que tener en cuenta que, al igual que las variables transient, los campos estáticos no son serializadas. De esta manera, en el proceso de deserialización, los campos estáticos tomarán su valor por defecto (0.0 para los float, null para objetos, etc…).


5. Métodos estáticos

Debido a que los métodos estáticos son enlazados en tiempo de compilación mediante static binding usando la información del tipo, no es posible realizar sobreescritura (override) de métodos.

class Padre {

    public static void metodoEstatico() {
        System.out.println("Invocación desde el padre.");
    }
}

class Hijo extends Padre {

    public static void metodoEstatico() {
        System.out.println("Invocación desde el hijo.");
    }
}

public class BindingTest {

    public static void main(String[] args) {
        Padre p = new Hijo();
        p.metodoEstatico();
        Hijo h = new Hijo();
        h.metodoEstatico();
    }
}
Salida:
Invocación desde el padre.
Invocación desde el hijo.

En la salida podemos comprobar como el método que se invocará vendrá determinado por el tipo desde donde se invoque, aunque los objetos sean del mismo tipo (de la clase Hijo en este caso).

Otra de las consecuencias del static binding es que los métodos estáticos no pueden ser declarados como abstract, por el mismo motivo que la imposibilidad de la sobrecarga de métodos.

Uno de las características que produce más dolores de cabeza al tratar con métodos y variables estáticas es que, debido a su naturaleza, no son seguras para la programación con hilos. Es necesario hacer uso de las directivas synchronized de los métodos y de las variaciones atómicas de los tipos nativos con los campos.


6. Clases y bloques estáticos

Las clases internas pueden ser declaradas estáticas, de tal manera que aumenten la cohesión de la clase que las engloba. Hay que tener en cuenta que, como el resto de clases internas, el compilador crea un fichero .class por cada una de estas clases. Un ejemplo típico de clases internas estáticas son los comparadores de objetos:

public class Objeto {

    private int campo1;
    private String campo2;

    public Objeto(int campo1, String campo2) {
        this.campo1 = campo1;
        this.campo2 = campo2;
    }

    private static class ObjetoComparator implements Comparator<Objeto> {

        @Override
        public int compare(Objeto o1, Objeto o2) {
            return o2.campo1 - o1.campo1;
        }

    }
}

Es posible declarar bloques de código como estáticos, de tal manera que sean ejecutados cuando se cargue la clase. Este tipo de bloques se conocen como bloques de inicialización estáticos (static initializer block). Si no se declara un bloque de este tipo de forma explícita, el compilador Just-in-Time combina todos los campos estáticos en un bloque y los ejecuta durante la carga de clases. Aunque estos bloques no puedan lanzar checked exceptions, las unchecked pueden ocurrir, resultando en un ExceptionInInitializerError.

public class Objeto {

    private static int campo1;

    static {
        campo1 = 10;
    }
}

Los bloques de inicialización pueden ser, además, sustituidos por métodos estáticos:

public class Objeto {

    private static int campo1 = inicializaCampo();

    private static int inicializaCampo() {
        return 10;
    }

}

Como se observa en los ejemplos anteriores, esta funcionalidad permite dotar de cierta lógica la inicialización de variables de clase estáticas sin necesidad de incluirlo en su constructor.


7. Imports estáticos

Una de las características incluidas en Java 5 fué la capacidad de importar los métodos y variables estáticas de un módulo y acceder a ellos como si hubieran sido declarados en la propia clase. Es especialmente útil, y mejora la legibilidad, cuando se están definiendo test unitarios, ya que la mayoría de los métodos de aserción de JUnit son estáticos.

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class ImportEstático {

    @Test
    public void test() {
        assertEquals(1, 1);
    }

}

8. Conclusiones

La directiva static permite el acceso a métodos y variables de clase sin la necesidad de instanciar un objeto de dicha clase, permitiendo la inicialización de forma cómoda y durante la carga de clase. Además, los import estáticos mejoran la legibilidad de nuestro código, así como las clases estáticas internas la cohesión.


9. Referencias

Clustering de Apache Tomcat

$
0
0

En este tutorial veremos cómo crear un clúster de instancias de Apache Tomcat, haciendo uso de la extensión mod_jk de Apache Web Server.

0. Índice de contenidos


1. Introducción

Una de las extensiones de Apache Web Server que nos permite implementar un cluster de servidores Tomcat y su correspondiente balanceador de carga es mod_jk, antes conocido como mod_jserv.

Durante este tutorial se explicará como realizar una instalación desde cero de un clúster de servidores Apache Tomcat, la motivación para implementar éste tipo de arquitectura y una pequeña prueba con una aplicación sencilla que nos permita observar el balanceador de carga en funcionamiento.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • MacBook Pro Intel Core 2 Duo 8GB RAM
  • SO: Yosemite 10.10.5
  • IDE: Eclipse Mars 4.5.0
  • JDK 1.7
  • Máquina Virtual: VMWare Fusion 7 corriendo Ubuntu Server 14.04

3. Motivación

Supongamos que tenemos un sistema en producción en una sóla máquina física. Si por cualquier motivo dicho sistema parase de dar servicio (fallo en el hardware, una aplicación bloquea el sistema operativo o hay una subida de tensión y la máquina se apaga), nadie podría acceder a nuestras aplicaciones./p>

¿Cuál podría ser la opción para aumentar la disponibilidad de nuestro sistema?

Una de las ideas básicas, y es la que tendremos en cuenta durante el tutorial, es la replicación de nuestro servidor de aplicaciones. De esta manera, si uno de estos servidores cae, otro tomará su lugar para seguir dando servicio a nuestro sistema.

A este concepto de tener una serie de componentes disjuntos que, desde el exterior, parezcan comportarse como si fueran uno único se le denomina clustering.

Sin embargo, ahora se nos presenta un nuevo problema: si tenemos n instancias de nuestro servidor replicado, ¿cómo sabemos a que dirección o puerto debemos mandar la petición? ¿Están todos los miembros o nodos del clúster disponibles? ¿De verdad debemos nosotros saber todas éstas cosas?

Otro de los componentes de un sistema de clustering es el denominado balanceador de carga o load balancer. Este componente es el que se expondrá a los posibles clientes del sistema, con una única dirección IP y puerto sobre el que escuchar. Él será el encargado de mantener la cuenta de los nodos sanos y los no disponibles, redirigiendo el tráfico a unos u otros.

En este tutorial, utilizaremos Apache Tomcat como servidor de aplicaciones (en realidad se trata de un servidor de servlets), una aplicación JSP de prueba y para el balanceo de carga usaremos Apache Web Server junto con la extensión mod_jk.


4. Instalación de Apache Tomcat, Apache Web Server (httpd) y mod_jk

En Ubuntu Server 14.04, tenemos disponible el gestor de paquetes apt-get, por lo que lo usaremos para hacer la instalación de casi todos los componentes, por su facilidad de uso y delegación de responsabilidades que nos ofrece. En determinados casos realizaremos la descarga manual del software por temas de claridad.


Apache Tomcat

Vamos a la página de descarga de Apache Tomcat 8 y nos descargamos el fichero comprimido con el core del servidor.

Abrimos una terminal y descomprimimos y copiamos el contenido del fichero comprimido a donde deseemos, en nuestro caso en /usr/share/tomcat:

tar zxvf apache-tomcat-<version>.tar.gz
mv apache-tomcat-<version> /usr/share/tomcat

Vamos a arrancar el servidor Tomcat para probar que todo ha salido bien:

cd /usr/share/tomcat
bin/startup.sh

Deberíamos ver la siguiente salida por consola:

Using CATALINA_BASE:   /usr/share/tomcat
Using CATALINA_HOME:   /usr/share/tomcat
Using CATALINA_TMPDIR: /usr/share/tomcat/temp
Using JRE_HOME:        /usr
Using CLASSPATH:       /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar
Tomcat started.

No solo eso, si desde un navegador accedemos a

localhost:8080
veremos como Tomcat nos sirve su gestor.

new-tomcat

Paremos ahora la ejecución del servidor, ya que realizaremos una serie de cambios en su configuración:

bin/shutdown.sh

Apache Web Server (httpd)

La forma más sencilla de instalar Apache en Ubuntu Server es hacer uso del gestor de paquetes APT:

apt-get install apache2

Iniciemos el servidor Apache:

/etc/init.d/apache2 start

Si ahora accedemos a

localhost:80
, veremos la página de bienvenida de Apache HTTPD.

new-apache

Paremos el servidor:

/etc/init.d/apache2 stop

Conexión mod_jk

Para poder realizar el balanceo de carga, el servidor Apache necesita una conexión específica. Entre las opciones disponibles tenemos las extensiones mod_jk y mod_proxy.

En este tutorial usaremos mod_jk, ya que aunque su uso sea ligeramente más complicado (mod_proxy viene dentro del paquete de Apache HTTP desde la versión 2.2) nos permite tener un control mucho más amplio del balanceo de carga.

La instalación es muy sencilla:

apt-get install libapache2-mod-jk

Comprobamos que se ha instalado correctamente:

ls /usr/lib/apache2/modules/mod_jk.so

Al instalar la extensión, se nos generarn una serie de ficheros que serán los que debemos modificar (lo veremos en su apartado correspondiente):

  • /etc/apache2/mods-enabled/jk.conf: Fichero de configuración de la extensión donde podremos definir los parámetros generales de comportamiento.
  • /etc/apache2/mods-enabled/jk.load: Apunta al servicio instalado para cargarlo cuando se arranca el Apache.
  • /etc/libapache2-mod-jk/workers.properties: Especificación de los nodos del clúster, comportamiento y rutas permitidas.
  • /var/log/apache2/mod_jk.log: Log de ejecución de los eventos de mod_jk.

5. Múltiples instancias de Tomcat

Debido al diseño modular y segregado de la implementación de Apache Tomcat, es posible tener en una misma máquina física múltiples instancias con su propio espacio de aplicaciones y configuraciones.

En el directorio donde tengamos instalado el Tomcat (en el caso de este tutorial, y como hemos visto, en /usr/share/tomcat) tendremos las siguientes carpetas:

  • bin: ejecutables y scripts del Tomcat.
  • conf: ficheros de configuración como los nombres de servidor, puertos para cada tipo de servicio, etc.
  • logs: logs de ejecución del servidor.
  • temp: los ficheros temporales que necesite el Tomcat se crearán aquí.
  • webapps: aquí irán todas las aplicaciones desplegadas en el servidor.
  • work: Tomcat escribirá aquí todo lo que necesite durante la ejecución, como el código generado de los servlets JSP o las sesiones serializadas durante el reinicio del servidor.

Vamos a crear un directorio donde descansarán los ficheros necesarios para cada instancia de Tomcat:

mkdir /usr/tmp/tomcat-instances
cd /usr/tmp/tomcat-instances

Los directorios necesarios para ejecutar una instancia independiente son conf, temp, webapps y logs. Los copiaremos del directorio base de Tomcat a una nueva carpeta que denominaremos tomcat-instance1:

mkdir tomcat-instance1
cp -R /usr/share/tomcat/conf /usr/share/tomcat/temp /usr/share/tomcat/webapps /usr/share/tomcat/logs tomcat-instance1

En el archivo de configuración conf/server.xml se especifican los puertos de cada conexión que usará la instancia. Cada una de las instancias de Tomcat debe tener los siguientes puertos diferentes a todos los demás, como mínimo, para poder funcionar:

  • Shutdown port: Puerto sobre el que se quedar esperando para la señal de apagado.
  • HTTP port: Puerto que entiende HTTP. Es el que se expone al cliente.
  • AJP port: Puerto sobre el que se realizará la comunicación entre componentes para mod_jk mediante el protocolo AJP.

Así, si abrimos el fichero de configuración del servidor de la primera instancia, los puertos que debemos modificar son los siguientes:

tomcat-instance1/conf/server.xml
[...]
<Server port="8005" shutdown="SHUTDOWN">
[...]
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />
[...]
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
[...]

Con una herramienta como vim o nano modificaremos estos valores para que no entre en conflictos con otras instancias:

  • Shutdown: 6005
  • HTTP: 6080
  • AJP: 6009

Con esto ya tendríamos preparada la primera instancia de independiente de Tomcat. Repliquemos este árbol de ficheros en una nueva instancia tomcat-instance2:

cp -R tomcat-instance1 tomcat-instance2

A esta nueva instancia le asignaremos los siguientes puertos:

  • Shutdown: 7005
  • HTTP: 7080
  • AJP: 7009

Lo único que nos queda por hacer para tener la configuración preparada de las instancias de Tomcat son los scripts de arranque y apagado de cada una de ellas. Tomcat utiliza 5 variables de entorno a la hora de arrancar: CATALINA_HOME, CATALINA_BASE, CATALINA_TMPDIR, JRE_HOME/JAVA_HOME y CLASSPATH.

De todas ellas, las que tendremos que tener en cuenta para el clustering son:

  • CATALINA_HOME: Apunta a la ruta donde tengamos instalado el Tomcat. Desde aquí se puede acceder a las carpetas bin y lib. Se le da valor de forma automática.
  • CATALINA_BASE: Resulta un complemento de CATALINA_HOME que apunta a las carpetas conf y webapps. Ésta variable es la que deberemos modificar dinámicamente para iniciar una u otra instancia.

Para hacernos las cosas un poco más fciles crearemos una serie de scripts de arranque y apagado de las instancias en bash. Éstos scripts deberan residir en la carpeta base tomcat-instances.

Empezemos con el script de arranque:

startup-instance.sh
#!/bin/bash

NUM_OF_TOMCATS=2

# cambio de directorio al nativo de Tomcat
cd /usr/share/tomcat/bin

if [ $# -eq 0 ]; then
    echo "No args"
else
    if [ $1 = "all" ]; then
        for i in `seq 1 $NUM_OF_TOMCATS`; do
            export CATALINA_BASE=/usr/tmp/tomcat-instances/tomcat-instance$i
            ./startup.sh
        done
    else
        export CATALINA_BASE=/usr/tmp/tomcat-instances/tomcat-instance$1
        ./startup.sh
    fi
fi

Como se puede observar, recibe un único argumento de entrada (el resto, si los hubiera, son ignorados) que determina el número de instancia que queremos levantar. Podemos utilizar la cadena “all” para levantar todas las instancias.

Hay que acordarse siempre de darle permisos de ejecución al script. Si todo ha ido bien, al ejecutar este script, tendremos dos instancias separadas del Tomcat, una sirviendo en el puerto 6080 y otra en el 7080:

chmod +x startup-instance.sh
./startup-instance.sh all

multiple-tomcats

El script de apagado es análogo al de arranque:

shutdown-instance.sh
#!/bin/bash

NUM_OF_TOMCATS=2

# cambio de directorio al nativo de Tomcat
cd /usr/share/tomcat/bin

if [ $# -eq 0 ]; then
        echo "No args"
else
    if [ $1 = "all" ]; then
        for i in `seq 1 $NUM_OF_TOMCATS`; do
            export CATALINA_BASE=/usr/tmp/tomcat-instances/tomcat-instance$i
            ./shutdown.sh
        done
    else
        export CATALINA_BASE=/usr/tmp/tomcat-instances/tomcat-instance$1
        ./shutdown.sh
    fi
fi
chmod +x shutdown-instance.sh

Como comodidad adicional, podemos crear un script de reinicio de instancias que llame a los dos anteriores:

restart-instance.sh
#!/bin/bash

if [ $# -eq 0 ]; then
    echo "No args"
else
    ./shutdown-instance.sh $1
    ./startup-instance.sh $1
fi
chmod +x restart-instance.sh

Si ejecutamos este script de reinicio y no tenemos instancias que apagar nos saltará una excepción del Tomcat, pero proseguirá su ejecución sin problemas. Por ejemplo:

Using CATALINA_BASE:   /usr/tmp/tomcat-instances/tomcat-instance2
Using CATALINA_HOME:   /usr/share/tomcat
Using CATALINA_TMPDIR: /usr/tmp/tomcat-instances/tomcat-instance2/temp
Using JRE_HOME:        /usr
Using CLASSPATH:       /usr/share/tomcat/bin/bootstrap.jar:/usr/share/tomcat/bin/tomcat-juli.jar
ago 21, 2015 10:19:50 PM org.apache.catalina.startup.Catalina stopServer
GRAVE: Could not contact localhost:7005. Tomcat may not be running.
ago 21, 2015 10:19:50 PM org.apache.catalina.startup.Catalina stopServer
GRAVE: Catalina.stop:
java.net.ConnectException: Conexión rehusada
        at java.net.PlainSocketImpl.socketConnect(Native Method)
        at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:339)
        at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
        at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
        at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
        at java.net.Socket.connect(Socket.java:579)
        at java.net.Socket.connect(Socket.java:528)
        at java.net.Socket.<init>(Socket.java:425)
        at java.net.Socket.<init>(Socket.java:208)
        at org.apache.catalina.startup.Catalina.stopServer(Catalina.java:450)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:606)
        at org.apache.catalina.startup.Bootstrap.stopServer(Bootstrap.java:400)
        at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:487)

6. Balanceo de carga con mod_jk

El conector mod_jk permite la comunicación entre servidores Tomcat y Apache haciendo uso del protocolo propio Apache JServ Protocol (AJP).

Dependiendo de la instalación que hayamos hecho tanto de Apache como de mod_jk, los archivos de configuración del balanceador variarán en localización, pero no en contenido. Nosotros usaremos el árbol de directorios que nos crea automáticamente APT al instalar ambos.

La configuración más sencilla de un balanceador está compuesta de los siguientes elementos:

  • workers.properties: fichero de especificación de cada nodo trabajador del clúster, así como el comportamiento del balanceador (si es definido).
  • jk.conf: configuración del propio conector mod_jk (localización de los ficheros de workers, logs, intetrvalo de polling, etc…)

Empezemos por configurar el fichero /etc/libapache2-mod-jk/workers.properties:

workers.properties
worker.list=balancer,stat

worker.tomcat1.type=ajp13
worker.tomcat1.port=6009
worker.tomcat1.host=localhost

worker.tomcat2.type=ajp13
worker.tomcat2.port=7009
worker.tomcat2.host=localhost

worker.balancer.type=lb
worker.balancer.balance_workers=tomcat1,tomcat2

worker.stat.type=status

Hemos definido dos nodos denominados tomcat1 y tomcat2, uno por cada instancia del Tomcat. La información de cada nodo es la que definimos en su momento en cada uno de los ficheros server.xml de las instancias en el apartado del conector AJP, por ello es que el tipo sea “ajp13” o protocolo AJP v1.3.

Definimos, además, dos trabajadores del mod_jk, uno que se encargará de proporcionarnos una consola de monitorización de la extensión (trabajador “stat”) y otro que nos sirva del propio balanceador (trabajador “balancer”). A este último es al que le hemos añadido los dos nodos Tomcat como unidades de proceso.

El siguiente paso sería definir la configuración del Apache para que cargue la extensión mod_jk y sepa donde encontrar el fichero de workers, los configure según la ruta de entrada, etc. Ambos ficheros se encuentran en /etc/apache2/mods-enabled si lo hemos instalado mediante APT o en $APACHE_HOME/httpd.conf si lo hemos hecho de forma manual.

jk.load
# Carga el módulo mod_jk y lo denomina jk_module
LoadModule jk_module /usr/lib/apache2/modules/mod_jk.so
jk.conf
<IfModule jk_module>

    # Localización del fichero de trabajadores
    JkWorkersFile /etc/libapache2-mod-jk/workers.properties

    # Localización donde escribir los logs de mod_jk
    JkLogFile /var/log/apache2/mod_jk.log

    # Nivel de criticidad de los logs (trace,debug,info,warn,error)
    JkLogLevel debug

    # Fichero de memoria compartida
    JkShmFile /var/log/apache2/jk-runtime-status

    # Cada intervalo realiza tareas internas como
    # prueba de conexiones ociosas, redimensionamiento de
    # la reserva de conexiones, etc...
    JkWatchdogInterval 60

    # Definimos un nuevo virtual host en el puerto 80
    # (el de HTTP de Apache) especificando la ruta de la que
    # se encargar el mod_jk
    <VirtualHost *:80>
        ServerName  localhost
        ServerAdmin localhost
        JkMount /status stat
        JkMount /* balancer
    </VirtualHost>

</IfModule>

Guardamos los cambios en ambos ficheros y reiniciamos el servidor Apache para que sean efectivos:

apache2ctl restart

Si todo ha ido bien, podemos acceder al fichero /var/log/apache2/mod_jk.log para comprobar la creación y registro del balanceador y sus nodos, ya que hemos definido el nivel de detalle de dicho log a “debug”.

Si accedemos ahora a

localhost:80/status
nos encontramos con la herramienta de monitorización del balanceador de carga. Desde aquí podremos, además de comprobar el funcionamiento de mod_jk en tiempo real, modificar ciertos valores en caliente, como deshabilitar/eliminar nodos, añadir peso a un nodo o cambiar el número de reintentos antes de dar por caído uno de ellos, por ejemplo.

jk-status


7. Despliegue de la aplicación de prueba

Si ahora accedemos a

localhost:80/
será el balanceador el encargado de redirigirnos a uno de los Tomcat levantados:

jk-manager

Para que sea un poco más visual, vamos a instalar una pequeña aplicación web de prueba que nos muestre de forma sencilla cual de las instancias de Tomcat nos está sirviendo el balanceador. En este enlance se puede descargar el WAR ya empaquetado.

La aplicación es muy sencilla, con un único fichero JPA que muestra un mensaje de bienvenida y la ruta donde se encuentra:

<html>
<body>

    <h2>Hello World! from</h2>
    <h3><%=getServletContext().getRealPath("/")%></h3>

</body>
</html>

El despligue de la aplicación es terriblemente sencilla. Lo único que deberemos hacer es copiar pruebatomcat.war en la carpeta webapps de cada una de las instancias de Tomcat y, por si acaso, reiniciar ambas.

Ahora, si con un navegador accedemos a

localhost:80/pruebatomcat
observaremos como mod_jk nos muestra una de las dos instancias de Tomcat. Si refrescamos varias veces la página, veremos como dichos nodos se alternan, ya que, por defecto, la política que sigue el balanceador es Round Robin.

jk-balancer-1

jk-balancer-2


8. Conclusiones

La extensión mod_jk para la intercomunicación entre servidores Apache y Tomcat nos permite una forma sencilla de montar arquitecturas de disponibilidad no demasiado complicadas. Además, tiene la potencia de hacer sistemas mucho más complejos, debido a su capacidad de configuración.


9. Referencias

Introducción a Apache Spark – Batch y Streaming

$
0
0

Vamos a dar un pequeño paseo por el framework de computación paralela más popular del momento, Apache Spark.

Índice de contenidos


1. Introducción

Con todo el revuelo que se está montando alrededor del mundo del Big Data y con la asistencia al evento técnico Big Data Spain 2015 del equipo de Autentia (del que ya hicimos dos entradas sobre nuestras impresiones: primer día y segundo día), nos ha parecido una buena idea investigar y cacharrear con las herramientas que más sonaron durante las conferencias.

Comenzamos con el ya rey de la computación paralela, Apache Spark, proyecto de la fundación Apache que más interés está suscitando en el mundo del Big Data.


2. Apache Spark y la ciencia de datos

Apache Spark es un framework computación en clúster de código abierto que se diferencia con Hadoop (el otro gran proyecto de este tipo) en el uso de operaciones en memoria divididas en varias fases de procesamiento. Hadoop, como es bien conocido, hace uso del clásico modelo de programación publicado por Google MapReduce.

Debido al cambio de contexto que está sufriendo la ciencia de datos, el nuevo enfoque de Apache Spark pasando del procesamiento en batch al streaming, permitiendo la exploración de datos ad hoc, así como la aplicación más sencilla de algoritmos de machine learning es el factor de diferenciación básico de este proyecto.

No es el objetivo de este tutorial ahondar mucho en el funcionamiento interno del framework, sin embargo cabe destacar que, en su versión out-of-the-box, lleva incluidas librerías específicas para tratar datos estructurados (Spark SQL), capacidades para el streaming de datos (Spark Streaming), machine learning (MLib) y computación sobre grafos (GraphX).

intro_spark_imagen1


2.1. Arquitectura de la computación en clúster

Las aplicaciones para Spark se ejecutan como un grupo independiente de procesos en clústeres, coordinados por el objeto SparkContext. Más específicamente,

SparkContext
puede conectarse a gestores de clúster que son los encargados de asignar recursos en el sistema. Una vez conectados, Spark puede encargar que se creen ejecutores o executors encargados de la computación en los nodos del clúster. Los trozos de código propio de los que se encargan estos ejecutores son denominados tasks o tareas.

intro_spark_imagen2


2.1. Conceptos clave

Repasemos a continuación los conceptos básico sobre los que se apoya Apache Spark para realizar su magia.

SparkContext

Se trata del context básico de Spark, desde donde se crean el resto de variables que maneja el framework. Sólo un

SparkContext
puede estar activo por JVM.

En la shell interactiva de Spark es creada automáticamente bajo el nombre

sc
, mientras que en otros entornos es necesario instanciarlo explícitamente. La configuración de
SparkContext
puede ser definida mediante un bundle específico llamado SparkConf. RDDs

Viene de las siglas en inglés de Resilient Distributed Datasets o, en castellano, “conjuntos distribuidos y flexibles de datos”. Según la propia documentación de Spark, representan una colección inmutable y particionada de elementos sobre los que se puede operar de forma paralela.

Dependiendo del origen de los datos que contiene, diferenciamos dos tipos de RDDs:

  • Colecciones paralelizadas basadas en colecciones de Scala.
  • Datasets de Hadoop creados a partir de ficheros almacenados en HDFS.

Son posibles dos operaciones bien diferenciadas sobre un RDD, dependiendo del resultado final:

  • Transformaciones, que crean nuevos conjuntos de datos, como puede ser la operación
    map()
    que pasa cada elemento por una determinada función y devuelve un nuevo RDD con el resultado.
  • Acciones, que devuelven un valor al driver del clúster después de llevar a cabo una computación sobre el conjunto de datos. Un ejemplo de este tipo es la función
    reduce()
    , que agrega todos los elementos de un RDD mediante una función y devuelve el resultado.

3. Instalación del entorno Scala + Apache Spark


3.1. Scala

Nosotros vamos a hacer esta introducción a Spark con Scala (también soporta Python, R y Java), por lo que deberemos descargarlo de la página oficial y moverlo a una localización de nuestra elección:

wget http://downloads.typesafe.com/scala/2.11.7/scala-2.11.7.tgz
tar zxvf scala-2.11.7
sudo mv scala-2.11.7 /usr/local/scala-2.11.7
sudo ln -snf /usr/local/scala-2.11.7 /usr/local/scala
echo "export SCALA_HOME=/usr/local/scala" >> ~/.bash_profile

El directorio

$SCALA_HOME/bin
debería ser añadido también al PATH del sistema para poder acceder a los binarios desde cualquier localización.

Comprobemos que la instalación se ha realizado correctamente:

$> $SCALA_HOME/bin/scala -version
Scala code runner version 2.11.7 -- Copyright 2002-2013, LAMP/EPFL

3.2. Apache Spark

Nosotros vamos a bajarnos directamente el framework de Spark, aunque es posible (y preferible en entornos más serios) incluir el artefacto Maven en nuestro proyecto:

<dependency>
	<groupId>org.apache.spark</groupId>
	<artifactId>spark-core_2.10</artifactId>
	<version>1.5.1</version>
</dependency>

Vamos a la página oficial de descarga, eligiendo la última release. Aquí se nos presentan varias opciones:

  • Código fuente listo para compilar por nosotros mismos que permite el uso de la versión de Hadoop que deseemos. Nosotros elegiremos esta opción por su flexibilidad.
  • Pre-compilado a falta de la dependencia de Hadoop, que la debemos suministrar nosotros.
  • Pre-compilado con diferentes versiones de Hadoop integradas.

Descargamos el fichero y lo descomprimimos:

wget http://d3kbcqa49mib13.cloudfront.net/spark-1.5.1.tgz
tar zxvf spark-1.5.1.tgz

Lo movemos a la localización que deseemos como hicimos con Scala:

sudo mv spark-1.5.1 /usr/local/spark-1.5.1
sudo ln -snf /usr/local/spark-1.5.1 /usr/local/spark

Es hora de compilar el framework como nos dice en su fichero README.md:

sudo mvn -DskipTests clean package

Dependiendo del entorno en el que trabajemos, ésta compilación será más o menos costosa. En mi caso fueron unos 25 minutos. Por último, es conveniente declarar la variable de entorno

$SPARK_HOME
y añadir
$SPARK_HOME/bin
al
$PATH
del sistema, como hicimos en la instalación de Scala.

Para comprobar que todo se ha instalado de forma correcta, abramos una sesión en el REPL de Spark, que es con lo que trabajaremos durante todo el tutorial:

$> spark-shell
Using Spark's repl log4j profile: org/apache/spark/log4j-defaults-repl.properties
To adjust logging level use sc.setLogLevel("INFO")
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 1.5.1
      /_/

Using Scala version 2.10.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_79)
Type in expressions to have them evaluated.
Type :help for more information.

Spark context available as sc.
SQL context available as sqlContext.

scala>

4. Jugando con Datasets y Spark-shell


4.1. Procesado en batch

Algo que buscamos todos cuando se empieza a buscar información sobre Big Data son Datasets interesantes y grandes para jugar con ellos. En ésta pagina podéis encontrar una colección de enlaces a páginas con montones de datasets públicos listos para usarse.

Nosotros hemos elegido el de salarios de los empleados públicos de la ciudad de Chicago, en EEUU. Un vistazo rápido para saber con qué estamos trabajando (se ha formateado el fichero de CSV a TSV):

Name	Position	Title	Department	Employee	Annual Salary
AARON ELVIA J	WATER RATE TAKER	WATER MGMNT	88968.00
AARON JEFFERY M	POLICE OFFICER	POLICE	80778.00
AARON KARINA	POLICE OFFICER	POLICE	80778.00
AARON KIMBERLEI R	CHIEF CONTRACT EXPEDITER	GENERAL SERVICES	84780.00

Abramos una sesión del REPL de Spark, si no la tenemos abierta:

spark-shell

Lo primero que debemos hacer siempre es cargar el dataset en el contexto de Spark. Recordemos que

SparkContext
se instancia automáticamente bajo el objeto
sc
:
scala> val rawData = sc.textFile("Current_Employee_Names__Salaries__and_Position_Titles_formatted.csv")
rawData: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[1] at textFile at <console>:21

Definiremos ahora una clase de Scala que represente un empleado:

scala> case class Employee(name: String, positionTitle: String, department: String, annualSalary: Float)
defined class Employee

Ahora sí, carguemos un RDD con el dataset:

scala> val employees = rawData.map(_.split("\t")).map(r => Employee(r(0), r(1), r(2), r(3).toFloat))
employees: org.apache.spark.rdd.RDD[Employee] = MapPartitionsRDD[3] at map at <console>:25

Es fácil entender ésta última operación: con todos los datos crudos, dividimos mediante tabulaciones y mapeamos cada columna a su atributo correspondiente de la clase

Employee
.

Si estás siguiendo este tutorial a la vez que escribes en la spark-shell, comprobarás que ninguna operación está siendo llevada a cabo, si no que éstas órdenes se están añadiendo a un buffer que se ejecutará en cuanto sea necesario una computación sobre los objetos.

Contemos el número de registros en el RDD:

scala> employees.count()
res0: Long = 32181

Es en las acciones cuando todas las operaciones dependientes son evaluadas.

count()
es una acción, así como
reduce()
, como hemos explicado en los conceptos clave.

Podemos jugar un poco más con este RDD. Por ejemplo, vamos a obtener el número de empleados que trabajan en un departamento con la palabra “FIRE” en el nombre:

scala> employees.filter(em => em.department.contains("FIRE")).count()
res1: Long = 4875

También podemos mostrar estos registros con la función

collect()
:
scala> employees.filter(em => em.department.contains("FIRE")).collect()
res2: Array[Employee] = Array(Employee(ABBATEMARCO JAMES J,FIRE ENGINEER,FIRE,90456.0), Employee(ABDELLATIF AREF R,FIREFIGHTER (PER ARBITRATORS AWARD)-PARAMEDIC,FIRE,98244.0), Employee(ABDOLLAHZADEH ALI,FIREFIGHTER/PARAMEDIC,FIRE,87720.0), Employee(ABDULLAH DANIEL N,FIREFIGHTER-EMT,FIRE,91764.0), ....

4.2. Spark Streaming

Como hemos comentado antes, el core de Apache Spark viene con una serie de librerías para diferentes áreas de Big Data. Entre ellas se encuentra Spark Streaming, evidentemente para el manejo de streaming de datos.

Vamos a ilustrar un pequeño ejemplo de uso de ésta librería con el HelloWorld del mundo del Big Data: WordCount o conteo de palabras en un texto.

Como servidor de streaming, vamos a utilizar el binario de UNIX netcat en modo contínuo y emitiendo en el puerto 9999:

nc -lk 9999

En nuestra

spark-shell
en otra consola diferente del netcat escribiremos lo siguiente:
import org.apache.spark.streaming._
import org.apache.spark.streaming.StreamingContext._

val ssc = new StreamingContext(sc, Seconds(1))

val lines = ssc.socketTextStream("localhost", 9999)

Como se puede ver, hemos importado la librería de streaming. El contexto que necesitamos usar ahora es StreamingContext, con un ratio de muestreo de 1 segundo. A la instancia de este contexto es necesario pasarle un

SparkContext
; como estamos en la shell, será
sc
.

Por último, abrimos un socket de texto en localhost en el puerto 9999, que es donde se encuentra sirviendo el netcat.

Implementemos ahora el algoritmo de WordCount:

// Dividimos las palabras por espacios
val words = lines.flatMap(_.split(" "))

// Creamos pares clave-valor con cada palabra
val pairs = words.map(word => (word, 1))
// Agregamos todas las palabras coincidentes
val wordCounts = pairs.reduceByKey(_ + _)

wordCounts.print()

Lo único que nos falta por hacer es comenzar el servicio de streaming y dejarlo a la espera:

ssc.start()
ssc.awaitTermination()

Ahora podemos escribir lo que queramos en la consola del netcat y veremos el conteo funcionando en la spark-shell:

// En netcat
lorem ipsum dolor sit dolor lorem amet

// En spark-shell
(sit,1)
(ipsum,1)
(dolor,2)
(amet,1)
(lorem,2)

5. Conclusiones

Hemos visto una introducción muy básica de cómo trabajar con Apache Spark, escribiendo nuestros aplicaciones en Scala, tanto en el batch processing más tradicional, así como en streaming. El siguiente paso podría ser crear un proyecto en Scala con Maven y conectarnos a una HDFS gestionada por Hadoop, o utilizar la API de streaming de Yahoo! Finance para análisis en tiempo real.


6. Referencias

Introducción a Apache Flink

$
0
0

En este tutorial haremos una pequeña introducción a la motivación por usar Apache Flink, sus ideas principales y un ejemplo de proyecto Maven haciendo uso del framework.

0. Índice de contenidos.

flink_logo


1. ¿Qué es Apache Flink?

Se trata de un motor de procesamiento de streams o flujos de datos que proporciona capacidades de distribución de datos, comunicaciones y, muy importante, tolerancia a fallos a las computaciones.

Cuando hablamos de computación distribuida, lo primero que se nos viene a la cabeza es el celebérrimo Apache Hadoop, del cual tenéis muchos tutoriales en este mismo portal. Lo que hizo muy popular a este framework fue el uso del modelo de programación ideado por Google, MapReduce. El modelo seguido por Apache Flink fue pensado desde el principio como alternativa a MapReduce, lo que no significa que no pueda acceder y hacer uso tanto de HDFS (Hadoop Distributed File System) como de YARN (MapReduce v2).


1.1. Componentes fundamentales

flink_components

El ecosistema de Flink se sustenta sobre el núcleo de Flink o Flink Core en el que se encuentran todas las APIs y librerías de utilidades básicas, entre ellas el optimizador de programas Flink o el constructor de streams.

Las dos APIs principales sobre

flink-core
que determinan el comportamiento, así como el entorno de trabajo de los programas ejecutados son:
  • DataSet API: Entorno de ejecución en el que las transformaciones se ejecutan sobre conjuntos de datos tomados de fuentes más estáticas (como ficheros o bases de datos locales).
  • DataStream API: Muy parecido a la API de DataSet, pero con la gran diferencia de que los datos son tomados o recolectados desde fuentes dinámicas, como sockets o colas de mensajes.

Las últimas releases de Apache Flink vienen con una serie de librerías adicionales que forman las capacidades más fronterizas del Big Data:

  • FlinkML como librería de machine learning en continuo crecimiento en número de algoritmos implmentados.
  • Gelly como API de creación y análisis de grafos.
  • Table API, en sus versiones tanto por lotes como de flujos, que permiten utilizar expresiones con una sintaxis SQL.

1.2. Arquitectura básica

A la hora de ejecutar un proceso, el sistema de Flink levanta un

JobManager
, que hace las veces de coordinador de todo el sistema, y uno o más
TaskManager
, encargados de ejecutar partes del programa en paralelo.

Cuando se envía un programa al sistema es el optimizador el encargado de transformarlo en un DataFlow, ejecutable de forma paralela por los

TaskManager
, coordinados por el
JobManager
.

El siguiente esquema muestra esta arquitectura básica:

flink_architecture

2. Apache Flink y Apache Spark

La comparación de Flink con el rey actual de la computación distribuida, Apache Spark (del cual podéis encontrar una guía de primeros pasos con él en éste tutorial), se antoja necesaria.

En la superficie, Flink es muy parecido a Spark, sin embargo, internamente se trata de un modelo de programación enteramente centrado en el procesado de flujos o stream processing, a diferencia de Spark que consigue esta funcionalidad gracias a un enfoque de pequeños procesos por lotes procesados rápidamente y de forma ligera, lo que los desarrolladores llaman micro-batching. En éste enlace podéis encontran más información sobre éste tema.

Otras diferencias fundamentales entre ambos modelos son la abstracción con la que trabaja cada uno de ellos: Spark procesa RDDs o bloques de datos, mientras que Flink realiza un procesado registro a registro en tiempo real.

El hecho de que soporte técnicas de machine learning o procesado de grafos de forma nativa sobre su motor de streaming y que sea más fácil para entornos ya implantados el paso de streaming a batch que al revés, hacen de Apache Flink lo que parece el futuro en éste campo.


2.1. Pero, ¿qué es streaming?

Estamos hablando en estos párrafos continuamente de procesamiento tanto de flujos o streams como de lotes o batchs, pero, ¿qué quieren decir exactamente estos dos conceptos?

El streaming viene a ser una nueva forma de pensar en la infraestructura de datos. Muchas fuentes de datos representan series de eventos que se producen de forma continua (por citar casos, logs, transacciones, sensores, etc.). Tradicionalmente, estos eventos se agrupaban de forma periódica en batchs o series, salvándose para luego ser procesadas en conjunto.

Esta forma de proceder introducía latencias innecesarias entre la generación de los datos y la obtención de resultados, así como asumía de forma implícita que los datos deben estar “completos” en algún momento y, por lo tanto, pueden ser usados en ese instante para realizar predicciones precisas.

A partir de esta forma de procesar por lotes, el siguiente paso lógico en analíticas es aprovechar la naturaleza continua de la generación de datos para procesarlos de la misma manera: como flujos de datos o streams.


3. Nuestra primera aplicación con Flink

Lo primero de todo que necesitamos para comenzar a desarrollar con Flink es el framework completo que nos proporciona la página oficial. Lo podemos mover a la localización que queramos; en todo éste tutorial nos referiremos a dicho path como

$FLINK_HOME
. Todo este proceso de descarga, relocalización y creación de variables de entorno lo dejamos como ejercicio para el lector.

Nosotros trabajaremos con tres scripts principales dentro de

$FLINK_HOME/bin
para arrancar servicios de Flink, aunque existen muchos más:
  • $FLINK_HOME/bin/start-local.sh
    que nos permite arrancar una instancia local de la arquitectura de Apache Flink y que será la encargada de ejecutar nuestras aplicaciones. Se levanta una página web en nuestra máquina local en la dirección localhost:8081.
  • $FLINK_HOME/bin/start-webclient.sh
    que nos levanta una página web que nos permite desplegar y visualizar los grafos de funcionamiento de las aplicaciones Flink en localhost:8080.

Existen scripts análogos para parar dichos servicios,

$FLINK_HOME/bin/stop-local.sh
y
$FLINK_HOME/bin/stop-webclient.sh
, respectivamente.

3.1. Generación del proyecto Maven

Podemos generar el arquetipo ya existente de ‘Primeros Pasos con Flink en Java’ que nos proporciona el propio Apache. Es posible utilizar el modo interactivo de generación de arquetipos de Maven para tener más control en el proceso. La siguiente orden es más directa:

mvn archetype:generate /
    -DarchetypeGroupId=org.apache.flink/
    -DarchetypeArtifactId=flink-quickstart-java /
    -DarchetypeVersion=0.10.0

Sin embargo, vamos a crear nosotros mismos el proyecto Maven con el fin de que nos quede un poco más claro qué es lo que estamos haciendo.

Nosotros trabajamos con el IDE Eclipse, por lo que crearemos un nuevo proyecto Maven sin arquetipo. El contenido interesante del

pom.xml
que utilizaremos será el siguiente:
pom.xml
[...]
<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<flink.version>0.10.0</flink.version>
	<jdk.version>1.7</jdk.version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.apache.flink</groupId>
		<artifactId>flink-java</artifactId>
		<version>${flink.version}</version>
	</dependency>
	<dependency>
		<groupId>org.apache.flink</groupId>
		<artifactId>flink-streaming-java</artifactId>
		<version>${flink.version}</version>
	</dependency>
	<dependency>
		<groupId>org.apache.flink</groupId>
		<artifactId>flink-clients</artifactId>
		<version>${flink.version}</version>
	</dependency>
</dependencies>

<build>
	<plugins>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-jar-plugin</artifactId>
			<version>2.5</version>
			<configuration>
				<archive>
					<manifestEntries>
						<Main-Class>com.autentia.tutoriales.flink.flink_intro.StreamingWordCount</Main-Class>
					</manifestEntries>
				</archive>
			</configuration>
		</plugin>

		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<version>3.1</version>
			<configuration>
				<source>${jdk.version}</source>
				<target>${jdk.version}</target>
			</configuration>
		</plugin>
	</plugins>
</build>
[...]

3.2. WordCount en streaming

Dentro de Big Data y originalmente en el mundo de data mining, WordCount o conteo de palabras dentro de un texto es el equivalente al HelloWorld en la programación clásica. Con esto jugaremos en ésta introducción a Apache Flink.

Según la documentación de Flink, los pasos básicos a la hora de desarrollar una aplicación con el framework son los siguientes:

  • Instanciar un entorno de ejecución, que puede ser para procesamiento batch con
    ExecutionEnvironment.getExecutionEnvironment()
    o para streaming con
    StreamExecutionEnvironment.getExecutionEnvironment()
    .
  • Obtener el conjunto de datos con el que trabajaremos, en forma de
    DataSet
    para batching o
    DataStream
    para streaming.
  • Realizar transformaciones sobre el conjunto de datos como
    filter()
    ,
    flatMap()
    o
    join()
    .

Nuestra implementación de WordCount con Flink en Java es la siguiente:

[...]
public class StreamingWordCount {

    public static void main(String[] args) throws Exception {

        if (args.length != 3) {
            System.err.println("USO:\nbatStreamingWordCount <host> <puerto> <ruta_salida>");
            return;
        }

        String host = args[0];
        Integer puerto = Integer.parseInt(args[1]);
        String rutaSalida = args[2];

        // instanciamos el entorno de ejecución
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // obtenemos el stream de datos del host provisto
        DataStream<String> texto = env.socketTextStream(hostName, port);

        DataStream<Tuple2<String, Integer>> conteo =
        // realizamos un mapeo de las palabras a tuplas de la forma (palabra, 1)
        text.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {

            public void flatMap(String valor, Collector<Tuple2<String, Integer>> salida) throws Exception {
                String[] palabras = valor.toLowerCase().split("\\W+");

                for (String palabra : palabras) {
                    if (palabra.isEmpty() == false) salida.collect(new Tuple2<String, Integer>(palabra, 1));
                }
            }
        })
        // utilizamos la palabra (posición 0) como clave, y sumamos las frecuencias (posición 1)
        .keyBy(0).sum(1);

        conteo.print();

        // execute program
        env.execute("WordCount en Streaming");
    }

}

Como hicimos en el tutorial de Apache Spark, vamos a utilizar el binario

netcat
para montar un servidor en local que “stremee” por el puerto 9999:
nc -lk 9999

Para esta primera prueba arrancamos la aplicación directamente desde Eclipse, que nos monta un mini-clúster independiente. En los argumentos del programa especificaremos “localhost” como host y “9999” como puerto de escucha. La salida inicial:

11/19/2015 09:27:12	Job execution switched to status RUNNING.
11/19/2015 09:27:12	Source: Socket Stream(1/1) switched to SCHEDULED
11/19/2015 09:27:12	Source: Socket Stream(1/1) switched to DEPLOYING
11/19/2015 09:27:12	Flat Map(1/2) switched to SCHEDULED
11/19/2015 09:27:12	Flat Map(1/2) switched to DEPLOYING
11/19/2015 09:27:12	Flat Map(2/2) switched to SCHEDULED
11/19/2015 09:27:12	Flat Map(2/2) switched to DEPLOYING
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(1/2) switched to SCHEDULED
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(1/2) switched to DEPLOYING
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(2/2) switched to SCHEDULED
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(2/2) switched to DEPLOYING
11/19/2015 09:27:12	Flat Map(2/2) switched to RUNNING
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(1/2) switched to RUNNING
11/19/2015 09:27:12	Flat Map(1/2) switched to RUNNING
11/19/2015 09:27:12	Source: Socket Stream(1/1) switched to RUNNING
11/19/2015 09:27:12	Keyed Aggregation -> Sink: Unnamed(2/2) switched to RUNNING

En el anterior log podemos observar como el

JobManager
se encarga de levantar dos instancias de
TaskManager
e ir planificando y desplegando cada una de las operaciones (
flatMap
o
Keyed Aggregation
, p.e.) en estos trabajadores.

Si en

netcat
escribimos, por ejemplo, los primeros párrafos del “Don Quijote” de Cervantes, obtendremos la siguiente salida de nuestra aplicación.
[...]
1> (acordarme,1)
1> (mucho,1)
1> (galgo,1)
1> (corredor,1)
1> (una,1)
1> (olla,1)
1> (duelos,1)
1> (quebrantos,1)
1> (los,2)
1> (lentejas,1)
1> (los,3)
1> (domingos,1)
1> (las,1)
1> (tres,1)
1> (adarga,1)
1> (roc,1)
1> (n,1)
2> (antigua,1)
1> (ca,1)
2> (flaco,1)
1> (carnero,1)
2> (que,2)
1> (salpic,1)
[...]

Es conveniente observar como, por ejemplo, la palabra “los” se repite dos veces con diferentes frecuencias. Este comportamiento es debido a la división del trabajo entre los

TaskManager
, así como a la frecuencia de muestreo en la lectura del socket.

3.3. Haciendo uso del cliente web de Flink

Vamos a repetir la misma prueba de concepto del WordCount en streaming, pero desplegando la aplicación a través del cliente web que nos proporciona el propio framework.

Como la salida de la aplicación ya no podrá ser por consola al estar siendo ejecutado de forma interna por el cliente de Flink, hemos reemplazado la impresión por pantalla al fichero en la ruta que se nos indique en los argumentos:

// conteo.print();
conteo.writeAsCsv(rutaSalida, WriteMode.OVERWRITE);

Lo primero que debemos hacer es arrancar tanto el framework en standalone como el cliente web:

$FLINK_HOME/bin/start-local.sh
$FLINK_HOME/bin/start-webclient.sh

Dirijámonos ahora a localhost:8080, donde se encuentra levantado el cliente web:

flink_client_start

Subimos nuestro .jar, especificamos host, puerto y ruta al resultado:

flink_client_arguments

Una de las características que hace a este cliente web especial es que es capaz de mostrarnos el grafo DataFlow optimizado de nuestra aplicación:

flink_client_dataflow

Una vez ejecutemos la aplicación, se nos redirige al gestor de trabajos de Apache Flink, donde podemos ver el estado de cada ejecución:

flink_manager

La salida ahora es por el fichero que específicamos en los argumentos:

[...]
por,2
dar,1
una,1
mano,1
de,11
coces,1
al,1
traidor,1
de,12
galal,1
n,5
al,2
ama,1
que,7
ten,2
a,10
y,7
aun,1
a,11
su,3
sobrina,1
de,13
a,12
adidura,1
[...]

No nos olvidemos de parar las instancias levantadas de Flink cuando acabemos de operar con ellas:

$> $FLINK_HOME/bin/stop-local.sh
$> $FLINK_HOME/bin/stop-webclient.sh

4. Conclusiones

Apache Flink está recibiendo muchísima atención desde sus fases más tempranas de desarrollo gracias a esa filosofía de cambio radical en la forma de pensar en el procesado y analíticas en el mundo del Big Data, por lo que conviene estar atentos a su evolución en los próximos tiempos.


5. Referencias

Introducción a Vaadin

$
0
0

En este tutorial vamos a ver una introducción a Vaadin, que es un framework de la capa de vista en Java

Índice de contenidos


1. Introducción

Vaadin es un framework de desarrollo de SPA que permite escribir el código de dichas aplicaciones en Java o en cualquier otro lenguaje soportado por la JVM 1.6+. Esto permite la programación de la interfaz gráfica en lenguajes como Java 8, Scala o Groovy, por ejemplo.

Uno de las características diferenciadores de Vaadin es que, contrario a las librerías y frameworks de JavaScript típicas, presenta una arquitectura centrada en el servidor, lo que implica que la mayoría de la lógica es ejecutada en los servidores remotos. Del lado del cliente, Vaadin está construido encima de Google Web Toolkit, con el que puede extenderse.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro Intel Core 2 Duo 8GB RAM.
  • Sistema Operativo: Mac OS Yosemite 10.10
  • Entorno de desarrollo: Eclipse Mars 4.5
  • JRE 1.8.0_51
  • Apache Maven 3.3.3
  • Apache Tomcat 8.0.24

3. Creación de un proyecto Vaadin

Para empezar a desarrollar aplicaciones en Vaadin se nos presentan dos alternativas muy sencillas:

  • Utilizar los plugins específicos para IDEs como NetBeans, Eclipse o IntelliJ (utilizan como gestor de dependencias Ivy)
  • Crear directamente un proyecto con Maven haciendo uso del arquetipo de aplicación con Vaadin.

En este tutorial se seguirá la segunda alternativa, ya que tenemos un mejor control de lo que estamos haciendo, reduciendos los niveles de “magia” considerablemente.

Empezamos creando el proyecto Vaadin en una terminal con Maven. Para ello utilizamos la última versión del arquetipo de aplicación de este tipo:

$ mvn archetype:generate \
    -DarchetypeGroupId=com.vaadin \
    -DarchetypeArtifactId=vaadin-archetype-application \
    -DarchetypeVersion=7.5.6

Iremos suministrando al asistente de Maven los datos de grupo y nombre de artefacto de nuestro proyecto. En mi caso:

  • Grupo: com.autentia.vaadin.
  • Artefacto: tutorial_vaadin.
  • Version: la por defecto, 1.0-SNAPSHOT.

Si todo ha ido bien, podremos cambiar al directorio creado y realizar una instalación de la aplicación Vaadin:

$ cd tutorial_vaadin
$ mvn clean install

Dependiendo de si ya tenemos las dependencias en nuestro repositorio local de Maven, ésta última operación será más o menos larga. Lo que se llevará la mayoría de tiempo es la compilación de los módulos de Widgets de Vaadin personalizados (esqueleto

MyAppWidgetSet
auto-generado por el arquetipo).

Como alternativa, podemos hacer uso del objetivo verify, que construirá y pasarás las pruebas a nuestro proyecto:

$ mvn verify

Vamos a arrancar la aplicación con el servidor Jetty empotrado en el arquetipo de Vaadin:

Si ahora abrimos un navegador y accedemos a

http://localhost:8080/
, veremos la aplicación de muestra de Vaadin ejecutándose:
$ mvn jetty:run

intro_vaadin_1


3.1. Importación a un proyecto Eclipse

Importemos ahora el proyecto en Eclipse mediante “Import Maven Project”.

NOTA: Si al realizar la importación del proyecto, Eclipse nos muestra un mensaje de error parecido a “Plugin execution not covered by lifecycle configuration […] update-theme” lo que me ha funcionado a mí es añadir el objetivo en conflicto (en este caso, update-theme) dentro de lifecycle-mapping de org.eclipse.me2:

pom.xml
<plugin>
    <groupId>org.eclipse.m2e</groupId>
    <artifactId>lifecycle-mapping</artifactId>
    [...]
    <goals>
        <goal>resources</goal>
        <goal>update-widgetset</goal>
        <goal>compile</goal>
        <goal>compile-theme</goal>
        <goal>update-theme</goal>
    </goals>
    [...]

4. Fundamentos de la clase UI

Entre los ficheros fuente que nos ha generado el arquetipo Maven de Vaadin, el más interesante es

MyUI.java
:
MyUI.java
@Theme("mytheme")
@Widgetset("com.autentia.vaadin.MyAppWidgetset")
public class MyUI extends UI {

    @Override
    protected void init(VaadinRequest vaadinRequest) {
        final VerticalLayout layout = new VerticalLayout();
        layout.setMargin(true);
        setContent(layout);

        Button button = new Button("Click Me");
        button.addClickListener(new Button.ClickListener() {
            @Override
            public void buttonClick(ClickEvent event) {
                layout.addComponent(new Label("Thank you for clicking"));
            }
        });
        layout.addComponent(button);

    }

    @WebServlet(urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true)
    @VaadinServletConfiguration(ui = MyUI.class, productionMode = false)
    public static class MyUIServlet extends VaadinServlet {
    }
}

Repasemos las partes más interesantes de este código:

  • Líneas 1-2: Mediante anotaciones, le decimos a Vaadin qué temas y conjunto de Widgets vamos a hacer uso. También podremos decirle el título de la página con
    @Title
    , por ejemplo.
  • Línea 3: La superclase
    UI
    representa una página que se mostrará en el cliente.
  • Línea 6: El método
    init()
    es el punto de entrada de toda subclase de UI. Su objetivo básico es la inicialización y configuración de la interfaz gráfica visible al usuario. Es ejecutada en cada refresco de página.
  • Línea 7: Como muchos frameworks de este tipo, la organización de la interfaz viene dada por la definición de layouts.
  • Líneas 11-12: Fácil definición de funciones de callback para la comunicación asíncrona con el servidor.
  • Líneas 22-24: Ya que hace uso de la especificación 3.0+ de Servlets, no existe necesidad de un archivo web.xml, si no que mediante anotaciones podemos exponer nuestra clase con una URI personalizada.

5. Layouts programáticos vs declarativos

A la hora de definir layouts, Vaadin nos permite elegir entre hacerlo mediante código Java (muy parecido a la definición con Swing) o haciendo uso de ficheros HTML específicos.

Por ejemplo, la creación de un layout de un formulario, en su versión programática sería algo parecido a lo siguiente:

SampleForm.java
public class SampleForm extends FormLayout {
    Button button1 = new Button("Click me", this::button1Action);
    Button button2 = new Button("DO NOT Click me", this::button2Action);
    TextField textField1 = new TextField("I am a TextField, nothing more");
    [...]
    public SampleForm() {
        configureComponents();
        buildLayout();
    }
    [...]
    private void configureComponents() {
        button1.setStyleName(ValoTheme.BUTTON_PRIMARY);
        button1.setClickShortcut(ShortcutAction.KeyCode.ENTER);
        setVisible(false);
    }

    private void buildLayout() {
        setSizeUndefined();
        setMargin(true);

        HorizontalLayout actions = new HorizontalLayout(button1, button2);
        actions.setSpacing(true);

        addComponents(actions, textField1);
    }
    [...]
}

La versión declarativa de este snippet constaría de un fichero HTML de definición de los espacios donde reside cada componente, la clase Java que lee dicho fichero y por último, el propio código de lógica del formulario.

SampleFormLayout.html
<v-form-layout size-auto margin>
    <v-horizontal-layout spacing>
        <v-button style-name="primary" _id="button1">Click me</v-button>
        <v-button _id="button2">DO NOT Click me</v-button>
    </v-horizontal-layout>
    <v-text-field caption="I am a TextField, nothing more" _id="textField1"></v-text-field>
</v-form-layout>
SampleFormLayout.java
@DesignRoot
public class SampleFormLayout extends FormLayout {
    protected Button button1;
    protected Button button2;

    public SampleFormLayout() {
        Design.read(this);
    }
}
SampleForm.java
public class SampleForm extends SampleFormLayout {
    [...]
    public SampleForm() {
        setVisible(false);
        button1.setClickShortcut(ShortcutAction.KeyCode.ENTER);

        button1.addClickListener(this::button1Action);
        button2.addClickListener(this::button2Action);
    }
[...]

El resultado de ambos es el mismo:

intro_vaadin_2


6. Conclusiones

Vaadin presenta una alternativa interesante a toda la fiebre de librerías y frameworks del lado del cliente con JavaScript para la creación de aplicaciones de una sola página, rápidas e interactivas, volviendo a la arquitectura del lado servidor. Bien vale que se le eche un ojo, ya que su comunidad no hace más que crecer.


7. Referencias


Referencias a objetos en Java: Soft, Weak, Phantom

$
0
0

En este tutorial veremos los tipos de referencia que mantiene la JVM a los objetos creados y cómo ayuda esto al Garbage Collector en su trabajo de limpieza.

Índice de contenidos


1. Introducción

El concepto de referencia está íntimamente ligado con el comportamiento del recolector de basura. Dependiendo de las referencias que tenga un determinado objeto, este podrá ser elegido para ser limpiado de memoria (referencia débil) o, por el contrario, puede seguir residiendo en el Heap (referencia fuerte).

Cada una de estas referencias denominadas débiles tienen una clase asociada dentro del paquete java.lang.Reference y existen desde la versión 1.2 del JDK.


2. Accesibilidad o Reachability

La propia documentación del paquete java.lang.ref define, en el apartado Reachability, los grados de accesibilidad de más a menos fuertes:

  • Un objeto es accesible de forma fuerte (strongly reachable) si se puede llegar a él sin la necesidad de atravesar ningún objeto de referencia. Un objeto recién creado es accesible de forma fuerte por el hilo que lo creó.
  • Un objeto es accesible de forma soft (softly reachable) si no es accesible de forma fuerte, pero sí a través de una cadena de
    SoftReferences
    .
  • Un objeto es accesible de forma débil (weakly reachable) si no es accesible de forma ni fuerte ni soft, pero sí a través de una cadena de
    WeakReferences
    . Cuando todas las referencias débiles a un objeto accesible de este tipo desaparecen, este objeto puede ser elegido para su finalización.
  • Un objeto es accesible de forma phantom (phantom reachable) si no es accesible de forma fuerte, soft o débil, ha sido finalizado y existe algún objeto de la clase
    PhantomReference
    apuntando al objeto.
  • Por último, un objeto es inaccesible (unreachable) y, por tanto, apto para ser liberado si no puede ser accesible de ninguna de las formas antes descritas.

3. Referencias Fuertes

Las referencias fuertes son las que nos podemos encontrar de forma natural en nuestro código, p.e.:

StringBuilder builder = new StringBuilder();

Este tipo de referencias hace que el objeto no sea elegible para liberarse o, dicho de otra manera: siempre que un objeto sea referenciado por una cadena de referencias fuertes, no puede ser limpiado del Heap.


4. Referencias Soft

De forma análoga a las referencias fuertes, se puede decir que el Garbage Collector puede liberar un objeto si este está referenciado únicamente por referencias débiles.

El grado más fuerte de este tipo de referencias son las SoftReferences. Los objetos con este tipo de referencias serán liberados únicamente cuando la JVM necesite desesperadamente más memoria.

Las

SoftReferences
se usan principalmente para implementar cachés simples sensibles al consumo de memoria mediante instancias directas de
SoftReference<T>
.

Dado el hecho de que mientras el objeto referenciado por una

SoftReference
sea accesible de forma fuerte esta referencia no será liberada, es posible implementar sistemas de cachés más complejas que, por ejemplo, impidan la finalización de las entradas que se más se han utilizado en los últimos accesos manteniendo referencias fuertes a dichas entradas y dejando a discreción del GC la liberación del resto.

4.1. SoftReferences y el Garbage Collector

Una de las preguntas que suelen surgir cuando uno busca información sobre objetos de referencia en Java y en particular sobre las SoftReference es: ¿cómo y cuándo decide el GC limpiar este tipo de referencias?

Para empezar, la JVM mantiene una variable de tiempo que guarda el momento (en milisegundos desde el inicio de la aplicación) de la última pasada del GC. A su vez, cada

SoftReference
tiene un campo de timestamp que indica cuándo ella o el objeto referenciado fueron accedidos por última vez. Esto permite una ordenación grosso modo de las
SoftReference
existentes: el tiempo transcurrido desde el último GC y cada acceso a las referencias.

Cuando un evento del GC se dispara, la decisión de limpiar una

SoftReference
se basa en dos factores:
  • Hace cuánto se produjo el último acceso a la referencia.
  • Cuánta memoria disponible queda.

Tomando los siguientes parámetros:

  • free_heap
    es la cantidad de espacio libre en el Heap en MB.
  • interval
    es el tiempo transcurrido entre la última recolección y el timestamp de la referencia.
  • ms_per_mb
    es un número constante de milisegundos que determina el tiempo a mantener una
    SoftReference
    por cada MB libre en el Heap.

Así, la decisión para limpiar una referencia viene dada por la condición:

interval <= free_heap * ms_per_mb

Por ejemplo, teniendo una

SoftReference
con un
timestamp
de 2000ms, la última pasada del GC fué hace 5000ms, el
ms_per_mb
es 1000 y el espacio libre en el Heap es 1MB:
5000 - 2000 <= 1 * 1000

Ya que 3000 > 1000, la referencia será limpiada.

Una última cosa a tener en cuenta es que las

SoftReferences
se mantienen siempre durante al menos una pasada del GC después de haber sido accedidas. Esto se debe a que para el cálculo de
interval
se usa el tiempo de la última recolección, no de la que se esté realizando en ese momento.

5. Referencias Débiles

Como hemos visto antes, un objeto que sólo tenga referencias débiles será liberado por el GC en la siguiente pasada de limpieza. Este tipo puede ser útil, por ejemplo, para almacenar metadatos.

Veamos un pequeño ejemplo para determinar la diferencia entre

SoftReference
y
WeakReference
(la referencia débil más fuerte).
Integer significado = new Integer("42");
WeakReference<Integer> referencia = new WeakReference<Integer>(significado);
significado = null;

Una vez que hemos desreferenciado la variable

significado
, ésta se convierte en apta para ser liberada ya que la única referencia a él es
referencia
, del tipo
WeakReference
.

Si ésta variable

referencia
fuera del tipo
SoftReference
, como hemos mostrado antes, la siguiente pasada del GC no finalizaría
significado
.

El ejemplo más conocido de uso de las referencias

WeakReference
es la clase WeakHashMap. Ésta clase tiene la particularidad de mantener el conjunto de claves mediante
WeakReferences
, lo que permite la liberación de entradas de forma automática cuando sus claves no se encuentran en uso.

5.1. Referencias Phantom

En el nivel más bajo desde el punto de las referencias débiles se encuentran las PhantomReferences. El significado y la diferencia de este tipo con el resto de las referencias débiles lo veremos en el siguiente apartado.


6. ReferenceQueues – Notificación del grado de accesibilidad

Es posible de manera programática ser avisado de cuando un objeto cambia su accesibilidad, registrando una referencia válida mediante las denominadas colas de referencias o ReferenceQueue en el momento en el que se crea dicha referencia.

En este ámbito es cuándo las PhantomReferences muestran su utilidad. Mientras que el resto de las referencias débiles (

WeakReference
y
SoftReference
) son liberadas antes de ser añadidas a una cola de referencias, las
PhantomReferences
deben ser sacadas de la cola en la que se ha registrado para marcarlas para ser liberadas.

7. Ciclo de vida de un objeto

El siguiente diagrama muestra el ciclo de vida básico por el que pasa un objeto, incluidas las transiciones entre referencias:

referencias-imagen1


8. Conclusión

Conocer la existencia y las diferencias entre los tipos de referencias que mantiene la JVM nos permite idear nuevas formas de trabajar con objetos que no tenemos porque estar manipulando constantemente, de tal manera que la huella que dejamos en memoria en nuestros programas sea lo más eficiente y ligera posible.


9. Fuentes

Viewing all 16 articles
Browse latest View live