Muchas aplicaciones que corren sobre la máquina virtual de java, incluyendo servicios como Apache Spark y Kafka y aplicaciones empresariales, se corren hoy en día en un contenedor de docker. Hasta ahora, correr la máquina virtual de java en un contenedor representaba problemas en cuando la asignación de memoria y el tamaño del CPU usable, lo cual conllevaba una perdida en el desempeño de las aplicaciones. Esto porque por defecto Java no tenia forma de conocer que estaba corriendo en un contenedor. Con el lanzamiento de Java 10, la máquina virtual de java ahora tiene forma de conocer las restricciones que el contenedor le impone a través de los grupos de control (cgroups). De manera que ahora tanto las restricciones de CPU como de memoria pueden ser gestionadas por una aplicación de Java que corre en un contenedor. Esto puede incluir:

  • Respetar los limites de CPU configurados en el contenedor
  • Configurar cuantos CPUs están disponibles en el contenedor
  • Configurar restricciones de CPU en el contenedor

Estas mejoras están disponibles en todos los sabores que conocemos de docker (mac, windows y linux).

Limites en la memoria del contenedor

Jasta Java 9, la máquina virtual no reconocía los limites del CPU configuradas por las banderas del contenedor. Ahora en Java 10, los limites de la memoria son automáticamente reconocidos y puestos en vigor.

Lo recomendado para cuando corremos una máquina virtual de java es que se le asigne 1/4 de la memoria física. Es decir que si tenemos un contenedor de docker con 512MB de memoria corriendo ne un host de 2GB de memoria, la maquina virtual de Java tendría que utilizar un máximo de 128MB de esos 512MB

Si probamos eso con una imagen de java 8, observamos lo siguiente:

$ docker container run -it -m512 --entrypoint bash openjdk:latest
$ docker-java-home/bin/java -XX:+PrintFlagsFinal -version | grep MaxHeapSize

> uintx MaxHeapSize                              := 524288000                          {product}
> openjdk version "1.8.0_162"

MaxHeapSizes := 524288000, nos dice que la máquina virtual no esta honrando lo que la clase ergonomics establece. Es decir que en lugar de usar 1/4 de la memoria del container, esta usando 1/4 de la memoria física.

Si probamos exactamente lo mismo con una imagen de java 10, vemos como el problema se soluciona:

$ docker container run -it -m512M --entrypoint bash openjdk:10-jdk
$ docker-java-home/bin/java -XX:+PrintFlagsFinal -version | grep MaxHeapSize

> size_t MaxHeapSize                              = 134217728                                {product} {ergonomic}
> openjdk version "10" 2018-03-20

MaxHeapSize := 134217728 nos indica un aproximado de 128MB, con lo cual si se respeta el limite por defecto establecido.

Configuración de los CPUs disponibles

Por defecto cada el acceso de cada contenedor a los ciclos del CPU de la maquina anfitrión son ilimitados. Si eso no es lo deseado se pueden configurar varias restricciones y Java 10 las reconocerá.

$ docker container run -it --cpus 2 openjdk:10-jdk
$ jshell > Runtime.getRuntime().availableProcessors()

> 1 ==> 2

Todos los CPUs reservados a Docker obtendrán la misma proporción de ciclos del CPU. Esta proporción puede ser modificada cambiando la porción de CPU relativamente a la carga del resto de containers. La proporción unicamente aplica cuando hay procesos corriendo que intensifican el uso del CPU. El tiempo uso de CPU varia según el numero de contenedores que están corriendo en el sistema operativo. Esto puede ser configurado en Java 10:

$ docker container run -it --cpu-shares 2048 openjdk:10-jdk
$ jshell> Runtime.getRuntime().availableProcessors()

> 1 ==> 2

El comando cpuset restringe a Java 10 en cuanto a cuales CPUs puede usar para ejecutar sus procesos:

$ docker run -it --cpuset-cpus="1,2,3" openjdk:10-jdk
$ jshell> Runtime.getRuntime().availableProcessors()

> 1 ==> 3

Reservando Memoria y CPU

Con Java 10, las configuraciones del contenedor pueden ser usadas para estimar las reservas de memoria y CPU que las aplicaciones necesitan para ser instaladas. Asumamos que la pila de memoria y CPU que requerimos para cada proceso corriendo en un contenedor ya fue determinada y JAVA_OPTS ya fue configurado. Por ejemplo, si nuestra aplicación esta distribuida a través de 10 nodos, y cinco nodos requieren 512Mb de memoria y 1024 en CPU-shares y los otros cinco nodos requieren 256 de memoria y 512 CPU-shares. Hay que notar que la proporción de 1 CPU esta representada por 1024.

Para memoria cada aplicación necesita un mínimo de 5GB de memoria reservada:

  • 512Mb x 5 = 2.56 Gb
  • 256Mb x 5 = 1.28 Gb

La aplicación requiere 8 CPUs para que corra eficientemente:

  • 1024 x 5 = 5 CPUs
  • 512 x 5 = 3 CPUs

La mejor practica es perfilar la aplicación para determinar la memoria y CPU que hay que reservar para cada proceso que corre en la JVM. De cualquier modo, Java 10 facilita las cosas al momento de dimensionar los contenedores para prevenir errores de falta de memoria en las aplicaciones, así como también reservando suficiente CPU para procesar las cargas de trabajo.

Categorized in:

Tagged in:

,