容器环境下 pod 的 cpu 资源不设置request会是什么样的表现
我们分别在 java 程序以及使用 stress 压测工具来模拟高负载情况下pod的资源负载情况。
环境
- Kubernetes 1.24
- Containerd 1.6.16
- CentOS 8 (node节点 12c32g)
- Java-openjdk 11
一,在不设置 containers.resources.requests.cpu
资源限制的情况下,程序能使用到多少CPU资源?
Deployment 资源定义文件:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
resources: {}
这里将 resources
字段设置为空。
stress是一个linux的压力测试工具,专门用于对设备的CPU、IO、内存、负载、磁盘等进行压测。 适用在监控告警类、主机资源跑高等测试上。
1. stress 压测
使用参数 -c 6
选项参数来模拟6个核心满载的情况,看看能否跑得上去:
$ apt update
$ apt install stress procps -y
$ stress -c 6
观察ps aux
进程的CPU使用率情况:
# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 3677 0.0 0.0 7280 828 pts/1 S+ 13:37 0:00 stress -c 6
root 3678 99.0 0.0 7280 84 pts/1 R+ 13:37 3:26 stress -c 6
root 3679 99.1 0.0 7280 84 pts/1 R+ 13:37 3:27 stress -c 6
root 3680 99.1 0.0 7280 84 pts/1 R+ 13:37 3:27 stress -c 6
root 3681 99.1 0.0 7280 84 pts/1 R+ 13:37 3:27 stress -c 6
root 3682 99.0 0.0 7280 84 pts/1 R+ 13:37 3:26 stress -c 6
root 3683 99.1 0.0 7280 84 pts/1 R+ 13:37 3:27 stress -c 6
其中PID 3677 为父进程,用于管理执行压测任务的worker进程。
我们可以看到,后续一共fork
了6个子进程,且每个子进程CPU占用率都几乎满载。
我们再来看一下 prometheus 监控中的pod cpu占用情况:
可以看到,的确CPU是使用了6个核心,符合预期。
我们大胆一点,把压力给到 16 个任务,看看能否把 node 的12个核心都压满:
$ stress -c 16
观察后台 ps aux
进程任务情况:
# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 3779 0.0 0.0 7280 880 pts/1 S+ 14:17 0:00 stress -c 16
root 3780 71.7 0.0 7280 88 pts/1 R+ 14:17 10:09 stress -c 16
root 3781 70.9 0.0 7280 88 pts/1 R+ 14:17 10:03 stress -c 16
root 3782 70.5 0.0 7280 88 pts/1 R+ 14:17 9:59 stress -c 16
root 3783 71.0 0.0 7280 88 pts/1 R+ 14:17 10:04 stress -c 16
root 3784 68.9 0.0 7280 88 pts/1 R+ 14:17 9:46 stress -c 16
root 3785 69.1 0.0 7280 88 pts/1 R+ 14:17 9:47 stress -c 16
root 3786 73.9 0.0 7280 88 pts/1 R+ 14:17 10:28 stress -c 16
root 3787 70.3 0.0 7280 88 pts/1 R+ 14:17 9:58 stress -c 16
root 3788 71.4 0.0 7280 88 pts/1 R+ 14:17 10:07 stress -c 16
root 3789 73.7 0.0 7280 88 pts/1 R+ 14:17 10:27 stress -c 16
root 3790 73.9 0.0 7280 88 pts/1 R+ 14:17 10:28 stress -c 16
root 3791 72.1 0.0 7280 88 pts/1 R+ 14:17 10:12 stress -c 16
root 3792 71.5 0.0 7280 88 pts/1 R+ 14:17 10:08 stress -c 16
root 3793 70.2 0.0 7280 88 pts/1 R+ 14:17 9:57 stress -c 16
root 3794 72.3 0.0 7280 88 pts/1 R+ 14:17 10:14 stress -c 16
root 3795 71.8 0.0 7280 88 pts/1 R+ 14:17 10:10 stress -c 16
可见,抛开k8s集群原有的一些运行在该node节点上的服务消耗以外,该pod是能够吃满剩余所有CPU资源的。
2. Java代码来跑并行任务
拿一个Java简单的 Concurrent 并行演示的例子来模拟:
// ForkJoinTest.java
import java.util.concurrent.*;
import java.util.function.*;
/**
* This program demonstrates the fork-join framework.
*
* @author Cay Horstmann
* @version 1.01 2015-06-21
*/
public class ForkJoinTest {
public static void main(String[] args) {
final int SIZE = 10000000;
var numbers = new double[SIZE];
for (int i = 0; i < SIZE; i++) numbers[i] = Math.random();
var counter = new Counter(numbers, 0, numbers.length, x -> x > 0.5);
var pool = new ForkJoinPool();
pool.invoke(counter);
System.out.println("ForkJoinPool size: " + pool.getPoolSize()); // poolsize 为计算机核心数
System.out.println(counter.join());
}
}
class Counter extends RecursiveTask<Integer> {
public static final int THRESHOLD = 1000;
private double[] values;
private int from;
private int to;
private DoublePredicate filter;
public Counter(double[] values, int from, int to, DoublePredicate filter) {
this.values = values;
this.from = from;
this.to = to;
this.filter = filter;
}
@Override
protected Integer compute() {
if (to - from < THRESHOLD) {
System.out.println("join task: from " + from + " to " + to);
int count = 0;
for (int i = from; i < to; i++) {
if (filter.test(values[i])) count++;
}
return count; // 返回当前子任务的结果合并值
} else {
System.out.println("fork task: from " + from + " to " + to);
int mid = (from + to) / 2; // 计算两者的平均值,取出中间值
var first = new Counter(values, from, mid, filter);
var second = new Counter(values, mid, to, filter);
// 分割子任务
// first.fork(); // 方式一
// second.fork(); // 方式一
invokeAll(first, second);
return first.join() + second.join();
}
}
}
安装 oenjdk-11 :
$ apt update && apt install wget unzip
$ wget https://github.com/ojdkbuild/contrib_jdk11u-ci/releases/download/jdk-11.0.15%2B10/jdk-11.0.15-ojdkbuild-linux-x64.zip
$ unzip jdk-11.0.15-ojdkbuild-linux-x64.zip
$ export PATH=$PATH:/jdk-11.0.15-ojdkbuild-linux-x64/bin
$ java --version
openjdk 11.0.15 2022-04-19 LTS
OpenJDK Runtime Environment 18.9 (build 11.0.15+10-LTS)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.15+10-LTS, mixed mode)
使用 javac
编译并运行java这段java代码:
$ javac ForkJoinTest.java
$ java ForkjoinTest
........
fork task: from 9998779 to 10000000
join task: from 9998779 to 9999389
join task: from 9999389 to 10000000
ForkJoinPool size: 1
5001986
这里的 pool.getPoolSize()
返回jdk获取到的容器CPU核心数,这里拿到值仅为 1 。
我们来使用参数获取一下 jdk 的 Flags 信息:
$ java -XX:+PrintFlagsFinal --version | grep -E "ActiveProcessorCount|ParallelGCThreads"
int ActiveProcessorCount = -1 {product} {default}
uint ParallelGCThreads = 0 {product} {default}
可见 ActiveProcessorCount
值为 -1。
ParallelGCThreads
会影响到JVM GC 性能。
我们再使用 jshell 获取一下使用的 cpu 核心数量:
jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 1
jvm 默认使用 1 核心。
二,在正确配置 containers.resources.requests.cpu
资源限制的情况下,程序能使用到多少CPU资源?
Deployment 资源定义文件:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment2
spec:
selector:
matchLabels:
app: nginx2
replicas: 1
template:
metadata:
labels:
app: nginx2
spec:
containers:
- name: nginx2
image: nginx:1.14.2
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
limits:
cpu: 8
这里将配置 resources
字段中的 CPU 资源大小,requests
保证分配给到100m
,limits
最大分配给到8
个核心。
stress是一个linux的压力测试工具,专门用于对设备的CPU、IO、内存、负载、磁盘等进行压测。 适用在监控告警类、主机资源跑高等测试上。
1. stress 压测
使用参数 -c 10
选项参数来模拟10个核心满载的情况,看看能否跑得上去:
$ apt update
$ apt install stress procps -y
$ stress -c 10
这里给到的压力是 10 个核心,已经超过了 limits
的最大值,看看实际的表现情况。
观察ps aux
进程的CPU使用率情况:
# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 3779 0.0 0.0 7280 880 pts/1 S+ 14:17 0:00 stress -c 16
root 3780 71.7 0.0 7280 88 pts/1 R+ 14:17 10:09 stress -c 16
root 3781 70.9 0.0 7280 88 pts/1 R+ 14:17 10:03 stress -c 16
root 3782 70.5 0.0 7280 88 pts/1 R+ 14:17 9:59 stress -c 16
root 3783 71.0 0.0 7280 88 pts/1 R+ 14:17 10:04 stress -c 16
root 3784 68.9 0.0 7280 88 pts/1 R+ 14:17 9:46 stress -c 16
root 3785 69.1 0.0 7280 88 pts/1 R+ 14:17 9:47 stress -c 16
root 3786 73.9 0.0 7280 88 pts/1 R+ 14:17 10:28 stress -c 16
root 3787 70.3 0.0 7280 88 pts/1 R+ 14:17 9:58 stress -c 16
root 3788 71.4 0.0 7280 88 pts/1 R+ 14:17 10:07 stress -c 16
root 3789 73.7 0.0 7280 88 pts/1 R+ 14:17 10:27 stress -c 16
root 3790 73.9 0.0 7280 88 pts/1 R+ 14:17 10:28 stress -c 16
root 3791 72.1 0.0 7280 88 pts/1 R+ 14:17 10:12 stress -c 16
root 3792 71.5 0.0 7280 88 pts/1 R+ 14:17 10:08 stress -c 16
root 3793 70.2 0.0 7280 88 pts/1 R+ 14:17 9:57 stress -c 16
root 3794 72.3 0.0 7280 88 pts/1 R+ 14:17 10:14 stress -c 16
root 3795 71.8 0.0 7280 88 pts/1 R+ 14:17 10:10 stress -c 16
其中 PID 3606 为剩下worker
子进程的父进程,本身不会给到负载。
可见,我们给了超出总可用核心数的参数,实际每个子进程都不能满载,但是总的子进程CPU占用率加起来恰好等于 800%,即符合8核心满载的情况。
在监控面板中,也获取到了pod的 requests
和 limits
大小,其中container nginx
的CPU使用率跑满8核心,和limits
持平,符合预期。
2. Java代码来跑并行任务
代码见上文。
使用 javac
编译并运行java这段java代码:
$ javac ForkJoinTest.java
$ java ForkjoinTest
........
fork task: from 8377685 to 8378906
join task: from 8375853 to 8376464
join task: from 8378295 to 8378906
ForkJoinPool size: 8
4999188
这里的 pool.getPoolSize()
返回jdk获取到的容器CPU核心数,这里拿到值为 8,说明jdk正确获取到了容器的CPU资源上限。
我们来使用参数获取一下 jdk 的 Flags 信息:
$ java -XX:+PrintFlagsFinal --version | grep -E "ActiveProcessorCount|ParallelGCThreads"
int ActiveProcessorCount = -1 {product} {default}
uint ParallelGCThreads = 8 {product} {default}
可见 ActiveProcessorCount
值为 -1。
ParallelGCThreads
会影响到JVM GC 性能。
我们再使用 jshell 获取一下使用的 cpu 核心数量:
jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 8
jvm 正确识别 8 个核心。
三,总结
在没有设置 pod 的cpu资源大小时,默认程序可以使用的资源为宿主机总资源数。
但是得益于在特定服务环境的情况下,比如这里的 java 服务,jdk 需要判断是否为传统vm类型还是容器运行时,会事先获取当前运行环境的可用资源大小,如果获取失败,则会使用默认值 1 核心来设置 jvm gc 任务线程配置。
在设置有 pod 的 cpu 资源大小时,jdk >= 1.8_191 版本后,会正确识别 cgroup 资源限制。
Java服务和传统服务的资源使用上限,也将等于 limits
允许的cpu资源大小。
所以推荐在 Java 环境下正确配置 containers.resources.requests
和 contaienrs.resources.limits
资源大小,避免因jvm获取cpu失败,选用默认的1核心运行任务。