Reading view

There are new articles available, click to refresh the page.

HAProxy EP 8: Load Balancing with Random Load Balancing

Load balancing distributes client requests across multiple servers to ensure high availability and reliability. One of the simplest load balancing algorithms is Random Load Balancing, which selects a backend server randomly for each client request.

Although this approach does not consider server load or other metrics, it can be effective for less critical applications or when the goal is to achieve simplicity.

What is Random Load Balancing?

Random Load Balancing assigns incoming requests to a randomly chosen server from the available pool of servers. This method is straightforward and ensures that requests are distributed in a non-deterministic manner, which may work well for environments with equally capable servers and minimal concerns about server load or state.

Step-by-Step Implementation with Docker

Step 1: Create Dockerfiles for Each Flask Application

We’ll use the same three Flask applications (app1.py, app2.py, and app3.py) as in previous examples.

Flask App 1 – (app.py)

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello from Flask App 1!"

@app.route("/data")
def data():
    return "Data from Flask App 1!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)


Flask App 2 – (app.py)


from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello from Flask App 2!"

@app.route("/data")
def data():
    return "Data from Flask App 2!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5002)

Flask App 3 – (app.py)

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello from Flask App 3!"

@app.route("/data")
def data():
    return "Data from Flask App 3!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5003)


Step 2: Create Dockerfiles for Each Flask Application

Create Dockerfiles for each of the Flask applications:

  • Dockerfile for Flask App 1 (Dockerfile.app1):
# Use the official Python image from Docker Hub
FROM python:3.9-slim

# Set the working directory inside the container
WORKDIR /app

# Copy the application file into the container
COPY app1.py .

# Install Flask inside the container
RUN pip install Flask

# Expose the port the app runs on
EXPOSE 5001

# Run the application
CMD ["python", "app1.py"]

  • Dockerfile for Flask App 2 (Dockerfile.app2):
FROM python:3.9-slim
WORKDIR /app
COPY app2.py .
RUN pip install Flask
EXPOSE 5002
CMD ["python", "app2.py"]


  • Dockerfile for Flask App 3 (Dockerfile.app3):

FROM python:3.9-slim
WORKDIR /app
COPY app3.py .
RUN pip install Flask
EXPOSE 5003
CMD ["python", "app3.py"]

Step 3: Create a Dockerfile for HAProxy

HAProxy Configuration file,


global
    log stdout format raw local0
    daemon

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

frontend http_front
    bind *:80
    default_backend servers

backend servers
    balance random
    random draw 2
    server server1 app1:5001 check
    server server2 app2:5002 check
    server server3 app3:5003 check

Explanation:

  • The balance random directive tells HAProxy to use the Random load balancing algorithm.
  • The random draw 2 setting makes HAProxy select 2 servers randomly and choose the one with the least number of connections. This adds a bit of load awareness to the random choice.
  • The server directives define the backend servers and their ports.

Step 4: Create a Dockerfile for HAProxy

Create a Dockerfile for HAProxy (Dockerfile.haproxy):

# Use the official HAProxy image from Docker Hub
FROM haproxy:latest

# Copy the custom HAProxy configuration file into the container
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

# Expose the port for HAProxy
EXPOSE 80


Step 5: Create a docker-compose.yml File

To manage all the containers together, create a docker-compose.yml file:


version: '3'

services:
  app1:
    build:
      context: .
      dockerfile: Dockerfile.app1
    container_name: flask_app1
    ports:
      - "5001:5001"

  app2:
    build:
      context: .
      dockerfile: Dockerfile.app2
    container_name: flask_app2
    ports:
      - "5002:5002"

  app3:
    build:
      context: .
      dockerfile: Dockerfile.app3
    container_name: flask_app3
    ports:
      - "5003:5003"

  haproxy:
    build:
      context: .
      dockerfile: Dockerfile.haproxy
    container_name: haproxy
    ports:
      - "80:80"
    depends_on:
      - app1
      - app2
      - app3

Explanation:

  • The docker-compose.yml file defines the services (app1, app2, app3, and haproxy) and their respective configurations.
  • HAProxy depends on the three Flask applications to be up and running before it starts.

Step 6: Build and Run the Docker Containers

Run the following command to build and start all the containers:


docker-compose up --build

This command builds Docker images for all three Flask apps and HAProxy, then starts them.

Step 7: Test the Load Balancer

Open your browser or use curl to make requests to the HAProxy server:

