Skip to content
Go back

Tìm hiểu về Service network

Updated:  at  06:35 AM

Trong phần Pod network chúng ta đều biết rằng Pod resource là đơn vị nhỏ nhất sẽ có chứa ứng dụng bên trong nó khi triển khai và loại resource này khá là linh hoạt (có thể scale up, scale down, stop, re-created, re-allocated,…)

Bởi sự linh hoạt này nên không có gì là chắc chắn đối với IP address của một Pod, IP address có thể liên tục bị thay đổi trong nhiều trường hợp → Vậy làm sao để connect giữa 02 Pod với nhau?

Lại quay về vấn đề cũ (khi có nhiều replicas của một service trong Docker Swarm), chúng ta lại nghĩ đến cần phải có một proxy như reverse-proxy/load balancer:

Kubernetes đã thiết kế một resource tương tự như thành phần proxy đã nói ở trên gọi là Service, Service resource có đặc điểm như sau:

Table of contents

Open Table of contents

Vậy Kubernetes Service hoạt động như thế nào?

Để tìm hiểu cách mà Service hoạt động, chúng ta sẽ tạo một Deployment resource như sau:

kind: Deployment
apiVersion: apps/v1
metadata:
  name: service-test
spec:
  replicas: 2
  selector:
    matchLabels:
      app: service_test_pod
  template:
    metadata:
      labels:
        app: service_test_pod
    spec:
      containers:
        - name: simple-http
          image: python:2.7
          imagePullPolicy: IfNotPresent
          command: ["/bin/bash"]
          args:
            [
              "-c",
              'echo "<p>Hello from $(hostname)</p>" > index.html; python -m SimpleHTTPServer 8080',
            ]
          ports:
            - name: http
              containerPort: 8080

Deployment sẽ triển khai 02 simple http server Pods và listen ở port 8080, kiểm tra địa chỉ IP của 02 Pods:

kubectl get pods --selector=app=service_test_pod -o jsonpath='{.items[*].status.podIP}'

Kiểm tra kết nối tới một trong 02 Pods trên bằng cách chạy một client Pod như sau:

apiVersion: v1
kind: Pod
metadata:
  name: service-test-client1
spec:
  restartPolicy: Never
  containers:
    - name: test-client1
      image: alpine
      command: ["/bin/sh"]
      args: ["-c", "echo 'GET / HTTP/1.1\\r\\n\\r\\n' | nc 10.0.2.2 8080"]

Client Pod ở trên sẽ tạo một kết nối tới địa chỉ IP 10.0.2.2 (có thể thay thế IP address này bằng địa chỉ IP của một trong 02 Pod ở trên). Xem kết quả chạy của client Pod đã triển khai bằng command kubectl logs service-test-client1.

Chúng ta thấy rằng việc kết nối thông qua IP address của Pod khá là khó khăn:

Từ vấn đề trên k8s có hỗ trợ chúng ta một loại resource là Service có thể giải quyết được vấn đề này, triển khai Service như sau:

kind: Service
apiVersion: v1
metadata:
  name: service-test
spec:
  selector:
    app: service_test_pod
  ports:
    - port: 80
      targetPort: 8080
kubectl get service service-test
NAME           CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service-test   10.3.241.152   <none>        80/TCP    11s

Service resource sẽ cấu hình một proxy để forward các request của client tới danh sách Pod thông qua labels đã được assign khi khởi tạo (labels trong Deployment resource).

Như khai báo Service ở trên, k8s sẽ tạo một Service có tên service-test gán với một địa chỉ IP và listen port 80. Điều này có nghĩa rằng client có thể kết nối tới các http server Pods thông qua IP address 10.3.241.152 và port 80:

Có lẽ chúng ta sẽ không cần phải dùng đến IP address của Service, k8s đã cung cấp một internal cluster DNS hỗ trợ resolves IP address của Service thông qua Service name. Thử kiếm tra bằng cách kết nối tới service-test như sau:

apiVersion: v1
kind: Pod
metadata:
  name: service-test-client2
spec:
  restartPolicy: Never
  containers:
    - name: test-client2
      image: alpine
      command: ["/bin/sh"]
      args: ["-c", "echo 'GET / HTTP/1.1\\r\\n\\r\\n' | nc service-test 80"]

