k8s 深入篇———— 守護容器[九]

前言

守護容器,也叫做deamonset, 只做整理

正文

顧名思義,DaemonSet 的主要作用,是讓你在 Kubernetes 集羣裏,運行一個 Daemon Pod。 所以,這個 Pod 有如下三個特徵:

這個 Pod 運行在 Kubernetes 集羣裏的每一個節點(Node)上;

每個節點上只有一個這樣的 Pod 實例;

當有新的節點加入 Kubernetes 集羣后,該 Pod 會自動地在新節點上被創建出來;而當舊節點被刪除後,它上面的 Pod 也相應地會被回收掉。

這個機制聽起來很簡單,但 Daemon Pod 的意義確實是非常重要的。我隨便給你列舉幾個例子:

  1. 各種網絡插件的 Agent 組件,都必須運行在每一個節點上,用來處理這個節點上的容器網絡;

  2. 各種存儲插件的 Agent 組件,也必須運行在每一個節點上,用來在這個節點上掛載遠程存儲目錄,操作容器的 Volume 目錄;

  3. 各種監控組件和日誌組件,也必須運行在每一個節點上,負責這個節點上的監控信息和日誌蒐集。

更重要的是,跟其他編排對象不一樣,DaemonSet 開始運行的時機,很多時候比整個 Kubernetes 集羣出現的時機都要早。

這個乍一聽起來可能有點兒奇怪。但其實你來想一下:如果這個 DaemonSet 正是一個網絡插件的 Agent 組件呢?

這個時候,整個 Kubernetes 集羣裏還沒有可用的容器網絡,所有 Worker 節點的狀態都是 NotReady(NetworkReady=false)。這種情況下,普通的 Pod 肯定不能運行在這個集羣上。所以,這也就意味着 DaemonSet 的設計,必須要有某種“過人之處”纔行。

爲了弄清楚 DaemonSet 的工作原理,我們還是按照老規矩,先從它的 API 對象的定義說起。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd-elasticsearch
        image: k8s.gcr.io/fluentd-elasticsearch:1.20
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers

這個 DaemonSet,管理的是一個 fluentd-elasticsearch 鏡像的 Pod。這個鏡像的功能非常實用:通過 fluentd 將 Docker 容器裏的日誌轉發到 ElasticSearch 中。

可以看到,DaemonSet 跟 Deployment 其實非常相似,只不過是沒有 replicas 字段;它也使用 selector 選擇管理所有攜帶了 name=fluentd-elasticsearch 標籤的 Pod。

而這些 Pod 的模板,也是用 template 字段定義的。在這個字段中,我們定義了一個使用 fluentd-elasticsearch:1.20 鏡像的容器,而且這個容器掛載了兩個 hostPath 類型的 Volume,分別對應宿主機的 /var/log 目錄和 /var/lib/docker/containers 目錄。

顯然,fluentd 啓動之後,它會從這兩個目錄裏蒐集日誌信息,並轉發給 ElasticSearch 保存。這樣,我們通過 ElasticSearch 就可以很方便地檢索這些日誌了。

需要注意的是,Docker 容器裏應用的日誌,默認會保存在宿主機的 /var/lib/docker/containers/{{. 容器 ID}}/{{. 容器 ID}}-json.log 文件裏,所以這個目錄正是 fluentd 的蒐集目標。

那麼,DaemonSet 又是如何保證每個 Node 上有且只有一個被管理的 Pod 呢?

顯然,這是一個典型的“控制器模型”能夠處理的問題。

DaemonSet Controller,首先從 Etcd 裏獲取所有的 Node 列表,然後遍歷所有的 Node。這時,它就可以很容易地去檢查,當前這個 Node 上是不是有一個攜帶了 name=fluentd-elasticsearch 標籤的 Pod 在運行。

而檢查的結果,可能有這麼三種情況:

  1. 沒有這種 Pod,那麼就意味着要在這個 Node 上創建這樣一個 Pod;

  2. 有這種 Pod,但是數量大於 1,那就說明要把多餘的 Pod 從這個 Node 上刪除掉;

  3. 正好只有一個這種 Pod,那說明這個節點是正常的。

其中,刪除節點(Node)上多餘的 Pod 非常簡單,直接調用 Kubernetes API 就可以了。

但是,如何在指定的 Node 上創建新 Pod 呢?

如果你已經熟悉了 Pod API 對象的話,那一定可以立刻說出答案:用 nodeSelector,選擇 Node 的名字即可。

