虛擬化技術淺析第二彈之初識Kubernetes

京東雲開發者發表於2023-01-17

作者:京東物流 楊建民

一、微服務架構起源

單體架構:可以理解為主要業務邏輯模組(我們編寫的程式碼模組,不包括獨立的中介軟體)執行在一個程式中的應用,最典型的是執行在一個Tomcat容器中,位於一個程式裡。單體架構好處是技術門檻低、程式設計工作量少、開發簡單快捷、除錯方便、環境容易搭建、容易釋出部署及升級,開發運維等總體成本很低、見效快。其缺點也明顯:

(1)單體應用系統比較膨脹與臃腫,耦合度高,導致進行可持續開發和運維很困難。

(2)單體應用難以承載迅速增長的使用者請求和需求。

基於Spring Framework的單體應用架構圖

分散式架構核心思想是把一個單一程式的系統拆分為功能上相互協作又能獨立部署在多個伺服器上的一組程式,這樣一來,系統可以根據實際業務需要,透過以下兩種方式實現某些獨立元件的擴容,提升吞吐量。

  • 水平擴充套件:透過增加伺服器數量進行擴容

  • 垂直擴充套件:給系統中的某些特殊業務分配更好的機器,提供更多資源,從而提升這些業務的系統負載和吞吐

分散式架構是將一個龐大的單體應用拆分成多個獨立執行的程式,這些程式能透過某種方式實現遠端呼叫,因此,分散式架構要解決的第一個核心技術問題就是獨立程式之間的遠端通訊。該問題的最早答案就是RPC技術(Remote Procedure Call),一種典型的微服務架構平臺的結構示意圖如下:

大家比較熟知的微服務架構框架有Dubbo與Spring Cloud,之後比較成功的微服務架構基本都和容器技術掛鉤了,其中最成功的、影響最大的當屬Kubernetes平臺了,與之相似的還有Docker公司推出的Docker Swarm(在2017年底,Docker Swarm也支援Kubernetes了)。

關於微服務架構的優勢由於文章篇幅有限,不再展開,但任何技術都存在兩面性,微服務架構具有一定的複雜性,如開發者必須掌握某種RPC技術,並且必須透過寫程式碼來處理RPC速度過慢或者呼叫失敗等複雜問題。為了解決微服務帶來的程式設計複雜性問題,一種新的架構設計思想出現了,這就是Service Mesh,Service Mesh定義是:一個用於處理服務於服務之間通訊(呼叫)的複雜的基礎架構設施。從本質上看,Service Mesh是一組網路代理程式組成的服務網路,這些代理會與使用者程式部署在一起,充當服務代理,這種代理後來在Google的Istio產品架構中稱為“Sidecar”,其實就是採用了代理模式的思想去解決程式碼入侵及重複編碼的問題,。下圖給出了Service Mesh最簡單的架構圖。Servie Mesh同樣不是本次的主角,感興趣的小夥伴可自行學習。

二、初識k8s

官方原文是:K8s is an abbreviation derived by replacing the 8 letters “ubernete” with 8.

k8s全稱kubernetes,名字來源於希臘語,意思為“舵手”或“領航員”,它是第一代容器技術的微服務架構(第二代是Servie Mesh)。

Kubernetes最初源於谷歌內部的Borg,提供了面向應用的容器叢集部署和管理系統。Kubernetes 的目標旨在消除編排物理/虛擬計算,網路和儲存基礎設施的負擔,並使應用程式運營商和開發人員完全將重點放在以容器為中心的原語上進行自助運營。Kubernetes 也提供穩定、相容的基礎(平臺),用於構建定製化的workflows 和更高階的自動化任務。

Kubernetes 具備完善的叢集管理能力,包括多層次的安全防護和准入機制、多租戶應用支撐能力、透明的服務註冊和服務發現機制、內建負載均衡器、故障發現和自我修復能力、服務滾動升級和線上擴容、可擴充套件的資源自動排程機制、多粒度的資源配額管理能力。

Kubernetes 還提供完善的管理工具,涵蓋開發、部署測試、運維監控等各個環節。

2.1 k8s架構與元件

Kubernetes主要由以下幾個核心元件組成:

  1. etcd儲存了整個叢集的狀態;
  2. apiserver提供了資源操作的唯一入口,並提供認證、授權、訪問控制、API註冊和發現等機制;
  3. controller manager負責維護叢集的狀態,比如故障檢測、自動擴充套件、滾動更新等;
  4. scheduler負責資源的排程,按照預定的排程策略將Pod排程到相應的機器上;
  5. kubelet負責維護容器的生命週期,同時也負責Volume(CVI)和網路(CNI)的管理;
  6. Container runtime負責映象管理以及Pod和容器的真正執行(CRI);
  7. kube-proxy負責為Service提供cluster內部的服務發現和負載均衡;

