Clúster Swarm ODROID-HC1

Deploying Stack Swarm in Docker Swarm

El equipo de Docker ha desarrollado una herramienta de agrupación y programación para contenedores de Docker, llamada swarm. Este artículo describe cómo se puede crear un clúster swarm basado en ODROID-HC1. Este clúster se puede instalar en un rack de 19″.

Figura 1 - El conjunto de dispositivos HC1 para este artículo

Desde hace varios meses, he estado ejecutando mis aplicaciones en un clúster casero de 4 unidades ODROID-HC1, ejecutando contenedores Docker organizados con swarm. El clúster está alimentado por una fuente de alimentación casera. Elegí ODROID-HC1 y no el ODROID-MC1 porque el primer modelo ofrece soporte SSD. Un sistema basado en SSD es mucho más rápido que otro que use tarjetas microSD.

Rack de 19" impreso en 3D

Figura 2 - Rack de 19" impreso en 3D

Figura 3 - Montaje inicial

Figura 4: Soldé los cables dupont para alimentar directamente el ventilador desde la entrada de 5V de cada ODROID-HC1, tal y como muestra la imagen.

Figura 5 – Este es el aspecto que tiene con todos los ventiladores instalados:

Figura 6 - La configuración final del rack sería similar a la que se muestra en esta imagen

Instalación de software

Opté por instalar Archlinux, que se puede obtener en https://archlinuxarm.org/platforms/armv7/samsung/odroid-xu4. Para la parte de administración de infraestructura, decidí usar Saltstack. Utilicé Saltstack para "templar" todos mis servidores. Instalé Saltstack Master y Minion (el NAS será el maestro para el resto de servidores), siguiendo los pasos que ya documenté en https://www.bluemind.org/deploying-saltstack-master-minion-archlinux-arm/

Desde el nodo maestro salt, una simple comprobación te muestra como todos los nodos están bajo control:

$ salt -E "node[1-4].local.lan" cmd.run 'cat /etc/hostname'
  node4.local.lan:
      node4
  node3.local.lan:
      node3
  node2.local.lan:
      node2
  node1.local.lan:
      node1

Rootfs en el SSD

Primero, particiona el ssd:

$ salt -E "node[1-4].local.lan" cmd.run 'echo \
-e "o\nn\np\n1\n\n\n\nw\nq\n" | fdisk /dev/sda'
Formatea la futura partición raíz:
$ salt -E "node[1-4].local.lan" cmd.run 'mkfs.ext4 -L ROOT /dev/sda1'
Monta la partición root del ssd:
$ salt -E "node[1-4].local.lan" cmd.run 'mount /dev/sda1 /mnt/'
Clona sdcard a la partición root del ssd:
$ salt -E "node[1-4].local.lan" cmd.run 'cd /;tar \
-c --one-file-system -f - . | (cd /mnt/; tar -xvf -)'
Cambia los parámetros de arranque para que la partición root sea /dev/sda1:
$ salt -E "node[1-4].local.lan" cmd.run 'sed -i -e \
"s/root\=PARTUUID=\${uuid}/root=\/dev\/sda1/" /boot/boot.txt'
Recompila la configuración de arranque:
$ salt -E "node[1-4].local.lan" cmd.run 'pacman -S --noconfirm \
uboot-tools'
$ salt -E "node[1-4].local.lan" cmd.run 'cd /boot; ./mkscr'
Reiniciar:
$ salt -E "node[1-4].local.lan" cmd.run 'reboot'
Elimina todo lo que hay en la tarjeta SD y pon los archivos /boot en la raíz
$ salt -E "node[1-4].local.lan" cmd.run 'mount /dev/mmcblk0p1 /mnt'
$ salt -E "node[1-4].local.lan" cmd.run 'cp -R /mnt/boot/* /boot/'
$ salt -E "node[1-4].local.lan" cmd.run 'rm -Rf /mnt/*'
$ salt -E "node[1-4].local.lan" cmd.run 'mv /boot/* /mnt/'
Adapta boot.txt puesto que los archivos de arranque están en la partición de arranque de root y no en el directorio /boot:
$ salt -E "node[1-4].local.lan" cmd.run 'sed -i -e "s/\/boot\//\//" \
/mnt/boot.txt'
$ salt -E "node[1-4].local.lan" cmd.run 'pacman -S --noconfirm \
uboot-tools'
$ salt -E "node[1-4].local.lan" cmd.run 'cd /mnt/; ./mkscr'
$ salt -E "node[1-4].local.lan" cmd.run 'cd /; umount /mnt'
$ salt -E "node[1-4].local.lan" cmd.run 'reboot'
Comprueba que /dev/sda es root:
$ salt -E "node[1-4].local.lan" cmd.run 'df -h | grep sda'
Node4.local.lan: /dev/sda1 118G 1.2G 117G 0% /
Node3.local.lan: /dev/sda1 118G 1.2G 117G 0% /
Node1.local.lan: /dev/sda1 118G 1.2G 117G 0% /
Node2.local.lan: /dev/sda1 118G 1.2G 117G 0% /

