대규모 읽기 및 쓰기 이해

이 문서를 참고하여 안정성 있는 고성능 애플리케이션 설계에 대해 정보에 입각한 결정을 내리세요. 이 문서에는 고급 Firestore 주제가 포함되어 있습니다. Firestore를 처음 사용하는 경우에는 빠른 시작 가이드를 참고하세요.

데이터베이스 크기와 트래픽 증가에 따라 애플리케이션이 계속 원활하게 작동하도록 하려면 Firestore 백엔드에서의 읽기 및 쓰기 방식을 이해하는 것이 도움이 됩니다. 또한 읽기 및 쓰기와 스토리지 레이어 간의 상호작용, 성능에 영향을 줄 수 있는 기본 제약조건을 이해해야 합니다.

애플리케이션을 설계하기 전에 다음 섹션에서 권장사항을 참조하세요.

고급 구성요소 이해

다음 다이어그램은 Firestore API 요청과 관련된 고급 구성요소를 보여줍니다.

고급 구성요소

SDK, 클라이언트 라이브러리, 드라이버

Firestore는 다양한 플랫폼의 SDK, 클라이언트 라이브러리, 드라이버를 지원합니다.

Google 프런트엔드(GFE)

이는 모든 Google Cloud 서비스에 공통적으로 적용되는 인프라 서비스입니다. GFE는 들어오는 요청을 수락하여 관련 Google 서비스(이 컨텍스트에서는 Firestore 서비스)로 전달합니다.

Firestore 서비스

Firestore 서비스는 인증, 승인, 할당량 확인을 포함하는 API 요청에 대해 검사를 수행하고 트랜잭션도 관리합니다. 이 Firestore 서비스에는 데이터 읽기 및 쓰기를 위해 스토리지 레이어와 상호작용하는 스토리지 클라이언트가 포함되어 있습니다.

Firestore 스토리지 레이어

Firestore 스토리지 레이어는 데이터 및 메타데이터와 Firestore에서 제공하는 관련 데이터베이스 기능을 모두 저장합니다. 다음 섹션에서는 Firestore 스토리지 레이어의 데이터 구성 방식과 시스템 확장 방식을 설명합니다. 데이터 구성 방식을 학습하면 확장 가능한 데이터 모델을 설계하고 Firestore의 권장사항을 더 잘 이해하는 데 도움이 됩니다.

키 범위 및 분할

Firestore는 NoSQL 문서 중심의 데이터베이스입니다. 컬렉션으로 정리되는 문서에 데이터를 저장합니다. 컬렉션 이름과 문서 ID는 문서의 고유 키를 형성합니다. 동일한 컬렉션의 문서는 키스페이스에 함께 저장됩니다. 해당 키스페이스 내에서 문서 ID는 해싱됩니다. 키 범위는 스토리지에서 연속된 키 범위를 나타냅니다.

Firestore는 여러 스토리지 서버에 걸쳐 컬렉션 내 데이터를 자동으로 파티셔닝합니다. 이러한 파티션을 분할이라고 합니다.

문서는 사전순으로 정렬되고 문서 데이터와 동일한 종류의 분할 및 배치에 참여하는 색인 항목을 생성할 수 있습니다.

동기식 복제

모든 쓰기는 Paxos를 사용하여 대부분의 복제본에 동기식으로 복제됩니다. 분할당 하나의 복제본이 리더로 간주되어 복제 프로세스를 조정합니다. 리더에 장애가 발생하면 새 리더가 선출됩니다. 복제본은 잠재적인 영역 장애에 대비하기 위해 서로 다른 영역에 있습니다. 결과적으로 확장 가능하며 가용성이 높은 시스템이 구축되어 워크로드 무게와 상관없이 대규모 읽기 및 쓰기 지연 시간이 모두 짧아집니다.

단일 리전과 멀티 리전 비교

데이터베이스를 만들 때 단일 리전 위치 또는 멀티 리전 위치를 선택해야 합니다.

단일 리전 위치는 us-west1과 같은 특정 지리적 위치입니다. 앞서 설명한 대로 Firestore 데이터베이스의 데이터 분할에는 선택한 리전 내의 여러 영역에 복제본이 있습니다.

