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

Docker : RabbitMQ / 1 consumer / 1 producer

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