본문 바로가기

SQLite 활용한 다중 테넌시와 데이터 관리 최적화 및 안정적 백업 전략

728x90

SQLite는 파일 기반의 경량 데이터베이스로, 특히 다수의 소규모 데이터를 관리하는 시나리오에서 뛰어난 성능을 발휘합니다. SQLite를 활용한 다중 테넌시 아키텍처, 데이터베이스 복사 및 백업 최적화, 그리고 Litestream을 통한 데이터 안정성 강화 방법입니다.

테넌트별 데이터베이스 아키텍처의 장점과 과제

테넌트별 데이터베이스 아키텍처란?

테넌트별 데이터베이스 아키텍처는 애플리케이션의 데이터를 독립적인 사용자 또는 그룹(테넌트) 단위로 분리하여, 각 테넌트마다 별도의 데이터베이스를 할당하는 패턴입니다. 이 아키텍처는 특히 소규모 테넌트가 다수인 환경에서 SQLite와 같은 서버리스 데이터베이스와 잘 어울립니다.

주요 장점

  1. 강력한 데이터 격리
    • 테넌트 간 데이터 유출 및 오염을 원천적으로 차단합니다.
    • 예: "Jane이 자신의 Site에 게시한 글이 실수로 Blake의 Site에 올라가는" 상황을 방지합니다.
  2. 단순화된 데이터 관리
    • 백업, 복원, 삭제 및 디버깅이 테넌트 단위로 이루어져 훨씬 간편합니다.
    • "디버깅이 훨씬 쉬워집니다. 특정 Site 내에서 무언가를 디버깅해야 하는 경우, 이 Site의 데이터만 다운로드하면 됩니다. 일반적으로 scp 명령 한 번이면 충분합니다."
  3. 효율적인 삭제
    • 테넌트 제거는 단순히 "unlink" 명령으로 데이터베이스 파일을 삭제하는 것만으로 가능합니다.
  4. SQLite와의 시너지
    • SQLite는 "작은 데이터, 많은 숫자"를 다루는 데 최적화되어 있으며, 파일 기반 특성상 수천 개의 소규모 데이터베이스 관리에 적합합니다.
    • "SQLite3는 수많은 작은 데이터베이스에서 성장합니다."

Rails/ActiveRecord 환경에서의 주요 과제

  1. ActiveRecord의 설계 불일치
    • ActiveRecord는 기본적으로 단일 데이터베이스 연결을 전제로 설계되어, 런타임에 동적으로 데이터베이스 연결을 전환하는 것이 복잡합니다.
    • "ActiveRecord는 연결을 모델에 고정해서 사용하는 구조로 설계되어 있어, 런타임에 테넌트 전환이 어렵습니다."
  2. 레거시 연결 관리 방식의 한계
    • establish_connection과 같은 이전 방식은 멀티스레딩 환경에서 ActiveRecord::ConnectionNotEstablished와 같은 오류를 유발하기 쉽습니다.
  3. 동적인 구성 관리 부재
    • database.yml과 같은 정적 설정을 사용하는 Rails의 기존 방식은 런타임에 테넌트가 생성, 삭제되거나 변경되는 시나리오를 지원하기 어렵습니다.
  4. 다중 데이터베이스 처리를 위한 내부 구조의 복잡성
    • ConnectionHandling, DatabaseConfig, DatabaseConfigurations, Resolver, PoolConfig, PoolManager 등 다양한 구성 요소들이 복잡하게 얽혀 있어 동적인 연결 관리가 어렵습니다.
300x250

Rails에서의 동적 테넌트별 데이터베이스 연결 관리 해결책

Rails 6 이상에서는 connected_to 기능을 활용하고 ActiveRecord의 내부 연결 풀 관리 방식을 이해하여 런타임에 동적으로 테넌트별 데이터베이스 연결을 설정하고 관리할 수 있습니다.

해결책의 핵심 요소

  1. connected_to 활용
    ActiveRecord::Base.connected_to(role: role_name) do
      # 특정 테넌트의 데이터베이스 컨텍스트 내에서 ActiveRecord 작업 수행
      pages = Page.order(created_at: :desc).limit(10)
      # Only selects from that site/tenant
    end
  2. 동적 연결 풀 생성
    MUX.synchronize do
      if ActiveRecord::Base.connection_handler.connection_pool_list(role_name).none?
        ActiveRecord::Base.connection_handler.establish_connection(database_config_hash, role: role_name)
      end
    end
  3. Rack 미들웨어 통합
    • 요청 처리 과정에서 Shardine::Middleware와 같은 Rack 미들웨어를 사용하여 요청별로 적절한 테넌트 데이터베이스 연결을 설정하고, 응답이 완료된 후 연결을 해제합니다.
    • "따라서 솔루션은 한 영역에 집중됩니다: ActiveRecord에서 해당 연결 풀을 관리하고 역할 및 샤드를 자동으로 명명하는 것을 인수하는 것입니다."
  4. Rack 스트리밍 바디 처리
    • 응답 바디가 스트리밍될 때 연결 관리를 안전하게 하기 위해 Rack::BodyProxy와 Fiber를 활용하여 connected_to 블록 외부에서도 연결 컨텍스트를 유지하고 해제합니다.
  5. legacy_connection_handling 비활성화 (Rails 6)
    • Rails 6에서는 이 해결책이 작동하도록 legacy_connection_handling 설정을 명시적으로 꺼야 합니다.

