官术网_书友最值得收藏!

1.4 Kubernetes的基本概念和術語

Kubernetes中的大部分概念如Node、Pod、Replication Controller、Service等都可以被看作一種資源對象,幾乎所有資源對象都可以通過Kubernetes提供的kubectl工具(或者API編程調用)執行增、刪、改、查等操作并將其保存在etcd中持久化存儲。從這個角度來看,Kubernetes其實是一個高度自動化的資源控制系統,它通過跟蹤對比etcd庫里保存的“資源期望狀態”與當前環境中的“實際資源狀態”的差異來實現自動控制和自動糾錯的高級功能。

在聲明一個Kubernetes資源對象的時候,需要注意一個關鍵屬性:apiVersion。以下面的Pod聲明為例,可以看到Pod這種資源對象歸屬于v1這個核心API。

        apiVersion: v1
        kind: Pod
        metadata:
          name: myweb
          labels:
            name: myweb
        spec:
          containers:
          - name: myweb
            image: kubeguide/tomcat-app:v1
            ports:
            - containerPort: 8080

Kubernetes平臺采用了“核心+外圍擴展”的設計思路,在保持平臺核心穩定的同時具備持續演進升級的優勢。Kubernetes大部分常見的核心資源對象都歸屬于v1這個核心API,比如Node、Pod、Service、Endpoints、Namespace、RC、PersistentVolume等。在版本迭代過程中,Kubernetes先后擴展了extensions/v1beta1、apps/v1beta1、apps/v1beta2等API組,而在1.9版本之后引入了apps/v1這個正式的擴展API組,正式淘汰(deprecated)了extensions/v1beta1、apps/v1beta1、apps/v1beta2這三個API組。

我們可以采用YAML或JSON格式聲明(定義或創建)一個Kubernetes資源對象,每個資源對象都有自己的特定語法格式(可以理解為數據庫中一個特定的表),但隨著Kubernetes版本的持續升級,一些資源對象會不斷引入新的屬性。為了在不影響當前功能的情況下引入對新特性的支持,我們通常會采用下面兩種典型方法。

◎ 方法1,在設計數據庫表的時候,在每個表中都增加一個很長的備注字段,之后擴展的數據以某種格式(如XML、JSON、簡單字符串拼接等)放入備注字段。因為數據庫表的結構沒有發生變化,所以此時程序的改動范圍是最小的,風險也更小,但看起來不太美觀。

◎ 方法2,直接修改數據庫表,增加一個或多個新的列,此時程序的改動范圍較大,風險更大,但看起來比較美觀。

顯然,兩種方法都不完美。更加優雅的做法是,先采用方法1實現這個新特性,經過幾個版本的迭代,等新特性變得穩定成熟了以后,可以在后續版本中采用方法2升級到正式版。為此,Kubernetes為每個資源對象都增加了類似數據庫表里備注字段的通用屬性Annotations,以實現方法1的升級。以Kubernetes 1.3版本引入的Pod的Init Container新特性為例,一開始,Init Container的定義是在Annotations中聲明的,如下面代碼中粗體部分所示,是不是很不美觀?

        apiVersion: v1
        kind: Pod
        metadata:
          name: myapp-pod
          labels:
            app: myapp
          annotations:
            pod.beta.kubernetes.io/init-containers: '[
              {
                  "name": "init-mydb",
                  "image": "busybox",
                  "command": [......]
              }
            ]'
        spec:
          containers:
        ......

在Kubernetes 1.8版本以后,Init container特性完全成熟,其定義被放入Pod的spec.initContainers一節,看起來優雅了很多:

        apiVersion: v1
        kind: Pod
        metadata:
          name: myapp-pod
          labels:
            app: myapp
        spec:
          # These containers are run during pod initialization
          initContainers:
          - name: init-mydb
            image: busybox
            command:
            - xxx

在Kubernetes 1.8中,資源對象中的很多Alpha、Beta版本的Annotations被取消,升級成了常規定義方式,在學習Kubernetes的過程中需要特別注意。

下面介紹Kubernetes中重要的資源對象。

1.4.1 Master

Kubernetes里的Master指的是集群控制節點,在每個Kubernetes集群里都需要有一個Master來負責整個集群的管理和控制,基本上Kubernetes的所有控制命令都發給它,它負責具體的執行過程,我們后面執行的所有命令基本都是在Master上運行的。Master通常會占據一個獨立的服務器(高可用部署建議用3臺服務器),主要原因是它太重要了,是整個集群的“首腦”,如果它宕機或者不可用,那么對集群內容器應用的管理都將失效。

在Master上運行著以下關鍵進程。

◎ Kubernetes API Server(kube-apiserver):提供了HTTP Rest接口的關鍵服務進程,是Kubernetes里所有資源的增、刪、改、查等操作的唯一入口,也是集群控制的入口進程。

◎ Kubernetes Controller Manager(kube-controller-manager):Kubernetes里所有資源對象的自動化控制中心,可以將其理解為資源對象的“大總管”。

◎ Kubernetes Scheduler(kube-scheduler):負責資源調度(Pod調度)的進程,相當于公交公司的“調度室”。

另外,在Master上通常還需要部署etcd服務,因為Kubernetes里的所有資源對象的數據都被保存在etcd中。

1.4.2 Node

除了Master,Kubernetes集群中的其他機器被稱為Node,在較早的版本中也被稱為Minion。與Master一樣,Node可以是一臺物理主機,也可以是一臺虛擬機。Node是Kubernetes集群中的工作負載節點,每個Node都會被Master分配一些工作負載(Docker容器),當某個Node宕機時,其上的工作負載會被Master自動轉移到其他節點上。

在每個Node上都運行著以下關鍵進程。

◎ kubelet:負責Pod對應的容器的創建、啟停等任務,同時與Master密切協作,實現集群管理的基本功能。

◎ kube-proxy:實現Kubernetes Service的通信與負載均衡機制的重要組件。

◎ Docker Engine(docker):Docker引擎,負責本機的容器創建和管理工作。

Node可以在運行期間動態增加到Kubernetes集群中,前提是在這個節點上已經正確安裝、配置和啟動了上述關鍵進程,在默認情況下kubelet會向Master注冊自己,這也是Kubernetes推薦的Node管理方式。一旦Node被納入集群管理范圍,kubelet進程就會定時向Master匯報自身的情報,例如操作系統、Docker版本、機器的CPU和內存情況,以及當前有哪些Pod在運行等,這樣Master就可以獲知每個Node的資源使用情況,并實現高效均衡的資源調度策略。而某個Node在超過指定時間不上報信息時,會被Master判定為“失聯”,Node的狀態被標記為不可用(Not Ready),隨后Master會觸發“工作負載大轉移”的自動流程。

我們可以執行下述命令查看在集群中有多少個Node:

        # kubectl get nodes
        NAME           STATUS     ROLES    AGE     VERSION
        k8s-node-1     Ready      <none>   350d    v1.14.0

然后,通過kubectl describe node <node_name>查看某個Node的詳細信息:

        $ kubectl describe node k8s-node-1
        Name:               k8s-node-1
        Roles:              <none>
        Labels:             beta.kubernetes.io/arch=amd64
                          beta.kubernetes.io/os=linux
                          kubernetes.io/arch=amd64
                          kubernetes.io/hostname=k8s-node-1
                          kubernetes.io/os=linux
        Annotations:      node.alpha.kubernetes.io/ttl: 0
        CreationTimestamp:  Tue, 17 Apr 2018 02:29:11 +0800
        Taints:             <none>
        Unschedulable:      false
        Conditions:
          Type                 Status  LastHeartbeatTime
    LastTransitionTime                Reason                       Message
          ----                 ------  -----------------
    ------------------                ------                       -------
          NetworkUnavailable   False   Fri, 29 Mar 2019 22:48:49 +0800   Fri, 29 Mar 2019
    22:48:49 +0800   CalicoIsUp                   Calico is running on this node
          MemoryPressure       False   Wed, 03 Apr 2019 01:04:25 +0800   Wed, 05 Dec 2018
    00:58:41 +0800   KubeletHasSufficientMemory   kubelet has sufficient memory
    available
          DiskPressure         False   Wed, 03 Apr 2019 01:04:25 +0800   Wed, 05 Dec 2018
    00:58:41 +0800   KubeletHasNoDiskPressure     kubelet has no disk pressure
          PIDPressure          False   Wed, 03 Apr 2019 01:04:25 +0800   Tue, 17 Apr 2018
    02:29:08 +0800   KubeletHasSufficientPID      kubelet has sufficient PID available
          Ready                True    Wed, 03 Apr 2019 01:04:25 +0800   Sat, 30 Mar 2019
    09:30:48 +0800   KubeletReady                 kubelet is posting ready status
        Addresses:
          InternalIP:  192.168.18.3
          Hostname:    k8s-node-1
        Capacity:
          cpu:                4
          ephemeral-storage:  49250820Ki
          hugepages-1Gi:      0
          hugepages-2Mi:      0
          memory:             1867048Ki
          pods:               110
        Allocatable:
          cpu:                4
          ephemeral-storage:  45389555637
          hugepages-1Gi:      0
          hugepages-2Mi:      0
          memory:             1764648Ki
          pods:               110
        System Info:
          Machine ID:                 83ae24d58f78439e8197b00e29a99093
          System UUID:                D0E74D56-035F-21F5-7340-DB6F90069202
          Boot ID:                    23c1c362-f9f0-49fe-a4d9-a5db919ca93d
          Kernel Version:             3.10.0-693.el7.x86_64
          OS Image:                   CentOS Linux 7 (Core)
          Operating System:           linux
          Architecture:               amd64
          Container Runtime Version:  docker://18.9.2
          Kubelet Version:            v1.14.0
          Kube-Proxy Version:         v1.14.0
        PodCIDR:                     10.244.0.0/24
        Non-terminated Pods:         (2 in total)
          Namespace                  Name                        CPU Requests  CPU Limits
    Memory Requests  Memory Limits  AGE
          ---------                  ----                        ------------  ----------
    ---------------  -------------  ---
          kube-system                coredns-767997f5b5-sqkqw    100m (2%)     0 (0%)
    70Mi (4%)        170Mi (9%)     3h16m
          kube-system                kube-flannel-ds-bvph5       100m (2%)     500m (12%)
    50Mi (2%)        150Mi (8%)     4d1h
        Allocated resources:
          (Total limits may be over 100 percent, i.e., overcommitted.)
          Resource           Requests    Limits
          --------           --------    ------
          cpu                200m (5%)   500m (12%)
          memory             120Mi (6%)  320Mi (18%)
          ephemeral-storage  0 (0%)      0 (0%)
        Events:              <none>

