กลับไปยังบทความ

DevOps

Kubernetes Service Types: ClusterIP, NodePort, LoadBalancer ต่างกันอย่างไร

อธิบาย Kubernetes service types ทั้ง ClusterIP, NodePort, LoadBalancer และ ExternalName รวมถึง headless Service กับ DNS ของ StatefulSet, บทบาทของ kube-proxy และ load balancer แบบ internal กับ external

24 กันยายน 25699 นาทีในการอ่าน

kubernetes services clusterip nodeport loadbalancer

Pod เกิดและตายอยู่ตลอด และทุก Pod ใหม่ก็ได้ IP ใหม่ Service จึงมีไว้ให้กลุ่มของ Pod มี virtual IP และชื่อ DNS ที่นิ่ง แล้วกระจาย connection ไปยัง Pod ที่พร้อมรับงานในขณะนั้น ส่วน Kubernetes service types เป็นตัวกำหนดว่าใครเข้าถึงที่อยู่นั้นได้บ้าง: ClusterIP สำหรับ traffic ภายในคลัสเตอร์, NodePort เปิด port คงที่บนทุก node, LoadBalancer ใช้ load balancer ภายนอก และ ExternalName เป็น DNS alias ชี้ไปยังสิ่งที่อยู่นอกคลัสเตอร์

บทความนี้อธิบายทีละแบบ รวมถึง headless Service ที่ StatefulSet ต้องพึ่ง และบทบาทจริงของ kube-proxy กับ external load balancer บนเส้นทางของ request

Service ของ Kubernetes ทำงานอย่างไร

Service เลือก Pod ด้วย label โดยมีสามส่วนทำงานร่วมกัน:

  1. EndpointSlice controller เก็บรายการ IP ของ Pod ที่ match และผ่าน readiness probe
  2. kube-proxy บนทุก node แปลงรายการนั้นเป็นกฎ iptables, IPVS หรือ nftables เพื่อส่ง traffic ที่ยิงมายัง IP ของ Service ไปให้ Pod ตัวใดตัวหนึ่ง
  3. CoreDNS ตั้งชื่อให้ Service เช่น orders.shop.svc.cluster.local หรือแค่ orders ถ้าเรียกจากใน namespace shop
yaml
apiVersion: v1
kind: Service
metadata:
  name: orders
  namespace: shop
spec:
  type: ClusterIP            # the default, can be omitted
  selector:
    app: orders
  ports:
    - name: http
      port: 80               # the port clients use on the Service IP
      targetPort: http       # the named containerPort on the Pod (e.g. 8080)

แยก port ให้ออก port คือ port ที่ client ต่อเข้ามาที่ Service ส่วน targetPort คือ port ที่ container ฟังอยู่ การตั้งชื่อให้ port ทำให้เปลี่ยน port ของ container ได้โดยไม่ต้องแก้ Service ส่วน nodePort จะมีเฉพาะ Service แบบ NodePort และ LoadBalancer

ClusterIP: traffic ระหว่าง service ภายในคลัสเตอร์

ClusterIP เป็นค่าเริ่มต้น Service จะได้ virtual IP จาก service range ของคลัสเตอร์ ซึ่งเข้าถึงได้จากในคลัสเตอร์เท่านั้น ใช้กับทุกอย่างที่ไม่ต้องเปิดออกข้างนอก เช่น backend คุยกับ database, API คุยกับ cache หรือ microservice คุยกันเอง

Virtual IP นี้ไม่ได้ผูกกับ network interface จริง มันมีอยู่แค่ในรูปของกฎการส่งต่อ นี่คือเหตุผลที่มักจะ ping ไม่ได้ ทั้งที่ Service ทำงานปกติ ให้ทดสอบด้วย request จริงแทน:

bash
kubectl run -it --rm curl --image=curlimages/curl --restart=Never -- \
  curl -s http://orders.shop/healthz

มีสองพฤติกรรมที่มักทำให้คนแปลกใจ:

  • กระจายโหลดต่อ connection ไม่ใช่ต่อ request kube-proxy ทำงานที่ L4 ถ้า client ถือ connection HTTP/2 หรือ gRPC ยาว ๆ ไว้เส้นเดียว ทุก request จะไปตก Pod เดิม traffic จึงไม่สมดุลได้มาก วิธีแก้คือ client-side load balancing หรือใช้ service mesh ดู pattern ต่าง ๆ ได้ใน การสื่อสารระหว่าง microservice ให้ทนทาน
  • เฉพาะ Pod ที่ ready เท่านั้นที่ได้ traffic ถ้า readiness probe ไม่ผ่าน Pod จะหลุดออกจาก EndpointSlice และถ้าไม่มี Pod ไหน ready เลย Service ก็จะปฏิเสธ connection

