Xử lý lỗi mất cân bằng tải trong Kubernetes khi lưu lượng chỉ dồn vào một Pod

Trong môi trường Kubernetes, đôi khi bạn sẽ gặp tình trạng dù đã scale lên nhiều Pod (ví dụ 4 Pod), nhưng khi thực hiện kiểm thử hiệu năng (stress test) với hàng trăm request đồng thời, tất cả các request đó vẫn chỉ được xử lý bởi một Pod duy nhất. Hiện tượng này thường do cơ chế phân phối lưu lượng của Service hoặc cách thức duy trì kết nối của client.

1. Phân tích nguyên nhân gốc rễ

Cơ chế Load Balancing mặc định của Kubernetes Service dựa vào kube-proxy. Nếu không được cấu hình tối ưu, nó có thể gặp các vấn đề sau:

  • Session Affinity: Service được cấu hình để duy trì kết nối từ một IP client đến một Pod cố định.
  • HTTP Keep-Alive: Client tái sử dụng các kết nối TCP cũ, khiến các request tiếp theo đi theo con đường cũ đã thiết lập.
  • Cơ chế iptables: Mặc định iptables sử dụng thuật toán chọn Pod ngẫu nhiên, không dựa trên tải thực tế (Least Connections).

2. Các bước kiểm tra hệ thống

Trước tiên, hãy xác định trạng thái của Service và các Endpoint liên quan bằng các lệnh sau:

# Kiểm tra cấu hình chi tiết của Service
kubectl describe svc <ten-service>

# Xác nhận danh sách các Pod đang nhận traffic
kubectl get endpoints <ten-service>

# Kiểm tra nhãn (label) để đảm bảo Service chọn đúng các Pod mục tiêu
kubectl get pods -l app=<label-cua-ban> -o wide

Để chẩn đoán chính xác hơn từ phía ứng dụng, bạn có thể tạo một công cụ kiểm tra bằng Java để đếm số lượng request phản hồi từ mỗi Pod:

@Service
public class TrafficAnalyzer {
    
    private final RestTemplate restTemplate = new RestTemplate();
    private static final Logger log = LoggerFactory.getLogger(TrafficAnalyzer.class);

    public void runAnalysis(String serviceUrl, int totalRequests) {
        Map<String, Integer> hitStats = new ConcurrentHashMap<>();
        ExecutorService executor = Executors.newFixedThreadPool(10);
        CountDownLatch latch = new CountDownLatch(totalRequests);

        for (int i = 0; i < totalRequests; i++) {
            executor.submit(() -> {
                try {
                    // Endpoint này nên trả về tên Pod đang xử lý
                    ResponseEntity<Map> response = restTemplate.getForEntity(serviceUrl + "/actuator/info", Map.class);
                    String podId = (String) response.getBody().get("podName");
                    hitStats.merge(podId != null ? podId : "Unknown", 1, Integer::sum);
                } catch (Exception e) {
                    log.error("Request failed: {}", e.getMessage());
                } finally {
                    latch.countDown();
                }
            });
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        log.info("Kết quả phân bổ lưu lượng: {}", hitStats);
        executor.shutdown();
    }
}

3. Giải pháp khắc phục

3.1. Tắt tính năng Session Affinity

Nếu sessionAffinity được đặt là ClientIP, Kubernetes sẽ luôn gửi request từ một client nhất định đến cùng một Pod trong một khoảng thời gian. Hãy đảm bảo giá trị này là None.

kind: Service
metadata:
  name: my-service
spec:
  sessionAffinity: None # Đảm bảo không duy trì phiên theo IP
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

3.2. Chuyển đổi kube-proxy sang chế độ IPVS

Chế độ iptables mặc định thực hiện phân phối dựa trên xác suất. Trong khi đó, ipvs hỗ trợ nhiều thuật toán thông minh hơn như Least Connection (lc) - chuyển request đến Pod có ít kết nối nhất.

Cấu hình trong KubeProxyConfiguration:

apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
  scheduler: "lc" # Sử dụng thuật toán Least Connection

3.3. Xử lý vấn đề Connection Pooling tại Client

Trong Java, các HTTP Client (như OkHttp, Apache HttpClient) thường giữ kết nối TCP mở để tăng hiệu năng. Khi chạy áp lực lớn, tất cả request có thể đi qua một vài kết nối TCP duy nhất đã được gán vào một Pod từ trước.

Bạn cần cấu hình lại Connection Pool để buộc đóng các kết nối quá lâu hoặc xoay vòng kết nối:

@Configuration
public class HttpClientConfig {

    @Bean
    public RestTemplate customRestTemplate() {
        // Cấu hình Connection Provider với giới hạn thời gian sống của kết nối
        ConnectionProvider poolProvider = ConnectionProvider.builder("custom-pool")
                .maxConnections(50)
                .maxLifeTime(Duration.ofSeconds(10)) // Buộc kết nối mới sau 10 giây
                .maxIdleTime(Duration.ofSeconds(5))
                .build();

        HttpClient client = HttpClient.create(poolProvider);
        return new RestTemplate(new ReactorClientHttpConnector(client));
    }
}

3.4. Cấu hình TTL cho DNS

Nếu ứng dụng cache kết quả phân giải DNS quá lâu, nó sẽ không nhận biết được sự thay đổi của các IP Pod phía sau Service ClusterIP. Hãy điều chỉnh TTL (Time To Live) của DNS trong JVM:

// Thiết lập trực tiếp trong mã nguồn hoặc qua tham số JVM: -Dsun.net.inetaddr.ttl=10
java.security.Security.setProperty("networkaddress.cache.ttl", "10");

4. Sử dụng Service Mesh để điều tiết thông minh

Nếu các giải pháp trên vẫn chưa đạt kỳ vọng, việc triển khai Service Mesh như Istio sẽ cung cấp khả năng kiểm soát lưu lượng ở Layer 7 (HTTP) thay vì chỉ Layer 4 (TCP).

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: dynamic-lb-rule
spec:
  host: my-service.default.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      simple: LEAST_CONN # Istio sẽ điều phối dựa trên số lượng request thực tế

5. Tối ưu hóa công cụ kiểm thử hiệu năng

Khi sử dụng các công cụ như ab (Apache Bench) hoặc wrk, hãy lưu ý cấu hình để tránh việc "dính" kết nối:

  • Sử dụng -H "Connection: close" để buộc mỗi request tạo một kết nối mới (dù hiệu năng sẽ giảm nhưng Load Balancing sẽ đều hơn).
  • Sử dụng công cụ hiện đại như hey với tùy chọn disable keep-alive: hey -n 1000 -c 100 -disable-keepalive http://service-url.

Thẻ: Kubernetes Load Balancing kube-proxy ipvs Service Mesh

Đăng vào ngày 8 tháng 8 lúc 07:58