上述命令展示了Node的如下關鍵信息。

◎ Node的基本信息:名稱、標簽、創建時間等。

◎ Node當前的運行狀態:Node啟動后會做一系列的自檢工作,比如磁盤空間是否不足(DiskPressure)、內存是否不足(MemoryPressure)、網絡是否正常(NetworkUnavailable)、PID資源是否充足(PIDPressure)。在一切正常時設置Node為Ready狀態(Ready=True),該狀態表示Node處于健康狀態,Master將可以在其上調度新的任務了(如啟動Pod)。

◎ Node的主機地址與主機名。

◎ Node上的資源數量:描述Node可用的系統資源,包括CPU、內存數量、最大可調度Pod數量等。

◎ Node可分配的資源量:描述Node當前可用于分配的資源量。

◎ 主機系統信息:包括主機ID、系統UUID、Linux kernel版本號、操作系統類型與版本、Docker版本號、kubelet與kube-proxy的版本號等。

◎ 當前運行的Pod列表概要信息。

◎ 已分配的資源使用概要信息,例如資源申請的最低、最大允許使用量占系統總量的百分比。

◎ Node相關的Event信息。

1.4.3 Pod

Pod是Kubernetes最重要的基本概念,如圖1.4所示是Pod的組成示意圖,我們看到每個Pod都有一個特殊的被稱為“根容器”的Pause容器。Pause容器對應的鏡像屬于Kubernetes平臺的一部分,除了Pause容器,每個Pod還包含一個或多個緊密相關的用戶業務容器。

圖1.4 Pod的組成示意圖

為什么Kubernetes會設計出一個全新的Pod的概念并且Pod有這樣特殊的組成結構?

原因之一:在一組容器作為一個單元的情況下,我們難以簡單地對“整體”進行判斷及有效地行動。比如,一個容器死亡了,此時算是整體死亡么?是N/M的死亡率么?引入業務無關并且不易死亡的Pause容器作為Pod的根容器,以它的狀態代表整個容器組的狀態,就簡單、巧妙地解決了這個難題。

原因之二:Pod里的多個業務容器共享Pause容器的IP,共享Pause容器掛接的Volume,這樣既簡化了密切關聯的業務容器之間的通信問題,也很好地解決了它們之間的文件共享問題。

Kubernetes為每個Pod都分配了唯一的IP地址,稱之為Pod IP,一個Pod里的多個容器共享Pod IP地址。Kubernetes要求底層網絡支持集群內任意兩個Pod之間的TCP/IP直接通信,這通常采用虛擬二層網絡技術來實現,例如Flannel、Open vSwitch等,因此我們需要牢記一點:在Kubernetes里,一個Pod里的容器與另外主機上的Pod容器能夠直接通信。

Pod其實有兩種類型:普通的Pod及靜態Pod(Static Pod)。后者比較特殊,它并沒被存放在Kubernetes的etcd存儲里,而是被存放在某個具體的Node上的一個具體文件中,并且只在此Node上啟動、運行。而普通的Pod一旦被創建,就會被放入etcd中存儲,隨后會被Kubernetes Master調度到某個具體的Node上并進行綁定(Binding),隨后該Pod被對應的Node上的kubelet進程實例化成一組相關的Docker容器并啟動。在默認情況下,當Pod里的某個容器停止時,Kubernetes會自動檢測到這個問題并且重新啟動這個Pod(重啟Pod里的所有容器),如果Pod所在的Node宕機,就會將這個Node上的所有Pod重新調度到其他節點上。Pod、容器與Node的關系如圖1.5所示。

圖1.5 Pod、容器與Node的關系

Kubernetes里的所有資源對象都可以采用YAML或者JSON格式的文件來定義或描述,下面是我們在之前的Hello World例子里用到的myweb這個Pod的資源定義文件:

        apiVersion: v1
        kind: Pod
        metadata:
          name: myweb
          labels:
            name: myweb
        spec:
          containers:
          - name: myweb
            image: kubeguide/tomcat-app:v1
            ports:
            - containerPort: 8080
            env:
            - name: MYSQL_SERVICE_HOST
              value: 'mysql'
            - name: MYSQL_SERVICE_PORT
              value: '3306'

Kind為Pod表明這是一個Pod的定義,metadata里的name屬性為Pod的名稱,在metadata里還能定義資源對象的標簽,這里聲明myweb擁有一個name=myweb的標簽。在Pod里所包含的容器組的定義則在spec一節中聲明,這里定義了一個名為myweb、對應鏡像為kubeguide/tomcat-app:v1的容器,該容器注入了名為MYSQL_SERVICE_HOST='mysql'和MYSQL_SERVICE_PORT='3306'的環境變量(env關鍵字),并且在8080端口(containerPort)啟動容器進程。Pod的IP加上這里的容器端口(containerPort),組成了一個新的概念——Endpoint,它代表此Pod里的一個服務進程的對外通信地址。一個Pod也存在具有多個Endpoint的情況,比如當我們把Tomcat定義為一個Pod時,可以對外暴露管理端口與服務端口這兩個Endpoint。

我們所熟悉的Docker Volume在Kubernetes里也有對應的概念——Pod Volume,后者有一些擴展,比如可以用分布式文件系統GlusterFS實現后端存儲功能;Pod Volume是被定義在Pod上,然后被各個容器掛載到自己的文件系統中的。

這里順便提一下Kubernetes的Event概念。Event是一個事件的記錄,記錄了事件的最早產生時間、最后重現時間、重復次數、發起者、類型,以及導致此事件的原因等眾多信息。Event通常會被關聯到某個具體的資源對象上,是排查故障的重要參考信息,之前我們看到Node的描述信息包括了Event,而Pod同樣有Event記錄,當我們發現某個Pod遲遲無法創建時,可以用kubectl describe pod xxxx來查看它的描述信息,以定位問題的成因,比如下面這個Event記錄信息表明Pod里的一個容器被探針檢測為失敗一次:

          Events:
            FirstSeen  LastSeen Count    From     SubobjectPath         Type     Reason
          Message
            ---------  -------------    ----     -------------         --------------
          -------
            10h         12m      32    {kubelet k8s-node-1}     spec.containers{kube2sky}
          Warning      Unhealthy    Liveness probe failed: Get
      http://172.17.1.2:8080/healthz: net/http: request canceled (Client.Timeout exceeded
      while awaiting headers)

每個Pod都可以對其能使用的服務器上的計算資源設置限額,當前可以設置限額的計算資源有CPU與Memory兩種,其中CPU的資源單位為CPU(Core)的數量,是一個絕對值而非相對值。

對于絕大多數容器來說,一個CPU的資源配額相當大,所以在Kubernetes里通常以千分之一的CPU配額為最小單位,用m來表示。通常一個容器的CPU配額被定義為100~300m,即占用0.1~0.3個CPU。由于CPU配額是一個絕對值,所以無論在擁有一個Core的機器上,還是在擁有48個Core的機器上,100m這個配額所代表的CPU的使用量都是一樣的。與CPU配額類似,Memory配額也是一個絕對值,它的單位是內存字節數。

在Kubernetes里,一個計算資源進行配額限定時需要設定以下兩個參數。

