권장사항

Datastore 모드의 Firestore를 사용하는 애플리케이션을 빌드할 때 여기에 나열된 권장사항을 빠른 참조로 활용할 수 있습니다. 이 페이지는 Datastore 모드의 기본적인 사용법을 안내하지 않으므로 Datastore 모드를 시작한 지 얼마 안 되는 신규 사용자에게는 적합하지 않을 수 있습니다. 신규 사용자라면 Datastore 모드의 Firestore 시작하기부터 살펴보는 것이 좋습니다.

일반

  • 네임스페이스 이름, 종류 이름, 속성 이름, 커스텀 키 이름에는 항상 UTF-8 문자를 사용합니다. 이러한 이름에 UTF-8 이외의 문자를 사용하면 Datastore 모드 기능에 지장을 줄 수 있습니다. 예를 들어 속성 이름에 UTF-8 이외의 문자를 사용하면 속성을 사용하는 색인이 생성되지 않을 수 있습니다.
  • 종류 이름 또는 커스텀 키 이름에 슬래시(/)를 사용하지 마세요. 이러한 이름에 슬래시를 사용하면 추후 기능에 지장을 줄 수 있습니다.
  • Cloud Project ID에 민감한 정보를 저장하지 않도록 유의하세요. Cloud Project ID는 프로젝트 수명 이후에도 보관될 수 있습니다.

API 호출

  • 읽기, 쓰기, 삭제 시 단일 작업 대신 일괄 작업을 사용하세요. 일괄 작업은 단일 작업과 동일한 오버헤드로 여러 작업을 수행하기 때문에 보다 효율적입니다.
  • 트랜잭션이 실패하면 트랜잭션 롤백을 시도하세요. 롤백은 트랜잭션 중인 동일한 리소스를 두고 경쟁하는 다른 요청의 재시도 지연 시간을 최소화합니다. 하지만 롤백 자체도 실패할 수 있으므로 한 번 시도해 보는 것에 의미를 둬야 합니다.
  • 가능하다면 동기식 호출 대신 비동기식 호출을 사용하세요. 비동기식 호출은 지연 시간 영향을 최소화합니다. 예를 들어 응답을 렌더링하기 전에 쿼리 결과와 동기식 lookup() 결과가 필요한 애플리케이션을 생각해 보세요. lookup() 및 쿼리에 데이터 종속 항목이 없다면 쿼리를 시작하기 전에 lookup()이 완료될 때까지 동기식으로 대기할 필요가 없습니다.

항목

  • 동일한 항목(키별)을 동일한 커밋에 여러 번 포함하지 마세요. 동일한 항목을 동일한 커밋에 여러 번 포함하면 지연 시간이 영향을 받을 수 있습니다.

  • 항목 업데이트 섹션을 참조하세요.

  • 키 이름을 항목 생성 시 제공하지 않으면 자동으로 생성됩니다. 이름은 키스페이스에 균등하게 배포되도록 할당됩니다.
  • 커스텀 이름을 사용하는 키의 경우 항상 슬래시(/)를 제외하고 UTF-8 문자를 사용합니다. UTF-8 이외의 문자는 Datastore 모드 내보내기 파일을 BigQuery로 가져오기와 같은 다양한 프로세스에 지장을 줍니다. 슬래시는 추후 기능에 지장을 줄 수 있습니다.
  • 숫자 ID를 사용하는 키의 경우:
    • ID에 음수를 사용하지 마세요. 음수 ID는 정렬에 지장을 줄 수 있습니다.
    • ID에 0(영) 값을 사용하지 마세요. 이 값을 사용하면 ID가 자동으로 할당됩니다.
    • 항목을 만들 때 원하는 숫자 ID를 수동으로 할당하려면 allocateIds() 메서드를 통해 애플리케이션에 ID 블록을 할당합니다. 그러면 Datastore 모드에서 수동 숫자 ID를 다른 항목에 할당하지 않습니다.
  • 항목을 만들 때 고유한 숫자 ID 또는 커스텀 이름을 할당하려면 다음과 같은 단조 증가 값을 사용하지 마세요.

    1, 2, 3, …,
    "Customer1", "Customer2", "Customer3", ….
    "Product 1", "Product 2", "Product 3", ….
    

    애플리케이션에서 순차 번호 지정과 같은 대량의 트래픽이 생성되면 핫스팟이 발생하여 Datastore 모드 지연 시간이 영향을 받을 수 있습니다. 연속된 숫자 ID 발급을 방지하려면 allocateIds() 메서드로 번호 ID를 가져옵니다. allocateIds() 메서드는 숫자 ID를 적절히 분포된 순서로 생성합니다.

  • 키를 지정하거나 생성된 이름을 저장하면 나중에 항목을 찾는 쿼리를 수행할 필요 없이 관련 항목에 lookup()을 수행할 수 있습니다.

