아키텍처 — FoundationDB ON 문서

ko생성일: 2025. 4. 29.

FoundationDB의 유연하고 확장 가능한 아키텍처 및 트랜잭션 처리 방식을 상세히 설명합니다. 데이터 저장, 처리 및 트랜잭션 시스템 복구 과정을 다룹니다.

아키텍처 — FoundationDB ON 문서

FoundationDB는 아키텍처를 유연하며 운영이 쉽도록 만듭니다. 애플리케이션은 데이터를 FoundationDB에 직접 전송할 수도 있고, 새로운 데이터 모델을 제공하거나 기존 시스템과의 호환성을 갖추거나 전체 프레임워크 역할을 하는 레이어라는 사용자 작성 모듈을 통해서도 전송할 수 있습니다. 두 경우 모두, 모든 데이터는 정렬된 트랜잭션 키-값 API로 단일 장소에 저장됩니다.

다음 다이어그램은 논리적 아키텍처를 자세히 보여줍니다.

그림 1: image0

FoundationDB 상세 아키텍처

FoundationDB 아키텍처는 역할이 구분된 비결합형 디자인을 채택합니다. 여러 프로세스가 각기 다른 역할(예: Coordinator, Storage Server, Master)을 담당합니다. 클러스터는 다양한 역할을 별도의 프로세스로 할당하려고 하지만, 클러스터 모집 목표에 따라 여러 비상태적 역할이 단일 프로세스에 함께 할당될 수도 있습니다. 데이터베이스 확장은 역할별 프로세스를 수평적으로 늘리는 방식으로 이루어집니다.

코디네이터(Coordinators)

모든 클라이언트와 서버는 클러스터 파일을 이용해 FoundationDB 클러스터에 접속하는데, 이 파일에는 코디네이터의 IP:PORT가 포함되어 있습니다. 클라이언트와 서버 모두 코디네이터를 통해 클러스터 컨트롤러에 연결합니다. 서버는 클러스터 컨트롤러가 없다면 직접 컨트롤러가 되려고 시도하며, 컨트롤러가 선출되면 등록합니다. 클라이언트는 클러스터 컨트롤러를 활용해 최신 GRV 프록시와 커밋 프록시 목록을 유지합니다.

클러스터 컨트롤러(Cluster Controller)

클러스터 컨트롤러는 다수의 코디네이터 투표로 선출되는 싱글턴입니다. 이 컨트롤러는 클러스터 내 모든 프로세스의 진입점이며, 프로세스 장애 감지, 역할 할당, 시스템 정보 전달 등을 담당합니다.

마스터(Master)

마스터는 쓰기 서브시스템의 세대를 이전에서 다음으로 전환하는 역할을 조율합니다. 쓰기 서브시스템에는 마스터, GRV 프록시, 커밋 프록시, 리졸버, 트랜잭션 로그가 포함됩니다. 이 세 역할은 하나의 단위로 취급되어, 그 중 하나라도 실패하면 세 가지 역할 모두 대체를 모집합니다. 마스터는 커밋 프록시에 변이 배치에 대한 커밋 버전을 제공합니다.

과거에는 Ratekeeper와 Data Distributor가 마스터와 동일 프로세스에 배치되었으나, 6.2버전 이후 둘 다 클러스터 내 싱글턴이 되었으며 수명도 마스터와 분리되었습니다.

그림 2: image1

GRV 프록시(GRV Proxies)

GRV 프록시는 읽기 버전을 제공하고 Ratekeeper와 연동해 읽기 버전 공급률을 제어합니다. 읽기 버전을 제공하기 위해 모든 마스터에 현재 시점에서 가장 큰 커밋 버전을 요청하고, 동시에 트랜잭션 로그가 중지되지 않았는지도 확인합니다. Ratekeeper는 GRV 프록시의 읽기 버전 공급 속도를 인위적으로 느리게 할 수 있습니다.

커밋 프록시(Commit Proxies)

커밋 프록시는 트랜잭션 커밋, 커밋 버전을 마스터에 보고, 각 키 범위에 대한 스토리지 서버 추적을 담당합니다.

커밋은 다음과 같이 처리됩니다:

  • 마스터로부터 커밋 버전을 수신합니다.
  • 리졸버를 사용해 트랜잭션이 이전 커밋 트랜잭션과 충돌하는지 판단합니다.
  • 트랜잭션 로그에 데이터를 기록하여 트랜잭션을 영속화합니다.