ถ้าต้องการ sticky session ที่ระดับ L4 ให้ตั้ง sessionAffinity: ClientIP เพื่อผูก IP ของ client แต่ละรายไว้กับ Pod เดียว

NodePort: port คงที่บนทุก node

NodePort Service ก็คือ ClusterIP Service ที่เพิ่ม port (ค่าเริ่มต้นอยู่ในช่วง 30000–32767) เปิดไว้บน ทุก node traffic ที่ยิงมาที่ <IP ของ node ไหนก็ได้>:<nodePort> จะถึง Service แม้ node นั้นจะไม่มี Pod ของ Service รันอยู่เลย

yaml
apiVersion: v1
kind: Service
metadata:
  name: orders-nodeport
  namespace: shop
spec:
  type: NodePort
  selector:
    app: orders
  ports:
    - port: 80
      targetPort: http
      nodePort: 30080        # optional; omitted means auto-assigned

ข้อเสียเหล่านี้คือเหตุผลที่แทบไม่มีใครเปิด NodePort ให้ผู้ใช้เข้าตรง ๆ:

  • เลข port สูงและแปลกตา
  • client ต้องรู้ IP ของ node เอง และต้องเลิกใช้ node ที่ล่มไปเอง
  • ไม่มีอะไรอยู่ข้างหน้าคอยกระจาย traffic ข้าม node และตรวจสุขภาพ node

NodePort เหมาะกับการทดสอบเร็ว ๆ, กับระบบ bare metal ที่มี load balancer ของตัวเองวางอยู่ข้างหน้า และเป็นฐานที่ LoadBalancer ใช้ต่อยอด

LoadBalancer: เปิด Service ออกนอกคลัสเตอร์

LoadBalancer Service สร้างต่อจาก NodePort อีกชั้น cloud-controller-manager จะขอ load balancer จาก cloud แล้วชี้ไปที่ node port (บาง cloud ชี้ตรงไปที่ IP ของ Pod ได้) จากนั้นเขียนที่อยู่ของ LB ลงใน status ของ Service:

yaml
apiVersion: v1
kind: Service
metadata:
  name: orders-public
  namespace: shop
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app: orders
  ports:
    - port: 443
      targetPort: https
bash
kubectl get svc orders-public -n shop
# NAME            TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)
# orders-public   LoadBalancer   10.96.41.12    203.0.113.25    443:31544/TCP

externalTrafficPolicy กำหนดว่า traffic ที่มาถึง node แล้วจะไปต่ออย่างไร:

Policyพฤติกรรมสิ่งที่ต้องแลก
Cluster (ค่าเริ่มต้น)node ไหนก็รับได้ แล้วส่งต่อไปยัง Pod ซึ่งอาจอยู่คนละ nodeมี hop เพิ่ม และ Pod จะเห็น IP ของ node แทน IP ของ client
Localnode ส่งต่อเฉพาะ Pod ที่อยู่ใน node ตัวเอง node ที่ไม่มี Pod จะไม่ผ่าน health check ของ LBเก็บ IP ของ client ไว้ได้และไม่มี hop เพิ่ม แต่โหลดอาจไม่สมดุล

LoadBalancer Service แต่ละตัวมักได้ load balancer และ IP ของตัวเอง ถ้ามีหลาย service ค่าใช้จ่ายก็บานตาม สำหรับ traffic แบบ HTTP ให้ใช้ load balancer ตัวเดียววางหน้า Ingress controller แล้ว route ตาม host และ path ตามที่อธิบายไว้ใน Kubernetes Ingress: routing และ TLS

Load balancer แบบ internal กับ external

โดยค่าเริ่มต้น load balancer จะหันหน้าออก internet ถ้าต้องการเปิด Service ให้เฉพาะ network ภายใน เช่น VPC อื่นหรือ VPN ของออฟฟิศ ให้ขอ load balancer แบบ internal ผ่าน annotation ของแต่ละ provider:

yaml
metadata:
  annotations:
    service.beta.kubernetes.io/azure-load-balancer-internal: "true"   # AKS
    # networking.gke.io/load-balancer-type: "Internal"                # GKE