Pruebas de rendimiento SSD

Llevar a cabo pruebas de rendimiento es algo complejo y no voy a decir que las que realicé eran perfectas, pero al menos nos da una idea de cómo de rápido puede ser un SSD. El SSD que usé en cada uno de los ODROID-HC1 es un Sandisk X400 de 128GB. Ejecute la siguiente prueba 3 veces:

hdparm -tT /dev/sda => 362.6 Mb/s

dd write 4k => 122 Mb/s

$ sync; dd if=/dev/zero of=/benchfile bs=4k count=1048476; sync
dd write 1m => 119 Mb/s
$ sync; dd if=/dev/zero of=/benchfile bs=1M count=4096; sync
dd read 4k => 307 Mb/s
$ echo 3 > /proc/sys/vm/drop_caches
$ dd if=/benchfile of=/dev/null bs=4k count=1048476
dd read 1m => 357 Mb/s
$ echo 3 > /proc/sys/vm/drop_caches
$ dd if=/benchfile of=/dev/null bs=1M count=4096
Realicé la misma prueba con afinidad IRQ para los núcleos grandes, pero no observé ningún impacto significativo en el rendimiento.

Finalizar la instalación

No voy a copiar y pegar todos mis estados y plantillas saltstack en este artículo, ya que obviamente depende de las necesidades y gustos de cada persona. Básicamente, mi plantilla de "Nodo HC1" hace lo siguiente en cada nodo:

  • Cambiar el listado de copias
  • Instalar scripts personalizados de administrador de sistema
  • Eliminar alarmuser
  • Añadir algunas herramientas de administrador del sistema (lsof, wget, etc.)
  • Modificar el planificador mmc y ssd a fecha límite
  • Añadir mi usuario
  • Instalar cron
  • Configurar la rotación de los registros log
  • Ajustar journald config (RuntimeMaxUse=50M and Storage=volatile para reducir la escritura en el almacenamiento flash)
  • Añadir funciones de correo (ssmtp)

Luego cambié la contraseña de mi usuario usando saltstack:

$ salt "node1.local.lan" shadow.gen_password 'xxxxxx' # gives password hash in return
$ salt "node2.local.lan" shadow.set_password myuser 'the_hash_here'
Finalmente, para garantizar que la corrupción del disco no detenga el arranque de un nodo, forcé fsck en el momento del arranque en todos los nodos:

  • añadiendo “fsck.mode=force” in la linea del kernel dentro de /boot/boot.txt
  • compilándolo con mkscr
  • reiniciando

Implementar Docker Swarm

El módulo swarm dentro de mi saltstack no es reconocido, a pesar de usar los módulos de la versión 2018.3.1. Así que terminé ejecutando los comandos directamente, lo cual no es realmente un problema ya que no iba a añadir un nodo todos los días.

Compilar el nodo maestro:

$ salt "node1.local.lan" cmd.run 'docker swarm init'
Añadir un nodo trabajador:
$ salt "node4.local.lan" cmd.run 'docker swarm join --token xxxxx \
node1.local.lan:2377'
Añadir un segundo y tercer nodo maestro para tener capacidad de respuesta ante posibles fallos:
$ salt "node1.local.lan" cmd.run 'docker swarm join-token manager'
$ salt "node3.local.lan" cmd.run 'docker swarm join --token xxxxx \
192.168.1.1:2377'
$ salt "node2.local.lan" cmd.run 'docker swarm join --token xxxxx \
192.168.1.1:2377'
Al verificar el estado de todos los nodos con "docker node ls", ahora podemos ver un nodo jefe y 2 nodos que son "accesibles". Luego, implementé una configuración personalizada con un demonio docker (daemon.json) para cambiar el controlador del almacenamiento a overlay2 (uno por defecto para reducir la velocidad en xu4) y permite usar mi registro de docker personalizado:
{
  "insecure-registries":["myregistry.local.lan:5000"],
  "storage-driver": "overlay2"
}