\xff 바이트로 시작하는 키 공간은 시스템 메타데이터 용도로 예약되어 있습니다. 이 키 공간에 커밋된 모든 변이는 리졸버를 거쳐 모든 커밋 프록시에 분배됩니다. 이 메타데이터에는 각 키 범위와 해당 범위의 데이터를 갖고 있는 스토리지 서버 매핑이 포함되어 있습니다. 커밋 프록시는 해당 정보를 클라이언트에 요청 시 제공합니다. 클라이언트는 이 매핑을 캐시하며, 만약 잘못된 서버에 키를 요청하면 캐시를 비우고 커밋 프록시로부터 최신 서버 목록을 받아옵니다.

트랜잭션 로그(Transaction Logs)

트랜잭션 로그는 변이를 디스크에 영속시켜 빠른 커밋 지연 시간을 보장합니다. 로그는 커밋 프록시로부터 커밋을 버전 순으로 받아, 데이터가 디스크의 append-only 변이 로그에 기록되고 fsync될 때만 커밋 프록시에 응답합니다. 디스크에 쓰이기 전에, 데이터를 해당 변이에 책임 있는 스토리지 서버에도 동시에 전달합니다. 스토리지 서버가 변이를 영속화하면, 로그에서 해당 변이를 삭제(pop)합니다. 일반적으로 초기 커밋 후 약 6초 이내에 발생합니다. 로그 디스크에서의 읽기는 프로세스가 재부팅될 때만 일어납니다. 만약 스토리지 서버가 실패하면, 해당 서버로 예정된 변이는 로그에 누적됩니다. 데이터 배포가 누락된 데이터를 다른 스토리지 서버로 이동시키면, 실패 서버를 위한 로그 데이터는 버려집니다.

리졸버(Resolvers)

리졸버는 트랜잭션 간의 충돌 여부를 판단합니다. 트랜잭션이 읽은 키가 해당 트랜잭션의 읽기 버전과 커밋 버전 사이에 쓰기가 있을 경우 충돌로 간주합니다. 리졸버는 최근 5초간 커밋된 쓰기 내역을 메모리에 보관하며, 새로운 트랜잭션의 읽기와 이 커밋 내역을 비교하여 판단합니다.

스토리지 서버(Storage Servers)

클러스터 내 대부분의 프로세스는 스토리지 서버입니다. 각 서버는 키 범위를 할당 받아 해당 범위의 모든 데이터를 저장합니다. 최근 5초간 변이는 메모리에 보관하고, 5초 전 시점 기준 데이터는 디스크에 저장합니다. 클라이언트는 최근 5초 이내 버전에서만 읽을 수 있으며, 그렇지 않으면 transaction_too_old 오류가 발생합니다. SSD 스토리지 엔진은 SQLite 기반의 B-트리로 데이터를 저장합니다. 메모리 스토리지 엔진은 메모리에 append-only 로그 방식으로 저장하며, 디스크에서 읽는 것은 리부팅 시뿐입니다. 곧 출시될 FoundationDB 7.0에서는 새로 개발된 Redwood 엔진으로 B-트리 스토리지 엔진이 대체됩니다.

데이터 디스트리뷰터(Data Distributor)

데이터 디스트리뷰터는 스토리지 서버의 수명 관리, 데이터 범위별 담당 서버 결정, 데이터의 균등 분산 역할을 수행합니다. 싱글턴으로 클러스터 컨트롤러에 의해 모집 및 모니터링됩니다. 자세한 내용은 내부 문서를 참고하세요.

Ratekeeper

Ratekeeper는 시스템 부하를 모니터링하고 클러스터가 포화상태에 가까워지면 GRV 프록시의 읽기 버전 제공 속도를 낮춤으로써 클라이언트 트랜잭션 속도를 늦춥니다. 클러스터 내 싱글턴으로 클러스터 컨트롤러가 모집, 관리합니다.

클라이언트(Clients)

클라이언트는 특정 언어 바인딩(즉, 클라이언트 라이브러리)과 연동해 FoundationDB 클러스터와 통신합니다. 언어 바인딩은 여러 C 라이브러리 버전 동시 로드를 지원하므로 구버전 클러스터와도 연결이 가능합니다. 현재 C, Go, Python, Java, Ruby 바인딩이 공식 지원됩니다.

트랜잭션 처리

FoundationDB의 데이터베이스 트랜잭션은 클라이언트가 GRV 프록시 중 하나에 최신 읽기 버전을 요청하는 것으로 시작합니다. 이 읽기 버전은 해당 클라이언트가 알고 있는(심지어 FoundationDB 클러스터 외부 경로로 얻었을 수도 있는) 어떤 커밋 버전보다 항상 큽니다. 이는 클라이언트가 이전 커밋 결과를 반드시 볼 수 있게 하기 위함입니다.

