一、簡介
DaemonSet 確保所有(或者某些)節點上運行一個 Pod 的副本。當有節點加入集羣時, 也會爲他們新增一個 Pod 。當有節點從集羣移除時,這些 Pod 也會被回收。刪除 DaemonSet 將會刪除它建立的全部 Pod。node
DaemonSet 的一些典型用法:python
在每一個節點上運行集羣存守護進程。例如 glusterd、cephweb
在每一個節點上運行日誌收集守護進程。例如 fluentd、logstashdocker
在每一個節點上運行監控守護進程。例如 Prometheus Node Exporter、Sysdig Agent、collectd、Dynatrace OneAgent、APPDynamics Agent、Datadog agent、New Relic agent、Ganglia gmond、Instana Agent 等shell
一種簡單的用法是爲每種類型的守護進程在全部的節點上都啓動一個 DaemonSet。一個稍微複雜的用法是爲同一種守護進程部署多個 DaemonSet;每一個具備不一樣的標誌, 而且對不一樣硬件類型具備不一樣的內存、CPU 要求。vim
二、建立DaemonSet
Google Cloud 的 Kubernetes 集羣就會在全部的節點上啓動 fluentd 和 Prometheus 來收集節點上的日誌和監控數據,想要建立用於日誌收集的守護進程其實很是簡單,咱們可使用以下所示的代碼:centos
[root@yygh-de ~]# vim DaemonSet.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-elasticsearch
namespace: kube-system
spec:
selector:
matchLabels:
name: fluentd-elasticsearch
template:
metadata:
labels:
name: fluentd-elasticsearch
spec:
containers:
- name: fluentd-elasticsearch
image: k8s.gcr.io/fluentd-elasticsearch:1.20
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
當咱們使用 kubectl apply -f
建立上述的 DaemonSet 時,它會在 Kubernetes 集羣的 kube-system
命名空間中建立 DaemonSet 資源並在全部的節點上建立新的 Pod:api
[root@yygh-de ~]# kubectl get daemonsets.apps fluentd-elasticsearch --namespace kube-system
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
fluentd-elasticsearch 1 1 1 1 1 <none> 19h
[root@yygh-de ~]# kubectl get pods --namespace kube-system --label name=fluentd-elasticsearch
NAME READY STATUS RESTARTS AGE
fluentd-elasticsearch-kvtwj 1/1 Running 0 19h
因爲集羣中只存在一個 Pod,因此 Kubernetes 只會在該節點上建立一個 Pod,若是咱們向當前的集羣中增長新的節點時,Kubernetes 就會建立在新節點上建立新的副本,總的來講,咱們可以獲得如下的拓撲結構:數組
集羣中的 Pod 和 Node 一一對應,而 DaemonSet 會管理所有機器上的 Pod 副本,負責對它們進行更新和刪除。安全
三、實現原理
全部的 DaemonSet 都是由控制器負責管理的,與其餘的資源同樣,用於管理 DaemonSet 的控制器是 DaemonSetsController
,該控制器會監聽 DaemonSet、ControllerRevision、Pod 和 Node 資源的變更。
大多數的觸發事件最終都會將一個待處理的 DaemonSet 資源入棧,下游 DaemonSetsController
持有的多個工做協程就會從隊列裏面取出資源進行消費和同步。
四、同步
DaemonSetsController
同步 DaemonSet 資源使用的方法就是 syncDaemonSet
,這個方法從隊列中拿到 DaemonSet 的名字時,會先從集羣中獲取最新的 DaemonSet 對象並經過 constructHistory
方法查找當前 DaemonSet 所有的歷史版本:
func (dsc *DaemonSetsController) syncDaemonSet(key string) error {
namespace, name, _ := cache.SplitMetaNamespaceKey(key)
ds, _ := dsc.dsLister.DaemonSets(namespace).Get(name)
dsKey, _ := controller.KeyFunc(ds)
cur, old, _ := dsc.constructHistory(ds)
hash := cur.Labels[apps.DefaultDaemonSetUniqueLabelKey]
dsc.manage(ds, hash)
switch ds.Spec.UpdateStrategy.Type {
case apps.OnDeleteDaemonSetStrategyType:
case apps.RollingUpdateDaemonSetStrategyType:
dsc.rollingUpdate(ds, hash)
}
dsc.cleanupHistory(ds, old)
return dsc.updateDaemonSetStatus(ds, hash, true)
}
而後調用的 manage
方法會負責管理 DaemonSet 在節點上 Pod 的調度和運行,rollingUpdate
會負責 DaemonSet 的滾動更新;前者會先找出找出須要運行 Pod 和不須要運行 Pod 的節點,並調用 syncNodes
對這些須要建立和刪除的 Pod 進行同步:
func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete, nodesNeedingDaemonPods []string, hash string) error {
dsKey, _ := controller.KeyFunc(ds)
generation, err := util.GetTemplateGeneration(ds)
template := util.CreatePodTemplate(ds.Spec.Template, generation, hash)
createDiff := len(nodesNeedingDaemonPods)
createWait := sync.WaitGroup{}
createWait.Add(createDiff)
for i := 0; i < createDiff; i++ {
go func(ix int) {
defer createWait.Done()
podTemplate := template.DeepCopy()
if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
podTemplate.Spec.Affinity = util.ReplaceDaemonSetPodNodeNameNodeAffinity(podTemplate.Spec.Affinity, nodesNeedingDaemonPods[ix])
dsc.podControl.CreatePodsWithControllerRef(ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
} else {
podTemplate.Spec.SchedulerName = "kubernetes.io/daemonset-controller"
dsc.podControl.CreatePodsOnNode(nodesNeedingDaemonPods[ix], ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
}
}(i)
}
createWait.Wait()
獲取了 DaemonSet 中的模板之以後,就會開始並行地爲節點建立 Pod 副本,併發建立的過程使用了 for 循環、Goroutine 和 WaitGroup
保證程序運行的正確,然而這裏使用了特性開關來對調度新 Pod 的方式進行了控制,咱們會在接下來的調度一節介紹 DaemonSet 調度方式的變遷和具體的執行過程。
當 Kubernetes 建立了須要建立的 Pod 以後,就須要刪除全部節點上沒必要要的 Pod 了,這裏使用一樣地方式併發地對 Pod 進行刪除:
deleteDiff := len(podsToDelete)
deleteWait := sync.WaitGroup{}
deleteWait.Add(deleteDiff)
for i := 0; i < deleteDiff; i++ {
go func(ix int) {
defer deleteWait.Done()
dsc.podControl.DeletePod(ds.Namespace, podsToDelete[ix], ds)
}(i)
}
deleteWait.Wait()
return nil
}
到了這裏咱們就完成了節點上 Pod 的調度和運行,爲一些節點建立 Pod 副本的同時刪除另外一部分節點上的副本,manage
方法執行完成以後就會調用 rollingUpdate
方法對 DaemonSet 的節點進行滾動更新並對控制器版本進行清理並更新 DaemonSet 的狀態,文章後面的部分會介紹滾動更新的過程和實現。
五、調度
在早期的 Kubernetes 版本中,全部 DaemonSet Pod 的建立都是由 DaemonSetsController
負責的,而其餘的資源都是由 kube-scheduler 進行調度,這就致使了以下的一些問題:
DaemonSetsController
沒有辦法在節點資源變動時收到通知 (#46935, #58868);DaemonSetsController
沒有辦法遵循 Pod 的親和性和反親和性設置 (#29276);DaemonSetsController
可能須要二次實現 Pod 調度的重要邏輯,形成了重複的代碼邏輯 (#42028);多個組件負責調度會致使 Debug 和搶佔等功能的實現很是困難;
設計文檔 Schedule DaemonSet Pods by default scheduler, not DaemonSet controller 中包含了使用 DaemonSetsController
調度時遇到的問題以及新設計給出的解決方案。
若是咱們選擇使用過去的調度方式,DeamonSetsController
就會負責在節點上建立 Pod,經過這種方式建立的 Pod 的 schedulerName
都會被設置成 kubernetes.io/daemonset-controller
,可是在默認狀況下這個字段通常爲 default-scheduler
,也就是使用 Kubernetes 默認的調度器 kube-scheduler 進行調度:
func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete, nodesNeedingDaemonPods []string, hash string) error {
// ...
for i := 0; i < createDiff; i++ {
go func(ix int) {
podTemplate := template.DeepCopy()
if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
// ...
} else {
podTemplate.Spec.SchedulerName = "kubernetes.io/daemonset-controller"
dsc.podControl.CreatePodsOnNode(nodesNeedingDaemonPods[ix], ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
}
}(i)
}
// ...
}
DaemonSetsController
在調度 Pod 時都會使用 CreatePodsOnNode
方法,這個方法的實現很是簡單,它會先對 Pod 模板進行驗證,隨後調用 createPods
方法經過 Kubernetes 提供的 API 建立新的副本:
func (r RealPodControl) CreatePodsWithControllerRef(namespace string, template *v1.PodTemplateSpec, controllerObject runtime.Object, controllerRef *metav1.OwnerReference) error {
if err := validateControllerRef(controllerRef); err != nil {
return err
}
return r.createPods("", namespace, template, controllerObject, controllerRef)
}
DaemonSetsController
經過節點選擇器和調度器的謂詞對節點進行過濾,createPods
會直接爲當前的 Pod 設置 spec.NodeName
屬性,最後獲得的 Pod 就會被目標節點上的 kubelet 建立。
除了這種使用 DaemonSetsController
管理和調度 DaemonSet 的方法以外,咱們還可使用 Kubernetes 默認的方式 kube-scheduler 建立新的 Pod 副本:
func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete, nodesNeedingDaemonPods []string, hash string) error {
// ...
for i := 0; i < createDiff; i++ {
go func(ix int) {
podTemplate := template.DeepCopy()
if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
podTemplate.Spec.Affinity = util.ReplaceDaemonSetPodNodeNameNodeAffinity(podTemplate.Spec.Affinity, nodesNeedingDaemonPods[ix])
dsc.podControl.CreatePodsWithControllerRef(ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
} else {
// ...
}
}(i)
}
// ...
}
這種狀況會使用 NodeAffinity 特性來避免發生在 DaemonSetsController
中的調度:
DaemonSetsController
會在podsShouldBeOnNode
方法中根據節點選擇器過濾全部的節點;對於每個節點,控制器都會建立一個遵循如下節點親和的 Pod;
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- nodeSelectorTerms:
matchExpressions:
- key: kubernetes.io/hostname
operator: in
values:
- dest_hostname
當節點進行同步時,DaemonSetsController 會根據節點親和的設置來驗證節點和 Pod 的關係;
若是調度的謂詞失敗了,DaemonSet 持有的 Pod 就會保持在 Pending 的狀態,因此能夠經過修改 Pod 的優先級和搶佔保證集羣在高負載下也能正常運行 DaemonSet 的副本;
Pod 的優先級和搶佔功能在 Kubernetes 1.8 版本引入,1.11 時轉變成 beta 版本,在目前最新的 1.13 中依然是 beta 版本,感興趣的讀者能夠閱讀 Pod Priority and Preemption 文檔瞭解相關的內容。
六、滾動更新
DaemonSetsController
對滾動更新的實現其實比較簡單,它其實就是根據 DaemonSet 規格中的配置,刪除集羣中的 Pod 並保證同時不可用的副本數不會超過 spec.updateStrategy.rollingUpdate.maxUnavailable
,這個參數也是 DaemonSet 滾動更新能夠配置的惟一參數:
func (dsc *DaemonSetsController) rollingUpdate(ds *apps.DaemonSet, hash string) error {
nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
_, oldPods := dsc.getAllDaemonSetPods(ds, nodeToDaemonPods, hash)
maxUnavailable, numUnavailable, err := dsc.getUnavailableNumbers(ds, nodeToDaemonPods)
oldAvailablePods, oldUnavailablePods := util.SplitByAvailablePods(ds.Spec.MinReadySeconds, oldPods)
var oldPodsToDelete []string
for _, pod := range oldUnavailablePods {
if pod.DeletionTimestamp != nil {
continue
}
oldPodsToDelete = append(oldPodsToDelete, pod.Name)
}
for _, pod := range oldAvailablePods {
if numUnavailable >= maxUnavailable {
break
}
oldPodsToDelete = append(oldPodsToDelete, pod.Name)
numUnavailable++
}
return dsc.syncNodes(ds, oldPodsToDelete, []string{}, hash)
}
刪除 Pod 的順序其實也很是簡單而且符合直覺,上述代碼會將不可用的 Pod 先加入到待刪除的數組中,隨後將歷史版本的可用 Pod 加入待刪除數組 oldPodsToDelete
,最後調用 syncNodes
完成對副本的刪除。
七、刪除
與 Deployment、ReplicaSet 和 StatefulSet 同樣,DaemonSet 的刪除也會致使它持有的 Pod 的刪除,若是咱們使用以下的命令刪除該對象,咱們能觀察到以下的現象:
[root@yygh-de ~]# kubectl delete daemonsets.apps fluentd-elasticsearch --namespace kube-system
daemonset.apps "fluentd-elasticsearch" deleted
[root@yygh-de ~]# kubectl get pods --watch --namespace kube-system
fluentd-elasticsearch-wvffx 1/1 Terminating 0 14s
這部分的工做就都是由 Kubernetes 中的垃圾收集器完成的,讀者能夠閱讀 垃圾收集器 瞭解集羣中的不一樣對象是如何進行關聯的以及在刪除單一對象時如何觸發級聯刪除的原理。
八、總結
DaemonSet 其實就是 Kubernetes 中的守護進程,它會在每個節點上建立可以提供服務的副本,不少雲服務商都會使用 DaemonSet 在全部的節點上內置一些用於提供日誌收集、統計分析和安全策略的服務。
在研究 DaemonSet 的調度策略的過程當中,咱們其實可以經過一些歷史的 issue 和 PR 瞭解到 DaemonSet 調度策略改動的緣由,也能讓咱們對於 Kubernetes 的演進過程和設計決策有一個比較清楚的認識。
若是文章有任何錯誤歡迎不吝賜教,其次你們有任何關於運維的疑難雜問,也歡迎和你們一塊兒交流討論。關於運維學習、分享、交流,筆者開通了微信公衆號【運維貓】,感興趣的朋友能夠關注下,歡迎加入,創建屬於咱們本身的小圈子,一塊兒學運維知識。羣主還經營一家Orchis飾品店,喜歡的小夥伴歡迎👏前來下單。
掃描二維碼
獲取更多精彩
運維貓公衆號
有須要技術交流的小夥伴能夠加我微信,期待與你們共同成長,本人微信:
掃描二維碼
添加私人微信
運維貓博主
掃碼加微信
最近有一些星友諮詢我知識星球的事,我也想繼續在星球上發佈更優質的內容供你們學習和探討。運維貓公衆號平臺致力於爲你們提供免費的學習資源,知識星球主要致力於即將入坑或者已經入坑的運維行業的小夥伴。
點擊閱讀原文 查看更多精彩內容!!!
本文分享自微信公衆號 - 運維貓(centos15)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。