SQLite 데이터베이스 복사 및 백업 최적화

대규모 SQLite 데이터베이스를 복사하거나 백업할 때, .dump 명령을 사용하여 데이터베이스의 SQL 텍스트 덤프를 생성하고 이를 압축하여 전송하는 방식은 원본 데이터베이스 파일 직접 복사보다 훨씬 빠르고 안정적입니다.

기존 방식의 문제점

  1. 인덱스로 인한 파일 크기 증가
    • SQLite 데이터베이스 파일에는 데이터 자체뿐만 아니라 인덱스 정보도 포함되어 있어 파일 크기가 커지고 복사 속도가 느려집니다.
    • "인덱스는 데이터를 반복해서 저장하지 않습니다. 그들은 테이블에 있는 데이터를 복제하여 쿼리를 빠르게 만듭니다. 인덱스를 복사하면 동일한 데이터를 여러 번 복사하게 되어 전송이 덜 효율적입니다."
  2. 전송 중 데이터 변경 시 손상 위험
    • 복사 도중에 데이터베이스가 변경되면 파일의 앞부분과 뒷부분이 일치하지 않아 "database disk image is malformed" 오류가 발생할 수 있습니다.

개선된 방식 (.dump + 압축)

  1. 텍스트 덤프
    • sqlite3 my_database.db .dump 명령은 데이터베이스의 전체 내용을 SQL 명령문 시퀀스로 출력합니다.
    • 인덱스는 CREATE INDEX와 같은 명령 한 줄로 표현되어 실제 인덱스 데이터가 포함되지 않습니다.
    • "결정적으로, 이것은 크고 디스크를 많이 차지하는 인덱스를 한 줄의 텍스트로 줄입니다. 그것은 인덱스를 생성하는 지시일 뿐, 인덱스 자체가 아닙니다."
  2. 압축 효율
    • 텍스트 덤프는 반복적인 SQL 명령문으로 구성되어 있어 gzip과 같은 압축 도구에 매우 효과적입니다.
    • "SQL 명령문은 매우 반복적이므로 이 텍스트는 압축에 잘 반응합니다."
    • 원본 데이터베이스의 1/14 수준으로 크기가 줄어들 수 있습니다.
  3. 안정적인 복사 소스
    • 텍스트 덤프를 생성한 후에는 해당 파일의 내용이 변경되지 않으므로, rsync와 같은 도구로 복사하더라도 일관되고 손상되지 않은 복사본을 얻을 수 있습니다.
    • "복사 작업을 시작하기 전에 텍스트 덤프를 생성함으로써 rsync에 안정적인 복사 소스를 제공합니다."

작업 절차

  1. 서버에서 SQLite 데이터베이스를 .dump 명령과 gzip 압축을 사용하여 텍스트 파일로 덤프합니다.
    ssh username@server "sqlite3 my_remote_database.db .dump | gzip -c > my_remote_database.db.txt.gz"
  2. 압축된 텍스트 파일을 rsync를 사용하여 로컬 컴퓨터로 복사합니다.
    rsync --progress username@server:my_remote_database.db.txt.gz my_local_database.db.txt.gz
  3. 서버에서 임시로 생성된 압축 파일을 삭제합니다.
    ssh username@server "rm my_remote_database.db.txt.gz"
  4. 로컬에서 압축된 파일을 해제합니다.
    gunzip my_local_database.db.txt.gz
  5. 텍스트 덤프 파일을 SQLite로 파이핑하여 데이터베이스를 재구성합니다.
    cat my_local_database.db.txt | sqlite3 my_local_database.db
  6. 로컬에서 임시 텍스트 파일을 삭제합니다.
    rm my_local_database.db.txt

Litestream을 활용한 SQLite 데이터 안정성 강화

Litestream은 SQLite 애플리케이션의 데이터 변경 사항(WAL)을 S3 호환 객체 저장소로 지속적으로 스트리밍하여 서버 장애 발생 시 데이터 손실 없이 효율적인 복구를 가능하게 하는 오픈 소스 도구입니다. 최근 LiteFS의 아이디어를 통합하여 시점 복구 및 다수의 데이터베이스 동기화 기능이 강화되었습니다.

Litestream의 기본 동작

  1. SQLite 애플리케이션과 별도의 프로세스로 실행되며, 애플리케이션 코드 변경 없이 작동합니다.
  2. SQLite의 WAL(Write-Ahead Log) 체크포인팅 프로세스를 제어하여 데이터 변경 사항을 가로챕니다.
  3. 변경 사항을 S3와 같은 객체 저장소로 지속적으로 스트리밍합니다.
  4. 서버 장애 시 객체 저장소에서 데이터베이스를 효율적으로 복원할 수 있습니다.

최근 업데이트의 주요 특징

1. 빠른 시점 복구 (Point-in-time Restores)

  • 기존 Litestream은 모든 WAL 페이지를 복제하여 복원 시 모든 변경 사항을 재생해야 했지만, LiteFS에서 사용된 LTX 파일 포맷과 Compaction 기법을 도입했습니다.
  • LTX 파일은 트랜잭션 기반의 정렬된 페이지 변경 범위를 기록하며, 여러 LTX 파일을 병합(compaction)하여 최신 버전의 페이지만 남길 수 있습니다.
  • 이는 LSM 트리와 유사한 방식으로, 복구 시 중복 페이지 재생을 최소화하여 복구 속도와 효율성을 대폭 향상시킵니다.
  • "이 과정, 즉 더 작은 시간 범위를 더 큰 시간 범위로 결합하는 것을 Compaction이라고 합니다. 이를 통해 SQLite 데이터베이스를 특정 시점으로 재생할 수 있으며, 최소한의 중복 페이지를 사용할 수 있습니다."

2. 단일 리더 보장 (Compare-and-Swap as a Service - CASAAS)

  • 이전 Litestream은 여러 인스턴스가 동일한 대상에 복제할 때 문제가 발생할 수 있었습니다.
  • LiteFS는 Consul을 사용했지만, 새로운 Litestream은 S3와 같은 최신 객체 저장소의 조건부 쓰기 (conditional write) 지원을 활용하여 별도의 종속성 없이 리더 싱글톤을 구현합니다.
  • "현대적인 객체 저장소는 우리를 위해 이 문제를 해결해 줍니다. 조건부 쓰기 지원을 제공합니다. 조건부 쓰기를 통해 시간 기반 리스를 구현할 수 있습니다."
  • 이를 통해 임시 노드 환경에서도 여러 Litestream 인스턴스가 동시에 실행되어도 혼동 없이 안정적으로 작동할 수 있습니다.

3. 경량 Read Replica (향후)

  • LiteFS는 FUSE 파일시스템을 사용했지만, 복잡성이 높았습니다. LiteFS는 LiteVFS라는 SQLite Virtual Filesystem(VFS) 확장 모듈을 도입하여 FUSE 없이도 다양한 환경에서 작동 가능하게 했습니다.
  • 향후 Litestream에도 동일한 VFS 기반 레이어를 적용하여 S3와 같은 객체 저장소에서 직접 페이지를 가져오고 캐시하는 Read Replica 기능을 제공할 계획입니다.
  • 로컬 DB만큼 빠르지는 않지만 캐싱 및 프리페칭을 통해 성능을 확보할 것으로 기대됩니다.
  • "우리는 LiteVFS와 동일한 트릭을 사용하고 있습니다. VFS 기반 Read Replica 레이어를 구축하고 있습니다. S3 호환 객체 저장소에서 직접 페이지를 가져오고 캐시할 수 있습니다."

4. 다수 데이터베이스 동기화

  • 이전 Litestream 설계에서는 WAL 변경 사항 폴링 및 느린 복원 때문에 단일 프로세스에서 많은 수의 데이터베이스를 복제하는 것이 비효율적이었습니다.
  • LTX로 전환하면서 이 문제가 해결되었으며, 이제 수백 또는 수천 개의 데이터베이스가 있는 디렉토리(/data/*.db)도 효율적으로 복제할 수 있게 되었습니다.
  • "이제 LTX로 전환했으므로 더 이상 문제가 되지 않습니다. 따라서 해당 디렉토리에 수백 또는 수천 개의 데이터베이스가 있더라도 /data/*.db를 복제하는 것이 가능해야 합니다."

활용성

  • Litestream은 완전한 오픈 소스이며 Fly.io에 종속되지 않고 어디서든 사용 가능합니다.
  • 최근 LLM 기반 코드 생성 에이전트와 같이 데이터 롤백 및 분기가 중요한 분야에서도 Litestream의 발전된 시점 복구 기능이 중요한 기반이 될 수 있습니다.

 

SQLite를 활용한 다중 테넌시 아키텍처는 특히 소규모 테넌트가 많은 경우에 강력한 이점을 제공합니다. Rails와 ActiveRecord 환경에서는 동적인 테넌트별 데이터베이스 연결 관리가 복잡하지만, connected_to 및 내부 연결 관리 메커니즘을 이해하고 활용함으로써 해결책을 구축할 수 있습니다.

 

또한, .dump를 활용한 백업 및 복사 최적화는 SQLite의 관리 효율성을 더욱 높입니다. Litestream은 SQLite의 임베디드 특성으로 인한 데이터 지속성 문제를 해결하고, 최근 업데이트를 통해 빠른 시점 복구, 단일 리더 보장 및 다수 데이터베이스 동기화 기능을 강화하여 SQLite 기반 애플리케이션의 안정성과 확장성을 크게 향상시키고 있습니다.

 

이러한 발전들은 "작은 데이터, 많은 숫자"를 다루는 새로운 아키텍처 패턴에서 SQLite의 가능성을 더욱 확장시키고 있으며, 특히 다중 테넌시 아키텍처나 마이크로서비스 환경에서 SQLite의 활용 가치를 높이고 있습니다.

728x90
그리드형(광고전용)

댓글