CHAPTER 05Part II · Fennel, year 3

Who runs where: scheduling, resources and cost

Fennel’s first full cloud bill since it moved onto Kubernetes is higher than the last one before the move. That same month, a Pod wouldn’t start and a service kept restarting. This chapter traces all three problems back to the same two numbers, which each team wrote down for every service, and shows how Kubernetes uses them to decide where each Pod runs and how many machines Fennel pays for.

Read
25 MIN
For
TECHNICAL MANAGERS
Chapter
05 / 15
Checks
08

§ 01 Year 3: the first bill since the move

Cast

Morgan
Co-founder. Runs Fennel’s money and reads the cloud bill.
Jordan
Co-founder, now managing the engineers: that’s you, in this story.
Ari
Platform lead. Led Fennel’s move onto Kubernetes.
I didn’t read the last chapter. What happened?
  • In Chapter 4, in Year 3, Fennel moved onto managed Kubernetes. Fennel writes down what it wants running, such as “3 copies of storefront”, in files called manifests, and Kubernetes keeps those goals in the cluster’s record. When Ari applied Fennel’s manifests to the new cluster, its services were placed on the cluster’s four nodes, 01 to 04. Ari applied storefront’s manifest last, and Kubernetes put its three Pods on three different nodes.
  • Many small loops each compare one part of the record with what is actually running, and act when the two differ. The ReplicaSet loop writes a record for each Pod (Kubernetes’ wrapper around a running container), the scheduler gives each new Pod a node (a machine in the cluster), and the node agent on that node starts it.
  • One Saturday at 03:04, node 03 died. With default settings, its Pods were replaced on other nodes within about six or seven minutes, and nobody had to wake up. Since then the cluster has run on three nodes. On a Wednesday, a missing setting made notifications crash again and again; its node agent kept restarting it, waiting longer each time, which engineers call a crash loop.
  • To understand any loop, ask four things: which loop is it, what does it want (the desired state), what does it see (the real state), and what does it do when the two differ?
RETRIEVECheck 01 / 08

At 03:04 in Chapter 4, a new orders Pod appeared in the cluster’s record with no node yet, and soon it was running on node 04. Which loop gave it node 04, and what did that loop see?

Since 03:04, when node 03 died, Fennel’s cluster has run on three nodes: 01, 02 and 04. On Friday evening, as the dinner rush began, the dispatch team asked for twice as many copies of dispatch, and one of the new Pods didn’t start. Ari’s dashboard listed it as Pending for most of an hour, until Ari’s team rented two more machines and added them to the cluster as nodes 05 and 06.

On Tuesday, the orders Pod restarted three times between lunch and the end of the dinner rush. Each time, customers saw errors for a moment, and each time the dashboard gave the same reason: OOMKilled.

At the end of the month, Morgan opened the first full cloud bill since the move. Before the move, Fennel had given back the spare machines Sam used to keep and was running on four, and the new bill was higher than the last one from those days.

“We moved onto Kubernetes so our machines would be used better. Now we pay for five of them instead of four, and Ari’s dashboard says they’re less than a third busy, even on a Friday night. What are we paying for?”

Morgan

Jordan forwards the question to Ari, whose answer covers Friday and Tuesday as well.

“All three come back to two numbers our teams wrote down for every service when we moved.”

Ari

Ari’s answer starts with the loop that picked node 04 for orders after node 03 died, and it ends with the questions Jordan should ask before anyone changes those two numbers.

§ 02 What the scheduler counts

In Chapter 4’s drawings, every Pod had a size, every node had room for six units, and the scheduler looked for “a node with room”. Kubernetes didn’t measure those sizes, though. Each team wrote them into its service’s manifest when Fennel moved, declaring how much memory and how much processor each copy of the service should have set aside for it. The figure below shows the part of storefront’s manifest that does this. Engineers call these numbers the Pod’s requests. In it, storefront’s two units of room appear as its real memory request: 4 GB.

Fig. 01A manifest · storefront
# … the rest of
# storefront’s manifest
containers:
- name: storefront
image: storefront:v38
resources:

how much each copy needs

requests:

set aside for each copy

memory: 4Gi

4 GB of memory

cpu: 500m

