今天分享一下【Kube.NETes】 DaemonSet 詳解,豐富個人簡歷,提高面試level,給自己增加一點談資,秒變面試小達人,BAT不是夢。
三分鐘你將學會:
- DaemonSet是什么?
- DaemonSet的應用場景
- DaemonSet 對象詳解
- DaemonSet的常見問題及解決方案
一、介紹DaemonSet
1、為什么需要DaemonSet
在 Kubernetes 集群中,通常需要在每個節點上運行守護進程來監視節點健康狀態、收集日志等,這些守護進程通常稱為系統級守護進程,如 Kubernetes Cluster Autoscaler 和 Kubernetes DNS。使用 Deployment 或 StatefulSet 可以創建 Pod,這些 Pod 可以被調度到任何節點上運行,但是在某些情況下,需要確保每個節點上都運行著一個 Pod 副本 版本,即需要使用 DaemonSet。
2、DaemonSet 簡介
DaemonSet 是 Kubernetes 中一種類型的控制器對象,用于在每個節點上運行一個 Pod 副本版本,確保每個節點上都有一個或多個 Pod 副本。DaemonSet 控制器可以保證在新增節點時自動在新增的節點上創建 Pod 副本,同時在節點刪除時,自動刪除該節點上的 Pod 副本。
3、DaemonSet 與其他 Kubernetes 對象的區別
DaemonSet 在運行時,會在集群中的每個節點上創建一個 Pod 副本,而其他的控制器如 Deployment 和 StatefulSet 創建的 Pod,會盡可能地讓它們在集群的不同節點上運行。另外,DaemonSet 在節點加入和退出時,會自動處理 Pod 的創建和刪除,因此可以保證在整個集群中的每個節點上都運行著一個 Pod 副本,適合用于運行集群服務的 daemon 容器或一些常駐內存的服務。而 Deployment 和 StatefulSet 更適用于部署需要動態擴縮容的應用程序。
對象類型 |
說明 |
控制器 |
Pods數量 |
Deployment |
管理多個 ReplicaSets,用于應用程序的版本控制和滾動升級 |
控制器 |
可以控制多個Pods數量 |
StatefulSet |
用于有狀態應用程序,例如數據庫,確保每個實例具有唯一的網絡標識符和穩定的存儲 |
控制器 |
可以控制多個Pods數量 |
DaemonSet |
用于運行守護進程(如日志收集器),它會在每個工作節點上運行一個副本 |
控制器 |
等于節點數量 |
Job |
用于批處理任務,例如數據轉換或任務調度 |
無 |
一次性 |
CronJob |
用于定期執行具有固定時間間隔的任務 |
無 |
可以控制多個Pods數量 |
二、創建DaemonSet
1、使用 kubectl 命令創建 DaemonSet
使用 kubectl 命令創建 DaemonSet 的步驟如下:
(1)使用以下命令創建 YAML 文件:
apiVersion: Apps/v1
kind: DaemonSet
metadata:
name: example-daemonset
labels:
app: example
spec:
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
containers:
- name: example-container
image: Nginx
在此示例中,我們創建了一個名為 "example-daemonset" 的 DaemonSet,并使用 Nginx 容器作為模板。請注意,標簽 "app: example" 在這里起到關鍵作用,因為它將用于選擇要運行此 DaemonSet 的節點。
(2)使用以下命令創建 DaemonSet:
kubectl create -f example-daemonset.yaml
這將使用先前創建的 YAML 文件創建 DaemonSet。您可以通過使用以下命令來驗證是否已創建 DaemonSet:
kubectl get daemonsets
如果您看到 "example-daemonset",則表示 DaemonSet 已成功創建。
2、使用 YAML 文件創建DaemonSet
要使用 YAML 文件創建 DaemonSet,請執行以下步驟:
(1)創建一個 YAML 文件并使用以下內容:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: example-daemonset
labels:
app: example
spec:
selector:
matchLabels:
app: example
template:
metadata:
labels:
app: example
spec:
containers:
- name: example-container
image: nginx
在此示例中,我們創建了一個名為 "example-daemonset" 的 DaemonSet,并使用 Nginx 容器作為模板。請注意,標簽 "app: example" 在這里起到關鍵作用,因為它將用于選擇要運行此 DaemonSet 的節點。
(2)使用以下命令創建 DaemonSet:
kubectl apply -f example-daemonset.yaml
這將使用先前創建的 YAML 文件創建 DaemonSet。您可以通過使用以下命令來驗證是否已創建 DaemonSet:
kubectl get daemonsets
如果您看到 "example-daemonset",則表示 DaemonSet 已成功創建。
3、使用 Terraform 創建 DaemonSet
使用 Terraform 創建 DaemonSet 是一種自動化部署和管理 Kubernetes 應用程序的方法。Terraform 是一種基礎設施即代碼工具,允許您編寫代碼來定義和管理基礎設施。
要使用 Terraform 創建 DaemonSet,您需要做以下幾步:
- 安裝 Terraform 工具:請根據您的操作系統在 Terraform 官方網站(https://www.terraform.io/downloads.html)上查找相應的安裝指南。
- 創建一個 Terraform 項目:在一個新的目錄中,創建一個 main.tf 文件,并添加以下內容。
provider "kubernetes" {
config_context_cluster = "my-k8s-cluster"
}
resource "kubernetes_daemonset" "my-daemonset" {
metadata {
name = "my-daemonset"
}
spec {
selector {
match_labels = {
app = "my-daemonset"
}
}
template {
metadata {
labels = {
app = "my-daemonset"
}
}
spec {
containers {
name = "my-container"
image = "nginx:1.19.0-alpine"
ports {
name = "http"
container_port = 80
}
volume_mounts {
name = "html"
mount_path = "/usr/share/nginx/html"
}
}
volumes {
name = "html"
config_map {
name = "my-daemonset-configmap"
items {
key = "index.html"
path = "index.html"
}
}
}
}
}
}
}
此代碼將創建一個名為 my-daemonset 的 DaemonSet,該 DaemonSet 包含一個名為 my-container 的容器,并使用 Nginx 映像。
(3)初始化 Terraform 項目:使用以下命令初始化 Terraform 項目。
$ terraform init
(4)配置 Terraform 項目:使用以下命令配置 Terraform 項目。
$ terraform apply
此命令將使用 Terraform 創建 DaemonSet。
三、DaemonSet的應用場景
1、在節點上部署系統級守護進程
DaemonSet 最常見的應用場景之一是在節點上部署系統級守護進程。例如,Kubernetes 官方提供的 kube-proxy
和 kube-dns
組件都是以 DaemonSet 的形式運行在每個節點上的。這些組件是 Kubernetes 集群中非常重要的系統級進程,需要在每個節點上運行,以確保 Kubernetes 集群的正常運行。
以下是部署 kube-proxy DaemonSet 的示例 YAML 文件:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-proxy
namespace: kube-system
labels:
k8s-app: kube-proxy
spec:
selector:
matchLabels:
k8s-app: kube-proxy
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
k8s-app: kube-proxy
spec:
containers:
- name: kube-proxy
image: k8s.gcr.io/kube-proxy:v1.22.0
securityContext:
privileged: true
command:
- /usr/local/bin/kube-proxy
args:
- --cnotallow=/var/lib/kube-proxy/config.conf
volumeMounts:
- name: kube-proxy-config
mountPath: /var/lib/kube-proxy
volumes:
- name: kube-proxy-config
configMap:
name: kube-proxy
在這個 YAML 文件中,我們使用 apps/v1 API 版本創建了一個名為 kube-proxy 的 DaemonSet。它的 selector 字段指定了需要運行這個 DaemonSet 的 Pod 的標簽,updateStrategy 指定了更新策略,這里使用的是滾動更新。template 字段定義了 Pod 的模板,包括容器、掛載的卷和命令參數等。
2、在節點上運行普通容器
在 Kubernetes 集群中,通常有許多需要在每個節點上運行的容器,例如日志收集代理、監控代理、安全代理等。使用 DaemonSet 控制器,可以方便地在每個節點上運行這些容器。
下面是一個運行 fluentd 日志收集代理的 DaemonSet 的示例 YAML 文件:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
spec:
selector:
matchLabels:
name: fluentd
template:
metadata:
labels:
name: fluentd
spec:
containers:
- name: fluentd
image: fluent/fluentd:v1.7-1
volumeMounts:
- name: varlog
mountPath: /var/log
volumes:
- name: varlog
hostPath:
path: /var/log
在上面的 YAML 文件中,我們定義了一個名為 fluentd 的 DaemonSet。該 DaemonSet 會在每個節點上運行一個名為 fluentd 的容器。
該容器使用 fluent/fluentd:v1.7-1 鏡像,并將主機節點上的 /var/log 目錄掛載到容器中的 /var/log 目錄下。這樣,該容器就可以從節點上的日志文件中收集日志。
3、維護集群狀態
另一個常見的 DaemonSet 應用場景是維護集群狀態。在 Kubernetes 集群中,有許多需要在每個節點上運行的控制器,例如網絡插件、存儲插件、DNS 插件等。這些控制器通常需要在每個節點上運行,以維護集群狀態。
例如,CNI(Container Network Interface)插件負責為 Kubernetes 集群中的容器分配 IP 地址和路由信息,因此需要在每個節點上運行。Kubernetes 官方提供的 CNI 插件就是以 DaemonSet 的形式運行在每個節點上的。
下面是部署 CNI 插件的示例 YAML 文件:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-flannel-ds-amd64
namespace: kube-system
labels:
tier: node
app: flannel
spec:
selector:
matchLabels:
app: flannel
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: flannel
spec:
containers:
- name: kube-flannel
image: quay.io/coreos/flannel:v0.14.0
command:
- /opt/bin/flanneld
args:
- --ip-masq
- --kube-subnet-mgr
- --iface=enp0s8 # 這里需要根據實際網絡接口修改
securityContext:
privileged: true
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: flannel-cfg
mountPath: /etc/kube-flannel/
hostNetwork: true
volumes:
- name: flannel-cfg
configMap:
name: kube-flannel-cfg
在這個 YAML 文件中,我們使用 apps/v1 API 版本創建了一個名為 kube-flannel-ds-amd64 的 DaemonSet。它的 selector 字段指定了需要運行這個 DaemonSet 的 Pod 的標簽,updateStrategy 指定了更新策略,這里使用的是滾動更新。template 字段定義了 Pod 的模板,包括容器、掛載的卷和命令參數等。
這個 YAML 文件中定義了一個名為 kube-flannel 的容器,使用 quay.io/coreos/flannel:v0.14.0 鏡像,并將容器的網絡接口設置為 enp0s8。該容器還從 configMap 掛載了配置文件,并開啟了特權模式。
4、在節點上運行工具
除了守護進程、普通容器和控制器之外,還可以使用 DaemonSet 在每個節點上運行工具。這些工具通常用于調試、監控和診斷集群狀態。
例如,使用 DaemonSet在每個節點上運行診斷工具的示例 YAML 文件如下所示:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: diagnostic-tool
spec:
selector:
matchLabels:
app: diagnostic-tool
template:
metadata:
labels:
app: diagnostic-tool
spec:
containers:
- name: diagnostic-tool
image: your-Docker-image
command:
- sh
- -c
- |
while true; do
# do some diagnostic tasks
sleep 60
done
volumeMounts:
- name: host-var-run
mountPath: /host/var/run
readOnly: true
volumes:
- name: host-var-run
hostPath:
path: /var/run
在這個 YAML 文件中,我們定義了一個名為 diagnostic-tool 的 DaemonSet。該 DaemonSet 會在每個節點上運行一個名為 diagnostic-tool 的容器。
該容器使用一個自定義的 Docker 鏡像,并執行一個 while 循環,在其中執行一些診斷任務。此外,容器還將節點上的 /var/run 目錄掛載到容器中的 /host/var/run 目錄下,以便讀取節點上的運行時信息。
使用 DaemonSet 運行診斷工具可以快速定位節點和集群級別的問題,例如網絡問題、存儲問題和性能問題等。
四、DaemonSet 對象詳解
1、DaemonSet的結構及其各個部分的作用
DaemonSet 是 Kubernetes 中一種類型的控制器對象,用于在每個節點上運行一個 Pod 副本版本,確保每個節點上都有一個或多個 Pod 副本。DaemonSet 控制器可以保證在新增節點時自動在新增的節點上創建 Pod 副本,同時在節點刪除時,自動刪除該節點上的 Pod 副本。
在 DaemonSet 對象中,有以下幾個部分:
- metadata:元數據部分包含了對象的名稱、命名空間、標簽等信息。
- spec:規格部分包含了 DaemonSet 對象的規格,如選擇器、Pod 模板等。
- status:狀態部分包含了 DaemonSet 對象的當前狀態信息,如運行中的 Pod 數量等。
其中,spec 部分是 DaemonSet 對象中最重要的部分,它包含了以下幾個字段:
- selector:指定了哪些節點需要運行 Pod 副本。可以使用節點標簽選擇器指定節點的標簽,也可以使用節點名稱選擇器指定節點的名稱。
- template:指定了要運行在節點上的 Pod 模板。模板中可以指定容器鏡像、啟動命令等信息。
- updateStrategy:指定了 DaemonSet 的更新策略。默認情況下,DaemonSet 會在每個節點上運行一個 Pod 副本,如果需要更新 Pod 版本,則會逐個節點進行更新。可以使用 RollingUpdate 策略實現平滑的更新過程,也可以使用 OnDelete 策略實現在節點刪除時更新 Pod 版本。
2、DaemonSet 的生命周期
DaemonSet 的生命周期包括以下幾個階段:
- 創建 DaemonSet:使用 kubectl apply 或 kubectl create 命令創建一個 DaemonSet 對象。
- DaemonSet 控制器創建 Pod:當 DaemonSet 被創建時,DaemonSet 控制器會根據 Pod 模板創建 Pod 副本,并在每個節點上運行一個 Pod 副本。
- 新節點加入集群:當新的節點加入集群時,DaemonSet 控制器會自動在新節點上創建一個 Pod 副本,確保每個節點上都有一個 Pod 副本。
- 節點被刪除:當一個節點被刪除時,DaemonSet 控制器會自動刪除該節點上的 Pod 副本,以確保每個節點上都有一個 Pod 副本。
- 更新 DaemonSet:當需要更新 DaemonSet 時,可以使用 kubectl apply 或 kubectl edit 命令修改 DaemonSet 對象的配置。這會導致 DaemonSet 控制器創建新的 Pod 副本,并逐步替換舊的 Pod 副本,以確保每個節點上都有一個新的 Pod 副本。
- 刪除 DaemonSet:當不再需要 DaemonSet 時,可以使用 kubectl delete 命令刪除 DaemonSet 對象。此時,DaemonSet 控制器會刪除所有的 Pod 副本。
3、DaemonSet 的選擇器
選擇器是 DaemonSet 對象的一部分,用于確定在哪些節點上運行該 DaemonSet 的 Pod 副本。我們可以使用 Pod 模板中的標簽或注釋,或者在 DaemonSet 對象的選擇器中指定標簽或注釋,來確定選擇器。
以下是一個示例 DaemonSet YAML 文件,其中使用標簽選擇器來確定在哪些節點上運行該 DaemonSet 的 Pod 副本:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: my-daemonset
labels:
app: my-app
spec:
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-container
image: my-image
command: [ "sh", "-c", "echo Hello from the DaemonSet pod && sleep 3600" ]
nodeSelector:
disktype: ssd
在這個示例中,我們使用 nodeSelector 來指定只在磁盤類型為 ssd 的節點上運行該 DaemonSet 的 Pod 副本。
4、DaemonSet 的更新策略
更新策略用于確定如何更新 DaemonSet 對象的 Pod 副本。在 Kubernetes 中,有以下三種更新策略可供選擇:
- RollingUpdate:滾動更新。這是默認的更新策略。它允許您將 DaemonSet 的 Pod 副本版本逐個更新。即首先更新一個節點上的 Pod,然后等待它的更新成功后再更新下一個節點上的 Pod,以此類推。這種策略確保了在更新期間至少有一個 Pod 副本可用,從而最小化了服務的中斷時間。
- OnDelete:當 DaemonSet 對象的 Pod 副本有更新時,不會自動對舊的 Pod 副本進行更新。相反,舊的 Pod 副本將在刪除后自動被新的 Pod 副本替換。這種策略適用于不需要連續更新的應用程序。
- Pause:暫停更新。這種策略將停止 DaemonSet 的自動更新,直到您手動恢復更新。這種策略適用于需要手動控制更新過程的應用程序。
更新策略可以在 DaemonSet 對象的 spec 字段中進行配置。以下是一個使用 RollingUpdate 策略的 DaemonSet 示例:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx-daemonset
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
terminationGracePeriodSeconds: 30
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
在這個 YAML 文件中,我們定義了一個名為 nginx-daemonset 的 DaemonSet。該 DaemonSet 會在每個節點上運行一個名為 nginx 的容器。
該容器使用了最新版本的 Nginx 鏡像。容器在終止時有 30 秒的 grace period,以確保正在進行的請求可以完成。
該 DaemonSet 對象使用 RollingUpdate 策略,并指定了 maxUnavailable 選項,該選項指定了在更新期間最多允許一個 Pod 副本不可用。這確保了在更新期間始終有至少一個 Pod 副本可用。
五、DaemonSet 的常見問題及解決方案
1、容器無法在節點上創建
(1)問題描述
當您創建 DaemonSet 時,您可能會遇到以下錯誤:
Error creating: pods "XXX" is forbidden: node "YYY" cannot be used because it is unschedulable
這個錯誤消息表示,調度程序無法在某個節點上安排 DaemonSet Pod 的運行。通常,這是因為節點處于不可調度的狀態,例如它被標記為“已維護”或“故障”。
(2)解決方案
要解決這個問題,您需要檢查節點的狀態。您可以使用以下命令來檢查節點的狀態:
kubectl get nodes
如果節點的狀態是“維護”或“故障”,您需要將其恢復為可用狀態。您可以使用以下命令來將節點重新調度:
kubectl uncordon <node-name>
2、更新失敗
(1)問題描述
當您更新 DaemonSet 時,您可能會遇到以下錯誤:
Update failed. First seen error: error updating status for daemonset
這個錯誤消息表示,DaemonSet 更新失敗。通常,這是因為某個節點上的 Pod 處于不可用狀態,例如節點故障或容器崩潰。
(2)解決方案
要解決這個問題,您需要檢查節點和 Pod 的狀態。您可以使用以下命令來檢查節點和 Pod 的狀態:
kubectl get nodes
kubectl get pods -n <namespace>
如果您發現節點或 Pod 處于不可用狀態,您需要將其恢復為可用狀態。您可以使用以下命令來重新啟動節點或 Pod:
kubectl delete pod <pod-name> -n <namespace>
3、網絡配置問題
(1)問題描述
當您創建 DaemonSet 時,您可能會遇到以下錯誤:
Failed to create pod: <pod-name>
Error syncing pod
這個錯誤消息表示,Pod 同步失敗。通常,這是因為網絡配置不正確。
(2)解決方案
要解決這個問題,您需要檢查網絡配置。您可以使用以下命令來檢查網絡配置:
kubectl describe pod <pod-name> -n <namespace>
如果您發現網絡配置不正確,您需要更新它。您可以使用以下命令來更新網絡配置:
kubectl edit pod <pod-name> -n <namespace>
4、如何監視 DaemonSet 運行狀態
在 Kubernetes 中,監視 DaemonSet 的運行狀態可以通過以下幾種方式實現:
(1)使用 kubectl 命令行工具
kubectl 命令行工具提供了多種監視 DaemonSet 運行狀態的命令,如下所示:
- kubectl get daemonset:列出當前集群中所有的 DaemonSet。
- kubectl describe daemonset <daemonset-name>:查看指定 DaemonSet 的詳細信息,包括 Pod 的狀態、事件和控制器的狀態等。
- kubectl rollout status daemonset <daemonset-name>:查看 DaemonSet 的升級進度和狀態。
- kubectl logs <pod-name>:查看 Pod 的日志輸出。
(2)使用 Kubernetes Dashboard
Kubernetes Dashboard 提供了一個用戶友好的 Web 界面,可用于監視 DaemonSet 的運行狀態。在 Kubernetes Dashboard 中,可以查看所有 DaemonSet 和它們的 Pod,還可以查看各個 Pod 的詳細信息,包括 Pod 的日志輸出。
(3)使用 Prometheus 和 Grafana
Prometheus 和 Grafana 是流行的監視和指標收集工具,可以用于監視 DaemonSet 的運行狀態。通過 Prometheus 收集集群中的指標,使用 Grafana 可視化這些指標。可視化的指標包括 DaemonSet 的 Pod 數量、節點上的 CPU 使用情況和內存使用情況等。
5、如何排除問題和調試
在使用 DaemonSet 過程中,可能會遇到各種問題。以下是一些常見的問題及其解決方案。
(1)Pod 處于 Pending 狀態
當 DaemonSet 中的 Pod 處于 Pending 狀態時,有以下幾種可能的原因:
- 節點資源不足:Pod 需要的資源(例如 CPU、內存等)超出了節點可用資源。解決方法是添加更多的節點或調整 Pod 的資源請求。
- 節點標簽不匹配:如果 DaemonSet 指定了節點選擇器,但節點沒有匹配的標簽,則 Pod 將處于 Pending 狀態。解決方法是為節點添加匹配的標簽。
- Pod 調度失敗:如果沒有足夠的節點滿足 Pod 的調度要求,則 Pod 將處于 Pending 狀態。解決方法是添加更多的節點或調整 Pod 的調度要求。
(2)Pod 啟動失敗
當 DaemonSet 中的 Pod 啟動失敗時,有以下幾種可能的原因:
- 容器鏡像拉取失敗:Pod 配置中指定的容器鏡像不存在或拉取失敗。解決方法是確認容器鏡像的可用性,并檢查 Pod 配置中的容器鏡像名稱和版本是否正確。
- 容器啟動命令或參數不正確:如果容器的啟動命令或參數不正確,則容器將無法啟動。
- 解決方法是檢查 Pod 配置中的容器的啟動命令和參數是否正確。
- 容器配置錯誤:如果容器的配置文件存在錯誤,則容器可能無法啟動或啟動后立即崩潰。解決方法是檢查容器的配置文件是否正確,并重新啟動 Pod。
(3)Pod 運行時錯誤
當 DaemonSet 中的 Pod 運行時出現錯誤時,有以下幾種可能的原因:
- 容器內部錯誤:容器內部可能會發生錯誤,例如進程崩潰或配置文件錯誤。
- 解決方法是檢查容器日志,確定錯誤原因,并修復容器內部問題。
- 節點故障:如果節點出現故障,則節點上運行的所有 Pod 可能會受到影響。
- 解決方法是檢查節點的健康狀況,并在必要時重啟節點。
- 網絡問題:如果 Pod 無法與其他服務或資源通信,則可能存在網絡問題。解決方法是檢查網絡配置,確保 Pod 能夠訪問所需的服務或資源。
(4)如何排除問題和調試
在排除問題和調試時,可以使用以下方法:
- 查看日志:使用 kubectl logs 命令查看容器的日志,以了解容器內部發生的錯誤和故障。
- 使用 kubectl describe 命令:使用 kubectl describe 命令查看 Pod 和其他相關對象的詳細信息,以確定問題所在。
- 使用 kubectl exec 命令:使用 kubectl exec 命令在容器內部運行命令,以檢查容器的狀態和配置文件。
- 使用 kubectl get 命令:使用 kubectl get 命令查看集群中的對象,以確定節點和 Pod 的狀態。
- 使用 kubectl events 命令:使用 kubectl events 命令查看 Kubernetes 事件,以了解 Pod 和其他對象的狀態變化。
本文轉載自微信公眾號「哪吒編程」