kubernetes部署失敗的原因


1.錯誤的容器鏡像/非法的倉庫權限

其中兩個最普遍的問題是:(a)指定了錯誤的容器鏡像,(b)使用私有鏡像卻不提供倉庫認證信息。這在首次使用 Kubernetes 或者綁定 CI/CD 環境時尤其棘手。

讓我們看個例子。首先我們創建一個名為 fail 的 deployment,它指向一個不存在的 Docker 鏡像:

$ kubectl run fail --image=rosskukulinski /dne :v1.0.0

然后我們查看 Pods,可以看到有一個狀態為 ErrImagePull 或者 ImagePullBackOff 的 Pod:

$ kubectl get pods
NAME                    READY     STATUS             RESTARTS   AGE
fail-1036623984-hxoas   0 /1        ImagePullBackOff   0          2m

想查看更多信息,可以 describe 這個失敗的 Pod: 

$ kubectl describe pod fail-1036623984-hxoas

查看 describe 命令的輸出中 Events 這部分,我們可以看到如下內容:

Events:
FirstSeen    LastSeen    Count   From                        SubObjectPath       Type        Reason      Message
---------    --------    -----   ----                        -------------       --------    ------      -------
5m        5m      1   {default-scheduler }                            Normal      Scheduled   Successfully assigned fail-1036623984-hxoas to gke-nrhk-1-default-pool-a101b974-wfp7
5m        2m      5   {kubelet gke-nrhk-1-default-pool-a101b974-wfp7} spec.containers{fail}   Normal      Pulling     pulling image  "rosskukulinski/dne:v1.0.0"
5m        2m      5   {kubelet gke-nrhk-1-default-pool-a101b974-wfp7} spec.containers{fail}   Warning     Failed      Failed to pull image  "rosskukulinski/dne:v1.0.0" : Error: image rosskukulinski /dne  not found
5m        2m      5   {kubelet gke-nrhk-1-default-pool-a101b974-wfp7}             Warning     FailedSync  Error syncing pod, skipping: failed to  "StartContainer"  for  "fail"  with ErrImagePull:  "Error: image rosskukulinski/dne not found"
 
5m    11s 19  {kubelet gke-nrhk-1-default-pool-a101b974-wfp7} spec.containers{fail}   Normal  BackOff     Back-off pulling image  "rosskukulinski/dne:v1.0.0"
5m    11s 19  {kubelet gke-nrhk-1-default-pool-a101b974-wfp7}             Warning FailedSync  Error syncing pod, skipping: failed to  "StartContainer"  for  "fail"  with ImagePullBackOff:  "Back-off pulling image \"rosskukulinski/dne:v1.0.0\""

顯示錯誤的那句話:Failed to pull image "rosskukulinski/dne:v1.0.0": Error: image rosskukulinski/dne not found 告訴我們 Kubernetes無法找到鏡像 rosskukulinski/dne:v1.0.0。

為什么 Kubernetes 拉不下來鏡像?除了網絡連接問題外,還有三個主要元凶: 

  • 鏡像 tag 不正確
  • 鏡像不存在(或者是在另一個倉庫)
  • Kubernetes 沒有權限去拉那個鏡

如果你沒有注意到你的鏡像 tag 的拼寫錯誤,那么最好就用你本地機器測試一下。

通常我會在本地開發機上,用 docker pull 命令,帶上 完全相同的鏡像 tag,來跑一下。比如上面的情況,我會運行命令 docker pull rosskukulinski/dne:v1.0.0。

  • 如果這成功了,那么很可能 Kubernetes 沒有權限去拉取這個鏡像。參考鏡像拉取 Secrets 來解決這個問題。
  • 如果失敗了,那么我會繼續用不顯式帶 tag 的鏡像測試 - docker pull rosskukulinski/dne - 這會嘗試拉取 tag 為 latest 的鏡像。如果這樣成功,表明原來指定的 tag 不存在。這可能是人為原因,拼寫錯誤,或者 CI/CD 的配置錯誤。


如果 docker pull rosskukulinski/dne(不指定 tag)也失敗了,那么我們碰到了一個更大的問題:我們所有的鏡像倉庫中都沒有這個鏡像。默認情況下,Kubernetes 使用 Dockerhub 鏡像倉庫,如果你在使用 Quay.ioAWS ECR,或者 Google Container Registry,你要在鏡像地址中指定這個倉庫的 URL,比如使用 Quay,鏡像地址就變成 quay.io/rosskukulinski/dne:v1.0.0

 
如果你在使用 Dockerhub,那你應該再次確認你發布鏡像到 Dockerhub 的系統,確保名字和 tag 匹配你的 deployment 正在使用的鏡像。

注意:觀察 Pod 狀態的時候,鏡像缺失和倉庫權限不正確是沒法區分的。其它情況下,Kubernetes 將報告一個 ErrImagePull 狀態。

 

2. 應用啟動之后又掛掉