멀티 리전 위치는 Firestore가 데이터베이스의 복제본을 저장하는 정의된 리전 집합으로 구성됩니다. Firestore의 멀티 리전 배포에서 두 리전에는 데이터베이스에 전체 데이터의 전체 복제본이 있습니다. 세 번째 리전에는 전체 데이터 세트를 관리하지는 않지만 복제에는 참여하는 감시 복제본이 있습니다. Firestore는 여러 리전 간에 데이터를 복제하므로 리전 전체가 손실되더라도 데이터를 읽고 쓸 수 있습니다.

리전의 위치에 대한 자세한 내용은 Firestore 위치를 참조하세요.

단일 리전과 멀티 리전 비교

쓰기 수명 이해

드라이버는 단일 문서를 생성, 업데이트 또는 삭제하여 데이터를 쓸 수 있습니다. 단일 문서에 쓰기 작업을 수행하려면 스토리지 레이어에서 문서 및 관련 색인 항목을 모두 원자적으로 업데이트해야 합니다. Firestore는 하나 이상의 문서에 대해 여러 읽기 및 쓰기로 구성된 원자적 작업도 지원합니다.

Firestore는 모든 종류의 쓰기에 관계형 데이터베이스의 ACID 속성 (원자성, 일관성, 격리성, 내구성)을 제공합니다. Firestore는 직렬 가능성도 제공합니다. 즉, 모든 트랜잭션이 순차적으로 실행되는 것처럼 나타납니다.

쓰기 트랜잭션의 고급 단계

드라이버가 앞서 언급한 방법 중 하나를 사용하여 쓰기를 실행하거나 트랜잭션을 커밋할 때 내부적으로 이 작업은 스토리지 레이어에서 데이터베이스 읽기-쓰기 트랜잭션으로 실행됩니다. 트랜잭션을 통해 Firestore는 앞에서 언급한 ACID 속성을 제공할 수 있습니다.

트랜잭션의 첫 번째 단계로 Firestore는 기존 문서를 읽고 문서의 데이터에 적용할 변형을 결정합니다.

여기에는 관련 색인 업데이트도 포함됩니다.

  • 문서에 추가되는 색인 필드는 색인에 해당 항목을 삽입해야 합니다.
  • 문서에서 삭제되는 색인이 생성된 필드는 색인에서 해당 항목을 삭제해야 합니다.
  • 문서에서 수정 중인 색인이 생성된 필드는 색인에서 삭제(이전 값의 경우) 및 삽입 (새 값의 경우)이 모두 필요합니다.

앞서 언급한 변형을 계산하기 위해 Firestore에서 프로젝트의 색인 생성 구성을 읽습니다. 색인 생성 구성은 프로젝트의 색인에 대한 정보를 저장합니다.

변형이 계산되면 Firestore가 트랜잭션 내에서 이를 수집한 후 커밋합니다.

스토리지 레이어의 쓰기 트랜잭션 이해

앞서 설명한 대로 Firestore의 쓰기에는 스토리지 레이어의 읽기-쓰기 트랜잭션이 포함됩니다. 데이터 레이아웃에 따라 쓰기에 하나 이상의 분할이 포함될 수 있습니다.

다음 다이어그램에서 Firestore 데이터베이스에는 단일 영역에 있는 3개의 서로 다른 스토리지 서버에서 호스팅되는 8개의 분할(1~8로 표시)이 있으며 분할은 각각 3개 이상의 서로 다른 영역에 복제됩니다. 각 분할에는 Paxos 리더가 하나씩 있고 Paxos 리더는 분할마다 다른 영역에 있을 수 있습니다.

Firestore 데이터베이스 분할

다음과 같이 Restaurants 컬렉션이 있는 Firestore 데이터베이스를 살펴보겠습니다.

레스토랑 컬렉션

드라이버는 priceCategory 필드 값을 업데이트하여 Restaurant 컬렉션의 문서에 다음과 같은 변경사항을 요청합니다.

컬렉션의 문서에 대한 변경사항

다음 고급 단계에서는 쓰기의 일부로 어떤 일이 일어나는지를 설명합니다.

  1. 읽기-쓰기 트랜잭션을 만듭니다.
  2. Restaurants 컬렉션의 restaurant1 문서를 읽습니다.
  3. 문서의 색인을 읽습니다.
  4. 데이터에 적용할 변형을 계산합니다. 이 경우 다섯 가지 변형이 있습니다.
    • M1: priceCategory 필드의 변경된 값을 반영하도록 restaurant1 행을 업데이트합니다.
    • M2 및 M3: priceCategory의 이전 색인 항목을 삭제합니다.
    • M4 및 M5: priceCategory의 새 색인 항목을 추가합니다.
  5. 이러한 변형을 커밋합니다.

