Deploy ứng dụng load balancer sử dụng Nginx với Docker

Bài viết được sự cho phép của tác giả Nguyễn Hữu Khanh

Load balancer là một khái niệm về cân bằng tải, được sử dụng để giúp ứng dụng của chúng ta có thể handle một số lượng lớn các request từ người dùng. Chúng ta sẽ deploy ứng dụng của mình lên nhiều máy khác nhau, behind một load balancer, các request từ người dùng sẽ gọi tới load balancer và sẽ được load balancer forward tới một trong các máy này… Nginx là một trong những web server có thể hỗ trợ chúng ta hiện thực phần load balancer đó các bạn! Cụ thể như thế nào? Trong bài viết này, mình sẽ hướng dẫn các bạn deploy ứng dụng load balancer sử dụng Nginx với Docker các bạn nhé!

  Cách tạo một Docker đơn giản cho Node.JS
  Cách thiết lập một dự án Symfony để làm việc với Docker Subdomains

Xem thêm các việc làm Java lương cao trên TopDev

Để làm ví dụ cho bài viết này, mình sẽ sử dụng Docker Compose để mô phỏng một mô hình deploy sử dụng load balancer. Mình sẽ deploy ứng dụng ví dụ trong bài viết Giới thiệu về Docker Compose trên 3 container khác nhau sử dụng chung một PostgreSQL, một Nginx container làm nhiệm vụ load balancer forward request của người dùng tới 1 trong 3 container này:

Ứng dụng ví dụ

Với ứng dụng ví dụ, để biết Ngnix đang forward request của người dùng tới instance nào, mình sẽ add thêm code trong phương thức helloDockerCompose() của request “/hello” in ra dòng log “Received request …” như sau:

package com.huongdanjava.springboot;

import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.jdbc.metadata.HikariDataSourcePoolMetadata;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zaxxer.hikari.HikariDataSource;

@SpringBootApplication
@RestController
public class SpringBootDockerComposeApplication {

private static final Logger LOGGER =
LoggerFactory.getLogger(SpringBootDockerComposeApplication.class);

@Autowired
private DataSource dataSource;

@RequestMapping("/hello")
public String helloDockerCompose() {
LOGGER.info("Received request ...");

Integer idleConnection =
new HikariDataSourcePoolMetadata((HikariDataSource) dataSource).getIdle();

return "Hello Docker Compose! Idle connection to database is " + idleConnection;
}

public static void main(String[] args) {
SpringApplication.run(SpringBootDockerComposeApplication.class, args);
}

}

Khi chạy ứng dụng, chúng ta sẽ take a look vào Console của Docker Compose để biết là request đang được forward tới container nào.

Hãy build lại Docker Image cho ứng dụng ví dụ của chúng ta các bạn nhé!

Cấu hình của ứng dụng ví dụ cho 3 container tương ứng với 3 services trong tập tin docker-compose.yml, trong bài viết Giới thiệu về Docker Compose  sẽ như sau:

spring_boot_docker_compose_1:
image: spring-boot-docker-compose
container_name: spring_boot_docker_compose_1
depends_on:
- postgresql
environment:
DATABASE_USERNAME: "khanh"
DATABASE_PASSWORD: "123456"
DATABASE_HOST: "postgresql"
DATABASE_NAME: "test"
DATABASE_PORT: 5432
ports:
- 8081:8080
networks:
- huongdanjava

spring_boot_docker_compose_2:
image: spring-boot-docker-compose
container_name: spring_boot_docker_compose_2
depends_on:
- postgresql
environment:
DATABASE_USERNAME: "khanh"
DATABASE_PASSWORD: "123456"
DATABASE_HOST: "postgresql"
DATABASE_NAME: "test"
DATABASE_PORT: 5432
ports:
- 8082:8080
networks:
- huongdanjava

spring_boot_docker_compose_3:
image: spring-boot-docker-compose
container_name: spring_boot_docker_compose_3
depends_on:
- postgresql
environment:
DATABASE_USERNAME: "khanh"
DATABASE_PASSWORD: "123456"
DATABASE_HOST: "postgresql"
DATABASE_NAME: "test"
DATABASE_PORT: 5432
ports:
- 8083:8080
networks:
- huongdanjava

Mỗi container sẽ expose port khác nhau bao gồm: 8081, 8082 và 8083.
Cấu hình của PostgreSQL database trong tập tin docker-compose.yaml, không đổi các bạn nhé!

Nginx

