Engineering

[proxmox] Fixing "CPU does not support x86-64-v2"

Modern container images require x86-64-v2. The default CPU model your hypervisor hands a VM does not have those instructions, so the process dies inside glibc before the application ever runs.

삽질하는개발자

The default CPU model kvm64 failing to meet the x86-64-v2 level a container image requires

We brought up a stack on a freshly created VM and one object-storage container stayed unhealthy. The log had exactly one line:

Fatal glibc error: CPU does not support x86-64-v2

Assuming a misconfiguration, we went through the environment variables again. But this message does not come from the application. glibc emits it before the binary even reaches main().

This is not an application error

glibc can be built to require a given microarchitecture level. A binary built that way checks CPU features at load time and dies on the spot if they are missing.

Which gives the error these traits:

  • It appears on the container’s first line of stdout, not in application logs
  • No configuration change makes any difference
  • The same image runs fine on a different server

The third one is what misleads people. It is not the image and not the config, so all that is left is the environment — and “environment” is not usually a category people expect the CPU to fall into.

x86-64 microarchitecture levels

x86-64 groups generational instruction sets into levels. v1 is the original x86-64 baseline; v2 covers instructions that became common around 2009.

The v2 requirements that are easiest to check in practice:

cx16, lahf_lm, popcnt, sse3(pni), sse4_1, sse4_2, ssse3

On physical servers this is rarely a concern — anything less than a decade or so old satisfies all of it. Virtualization is where it bites.

The default was the problem

If you do not specify a CPU type when creating a VM, the hypervisor picks a default. You can see exactly what it chose by looking at the process arguments on the host.

tr '\0' ' ' < /proc/$(pgrep -f "kvm -id 100")/cmdline | grep -o '\-cpu [^ ]*'

The result:

-cpu kvm64,enforce,+kvm_pv_eoi,+kvm_pv_unhalt,+lahf_lm,+sep

kvm64. Inside a guest running on that model, CPU info reads:

model name : Common KVM processor

A table comparing which instructions each CPU model exposes to the guest — kvm64 provides only cx16 and is missing ssse3, sse4_1, sse4_2 and popcnt

The CPU name the guest sees also differs per model, which alone is enough to guess which one you are on.

CPU model Guest model name
kvm64 (default) Common KVM processor
x86-64-v2-AES QEMU Virtual CPU version 2.5+
host the host CPU name verbatim

kvm64 provides cx16 but is missing everything from ssse3 through sse4_2. It does not meet the x86-64-v2 requirement. However modern the host CPU is, those instructions are not exposed to the guest.

On the same host, a VM configured with x86-64-v2-AES comes up as:

-cpu qemu64,+aes,enforce,+kvm_pv_eoi,+kvm_pv_unhalt,+pni,+popcnt,+sse4.1,+sse4.2,+ssse3

The flags are added explicitly — the hypervisor turning on each item v2 needs.

What to switch to

Two main options.

host exposes the host CPU’s features directly. Best performance, and any image will run. The cost is that you cannot live-migrate to a node with a different CPU, since instructions the guest is already using may not exist on the target.

x86-64-v2-AES is a named model with just the required instructions enabled. Migration keeps working across mixed CPU generations as long as every node uses the same model.

We chose host here. This VM’s disk is on local storage tied to one node, so migration was impossible anyway. When a constraint already exists, take the better-performing option.

Conversely, for a VM that moves between nodes, host will bite you later. The moment you need to migrate is usually not a planned one — it is when a node has a problem.

One caveat: changing the CPU type requires a reboot. It does not apply to a running VM, so if the machine is in service you need a restart window.

How to check

Two places.

On the host, what CPU model the VM actually started with:

tr '\0' ' ' < /proc/$(pgrep -f "kvm -id <VMID>")/cmdline | grep -o '\-cpu [^ ]*'

Inside the guest, whether the flags you need are visible:

grep -m1 'model name' /proc/cpuinfo
for f in ssse3 sse4_1 sse4_2 popcnt cx16 lahf_lm; do
  printf '%-8s %s\n' "$f" "$(grep -qw $f /proc/cpuinfo && echo yes || echo no)"
done

If the config file has no CPU type entry, you are on the default. Counting one node, 8 of 9 VMs had no CPU type specified. Nothing had broken so far only because the images running on them did not require v2.

Why it took so long to find

We started from the wrong premise. One container was unhealthy, so we assumed it was that container’s problem and dug through image tags and environment variables first.

The error message contained the word glibc and we did not stop to consider what that meant. If the runtime raised the error rather than the application, the cause lies outside the application. Identifying who emitted the message would have taken us to the CPU far sooner.

Symptoms pointing somewhere other than the cause is a recurring pattern. In Fixing ssh “Connection timed out during banner exchange”, a port scan reporting open is precisely what removed the firewall from suspicion.

To summarize: if a low-level error unrelated to the application appears when you put a modern image on an older VM, it is worth checking when and with what settings that VM was created. A VM built a few years ago is usually still on defaults, and those defaults were not chosen with today’s images in mind.

For the case where a default silently consumes resources instead, see pg_wal that will not shrink — start with replication slots. There the default was “unlimited.”