색인

  • 속성이 쿼리에 필요하지 않은 경우 색인에서 속성을 제외합니다. 불필요한 속성 색인으로 인해 지연 시간이 늘고 색인 항목의 스토리지 비용이 증가할 수 있습니다.
  • 복합 색인이 지나치게 많지 않도록 하세요. 복합 색인을 과도하게 사용하면 쓰기 지연 시간이 늘고 색인 항목의 스토리지 비용이 증가할 수 있습니다. 사전 정의된 색인 없이 대량의 데이터세트에 대한 임시 쿼리를 실행해야 하는 경우에는 BigQuery를 사용합니다.
  • 단조 증가 값(예: NOW() 타임스탬프)으로 속성 색인을 생성하지 마세요. 이러한 색인을 유지하면 핫스팟이 발생하여 읽기 및 쓰기 속도가 높은 애플리케이션의 Datastore 모드 지연 시간이 영향을 받을 수 있습니다. 단조로운 속성을 처리하는 방법에 대한 자세한 내용은 아래의 좁은 키 범위에 대한 빠른 읽기/쓰기 속도를 참조하세요.

속성

  • 문자열 유형의 속성에는 항상 UTF-8 문자를 사용하세요. 문자열 유형의 속성에 UTF-8 이외의 문자를 사용하면 쿼리에 지장을 줄 수 있습니다. UTF-8 이외의 문자가 포함된 데이터를 저장해야 하는 경우에는 바이트 문자열을 사용합니다.
  • 속성 이름에 구두점을 사용하지 마세요. 속성 이름에 구두점이 있으면 포함된 항목 속성의 색인 생성이 지장을 받습니다.

쿼리

  • 쿼리 결과 중 키에만 액세스하면 되는 경우에는 키 전용 쿼리를 사용하세요. 키 전용 쿼리는 전체 항목을 검색하는 것보다 낮은 지연 시간과 비용으로 결과를 반환합니다.
  • 항목에서 특정 속성에만 액세스하면 되는 경우에는 프로젝션 쿼리를 사용하세요. 프로젝션 쿼리는 전체 항목 검색보다 낮은 지연 시간과 비용으로 결과를 반환합니다.
  • 마찬가지로 쿼리 필터에 포함된 속성(예: order by 절에 나열된 속성)에만 액세스해야 하는 경우에는 프로젝션 쿼리를 사용합니다.
  • 오프셋 대신 커서를 사용하세요. 오프셋을 사용하면 건너뛴 항목이 애플리케이션에 반환되는 것을 방지할 수 있지만 이러한 항목은 여전히 내부적으로 검색됩니다. 건너뛴 항목은 쿼리 지연 시간에 영향을 미치며 검색에 필요한 읽기 작업 비용이 애플리케이션에 청구됩니다.

규모 확장을 위한 설계

다음 권장사항에서는 경합 문제가 발생하는 상황을 피하는 방법에 대해 설명합니다.

항목 업데이트