nodeSelector:
    name: <Node 名字 >

沒錯。

不過,在 Kubernetes 項目裏,nodeSelector 其實已經是一個將要被廢棄的字段了。因爲,現在有了一個新的、功能更完善的字段可以代替它,即:nodeAffinity。我來舉個例子:

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: metadata.name
            operator: In
            values:
            - node-geektime

在這個 Pod 裏,我聲明瞭一個 spec.affinity 字段,然後定義了一個 nodeAffinity。其中,spec.affinity 字段,是 Pod 裏跟調度相關的一個字段。關於它的完整內容,我會在講解調度策略的時候再詳細闡述。

而在這裏,我定義的 nodeAffinity 的含義是:

  1. requiredDuringSchedulingIgnoredDuringExecution:它的意思是說,這個 nodeAffinity 必須在每次調度的時候予以考慮。同時,這也意味着你可以設置在某些情況下不考慮這個 nodeAffinity;

  2. 這個 Pod,將來只允許運行在“metadata.name”是“node-geektime”的節點上。

所以,我們的 DaemonSet Controller 會在創建 Pod 的時候,自動在這個 Pod 的 API 對象裏,加上這樣一個 nodeAffinity 定義。其中,需要綁定的節點名字,正是當前正在遍歷的這個 Node。

此外,DaemonSet 還會給這個 Pod 自動加上另外一個與調度相關的字段,叫作 tolerations。這個字段意味着這個 Pod,會“容忍”(Toleration)某些 Node 的“污點”(Taint)。

而 DaemonSet 自動加上的 tolerations 字段,格式如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: with-toleration
spec:
  tolerations:
  - key: node.kubernetes.io/unschedulable
    operator: Exists
    effect: NoSchedule

這個 Toleration 的含義是:“容忍”所有被標記爲 unschedulable“污點”的 Node;“容忍”的效果是允許調度。

而在正常情況下,被標記了 unschedulable“污點”的 Node,是不會有任何 Pod 被調度上去的(effect: NoSchedule)。可是,DaemonSet 自動地給被管理的 Pod 加上了這個特殊的 Toleration,就使得這些 Pod 可以忽略這個限制,繼而保證每個節點上都會被調度一個 Pod。當然,如果這個節點有故障的話,這個 Pod 可能會啓動失敗,而 DaemonSet 則會始終嘗試下去,直到 Pod 啓動成功。

假如當前 DaemonSet 管理的,是一個網絡插件的 Agent Pod,那麼你就必須在這個 DaemonSet 的 YAML 文件裏,給它的 Pod 模板加上一個能夠“容忍”node.kubernetes.io/network-unavailable“污點”的 Toleration。正如下面這個例子所示:

template:
    metadata:
      labels:
        name: network-plugin-agent
    spec:
      tolerations:
      - key: node.kubernetes.io/network-unavailable
        operator: Exists
        effect: NoSchedule

在 Kubernetes 項目中,當一個節點的網絡插件尚未安裝時,這個節點就會被自動加上名爲node.kubernetes.io/network-unavailable的“污點”。

而通過這樣一個 Toleration,調度器在調度這個 Pod 的時候,就會忽略當前節點上的“污點”,從而成功地將網絡插件的 Agent 組件調度到這臺機器上啓動起來。

這種機制,正是我們在部署 Kubernetes 集羣的時候,能夠先部署 Kubernetes 本身、再部署網絡插件的根本原因:因爲當時我們所創建的 Weave 的 YAML,實際上就是一個 DaemonSet。

至此,通過上面這些內容,你應該能夠明白,DaemonSet 其實是一個非常簡單的控制器。在它的控制循環中,只需要遍歷所有節點,然後根據節點上是否有被管理 Pod 的情況,來決定是否要創建或者刪除一個 Pod。

只不過,在創建每個 Pod 的時候,DaemonSet 會自動給這個 Pod 加上一個 nodeAffinity,從而保證這個 Pod 只會在指定節點上啓動。同時,它還會自動給這個 Pod 加上一個 Toleration,從而忽略節點的 unschedulable“污點”。

當然,你也可以在 Pod 模板里加上更多種類的 Toleration,從而利用 DaemonSet 實現自己的目的。比如,在這個 fluentd-elasticsearch DaemonSet 裏,我就給它加上了這樣的 Toleration:

tolerations:
- key: node-role.kubernetes.io/master
  effect: NoSchedule