Imágenes docker para el cluster Swarm.

A partir de ahora, usar un organizador de contenedores implica utilizar contenedores sin estado o usar una solución de almacenamiento global. Primero intenté usar glusterfs en todos los nodos. Funcionaba perfectamente, pero era demasiado lento (entre 25 y 36 Mb / s, incluso con configuraciones optimizadas y afinidad IRQ a los grandes núcleos). Terminé con una solución simple pero muy eficiente que cubría mis necesidades: Una copia de seguridad diaria automatizada de todos los volúmenes de todos los nodos (en una unidad de red) Una copia de seguridad de la base de datos mysql diaria automatizada en todos los nodos (ejecutar solo cuando se detecta mysql) Contenedores que pueden restaurar sus volúmenes desde la copia de seguridad durante el primer inicio Una limpieza diaria automatizada de contenedores y volúmenes en todos los nodos

Por lo tanto, cada vez que se cierra un nodo o se reinicia una pila, cada contenedor puede iniciarse en cualquier nodo, recuperando sus datos automáticamente (si no es sin estado).

El script de copia de seguridad diaria se muestra a continuación:

# monthly saved backup
firstdayofthemonth=`date '+%d'`
if [ $firstdayofthemonth == 01 ] ; then
  BACKUP_DIR="$BACKUP_DIR/monthly"
else
  firstdayoftheweek=$(date +"%u")
  if [ day == 1 ]; then
  BACKUP_DIR="$BACKUP_DIR/weekly"
  fi
fi
 
volumeList=$(ls /var/lib/docker/volumes | grep $DOCKER_VOLUME_LIST_PATTERN)
 
for volume in $volumeList
do
  archiveName=$(echo $volume | cut -d_ -f2-)
  mv "$BACKUP_DIR/$archiveName.tar.gz" \
"$BACKUP_DIR/$archiveName.tar.gz.old"
  cd /var/lib/docker/volumes/$volume/_data/
  tar -czf $BACKUP_DIR/$archiveName.tar.gz * 2>&1
  rm "$BACKUP_DIR/$archiveName.tar.gz.old"
done
Este es el script de limpieza diaria:
# remove unused containers and images
docker system prune -a -f
 
# remove unused volumes
volumeToRemove=$(docker volume ls -qf dangling=true)
 
if [ ! -z "$volumeToRemove" ]; then
  docker volume rm $volumeToRemove
fi

Compilar una simple imagen distribuida

Para crear un simple sistema de compilación distribuida, hice algunos scripts para distribuir mis contenedores docker acoplados en los 4 dispositivos. Todos los contenedores se colocan en un registro local, etiquetados con la fecha actual. El compilador local de imágenes crea, etiqueta y coloca en el registro (nombre del script: docker_build_image):

