Idées fausses courantes sur la limitation des ressources d'application Java lors de l'utilisation de Kubernetes

Dans le premier article de cette série, nous examinerons certaines des idées fausses courantes sur la limitation des ressources d'application Java lors de l'utilisation de ** Kubernetes **.

Dans cette série d'articles, nous explorerons certains des problèmes courants rencontrés par les entreprises clientes lors de l'utilisation de Kubernetes.

À mesure que la technologie des conteneurs (https://www.alibabacloud.com/en/product/container-service) devient plus sophistiquée, de plus en plus de clients professionnels choisissent Docker et Kubernetes comme base de leur plate-forme d'application. Cependant, ces clients rencontrent en fait de nombreux problèmes. Dans cette série d'articles, nous partagerons quelques idées et meilleures pratiques tirées de l'expérience de l'équipe des services de conteneurs Alibaba Cloud qui a aidé nos clients à naviguer dans ce processus.

Pour les déploiements en conteneur d'applications Java, des rapports indiquent que le conteneur d'application Java actif est mystérieusement tué par OOM Killer, même si vous avez défini des limites de ressources de conteneur.

Ce problème est le résultat d'une erreur très courante consistant à ne pas définir correctement la limite de ressources du conteneur et la taille de segment JVM correspondante.

Prenons par exemple l'application Tomcat. Son code d'instance et son fichier de déploiement Kubernetes sont disponibles sur GitHub.

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

Il utilise la définition de pod Kubernetes suivante.

  1. L'application dans le pod est le conteneur d'initialisation et est responsable de la copie d'une application JSP dans le répertoire "webapps" du conteneur Tomcat. Remarque: Dans l'image, l'application JSP index.jsp est utilisée pour afficher les informations sur la JVM et les ressources système.
  2. Le conteneur Tomcat reste actif et limite l'utilisation maximale de la mémoire à 256 Mo.
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: {}

Exécutez la commande suivante pour déployer et tester votre application.

$ 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/
...

Les informations telles que le processeur et la mémoire du système sont désormais affichées au format HTML. Vous pouvez utiliser la commande html2text pour convertir les informations au format texte.

Remarque: nous testons l'application sur un nœud 4G 2C. Les tests dans différents environnements peuvent donner des résultats légèrement différents.

$ 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)

Comme vous pouvez le voir, la mémoire système du conteneur est de 3 951 Mo, mais la taille maximale du segment de mémoire JVM est de 878 Mo. Pourquoi cela arrive-t-il? N'avez-vous pas défini la capacité des ressources du conteneur sur 256 Mo? Dans cette situation, l'utilisation de la mémoire de l'application est supérieure à 256 Mo, mais la machine virtuelle Java n'implémente pas le nettoyage de la mémoire (GC). Au contraire, le processus JVM est tué directement par le tueur OOM du système.

Cause fondamentale du problème:

  1. Si vous ne définissez pas la taille du tas JVM, la taille maximale du tas sera définie par défaut en fonction de la taille de la mémoire de l'environnement hôte.
  2. Le conteneur Docker utilise des groupes de contrôle pour limiter les ressources utilisées par le processus. Par conséquent, si la JVM dans le conteneur utilise toujours les paramètres par défaut basés sur la mémoire et les cœurs de processeur de l'environnement hôte, cela entraînera un calcul incorrect du tas JVM.

De même, le nombre de threads du compilateur JVM GC et JIT par défaut est déterminé par le nombre de cœurs de processeur hôte. Si vous exécutez plusieurs applications Java sur un seul nœud, même si vous définissez des limites de processeur, le thread GC peut empêcher le basculement entre les applications, ce qui peut avoir un impact sur les performances des applications.

Maintenant que nous connaissons la cause première de ce problème, il est facile à résoudre.

Solution

Activer la reconnaissance des ressources pour le groupe c

La communauté Java est également consciente de ce problème et prend désormais en charge la détection automatique des limites de ressources de conteneur dans Java SE 8u131 + et JDK 9: [https://blogs.oracle.com/java-platform-group/java] -se-support-for-docker-cpu-and-memory-limits](https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory- limites? spm = a2c65.11461447.0.0.19b432f1RHoeWq)

Pour utiliser cette méthode, ajoutez les paramètres suivants:

java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap ⋯

En continuant de l'exemple précédent de conteneur Tomcat, ajoutez la variable d'environnement" 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: {}

Maintenant, déployez le nouveau pod et répétez le test.

$ 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)