◎ Requests:該資源的最小申請量,系統必須滿足要求。

◎ Limits:該資源最大允許使用的量,不能被突破,當容器試圖使用超過這個量的資源時,可能會被Kubernetes“殺掉”并重啟。

通常,我們會把Requests設置為一個較小的數值,符合容器平時的工作負載情況下的資源需求,而把Limit設置為峰值負載情況下資源占用的最大量。下面這段定義表明MySQL容器申請最少0.25個CPU及64MiB內存,在運行過程中MySQL容器所能使用的資源配額為0.5個CPU及128MiB內存:

        spec:
          containers:
          - name: db
            image: mysql
            resources:
              requests:
              memory: "64Mi"
              cpu: "250m"
              limits:
              memory: "128Mi"
              cpu: "500m"

本節最后給出Pod及Pod周邊對象的示意圖作為總結,如圖1.6所示,后面部分還會涉及這張圖里的對象和概念,以進一步加強理解。

圖1.6 Pod及周邊對象

1.4.4 Label

Label(標簽)是Kubernetes系統中另外一個核心概念。一個Label是一個key=value的鍵值對,其中key與value由用戶自己指定。Label可以被附加到各種資源對象上,例如Node、Pod、Service、RC等,一個資源對象可以定義任意數量的Label,同一個Label也可以被添加到任意數量的資源對象上。Label通常在資源對象定義時確定,也可以在對象創建后動態添加或者刪除。

我們可以通過給指定的資源對象捆綁一個或多個不同的Label來實現多維度的資源分組管理功能,以便靈活、方便地進行資源分配、調度、配置、部署等管理工作。例如,部署不同版本的應用到不同的環境中;監控和分析應用(日志記錄、監控、告警)等。一些常用的Label示例如下。

◎ 版本標簽:"release" : "stable"、"release" : "canary"。

◎ 環境標簽:"environment":"dev"、"environment":"qa"、"environment":"production"。

◎ 架構標簽:"tier" : "frontend"、"tier" : "backend"、"tier" : "middleware"。

◎ 分區標簽:"partition" : "customerA"、"partition" : "customerB"。

◎ 質量管控標簽:"track" : "daily"、"track" : "weekly"。

Label相當于我們熟悉的“標簽”。給某個資源對象定義一個Label,就相當于給它打了一個標簽,隨后可以通過Label Selector(標簽選擇器)查詢和篩選擁有某些Label的資源對象,Kubernetes通過這種方式實現了類似SQL的簡單又通用的對象查詢機制。

Label Selector可以被類比為SQL語句中的where查詢條件,例如,name=redis-slave這個Label Selector作用于Pod時,可以被類比為select * from pod where pod's name =‘redis-slave'這樣的語句。當前有兩種Label Selector表達式:基于等式的(Equality-based)和基于集合的(Set-based),前者采用等式類表達式匹配標簽,下面是一些具體的例子。

◎ name = redis-slave:匹配所有具有標簽name=redis-slave的資源對象。

◎ env != production:匹配所有不具有標簽env=production的資源對象,比如env=test就是滿足此條件的標簽之一。

后者則使用集合操作類表達式匹配標簽,下面是一些具體的例子。

◎ name in(redis-master, redis-slave):匹配所有具有標簽name=redis-master或者name= redis-slave的資源對象。

◎ name not in(php-frontend):匹配所有不具有標簽name=php-frontend的資源對象。

可以通過多個Label Selector表達式的組合實現復雜的條件選擇,多個表達式之間用“,”進行分隔即可,幾個條件之間是“AND”的關系,即同時滿足多個條件,比如下面的例子:

          name=redis-slave,env!=production
          name notin (php-frontend),env!=production

以myweb Pod為例,Label被定義在其metadata中:

          apiVersion: v1
          kind: Pod
          metadata:
            name: myweb
            labels:
              app: myweb

管理對象RC和Service則通過Selector字段設置需要關聯Pod的Label:

          apiVersion: v1
          kind: ReplicationController
          metadata:
            name: myweb
          spec:
            replicas: 1
            selector:
              app: myweb
            template:
            ......

          apiVersion: v1
          kind: Service
          metadata:
            name: myweb
          spec:
            selector:
            app: myweb
            ports:
            - port: 8080

其他管理對象如Deployment、ReplicaSet、DaemonSet和Job則可以在Selector中使用基于集合的篩選條件定義,例如:

        selector:
          matchLabels:
            app: myweb
          matchExpressions:
            - {key: tier, operator: In, values: [frontend]}
            - {key: environment, operator: NotIn, values: [dev]}

matchLabels用于定義一組Label,與直接寫在Selector中的作用相同;matchExpressions用于定義一組基于集合的篩選條件,可用的條件運算符包括In 、 NotIn 、 Exists和DoesNotExist。

如果同時設置了matchLabels和matchExpressions,則兩組條件為AND關系,即需要同時滿足所有條件才能完成Selector的篩選。

Label Selector在Kubernetes中的重要使用場景如下。

◎ kube-controller進程通過在資源對象RC上定義的Label Selector來篩選要監控的Pod副本數量,使Pod副本數量始終符合預期設定的全自動控制流程。

◎ kube-proxy進程通過Service的Label Selector來選擇對應的Pod,自動建立每個Service到對應Pod的請求轉發路由表,從而實現Service的智能負載均衡機制。

◎ 通過對某些Node定義特定的Label,并且在Pod定義文件中使用NodeSelector這種標簽調度策略,kube-scheduler進程可以實現Pod定向調度的特性。

在前面的留言板例子中,我們只使用了一個name=XXX的Label Selector。看一個更復雜的例子:假設為Pod定義了3個Label:release、env和role,不同的Pod定義了不同的Label值,如圖1.7所示,如果設置“role=frontend”的Label Selector,則會選取到Node 1和Node 2上的Pod。

如果設置“release=beta”的Label Selector,則會選取到Node 2和Node 3上的Pod,如圖1.8所示。

總之,使用Label可以給對象創建多組標簽,Label和Label Selector共同構成了Kubernetes系統中核心的應用模型,使得被管理對象能夠被精細地分組管理,同時實現了整個集群的高可用性。

圖1.7 Label Selector的作用范圍1

圖1.8 Label Selector的作用范圍2

1.4.5 Replication Controller

在上一節的例子中已經對Replication Controller(簡稱RC)的定義和作用做了一些說明,本節對RC的概念進行深入描述。

RC是Kubernetes系統中的核心概念之一,簡單來說,它其實定義了一個期望的場景,即聲明某種Pod的副本數量在任意時刻都符合某個預期值,所以RC的定義包括如下幾個部分。

◎ Pod期待的副本數量。

◎ 用于篩選目標Pod的Label Selector。

◎ 當Pod的副本數量小于預期數量時,用于創建新Pod的Pod模板(template)。

下面是一個完整的RC定義的例子,即確保擁有tier=frontend標簽的這個Pod(運行Tomcat容器)在整個Kubernetes集群中始終只有一個副本:

        apiVersion: v1
        kind: ReplicationController
        metadata:
          name: frontend
        spec:
          replicas: 1
          selector:
            tier: frontend
          template:
            metadata:
              labels:
              app: app-demo
              tier: frontend
            spec:
              containers:
              - name: tomcat-demo
              image: tomcat
              imagePullPolicy: IfNotPresent
              env:
              - name: GET_HOSTS_FROM
                value: dns
              ports:
              - containerPort: 80

在我們定義了一個RC并將其提交到Kubernetes集群中后,Master上的Controller Manager組件就得到通知,定期巡檢系統中當前存活的目標Pod,并確保目標Pod實例的數量剛好等于此RC的期望值,如果有過多的Pod副本在運行,系統就會停掉一些Pod,否則系統會再自動創建一些Pod。可以說,通過RC,Kubernetes實現了用戶應用集群的高可用性,并且大大減少了系統管理員在傳統IT環境中需要完成的許多手工運維工作(如主機監控腳本、應用監控腳本、故障恢復腳本等)。

下面以有3個Node的集群為例,說明Kubernetes如何通過RC來實現Pod副本數量自動控制的機制。假如在我們的RC里定義redis-slave這個Pod需要保持兩個副本,系統將可能在其中的兩個Node上創建Pod。圖1.9描述了在兩個Node上創建redis-slave Pod的情形。

圖1.9 在兩個Node上創建redis-slave Pod

假設Node 2上的Pod 2意外終止,則根據RC定義的replicas數量2,Kubernetes將會自動創建并啟動一個新的Pod,以保證在整個集群中始終有兩個redis-slave Pod運行。

如圖1.10所示,系統可能選擇Node 3或者Node 1來創建一個新的Pod。

圖1.10 根據RC定義創建新的Pod

此外,在運行時,我們可以通過修改RC的副本數量,來實現Pod的動態縮放(Scaling),這可以通過執行kubectl scale命令來一鍵完成:

        $ kubectl scale rc redis-slave --replicas=3
        scaled

Scaling的執行結果如圖1.11所示。