Kiểm tra logs của client Pod nhận thấy kết quả trả về không khác gì với cách truy cập bằng IP address. Nếu chạy lại nhiều lần client Pod ở trên chúng ta sẽ thấy kết quả trả về sẽ được LB round robin tới 02 http server Pods. Great 😉

Service Network

Kiến trúc kết nối giữa client Pod và server Pod (triển khai ở trên) được phác hoạ lại như sau:

Chúng ta có dải mạng 10.100.0.0/24 chính là network chứa IP address của các server node trong cụm k8s, các server node sẽ giao tiếp với nhau thông qua dải mạng này.

Dải mạng 10.0.0.0/14 là network do k8s tạo và quản lý riêng trên từng server node (mỗi server node sẽ có lớp mạng riêng):

Bảng Routing table thực chất sẽ được đặt trên tất cả các host (node) và được quản lý bởi kube-proxy, khi có bất cứ thông tin thay đổi nào thì k8s master sẽ thực hiện gửi đồng bộ xuống cho kube-proxy và nó sẽ update rules vào Routing table.

Khi chúng ta tạo Service resource, thực chất nó sẽ hoạt động như sau:

kubectl describe services service-test
Name:                   service-test
Namespace:              default
Labels:                 <none>
Selector:               app=service_test_pod
Type:                   ClusterIP
IP:                     10.3.241.152
Port:                   http    80/TCP
Endpoints:              10.0.1.2:8080,10.0.2.2:8080
Session Affinity:       None
Events:                 <none>

Bây giờ chúng ta sẽ bắt đầu quá trình từ client Pod khi kết nối tới server Pod thông qua Service resource với diagram sau đây:

Có một vấn đề là làm sao để NAT được gói tin tới đúng địa chỉ IP của server Pod, và các gói tin này được cân bằng tải đều giữa 02 Pod như thế nào?

Câu trả lời đó chính là kube-proxy và network provider, xem diagram chi tiết dưới đây:

Netfilter là một công cụ trong kernel space cho phép cập nhật rules liên quan tới định tuyến gói tin trong linux, chắc hẳn chúng ta đều biết đến thằng iptables — nó chính là interface tương tác với netfilter để cấu hình firewall (khác nhau giữa netfilter và iptables xem ở đây).

Trong kiến trúc của k8s thì thằng kube-proxy sẽ tương tác với interface network provider và thằng network provider này có nhiệm vụ tương tác với netfilter trong kernel space để cập nhật rules được gửi từ apiserver:

Tất cả các routing rules sẽ được liên tục cập nhật vào Routing table của netfilter, việc forward gói tin hay cân bằng tải các gói tin đều do thằng netfilter này làm hết. Cân bằng tải thì được thực hiện bằng cơ chế IPVS, iptables,… sẽ tuỳ thuộc theo thằng network provider được sử dụng là gì.

Diagram chi tiết hơn về việc gói tin được xử lý với netfilter, chi tiết có thể xem thêm ở đâyDiagram chi tiết hơn về việc gói tin được xử lý với netfilter, chi tiết có thể xem thêm ở đây

Network provider (CNI plugins)

Xem chi tiết so sánh giữa các CNI plugins ở đâyXem chi tiết so sánh giữa các CNI plugins ở đây

Phân loại Service resource

Service CÓ proxy

ClusterIP

Ở ví dụ trước chúng ta sử dụng Service với type mặc định là ClusterIP, loại Service này được sử dụng để giao tiếp nội bộ trong cụm k8s. Ví dụ như client Pod kết nối tới 02 server Pod thông qua Service resource, ClusterIP chỉ là một IP ảo 10.3.241.152 - chỉ có những Pod resource chạy bên trong cụm mới có thể kết nối đến ClusterIP này.

apiVersion: v1
kind: Service
metadata:
  name: service-test
spec:
  type: ClusterIP # Chỉ tạo Virtual IP
  selector:
    app: service_test_pod # Label selector
  ports:
    - protocol: TCP # Protocol
      port: 80 # Port của Service
      targetPort: 8080 # Port của Pod

NodePort

Vấn đề ở đây là làm sao cho client có thể kết nối tới các service Pod trong cụm k8s? từ đây k8s mới cung cấp loại Service type là NodePort. Các client ở bên ngoài cụm k8s lúc này có thể truy cập vào cụm thông qua IP address 10.100.0.2

