Vẫn theo khung sườn đã định trước từ phần 1, trong phần này tôi sẽ giới thiệu cách cấu hình nginx để thực hiện vai trò của một load balancer.

Trước khi đi vào cách triển khai, tôi sẽ đi qua một chút về khái niệm load balancing.

Sơ lược về load balancing

Hình vẽ dưới là mô hình load balancing điển hình. Đây cũng là mô hình load balancing nginx được triển khai trong bài viết này

alt text

Load balancer là một reverse proxy phân chia request đến nhiều backend server nhằm mở rộng băng thông, giảm độ trễ của công tác xử lý, giảm tải cho mỗi backend server, đảm bảo fault-tolerance.

Tôi sẽ đi vào từng ý một trong đoạn trên.

Reverse proxy:

Đối lập với forward proxy, đây là một loại proxy phía server. Server đóng vai trò làm reverse proxy sẽ chắn trước các request từ client đẩy đến và che dấu toàn bộ backend server đằng sau. Client sẽ không thể biết đang có một nhóm các backend server đằng sau reverse proxy phục vụ cho nó ( cái này gọi là đặc tính trong suốt của proxy. Đặc tính này giúp đơn giản hóa nhiều cho kết nối từ client. Client biết càng ít thì càng tốt )

Tại sao load balancer lại mở rộng băng thông ?

Thay vì bạn có một đường network đến một backend server thì bây giờ bạn có hai đường đến hai backend server. Thực chất băng thông chỉ mở rộng từ load balancer đến backend server còn băng thông từ client đến balancer vẫn không đổi (vi tôi chỉ có một load balancer)

Tại sao load balancer lại giảm độ trễ của công tác xử lý ?

Tuy có nhiều hơn một backend server để xử lý các request nhưng một request vẫn chỉ do một backend server xử lý. Nếu backend server này đang xử lý quá nhiều request, các request đến sau sẽ phải đợi. Điều này gây trễ trong công tác xử lý. Load balancer giải quyết điều này bằng các thuật toán kiểm tra. Load balancer có thể xác định được backend server nào đang có ít connect đến nhất (hàng đợi ngắn nhất) để đẩy request đến nhờ vậy đỗ trễ xử lý của backend sẽ giảm đi (Tuy vậy giải pháp này không giải quyết triệt để vấn đề do các request khác nhau lại cần thời gian xử lý khác nhau, tuy ít request mà toàn heavy long request thì xử lý vẫn chậm như thường)

Tại sao load balancer lại giảm tải cho mỗi backend server ?

Rất đơn gỉan, một khối lượng 1000 request được chia cho hai backend server thì tất nhiên khối lượng xử lý trên từng backend server sẽ giảm đáng kể. Chi tiết khối lượng xử lý trên từng con có thể khác biệt tùy thuộc vào thuật toán mà load balancer sử dụng để quyết định đẩy request đến backend nào.

Load balancer hỗ trợ fault tolerance thế nào?

Fault tolerance dịch là khả nẳng chịu lỗi thì phải. Tôi thấy dịch hơi chuối nên để nguyên tiếng Anh. Thay vì đưa ra định nghĩa cho fault tolerance, tôi sẽ đưa ra tình huống mà tính năng này của load balancer bộc lộ. Giả sử có một backend server trong cụm bị down hoặc network của nó có vấn đề. Nếu load balancer lúc nào cũng chăm chăm đẩy request về backend mà không biết đến tình trạng các backend thì có thể xảy ra hiện tượng một cơ số request đẩy về server bị down sẽ không được xử lý. Để giải quyết tình huống này, một load balancer cần có cơ chế gọi là health check. Nó sẽ định kỳ kiểm tra tình trạng của các backend server. Nếu server nào down nó sẽ bỏ qua server đó khỏi cụm backend. Các request từ client sẽ không còn được load balancer forward về đó nữa. Tóm lại, fault tolerance là cơ chế cho phép hệ thống vẫn đáp ứng dịch vụ bình thường khi có lỗi xảy ra.

Với những lý do kể trên có thể thấy load balancer có thể giúp tăng performance, reliability cho web application. Mô hình này cũng rất dễ mở rộng. Bạn chỉ cần thêm backend server vào cụm.

Nhiều ưu điểm quá vậy nhược điểm của load balancer là gì ?

Có thể nhiều người nghĩ load balancer sẽ dễ bị quá tải khi mà cần phải hứng một lượng request của hai con backend nhưng thực sự không phải vậy. Nhiệm vụ của con này chỉ là forward request đến backend server. Thực tế, tôi thấy các load balancer có load rất thấp. Vấn đề nằm ở network nhiều hơn. Việc tập trung request vào một con load balancer có thể gây nghẽn network trên con này.

