CPU Pinning with taskset and cpuset for Worker Isolation
AI generated
$
/etc
Linux · CPU Pinning · cpuset · Worker Isolation
CPU Pinning with taskset and cpuset
How PHP-FPM workers and batch jobs stop fighting over cores

nice and ionice only lower the priority of background jobs, but do not prevent them from briefly occupying entire cores. CPU pinning goes a step further: it reserves specific cores exclusively for PHP-FPM workers, while reindexing and cron are only allowed to run on other cores.

17 min read taskset · cgroups cpuset · systemd AllowedCPUs Linux · Multi-Core Servers · Magento

1. Why CPU pinning goes beyond nice

nice and ionice lower the priority of background jobs, but guarantee no fixed separation: as soon as a core is free, even a low-priority process is allowed to occupy it completely for a moment. For many scenarios this is enough, but for very large reindex jobs or compute-intensive batch processing, even this brief full utilization of a core can cause noticeable latency spikes for concurrently running PHP-FPM workers, if the scheduler happens to move the worker process onto exactly that core in the meantime.

CPU pinning solves this problem structurally: instead of only lowering priority, a process or process group is assigned a fixed subset of the available CPU cores that it stays confined to. PHP-FPM workers then run exclusively on a reserved core group, while reindexing, backups, and other batch jobs use a completely separate core group. Both workloads can then run simultaneously at full load without affecting each other.

This article shows how CPU pinning is implemented with taskset for individual processes and with cgroups cpuset for permanent isolation, and how this isolation can be sensibly combined with a server's NUMA topology.

2. How the scheduler distributes processes without pinning

Without explicit configuration, the Completely Fair Scheduler (CFS) continuously decides on which core a process runs next, based on current utilization and cache locality. This flexibility is sensible for most workloads, because it automatically maintains balanced core utilization without requiring manual administrator intervention.

For very different workload types on the same server, this exact flexibility is sometimes a disadvantage: a compute-intensive batch job and a latency-sensitive PHP-FPM worker can repeatedly get scheduled onto the same core, leading to cache eviction, so-called cache thrashing, where constantly alternating processes overwrite the same CPU cache with different data. CPU pinning prevents this back-and-forth by giving the scheduler, from the outset, only a subset of cores to choose from for each workload type.

3. taskset: setting CPU affinity for individual processes

The taskset command sets or reads a process's CPU affinity mask, i.e. the set of cores the process is allowed to run on. A new process can be started directly with a core mask, an already-running process can be adjusted afterward by PID without restarting it. This form of CPU pinning is ideal for quick, targeted tests, for example to check whether a specific batch job actually benefits from a core restriction before setting up a permanent solution.

For production configurations, however, taskset is only a first step, because the binding does not automatically apply to newly started child processes and has to be manually repeated on every restart of the target process. For permanent, inheritable isolation, cgroups cpuset is a much better fit.


# Start a new process pinned to cores 4-7 only
taskset -c 4-7 php bin/magento indexer:reindex

# Pin an already-running process by PID to cores 0-3
taskset -cp 0-3 24817

# Check the current CPU affinity of a running process
taskset -cp 24817

4. cgroups cpuset: reserving core groups permanently

The cgroups cpuset controller allows an entire process group to be permanently confined to a fixed set of cores, including all child processes started later within that group. This makes cpuset the more robust choice for CPU pinning on production servers: one cgroup for PHP-FPM workers with cores 0 to 7, a separate cgroup for batch jobs with cores 8 to 15, and both groups persist regardless of individual process restarts.

On modern distributions with cgroups v2, these core groups can be managed directly through the filesystem under /sys/fs/cgroup/, via the cpuset.cpus file per group. Additionally, cpuset.mems analogously controls which NUMA memory nodes are available to a group, bringing CPU pinning and NUMA binding together in a single configuration.


#!/usr/bin/env bash
# setup-cpusets.sh — create dedicated cgroups for web vs. batch workloads
set -euo pipefail

mkdir -p /sys/fs/cgroup/web-workers
mkdir -p /sys/fs/cgroup/batch-jobs

# Web workers get cores 0-7, memory from NUMA node 0
echo "0-7"   > /sys/fs/cgroup/web-workers/cpuset.cpus
echo "0"     > /sys/fs/cgroup/web-workers/cpuset.mems

# Batch jobs get cores 8-15, memory from NUMA node 1
echo "8-15"  > /sys/fs/cgroup/batch-jobs/cpuset.cpus
echo "1"     > /sys/fs/cgroup/batch-jobs/cpuset.mems

5. Restricting PHP-FPM pools to dedicated cores

For PHP-FPM itself, CPU pinning can be configured via the systemd service wrapper or directly at the start of the master process, so that all worker child processes it spawns automatically inherit the binding. It is important that the binding is applied to the master process, not individually to every single worker, because PHP-FPM starts and stops workers dynamically, depending on the configured process manager mode.

With multiple PHP-FPM pools for different Magento websites on the same server, CPU pinning can additionally be used to separate critical pools from less critical ones, for example the pool for the main shop on more heavily prioritized cores, while a rarely used B2B pool runs on a smaller set of cores.


# /etc/systemd/system/php8.4-fpm.service.d/cpuset.conf
[Service]
# Restrict the PHP-FPM master and all its worker children to cores 0-7
AllowedCPUs=0-7

