前言
Pod是k8s中最小的運(yùn)行單元,Pod最常見的控制器就是 Deployment 和 Statefulset, 其他兩種 Job/CronJob 、DaemonSet 這樣的控制器較少見一些。
一、DaemonSet
1.1 DaemonSet基本屬性
顧名思義,DaemonSet 的主要作用,是讓你在 Kubernetes 集群里,運(yùn)行一個(gè) Daemon Pod。 所以,這個(gè) Pod 有如下三個(gè)特征:
- 這個(gè) Pod 運(yùn)行在 Kubernetes 集群里的每一個(gè)節(jié)點(diǎn)(Node)上,每個(gè)節(jié)點(diǎn)上只有一個(gè)這樣的 Pod 實(shí)例
- 當(dāng)有新的節(jié)點(diǎn)加入 Kubernetes 集群后,該 Pod 會(huì)自動(dòng)地在新節(jié)點(diǎn)上被創(chuàng)建出來(lái)
- 而當(dāng)舊節(jié)點(diǎn)被刪除后,它上面的 Pod 也相應(yīng)地會(huì)被回收掉。
這個(gè)機(jī)制聽起來(lái)很簡(jiǎn)單,但 Daemon Pod 的意義確實(shí)是非常重要的,業(yè)務(wù)場(chǎng)景包括:
- 各種網(wǎng)絡(luò)插件的 Agent 組件,都必須運(yùn)行在每一個(gè)節(jié)點(diǎn)上,用來(lái)處理這個(gè)節(jié)點(diǎn)上的容器網(wǎng)絡(luò)
- 各種存儲(chǔ)插件的 Agent 組件,也必須運(yùn)行在每一個(gè)節(jié)點(diǎn)上,用來(lái)在這個(gè)節(jié)點(diǎn)上掛載遠(yuǎn)程存儲(chǔ)目錄,操作容器的 Volume 目錄
- 各種監(jiān)控組件和日志組件,也必須運(yùn)行在每一個(gè)節(jié)點(diǎn)上,負(fù)責(zé)這個(gè)節(jié)點(diǎn)上的監(jiān)控信息和日志搜集。
為了弄清楚 DaemonSet 的工作原理,我們還是按照老規(guī)矩,先從它的 API 對(duì)象的定義說(shuō)起。
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
這個(gè) DaemonSet,管理的是一個(gè) fluentd-elasticsearch 鏡像的 Pod。這個(gè)鏡像的功能非常實(shí)用:通過(guò) fluentd 將 Docker 容器里的日志轉(zhuǎn)發(fā)到 ElasticSearch 中。
可以看到,DaemonSet 跟 Deployment 其實(shí)非常相似,只不過(guò)是沒(méi)有 replicas 字段;它也使用 selector 選擇管理所有攜帶了 name=fluentd-elasticsearch 標(biāo)簽的 Pod。
而這些 Pod 的模板,也是用 template 字段定義的。在這個(gè)字段中,我們定義了一個(gè)使用 fluentd-elasticsearch:1.20 鏡像的容器,而且這個(gè)容器掛載了兩個(gè) hostPath 類型的 Volume,分別對(duì)應(yīng)宿主機(jī)的 /var/log 目錄和 /var/lib/docker/containers 目錄。
顯然,fluentd 啟動(dòng)之后,它會(huì)從這兩個(gè)目錄里搜集日志信息,并轉(zhuǎn)發(fā)給 ElasticSearch 保存。這樣,我們通過(guò) ElasticSearch 就可以很方便地檢索這些日志了。
需要注意的是,Docker 容器里應(yīng)用的日志,默認(rèn)會(huì)保存在宿主機(jī)的 /var/lib/docker/containers/{{. 容器 ID}}/{{. 容器 ID}}-json.log 文件里,所以這個(gè)目錄正是 fluentd 的搜集目標(biāo)。
1.2 DaemonSet 如何保證每個(gè) Node 上有且只有一個(gè)被管理的 Pod
DaemonSet 是如何保證每個(gè) Node 上有且只有一個(gè)被管理的 Pod 呢?
回答:DaemonSet Controller,首先從 Etcd 里獲取所有的 Node 列表,然后遍歷所有的 Node。這時(shí),它就可以很容易地去檢查,當(dāng)前這個(gè) Node 上是不是有一個(gè)攜帶了 name=fluentd-elasticsearch 標(biāo)簽的 Pod 在運(yùn)行。 而檢查的結(jié)果,可能有這么三種情況:
(1) 沒(méi)有這種 Pod,那么就意味著要在這個(gè) Node 上創(chuàng)建這樣一個(gè) Pod [較復(fù)雜, 通過(guò) nodeAffinity 實(shí)現(xiàn), 下面詳解]
(2) 有這種 Pod,但是數(shù)量大于 1,那就說(shuō)明要把多余的 Pod 從這個(gè) Node 上刪除掉 [簡(jiǎn)單, 直接調(diào)用 Kubernetes API 實(shí)現(xiàn)]
(3) 正好只有一個(gè)這種 Pod,那說(shuō)明這個(gè)節(jié)點(diǎn)是正常的。 [不需要處理該Node]
如何在指定的 Node 上創(chuàng)建新 Pod 呢?
如果你已經(jīng)熟悉了 Pod API 對(duì)象的話,那一定可以立刻說(shuō)出答案:用 nodeSelector,選擇 Node 的名字即可。
不過(guò),在 Kubernetes 項(xiàng)目里,nodeSelector 其實(shí)已經(jīng)是一個(gè)將要被廢棄的字段了。因?yàn)?,現(xiàn)在有了一個(gè)新的、功能更完善的字段可以代替它,即:nodeAffinity 節(jié)點(diǎn)親和性。我來(lái)舉個(gè)例子:
apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: metadata.name
operator: In
values:
- node-geektime
在這個(gè) Pod 里,我聲明了一個(gè) spec.affinity 字段,然后定義了一個(gè) nodeAffinity。其中,spec.affinity 字段,是 Pod 里跟調(diào)度相關(guān)的一個(gè)字段。
而在這里,我定義的 nodeAffinity 的含義是:
- requiredDuringSchedulingIgnoredDuringExecution:它的意思是說(shuō),這個(gè) nodeAffinity 必須在每次調(diào)度的時(shí)候予以考慮。同時(shí),這也意味著你可以設(shè)置在某些情況下不考慮這個(gè) nodeAffinity
- 這個(gè) Pod,將來(lái)只允許運(yùn)行在“metadata.name”是“node-geektime”的節(jié)點(diǎn)上。
在這里,你應(yīng)該注意到 nodeAffinity 的定義,可以支持更加豐富的語(yǔ)法,比如 operator: In(即:部分匹配;如果你定義 operator: Equal,就是完全匹配),這也正是 nodeAffinity 會(huì)取代 nodeSelector 的原因之一。
所以,我們的 DaemonSet Controller 會(huì)在創(chuàng)建 Pod 的時(shí)候,自動(dòng)在這個(gè) Pod 的 API 對(duì)象里,加上這樣一個(gè) nodeAffinity 定義。其中,需要綁定的節(jié)點(diǎn)名字,正是當(dāng)前正在遍歷的這個(gè) Node。
當(dāng)然,DaemonSet 并不需要修改用戶提交的 YAML 文件里的 Pod 模板,而是在向 Kubernetes 發(fā)起請(qǐng)求之前,直接修改根據(jù)模板生成的 Pod 對(duì)象。這個(gè)思路,也正是我在前面講解 Pod 對(duì)象時(shí)介紹過(guò)的。
此外,DaemonSet 還會(huì)給這個(gè) Pod 自動(dòng)加上另外一個(gè)與調(diào)度相關(guān)的字段,叫作 tolerations。這個(gè)字段意味著這個(gè) Pod,會(huì)“容忍”(Toleration)某些 Node 的“污點(diǎn)”(Taint)。
而 DaemonSet 自動(dòng)加上的 tolerations 字段,格式如下所示:
apiVersion: v1
kind: Pod
metadata:
name: with-toleration
spec:
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
effect: NoSchedule
這個(gè) Toleration 的含義是:“容忍”所有被標(biāo)記為 unschedulable“污點(diǎn)”的 Node;“容忍”的效果是允許調(diào)度。
而在正常情況下,被標(biāo)記了 unschedulable“污點(diǎn)”的 Node,是不會(huì)有任何 Pod 被調(diào)度上去的(effect: NoSchedule)??墒?,DaemonSet 自動(dòng)地給被管理的 Pod 加上了這個(gè)特殊的 Toleration,就使得這些 Pod 可以忽略這個(gè)限制,繼而保證每個(gè)節(jié)點(diǎn)上都會(huì)被調(diào)度一個(gè) Pod。當(dāng)然,如果這個(gè)節(jié)點(diǎn)有故障的話,這個(gè) Pod 可能會(huì)啟動(dòng)失敗,而 DaemonSet 則會(huì)始終嘗試下去,直到 Pod 啟動(dòng)成功。
這時(shí),你應(yīng)該可以猜到,我在前面介紹到的 DaemonSet 的“過(guò)人之處”,其實(shí)就是依靠 Toleration 實(shí)現(xiàn)的。
假如當(dāng)前 DaemonSet 管理的,是一個(gè)網(wǎng)絡(luò)插件的 Agent Pod,那么你就必須在這個(gè) DaemonSet 的 YAML 文件里,給它的 Pod 模板加上一個(gè)能夠“容忍”node.kubernetes.io/network-unavailable“污點(diǎn)”的 Toleration。正如下面這個(gè)例子所示:
template:
metadata:
labels:
name: network-plugin-agent
spec:
tolerations:
- key: node.kubernetes.io/network-unavailable
operator: Exists
effect: NoSchedule
在 Kubernetes 項(xiàng)目中,當(dāng)一個(gè)節(jié)點(diǎn)的網(wǎng)絡(luò)插件尚未安裝時(shí),這個(gè)節(jié)點(diǎn)就會(huì)被自動(dòng)加上名為node.kubernetes.io/network-unavailable的“污點(diǎn)”。
而通過(guò)這樣一個(gè) Toleration,調(diào)度器在調(diào)度這個(gè) Pod 的時(shí)候,就會(huì)忽略當(dāng)前節(jié)點(diǎn)上的“污點(diǎn)”,從而成功地將網(wǎng)絡(luò)插件的 Agent 組件調(diào)度到這臺(tái)機(jī)器上啟動(dòng)起來(lái)。
至此,通過(guò)上面這些內(nèi)容,你應(yīng)該能夠明白,DaemonSet 其實(shí)是一個(gè)非常簡(jiǎn)單的控制器。在它的控制循環(huán)中,只需要遍歷所有節(jié)點(diǎn),然后根據(jù)節(jié)點(diǎn)上是否有被管理 Pod 的情況,來(lái)決定是否要?jiǎng)?chuàng)建或者刪除一個(gè) Pod。
實(shí)現(xiàn)方式是:在創(chuàng)建每個(gè) Pod 的時(shí)候,DaemonSet 會(huì)自動(dòng)給這個(gè) Pod 加上一個(gè) nodeAffinity,從而保證這個(gè) Pod 只會(huì)在指定節(jié)點(diǎn)上啟動(dòng)。同時(shí),它還會(huì)自動(dòng)給這個(gè) Pod 加上一個(gè) Toleration,從而忽略節(jié)點(diǎn)的 unschedulable“污點(diǎn)”。
當(dāng)然,你也可以在 Pod 模板里加上更多種類的 Toleration,從而利用 DaemonSet 達(dá)到自己的目的。比如,在這個(gè) fluentd-elasticsearch DaemonSet 里,我就給它加上了這樣的 Toleration:
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
這是因?yàn)樵谀J(rèn)情況下,Kubernetes 集群不允許用戶在 Master 節(jié)點(diǎn)部署 Pod。因?yàn)?,Master 節(jié)點(diǎn)默認(rèn)攜帶了一個(gè)叫作node-role.kubernetes.io/master的“污點(diǎn)”。所以,為了能在 Master 節(jié)點(diǎn)上部署 DaemonSet 的 Pod,我就必須讓這個(gè) Pod“容忍”這個(gè)“污點(diǎn)”。
在理解了 DaemonSet 的工作原理之后,接下來(lái)我就通過(guò)一個(gè)具體的實(shí)踐來(lái)幫你更深入地掌握 DaemonSet 的使用方法。
1.3 DaemonSet控制版本
首先,創(chuàng)建這個(gè) DaemonSet 對(duì)象:
# 新建一個(gè)daemonset
$ kubectl create -f fluentd-elasticsearch.yaml
DaemonSet Pod資源占用:在 DaemonSet 上,我們一般都應(yīng)該加上 resources 字段,來(lái)限制它的 CPU 和內(nèi)存使用,防止它占用過(guò)多的宿主機(jī)資源。 在實(shí)際的使用中,強(qiáng)烈建議將 DaemonSet 的 Pod 都設(shè)置為 Guaranteed 的 QoS 類型。如果不這樣做,一旦 DaemonSet 的 Pod 被回收,它又會(huì)立即在原宿主機(jī)上被重建出來(lái),這就使得前面資源回收的動(dòng)作,完全沒(méi)有意義了。[ 使用 Guaranteed 服務(wù)質(zhì)量,Pod 因?yàn)橘Y源不足被刪除,就不會(huì)又重新新建起來(lái),因?yàn)?Guaranteed 服務(wù)質(zhì)量會(huì)先檢查 Node 上的資源是否滿足 Pod ]
QoS(Quality of Service,服務(wù)質(zhì)量)是通過(guò)為不同類型的 Pod 分配資源來(lái)控制和優(yōu)化 Kubernetes 集群中的服務(wù)性能和可用性。Kubernetes(K8s)中有四種類型的Pod QoS(Quality of Service)級(jí)別,分別是:
(1) BestEffort(最低保證):這是最低級(jí)別的QoS,表示對(duì)Pod的資源使用沒(méi)有特定的需求,可以與其他Pod共享節(jié)點(diǎn)的資源。這些Pod不會(huì)被調(diào)度程序主動(dòng)殺死以釋放資源,也不會(huì)受到其他Pod的限制。
(2) Burstable(可突發(fā)):這是介于BestEffort和Guaranteed之間的QoS級(jí)別。Pod可以請(qǐng)求并使用特定數(shù)量的資源,但若資源不足時(shí),它們?nèi)匀豢梢员徽{(diào)度到節(jié)點(diǎn)上,并與其他Pod共享資源。
(3) Guaranteed(保證):這是最高級(jí)別的QoS級(jí)別,表示Pod對(duì)資源的需求是固定且不可削減的。這些Pod具有優(yōu)先權(quán),并且會(huì)優(yōu)先獲得可用資源。如果節(jié)點(diǎn)資源不足,調(diào)度程序可以選擇殺死其他QoS級(jí)別較低的Pod來(lái)保證Guaranteed級(jí)別的Pod的資源需求得到滿足。[特點(diǎn): Pod 的請(qǐng)求值和限制值相等,即request下限和limit上限相同,使得 Kubernetes 可根據(jù)此進(jìn)行資源管理和調(diào)度。]
(4) Not to be evicted(禁止驅(qū)逐):這是一種特殊的QoS級(jí)別,用于在Pod不能被主動(dòng)殺死以釋放資源的情況下標(biāo)識(shí)一個(gè)Pod。這可能是由于Pod的configuration屬性設(shè)置為"do not evict"或者因?yàn)檎褂脧椥苑植际酱鎯?chǔ)、系統(tǒng)暫?;蚱渌?qū)е翽od無(wú)法驅(qū)逐。
綜上,Guaranteed 是一種可靠性較高的 QoS 類型,Pod 被設(shè)置為 Guaranteed 類型時(shí),Kubernetes 會(huì)分配足夠的資源來(lái)滿足 Pod 的需求,以確保它們始終可用。
kubectl drain <NodeName> --force --ignore-daemonsets
解釋:(1) kubectl drain 是一個(gè) Kubernetes 命令,drain 譯為遷移,用于將一個(gè)節(jié)點(diǎn)上的 Pod 遷移至其他節(jié)點(diǎn)。當(dāng)需要從集群中刪除一個(gè)節(jié)點(diǎn)或維護(hù)一個(gè)節(jié)點(diǎn)時(shí),可以使用該命令確保節(jié)點(diǎn)上的 Pod 不會(huì)丟失。
(2) 指定 <NodeName>
參數(shù)表示要進(jìn)行 Pod 遷移操作的節(jié)點(diǎn)名稱,表示這個(gè)節(jié)點(diǎn)上的 Pod 都遷走。
(3) --force
參數(shù)表示強(qiáng)制進(jìn)行 Pod 遷移操作。使用該參數(shù),即使 Pod 處于未就緒或無(wú)法刪除的狀態(tài),也會(huì)強(qiáng)制進(jìn)行遷移。
(4) --ignore-daemonsets
參數(shù)表示在進(jìn)行 Pod 遷移時(shí)忽略守護(hù)進(jìn)程集(DaemonSet)。守護(hù)進(jìn)程集是一種類型的 Pod,在每個(gè)節(jié)點(diǎn)上都會(huì)運(yùn)行,并且不能被驅(qū)逐。
整體解釋:使用該命令時(shí),Kubernetes 將調(diào)度調(diào)動(dòng)一個(gè)控制器(Cordon Controller),將節(jié)點(diǎn)設(shè)置為不可調(diào)度狀態(tài),然后將節(jié)點(diǎn)上的 Pod 逐個(gè)遷移到其他節(jié)點(diǎn)。一旦所有 Pod 都被遷移到其他節(jié)點(diǎn),節(jié)點(diǎn)將被標(biāo)記為SchedulingDisabled,表示該節(jié)點(diǎn)不可用。另外注意,在使用該命令之前,請(qǐng)確保已經(jīng)使用適當(dāng)?shù)姆椒ê筒呗詫⒇?fù)載移到其他可用節(jié)點(diǎn)。
通過(guò) kubectl get 查看一下 Kubernetes 集群里的 DaemonSet 對(duì)象,如下:
# 查看DaemonSet
$ kubectl get ds -n kube-system fluentd-elasticsearch
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
fluentd-elasticsearch 2 2 2 2 2 <none> 1h
# 查看DaemonSet的Pod
$ kubectl get pod -n kube-system -l name=fluentd-elasticsearch
NAME READY STATUS RESTARTS AGE
fluentd-elasticsearch-dqfv9 1/1 Running 0 53m
fluentd-elasticsearch-pf9z5 1/1 Running 0 53m
Kubernetes 里比較長(zhǎng)的 API 對(duì)象都有短名字,比如 DaemonSet 對(duì)應(yīng)的是 ds,Deployment 對(duì)應(yīng)的是 deploy。DaemonSet 和 Deployment 一樣,也有 DESIRED、CURRENT 等多個(gè)狀態(tài)字段。這也就意味著,DaemonSet 可以像 Deployment 那樣,進(jìn)行版本管理。這個(gè)版本,可以使用 kubectl rollout history 看到:
$ kubectl rollout history daemonset fluentd-elasticsearch -n kube-system
daemonsets "fluentd-elasticsearch"
REVISION CHANGE-CAUSE
1 <none>
接下來(lái),我們來(lái)把這個(gè) DaemonSet 的容器鏡像版本到 v2.2.0,生成 DaemonSet 版本2 (后面用來(lái)做版本回滾的)
$ kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record -n=kube-system
Kubernetes 命令,用于更新一個(gè)指定守護(hù)進(jìn)程集(DaemonSet)中的容器鏡像。它將一個(gè)容器鏡像的新版本指定給一個(gè)特定的守護(hù)進(jìn)程集,該守護(hù)進(jìn)程集屬于 kube-system 命名空間。
解釋命令中的參數(shù):
- set image 表示要設(shè)置容器鏡像的命令。
- ds/fluentd-elasticsearch 指定了要更新鏡像的守護(hù)進(jìn)程集的名稱。ds 表示 DaemonSet。
- fluentd-elasticsearch 是要更新的容器的名稱。
- fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 指定了新的容器鏡像。fluentd-elasticsearch 是容器的名稱,k8s.gcr.io/fluentd-elasticsearch:v2.2.0 是要更新的容器鏡像的新版本。
-
--record
參數(shù)表示將此操作記錄到事件日志中。 - -n=kube-system 參數(shù)指定了 Kubernetes 命名空間,kube-system 是一個(gè)特殊的命名空間,用于運(yùn)行 Kubernetes 系統(tǒng)組件和 DaemonSet。
運(yùn)行該命令后,Kubernetes 將更新 kube-system 命名空間中 fluentd-elasticsearch 守護(hù)進(jìn)程集中的容器鏡像為指定版本。
接下來(lái),我們可以使用 kubectl rollout status 命令看到這個(gè)“滾動(dòng)更新”的過(guò)程,如下所示:
$ kubectl rollout status ds/fluentd-elasticsearch -n kube-system
Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 0 out of 2 new pods have been updated...
Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 0 out of 2 new pods have been updated...
Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 1 of 2 updated pods are available...
daemon set "fluentd-elasticsearch" successfully rolled out
有了版本號(hào),你也就可以像 Deployment 一樣,將 DaemonSet 回滾到某個(gè)指定的歷史版本了。
Deployment 管理這些版本,靠的是“一個(gè)版本對(duì)應(yīng)一個(gè) ReplicaSet 對(duì)象”??墒?,DaemonSet 控制器操作的直接就是 Pod,不可能有 ReplicaSet 這樣的對(duì)象參與其中。實(shí)際上,DaemonSet 的這些版本是通過(guò)一種 kind: ControllerRevision 的資源來(lái)管理的。
在 Kubernetes 項(xiàng)目中,任何你覺(jué)得需要記錄下來(lái)的狀態(tài),都可以被用 API 對(duì)象的方式實(shí)現(xiàn)。當(dāng)然,“版本”也不例外。
Kubernetes v1.7 之后添加了一個(gè) API 對(duì)象,名叫 ControllerRevision,專門用來(lái)記錄某種 Controller 對(duì)象的版本。比如,你可以通過(guò)如下命令查看 fluentd-elasticsearch 對(duì)應(yīng)的 ControllerRevision:
# 查看 controllerrevision 控制版本資源
$ kubectl get controllerrevision -n kube-system -l name=fluentd-elasticsearch
NAME CONTROLLER REVISION AGE
fluentd-elasticsearch-64dc6799c9 daemonset.apps/fluentd-elasticsearch 2 1h
$ kubectl describe controllerrevision fluentd-elasticsearch-64dc6799c9 -n kube-system
Name: fluentd-elasticsearch-64dc6799c9
Namespace: kube-system
Labels: controller-revision-hash=2087235575
name=fluentd-elasticsearch
Annotations: deprecated.daemonset.template.generation=2
kubernetes.io/change-cause=kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record=true --namespace=kube-system
API Version: apps/v1
Data:
Spec:
Template:
$ Patch: replace
Metadata:
Creation Timestamp: <nil>
Labels:
Name: fluentd-elasticsearch
Spec:
Containers:
Image: k8s.gcr.io/fluentd-elasticsearch:v2.2.0
Image Pull Policy: IfNotPresent
Name: fluentd-elasticsearch
...
Revision: 2 # 這就是版本資源
Events: <none>
就會(huì)看到,這個(gè) ControllerRevision 對(duì)象,實(shí)際上是在 Data 字段保存了該版本對(duì)應(yīng)的完整的 DaemonSet 的 API 對(duì)象。并且,在 Annotation 字段保存了創(chuàng)建這個(gè)對(duì)象所使用的 kubectl 命令。
接下來(lái),我們可以嘗試將這個(gè) DaemonSet 回滾到 Revision=1 時(shí)的狀態(tài):
# DaemonSet 回滾到 Revision=1 時(shí)的狀態(tài)
$ kubectl rollout undo daemonset fluentd-elasticsearch --to-revision=1 -n kube-system
daemonset.extensions/fluentd-elasticsearch rolled back
這個(gè) kubectl rollout undo 操作,實(shí)際上相當(dāng)于讀取到了 Revision=1 的 ControllerRevision 對(duì)象保存的 Data 字段。而這個(gè) Data 字段里保存的信息,就是 Revision=1 時(shí)這個(gè) DaemonSet 的完整 API 對(duì)象。
所以,現(xiàn)在 DaemonSet Controller 就可以使用這個(gè)歷史 API 對(duì)象,對(duì)現(xiàn)有的 DaemonSet 做一次 PATCH 操作(等價(jià)于執(zhí)行一次 kubectl apply -f “舊的 DaemonSet 對(duì)象”),從而把這個(gè) DaemonSet“更新”到一個(gè)舊版本。
這也是為什么,在執(zhí)行完這次回滾完成后,你會(huì)發(fā)現(xiàn),DaemonSet 的 Revision 并不會(huì)從 Revision=2 退回到 1,而是會(huì)增加成 Revision=3。這是因?yàn)?,一個(gè)新的 ControllerRevision 被創(chuàng)建了出來(lái)。
二、Job
容器按照持續(xù)運(yùn)行的時(shí)間可分為兩類:服務(wù)類容器和工作類容器。
服務(wù)類容器通常持續(xù)提供服務(wù),需要一直運(yùn)行,比如 http server,daemon 等。 [Deployment、ReplicaSet 和 DaemonSet管理]
工作類容器則是一次性任務(wù),比如批處理程序,完成后容器就退出。 [Job/CronJob管理]
Job分類:普通任務(wù)(Job)和定時(shí)任務(wù)(CronJob) 一次性執(zhí)行。
Job應(yīng)用場(chǎng)景:離線數(shù)據(jù)處理,視頻解碼等業(yè)務(wù)
小結(jié):服務(wù)類使用Deployment、ReplicaSet 和 DaemonSet管理,作業(yè)類使用Job/CronJob管理
2.1 Job引入
無(wú)論是 Deployment、StatefulSet,以及 DaemonSet 這三個(gè)編排概念,它們主要編排的對(duì)象,都是“在線業(yè)務(wù)”,即:Long Running Task(長(zhǎng)作業(yè))。比如,我在前面舉例時(shí)常用的 Nginx、Tomcat,以及 MySQL 等等。這些應(yīng)用一旦運(yùn)行起來(lái),除非出錯(cuò)或者停止,它的容器進(jìn)程會(huì)一直保持在 Running 狀態(tài)。
但是,有一類作業(yè)顯然不滿足這樣的條件,這就是“離線業(yè)務(wù)”,或者叫作 Batch Job(計(jì)算業(yè)務(wù))。這種業(yè)務(wù)在計(jì)算完成后就直接退出了,而此時(shí)如果你依然用 Deployment 來(lái)管理這種業(yè)務(wù)的話,就會(huì)發(fā)現(xiàn) Pod 會(huì)在計(jì)算結(jié)束后退出,然后被 Deployment Controller 不斷地重啟;而像“滾動(dòng)更新”這樣的編排功能,更無(wú)從談起了。
所以,早在 Borg 項(xiàng)目中,Google 就已經(jīng)對(duì)作業(yè)進(jìn)行了分類處理,提出了 LRS(Long Running Service)和 Batch Jobs 兩種作業(yè)形態(tài),對(duì)它們進(jìn)行“分別管理”和“混合調(diào)度”。
不過(guò),在 2015 年 Borg 論文剛剛發(fā)布的時(shí)候,Kubernetes 項(xiàng)目并不支持對(duì) Batch Job 的管理。直到 v1.4 版本之后,社區(qū)才逐步設(shè)計(jì)出了一個(gè)用來(lái)描述離線業(yè)務(wù)的 API 對(duì)象,它的名字就是:Job。
Job API 對(duì)象的定義非常簡(jiǎn)單,我來(lái)舉個(gè)例子,如下所示:
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: resouer/ubuntu-bc
command: ["sh", "-c", "echo 'scale=10000; 4*a(1)' | bc -l "]
restartPolicy: Never
backoffLimit: 4
此時(shí),相信你對(duì) Kubernetes 的 API 對(duì)象已經(jīng)不再陌生了。在這個(gè) Job 的 YAML 文件里,你肯定一眼就會(huì)看到一位“老熟人”:Pod 模板,即 spec.template 字段。
在這個(gè) Pod 模板中,我定義了一個(gè) Ubuntu 鏡像的容器(準(zhǔn)確地說(shuō),是一個(gè)安裝了 bc 命令的 Ubuntu 鏡像),它運(yùn)行的程序是:
echo "scale=10000; 4*a(1)" | bc -l
其中,bc 命令是 Linux 里的“計(jì)算器”;-l 表示,我現(xiàn)在要使用標(biāo)準(zhǔn)數(shù)學(xué)庫(kù);而 a(1),則是調(diào)用數(shù)學(xué)庫(kù)中的 arctangent 函數(shù),計(jì)算 atan(1)。這是什么意思呢?
中學(xué)知識(shí)告訴我們:tan(π/4) = 1。所以,4*atan(1)正好就是π,也就是 3.1415926…。
所以,這其實(shí)就是一個(gè)計(jì)算π值的容器。而通過(guò) scale=10000,我指定了輸出的小數(shù)點(diǎn)后的位數(shù)是 10000。在我的計(jì)算機(jī)上,這個(gè)計(jì)算大概用時(shí) 1 分 54 秒。
但是,跟其他控制器不同的是,Job 對(duì)象并不要求你定義一個(gè) spec.selector 來(lái)描述要控制哪些 Pod。具體原因,我馬上會(huì)講解到。
現(xiàn)在,我們就可以創(chuàng)建這個(gè) Job 了:
$ kubectl create -f job.yaml
在成功創(chuàng)建后,我們來(lái)查看一下這個(gè) Job 對(duì)象,如下所示:
$ kubectl describe jobs/pi
Name: pi
Namespace: default
Selector: controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
Labels: controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
job-name=pi
Annotations: <none>
Parallelism: 1
Completions: 1
..
Pods Statuses: 0 Running / 1 Succeeded / 0 Failed
Pod Template:
Labels: controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
job-name=pi
Containers:
...
Volumes: <none>
Events:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
1m 1m 1 {job-controller } Normal SuccessfulCreate Created pod: pi-rq5rl
可以看到,這個(gè) Job 對(duì)象在創(chuàng)建后,它的 Pod 模板,被自動(dòng)加上了一個(gè) controller-uid=< 一個(gè)隨機(jī)字符串 > 這樣的 Label。而這個(gè) Job 對(duì)象本身,則被自動(dòng)加上了這個(gè) Label 對(duì)應(yīng)的 Selector,從而 保證了 Job 與它所管理的 Pod 之間的匹配關(guān)系。
而 Job Controller 之所以要使用這種攜帶了 UID 的 Label,就是為了避免不同 Job 對(duì)象所管理的 Pod 發(fā)生重合。需要注意的是,這種自動(dòng)生成的 Label 對(duì)用戶來(lái)說(shuō)并不友好,所以不太適合推廣到 Deployment 等長(zhǎng)作業(yè)編排對(duì)象上。
接下來(lái),我們可以看到這個(gè) Job 創(chuàng)建的 Pod 進(jìn)入了 Running 狀態(tài),這意味著它正在計(jì)算 Pi 的值。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-rq5rl 1/1 Running 0 10s
而幾分鐘后計(jì)算結(jié)束,這個(gè) Pod 就會(huì)進(jìn)入 Completed 狀態(tài):
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-rq5rl 0/1 Completed 0 4m
這也是我們需要在 Pod 模板中定義 restartPolicy=Never 的原因:離線計(jì)算的 Pod 永遠(yuǎn)都不應(yīng)該被重啟,否則它們會(huì)再重新計(jì)算一遍。
事實(shí)上,restartPolicy 在 Job 對(duì)象里只允許被設(shè)置為 Never 和 OnFailure;而在 Deployment 對(duì)象里,restartPolicy 則只允許被設(shè)置為 Always。
此時(shí),我們通過(guò) kubectl logs 查看一下這個(gè) Pod 的日志,就可以看到計(jì)算得到的 Pi 值已經(jīng)被打印了出來(lái):
$ kubectl logs pi-rq5rl
3.141592653589793238462643383279...
這時(shí)候,你一定會(huì)想到這樣一個(gè)問(wèn)題,如果這個(gè)離線作業(yè)失敗了要怎么辦?
比如,我們?cè)谶@個(gè)例子中定義了 restartPolicy=Never,那么離線作業(yè)失敗后 Job Controller 就會(huì)不斷地嘗試創(chuàng)建一個(gè)新 Pod,如下所示:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-55h89 0/1 ContainerCreating 0 2s
pi-tqbcz 0/1 Error 0 5s
可以看到,這時(shí)候會(huì)不斷地有新 Pod 被創(chuàng)建出來(lái)。
當(dāng)然,這個(gè)嘗試肯定不能無(wú)限進(jìn)行下去。所以,我們就在 Job 對(duì)象的 spec.backoffLimit 字段里定義了重試次數(shù)為 4(即,backoffLimit=4),而這個(gè)字段的默認(rèn)值是 6。
需要注意的是,Job Controller 重新創(chuàng)建 Pod 的間隔是呈指數(shù)增加的,即下一次重新創(chuàng)建 Pod 的動(dòng)作會(huì)分別發(fā)生在 10 s、20 s、40 s …后。
而如果你定義的 restartPolicy=OnFailure,那么離線作業(yè)失敗后,Job Controller 就不會(huì)去嘗試創(chuàng)建新的 Pod。但是,它會(huì)不斷地嘗試重啟 Pod 里的容器。這也正好對(duì)應(yīng)了 restartPolicy 的含義。
如前所述,當(dāng)一個(gè) Job 的 Pod 運(yùn)行結(jié)束后,它會(huì)進(jìn)入 Completed 狀態(tài)。但是,如果這個(gè) Pod 因?yàn)槟撤N原因一直不肯結(jié)束呢?
在 Job 的 API 對(duì)象里,有一個(gè) spec.activeDeadlineSeconds 字段可以設(shè)置最長(zhǎng)運(yùn)行時(shí)間,比如:
spec:
backoffLimit: 5
activeDeadlineSeconds: 100
一旦運(yùn)行超過(guò)了 100 s,這個(gè) Job 的所有 Pod 都會(huì)被終止。并且,你可以在 Pod 的狀態(tài)里看到終止的原因是 reason: DeadlineExceeded。
2.2 Job執(zhí)行
Job執(zhí)行成功
先看一個(gè)簡(jiǎn)單的 Job 配置文件 myjob.yml:
[root@k8s-master ~]# cat myjob.yml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
command: ["echo","hello k8s job"]
restartPolicy: Never #Never 程序退出了就不再重啟了,不管正確還是錯(cuò)誤退出
解釋:
- batch/v1 是當(dāng)前 Job 的 apiVersion。
- kind 指明當(dāng)前資源的類型為 Job。
- restartPolicy 指定什么情況下需要重啟容器。對(duì)于 Job,只能設(shè)置為 Never 或者 OnFailure。對(duì)于其他 controller(比如 Deployment)可以設(shè)置為 Always 。
通過(guò) kubectl apply -f myjob.yml 啟動(dòng) Job。
[root@k8s-master ~]# kubectl apply -f myjob.yml
job.batch/myjob created
kubectl get job 查看 Job 的狀態(tài):
[root@k8s-master ~]# kubectl get job
NAME COMPLETIONS DURATION AGE
myjob 1/1 21s 9m58s
可以看到按照預(yù)期啟動(dòng)了一個(gè) Pod,并且已經(jīng)成功執(zhí)行。(Pod 執(zhí)行完畢后容器已經(jīng)退出)
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myjob-q54fk 0/1 Completed 0 9m53s
kubectl logs 可以查看 Pod 的標(biāo)準(zhǔn)輸出:
[root@k8s-master ~]# kubectl logs myjob-q54fk
hello k8s job
以上是 Pod 成功執(zhí)行的情況,如果 Pod 失敗了會(huì)怎么樣呢?
Job執(zhí)行失敗
先刪除之前的 Job:
[root@k8s-master ~]# kubectl delete -f myjob.yml
job.batch "myjob" deleted
修改 myjob.yml,故意引入一個(gè)錯(cuò)誤,只需要修改command。
command: ["error command","hello k8s job"]
運(yùn)行新的 Job 并查看狀態(tài)
[root@k8s-master ~]# kubectl apply -f myjob.yml
job.batch/myjob created
[root@k8s-master ~]# kubectl get job
NAME COMPLETIONS DURATION AGE
myjob 0/1 9s 9s
當(dāng)前 SUCCESSFUL 的 Pod 數(shù)量為 0,查看 Pod 的狀態(tài):
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myjob-5fjdh 0/1 ContainerCannotRun 0 2m36s
myjob-7wfjz 0/1 ContainerCannotRun 0 2m
myjob-9w96k 0/1 ContainerCannotRun 0 59s
myjob-chlxz 0/1 ContainerCannotRun 0 100s
myjob-gdqbg 0/1 ContainerCannotRun 0 2m23s
可以看到有多個(gè) Pod,狀態(tài)均不正常。kubectl describe pod 查看某個(gè) Pod 的啟動(dòng)日志:
[root@k8s-master ~]# kubectl describe pod myjob-5fjdh
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled <unknown> default-scheduler Successfully assigned default/myjob-5fjdh to k8s-master
Normal Pulling 3m22s kubelet, k8s-master Pulling image "busybox"
Normal Pulled 3m13s kubelet, k8s-master Successfully pulled image "busybox"
Normal Created 3m12s kubelet, k8s-master Created container hello
Warning Failed 3m12s kubelet, k8s-master Error: failed to start container "hello": Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"error command\": executable file not found in $PATH": unknown
日志顯示沒(méi)有可執(zhí)行程序,符合我們的預(yù)期。
下面解釋一個(gè)現(xiàn)象:為什么 kubectl get pod 會(huì)看到這么多個(gè)失敗的 Pod?
原因是:當(dāng)?shù)谝粋€(gè) Pod 啟動(dòng)時(shí),容器失敗退出,根據(jù) restartPolicy: Never,此失敗容器不會(huì)被重啟,但 Job DESIRED 的 Pod 是 1 (DESIRED 是期望的意思,即期望的Pod是1),目前 SUCCESSFUL 為 0,不滿足,所以 Job controller 會(huì)啟動(dòng)新的 Pod,直到 SUCCESSFUL 為 1。對(duì)于我們這個(gè)例子,SUCCESSFUL 永遠(yuǎn)也到不了 1,所以 Job controller 會(huì)一直創(chuàng)建新的 Pod。為了終止這個(gè)行為,只能刪除 Job。
[root@k8s-master ~]# kubectl delete -f myjob.yml
job.batch "myjob" deleted
如果將 restartPolicy 設(shè)置為 OnFailure 會(huì)怎么樣?下面我們實(shí)踐一下,修改 myjob.yml 后重新啟動(dòng)。
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myjob-m5h8w 0/1 CrashLoopBackOff 4 3m57s
這里只有一個(gè) Pod,不過(guò) RESTARTS 為 4,而且不斷增加,說(shuō)明 OnFailure 生效,容器失敗后會(huì)自動(dòng)重啟。
Job 兩種重啟策略
(1) 如果在 Pod 模板中定義 restartPolicy=Never ,那么離線作業(yè)失敗后,Pod 永遠(yuǎn)都不應(yīng)該被重啟,但是會(huì)嘗試創(chuàng)建新的 Pod。
(2) 如果在 Pod 模板中定義 restartPolicy=OnFailure,那么離線作業(yè)失敗后,Job Controller 就不會(huì)去嘗試創(chuàng)建新的 Pod,但是會(huì)不斷地嘗試重啟 Pod 里的容器,但是受到 spec.backoffLimit 字段限定重試次數(shù)。
并行執(zhí)行 Job
有時(shí),我們希望能同時(shí)運(yùn)行多個(gè) Pod,提高 Job 的執(zhí)行效率。這個(gè)可以通過(guò) parallelism 設(shè)置。
[root@k8s-master ~]# cat job.yml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
parallelism: 2
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
command: ["echo","hello k8s job"]
restartPolicy: OnFailure
這里我們將并行的 Pod 數(shù)量設(shè)置為 2,實(shí)踐一下:
[root@k8s-master ~]# kubectl apply -f job.yml
job.batch/myjob created
[root@k8s-master ~]# kubectl get job
NAME COMPLETIONS DURATION AGE
myjob 0/1 of 2 18s 18s
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myjob-5fjdh 0/1 Completed 0 21s
myjob-tdhxz 0/1 Completed 0 21s
Job 一共啟動(dòng)了兩個(gè) Pod,而且 AGE 相同,可見是并行運(yùn)行的。
我們還可以通過(guò) completions 設(shè)置 Job 成功完成 Pod 的總數(shù):
spec:
completions: 6
parallelism: 2
上面配置的含義是:每次運(yùn)行兩個(gè) Pod,直到總共有 6 個(gè) Pod 成功完成。實(shí)踐一下:
[root@k8s-master ~]# kubectl get job
NAME COMPLETIONS DURATION AGE
myjob 2/6 22s 22s
[root@k8s-master ~]# kubectl get job
NAME COMPLETIONS DURATION AGE
myjob 5/6 33s 33s
[root@k8s-master ~]# kubectl get job
NAME COMPLETIONS DURATION AGE
myjob 6/6 35s 42s
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myjob-7wfjz 0/1 Completed 0 49s
myjob-9w96k 0/1 Completed 0 29s
myjob-chlxz 0/1 Completed 0 44s
myjob-cqgd2 0/1 Completed 0 25s
myjob-gdqbg 0/1 Completed 0 49s
myjob-m5h8w 0/1 Completed 0 22s
DESIRED 和 SUCCESSFUL 均為 6,符合預(yù)期。如果不指定 completions 和 parallelism,默認(rèn)值均為 1。
上面的例子只是為了演示 Job 的并行特性,實(shí)際用途不大。不過(guò)現(xiàn)實(shí)中確實(shí)存在很多需要并行處理的場(chǎng)景。比如批處理程序,每個(gè)副本(Pod)都會(huì)從任務(wù)池中讀取任務(wù)并執(zhí)行,副本越多,執(zhí)行時(shí)間就越短,效率就越高。這種類似的場(chǎng)景都可以用 Job 來(lái)實(shí)現(xiàn)。
尾聲
本文知識(shí)點(diǎn)(DaemonSet):
(1) DaemonSet 通過(guò) nodeAffinity 和 Toleration 這兩個(gè)調(diào)度器的小功能,保證了每個(gè)節(jié)點(diǎn)上有且只有一個(gè) Pod。
(2) DaemonSet 使用 ControllerRevision,來(lái)保存和管理自己對(duì)應(yīng)的“版本”。這種“面向 API 對(duì)象”的設(shè)計(jì)思路,大大簡(jiǎn)化了控制器本身的邏輯,也正是 Kubernetes 項(xiàng)目“聲明式 API”的優(yōu)勢(shì)所在。
StatefulSet 也是直接控制 Pod 對(duì)象的,也是使用 ControllerRevision 進(jìn)行版本管理 [ControllerRevision 其實(shí)是 k8s 中一個(gè)通用的版本管理對(duì)象]
四種控制器管理Pod
Deployment - ReplicaSet - Pod
Statefulset - Pod
Job - Pod
Statefulset - Pod
只有Deployment是間接管理Pod,其他三種都是直接管理Pod
本文知識(shí)點(diǎn)(Job):文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-573141.html
- Job沒(méi)有選擇器:跟其他控制器不同的是,Job 對(duì)象并不要求你定義一個(gè) spec.selector 來(lái)描述要控制哪些 Pod。原因是:這個(gè) Job 對(duì)象在創(chuàng)建后,它的 Pod 模板,被自動(dòng)加上了一個(gè) controller-uid=< 一個(gè)隨機(jī)字符串 > 這樣的 Label。而這個(gè) Job 對(duì)象本身,則被自動(dòng)加上了這個(gè) Label 對(duì)應(yīng)的 Selector,從而 保證了 Job 與它所管理的 Pod 之間的匹配關(guān)系。而 Job Controller 之所以要使用這種攜帶了 UID 的 Label,就是為了避免不同 Job 對(duì)象所管理的 Pod 發(fā)生重合。需要注意的是,這種自動(dòng)生成的 Label 對(duì)用戶來(lái)說(shuō)并不友好,所以不太適合推廣到 Deployment 等長(zhǎng)作業(yè)編排對(duì)象上。
- Job重啟策略:在 Deployment 對(duì)象里,restartPolicy 則只允許被設(shè)置為 Always;restartPolicy 在 Job 對(duì)象里只允許被設(shè)置為 Never 和 OnFailure。
- 如果在 Pod 模板中定義 restartPolicy=Never ,那么離線作業(yè)失敗后,Pod 永遠(yuǎn)都不應(yīng)該被重啟,但是會(huì)嘗試創(chuàng)建新的 Pod。
- 如果在 Pod 模板中定義 restartPolicy=OnFailure,那么離線作業(yè)失敗后,Job Controller 就不會(huì)去嘗試創(chuàng)建新的 Pod,但是會(huì)不斷地嘗試重啟 Pod 里的容器,但是受到 spec.backoffLimit 字段限定重試次數(shù)。
- Job重試間隔時(shí)間與正常執(zhí)行時(shí)間限制:如果Job失敗,Job Controller 重新創(chuàng)建 Pod 的間隔是呈指數(shù)增加的;即使 Job 正常執(zhí)行,通過(guò) spec.activeDeadlineSeconds 字段可以設(shè)置最長(zhǎng)運(yùn)行時(shí)間,一旦超過(guò)時(shí)間,自動(dòng)停止
- 無(wú)論restartPolicy設(shè)置為Never還是OnFailure,Job的成功或失敗狀態(tài)都會(huì)通過(guò)Job的狀態(tài)(.status)字段中的條件(.status.conditions)來(lái)表示。
- Job可以作為并行執(zhí)行,通過(guò) completions 和 parallelism 兩個(gè)屬性設(shè)置。
參考資料:DaemonSet守護(hù)進(jìn)程
初識(shí)Job
Job執(zhí)行文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-573141.html
到了這里,關(guān)于Kubernetes_Pod_DaemonSet與Job/CronJob的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!