Skip to content

Latest commit

 

History

History
93 lines (61 loc) · 10.5 KB

File metadata and controls

93 lines (61 loc) · 10.5 KB

v6.19 — Linux Kernel Live Update Orchestrator: kexec로 VM을 안 끄고 커널을 업데이트하는 법

메타데이터

항목 내용
프로젝트 Linux Kernel
관련 릴리스 6.16(KHO 최초 merge), 6.19(Live Update Orchestrator 프레임워크, 2026-02-08) — 이후로도 활발히 개선 커밋이 이어지고 있음
핵심 컴포넌트 kernel/liveupdate/(kexec_handover.c, luo_core.c, luo_file.c, luo_flb.c, luo_session.c)
제안자 Pasha Tatashin(Soleen) 주도, Google/Microsoft/Amazon 기여자 다수 참여
발견 출처 Live Update Orchestrator — Linux Kernel documentation, LWN.net: Live Update Orchestrator
검증 근거 torvalds/linux kernel/liveupdate/ 실제 소스, 커밋 히스토리

개요

클라우드에서 커널을 하이퍼바이저로 쓰는 경우(예: KVM 호스트), 커널을 새 버전으로 업데이트하려면 전통적으로 시스템을 재부팅해야 했고, 그 위에서 돌던 VM들도 전부 내려가거나 다른 호스트로 마이그레이션해야 했다. Live Update Orchestrator(LUO)는 kexec(커널이 부트로더를 거치지 않고 다른 커널로 곧바로 넘어가는 메커니즘)를 이용해, 선택된 자원(메모리 상태, 파일 디스크립터, 심지어 DMA 중인 디바이스)을 새 커널로 그대로 넘겨주면서 커널 버전을 올리는 프레임워크다. Linux 6.16에서 그 기반인 KHO(Kexec Handover)가 먼저 들어왔고, 6.19에서 이를 오케스트레이션하는 LUO 프레임워크가 합쳐졌다.

사전 지식 — 재부팅 없이 "업데이트"하는 기존 방법들과 그 한계

리눅스에는 이미 "재부팅 안 하고 커널을 바꾸는" 방법이 두 가지 있었는데, 둘 다 이번 문제엔 맞지 않았다.

1. Live Patching(kpatch/livepatch): 실행 중인 커널의 특정 함수 하나를 새 버전으로 바이너리 레벨에서 갈아 끼우는 기술이다. 보안 패치처럼 "함수 몇 개만 고치면 되는" 상황에는 훌륭하지만, 커널 메이저 버전을 완전히 새 걸로 올리는 건 못 한다 — 자료구조 레이아웃이나 서브시스템 초기화 순서가 통째로 바뀔 수 있는데, 그건 함수 단위 교체로 감당이 안 된다.

2. CRIU 같은 체크포인트/복원: 프로세스의 상태(메모리, 열린 파일 등)를 통째로 디스크나 다른 곳에 저장했다가 나중에 복원하는 유저스페이스 도구다. 하지만 프로세스 하나 단위로 동작하고, 커널 자체가 관리하는 자원(예: KVM이 물고 있는 VM의 GPA↔HPA 매핑, VFIO가 관리하는 디바이스의 IOMMU 매핑)까지는 손이 안 닿는다. 게다가 저장/복원 과정에서 실제 메모리 내용을 복사해야 해서, 데이터가 클수록 다운타임이 길어진다.

kexec: 일반적인 재부팅은 커널 → 펌웨어/부트로더 → 새 커널 순서를 거치는데, kexec는 이 중간 단계(펌웨어 초기화, 부트로더)를 건너뛰고 지금 돌고 있는 커널이 메모리에 이미 올라와 있는 다음 커널 이미지로 직접 점프한다. 그래서 하드웨어 재초기화 없이 몇 초 안에 새 커널이 뜬다 — LUO는 바로 이 위에 "메모리 상태까지 같이 넘기는" 기능을 얹은 것이다.

변경 분석 — KHO가 메모리를, LUO가 그 위의 자원 생명주기를 관리한다

LUO는 두 레이어로 나뉜다: **KHO(Kexec Handover)**가 물리 메모리를 kexec 경계 너머로 "보존"하는 저수준 메커니즘이고, LUO 자체는 그 위에서 파일 디스크립터 같은 상위 레벨 자원의 준비/동결/복원 순서를 조율하는 상태 기계다.

KHO — FDT와 radix tree로 "이 페이지는 건드리지 마" 표시하기

kernel/liveupdate/kexec_handover.c를 보면, KHO는 보존할 데이터를 **FDT(Flattened Device Tree, 부트로더가 하드웨어 정보를 커널에 넘길 때 쓰는 그 형식)**로 직렬화하고, 어떤 물리 페이지가 "보존됨"인지는 별도의 radix tree로 추적한다:

/* kernel/liveupdate/kexec_handover.c */
static unsigned long kho_radix_encode_key(phys_addr_t phys, unsigned int order)
{
	unsigned long h = 1UL << (KHO_ORDER_0_LOG2 - order);
	unsigned long l = phys >> (PAGE_SHIFT + order);
	return h | l;
}

이 인코딩된 키로 radix tree의 비트맵에 "이 페이지는 보존 대상"이라고 표시해두면, 새 커널이 부팅할 때 메모리 할당자(mm/memblock.c 초기화 경로)가 이 비트맵을 참조해서 그 물리 페이지들을 절대 재사용하지 않고 그대로 비워둔다 — 그래서 이전 커널이 거기 남겨둔 데이터가 새 커널이 올라오는 과정에서 덮어써지지 않는다.

LUO — 상태 기계와 콜백 기반 핸들러

