- Tines의
3B는 실행 후 폐기하는 Linux 샌드박스에 영속 파일시스템을 연결. Git 저장소와 SQLite, DuckDB 등을 다음 실행에서도 이어 쓰면서, 성공한 실행의 변경만 일괄 반영
- 실행마다
고정된 스냅샷을 읽고, 실패하면 파일 변경을 폐기. 다른 실행이나 UI에는 반쯤 작성된 파일이 보이지 않도록 구성
수 GB의 데이터도 복사 없이 분기해 운영 데이터에 영향을 주지 않고 실험 가능. 같은 스냅샷을 가리키는 포인터만 추가하고, 이후 변경된 부분만 따로 저장
- 여러 실행이 동시에 작업해도
서로 다른 파일의 변경은 재적용하고, 같은 파일의 변경은 충돌 오류로 처리. 조용히 다른 실행의 결과를 덮어쓰는 일을 방지
- gVisor 기반 로컬 입출력과 자체 Rust 스토리지 엔진을 결합.
로컬 디스크를 우선하고 S3는 복구와 오래된 데이터 보관에 활용해 지연 시간과 비용, 자체 호스팅 요구를 함께 맞춘 설계
실행은 폐기하고 데이터는 남기는 구조
-
Tines의
3B는 워크플로의 각 단계를 하나의 컴퓨터로 취급함
-
사람이나 에이전트가 작성한 코드를 새로운 Linux 샌드박스에서 실행하고, 프록시를 통해 자격 증명을 전달함
-
작업 후 샌드박스를 폐기해 남은 상태가 이후 보안 문제를 일으키지 않도록 함
-
고객은 이미
영속 상태가 필요한 작업을 수행하고 있음
-
저장소를 복제해 분석하고, Salesforce 리드를 SQLite 데이터베이스로 보관함
-
수신 웹훅을 DuckDB에 저장해 다른 워크플로에서 검색함
-
파일시스템은 샌드박스보다 오래 유지되며, 실행과 파일 변경을
전부 반영하거나 전부 버리는 단위로 묶음
-
에이전트와 초안 분기에서 작업해도 운영 분기의 데이터에는 영향을 주지 않음
-
다른 실행은 해당 단계가 성공적으로 끝나기 전까지 변경 사항을 볼 수 없음
-
구성은
샌드박스, Nest, Spongeblob의 세 계층으로 나뉨
-
샌드박스가 코드를 실행하고, 바로 바깥의 작은 gVisor gofer 확장인 Nest가 파일 작업을 처리함
-
Spongeblob은 Blobstore라고도 부르는 자체 스토리지 엔진으로, 모든 볼륨을 저장하고 S3가 있으면 복구와 로컬 데이터 축출을 위해 복사함
설계를 결정한 요구사항과 볼륨 인터페이스
실제 POSIX 파일시스템처럼 동작해야 함
는 디렉터리와 mmap
, fsync
, 이름 변경에 의존하며 별도 SDK를 채택하지 않음
-
영속 상태를
단계를 실행하는 머신에 묶을 수 없음
-
다음 단계가 며칠 뒤 다른 머신에서 실행될 수 있음
-
주요 실행 경로는
빠르고 로컬이어야 함
-
일반적인 128KB 쓰기의 내구성 확보 목표는 2~4ms였음
-
작은 쓰기가 많은 SQLite에서 매번 네트워크 왕복이 발생하는 S3 기반 네트워크 파일시스템은 이 목표를 맞추기 어려움
병렬 쓰기와 S3 없는 자체 호스팅을 지원해야 함
-
같은 볼륨에 쓰는 두 단계가 데이터를 손상시키거나 서로의 작업을 조용히 덮어써서는 안 됨
-
복구 시 일관성을 맞춰야 할 구성 요소를 줄이기 위해
내구성 로그 하나와 디스크당 소유자 하나를 목표로 삼음
-
별도 데이터베이스나 합의 시스템 없이 시작하고, 요구사항이 강제하는 부분에만 복잡성을 추가함
-
단계는 Dockerfile의
VOLUME
한 줄로 볼륨을 선언하며, /storage/<name>
디렉터리로 마운트함
- 읽기 전용/읽기 쓰기, 분기/단일 실행 범위, 동시 쓰기 공유/순차 사용을 선택할 수 있음
- 실행을 시작하면 종료할 때까지 하나의 볼륨 버전에 고정됨
Nest의 파일 열기와 로컬 입출력
gVisor가 시스템 호출을 가로채고 /storage
관련 작업을 Nest에 전달함
-
gVisor가 이미 파일 작업을 gofer로 전달하므로, FUSE 계층을 추가하는 대신 자체 gofer를 확장함
-
실제 파일 디스크립터를 전달할 수 있어 매번 FUSE 데몬을 거치지 않고 로컬 파일에 접근함
-
볼륨을 열 때 전체 내용을 내려받지 않고
디렉터리 탐색에 따라 지연 조회함
-
파일 열기는 LisaFS를 통해 Nest로 전달됨
-
Nest가 커밋된 파일을 호스트의 메모리 기반
tmpfs
임시 파일로 복사하고 파일 디스크립터를 샌드박스에 넘김
은 메모리 속도로 로컬 파일에 접근하고, 열기와 이름 변경, 목록 조회 같은 제어 작업만 샌드박스 경계를 넘음
-
현재는
파일을 열 때 전체를 복사한다는 비용이 있음
-
파일 데이터를 필요할 때 가져오는 방식도 시도했지만, 실제 프로그램이 같은 페이지를 반복해서 읽어 커널 페이지 캐시가 프록시보다 빨랐음
-
작은 파일은 미리 복사하고 큰 파일은 실제 접근하는 블록만 가져오는 혼합 방식을 실험 중임
-
Nest는 생성, 이름 변경, 삭제, 파일 내 쓰기 영역을 추적하며, 파일을 열 때 청크 지문을 만들어
**공유 **mmap
변경도 감지함
는 로컬 복사본만 플러시하며, 영속 변경의 내구성은 단계가 끝나고 커밋할 때 확보됨
- 충돌, 메모리 부족, 종료 코드 1로 실패하면 로컬 변경을 버리고 볼륨 버전은 유지함
- 실행 중 변경을 공개하는 전용 마운트는 필요해지면 추가할 수 있지만, 현재 기본 동작은 실패한 변경을 폐기하는 것임
성공한 실행마다 만드는 스냅샷
- 볼륨은
불변 스냅샷의 연속과 현재 스냅샷 포인터 하나로 구성됨
- 단계가 성공하면 Nest가 변경된 데이터를 업로드하고, Spongeblob이 현재 스냅샷 위에 새 스냅샷을 만든 뒤 포인터를 이동함
- Git 커밋처럼 전체 트리의 상태를 기록하지만, 성공한 단계마다 자동으로 생성됨
다중 버전 동시성 제어(MVCC) 방식으로 실행 중 읽는 버전을 유지함
- 스냅샷 42를 읽는 동안 다른 실행이 43을 공개해도 계속 42를 읽음
- 읽기와 쓰기가 서로를 기다리지 않음
쓰기 시 복사(COW) 로 변경된 트리 부분만 다시 기록함
- 큰 볼륨에서 파일 하나를 바꾸면 해당 리프, 상위 디렉터리 노드, 루트 등 소수 노드만 새로 쓰고 나머지는 이전 스냅샷과 공유함
동시 쓰기 충돌과 복사 없는 분기
-
공개 작업은 포인터에 대한
비교 후 교환(CAS) 으로 처리함
-
포인터가 여전히 42일 때만 43으로 이동하는 식으로 다른 실행의 선행 커밋을 감지함
-
다른 실행이 먼저 커밋했다면 두 실행의 변경 사항을 비교함
-
서로
다른 파일을 바꿨으면 새 스냅샷 위에 변경을 재적용하고, 같은 파일을 바꿨으면 경로를 명시한 충돌 오류로 실패함
-
구체적인 충돌 처리 의미론은 아직 다듬는 중임
포크는 같은 스냅샷을 가리키는 포인터 하나를 추가하는 작업임
-
수 GB 볼륨도 빈 볼륨과 같은 속도로 포크할 수 있고, 이후 두 포인터는 독립적으로 이동함
-
현재 새 초안 분기는 비어 있는 상태나 운영 분기에서 시작할 수 있음
-
곧 해당 분기가 파생된 분기에서도 포크할 수 있도록 할 예정임
-
분기 간
파일 병합은 지원하지 않음
-
각 분기가 독립적인 복사본을 사용하며, 파일시스템용 3방향 병합 로직은 만들지 않기로 함
-
UI와 에이전트는 분기 포인터의 마지막 커밋을 읽으므로 워크플로 실행 중에도 반쯤 쓰인 파일을 보지 않음
Spongeblob의 단일 디스크 소유 구조
Rust로 작성한 Spongeblob은 내장형 데이터베이스와 서비스로 나뉨
-
내장형 엔진은 불변 객체, 버전 트리, 포인터만 다룸
-
서비스는 이를 파일시스템 볼륨과 단계 출력으로 해석하고 쓰기 충돌 같은 제품 의미론을 구현함
-
디스크는 한 번에
프로세스 하나만 소유하므로 여러 프로세스의 쓰기 조정이 필요하지 않음
-
대신 배포할 때 기존 프로세스가 물러나고 새 프로세스가 같은 디스크를 다시 여는 짧은 인계가 필요함
-
하나의 거대한 인스턴스 대신
샤딩된 3B 스택마다 별도 엔진과 디스크를 운영함
-
바쁜 워크로드 하나가 다른 스택의 읽기와 쓰기를 늦추지 않도록 함
-
현재 프로덕션에서 일반적인 읽기는 약 1ms, 쓰기는 2ms 미만임
pack 로그와 그룹 커밋
- Nest가 변경을 공개하면 Spongeblob은 디스크의 큰 파일인
pack 끝에 데이터를 추가하고 fsync
를 호출함
- 비슷한 시점에 도착한 여러 단계의 쓰기는
그룹 커밋으로 같은 fsync
를 공유함
로 저장돼 pack 자체가 로그 역할을 함
- 별도 저널 파일은 없음
- 인덱스 갱신, 오래된 데이터 정리, S3 업로드는 비동기로 처리함
- 데이터는 대부분이 불필요해진 pack에서 살아 있는 내용을 옮겨 정리할 때까지 처음 기록된 위치에 남음
LSM 인덱스와 자체 엔진의 절충
LSM 트리 인덱스가 파일의 최신 버전과 데이터가 들어 있는 pack 위치를 관리함
-
새 항목은 메모리의 정렬 테이블에 들어가고, 가득 찬 테이블은 불변 정렬 파일로 기록됨
-
백그라운드 작업이 이 파일들을 점진적으로 병합함
-
RocksDB와
WiscKey의 설계를 참고해 큰 데이터는 트리 밖의 pack에 두고, 인덱스에는 작은 참조 항목만 저장함
-
데이터와 인덱스의 분리는
읽기 분산과 정리 쓰기 비용을 남김
-
파일 읽기는 인덱스 조회와 해당 블록이 있는 pack의 범위 읽기로 구성됨
-
변경 블록만 새 pack에 함께 저장하므로 반복 수정된 파일은 여러 pack에 흩어져 열 때 더 많은 읽기가 필요함
-
pack의 절반 이상이 불필요해진 뒤에만 남은 데이터를 옮겨, 파일 데이터의 생애 전체 기록량을 약 두 배 이내로 제한함
-
RocksDB도 사용할 수 있었겠지만,
자체 데이터에 맞는 형식과 자원 관리를 위해 작은 엔진을 직접 구현함
-
데이터를 제자리에서 읽는 형식과, 저널/인덱스/백그라운드 작업의 자원을 한 프로세스가 함께 관리하는 구조를 원했음
-
RocksDB의 컴팩션 필터와 공유 쓰기 버퍼 예산에서도 많은 아이디어를 얻음
-
어떤 키-값 저장소를 사용하더라도 스냅샷 트리, 포크 기반 요소, 임대(lease), 충돌 검사, 이미지 청크 저장은 별도로 구현해야 했음
FoundationDB 같은 별도 메타데이터 클러스터는 도입하지 않음
- 파일 조회마다 네트워크 왕복이 추가되고, 각 스택과 자체 호스팅 환경에 pack과 일관성을 맞춰야 할 운영 시스템이 하나 더 생기기 때문임
로컬의 활성 데이터와 S3의 비활성 데이터
-
워크플로는 최근 몇 차례 실행에서 쓴 파일을 다시 여는 경우가 많아,
로컬 디스크를 활성 데이터의 내구성 계층, S3를 비활성 데이터 계층으로 사용함
-
pack은 가득 차거나
30초가 지나면 봉인돼 더 이상 바뀌지 않음
-
더 짧은 복구 시점을 원하면 시간을 설정할 수 있음
-
S3가 설정돼 있으면 봉인된 pack을 비동기로 업로드하고, 저장소 재구축에 필요한 복구 체크포인트에 기록함
-
S3에 안전하게 저장된 pack만
로컬 축출 대상이 됨
-
디스크 사용량이 상한 임계치를 넘으면 한동안 읽히지 않은 pack의 로컬 복사본을 삭제함
-
다시 읽을 때 S3에서 가져와 검증하고 제공하며, 사용되는 동안 로컬에 유지함
-
파일 덮어쓰기와 스냅샷 만료로 불필요해진 데이터를
pack 단위로 정리함
-
대부분이 불필요한 pack은 살아 있는 데이터만 새 pack으로 옮김
-
완전히 불필요한 pack은 어떤 체크포인트도 참조하지 않을 때 삭제함
-
S3 없는 자체 호스팅에서는 모든 pack을 디스크에 유지하며, 정리만으로 부족하면 디스크를 확장함
S3에 직접 쓰지 않는 이유
자체 호스팅, 지연 시간, 비용을 함께 고려해 로컬 디스크 우선 구조를 선택함
-
Nest가 페이지 단위 입출력을 흡수하므로 Spongeblob은 주로 파일을 열 때의 읽기와 실행 종료 시 공개 작업을 처리함
-
로컬 디스크에서는 1~2ms 수준이지만, AWS가 안내하는 작은 S3 요청의 지연은 수십 ms로 쓰기 목표를 넘음
-
모든 쓰기를 S3로 보내면 배치하더라도 현재 S3 요청 비용의 몇 배가 들고 트래픽에 따라 증가할 것으로 계산함
-
이 선택이 설계상 고정된 것은 아니며,
로그 끝부분만 S3 Express One Zone으로 이동하는 방안을 검토 중임
내구성, 일관성, 가용성의 보장 범위
내구성은 쓰기가 속한 그룹의 디스크 fsync
가 끝나면 확보됨
- 클라우드에서는 가용 영역 내부에서 복제되는 EBS를 사용하므로 프로세스 충돌, 재시작, 배포로 승인된 쓰기가 사라지지 않음
- EBS 볼륨 자체가 사라지면 마지막 S3 복구 체크포인트로 재구축하며, 체크포인트는 최신 쓰기에서 약 1분 이내로 유지함
- S3 없는 자체 호스팅의 내구성은 해당 디스크가 제공하는 수준에 따름
일관성은 실행별 고정 스냅샷과 순서화된 공개 작업으로 보장함
- 디스크 소유자 하나와 포인터 CAS 덕분에 다른 단계, UI, 에이전트는 변경 전체 또는 변경 전 상태만 봄
가용성에는 소유권 인계에 따른 제약이 있음
- 배포 시 기존 프로세스가 작업을 마무리하고 새 프로세스에 디스크를 넘기는 데 보통 1~2초가 걸림
- 앞단 라우터가 소유자를 확인해 새 프로세스가 준비되면 트래픽을 전환함
- 인계 중 요청에는 1초 뒤 재시도하라는 응답을 빠르게 반환함
로그 끝부분의 위치와 다음 개선 과제
-
쓰기의 성립 여부는
저널이 결정하며 인덱스, 스냅샷, 포크는 여기서 파생되므로, 나머지 구조를 다시 설계하지 않고 로그 끝부분의 저장 위치를 바꿀 수 있음
-
로그 끝부분을
S3 Express One Zone에 동기 기록하면 커밋당 약 7~10ms가 들 것으로 예상함
-
현재 쓰기 목표를 넘지만 S3 Standard보다는 차이가 작음
-
볼륨 자체 유실에 따른 최근 쓰기 손실 구간을 없애고, 새 소유자가 이전 디스크에 의존하지 않게 할 수 있음
-
단일 가용 영역 서비스이므로 EBS와 마찬가지로 가용 영역 전체 유실은 해결하지 못함
-
읽기, pack, 인덱스, 스냅샷은 로컬에 남고 로그 끝부분만 이동하는 방안임
-
대안인
여러 머신의 쿼럼 기반 로그는 빠른 커밋을 유지할 수 있지만 자체 합의 시스템을 운영해야 함
-
현재 구조는
불변 스냅샷, CAS 포인터, 포인터 하나짜리 포크, 콘텐츠 기반 데이터 식별을 단일 내구성 로그 위에 결합함
-
초안과 운영 데이터 분리, 실행 단위 원자적 커밋, 동시 쓰기 덮어쓰기 방지, 실행 중 파일 조회가 이 기반에서 구현됨
-
SQLite, JSONL, DuckDB 등 볼륨 전체를 데이터 복사 없이 분기할 수 있음
-
같은 구조를
샌드박스 기본 이미지와 루트 파일시스템에도 활용할 수 있음
-
Spongeblob으로 뒷받침하면 빠른 부팅과 이미지 간 청크 공유가 가능함
-
다음 과제는
큰 파일 열기 비용과 인덱스 병합 비용을 줄이는 것임
-
어떤 파일을 열 때 복사하고 어떤 파일을 요청 시 가져올지 결정할 예정임
-
가장 큰 백그라운드 비용인 인덱스 병합이 같은 변경량에 대해 더 적은 작업을 하도록 개선하려 함
-
v2에서는 S3 Express와 쿼럼 로그 중 하나를 선택해 디스크 유실과 소유권 인계의 제약을 줄이려 함
댓글과 토론