無論你是在 Kubernetes 上啟動新應用,還是遷移應用到已存在的平台,應用在啟動之后就掛掉都是一個比較常見的現象。

我們創建一個 deployment,它的應用會在1秒后掛掉:

$ kubectl run crasher --image=rosskukulinski /crashing-app

我們看一下 Pods 的狀態:

$ kubectl get pods
NAME                       READY     STATUS             RESTARTS   AGE
crasher-2443551393-vuehs   0 /1        CrashLoopBackOff   2          54s

CrashLoopBackOff 告訴我們,Kubernetes 正在盡力啟動這個 Pod,但是一個或多個容器已經掛了,或者正被刪除。

讓我們 describe 這個 Pod 去獲取更多信息

$ kubectl describe pod crasher-2443551393-vuehs
Name:        crasher-2443551393-vuehs
Namespace:    fail
Node:        gke-nrhk-1-default-pool-a101b974-wfp7 /10 .142.0.2
Start Time:    Fri, 10 Feb 2017 14:20:29 -0500
Labels:        pod-template- hash =2443551393
     run=crasher
Status:        Running
IP:        10.0.0.74
Controllers:    ReplicaSet /crasher-2443551393
Containers:
crasher:
Container ID:    docker: //51c940ab32016e6d6b5ed28075357661fef3282cb3569117b0f815a199d01c60
Image:        rosskukulinski /crashing-app
Image ID:        docker: //sha256 :cf7452191b34d7797a07403d47a1ccf5254741d4bb356577b8a5de40864653a5
Port:       
State:        Terminated
   Reason:        Error
   Exit Code:    1
   Started:        Fri, 10 Feb 2017 14:22:24 -0500
   Finished:        Fri, 10 Feb 2017 14:22:26 -0500
Last State:        Terminated
   Reason:        Error
   Exit Code:    1
   Started:        Fri, 10 Feb 2017 14:21:39 -0500
   Finished:        Fri, 10 Feb 2017 14:21:40 -0500
Ready:        False
Restart Count:    4
...

好可怕,Kubernetes 告訴我們這個 Pod 正被 Terminated,因為容器里的應用掛了。我們還可以看到應用的 Exit Code 是 1。后面我們可能還會看到一個 OOMKilled 錯誤。

我們的應用正在掛掉?為什么?

首先我們查看應用日志。假定你發送應用日志到 stdout(事實上你也應該這么做),你可以使用 kubectl logs 看到應用日志:

$ kubectl logs crasher-2443551393-vuehs

不幸的是,這個 Pod 沒有任何日志。這可能是因為我們正在查看一個新起的應用實例,因此我們應該查看前一個容器:

$ kubectl logs crasher-2443551393-vuehs --previous

什么!我們的應用仍然不給我們任何東西。這個時候我們應該給應用加點啟動日志了,以幫助我們定位這個問題。我們也可以本地運行一下這個容器,以確定是否缺失環境變量或者掛載卷。

 

 

3. 缺失 ConfigMap 或者 Secret

Kubernetes 最佳實踐建議通過 ConfigMaps 或者 Secrets 傳遞應用的運行時配置。這些數據可以包含數據庫認證信息,API endpoints,或者其它配置信息。

一個常見的錯誤是,創建的 deployment 中引用的 ConfigMaps 或者 Secrets 的屬性不存在,有時候甚至引用的 ConfigMaps 或者 Secrets 本身就不存在。

缺失 ConfigMap

第一個例子,我們將嘗試創建一個 Pod,它加載 ConfigMap 數據作為環境變量:

# configmap-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: configmap-pod
spec:
containers:
- name:  test -container
   image: gcr.io /google_containers/busybox
   command : [  "/bin/sh" "-c" "env"  ]
   env :
     - name: SPECIAL_LEVEL_KEY
       valueFrom:
         configMapKeyRef:
           name: special-config
           key: special.how

讓我們創建一個 Pod:kubectl create -f configmap-pod.yaml。在等待幾分鍾之后,我們可以查看我們的 Pod:

$ kubectl get pods
NAME            READY     STATUS              RESTARTS   AGE
configmap-pod   0 /1        RunContainerError   0          3s

Pod 狀態是 RunContainerError 。我們可以使用 kubectl describe 了解更多:

$ kubectl describe pod configmap-pod
[...]
Events:
FirstSeen    LastSeen    Count   From                        SubObjectPath           Type        Reason      Message
---------    --------    -----   ----                        -------------           --------    ------      -------
20s        20s     1   {default-scheduler }                                Normal      Scheduled   Successfully assigned configmap-pod to gke-ctm-1-sysdig2-35e99c16-tgfm
19s        2s      3   {kubelet gke-ctm-1-sysdig2-35e99c16-tgfm}   spec.containers{ test -container} Normal      Pulling     pulling image  "gcr.io/google_containers/busybox"
18s        2s      3   {kubelet gke-ctm-1-sysdig2-35e99c16-tgfm}   spec.containers{ test -container} Normal      Pulled      Successfully pulled image  "gcr.io/google_containers/busybox"
18s        2s      3   {kubelet gke-ctm-1-sysdig2-35e99c16-tgfm}                   Warning     FailedSync  Error syncing pod, skipping: failed to  "StartContainer"  for  "test-container"  with RunContainerError:  "GenerateRunContainerOptions: configmaps \"special-config\" not found"

