Konrad Kowalski (rootsher)Principal Platform & Reliability Architect000100011010000010111001001010011010001000110100

Noisy neighbor and CPU overcommit: when one Pod breaks another

date
category
Capacity & Performance
also in
Containers
reading
5 min / 1050 words

CPU is an easy resource to share.

If workload A does not use the processor for a while, workload B can use the free capacity.

That is exactly what enables:

text
CPU overcommit

and means we do not have to give every workload a physical core of its own.

The problem appears when several workloads want to burst at the same time.

Then one of them may start taking CPU away from the others.

That is the classic:

text
noisy neighbor

What CPU overcommit is

Assume a node:

text
16 CPU

We have four workloads:

text
A request = 2 CPU
B request = 2 CPU
C request = 2 CPU
D request = 2 CPU

The scheduler sees:

text
SUM(requests) = 8 CPU

that is, only half of the node's capacity.

But each workload may in reality burst up to:

text
8 CPU

if it has no limit.

The theoretical maximum demand:

text
4 x 8 CPU = 32 CPU

on a machine with:

text
16 CPU

That is overcommit.

We did not physically allocate:

text
32 CPU

We simply assume:

text
not everyone will need their maximum at the same time

Overcommit on its own is not a problem

Assume:

text
A uses 6 CPU
B uses 1 CPU
C uses 0.5 CPU
D uses 1 CPU

In total:

text
8.5 CPU

on a node with:

text
16 CPU

Everything works fine.

Workload A uses far more than its:

text
request = 2 CPU

and that is perfectly normal.

It is simply using the idle capacity the others left behind.

Overcommit is therefore a mechanism that lets us increase infrastructure utilisation.

Without it we could have:

text
requests = 16 CPU
actual usage = 5 CPU

and half the machine sitting idle.


The problem starts with a simultaneous peak

Now every workload suddenly needs:

text
8 CPU

Demand:

text
A = 8
B = 8
C = 8
D = 8

SUM = 32 CPU

Capacity:

text
16 CPU

We have:

text
demand = 2 x capacity

All that work cannot be executed at once.

Tasks start waiting in the runqueue.

We get:

text
CPU contention

and that is exactly when the CPU weights of the individual workloads start to matter.


A request starts acting as a negotiating position

Assume two Pods:

text
Pod A request = 1 CPU
Pod B request = 4 CPU

Both have no limit and both want to use:

text
8 CPU

As long as the node has plenty of free CPU, both may get more than their request.

But once the processor starts running short, their relative weights derived from the requests start to matter.

Simplified:

text
A : B
1 : 4

So Pod B has a much stronger position under contention.

That means an understated request is not only a kube-scheduler problem.

It may also mean:

text
a smaller share of CPU
when the node is overloaded

Noisy neighbor

Assume a node:

text
8 CPU

Two workloads run on it.

API

text
request = 4 CPU
normal usage = 3 CPU

Batch

text
request = 500m
normal usage = 200m

In the normal situation:

text
API   = 3 CPU
Batch = 0.2 CPU

There is no problem.

Now batch starts intensive processing and wants:

text
8 CPU

Total demand:

text
API wants   4 CPU
Batch wants 8 CPU

SUM = 12 CPU

on a node with:

text
8 CPU

Batch becomes a noisy neighbor.

Not because it is doing anything "illegal".

If it has no limit, it may try to use all the available capacity.

The problem is that its burst starts affecting the API's latency.


Why a noisy neighbor can be hard to spot

We look at the API:

text
CPU usage = 3 CPU

Normally:

text
CPU usage = 3 CPU

During the problem:

text
CPU usage = 3 CPU

At first glance nothing changed.

But the API may now want:

text
5 CPU

and only be getting:

text
3 CPU

The missing execution time materialises as:

text
runnable waiting

That is:

text
PSI rises
runqueue latency rises
application latency rises

The application's CPU usage itself may not go up at all.

That is why a noisy neighbor is often misdiagnosed as:

text
the application slowed down by itself

Understated requests lead to aggressive bin packing

Assume a node:

text
16 CPU

We have 16 Pods.

Each really needs, during normal traffic:

text
1 CPU

but declares:

text
request = 250m

The scheduler sees:

text
16 x 0.25
=
4 CPU

So the node looks very loosely filled.

It may therefore accept more Pods.

The real demand, however, is already:

text
16 CPU

that is, the whole capacity.

Every further burst leads straight to contention.

In such a system the problem is not that Linux schedules badly.

