小服務(wù)開(kāi)始講)
你寫(xiě)了一個(gè) HTTP 服務(wù)在自己電腦上運(yùn)行得很好。把代碼發(fā)給同事后他說(shuō)“啟動(dòng)失敗依賴(lài)版本不對(duì)?!蹦阈藓靡蕾?lài)又把服務(wù)部署到服務(wù)器。過(guò)了幾天訪問(wèn)量上來(lái)了于是你啟動(dòng)了三個(gè)副本?,F(xiàn)在新的問(wèn)題來(lái)了三個(gè)副本該放在哪些機(jī)器上某臺(tái)機(jī)器壞了誰(shuí)來(lái)補(bǔ)一個(gè)副本發(fā)布新版本時(shí)怎樣讓服務(wù)持續(xù)可用副本的地址變了用戶該訪問(wèn)誰(shuí)Docker 主要幫我們處理“怎樣把應(yīng)用及其運(yùn)行環(huán)境交付出去”。Kubernetes通常簡(jiǎn)稱(chēng) K8s主要幫我們處理“怎樣讓這些應(yīng)用在一組機(jī)器上持續(xù)運(yùn)行”。這篇文章就沿著這個(gè)小服務(wù)的部署過(guò)程把兩者串起來(lái)。一、先別急著學(xué)命令容器是什么假設(shè)你的服務(wù)依賴(lài) Python、幾個(gè)第三方庫(kù)和一份配置文件。傳統(tǒng)部署方式是先準(zhǔn)備一臺(tái)服務(wù)器再逐項(xiàng)安裝、配置。只要服務(wù)器上的版本與開(kāi)發(fā)環(huán)境有差異就可能出現(xiàn)“本地正常線上報(bào)錯(cuò)”。容器提供了另一種交付方式把運(yùn)行應(yīng)用需要的文件、依賴(lài)和配置方式打包成鏡像再用鏡像啟動(dòng)容器。這里有三個(gè)詞需要分清名詞可以怎樣理解鏡像 Image一份用于啟動(dòng)應(yīng)用的打包結(jié)果容器 Container鏡像運(yùn)行起來(lái)后的實(shí)例鏡像倉(cāng)庫(kù) Registry存放、分發(fā)鏡像的地方一個(gè)鏡像可以啟動(dòng)多個(gè)容器就像同一份程序可以運(yùn)行多個(gè)進(jìn)程。不過(guò)容器并不只是“換了個(gè)名字的進(jìn)程”它通常擁有隔離的文件系統(tǒng)、網(wǎng)絡(luò)視圖等環(huán)境也可以被限制使用多少 CPU 和內(nèi)存。容器和虛擬機(jī)最大的區(qū)別是虛擬機(jī)通常運(yùn)行自己的客戶操作系統(tǒng)Linux 容器共享宿主機(jī)內(nèi)核隔離的是進(jìn)程的運(yùn)行環(huán)境。因此容器一般更輕啟動(dòng)也更快。這里說(shuō)的是 Linux 容器在 Windows 或 macOS 上使用 Docker Desktop 運(yùn)行它們時(shí)底層通常還有一個(gè) Linux 虛擬機(jī)。你暫時(shí)不用記住 namespace、cgroups 等內(nèi)核術(shù)語(yǔ)。先記住一句話容器讓?xiě)?yīng)用以相對(duì)隔離、可重復(fù)的環(huán)境運(yùn)行但它仍然依賴(lài)宿主機(jī)提供的內(nèi)核能力。Docker 官方容器入門(mén)二、Docker 是怎樣把代碼變成容器的假設(shè)我們有一個(gè)監(jiān)聽(tīng)8080端口的小服務(wù)入口文件叫app.py。我們可以寫(xiě)一個(gè) Dockerfile告訴 Docker 怎樣制作鏡像FROM python:3.13-slim WORKDIR /app COPY . . EXPOSE 8080 CMD [python, app.py]逐行看其實(shí)沒(méi)有什么神秘的FROM以哪個(gè)現(xiàn)成鏡像為基礎(chǔ)。WORKDIR后續(xù)操作在哪個(gè)目錄執(zhí)行。COPY把代碼復(fù)制進(jìn)鏡像。EXPOSE記錄應(yīng)用使用的端口它本身不會(huì)把端口開(kāi)放給宿主機(jī)。CMD容器啟動(dòng)時(shí)默認(rèn)運(yùn)行什么命令。接著構(gòu)建并運(yùn)行dockerbuild-thello-api:1.0.dockerrun--rm-p8080:8080 hello-api:1.0-p 8080:8080的左邊是宿主機(jī)端口右邊是容器端口。訪問(wèn)宿主機(jī)的8080請(qǐng)求才能轉(zhuǎn)發(fā)到容器的8080。還要確保應(yīng)用在容器里監(jiān)聽(tīng)的是0.0.0.0而不只是127.0.0.1。至此我們完成了一條最基本的鏈路代碼 → Dockerfile → 鏡像 → 容器 → 對(duì)外提供服務(wù)如果要把鏡像交給另一臺(tái)機(jī)器還需要給它標(biāo)記倉(cāng)庫(kù)地址、推送到鏡像倉(cāng)庫(kù)然后在目標(biāo)機(jī)器拉取并運(yùn)行。鏡像讓交付更一致但不保證應(yīng)用一定正確環(huán)境變量寫(xiě)錯(cuò)、數(shù)據(jù)庫(kù)連不上照樣會(huì)啟動(dòng)失敗。Docker 官方鏡像構(gòu)建指南一個(gè)很常見(jiàn)的困惑容器里的localhost是誰(shuí)假設(shè)應(yīng)用容器需要訪問(wèn)數(shù)據(jù)庫(kù)容器。你在應(yīng)用配置里寫(xiě)localhost:5432通常會(huì)連不上。因?yàn)檎驹趹?yīng)用容器內(nèi)部看localhost指的是應(yīng)用容器自己不是數(shù)據(jù)庫(kù)容器更不是宿主機(jī)。這也是容器網(wǎng)絡(luò)要解決的問(wèn)題讓容器能夠通過(guò)合適的網(wǎng)絡(luò)和名稱(chēng)互相訪問(wèn)。用 Docker Compose 管理一組本地容器時(shí)應(yīng)用通??梢酝ㄟ^(guò)數(shù)據(jù)庫(kù)的服務(wù)名找到它例如db:5432。另一個(gè)常見(jiàn)困惑容器刪除后數(shù)據(jù)去哪了容器適合被創(chuàng)建、停止和替換。如果把數(shù)據(jù)庫(kù)數(shù)據(jù)只寫(xiě)在容器自身的可寫(xiě)層里刪除容器時(shí)這些數(shù)據(jù)通常也會(huì)跟著消失。所以數(shù)據(jù)庫(kù)這類(lèi)需要長(zhǎng)期保存的數(shù)據(jù)要放在容器生命周期之外例如使用 Docker volume。你可以把它理解為容器負(fù)責(zé)運(yùn)行程序持久化存儲(chǔ)負(fù)責(zé)留住數(shù)據(jù)。到這里Docker 已經(jīng)能很好地幫助我們制作鏡像、運(yùn)行容器以及在一臺(tái)機(jī)器上組織幾個(gè)相關(guān)服務(wù)。接下來(lái)問(wèn)題轉(zhuǎn)向多臺(tái)機(jī)器。三、為什么有了 Docker還需要 K8s現(xiàn)在老板要求我們的服務(wù)始終保持三個(gè)副本。你可以手動(dòng)在幾臺(tái)服務(wù)器上執(zhí)行docker run但隨后就要持續(xù)回答這些問(wèn)題三個(gè)副本各跑在哪臺(tái)機(jī)器一個(gè)副本退出后誰(shuí)發(fā)現(xiàn)并重啟它整臺(tái)機(jī)器失聯(lián)后誰(shuí)在別處補(bǔ)齊副本發(fā)布新鏡像時(shí)怎樣逐步替換舊副本副本隨時(shí)可能更換流量該發(fā)往哪里K8s 把這些事情組織成一套機(jī)制。你告訴它想要的狀態(tài)例如“運(yùn)行三個(gè)副本使用這個(gè)鏡像”它持續(xù)觀察實(shí)際狀態(tài)并設(shè)法縮小兩者的差距。這叫聲明式管理??梢赃@樣理解兩者的分工Docker把應(yīng)用制成鏡像并能運(yùn)行容器 K8s在集群里安排、維護(hù)和連接容器化應(yīng)用這里有一個(gè)面試中容易被問(wèn)到的細(xì)節(jié)**K8s 可以運(yùn)行 Docker 構(gòu)建的鏡像但節(jié)點(diǎn)不一定使用 Docker Engine 來(lái)運(yùn)行容器。**K8s 通過(guò)容器運(yùn)行時(shí)接口與兼容的運(yùn)行時(shí)協(xié)作常見(jiàn)選擇包括 containerd如果使用 Docker Engine則需要相應(yīng)的適配組件。Kubernetes 官方容器運(yùn)行時(shí)文檔四、K8s 集群里誰(shuí)負(fù)責(zé)什么先看全局。一個(gè) K8s 集群通常由控制平面和工作節(jié)點(diǎn) Node組成。工作節(jié)點(diǎn)是實(shí)際運(yùn)行應(yīng)用的機(jī)器??刂破矫尕?fù)責(zé)接收你的要求、記錄集群狀態(tài)、決定任務(wù)放到哪里并持續(xù)檢查實(shí)際情況。不用一開(kāi)始就背所有組件先順著一次部署看你提交一份配置“運(yùn)行三個(gè)hello-api副本?!盇PI Server接收這個(gè)請(qǐng)求。集群把期望狀態(tài)記錄下來(lái)etcd是保存集群數(shù)據(jù)的關(guān)鍵存儲(chǔ)??刂破靼l(fā)現(xiàn)“期望三個(gè)實(shí)際零個(gè)”于是創(chuàng)建相應(yīng)的工作負(fù)載。Scheduler為尚未分配節(jié)點(diǎn)的 Pod 選擇合適的 Node。Node 上的kubelet配合容器運(yùn)行時(shí)把容器真正運(yùn)行起來(lái)。如果后來(lái)一個(gè)副本消失相關(guān)控制器會(huì)繼續(xù)嘗試讓實(shí)際數(shù)量回到三個(gè)。注意“嘗試”二字如果集群資源不足、鏡像拉取失敗或配置有誤K8s 不可能憑空讓服務(wù)正常運(yùn)行。它會(huì)暴露狀態(tài)和事件供你排查。Kubernetes 官方集群組件文檔五、Pod、Deployment、Service最重要的三個(gè)對(duì)象初學(xué) K8s 容易被大量名詞勸退。先抓住三個(gè)已經(jīng)能解釋一條基本部署鏈路。Pod應(yīng)用實(shí)際運(yùn)行的地方**Pod 是 K8s 部署和調(diào)度的基本單位。**最常見(jiàn)的 Pod 里只有一個(gè)主要業(yè)務(wù)容器一個(gè) Pod 也可以包含多個(gè)需要緊密協(xié)作的容器。同一個(gè) Pod 內(nèi)的容器共享網(wǎng)絡(luò)環(huán)境可以通過(guò)localhost相互訪問(wèn)。Pod 不適合被當(dāng)成一臺(tái)永久不變的小服務(wù)器。它可能被替換地址也可能變化。因此別把“某個(gè) Pod 的 IP”寫(xiě)死在客戶端配置里。Deployment保持副本數(shù)量管理版本更新如果你直接創(chuàng)建一個(gè) Pod它消失后誰(shuí)來(lái)確保業(yè)務(wù)還保持三個(gè)副本通常我們會(huì)創(chuàng)建Deployment。它描述使用哪個(gè)鏡像、運(yùn)行多少副本以及怎樣更新。例如apiVersion:apps/v1kind:Deploymentmetadata:name:hello-apispec:replicas:3selector:matchLabels:app:hello-apitemplate:metadata:labels:app:hello-apispec:containers:-name:hello-apiimage:your-registry/hello-api:1.0ports:-containerPort:8080先忽略 YAML 的層級(jí)只看兩處replicas: 3我們希望有三個(gè)副本。image: ...:1.0這些副本運(yùn)行哪個(gè)鏡像。template則是在說(shuō)新建出來(lái)的 Pod 應(yīng)該長(zhǎng)什么樣。發(fā)布新版時(shí)修改鏡像版本Deployment 可以逐步創(chuàng)建新 Pod、替換舊 Pod出現(xiàn)問(wèn)題時(shí)也可以回滾。它適合管理我們這個(gè)無(wú)狀態(tài)的 HTTP 服務(wù)。Kubernetes 官方 Deployment 文檔Service給變化的 Pod 一個(gè)穩(wěn)定入口假設(shè)三個(gè) Pod 的地址分別是 A、B、C。B 故障后被替換為 D??蛻舳瞬粦?yīng)該每次都重新認(rèn)識(shí)這些地址。Service提供相對(duì)穩(wěn)定的訪問(wèn)方式并把請(qǐng)求導(dǎo)向符合條件的后端 Pod。它通過(guò)標(biāo)簽選擇目標(biāo)apiVersion:v1kind:Servicemetadata:name:hello-apispec:selector:app:hello-apiports:-port:80targetPort:8080這里的selector要與 Pod 的標(biāo)簽對(duì)應(yīng)。port: 80是 Service 提供的端口targetPort: 8080是應(yīng)用容器監(jiān)聽(tīng)的端口。集群內(nèi)的其他應(yīng)用可以通過(guò) Service 名稱(chēng)訪問(wèn)它而不需要記住每個(gè) Pod 的 IP。Kubernetes 官方 Service 文檔把三個(gè)對(duì)象連起來(lái)就是Deployment我想保持 3 個(gè)副本 ↓ Pod三個(gè)實(shí)際運(yùn)行應(yīng)用的單位 ↑ Service為訪問(wèn)這些 Pod 提供穩(wěn)定入口如果要讓集群外的用戶通過(guò)域名訪問(wèn)通常還要配置面向外部流量的入口例如 Gateway API、Ingress或者在支持的環(huán)境中使用LoadBalancer類(lèi)型的 Service。Service 解決穩(wěn)定訪問(wèn)后端的問(wèn)題不意味著應(yīng)用自動(dòng)擁有公網(wǎng)域名。Kubernetes 官方網(wǎng)絡(luò)概念六、應(yīng)用跑起來(lái)了為什么還是會(huì)出故障到這里我們已經(jīng)可以發(fā)布服務(wù)但“進(jìn)程存在”不等于“服務(wù)可用”。服務(wù)還沒(méi)準(zhǔn)備好就開(kāi)始接請(qǐng)求應(yīng)用可能要加載配置、連接數(shù)據(jù)庫(kù)啟動(dòng)需要十幾秒。此時(shí)容器雖然運(yùn)行了卻還不能處理請(qǐng)求。**就緒探針readiness probe**回答的是“現(xiàn)在能接流量嗎”未就緒的 Pod 不會(huì)通過(guò)對(duì)應(yīng)的 Service 接收流量。**存活探針liveness probe**回答的是“這個(gè)容器是否已經(jīng)卡死需要重啟”兩者不是一回事。把一個(gè)啟動(dòng)很慢的服務(wù)錯(cuò)誤地配置為“沒(méi)準(zhǔn)備好就重啟”反而可能造成反復(fù)重啟。Kubernetes 官方探針文檔副本數(shù)量寫(xiě)了三個(gè)卻只跑起來(lái)一個(gè)可能是節(jié)點(diǎn)沒(méi)有足夠 CPU 或內(nèi)存也可能是鏡像拉取失敗。K8s 中的資源請(qǐng)求會(huì)參與調(diào)度應(yīng)用至少需要多少資源資源限制則約束它最多能使用多少。填寫(xiě)這些數(shù)值需要依據(jù)應(yīng)用的實(shí)際運(yùn)行情況而不是隨意復(fù)制示例。數(shù)據(jù)庫(kù)也直接按同樣方法部署三個(gè)副本先停一下。我們的 HTTP 服務(wù)可以把任意請(qǐng)求交給任意一個(gè)副本副本之間通??梢曰Q這叫無(wú)狀態(tài)應(yīng)用。數(shù)據(jù)庫(kù)有持久數(shù)據(jù)、身份和復(fù)制關(guān)系部署方式復(fù)雜得多。理解了 Deployment不等于所有服務(wù)都應(yīng)該套用同一份 YAML。配置也要單獨(dú)考慮普通配置可以使用 ConfigMap密碼、令牌等敏感數(shù)據(jù)可以使用 Secret同時(shí)仍需正確設(shè)置訪問(wèn)權(quán)限。數(shù)據(jù)持久化則涉及卷和持久卷聲明等機(jī)制。Kubernetes 官方配置概念七、遇到故障時(shí)怎樣查只背對(duì)象名面試時(shí)很容易露餡。更有用的是知道排查順序。假設(shè)發(fā)布后用戶訪問(wèn)失敗可以沿著請(qǐng)求路徑問(wèn)外部入口 → Service → Pod → 容器進(jìn)程 → 應(yīng)用依賴(lài)逐層看**Pod 根本沒(méi)創(chuàng)建**看 Deployment 的期望數(shù)量和事件。**Pod 一直 Pending**看是否無(wú)法調(diào)度例如資源不足。**Pod 在重啟**看容器日志、退出原因和探針配置。**Pod 正常Service 卻訪問(wèn)不到**檢查 Service 的標(biāo)簽選擇器、端口和就緒狀態(tài)。**容器正常但接口報(bào)錯(cuò)**再看應(yīng)用日志、配置和數(shù)據(jù)庫(kù)連接。常用命令不多先熟悉這些就夠了kubectl get pods kubectl describe podpod-namekubectl logspod-namekubectl get deployment kubectl getservice關(guān)鍵不是背命令而是知道每條命令要驗(yàn)證哪個(gè)猜想。get看總體狀態(tài)describe看詳細(xì)信息和事件logs看應(yīng)用輸出。八、現(xiàn)在用兩分鐘把 Docker 和 K8s 講給面試官聽(tīng)你可以這樣說(shuō)但最好換成自己的表達(dá)我理解 Docker 首先解決的是應(yīng)用交付和運(yùn)行環(huán)境一致性的問(wèn)題。開(kāi)發(fā)者用 Dockerfile 把代碼、依賴(lài)和啟動(dòng)方式構(gòu)建成鏡像鏡像可以推送到倉(cāng)庫(kù)再在其他機(jī)器上啟動(dòng)為容器。鏡像是一份打包結(jié)果容器是運(yùn)行中的實(shí)例。當(dāng)應(yīng)用需要在多臺(tái)機(jī)器上運(yùn)行多個(gè)副本時(shí)還需要處理調(diào)度、故障恢復(fù)、更新和服務(wù)發(fā)現(xiàn)。Kubernetes 就是管理這些容器化應(yīng)用的平臺(tái)。我們通常用 Deployment 聲明鏡像和副本數(shù)量它管理 Pod 的創(chuàng)建與更新Pod 是實(shí)際運(yùn)行容器的基本單位Service 則為可能不斷變化的 Pod 提供穩(wěn)定的訪問(wèn)入口。比如我要發(fā)布三個(gè) HTTP 服務(wù)副本會(huì)先構(gòu)建并推送鏡像再創(chuàng)建一個(gè)副本數(shù)為三的 Deployment 和一個(gè) Service。某個(gè) Pod 消失后控制器會(huì)嘗試補(bǔ)齊發(fā)布新版本時(shí)可以通過(guò) Deployment 逐步替換舊 Pod。我會(huì)用就緒探針控制何時(shí)接流量并通過(guò) Pod 事件和日志排查啟動(dòng)問(wèn)題。如果你能講清這段話再回答“容器和虛擬機(jī)有什么區(qū)別”“Pod 和容器有什么區(qū)別”“為什么需要 Service”就已經(jīng)具備和同事、面試官展開(kāi)基礎(chǔ)討論的能力了。最后把整篇文章壓縮成一張腦圖寫(xiě)好應(yīng)用 ↓ Dockerfile 定義怎樣打包 ↓ 構(gòu)建鏡像推送到鏡像倉(cāng)庫(kù) ↓ K8s 的 Deployment 聲明鏡像和副本數(shù)量 ↓ Pod 在各個(gè) Node 上運(yùn)行容器 ↓ Service 把請(qǐng)求送到可用的 Pod ↓ 控制器持續(xù)檢查實(shí)際狀態(tài)是否符合期望狀態(tài)Docker 讓“把應(yīng)用帶過(guò)去并運(yùn)行”變得可重復(fù)K8s 讓“在多臺(tái)機(jī)器上持續(xù)運(yùn)行這些應(yīng)用”變得可管理。學(xué)到這里不必馬上鉆進(jìn)網(wǎng)絡(luò)插件、調(diào)度算法或集群安裝細(xì)節(jié)。先親手完成一次“構(gòu)建鏡像 → 運(yùn)行容器 → 部署三個(gè) Pod → 通過(guò) Service 訪問(wèn) → 刪除一個(gè) Pod 看它恢復(fù)”這些概念就會(huì)從名詞變成你親眼見(jiàn)過(guò)的過(guò)程。