Is your backend CPU-intensive or I/O intensive?

Photo by ian dooley on Unsplash

Is your backend CPU-intensive or I/O intensive?

Understanding your application's resource utilization is paramount in the ever-evolving landscape of backend development. Is it the CPU working tirelessly to crunch numbers, or are the input/output operations causing the delays? Welcome to our exploration of backend workloads and the invaluable insights provided by the top command.

Understanding CPU-Intensive Workloads

As the name suggests, CPU-intensive workloads are tasks and processes that primarily rely on the central processing unit (CPU) for their execution. These tasks require substantial computational power to perform calculations, data processing, or other operations. CPU-intensive workloads can monopolize CPU resources, leaving fewer processing cycles available for other tasks.

To illustrate CPU-intensive workloads, let's consider a few real-world scenarios:

  1. Video Encoding: Video encoding involves compressing video files, a process that requires substantial computational power. Whether you're editing a video or streaming content, video encoding is CPU-intensive.

  2. Scientific Simulations: Scientific simulations, such as climate modeling or molecular dynamics simulations, involve complex mathematical calculations that can utilize all available CPU cores for extended periods.

  3. Data Analysis: Data analysis tasks, especially those involving large datasets and complex algorithms (e.g., machine learning models), can be CPU-intensive.

  4. Compiling Software: Compiling source code into executable programs can be CPU-intensive, especially when dealing with large codebases.

  5. Rendering Graphics: In graphic-intensive applications like 3D rendering or computer-aided design (CAD), rendering complex scenes can tax the CPU.

Understanding I/O-Intensive Workloads

I/O-intensive workloads are a critical consideration in backend computing. They revolve around tasks and processes that heavily rely on input/output (I/O) operations, such as reading from or writing to storage devices, network communications, and file manipulation.

Let's explore some real-world examples to illustrate I/O-intensive workloads:

  1. Database Queries: Database management systems frequently perform I/O-intensive tasks when reading or writing data to disk. Complex queries or high-volume transactions can intensify I/O operations.

  2. File Transfers: File servers and data backup processes involve substantial I/O activity, as files are moved, copied, or retrieved from storage devices.

  3. Network Communications: In networked applications, data is constantly exchanged between systems. Network-intensive tasks, such as streaming media or transferring large files over the internet, can stress I/O resources.

  4. Virtualization and Containerization: Virtual machines (VMs) and containers often rely on I/O operations to manage disk images and network connectivity. These operations can become intensive, particularly in environments with multiple VMs or containers.

  5. Logging and Logging Systems: Logging data to files or databases is a common practice in software applications. When log volumes are high, I/O-intensive logging can affect performance.

A Practical Example

In this section, we will delve into the examination of two Node.js applications, each with distinct performance characteristics. In contrast, the first application will specialize in image resizing, a computationally demanding CPU-intensive task. In contrast, the second application will be dedicated to executing database queries, representing an I/O-intensive task.

To gain valuable insights into the performance dynamics of these workloads, we will employ the powerful top command. This tool will allow us to closely scrutinize how each application utilizes system resources, providing us with a comprehensive understanding of their impact on system performance.

Source code: https://github.com/The-Flash/cpu-io-workload-demo

CPU Intensive Workload

Source code: https://github.com/The-Flash/cpu-io-workload-demo/tree/main/cpu-workload

In this application, we have a server responsible for resizing an Unsplash image and subsequently writing the resized image to the filesystem.

console.log("Process ID", process.pid);
const image = path.join(__dirname, "unsplash.jpg");

app.get("/resize-images", async (_req, res) => {
  try {
    const filename = path.join(__dirname, "outputs", `${uuidv4()}.jpg`);
    await sharp(image).resize(200, 200).toBuffer();
    await fs.writeFile(filename, data);
    res.status(200).json({
      message: `Saved file to ${filename}`,
    });
  } catch (e) {
    res.status(500).json({ e });
  }
});

Load testing

To assess the server's impact over an extended period, we will employ the loadtest tool to simulate traffic.

