Service: Địa Chỉ Ổn Định và Cân Bằng Tải

K
Kai··5 min read

Bài 4 để lại một vấn đề thực tế: Deployment giữ 3 pod sống, nhưng mỗi pod một IP nội bộ, và mỗi lần pod được dựng lại (self-healing, rolling update) IP đổi. Nếu service A muốn gọi service B, nó không thể nhớ IP của từng pod B — những IP đó là cát chảy. Service là lời giải: một địa chỉ ổn định đứng trước nhóm pod, kèm cân bằng tải miễn phí.

Vấn đề: pod phù du, IP bất định

   Không có Service:                  Có Service:
   client ──► pod 10.244.0.15 ?       client ──► Service "web" (IP cố định)
              pod 10.244.0.16 ?                       │ tự chia tải
              pod 10.244.0.17 ?                        ├─► pod 10.244.0.15
              (IP nào? đổi liên tục)                   ├─► pod 10.244.0.16
                                                       └─► pod 10.244.0.17

Service cho một IP ảo (ClusterIP) và một tên DNS không bao giờ đổi suốt vòng đời của nó. Client chỉ cần biết "web"; Service lo phần tìm pod nào còn sống và chia lưu lượng cho chúng.

Service tìm pod bằng gì? Label selector

Mấu chốt khiến mọi thứ ăn khớp: Service không gắn cứng vào pod nào. Nó dùng label selector (giống Deployment ở Bài 4) để chọn động những pod khớp nhãn — pod nào mang nhãn đúng thì tự động nằm sau Service, kể cả pod vừa sinh ra một giây trước.

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP
  selector:
    app: web          # chọn mọi pod có nhãn app=web
  ports:
    - port: 80        # cổng của Service
      targetPort: 80  # cổng trên pod
kubectl apply -f service-clusterip.yaml
kubectl get svc web
NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
web    ClusterIP   10.106.68.120   <none>        80/TCP    0s

Service được cấp IP ảo 10.106.68.120 — ổn định cho tới khi xoá. Nó tìm thấy pod nào? Xem endpoints (danh sách IP pod thật mà Service đang trỏ tới):

kubectl describe svc web | grep -iE "Selector|Endpoints"
Selector:    app=web
Endpoints:   10.244.0.15:80,10.244.0.16:80,10.244.0.17:80

Đúng 3 IP của 3 pod web. Đây là phần động: pod chết và pod mới sinh ra, Kubernetes tự cập nhật danh sách endpoint này — Service luôn trỏ tới tập pod đang sống. Bạn không phải làm gì cả.

DNS nội bộ: gọi service bằng tên

Nhớ coredns ở Bài 1? Nhờ nó, mỗi Service có một tên DNS trong cluster. Pod khác chỉ cần gọi http://web là tới. Thử từ một pod tạm:

kubectl run tmp --image=busybox:1.36 --rm -it -- sh
# bên trong:
wget -qO- http://web | grep title
<title>Welcome to nginx!</title>

Gọi bằng tên web, không cần biết IP. Tên DNS đầy đủ có dạng <service>.<namespace>.svc.cluster.local:

nslookup web.default.svc.cluster.local
Name:    web.default.svc.cluster.local
Address: 10.106.68.120

Trỏ về đúng ClusterIP. Trong cùng namespace bạn chỉ cần tên ngắn web; khác namespace thì thêm web.<namespace>. Đây là cơ chế khiến các microservice tìm nhau trong Kubernetes — không cần service discovery thủ công, DNS lo hết.

Ba loại Service: ClusterIP, NodePort, LoadBalancer

Khác nhau ở chỗ "ai gọi được Service này".

   ClusterIP   ──  chỉ TRONG cluster gọi được (mặc định). Cho giao tiếp nội bộ.
   NodePort    ──  mở một cổng (30000-32767) trên MỌI node → ngoài cluster gọi được.
   LoadBalancer──  xin cloud cấp một load balancer + IP công khai. Cho production trên cloud.

ClusterIP (mặc định) là cái ta vừa tạo — chỉ truy cập được từ bên trong cluster, hợp cho service nói chuyện với nhau (database, backend API...).

NodePort mở một cổng cố định trên node để bên ngoài vào được:

spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080      # cổng trên node (dải 30000-32767)
kubectl get svc web-nodeport
NAME           TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
web-nodeport   NodePort   10.107.188.99   <none>        80:30080/TCP   0s

80:30080 nghĩa là cổng 30080 trên node ánh xạ vào cổng 80 của Service. Truy cập từ trong node minikube:

minikube ssh "curl -s http://localhost:30080 | grep title"
<title>Welcome to nginx!</title>

Với driver Docker, NodePort không phơi thẳng ra máy host. minikube có lệnh tiện minikube service web-nodeport --url để mở một tunnel và cho bạn URL gọi từ máy thật.

LoadBalancer là loại dùng ở production trên cloud: Kubernetes nhờ nhà cung cấp (AWS/GCP/Azure) tạo một load balancer thật với IP công khai. Trên minikube không có cloud, nên EXTERNAL-IP sẽ kẹt ở <pending> — trừ khi chạy minikube tunnel để giả lập. Trong thực tế production, NodePort hiếm khi dùng trực tiếp; người ta đặt một Ingress (Bài 9) phía trước để định tuyến HTTP gọn gàng hơn.

kube-proxy: ai làm việc chia tải

Quay lại Bài 1: kube-proxy chạy trên mỗi node là thành phần biến Service thành hiện thực. Khi bạn tạo Service, kube-proxy cài các luật mạng (iptables/IPVS) trên node để: lưu lượng gửi tới ClusterIP được chuyển hướng tới một trong các pod endpoint, xoay vòng để chia tải. Việc này xảy ra ở tầng kernel, rất nhẹ — Service không phải một proxy chạy giữa đường làm chậm request, mà chỉ là luật định tuyến. Đó là vì sao Service gần như không tốn chi phí hiệu năng.

Tổng kết

Service cho một địa chỉ ổn định (ClusterIP + tên DNS) đứng trước một nhóm pod được chọn động qua label selector, và cân bằng tải giữa chúng — giải đúng vấn đề "pod phù du, IP đổi liên tục". Nhờ coredns, pod gọi nhau bằng tên (web hoặc web.<namespace>). Ba loại: ClusterIP (nội bộ, mặc định), NodePort (mở cổng trên node ra ngoài), LoadBalancer (IP công khai qua cloud). Đứng sau hậu trường là kube-proxy cài luật mạng ở tầng kernel — nhẹ và nhanh.

Cluster giờ đã có thứ tự, nhưng mọi thứ đang nằm chung một chỗ. Bài 6: Namespace, Label và Selector — cách chia ngăn và gắn thẻ tài nguyên, chính là cơ chế mà Deployment và Service vừa dùng để tìm pod.