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″.
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
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; syncdd write 1m => 119 Mb/s
$ sync; dd if=/dev/zero of=/benchfile bs=1M count=4096; syncdd read 4k => 307 Mb/s
$ echo 3 > /proc/sys/vm/drop_caches $ dd if=/benchfile of=/dev/null bs=4k count=1048476dd read 1m => 357 Mb/s
$ echo 3 > /proc/sys/vm/drop_caches $ dd if=/benchfile of=/dev/null bs=1M count=4096Realicé 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" doneEste 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:latestCompilar 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 doneDistribuye 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