在 k8s 搞出 pod 概念的時候,其實 docker 官方就已經推出自己的容器編排應用 swarm。這一套服務可以幫助在不同節點上的容器,進行統一的管理,主要針對容器的啟停,運維,還有部署,注意我這裡沒有提到“編排”,個人覺得確實在 swarm 中並沒有容器編排這一概念(ps:相對於 k8s 的編排,swarm 確實顯得有一丟丟稚嫩),其中關於容器中應用的部署流程,運維監控,還有日誌收集這些基本都要自己動手實現。在 swarm 中主要有兩個角色,一個是 manage,另一個是 worker,不同於 k8s 那麼多眼花繚亂的概念物件,swarm 就顯得樸實無華。但是技術的進步都是從簡入繁的,搞明白了容器編排的發展歷程,也許你對 k8s 為什麼有 pod 這一概念,並且後續產生了這麼多的抽象物件就有一定理解了。
我們先來看一下 Swarm 中的兩種角色:
- manager - 釋出配置和管理節點
- worker - 每臺容器主機節點,承載了容器應用
Manager 為了避免單點故障,一般會在叢集中使用多個節點,一般當配置釋出和修改的時候,他們直接進行狀態的同步,並且上線後會選出一個主要的 leader,選舉使用的協議是 Raft ,有點類似於 zk, 使得 manager 避免腦裂
Worker 主要的工作是負責從 manager 那裡,獲取操作的 docker 命令,獲取到的關於 build ,network 和 volume之類的配置然後同步到當前節點,他們是通過內部協議來進行通訊的
以下是命令在 manager 和 worker 之間進行的示意圖:
如果需要你自己親自設計一套容器編排的應用架構,估計你的架構設計也和 swarm 差不多。下面一一解釋一下圖中的各個階段流程是什麼意思:
swarm manager:
1. API:接收命令。建立一個 service 應用(通過API呼叫輸入)
2. orchestrator:對 service 物件建立的 task 進行排程工作(編排)
3. allocater:為各個 task 分配IP地址(分配IP)
4. dispatcher:將 task分發到 nodes (分發任務)
5. scheduler:安排一個 worker 執行 task(執行任務)
worker node:
1. 連結到分發器接收指定的排程任務 task
2. 將被指派到的 task 在本節點上執行
在 swarm 中排程的最小單位是 service。其實這個 service 可以看成是一個容器,類似於 docker- compose 內 service 的描述,也就是一個容器內,會啟動不同的程式,這也就意味著 service 會比較“重”,以業務維度要組成,共同組成一個容器應用,來統一排程。
不同於 swarm 中的 service 設計,我們來看看 k8s 的 pod。
pod 是一組並置的容器,代表了 k8s 中基本構建模組。在實際應用中我們並不會單獨部署容器,更多的是針對一組 pod 的容器進行部署和操作。然而這並不意味著一個 pod 總是要包含多個容器--實際上只包含單獨容器的 pod 也是很常見的。一個 pod 的所有容器都執行在同一節點上,所以說 pod 是 k8s 的最小排程單元。那為什麼 k8s 中會將編排單元抽象成 pod 呢?在學習 k8s 之前我一直有這疑問,因為我覺得 k8s 的物件實在是太“抽象”了,一但你將其從真實的業務場景中脫離出來,你會覺得這些人就是在專門搞一些讓人費解的概念,提高了初級小白的入門門檻。對於這點疑問,在看了張磊老師的《深入剖析Kubernetes》中關於為什麼我們需要 pod 的描述,我才回過味來,pod 其實可以看成一“程式組”,組內各個程式是“超親密的關係”,容器之間進行緊密協作。這些具有“超親密關係”容器的典型特徵包括但不限於:互相之間會發生直接的檔案交換、使用 localhost 或者 Socket 檔案進行本地通訊、會發生非常頻繁的遠端呼叫、需要共享某些 Linux Namespace(比如,一個容器要加入另一個容器的 Network Namespace)等等。
關於 Pod 網路
如上所述,由於一個 pod 中的容器執行於相同的 Network 名稱空間中,因此他們共享相同的 IP 地址和埠空間, 這意味著同一個 pod 中的網路執行的多個程式需要注意不能繫結相同的埠號,否則會引起埠衝突,同一 pod 中的所有容器都可以通過 localhost 與同一個 pod 中的其他容器進行通訊。kubernetes 叢集中所有的 pod 都在同一個共享網路地址空間中,這意味著每個 pod 都可以通過其他 pod 的 IP 地址來實現相互訪問,所以這也表示他們之間沒有 NAT (網路地址轉換)閘道器。當兩個 pod 彼此之間傳送網路資料的時候,他們都會把對方的實際 ip 地址看做資料包中的源 ip。所以你知道為什麼在節點內網不聯通的情況下, k8s 並不能通過分配 pod 的 ip 進行排程訪問了嗎?
因為,pod 之間的通訊其實是非常簡單的,無論是將兩個 pod 安排單一的還是不同的工作節點上,這些 pod 不管實際節點間的網路拖布結構如何,這些 pod 內的容器能夠像在無 NAT 的平坦網路中一樣相互通訊,就像區域網(LAN)上的計算機一樣。每個 pod 都有自己的 IP 地址,並且可以通過這個專門的網路實現 pod 之間互相訪問。這個專門的網路通常是由額外的的軟體基於真實鏈路實現的,所以說 k8s 有很多網路外掛實現,以應對不同的網路鏈路情況。
關於合理的 pod 安排
pod 應該比較輕量,我們應該將應用程式組織到多個 pod 中,而每個 pod 只包含緊密相關的元件或者程式。也就是說要不要分配容器到同一個 pod 中,主要看他們是不是緊密耦合的關係。比如它們需要一起執行,還是可以在不同的主機上執行?它們代表的是一個整體還是相互獨立的元件?它們必須一起擴縮容還是可以分別進行?一般情況下,一個 pod 應用可能由一個主程式和多個輔助程式組成。輔助程式其實就是類似 sidecar 容器,包括日誌輪轉器和收集器,資料處理器,通訊介面卡等等。
關於 Pod 的 Yaml 描述檔案
pod 定義由幾個部分組成:
- metadata 包括名稱、名稱空間、標籤和關於該容器的其他資訊。
- spec 包含 pod 內容的實現說明,例如 pod 的容器、卷和其他資料。
- status 包含執行中的 pod 的當前資訊,例如 pod 所處的條件,每個容器的描述和狀態,以及內部 IP 和其他基本資訊
apiVersion: v1 ## 描述檔案遵循 v1 版的 kubernetes API
kind: Pod ## 描述型別 pod
metadata:
name: kubia-manual ## pod 名稱
spec:
containers: ## 包含的容器
- image: luksa/kubia ## 建立容器所用的映象
name:kubia
ports:
- containerPort: 8080 ## 應用監聽的埠
protocol: TCP
關於 pod 相關的api操作
## 建立 pod
$ kubectl create -f kubia-manual.yaml
## 得到 pod 宣告的完整定義
$ kubectl get po kubia-manual -0 yaml
## 檢視新建立的 pod
$ kubectl get pods
## 檢視 pod 的詳情資訊,這個非常有用,可以排查除錯 pod 建立排程的問題
$ kubectl describe pod {pod名稱} --namespace={名稱空間}
## 獲取 pod 執行日誌,注意 k8s 在日誌檔案達到 10 MB的時候會自動輪替,logs 命令僅顯示最後一次輪替後的日誌條目
$ kubectl logs {pod名字}
## 如果是多容器 pod 需要檢視某個特定容器的日誌時,需要新增 -c 引數
$ kubectl logs {pod名字} -c {容器名稱}
在 pod 除錯中你可以使用 kubectl expose 命令建立一個 service,以便在外部訪問到這個pod。但是在實際開發除錯中,你如果不想建立一個service來與某個特定的 pod 通訊,還可以通過本地轉發的方式來進行訪問。比如在本機 kubectl 中,可以不登入 k8s 叢集的情況下,本地進行除錯 pod
## 可以將本機器的本地埠轉發到指定 pod 的埠
$ kubectl port-forward {pod名稱} {本地埠}:{pod埠}
## 可以使用 curl 命令來訪問連線到 pod
$ curl localhost:{本地埠}
這樣可以在管理機上訪問 k8s 叢集,當然如果管理機是在外網的情況下,這樣會比較危險,如果機器和k8s叢集同屬一個內網,並且外網訪問不到k8s集,這樣的除錯方式,比一個個pod登入上去手動敲 curl 要來的方便一些。
關於 pod 的標籤和名稱空間
k8s 裡有大量的通過標籤來組織 pod 和所有其他 kubernetes 物件的例子,這裡簡單的彙總一下 api,方便以後查閱。
## 建立標籤,在描述檔案 yaml 中新增
metadata:
labels:
app: appname
env: pod
## 根據標籤獲取pod
$ kubectl get po -L 標籤名1,標籤名2
## 更換標籤
$ kubectl label po {pod名字} {標籤名}={標籤更改值} --overwrite
## 多條件的標籤過濾
$ kubectl get po -l {標籤名}!={標籤值}
$ kubectl get po -l {標籤名} in ({標籤值1},{標籤值2})
$ kubectl get po -l {標籤名} notin ({標籤值1},{標籤值2})
## 刪除 pod
$ kubectl delete po {pod名字}
在刪除 pod 的時候,kubernetes 會向 pod 傳送一個 SIGTERM 訊號並等待一段秒數,如果沒有響應則通過 SIGKILL 訊號進行終止。所以為了確保程式正常關閉,程式需要正確處理 SIGTERM 訊號。
除了標籤,pod還有名稱空間的概念。首先,由於每個物件都可以有多個標籤,因此這些物件組可以重疊。而名稱空間是用來將物件分割成完全獨立且不重疊的組。比如這些名稱空間組成的不同組,可以在多租戶環境中分配資源,將資源分配為生產、開發和測試環境等等。我覺得名稱空間提供的隔離更趨向於運維,排程層面的,而標籤則趨向於業務層面的區分隔離。
## 建立一個名稱空間
apiversion: v1
kind: Namespace
metadata:
name: {名稱空間名字}
## 在名稱空間中建立物件,可以在 metadata 欄位中新增 namespace: 名稱空間名字 屬性
$ kubectl create -f ... -n {名稱空間}
這裡需要注意的是,名稱空間的隔離並不意味著網路也是隔斷的,名稱空間之間是否提供網路隔離取決於 k8s 自己所使用的網路解決方案,也就是說只要知道某個 pod 的 IP,不管名稱空間如何,都能將流量傳送到這個 pod。所以如果在名稱空間中做網路隔離時,還是需要去配置排程 node 節點的分配,還有網路的物理隔離。