Khi tạo Service resource có type là NodePort, k8s sẽ tự động tạo thành phần ClusterIP (tự tạo Virtual IP) rồi mapping tới một port ngẫu nhiên (30000~32767) trên tất cả các worker node (port này có thể được cấu hình fix cứng thông qua yaml file).

Ví dụ ở hình bên trên, client có thể truy cập vào địa chỉ IP 10.100.0.2:30080 hoặc 10.100.0.3:30080 (miễn sao client cùng dải mạng với cụm k8s). Port 30080 sẽ được listen trên card eth0 của tất cả các worker node, đây chính là lý do tại sao không thể tạo nhiều Servicce (NodePort) fix cứng nodePort giống nhau.

---
apiVersion: v1
kind: Service
metadata:
  name: service-test
spec:
  type: NodePort # Virtual IP + map host port
  selector:
    app: service_test_pod # Label selector
  ports:
    - protocol: TCP # Protocol
      port: 80 # Port của Service
      targetPort: 8080 # Port của Pod
      nodePort: 30080 # Port của Host (optional)

LoadBalancer

Lúc này, tất cả các worker node đều listen chung một port duy nhất, client có thể truy cập vào bất kỳ node nào trong cụm với port duy nhất trên để kết nối tới Pod bên trong. Vấn đề ở đây là chúng ta cung cấp địa chỉ IP của worker node nào cho client? nhỡ worker node đó chết thì sao?

Ví dụ đơn giản nhất là trên dịch vụ Cloud, các worker node nằm trên dải VPC network mà client từ bên ngoài internet không thể truy cập vào được. Chúng ta phải cần một con Load Balancer (có 1 chân public IP phơi ra cho client truy cập) làm nhiệm vụ cân bằng tải tới các worker node bên trong dải VPC network kia. Đây chính là Service resource có type là LoadBalancer.

Tuy nhiên loại này không được sử dụng phổ biến trong thực tế, nó cũng chả khác gì con cân bằng tải thông thường cả (nginx, haproxy, envoy,…) Vì vậy để giải quyết vấn đề trên, đa số thường chọn giải pháp tự dựng Load Balancer cho cụm k8s (có thể cấu hình được nhiều thứ hơn so với Service type LoadBalancer của k8s).

Khi sử dụng resource này trên cloud (ví dụ như Google Cloud, AWS, Azure,…) bọn nó sẽ tự động gán một public IP cho Service → tất nhiên là nó sẽ tính phí trên mỗi public IP tạo ra.

---
apiVersion: v1
kind: Service
metadata:
  name: service-test
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
    service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443,8443"
spec:
  type: LoadBalancer # Virtual IP + map host port + create LB
  selector:
    app: service_test_pod # Label selector
  ports:
    - protocol: TCP # Protocol
      port: 80 # Port của Service
      targetPort: 8080 # Port của Pod
      nodePort: 30080 # Port của Host (optional)

Service KHÔNG proxy (Headless Service)

---
apiVersion: v1
kind: Service
metadata:
  name: service-test
spec:
  clusterIP: None # Không tạo Virtual IP
  selector:
    app: service_test_pod # Label selector
  ports:
    - protocol: TCP # Protocol
      targetPort: 8080 # Port của Pod

Một trường hợp sử dụng nữa đối với Service NO proxy là cấu hình kết nối tới Database server bên ngoài cụm (như diagram ở trên), chúng ta sẽ thiết kế Service resource với clusterIP là None và một Manual Endpoint để cấu hình địa chỉ IP và port (của PostgreSQL server).

Khi tạo Service resource, thông tin của Service sẽ được k8s apiserver cập nhật vào DNS server (chính là core-dns container trong kube-system namspace). Vì vậy trong golang API container có thể resolve được địa chỉ IP của database service là 192.0.2.42.

---
apiVersion: v1
kind: Service
metadata:
  name: database
spec:
  clusterIP: None
  ports:
    - protocol: TCP
      targetPort: 5432
---
apiVersion: v1
kind: Endpoints
metadata:
  name: database
subsets:
  - addresses:
      - ip: 192.0.2.42
    ports:
      - port: 5432

Tìm hiểu thêm về K8s Networking


Suggest Changes

Previous Post
Tìm hiểu về Pod network
Next Post
[AWS] EC2 Instance Lifecycle