在AWS亚马逊云上,我们运行并将容器化应用程序部署到PaaS管道中。基于Java或JVM的工作负载是部署到Pipeline
的重要工作负载之一,因此正确处理它们对我们和用户都非常重要。
对于我们来说,在spotguides中正确地调整容器的大小是很重要的,这样可以避免资源利用不足,并提供应用程序顺利运行所必需的资源。应避免利用不足,因为它会对成本产生负面影响。没有人愿意为CPU闲置的时间或从未使用过的内存付费。同时,我们需要为应用程序提供足够的资源,并对资源设置适当的限制,以实现所需的服务质量。如果应用程序试图使用超过容器限制的内存量,它将被杀死;低CPU限制会降低性能。
在这篇文章中,我们将从Kubernetes上的java10应用程序的角度讨论如何根据内存和CPU正确地调整容器的大小,以及如何使用管道来解决这些问题。为什么选择Java10?在容器中运行的JVM有一个众所周知的问题,JVM看到的是机器的可用内存,而不是容器中的可用内存。
关于这个问题的更多细节可以在这篇文章中找到。
Java9中添加了一个实验特性,以更好地识别容器CPU和内存限制。因为这是Java9中的一个实验特性,所以必须显式地启用它。Java10容器限制是自动识别和强制执行的,所以不用担心。
理论够了,让我们看看这是怎么回事。我们将在Java10上运行一个简单的应用程序,并观察内存使用情况和应用程序看到的CPU。此示例应用程序
- 将类加载到元空间以模仿类加载器的功能,并生成本机内存使用情况
- 创建对象以产生堆使用率
测试应用程序
我们使用javaassist
库从基类DynClassBase
动态生成新类,并使用反射实例化生成的类:
/**
* This class is used as the base from which we generate new classes.
*/
public class DynClassBase implements MemoryConsumer{
private byte []data = null; // data stored on heap
public void consumeSomeMemory(int amountInBytes) {
data = new byte[amountInBytes];
}
}
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(DynamicClassGen.class));
try {
for (int i = 0; i < genClassCount; i++) {
// generate a new class from DynClassBase
String className = DynClassBase.class.getCanonicalName();
CtClass cc = pool.get(className);
cc.setName(className + i);
Class genClass = cc.toClass();
Class<?> []ctorPrms = null;
Constructor ctor = genClass.getConstructor(ctorPrms);
MemoryConsumer memConsumer = (MemoryConsumer)ctor.newInstance(null);
memoryConsumers.add(memConsumer);
System.out.println("Instantiated class " + memConsumer.getClass().getSimpleName());
}
} catch (Exception e) {
e.printStackTrace();
}
必须通过DYN_CLASS_COUNT
环境变量指定要生成的类的数量。应用程序实例化每个生成的类,并调用consumeSomeMemory
方法在堆中分配内存。每次调用都将为每个对象分配MEM_USAGE_PER_OBJECT_MB
中指定的内存量。
Dockerfile文件
我们使用以下方法构建应用程序并将其刻录到Docker映像:
FROM maven:3.5-jdk-10-slim as builder
ADD . /dynamic-class-gen
WORKDIR /dynamic-class-gen
RUN mvn package
FROM openjdk:10-slim
WORKDIR /opt/dynamic-class-gen/lib
COPY --from=builder /dynamic-class-gen/target/dynamicclassgen-1.0.jar .
COPY --from=builder /dynamic-class-gen/target/dependency/javassist-3.22.0-GA.jar .
CMD $JAVA_HOME/bin/java $JVM_OPTS -cp javassist-3.22.0-GA.jar:dynamicclassgen-1.0.jar com.banzaicloud.dynclassgen.DynamicClassGen
cd dynamic-class-gen
docker build -t banzaicloud/dynclassgen:1.0 .
Kubernetes Pod
我们将使用以下Pod规范在Kubernetes上运行此应用程序:
apiVersion: v1
kind: Pod
metadata:
name: dyn-class-gen
spec:
containers:
- name: dyn-class-gen-container
image: banzaicloud/dynclassgen:1.0
env:
- name: DYN_CLASS_COUNT
value: "256"
- name: MEM_USAGE_PER_OBJECT_MB
value: "1"
resources:
requests:
memory: "64Mi"
cpu: 1
limits:
memory: "1Gi"
cpu: 2
restartPolicy: Never
整个源代码都可以在GitHub上获得。
试运行
kubectl create -f pod.yml
这将创建一个带有运行我们的应用程序的容器的pod。容器的最大内存限制为1GB。如果应用程序的内存使用超过此限制,则Kubernetes将对容器进行OOMKilled
。应用程序被配置为生成和实例化256(DYN_CLASS_COUNT
)类定义,每个实例化对象分配1MB(MEM_USAGE_PER_OBJECT_MB
)堆空间。默认情况下,JVM将最大堆大小设置为容器可用内存的1/4,在本例中为1GB/4=256MB
。
如果我们验证Pod,我们可以看到它处于错误状态:
kubectl get po dyn-class-gen
NAME READY STATUS RESTARTS AGE
dyn-class-gen 0/1 Error 0 10m
让我们看看我们能从Pod日志中发现什么:
kubectl logs dyn-class-gen
...
DynClassBase241 instance consuming 1MB
DynClassBase242 instance consuming 1MB
DynClassBase243 instance consuming 1MB
DynClassBase244 instance consuming 1MB
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.banzaicloud.dynclassgen.DynClassBase245.consumeSomeMemory(DynClassBase.java:25)
at com.banzaicloud.dynclassgen.DynamicClassGen.main(DynamicClassGen.java:72)
这表明应用程序由于java.lang.OutOfMemoryError: Java heap space,在堆上分配内存的第244次迭代之后的Java堆空间。但是,当内存限制为1GB,最大堆空间为256MB时,为什么呢?答案在于Java内存模型。每个活动java对象都有一些内存开销;javaassist
库在堆上存储了内部对象;内部字符串存储在堆上,引用对象的堆栈变量也存储在堆上,等等。
我们需要更多的堆空间。让我们将其增加20%到300MB,然后重新运行测试应用程序:
apiVersion: v1
kind: Pod
metadata:
name: dyn-class-gen
spec:
containers:
- name: dyn-class-gen-container
image: banzaicloud/dynclassgen:1.0
env:
- name: DYN_CLASS_COUNT
value: "256"
- name: MEM_USAGE_PER_OBJECT_MB
value: "1"
- name: JVM_OPTS
value: "-Xmx300M"
resources:
requests:
memory: "64Mi"
cpu: 1
limits:
memory: "1Gi"
cpu: 2
restartPolicy: Never
kubectl create -f pod.yml
我们看到,增加应用程序的最大堆大小有助于消除OutOfMemoryError
。由于各种原因,在应用程序的生命周期内,堆内存的使用可能会出现峰值。最大堆大小也必须预测这些需求。我们建议监视应用程序的内存使用情况,并根据需要调整最大堆大小。
我们设定极限了吗?还没有。我们还需要考虑本机内存的使用。我们得设置:
limits:
memory: "1Gi"
一个合理的值,以便有足够的空间容纳JVM本机内存,但同时,足够低的空间可以快速捕获爆炸JVM本机内存的泄漏。
监测
有一个名为heapster的工具可以用于集群的高级监视。让我们看一下内存使用仪表板显示的关于测试应用程序的内容。
我们可以看到,我们的测试应用程序使用309MB的内存。这包括为堆空间分配的300MB,但其余的是本机内存。基于这个简单的观察,我们有必要将容器内存限制从1GB降低到309MB+20%=~370MB
limits:
memory: "370Mi"
在操作企业应用程序时,必须使用特定于应用程序的度量在粒度级别监视它们的资源使用情况。查看我们的监控系列,了解AWS云如何对部署到Kubernetes的应用程序实施应用程序监控。
Vertical Pod Autoscaler
现在Java10自动识别并强制执行容器限制,看看这个过程是如何展开并与Kubernetes vertical pod autoscaler
交互的,这是很有趣的。
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1567.html
暂无评论