圖1.11 Scaling的執行結果

需要注意的是,刪除RC并不會影響通過該RC已創建好的Pod。為了刪除所有Pod,可以設置replicas的值為0,然后更新該RC。另外,kubectl提供了stop和delete命令來一次性刪除RC和RC控制的全部Pod。

應用升級時,通常會使用一個新的容器鏡像版本替代舊版本。我們希望系統平滑升級,比如在當前系統中有10個對應的舊版本的Pod,則最佳的系統升級方式是舊版本的Pod每停止一個,就同時創建一個新版本的Pod,在整個升級過程中此消彼長,而運行中的Pod數量始終是10個,幾分鐘以后,當所有的Pod都已經是新版本時,系統升級完成。通過RC機制,Kubernetes很容易就實現了這種高級實用的特性,被稱為“滾動升級”(Rolling Update),具體的操作方法詳見3.11節的說明。

Replication Controller由于與Kubernetes代碼中的模塊Replication Controller同名,同時“Replication Controller”無法準確表達它的本意,所以在Kubernetes 1.2中,升級為另外一個新概念——Replica Set,官方解釋其為“下一代的RC”。Replica Set與RC當前的唯一區別是,Replica Sets支持基于集合的Label selector(Set-based selector),而RC只支持基于等式的Label Selector(equality-based selector),這使得Replica Set的功能更強。下面是等價于之前RC例子的Replica Set的定義(省去了Pod模板部分的內容):

        apiVersion: extensions/v1beta1
        kind: ReplicaSet
        metadata:
          name: frontend
        spec:
          selector:
            matchLabels:
              tier: frontend
            matchExpressions:
              - {key: tier, operator: In, values: [frontend]}
          template:
          ......

kubectl命令行工具適用于RC的絕大部分命令同樣適用于Replica Set。此外,我們當前很少單獨使用Replica Set,它主要被Deployment這個更高層的資源對象所使用,從而形成一整套Pod創建、刪除、更新的編排機制。我們在使用Deployment時,無須關心它是如何創建和維護Replica Set的,這一切都是自動發生的。

Replica Set與Deployment這兩個重要的資源對象逐步替代了之前RC的作用,是Kubernetes 1.3里Pod自動擴容(伸縮)這個告警功能實現的基礎,也將繼續在Kubernetes未來的版本中發揮重要的作用。

最后總結一下RC(Replica Set)的一些特性與作用。

◎ 在大多數情況下,我們通過定義一個RC實現Pod的創建及副本數量的自動控制。

◎ 在RC里包括完整的Pod定義模板。

◎ RC通過Label Selector機制實現對Pod副本的自動控制。

◎ 通過改變RC里的Pod副本數量,可以實現Pod的擴容或縮容。

◎ 通過改變RC里Pod模板中的鏡像版本,可以實現Pod的滾動升級。

1.4.6 Deployment

Deployment是Kubernetes在1.2版本中引入的新概念,用于更好地解決Pod的編排問題。為此,Deployment在內部使用了Replica Set來實現目的,無論從Deployment的作用與目的、YAML定義,還是從它的具體命令行操作來看,我們都可以把它看作RC的一次升級,兩者的相似度超過90%。

Deployment相對于RC的一個最大升級是我們可以隨時知道當前Pod“部署”的進度。實際上由于一個Pod的創建、調度、綁定節點及在目標Node上啟動對應的容器這一完整過程需要一定的時間,所以我們期待系統啟動N個Pod副本的目標狀態,實際上是一個連續變化的“部署過程”導致的最終狀態。

Deployment的典型使用場景有以下幾個。

◎ 創建一個Deployment對象來生成對應的Replica Set并完成Pod副本的創建。

◎ 檢查Deployment的狀態來看部署動作是否完成(Pod副本數量是否達到預期的值)。

◎ 更新Deployment以創建新的Pod(比如鏡像升級)。

◎ 如果當前Deployment不穩定,則回滾到一個早先的Deployment版本。

◎ 暫停Deployment以便于一次性修改多個PodTemplateSpec的配置項,之后再恢復Deployment,進行新的發布。

◎ 擴展Deployment以應對高負載。

◎ 查看Deployment的狀態,以此作為發布是否成功的指標。

◎ 清理不再需要的舊版本ReplicaSets。

除了API聲明與Kind類型等有所區別,Deployment的定義與Replica Set的定義很類似:

        apiVersion: extensions/v1beta1               apiVersion: v1
        kind: Deployment                          kind: ReplicaSet
        metadata:                                 metadata:
          name: nginx-deployment                    name: nginx-repset

下面通過運行一些例子來直觀地感受Deployment的概念。創建一個名為tomcat-deployment.yaml的Deployment描述文件,內容如下:

        apiVersion: extensions/v1beta1
        kind: Deployment
        metadata:
          name: frontend
        spec:
          replicas: 1
          selector:
            matchLabels:
              tier: frontend
            matchExpressions:
              - {key: tier, operator: In, values: [frontend]}
          template:
            metadata:
              labels:
                app: app-demo
                tier: frontend
            spec:
              containers:
              - name: tomcat-demo
                image: tomcat
                imagePullPolicy: IfNotPresent
                ports:
                - containerPort: 8080

運行下述命令創建Deployment:

        # kubectl create -f tomcat-deployment.yaml
        deployment "tomcat-deploy" created

運行下述命令查看Deployment的信息:

        # kubectl get deployments
        NAME                   DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
        tomcat-deploy        1           1          1              1            4m

對上述輸出中涉及的數量解釋如下。

◎ DESIRED:Pod副本數量的期望值,即在Deployment里定義的Replica。

◎ CURRENT:當前Replica的值,實際上是Deployment創建的Replica Set里的Replica值,這個值不斷增加,直到達到DESIRED為止,表明整個部署過程完成。

◎ UP-TO-DATE:最新版本的Pod的副本數量,用于指示在滾動升級的過程中,有多少個Pod副本已經成功升級。

◎ AVAILABLE:當前集群中可用的Pod副本數量,即集群中當前存活的Pod數量。

運行下述命令查看對應的Replica Set,我們看到它的命名與Deployment的名稱有關系:

        # kubectl get rs
        NAME                           DESIRED   CURRENT   AGE
        tomcat-deploy-1640611518   1          1          1m

運行下述命令查看創建的Pod,我們發現Pod的命名以Deployment對應的Replica Set的名稱為前綴,這種命名很清晰地表明了一個Replica Set創建了哪些Pod,對于Pod滾動升級這種復雜的過程來說,很容易排查錯誤:

        # kubectl get pods
        NAME                                   READY     STATUS    RESTARTS   AGE
        tomcat-deploy-1640611518-zhrsc   1/1       Running   0           3m

運行kubectl describe deployments,可以清楚地看到Deployment控制的Pod的水平擴展過程,具體內容可參見第3章的說明,這里不再贅述。

Pod的管理對象,除了RC和Deployment,還包括ReplicaSet、DaemonSet、StatefulSet、Job等,分別用于不同的應用場景中,將在第3章進行詳細介紹。

1.4.7 Horizontal Pod Autoscaler

通過手工執行kubectl scale命令,我們可以實現Pod擴容或縮容。如果僅僅到此為止,顯然不符合谷歌對Kubernetes的定位目標——自動化、智能化。在谷歌看來,分布式系統要能夠根據當前負載的變化自動觸發水平擴容或縮容,因為這一過程可能是頻繁發生的、不可預料的,所以手動控制的方式是不現實的。

因此,在Kubernetes的1.0版本實現后,就有人在默默研究Pod智能擴容的特性了,并在Kubernetes 1.1中首次發布重量級新特性——Horizontal Pod Autoscaling(Pod橫向自動擴容,HPA)。在Kubernetes 1.2中HPA被升級為穩定版本(apiVersion: autoscaling/v1),但仍然保留了舊版本(apiVersion: extensions/v1beta1)。Kubernetes從1.6版本開始,增強了根據應用自定義的指標進行自動擴容和縮容的功能,API版本為autoscaling/v2alpha1,并不斷演進。

HPA與之前的RC、Deployment一樣,也屬于一種Kubernetes資源對象。通過追蹤分析指定RC控制的所有目標Pod的負載變化情況,來確定是否需要有針對性地調整目標Pod的副本數量,這是HPA的實現原理。當前,HPA有以下兩種方式作為Pod負載的度量指標。

◎ CPUUtilizationPercentage。

◎ 應用程序自定義的度量指標,比如服務在每秒內的相應請求數(TPS或QPS)。