curl http://localhost/
curl http://localhost/data

Observation:

  • With Random Load Balancing, each request should randomly hit one of the three backend servers.
  • Since the selection is random, you may not see a predictable pattern; however, the requests should be evenly distributed across the servers over a large number of requests.

Conclusion

By implementing Random Load Balancing with HAProxy, we’ve demonstrated a simple way to distribute traffic across multiple servers without relying on complex metrics or state information. While this approach may not be ideal for all use cases, it can be useful in scenarios where simplicity is more valuable than fine-tuned load distribution.

HAProxy EP 5: Load Balancing With Round Robin

Load balancing is crucial for distributing incoming network traffic across multiple servers, ensuring optimal resource utilization and improving application performance. One of the simplest and most popular load balancing algorithms is Round Robin. In this blog, we’ll explore how to implement Round Robin load balancing using Flask as our backend application and HAProxy as our load balancer.

What is Round Robin Load Balancing?

Round Robin load balancing works by distributing incoming requests sequentially across a group of servers.

For example, the first request goes to Server A, the second to Server B, the third to Server C, and so on. Once all servers have received a request, the cycle repeats. This algorithm is simple and works well when all servers have similar capabilities.

Step-by-Step Implementation with Docker

Step 1: Create Dockerfiles for Each Flask Application

We’ll create three separate Dockerfiles, one for each Flask app.

Flask App 1 (app1.py)

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello from Flask App 1!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)


Flask App 2 (app2.py)

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello from Flask App 2!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5002)


Flask App 3 (app3.py)


from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello from Flask App 3!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5003)

Each Flask app listens on a different port (5001, 5002, 5003).

Step 2: Dockerfiles for each flask application

Dockerfile for Flask App 1 (Dockerfile.app1)


# Use the official Python image from the Docker Hub
FROM python:3.9-slim

# Set the working directory inside the container
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY app1.py .

# Install Flask inside the container
RUN pip install Flask

# Expose the port the app runs on
EXPOSE 5001

# Run the application
CMD ["python", "app1.py"]

Dockerfile for Flask App 2 (Dockerfile.app2)


FROM python:3.9-slim
WORKDIR /app
COPY app2.py .
RUN pip install Flask
EXPOSE 5002
CMD ["python", "app2.py"]

Dockerfile for Flask App 3 (Dockerfile.app3)


FROM python:3.9-slim
WORKDIR /app
COPY app3.py .
RUN pip install Flask
EXPOSE 5003
CMD ["python", "app3.py"]

Step 3: Create a configuration for HAProxy

global
    log stdout format raw local0
    daemon

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

frontend http_front
    bind *:80
    default_backend servers

backend servers
    balance roundrobin
    server server1 app1:5001 check
    server server2 app2:5002 check
    server server3 app3:5003 check

Explanation:

  • frontend http_front: Defines the entry point for incoming traffic. It listens on port 80.
  • backend servers: Specifies the servers HAProxy will distribute traffic evenly the three Flask apps (app1, app2, app3). The balance roundrobin directive sets the Round Robin algorithm for load balancing.
  • server directives: Lists the backend servers with their IP addresses and ports. The check option allows HAProxy to monitor the health of each server.

Step 4: Create a Dockerfile for HAProxy

Create a Dockerfile for HAProxy (Dockerfile.haproxy)


# Use the official HAProxy image from Docker Hub
FROM haproxy:latest

# Copy the custom HAProxy configuration file into the container
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

# Expose the port for HAProxy
EXPOSE 80

Step 5: Create a Dockercompose file

To manage all the containers together, create a docker-compose.yml file


version: '3'

services:
  app1:
    build:
      context: .
      dockerfile: Dockerfile.app1
    container_name: flask_app1
    ports:
      - "5001:5001"

  app2:
    build:
      context: .
      dockerfile: Dockerfile.app2
    container_name: flask_app2
    ports:
      - "5002:5002"

  app3:
    build:
      context: .
      dockerfile: Dockerfile.app3
    container_name: flask_app3
    ports:
      - "5003:5003"

  haproxy:
    build:
      context: .
      dockerfile: Dockerfile.haproxy
    container_name: haproxy
    ports:
      - "80:80"
    depends_on:
      - app1
      - app2
      - app3

Explanation:

  • The docker-compose.yml file defines four services: app1, app2, app3, and haproxy.
  • Each Flask app is built from its respective Dockerfile and runs on its port.
  • HAProxy is configured to wait (depends_on) for all three Flask apps to be up and running.

