Docker 1.13: Microservices su Swarm nativo

27 gennaio 2017

Fabrizio Soppelsa

- RHCE, Autore di Linux&C, Autore di Linux Journal

Riassunto delle puntate precedenti: Swarm, il sistema di orchestration nativo, è stato rilasciato in Docker 1.12. Il “nuovo” Swarm (che chiameremo Swarm Mode) ricalca le funzionalità base di Kubernetes, se vogliamo, con la differenza saliente che è già integrato nel Docker Engine e include già Etcd – quindi non richiede installazioni e si basa su un’architettura decentralizzata.

Nella release 1.12 l’orchestration (SwarmKit e Swarm Mode) è stata la parte più importante, mentre sono stati sacrificati altri aspetti che ci ruotano intorno, per esempio Compose che non sopportava la sintassi per Swarm Mode.

Con il rilascio di Docker 1.13, finalmente è arrivato questo supporto, insieme agli Stack. Compose ora può usare una sintassi “v3” compatibile con Swarm Mode. Inoltre sono stati dichiarati stabili gli Stack, che non sono altro che astrazioni per creare i cosiddetti “bundle”, gruppi di task, un po’ come i pod di Kubernetes.

Siccome la pratica vale più della grammatica, vediamo un esempio su come modellare un microservice replicato su Swarm Mode, usando Compose e Stack.

I requisiti per seguire l’esempio di questo articolo sono:

  • Accesso a un hypervisor o a un tenant su un cloud provider
  • Docker client 1.13
  • Docker Machine > 0.8.0

Innanzitutto, ci serve uno Swarm di prova. Se ne avete già uno a portata, potete saltare questa sezione, e collegarvi alle API dello Swarm manager leader.

Uno Swarm minimale

Se non avete uno Swarm, vediamo come formarne uno con un paio di passaggi.

Innanzitutto creiamo 4 host Docker con Machine, denominati node-1, node-2, node-3 e node-4:

for i in seq 1 4; do 
docker-machine create -d virtualbox node-$i;
done

Qui abbiamo creato i server localmente su VirtualBox. Se li create su Amazon AWS o su OpenStack, dovete ricordarvi di aggiungerli al security group che consente traffico inbound sulle porte tcp/2376 e tcp/2377.

A questo punto, inizializziamo lo Swarm su node-1, che diventa il manager leader sull’interfaccia pubblica raggiungibile dagli altri nodi, in questo caso ha IP 192.168.99.100:

eval $(docker-machine env node-1)
docker swarm init --advertise-addr 192.168.99.100

Questo comando genera un token (secret) e in output stampa la stringa di join. La copiaincolliamo e con un ciclo facciamo join degli altri 3 nodi che diventeranno i 3 worker dello Swarm:

$ for i in 2 3 4; do
docker-machine ssh node-$i sudo \
docker swarm join \
 --token SWMTKN-1-4sltvbciukal9yu1lr21y8erfk0ec606ezqmnvepq4cq4y3t5v-dx8hedfc48x0axxz6k5dfgokk \
192.168.99.100:2377
done
This node joined a swarm as a worker.
This node joined a swarm as a worker.
This node joined a swarm as a worker.

La sintassi è: docker swarm join --token TOKEN IP-MANAGER:PORTA

Controlliamo lo Swarm dal leader manager ancora:

$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
910pxmwxbiuaspt3495lcnsy5    node-4    Ready   Active        
b3m48hq6kiwg5no1biu2mhlwx    node-3    Ready   Active        
fy4gw37npoqesl15uiidtgezx *  node-1    Ready   Active        Leader
y3aemnwnja6crpuz27flza31s    node-2    Ready   Active        

Adesso abbiamo uno Swarm funzionante. Prima di addentrarci nel deployment di un microservice, due cenni di teoria.

La teoria di Docker Swarm

Un cluster Docker Swarm Mode è un’astrazione che distribuisce task raggruppati in service su un’infrastruttura di Docker host.

Un Docker host è un server con il Docker daemon attivo, che può lanciare container. Un task è un’unità di lavoro, per esempio un container nginx. Un service è un gruppo di task, per esempio il solito WordPress che consiste di container (task) nginx, php e mysql.

I task di solito sono replicati (replicas) ed accessibili tramite load balancer (integrato in Swarm Mode, che internamente usa iptables e IPVS). Un nginx con fattore di replica 10 espone la porta 80 dal container all’host, e quando una request arriva alla porta 80 su un nodo dello Swarm, il load balancer la reindirizza in modalità round-robin a uno dei 10 nodi dello Swarm.

Aperta parentesi, in realtà la situazione sarebbe un po’ più complessa, perché Swarm integra una tabella detta di routing mesh. L’implementazione del networking in Swarm è comunque particolarmente robusta (ho testato il routing mesh su un cluster di 4700 nodi) ma non è il tema di questo post.

Tutti questi elementi – nodi, service, task, network – sono controllabili via API o più facilmente dalla riga di comando con il comando docker.

Modellare un microservice per Swarm