앱을 설계할 때 앱이 단일 항목을 얼마나 빠르게 업데이트할지 고려하세요. 워크로드 성능을 규정하는 가장 좋은 방법은 부하 테스트를 수행하는 것입니다. 앱에서 단일 항목을 업데이트할 수 있는 정확한 최대 속도는 워크로드에 따라 크게 달라집니다. 이러한 요소에는 쓰기 속도, 요청 간 경합, 영향을 받는 색인 수가 있습니다.

항목 쓰기 작업은 항목과 관련 색인을 업데이트하며 Datastore 모드의 Firestore는 복제본 쿼럼 전체에 쓰기 작업을 동기식으로 적용합니다. 쓰기 속도가 충분히 높아지면 데이터베이스에 경합, 긴 지연 시간 또는 기타 오류가 발생합니다.

좁은 키 범위에 대한 빠른 읽기/쓰기 속도

사전순으로 가까운 문서 또는 애플리케이션에 대한 읽기 또는 쓰기 속도가 높아지면 경합 오류가 발생합니다. 이러한 문제를 부하 집중이라고 부르며 다음 중 하나라도 해당할 경우 애플리케이션에 부하 집중이 발생할 수 있습니다.

  • 매우 높은 속도로 신규 항목을 생성하면서 단조 증가하는 자체 ID를 할당하는 경우

    Datastore 모드는 분산형 알고리즘을 사용해 키를 할당합니다. 자동 항목 ID를 사용하여 새 항목을 생성하면 쓰기 작업에 부하 집중이 발생하지 않습니다.

  • 기존의 연속 ID 할당 정책을 사용하여 매우 빠른 속도로 새 항목을 만듭니다.

  • 항목이 거의 없는 종류의 새 항목을 빠른 속도로 새 항목을 만듭니다.

  • 타임스탬프와 같이 색인이 생성되고 단조롭게 증가하는 속성 값을 갖는 새 항목을 매우 빠른 속도로 만듭니다.

  • 종류에서 빠른 속도로 항목을 삭제합니다.

  • 트래픽의 점진적 증가 없이 매우 빠른 속도로 데이터베이스에 쓰기 작업을 수행하는 경우

작은 범위의 키에 대한 쓰기 속도가 갑자기 증가하면 핫스팟으로 인해 쓰기 속도가 느려질 수 있습니다. Datastore 모드는 높은 부하를 지원하기 위해 키 공간을 결국 분할하게 됩니다.

단일 키를 매우 높은 속도로 읽는 경우를 제외하고 읽기 한도는 일반적으로 쓰기 한도보다 높습니다.

핫스팟은 항목 키와 색인에 모두 사용되는 키 범위에 적용할 수 있습니다.

경우에 따라 좁은 범위의 키에 대한 읽기 또는 쓰기를 방지하는 것보다 부하 집중이 애플리케이션에 더 큰 영향을 미칠 수 있습니다. 예를 들어 인스턴스를 시작하는 동안 핫 키에 대한 읽기 또는 쓰기가 이루어지면 로드 요청이 실패할 수 있습니다.

키 또는 색인 생성된 속성이 단조 증가한다면 임의의 해시를 앞에 추가하여 키가 여러 태블릿으로 샤딩되도록 만들 수 있습니다.

마찬가지로 정렬이나 필터를 사용하여 단조 증가(또는 감소)하는 속성을 쿼리해야 한다면 새 속성의 색인을 생성하는 방법을 대신 사용할 수 있습니다. 이 경우 데이터세트 전체에서 카디널리티가 높지만 수행하려는 쿼리 범위 내의 모든 항목에 공통된 값을 단조 값 앞에 추가합니다. 예를 들어 타임스탬프를 기준으로 항목을 쿼리하려고 하지만 한 번에 한 명의 사용자에 대한 결과만 필요하다면 타임스탬프 앞에 사용자 ID를 추가하고 이 신규 속성의 색인을 생성하면 됩니다. 이렇게 하면 사용자에 대한 쿼리 및 정렬된 결과를 계속 얻을 수 있고 사용자 ID가 있으므로 색인 자체가 올바르게 샤딩됩니다.