Annotation ที่ถูกต้องให้ดูจากเอกสารของ provider ที่ใช้ และ spec.loadBalancerSourceRanges ใช้จำกัด CIDR ของ client ที่อนุญาตได้ บน provider ที่รองรับ

LoadBalancer บน bare metal ด้วย MetalLB

ถ้าไม่มี cloud provider ก็ไม่มีใครมารับคำขอ EXTERNAL-IP จะค้างอยู่ที่ <pending> MetalLB เข้ามาเติมช่องว่างนี้ด้วยสอง component ที่รันอยู่ในคลัสเตอร์:

  • controller แจก IP จาก pool ที่เรากำหนด
  • speaker รันเป็น DaemonSet คอยประกาศ IP เหล่านั้นให้ network รู้ ผ่าน ARP ในโหมด Layer 2 หรือผ่าน BGP ไปยัง router
yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: public
  namespace: metallb-system
spec:
  addresses:
    - 203.0.113.10-203.0.113.20
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: public
  namespace: metallb-system
spec:
  ipAddressPools:
    - public

อย่าสับสนกับ kube-proxy external load balancer หรือ speaker ของ MetalLB ทำหน้าที่ดึง traffic จากข้างนอก เข้ามาถึง node จากนั้นกฎของ kube-proxy จึงเลือก Pod ให้ ทั้งคู่กระจายโหลดเหมือนกัน แต่อยู่คนละจุดของเส้นทาง ภาพรวมตั้งแต่ DNS ไปจนถึง Pod อยู่ในบทความ load balancing หลายชั้น

ExternalName: DNS alias

ExternalName Service ไม่มี selector, ไม่มี virtual IP และไม่ proxy อะไรเลย CoreDNS แค่ตอบกลับเป็น CNAME record:

yaml
apiVersion: v1
kind: Service
metadata:
  name: orders-db
  namespace: shop
spec:
  type: ExternalName
  externalName: orders.abc123.ap-southeast-1.rds.amazonaws.com

แอปเชื่อมต่อไปที่ orders-db และถ้าวันหนึ่งย้าย database เข้ามาในคลัสเตอร์ ก็แค่เปลี่ยนเป็น Service ปกติโดยไม่ต้องแก้ config ของแอป ข้อควรระวังคือ client ยังส่งชื่อที่มันได้รับไป ดังนั้น Host header ของ HTTP และการตรวจ TLS certificate จะเห็นชื่อ orders-db ไม่ใช่ hostname ภายนอก จึงเหมาะกับ protocol ที่ไม่ตรวจ hostname หรือกรณีที่ตั้งค่า client ให้ใช้ชื่อจริงได้

Headless Service และ DNS สำหรับ StatefulSet

ถ้าตั้ง clusterIP: None Service จะกลายเป็น headless ไม่มี virtual IP และ kube-proxy ไม่สนใจมัน แต่เมื่อ query DNS ด้วยชื่อ Service จะได้ IP ของ Pod ที่ ready ทุกตัวกลับมาแทน:

yaml
apiVersion: v1
kind: Service
metadata:
  name: kafka-headless
  namespace: data
spec:
  clusterIP: None
  selector:
    app: kafka
  ports:
    - name: broker
      port: 9092
text
$ nslookup kafka-headless.data.svc.cluster.local
Name:    kafka-headless.data.svc.cluster.local
Address: 10.244.1.17
Name:    kafka-headless.data.svc.cluster.local
Address: 10.244.2.9
Name:    kafka-headless.data.svc.cluster.local
Address: 10.244.3.22

ClusterIP ปกติซ่อนไว้ว่าเราไปถึง Pod ตัวไหน แต่ headless Service ให้ client เห็น Pod ทุกตัวและเลือกเองได้ เมื่อใช้คู่กับ StatefulSet ที่ตั้ง serviceName ชี้มาที่ Service นี้ Pod แต่ละตัวจะได้ชื่อ DNS คงที่ของตัวเองด้วย:

text
kafka-0.kafka-headless.data.svc.cluster.local
kafka-1.kafka-headless.data.svc.cluster.local
kafka-2.kafka-headless.data.svc.cluster.local

นี่คือวิธีที่ Kafka broker, replica ของ database และ member ของ etcd หากันเจอและอ้างถึงกันได้ ถ้า peer ต้องค้นหากันตั้งแต่ก่อนผ่าน readiness ให้ตั้ง publishNotReadyAddresses: true ส่วนฝั่ง StatefulSet อธิบายไว้ใน Kubernetes workloads