2.2 k8s設計理念

API設計原則

API物件是k8s叢集中的管理操作單元。k8s叢集系統每支援一項新功能,引入一項新技術,一定會新引入對應的API物件,支援對該功能的管理操作。例如副本集Replica Set對應的API物件是RS。

k8s採用宣告式操作,由使用者定義yaml,k8s的API負責建立。每個物件都有3大類屬性:後設資料metadata、規範spec和狀態status。後設資料是用來標識API物件的,每個物件都至少有3個後設資料:namespace,name和uid;除此以外還有各種各樣的標籤labels用來標識和匹配不同的物件,例如使用者可以用標籤env來標識區分不同的服務部署環境,分別用env=dev、env=testing、env=production來標識開發、測試、生產的不同服務。規範描述了使用者期望k8s叢集中的分散式系統達到的理想狀態(Desired State),例如使用者可以透過複製控制器Replication Controller設定期望的Pod副本數為3;status描述了系統實際當前達到的狀態(Status),例如系統當前實際的Pod副本數為2;那麼複製控制器當前的程式邏輯就是自動啟動新的Pod,爭取達到副本數為3。

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80


  • apiVersion - 建立物件的Kubernetes API 版本

  • kind - 要建立什麼樣的物件?

  • metadata- 具有唯一標示物件的資料,包括 name(字串)、UID和Namespace(可選項)

使用上述.yaml檔案建立Deployment,是透過在kubectl中使用kubectl create命令來實現。將該.yaml檔案作為引數傳遞。如下例子:

$ kubectl create -f docs/user-guide/nginx-deployment.yaml --record


k8s常見物件:Pod、複製控制器(Replication Controller,RC)、副本集(Replica Set,RS)、部署(Deployment)、服務(Service)、任務(Job)、儲存卷(Volume)、持久儲存卷和持久儲存卷宣告(Persistent Volume,PV、Persistent Volume Claim,PVC)、節點(Node)、ConfigMap、Endpoint等。

控制機制設計原則

  • 每個模組都可以在必要時優雅地降級服務控制邏輯應該只依賴於當前狀態。這是為了保證分散式系統的穩定可靠,對於經常出現區域性錯誤的分散式系統,如果控制邏輯只依賴當前狀態,那麼就非常容易將一個暫時出現故障的系統恢復到正常狀態,因為你只要將該系統重置到某個穩定狀態,就可以自信的知道系統的所有控制邏輯會開始按照正常方式執行。

  • 假設任何錯誤的可能,並做容錯處理。在一個分散式系統中出現區域性和臨時錯誤是大機率事件。錯誤可能來自於物理系統故障,外部系統故障也可能來自於系統自身的程式碼錯誤,依靠自己實現的程式碼不會出錯來保證系統穩定其實也是難以實現的,因此要設計對任何可能錯誤的容錯處理。

  • 儘量避免複雜狀態機,控制邏輯不要依賴無法監控的內部狀態。因為分散式系統各個子系統都是不能嚴格透過程式內部保持同步的,所以如果兩個子系統的控制邏輯如果互相有影響,那麼子系統就一定要能互相訪問到影響控制邏輯的狀態,否則,就等同於系統裡存在不確定的控制邏輯。

  • 假設任何操作都可能被任何操作物件拒絕,甚至被錯誤解析。由於分散式系統的複雜性以及各子系統的相對獨立性,不同子系統經常來自不同的開發團隊,所以不能奢望任何操作被另一個子系統以正確的方式處理,要保證出現錯誤的時候,操作級別的錯誤不會影響到系統穩定性。

  • 每個模組都可以在出錯後自動恢復。由於分散式系統中無法保證系統各個模組是始終連線的,因此每個模組要有自我修復的能力,保證不會因為連線不到其他模組而自我崩潰。

  • 每個模組都可以在必要時優雅地降級服務。所謂優雅地降級服務,是對系統魯棒性的要求,即要求在設計實現模組時劃分清楚基本功能和高階功能,保證基本功能不會依賴高階功能,這樣同時就保證了不會因為高階功能出現故障而導致整個模組崩潰。根據這種理念實現的系統,也更容易快速地增加新的高階功能,以為不必擔心引入高階功能影響原有的基本功能。

三、資源管理