Step 6: Build and Run the Docker Containers

Run the following commands to build and start all the containers:


# Build and run the containers
docker-compose up --build

This command will build Docker images for all three Flask apps and HAProxy and start them up in the background.

You should see the responses alternating between “Hello from Flask App 1!”, “Hello from Flask App 2!”, and “Hello from Flask App 3!” as HAProxy uses the Round Robin algorithm to distribute requests.

Step 7: Test the Load Balancer

Open your browser or use a tool like curl to make requests to the HAProxy server:


curl http://localhost

Tasks – Docker

  1. Install Docker on your local machine. Verify the installation by running the hello-world container.
  2. Pull the nginx image from Docker Hub and run it as a container. Map port 80 of the container to port 8080 of your host.
  3. Create a Dockerfile for a simple Node.js application that serves “Hello World” on port 3000. Build the Docker image with tag my-node-app and run a container. Below is the sample index.js file.

const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.send('Hello World');
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

4. Tag the Docker image my-node-app from Task 3 with a version tag v1.0.0.

5. Push the tagged image from Task 4 to your Docker Hub repository.

6. Run a container from the ubuntu image and start an interactive shell session inside it. You can run commands like ls, pwd, etc.

7. Create a Dockerfile for a Go application that uses multi-stage builds to reduce the final image size. The application should print “Hello Docker”. Sample Go code.


package main

import "fmt"

func main() {
    fmt.Println("Hello Docker")
}

8. Create a Docker volume and use it to persist data for a MySQL container. Verify that the data persists even after the container is removed. Try creating a test db.

9. Create a custom Docker network and run two containers (e.g., nginx and mysql) on that network. Verify that they can communicate with each other.

10. Create a docker-compose.yml file to define and run a multi-container Docker application with nginx as a web server and mysql as a database.

11. Scale the nginx service in the previous Docker Compose setup to run 3 instances.

12. Create a bind mount to share data between your host system and a Docker container running nginx. Modify a file on your host and see the changes reflected in the container.

13. Add a health check to a Docker container running a simple Node.js application. The health check should verify that the application is running and accessible.

Sample Healthcheck API in node.js,


const express = require('express');
const app = express();

// A simple route to return the status of the application
app.get('/health', (req, res) => {
    res.status(200).send('OK');
});

// Example main route
app.get('/', (req, res) => {
    res.send('Hello, Docker!');
});

// Start the server on port 3000
const port = 3000;
app.listen(port, () => {
    console.log(`App is running on http://localhost:${port}`);
});

14. Modify a Dockerfile to take advantage of Docker’s build cache, ensuring that layers that don’t change are reused.

15. Run a PostgreSQL database in a Docker container and connect to it using a database client from your host.

16. Create a custom Docker network and run a Node.js application and a MongoDB container on the same network. The Node.js application should connect to MongoDB using the container name.


const mongoose = require('mongoose');

mongoose.connect('mongodb://mongodb:27017/mydatabase', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
}).then(() => {
  console.log('Connected to MongoDB');
}).catch(err => {
  console.error('Connection error', err);
});

17. Create a docker-compose.yml file to set up a MEAN (MongoDB, Express.js, Angular, Node.js) stack with services for each component.

18. Use the docker stats command to monitor resource usage (CPU, memory, etc.) of running Docker containers.

docker run -d --name busybox1 busybox sleep 1000
docker run -d --name busybox2 busybox sleep 1000

19. Create a Dockerfile for a simple Python Flask application that serves “Hello World”.

20. Configure Nginx as a reverse proxy to forward requests to a Flask application running in a Docker container.

21. Use docker exec to run a command inside a running container.


docker run -d --name ubuntu-container ubuntu sleep infinity

22. Modify a Dockerfile to create and use a non-root user inside the container.

23. Use docker logs to monitor the output of a running container.

24. Use docker system prune to remove unused Docker data (e.g., stopped containers, unused networks).

25. Run a Docker container in detached mode and verify that it’s running in the background.

26. Configure a Docker container to use a different logging driver (e.g., json-file or syslog).

27. Use build arguments in a Dockerfile to customize the build process.


from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, Docker Build Arguments!'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

28. Set CPU and memory limits for a Docker container (for busybox)

Docker Ep 12 – Cheatsheet

Here’s a Docker cheat sheet that covers the most commonly used Docker commands, organized by categories.

