Pod對象基本概念
Pod,實際上是在扮演傳統基礎設施里“虛擬機”的角色;而容器,則是這個虛擬機里運行的用戶程序。
Kubernetes 集群中的 Pod 可被用於以下兩個主要用途:
- 運行單個容器的 Pod。"每個 Pod 一個容器"模型是最常見的 Kubernetes 用例;在這種情況下,可以將 Pod 看作單個容器的包裝器,並且 Kubernetes 直接管理 Pod,而不是容器。
- 運行多個協同工作的容器的 Pod。 Pod 可能封裝由多個緊密耦合且需要共享資源的共處容器組成的應用程序。一個Pod可能包含一個或者多個緊密相連的應用,這些應用可能是在同一個物理主機或虛擬機上。同一個Pod中的應用可以共享磁盤,磁盤是Pod級的,應用可以通過文件系統調用,額外的,一個Pod可能會定義頂級的cgroup隔離。
Pod的生命周期
Pod 生命周期的變化,主要體現在 Pod API 對象的 Status 部分,它有如下幾種可能的情況:
- Pending。這個狀態意味着,Pod 的 YAML 文件已經提交給了 Kubernetes,API 對象已經被創建並保存在 Etcd 當中。但是,這個 Pod 里有些容器因為某種原因而不能被順利創建。比如,調度不成功。
- Running。這個狀態下,Pod 已經調度成功,跟一個具體的節點綁定。它包含的容器都已經創建成功,並且至少有一個正在運行中。
- Succeeded。這個狀態意味着,Pod 里的所有容器都正常運行完畢,並且已經退出了。這種情況在運行一次性任務時最為常見。
- Failed。這個狀態下,Pod 里至少有一個容器以不正常的狀態(非 0 的返回碼)退出。這個狀態的出現,意味着你得想辦法 Debug 這個容器的應用,比如查看 Pod 的 Events 和日志。
- Unknown。這是一個異常狀態,意味着 Pod 的狀態不能持續地被 kubelet 匯報給 kube-apiserver,這很有可能是主從節點(Master 和 Kubelet)間的通信出現了問題。
Pod 對象的 Status 字段,還可以再細分出一組 Conditions。這些細分狀態的值包括:PodScheduled、Ready、Initialized,以及 Unschedulable。它們主要用於描述造成當前 Status 的具體原因是什么。
Pod中的資源限制
Kubernetes通過cgroups限制容器的CPU和內存等計算資源,包括requests和limits。
如果 Pod 運行所在的節點具有足夠的可用資源,容器可能(且可以)使用超出對應資源 request
屬性所設置的資源量,request屬性主要目的是為容器預留資源。不過,容器不可以使用超出其資源 limit
屬性所設置的資源量。
Pod 中的每個容器都可以指定以下的一個或者多個值:
spec.containers[].resources.limits.cpu
spec.containers[].resources.limits.memory
spec.containers[].resources.limits.hugepages-<size>
spec.containers[].resources.requests.cpu
spec.containers[].resources.requests.memory
spec.containers[].resources.requests.hugepages-<size>
CPU 資源的約束和請求以 cpu 為單位。Kubernetes 中的一個 cpu 等於雲平台上的 1 個 vCPU/核和裸機 Intel 處理器上的 **1 個超線程 **。CPU 總是按絕對數量來請求的,不可以使用相對數量; 0.1 的 CPU 在單核、雙核、48 核的機器上的意義是一樣的。
內存的約束和請求以字節為單位。你可以使用以下后綴之一以一般整數或定點整數形式來表示內存: E、P、T、G、M、K。
如果沒有設置一個CPU或內存的限制,那么這個container在當節點使用的資源是沒有上限的。但是如果設置了默認container資源限制,那么就會被限制在默認值之下。
Pod中的網絡資源
Pod是一個邏輯概念,是一組共享了某些資源的容器。Pod 里的所有容器,共享的是同一個 Network Namespace,並且可以聲明共享同一個 Volume。
在 Kubernetes 項目里有一個中間容器,這個容器叫作 Infra 容器。Infra 容器永遠都是第一個被創建的容器,而其他用戶定義的容器,則通過 Join Network Namespace 的方式,與 Infra 容器關聯在一起。
在 Kubernetes 項目里,Infra 容器一定要占用極少的資源,所以它使用的是一個非常特殊的鏡像,叫作:k8s.gcr.io/pause。這個鏡像是一個用匯編語言編寫的、永遠處於“暫停”狀態的容器,解壓后的大小也只有 100~200 KB 左右。
如上圖所示,這個 Pod 里有兩個用戶容器 A 和 B,還有一個 Infra 容器。在 Infra 容器“Hold 住”Network Namespace 后,用戶容器就可以加入到 Infra 容器的 Network Namespace 當中了。
這也就意味着,對於 Pod 里的容器 A 和容器 B 來說:
- 它們可以直接使用 localhost 進行通信;
- 它們可以直接使用 localhost 進行通信;
- 它們看到的網絡設備跟 Infra 容器看到的完全一樣;
- 一個 Pod 只有一個 IP 地址,也就是這個 Pod 的 Network Namespace 對應的 IP 地址;
- 當然,其他的所有網絡資源,都是一個 Pod 一份,並且被該 Pod 中的所有容器共享;
- Pod 的生命周期只跟 Infra 容器一致,而與容器 A 和 B 無關。
所以對於同一個 Pod 里面的所有用戶容器來說,它們的進出流量,也可以認為都是通過 Infra 容器完成的。
Pod中的Volume
Volume也是設計在 Pod 層級。一個 Volume 對應的宿主機目錄對於 Pod 來說就只有一個,Pod 里的容器只要聲明掛載這個 Volume,就一定可以共享這個 Volume 對應的宿主機目錄。
apiVersion: v1
kind: Pod
metadata:
name: two-containers
spec:
restartPolicy: Never
volumes:
- name: shared-data
hostPath:
path: /data
containers:
- name: nginx-container
image: nginx
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
- name: debian-container
image: debian
volumeMounts:
- name: shared-data
mountPath: /pod-data
command: ["/bin/sh"]
args: ["-c", "echo Hello from the debian container > /pod-data/index.html"]
在這個例子中,debian-container 和 nginx-container 都聲明掛載了 shared-data 這個 Volume。而 shared-data 是 hostPath 類型。所以,它對應在宿主機上的目錄就是:/data。而這個目錄,其實就被同時綁定掛載進了上述兩個容器當中。這樣,nginx-container 可以從它的 /usr/share/nginx/html 目錄中,讀取到 debian-container 生成的 index.html 文件。
注意:這里有多個container,那么如果想進入其中一個container中,可以用如下命令:
kubectl exec -it two-containers --container debian-container -- /bin/bash
Pod中重要字段含義與用法
NodeSelector
是一個供用戶將 Pod 與 Node 進行綁定的字段,用法如下所示:
apiVersion: v1
kind: Pod
...
spec:
nodeSelector:
disktype: ssd
這樣所創建的Pod只能運行在攜帶了“disktype: ssd”標簽(Label)的節點上;否則,它將調度失敗。
HostAliases
定義了 Pod 的 hosts 文件(比如 /etc/hosts)里的內容
apiVersion: v1
kind: Pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
...
這樣,這個 Pod 啟動后,/etc/hosts 文件的內容將如下所示:
cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3 foo.remote
10.1.2.3 bar.remote
在k8s項目中, 如果要設置hosts 文件里的內容,一定要通過這種方法。否則,如果直接修改了 hosts 文件的話,在 Pod 被刪除重建之后,kubelet 會自動覆蓋掉被修改的內容。
shareProcessNamespace
表示這個 Pod 里的容器要共享 PID Namespace。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
shareProcessNamespace: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true
在這個 YAML 文件中定義了兩個容器:一個是 nginx 容器,一個是開啟了 tty 和 stdin 的 shell 容器。
在這個pod被創建后,我們可以使用attach命令進入到container中與之進行交互:
# 創建
$ kubectl create -f nginx.yaml
# 連接到shell
$ kubectl attach -it nginx -c shell
#查看正在運行的進程
/ # ps ax
PID USER TIME COMMAND
1 root 0:00 /pause
8 root 0:00 nginx: master process nginx -g daemon off;
14 101 0:00 nginx: worker process
15 root 0:00 sh
21 root 0:00 ps ax
從上面輸出結果可以看到整個 Pod 里的每個容器的進程,對於所有容器來說都是可見的:它們共享了同一個 PID Namespace。
除此之外,我們還可以定義共享宿主機的 Network、IPC 和 PID Namespace。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
hostNetwork: true
hostIPC: true
hostPID: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true
這個 Pod 里的所有容器,會直接使用宿主機的網絡、直接與宿主機進行 IPC 通信、看到宿主機里正在運行的所有進程。
ImagePullPolicy
定義了鏡像拉取的策略,它是一個 Container 級別的屬性。
ImagePullPolicy 的值默認是 IfNotPresent,即每次創建 Pod 都重新拉取一次鏡像。另外,當容器的鏡像是類似於 nginx 或者 nginx:latest 這樣的名字時,ImagePullPolicy 會被認為Always ,即總是會去拉取最新的鏡像。
Lifecycle
可以在容器狀態發生變化時觸發一系列“鈎子”。
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
上面的配置中,postStart指的是在容器啟動后,立刻執行一個指定的操作;preStop是容器被殺死之前(比如,收到了 SIGKILL 信號)。
Pod中的init containers
一個Pod中可以有多個container,所以它也能有一個或多個init container,init container能在其他container開始前運行。每個init container都會運行完成后下一個container才能開始。
如果Pod中的init container運行失敗,Kubernetes 會重啟Pod直到init container運行成功;如果這個Pod的restartPolicy策略是Never,那么Kubernetes 在init container運行失敗后將不會啟動。
下面我們舉個例子:
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox:1.28
command: ['sh', '-c', 'echo The app is running! && sleep 3600']
initContainers:
- name: init-myservice
image: busybox:1.28
command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
- name: init-mydb
image: busybox:1.28
command: ['sh', '-c', "until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done"]
這個pod中運行了兩個init container,init-myservice和init-mydb,他們都會在容器運行時執行一個命令,只有當這個命令執行完成了才能繼續往下運行。
啟動這個pod:
kubectl apply -f myapp.yaml
查看pod狀態:
kubectl get -f myapp.yaml
NAME READY STATUS RESTARTS AGE
myapp-pod 0/1 Init:0/2 0 6m
可以看到這個pod一直在init中。
我們查看一下詳情:
kubectl describe -f myapp.yaml
...
Init Containers:
init-myservice:
[...]
State: Running
[...]
init-mydb:
[...]
State: Waiting
Reason: PodInitializing
Ready: False
...
Containers:
myapp-container:
...
State: Waiting
Reason: PodInitializing
Ready: False
Restart Count: 0
Environment: <none>
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled <unknown> default-scheduler Successfully assigned default/myapp-pod to 192.168.13.130
Normal Pulling 41s kubelet, 192.168.13.130 Pulling image "busybox:1.28"
Normal Pulled 27s kubelet, 192.168.13.130 Successfully pulled image "busybox:1.28"
Normal Created 27s kubelet, 192.168.13.130 Created container init-myservice
Normal Started 27s kubelet, 192.168.13.130 Started container init-myservice
我們可以看到init container是依次往下執行,init-myservice處於running,init-mydb處於Waiting狀態。myapp-container也是處於Waiting狀態。
我們查看一下日志:
kubectl logs myapp-pod -c init-myservice
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local
waiting for myservice
nslookup: can't resolve 'myservice.default.svc.cluster.local'
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local
...
kubectl logs myapp-pod -c init-mydb
Error from server (BadRequest): container "init-mydb" in pod "myapp-pod" is waiting to start: PodInitializing
可以看到init-myservice一直在循環執行nslookup,init-mydb還沒初始化。
現在我們實現兩個service:
---
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376
---
apiVersion: v1
kind: Service
metadata:
name: mydb
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9377
在上面這個yaml運行之后,我們在看看myapp pod狀態
kubectl get -f myapp.yaml
NAME READY STATUS RESTARTS AGE
myapp-pod 1/1 Running 0 9m
發現已經成功運行。
Pod中的Projected Volume
Projected Volume是用來為容器提供預先定義好的數據。
到目前為止,Kubernetes 支持的 Projected Volume 一共有四種:
- Secret;
- ConfigMap;
- Downward API;
- ServiceAccountToken。
Secret
作用是把 Pod 想要訪問的加密數據,存放到 Etcd 中。然后,你就可以通過在 Pod 的容器里掛載 Volume 的方式,訪問到這些 Secret 里保存的信息了。
先申明一下user和pass:
$ cat ./username.txt
admin
$ cat ./password.txt
c1oudc0w!
$ kubectl create secret generic user --from-file=./username.txt
$ kubectl create secret generic pass --from-file=./password.txt
username.txt 和 password.txt 文件里,存放的就是用戶名和密碼;而 user 和 pass,則是我為 Secret 對象指定的名字。
如果要查看secret的話,使用kubectl get就可以
$ kubectl get secrets
NAME TYPE DATA AGE
user Opaque 1 51s
pass Opaque 1 51s
除此之外,還可以使用YAML 文件的方式來創建這個 Secret 對象,比如:
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
user: YWRtaW4=
pass: MWYyZDFlMmU2N2Rm
需要注意的是,Secret 對象要求這些數據必須是經過 Base64 轉碼的,以免出現明文密碼的安全隱患。
然后創建一個pod,使用secret
如下:
apiVersion: v1
kind: Pod
metadata:
name: test-projected-volume
spec:
containers:
- name: test-secret-volume
image: busybox
args:
- sleep
- "86400"
volumeMounts:
- name: mysql-cred
mountPath: "/projected-volume"
readOnly: true
volumes:
- name: mysql-cred
projected:
sources:
- secret:
name: user
- secret:
name: pass
然后在容器中,查看:
$ kubectl exec -it test-projected-volume -- /bin/sh
$ ls /projected-volume/
user
pass
$ cat /projected-volume/user
root
$ cat /projected-volume/pass
1f2d1e2e67df
像這樣通過掛載方式進入到容器里的 Secret,一旦其對應的 Etcd 里的數據被更新,這些 Volume 里的文件內容,同樣也會被更新。
ConfigMap
ConfigMap 保存的是不需要加密的、應用所需的配置信息。
比如,一個 Java 應用所需的配置文件(.properties 文件),就可以通過下面這樣的方式保存在 ConfigMap 里:
# .properties文件的內容
$ cat example/ui.properties
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
# 從.properties文件創建ConfigMap
$ kubectl create configmap ui-config --from-file=example/ui.properties
# 查看這個ConfigMap里保存的信息(data)
$ kubectl get configmaps ui-config -o yaml
apiVersion: v1
data:
ui.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
name: ui-config
...
Downward API
可以讓 Pod 里的容器能夠直接獲取到這個 Pod API 對象本身的信息。
apiVersion: v1
kind: Pod
metadata:
name: test-downwardapi-volume
labels:
zone: us-est-coast
cluster: test-cluster1
rack: rack-22
spec:
containers:
- name: client-container
image: k8s.gcr.io/busybox
command: ["sh", "-c"]
args:
- while true; do
if [[ -e /etc/podinfo/labels ]]; then
echo -en '\n\n'; cat /etc/podinfo/labels; fi;
sleep 5;
done;
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
readOnly: false
volumes:
- name: podinfo
projected:
sources:
- downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
通過這樣的聲明方式,當前 Pod 的 Labels 字段的值,就會被 Kubernetes 自動掛載成為容器里的 /etc/podinfo/labels 文件。
Downward API 支持的字段已經非常豐富了,比如:
1. 使用fieldRef可以聲明使用:
spec.nodeName - 宿主機名字
status.hostIP - 宿主機IP
metadata.name - Pod的名字
metadata.namespace - Pod的Namespace
status.podIP - Pod的IP
spec.serviceAccountName - Pod的Service Account的名字
metadata.uid - Pod的UID
metadata.labels['<KEY>'] - 指定<KEY>的Label值
metadata.annotations['<KEY>'] - 指定<KEY>的Annotation值
metadata.labels - Pod的所有Label
metadata.annotations - Pod的所有Annotation
2. 使用resourceFieldRef可以聲明使用:
容器的CPU limit
容器的CPU request
容器的memory limit
容器的memory request
Service Account
Service Account 對象的作用,就是 Kubernetes 系統內置的一種“服務賬戶”,它是 Kubernetes 進行權限分配的對象。
Service Account 的授權信息和文件保存在它所綁定的一個特殊的 Secret 對象ServiceAccountToken中。
如果你查看一下任意一個運行在 Kubernetes 集群里的 Pod,就會發現,每一個 Pod,都已經自動聲明一個類型是 Secret、名為 default-token-xxxx 的 Volume,然后 自動掛載在每個容器的一個固定目錄上。比如:
$ kubectl describe pod nginx-deployment-5c678cfb6d-lg9lw
Containers:
...
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-s8rbq (ro)
Volumes:
default-token-s8rbq:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-s8rbq
Optional: false
這個 Secret 類型的 Volume,正是默認 Service Account 對應的 ServiceAccountToken。
一旦 Pod 創建完成,容器里的應用就可以直接從這個默認 ServiceAccountToken 的掛載目錄里訪問到授權信息和文件。這個容器內的路徑在 Kubernetes 里是固定的,即:/var/run/secrets/kubernetes.io/serviceaccount。
Pod中的健康檢查和恢復機制
Pod中的健康檢查是kubectl在對Container的一個定期的檢查。主要有三種實現方式:
- 命令執行ExecAction:在Container中會定期的執行一行命令,如果這個命令能執行成功,那么就認為是健康的;
- TCP Socket Action:對Container中的ip和端口進行TCP檢查,如果端口是通的,那么就認為是健康的;
- HTTP GET Action:通過http請求的方式來檢查Container中的服務是否正常。
下面我們看一個命令執行的例子:
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: test-liveness-exec
spec:
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
在這個 Pod 中在 /tmp 目錄下創建了一個 healthy 文件30 s 過后,它會把這個文件刪除掉。並且還定義了livenessProbe,然后會根據執行的命令在容器啟動 5 s 后開始執行(initialDelaySeconds: 5),每 5 s 執行一次(periodSeconds: 5),如果命令執行成功就認為是健康的。
創建好后,可以過30s看一下狀態:
kubectl describe pod test-liveness-exec
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
2s 2s 1 {kubelet worker0} spec.containers{liveness} Warning Unhealthy Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
event里面會報告容器是不健康的。
然后在看一下Pod狀態:
$ kubectl get pod test-liveness-exec
NAME READY STATUS RESTARTS AGE
liveness-exec 1/1 Running 1 1m
發現Pod已經被重啟過一次了。這也就是Pod 恢復機制,它是 Pod 的 Spec 部分的一個標准字段(pod.spec.restartPolicy),默認值是 Always,即:任何時候這個容器發生了異常,它一定會被重新創建。
注意:Pod 的恢復過程,永遠都是發生在當前節點上,而不會跑到別的節點上去。
Pod 恢復機制有以下幾種:
-
Always:在任何情況下,只要容器不在運行狀態,就自動重啟容器;
-
OnFailure: 只在容器 異常時才自動重啟容器;
-
Never: 從來不重啟容器
只要 Pod 的 restartPolicy 指定的策略允許重啟異常的容器(比如:Always),那么這個 Pod 就會保持 Running 狀態,並進行容器重啟。
對於包含多個容器的 Pod,只有它里面所有的容器都進入異常狀態后,Pod 才會進入 Failed 狀態。
Pod 預設置:PodPreset
比如可以先定義一個 PodPreset 對象:
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
name: allow-database
spec:
selector:
matchLabels:
role: frontend
env:
- name: DB_PORT
value: "6379"
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
在這個 PodPreset 的定義中,首先是一個 selector,表示只會作用於 selector 所定義的、帶有“role: frontend”標簽的 Pod 對象。
然后定義了env、volumeMounts等。
然后我們再寫一個yaml:
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
spec:
containers:
- name: website
image: nginx
ports:
- containerPort: 80
那么創建的適合,先創建PodPreset,然后再創建website的Pod,然后查看一下這個 Pod 的 API 對象:
$ kubectl get pod website -o yaml
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
annotations:
podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
containers:
- name: website
image: nginx
volumeMounts:
- mountPath: /cache
name: cache-volume
ports:
- containerPort: 80
env:
- name: DB_PORT
value: "6379"
volumes:
- name: cache-volume
emptyDir: {}
PodPreset 里定義的內容,只會在 Pod API 對象被創建之前追加在這個對象本身上,而不會影響任何 Pod 的控制器的定義。