트래픽 늘리기

키스페이스의 신규 종류 또는 부분에 대한 트래픽을 점진적으로 늘리세요.

Datastore 모드의 Firestore가 트래픽 증가에 대처할 수 있는 시간을 충분히 확보하려면 새로운 종류에 대한 트래픽을 점진적으로 늘려야 합니다. 새로운 종류인 경우에는 최대 작업 수를 초당 500개로 제한하고 5분마다 50%씩 트래픽을 늘리는 것이 좋습니다. 이 증가 일정에 따르면 이론상으로 90분 후에는 초당 작업 수를 740,000개로 늘릴 수 있습니다. 쓰기 작업이 키 범위 전반에 걸쳐 비교적 균등하게 분산되도록 만드세요. Google SRE에서는 이 법칙을 '500/50/5' 법칙이라 합니다.

이러한 점진적인 증가 패턴은 A 종류 사용을 중단하고 대신 B 종류를 사용하도록 코드를 변경하는 경우에 특히 중요합니다. 단편적으로 보면 B 종류를 읽고 B 종류가 없으면 A 종류를 읽도록 코드를 변경하면 이 마이그레이션을 처리할 수 있다고 생각할 수도 있습니다. 그러나 이렇게 하면 키스페이스 영역이 매우 좁아지므로 새로운 종류에 트래픽이 급증할 수 있습니다.

동일한 종류 내에서 다양한 범위의 키를 사용하도록 항목을 마이그레이션하는 경우에도 동일한 문제가 발생할 수 있습니다.

새로운 종류 또는 키로 항목을 마이그레이션하기 위해 사용하는 전략은 데이터 모델에 따라 달라집니다. 아래에서는 '병렬 읽기'로 알려진 예제 전략을 소개합니다. 이 전략이 내 데이터에 효과가 있는지 여부는 스스로 판단해야 합니다. 마이그레이션 도중 병렬 작업이 비용에 미치는 영향을 중요하게 고려하세요.

우선 기존 항목 또는 키를 읽습니다. 기존 항목이 없는 경우에는 신규 항목이나 키에서 읽을 수 있습니다. 부재 항목 읽기 속도가 높으면 핫스팟으로 이어질 수 있으므로 점차적으로 로드를 늘려야 합니다. 기존 항목을 신규 항목에 복사한 후 기존 항목을 삭제하는 전략이 바람직합니다. 새로운 키스페이스가 적절히 분산되도록 병렬 읽기를 점차적으로 증가하세요.

신규 종류에 대한 읽기 또는 쓰기를 점차적으로 늘리려면 임의 비율의 사용자가 신규 항목에 쓰기 작업을 수행할 수 있도록 사용자 ID의 확정 해시를 사용하는 전략을 사용할 수 있습니다. 사용자 ID 해시의 결과가 임의 기능 또는 사용자 행동에 의해 왜곡되지 않았는지 확인하세요.

그동안 Dataflow 작업을 실행하여 기존 항목 또는 키의 모든 데이터를 신규 항목 또는 키에 복사하세요. 일괄 작업 수행 시 핫스팟을 방지하려면 연속 키에 쓰기 작업을 수행하면 안 됩니다. 일괄 작업이 완료되면 새로운 위치에서만 읽기가 가능해집니다.

이 전략을 개선하려면 사용자를 한 번에 소량 배치씩 마이그레이션하면 됩니다. 사용자 항목에 사용자의 마이그레이션 상태를 추적하는 필드를 추가하고 사용자 ID의 해시에 근거하여 마이그레이션할 사용자 배치를 선택합니다. 그러면 맵리듀스 또는 Dataflow 작업이 이 사용자 배치의 키를 마이그레이션합니다. 이때 마이그레이션이 진행 중인 사용자는 병렬 읽기를 사용하게 됩니다.