Comme vous pouvez le voir, la taille maximale du tas JVM a été modifiée à 112 Mo et l'application ne sera pas tuée par le tueur OOM. Mais cela soulève un autre problème. Pourquoi définir la mémoire maximale du segment JVM sur seulement 112 Mo lorsque je définis la limite de mémoire maximale du conteneur sur 256 Mo?

La réponse concerne les détails de la gestion de la mémoire JVM. La consommation de mémoire dans la machine virtuelle Java comprend à la fois la mémoire de segment de mémoire et la mémoire sans segment de mémoire. La mémoire requise pour les métadonnées de classe, le code compatible JIT, les piles de threads, GC, etc. est récupérée à partir de la mémoire non-tas. Par conséquent, en fonction des limites de ressources du groupe de contrôle, la machine virtuelle Java réserve une partie de sa mémoire pour ne pas se surcharger afin d'assurer la stabilité du système. (Dans l'exemple précédent, après le démarrage de Tomcat, vous pouvez voir que la mémoire non-tas occupe près de 32 Mo).

La dernière version de JDK 10 a été encore optimisée et améliorée pour les opérations JVM à l'intérieur des conteneurs.

Connaissance des limites de ressources de groupe de contrôle dans les conteneurs

Si vous ne pouvez pas profiter des nouvelles fonctionnalités de JDK 8 et 9 (par exemple, si vous utilisez toujours une ancienne application JDK 6), utilisez un script à l'intérieur du conteneur pour obtenir les limites de ressources pour le groupe de contrôle du conteneur et utilisez-le pour obtenir les limites de ressources de la JVM. Vous pouvez définir la taille du tas.

À partir de Docker 1.7, les informations de groupe de contrôle du conteneur sont montées à l'intérieur du conteneur, et les applications peuvent obtenir des paramètres tels que la mémoire et le processeur à partir de fichiers tels que / sys / fs / cgroup / memory / memory.limit_in_bytes. Par conséquent, la commande de lancement de l'application dans le conteneur contient les paramètres de ressources corrects basés sur les paramètres de groupe de contrôle, tels que -Xmx, -XX: ParallelGCThreads.

Conclusion

Dans cet article, nous examinerons certains problèmes de configuration de tas courants qui se produisent lors de l'exécution d'applications Java dans des conteneurs. Contrairement aux machines virtuelles, les conteneurs ont des limites de ressources implémentées à l'aide de c-groups. De plus, les allocations de mémoire et de CPU peuvent provoquer des conflits de ressources et des problèmes si le processus de conteneur interne n'est pas conscient des limites de cgroup.

Ce problème peut être résolu très facilement en utilisant de nouvelles fonctionnalités JVM et des scripts personnalisés pour définir correctement les limites de ressources. Ces solutions résolvent la plupart des problèmes de limitation des ressources.

Cependant, ces solutions laissent un problème de limitation des ressources qui affecte les applications de conteneur non résolu. Certains outils de surveillance et commandes système plus anciens, tels que "free" et "top", obtiennent toujours les paramètres de processeur et de mémoire de l'hôte lorsqu'ils sont exécutés à l'intérieur du conteneur. Cela signifie que la consommation de ressources ne peut pas être calculée avec précision lorsque certains outils de surveillance s'exécutent à l'intérieur du conteneur. Une solution courante à ce problème proposée par la communauté est LXCFS Pour maintenir la cohérence entre le comportement de visualisation des ressources du conteneur et la machine virtuelle. spm = a2c65.11461447.0.0.19b432f1eicljw) doit être utilisé. Les articles suivants aborderont l'utilisation de cette méthode sur Kubernetes.

Alibaba Cloud Kubernetes Service est le premier service à être certifié pour l'intégrité de Kubernetes. Simplifie la gestion du cycle de vie des clusters Kubernetes et fournit une intégration intégrée dans les produits Alibaba Cloud. En outre, le service optimise davantage l'expérience des développeurs Kubernetes, permettant aux utilisateurs de se concentrer sur la valeur des applications cloud et sur l'innovation.

Recommended Posts

Idées fausses courantes sur la limitation des ressources d'application Java lors de l'utilisation de Kubernetes
Exception inattendue lors de l'utilisation de DateTimeFormatter de Java
Essayez d'utiliser le framework Java Nablarch [Application Web]
Injection de dépendances Spring à l'aide de Java, Kotlin
ERRORCODE = -4471 se produit dans une application Java qui utilise Db2.