CPUUtilizationPercentage是一個算術平均值,即目標Pod所有副本自身的CPU利用率的平均值。一個Pod自身的CPU利用率是該Pod當前CPU的使用量除以它的Pod Request的值,比如定義一個Pod的Pod Request為0.4,而當前Pod的CPU使用量為0.2,則它的CPU使用率為50%,這樣就可以算出一個RC控制的所有Pod副本的CPU利用率的算術平均值了。如果某一時刻CPUUtilizationPercentage的值超過80%,則意味著當前Pod副本數量很可能不足以支撐接下來更多的請求,需要進行動態擴容,而在請求高峰時段過去后,Pod的CPU利用率又會降下來,此時對應的Pod副本數應該自動減少到一個合理的水平。如果目標Pod沒有定義Pod Request的值,則無法使用CPUUtilizationPercentage實現Pod橫向自動擴容。除了使用CPUUtilizationPercentage,Kubernetes從1.2版本開始也在嘗試支持應用程序自定義的度量指標。

在CPUUtilizationPercentage計算過程中使用到的Pod的CPU使用量通常是1min內的平均值,通常通過查詢Heapster監控子系統來得到這個值,所以需要安裝部署Heapster,這樣便增加了系統的復雜度和實施HPA特性的復雜度。因此,從1.7版本開始,Kubernetes自身孵化了一個基礎性能數據采集監控框架——Kubernetes Monitoring Architecture,從而更好地支持HPA和其他需要用到基礎性能數據的功能模塊。在Kubernetes Monitoring Architecture中,Kubernetes定義了一套標準化的API接口Resource Metrics API,以方便客戶端應用程序(如HPA)從Metrics Server中獲取目標資源對象的性能數據,例如容器的CPU和內存使用數據。到了Kubernetes 1.8版本,Resource Metrics API被升級為metrics.k8s.io/v1beta1,已經接近生產環境中的可用目標了。

下面是HPA定義的一個具體例子:

        apiVersion: autoscaling/v1
        kind: HorizontalPodAutoscaler
        metadata:
          name: php-apache
          namespace: default
        spec:
          maxReplicas: 10
          minReplicas: 1
          scaleTargetRef:
            kind: Deployment
            name: php-apache
          targetCPUUtilizationPercentage: 90

根據上面的定義,我們可以知道這個HPA控制的目標對象為一個名為php-apache的Deployment里的Pod副本,當這些Pod副本的CPUUtilizationPercentage的值超過90%時會觸發自動動態擴容行為,在擴容或縮容時必須滿足的一個約束條件是Pod的副本數為1~10。

除了可以通過直接定義YAML文件并且調用kubectrl create的命令來創建一個HPA資源對象的方式,還可以通過下面的簡單命令行直接創建等價的HPA對象:

        # kubectl autoscale deployment php-apache --cpu-percent=90--min=1--max=10

第2章將會給出一個完整的HPA例子來說明其用法和功能。

1.4.8 StatefulSet

在Kubernetes系統中,Pod的管理對象RC、Deployment、DaemonSet和Job都面向無狀態的服務。但現實中有很多服務是有狀態的,特別是一些復雜的中間件集群,例如MySQL集群、MongoDB集群、Akka集群、ZooKeeper集群等,這些應用集群有4個共同點。

(1)每個節點都有固定的身份ID,通過這個ID,集群中的成員可以相互發現并通信。

(2)集群的規模是比較固定的,集群規模不能隨意變動。

(3)集群中的每個節點都是有狀態的,通常會持久化數據到永久存儲中。

(4)如果磁盤損壞,則集群里的某個節點無法正常運行,集群功能受損。

如果通過RC或Deployment控制Pod副本數量來實現上述有狀態的集群,就會發現第1點是無法滿足的,因為Pod的名稱是隨機產生的,Pod的IP地址也是在運行期才確定且可能有變動的,我們事先無法為每個Pod都確定唯一不變的ID。另外,為了能夠在其他節點上恢復某個失敗的節點,這種集群中的Pod需要掛接某種共享存儲,為了解決這個問題,Kubernetes從1.4版本開始引入了PetSet這個新的資源對象,并且在1.5版本時更名為StatefulSet,StatefulSet從本質上來說,可以看作Deployment/RC的一個特殊變種,它有如下特性。

◎ StatefulSet里的每個Pod都有穩定、唯一的網絡標識,可以用來發現集群內的其他成員。假設StatefulSet的名稱為kafka,那么第1個Pod叫kafka-0,第2個叫kafka-1,以此類推。

◎ StatefulSet控制的Pod副本的啟停順序是受控的,操作第n個Pod時,前n-1個Pod已經是運行且準備好的狀態。

◎ StatefulSet里的Pod采用穩定的持久化存儲卷,通過PV或PVC來實現,刪除Pod時默認不會刪除與StatefulSet相關的存儲卷(為了保證數據的安全)。

StatefulSet除了要與PV卷捆綁使用以存儲Pod的狀態數據,還要與Headless Service配合使用,即在每個StatefulSet定義中都要聲明它屬于哪個Headless Service。Headless Service與普通Service的關鍵區別在于,它沒有Cluster IP,如果解析Headless Service的DNS域名,則返回的是該Service對應的全部Pod的Endpoint列表。StatefulSet在Headless Service的基礎上又為StatefulSet控制的每個Pod實例都創建了一個DNS域名,這個域名的格式為:

        $(podname).$(headless service name)

比如一個3節點的Kafka的StatefulSet集群對應的Headless Service的名稱為kafka,StatefulSet的名稱為kafka,則StatefulSet里的3個Pod的DNS名稱分別為kafka-0.kafka、kafka-1.kafka、kafka-3.kafka,這些DNS名稱可以直接在集群的配置文件中固定下來。

1.4.9 Service

1.概述

Service服務也是Kubernetes里的核心資源對象之一,Kubernetes里的每個Service其實就是我們經常提起的微服務架構中的一個微服務,之前講解Pod、RC等資源對象其實都是為講解Kubernetes Service做鋪墊的。圖1.12顯示了Pod、RC與Service的邏輯關系。

圖1.12 Pod、RC與Service的邏輯關系

從圖1.12中可以看到,Kubernetes的Service定義了一個服務的訪問入口地址,前端的應用(Pod)通過這個入口地址訪問其背后的一組由Pod副本組成的集群實例,Service與其后端Pod副本集群之間則是通過Label Selector來實現無縫對接的。RC的作用實際上是保證Service的服務能力和服務質量始終符合預期標準。

通過分析、識別并建模系統中的所有服務為微服務——Kubernetes Service,我們的系統最終由多個提供不同業務能力而又彼此獨立的微服務單元組成的,服務之間通過TCP/IP進行通信,從而形成了強大而又靈活的彈性網格,擁有強大的分布式能力、彈性擴展能力、容錯能力,程序架構也變得簡單和直觀許多,如圖1.13所示。

圖1.13 Kubernetes提供的微服務網格架構

既然每個Pod都會被分配一個單獨的IP地址,而且每個Pod都提供了一個獨立的Endpoint(Pod IP+ContainerPort)以被客戶端訪問,現在多個Pod副本組成了一個集群來提供服務,那么客戶端如何來訪問它們呢?一般的做法是部署一個負載均衡器(軟件或硬件),為這組Pod開啟一個對外的服務端口如8000端口,并且將這些Pod的Endpoint列表加入8000端口的轉發列表,客戶端就可以通過負載均衡器的對外IP地址+服務端口來訪問此服務。客戶端的請求最后會被轉發到哪個Pod,由負載均衡器的算法所決定。

Kubernetes也遵循上述常規做法,運行在每個Node上的kube-proxy進程其實就是一個智能的軟件負載均衡器,負責把對Service的請求轉發到后端的某個Pod實例上,并在內部實現服務的負載均衡與會話保持機制。但Kubernetes發明了一種很巧妙又影響深遠的設計:Service沒有共用一個負載均衡器的IP地址,每個Service都被分配了一個全局唯一的虛擬IP地址,這個虛擬IP被稱為Cluster IP。這樣一來,每個服務就變成了具備唯一IP地址的通信節點,服務調用就變成了最基礎的TCP網絡通信問題。

我們知道,Pod的Endpoint地址會隨著Pod的銷毀和重新創建而發生改變,因為新Pod的IP地址與之前舊Pod的不同。而Service一旦被創建,Kubernetes就會自動為它分配一個可用的Cluster IP,而且在Service的整個生命周期內,它的Cluster IP不會發生改變。于是,服務發現這個棘手的問題在Kubernetes的架構里也得以輕松解決:只要用Service的Name與Service的Cluster IP地址做一個DNS域名映射即可完美解決問題。現在想想,這真是一個很棒的設計。

說了這么久,下面動手創建一個Service來加深對它的理解。創建一個名為tomcat-service.yaml的定義文件,內容如下:

        apiVersion: v1
        kind: Service
        metadata:
          name: tomcat-service
        spec:
          ports:
          - port: 8080
          selector:
            tier: frontend

上述內容定義了一個名為tomcat-service的Service,它的服務端口為8080,擁有“tier= frontend”這個Label的所有Pod實例都屬于它,運行下面的命令進行創建:

        #kubectl create -f tomcat-server.yaml
        service "tomcat-service" created

