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 โดยมีสามส่วนทำงานร่วมกัน:
- EndpointSlice controller เก็บรายการ IP ของ Pod ที่ match และผ่าน readiness probe
- kube-proxy บนทุก node แปลงรายการนั้นเป็นกฎ iptables, IPVS หรือ nftables เพื่อส่ง traffic ที่ยิงมายัง IP ของ Service ไปให้ Pod ตัวใดตัวหนึ่ง
- CoreDNS ตั้งชื่อให้ Service เช่น
orders.shop.svc.cluster.localหรือแค่ordersถ้าเรียกจากใน namespaceshop
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 จริงแทน:
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 รันอยู่เลย
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:
apiVersion: v1
kind: Service
metadata:
name: orders-public
namespace: shop
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: orders
ports:
- port: 443
targetPort: httpskubectl 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/TCPexternalTrafficPolicy กำหนดว่า traffic ที่มาถึง node แล้วจะไปต่ออย่างไร:
| Policy | พฤติกรรม | สิ่งที่ต้องแลก |
|---|---|---|
Cluster (ค่าเริ่มต้น) | node ไหนก็รับได้ แล้วส่งต่อไปยัง Pod ซึ่งอาจอยู่คนละ node | มี hop เพิ่ม และ Pod จะเห็น IP ของ node แทน IP ของ client |
Local | node ส่งต่อเฉพาะ 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:
metadata:
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true" # AKS
# networking.gke.io/load-balancer-type: "Internal" # GKEAnnotation ที่ถูกต้องให้ดูจากเอกสารของ 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
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:
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 ทุกตัวกลับมาแทน:
apiVersion: v1
kind: Service
metadata:
name: kafka-headless
namespace: data
spec:
clusterIP: None
selector:
app: kafka
ports:
- name: broker
port: 9092$ 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.22ClusterIP ปกติซ่อนไว้ว่าเราไปถึง Pod ตัวไหน แต่ headless Service ให้ client เห็น Pod ทุกตัวและเลือกเองได้ เมื่อใช้คู่กับ StatefulSet ที่ตั้ง serviceName ชี้มาที่ Service นี้ Pod แต่ละตัวจะได้ชื่อ DNS คงที่ของตัวเองด้วย:
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
| Type | Virtual 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 ที่ไม่ตอบ
- ดู endpoint:
kubectl get endpointslices -n shop -l kubernetes.io/service-name=ordersถ้าว่าง แปลว่า selector ไม่ match หรือไม่มี Pod ไหน ready - เทียบ label:
kubectl get pods -n shop -l app=orders --show-labels - ตรวจว่า
targetPortตรงกับ port ที่ container ฟังอยู่จริง - ทดสอบ DNS และ HTTP จาก Pod ชั่วคราวใน namespace เดียวกัน
- หา NetworkPolicy ที่อาจบล็อก traffic อยู่
- ถ้า
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 แต่ละแบบบนคลัสเตอร์จริง
