问题引出

有个需求,要对服务打流量,从而让CPU占用达到峰值,进而查看系统的状态。但是由于基础设施的原因,只能通过ab打流量,因此我能打的流量的受限于客户机的带宽。这个时候怎么加ab的并发量和请求数,也只能让CPU的占用达到一个阈值。这个时候自然就想到直接上服务器,给它的CPU加压,达到加压测试的效果。

问题描述

那该如何加压?Linux上自然有工具的吧。

在 Linux 上给 CPU 加压通常是为了测试系统的性能、稳定性或者进行基准测试。你可以通过多种方法让 CPU 占用率提高,从而进行负载测试。以下是一些常用的方法:

1. 使用 stress 工具

stress 是一个用于对系统进行负载测试的工具,可以使 CPU、内存、I/O 等资源都达到高负载。

使用 stress 给 CPU 加压

1
stress --cpu 4 --timeout 60
  • --cpu 4:表示使用 4 个 CPU 线程进行压力测试。
  • --timeout 60:表示测试持续 60 秒。

2. 使用 stress-ng 工具

stress-ngstress 的一个扩展版本,功能更为强大。

使用 stress-ng 给 CPU 加压

1
stress-ng --cpu 4 --timeout 60s
  • --cpu 4:表示使用 4 个 CPU 线程。
  • --timeout 60s:表示测试持续 60 秒。

3. 使用 yes 命令

yes 命令会持续输出一个字符串(默认为 y),从而让 CPU 处于高负载状态。

1
yes > /dev/null &
  • 这个命令会在后台运行 yes,并将输出重定向到 /dev/null,以防止输出过多的数据。

你可以运行多个 yes 命令来进一步增加负载,例如:

1
2
yes > /dev/null &
yes > /dev/null &

4. 使用 dd 命令

dd 命令可以用来进行 I/O 操作,通过生成大量的数据来使 CPU 处于高负载状态。

1
dd if=/dev/zero of=/dev/null bs=1M count=1000 &
  • if=/dev/zero:指定输入文件。
  • of=/dev/null:指定输出文件。
  • bs=1M:块大小为 1MB。
  • count=1000:总共读取 1000 块数据。

5. 使用 Python 脚本

你可以使用 Python 脚本来生成 CPU 负载。

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
import threading
import time

def cpu_load():
while True:
pass

if __name__ == "__main__":
for _ in range(4):
thread = threading.Thread(target=cpu_load)
thread.start()

保存为 cpu_load.py,然后运行:

1
python3 cpu_load.py

6. 使用 taskset 结合自定义负载脚本

taskset 可以将进程绑定到特定的 CPU 核心。结合自定义负载脚本,你可以实现更加精确的负载测试。

使用 taskset 运行 yes 命令:

1
2
taskset -c 0 yes > /dev/null &
taskset -c 1 yes > /dev/null &
  • -c 0-c 1:将 yes 命令绑定到 CPU 核心 0 和 1。

其实大概方法就是让CPU干点计算量,从而增加占用。

由于环境问题,我的服务器上安装软件包不是很方便,于是最方便的反而是写一个脚本来增压,也就是上面的:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
import threading
import time

def cpu_load():
while True:
pass

if __name__ == "__main__":
for _ in range(4):
thread = threading.Thread(target=cpu_load)
thread.start()

直接运行这个脚本,结果却让我很吃惊:

image-20240721110529089

CPU的占用直接达到了100%

这种其实有点和我的经验不一致啊:CPU占用是由于CPU有计算任务要去干,而我这个脚本就是个空循环,压根没有去干什么事情啊。同时在我的经验中,要是CPU占用满了,电脑自然会卡顿。

问题分析

那么回到最初的问题,CPU占用到底和什么有关系?

CPU 的占用率受多种因素影响,包括系统负载、进程行为、硬件性能等。以下是一些主要的因素,这些因素可以影响 CPU 的占用率:

1. 运行的进程和线程

  • 进程数量:系统中运行的进程数量会影响 CPU 占用。如果有大量进程同时运行,尤其是那些 CPU 密集型的进程,会导致 CPU 占用率增加。
  • 线程数:多线程应用程序可能会在多个 CPU 核心上同时执行,这可能会增加 CPU 占用率。

