Bonjour à tous,
Dans le but de découvrir Docker, j'ai décidé d'approfondir les communications inter-conteneurs pour comprendre en détails leurs fonctionnement.
Principes et Technologies
Trois services sont enregistrés dans le fichier principal docker-compose.yml :
- rabbitmq : le serveur rabbitmq, qui centralise les communications, tourne sur ubuntu et met à disposition le service rabbitmq-server sur le port 5672
- producer : émet des messages vers le serveur sur le channel 'hello'
- consumer : lit les messages en queue depuis le serveur sur le channel 'hello'
La gestion des émetteurs de messages doit être scalable
Serveur RabbitMQ
Le serveur RabbitMQ est la plateforme centrale (le hub) qui va permettre la communication entre le consumer et les producers.
Il est définit par un fichier Dockerfile simple :
FROM ubuntu:20.04
RUN apt update -y && apt install rabbitmq-server -y
EXPOSE 4369 5671 5672 15691 15692 25672
CMD service rabbitmq-server start
Nous utilisons ubuntu pour sa simplicité et parce que je connais bien l'image.
On y installe le paquet rabbitmq-server
et on ouvre les ports dont on a besoin. Puis on lance le service rabbitmq-server
.
Producer
Le role du producer est d'envoyer des messages vers le serveur RabbitMQ. Pour l'envoi des messages nous utiliserons un script Python. Qui tournera dans un conteneur Docker.
Script Python :
from time import sleep
sleep(10) # wait for RabbitMQ server to start
import pika
from random import random
producer_id = int(random()*8999+1000) # Generate a random ID for the producer (to differenciate the producers on scaling)
connection = pika.BlockingConnection(
pika.ConnectionParameters('rabbitmq')
)
channel = connection.channel()
channel.queue_declare(queue='hello') # Use channel 'hello'
for i in range(99): # run 100 times
msg = 'Interaction n°'+str(i)+' | producer #'+str(producer_id) # Message to send to RabbitMQ broker
channel.basic_publish(exchange='',
routing_key='hello',
body=msg # Automatic encoding to utf-8
)
print(" [x] Sent '"+msg) # Debug purpose
sleep(1.5) # Wait 1.5 seconds
channel.close() # Don't forget to close connection to channel
Le script python sera executé dans ce conteneur Docker :
# producer
FROM python:3.8.5-slim
WORKDIR /app
# requirements.txt contains one line : pika
COPY requirements.txt /app
RUN pip install -r requirements.txt
# copy python script
COPY producer/producer.py /app/main.py
# allow other containers/PCs to connect; sometimes necessary
#EXPOSE 5551
# execute python script
CMD ["python", "main.py", "*"]
Consumer
Le consumer va lire les messages qui seront reçus par le broker.
Le fonctionnement es similaire au producer. Le conteneur Docker execute un script python.
Script Python :
from time import sleep
sleep(11) # Wait one more second than producer
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='rabbitmq')
)
channel = connection.channel()
# execute this method everytime a message is received on broker
def callback(ch, method, properties, body):
print("[x] received %r" % body.decode('utf-8'))
# link callback function and 'hello' channel on broker
channel.basic_consume(
queue='hello', on_message_callback=callback, auto_ack=True
)
print("[*] Waiting for messages consumer")
channel.start_consuming()
Le Dockerfile est semblable à celui des producers :
# consumer
FROM python:3.8.5-slim
WORKDIR /app
COPY requirements.txt /app
RUN pip install -r requirements.txt
COPY consumer/consumer.py /app/main.py
# allow other containers/PCs to connect; sometimes necessary
#EXPOSE 5551
CMD ["python", "main.py", "*"]
Liaison des conteneurs
Pour lier les conteneurs nous allons utiliser docker-compose
Nous aurons trois services :
- RabbitMQ
- Consumer
- Producer (scalable : on peut en mettre autant qu'on veut)
On part donc sur un fichier docker-compose.yml avec cette structure :
services:
rabbitmq:
consumer:
producer:
On va aussi définir un réseau sur lequel les services vont se connecter :
services:
rabbitmq:
networks:
- myNetwork
consumer:
networks:
- myNetwork
producer:
networks:
- myNetwork
networks:
myNetwork:
Puis on sait que consumer et producer dépendent de RabbitMQ :
services:
rabbitmq:
networks:
- myNetwork
consumer:
depends_on:
- "producer"
networks:
- myNetwork
stdin_open: true # same as docker -i (interactive)
tty: true # same as docker -t (tty); see if sub actually receives pub messages
producer:
depends_on:
- "rabbitmq"
networks:
- myNetwork
networks:
myNetwork:
Il reste à ajouter les instructions de build (chemins de context + dockerfile) pour le consumer et producer. Et les informations d'image pour rabbitmq.
On ajoutera aussi que l'on souhaite se connecter au terminal (tty) du conteneur 'consumer' pour voir les messages lus.
On obtient finalement un fichier docker-compose.yml comme celui-ci :
version: "3"
services:
rabbitmq:
image: rabbitmq:3-management
hostname: rabbitmq
networks:
- myNetwork
consumer:
build:
context: . # Docker context from folder of this file; needed to include requirement.txt
dockerfile: ./consumer/Dockerfile
depends_on:
- "producer"
networks:
- myNetwork
stdin_open: true # same as docker -i (interactive)
tty: true # same as docker -t (tty); see if sub actually receives pub messages
producer:
build:
context: .
dockerfile: ./producer/Dockerfile
depends_on:
- "rabbitmq"
networks:
- myNetwork
networks:
myNetwork:
Tests et commandes
Test avec 1 consumer et 1 producer
docker-compose up
Test avec 1 consumer et X producers
docker-compose up --scale producer=X
Exemple avec producer=5
:
Voici un fichier .zip contenant l'arborescence complète et tous les fichiers du projet : ICI