Perfetto. Con uno Swarm a disposizione e con le idee più chiare, adesso siamo pronti a lanciare il nostro servizio: Invece del solito palloso esempio di WordPress con Nginx e MySQL, lanciamo un cluster Apache Spark.

Spark rientra nel filone dei cluster per il Big Data. Con Spark si possono analizzare dati grezzi, scalare dati strutturati, lanciare job di calcolo e molto altro. Laddove possibile, rappresenta un’alternativa un po’ più agile ad Hadoop.

La topologia iniziale del nostro cluster Spark è la seguente:

  • 1 Spark manager
  • 3 Spark worker

Il manager è un po’ il controller del cluster col quale ci si interfaccia, mentre i worker sono i muli da soma che eseguono il lavoro.

L’idea è quella di creare un Docker Stack da lanciare su Swarm. Una volta modellati i microservice iniziali, con i comandi Docker Swarm, sarà possibile scalare la dimensione del cluster Spark all’occorrenza, come vedremo poi.

Per prima cosa, dichiariamo lo Swarm manager come nodo di servizio Swarm e non abilitato ad ospitare container, quindi lo mettiamo in modalità drain. Anche se teoricamente uno Swarm manager può essere un Docker host, è best practice specialmente su cluster con almeno 500 nodi dedicare gli Swarm manager solo a Swarm, visto il carico che Raft ed Etcd possono generare. Quindi è un’operazione che mi viene automatica da digitare:

$ docker node update --availability drain node-1

Adesso creiamo una rete overlay VxLAN interna per la comunicazione tra i container del cluster Spark, e la chiamiamo spark. Altra best practice – inizializzare e usare reti separate dedicate ai servizi invece delle reti di default di Swarm:

$ docker network create --driver overlay --subnet 10.0.0.0/22 spark

Qui specifichiamo la subnet, perché è quella di default usata dalle immagini che andremo ad usare. Ciò ci evita, in questo primo post, passaggi extra.

Creiamo ora il file YAML per modellare i microservice. È abbastanza semplice da capire, e la sua sintassi riprende i concetti Swarm. Quindi, determinato il servizio, l’immagine da usare e le reti di appoggio (fin qui niente di diverso rispetto a Compose v2), possiamo aggiungere ai servizi un blocco deploy, dove specificare repliche e opzioni ulteriori:

version: '3'

services:
  spark-master:
    image: fsoppelsa/spark-master
    networks:
      - spark
    ports:
      - 8080:8080
      - 7077:7077
      - 6066:6066
    deploy:
      replicas: 1
      update_config:
        parallelism: 1
        delay: 10s

  spark-worker:
    image: fsoppelsa/spark-worker
    networks:
      - spark
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s

networks:
  spark:
    external: true

Deployment di uno Stack su Swarm

Salviamo questo file come spark.yml e siamo ora pronti a lanciare e creare tutto con il comando:

docker stack deploy -c spark.yml spark

Dopo un po’ di minuti necessari a scaricare le immagini dal Docker Hub, possiamo controllare lo status del sistema che abbiamo creato. Controlliamo gli stack attivi:

$ docker stack ls
NAME   SERVICES
spark  2

I servizi Swarm:

$ docker service ls
ID            NAME                MODE        REPLICAS  IMAGE
p08gv8jecnr3  spark_spark-master  replicated  1/1       fsoppelsa/spark-master:latest
shnxps5x2ev5  spark_spark-worker  replicated  3/3       fsoppelsa/spark-worker:latest

I nodi dove il servizio spark-worker ha lanciato i container:

$ docker service ps spark_spark-worker
ID            NAME                  IMAGE                          NODE    DESIRED STATE  CURRENT STATE           ERROR  PORTS
r7skx21oukv9  spark_spark-worker.1  fsoppelsa/spark-worker:latest  node-3  Running        Running 11 minutes ago         
ksrck05kf3hr  spark_spark-worker.2  fsoppelsa/spark-worker:latest  node-4  Running        Running 11 minutes ago         
pwptx8dm1tkx  spark_spark-worker.3  fsoppelsa/spark-worker:latest  node-2  Running        Running 11 minutes ago

Ci colleghiamo ora alla porta 8080 di un qualsiasi nodo dello Swarm per verificare che effettivamente abbiamo un cluster Spark funzionante:

spark1

Abbiamo 3 worker attivi. Che bello!

E adesso? Possiamo per esempio aggiungere altri worker con l’operazione scale:

$ docker service scale spark_spark-worker=10
spark_spark-worker scaled to 10
$ docker service ls
ID            NAME                MODE        REPLICAS  IMAGE
p08gv8jecnr3  spark_spark-master  replicated  1/1       fsoppelsa/spark-master:latest
shnxps5x2ev5  spark_spark-worker  replicated  10/10     fsoppelsa/spark-worker:latest

Dopo qualche secondo, verifichiamo che le repliche siano effettivamente 10:

spark2

Questo conclude il post su Compose, Stack e Swarm e Docker 1.13. Potete lasciare un commento con gli argomenti Docker che desiderereste vedere trattati su questo blog, e proverò a scriverci un post. Alla prossima!