6. Configuring AllowedCPUs in systemd units

Since systemd version 244, AllowedCPUs= is available as a native directive that directly sets a cgroups cpuset restriction for a unit, without requiring manual management of files under /sys/fs/cgroup/. This directive can be applied to any service, slice, or scope unit and is automatically inherited by all processes that unit later spawns.

For clean, system-wide separation, it is recommended to create a dedicated systemd slice for web workloads and another for batch jobs, each with its own AllowedCPUs= value, and to assign all relevant services to that slice. This centralizes the CPU pinning configuration in one place, instead of scattering it across many individual unit files.


# /etc/systemd/system/web-workers.slice
[Slice]
AllowedCPUs=0-7

# /etc/systemd/system/batch-jobs.slice
[Slice]
AllowedCPUs=8-15

# Assign a service to a slice:
# systemctl set-property php8.4-fpm.service Slice=web-workers.slice

7. Planning CPU pinning and NUMA topology together

CPU pinning without regard for a server's NUMA topology can create new problems of its own: if PHP-FPM workers are pinned to cores on node 0 while the memory they use, for example OPcache shared memory, is allocated on node 1, additional remote memory accesses occur, partly undoing the isolation. The core groups for CPU pinning should therefore always be drawn along actual NUMA node boundaries, determined via numactl --hardware.

For a two-node server, this means in practice: pin PHP-FPM workers to the cores and memory of node 0, batch jobs to the cores and memory of node 1. This combination of CPU pinning and NUMA binding delivers the strongest isolation, because neither CPU time nor memory bandwidth needs to be shared between the two workload types.

8. Common mistakes in CPU pinning

The most common mistake is reserving too few cores for the web worker group, under the assumption that more isolation is always better. If the reserved core set is too tight, PHP-FPM can no longer overflow onto unused cores of the batch group during load spikes, even though they might be free at that moment, artificially limiting the server's overall capacity. Core allocation should therefore always be based on realistic load measurements, not a blanket rule of thumb.

A second mistake is setting up CPU pinning without checking whether the hypervisor on virtualized servers even respects the configuration. On heavily overcommitted cloud instances with shared vCPUs, a cpuset restriction set in the guest system can remain ineffective, because the host manages the actual core allocation independently anyway.

9. Isolation strategies compared

The following table compares the different approaches to CPU pinning and isolation on a Magento server.

Approach Persistence Degree of isolation
nice / ionice Per process start Low, relative priority only
taskset Manual after every restart Medium, hard core restriction
cgroups cpuset (manual) Permanent, inheritable High, including child processes
systemd AllowedCPUs / Slice Permanent, centrally managed High, system-wide consistent

For production Magento servers, the combination of systemd slices with AllowedCPUs= and NUMA-aware core allocation is the most robust solution. taskset remains useful for quick tests, but should not be the only measure for a permanent production configuration.

Mironsoft

Worker isolation and CPU pinning for Magento servers

Are batch jobs and PHP-FPM fighting over the same cores?

We set up NUMA-aware CPU pinning via systemd slices, separate web workers from batch jobs, and validate the isolation under real load.

Core allocation

Plan web and batch core groups based on real load measurements

systemd slices

Configure AllowedCPUs centrally and permanently

NUMA alignment

Align CPU and memory binding along node boundaries

10. Summary

CPU pinning goes beyond the relative prioritization of nice and ionice and creates a hard, structural separation between workload types. taskset is suitable for quick, targeted tests of individual processes, but offers no persistence across restarts. cgroups cpuset and the systemd directive AllowedCPUs= provide permanent, inheritable isolation that automatically applies to all child processes of a group.

CPU pinning has its greatest effect when planned together with a server's NUMA topology: core groups should be drawn along NUMA node boundaries, so that neither CPU time nor memory bandwidth needs to be shared between isolated workloads. For production Magento servers, combining systemd slices with NUMA-aware core allocation is the most robust and lowest-maintenance solution.

CPU Pinning with taskset and cpuset — Key Takeaways

Quick test

taskset -c for manual, temporary core restriction of individual processes.

Permanent isolation

cgroups cpuset.cpus or systemd AllowedCPUs= per slice.

Respect NUMA

Draw core groups along node boundaries from numactl --hardware.

Verify capacity

Base core allocation on real load measurements, not a rule of thumb.

11. FAQ: CPU Pinning with taskset and cpuset

1CPU pinning vs. nice?
nice is relative priority, CPU pinning is a hard core restriction.
2taskset or cpuset?
taskset for tests, cpuset or AllowedCPUs for permanent production.
3Do child processes inherit the binding?
Yes with cgroups and systemd, with taskset only under certain conditions.
4Set up CPU pinning for PHP-FPM?
Via AllowedCPUs= in a systemd override file for the service.
5What does cpuset.mems do?
Sets allowed NUMA memory nodes for a cgroup.
6Why respect NUMA?
Otherwise additional remote memory accesses occur despite core isolation.
7How many cores for web workers?
Based on real load measurements, not a rule of thumb.
8Does it work on every cloud instance?
Not always, overcommitted shared vCPUs can nullify the effect.
9What is a systemd slice?
A cgroups grouping of multiple units with shared resource limits.
10Does it replace nice and ionice?
No, both mechanisms complement each other.