3P by GN⁺ | ★ favorite | 댓글 1개
  • OpenRun은 내부 도구용 웹 앱 배포 플랫폼으로, 정적 파일·앱 코드·설정 파일을 파일 시스템 대신 SQLite에 저장해 배포 상태를 데이터베이스 중심으로 관리함
  • 여러 파일이 함께 바뀌는 앱 업데이트를 하나의 트랜잭션으로 처리해, 버전 전환 중 깨진 웹 페이지가 제공되는 상황을 막는 것이 핵심 목적임
  • 압축 전 SHA256 해시를 기본 키로 쓰면서 앱 버전 간, staging·preview·production 앱 간 중복 파일 저장을 줄임
  • SQLite 저장 방식은 롤백, 백업, ETag용 해시 저장, Brotli 압축 저장을 단순하게 만들며, 필요하면 GZip·비압축 데이터도 컬럼 추가로 함께 다룰 수 있음
  • 현재는 단일 노드에서 동작하고, 멀티 노드 지원 시 공유 Postgres와 로컬 SQLite 파일 캐시를 함께 써 지연을 줄이는 방향을 계획 중임

OpenRun의 파일 저장 방식

  • OpenRun은 코드 우선(code-first) 내부 도구를 위한 오픈소스 배포 플랫폼이며, 단일 노드 또는 Kubernetes 클러스터에 GitOps 방식으로 웹 앱을 배포함
  • 일반적인 웹 서버처럼 파일 시스템에 정적 콘텐츠를 두는 대신, OpenRun은 정적 파일·앱 코드·설정 파일 같은 앱 데이터를 SQLite에 저장함
  • 앱 메타데이터가 동적으로 생성되기 때문에 데이터베이스 저장이 자연스럽고, 파일까지 같은 저장 계층에서 다루면 배포 상태를 함께 관리하기 쉬움
  • 앱 생성과 업데이트 시 파일은 GitHub 또는 로컬 디스크에서 SQLite 데이터베이스로 업로드됨
  • 개발 모드에서만 로컬 파일 시스템을 사용함

SQLite를 선택한 이유

  • 트랜잭션 업데이트가 가장 큰 장점임
    • 여러 파일 변경을 하나의 트랜잭션으로 묶어 처리할 수 있음
    • 격리성 덕분에 업데이트 도중 깨진 웹 앱이 제공되지 않음
  • 배포 오류가 나면 데이터베이스 트랜잭션 단위로 롤백할 수 있음
    • 여러 앱이 동시에 업데이트되는 경우에도 한 번에 되돌릴 수 있음
    • 파일 시스템에서 변경된 파일을 찾아 정리하는 방식보다 단순함
  • OpenRun은 모든 업데이트를 자동으로 버전 관리하며, 파일 데이터는 다음 스키마의 테이블에 저장됨
CREATE TABLE files (sha text, compression_type text, content blob, create_time datetime, PRIMARY KEY(sha));
  • 압축 전 콘텐츠의 SHA256 해시를 기본 키로 사용하므로 같은 파일 내용은 여러 버전에서 한 번만 저장됨
  • production 앱마다 staging app이 있고, 여러 preview apps를 가질 수 있어 파일 중복이 생길 수 있음
    • SQLite 기반 저장은 앱 간에도 같은 콘텐츠의 파일을 중복 저장하지 않게 해줌

백업, 캐싱, 압축 처리

  • 전체 시스템 상태, 메타데이터, 파일을 SQLite 백업 도구인 Litestream 등으로 백업할 수 있음
  • 브라우저 캐싱용 ETag 헤더에 필요한 콘텐츠 SHA를 파일 업로드 시 한 번 저장해두면 나중에 다시 계산할 필요가 없음
  • 파일 내용은 SQLite 테이블에 Brotli 압축 형태로 저장됨
    • 데이터베이스 방식에서는 files 테이블에 컬럼을 추가해 GZip 압축 데이터나 비압축 데이터도 저장할 수 있음

성능과 멀티 노드 계획

  • OpenRun에서 SQLite 데이터베이스 방식은 좋은 성능을 제공함
  • 파일 시스템 기반의 동등한 구현이 없어 직접 비교 벤치마크는 수행하지 않음
  • SQLite 팀의 벤치마크에 따르면 일부 워크로드에서는 SQLite가 직접 파일 시스템 사용보다 더 나은 성능을 낼 수 있음
  • OpenRun은 현재 단일 노드에서 실행됨
  • 향후 멀티 노드 지원이 추가되면 메타데이터와 파일 데이터 저장에 로컬 SQLite 대신 공유 Postgres 데이터베이스를 사용할 계획임
    • 이 방식은 지연 문제가 생길 수 있음
    • Postgres 접근 지연을 피하기 위해 로컬 SQLite 데이터베이스를 파일 캐시로 사용할 계획임

파일 시스템 방식이 더 흔한 이유

  • 대부분의 웹 서버가 파일 시스템을 사용하는 이유 중 하나는 편의성
    • rsync, tar 같은 기존 파일 시스템 도구로 파일을 복사하고 업데이트할 수 있음
  • 또 다른 이유는 역사적인 배경임
    • 좋은 인프로세스 관계형 데이터베이스가 있기 전부터 파일 시스템이 사용되어 왔음
  • 데이터베이스를 파일 저장소로 쓰려면 파일 업로드용 API 인터페이스가 필요하며, 항상 실현 가능한 방식은 아님

댓글과 토론

Hacker News 의견들
  • 몇 년 전 이 아이디어를 실험해 봤고, 일부는 “35% Faster Than The Filesystem” 글에서 영감을 받았음: https://www.sqlite.org/fasterthanfs.html
    당시 메모는 여기 있음: https://simonwillison.net/2020/Jul/30/fun-binary-data-and-sq...
    Datasette에서 SQLite로 정적 파일을 서빙하는 플러그인으로 https://datasette.io/plugins/datasette-media를 만들었고 잘 동작하지만, 솔직히 만든 뒤로 많이 쓰지는 않았음
    관련 개념으로 SQLite에서 지도 타일을 서빙하는 방식도 있고, https://datasette.io/plugins/datasette-tiles가 그 일을 함. MBTiles 형식이 알고 보니 PNG가 가득 든 SQLite 데이터베이스였음
    파일 서빙용 SQLite를 실험하려면 데이터베이스 초기 구성에 “sqlite-utils insert-files” CLI 도구가 유용할 수 있음: https://sqlite-utils.datasette.io/en/stable/cli.html#inserti...

    • 성능 관점에서는 콘텐츠 해시 기반 파일명 방식이 SQLite 저장소에서 구현하기 더 쉬움
      콘텐츠 해시는 파일 업로드 때 한 번만 만들면 되고, 웹 서버가 재시작될 때마다 만들거나 실제 파일명을 바꾸는 빌드 단계를 둘 필요가 없음. 파일 시스템 파일에도 동적으로 적용할 수는 있지만(https://github.com/benbjohnson/hashfs의 embedFS 구현 참고), 데이터베이스가 조금 더 쉽게 만들어 줌
    • SQLite 데이터베이스에서 xsendfile처럼 sendfile을 할 방법이 있는지 궁금함
      requests-cache는 기억이 맞다면 SQLite에 (date, URI)로 요청을 캐시함: https://github.com/requests-cache/requests-cache/blob/main/r...
      pyfilesystem SQLite 검색: https://www.google.com/search?q=pyfilesystem+sqlite
      sendfile mmap SQLite 검색: https://www.google.com/search?q=sendfile+mmap+sqlite
      https://github.com/adamobeng/wddbfs는 “sqlite 데이터베이스 내용을 읽을 수 있는 webdavfs 제공자”임
      Unix 파일 권한과 xattrs 확장 파일 속성 권한을 SQLite 위에 얹어 파일 시스템을 구현하는 좋은 방법도 있을 듯함
      SQLite가 예컨대 ngx_http_memcached_module.c보다 빠르거나 편할까? SQLite에 셀 단위 ACL도 있는지 궁금함
    • SQLite를 쓰면 열린 데이터베이스 연결을 유지하므로, SQLite가 이미 캐시했을 가능성이 높은 콘텐츠에 대해서만 요청을 보내면 됨
      정적 파일을 읽을 때는 매 요청마다 파일을 열고, 읽고, 닫아야 해서 파일 시스템 계층이 파일 내용을 캐시했더라도 문맥 전환이 더 많아짐. 이걸 빠르게 하고 싶다면 모든 것을 데이터베이스로 바꾸는 게 아니라 캐싱 프런트엔드를 붙이는 게 적절함. SQLite보다 빠르고 유지보수와 문제 해결도 쉬움
    • 시스템 호출과 사용자 공간·커널 사이 복사를 너무 많이 하지 않으면 뭐든 35% 더 빠른 것 아닌가 싶음
      완전히 사용자 공간에서 도는 파일 시스템도 포함됨. FUSE는 호출이 커널을 거치므로 제외함
  • “트랜잭션 업데이트”가 주된 이점이라는 말에는 한계가 있음. 서버가 SQLite를 쓰든 파일 시스템을 쓰든, 그 자체만으로는 업데이트 중 깨진 웹앱을 막을 수 없음
    브라우저의 각 페이지는 별도 HTTP 요청으로 가져오는 리소스 트리라서 서버 쪽 트랜잭션/원자적 업데이트 시스템의 대상이 아님. 서버에서 모든 리소스를 트랜잭션으로 바꿔도 브라우저는 옛 리소스와 새 리소스가 섞인 조합을 볼 수 있음
    보통 해법은 페이지의 모든 하위 리소스(JavaScript 번들, 스타일시트, 미디어 등)를 콘텐츠 해시나 버전이 들어간 이름(URL)으로 두는 것임. 루트 HTML 문서가 버전 X를 로드하면 모든 하위 리소스도 대응되는 버전 X를 로드해야 함
    또한 X에서 Y로 업데이트할 때도, Y 페이지를 서빙하기 시작한 뒤 한동안은 X 하위 리소스를 계속 제공해야 함. 아직 X 페이지를 로딩 중인 브라우저가 합리적으로 없다고 확신할 때까지 유지하지 않으면 X 페이지가 깨질 수 있음
    그래서 루트 HTML과 하위 리소스를 하나의 원자적으로 교체되는 번들에 넣고 싶어 하면 오히려 안 됨. 이전 하위 리소스가 아직 참조될 수 있는데 제거해 버리기 때문임
    경우에 따라 미디어 파일 같은 일부 하위 리소스는 HTML 문서와 별도로 버전 관리하고 싶을 수도 있음. JavaScript 덩어리나 스타일시트 같은 앱 구조 요소의 캐시를 전부 무효화하지 않고 갱신하려면 페이지 빌드 시스템에서도 이를 고려해야 할 수 있음

    • “브라우저가 아직 버전 X 페이지를 로딩 중일 리 없다고 확신할 때까지”가 생각보다 오래 걸림
      큰 회사에서 이걸 실험했을 때(그 시기 웹의 상당 부분을 봤음), 사용자의 대부분(80% 초과)은 웹앱에 약 2~3일 머물렀음. 주말 동안 탭을 열어두는 사람들이 있어 편향됐을 가능성이 큼
      95% 지점은 약 2주였고, 100%는 약 600일이었음. 거의 2년 동안 탭을 열어둔 사용자가 있었던 셈임
      100%를 목표로 하면 꽤 오래 기다려야 함. 이 수치는 전부 기억에 의존한 것이고, 이제 그 회사에 다니지는 않음
    • 하이퍼미디어 기반 웹앱에서는 페이지 안 상호작용이 작은 HTML 조각을 반환함. 더 큰 UI 변화에는 전체 페이지 갱신이 권장되고, 이는 MPA 방식에 가까움
      사용자가 한 페이지에 오래 머물다가 깨진 링크를 받는 시나리오는 SPA 쪽 문제에 더 가까움
      대체로 동의하지만, 트랜잭션 업데이트가 막아 주는 것은 업데이트 관련 문제의 한 범주뿐임. 앱 수준의 다른 문제로도 깨진 경험이 생길 수 있음
      콘텐츠 해시로 참조되는 정적 콘텐츠의 이전 버전을 계속 서빙하는 것은 가능하지만, Clace에는 현재 구현되어 있지 않음
    • 이 방식은 프런트엔드 캐시와 CDN에도 도움이 됨
      핵심 요령은 HTML 변경보다 HTML이 아닌 변경을 먼저 업로드해서, 존재하기 전 파일이 참조되지 않게 하는 것임. 앱을 최대한 복잡하게 만들고 싶다면 업로드에 깊이 우선 탐색을 적용할 수 있음. 하지만 정신 건강을 중시한다면 문제를 완화해서 앱에서 자산 우선 업로드를 택하는 편이 낫다
  • 2011/2012년에 작은 게임 개발사에서 일할 때, 추천해서 100KB 미만 자산을 모두 sqlite3 DB로 옮겼고, “pak 파일”을 만든 뒤 그 파일들의 오프셋을 sqlite3 DB 안에 저장했음
    이 선택은 Richard Hipp의 사후 분석 발표에서, 돌이켜보면 BLOB을 inode처럼 다뤄서 데이터베이스 안 더 뒤쪽 오프셋으로 두고 BLOB은 파일에 덧붙이는 식이었으면 좋았겠다고 말한 데 영향을 받았음
    자산 로딩이 엄청나게 빨랐음. 모바일 게임이라 DB에 없는 자산은 아주 일부뿐이었음. 이후 사람들이 이 방식을 더 채택하는 걸 보는 것도 흥미로움
    또 하나 놓치기 쉬운 장점은 콘텐츠 옆에 거의 무한히 메타데이터를 붙일 수 있어서 “비슷한” 파일을 데이터베이스 질의로 찾을 수 있다는 점임
    DB에 메타데이터를 엄청 넣었고, 최종 pak 파일은 200MB, 데이터베이스는 20MB 정도였던 것 같음. 다시 말하지만 모바일 게임이었음
    클라이언트 쪽에서 가장 나빴던 일은 서버 쪽 복잡성 때문에 줄이지 못한 이중 내부 조인 하나였음. 서버 구현을 우리가 못 했던 게 답답했는데, 함께 일한 쪽이 소프트웨어 개발을 매우 못해서 백엔드 명세 전체를 알리지 않고 바꿔 빌드가 갑자기 깨지곤 했음
    게임 리플레이에도 별도 sqlite3 데이터베이스를 썼고, 경기 종료 후 전체 게임을 재생하면서 각 상대가 무엇을 했는지 볼 수 있었음. 자동화 테스트에도 아주 좋았음

  • lix 변경 제어 시스템에서도 파일 시스템과 git을 다루는 대신 파일을 SQLite에 넣는 방식으로 가게 됨. 이 글이 우리가 겪은 문제를 다룸: https://opral.substack.com/i/150054233/breaking-git-compatib...
    파일 잠금, 동시성 같은 문제는 SQLite가 해결해 줌
    SQLite를 쓰면 플랫폼별 파일 시스템 API 대신 SQL로 파일을 질의할 수 있음
    SQL 질의는 Kysely https://kysely.dev/로 ORM 없이도 타입 안전하게 쓸 수 있음

    • Kysely로 SQL 질의를 타입 안전하게 쓴다는 건 F# 타입 제공자에서 보던 방식보다 더 좋아 보임
    • SQLite를 파일 시스템 위의 추상화 계층으로 쓰는 걸 매우 지지함
      다만 SQLite 데이터베이스는 vacuum하지 않으면 줄어들지 않는다는 점을 조심해야 함. 기본적으로 데이터를 별도 파일로 복사하고 원본을 삭제하는 작업임
      애플리케이션 안에서 말이 되는 시점에 수동으로 해야 하는 작업이라, 바이너리 데이터를 쓰고 지우는 식으로 쓸 때는 디스크 사용량을 주의해야 함
    • 파일에 원하는 만큼 메타데이터를 붙일 수 있음
    • lix 프로젝트와 관련해 Fossil을 봤는지 궁금함. 작은 변경만으로 하려는 일을 할 수 있을 것처럼 보임
  • 흥미롭게도 내가 만든 정적 사이트 생성기 CMS는 여기 방식과 정확히 반대로 동작함
    웹사이트를 개발/업데이트하는 동안 모든 페이지와 글은 SQLite 데이터베이스의 엔트리이고, 편집 가능한 웹사이트 버전을 보여 주는 웹 인터페이스로 조작함
    그런 다음 웹사이트를 정적 페이지로 파일 시스템에 덤프해서 직접 배포하거나, zip으로 내려받아 완전 정적 호스팅 서비스를 포함한 다른 곳에 업로드해 배포함

    • 지금은 Zola를 쓰고 있지만, 여러 이유로 더 유연하게 조정하려고 직접 만들까 고민 중이고 이런 접근이 계속 떠오름. 실제로 써 보니 어떤지 궁금함
  • SQLite의 “Appropriate Uses For SQLite” https://www.sqlite.org/whentouse.html에 따르면, SQLite가 처리할 수 있는 웹 트래픽은 사이트가 데이터베이스를 얼마나 무겁게 쓰는지에 달려 있음
    일반적으로 하루 100K 히트 미만 사이트는 SQLite로 잘 동작해야 함. 100K/일은 보수적 추정치이지 엄격한 상한은 아님. SQLite는 그 10배 트래픽도 처리한 사례가 있음
    SQLite 웹사이트(https://www.sqlite.org/)도 당연히 SQLite를 쓰며, 2015년 기준 하루 약 400K~500K HTTP 요청을 처리했고 그중 15~20%는 데이터베이스를 건드리는 동적 페이지였음. 동적 콘텐츠는 웹페이지당 약 200개 SQL 문을 사용함
    이 구성은 물리 서버를 다른 23개 VM과 공유하는 단일 VM에서 돌면서도 대부분의 시간에 로드 평균을 0.1 아래로 유지함. 참고: https://news.ycombinator.com/item?id=33975635

    • 100K/일은 초당 2개도 안 됨. SQLite 페이지의 그 수치는 업데이트가 필요해 보임
      정적 파일 서빙처럼 읽기 위주 작업부하라면 SQLite는 훨씬 더 많이 처리할 수 있음. 콘텐츠 캐싱 헤더가 설정되어 있으면 브라우저가 콘텐츠를 캐시하므로 서버 요청은 새 클라이언트에만 필요해짐
      대부분의 용례에서 SQLite가 병목이 될 것 같지는 않음
  • 2017년 “35% Faster Than The Filesystem” 페이지만 근거로 SQLite로 정적 콘텐츠를 서빙하겠다는 아이디어는 좋게 말해도 설익어 보임
    Nginx 같은 현대적 웹 서버는 정적 파일 처리에 최적 전략을 사용함. sendfile에서 시작해 io_uring과 splice 작업까지 이어지고, epoll, kqueue, eventport 중 필요에 맞는 기반 위의 잘 설계된 스레드 풀 안에서 동작함
    반면 SQLite가 기본적으로 제공할 수 있는 최선은 메모리 매핑 I/O 지원(https://www.sqlite.org/mmap.html) 정도임
    이 방식은 로컬 호스팅 웹앱 같은 단일 클라이언트 서비스에는 잘 맞을 수 있음(https://github.com/electron/asar도 참고). 하지만 큰 웹사이트에서는 다른 댓글들처럼 존재하지 않는 문제를 풀려고 하는 셈임

  • 고성능 과학 계산을 많이 하는데, 특히 병렬로 데이터에 접근할 때 가장 유연하고 빠른 방식이 램디스크 위 읽기 전용 SQLite 데이터베이스인 경우가 많았음
    굉장히 해키하게 느껴지지만, 지금까지 찾은 어떤 방식보다 설정이 쉽고 빠름

    • 솔직히 해킹처럼 느껴지지는 않음. 빠르고 효율적인 병렬 읽기는 SQLite가 명시적으로 잘 하도록 설계된 부분임
      천문학 쪽 친구가 과학 분야의 많은 사람들이 데이터베이스에 익숙해져야 한다고 본 적이 있음. 그렇지 않으면 결국 자신도 모르게 엄청난 노력을 들여 형편없는 자체 데이터베이스를 만들게 된다는 것임
    • 결과를 글로 정리해 주면 좋겠음
    • HDF5를 쓰는 것보다도 성능이 좋은지 궁금함
  • 왜 이 접근이 더 흔하지 않냐면, 파일 시스템은 파일 처리에 뛰어나기 때문
    원자적 업데이트가 필요하면 새 디렉터리에 체크아웃하고 심볼릭 링크를 바꾸면 됨
    데이터베이스를 파일 시스템처럼 쓰는 여러 버전을 봤는데, 좋은 면도 있지만 일이 터졌을 때 악몽 같은 면도 있음

    • 딱 같은 생각을 했음. 그냥 v2/index.html을 만드는 방식은 어디 갔나 싶음
      그러면 btrfs 같은 걸 써서 파일 시스템 계층에서 중복 제거도 할 수 있음
  • 앱 업데이트 때 파일이 많이 바뀔 수 있으니 데이터베이스를 쓰면 모든 변경을 트랜잭션으로 원자적으로 처리할 수 있고, 버전 변경 중 깨진 웹페이지 서빙을 막는다는 주장에는 문제가 있음
    SQLite 파일은 직렬화 가능 격리를 달성하려고 쓰기 중 읽기에 대해 잠긴다는 점 때문임. 그러면 데이터베이스 작업은 오프라인 파일에서 하고, 프로덕션의 기존 파일과 새 파일을 교체하는 편이 낫다는 결론이 나옴
    이는 결국 tar 파일을 쓰거나, 새 콘텐츠로 교체할 별도 디렉터리를 쓰라는 뜻에 가까움
    정적 파일은 정적으로 서빙하는 게 훨씬 쉬움. 실시간 SQLite 연결을 관리하고 이상한 “업데이트 동시성” 마법을 달성하려는 프로그램에서 서빙할 필요가 없음. 이 문제는 전혀 어렵지 않게 풀 수 있음
    CMS를 SQLite 데이터베이스로 관리하는 것은 괜찮지만, 콘텐츠가 정적이고 실시간으로 서빙한다면 정적 파일을 쓰는 편이 낫다