Events 章節的最后一條告訴我們什么地方錯了。Pod 嘗試訪問名為 special-config 的 ConfigMap,但是在該 namespace 下找不到。一旦我們創建這個 ConfigMap,Pod 應該重啟並能成功拉取運行時數據。
在 Pod 規格說明中訪問 Secrets 作為環境變量會產生相似的錯誤,就像我們在這里看到的 ConfigMap錯誤一樣。
但是假如你通過 Volume 來訪問 Secrets 或者 ConfigMap會發生什么呢?

缺失 Secrets

下面是一個pod規格說明,它引用了名為 myothersecret 的 Secrets,並嘗試把它掛為卷:

# missing-secret.yaml
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
spec:
containers:
- name:  test -container
   image: gcr.io /google_containers/busybox
   command : [  "/bin/sh" "-c" "env"  ]
   volumeMounts:
     - mountPath:  /etc/secret/
       name: myothersecret
restartPolicy: Never
volumes:
- name: myothersecret
   secret:
     secretName: myothersecret

讓我們用 kubectl create -f missing-secret.yaml 來創建一個 Pod。

幾分鍾后,我們 get Pods,可以看到 Pod 仍處於 ContainerCreating 狀態:

$ kubectl get pods
NAME            READY     STATUS              RESTARTS   AGE
secret-pod   0 /1        ContainerCreating   0          4h

這就奇怪了。我們 describe 一下,看看到底發生了什么: 

$ kubectl describe pod secret-pod
Name:        secret-pod
Namespace:    fail
Node:        gke-ctm-1-sysdig2-35e99c16-tgfm /10 .128.0.2
Start Time:    Sat, 11 Feb 2017 14:07:13 -0500
Labels:       
Status:        Pending
IP:       
Controllers:   
 
[...]
 
Events:
FirstSeen    LastSeen    Count   From                        SubObjectPath   Type        Reason      Message
---------    --------    -----   ----                        -------------   --------    ------      -------
18s        18s     1   {default-scheduler }                        Normal      Scheduled   Successfully assigned secret-pod to gke-ctm-1-sysdig2-35e99c16-tgfm
18s        2s      6   {kubelet gke-ctm-1-sysdig2-35e99c16-tgfm}           Warning     FailedMount MountVolume.SetUp failed  for  volume  "kubernetes.io/secret/337281e7-f065-11e6-bd01-42010af0012c-myothersecret"  (spec.Name:  "myothersecret" ) pod  "337281e7-f065-11e6-bd01-42010af0012c"  (UID:  "337281e7-f065-11e6-bd01-42010af0012c" ) with: secrets  "myothersecret"  not found

Events 章節再次解釋了問題的原因。它告訴我們 Kubelet 無法從名為 myothersecret 的 Secret 掛卷。為了解決這個問題,我們可以創建 myothersecret,它包含必要的安全認證信息。一旦 myothersecret 創建完成,容器也將正確啟動。

 

4. 活躍度/就緒狀態探測失敗

在 Kubernetes 中處理容器問題時,開發者需要學習的重要一課是,你的容器應用是 running 狀態,不代表它在工作。

Kubernetes 提供了兩個基本特性,稱作活躍度探測和就緒狀態探測。本質上來說,活躍度/就緒狀態探測將定期地執行一個操作(例如發送一個 HTTP 請求,打開一個 tcp 連接,或者在你的容器內運行一個命令),以確認你的應用和你預想的一樣在工作。

如果活躍度探測失敗,Kubernetes 將殺掉你的容器並重新創建一個。如果就緒狀態探測失敗,這個 Pod 將不會作為一個服務的后端 endpoint,也就是說不會流量導到這個 Pod,直到它變成 Ready。

如果你試圖部署變更你的活躍度/就緒狀態探測失敗的應用,滾動部署將一直懸掛,因為它將等待你的所有 Pod 都變成 Ready。

這個實際是怎樣的情況?以下是一個 Pod 規格說明,它定義了活躍度/就緒狀態探測方法,都是基於8080端口對 /healthy 路由進行健康檢查:

apiVersion: v1
kind: Pod
metadata:
name: liveness-pod
spec:
containers:
- name:  test -container
   image: rosskukulinski /leaking-app
   livenessProbe:
     httpGet:
       path:  /healthz
       port: 8080
     initialDelaySeconds: 3
     periodSeconds: 3
   readinessProbe:
     httpGet:
       path:  /healthz
       port: 8080
     initialDelaySeconds: 3
     periodSeconds: 3