luo_core.c의 문서 주석에 명시된 것처럼, LUO는 워크로드에 종류 무관하게(VM, 컨테이너, memcached 같은 인메모리 캐시 등) 커널 버전을 통째로 올리는 걸 지원하는 게 목표다. 파일 하나를 "보존"하는 생명주기는 luo_file.c에 정확히 5단계로 문서화돼 있다:

  1. Preserve(정상 운영 중): 유저스페이스 에이전트가 ioctl로 luo_preserve_file()을 호출하면, 그 파일 타입(memfd, vfio 등)을 담당하는 핸들러의 .preserve()가 불려서 상태를 저장하고 opaque한 u64 핸들을 돌려준다. 워크로드가 여전히 돌고 있는 중에 미리 해두는 무거운 작업이라, 실제 재부팅 시점의 다운타임을 줄인다.
  2. Freeze(재부팅 직전): reboot() 시스템 콜 경로에서 luo_file_freeze()가 모든 보존된 파일의 .freeze()를 호출한다 — 이 시점부터는 유저스페이스가 더 이상 그 파일을 바꿀 수 없다. 메타데이터를 KHO가 넘길 연속된 메모리 블록으로 직렬화한다.
  3. Deserialize(새 커널, /dev/liveupdate 최초 open 시): KHO가 넘겨준 메모리 영역에서 직렬화된 데이터를 읽어 struct luo_file 목록을 새 커널 안에 재구성한다.
  4. Retrieve(새 커널, 유저스페이스 준비 완료 후): 유저스페이스가 토큰을 주면 luo_retrieve_file()이 해당 핸들러의 .retrieve()를 호출해 struct file을 재생성하고 새 FD를 돌려준다 — 순서 무관하게 어느 파일이든 먼저 복원 가능하다.
  5. Finish(정리): luo_file_finish().finish()를 호출해 최종 정리를 하고 그 파일에 대한 소유권을 넘겨준다.

전체 진행 상태는 아래처럼 4단계로 나뉜다(LWN 기사 기준):

NORMAL → PREPARED → FROZEN → UPDATED
 (평상시)  (자원 등록됨) (재부팅 직전 동결) (새 커널에서 복원 가능)

실패 처리가 촘촘하다: Freeze 단계에서 어느 한 핸들러의 .freeze()라도 실패하면, 이미 성공한 것들에 대해 .unfreeze()를 역순으로 호출해 되돌리고 reboot() 자체를 에러로 실패시킨다 — 즉 "절반만 얼린 채로 재부팅되는" 상태는 발생하지 않는다. 새 커널에서 .finish()가 실패하면 LUO가 그 자원의 소유권을 계속 들고 있다가 다음 live update 때 다시 시도하거나 그때 폐기한다.

FLB(File-Lifecycle-Bound) 객체: 여러 파일이 공유하는 전역 상태(예: 하나의 IOMMU 도메인을 여러 VFIO 파일이 같이 씀)는 파일 하나에 묶이지 않으므로 따로 관리한다 — 그 상태에 의존하는 첫 번째 파일이 보존될 때 같이 보존되고, 마지막 파일이 끝날 때 정리된다. 참조 카운팅을 프레임워크가 대신 해줘서, 핸들러 코드가 "몇 명이 이걸 쓰고 있는지" 직접 셀 필요가 없다.

설계 결정 및 트레이드오프

왜 ioctl 기반 /dev/liveupdate인가: LWN 기사에 따르면 v2 개정에서 sysfs 기반 제어를 ioctl 기반으로 바꿨다. sysfs는 "파일 하나 읽고 쓰기"엔 편하지만, 세션 생성·복수 파일 등록·상태 전이처럼 여러 단계로 이어지는 트랜잭션성 작업엔 원자성을 보장하기 어렵다 — ioctl은 커널이 요청 하나를 통째로 처리할 수 있어 이런 워크플로우에 더 자연스럽다.

왜 워크로드에 구애받지 않게(workload-agnostic) 설계했는가: 실제로 이 작업을 밀어붙인 동기는 클라우드 하이퍼바이저(KVM 호스트) 업데이트지만, luo_core.c 주석은 명시적으로 "VM뿐 아니라 컨테이너, 고성능 데이터베이스, 네트워킹 서비스에도 쓸 수 있게" 설계했다고 밝힌다. 특정 서브시스템(KVM)에 종속된 API를 만들었다면 더 빨리 완성됐겠지만, 재사용성을 위해 "파일 하나를 보존/복원하는 범용 계약(핸들러 콜백)"으로 추상화하는 쪽을 택했다 — 그 대가로 각 서브시스템(memfd, vfio 등)이 각자 핸들러를 구현해야 하는 초기 도입 비용이 생긴다.

왜 KHO와 LUO를 분리했는가: KHO는 순수하게 "물리 메모리를 안 건드리고 넘긴다"는 낮은 수준의 문제만 풀고, LUO는 "그 메모리 위에 있는 자원을 언제/어떤 순서로 준비·동결·복원할지"라는 조율 문제를 푼다. 이렇게 나누면 KHO 하나만으로도 (LUO 없이) 다른 용도로 메모리를 넘기고 싶은 코드가 재사용할 수 있다 — 실제로 KHO는 6.16에서 LUO보다 먼저, 독립적으로 merge됐다.

더 살펴볼 점

  • 이 메커니즘이 실제 프로덕션 클라우드 환경(GCP, AWS 등)에서 하이퍼바이저 업데이트에 어느 정도까지 채택됐을까?
  • CRIU와 LUO를 같은 시스템에서 같이 쓸 수 있을까, 아니면 서로 경쟁하는 접근인가?
  • FLB의 참조 카운팅 모델이 두 서브시스템이 같은 자원에 순환적으로 의존하는 경우에도 안전하게 동작할까?

참고 자료

참고 외 별도 확인 링크

본문 참고 자료로 충분했다.