Trước khi đi vào chi tiết cấu hình load balancer với Nginx như thế nào, chúng ta sẽ điểm sơ qua một số thuật toán được sử dụng để hiện thực load balancer các bạn nhé! Chúng ta có một số thuật toán cơ bản sau:

  • Round Robin: các request từ người dùng sẽ được forward lần lượt đến tất cả các máy theo thứ tự. Có nghĩa là ứng dụng của chúng ta được deploy lên bao nhiêu máy thì request của người dùng sẽ lần lượt được forward đến tất cả các máy này. Request đầu tiên sẽ vào máy 1, request thứ 2 sẽ vào máy 2, … sau khi máy cuối cùng được sử dụng thì request tiếp theo sẽ vào máy 1.
  • Weighted Round Robin: tương tự như thuật toán Round Robin nhưng các máy deploy ứng dụng sẽ có cấu hình khác nhau. Máy nào có cấu hình cao hơn sẽ được đánh trọng số cao hơn và nhận được nhiều request hơn các bạn nhé!
  • Dynamic Round Robin: thuật toán này giống như Weighted Round Robin nhưng có sự khác biệt là trọng số của các máy sẽ không cố định. Cấu hình của các máy sẽ được kiểm tra liên tục, do đó trọng số sẽ thay đổi liên tục.
  • Fastest: thuật toán này dựa vào thời gian response của máy trong setup load balancer. Máy nào có thời gian response nhanh hơn sẽ được chọn để forward request.
  • Least Connections: máy nào có ít kết nối nhất sẽ được forward request tới.

Trong bài viết này, mình sẽ sử dụng thuật toán Round Robin với Nginx để làm ví dụ các bạn nhé!

Mình sẽ build một custom Nginx Docker image với Dockerfile có nội dung như sau:

FROM nginx:latest

RUN rm /etc/nginx/conf.d/default.conf

COPY nginx.conf /etc/nginx/conf.d/default.conf

trong đó, tập tin nginx.conf có nội dung như sau:

upstream apps {
server 172.17.0.1:8081;
server 172.17.0.1:8082;
server 172.17.0.1:8083;
}

server {
location / {
proxy_pass http://apps;
}
}

Trong tập tin nginx.conf trên, mình đã sử dụng reverse proxy của Nginx với directive upstream để hiện thực load balancer. Khi một request từ người dùng đến, request này sẽ được forward một trong 3 server được khai báo bên trong directive upstream này. Mặc định thì Nginx hỗ trợ thuật toán round-robin nên các bạn không cần khai báo gì thêm ngoài thông tin của 3 server.

Cũng cần nói thêm là khi start các container 172.17.0.1 là địa chỉ IP mặc định khi các container được start, nên như các bạn thấy, mình sử dụng 172.17.0.1 cho 3 container của ứng dụng ví dụ, với các port khác nhau như đã khai báo trong tập tin docker-compose.yaml.

Các bạn cần build custom Docker image này bằng câu lệnh sau:

docker build -t nginx:0.0.1 .

Kết quả:

Sau khi đã có Docker Image của Nginx, các bạn có thể khai báo một service cho Nginx trong tập tin docker-compose.yaml như sau:

nginx:
image: nginx:0.0.1
ports:
- 80:80

Đến đây thì chúng ta đã hoàn thành việc cấu hình để deploy load balancer cho ứng dụng ví dụ của chúng ta. Nội dung của tập tin docker-compose.yaml lúc này như sau:

version: '3.8'

services:
postgresql: 
image: postgres:13.3
container_name: postgres
restart: on-failure:5
environment:
POSTGRES_PASSWORD: "123456"
POSTGRES_USER: "khanh"
POSTGRES_DB: "test"
volumes:
- /Users/khanh/data:/var/lib/postgresql/data
ports:
- 5432:5432
networks:
- huongdanjava

spring_boot_docker_compose_1:
image: spring-boot-docker-compose
container_name: spring_boot_docker_compose_1
depends_on:
- postgresql
environment:
DATABASE_USERNAME: "khanh"
DATABASE_PASSWORD: "123456"
DATABASE_HOST: "postgresql"
DATABASE_NAME: "test"
DATABASE_PORT: 5432
ports:
- 8081:8080
networks:
- huongdanjava

spring_boot_docker_compose_2:
image: spring-boot-docker-compose
container_name: spring_boot_docker_compose_2
depends_on:
- postgresql
environment:
DATABASE_USERNAME: "khanh"
DATABASE_PASSWORD: "123456"
DATABASE_HOST: "postgresql"
DATABASE_NAME: "test"
DATABASE_PORT: 5432
ports:
- 8082:8080
networks:
- huongdanjava

spring_boot_docker_compose_3:
image: spring-boot-docker-compose
container_name: spring_boot_docker_compose_3
depends_on:
- postgresql
environment:
DATABASE_USERNAME: "khanh"
DATABASE_PASSWORD: "123456"
DATABASE_HOST: "postgresql"
DATABASE_NAME: "test"
DATABASE_PORT: 5432
ports:
- 8083:8080
networks:
- huongdanjava

nginx:
image: nginx:0.0.1
ports:
- 80:80

networks:
huongdanjava:
driver: bridge

Chạy tập tin này với docker compose up, các bạn sẽ thấy kết quả như sau:
Lúc này, nếu các bạn request tới http://localhost/hello lần thứ nhất, các bạn sẽ thấy Console của Docker Compose có kết quả như sau:

Container spring_boot_docker_compose_1 đã handle request này.

Lần thứ 2:

Lần này thì container spring_boot_docker_compose_2 handle request.
Lần thứ 3:

Và lần thứ tư sẽ quay lại spring_boot_docker_compose_1:

Đúng như expectation của chúng ta rồi đó các bạn!

Bài viết gốc được đăng tải tại huongdanjava.com

Có thể bạn quan tâm:

Xem thêm Việc làm IT hấp dẫn trên TopDev