我們之前在tomcat-deployment.yaml里定義的Tomcat的Pod剛好擁有這個標簽,所以剛才創建的tomcat-service已經對應一個Pod實例,運行下面的命令可以查看tomcat-service的Endpoint列表,其中172.17.1.3是Pod的IP地址,端口8080是Container暴露的端口:

        # kubectl get endpoints
        NAME             ENDPOINTS             AGE
        kubernetes       192.168.18.131:6443   15d
        tomcat-service   172.17.1.3:8080       1m

你可能有疑問:“說好的Service的Cluster IP呢?怎么沒有看到?”運行下面的命令即可看到tomct-service被分配的Cluster IP及更多的信息:

        # kubectl get svc tomcat-service -o yaml
        apiVersion: v1
        kind: Service
        metadata:
          creationTimestamp: 2016-07-21T17:05:52Z
          name: tomcat-service
          namespace: default
          resourceVersion: "23964"
          selfLink: /api/v1/namespaces/default/services/tomcat-service
          uid: 61987d3c-4f65-11e6-a9d8-000c29ed42c1
        spec:
          clusterIP: 169.169.65.227
          ports:
          - port: 8080
            protocol: TCP
            targetPort: 8080
          selector:
            tier: frontend
          sessionAffinity: None
          type: ClusterIP
        status:
          loadBalancer: {}

在spec.ports的定義中,targetPort屬性用來確定提供該服務的容器所暴露(EXPOSE)的端口號,即具體業務進程在容器內的targetPort上提供TCP/IP接入;port屬性則定義了Service的虛端口。前面定義Tomcat服務時沒有指定targetPort,則默認targetPort與port相同。

接下來看看Service的多端口問題。

很多服務都存在多個端口的問題,通常一個端口提供業務服務,另外一個端口提供管理服務,比如Mycat、Codis等常見中間件。Kubernetes Service支持多個Endpoint,在存在多個Endpoint的情況下,要求每個Endpoint都定義一個名稱來區分。下面是Tomcat多端口的Service定義樣例:

        apiVersion: v1
        kind: Service
        metadata:
          name: tomcat-service
        spec:
          ports:
          - port: 8080
          name: service-port
          - port: 8005
          name: shutdown-port
          selector:
            tier: frontend

多端口為什么需要給每個端口都命名呢?這就涉及Kubernetes的服務發現機制了,接下來進行講解。

2.Kubernetes的服務發現機制

任何分布式系統都會涉及“服務發現”這個基礎問題,大部分分布式系統都通過提供特定的API接口來實現服務發現功能,但這樣做會導致平臺的侵入性比較強,也增加了開發、測試的難度。Kubernetes則采用了直觀樸素的思路去解決這個棘手的問題。

首先,每個Kubernetes中的Service都有唯一的Cluster IP及唯一的名稱,而名稱是由開發者自己定義的,部署時也沒必要改變,所以完全可以被固定在配置中。接下來的問題就是如何通過Service的名稱找到對應的Cluster IP。

最早時Kubernetes采用了Linux環境變量解決這個問題,即每個Service都生成一些對應的Linux環境變量(ENV),并在每個Pod的容器啟動時自動注入這些環境變量。以下是tomcat-service產生的環境變量條目:

        TOMCAT_SERVICE_SERVICE_HOST=169.169.41.218
        TOMCAT_SERVICE_SERVICE_PORT_SERVICE_PORT=8080
        TOMCAT_SERVICE_SERVICE_PORT_SHUTDOWN_PORT=8005
        TOMCAT_SERVICE_SERVICE_PORT=8080
        TOMCAT_SERVICE_PORT_8005_TCP_PORT=8005
        TOMCAT_SERVICE_PORT=tcp://169.169.41.218:8080
        TOMCAT_SERVICE_PORT_8080_TCP_ADDR=169.169.41.218
        TOMCAT_SERVICE_PORT_8080_TCP=tcp://169.169.41.218:8080
        TOMCAT_SERVICE_PORT_8080_TCP_PROTO=tcp
        TOMCAT_SERVICE_PORT_8080_TCP_PORT=8080
        TOMCAT_SERVICE_PORT_8005_TCP=tcp://169.169.41.218:8005
        TOMCAT_SERVICE_PORT_8005_TCP_ADDR=169.169.41.218
        TOMCAT_SERVICE_PORT_8005_TCP_PROTO=tcp

在上述環境變量中,比較重要的是前3個環境變量。可以看到,每個Service的IP地址及端口都有標準的命名規范,遵循這個命名規范,就可以通過代碼訪問系統環境變量來得到所需的信息,實現服務調用。

考慮到通過環境變量獲取Service地址的方式仍然不太方便、不夠直觀,后來Kubernetes通過Add-On增值包引入了DNS系統,把服務名作為DNS域名,這樣程序就可以直接使用服務名來建立通信連接了。目前,Kubernetes上的大部分應用都已經采用了DNS這種新興的服務發現機制,后面會講解如何部署DNS系統。

3.外部系統訪問Service的問題

為了更深入地理解和掌握Kubernetes,我們需要弄明白Kubernetes里的3種IP,這3種IP分別如下。

◎ Node IP:Node的IP地址。

◎ Pod IP:Pod的IP地址。

◎ Cluster IP:Service的IP地址。

首先,Node IP是Kubernetes集群中每個節點的物理網卡的IP地址,是一個真實存在的物理網絡,所有屬于這個網絡的服務器都能通過這個網絡直接通信,不管其中是否有部分節點不屬于這個Kubernetes集群。這也表明在Kubernetes集群之外的節點訪問Kubernetes集群之內的某個節點或者TCP/IP服務時,都必須通過Node IP通信。

其次,Pod IP是每個Pod的IP地址,它是Docker Engine根據docker0網橋的IP地址段進行分配的,通常是一個虛擬的二層網絡,前面說過,Kubernetes要求位于不同Node上的Pod都能夠彼此直接通信,所以Kubernetes里一個Pod里的容器訪問另外一個Pod里的容器時,就是通過Pod IP所在的虛擬二層網絡進行通信的,而真實的TCP/IP流量是通過Node IP所在的物理網卡流出的。

最后說說Service的Cluster IP,它也是一種虛擬的IP,但更像一個“偽造”的IP網絡,原因有以下幾點。

◎ Cluster IP僅僅作用于Kubernetes Service這個對象,并由Kubernetes管理和分配IP地址(來源于Cluster IP地址池)。

◎ Cluster IP無法被Ping,因為沒有一個“實體網絡對象”來響應。

◎ Cluster IP只能結合Service Port組成一個具體的通信端口,單獨的Cluster IP不具備TCP/IP通信的基礎,并且它們屬于Kubernetes集群這樣一個封閉的空間,集群外的節點如果要訪問這個通信端口,則需要做一些額外的工作。

◎ 在Kubernetes集群內,Node IP網、Pod IP網與Cluster IP網之間的通信,采用的是Kubernetes自己設計的一種編程方式的特殊路由規則,與我們熟知的IP路由有很大的不同。

根據上面的分析和總結,我們基本明白了:Service的Cluster IP屬于Kubernetes集群內部的地址,無法在集群外部直接使用這個地址。那么矛盾來了:實際上在我們開發的業務系統中肯定多少有一部分服務是要提供給Kubernetes集群外部的應用或者用戶來使用的,典型的例子就是Web端的服務模塊,比如上面的tomcat-service,那么用戶怎么訪問它?

采用NodePort是解決上述問題的最直接、有效的常見做法。以tomcat-service為例,在Service的定義里做如下擴展即可(見代碼中的粗體部分):

        apiVersion: v1
        kind: Service
        metadata:
          name: tomcat-service
        spec:
         type: NodePort
         ports:
          - port: 8080
            nodePort: 31002
         selector:
            tier: frontend

其中,nodePort:31002這個屬性表明手動指定tomcat-service的NodePort為31002,否則Kubernetes會自動分配一個可用的端口。接下來在瀏覽器里訪問http://<nodePort IP>:31002/,就可以看到Tomcat的歡迎界面了,如圖1.14所示。

圖1.14 通過NodePort訪問Service

NodePort的實現方式是在Kubernetes集群里的每個Node上都為需要外部訪問的Service開啟一個對應的TCP監聽端口,外部系統只要用任意一個Node的IP地址+具體的NodePort端口號即可訪問此服務,在任意Node上運行netstat命令,就可以看到有NodePort端口被監聽:

        # netstat -tlp | grep 31002
        tcp6  0   0 [::]:31002         [::]:*               LISTEN      1125/kube-proxy

但NodePort還沒有完全解決外部訪問Service的所有問題,比如負載均衡問題。假如在我們的集群中有10個Node,則此時最好有一個負載均衡器,外部的請求只需訪問此負載均衡器的IP地址,由負載均衡器負責轉發流量到后面某個Node的NodePort上,如圖1.15所示。

圖1.15 NodePort與Load balancer

