Let’s Bootc! [7] - OSTree : bootc는 재부팅이 왜 이렇게 빠를까?

Let’s Bootc! [7] - OSTree : bootc는 재부팅이 왜 이렇게 빠를까?

Summary OSTree는 운영체제 바이너리의 원자적 전환을 지원하는 기술로, Git의 content-addressed 모델을 기반으로 합니다. 시스템 업데이트 중 전원이 꺼져도 이전 또는 새로운 시스템 중 하나로 부팅되며, 하드링크를 사용하여 디스크 공간을 절약하고 롤백이 용이합니다. bootc 프로젝트는 OSTree의 개념을 OCI 컨테이너 이미지로 확장하여 기존 도구와 인프라를 활용합니다.


Image

[6] 재부팅이 왜 이렇게 빠른가? - OSTree의 원자적 전환

그동안 부트 테이블 컨테이너(bootable container)를 계속 빌드하고 알아보면서, 아마 이런 의문이 생기셨을 것 같습니다. “과연 bootc도 Git처럼 이력이 관리되는 걸까?”, “바뀐 부분만 저장된다면 Git과 비슷하게 동작하는 건가?” 같은 생각들 말입니다.

만약 이런 궁금증을 가지셨다면 정답에 굉장히 가깝습니다. 오늘은 바로 그 궁금증을 해결하기 위해, 이와 밀접한 관련이 있는 기술인 OSTree에 대해 자세히 작성해 보려고 합니다. 우리가 그동안 기술을 다루며 느꼈던 고민들이 어떻게 해결되는지, 그 흥미로운 과정을 함께 살펴보겠습니다.


Hooking : apt upgrade 중에, 전원이 꺼진다면 무슨 일이 발생될까?

OSTree가 무엇인지 설명하기 전에, 이 기술이 왜 필요했는지부터 생각해 봐야 합니다.

전통적인 패키지 관리자를 떠올려 봅시다. dnf updateapt upgrade를 실행하면 어떤 일이 벌어질까요? 수십, 수백 개의 패키지가 순차적으로 설치됩니다. 각 패키지마다 파일이 풀리고, 설정이 적용되고, 스크립트가 실행됩니다.

여기서 질문 하나. 이 과정 중에 전원이 꺼지면?

시스템은 절반만 업데이트된 상태가 됩니다. 어떤 라이브러리는 새 버전이고 어떤 바이너리는 구 버전입니다. 의존성이 깨집니다. 부팅이 안 될 수도 있습니다. 운이 좋으면 복구할 수 있고, 운이 나쁘면 재설치입니다.

현업에서 이런 상황을 몇 번 겪어보면 업데이트가 두려워집니다. “돌아가는 시스템 건드리지 마라"는 격언이 괜히 나온 게 아닙니다.


OSTree의 탄생 배경

OSTree는 2011년 10월, Colin Walters라는 엔지니어가 시작했습니다. 그는 당시 GNOME 프로젝트의 핵심 컴포넌트들, upower, NetworkManager, gnome-shell 같은 것들을 개발하고 있었습니다.

시스템 레벨 소프트웨어를 개발할 때 특유의 고통이 있습니다. 테스트하려면 호스트 환경을 건드려야 하는데, 잘못 건드리면 개발 머신 자체가 망가집니다. Walters는 2013년 블로그에서 이렇게 회고합니다.

패키지 기반 배포판들은 stable, testing, unstable 같은 레이어를 관리합니다. 그런데 suspend 기능이 작동하는지 확인하려고 stable에서 unstable로 올렸다가 다시 내리려고 하면, 패키지 시스템이 저항합니다. “newer is better"라는 개념이 dpkg와 rpm에 깊이 박혀 있기 때문입니다.

되돌아보면, 그가 원한 것은 단순했습니다. Firefox의 Nightly처럼 최신 빌드를 돌려보다가 문제가 생기면 이전 버전으로 쉽게 돌아갈 수 있는 메커니즘. 그런데 운영체제 수준에서.

2012년 GUADEC에서 그가 OSTree를 처음 발표할 때 사용한 표현이 있습니다. “Git의 content-addressed object storage 모델에서 영감을 받은 원자적 운영체제 이미지 시스템.”

여기서 Git이 등장합니다.


왜 Git인가

Git은 소스 코드 버전 관리 시스템입니다. 그런데 Git의 핵심 아이디어는 사실 코드와 무관합니다.