half a processor core (engineers write thousandths of a core)

Fig. 01A team writes its requests into the manifest. Kubernetes doesn’t measure them; it reserves exactly what’s written.

New term · 01 / 04

requests

The memory and processor a Pod reserves on its node, written in its manifest; the scheduler places Pods by them.

memory and processor, counted separately

Chapter 4’s “room”, written down.

Each node, in turn, offers a fixed amount of both. Fennel’s nodes are rented machines with 16 GB of memory and 4 processor cores each. Part of every machine is kept back for its operating system and for Kubernetes’ own programs, so the Pods on a node can reserve only about 13 GB of memory and 3.9 cores between them.

Why can Pods reserve only about 13 GB of a 16 GB node?

Every machine runs its own operating system and Kubernetes’ own programs, such as the node agent, and they need memory and processor too. So part of each node is kept back for them, and the scheduler operates only on the rest, which engineers call the node’s allocatable room. On one provider, for example, a 16 GB node keeps back up to about 2.7 GB of memory, depending on the version it was set up with, but less than a tenth of a core, which is roughly where Fennel’s 13 GB and 3.9 cores come from. Small machines lose a bigger share: on the same provider, a 4 GB node keeps back about a quarter of its memory.

Memory and processor are counted separately, and a Pod fits on a node only if both of its requests fit into what that node still has unreserved. The figure below shows why that matters: a node can have plenty of memory left while its processor is nearly all reserved, and then a new dispatch Pod doesn’t fit there, however much memory is free.

Fig. 02One node, two counts
Fig. 02A Pod fits on a node only if both of its requests fit. This node has memory to spare, but its processor is nearly all reserved.

The loop that makes this check is the scheduler, the part of Kubernetes that gives each new Pod a node. It works in two steps. First, it rules out every node that doesn’t have room for the Pod’s requests. Then it scores the nodes that are left and picks the one with the best score, choosing at random if two are tied. With default settings, the score mainly rewards the node with the most room left unreserved. The scheduler also prefers, by default, to spread copies of the same service across different nodes, but only as a preference: if it can’t spread them, it places them anyway.

That’s why, in Chapter 4, when Ari applied storefront’s manifest to the new cluster, Kubernetes put its three Pods on three different nodes. Nodes 01 and 04 had the most room left, so the first Pod went to one of them and the second to the other. The third went to node 02, which had no storefront copy yet and more room than node 03. Notice what the scheduler wasn’t trying to do: fit Fennel onto as few nodes as possible.

Teams can also add placement rules of their own, such as keeping a service’s copies on different nodes, sending some Pods only to particular machines, or keeping ordinary work off special ones; your engineers will tune these.

PREDICTCheck 02 / 08

On a quiet Tuesday morning, Ari’s dashboard shows node 02 at 15% busy. The Pods already on it have reserved all but 1 GB of its memory. A new Pod requests 2 GB. Will the scheduler put it on node 02?

The scheduler never looks at how busy a node is. When it checks for room, it counts what the Pods on each node have reserved, not what they are using. The Kubernetes documentation says the scheduler refuses a node whose reservations are full “although actual memory or CPU resource usage on nodes is very low”, and it gives the reason: the room is protected for when “resource usage later increases, for example, during a daily peak in request rate.” A reservation is a promise to the Pod, and the scheduler keeps it even while the Pod isn’t using the room. A reservation isn’t a cap, though: a Pod can use less than it reserved, or more, as long as its node has room to spare. What stops it, and what happens when the room runs out, come later in the chapter.

ANALOGYYou know this from budgeting. Think of each node as a department’s budget, each request as the budget a project reserves, and the scheduler as finance, which approves a new project only in a department whose reservations still leave room for it. Whether the projects have actually spent their money doesn’t come into it.

WHERE THIS ANALOGY BREAKSA finance team would notice a department sitting on unspent money and move it to where it’s needed. The scheduler never does: reserved room that a Pod isn’t using stays reserved, at 3 a.m. as at the dinner rush, and no other Pod can be placed in it.