if [ $# -lt 3 ];  then
  echo "Usage: $0   "
  echo "Example : $0 myImage armv7h myregistry.local.lan:5000"
  echo ""
  exit 0
fi
 
arch="$2"
imageName="$arch/$1"
registry="$3"
tag=`date +%Y%m%d`
 
docker build --rm -t $registry/$imageName:$tag -t \
$registry/$imageName:latest .
docker push $registry/$imageName
docker rmi -f $registry/$imageName:$tag
docker rmi -f $registry/$imageName:latest
Compilar varias imágenes en el argumento (nombre del script: docker_build_batch):
# usage : default build all
if [[ "$1" == "-h" ]]; then
  echo "Usage: $0 [image folder 1] [image folder 2] ..."
  echo "Example :"
  echo "  build two images : $0 mariadb mosquitto"
  echo ""
  exit 0
fi
 
# if any parameter, use it/them as docker image to build
if [[ $# -gt 0 ]]; then
  DOCKER_IMAGES_DIR="${@:1}"
else
  echo "Nothing to build. try -h for help"
fi
 
echo -e "\e[1m--- going to build the following images :"
echo -e "\e[1m$DOCKER_IMAGES_DIR\n"
 
# build and send to repository
for image in $DOCKER_IMAGES_DIR
do
  echo -e "\e[1m--- start build of $image:"
  cd /home/docker/$image
  docker_build_image $image armv7h myregistry.local.lan:5000
done
Distribuye las compilaciones utilizando saltstack en el maestro salt, utilizando el script anterior. La imagen especial "archlinux" se compila primero si se encuentra, ya que el resto de imágenes dependen de ésta.
DOCKER_IMAGES_DIR=""
SPECIAL_NAME="archlinux_image_builder"
NODES[0]=""
 
# usage : default build all
if [[ "$1" == "-h" ]]; then
  echo "Usage: $0 [image folder 1] [image folder 2] ..."
  echo "Examples :"
  echo "  build all found images : $0"
  echo "  build two images : $0 mariadb archlinux_image_builder"
  echo ""
  exit 0
fi

echo -e "\e[1m--- Update repository (git pull)\n"
# update git repository
cd /home/docker
git pull

# if any parameter, use it/them as docker image to build
if [[ $# -gt 0 ]]; then
  DOCKER_IMAGES_DIR="${@:1}"
else
  DOCKER_IMAGES_DIR=$(ls -d */ | cut -f1 -d'/')
fi

echo -e "\e[1m--- going to build the following images :"
echo -e "\e[1m$DOCKER_IMAGES_DIR\n"
 
 
# if archlinux images in array, build it first
if [[ $DOCKER_IMAGES_DIR = *"$SPECIAL_NAME"* ]]; then
  echo -e "\e[1m--- found special image: $SPECIAL_NAME, start to build   
           it first"
  echo -e "\e[1m--- update repository on node1\n"
  salt "hulk1.local.lan" cmd.run "cd /home/docker; git pull"
 
  echo -e "\e[1m--- build $SPECIAL_NAME image on hulk1\n"
  salt "hulk1.local.lan" cmd.run "cd /home/docker/$SPECIAL_NAME; \
./mkimage-arch.sh armv7 registry.local.lan:5000"
 
  DOCKER_IMAGES_DIR=${DOCKER_IMAGES_DIR//$SPECIAL_NAME/}
fi

# update repository on all nodes
echo -e "\e[1m--- update repository on node[1-4]\n"
salt -E "node[1-4].local.lan" cmd.run "cd /home/docker; git pull"

# Prepare build processes on known swarm nodes
i=0
for image in $DOCKER_IMAGES_DIR
do
  NODES[$i]="${NODES[$i]} $image"
  i=$((i + 1))
 
  if [[ $i -gt 3 ]]; then
      i=0
  fi
done
 
echo -e "\e[1m--- build plan :"
echo -e "\e[1m--- node1 : ${NODES[0]}"
echo -e "\e[1m--- node2 : ${NODES[1]}"
echo -e "\e[1m--- node3 : ${NODES[2]}"
echo -e "\e[1m--- node4 : ${NODES[3]}\n"

# distribute and launch build plan
salt "node1.local.lan" cmd.run "docker_build_batch ${NODES[0]}"
salt "node2.local.lan" cmd.run "docker_build_batch ${NODES[1]}"
salt "node3.local.lan" cmd.run "docker_build_batch ${NODES[2]}"
salt "node4.local.lan" cmd.run "docker_build_batch ${NODES[3]}"
echo -e "\e[1m--- build plan finished"
Las fuentes están disponibles en GitHub (https://github.com/jit06/docker-images) y Thingiverse (https://www.thingiverse.com/thing:3218912). Para comentarios, preguntas y sugerencias, visita el artículo original en https://www.bluemind.org/odroid-hc1-based-swarm-cluster-19-rack.

Referencias:

https://docs.docker.com/get-started/part4/#introduction https://archlinuxarm.org/platforms/armv7/samsung/odroid-xu4 https://wiki.archlinux.org/index.php/Saltstack https://www.bluemind.org/deploying-saltstack-master-minion-archlinux-arm/ https://www.bluemind.org/odroid-hc1-based-swarm-cluster-19-rack

Be the first to comment

Leave a Reply