圖1.15中的Load balancer組件獨立于Kubernetes集群之外,通常是一個硬件的負載均衡器,或者是以軟件方式實現的,例如HAProxy或者Nginx。對于每個Service,我們通常需要配置一個對應的Load balancer實例來轉發流量到后端的Node上,這的確增加了工作量及出錯的概率。于是Kubernetes提供了自動化的解決方案,如果我們的集群運行在谷歌的公有云GCE上,那么只要把Service的type=NodePort改為type=LoadBalancer,Kubernetes就會自動創建一個對應的Load balancer實例并返回它的IP地址供外部客戶端使用。其他公有云提供商只要實現了支持此特性的驅動,則也可以達到上述目的。此外,裸機上的類似機制(Bare Metal Service Load Balancers)也在被開發。

1.4.10 Job

批處理任務通常并行(或者串行)啟動多個計算進程去處理一批工作項(work item),在處理完成后,整個批處理任務結束。從1.2版本開始,Kubernetes支持批處理類型的應用,我們可以通過Kubernetes Job這種新的資源對象定義并啟動一個批處理任務Job。與RC、Deployment、ReplicaSet、DaemonSet類似,Job也控制一組Pod容器。從這個角度來看,Job也是一種特殊的Pod副本自動控制器,同時Job控制Pod副本與RC等控制器的工作機制有以下重要差別。

(1)Job所控制的Pod副本是短暫運行的,可以將其視為一組Docker容器,其中的每個Docker容器都僅僅運行一次。當Job控制的所有Pod副本都運行結束時,對應的Job也就結束了。Job在實現方式上與RC等副本控制器不同,Job生成的Pod副本是不能自動重啟的,對應Pod副本的RestartPoliy都被設置為Never。因此,當對應的Pod副本都執行完成時,相應的Job也就完成了控制使命,即Job生成的Pod在Kubernetes中是短暫存在的。Kubernetes在1.5版本之后又提供了類似crontab的定時任務——CronJob,解決了某些批處理任務需要定時反復執行的問題。

(2)Job所控制的Pod副本的工作模式能夠多實例并行計算,以TensorFlow框架為例,可以將一個機器學習的計算任務分布到10臺機器上,在每臺機器上都運行一個worker執行計算任務,這很適合通過Job生成10個Pod副本同時啟動運算。

在第3章會繼續深入講解Job的實現原理及對應的案例。

1.4.11 Volume

Volume(存儲卷)是Pod中能夠被多個容器訪問的共享目錄。Kubernetes的Volume概念、用途和目的與Docker的Volume比較類似,但兩者不能等價。首先,Kubernetes中的Volume被定義在Pod上,然后被一個Pod里的多個容器掛載到具體的文件目錄下;其次,Kubernetes中的Volume與Pod的生命周期相同,但與容器的生命周期不相關,當容器終止或者重啟時,Volume中的數據也不會丟失。最后,Kubernetes支持多種類型的Volume,例如GlusterFS、Ceph等先進的分布式文件系統。

Volume的使用也比較簡單,在大多數情況下,我們先在Pod上聲明一個Volume,然后在容器里引用該Volume并掛載(Mount)到容器里的某個目錄上。舉例來說,我們要給之前的Tomcat Pod增加一個名為datavol的Volume,并且掛載到容器的/mydata-data目錄上,則只要對Pod的定義文件做如下修正即可(注意代碼中的粗體部分):

          template:
            metadata:
              labels:
                app: app-demo
                tier: frontend
            spec:
              volumes:
                - name: datavol
                emptyDir: {}
              containers:
              - name: tomcat-demo
                image: tomcat
                volumeMounts:
                - mountPath: /mydata-data
                  name: datavol
                imagePullPolicy: IfNotPresent

除了可以讓一個Pod里的多個容器共享文件、讓容器的數據寫到宿主機的磁盤上或者寫文件到網絡存儲中,Kubernetes的Volume還擴展出了一種非常有實用價值的功能,即容器配置文件集中化定義與管理,這是通過ConfigMap這種新的資源對象來實現的,后面會詳細說明。

Kubernetes提供了非常豐富的Volume類型,下面逐一進行說明。

1.emptyDir

一個emptyDir Volume是在Pod分配到Node時創建的。從它的名稱就可以看出,它的初始內容為空,并且無須指定宿主機上對應的目錄文件,因為這是Kubernetes自動分配的一個目錄,當Pod從Node上移除時,emptyDir中的數據也會被永久刪除。emptyDir的一些用途如下。

◎ 臨時空間,例如用于某些應用程序運行時所需的臨時目錄,且無須永久保留。

◎ 長時間任務的中間過程CheckPoint的臨時保存目錄。

◎ 一個容器需要從另一個容器中獲取數據的目錄(多容器共享目錄)。

目前,用戶無法控制emptyDir使用的介質種類。如果kubelet的配置是使用硬盤,那么所有emptyDir都將被創建在該硬盤上。Pod在將來可以設置emptyDir是位于硬盤、固態硬盤上還是基于內存的tmpfs上,上面的例子便采用了emptyDir類的Volume。

2.hostPath

hostPath為在Pod上掛載宿主機上的文件或目錄,它通常可以用于以下幾方面。

◎ 容器應用程序生成的日志文件需要永久保存時,可以使用宿主機的高速文件系統進行存儲。

◎ 需要訪問宿主機上Docker引擎內部數據結構的容器應用時,可以通過定義hostPath為宿主機/var/lib/docker目錄,使容器內部應用可以直接訪問Docker的文件系統。

在使用這種類型的Volume時,需要注意以下幾點。

◎ 在不同的Node上具有相同配置的Pod,可能會因為宿主機上的目錄和文件不同而導致對Volume上目錄和文件的訪問結果不一致。

◎ 如果使用了資源配額管理,則Kubernetes無法將hostPath在宿主機上使用的資源納入管理。

在下面的例子中使用宿主機的/data目錄定義了一個hostPath類型的Volume:

              volumes:
              - name: "persistent-storage"
                hostPath:
                  path: "/data"

3.gcePersistentDisk

使用這種類型的Volume表示使用谷歌公有云提供的永久磁盤(Persistent Disk,PD)存放Volume的數據,它與emptyDir不同,PD上的內容會被永久保存,當Pod被刪除時,PD只是被卸載(Unmount),但不會被刪除。需要注意的是,你需要先創建一個PD,才能使用gcePersistentDisk。

使用gcePersistentDisk時有以下一些限制條件。

◎ Node(運行kubelet的節點)需要是GCE虛擬機。

◎ 這些虛擬機需要與PD存在于相同的GCE項目和Zone中。

通過gcloud命令即可創建一個PD:

        gcloud compute disks create --size=500GB --zone=us-central1-a my-data-disk

定義gcePersistentDisk類型的Volume的示例如下:

          volumes:
          - name: test-volume
            # This GCE PD must already exist.
            gcePersistentDisk:
              pdName: my-data-disk
              fsType: ext4

4.awsElasticBlockStore

與GCE類似,該類型的Volume使用亞馬遜公有云提供的EBS Volume存儲數據,需要先創建一個EBS Volume才能使用awsElasticBlockStore。

使用awsElasticBlockStore的一些限制條件如下。

◎ Node(運行kubelet的節點)需要是AWS EC2實例。

◎ 這些AWS EC2實例需要與EBS Volume存在于相同的region和availability-zone中。

◎ EBS只支持單個EC2實例掛載一個Volume。

通過aws ec2 create-volume命令可以創建一個EBS Volume:

        aws ec2 create-volume --availability-zone eu-west-1a --size 10--volume-type gp2

定義awsElasticBlockStore類型的Volume的示例如下:

          volumes:
          - name: test-volume
            # This AWS EBS volume must already exist.
            awsElasticBlockStore:
              volumeID: aws://<availability-zone>/<volume-id>
              fsType: ext4

5.NFS

使用NFS網絡文件系統提供的共享目錄存儲數據時,我們需要在系統中部署一個NFS Server。定義NFS類型的Volume的示例如下:

          volumes:
            - name: nfs
                nfs:
                  # 改為你的NFS服務器地址
                  server: nfs-server.localhost
                  path: "/"

6.其他類型的Volume

◎ iscsi:使用iSCSI存儲設備上的目錄掛載到Pod中。

◎ flocker:使用Flocker管理存儲卷。

◎ glusterfs:使用開源GlusterFS網絡文件系統的目錄掛載到Pod中。

◎ rbd:使用Ceph塊設備共享存儲(Rados Block Device)掛載到Pod中。

◎ gitRepo:通過掛載一個空目錄,并從Git庫clone一個git repository以供Pod使用。

◎ secret:一個Secret Volume用于為Pod提供加密的信息,你可以將定義在Kubernetes中的Secret直接掛載為文件讓Pod訪問。Secret Volume是通過TMFS(內存文件系統)實現的,這種類型的Volume總是不會被持久化的。

1.4.12 Persistent Volume

