Tổng Quan Về Cân Bằng Tải Trong Dubbo
Trong kiến trúc vi dịch vụ, Apache Dubbo đóng vai trò là một framework RPC mạnh mẽ giúp phân tán tải trọng và tránh lỗi đơn điểm (SPOF). Khi một dịch vụ được triển khai trên nhiều instance (nhà cung cấp - Provider), việc lựa chọn instance nào để xử lý yêu cầu từ phía người tiêu dùng (Consumer) được quyết định bởi thuật toán cân bằng tải (Load Balancing).
Phân Biệt Định Tuyến, Cân Bằng Tải Và Dung Lỗi Cụm
Trước khi đi sâu vào các thuật toán, cần làm rõ ba khái niệm thường bị nhầm lẫn trong quy trình gọi RPC:
- Định tuyến (Routing): Lọc ra một tập hợp con các Provider dựa trên các quy tắc nghiệp vụ hoặc địa lý.
- Cân bằng tải (Load Balancing): Chọn ra một Provider cụ thể từ tập hợp con đã được lọc.
- Dung lỗi cụm (Cluster Fault Tolerance): Xử lý các tình huống khi Provider được chọn gặp sự cố (ví dụ: thử lại, báo lỗi nhanh).
Ví dụ minh họa: Giả sử hệ thống có 15 node ở Hà Nội và 25 node ở TP.HCM. Một Consumer ở Đà Nẵng khởi tạo yêu cầu:
- Định tuyến: Dựa trên cấu hình vùng miền, yêu cầu được chuyển hướng đến cụm 25 node ở TP.HCM để giảm độ trễ mạng.
- Cân bằng tải: Thuật toán ngẫu nhiên chọn ra node số 12 trong 25 node đó.
- Gọi thất bại: Node 12 không phản hồi hoặc ném ra ngoại lệ.
- Dung lỗi cụm: Kích hoạt cơ chế Failover, thử lại yêu cầu trên một node khác.
- Thử lại thành công: Node số 8 xử lý thành công yêu cầu và trả về kết quả.
Quy trình này cho thấy định tuyến xảy ra đầu tiên, tiếp theo là cân bằng tải, và cuối cùng là dung lỗi cụm.
Các Thuật Toán Cân Bằng Tải Tích Hợp Sẵn
Dubbo cung cấp 4 chiến lược mặc định để phân phối lưu lượng:
1. Cân Bằng Tải Ngẫu Nhiên (RandomLoadBalance)
Đây là chiến lược mặc định. Thay vì chọn ngẫu nhiên thuần túy, Dubbo hỗ trợ khái niệm trọng số (weight). Các server có cấu hình phần cứng mạnh hơn có thể được gán trọng số cao hơn để nhận nhiều request hơn.
Cơ chế hoạt động: Nếu tất cả các node có cùng trọng số (mặc định là 100), hệ thống sẽ chọn ngẫu nhiên một chỉ số mảng. Nếu trọng số khác nhau, hệ thống sẽ tính tổng trọng số và tạo một số ngẫu nhiên trong khoảng từ 0 đến tổng đó, sau đó dò tìm node tương ứng dựa trên các phân đoạn trọng số.
Giả sử có 3 node với trọng số lần lượt là 20, 30, 50 (Tổng = 100). Một số ngẫu nhiên là 65 sẽ rơi vào khoảng của node thứ 3 (20+30=50 < 65 <= 100).
public class CustomRandomLoadBalance extends AbstractLoadBalance {
private final ThreadLocalRandom random = ThreadLocalRandom.current();
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int nodeCount = invokers.size();
int sumOfWeights = 0;
boolean isUniformWeight = true;
for (int i = 0; i < nodeCount; i++) {
int currentWeight = getWeight(invokers.get(i), invocation);
sumOfWeights += currentWeight;
if (isUniformWeight && i > 0 && currentWeight != getWeight(invokers.get(i - 1), invocation)) {
isUniformWeight = false;
}
}
if (sumOfWeights > 0 && !isUniformWeight) {
int randomOffset = random.nextInt(sumOfWeights);
for (int i = 0; i < nodeCount; i++) {
randomOffset -= getWeight(invokers.get(i), invocation);
if (randomOffset < 0) {
return invokers.get(i);
}
}
}
return invokers.get(random.nextInt(nodeCount));
}
}
2. Cân Bằng Tải Vòng Tròn (RoundRobinLoadBalance)
Phân phối request lần lượt qua từng node theo vòng lặp. Thuật toán này cũng tôn trọng trọng số, đảm bảo tỷ lệ phân phối chính xác theo cấu hình. Tuy nhiên, điểm yếu chí mạng của nó là sự tích tụ request trên các node chậm. Nếu một node xử lý chậm nhưng tiến trình chưa chết, các request sẽ bị dồn ứ tại node đó, kéo sập hiệu năng chung của hệ thống do hàng chờ bị quá tải.
3. Cân Bằng Tải Theo Số Lượng Kết Nối Hoạt Động Thấp Nhất (LeastActiveLoadBalance)
Thuật toán này ưu tiên các node có số lượng request đang xử lý (active count) ít nhất. "Active count" là hiệu số giữa số request nhận vào và số request đã xử lý xong. Node nào xử lý càng chậm, active count càng cao, do đó hệ thống sẽ tự động điều hướng các request mới sang các node nhanh hơn.
if (minActiveCount == 1) {
// Chỉ có một node có active count thấp nhất, trả về ngay
return invokers.get(minActiveIndexes[0]);
}
if (!isUniformWeight && totalWeight > 0) {
// Nếu có nhiều node cùng active count thấp nhất, dùng trọng số để chọn
int weightOffset = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < minActiveCount; i++) {
int idx = minActiveIndexes[i];
weightOffset -= getWeight(invokers.get(idx), invocation);
if (weightOffset <= 0) {
return invokers.get(idx);
}
}
}
4. Cân Bằng Tải Băm Nhất Quán (ConsistentHashLoadBalance)
Sử dụng thuật toán Consistent Hashing để đảm bảo các request có cùng tham số sẽ luôn được gửi đến cùng một node. Khi một node bị loại bỏ khỏi cụm, các request sẽ được phân tán đều sang các node còn lại thông qua các virtual nodes (node ảo), tránh gây sốc tải cho các node sống sót.
Mặc định, Dubbo chỉ băm tham số đầu tiên của phương thức. Để thay đổi, ta cấu hình:
<dubbo:parameter key="hash.arguments" value="0,1,2" />
Số lượng node ảo mặc định là 160. Có thể điều chỉnh qua tham số:
<dubbo:parameter key="hash.nodes" value="256" />
Chiến lược này rất hữu ích khi kết hợp với cache cục bộ trên Provider, giúp tăng tỷ lệ hit cache và giảm tải cho database hoặc Redis.
Cấu Hình Chiến Lược Cân Bằng Tải
Bạn có thể áp dụng cấu hình ở nhiều cấp độ khác nhau. Dưới đây là các ví dụ sử dụng chiến lược roundrobin:
Cấp độ dịch vụ phía Provider:
<dubbo:service interface="com.example.api.PaymentService" loadbalance="roundrobin" />
Cấp độ dịch vụ phía Consumer:
<dubbo:reference interface="com.example.api.PaymentService" loadbalance="roundrobin" />
Cấp độ phương thức phía Provider:
<dubbo:service interface="com.example.api.PaymentService">
<dubbo:method name="processRefund" loadbalance="roundrobin"/>
</dubbo:service>
Cấp độ phương thức phía Consumer:
<dubbo:reference interface="com.example.api.PaymentService">
<dubbo:method name="processRefund" loadbalance="roundrobin"/>
</dubbo:reference>
Thứ tự ưu tiên cấu hình (từ cao xuống thấp):
- Phương thức phía Consumer (Client method)
- Dịch vụ phía Consumer (Client interface)
- Phương thức phía Provider (Server method)
- Dịch vụ phía Provider (Server interface)
Mở Rộng Thuật Toán Cân Bằng Tải Tùy Chỉnh
Nếu các thuật toán có sẵn không đáp ứng được nghiệp vụ đặc thù, bạn có thể tự viết chiến lược mới bằng cách triển khai giao diện LoadBalance thông qua cơ chế SPI của Dubbo.
Bước 1: Triển khai giao diện LoadBalance
package com.example.dubbo.cluster;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.cluster.LoadBalance;
import org.apache.dubbo.common.URL;
import java.util.List;
public class FirstNodeLoadBalance implements LoadBalance {
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
if (invokers == null || invokers.isEmpty()) {
return null;
}
// Luôn chọn node đầu tiên trong danh sách
return invokers.get(0);
}
}
Bước 2: Khai báo SPI
Tạo file org.apache.dubbo.rpc.cluster.LoadBalance trong thư mục src/main/resources/META-INF/dubbo/ với nội dung:
firstNode=com.example.dubbo.cluster.FirstNodeLoadBalance
Bước 3: Áp dụng vào cấu hình XML
<dubbo:reference id="orderService" interface="com.example.api.OrderService" loadbalance="firstNode" />