초안프로덕션 빌드에서 제외되며 검색엔진에도 노출되지 않습니다.

회사 테크

컨테이너가 glibc 오류로 죽었습니다 — 범인은 VM의 기본 CPU 모델

최신 컨테이너 이미지는 x86-64-v2를 요구합니다. 가상화 하이퍼바이저가 기본으로 주는 CPU 모델에는 그 명령어가 없어, 애플리케이션 오류가 아니라 glibc 단계에서 죽습니다.

삽질하는개발자

뇌파 곡선 일러스트

새로 만든 VM에 스택을 올렸는데 오브젝트 스토리지 컨테이너 하나만 계속 unhealthy였습니다. 로그에는 딱 한 줄이 있었습니다.

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

설정을 잘못 준 줄 알고 환경변수부터 다시 봤습니다. 그런데 이 메시지는 애플리케이션이 낸 것이 아닙니다. 바이너리가 main()에 진입하기도 전에 glibc가 낸 것입니다.

애플리케이션 오류가 아닙니다

glibc는 특정 마이크로아키텍처 레벨 이상을 요구하도록 빌드할 수 있습니다. 그렇게 빌드된 바이너리는 로더 단계에서 CPU 기능을 확인하고, 모자라면 그 자리에서 죽습니다.

그래서 이 오류는 다음과 같은 특징을 가집니다.

  • 애플리케이션 로그가 아니라 컨테이너 표준출력 첫 줄에 찍힙니다
  • 설정을 어떻게 바꿔도 동일합니다
  • 같은 이미지가 다른 서버에서는 멀쩡히 돕니다

세 번째가 특히 사람을 헷갈리게 만듭니다. 이미지 문제도 아니고 설정 문제도 아니니 남는 것은 환경인데, “환경”의 범위에 CPU가 들어 있다고는 잘 생각하지 않습니다.

x86-64 마이크로아키텍처 레벨

x86-64는 세대별 명령어 집합을 레벨로 묶어 부릅니다. v1은 최초 x86-64 베이스라인이고, v2는 2009년 전후 세대에서 보편화된 명령어들을 포함합니다.

v2가 요구하는 것 중 실무에서 확인하기 쉬운 항목은 이렇습니다.

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

물리 서버에서는 신경 쓸 일이 거의 없습니다. 십수 년 전 CPU가 아니라면 전부 만족하기 때문입니다. 문제는 가상화입니다.

기본값이 문제였습니다

VM을 만들 때 CPU 타입을 지정하지 않으면 하이퍼바이저가 기본 모델을 씁니다. 실제로 어떤 값이 들어가는지는 호스트에서 프로세스 인자를 보면 바로 확인됩니다.

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

결과입니다.

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

kvm64입니다. 이 모델로 뜬 게스트 안에서 CPU 정보를 보면 이렇게 나옵니다.

model name : Common KVM processor
CPU 모델 게스트가 보는 이름 sse4_2 popcnt ssse3 cx16
kvm64 (기본값) Common KVM processor 없음 없음 없음 있음
x86-64-v2-AES QEMU Virtual CPU version 2.5+ 있음 있음 있음 있음
host 호스트 CPU 이름 그대로 있음 있음 있음 있음

kvm64cx16은 주지만 ssse3부터 sse4_2까지가 전부 빠져 있습니다. x86-64-v2 요건을 만족하지 못합니다. 호스트 CPU가 아무리 최신이어도, 게스트에는 그 명령어가 노출되지 않습니다.

같은 호스트에서 x86-64-v2-AES로 지정된 VM은 이렇게 뜹니다.

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

플래그가 명시적으로 추가되어 있습니다. 하이퍼바이저가 v2에 필요한 항목을 하나씩 켜주는 방식입니다.

무엇으로 바꿀 것인가

선택지는 크게 둘입니다.

host 는 호스트 CPU 기능을 그대로 노출합니다. 성능상 가장 유리하고 어떤 이미지든 돕니다. 대신 다른 CPU를 가진 노드로 라이브 마이그레이션을 할 수 없습니다. 게스트가 이미 쓰고 있는 명령어가 대상 노드에 없을 수 있기 때문입니다.

x86-64-v2-AES 는 필요한 명령어만 켠 이름 있는 모델입니다. 클러스터 안에 CPU 세대가 섞여 있어도 같은 모델을 쓰면 마이그레이션이 유지됩니다.

이번 건에서는 host를 골랐습니다. 이 VM의 디스크가 특정 노드에만 있는 로컬 스토리지라 어차피 마이그레이션이 불가능했기 때문입니다. 제약이 이미 있는 상황이면 성능이 나은 쪽을 고르는 편이 낫습니다.

반대로 여러 노드를 오가는 VM이라면 host는 나중에 발목을 잡습니다. 마이그레이션이 필요한 시점은 대개 계획된 시점이 아니라 노드에 문제가 생겼을 때입니다.

한 가지 주의할 점은 CPU 타입 변경이 재부팅을 요구한다는 것입니다. 실행 중인 VM에 적용되지 않으므로, 서비스 중이라면 재시작 시점을 잡아야 합니다.

확인 방법

의심이 들면 두 곳만 보면 됩니다.

호스트에서, 해당 VM이 실제로 어떤 CPU 모델로 떠 있는지 봅니다.

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

게스트 안에서, 필요한 플래그가 실제로 보이는지 봅니다.

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

설정 파일에 CPU 타입 항목이 없다면 기본값을 쓰고 있다는 뜻입니다. 이번에 한 노드를 세어보니 VM 9대 중 8대가 CPU 타입을 지정하지 않은 상태였습니다. 지금까지 문제가 없었던 것은 그 위에서 돌던 이미지들이 v2를 요구하지 않았기 때문일 뿐입니다.

왜 늦게 찾았을까

시작이 잘못됐습니다. 컨테이너 하나가 unhealthy였으므로 그 컨테이너의 문제라고 전제했고, 이미지 태그와 환경변수를 먼저 뒤졌습니다.

오류 메시지에 glibc라는 단어가 들어 있는데도 그 의미를 짚지 않았습니다. 애플리케이션이 아니라 런타임이 낸 오류라면 원인은 애플리케이션 바깥에 있습니다. 메시지를 낸 주체가 누구인지 먼저 확인했다면 훨씬 빨리 CPU에 도달했을 것입니다.

정리하면 이렇습니다. 오래된 VM에 최신 이미지를 올리는데 애플리케이션과 무관해 보이는 저수준 오류가 난다면, VM을 언제 어떤 설정으로 만들었는지부터 확인해 볼 만합니다. 몇 년 전에 만든 VM은 대개 기본값 그대로이고, 그 기본값은 최신 이미지를 염두에 두고 정해진 것이 아닙니다.