讓我們創建這個 Pod:kubectl create -f liveness.yaml,過幾分鍾后查看發生了什么:

$ kubectl get pods
NAME           READY     STATUS    RESTARTS   AGE
liveness-pod    0 / 1        Running    4           2m 

2分鍾以后,我們發現 Pod 仍然沒處於 Ready 狀態,並且它已被重啟了4次。讓我們 describe 一下查看更多信息:

$ kubectl describe pod liveness-pod
Name:        liveness-pod
Namespace:    fail
Node:        gke-ctm- 1 -sysdig2-35e99c16-tgfm/ 10.128 . 0.2
Start Time:    Sat,  11  Feb  2017  14 : 32 : 36  - 0500
Labels:       
Status:        Running
IP:         10.108 . 88.40
Controllers:   
Containers:
test-container:
Container ID:    docker: //8fa6f99e6fda6e56221683249bae322ed864d686965dc44acffda6f7cf186c7b
Image:        rosskukulinski/leaking-app
Image ID:        docker: //sha256:7bba8c34dad4ea155420f856cd8de37ba9026048bd81f3a25d222fd1d53da8b7
Port:       
State:        Running
   Started:        Sat,  11  Feb  2017  14 : 40 : 34  - 0500
Last State:        Terminated
   Reason:        Error
   Exit Code:     137
   Started:        Sat,  11  Feb  2017  14 : 37 : 10  - 0500
   Finished:        Sat,  11  Feb  2017  14 : 37 : 45  - 0500
[...]
Events:
FirstSeen    LastSeen    Count   From                        SubObjectPath           Type        Reason      Message
---------    --------    -----   ----                        -------------           --------    ------      -------
8m        8m       1    { default -scheduler }                                Normal      Scheduled   Successfully assigned liveness-pod to gke-ctm- 1 -sysdig2-35e99c16-tgfm
8m        8m       1    {kubelet gke-ctm- 1 -sysdig2-35e99c16-tgfm}   spec.containers{test-container} Normal      Created     Created container with docker id 0fb5f1a56ea0; Security:[seccomp=unconfined]
8m        8m       1    {kubelet gke-ctm- 1 -sysdig2-35e99c16-tgfm}   spec.containers{test-container} Normal      Started     Started container with docker id 0fb5f1a56ea0
7m        7m       1    {kubelet gke-ctm- 1 -sysdig2-35e99c16-tgfm}   spec.containers{test-container} Normal      Created     Created container with docker id 3f2392e9ead9; Security:[seccomp=unconfined]
7m        7m       1    {kubelet gke-ctm- 1 -sysdig2-35e99c16-tgfm}   spec.containers{test-container} Normal      Killing     Killing container with docker id 0fb5f1a56ea0: pod  "liveness-pod_fail(d75469d8-f090-11e6-bd01-42010af0012c)"  container  "test-container"  is unhealthy, it will be killed and re-created.
8m    16s  10   {kubelet gke-ctm- 1 -sysdig2-35e99c16-tgfm}   spec.containers{test-container} Warning Unhealthy   Liveness probe failed: Get http: //10.108.88.40:8080/healthz: dial tcp 10.108.88.40:8080: getsockopt: connection refused
8m    1s   85   {kubelet gke-ctm- 1 -sysdig2-35e99c16-tgfm}   spec.containers{test-container} Warning Unhealthy   Readiness probe failed: Get http: //10.108.88.40:8080/healthz: dial tcp 10.108.88.40:8080: getsockopt: connection refused

Events 章節再次救了我們。我們可以看到活躍度探測和就緒狀態探測都失敗了。關鍵的一句話是 container "test-container" is unhealthy, it will be killed and re-created。這告訴我們 Kubernetes 正在殺這個容器,因為容器的活躍度探測失敗了。

這里有三種可能性:

  1. 你的探測不正確,健康檢查的 URL 是否改變了?
  2. 你的探測太敏感了, 你的應用是否要過一會才能啟動或者響應?
  3. 你的應用永遠不會對探測做出正確響應,你的數據庫是否配置錯了

查看 Pod 日志是一個開始調測的好地方。一旦你解決了這個問題,新的 deployment 應該就能成功了。

 

5. 超出CPU/內存的限制

 Kubernetes 賦予集群管理員限制 Pod 和容器的 CPU 或內存數量的能力。作為應用開發者,你可能不清楚這個限制,導致 deployment 失敗的時候一臉困惑。

我們試圖部署一個未知 CPU/memory 請求限額的 deployment:

# gateway.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: gateway
spec:
template:
metadata:
   labels:
     app: gateway
spec:
   containers:
     - name: test-container
       image: nginx
       resources:
         requests:
           memory: 5Gi

你會看到我們設了 5Gi 的資源請求。讓我們創建這個 deployment:kubectl create -f gateway.yaml。