容器雲平臺如何對租戶可用資源進行精細管理,對平臺的可用性、可維護性和易用性起著至關重要的作用,是容器雲平臺能夠為使用者提供豐富的微服務管理的基石。在雲端計算領域,資源可被分為計算資源、網路資源和儲存資源三大類,也可被分別稱作計算雲、網路雲和儲存雲。

3.1、計算資源管理

Namespace

在k8s叢集中,提供計算資源的實體叫做Node,Node既可以是物理機伺服器,也可以是虛擬機器伺服器,每個Node提供了CPU、記憶體、磁碟、網路等資源。每個Node(節點)具有執行pod的一些必要服務,並由Master元件進行管理,Node節點上的服務包括Docker、kubelet和kube-proxy。

透過引入Namespace,k8s將叢集近一步劃分為多個虛擬分組進行管理,Namespace所實現“分割槽”是邏輯上的,並不與實際資源繫結,它用於多租戶場景實現資源分割槽和資源最大化利用。

大多數Kubernetes資源(例如pod、services、replication controllers或其他)都在某些Namespace中,但Namespace資源本身並不在Namespace中。而低階別資源(如Node和persistentVolumes)不在任何Namespace中。Events是一個例外:它們可能有也可能沒有Namespace,具體取決於Events的物件。

Pod

Pod是Kubernetes建立或部署的最小/最簡單的基本單位,一個Pod代表叢集上正在執行的一個程式。

一個Pod封裝一個應用容器(也可以有多個容器),儲存資源、一個獨立的網路IP以及管理控制容器執行方式的策略選項。Pod代表部署的一個單位:Kubernetes中單個應用的例項,它可能由單個容器或多個容器共享組成的資源。

每個Pod都是執行應用的單個例項,如果需要水平擴充套件應用(例如,執行多個例項),則應該使用多個Pods,每個例項一個Pod。在Kubernetes中,這樣通常稱為Replication。Replication的Pod通常由Controller建立和管理。Controller可以建立和管理多個Pod,提供副本管理、滾動升級和叢集級別的自愈能力。例如,如果一個Node故障,Controller就能自動將該節點上的Pod排程到其他健康的Node上。

Container

docker本身比較重,2015年OCI(Open ContainerInitiative)誕生,它定義了映象標準、執行時標準和分發標準,由於k8s 本身不具備建立容器的能力,是透過 kubelet 元件呼叫容器執行時 API 介面和命令來建立容器,Kubernete 與 容器執行時的關係是歷史性的,也很複雜。但是隨著 Kubernete 棄用 Docker ,目前主流的執行時主要是 containerd 和 CRI-O

“one-container-per-Pod”模式是Kubernetes最常見的用法,一個Pod也可以有多個容器。

三個級別的計算資源管理

在k8s中,可以從Namespace、Pod和Container三個級別區管理資源的配置和限制。例如:

  • 容器級別可以透過Resource Request、Resource Limits配置項
apiVersion: v1
kind: Pod
metadata:
  name: memory-demo-3
spec:
  containers:
  - name: memory-demo-3-ctr
    image: vish/stress
    resources:
      limits:
        memory: "1000Gi"
      requests:
        memory: "1000Gi"
    args:
    - -mem-total
    - 150Mi
    - -mem-alloc-size
    - 10Mi
    - -mem-alloc-sleep
    - 1s


  • Pod級別可以透過建立LimitRange物件完成設定,這樣可以對Pod所含容器進行統一配置
apiVersion: v1
kind: LimitRange
metadata:
  name: mylimits
spec:
  limits:
  - max:
      cpu: "4"
      memory: 2Gi
    min:
      cpu: 200m
      memory: 6Mi
    maxLimitRequestRatio:
      cpu: 3
      memory: 2
    type: Pod
  - default:
      cpu: 300m
      memory: 200Mi
    defaultRequest:
      cpu: 200m
      memory: 100Mi
    max:
      cpu: "2"
      memory: 1Gi
    min:
      cpu: 100m
      memory: 3Mi
    maxLimitRequestRatio:
      cpu: 5
      memory: 4
    type: Container


  • Namespace級別可以透過對ReSourceQuota資源物件的配置,提供一個總體的資源使用量限制,這個限制可以是對所有Poid使用的計算資源總量上限,也可以是對所有Pod某種型別物件的總數量上限(包括可以建立的Pod、RC、Service、Secret、ConfigMap及PVC等物件的數量)
apiVersion: v1
kind: ResourceQuota
metadata:
  name: pod-demo