2. 进程类型

  • CPU 密集型进程:这些进程主要使用 CPU 资源进行计算,如数据处理、计算密集型任务等。它们通常会导致较高的 CPU 占用率。
  • I/O 密集型进程:这些进程主要进行输入/输出操作,如文件读写、网络传输等。虽然这些进程主要消耗 I/O 资源,但在等待 I/O 操作完成时也可能会间接增加 CPU 占用。

3. 系统负载

  • 负载平均值:负载平均值是系统负载的一个指标,它表示在过去 1、5、15 分钟内,平均有多少个进程正在等待 CPU 时间。较高的负载平均值通常意味着系统过载,可能会导致 CPU 占用率高。

4. 应用程序行为

  • 优化和效率:某些应用程序可能没有优化好,导致 CPU 占用率高。例如,死循环、内存泄漏或不必要的计算可能导致 CPU 过度使用。
  • 优先级和调度:应用程序的优先级和调度策略也会影响 CPU 占用率。高优先级的进程可能会获得更多的 CPU 时间。

5. 系统配置

  • CPU 核心数:系统中 CPU 核心的数量会影响 CPU 占用率。单核 CPU 的系统通常比多核 CPU 的系统更容易出现高占用情况,因为所有任务都需要在一个核心上处理。
  • 超线程技术:启用超线程技术的 CPU 允许每个物理核心同时处理多个线程,这可以提高并行处理能力,影响 CPU 占用情况。

6. 系统资源

  • 内存:系统内存不足可能导致频繁的交换(swap),从而增加 CPU 的负担。频繁的交换会导致 CPU 花费更多时间处理内存操作。
  • 磁盘 I/O:磁盘读写操作的高负载可能导致 CPU 处理 I/O 请求的时间增加,间接影响 CPU 占用率。

7. 后台任务和服务

  • 系统守护进程和服务:后台运行的守护进程和服务也可能占用 CPU 资源。例如,数据库服务、日志记录、监控工具等可能会导致 CPU 占用率增加。
  • 定时任务:计划任务(如 cron 任务)或定期运行的脚本也可能在特定时间段内增加 CPU 占用。

8. 系统和应用程序的更新

  • 软件更新:系统和应用程序的更新可能会改变它们的资源使用模式,新的功能或修复可能会影响 CPU 占用率。
  • 驱动程序:硬件驱动程序的更新或更改也可能影响 CPU 的使用情况。

监控和诊断工具

  • **tophtop**:可以实时查看 CPU 占用情况和各进程的 CPU 使用情况。
  • **ps**:可以查看当前运行的进程及其 CPU 占用情况。
  • **sar**:可以提供详细的系统性能数据,包括 CPU 使用情况。
  • **iotop**:可以监控 I/O 操作,帮助确定 I/O 密集型进程对 CPU 的影响。

总结

CPU 的占用率与系统中的多种因素相关,包括运行的进程和线程、进程类型、系统负载、应用程序行为、系统配置、内存和磁盘 I/O、后台任务以及系统和应用程序的更新。通过监控这些因素,你可以更好地理解和管理 CPU 的使用情况,优化系统性能。

根据上面说的第四点,是否存在说法:死循环导致CPU占用飙升

那么尝试改一下死循环验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
import threading
import time

def cpu_load():
while True:
# 把空循环换成sleep
time.sleep(1)

if __name__ == "__main__":
for _ in range(4):
thread = threading.Thread(target=cpu_load)
thread.start()

再看一眼占用情况:

image-20240721112333428

直接降低到0了!也就是说,**无限空循环会造成CPU占用达到100%**!

那么更加糊涂了,空循环什么事情都没干,理论上占用更低才对啊。

分析一下:

原因分析

  1. 持续运行
    • 空循环没有任何退出条件,会一直运行下去。
    • 由于没有任何休眠或阻塞操作,循环在每个时钟周期都会执行一次迭代。
  2. 无I/O操作
    • 循环没有进行任何I/O操作(如文件读写、网络通信等),这些操作通常会导致进程进入阻塞状态,从而释放CPU资源给其他进程。
    • 因此,空循环不会等待任何外部事件,始终处于运行状态。
  3. 无休眠或等待
    • 空循环没有使用任何形式的休眠或等待指令(如 sleep)。
    • 因此,CPU没有任何时间可以进入空闲状态。
  4. CPU时间片耗尽
    • 现代操作系统使用时间片轮转调度算法(time-slicing)来分配CPU时间。
    • 在时间片内,空循环将会持续运行,直到时间片耗尽,操作系统再将CPU资源分配给其他进程。
    • 在多核CPU系统中,如果每个核心上都有这样的空循环,每个核心的CPU使用率都将达到100%。

大概理解就是:CPU是分时调度的,进程通过时间片申请调度,但是空循环中不存在阻塞,能够一直申请到时间片,于是就会耗尽CPU的调度时间,造成CPU占用100%!

问题拓展

在通过sleep(1)降低CPU的占用的时候,忽然想起很久之前看到的,Java中sleep(0)有什么作用,当时还不算太明白,结合今天的总结,大概最终目的也是让出分时片,防止一个线程对CPU的高占用吧。

在 Java 中,有时候开发者会刻意使用 Thread.sleep(0),这种操作虽然看起来奇怪,但确实有一些实际应用场景和好处。以下是一些主要原因和相关解释:

1. 让出 CPU 时间片

Thread.sleep(0) 可以让当前线程主动放弃 CPU 时间片,从而使调度器有机会重新安排线程的执行。尽管这不会让线程真正休眠(因为时间为 0),但它确实会触发线程调度器的重新调度。

  • 多线程环境:在多线程环境中,如果有多个线程在竞争 CPU 资源,使用 Thread.sleep(0) 可以让当前线程让出 CPU 时间片,使其他线程有机会执行,从而实现更公平的调度。

  • 防止忙等待:在一些情况下,程序可能会进入一个忙等待的循环。如果线程执行了 Thread.sleep(0),它可以防止当前线程长时间占用 CPU,从而提高系统的整体响应性。

2. 提高系统的响应性

在某些情况下,使用 Thread.sleep(0) 可以提高系统的响应性,尤其是在 GUI 应用程序中。

  • 用户界面更新:在 GUI 应用程序中,如果某个线程占用了大量的 CPU 时间,可能会导致界面卡顿。通过使用 Thread.sleep(0),可以让出时间片,使用户界面的更新线程有机会执行,从而提高用户体验。

3. 协助垃圾回收

在 Java 中,垃圾回收器(GC)通常在系统空闲时运行。使用 Thread.sleep(0) 可以让当前线程暂时放弃 CPU,使垃圾回收器有机会运行,清理内存。

  • 内存管理:在内存紧张的情况下,适当的 Thread.sleep(0) 调用可以为垃圾回收器提供机会,减少内存压力。

4. 修复线程调度器的潜在问题

在某些平台和 JVM 实现中,线程调度器可能存在一些调度问题,通过使用 Thread.sleep(0) 可以触发调度器重新调度,有时可以避免一些潜在的问题。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SleepZeroExample {
public static void main(String[] args) {
Runnable task = () -> {
while (true) {
// Do some work here

// Yield the processor to other threads
Thread.sleep(0);
}
};

Thread thread = new Thread(task);
thread.start();
}
}

在这个示例中,Thread.sleep(0) 在循环中被调用,提示调度器可以重新调度其他线程。尽管当前线程不会真正进入休眠,但它有机会让其他线程获得执行时间。

需要注意的事项

  • 平台依赖性Thread.sleep(0) 的行为可能会因平台和 JVM 实现而异。在某些平台上,它可能没有预期的效果。
  • 调度器实现:不同的操作系统和 JVM 可能有不同的线程调度实现,因此需要根据具体情况进行测试和优化。
  • 使用场景Thread.sleep(0) 并不是解决所有线程调度问题的万能方法,应根据具体场景和需求谨慎使用。

总结

使用 Thread.sleep(0) 可以让当前线程主动放弃 CPU 时间片,触发线程调度器的重新调度,从而改善系统的响应性、防止忙等待、协助垃圾回收,并修复一些潜在的调度问题。然而,其效果可能因平台和 JVM 实现而异,因此需要根据具体情况进行测试和优化。