這是因爲在默認情況下,Kubernetes 集羣不允許用戶在 Master 節點部署 Pod。因爲,Master 節點默認攜帶了一個叫作node-role.kubernetes.io/master的“污點”。所以,爲了能在 Master 節點上部署 DaemonSet 的 Pod,我就必須讓這個 Pod“容忍”這個“污點”。

kubectl create -f fluentd-elasticsearch.yaml

創建一下。

需要注意的是,在 DaemonSet 上,我們一般都應該加上 resources 字段,來限制它的 CPU 和內存使用,防止它佔用過多的宿主機資源。

而創建成功後,你就能看到,如果有 N 個節點,就會有 N 個 fluentd-elasticsearch Pod 在運行。比如在我們的例子裏,會有1個 Pod,如下所示:

因爲我只弄了一個主節點

而如果你此時通過 kubectl get 查看一下 Kubernetes 集羣裏的 DaemonSet 對象:

就會發現 DaemonSet 和 Deployment 一樣,也有 DESIRED、CURRENT 等多個狀態字段。這也就意味着,DaemonSet 可以像 Deployment 那樣,進行版本管理。這個版本,可以使用 kubectl rollout history 看到:

kubectl rollout history daemonset fluentd-elasticsearch -n kube-system

在 Kubernetes 項目中,任何你覺得需要記錄下來的狀態,都可以被用 API 對象的方式實現。當然,“版本”也不例外。

Kubernetes v1.7 之後添加了一個 API 對象,名叫ControllerRevision,專門用來記錄某種 Controller 對象的版本。比如,你可以通過如下命令查看 fluentd-elasticsearch 對應的 ControllerRevision:

kubectl describe controllerrevision fluentd-elasticsearch-64dc6799c9 -n kube-system

就會看到,這個 ControllerRevision 對象,實際上是在 Data 字段保存了該版本對應的完整的 DaemonSet 的 API 對象。並且,在 Annotation 字段保存了創建這個對象所使用的 kubectl 命令。

接下來,我們可以嘗試將這個 DaemonSet 回滾到 Revision=1 時的狀態:

 kubectl rollout undo daemonset fluentd-elasticsearch --to-revision=1 -n kube-system

這個 kubectl rollout undo 操作,實際上相當於讀取到了 Revision=1 的 ControllerRevision 對象保存的 Data 字段。而這個 Data 字段裏保存的信息,就是 Revision=1 時這個 DaemonSet 的完整 API 對象。

所以,現在 DaemonSet Controller 就可以使用這個歷史 API 對象,對現有的 DaemonSet 做一次 PATCH 操作(等價於執行一次 kubectl apply -f “舊的 DaemonSet 對象”),從而把這個 DaemonSet“更新”到一箇舊版本。

這也是爲什麼,在執行完這次回滾完成後,你會發現,DaemonSet 的 Revision 並不會從 Revision=2 退回到 1,而是會增加成 Revision=3。這是因爲,一個新的 ControllerRevision 被創建了出來。

ControllerRevision 和 ReplicaSet 是 Kubernetes 中的兩個概念,它們之間有以下區別:

1. 功能:ControllerRevision 是一個記錄控制器版本的資源對象,用於實現有狀態應用程序的滾動更新。ReplicaSet 是一個控制器,用於管理 Pod 副本的創建、刪除和擴縮容。

2. 對象類型:ControllerRevision 是一個自定義資源定義(Custom Resource Definition,CRD)對象,用於記錄控制器的版本信息。ReplicaSet 是 Kubernetes 內置的資源對象,用於定義 Pod 副本集。

3. 控制器類型:ControllerRevision 用於有狀態應用程序的滾動更新,例如 StatefulSet 控制器。ReplicaSet 用於無狀態應用程序的副本管理,例如 Deployment 控制器。

4. 更新方式:ControllerRevision 通過記錄每個版本的控制器模板和副本數來實現有狀態應用程序的滾動更新。ReplicaSet 通過指定 Pod 模板和副本數量來創建和管理 Pod 副本。

5. 生命週期:ControllerRevision 的生命週期與控制器的滾動更新策略有關,它會記錄每次控制器模板的變化。ReplicaSet 的生命週期與 Pod 副本集的創建和刪除有關,它會根據副本數量的變化來創建或刪除 Pod。

總的來說,ControllerRevision 是一個記錄控制器版本的資源對象,用於實現有狀態應用程序的滾動更新,而 ReplicaSet 是一個控制器,用於管理 Pod 副本的創建、刪除和擴縮容。

下一節job 和 cronjob。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章