spec:
  hard:
  request.cpu: "4"
  request.memory: 8GB
  limit.memory:16GB
  pods: "2"


3.2 網路資源管理

k8s的ip模型

node Ip:node節點的ip,為物理ip.

pod Ip:pod的ip,即docker 容器的ip,為虛擬ip。

cluster Ip:service 的ip,為虛擬ip。提供一個叢集內部的虛擬IP以供Pod訪問。實現原理是透過Linux防火牆規則,屬於NAT技術。當訪問ClusterIP時,請求將被轉發到後端的例項上,如果後端例項有多個,就順便實現了負載均衡,預設是輪訓方式。

跨主機容器網路方案

在k8s體系中,k8s的網路模型設計的一個基本原則:每個pos都擁有一個獨立的IP地址,而且假定所有的Pod都在一個可以直接聯通的、扁平的網路空間中,不管它們是否執行在同一個Node(宿主機)中,都可以直接透過對方的IP進行訪問。但k8s本身並不提供跨主機的容器網路解決方案。公有云環境(例如AWS、Azure、GCE)通常都提供了容器網路方案,但是在私有云環境下,仍然需要容器雲平臺位不同的租戶提供各種容器網路方案。

目前,為容器設定Overlay網路是最主流的跨主機容器網路方案。Overlay網路是指在不改變原有網路配置的前提下,透過某種額外的網路協議,將原IP報文封裝起來形成一個邏輯上的網路。在k8s平臺上,建議透過CNI外掛的方式部署容器網路。

CNI(Container Network Interface)是CNCF基金會下的一個專案,由一組用於配置容器的網路介面的規範和庫組成,它定義的是容器執行環境與網路外掛之間的介面規範,僅關心容器建立時的網路配置和容器被銷燬是網路資源的釋放,並且一個容器可以繫結多個CNI網路外掛加入網路中,如下圖。

目前比較流程的CNI外掛實現方式有Flannel、Calico、macvlan、Open vSwitch、直接路由。

Ingress

在k8s叢集內,應用預設以Service的形式提供服務,有kube-proxy實現Service到容器的負載均衡器的功能,如下圖定義一個mysql service:

kind: Service
apiVersion: v1
metadata:
  name: mysql-master
spec:
  selector:
    app: mysql-master
  ports:
      port: 3306
      targetPort: 3306


此時,叢集外是無法訪問這個Service的。對於需要k8s叢集外的客戶端提供服務的Service,可以透過Ingress將服務暴露出去,並且如果該叢集(網路)擁有真實域名,則還能將Service直接與域名進行對接。

k8s將一個Ingress資源物件的定義和一個具體的Ingress Controller相結合來實現7層負載均衡器。Ingress Controller在轉發客戶請求到後端服務時,將跳過kube-proxy提供的4層負載均衡器的功能,直接轉發到Service的後端Pod(Endpoints),以提高網路轉發效率。

如上圖展示了一個典型的HTTP層路由的Ingress例子,其中:

  • 對http://mywebsite.com/api的訪問將被路由到後端名為“api”的Service;

  • 對http://mywebsite.com/web的訪問將被路由到後端名為“web”的Service;

  • 對http://mywebsite.com/doc的訪問將被路由到後端名為“doc”的Service。

如下是一個典型的Ingress策略定義,Ingress Controller將對目標地址http://mywebsite.com/demo的訪問請求轉發到叢集內部服務的webapp(webapp:8080/demo)

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
   name: mywebsite-ingress
spec:
   rules:
   -host:mywebsite.com
    http:
       paths:
      - path: /demo
          backend:
           serviceName: webapp
           servicePort: 8080


常用的Ingress Controller有:Nginx、HAProxy、Traefik、apisix等。

3.3 儲存資源

k8s支援的Volume型別

臨時目錄(emptyDir)

使用emptyDir,當Pod分配到Node上時,將會建立emptyDir,並且只要Node上的Pod一直執行,Volume就會一直存。當Pod(不管任何原因)從Node上被刪除時,emptyDir也同時會刪除,儲存的資料也將永久刪除。注:刪除容器不影響emptyDir。

配置類

  • ConfigMap:將儲存在ConfigMap資源物件中的配置檔案資訊掛載到容器的某個目錄下

  • Secret:將儲存在Secret資源物件中的密碼金鑰等資訊掛載到容器內的某個檔案中

  • DownwardApI:將downward API的資料以環境變數或檔案的形式注入容器中

  • gitRepo:將某Git程式碼庫掛載到容器內的某個目錄下