Docker Basics

  • docker --version: Show the Docker version installed on your system.
  • docker info: Display system-wide information, including Docker version, number of containers, and images.
  • docker help: Get help on Docker commands.

Docker Images

  • docker images: List all Docker images on your system.
  • docker pull <image>: Download an image from a Docker registry (e.g., Docker Hub).
  • docker build -t <image_name> .: Build an image from a Dockerfile in the current directory and tag it with a name.
  • docker tag <image_id> <new_image_name>: Tag an image with a new name.
  • docker rmi <image>: Remove one or more images.
  • docker history <image>: Show the history of an image (layers).

Docker Containers

  • docker ps: List all running containers.
  • docker ps -a: List all containers (running and stopped).
  • docker run <image>: Run a container from an image.
  • docker run -d <image>: Run a container in detached mode (in the background).
  • docker run -it <image>: Run a container in interactive mode with a terminal.
  • docker run -p <host_port>:<container_port> <image>: Map a port from the host to the container.
  • docker stop <container>: Stop a running container.
  • docker start <container>: Start a stopped container.
  • docker restart <container>: Restart a running container.
  • docker rm <container>: Remove a stopped container.
  • docker logs <container>: View the logs of a container.
  • docker exec -it <container> <command>: Execute a command inside a running container (e.g., bash to open a shell).

Docker Networks

  • docker network ls: List all Docker networks.
  • docker network create <network_name>: Create a new Docker network.
  • docker network inspect <network_name>: View detailed information about a network.
  • docker network connect <network_name> <container>: Connect a container to a network.
  • docker network disconnect <network_name> <container>: Disconnect a container from a network.
  • docker network rm <network_name>: Remove a Docker network.

Docker Volumes

  • docker volume ls: List all Docker volumes.
  • docker volume create <volume_name>: Create a new Docker volume.
  • docker volume inspect <volume_name>: View detailed information about a volume.
  • docker volume rm <volume_name>: Remove a Docker volume.
  • docker run -v <volume_name>:<container_path> <image>: Mount a volume inside a container.

Docker Compose

  • docker-compose up: Start the services defined in a docker-compose.yml file.
  • docker-compose down: Stop and remove containers, networks, volumes, and images created by docker-compose up.
  • docker-compose build: Build or rebuild services defined in a docker-compose.yml file.
  • docker-compose ps: List containers managed by Docker Compose.
  • docker-compose logs: View logs for services managed by Docker Compose.
  • docker-compose exec <service> <command>: Execute a command in a running service.

Dockerfile Directives

  • FROM: Specifies the base image.
  • WORKDIR: Sets the working directory inside the container.
  • COPY: Copies files from the host to the container.
  • RUN: Executes a command in the container.
  • CMD: Specifies the command to run when the container starts.
  • EXPOSE: Specifies the port on which the container will listen.
  • ENV: Sets environment variables.
  • ENTRYPOINT: Configures the container to run as an executable.

Docker Cleanup Commands

  • docker system prune: Remove unused data (stopped containers, unused networks, dangling images, etc.).
  • docker container prune: Remove all stopped containers.
  • docker image prune: Remove unused images.
  • docker volume prune: Remove all unused volumes.
  • docker network prune: Remove all unused networks.

Miscellaneous

  • docker inspect <container_or_image>: Return low-level information on Docker objects (containers, images, volumes, etc.).
  • docker stats: Display a live stream of container(s) resource usage statistics.
  • docker top <container>: Display the running processes of a container.
  • docker cp <container>:<path> <local_path>: Copy files from a container to the host or vice versa.

Docker Ep 9: The Building Blocks – Detailed Structure of a Dockerfile

Alex now knows the basics, but it’s time to get their hands dirty by writing an actual Dockerfile.

The FROM Instruction: Choosing the Foundation

The first thing Alex learns is the FROM instruction, which sets the base image for their container. It’s like choosing the foundation for a house.

  • Purpose:
    • The FROM instruction initializes a new build stage and sets the Base Image for subsequent instructions.
  • Choosing a Base Image:
    • Alex decides to use a Python base image for their application. They learn that python:3.9-slim is a lightweight version, saving space and reducing the size of the final image.

FROM python:3.9-slim

Example: Think of FROM as picking the type of bread for your sandwich. Do you want white, whole wheat, or maybe something gluten-free? Your choice sets the tone for the rest of the recipe.

The LABEL Instruction: Adding Metadata (Optional)