Mô hình load balancer trong hình vẽ ở trên có một điểm dở là tất tần tật đều tập trung vào load balancer. Đây là một SPOF (Single point of failure). Nếu con load balancer này down thì cả hệ thống sụp đổ. Vấn đề này có thể giải quyết bằng cách sử dụng thêm một con load balancer nữa dự phòng và thực hiện failover giữa hai con bằng keepalived. Chi tiết cách thực hiện tôi sẽ dành vào trong một bài viết khác.

Mô hình sử dụng trong bài viết

Tôi dùng ba server. Cả ba server đều cài nginx. Cả ba đều cài đặt vhost vhost.example.com (cách làm các bạn có thể xem trong các bài viết Cấu hình nginx cơ bản phần 1 và phần 2). Tôi vẫn tiếp tục hỗ trợ ssl cho vhost này. Trong ba server đo, một server làm load balancer. Con này sẽ listen trên port 443, hỗ trợ https – tôi tiếp tục dùng web server đã sử dụng làm demo trong các bài viết trước, lần này tôi chỉ nâng cấp cho nó để hỗ trợ load balancer thôi. Trong mô hình có sự tham gia của load balancer, tôi chỉ cần mã hóa đường truyền qua https giữa client và load balancer. Hai server còn lại làm backend, không cần hỗ trợ https, chỉ cần listen trên port 80

Chi tiết cách làm

Trên load balancer:

Kế thừa các cấu hình trong bài viết Cấu hình nginx cơ bản – Phần 3. Tôi chỉ bổ sung một chút.

Trong /usr/local/nginx/conf/nginx.conf, tôi thêm dòng sau:
include /usr/local/nginx/conf/upstream;

Nội dung file này sẽ như sau:

user nginx; worker_processes 1; error_log /var/log/nginx/error.log; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /usr/local/nginx/conf/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; server { listen 80; server_name localhost; location / { root html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } include /usr/local/nginx/conf/upstream; include /usr/local/nginx/conf.d/*.conf; } 

Tạo một file tương ứng /usr/local/nginx/conf/upstream. File này sẽ chứa thông tin về các cụm load balancer. Nội dung file này như sau:

upstream backend { server 192.168.3.241:80 weight=3 max_fails=3 fail_timeout=10s; server 192.168.3.242:80 max_fails=3 fail_timeout=10s; } 

Tôi có thể bổ sung thêm nhiều upstream khác nhau. Mỗi upstream là một cụm load balancer. Mặc định nginx sẽ sử dụng giải thuật cân bằng tải round robin. Giải thuật này rất đơn giản. Lần lượt các request được đẩy về từng backend server theo tỉ lệ 1:1 Trong file cấu hình upstream tôi có điều chỉnh chút ít khi đặt trọng số là 3 cho backend server thứ nhất. Khi đó tỉ lệ sẽ là 3:1. Cứ sau 3 request liên tiếp đến 192.168.3.241 thì sẽ có tiếp đó một request đến 192.168.3.242 Các tham số max_fails và fail_timeout dùng để đảm bảo health check. Kỹ thuật health check này sẽ loại bỏ backend server bị down sau một khoảng thời gian check thất bại và sẽ đưa backend server đó trở lại sau một khoảng thời gian check thành công.

Trong /usr/local/nginx/conf.d/vhost.example.com.conf, tôi bổ sung thêm các dòng sau:

proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; 

Tôi sẽ đi lần lượt vào từng dòng cấu hình
proxy_pass http://backend; Các bạn có để ý là giá trị backend cũng được khai báo trong upstream file. Đây là một ánh xạ để load balancer tìm thấy các backend server cho vhost này.

proxy_set_header Host $host; Dòng này cực kỳ quan trọng. Nếu không có dòng này, request dù được đẩy đi nhưng không thể được backend server nhận diện. Backend server sẽ trả về lỗi 404. Trong bài cấu hình nginx cơ bản – phần 2, khi giải thích cơ chế mà web server nhận diện một request thuộc về vhost nào tôi có đề cập đến trường Host nằm trong request header. Khi đi qua reverse proxy, mặc định trường Host này sẽ bị thay thế thành tên cụm backend server. Capture bằng tcpdump, tôi thấy request forward đến backend server có giá trị trường Host là Host: backendrn Rõ ràng là với trường Host này backend sẽ không biết phải xử lý thế nào. Tôi muốn giá trị Host của request được forward phải là: Host: vhost.example.comrn. Dòng cấu hình proxy_set_header Host $host;sẽ đơn giản set lại host header bằng đúng host header của request đến và thế là backend server sẽ biết được phải làm gì với các forwarded request này.

proxy_set_header X-Real-IP $remote_addr; X-Real-IP là một trường cho biết IP của client đã kết nối đến proxy. Dòng cấu hình trên sẽ đặt IP của client vào trừong X-Real-IP trong request được forward đến backend server

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; X-Forwarded-For là một trường cho biết danh sách gồm client ip và các proxy ip mà request này đã đi qua. Trường hợp có một proxy thì giá trị trường này cũng giống X-Real-IP. Dòng cấu hình trên sẽ đặt IP của client vào trừong X-Forwarded-For trong request được forward đến backend server

Một số forward proxy thực hiện chức năng ẩn danh sẽ hoàn toàn không set giá trị nào vào các trường X-Forwarded-For hay X-Real-IP. Do đó server nhận được request sẽ không thể nào biết client nào đang thực hiện request đằng sau proxy ( Đây là một đặc tính che dấu thông tin của proxy )

proxy_set_header X-Forwarded-Proto $scheme; Dòng cấu hình trên sẽ đặt giao thức mà client dùng để kết nối với proxy. Trong demo đi kèm bài viết này thì giá trị đó là https.

Bên cạnh các bổ sung, tôi có loại bỏ đi vài dòng như location ~ .php

Kết quả cuối cùng file này có nội dung như sau:

server{ listen 443; server_name vhost.example.com www.vhost.example.com; error_log /var/log/nginx/vhost.example.com_error.log error; access_log /var/log/nginx/vhost.example.com_access.log main; auth_basic "private site"; auth_basic_user_file /usr/local/etc/.vhost.example.com.htpasswd; ssl on; ssl_certificate /usr/local/nginx/certificate/server.crt; ssl_certificate_key /usr/local/nginx/certificate/server.key; allow 192.168.3.0/24; deny all; location /{ index index.html index.php; proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } 

Tại sao lại phải loại bỏ các location khác như location ~ .php hay location /public/ mà tôi đã sử dụng trong các bài trước đó ?

Bởi vì con web server này đang đóng vai trò load balancer cho vhost vhost.example.com Nó sẽ không xử lý các request đến vhost này nữa. Nếu tôi không loại bỏ các location đó thì load balancer tự phục vụ request luôn. Đây không phải là điều tôi mong muốn

Một điều chỉnh nữa mà bạn có thể thực hiện dù cho nếu không làm thì load balancer vẫn hoạt động. Đó là
xóa bỏ dòng root /home/www/vhost.example.com;
và xóa cả root web /home/www/vhost.example.com
vì hiện tại web server không còn cần phục vụ request nữa. Nó chỉ forward request mà thôi.

Trên backend server:
Chỉ cần cấu hình backend server như trong bài cấu hình nginx cơ bản – phần 2 là được. Nội dung file cấu hình vhost.example.com.conf trên cả hai backend server như sau:

server{ listen 80; server_name vhost.example.com www.vhost.example.com; root /home/www/vhost.example.com; error_log /var/log/nginx/vhost.example.com_error.log error; access_log /var/log/nginx/vhost.example.com_access.log main; location /{ index index.html index.php; } location ~ .php { fastcgi_pass unix:/tmp/php_fpm.sock; fastcgi_index index.php; include /usr/local/nginx/conf/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; } } 

Một điểm quan trọng cần đảm bảo là cả hai backend server này có phần thư mục web root đồng bộ nhau. Các bạn có thể thực hiện việc này bằng cách rsync thư mục giữa hai server bằng một cron job định kỳ hàng phút. Đây có lẽ là cách làm đơn giản nhất.

Chú ý
Các kết nối kiểu web socket qua proxy cần vài điều chỉnh về proxy_set_header. Các bạn có thể xem qua tài liệu này: http://nginx.com/blog/websocket-nginx/

Test

Cấu hình vẫn hệt như trên. Trên backend server 192.168.3.241, tôi chuẩn bị file test.php trong /home/www/vhost.example.com có nội dung như sau:

<?php echo "241 php" ?> 

còn trên backend server 192.168.3.242, tôi chuẩn bị file test.php có nội dung:

<?php echo "242 php"; ?> 

Restart service nginx trên cả ba con server. Sau đó trên browser truy cập vào địa chỉ:
https://vhost.example.com/test.php

Bạn sẽ thấy ba request đầu sẽ trả về 241 php.

alt text

Request thứ tư sẽ trả về 242 php
alt text


Sau đó chu kỳ lại lặp lại.

Kết thúc phần 5. Trong phần tới, tôi sẽ tiếp tục trình bày về cấu hình nginx thực hiện chức năng của một proxy cache.

Comments

comments