之前提到的Volume是被定義在Pod上的,屬于計算資源的一部分,而實際上,網絡存儲是相對獨立于計算資源而存在的一種實體資源。比如在使用虛擬機的情況下,我們通常會先定義一個網絡存儲,然后從中劃出一個“網盤”并掛接到虛擬機上。Persistent Volume(PV)和與之相關聯的Persistent Volume Claim(PVC)也起到了類似的作用。

PV可以被理解成Kubernetes集群中的某個網絡存儲對應的一塊存儲,它與Volume類似,但有以下區別。

◎ PV只能是網絡存儲,不屬于任何Node,但可以在每個Node上訪問。

◎ PV并不是被定義在Pod上的,而是獨立于Pod之外定義的。

◎ PV目前支持的類型包括:gcePersistentDisk、AWSElasticBlockStore、AzureFile、AzureDisk、FC(Fibre Channel)、Flocker、NFS、iSCSI、RBD(Rados Block Device)、CephFS、Cinder、GlusterFS、VsphereVolume、Quobyte Volumes、VMware Photon、Portworx Volumes、ScaleIO Volumes和HostPath(僅供單機測試)。

下面給出了NFS類型的PV的一個YAML定義文件,聲明了需要5Gi的存儲空間:

          apiVersion: v1
          kind: PersistentVolume
        metadata:
          name: pv0003
        spec:
          capacity:
            storage: 5Gi
          accessModes:
            - ReadWriteOnce
          nfs:
            path: /somepath
            server: 172.17.0.2

比較重要的是PV的accessModes屬性,目前有以下類型。

◎ ReadWriteOnce:讀寫權限,并且只能被單個Node掛載。

◎ ReadOnlyMany:只讀權限,允許被多個Node掛載。

◎ ReadWriteMany:讀寫權限,允許被多個Node掛載。

如果某個Pod想申請某種類型的PV,則首先需要定義一個PersistentVolumeClaim對象:

        kind: PersistentVolumeClaim
        apiVersion: v1
        metadata:
          name: myclaim
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 8Gi

然后,在Pod的Volume定義中引用上述PVC即可:

        volumes:
            - name: mypd
              persistentVolumeClaim:
              claimName: myclaim

最后說說PV的狀態。PV是有狀態的對象,它的狀態有以下幾種。

◎ Available:空閑狀態。

◎ Bound:已經綁定到某個PVC上。

◎ Released:對應的PVC已經被刪除,但資源還沒有被集群收回。

◎ Failed:PV自動回收失敗。

共享存儲的原理解析和實踐指南詳見第8章的說明。

1.4.13 Namespace

Namespace(命名空間)是Kubernetes系統中的另一個非常重要的概念,Namespace在很多情況下用于實現多租戶的資源隔離。Namespace通過將集群內部的資源對象“分配”到不同的Namespace中,形成邏輯上分組的不同項目、小組或用戶組,便于不同的分組在共享使用整個集群的資源的同時還能被分別管理。

Kubernetes集群在啟動后會創建一個名為default的Namespace,通過kubectl可以查看:

        $ kubectl get namespaces
        NAME          LABELS             STATUS
        default       <none>             Active

接下來,如果不特別指明Namespace,則用戶創建的Pod、RC、Service都將被系統創建到這個默認的名為default的Namespace中。

Namespace的定義很簡單。如下所示的YAML定義了名為development的Namespace。

        apiVersion: v1
        kind: Namespace
        metadata:
          name: development

一旦創建了Namespace,我們在創建資源對象時就可以指定這個資源對象屬于哪個Namespace。比如在下面的例子中定義了一個名為busybox的Pod,并將其放入development這個Namespace里:

        apiVersion: v1
        kind: Pod
        metadata:
          name: busybox
          namespace: development
        spec:
          containers:
          - image: busybox
            command:
              - sleep
              - "3600"
            name: busybox

此時使用kubectl get命令查看,將無法顯示:

        $ kubectl get pods
        NAME      READY     STATUS    RESTARTS   AGE

這是因為如果不加參數,則kubectl get命令將僅顯示屬于default命名空間的資源對象。

可以在kubectl命令中加入--namespace參數來查看某個命名空間中的對象:

        # kubectl get pods --namespace=development
        NAME          READY     STATUS    RESTARTS   AGE
        busybox       1/1       Running   0          1m

當給每個租戶創建一個Namespace來實現多租戶的資源隔離時,還能結合Kubernetes的資源配額管理,限定不同租戶能占用的資源,例如CPU使用量、內存使用量等。關于資源配額管理的問題,在后面的章節中會詳細介紹。

1.4.14 Annotation

Annotation(注解)與Label類似,也使用key/value鍵值對的形式進行定義。不同的是Label具有嚴格的命名規則,它定義的是Kubernetes對象的元數據(Metadata),并且用于Label Selector。Annotation則是用戶任意定義的附加信息,以便于外部工具查找。在很多時候,Kubernetes的模塊自身會通過Annotation標記資源對象的一些特殊信息。

通常來說,用Annotation來記錄的信息如下。

◎ build信息、release信息、Docker鏡像信息等,例如時間戳、release id號、PR號、鏡像Hash值、Docker Registry地址等。

◎ 日志庫、監控庫、分析庫等資源庫的地址信息。

◎ 程序調試工具信息,例如工具名稱、版本號等。

◎ 團隊的聯系信息,例如電話號碼、負責人名稱、網址等。

1.4.15 ConfigMap

為了能夠準確和深刻理解Kubernetes ConfigMap的功能和價值,我們需要從Docker說起。我們知道,Docker通過將程序、依賴庫、數據及配置文件“打包固化”到一個不變的鏡像文件中的做法,解決了應用的部署的難題,但這同時帶來了棘手的問題,即配置文件中的參數在運行期如何修改的問題。我們不可能在啟動Docker容器后再修改容器里的配置文件,然后用新的配置文件重啟容器里的用戶主進程。為了解決這個問題,Docker提供了兩種方式:

◎ 在運行時通過容器的環境變量來傳遞參數;

◎ 通過Docker Volume將容器外的配置文件映射到容器內。

這兩種方式都有其優勢和缺點,在大多數情況下,后一種方式更合適我們的系統,因為大多數應用通常從一個或多個配置文件中讀取參數。但這種方式也有明顯的缺陷:我們必須在目標主機上先創建好對應的配置文件,然后才能映射到容器里。

上述缺陷在分布式情況下變得更為嚴重,因為無論采用哪種方式,寫入(修改)多臺服務器上的某個指定文件,并確保這些文件保持一致,都是一個很難完成的目標。此外,在大多數情況下,我們都希望能集中管理系統的配置參數,而不是管理一堆配置文件。針對上述問題,Kubernetes給出了一個很巧妙的設計實現,如下所述。

首先,把所有的配置項都當作key-value字符串,當然value可以來自某個文本文件,比如配置項password=123456、user=root、host=192.168.8.4用于表示連接FTP服務器的配置參數。這些配置項可以作為Map表中的一個項,整個Map的數據可以被持久化存儲在Kubernetes的Etcd數據庫中,然后提供API以方便Kubernetes相關組件或客戶應用CRUD操作這些數據,上述專門用來保存配置參數的Map就是Kubernetes ConfigMap資源對象。

接下來,Kubernetes提供了一種內建機制,將存儲在etcd中的ConfigMap通過Volume映射的方式變成目標Pod內的配置文件,不管目標Pod被調度到哪臺服務器上,都會完成自動映射。進一步地,如果ConfigMap中的key-value數據被修改,則映射到Pod中的“配置文件”也會隨之自動更新。于是,Kubernetes ConfigMap就成了分布式系統中最為簡單(使用方法簡單,但背后實現比較復雜)且對應用無侵入的配置中心。ConfigMap配置集中化的一種簡單方案如圖1.16所示。

圖1.16 ConfigMap配置集中化的一種簡單方案

1.4.16 小結

上述這些組件是Kubernetes系統的核心組件,它們共同構成了Kubernetes系統的框架和計算模型。通過對它們進行靈活組合,用戶就可以快速、方便地對容器集群進行配置、創建和管理。除了本章所介紹的核心組件,在Kubernetes系統中還有許多輔助配置的資源對象,例如LimitRange、ResourceQuota。另外,一些系統內部使用的對象Binding、Event等請參考Kubernetes的API文檔。

在第2章中,我們將深入實踐并全面掌握Kubernetes的各種使用技巧。

主站蜘蛛池模板: 防城港市| 万荣县| 浦江县| 阿巴嘎旗| 池州市| 廉江市| 金塔县| 正宁县| 皋兰县| 浦县| 呼图壁县| 柞水县| 昭平县| 阿合奇县| 灌云县| 青冈县| 陵川县| 水城县| 兰西县| 楚雄市| 惠安县| 贵德县| 保靖县| 介休市| 靖宇县| 雷波县| 庆安县| 武宣县| 峨眉山市| 宜州市| 尼玛县| 霍邱县| 永城市| 北安市| 宜昌市| 道孚县| 洮南市| 常州市| 汨罗市| 贵德县| 沁水县|