Next, Alex discovers the LABEL instruction. While optional, it’s a good practice to include metadata about the image.

  • Purpose:
    • The LABEL instruction adds metadata like version, description, or maintainer information to the image.
  • Example:
    • Alex decides to add a maintainer label:

LABEL maintainer="alex@example.com"

Story Note: This is like writing your name on a sandwich wrapper, so everyone knows who made it and what’s inside.

The RUN Instruction: Building the Layers

The RUN instruction is where Alex can execute commands inside the image, such as installing dependencies.

  • Purpose:
    • The RUN instruction runs any commands in a new layer on top of the current image and commits the results.
  • Example:
    • To install the Flask framework, Alex writes:

RUN pip install flask

They also learn to combine commands to reduce layers:


RUN apt-get update && apt-get install -y curl

Story Note: Imagine slicing tomatoes and cheese for your sandwich and placing them carefully on top. Each ingredient (command) adds a layer of flavor.

The COPY and ADD Instructions: Bringing in Ingredients

Now, Alex needs to bring their application code into the container, which is where the COPY and ADD instructions come into play.

  • COPY:
    • The COPY instruction copies files or directories from the host filesystem into the container’s filesystem.
  • ADD:
    • The ADD instruction is similar to COPY but with additional features, like extracting compressed files.
  • Example:
    • Alex copies their application code into the container:

COPY . /app

Story Note: This is like moving ingredients from your fridge (host) to the counter (container) where you’re preparing the sandwich.

The WORKDIR Instruction: Setting the Workspace

Alex learns that setting a working directory makes it easier to manage paths within the container.

  • Purpose:
    • The WORKDIR instruction sets the working directory for subsequent instructions.
  • Example:
    • Alex sets the working directory to /app:

WORKDIR /app

Story Note: This is like setting up a designated area on your counter where you’ll assemble your sandwich—keeping everything organized.

The CMD and ENTRYPOINT Instructions: The Final Touch

Finally, Alex learns how to define the default command that will run when the container starts.

  • CMD:
    • Provides defaults for an executing container, but can be overridden.
  • ENTRYPOINT:
    • Configures a container that will run as an executable, making it difficult to override.
  • Example:
    • Alex uses CMD to specify the command to start their Flask app:

CMD ["python", "app.py"]

Story Note: Think of CMD as the final step in making your sandwich—like deciding to add a toothpick to hold everything together before serving.

Below is an example Dockerfile of a flask application,


# Use an official Python runtime as a parent image
FROM python:3.9-slim

# Set the working directory in the container
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Make port 80 available to the world outside this container
EXPOSE 80

# Define environment variable
ENV NAME World

# Run app.py when the container launches
CMD ["python", "app.py"]

Breakdown of the Dockerfile:

  1. FROM python:3.9-slim:
    • This line specifies the base image. In this case, it uses a slim version of Python 3.9, which is lightweight and sufficient for a simple Flask application.
  2. WORKDIR /app:
    • This sets the working directory inside the container to /app. All subsequent commands will be run inside this directory.
  3. COPY . /app:
    • This copies everything from your current directory on the host machine into the /app directory inside the container.
  4. RUN pip install –no-cache-dir -r requirements.txt:
    • This installs the necessary Python packages listed in the requirements.txt file. The --no-cache-dir option reduces the image size by not caching the downloaded packages.
  5. EXPOSE 80:
    • This makes port 80 available for external access. It’s where the Flask application will listen for incoming requests.
  6. ENV NAME World:
    • This sets an environment variable NAME to “World”. You can access this variable in your Python code.
  7. CMD [“python”, “app.py”]:
    • This tells the container to run the app.py file using Python when it starts.

Example Flask Application (app.py):

To complete the example, here’s a simple Flask application you can use:


from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    name = os.getenv('NAME', 'World')
    return f'Hello, {name}!'

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80)

Example requirements.txt:

And here’s the requirements.txt file listing the dependencies for the Flask app:


Flask==2.0.3

Building and Running the Docker Image:

  1. Build the Docker image using the Dockerfile:
docker build -t my-flask-app .

2. Run the Docker container:


docker run -p 4000:80 my-flask-app
  • This maps port 4000 on your host machine to port 80 in the container.

Open your browser and go to http://localhost:4000, and you should see “Hello, World!” displayed on the page.

You can customize the ENV NAME in the Dockerfile or by passing it as an argument when running the container:


docker run -p 4000:80 -e NAME=Alex my-flask-app

This will display “Hello, Alex!” instead.

❌