npm install -g loadtest

To initiate the server, you can use the command yarn start:cpuload.

To simulate traffic, execute the following command in your terminal:

loadtest -n 1000 -c 20 http://localhost:8000/resize-image

While the load test is in progress, we will analyze the top command's output, which will provide us with valuable insights into how this application affects the CPU.

The application conveniently logs its Process ID (PID), allowing us to filter it out using the following command:

top -p PID

From the output, we observe that the server utilizes 407.6% of the CPU (%CPU). You might wonder how this is possible. Well, it's important to note that this CPU consists of eight cores, with each core representing 100%. Therefore, the server's usage of approximately 407.6% indicates that it is utilizing around four cores.

Crucially, we should pay attention to the "0.0 wa" metric on the third line of the output. This metric signifies the percentage of CPU time spent in an idle state due to waiting for I/O operations to complete. It serves as an indicator of the CPU's time spent awaiting data from storage devices or network operations. In this case, the server's minimal I/O activity results in a reading of 0.0.

Based on this analysis, we can confidently conclude that our workload is indeed CPU-intensive.

I/O Intensive workload

Source code: https://github.com/The-Flash/cpu-io-workload-demo/tree/main/io-workload

In the next application, we have a server that writes 10,000 randomly generated names into a PostgreSQL table.

console.log("Process ID", process.pid);
const pool = new Pool({
  user: process.env.DB_USER,
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  password: process.env.DB_PASSWORD,
});

app.get("/data", async (req, res) => {
  try {
    const client = await pool.connect();
    const result = await client.query(
      "INSERT INTO users(name) SELECT CONCAT('NAME_', generate_series) FROM generate_series(1, 10000) "
    );
    const data = result.rows;
    client.release();
    res.json(data);
  } catch (e) {
    console.error(e);
    res.status(500).json({ e });
  }
});

To initiate the server, you can use the command yarn start:ioload.

Let's simulate traffic by executing the following command in the terminal:

loadtest -n 1000 -c 20 http://localhost:8001/data

Let's delve into the impact of this workload on the CPU by harnessing the top command.

top -p PID

In this instance, you'll notice a notable contrast in the %CPU utilization, which registers at a relatively low 6.3%. However, what warrants our attention is the modest uptick in the CPU's waiting time. This uptick is a direct result of our workload necessitating some time to write data to the disk.

This stark difference from the previous application highlights how varying tasks and I/O demands can distinctly influence CPU behavior and system performance.

Other metrics provided by top

  1. us (User): This represents the percentage of CPU time spent executing user-space processes. It indicates the portion of CPU usage attributed to user applications or processes that are not part of the operating system.

  2. sy (System): This represents the percentage of CPU time spent executing system-level processes. It includes tasks that are part of the operating system kernel, such as handling hardware events and system calls.

  3. ni (Nice): This represents the percentage of CPU time spent executing processes with an adjusted priority (niceness). Processes with lower niceness values get more CPU time, while processes with higher niceness values get less. This value is sometimes also referred to as "renice" or "nice" value.

  4. id (Idle): This represents the percentage of CPU time that is idle and not being used. It indicates the amount of available CPU resources that are currently not in use.

  5. wa (Wait): This represents the percentage of CPU time spent in an idle state due to waiting for I/O operations to complete. It's an indicator of how much time the CPU spends waiting for data from storage devices or network operations.

  6. hi (Hardware Interrupts): This represents the percentage of CPU time spent handling hardware interrupts. These interrupts occur when hardware devices need attention from the CPU, such as when data is received from a network interface.

  7. si (Software Interrupts): This represents the percentage of CPU time spent handling software interrupts. Software interrupts occur when software components, such as drivers or the operating system, request attention from the CPU.

  8. st (Steal Time): This value is relevant in virtualized environments. It represents the percentage of CPU time "stolen" by a hypervisor from a virtual machine (VM) running on the host. It occurs when the host system needs more CPU resources and takes them away from VMs.