เปรียบเทียบ Kubernetes service types

TypeVirtual IPเข้าถึงได้จากใช้กับ
ClusterIPมีภายในคลัสเตอร์service เรียกหากัน
NodePortมี<nodeIP>:30000–32767ทดสอบ, bare metal ที่มี LB ของตัวเอง
LoadBalancerมีที่อยู่ของ LB แบบ external หรือ internalเปิด service แบบ TCP/UDP, Ingress controller
ExternalNameไม่มีภายในคลัสเตอร์ (DNS อย่างเดียว)alias ไปยัง host ภายนอก
Headlessไม่มีภายในคลัสเตอร์ (DNS ราย Pod)StatefulSet, client-side discovery

Debug Service ที่ไม่ตอบ

  1. ดู endpoint: kubectl get endpointslices -n shop -l kubernetes.io/service-name=orders ถ้าว่าง แปลว่า selector ไม่ match หรือไม่มี Pod ไหน ready
  2. เทียบ label: kubectl get pods -n shop -l app=orders --show-labels
  3. ตรวจว่า targetPort ตรงกับ port ที่ container ฟังอยู่จริง
  4. ทดสอบ DNS และ HTTP จาก Pod ชั่วคราวใน namespace เดียวกัน
  5. หา NetworkPolicy ที่อาจบล็อก traffic อยู่
  6. ถ้า EXTERNAL-IP ค้าง <pending> ให้ตรวจว่ามี cloud controller หรือ MetalLB รันอยู่ และยังมี IP ว่างให้แจก

คำถามที่พบบ่อย

Service type ค่าเริ่มต้นของ Kubernetes คืออะไร

ClusterIP ถ้าไม่ระบุ type Service จะเข้าถึงได้จากในคลัสเตอร์เท่านั้น

NodePort กับ LoadBalancer ต่างกันอย่างไร

NodePort เปิด port บนทุก node และ client ต้องหาทางเข้าถึง node เอง ส่วน LoadBalancer เพิ่ม load balancer แบบ external หรือ internal วางไว้หน้า node port เหล่านั้น ได้ที่อยู่เดียวที่นิ่งพร้อม health check

ทำไม ping ClusterIP ไม่ได้

ClusterIP มีอยู่แค่ในรูปกฎการส่งต่อสำหรับ port ของ Service ไม่ได้เป็น interface จริง ICMP จึงมักไม่ได้รับคำตอบ ให้ทดสอบด้วย protocol จริงอย่าง curl หรือ nc

ควรใช้ headless Service เมื่อไหร่

เมื่อ client ต้องเข้าถึง Pod ตัวใดตัวหนึ่งโดยเฉพาะ ไม่ใช่ตัวไหนก็ได้ ซึ่งส่วนใหญ่คือ member ของ StatefulSet อย่าง Kafka broker หรือ replica ของ database

ควรใช้ LoadBalancer Service หรือ Ingress

สำหรับ HTTP และ HTTPS โดยทั่วไปใช้ LoadBalancer ตัวเดียวให้ Ingress controller แล้วเขียนกฎ Ingress ให้แต่ละแอป ส่วน TCP หรือ UDP ดิบ ๆ เช่น database หรือ MQTT ให้ใช้ LoadBalancer Service

เลือก Service type ให้เหมาะ

  • เริ่มจาก ClusterIP เพราะ Service ส่วนใหญ่ไม่ต้องการอะไรมากกว่านี้
  • เปิด HTTP ผ่าน Ingress แทนการเปิด LoadBalancer แยกให้ทุกแอป
  • ใช้ LoadBalancer กับ protocol ที่ไม่ใช่ HTTP และใช้ internal load balancer ถ้าเข้าถึงแค่จาก network ภายใน
  • ใช้ headless Service กับ StatefulSet และ client-side discovery
  • เมื่อมีปัญหา ให้ดู EndpointSlice เป็นอย่างแรก

คอร์ส DevOps ฟรีของ Vectorkub เปิดให้ลองใช้ Service แต่ละแบบบนคลัสเตอร์จริง

พร้อมสร้างสิ่งที่ยิ่งใหญ่หรือยัง?

เล่าโปรเจคของคุณให้เราฟัง — เราจะพาคุณไปถึง

เลือกคุกกี้ที่เราใช้ได้ คุกกี้ที่จำเป็นช่วยให้เว็บไซต์ทำงานได้และเปิดใช้งานเสมอ