In Chapter 4’s terms, the scheduler is one implementation of the reconcile loop, and you can now fill in its loop lens completely. What it wants, the desired state, is every Pod on a node with room for its requests. What it sees, the real state, is Pods that have no node, and how much memory and processor each node has left unreserved. When the two differ, because a Pod has no node, it rules out the nodes that are too full, picks the emptiest of the rest, and writes that node into the Pod’s record. That leaves one question for the next section: what happens when no node passes the first step?

§ 03 Friday, 17:45: a Pod with nowhere to go

Since node 03 died, Fennel’s cluster has run on three nodes, 01, 02 and 04, and the Pods on them have reserved most of their room. Friday’s dinner rush is when dispatch, the service that assigns drivers to orders, is busiest. At 17:45 the dispatch team decides it wants six copies running tonight instead of three, and changes the number in dispatch’s manifest.

The following widget follows the next hour step by step as you scroll. It’s drawn similarly to how it was in Chapter 4, with one change: it now counts in real units. Each node’s column stands for the 13 GB of memory its Pods can reserve, each Pod’s height is its memory request, and memory that no Pod has reserved is drawn as dashed space. Processor is reserved separately, so it gets its own line at the foot of each node, counting the cores its Pods have reserved out of the 3.9 they can. During the scheduler’s steps, each node also shows its verdict for the Pod being placed: it fits, or the node is short of memory, of processor, or of both. The record and the loops work as they did in Chapter 4.

  1. PREDICTCheck 03 / 08

    The third new dispatch Pod needs 2 GB of memory and one processor core. Between them, the three nodes still have 3 GB of memory and about 3 cores free. What happens to it?

  2. Answer the question above to see what happens

  3. Answer the question above to see what happens

  4. Answer the question above to see what happens

W-01Fennel’s requests and nodes are fiction. The scheduler’s reason is in the format engineers see; its exact wording varies between Kubernetes versions.

For most of that hour, the third dispatch copy was pending, and Pending is the word Ari’s dashboard showed for it.

New term · 02 / 04

pending

A Pod the scheduler hasn’t been able to place yet. It waits, and the scheduler keeps trying and records why.

Not failed; it waits until something makes room.

Each time the scheduler fails to place a Pod, it records why. On Friday the line read 0/3 nodes are available: 2 Insufficient cpu, 3 Insufficient memory., which is in the format engineers see. Read it from left to right. “0/3 nodes are available” means that none of the three nodes passed the first step. The rest counts what each node was short of: all three lacked 2 GB of free memory, and two of them, 01 and 02, also lacked a whole free core. So the line already answers the first question to ask about a pending Pod: what is short, and where?

Notice what the line doesn’t say, which is that the cluster was full. Between them, the three nodes had 3 GB of memory free, more than the Pod’s request of 2 GB, but only in pieces too small for it; free room stranded like this is common on real clusters, and it matters to Morgan, because those leftover pieces on 01, 02 and 04 are room Fennel pays for that no Pod of this size can use.

There are two wrong ways to read Pending. It isn’t a failure: nobody has to resubmit anything, because the scheduler keeps trying and places the Pod as soon as a node has room for it. But it isn’t harmless either, because in Fennel’s cluster no loop makes room by itself. Someone has to add a node or free some room, as Ari’s team did at about 18:40. (Chapter 8 introduces a loop that can add nodes.) When a Pod is pending, these are the questions to ask:

  • “What’s short, and on how many nodes?”
  • “Is it short because demand really grew, or because what we reserved is more than we use?”

The second question leads straight to Morgan’s bill.

Is “pending” always about room?

No. Pending also covers the time a Pod that already has a node spends downloading its image before it can start, as in Chapter 4. The scheduler’s recorded reason tells the two apart: a Pod waiting for room has one, and a Pod that is downloading its image already has a node.

§ 04 What Morgan pays for

Fennel rents its machines from a provider, the cloud company that owns them. In the provider’s standard mode, which is the one Fennel uses, the bill counts machines: Fennel pays for each node for as long as it exists, whether the room on it is reserved, used or empty. So Morgan’s five nodes constitute the bill. Nothing in Fennel’s cluster rents or adds nodes by itself. Ari’s team decides how many machines to rent and adds them by hand (Chapter 8 introduces a loop that can). What the requests decide is how many nodes Fennel needs: on Friday, the Pods’ reservations didn’t fit on three nodes, so a Pod waited until Ari’s team added two.

PREDICTCheck 04 / 08

After Friday, the payments team doubles its memory request “to be safe”. Its Pods use no more memory than before. What does change for Fennel?

The following widget works out how many nodes Fennel needs for a given set of memory requests. Like the scheduler, it places every Pod by its requests, with both of its numbers on one node; unlike the scheduler, it packs the Pods as tightly as they will go, so it counts the fewest nodes Fennel could rent. It starts with the memory requests Fennel’s teams have been using in this chapter, and keeps room for one more node so that Fennel can lose one. Each Pod’s outline is what it reserves; the fill inside is what it used at last Friday’s rush. There is no target. Some things to try:

  • Set each request to what that service used at the rush. How many nodes does Fennel need now? Look at the processor line under each node: what stops the count going lower?
  • Double payments’ request, as in the question above. Does the count change? How far can it go before it costs a node?
  • Push orders past 13 GB. What happens?

W-02The widget packs Pods as tightly as they go, to count the fewest nodes; the scheduler, by default, spreads Pods out instead, so a real cluster needs at least this many. Each change places every Pod afresh, while a real cluster never moves running Pods. Processor requests, and the memory request of notifications, stay as the teams set them.

A Pod whose request is bigger than the room any node has for Pods is never placed: it stays pending for good, however long the scheduler keeps trying. The answer is a bigger node, a machine with more memory, which Ari’s team would rent and add by hand.

PREDICTCheck 05 / 08

A month on, nodes 05 and 06 are still nearly empty. With default settings, what does Kubernetes do about them?

Fitting Pods’ reservations onto as few nodes as possible is called bin packing, and the widget above does it to count the fewest nodes Fennel could manage with. The scheduler, by default, doesn’t. It prefers the emptiest node and spreads copies out, which is safer for Pods but isn’t the cheapest arrangement. Packing tightly is a setting engineers can switch on, and some fully managed modes pack for you. Either way, the scheduler only places new Pods. It never moves running ones to empty a node, because Pods are replaced, not moved.

The widget’s “Room for Pods on the rented nodes” bar splits the room nobody uses into two kinds, room no Pod reserved (spare nodes, or pieces stranded too small for any Pod) and room reserved but not used, and the “less than a third busy” on Ari’s dashboard counts both.

How common is this?

Two surveys give a rough sense, each from one source. One vendor’s dataset, Datadog’s State of Cloud Costs 2024, comes from organizations that use its cost tool on their AWS bills, and covers all containers on AWS machines, not only Kubernetes, from May 2023 to April 2024. It found that 83% of container spend went on idle room: about 54 points on room no Pod had reserved, and 29 on requests bigger than the workloads needed.

In a community survey by the CNCF, run from June to November 2023 with just over 100 respondents, 49% said Kubernetes had driven their cloud spend up. Over-provisioning, which means paying for more capacity than workloads need, was by far the most-cited cause, named by 70%.

These are single surveys, not industry averages.

Some setups bill per Pod instead of per machine. They charge for what each Pod requests, not for what it uses, so the lesson is the same: padding costs money.

What does a per-Pod mode charge for, exactly?

On one provider, the per-Pod mode charges, per second, for the processor, memory and temporary disk space that each running Pod requests, not for what it uses. Pods that are waiting to be placed aren’t billed there, but Pods that keep crash-looping are. Another provider’s per-Pod option adds 256 MB to each Pod for Kubernetes’ own parts and rounds up to the next size it offers, so a Pod that requests 1 core and 8 GB is given, and billed for, 2 cores and 9 GB. Not every fully managed mode bills per Pod: some still bill per machine. Prices are the subject of Chapter 14.

Here is the answer to Morgan’s question: Fennel rents five nodes because the teams’ padded requests filled three nodes on paper while the services used far less, so Ari’s team rented two more. One of those, node 06, has sat nearly empty since, a node’s worth of spare room; whether to keep it is a choice, and the next section explains why Fennel might want to do that. Requests closer to what each service uses at its busiest would let Fennel rent fewer nodes, but not as few as possible.

§ 05 Room on purpose

Nodes go down, as node 03 did in Chapter 4. When one does, its Pods are replaced, and the scheduler has to place the replacements on the nodes that are left; if those nodes are full, the replacements wait for room, like the dispatch Pod at the dinner rush. Spare nodes are insurance against that: room kept free so that a lost node’s Pods have somewhere to go. The insurance is paid for a node at a time, and the “Keep room to lose a node” switch in the widget above shows what it costs; how much to keep is a decision for engineers and finance together.

“If we lose a node during the dinner rush, where do its Pods go?”

The question to ask

§ 06 Tuesday: when a Pod uses too much

On Tuesday, Ari’s dashboard showed the orders Pod, orders-q2rw, with a restart count of 3. Its last state read Terminated · Reason: OOMKilled · Exit code: 137. Engineers will recognise the 137, but what matters for you is the word before it.

A Pod can use more than it reserves, as §02 said, but not without end. A manifest can also set limits: the most memory and processor each copy may use on its node. Here is orders’ manifest with both: it reserves 6 GB of memory and may use up to 7 GB.

Fig. 03A manifest · orders
resources:
requests:

set aside for each copy (what the scheduler counts)

memory: 6Gi
cpu: "1"
limits:

the most each copy may use

memory: 7Gi

over this, the container is killed

cpu: "1"

at this, the container is slowed

Fig. 03Requests are what each copy reserves; limits are the most it may use on its node.

New term · 03 / 04

limits

The most memory and processor a Pod’s container may use. They cap use; reserving room is the request’s job.

Over the memory limit: killed. Over the processor limit: slowed.

The chart below shows orders’ memory through Tuesday, against those two numbers.

Fig. 04orders · memory, Tuesday
Fig. 04Three numbers about one Pod: what it reserves, what it may use, and what it used. A single day can’t tell you why the line keeps climbing.

Each time orders’ memory reached 7 GB, the operating system on the node stopped the container. Engineers say it was OOM-killed, from “out of memory”. The node agent then restarted the container, in the same Pod on the same node, so this was not a replacement like the ones after node 03 died, and the restart count went up by one. Each restart left a short gap in which customers couldn’t place orders. If the kills keep coming, the node agent waits longer and longer between restarts, just as it does with any crash-looping container, as we saw in Chapter 4.

New term · 04 / 04

OOM-killed

Stopped by the node’s operating system for going over its memory limit (“out of memory”), then restarted in place.

Engineers see OOMKilled, and exit code 137.

Processor works differently. A container that reaches its processor limit isn’t killed. Instead it is slowed down to its limit (engineers say throttled), because processor time is handed out in slices that can be held back, while memory a program is already holding can’t be taken away. So the symptom of a processor cap is “it got slow”, and the symptom of a memory cap is “it restarted”.

PREDICTCheck 06 / 08

At the next sale, storefront needs more processor than its limit allows, and orders needs more memory than its limit allows. What do customers notice?

ANALOGYBack to budgets. If a request is the budget a project reserves, a limit is the spending cap on the project’s card. A project may spend beyond its reservation, up to its cap, while the department has money to spare.

WHERE THIS ANALOGY BREAKSReal spending caps work one way, but Kubernetes has two. The processor card never declines; it just slows the spending down. The memory card cuts the project off and starts it again from nothing. And spending above your reservation isn’t safe just because it’s allowed: when money runs short, the projects that are over their reservation are cut first, as §07 explains.

What if a team sets no memory limit at all?

Then nothing caps it. The container can use all the memory on its node, and if the node runs out, the Kubernetes documentation says a container with no limits is more likely to be the one killed. Leaving the limit off isn’t the safe choice: it lets one container put the whole node at risk. The opposite case also exists: if a team sets only a limit and no request, Kubernetes uses the limit as the request too, so the container reserves that much.

§ 07 Using more than you reserved

Besides starting and restarting containers, as in Chapter 4, each node’s agent keeps an eye on how much memory its node has left. Ordinarily, Pods that use more than they reserved cause no trouble, because the node has room to spare.

If the node’s free memory falls too low, though, its agent removes Pods to protect the node and everything else running on it. That’s an eviction. The agent goes first for the Pods using the most memory beyond what they reserved, and Pods that stay within their requests go last. Unlike the kill in §06, an eviction removes the Pod itself. The ReplicaSet loop then writes a new Pod record, as it does for any Pod that disappears, and the scheduler has to place the replacement; on a full cluster, the replacement waits, like the dispatch Pod that waited at the dinner rush. So a request set below what a service really uses doesn’t save that service any room. It puts the service first in line.

APPLYCheck 07 / 08

To cut the bill, Morgan suggests halving every service’s memory request. What would you expect at the next Friday rush?

The table below shows the loops you’ve met so far through the loop lens, with what this chapter added to each.

Fig. 05Which loop: wants, sees, does when they differ
LoopWants (desired state)Sees (real state)Does when they differSam’s job, now a loop’s
The memory kill itself is the operating system’s, on the node; the node agent restarts the container afterwards.
Fig. 05The same loops as in Chapter 4, filled in further. Pending Pods and evictions are differences between desired and real state that the loops act on; adding room is a job no loop in Fennel’s cluster does yet.

§ 08 The conversation

On Tuesday evening, after the third restart, Ari sends Jordan a message.

“We need to bump memory limits on orders.”

Ari
APPLYCheck 08 / 08 · think, then reveal

Ari messages Jordan: “We need to bump memory limits on orders.” What would you ask? Think of one or two questions, then show the answer.

In short

  • Each team writes down two numbers per service: what each copy reserves (its requests) and the most it may use (its limits).
  • The scheduler places each new Pod by its requests, memory and processor counted separately, on one node that has room for both. It never looks at how busy a node is.
  • If no node has room, the Pod waits, pending, and the scheduler keeps trying; nothing in Fennel’s cluster makes room by itself.
  • Requests, packed onto nodes, decide how many machines you rent. Padding them buys nodes; cutting them below what services use buys evictions. Some spare room is insurance, on purpose.
  • At its memory limit a container is killed and restarted; at its processor limit it’s slowed. Before raising a limit, ask whether it’s a leak or real growth, and what it does to the node count.

Every loop so far has kept the same version of each service running. In the next chapter, Fennel’s teams start shipping several times a day, and a loop takes over the job Sam once did by hand on a Tuesday in Chapter 3: replacing one version of a service with the next, without anyone having to type “Nobody deploy anything”.

End of chapter 05

Next: Shipping without downtime

Chapter 06 · 27 min · Under review Shipping without downtime: rollouts and rollbacks Fennel’s teams now ship several times a day without a release night. One Thursday of catalog releases, one routine and one that went wrong, shows how Kubernetes replaces one version with the next, which bad releases its health checks stop and which they don’t, and what going back really does. Start chapter 06 →

Course

chapters
15
running
05
in review
02
pending
08
  1. Part IBefore Kubernetes03/03 running

    1. Ch 0117 minOne app, one serverRunning
    2. Ch 0218 min“Works on my machine”containersRunning
    3. Ch 0317 minMany containers, many machinesthe orchestration problemRunning
  2. Part IIThe core model02/05 running · 02 in review

    1. Ch 0423 minSay what you want, not what to dodesired state and the reconcile loopRunning
    2. Ch 0525 minWho runs wherescheduling, resources and costYou are here
    3. Ch 0627 minShipping without downtimerollouts and rollbacksUnder review
    4. Ch 0724 minFinding each otherservices and trafficUnder review
    5. Ch 08The Friday dinner rushautoscalingPending
  3. Part IIIThe hard parts00/04 running

    1. Ch 09Configuration and secretsPending
    2. Ch 10The database problemstatePending
    3. Ch 11Many teams, one clusternamespaces, permissions and quotasPending
    4. Ch 12Seeing insideincidents in a self-healing systemPending
  4. Part IVOwning it00/02 running

    1. Ch 13Teaching the cluster new tricksextensions and the ecosystemPending
    2. Ch 14What all this costsmanaged Kubernetes, platform teams and when not to use itPending
  5. CapstoneMixed practice00/01 running

    1. Ch 15Fennel’s hard weekPending
Fig. 00The whole course: 15 chapters in 4 parts and a capstone. Raised tiles are running; ones marked Under review are written and readable, still being checked; dashed ones are Pending, planned but not written yet.