The problem was created earlier:

text
the request does not describe the real demand

Overstated requests have the opposite cost

We can also overshoot in the other direction.

The workload really uses:

text
500m

but has:

text
request = 4 CPU

Ten such workloads:

text
real usage = 5 CPU
requests   = 40 CPU

The scheduler needs far more nodes than the real usage would suggest.

We get:

text
poor bin packing
low utilisation
higher cost

That is why a correct request is not:

text
as small as possible

nor:

text
as large as possible, to be safe

It is an attempt to describe the workload's real CPU demand in a way that makes sense for that specific kind of application.


Batch and a latency-sensitive workload on one node

This is the classic noisy neighbor case.

Assume:

text
API:
request = 4 CPU
no limit

Batch:
request = 1 CPU
no limit

Node:

text
8 CPU

Normally:

text
API   = 3 CPU
Batch = 1 CPU

That leaves:

text
4 CPU idle

Batch can use that capacity.

That is good.

The problem starts when:

text
API wants = 6 CPU
Batch wants = 8 CPU

Both workloads try to burst.

Demand:

text
14 CPU

Capacity:

text
8 CPU

The scheduler has to divide the processor.

Batch may still do a huge amount of work while at the same time increasing the API's scheduler wait.

That is the typical conflict:

text
throughput workload
vs
latency-sensitive workload

Does a CPU limit solve noisy neighbor?

It may.

Assume:

text
Batch:
request = 1 CPU
limit   = 2 CPU

Now batch cannot use:

text
8 CPU

even if it wants to.

Its maximum bandwidth:

text
2 CPU

That protects the other workloads from a very aggressive batch burst.

But we pay for it with another property:

text
if the node has 6 CPU idle,
batch may still use at most 2 CPU

So a limit improves isolation at the cost of being able to use idle capacity.

That is a trade-off.


Without limits we use CPU more efficiently

Assume:

text
node = 16 CPU

The workloads normally need:

text
8 CPU

but one batch job can use an extra:

text
8 CPU

Without a limit:

text
normal:
8 CPU workload
8 CPU idle

batch active:
16 CPU used

Excellent hardware utilisation.

With a batch limit of:

text
2 CPU

during batch processing we may have:

text
10 CPU used
6 CPU idle

even though the application has work to do.

Limits give predictability.

No limits give flexibility.


CPU overcommit is therefore a capacity-planning decision

There is no universal value like:

text
good overcommit = 2:1

Everything depends on the characteristics of the workloads.

If you have 100 services that:

text
use 10% of their peak CPU most of the time

heavy overcommit may work very well.

If they all scale from the same event:

text
Black Friday
a cron at 00:00
a restart after a deploy
cache invalidation
a sudden traffic spike

their bursts are correlated.

Then the assumption:

text
not everyone needs CPU at the same time

stops working.


Correlated bursts

This is an especially important case.

Assume ten workloads.

Each with:

text
average = 500m
peak    = 2 CPU

If the peaks appear randomly, on average we can share capacity well.

But if they all react to the same traffic:

text
request enters frontend
       |
       v
service A
       |
       +--> B
       +--> C
       +--> D

a rise in traffic can increase the demand of the whole service graph at once.

Then:

text
peak A
peak B
peak C
peak D

are not independent.

Overcommit based only on average CPU may look excellent right up until real production traffic.


How to recognise a noisy neighbor

The typical picture:

text
application latency rises

but:

text
the application is not being throttled

At the same time:

text
CPU PSI rises

and on the node:

text
other workloads increase their CPU usage sharply

That is a signal:

text
the problem may be shared,
not local to a single Pod

What matters most is comparing the behaviour of:

text
the workload

with:

text
the rest of the node

If one Pod's problem overlaps in time with another one's large burst, we have a very strong lead.


Mental model

Overcommit:

text
SUM(possible CPU demand)
>
physical CPU capacity

is not a problem in itself.

The problem starts when, at the same time:

text
actual runnable demand
>
physical CPU capacity

That is when contention appears.

A noisy neighbor is a workload whose change in demand worsens the execution conditions of other workloads sharing the same resource.

Most importantly:

text
a CPU request

is not just a number needed to pass the admission scheduler.

It influences:

text
placement
bin packing
relative CPU weight
behaviour under contention

Understated requests let you use a cluster very efficiently, right up to the moment real demand arrives all at once.

And then overcommit stops being an optimisation.

It becomes a queue of runnable tasks.