現在我們可以看到我們的 Pod: 

$ kubectl get pods
No resources found.

為啥,讓我們用 describe 來觀察一下我們的 deployment:

$ kubectl describe deployment/gateway
Name:            gateway
Namespace:        fail
CreationTimestamp:    Sat,  11  Feb  2017  15 : 03 : 34  - 0500
Labels:            app=gateway
Selector:        app=gateway
Replicas:         0  updated |  1  total |  0  available |  1  unavailable
StrategyType:        RollingUpdate
MinReadySeconds:     0
RollingUpdateStrategy:     0  max unavailable,  1  max surge
OldReplicaSets:       
NewReplicaSet:        gateway- 764140025  ( 0 / 1  replicas created)
Events:
FirstSeen    LastSeen    Count   From                SubObjectPath   Type        Reason          Message
---------    --------    -----   ----                -------------   --------    ------          -------
4m        4m       1    {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica set gateway- 764140025  to  1

基於最后一行,我們的 deployment 創建了一個 ReplicaSet(gateway-764140025) 並把它擴展到 1。這個是用來管理 Pod 生命周期的實體。我們可以 describe 這個 ReplicaSet:

$ kubectl describe rs/gateway- 764140025
Name:        gateway- 764140025
Namespace:    fail
Image(s):    nginx
Selector:    app=gateway,pod-template-hash= 764140025
Labels:        app=gateway
     pod-template-hash= 764140025
Replicas:     0  current /  1  desired
Pods Status:     0  Running /  0  Waiting /  0  Succeeded /  0  Failed
No volumes.
Events:
FirstSeen    LastSeen    Count   From                SubObjectPath   Type        Reason      Message
---------    --------    -----   ----                -------------   --------    ------      -------
6m        28s      15   {replicaset-controller }            Warning     FailedCreate    Error creating: pods  "gateway-764140025-"  is forbidden: [maximum memory usage per Pod is 100Mi, but request is  5368709120 ., maximum memory usage per Container is 100Mi, but request is 5Gi.]

哈知道了。集群管理員設置了每個 Pod 的最大內存使用量為 100Mi(好一個小氣鬼!)。你可以運行 kubectl describe limitrange 來查看當前租戶的限制。

你現在有3個選擇:

  1. 要求你的集群管理員提升限額
  2. 減少 deployment 的請求或者限額設置
  3. 直接編輯限額

6. 資源配額

和資源限額類似,Kubernetes 也允許管理員給每個 namespace 設置資源配額。這些配額可以在 Pods,Deployments,PersistentVolumes,CPU,內存等資源上設置軟性或者硬性限制。

讓我們看看超出資源配額后會發生什么。以下是我們的 deployment 例子:

# test-quota.yaml
apiVersion: extensions /v1beta1
kind: Deployment
metadata:
name: gateway- quota
spec:
template:
spec:
   containers:
     - name:  test -container
       image: nginx

我們可用 kubectl create -f test-quota.yaml 創建,然后觀察我們的 Pods: 

$ kubectl get pods
NAME                            READY     STATUS    RESTARTS   AGE
gateway- quota -551394438-pix5d   1 /1        Running   0          16s

看起來很好,現在讓我們擴展到 3 個副本:kubectl scale deploy/gateway-quota --replicas=3,然后再次觀察 Pods:

$ kubectl get pods
NAME                            READY     STATUS    RESTARTS   AGE
gateway- quota -551394438-pix5d   1 /1        Running   0          9m

啊,我們的pod去哪了?讓我們觀察一下 deployment:

$ kubectl describe deploy /gateway-quota
Name:            gateway- quota
Namespace:        fail
CreationTimestamp:    Sat, 11 Feb 2017 16:33:16 -0500
Labels:            app=gateway
Selector:        app=gateway
Replicas:        1 updated | 3 total | 1 available | 2 unavailable
StrategyType:        RollingUpdate
MinReadySeconds:    0
RollingUpdateStrategy:    1 max unavailable, 1 max surge
OldReplicaSets:       
NewReplicaSet:        gateway- quota -551394438 (1 /3  replicas created)
Events:
FirstSeen    LastSeen    Count   From                SubObjectPath   Type        Reason          Message
---------    --------    -----   ----                -------------   --------    ------          -------
9m        9m      1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica  set  gateway- quota -551394438 to 1
5m        5m      1   {deployment-controller }            Normal      ScalingReplicaSet   Scaled up replica  set  gateway- quota -551394438 to 3

在最后一行,我們可以看到 ReplicaSet 被告知擴展到 3 。我們用 describe 來觀察一下這個 ReplicaSet 以了解更多信息:

kubectl describe replicaset gateway- quota -551394438
Name:        gateway- quota -551394438
Namespace:    fail
Image(s):    nginx
Selector:    app=gateway,pod-template- hash =551394438
Labels:        app=gateway
     pod-template- hash =551394438
Replicas:    1 current / 3 desired
Pods Status:    1 Running / 0 Waiting / 0 Succeeded / 0 Failed
No volumes.
Events:
FirstSeen    LastSeen    Count   From                SubObjectPath   Type        Reason          Message
---------    --------    -----   ----                -------------   --------    ------          -------
11m        11m     1   {replicaset-controller }            Normal      SuccessfulCreate    Created pod: gateway- quota -551394438-pix5d
11m        30s     33  {replicaset-controller }            Warning     FailedCreate        Error creating: pods  "gateway-quota-551394438-"  is forbidden: exceeded  quota : compute-resources, requested: pods=1, used: pods=1, limited: pods=1

哦!我們的 ReplicaSet 無法創建更多的 pods 了,因為配額限制了:exceeded quota: compute-resources, requested: pods=1, used: pods=1, limited: pods=1。

和資源限額類似,我們也有 3 個選項:

  1. 要求集群管理員提升該 namespace 的配額
  2. 刪除或者收縮該 namespace 下其它的 deployment
  3. 直接編輯配額

7. 集群資源不足

除非你的集群開通了集群自動伸縮功能,否則總有一天你的集群中 CPU 和內存資源會耗盡。

這不是說 CPU 和內存被完全使用了,而是指它們被 Kubernetes 調度器完全使用了。如同我們在第 5 點看到的,集群管理員可以限制開發者能夠申請分配給 pod 或者容器的 CPU 或者內存的數量。聰明的管理員也會設置一個默認的 CPU/內存 申請數量,在開發者未提供申請額度時使用。

如果你所有的工作都在 default 這個 namespace 下工作,你很可能有個默認值 100m 的容器 CP U申請額度,對此你甚至可能都不清楚。運行 kubectl describe ns default 檢查一下是否如此。

我們假定你的 Kubernetes 集群只有一個包含 CPU 的節點。你的 Kubernetes 集群有 1000m 的可調度 CPU。

當前忽略其它的系統 pods(kubectl -n kube-system get pods),你的單節點集群能部署 10 個 pod(每個 pod 都只有一個包含 100m 的容器)。

10 Pods * (1 Container * 100m) = 1000m == Cluster CPUs

當你擴大到 11 個的時候,會發生什么?

下面是一個申請 1CPU(1000m)的 deployment 例子:

# cpu-scale.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: cpu-scale
spec:
template:
metadata:
   labels:
     app: cpu-scale
spec:
   containers:
     - name: test-container
       image: nginx
       resources:
         requests:
           cpu:  1

我把這個應用部署到有 2 個可用 CPU 的集群。除了我的 cpu-scale 應用,Kubernetes 內部服務也在消耗 CPU 和內存。

我們可以用 kubectl create -f cpu-scale.yaml 部署這個應用,並觀察 pods:

$ kubectl get pods
NAME                        READY     STATUS    RESTARTS   AGE
cpu-scale- 908056305 -xstti    1 / 1        Running    0           5m

第一個 pod 被調度並運行了。我們看看擴展一個會發生什么:

$ kubectl scale deploy/cpu-scale --replicas= 2
deployment  "cpu-scale"  scaled
$ kubectl get pods
NAME                        READY     STATUS    RESTARTS   AGE
cpu-scale- 908056305 -phb4j    0 / 1        Pending    0           4m
cpu-scale- 908056305 -xstti    1 / 1        Running    0           5m

我們的第二個pod一直處於 Pending,被阻塞了。我們可以 describe 這第二個 pod 查看更多的信息:

$ kubectl describe pod cpu-scale- 908056305 -phb4j
Name:        cpu-scale- 908056305 -phb4j
Namespace:    fail
Node:        gke-ctm- 1 -sysdig2-35e99c16-qwds/ 10.128 . 0.4
Start Time:    Sun,  12  Feb  2017  08 : 57 : 51  - 0500
Labels:        app=cpu-scale
     pod-template-hash= 908056305
Status:        Pending
IP:       
Controllers:    ReplicaSet/cpu-scale- 908056305
[...]
Events:
FirstSeen    LastSeen    Count   From            SubObjectPath   Type        Reason          Message
---------    --------    -----   ----            -------------   --------    ------          -------
3m        3m       1    { default -scheduler }            Warning     FailedScheduling    pod (cpu-scale- 908056305 -phb4j) failed to fit in any node
fit failure on node (gke-ctm- 1 -sysdig2-35e99c16-wx0s): Insufficient cpu
fit failure on node (gke-ctm- 1 -sysdig2-35e99c16-tgfm): Insufficient cpu
fit failure on node (gke-ctm- 1 -sysdig2-35e99c16-qwds): Insufficient cpu

好吧,Events 模塊告訴我們 Kubernetes 調度器(default-scheduler)無法調度這個 pod 因為它無法匹配任何節點。它甚至告訴我們每個節點哪個擴展點失敗了(Insufficient cpu)。

那么我們如何解決這個問題?如果你太渴望你申請的 CPU/內存 的大小,你可以減少申請的大小並重新部署。當然,你也可以請求你的集群管理員擴展這個集群(因為很可能你不是唯一一個碰到這個問題的人)。

現在你可能會想:我們的 Kubernetes 節點是在我們的雲提供商的自動伸縮群組里,為什么他們沒有生效呢?

原因是,你的雲提供商沒有深入理解 Kubernetes 調度器是做啥的。利用 Kubernetes 的集群自動伸縮能力允許你的集群根據調度器的需求自動伸縮它自身。如果你在使用 GCE,集群伸縮能力是一個 beta 特性

 

8. 持久化卷掛載失敗

另一個常見錯誤是創建了一個引用不存在的持久化卷(PersistentVolumes)的 deployment。不論你是使用 PersistentVolumeClaims(你應該使用這個!),還是直接訪問持久化磁盤,最終結果都是類似的。

下面是我們的測試 deployment,它想使用一個名為 my-data-disk 的 GCE 持久化卷:

# volume-test.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: volume-test
spec:
template:
metadata:
   labels:
     app: volume-test
spec:
   containers:
     - name: test-container
       image: nginx
       volumeMounts:
       - mountPath: /test
         name: test-volume
   volumes:
   - name: test-volume
     # This GCE PD must already exist (oops!)
     gcePersistentDisk:
       pdName: my-data-disk
       fsType: ext4

讓我們創建這個 deployment:kubectl create -f volume-test.yaml,過幾分鍾后查看 pod:

kubectl get pods
NAME                           READY     STATUS              RESTARTS   AGE
volume-test- 3922807804 -33nux    0 / 1        ContainerCreating    0           3m

3 分鍾的等待容器創建時間是很長了。讓我們用 describe 來查看這個 pod,看看到底發生了什么:

$ kubectl describe pod volume-test- 3922807804 -33nux
Name:        volume-test- 3922807804 -33nux
Namespace:    fail
Node:        gke-ctm- 1 -sysdig2-35e99c16-qwds/ 10.128 . 0.4
Start Time:    Sun,  12  Feb  2017  09 : 24 : 50  - 0500
Labels:        app=volume-test
     pod-template-hash= 3922807804
Status:        Pending
IP:       
Controllers:    ReplicaSet/volume-test- 3922807804
[...]
Volumes:
test-volume:
Type:    GCEPersistentDisk (a Persistent Disk resource in Google Compute Engine)
PDName:    my-data-disk
FSType:    ext4
Partition:     0
ReadOnly:     false
[...]
Events:
FirstSeen    LastSeen    Count   From                        SubObjectPath   Type        Reason      Message
---------    --------    -----   ----                        -------------   --------    ------      -------
4m        4m       1    { default -scheduler }                        Normal      Scheduled   Successfully assigned volume-test- 3922807804 -33nux to gke-ctm- 1 -sysdig2-35e99c16-qwds
1m        1m       1    {kubelet gke-ctm- 1 -sysdig2-35e99c16-qwds}           Warning     FailedMount Unable to mount volumes  for  pod  "volume-test-3922807804-33nux_fail(e2180d94-f12e-11e6-bd01-42010af0012c)" : timeout expired waiting  for  volumes to attach/mount  for  pod  "volume-test-3922807804-33nux" / "fail" . list of unattached/unmounted volumes=[test-volume]
1m        1m       1    {kubelet gke-ctm- 1 -sysdig2-35e99c16-qwds}           Warning     FailedSync  Error syncing pod, skipping: timeout expired waiting  for  volumes to attach/mount  for  pod  "volume-test-3922807804-33nux" / "fail" . list of unattached/unmounted volumes=[test-volume]
3m        50s      3    {controller-manager }                       Warning     FailedMount Failed to attach volume  "test-volume"  on node  "gke-ctm-1-sysdig2-35e99c16-qwds"  with: GCE persistent disk not found: diskName= "my-data-disk"  zone= "us-central1-a"

很神奇! Events 模塊留有我們一直在尋找的線索。我們的 pod 被正確調度到了一個節點(Successfully assigned volume-test-3922807804-33nux to gke-ctm-1-sysdig2-35e99c16-qwds),但是那個節點上的 kubelet 無法掛載期望的卷 test-volume。那個卷本應該在持久化磁盤被關聯到這個節點的時候就被創建了,但是,正如我們看到的,controller-manager 失敗了:Failed to attach volume "test-volume" on node "gke-ctm-1-sysdig2-35e99c16-qwds" with: GCE persistent disk not found: diskName="my-data-disk" zone="us-central1-a"。

 最后一條信息相當清楚了:為了解決這個問題,我們需要在 GKE 的 us-central1-a 區中創建一個名為 my-data-disk 的持久化卷。一旦這個磁盤創建完成,controller-manager 將掛載這塊磁盤,並啟動容器創建過程。 

 

9. 校驗錯誤

看着整個 build-test-deploy 任務到了 deploy 步驟卻失敗了,原因竟是 Kubernetes 對象不合法。還有什么比這更讓人沮喪的!

 你可能之前也碰到過這種錯誤:

$ kubectl create -f test-application.deploy.yaml
error: error validating  "test-application.deploy.yaml" : error validating data: found invalid field resources  for  v1.PodSpec;  if  you choose to ignore these errors, turn validation off with --validate= false

 在這個例子中,我嘗試創建以下 deployment:

# test-application.deploy.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: test-app
spec:
template:
metadata:
   labels:
     app: test-app
spec:
   containers:
   - image: nginx
     name: nginx
   resources:
     limits:
       cpu: 100m
       memory: 200Mi
     requests:
       cpu: 100m
       memory: 100Mi

 

一眼望去,這個 YAML 文件是正確的,但錯誤消息會證明是有用的。錯誤說的是 found invalid field resources for v1.PodSpec,再仔細看一下 v1.PodSpec, 我們可以看到 resource 對象變成了 v1.PodSpec 的一個子對象。事實上它應該是 v1.Container 的子對象。在把 resource 對象縮進一層后,這個 deployment 對象就可以正常工作了。

除了查找縮進錯誤,另一個常見的錯誤是寫錯了對象名(比如 peristentVolumeClaim 寫成了 persistentVolumeClaim)。這個錯誤曾經在我們時間很趕的時候絆住了我和另一位高級工程師。

為了能在早期就發現這些錯誤,我推薦在 pre-commit 鈎子或者構建的測試階段添加一些校驗步驟。

例如,你可以:

  1. 用 python -c 'import yaml,sys;yaml.safe_load(sys.stdin)' < test-application.deployment.yaml 驗證 YAML 格式
  2. 使用標識 --dry-run 來驗證 Kubernetes API 對象,比如這樣:kubectl create -f test-application.deploy.yaml --dry-run --validate=true


重要提醒:校驗 Kubernetes 對象的機制是在服務端的校驗,這意味着 kubectl 必須有一個在工作的 Kubernetes 集群與之通信。不幸的是,當前 kubectl 還沒有客戶端的校驗選項,但是已經有 issue(kubernetes/kubernetes #29410 和 kubernetes/kubernetes #11488)在跟蹤這個缺失的特性了。

 

10. 容器鏡像沒有更新

我了解的在使用 Kubernetes 的大多數人都碰到過這個問題,它也確實是一個難題。

這個場景就像下面這樣:

  1. 使用一個鏡像 tag(比如:rosskulinski/myapplication:v1) 創建一個 deployment
  2. 注意到 myapplication 鏡像中存在一個 bug
  3. 構建了一個新的鏡像,並推送到了相同的 tag(rosskukulinski/myapplication:v1)
  4. 刪除了所有 myapplication 的 pods,新的實例被 deployment 創建出了
  5. 發現 bug 仍然存在
  6. 重復 3-5 步直到你抓狂為止


這個問題關系到 Kubernetes 在啟動 pod 內的容器時是如何決策是否做 docker pull 動作的。

在 v1.Container 說明中,有一個選項 ImagePullPolicy:

Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise.

因為我們把我們的鏡像 tag 標記為 :v1,默認的鏡像拉取策略是 IfNotPresent。Kubelet 在本地已經有一份 rosskukulinski/myapplication:v1 的拷貝了,因此它就不會在做 docker pull 動作了。當新的 pod 出現的時候,它仍然使用了老的有問題的鏡像。

有三個方法來解決這個問題:

  1. 切成 :latest tag(千萬不要這么做!
  2. deployment 中指定 ImagePullPolicy: Always
  3. 使用唯一的 tag(比如基於你的代碼版本控制器的 commit id)


在開發階段或者要快速驗證原型的時候,我會指定 ImagePullPolicy: Always 這樣我可以使用相同的 tag 來構建和推送。

然而,在我的產品部署階段,我使用基於 Git SHA-1 的唯一 tag。這樣很容易查到產品部署的應用使用的源代碼。

總結

哇哦,我們有這么多地方要當心。到目前為止,你應該已經成為一個能定位,識別和修復失敗的 Kubernetes 部署的專家了。

一般來說,大部分常見的部署失敗都可以用下面的命令定位出來:

  • kubectl describe deployment/<deployname>
  • kubectl get events --namespace=<namespace>
  • kubectl describe replicaset/<rsname>
  • kubectl get pods
  • kubectl describe pod/<podname>
  • kubectl logs <podname> --previous


在追求自動化,把我從繁瑣的定位工作中解放出來的過程中,我寫了一個 bash 腳本,它在 CI/CD 的部署過程中任何失敗的時候,都可以跑。在 Jenkins/CircleCI 等的構建輸出中,將顯示有用的 Kubernetes 信息,幫助開發者快速找到任何明顯的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM