logo
Published on

HW2. Apple Silicon에서 KVM으로 HTCondor용 VM 3대 만들기

Authors
  • avatar
    Name
    seren-wib
    Twitter
Contents

교재 4장 "VirtualBox 기반 HTCondor 클러스터 생성"을 따라 가상머신 3대를 만들고 SSH 키 인증까지 설정한 과제다. 문제는 내 노트북이 Apple M3(ARM64) 맥이라는 거였다. x86용 VirtualBox VM을 그대로 쓸 수 없어서 하이퍼바이저를 KVM/libvirt로 바꿔서 진행했다.

0. 실습 환경

macOS (Apple M3)
└─ UTM (Apple Virtualization, 중첩 가상화)
   └─ Ubuntu 24.04 ARM64   (호스트 yoosung-vm, 강의의 "Ubuntu 호스트")
      └─ KVM + libvirt     (VirtualBox 대체)
         ├─ main    Rocky Linux 10.2   192.168.56.101    main.cloud.org
         ├─ work1   Rocky Linux 10.2   192.168.56.102    work1.cloud.org
         └─ work2   Rocky Linux 10.2   192.168.56.103    work2.cloud.org

교재(VirtualBox)와 바꾼 부분은 이렇다.

교재 (VirtualBox)이 환경 (KVM/libvirt)
Host-only 어댑터 vboxnet0 (192.168.56.0/24)libvirt 격리 네트워크 cloud56 (브리지 virbr56, 호스트 192.168.56.1)
NAT 어댑터libvirt default NAT 네트워크 (192.168.122.0/24)
VBoxManage clonevmvirt-clone --auto-clone
인터페이스 enp0s3 / enp0s8enp1s0 (NAT) / enp2s0 (사설 고정 IP)

커널 인자 pci=nomsi: 중첩 가상화에서는 게스트 virtio 장치의 MSI 인터럽트가 전달되지 않아 Rocky 부팅이 멈췄다. 커널 인자에 pci=nomsi를 넣어 예전 INTx 인터럽트를 쓰게 해서 해결했다(자세한 건 맨 아래). GRUB_TIMEOUT=0으로 바꾸면 GRUB 메뉴에서 커널 인자를 손으로 넣을 수 없게 되니까, 이 인자를 부트로더에 먼저 영구 반영하고 나서 타임아웃을 0으로 바꿨다.

작업 순서: 원본 VM rocky-base에 GRUB_TIMEOUT=0을 설정하고 → 3대로 복제한 다음 → 각 VM에 고정 IP·호스트네임과 /etc/hosts를 설정했다. 실제로는 (b) → (c) → (a) 순서였지만 아래는 과제 항목 순서대로 정리했다.

1. HTCondor 클러스터용 VM 만들기

(a) 사설 고정 IP 지정

사설 네트워크 인터페이스 enp2s0에 NetworkManager로 고정 IP(192.168.56.101~103/24)를 줬다. 외부로 안 나가는 내부망이라 게이트웨이는 지정하지 않았다. 호스트네임도 같이 바꿨다.

sudo nmcli con mod enp2s0 ipv4.method manual ipv4.addresses 192.168.56.101/24 connection.autoconnect yes
sudo nmcli con up enp2s0
sudo hostnamectl set-hostname main.cloud.org

work1, work2도 IP를 .102, .103으로, 호스트네임을 work1.cloud.org, work2.cloud.org로 바꿔서 똑같이 실행했다.

설정 전 main, enp2s0은 UP이지만 IP가 없음

설정 전 main. enp2s0은 UP이지만 IP가 없다.

main 고정 IP 지정 후

main: enp2s0 = 192.168.56.101/24, hostname = main.cloud.org

work1 고정 IP

work1: enp2s0 = 192.168.56.102/24

work2 고정 IP

work2: enp2s0 = 192.168.56.103/24

(b) 부팅 타임아웃 0 (GRUB_TIMEOUT=0)

복제 전에 원본 VM(rocky-base)에서 /etc/default/grub의 GRUB_TIMEOUT을 0으로 바꾸고 grub2-mkconfig로 /boot/grub2/grub.cfg를 다시 만들었다.

/etc/default/grub은 grub.cfg를 만들 때 쓰는 설정 파일이고, 부팅할 때 GRUB이 실제로 읽는 건 grub.cfg다. 그래서 고친 다음 꼭 다시 생성해야 한다.

sudo sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/' /etc/default/grub
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo reboot
/etc/default/grub

GRUB_TIMEOUT=0, 커널 인자에 pci=nomsi 유지

grub2-mkconfig

grub2-mkconfig로 grub.cfg 재생성

재부팅 시 GRUB 메뉴 없이 부팅

재부팅하면 GRUB 메뉴 없이 곧바로 커널 부팅이 시작된다.

부팅 완료 후 로그인 프롬프트

부팅 완료 후 로그인 프롬프트

/proc/cmdline

/proc/cmdline: 재부팅 후에도 pci=nomsi가 적용돼 있다.

grub.cfg set timeout=0

생성된 grub.cfg의 67·71행 set timeout=0

(c) 명령어로 VM 3대 복제

VBoxManage clonevm 대신 libvirt의 virt-clone을 썼다. 원본을 끈 상태에서 실행해야 한다. --auto-clone은 디스크 파일(qcow2)과 UEFI 변수 파일(VARS.fd)을 새 이름으로 복사하고 MAC 주소를 새로 만든다.

sudo virsh shutdown rocky-base
sudo virt-clone --original rocky-base --name main --auto-clone
sudo virt-clone --original rocky-base --name work1 --auto-clone
sudo virt-clone --original rocky-base --name work2 --auto-clone
virsh list --all
virt-clone으로 3대 생성

virt-clone으로 main·work1·work2 생성, virsh list --all에 4대가 보인다.

진행 표시에서 main_VARS.fd가 "0 B"로 나오지만, 아래 ls -l을 보면 4개 파일 모두 67,108,864바이트(64MB)로 제대로 복사됐다. 진행률 표시만 0으로 찍힌 거다.

(d) 복제된 VM에 GRUB_TIMEOUT=0이 남아 있는지 확인

복제는 디스크 전체를 복사하니까 GRUB 설정도 그대로 따라온다. 세 VM 모두에서 설정 파일과 실제 부트 설정을 확인했다.

grep GRUB_TIMEOUT /etc/default/grub
sudo grep -n "set timeout=" /boot/grub2/grub.cfg
main GRUB 확인

main: GRUB_TIMEOUT=0, grub.cfg set timeout=0

work1 GRUB 확인

work1: 동일

work2 GRUB 확인

work2: 동일

160행의 ${menu_show_once_timeout}은 "다음 부팅 한 번만 메뉴 표시" 기능용 변수라 평소 부팅에는 적용되지 않는다.

(e) /etc/hosts에 세 호스트네임 등록

세 VM 모두 /etc/hosts 끝에 세 줄을 추가했다. 각 줄은 IP, FQDN, 짧은 별칭 순서다.

sudo tee -a /etc/hosts > /dev/null <<'EOF'
192.168.56.101 main.cloud.org main
192.168.56.102 work1.cloud.org work1
192.168.56.103 work2.cloud.org work2
EOF
main /etc/hosts

main의 /etc/hosts (8~10행)

work1 /etc/hosts

work1의 /etc/hosts

work2 /etc/hosts

work2의 /etc/hosts

(f) 호스트에서 세 VM으로 ping

호스트(Ubuntu)의 가상 브리지 virbr56(192.168.56.1)에서 세 VM의 IP로 ping을 보냈다. 세 대 모두 패킷 손실 0%였다.

호스트에서 ping

호스트 → 192.168.56.101~103 ping, 모두 0% packet loss

(g) VM끼리 호스트네임으로 ping

각 VM에서 ping -c 2 main / work1 / work2를 실행했다. 별칭이 /etc/hosts대로 main.cloud.org(192.168.56.101) 등으로 변환돼서 응답이 왔다.

자기 자신한테는 약 0.10.4ms, 다른 VM으로는 약 39ms가 나왔다. 중첩 가상화를 거치는 만큼 VM 사이 지연이 더 크다.

main에서 hostname ping

main에서 hostname으로 ping

work1에서 hostname ping

work1에서 hostname으로 ping

work2에서 hostname ping

work2에서 hostname으로 ping

(h) SSH로 복제된 VM에 접속

호스트에서 세 VM에 비밀번호로 SSH 접속하고 hostname으로 맞는 VM인지 확인했다. 첫 접속이라 호스트 키 확인 질문이 나와서 yes로 known_hosts에 등록했다.

비밀번호 SSH 접속

호스트 → main·work1·work2 SSH 접속 (비밀번호 인증)

세 VM의 호스트 키가 똑같다. ED25519 호스트 키 지문이 모두 SHA256:C+o0I1…로 같다. virt-clone은 디스크를 그대로 복사하니까 /etc/ssh/ssh_host_* 키까지 복제되기 때문이다. 그래서 두 번째 접속부터 "이 키는 다른 이름으로도 알려져 있다"는 메시지가 나온다. 실습엔 문제없지만 실제 운영에서는 복제 후 호스트 키를 다시 만들어야 서버를 구별할 수 있다.

2. SSH 키 인증 (Ch04-Extra)

(a) 호스트에서 개인키로 세 VM에 접속

호스트에서 ED25519 키 쌍을 만들고, ssh-copy-id로 공개키를 세 VM의 ~/.ssh/authorized_keys에 등록했다. 이후 접속은 호스트의 개인키(~/.ssh/id_ed25519)로 인증되니까 비밀번호를 묻지 않는다. 개인키는 호스트 밖으로 안 나가고 VM에는 공개키만 복사된다.

ssh-keygen -t ed25519
ssh-copy-id yoosung@192.168.56.101
ssh-copy-id yoosung@192.168.56.102
ssh-copy-id yoosung@192.168.56.103
ed25519 키 쌍 생성

호스트에서 ed25519 키 쌍 생성 (개인키 id_ed25519, 공개키 id_ed25519.pub)

ssh-copy-id

ssh-copy-id로 공개키 등록. VM마다 "Number of key(s) added: 1"

키 인증 접속

호스트 → 세 VM 키 인증 접속. 비밀번호 없이 각 hostname이 출력된다.

(b) VM끼리 개인키로 접속

세 VM에서 각자 ED25519 키 쌍을 만들고, 나머지 두 VM에 호스트네임으로 공개키를 등록했다. 총 6방향(3대 × 2)이다. VM마다 자기 개인키를 따로 가지니까 한 대의 키가 유출돼도 다른 VM의 키는 영향을 안 받는다.

ssh-keygen -t ed25519
ssh-copy-id work1
ssh-copy-id work2

main 기준이고, work1은 main·work2, work2는 main·work1로 대상만 바꿔서 실행했다.

main에서 work1, work2

main → work1, work2 (비밀번호 없이 접속)

work1에서 main, work2

work1 → main, work2

work2에서 main, work1

work2 → main, work1

복제하면서 겪은 문제

  • 시리얼 로그 경로 충돌: 복제된 VM이 원본의 시리얼 로그 경로(/var/log/rocky-base-serial.log)까지 그대로 물려받아서, 두 번째 VM을 켤 때 "Device or resource busy"로 실패했다. virsh dumpxml로 설정을 꺼내 경로를 VM 이름별로 바꾸고 virsh define으로 다시 등록해서 해결했다.
  • 같은 호스트 키: 위 (h)에서 말한 것처럼 디스크 복제 때문에 SSH 호스트 키가 세 대 모두 같다.

부록. 중첩 가상화에서 Rocky 부팅이 멈춘 문제

가장 오래 막혔던 건 Rocky Linux 게스트가 커널 부팅 7~8초 지점에서 멈추는 현상이었다. 결론부터 말하면 중첩 가상화 환경에서 MSI 인터럽트가 게스트에 전달되지 않아서 생긴 문제였고, 게스트 커널 인자 pci=nomsi 한 줄로 해결했다. KVM을 끄지 않고 해결해서 구성도 바꿀 필요가 없었다.

증상

Rocky Linux 10.2 aarch64 게스트를 KVM으로 올리면 커널 부팅 7~8초 지점에서 멈추고 더 진행하지 않았다. 도메인 상태는 running이고 cpu.time도 계속 올라갔다. VM은 켜져 있고 CPU도 쓰는데 부팅만 안 되는 상태다. 2코어로 돌리면 60초 뒤 RCU stall 경고까지 찍혔다.

[     7.300568] pcieport 0000:00:01.1: PME: Signaling with IRQ 51
[    67.306241] rcu: INFO: rcu_preempt detected stalls on CPUs/tasks:
[    67.346763] rcu:     Possible timer handling issue on cpu=0 timer-softirq=151
[    67.354764] rcu: rcu_preempt kthread starved for 6002 jiffies!
[    67.434740] rcu: Sending NMI from CPU 1 to CPUs 0:

그래픽 콘솔에서는 Display output is not active로만 보여서 원인을 알 수가 없었다. 시리얼 콘솔 출력을 파일로 남기도록 바꾸고 나서야 위 로그를 얻었다. 부팅이 어디까지 갔는지는 시리얼 로그 파일 크기로 비교했다. 정상 부팅이면 설치 프로그램 화면까지 로그가 계속 늘어난다.

먼저 해결한 두 문제

부팅 정지를 보기 전에 다른 문제 두 개가 먼저 나왔다.

① 시큐어 부트 펌웨어

virt-install에 --boot uefi만 주면 libvirt가 Microsoft 키가 등록된 AAVMF_CODE.ms.fd를 고른다. 이 펌웨어는 서명 안 된 커널을 거부해서 이런 오류가 났다.

error: ../../grub-core/loader/efi/linux.c:723:failed to install protocols.
Failed to boot both default and fallback entries.

시큐어 부트가 꺼진 펌웨어를 직접 지정해서 해결했다. 게스트 커널 로그에 secureboot: Secure boot disabled가 찍히는 것도 확인했다.

--boot loader=/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd,loader.readonly=yes,loader.type=pflash,loader.secure=no,nvram.template=/usr/share/AAVMF/AAVMF_VARS.fd

② 그래픽 콘솔이 안 뜸

aarch64 게스트는 SPICE 콘솔에 화면이 안 뜨고 Display output is not active만 보인다. 게다가 --cdrom으로는 커널 인자를 넘길 수 없다. 설치 방식을 이렇게 바꿨다.

  • --location <ISO>: ISO에서 vmlinuz와 initrd.img를 꺼내 직접 부팅한다. GRUB을 안 거쳐서 ①의 오류도 같이 피한다.
  • --graphics none + --extra-args "console=ttyAMA0,115200 inst.text": 설치 프로그램을 시리얼 텍스트 모드로 띄운다.
  • --serial pty,log.file=<경로>: 시리얼 출력을 파일로 남긴다. virsh console은 접속 시점에 따라 초기 출력을 놓쳐서 빈 화면으로 보일 때가 많다. 파일로 남겨야 부팅 로그를 처음부터 볼 수 있었다.

이 두 개를 해결하고 나서야 7~8초에서 멈추는 문제를 제대로 볼 수 있었다.

원인 분석

조건을 하나씩 바꿔 가며 네 번 돌려서 원인을 좁혔다.

실험바꾼 것시리얼 로그 크기결과
1vCPU 2 (기준)46,222 B7.30초 정지
2vCPU 1—8.22초 정지
3--virt-type qemu (TCG 에뮬레이션)115,139 B정상 진입
4KVM 유지 + pci=nomsi98,182 B정상 진입

가설 1. 중첩 환경의 타이머 문제 (틀림)

처음엔 RCU stall 메시지의 Possible timer handling issue on cpu=0을 보고 타이머 전달 실패를 의심했다. L1(Ubuntu)의 dmesg에서 kvm [1]: Hyp nVHE mode initialized successfully도 찾았다. 보통 ARM 하드웨어에서 KVM은 VHE 모드로 올라오는데 여기선 nVHE였다. Apple 가상화가 중첩용 EL2는 주지만 VHE 기능은 안 준다는 뜻이라 타이머에도 문제가 있을 수 있다고 봤다.

하지만 실험 1과 2는 멈춘 줄이 서로 달라서 특정 드라이버 하나의 문제로 보기 어려웠다. 그리고 두 번 다 멈추기 직전에 PCI 장치를 깨우고 있었다. 그래서 RCU stall은 인터럽트를 못 받은 뒤에 따라 나온 증상으로 보고 이 가설을 버렸다.

가설 2. 중첩 KVM 자체의 문제 (범위를 좁힘)

같은 ISO·커널·펌웨어에서 --virt-type qemu로 KVM 없이 TCG 에뮬레이션으로 돌렸더니 설치 프로그램까지 정상으로 들어갔다(실험 3). 바꾼 게 KVM 하나뿐이니 문제는 중첩 가상화 쪽이다. 다만 TCG는 10~20배 느려서 실습에는 못 쓰고 원인을 가리는 데만 썼다.

가설 3. MSI 인터럽트 전달 실패 (실제 원인)

두 번 다 PCI 장치에서 멈췄으니 인터럽트 경로를 의심했다. ARM에서 PCI 장치는 MSI-X로 인터럽트를 보내고 인터럽트 컨트롤러(GIC)가 받는다. L1의 dmesg에서 이걸 확인했다.

GICv2m: range[mem 0x1fff0000-0x1fff0fff], SPI[128:255]
kvm [1]: GICv3: no GICV resource entry
kvm [1]: disabling GICv2 emulation

L1에는 MSI를 처리하는 ITS가 없고 예전 방식인 GICv2m을 쓰고 있었다. 이 구성에서는 MSI가 게스트까지 안 가서, virtio-pci 장치가 보낸 인터럽트를 게스트가 못 받고 그 자리에서 멈춘 걸로 보인다.

해결

게스트 커널 인자에 pci=nomsi 하나를 추가했다. 이러면 게스트가 MSI 대신 예전 방식인 INTx로 인터럽트를 받아서 문제 있는 경로를 아예 안 쓴다.

--extra-args "console=ttyAMA0,115200 inst.text pci=nomsi"
조건시리얼 로그결과
KVM, 인자 없음46,222 B7~8초 정지
KVM, pci=nomsi98,182 B설치 프로그램 정상 진입

이 방법을 고른 이유

  • KVM을 끄지 않아도 된다. TCG처럼 느려지지 않는다.
  • 구성을 안 바꾼다. 추가한 건 게스트 커널 인자 한 줄뿐이다.
  • 단점은 I/O가 조금 느려지는 정도다. INTx는 MSI보다 효율이 떨어지지만 VM 3대짜리 실습에서는 문제없었다.

재현에 필요한 설정

항목값이유
펌웨어AAVMF_CODE.no-secboot.fd시큐어 부트 펌웨어는 서명 없는 커널을 거부
부팅 방식--location <ISO>커널 인자를 넘기려고 직접 부팅
콘솔--graphics none + --serial pty,log.file=aarch64는 그래픽 콘솔이 안 뜸, 초기 로그 보존
커널 인자pci=nomsi중첩 환경에서 MSI가 전달되지 않는 문제 회피
패키지qemu-efi-aarch64 포함aarch64는 BIOS가 없어서 UEFI 펌웨어가 필요