Firestore 서비스의 스토리지 클라이언트는 변경할 행의 키를 소유한 분할을 조회합니다. 분할 3이 M1을, 분할 6이 M2~M5를 제공하는 경우를 생각해 보겠습니다. 이러한 모든 분할을 참여자로 포함하는 분산형 트랜잭션이 있습니다. 참여자 분할에는 이전에 데이터를 읽기-쓰기 트랜잭션의 일부로 읽었던 다른 분할도 포함될 수 있습니다.

다음 단계에서는 커밋의 일부로 어떤 일이 일어나는지를 설명합니다.

  1. 스토리지 클라이언트가 커밋을 실행합니다. 커밋에는 M1~M5 변형이 포함되어 있습니다.
  2. 분할 3과 6은 이 트랜잭션의 참여자입니다. 분할 3과 같이 참여자 중 하나가 조정자로 선택됩니다. 조정자는 모든 참여자 간에 트랜잭션이 자동으로 커밋 또는 중단되는지 확인합니다.
    • 이러한 분할의 리더 복제본이 참여자와 조정자가 수행한 작업을 담당합니다.
  3. 각 참여자와 조정자는 각각의 복제본으로 Paxos 알고리즘을 실행합니다.
    • 리더가 복제본으로 Paxos 알고리즘을 실행합니다. 대부분의 복제본이 리더에 ok to commit 응답을 반환하면 쿼럼이 달성됩니다.
    • 그런 다음 각 참여자는 준비(2단계 커밋의 첫 번째 단계)가 되면 조정자에게 알립니다. 참여자가 트랜잭션을 커밋할 수 없는 경우에는 전체 트랜잭션이 aborts입니다.
  4. 조정자는 자신을 비롯해 모든 참여자가 준비되었음을 확인하면 accept 트랜잭션 결과를 모든 참여자에게 전달합니다(2단계 커밋의 두 번째 단계). 이 단계에서는 각 참여자가 안정적인 스토리지에 커밋 결정을 기록하고 트랜잭션이 커밋됩니다.
  5. 조정자는 Firestore의 스토리지 클라이언트에 트랜잭션이 커밋되었다고 응답합니다. 동시에 조정자와 모든 참가자가 데이터에 변형을 적용합니다.

커밋 수명 주기

Firestore 데이터베이스가 작으면 단일 분할이 M1~M5 변형의 모든 키를 소유하는 경우가 있습니다. 이 경우 트랜잭션에는 참여자가 하나뿐이며 앞서 언급한 2단계 커밋이 필요하지 않으므로 쓰기가 더 빨라집니다.

멀티 리전의 쓰기

멀티 리전 배포에서 여러 리전에 복제본을 분산하면 가용성이 높아지지만 성능 비용이 발생합니다. 서로 다른 리전의 복제본 간 통신에는 왕복 시간이 더 오래 걸립니다. 따라서 Firestore 작업의 기준 지연 시간은 단일 리전 배포에 비해 약간 더 깁니다.

Google은 분할의 리더십을 항상 기본 리전에 유지하는 방식으로 복제본을 구성합니다. 이 기본 리전으로부터 트래픽이 Firestore 서버로 수신됩니다. 이러한 리더십 결정이 Firestore의 스토리지 클라이언트와 복제본 리더(또는 다중 분할 트랜잭션의 조정자) 간의 통신 왕복 지연을 줄여줍니다.

읽기 수명 이해

이 섹션에서는 Firestore의 읽기에 대해 자세히 설명합니다. 특히 쿼리는 문서 읽기와 색인 항목 읽기가 혼합되어 있습니다.

데이터베이스 트랜잭션을 사용하여 스토리지 레이어의 데이터 읽기를 내부적으로 수행하면 일관된 읽기를 보장할 수 있습니다. 그러나 쓰기에 사용되는 트랜잭션과 달리 이러한 트랜잭션은 잠금을 사용하지 않습니다. 대신 타임스탬프를 선택한 다음 해당 타임스탬프에서 모든 읽기를 실행합니다. 잠금을 획득하지 않으므로 동시 실행되는 읽기-쓰기 트랜잭션을 차단하지 않습니다. 이 트랜잭션을 실행하기 위해 Firestore의 스토리지 클라이언트는 읽기 타임스탬프를 선택하는 방법을 스토리지 레이어에 알려주는 타임스탬프 경계를 지정합니다. Firestore의 스토리지 클라이언트에서 선택한 타임스탬프 경계의 유형은 읽기 요청의 읽기 옵션에 따라 결정됩니다.

