본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

LiteFS가 동작하는 방식

원문 보기 위키 갱신

LiteFS가 SQLite 데이터베이스를 어떻게 복제하고 고가용성을 유지하는지 그 내부 동작을 알려드릴게요. SQLite 트랜잭션을 어떻게 캡처하는지, 복제 위치를 어떻게 관리하는지, 임대(lease) 시스템으로 클러스터를 어떻게 관리하는지, 그리고 split brain을 어떻게 처리하는지 함께 살펴보아요.

출처: 문서

본문

LiteFS는 SQLite 데이터베이스를 복제하기 위해 특별히 구축된 분산 파일 시스템입니다. 이를 통해 각 애플리케이션 노드가 데이터베이스의 전체 로컬 사본을 갖고 최소한의 지연으로 요청에 응답할 수 있습니다. LiteFS는 고가용성을 위해 설계되어 치명적인 장애가 발생하더라도 운영을 계속할 수 있습니다.

SQLite 트랜잭션 캡처하기

SQLite는 모든 트랜잭션에서 데이터베이스 파일의 페이지 일부를 업데이트하는 방식으로 동작합니다. 이는 파일 시스템 API를 사용해 수행하며, 정확한 단계는 롤백 저널(rollback journal)을 사용하는지 쓰기-앞 로그(WAL)를 사용하는지에 따라 달라집니다.

LiteFS는 이러한 API 호출을 가로채고 각 트랜잭션의 페이지 세트를 복사해 내는 통과(passthrough) 파일 시스템 역할을 합니다. 이러한 페이지 세트는 LTX라는 내부 형식으로 저장되며, 이 형식은 정확성을 보장하기 위해 광범위한 일관성 검사를 수행합니다.

LTX 파일의 페이지 세트를 순서대로 적용하면 어떤 시점에서든 SQLite 데이터베이스의 상태를 재구성할 수 있습니다. 또한 이를 다른 노드에 복사해 변경 사항을 실시간으로 클러스터에 복제할 수도 있습니다.

롤백 저널 처리

SQLite가 DELETE, PERSIST, TRUNCATE 저널 모드 중 하나로 트랜잭션을 생성할 때 다음 단계가 발생합니다:

  1. RESERVED 바이트에 배타적 잠금을 획득합니다.
  2. 트랜잭션을 시작하기 위해 저널 파일을 생성합니다.
  3. 이전 페이지를 저널에 복사하면서 메인 데이터베이스 파일의 페이지를 업데이트합니다.
  4. 트랜잭션을 완료하기 위해 저널 파일을 무효화합니다.
  5. RESERVED 바이트의 잠금을 해제합니다.

LiteFS는 데이터베이스 파일에 대한 write(2) 호출을 기반으로 오염된 더티 데이터베이스 페이지 세트를 유지합니다. 저널 파일이 무효화되면 LiteFS는 더티 페이지를 순차 순서로 LTX 트랜잭션 파일로 복사해 냅니다.

쓰기-앞 로그(WAL) 처리

SQLite가 WAL 저널 모드로 트랜잭션을 생성할 때 다음 단계가 발생합니다:

  1. WAL_WRITE_LOCK에 배타적 잠금을 획득합니다.
  2. 새 페이지 또는 업데이트된 페이지마다 WAL 프레임을 WAL 끝에 씁니다.
  3. 커밋 시 마지막 WAL 프레임 헤더의 commit 필드에 데이터베이스의 새 크기를 씁니다.
  4. WAL_WRITE_LOCK의 배타적 잠금을 해제합니다.

모든 페이지 변경이 쓰기-앞 로그에 있으므로 LiteFS는 더티 페이지 세트를 유지할 필요가 없습니다. 대신 WAL의 이전 종료 오프셋부터 끝까지 읽어 새 및 변경된 모든 페이지를 LTX 파일로 캡처합니다.

복제 위치(Replication Position)

LiteFS 디렉토리 안의 각 데이터베이스는 복제 위치를 포함합니다. 이 위치는 트랜잭션 ID(TXID)와 데이터베이스 내용의 롤링 체크섬의 조합입니다. TXID는 쓰기 트랜잭션이 발생할 때마다 증가하는 정수입니다.

LiteFS 마운트 디렉토리의 데이터베이스 위치 파일을 읽어 노드 간의 상대적 위치를 추적할 수 있습니다. 예를 들어 /mnt/db라는 데이터베이스가 있다면 /mnt/db-pos 파일에서 위치를 읽을 수 있습니다:

$ cat /mnt/db-pos
00000000000003e8/a3b2b72f1147c9bc

출력은 16진수로 인코딩된 TXID 다음에 슬래시, 그리고 데이터베이스 롤링 체크섬 순입니다.

임대(lease)를 사용한 클러스터 관리

SQLite는 단일 작성자(single-writer) 데이터베이스로 동작하므로 한 번에 하나의 트랜잭션만 쓸 수 있습니다. 이 때문에 임대 시스템을 사용해 주어진 시점에 단일 노드가 "primary" 노드 역할을 합니다. 모든 쓰기는 해당 노드로 보내야 하며, 그 노드가 변경 사항을 나머지 클러스터에 전파합니다.

하지만 노드는 죽거나 클러스터에서 연결이 끊길 수 있으므로 primary는 동적으로 변경될 수 있어야 합니다. LiteFS는 클러스터 구성원이 빠르고 자주 변경될 수 있는 임시(ephemeral) 환경에서 실행되도록 설계되었으므로, Consul을 통한 임대를 사용해 현재 primary 노드를 결정합니다.

LiteFS 노드가 시작되면 Consul 서버에서 현재 primary 노드에 대한 키를 확인합니다. primary 노드가 없으면 LiteFS 노드는 Consul에서 임대를 획득해 스스로 primary 노드가 되려고 시도합니다. 성공하면 Consul 키를 업데이트해 API URL을 공유하고, 다른 LiteFS 노드가 연결할 수 있게 합니다.

Split brain 처리

LiteFS는 *비동기 복제(asynchronous replication)*를 사용합니다. 즉, 다음 트랜잭션으로 진행하기 전에 쓰기가 다른 노드에 커밋되었는지 확인하지 않습니다. 이 트레이드오프는 일부 내구성(durability)을 희생하는 대신 쓰기 처리량을 높이기 위한 것입니다.

예상치 못한 primary 노드 장애가 발생하면, primary 노드가 로컬에서 적용했지만 다른 노드로 복사되지 않은 미복제 쓰기를 가지고 있다면 클러스터의 나머지와 동기화가 어긋날 수 있습니다. 새 primary가 선출되면 이전 primary가 새 primary의 트랜잭션을 적용하려다 데이터를 손상시킬 수 있습니다.

다행히 LiteFS는 데이터베이스 전체 내용의 롤링 체크섬을 유지하므로 이 Split을 감지하고 새 primary에서 동기화가 어긋난 노드로 자동으로 스냅샷을 가져옵니다.

동기 복제(synchronous replication)와 시간 제약 비동기 복제(time-bound asynchronous replication)는 향후 개발을 위해 계획된 기능입니다.

더 알아보기 (Learn more)