마이그레이션이 진행되는 동안 기존 및 신규 항목 모두에 이중 쓰기를 수행하지 않으면 롤백을 쉽게 수행할 수 없으며, Datastore 모드 비용이 증가하는 원인이 됩니다.

삭제

작은 범위의 키에서 대량의 항목을 삭제하지 않도록 유의하세요.

Datastore 모드의 Firestore는 삭제된 항목을 없애고, 읽기 및 쓰기 효율성이 높아지도록 데이터를 재구성하기 위해 테이블을 주기적으로 재작성합니다. 이 프로세스를 압축이라고 부릅니다.

작은 범위의 키에서 대량의 Datastore 모드 항목을 삭제하면 압축이 완료될 때까지 이 부분의 색인에 대한 쿼리 속도가 느려집니다. 최악의 경우에는 결과를 반환하기 전에 쿼리 시간이 초과될 수 있습니다.

항목의 만료 시간을 나타내기 위해 색인된 필드에 타임스탬프 값을 사용하는 이 패턴은 되도록 피해야 합니다. 만료된 항목을 검색하려면 이 색인 필드에 대해 쿼리를 수행해야 합니다. 이 필드는 키스페이스의 일부가 가장 최근 삭제한 항목의 색인 항목과 겹친 상태로 상주할 가능성이 높습니다.

만료 타임스탬프 앞에 고정 길이 문자열을 추가하는 '분할 쿼리'를 실행하면 성능을 향상시킬 수 있습니다. 동일한 타임스탬프의 항목이 색인의 키 범위에 걸쳐 검색되도록 전체 문자열에 대해 색인이 정렬됩니다. 각 분할에서 결과를 가져오려면 여러 쿼리를 병행해서 실행하세요.

만료 타임스탬프 문제를 보다 만족스럽게 해결하는 방법은 주기적으로 업데이트되는 글로벌 카운터인 '세대 번호'를 사용하는 것입니다. 세대 번호는 만료 타임스탬프 앞에 추가되므로 쿼리는 세대 번호, 분할, 타임스탬프 순의 기준으로 정렬됩니다. 기존 항목의 삭제는 이전 세대에서 이루어집니다. 삭제되지 않은 모든 항목의 세대 번호는 증분되어야 합니다. 삭제가 완료되면 다음 세대로 이동하세요. 압축이 완료될 때까지 이전 세대에 대한 쿼리는 저조한 성능을 보입니다. 최종 일관성으로 인한 결과 누락의 위험성을 줄이려면 삭제할 항목 목록을 가져오는 색인 쿼리가 이루어지기 전에 여러 세대가 완료되기를 기다려야 할 수 있습니다.

샤딩 및 복제

샤딩 또는 복제를 사용하여 핫스팟에 대처합니다.

Datastore 모드의 Firestore에서 허용하는 것보다 높은 속도로 키 범위 영역을 읽어야 한다면 복제를 사용하면 됩니다. 이 방식을 사용하면 단일 항목에서 지원하는 읽기 속도보다 N배 빠른 속도로 동일한 항목 사본 N개를 저장할 수 있습니다.

Datastore 모드의 Firestore에서 허용하는 속도보다 빠른 속도로 키 범위 부분에 쓰기 작업을 수행해야 하는 경우에는 샤딩을 사용하면 됩니다. 샤딩은 항목을 작은 부분으로 나눕니다.

샤딩 시 일반적인 실수에는 다음이 해당됩니다.

  • 시간 프리픽스를 사용한 분할. 시간이 다음 프리픽스로 넘어가면 분할되지 않은 새로운 부분이 핫스팟이 됩니다. 대신 쓰기 작업의 부분을 신규 프리픽스로 점차적으로 롤오버해야 합니다.

  • 가장 사용률이 높은 항목만 분할. 총 항목 중 소량의 일부만 분할하면 사용률이 높은 항목 간의 행이 충분하지 않아 서로 다른 분할에 상주하게 됩니다.

다음 단계