이후, 클라이언트는 여러 번 스토리지 서버에 읽기 요청을 보내고 해당 읽기 버전 시점의 값을 받습니다. 클라이언트의 쓰기 작업은 클러스터와 접촉 없이 로컬 메모리에 저장됩니다. 기본적으로 동일 트랜잭션 내에서 쓴 키를 읽을 때는 새로 쓴 값을 반환합니다.

커밋 시점에는 클라이언트가 트랜잭션 데이터(모든 읽기/쓰기 내역)를 커밋 프록시에 전송하고, 커밋 완료 또는 중단 응답을 대기합니다. 트랜잭션이 다른 트랜잭션과 충돌해 커밋이 불가할 경우, 클라이언트는 처음부터 트랜잭션을 다시 시도할 수 있습니다. 커밋 완료 시, 커밋 프록시는 커밋 버전을 클라이언트와 마스터 모두에 반환해 GRV 프록시가 최신 커밋 버전에 접근할 수 있도록 합니다. 이 커밋 버전은 읽기 버전보다 크며, 마스터가 결정합니다.

FoundationDB 아키텍처는 클라이언트의 읽기와 쓰기(트랜잭션 커밋) 확장을 분리합니다. 클라이언트는 샤딩된 스토리지 서버에 직접 읽기 요청을 보내므로, 읽기 확장은 스토리지 서버 수에 비례해 선형적으로 커집니다. 마찬가지로, 쓰기도 커밋 프록시, 리졸버, 로그 서버 프로세스를 추가하여 확장됩니다.

읽기 버전 결정

클라이언트가 GRV 프록시에 읽기 버전을 요청하면, GRV 프록시는 마스터에 최신 커밋 버전을 요청하고 복제 정책을 만족하는 트랜잭션 로그 세트가 활성 상태인지 확인합니다. 이후 최대 커밋 버전을 읽기 버전으로 클라이언트에 반환합니다.

그림 3: image2

GRV 프록시가 최신 커밋 버전에 대해 마스터에 문의하는 이유는, 마스터가 모든 커밋 프록시의 커밋 버전 중 가장 큰 값을 중앙에서 유지하기 때문입니다.

트랜잭션 로그가 복제 정책을 만족할 만큼 활성인지 확인하는 것은 GRV 프록시가 새로운 세대로 교체되지 않았음을 보장하기 위함입니다. GRV 프록시는 각 세대마다 모집되는 비상태 역할이므로, 복구 과정에서 이전 GRV 프록시가 아직 살아 있다면 이전 GRV 프록시 역시 읽기 버전을 발급할 수 있습니다. 이로 인해, 읽기 전용 트랜잭션은 오래된 결과를 볼 수 있습니다(읽기-쓰기 트랜잭션은 중단됨). 복제 정책을 만족하는 트랜잭션 로그가 모두 활성임을 확인하면, GRV 프록시는 복구가 없었음을 보장해 읽기 전용 트랜잭션이 최신 데이터를 읽게 합니다.

클라이언트가 단순히 마스터에 읽기 버전을 직접 요청할 수 없는 이유는 마스터의 역할은 확장되지 않아서 부담이 커지기 때문입니다. 실제로 읽기 버전 발급은 큰 비용이 들지 않지만, 마스터도 Ratekeeper로부터 트랜잭션 예산을 받아야 하며, 요청을 배치 처리하고 수천 개 클라이언트 네트워크 연결을 유지해야 합니다.

그림 4: image3

트랜잭션 커밋

클라이언트 트랜잭션 커밋 단계는 다음과 같습니다:

  1. 클라이언트가 커밋 프록시에 트랜잭션을 전송합니다.
  2. 커밋 프록시가 마스터에 커밋 버전을 요청합니다.
  3. 마스터가 이전까지의 모든 커밋 버전보다 더 큰 커밋 버전을 반환합니다.
  4. 커밋 프록시는 읽기/쓰기 충돌 범위와 커밋 버전을 리졸버(들)에 전달합니다.
  5. 리졸버가 커밋 버전을 기준으로 트랜잭션을 정렬해, 이전 트랜잭션과의 충돌 여부를 판정합니다.
    • 충돌 있다면, 커밋 프록시는 클라이언트에 not_committed 오류로 응답합니다.
    • 충돌 없다면, 변이와 커밋 버전을 트랜잭션 로그에 전송합니다.
  6. 변이가 로그에 영속화되면, 커밋 프록시는 성공을 사용자에게 응답합니다.

커밋 프록시는 리졸버별로 책임지는 키 범위만 전송하며, 리졸버 중 하나라도 충돌을 감지하면 트랜잭션이 커밋되지 않습니다. 한 리졸버만 충돌을 감지한 경우, 다른 리졸버는 여전히 해당 트랜잭션이 성공했다고 생각하므로, 미래에 이와 겹치는 쓰기 충돌 범위가 있는 트랜잭션을 실패(불필요하게)시킬 수 있습니다. 실제로는 잘 설계된 워크로드에서는 충돌 비율이 매우 적으므로, 이와 같은 증폭은 성능에 큰 영향이 없습니다. 또한 각 트랜잭션의 충돌 범위는 최대 5초간만 유지되어, 불필요한 충돌 기회도 제한됩니다.