스토리지 레이어의 읽기 트랜잭션 이해

이 섹션에서는 읽기 유형과 Firestore의 스토리지 레이어에서 읽기가 처리되는 방식을 설명합니다.

강력 읽기

기본적으로 Firestore 읽기에는 strong consistency가 있습니다. strong consistency는 Firestore 읽기가 읽기 시작할 때까지 커밋된 모든 쓰기를 반영하는 최신 버전의 데이터를 반환한다는 뜻입니다.

단일 분할 읽기

Firestore의 스토리지 클라이언트는 읽을 행의 키를 소유한 분할을 조회합니다. 이전 섹션의 분할 3에서 읽어야 한다고 가정해 보겠습니다. 클라이언트는 왕복 지연 시간을 줄이기 위해 가장 가까운 복제본에 읽기 요청을 보냅니다.

선택한 복제본에 따라 이 시점에서 다음과 같은 상황이 발생할 수 있습니다.

  • 읽기 요청은 리더 복제본(영역 A)으로 전달됩니다.
    • 리더는 항상 최신 상태이므로 읽기를 직접 진행할 수 있습니다.
  • 읽기 요청이 리더가 아닌 복제본(예: 영역 B)으로 전달됩니다.
    • 분할 3은 내부 상태를 통해 충분한 정보가 있다는 것을 인지하고 읽기를 제공합니다.
    • 분할 3은 최신 데이터가 확인되었는지 여부를 인식하지 못할 수 있습니다. 리더에 메시지를 전송해서 읽기 제공을 위해 적용해야 하는 마지막 트랜잭션의 타임스탬프를 요청합니다. 트랜잭션이 적용되면 읽기를 진행할 수 있습니다.

그러면 Firestore에서 응답을 클라이언트에 반환합니다.

다중 분할 읽기

여러 분할에서 읽기를 수행해야 하는 상황에서는 모든 분할에 동일한 메커니즘이 적용됩니다. 모든 분할에서 데이터가 반환되면 Firestore의 스토리지 클라이언트가 결과를 결합합니다. 그러면 Firestore가 이 데이터로 클라이언트에 응답합니다.

핫스팟 방지

Firestore의 분할은 필요할 때 또는 키 공간이 확장될 때 더 많은 스토리지 서버에 트래픽을 제공하는 작업을 분산하기 위해 자동으로 더 작은 조각으로 분할됩니다. 초과 트래픽을 처리하기 위해 생성된 분할은 트래픽이 사라지더라도 약 24시간 동안 보관됩니다. 따라서 반복적으로 트래픽이 급증하면 분할이 유지되고 필요할 때마다 더 많은 분할이 도입됩니다. 이러한 메커니즘은 트래픽 부하 또는 데이터베이스 크기가 증가하면 Firestore 데이터베이스가 자동 확장되는 데 도움이 됩니다. 하지만 몇 가지 제한사항이 있습니다.

스토리지와 부하를 분할하는 데 시간이 걸리는 데다가, 트래픽을 너무 빨리 늘리면 서비스가 조정되는 동안 지연 시간 증가 또는 기한 초과 오류(일반적으로 핫스팟이라고 함)가 발생할 수 있습니다. 권장사항은 키 범위 전반에 작업을 분산하면서 데이터베이스의 컬렉션에서 트래픽을 점진적으로 늘리는 것입니다.

분할은 부하 증가에 따라 자동으로 생성되지만 Firestore에서는 전용 복제 스토리지 서버 집합을 사용하여 단일 문서를 제공할 때까지만 키 범위를 분할할 수 있습니다. 그 결과, 단일 문서에서 지속적으로 많은 양의 작업을 동시 실행하면 해당 문서에 핫스팟이 발생할 수 있습니다. 단일 문서에서 긴 지연 시간이 지속적으로 발생하는 경우 여러 문서에 걸쳐 데이터를 분할하거나 복제하도록 데이터 모델을 수정해야 합니다.

여러 작업에서 같은 문서를 동시에 읽거나 쓰려고 하면 경합 오류가 발생합니다.

이 페이지에 설명된 권장사항을 따르면 Firestore는 구성을 조정하지 않고도 임의의 대규모 워크로드를 처리하도록 확장할 수 있습니다.