前言
在kubernetes環境下,無論集群再大,對應的集群資源(cpu、memory、storage)總是有上限的,而默認情況下,我們啟動的pod、以及pod中運行的容器,對應的資源是不加限制的,理論上每個pod,或者是pod內運行的容器,可以無限使用資源直到把所在節點上的資源耗盡,造成節點崩潰,進而影響集群的穩定性,為了防止這種情形出現,kubernetes給我們提供了相應的機制來限制單個pod可以使用的資源,主要的物件有如下兩個
- LimitRanger
- ResourceQuota
LimitRanger
- LimitRanger是一種用來限制在特定命名空間下單個pod可以消耗資源的策略,主要防止單個pod將所在命名空間的資源全部搶占完的情況,通過以下幾個方面進行配置
- 限定單個pod或容器可消耗的計算資源(cpu、memory)在一定范圍內(最大、最小)
- 限定單個PersistentVolumeClaim可申請的storage在一定的范圍內
- 限定對特定資源的需求值(最小)和限定值(最大)的比例
- 自動對處于同一命名空間下,沒有追加資源限定的pod進行限定
-
使用流程如下
- 集群管理員創建在特定namespace下創建LimitRange
- 使用者在這個namespace下創建pod、容器、PersistentVolumeClaims物件
- LimitRanger準入控制器檢查待創建物件的資訊,如果沒有資源配置,就會用默認值來裝配這些物件
- 如果有資源配置,就檢查這些配置是否和LimitRanger定義的規范相沖突,如果沖突就拒絕創建相應的物件
- 如果namespace下定義了ResourceQuota,LimitRanger沒有定義默認的資源限額,待創建物件也沒有資源配置,則拒絕創建
服務質量(QoS)
根據創建的pod或container是否指定資源request和limit值,kubernettes將pod劃分成了3個等級
- Guaranteed 當pod中的所有容器對所有資源都定義了Limits和Requests,并且所有容器的Limits值和Requests值全部相等
- BestEffort pod中的所有容器都沒有定義資源配置
- Burstable 當一個pod不是Guaranteed也不是BestEffort時,該pod的QoS就是Burstable,例如 Pod中的一部分容器定義了Request值或者Limit值,或者都定義了,但是值不相等
kubernets在出現資源競爭時,會優先保證Guaranteed級別的pod正常運行,其次是Burstable,最后是BestEffort
ResourceQuota
ResourceQuota(資源配額)限定的是特定namespace下所有物件可用的資源總量,另外還能限定各種物件的創建數量
作業流程如下
- 因為ResourceQuota對整個namespace限定,所以不同的組作業在不同的namespace下方能相互不干擾
- 集群管理員對每個namespace創建相應的ResourceQuota
- 用戶在自己namespace下創建pod、容器等物件
- 如果要創建物件申請的資源和ResourceQuota有沖突,則系統拒絕創建
實際需求
通過LimitRanger或者創建pod時限定對應的Resource資訊,kubernetes在因為某些容器自身缺陷導致pod一直搶占系統資源時,自動幫我們發現問題,并停掉這些有問題pod,如果pod被controller控制,還會幫我們重新調度這個pod,使系統維持可用,
例如我們有如下代碼塊
@RequestMapping("/oom")
public String oom() {
log.info("request to oom");
PersonRepo pp = new PersonRepo();
pp.autoCreatePerson();
return "OK";
}
public static class PersonRepo {
private List<Person> repo = new ArrayList<>();
public void autoCreatePerson() {
for (long l = 0; ; l++) {
repo.add(new Person(l));
}
}
}
當呼叫oom方法時,由于程式編碼bug,導致應用程式會持續的創建Person物件,放在記憶體中,如果不加限制,這個程式會不斷增加記憶體,直到節點上分給kubernets的可用記憶體全部被消耗盡,
如果啟動時加上resource資訊
...
image: 192.168.0.107/k8s/resource-quotas-oom:0.0.4
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "582Mi"
cpu: "500m"
...
則當此容器消耗的記憶體達到582Mi時,k8s會直接kill掉這個容器,給出提示資訊OOMKilled,并把pod重啟(此pod中只有這一個容器),
這雖然保證了系統的可用性,可是從這個資訊中我們沒有辦法分析出到底是代碼的什么地方出現了問題,正常情況下,我們啟動java應用,都會在啟動命令中加上如下jvm引數
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/src/oomdump
告訴jvm,當系統出現oom時,給我們在特定的路徑下生成dump檔案,之后我們根據這個dump檔案分析具體出現錯誤的代碼,如果我們想在容器被k8s kill前生成dump檔案,我們還需要設定jvm的可用記憶體大小
-server -Xms512m -Xmx512m
并且如果jvm的最大記憶體(Xmx)值和k8s resource.limits.memory值一樣或著小的話,實際運行時在jvm 出現oom之前,會先觸發k8s的OOMKill,還是無法生成dump,所以要想生成dump檔案,resource.limits.memory的值要比Xmx的值大一些(50M~100M),
可是這樣設定后,因為還沒有達到resource.limits.memory這個值,雖然應用程式出現了oom,但是容器不會被k8s集群停掉,pod也不會重新啟動,造成系統回應變慢(容器的記憶體一直被占著,訪問其他請求也會變慢,并出現oom),這也不是我們想要的結果,所以在jvm中再追加一個引數,出現oom后讓應用直接停止
-XX:+ExitOnOutOfMemoryError
這樣,出現oom后,應用會先生成dump,之后會終止,系統日志資訊
2020-02-24 10:25:15.832 INFO 6 --- [nio-8080-exec-2] c.falcon.resource.quotas.oom.RunOom : request to oom
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /usr/src/oomdump ...
Heap dump file created [625772870 bytes in 25.588 secs]
Terminating due to java.lang.OutOfMemoryError: Java heap space
從日志可以看到,系統先生成dump,然后停止掉自己
pod 變化資訊
root@master:~# kubectl get pod -w
NAME READY STATUS RESTARTS AGE
jenkins-68d8b54c45-7pdhs 1/1 Running 0 2d5h
oom-deployment-64664f9454-kp65n 1/1 Running 6 26h
oom-deployment-64664f9454-kp65n 0/1 Error 6 26h
oom-deployment-64664f9454-kp65n 1/1 Running 7 26h
可以看到pod出現error,然后又自動恢復成Running狀態,k8s會自動將pod中停掉的容器再啟動起來,看下容器中/usr/src/oomdump目錄
root@master:~# kubectl exec -it oom-deployment-64664f9454-kp65n ls /usr/src
resource-quotas-oom-0.0.1-SNAPSHOT.jar
start.sh
怎么沒有我們生成的dump檔案,因為容器被重新啟動了,作業目錄也成了新容器的作業目錄,不包含已停掉的容器檔案,
查看完整pod資訊,有如下片段
root@master:~# kubectl get pod oom-deployment-64664f9454-kp65n -o yaml
...
containerStatuses:
- containerID: docker://70daa47c0f45c9d014e0f70b80a41ca6bc9fd219ca957cc035ee2a049d46166b
image: 192.168.0.107/k8s/resource-quotas-oom:0.0.4
imageID: docker-pullable://192.168.0.107/k8s/resource-quotas-oom@sha256:3ab65f8b5d17182abb0ca0ccc164e5f40bb1d0fcf14006c06af795d0419daa58
lastState:
terminated:
containerID: docker://95776b99ec7a27c03327e45f9e1fc1d3f058c9b814400eabafa9606534c6bc2a
exitCode: 3
...
有一個已經停掉的容器,對應的容器ID:95776b99ec7a27c03327e45f9e1fc1d3f058c9b814400eabafa9606534c6bc2a
到pod所在節點上,通過以下命令獲取對應的dump
root@slave:/opt/k8s/work# docker ps -a |grep 95776b99ec7a
95776b99ec7a ec0a1d08eb22 "/bin/sh -c ./start.…" 3 hours ago Exited (3) 13 minutes ago k8s_oom_oom-deployment-64664f9454-kp65n_default_9a47dc18-14bf-4d77-9ed4-5ac2c11d2bf7_6
root@slave:/opt/k8s/work# docker cp 95776b99ec7a:/usr/src/oomdump .
root@slave:/opt/k8s/work# ls -alh oomdump
-rw------- 1 root root 597M 2月 24 18:26 oomdump
- 其中95776b99ec7a是通過kubectl獲取到的容器ID的前12位(docker 默認顯示12位ID)
執行程序中發現,如果多次系統出現oom,k8s只會幫我們保留最近停掉的一個容器,再往直前的停掉的容器會被自動回收掉,發現這是因為kubelet有一個垃圾回收策略,這個引數可以通過kubelet的配置引數 maximum-dead-containers-per-container來設定,默認值是1,所以只會幫我們保留一個,詳情可參考Configuring kubelet Garbage Collection
疑惑
雖然通過jvm引數的設定,實作了程式oom時生成dump,之后再重啟,可是這樣設定后,其實k8s提供的resources資訊就沒有起到應有的作用了,不知道是否是用錯了方法還是k8s還有別的機制,在被OOMKill之前再執行個什么動作(preStop hook),等深入研究后看有沒有更優雅的實作方式
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/41224.html
標籤:其他