그림 5: image4

그림 6: image5

백그라운드 작업

트랜잭션 처리 외에도 다음과 같은 다양한 백그라운드 작업이 수행됩니다:

  • Ratekeeper는 GRV 프록시, 커밋 프록시, 트랜잭션 로그, 스토리지 서버 등에서 통계를 수집해 클러스터 목표 트랜잭션 속도를 계산합니다.
  • 데이터 분배는 모든 스토리지 서버를 모니터링하여, 데이터가 고르게 분산되도록 로드밸런싱 작업을 실행합니다.
  • 스토리지 서버는 트랜잭션 로그에서 변이를 가져와 스토리지 엔진에 기록, 영구 디스크에 저장합니다.
  • 커밋 프록시는 클라이언트 트랜잭션이 없더라도 주기적으로 빈 커밋을 전송해 커밋 버전이 계속 증가하도록 합니다.

그림 7: image6

트랜잭션 시스템 복구

트랜잭션 시스템은 FoundationDB 클러스터에서 쓰기 파이프라인을 담당하며, 성능은 트랜잭션 커밋 지연에 매우 중요합니다. 일반적인 복구는 수백 밀리초 내에 이루어지지만, 경우에 따라(몇 초 단위로) 더 오래 걸릴 수도 있습니다. 트랜잭션 시스템에 장애가 발생하면, 클러스터를 새로운 구성(청정 상태)으로 되돌리는 복구 과정이 실행됩니다. 구체적으로, 마스터 프로세스는 GRV 프록시, 커밋 프록시, 리졸버, 트랜잭션 로그의 상태를 모니터링합니다. 이 중 하나라도 장애가 발생하면, 마스터 프로세스가 종료되며 클러스터 컨트롤러가 이 이벤트를 감지하고 새로운 마스터와 트랜잭션 시스템 인스턴스를 모집, 복구를 조율합니다. 이로써 트랜잭션 처리는 여러 epoch(세대)로 나뉘며, 각 epoch은 고유한 마스터 프로세스를 가집니다.

각 epoch마다 마스터는 여러 단계를 거쳐 복구를 시작합니다. 우선, 코디네이터에서 이전 트랜잭션 시스템 상태를 읽어 해당 상태를 잠근 뒤(동시 복구 방지), 이전 트랜잭션 시스템 상태(모든 로그 서버 정보 포함)를 복구하고 이 로그 서버들이 더 이상 트랜잭션을 받지 않도록 중단시킵니다. 이후 새로운 GRV 프록시, 커밋 프록시, 리졸버, 트랜잭션 로그 세트를 모집합니다. 새 시스템이 준비되면, 마스터는 현재 트랜잭션 시스템 정보를 조정된 상태에 기록하고 이후 트랜잭션 커밋을 받기 시작합니다. 자세한 내용은 이 문서를 참고하세요.

GRV 프록시, 커밋 프록시, 리졸버는 무상태이므로 복구에 별다른 추가 작업이 필요 없습니다. 반면, 트랜잭션 로그는 커밋된 트랜잭션 로그를 저장하기 때문에, 모든 이전 커밋 로그가 스토리지 서버에 안전하게 전달 및 복원 가능하도록 보장해야 합니다. 즉, 커밋 프록시가 커밋 응답을 반환한 트랜잭션은 최소 복제도(예: 서버 3대)만큼 로그 서버에 영구 저장됩니다.

마지막으로, 복구 시 시간은 90초 앞으로 _빠르게 전진_됩니다. 이는 진행 중인 클라이언트 트랜잭션을 transaction_too_old 오류로 중단시키며, 재시도 시 새 트랜잭션 시스템에서 정상적으로 커밋됩니다.

commit_result_unknown 오류: 만약 트랜잭션 커밋 도중 복구가 발생(예: 커밋 프록시가 트랜잭션 로그에 변이를 전달한 시점)하면, 클라이언트는 commit_result_unknown 응답을 받고 트랜잭션을 재시도하게 됩니다. 이 시나리오에서 FoundationDB는 최초, 또는 재시도 트랜잭션을 모두 커밋해도 시스템적으로 허용됩니다. 즉, commit_result_unknown은 해당 트랜잭션이 커밋되었을 수도 있고 아닐 수도 있음을 의미하므로, 트랜잭션은 반드시 멱등하게 설계되어야 합니다.

참고 자료