你可能已經知道怎麼用 Docker 跑一個 container,但一開始接觸 Kubernetes 時,一定會冒出幾個問題。
Pod 跟 container 是同一個東西嗎?
為什麼 Kubernetes 不直接管理 container,而要多包一層叫做 Pod 的東西?
一個 Pod 裡面可以放好幾個 container,那到底什麼時候該放一個,什麼時候該放很多個?
這篇文章會先從最基本的 container 開始,再一路帶你理解 Kubernetes 和 Pod 這兩個概念是怎麼長出來的。
Container 是什麼:先複習一下基礎
在談 Pod 之前,我們先確認一下 container 的定義。
Container 是一個把應用程式和它需要的所有東西(程式碼、函式庫、環境變數)打包在一起的執行單位。
白話來說,container 就是「一個獨立跑起來的程式,帶著自己專屬的一份執行環境,不會跟其他程式互相干擾」。
你可以用這行指令跑一個 container:
docker run -d --name my-app nginx:latest這行指令做的事情很單純:啟動一個叫做 my-app 的 container,裡面跑 nginx 這個網頁伺服器。
這個 container 有自己的檔案系統、自己的網路介面、自己的行程空間(process space)。
到這裡你可能會想,那我把很多個 container 都用 docker run 跑起來,不就可以組成一個系統了嗎?
理論上可以,但這樣做在大規模系統裡會遇到幾個麻煩,而這正是 Kubernetes 想解決的問題。
Kubernetes 是什麼:管理 container 的自動化系統
在講 Pod 之前,我們先花一點篇幅講清楚 Kubernetes 本身在做什麼。
Kubernetes(常縮寫成 K8s)是一套用來自動部署、管理、擴展 container 的系統。
白話來說,如果你的系統只有一兩個 container,你自己手動 docker run 就夠了。
但當你的系統變成幾十個、幾百個 container,而且分散在好幾台機器上,人工管理就會出問題。
我們用一個實際場景來感受一下這個問題有多麻煩,在這之前先釐清一個常見的誤解。
假設你的網站有前端、後端、資料庫三個部分,直覺上你可能會想:那是不是前端一個 container、後端一個 container、資料庫一個 container,總共三個就好了?
這個直覺沒有錯,但只對了一半。
確實,前端、後端、資料庫通常會是三種不同的 container image,因為它們是完全不同的程式碼,各自負責不同的工作。
但同一種服務(例如後端 API),Kubernetes 通常不會只跑一個 container,而是會同時跑好幾個一模一樣的副本(這在 Kubernetes 裡叫做 replica)。
換句話說,container 的數量跟「程式碼裡有幾種功能」沒有直接關係,而是跟「這個服務需要多少運算資源來扛流量」有關。
我們用一個更具體的功能來說明。假設你的網站有一支「查詢剩餘票數」的 API,這支 API 的程式碼只有一份,打包成一個 container image。
平常同時只有 5 個人在查票,1 個 container 就能處理完這些請求。
但演唱會開賣的那一刻,同時有 5000 人在查票,1 個 container 的運算資源根本處理不完這麼多請求,回應速度會變得很慢,甚至逾時。
這時候 Kubernetes 不會去改「查詢剩餘票數」這支 API 的程式碼,而是把同一份 container image 再多開 10 份、20 份出來,讓這些 container 平行處理查票請求。
每一個 container 裡跑的都是完全相同的程式碼,差別只在於同時有好幾份在運作,分別處理不同使用者送來的請求。
所以前面提到的 10 個 container,指的不是 10 種不同的功能,而是同一份後端 API 的程式碼,同時跑了 10 份一模一樣的副本,分散在 3 台機器上,一起分擔流量。
回到最開始的問題,如果沒有 Kubernetes,這整個過程會遇到什麼麻煩。
平常這 10 個 container 分散跑在 3 台機器上,一切都很正常。
開賣前一刻,工程師得先手動確認流量預估,然後一台一台機器登入進去,手動多開 20 個新的 container,才能讓 10 個變成 30 個。
開賣進行到一半,其中一台機器的硬體忽然出了問題當機,上面跑的 container 全部消失,查票功能瞬間少了好幾份運算能力,剩餘的 container 開始扛不住湧入的請求。
這時候得有工程師立刻發現「某台機器掛了」,手動把消失的 container 一個一個在其他還活著的機器上重新建立起來,而且不能建錯地方,不然又會有機器過載。
如果這一切都要靠工程師手動下指令處理,不但反應速度跟不上瞬間湧入的請求,萬一開賣時間是半夜,還得把值班工程師叫起來救火。
哪個 container 掛了要手動重啟嗎?
流量變大要手動多開幾個 container 嗎?
機器壞了裡面的 container 要手動搬到別台機器嗎?
Kubernetes 存在的目的,就是把這些「手動處理」的工作變成自動化。
它做的事情大致可以歸納成幾件:
- 排程(scheduling):決定每個 container 應該跑在哪一台機器上。這些跑 container 的機器,在 Kubernetes 裡有個統一的稱呼,叫做節點(node)
- 自動修復(self-healing):container 掛掉時自動重啟或重新建立
- 擴展(scaling):根據流量或設定,自動增加或減少 container 的數量
- 服務發現與負載平衡:讓 container 之間可以互相找到對方,並且分散流量
要做到這些事,Kubernetes 需要先定義出「它管理的最小單位到底是什麼」,而這個最小單位,就是接下來要講的 Pod。
為什麼需要 Pod:container 之間需要「更緊密的合作關係」
先講結論:Pod 是 Kubernetes 裡最小的部署單位,它包著一個或多個 container,讓這些 container 共享同一個網路和儲存空間。
注意這句話的重點,Kubernetes 管理的最小單位不是 container,而是 Pod。
為什麼要多包一層?
想像一個情境:你有一個 web 應用程式的 container,另外還有一個負責蒐集 log 並上傳到遠端伺服器的 container。
這兩個 container 的關係很緊密,log container 需要讀到 web container 寫出來的 log 檔案,而且理想上,這兩個 container 應該要「同生共死」——web container 掛了,log container 也該一起被重新部署。
如果各自獨立用 docker run 跑,你要自己處理:兩個 container 之間怎麼共享檔案?怎麼確保它們被排到同一台機器上?怎麼確保它們一起重啟?
Pod 把這些問題直接解決掉。
被放進同一個 Pod 的 container,會自動共享:
- 網路命名空間(network namespace):同一個 Pod 裡的 container 可以用
localhost直接互相溝通,而且對外只有一個 IP 位址 - 儲存空間(storage volume):可以掛載同一塊 volume,讓不同 container 讀寫同一批檔案
Pod 的組成:一個 Pod 可以裝一個或多個 container
講完為什麼需要 Pod,我們來看 Pod 實際上長什麼樣子。
最常見的情況,一個 Pod 裡只放一個 container。
這時候 Pod 幾乎就等於是 container 的一層外殼,你可以先把它想成「Kubernetes 版本的 container」。
我們用一個 YAML 檔案來定義一個最簡單的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: my-web-pod
spec:
containers:
- name: web
image: nginx:latest
ports:
- containerPort: 80這份設定檔說的是:建立一個叫做 my-web-pod 的 Pod,裡面跑一個叫做 web 的 container,使用 nginx:latest 這個 image,並且對外開放 80 port。
spec.containers 是一個陣列(array),這代表你可以在同一個 Pod 裡放不只一個 container。
我們把剛剛提到的 log container 情境寫成 YAML,你就會看到「多容器 Pod」長什麼樣子:
apiVersion: v1
kind: Pod
metadata:
name: web-with-logger
spec:
containers:
- name: web
image: nginx:latest
volumeMounts:
- name: log-storage
mountPath: /var/log/nginx
- name: log-shipper
image: log-shipper:latest
volumeMounts:
- name: log-storage
mountPath: /var/log/nginx
volumes:
- name: log-storage
emptyDir: {}這裡多了一個 volumes 區塊,定義了一塊叫做 log-storage 的共享空間。
web 和 log-shipper 這兩個 container 都掛載(mount)了同一塊 log-storage,所以 web 寫進去的 log 檔案,log-shipper 可以直接讀到,完全不需要透過網路傳輸。
這種「主要工作 container 搭配一個輔助 container」的組合方式,在 Kubernetes 生態裡有個專有名詞,叫做 sidecar pattern。
Pod 的生命週期:Pod 會生也會死,而且不會自己重生
理解了 Pod 的組成之後,還有一個很重要的概念:Pod 是「一次性」的。
這句話的意思是,Pod 一旦被刪除或是所在的節點掛掉,這個 Pod 就永久消失了,Kubernetes 不會把它原地復活。
這跟很多人一開始的直覺不一樣,你可能會以為 Kubernetes 會「修好」壞掉的 Pod,但實際上它的做法是「丟掉壞的,生一個新的」。
Pod 的生命週期大致會經過這幾個狀態:
- Pending:Pod 已經被建立,但裡面的 container 還沒有全部啟動,可能還在等待資源或下載 image
- Running:Pod 已經被排到某個節點上,裡面至少有一個 container 正在執行
- Succeeded:Pod 裡的所有 container 都執行完成並正常結束(常見於一次性任務)
- Failed:Pod 裡至少有一個 container 執行失敗結束
- Unknown:Kubernetes 無法確定 Pod 目前的狀態,通常是因為跟該節點的通訊出了問題
正因為 Pod 是一次性、不會自我修復的,實務上我們幾乎不會直接建立單一 Pod,而是透過另一個更高層的資源來管理它。
常見誤區:直接建立 Pod,還是透過 Deployment 建立?
這是新手很容易踩到的地方,我們用對比的方式來看。
方式一:直接建立單一 Pod(不建議)
apiVersion: v1
kind: Pod
metadata:
name: my-app-pod
spec:
containers:
- name: app
image: my-app:v1這樣做的問題是:如果這個 Pod 所在的節點掛掉,或是這個 Pod 本身因為某個錯誤被刪除,不會有任何東西去重新建立它,你的服務就直接消失了。
方式二:透過 Deployment 管理 Pod(建議)
在看 YAML 之前,先講清楚 Deployment 是什麼。
Deployment 是 Kubernetes 提供的一種更上層的資源,專門用來監控和管理 Pod,確保「應該要有幾個 Pod 在跑」這件事情隨時成立。
你可以把 Deployment 想成是 Pod 的監工:它自己不是 Pod,但它會盯著 Pod 的數量,只要 Pod 少了,它就會依照事先寫好的樣板,生出新的 Pod 來補足。
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:v1這份 YAML 其實疊了兩層資源,我們拆開來看。
最外層,也就是 kind: Deployment 一直到 spec.replicas、spec.selector 這幾行,定義的是 Deployment 本身:它的名字叫 my-app,而且規定「隨時要維持 3 個 Pod 在跑」。
再往內看,spec.template 這個區塊,裡面完整寫的其實就是一份 Pod 的規格(跟前面單獨定義 Pod 的寫法一模一樣,只是被包在 Deployment 裡面而已)。
換句話說,Deployment 負責「管理」,而 template 裡描述的才是「要被管理的 Pod 長什麼樣子」。
差別在於,Deployment 會持續監控:「現在應該要有 3 個符合 app: my-app 這個 label 的 Pod 在跑」,只要少於 3 個,它就會自動生出新的 Pod 來補足。
了解了 Pod 該交給誰管理之後,我們回頭看看 Pod 內部本身:一個 Pod 到底該不該塞多個 container?
實務應用:什麼時候該用多容器 Pod?
看到這裡,你可能會想:既然一個 Pod 可以放多個 container,那是不是應該盡量把相關的 container 都塞進同一個 Pod?
答案是不建議這樣做,除非這些 container 真的需要緊密協作。
適合放進同一個 Pod 的情境
這三種組合方式,在 Kubernetes 生態裡都有專有名詞,我們一個一個看。
Sidecar
Sidecar 指的是「主 container 加上一個輔助 container」,輔助 container 負責延伸主 container 的功能,而且不需要去修改主 container 原本的程式碼。
前面提過的 log 蒐集就是最典型的例子:主 container(例如 web 應用程式)只負責把 log 寫到本機的檔案,完全不知道這些 log 之後會被送去哪裡。
旁邊的 sidecar container 負責讀取這些檔案,再把內容送到遠端的日誌系統(例如 Elasticsearch)。
web 應用程式的程式碼從頭到尾都不需要改,換日誌系統的時候,也只需要換掉 sidecar,不會動到主程式。
另一個常見的例子跟連線加密有關,我們先簡單說明一下背景。
TLS 是幫網路連線加密的技術,平常瀏覽器網址列前面看到的 https,就是靠 TLS 在保護資料傳輸的安全。
處理這種加密連線,通常需要一個負責解密的 proxy container,我們實際拆解一下這個 sidecar 在做什麼事。
外部使用者送過來的是加密過的流量,這些流量會先進到 sidecar container,而不是直接進到主 container。
sidecar 負責把這些加密流量解密,還原成一般的 HTTP 內容,再把解密後的內容轉發給主 container。
主 container 從頭到尾只看得到解密後的 HTTP 內容,它處理完之後,也是直接把一般的 HTTP 回應交回給 sidecar,由 sidecar 負責把回應重新加密,送回給外部使用者。
整個過程下來,主 container 完全不需要知道 TLS 是什麼、也不需要處理任何加解密的邏輯,這些複雜又牽涉資安的細節,全部由 sidecar 一手包辦。
這一切之所以做得到,關鍵在於 sidecar 和主 container 是在同一個 Pod 裡:它們共享同一個網路命名空間,所以可以直接用 localhost 互相溝通;也共享同一塊 volume,所以可以透過同一份檔案交換資料。少了這層共享,sidecar 就沒辦法在不碰主程式碼的情況下插進來做事。
用 sidecar 而不是直接把功能寫進主程式,好處在於職責被切乾淨了:主程式只需要專心做自己的事,log 怎麼蒐集、流量怎麼加解密,都是另一個團隊、甚至另一份程式碼的事。
這也代表同一個 sidecar(例如處理 log 蒐集的那個)可以直接拿去搭配公司裡其他不同的主 container 使用,不需要每個服務都重新寫一次蒐集 log 的邏輯。
Adapter(sidecar 的一種:專門做格式轉換)
Adapter 指的是輔助 container 負責把主 container 輸出的格式,轉換成外部系統看得懂的格式。
舉例來說,假設你有一個舊系統,輸出的監控數據是自己定義的格式,但你想接上 Prometheus 這種監控系統,而 Prometheus 只認得它自己的標準格式。
與其去改舊系統的程式碼(可能牽一髮動全身,甚至根本沒人敢改),你可以加一個 adapter container,專門負責把舊格式轉換成 Prometheus 看得懂的格式。
舊系統完全不用動,Prometheus 也能正常抓到數據。
具體運作上,adapter 通常是開一個固定格式的網路端口(例如 HTTP 的某個路徑),Prometheus 定期來這個端口抓資料。
adapter 收到請求後,才去讀舊系統輸出的原始資料,即時轉換成 Prometheus 看得懂的格式回傳。
舊系統本身完全不知道 Prometheus 的存在,它只負責把自己原本的監控數據寫出來。
Adapter 的價值在於「轉換邏輯」是可以重複使用的:只要輸出的原始格式一樣,同一個 adapter container 就能套用在好幾個不同的舊系統上,不需要每套系統都各自寫一份轉換程式。
Ambassador(sidecar 的一種:專門做連線代理)
Ambassador 指的是輔助 container 負責代理主 container 對外部服務的連線,把連線的複雜度藏起來,讓主 container 以為自己是在跟一個很單純的服務溝通。
舉例來說,主 container 的程式碼裡只寫了連到 localhost:6379(這是 Redis 預設的 port),以為自己連的是一個單一的 Redis。
但實際上,公司的 Redis 通常不是這麼單純,我們先解釋兩個背景概念。
叢集(cluster)指的是好幾台機器一起組成一個服務,對外看起來像單一個 Redis,但實際上資料是分散存在好幾台機器上,一起分擔工作。
主從(master-slave)則是這些機器之間的分工方式:其中一台叫做「主」,負責處理寫入資料;其他台叫做「從」,平常負責備份、或分擔讀取的工作。如果主的那一台忽然掛了,其中一台從會被拔擢成新的主,接手處理寫入,這個切換的過程就叫做 failover。
換句話說,公司的 Redis 背後其實是好幾台機器組成的叢集,而且究竟哪一台是「主」,還可能隨時因為 failover 而改變。
這些複雜的連線邏輯,全部交給 ambassador container 處理,我們拆解一下它實際上在做什麼。
ambassador 自己會在本機的 localhost:6379 假裝成一個 Redis,讓主 container 以為自己在跟單一個 Redis 講話。
主 container 每次送出的請求,實際上都先進到 ambassador,由 ambassador 負責去查「現在哪一台才是真正的主」,再把請求轉送過去。
就算之後叢集發生 failover、換了一台新的主,主 container 完全不會感覺到任何變化,因為它一直都只是在跟本機的 ambassador 講話,真正變動的路由邏輯,都被 ambassador 藏在背後處理掉了。
主程式的程式碼完全不用知道背後這麼複雜,未來就算 Redis 叢集怎麼調整,也只需要改 ambassador,不用動主程式。
這種設計還有一個額外的好處:同一份主程式的程式碼,可以直接搬到不同環境使用,完全不用改。
舉例來說,開發環境的 Redis 可能只是一台簡單的機器,正式環境的 Redis 卻是一整個叢集,兩邊的連線方式、位址、密碼都不一樣。
如果這些細節寫死在主程式裡,換環境就得改程式碼、重新測試。
但只要主程式永遠只連 localhost,換環境時只需要換掉那個環境對應的 ambassador container,主程式從頭到尾都不用動。
不適合放進同一個 Pod 的情境
不是所有「相關」的 container 都應該放進同一個 Pod,以下兩種情況特別容易讓新手誤判。
兩個沒有直接依賴關係的獨立服務
最常見的誤判是把前端網站和後端 API 放進同一個 Pod,因為感覺上「這兩個都是同一個產品的一部分」。
但前端和後端其實是兩種完全獨立的服務:它們通常用不同的程式語言寫,會在不同時間點更新版本,而且流量的成長速度也不一樣(促銷活動可能讓前端流量暴增,但後端 API 的流量增加幅度不見得一樣多)。
如果把它們塞進同一個 Pod,會發生兩個具體的問題。
第一,你沒辦法只針對前端多開幾份 container 來應付流量,因為整個 Pod 是綁在一起擴展的。
第二,只是想更新前端的一行文字,也得連帶重啟後端 API,等於無緣無故讓後端也跟著重新部署一次。
這種情況應該各自用獨立的 Pod(通常也就是各自獨立的 Deployment)部署,讓前端和後端可以各自獨立擴展(scale)、各自獨立更新版本,互不影響。
只是因為「懶得多寫一份設定檔」
另一種常見的誤判,是把兩個功能上根本沒有關聯的 container,因為想省事而硬塞進同一個 Pod,例如把公司內部兩個完全不相關的小工具寫進同一份 YAML 裡,只因為「反正都要部署,寫在一起比較快」。
這樣做的代價是:這兩個 container 從此被綁在一起了。
其中一個 container 需要重啟(不管是因為程式錯誤、還是單純要更新版本),另一個原本好好的 container 也會被迫跟著重啟。
未來想針對其中一個做資源調整(例如多給它一點記憶體),也會牽連到另一個。
省下的只是一開始少寫幾行 YAML,換來的卻是往後每一次維運都要多想一層「會不會影響到另一個」。
判斷的關鍵問題是:這兩個 container 是不是「一定要一起被建立、一起被刪除、一起被搬到同一台機器上」?
如果答案是肯定的,放進同一個 Pod 才有意義。
如果這兩個 container 應該要能夠獨立擴展、獨立更新版本,那就應該拆成兩個獨立的 Pod(通常也就是兩個獨立的 Deployment)。
重點整理
- Pod 是 Kubernetes 裡最小的部署單位,不是 container 本身
- 一個 Pod 可以包含一個或多個 container,這些 container 會共享同一個網路命名空間和儲存 volume
- 多容器 Pod 常見的用法叫做 sidecar pattern,主 container 搭配負責輔助工作的 container
- Pod 是一次性的,消失後不會自己重生,Kubernetes 不會原地修復壞掉的 Pod
- 實務上很少直接建立單一 Pod,而是透過 Deployment 這類資源間接管理,讓系統具備自動補足 Pod 數量的能力
- 是否該把多個 container 放進同一個 Pod,關鍵在於它們是不是需要「一起生、一起死、一起搬家」