모든 파일을 그 내용의 해시값으로 식별합니다. 같은 내용이면 같은 해시값입니다. 한 번만 저장됩니다. 커밋은 특정 시점의 전체 파일 트리를 가리키는 스냅샷입니다. 브랜치는 특정 커밋에 붙인 이름표입니다.

Walters의 발상은 이랬습니다. 이 모델을 운영체제 바이너리에 적용하면 어떨까?

파일시스템 전체를 하나의 스냅샷으로 취급하고, 각 버전을 커밋으로 기록하고, 브랜치처럼 특정 버전을 추적할 수 있다면. 운영체제 업그레이드를 Git checkout처럼 다룰 수 있지 않을까.

OSTree 프로젝트 홈페이지의 표현이 이것입니다. “Git for operating system binaries.”


Content-Addressed Object Store

OSTree의 핵심에는 Git과 유사한 content-addressed object store가 있습니다. 이 용어가 무엇을 의미하는지 정확히 짚어야 합니다.

일반적인 파일시스템에서 파일은 경로로 식별됩니다. /usr/bin/bash라는 경로가 파일을 가리킵니다.

content-addressed 시스템에서는 다릅니다. 파일 내용의 SHA256 해시값이 파일을 식별합니다. /usr/bin/bash의 내용을 해시하면 a3f2e8c9... 같은 값이 나옵니다. 저장소에서 이 파일을 찾으려면 해시값으로 찾습니다.

이 방식의 장점이 세 가지 있습니다.

첫째, 자동 중복 제거. 같은 내용의 파일은 같은 해시값을 가집니다. 시스템에 동일한 파일이 100개 있어도 저장소에는 한 번만 저장됩니다.

둘째, 무결성 검증. 파일을 읽을 때 내용을 해시해서 저장된 해시값과 비교하면 손상 여부를 즉시 알 수 있습니다.

셋째, 버전 간 공유. 운영체제 버전 A와 B에서 변경되지 않은 파일들은 같은 해시값을 가집니다. 저장소에서 같은 객체를 공유합니다.

여기까지는 Git과 거의 동일합니다. 그런데 결정적인 차이가 하나 있습니다.


Git과의 결정적 차이: 하드링크

Git은 파일을 체크아웃할 때 복사합니다. 저장소의 압축된 객체를 풀어서 작업 디렉토리에 새 파일로 만듭니다.

OSTree는 다릅니다. 하드링크를 사용합니다.

잠시 하드링크를 설명하겠습니다. Unix 파일시스템에서 파일은 두 부분으로 구성됩니다. 실제 데이터가 저장된 inode와, 그 inode를 가리키는 디렉토리 엔트리(파일 이름)입니다. 하드링크는 같은 inode를 가리키는 또 다른 디렉토리 엔트리입니다.

쉽게 말해, 하드링크된 파일들은 실제로는 같은 데이터를 공유합니다. 디스크 공간을 추가로 차지하지 않습니다.

OSTree가 배포를 체크아웃할 때 /ostree/repo에 저장된 객체들에 대한 하드링크를 생성합니다. 10GB짜리 운영체제 이미지를 체크아웃해도 실제로 10GB의 추가 공간이 필요하지 않습니다.

그런데 여기서 중대한 제약이 생깁니다.

하드링크된 파일을 수정하면 어떻게 될까요? 같은 inode를 공유하는 모든 “파일"에 영향을 미칩니다. 저장소의 원본 객체가 손상됩니다.

OSTree 공식 문서의 표현입니다.

The core OSTree model is like git in that it checksums individual files and has a content-addressed-object store. It’s unlike git in that it “checks out” the files via hardlinks, and they thus need to be immutable to prevent corruption.

체크아웃된 파일들은 반드시 **불변(immutable)**이어야 합니다. 이것이 bootc 시스템에서 /usr이 읽기 전용으로 마운트되는 근본적인 이유입니다. 기술적 선택이 아닙니다. 필수입니다.


저장소 구조 들여다보기

실제로 bootc 시스템에서 /ostree 디렉토리를 살펴보면 이런 구조가 보입니다.

/ostree/
├── repo/                    # 객체 저장소
│   ├── objects/             # 해시로 이름 붙여진 파일들
│   └── refs/                # 브랜치들
├── deploy/                  # 배포된 시스템들
│   └── centos/
│       └── abc123...0/      # 특정 커밋의 체크아웃
├── boot.0/                  # 스왑 디렉토리
└── boot.1/