本地儲存類

  • hostPath:將宿主機的目錄或檔案掛載到容器內進行使用

  • local:從v1.9版本引入,將本地儲存以PV形式提供給容器使用,並能夠給實現儲存空間的管理

共享儲存類

  • PV(Persistne Volume):將共享儲存定義為一種“持久儲存卷”,可以被多個容器共享使用

  • PVC(Persistne Volume Claim):使用者對儲存資源的一次“申請”,PVC申請的物件是PV,一旦申請成功,應用就能夠想使用本地目錄一樣使用共享儲存卷。下圖是一個PV物件ymal定義:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
  annotations:
        "volume.alpha.kubernetes.io/node-affinity": '{
            "requiredDuringSchedulingIgnoredDuringExecution": {
                "nodeSelectorTerms": [
                    { "matchExpressions": [
                        { "key": "kubernetes.io/hostname",
                          "operator": "In",
                          "values": ["example-node"]
                        }
                    ]}
                 ]}
              }'
spec:
    capacity:
      storage: 100Gi
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Delete
    storageClassName: local-storage
    local:
      path: /mnt/disks/ssd1


PV與PVC

PV和PVC相互關係生命週期如上圖所示,k8s的共享儲存供應模式包括靜態模式(Static)和動態模式(Dynamic),資源供應的結果就是建立好的PV。運維人員手動建立PV就是靜態,而動態模式的關鍵就是StorageClass,它的作用就是建立PV模板。

建立StorageClass裡面需要定義PV屬性比如儲存型別、大小等;另外建立這種PV需要用到儲存外掛。最終效果是,使用者提交PVC,裡面指定儲存型別,如果符合我們定義的StorageClass,則會為其自動建立PV並進行繫結。

下圖透過ymal建立一個StorageClass物件

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs    // 儲存分配器
parameters:
  type: gp2
reclaimPolicy: Retain   // 回收策略
mountOptions:
  - debug


StorageClass和PV、PVC之間的運作關係如下圖所示:

CSI

CSI(Container Storage Interface)與k8s的關係與CNI有點類似,CSI旨在容器和共享儲存之間建議一套標準的儲存訪問介面。在它誕生前,經歷了“in-tree”方式、FlexVolume模式。

CSI規範用語將儲存供應商程式碼與k8s程式碼完全解耦,儲存外掛的程式碼由儲存供應商自行維護。

和kube-apiserver直接進行互動的是K8S官方直接提供的一些sidecar容器,這些sidecar直接部署即可。這些sidecar容器(主要是上圖的三個主要部件)監聽自己對應的CRD,觸發對應的操作,透過UDS介面直接呼叫CSI driver的介面(例如CreateVolume() 、NodePublishVolme()等)來實現對卷的操作。

要開發CSI Drivers一般來說實現以下幾個服務:

  • CSI Identity service

允許呼叫者(Kubernetes元件和CSI sidecar容器)識別驅動程式及其支援的可選功能。

  • CSI Node service

NodePublishVolume, NodeUnpublishVolume 和 NodeGetCapabilities 是必須的。

所需的方法使呼叫者能夠使卷在指定的路徑上可用,並發現驅動程式支援哪些可選功能。

  • CSI Controller Service

實現CreateVolume、DeleteVolume介面

3.4 多叢集資源管理方案-叢集聯邦(Federation)

Federation是Kubernetes的子專案,其設計目標是對多個Kubernetess叢集進行統一管理,將使用者的應用部署到不同地域的資料中心。Federation引入了一個位於Kubernetes叢集只上的控制平面,遮蔽了後端各k8s子叢集,向客戶提供了統一的管理入口,如下圖:

Federation控制平面“封裝”了多個k8s叢集的Master角色,提供了統一的Master,包括Federation API Server、Federation Controller Manafer,使用者可以向操作單個叢集一樣操作Federation,還統一了全部k8s叢集的DNS、ConfigMap,並將資料儲存在集中的etcd資料庫中。

寫給讀者

對於k8s初學者來說,對k8s的第一印象應該是概念多,名詞多。《kubernetes權威指南:企業級容器雲實戰》這本書從企業實踐角度入手,講述了技術的演進,並在很多場景提供了不同技術實現的對比,結合k8s中文社群,這本書可以作為學習k8s的入門書籍。本文實際上是此書的一篇讀書筆記,文章從計算資源、網路資源、儲存資源三個方向展開,介紹了k8s裡一些常見的概念和物件。由於篇幅問題,很多也很重要的概念並沒有在文中詳細介紹,讀者可根據自身情況展開補充學習。關於k8s核心元件及工作原理將在後續陸續推出。