Kubernetes之路 1 - Java應用資源限制的迷思

clipboard.png

隨着容器技術的成熟,愈來愈多的企業客戶在企業中選擇Docker和Kubernetes做爲應用平臺的基礎。然而在實踐過程當中,還會遇到不少具體問題。本系列文章會記錄阿里雲容器服務團隊在支持客戶中的一些心得體會和最佳實踐。咱們也歡迎您經過郵件和釘釘羣和咱們聯繫,分享您的思路和遇到的問題。html

問題

有些同窗反映:本身設置了容器的資源限制,可是Java應用容器在運行中仍是會莫名奇妙地被OOM Killer幹掉。java

這背後一個很是常見的緣由是:沒有正確設置容器的資源限制以及對應的JVM的堆空間大小。git

咱們拿一個tomcat應用爲例,其實例代碼和Kubernetes部署文件能夠從Github中得到。github

git clone https://github.com/denverdino/system-info
cd system-info`

下面是一個Kubernetes的Pod的定義描述:web

  1. Pod中的app是一個初始化容器,負責把一個JSP應用拷貝到 tomcat 容器的 「webapps」目錄下。注:
    鏡像中JSP應用index.jsp用於顯示JVM和系統資源信息。
  2. tomcat 容器會保持運行,並且咱們限制了容器最大的內存用量爲256MB內存。
apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  initContainers:
  - image: registry.cn-hangzhou.aliyuncs.com/denverdino/system-info
    name: app
    imagePullPolicy: IfNotPresent
    command:
      - "cp"
      - "-r"
      - "/system-info"
      - "/app"
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:9-jre8
    name: tomcat
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - mountPath: /usr/local/tomcat/webapps
      name: app-volume
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  volumes:
  - name: app-volume
    emptyDir: {}

咱們執行以下命令來部署、測試應用docker

$ kubectl create -f test.yaml
pod "test" created
$ kubectl get pods test
NAME      READY     STATUS    RESTARTS   AGE
test      1/1       Running   0          28s
$ kubectl exec test curl http://localhost:8080/system-info/
...

咱們能夠看到HTML格式的系統CPU/Memory等信息,咱們也能夠用 html2text 命令將其轉化成爲文本格式。api

注意:本文是在一個 2C 4G的節點上進行的測試,在不一樣環境中測試輸出的結果會有所不一樣tomcat

$ kubectl exec test curl http://localhost:8080/system-info/ | html2text

Java version     Oracle Corporation 1.8.0_162
Operating system Linux 4.9.64
Server           Apache Tomcat/9.0.6
Memory           Used 29 of 57 MB, Max 878 MB
Physica Memory   3951 MB
CPU Cores        2
                                          **** Memory MXBean ****
Heap Memory Usage     init = 65011712(63488K) used = 19873704(19407K) committed
                      = 65536000(64000K) max = 921174016(899584K)
Non-Heap Memory Usage init = 2555904(2496K) used = 32944912(32172K) committed =
                      33882112(33088K) max = -1(-1K)

咱們能夠發現,容器中看到的系統內存是 3951MB,而JVM Heap Size最大是 878MB。納尼?!咱們不是設置容器資源的容量爲256MB了嗎?若是這樣,當應用內存的用量超出了256MB,JVM還沒對其進行GC,而JVM進程就會被系統直接OOM幹掉了。oracle

問題的根源在於:app

  • 對於JVM而言,若是沒有設置Heap Size,就會按照宿主機環境的內存大小缺省設置本身的最大堆大小。
  • Docker容器利用CGroup對進程使用的資源進行限制,而在容器中的JVM依然會利用宿主機環境的內存大小和CPU核數進行缺省設置,這致使了JVM
    Heap的錯誤計算。

相似,JVM缺省的GC、JIT編譯線程數量取決於宿主機CPU核數。若是咱們在一個節點上運行多個Java應用,即便咱們設置了CPU的限制,應用之間依然有可能由於GC線程搶佔切換,致使應用性能收到影響。

瞭解了問題的根源,咱們就能夠很是簡單地解決問題了

解決思路

開啓CGroup資源感知

Java社區也關注到這個問題,並在JavaSE8u131+和JDK9 支持了對容器資源限制的自動感知能力 https://blogs.oracle.com/java...

其用法就是添加以下參數

java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap …

咱們在上文示例的tomcat容器添加環境變量 「JAVA_OPTS」參數

apiVersion: v1
kind: Pod
metadata:
  name: cgrouptest
spec:
  initContainers:
  - image: registry.cn-hangzhou.aliyuncs.com/denverdino/system-info
    name: app
    imagePullPolicy: IfNotPresent
    command:
      - "cp"
      - "-r"
      - "/system-info"
      - "/app"
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: tomcat:9-jre8
    name: tomcat
    imagePullPolicy: IfNotPresent
    env:
    - name: JAVA_OPTS
      value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
    volumeMounts:
    - mountPath: /usr/local/tomcat/webapps
      name: app-volume
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "256Mi"
        cpu: "500m"
      limits:
        memory: "256Mi"
        cpu: "500m"
  volumes:
  - name: app-volume
    emptyDir: {}

咱們部署一個新的Pod,並重復相應的測試

$ kubectl create -f cgroup_test.yaml
pod "cgrouptest" created

$ kubectl exec cgrouptest curl http://localhost:8080/system-info/ | html2txt
Java version     Oracle Corporation 1.8.0_162
Operating system Linux 4.9.64
Server           Apache Tomcat/9.0.6
Memory           Used 23 of 44 MB, Max 112 MB
Physica Memory   3951 MB
CPU Cores        2
                                          **** Memory MXBean ****
Heap Memory Usage     init = 8388608(8192K) used = 25280928(24688K) committed =
                      46661632(45568K) max = 117440512(114688K)
Non-Heap Memory Usage init = 2555904(2496K) used = 31970840(31221K) committed =
                      32768000(32000K) max = -1(-1K)

咱們看到JVM最大的Heap大小變成了112MB,這很不錯,這樣就能保證咱們的應用不會輕易被OOM了。隨後問題又來了,爲何咱們設置了容器最大內存限制是256MB,而JVM只給Heap設置了112MB的最大值呢?

這就涉及到JVM的內存管理的細節了,JVM中的內存消耗包含Heap和Non-Heap兩類;相似Class的元信息,JIT編譯過的代碼,線程堆棧(thread stack),GC須要的內存空間等都屬於Non-Heap內存,因此JVM還會根據CGroup的資源限制預留出部份內存給Non Heap,來保障系統的穩定。(在上面的示例中咱們能夠看到,tomcat啓動後Non Heap佔用了近32MB的內存)

在最新的JDK 10中,又對JVM在容器中運行作了進一步的優化和加強。

容器內部感知CGroup資源限制

若是沒法利用JDK 8/9的新特性,好比還在使用JDK6的老應用,咱們還能夠在容器內部利用腳原本獲取容器的CGroup資源限制,並經過設置JVM的Heap大小。

Docker1.7開始將容器cgroup信息掛載到容器中,因此應用能夠從 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件獲取內存、 CPU等設置,在容器的應用啓動命令中根據Cgroup配置正確的資源設置 -Xmx, -XX:ParallelGCThreads等參數

https://yq.aliyun.com/article... 一文中已經有相應的示例和代碼,本文再也不贅述

總結

本文分析了Java應用在容器使用中一個常見Heap設置的問題。容器與虛擬機不一樣,其資源限制經過CGroup來實現。而容器內部進程若是不感知CGroup的限制,就進行內存、CPU分配可能致使資源衝突和問題。

咱們能夠很是簡單地利用JVM的新特性和自定義腳原本正確設置資源限制。這個能夠解決絕大多數資源限制的問題。

關於容器應用中資源限制還有一類問題是,一些比較老的監控工具或者free/top等系統命令,在容器中運行時依然會獲取到宿主機的CPU和內存,這致使了一些監控工具在容器中運行時沒法正常計算資源消耗。社區中常見的作法是利用 lxcfs 來讓容器在資源可見性的行爲和虛機保持一致,後續文章會介紹其在Kubernetes上的使用方案。

阿里雲Kubernetes服務 全球首批經過Kubernetes一致性認證,簡化了Kubernetes集羣生命週期管理,內置了與阿里雲產品集成,也將進一步簡化Kubernetes的開發者體驗,幫助用戶關注雲端應用價值創新。

原文做者:易立

原文連接

相關文章
相關標籤/搜索