Git의 .git 디렉토리와 비교해 보면 유사성이 보입니다. objects에 해시로 이름 붙여진 객체들, refs에 브랜치들.

저장소에는 네 종류의 객체가 있습니다.

commit - Git의 커밋과 유사합니다. 타임스탬프, 로그 메시지, 루트 디렉토리 참조를 포함합니다.

dirtree - Git의 tree 객체와 유사합니다. 디렉토리 내용물을 기록합니다.

dirmeta - Git에는 없습니다. 디렉토리의 메타데이터(uid, gid, 확장 속성)를 별도로 저장합니다. 왜 분리했을까요? SELinux 레이블 같은 확장 속성이 여러 디렉토리에서 동일할 수 있어서 중복을 피하기 위해서입니다.

content - 실제 파일 내용입니다. Git과 달리 uid, gid, 확장 속성을 포함합니다. 운영체제 파일은 이런 메타데이터가 중요하기 때문입니다.

한 가지 특이한 점. OSTree는 타임스탬프를 저장하지 않습니다. 모든 파일의 타임스탬프를 1970년 1월 1일로 취급합니다. 하드링크로 체크아웃하기 때문에 타임스탬프 기반 빌드 시스템과의 충돌을 피하려는 의도입니다.


원자적 전환: Swapped Directory Pattern

이제 핵심 질문으로 돌아옵니다. OSTree는 어떻게 원자적 업그레이드를 보장하는가?

공식 문서의 첫 문장이 명확합니다.

OSTree is designed to implement fully atomic and safe upgrades. If the system crashes or you pull the power, you will have either the old system, or the new one.

시스템이 충돌하거나 전원이 꺼져도 이전 시스템 또는 새 시스템. 둘 중 하나입니다. 중간 상태는 없습니다.

이를 구현하는 메커니즘이 swapped directory pattern입니다.

OSTree에서 /boot는 심볼릭 링크입니다. /ostree/boot.0 또는 /ostree/boot.1 중 하나를 가리킵니다.

공식 문서를 인용합니다.

To swap the contents atomically, if the current version is 0, we create /ostree/boot.1, populate it with the new contents, then atomically swap the symbolic link. Finally, the old contents can be garbage collected at any point.

현재 버전이 0이라고 가정합시다.

  1. /ostree/boot.1 디렉토리를 생성합니다.
  2. 새로운 부트 설정을 이 디렉토리에 채워 넣습니다.
  3. 모든 준비가 완료되면 /boot 심볼릭 링크를 원자적으로 교체합니다.
  4. 이전 내용은 나중에 가비지 컬렉션됩니다. 여기서 “원자적으로 교체"가 핵심입니다.

Unix 파일시스템에서 심볼릭 링크 교체는 rename() 시스템 콜로 수행됩니다. 이 연산은 POSIX 표준에 의해 원자적으로 보장됩니다. 중간 상태가 존재하지 않습니다. 교체가 완료되었거나 완료되지 않았거나.

이것이 전원을 아무 때나 꺼도 안전한 이유입니다.

심볼릭 링크 교체 이전에 전원이 꺼지면? 이전 시스템으로 부팅됩니다. 교체 이후에 꺼지면? 새 시스템으로 부팅됩니다.


배포 조립 과정

새로운 배포를 준비하는 전체 흐름을 따라가 봅시다.

1단계: 커밋 가져오기

OSTree는 원격 저장소에서 ref가 가리키는 커밋의 체크섬을 확인합니다. 해당 커밋이 로컬에 없으면 pull을 수행합니다. content-addressed 특성 덕분에 이미 가지고 있는 객체는 다시 받지 않습니다. 변경된 것만 다운로드합니다.

2단계: 배포 디렉토리 할당

/ostree/deploy/$STATEROOT/$CHECKSUM.$SERIAL 형태로 디렉토리가 생성됩니다. $SERIAL은 보통 0이지만, 같은 커밋이 두 번 이상 배포되면 증가합니다.

3단계: /etc 3-way merge

현재 부팅된 배포의 /etc, 그 배포의 기본 설정(/usr/etc), 새 배포의 기본 설정. 이 셋을 비교합니다.

