ช่วงนี้งานเยอะมากครับ ตอนแรกว่าจะ publish ตอน 2 กลางสัปดาห์ แต่ก็ไม่มีเวลาเขียนเลย
สำหรับตอนนี้จะเป็นส่วนของทฤษฎีที่เกี่ยวกับ Kubernetes ครับ ซึ่งจะใช้คำศัพท์เฉพาะตัวเยอะพอสมควร ซึ่งอยากแนะนำให้ดูภาพประกอบจาก The Children’s Illustrated Guide to Kubernetes ด้วยครับ คิดว่าเป็นบทความที่เขียนดีมากๆ ผมก็เคยยืมไปนำเสนอในบริษัทมาแล้วหนหนึ่ง
Kubernetes
หน้าที่ของ Kubernetes มีอยู่ 4 อย่างด้วยกัน
- ทำให้จัดการเครื่องคอมพิวเตอร์หลายๆ เครื่อง ได้จากที่เดียว
- วาง container ไว้ในเครื่องคอมพิวเตอร์เหล่านั้น ไม่ให้แออัดเกินไป
- จับตาดู container ที่วางไว้ไม่ให้มันงอแง
- ให้บริการ service และ resource ที่ container จำเป็นต้องใช้ในการทำงาน
ในบทความนี้เราจะลองมาดูกันว่า Kubernetes ทำหน้าที่เหล่านี้อย่างไรบ้าง
Cluster Design
ในระบบ Kubernetes แบ่งเครื่องคอมพิวเตอร์เป็น 2 แบบ คือ
- Master ทำหน้าที่ให้บริการ Kubernetes API (เป็น REST)
- Node ทำหน้าที่ run container
ซึ่งเราสามารถมีเครื่องในประเภทหนึ่งมากกว่า 1 ตัวก็ได้
Master
Master จะให้บริการ REST API ซึ่งโปรแกรม kubectl ใช้อยู่ในเบื้องหลัง
เวลาเราสั่งให้มันสร้าง object สักอย่าง (เช่น Pod) Master ก็จะเก็บ data ที่เราส่งเข้ามาไว้ใน etcd แล้วก็จัดหา Node ที่เหมาะสมให้ติดตั้ง Pod ตัวนั้น
ฉะนั้นแล้วถ้า Master ตาย ก็จะไม่สามารถสร้าง Pod หรือใช้ kubectl ได้ แต่ Container ที่รันอยู่ทั้งหมดยังสามารถใช้งานปกติ
Node
ในฝั่งของ Node นั้นก็จะมีหน้าที่รัน Container ต่างๆ ซึ่งเราสามารถดูสถานะของ node ได้ด้วยคำสั่งดังนี้
$ kubectl get nodes
NAME STATUS AGE VERSION
gke-tipme-n1-a7ee1a90-mwzz Ready 6d v1.7.2
$ kubectl describe no gke-tipme-n1-a7ee1a90-mwzz
[...]
Labels: beta.kubernetes.io/instance-type=n1-standard-1
failure-domain.beta.kubernetes.io/region=asia-southeast1
failure-domain.beta.kubernetes.io/zone=asia-southeast1-a
[...]
Capacity:
cpu: 1
memory: 3794520Ki
pods: 110
[...]
Non-terminated Pods:
Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits
--------- ---- ------------ ---------- --------------- -------------
kube-system event-exporter-v0.1.4-4272745813-86m8f 0 (0%) 0 (0%) 0 (0%) 0 (0%)
kube-system fluentd-gcp-v2.0-z02z6 100m (10%) 0 (0%) 200Mi (5%) 300Mi (8%)
kube-system heapster-v1.4.0-807765746-p8pd0 138m (13%) 138m (13%) 301656Ki (7%) 301656Ki (7%)
kube-system kube-dns-1413379277-mb7gl 260m (26%) 0 (0%) 110Mi (2%) 170Mi (4%)
kube-system kube-dns-autoscaler-3880103346-sfxdw 20m (2%) 0 (0%) 10Mi (0%) 0 (0%)
kube-system kube-proxy-gke-tipme-n1-a7ee1a90-mwzz 100m (10%) 0 (0%) 0 (0%) 0 (0%)
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
CPU Requests CPU Limits Memory Requests Memory Limits
------------ ---------- --------------- -------------
905m (90%) 138m (13%) 2087512Ki (55%) 2617944Ki (68%)
(ProTip: สามารถย่อ nodes เหลือ no ได้ เช่น kubectl get no)
จะเห็นว่า Kubernetes นั้นจะมีการบันทึกไว้ว่าเครื่องนี้มี resource เท่าไร ทั้ง CPU Core และ Memory นอกจากนี้ยังมี Label กำกับไว้ด้วย โดย Kubernetes จะแปะมาให้เราอัตโนมัติ เช่นใน GKE ก็จะมีการแปะ instance type, node pool, zone, region ให้ แต่ถ้าจะแปะเพิ่มเองก็ทำได้เช่นกัน
Pod
หน่วยเล็กที่สุดใน Kubernetes เรียกว่า Pod ซึ่งมันคือ กลุ่มของ container ที่จะอยู่ด้วยกัน ซึ่งแปลว่า
- 1 Pod จะอยู่บน 1 Node เท่านั้น ไม่ว่า pod นั้นจะมี container กี่ตัวก็ตาม
- ภายใน Pod จะ share network card กัน แปลว่าถ้ามี container นึงเปิด port 3000 อีก container หนึ่งสามารถเข้า localhost:3000 เพื่อคุยกันได้เลย (แต่จะเปิด port 3000 พร้อมกันไม่ได้)
- ฉะนั้น 1 Pod จะมี 1 IP เท่านั้น ไม่ได้มีตามจำนวน container
ใน use case 90% 1 Pod จะมีเพียงแค่ 1 Container เท่านั้น สำหรับตัวอย่าง use case ที่ 1 Pod มีหลาย container ก็จะเป็นขั้น advance หน่อย เช่น
- Kubernetes DNS เคยใช้ 3 container แบบนี้ (ปัจจุบันเปลี่ยน design ไปแล้ว)
- Container ตัวนึงจะรัน SkyDNS เป็น DNS Server
- SkyDNS ไม่รู้จัก Kubernetes ก็เลยจะต้องมีโปรแกรมตัวนึงที่อ่าน Kubernetes API มาแล้วเขียนลง database ให้ SkyDNS อ่าน
- สำหรับ Database ที่ใช้ก็คือ etcd ฉะนั้นเลยต้องมี etcd ส่วนตัวด้วย
- ผมเคยรัน Django กับ static server ใน Pod เดียวกัน เพราะจะแชร์ volume กัน ให้ Django collectstatic ไปใส่ volume ที่ใช้ร่วมกัน แล้วให้ web server serve
ข้อสำคัญของ Pod คือ Pod เป็น Immutable เช่นเดียวกับ Container เมื่อสร้าง Pod แล้วจะไม่สามารถแก้ไขได้ และ Pod ยัง #yolo ด้วย เมื่อตายแล้วจะไม่นำกลับมา start ใหม่อีกครั้ง
สำหรับหน้าตาของ Pod เราได้เห็นไปในตอนที่แล้วแล้ว คราวนี้จะลองมาเพิ่มของเล่นดูบ้าง
apiVersion: v1
kind: Pod
metadata:
name: postgres
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres
env:
- name: POSTGRES_PASSWORD
value: postgres
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
resources:
requests:
memory: "256Mi"
cpu: "30m"
limits:
memory: "512Mi"
cpu: "1"
livenessProbe:
tcpSocket:
port: 5432
readinessProbe:
tcpSocket:
port: 5432
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres
volumes:
- name: postgres
gcePersistentDisk:
pdName: postgres
fsType: ext4
จะสังเกตว่าจาก Pod definition นั้น จะเพิ่มความสามารถมากกว่า Docker ดังนี้
- Pod สามารถกำหนด resource request ได้ ซึ่ง Kubernetes จะไม่ยัด Pod ลงในเครื่องเดียวจนเกิน resource ที่มีอยู่ของเครื่องนั้น เช่น ถ้าเครื่องแรม 1GB ก็สามารถรัน Pod ที่ request แรม 256MB ได้ 4 ตัวเท่านั้น
- สำหรับ CPU Request นั้น Kubernetes กำหนดให้ 1 core (vCPU) = 1 แต่ปกติแล้วโปรแกรมไม่น่าจะใช้ CPU 100% ตลอดเวลา เราก็จะซอย CPU เล็กลงไป เช่น 0.5 ก็คือครึ่ง CPU ปกติจะนิยมเขียนกันในหน่วย millicpu เช่นในตัวอย่าง
30m
ก็เท่ากับ 0.03 CPU Core (3% ของ core) - ถ้า resource เต็มทุกเครื่อง Pod จะติดสถานะ Pending ไม่ได้รัน ฉะนั้นการกำหนด resource ให้เหมาะสมเป็นเรื่องสำคัญ
- สำหรับ CPU Request นั้น Kubernetes กำหนดให้ 1 core (vCPU) = 1 แต่ปกติแล้วโปรแกรมไม่น่าจะใช้ CPU 100% ตลอดเวลา เราก็จะซอย CPU เล็กลงไป เช่น 0.5 ก็คือครึ่ง CPU ปกติจะนิยมเขียนกันในหน่วย millicpu เช่นในตัวอย่าง
- สำหรับ resource limit นั้นจะเหมือนกับของ Docker นั่นคือถ้าใช้แรมเกิน 512MB ปุ๊บ pod จะโดนยิงทิ้งทันที ส่วน CPU Limit นั้นจะถูก throttle แทน
- การบังคับใช้ CPU Limit จะต้องใช้ Container-optimized OS เท่านั้นถึงจะมีผล
- เราสามารถกำหนด
livenessProbe
/readinessProbe
ได้ โดย Kubernetes จะ poll TCP เราเรื่อยๆ หรือจะให้ยิง web request ก็ได้ เพื่อตรวจสอบว่า Pod พร้อมทำงานอยู่ ถ้าหากไม่สามารถเชื่อมต่อได้ หรือ web request return error ก็จะทำดังนี้- ถ้าเป็น
readinessProbe
จะเอาออกจาก Load balancer - ถ้าเป็น
livenessProbe
pod จะถูกปิด
- ถ้าเป็น
นอกจากนี้ Pod ยังสามารถ mount disk ได้ด้วย ซึ่งในตัวอย่างจะใช้ disk ชื่อ postgres ใน GCE (เราจะต้องสร้างเตรียมไว้ก่อน) เวลา Pod ไปอยู่เครื่องไหน Kubernetes ก็จะบอก Compute Engine ให้นำ disk ไปต่อกับเครื่องนั้น และในเครื่องนั้นก็จะเอาไปต่อเข้าใน container อีกที และถ้าลบ Pod ก็จะปลด disk ออกจากเครื่องนั้นด้วย
ReplicationController
ปัญหาของ Pod คือมันมีชีวิตอยู่ได้แค่รอบเดียว เจ้า ReplicationController จะทำหน้าที่สร้าง Pod อีกทีหนึ่งให้รักษาจำนวนที่กำหนดไว้ตลอด อาจจะเทียบกับ Autoscale Group ใน AWS ก็ได้
หน้าตา ReplicationController จะเป็นแบบนี้
apiVersion: v1
kind: ReplicationController
metadata:
name: postgres
spec:
replicas: 1
selector:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres
env:
- name: POSTGRES_PASSWORD
value: postgres
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
resources:
requests:
memory: "256Mi"
cpu: "30m"
limits:
memory: "512Mi"
cpu: "1"
livenessProbe:
tcpSocket:
port: 5432
readinessProbe:
tcpSocket:
port: 5432
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres
volumes:
- name: postgres
gcePersistentDisk:
pdName: postgres
fsType: ext4
จะสังเกตว่าข้างใน spec.template นี่มันก๊อปมาจาก spec ของ Pod ทั้งดุ้น และจะมีการกำหนด replicas: 1
แปลว่าเอา 1 Pod ถ้าจะเอาเยอะๆ ก็แก้เลขได้เลย
(ReplicationController ไม่ใช่ immutable สามารถแก้ไขเลขได้ตลอดเวลาด้วยคำสั่ง kubectl edit rc postgres
หรือ kubectl scale rc postgres --replicas=10
ก็ได้)
พอสร้างตามนี้แล้วถ้าเรา kubectl get pod
ออกมาดู จะเห็นว่าชื่อ Pod ที่สร้างขึ้นมีข้อความสุ่มต่อท้าย และถ้าเราลองลบ Pod นั้นดู ก็จะเห็นว่าจะมี Pod ใหม่ถูกสร้างมาทดแทนตลอดเวลา
(อ่านเบื้องหลังเกมนี้ได้ blog Google Cloud)
ถามว่าทำแบบนี้ดีกว่าไป stop/start container ปกติยังไง?
คำตอบคือ Pod ที่ถูกสร้างใหม่ไม่จำเป็นต้องอยู่ที่เดิม ซึ่งจะมีประโยชน์มากๆ เวลาเราจะ maintenance เครื่องเราสามารถใช้ท่านี้ได้
$ kubectl cordon gke-tipme-n1-a7ee1a90-mwzz
$ kubectl drain gke-tipme-n1-a7ee1a90-mwzz
โดยคำสั่ง cordon จะทำให้เครื่องที่ระบุไม่มีการนำ pod ใหม่นำมาติดตั้ง พอเราสั่ง drain จะทำให้ pod ทุกตัวในเครื่องนั้นจะถูกลบ ReplicationController ก็จะรีบสร้าง pod มาชดเชยในเครื่องอื่นๆ ทันที
ถ้า Pod เปิดวนได้หลายครั้ง ก็จะทำให้ย้ายเครื่องได้ไม่สะดวกเท่าลบสร้างใหม่เรื่อยๆ
Service
ปัญหาถัดมาคือถ้าเรามี pod หลายตัว แล้วเราจะติดต่อหา pod ยังไงดี? แถม pod IP ไม่ fix ด้วยนะ ถ้าโดนลบ pod ใหม่ก็จะเปลี่ยน IP ใหม่
วิธีการใน Kubernetes ก็คือเราจะต้องประกาศ service หน้าตาแบบนี้
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
จะสังเกตว่าใน spec จะมีการกำหนด selector ซึ่งจะไปตรงกับ labels ใน Pod อีกทีนึง ตรง labels นี้ไม่จำเป็นจะต้องใช้ app: postgres
เสมอไป แต่จะใช้เป็น key-value อะไรก็ได้ เช่น tier: backend
ก็ไม่ได้ห้ามอะไร (แต่ต้องตรงกับใน Pod นะ)
สำหรับ service ตัวอย่างก็จะประกาศว่า service postgres จะเปิด port 5432 ต่อเข้าหา pod ที่มี label postgres
เมื่อ add เข้าไปแล้วก็สามารถใช้คำสั่ง kubectl get services
หรือย่อ kubectl get svc
เพื่อดูสถานะได้
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
postgres 10.27.254.4 <none> 5432/TCP 19d
ก็จะเห็นว่า service จะมี IP เฉพาะตัวของมันเลย โดยด้านหลังสิ่งที่ Kubernetes ทำก็คือ เซต iptables DNAT ให้เวลาส่งข้อมูลมาที่ IP นี้ ถูก rewrite หา IP ของ Pod อีกทีหนึ่ง โดยจะมีการ load balance ให้ด้วย
เวลาจะต่อเข้าหา service นั้นก็สามารถใช้ชื่อ DNS เรียกว่า postgres
ได้เลย ไม่จำเป็นจะต้องจำ IP ตรงๆ หรืออ่านจาก environment variable POSTGRES_SERVICE_HOST
ก็จะได้ IP ออกมาเหมือนกัน
นี่คือบริการ service discovery ที่ใช้ใน Kubernetes แบบนึง (อีกแบบนึงก็คือไป query จาก Kubernetes API ตรงๆ ก็ได้เหมือนกัน)
Deployment
เมื่อกี้เราพบว่า ReplicationController สร้าง Pod แล้ว
Deployment คือผู้สร้าง ReplicationController อีกทีนึง (จริงๆ มันใช้ ReplicaSet ที่เป็น API ใหม่กว่า RC) และเป็นจุดขายของ Kubernetes เลยทีเดียว
โดย Deployment จะมีการทำงานแบบนี้
- เวลาสร้าง Deployment ครั้งแรก มันจะไปสร้าง ReplicaSet แล้ว ReplicaSet จะไปสร้าง Pod
- เราสามารถแก้ไข deployment ได้ ด้วยคำสั่ง
kubectl apply -f deployment.yaml
(ใส่พาธไปที่ไฟล์ที่เก็บ deployment ที่แก้แล้ว) หรือจะใช้kubectl edit deployment name
ก็ได้ - เมื่อแก้ไขแล้ว Deployment จะสร้าง ReplicaSet ขึ้นมาใหม่อีกอันหนึ่ง (เท่ากับว่ามี 2 ตัว)
- ReplicaSet ใหม่จะเปิด Pod ขึ้นมา 1 ตัว
- พอ Pod ตัวนี้สร้างเสร็จแล้ว (หรือถ้า config readinessProbe ไว้ก็จะรอจน Ready แล้ว) Deployment จะลดขนาด ReplicaSet เดิมลง 1 และเพิ่มขนาด ReplicaSet ใหม่อีก 1
- ทำซ้ำไปเรื่อยๆ จนกระทั่ง ReplicaSet ใหม่ขนาดเท่าเดิม และอันเดิมขนาดเหลือ 0
พูดง่ายๆ ก็คือ Deployment จะแทนที่ Pod เก่าด้วย Pod ใหม่ทีละ Pod ทำให้เวลา deploy แล้วระบบเราจะไม่มี downtime เกิดขึ้นเลย
นอกจากนี้ Deployment ยังจะเก็บ ReplicaSet ของเก่าไว้ด้วย นั่นแปลว่าถ้า deploy แล้วพังก็สามารถสั่ง kubectl rollout undo
เพื่อถอยกลับได้อีกด้วย
หน้าตาของ Deployment จะเป็นแบบนี้
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: tm-static
spec:
replicas: 1
template:
metadata:
labels:
app: tmstreamlabs
service: static
spec:
containers:
- name: static
image: registry/tmstreamlabs:version
command: [/caddy, -conf, /app/Caddyfile]
resources:
requests:
memory: "64Mi"
cpu: "1m"
readinessProbe: &probe
httpGet:
path: /js/runtime.js
port: 3030
livenessProbe: *probe
(ตรง readinessProbe/livenessProbe ผมให้มันก๊อปกันมาโดยใช้ YAML Anchor ที่ต้องทั้งใส่ 2 แบบเพราะต้องการให้เช็คตอน deploy ว่า ready แล้วหรือยัง และเวลารันอยู่ถ้าค้างก็ให้ kill ไปเลย ไม่ใช่แค่เอาออกจาก service)
อ้อ ถ้าใครชอบไม่ติด tag Docker image ใช้แต่ latest จะใช้ Deployment ไม่ได้ เพราะ apply ไปแล้วมันจะไม่เจอว่าแก้อะไร
Namespace
สุดท้ายถ้าหากเราใช้ cluster กันหลายๆ project สามารถแยกของไม่ให้ปนกันได้ด้วยการใช้ Namespace แบบนี้
apiVersion: v1
kind: Namespace
metadata:
name: app2
เวลาเราจะทำอะไรก็เรียก kubectl --namespace app2 ...
ก็จะเห็นเฉพาะของที่อยู่ใน namespace นั้นๆ และเวลาเรียก service ก็จะพิมพ์ชื่อได้เฉพาะใน namespace เดียวกันเท่านั้น
(แต่ไม่ได้บล็อคนะ แค่ต้องพิมพ์ชื่อเต็มๆ ของ service คือ postgres.default
)
โดยปกติแล้ว Kubernetes จะมี namespace สำคัญๆ 2 อัน ก็คือ default
ที่เราใช้ได้เวลาไม่ระบุชื่อ namespace และ kube-system
ที่เก็บ service ของ Kubernetes
สรุป
จากตอนนี้เราจะเห็นว่า Kubernetes มีการออกแบบตามสไตล์ Design for failure มากๆ ถ้าอะไรสักอันพังไปใน cluster ก็จะไม่ส่งผลกระทบต่อการทำงาน และเผลอๆ จะสามารถกู้ได้เองอีกด้วย
- ถ้า Master ตาย ระบบยังสามารถทำงานได้ปกติ แค่ไม่สามารถ schedule pod ใหม่ได้
- ถ้า Node ตาย Pod ก็จะถูก ReplicationController สร้างใหม่ที่เครื่องอื่นๆ
- ถ้า Pod ค้าง ก็จะตรวจพบจาก livenessProbe แล้ว kill ทิ้งไปสร้างใหม่
บางจุดอาจจะเห็นว่าใช้งานซับซ้อนพอสมควร แต่ก็เป็นวิธีที่ดีถ้าหากเราต้องการระบบที่เสถียรจริงๆ
ในตอนหน้าจะลองมาพูดถึงของเล่นที่ advance ไปกว่าตอนนี้กันบ้างครับ