규칙은 단순합니다. 사용자가 수정한 설정은 보존됩니다. 수정하지 않은 설정은 새 버전을 따릅니다.

4단계: 하드링크 팜 생성

저장소의 객체들에 대한 하드링크로 새 배포 디렉토리를 구성합니다. 실행 중인 시스템은 전혀 건드리지 않습니다.

5단계: 심볼릭 링크 교체

모든 준비가 완료되면 /boot 심볼릭 링크를 원자적으로 교체합니다.

여기서 중요한 점. 4단계까지는 얼마든지 실패해도 됩니다. 실행 중인 시스템에 영향이 없습니다. 5단계만 원자적으로 성공하면 됩니다.


재부팅이 빠른 이유

이제 처음 질문으로 돌아올 수 있습니다. bootc upgrade 후 재부팅이 왜 빠른가?

무거운 작업은 이미 끝났기 때문입니다.

전통적인 패키지 관리자는 재부팅 후에도 설정 스크립트가 돌고, 서비스가 재구성되고, 여러 가지 후처리가 일어납니다. OSTree 기반 시스템에서 재부팅은 그저 새로운 심볼릭 링크를 따라가는 것뿐입니다.

하드링크 팜은 이미 구성되어 있습니다. 커널과 initramfs는 이미 /boot/ostree에 배치되어 있습니다. 부트로더는 새로운 경로를 따라갈 뿐입니다.


롤백이 쉬운 이유

새 버전으로 업데이트했는데 문제가 생겼다고 가정합시다.

전통적인 시스템에서 롤백은 고통스럽습니다. 어떤 패키지가 문제인지 찾아야 하고, 이전 버전을 다시 설치해야 하고, 의존성을 맞춰야 합니다. 백업이 없으면 사실상 불가능합니다.

OSTree에서는 이전 배포가 그대로 남아 있습니다. 부트로더 메뉴에서 이전 항목을 선택하면 됩니다. bootc rollback 명령 하나로 다음 부팅 시 이전 배포로 돌아가도록 설정됩니다.

왜 가능할까요? A/B 구조 때문입니다. 새 배포를 준비할 때 이전 배포를 삭제하지 않습니다. 둘 다 공존합니다. 하드링크 덕분에 디스크 공간도 거의 추가로 들지 않습니다.


OSTree에서 bootc로

2023년, Colin Walters는 bootc 프로젝트를 시작합니다. OSTree의 개념을 OCI 컨테이너 이미지로 확장한 것입니다.

OSTree는 자체 저장소 형식과 전송 프로토콜을 사용합니다. bootc는 대신 OCI 이미지 형식을 사용합니다. Podman, Docker 같은 기존 도구들과 Docker Hub, GHCR 같은 기존 인프라를 그대로 활용할 수 있습니다.

그러나 배포 메커니즘의 핵심은 여전히 OSTree입니다. bootc 시스템 내부에서 컨테이너 이미지는 OSTree 커밋으로 변환됩니다. 원자적 전환, 하드링크 기반 중복 제거, A/B 배포. 모두 OSTree의 것입니다.


정리하며

처음 질문으로 돌아갑시다. bootc도 Git처럼 이력이 관리되는가?

정확히 그렇습니다. OSTree는 Git의 content-addressed model을 운영체제 바이너리에 적용한 것입니다. 커밋, 브랜치, 체크아웃. 개념이 거의 동일합니다.

바뀐 부분만 저장되는가?

그렇습니다. 해시가 같은 파일은 공유됩니다. 업그레이드 시 변경된 객체만 다운로드됩니다.

그렇다면 왜 불변 인프라인가?

하드링크 체크아웃이 불변성을 강제하기 때문입니다. 파일을 수정하면 저장소가 손상됩니다. 따라서 수정할 수 없습니다. “수정하지 않고 교체한다"는 원칙은 OSTree에서 선택이 아니라 구조적 제약입니다.

이것이 bootc가 불변 인프라인 이유입니다.

Comments

GitHub 계정으로 로그인하여 댓글을 남겨보세요. GitHub 로그인

댓글 시스템 설정이 필요합니다

GitHub Discussions 기반 댓글 시스템을 활성화하려면:

  1. Giscus 설정 페이지에서 설정 생성
  2. GISCUS_SETUP_GUIDE.md 파일의 안내를 따라 설정 완료
  3. Repository의 Discussions 기능 활성화

Repository 관리자만 설정할 수 있습니다.