BigQuery Storage Write API 소개

BigQuery Storage Write API는 BigQuery용 통합 데이터 수집 API입니다. 스트리밍 수집과 일괄 로드를 하나의 고성능 API로 결합합니다. Storage Write API를 사용하면 레코드를 실시간으로 BigQuery로 스트리밍하거나 많은 수의 레코드를 일괄 처리하고 단일 원자적 작업으로 커밋할 수 있습니다.

Storage Write API 사용의 이점

1회만 전달되는 시맨틱스. Storage Write API는 스트림 오프셋 사용을 통해 1회만 전달되는 시맨틱스를 지원합니다. tabledata.insertAll 메서드와 달리 Storage Write API는 레코드를 첨부할 때 클라이언트가 스트림 오프셋을 제공하는 경우 스트림 내에서 동일한 오프셋을 갖는 메시지를 중복해서 작성하지 않습니다.

스트림 수준 트랜잭션. 스트림에 데이터를 기록하고 데이터를 단일 트랜잭션으로 커밋할 수 있습니다. 커밋 작업이 실패하면 작업을 안전하게 다시 시도할 수 있습니다.

스트림 간 트랜잭션. 여러 작업자가 자신의 고유 스트림을 만들어 데이터를 독립적으로 처리할 수 있습니다. 모든 작업자가 완료되었으면 모든 스트림을 하나의 트랜잭션으로 커밋할 수 있습니다.

효율적인 프로토콜. Storage Write API는 HTTP를 통한 REST 대신 gRPC 스트리밍을 사용하기 때문에 이전의 insertAll 메서드보다 효율적입니다. 또한 Storage Write API는 JSON보다 효율적인 쓰기 형식인 프로토콜 버퍼 형태의 바이너리 형식을 지원합니다. 쓰기 요청은 보장된 순서 정렬과 비동기적으로 수행됩니다.

스키마 업데이트 감지. 클라이언트가 스트리밍하는 동안 기본 테이블 스키마가 변경되면 Storage Write API가 이를 클라이언트에 알립니다. 클라이언트는 업데이트된 스키마를 사용하여 다시 연결하거나 기존 연결에 계속 작성할지를 선택할 수 있습니다.

비용 절감. Storage Write API는 이전 insertAll 스트리밍 API보다 훨씬 저렴합니다. 또한 매월 최대 2TiB를 무료로 수집할 수 있습니다.

필수 권한

Storage Write API를 사용하려면 bigquery.tables.updateData 권한이 있어야 합니다.

다음과 같은 사전 정의된 Identity and Access Management(IAM) 역할에 bigquery.tables.updateData 권한이 포함되어 있습니다.

  • bigquery.dataEditor
  • bigquery.dataOwner
  • bigquery.admin

BigQuery의 IAM 역할과 권한에 대한 자세한 내용은 사전 정의된 역할 및 권한을 참조하세요.

인증 범위

Storage Write API를 사용하려면 다음 OAuth 범위 중 하나가 필요합니다.

  • https://www.googleapis.com/auth/bigquery
  • https://www.googleapis.com/auth/cloud-platform
  • https://www.googleapis.com/auth/bigquery.insertdata

자세한 내용은 인증 개요를 참조하세요.

Storage Write API 개요

Storage Write API의 핵심 추상화는 스트림입니다. 스트림은 BigQuery 테이블에 데이터를 작성합니다. 두 개 이상의 스트림이 동일한 테이블에 동시에 작성할 수 있습니다.

기본 스트림

Storage Write API는 지속적으로 데이터가 도착하는 스트리밍 시나리오에 맞게 설계된 기본 스트림을 제공합니다. 다음과 같은 특성이 있습니다.

  • 기본 스트림에 작성된 데이터를 쿼리에 즉시 사용할 수 있습니다.
  • 기본 스트림은 1회 이상 실행되는 시맨틱스를 지원합니다.
  • 기본 스트림을 명시적으로 만들 필요가 없습니다.

기존 tabledata.insertall API에서 마이그레이션하는 경우 기본 스트림을 사용하는 것이 좋습니다. 쓰기 시맨틱스는 비슷하지만 데이터 복원력이 더 우수하고 확장 제한도 적습니다.

API 흐름:

  1. AppendRows(루프)

자세한 내용 및 예시 코드는 1회 이상 실행되는 시맨틱스에 기본 스트림 사용을 참조하세요.

애플리케이션에서 만든 스트림

다음 동작 중 하나가 필요한 경우 스트림을 명시적으로 만들면 됩니다.

  • 스트림 오프셋을 사용한 1회만 쓰는 시맨틱스
  • 추가 ACID 속성 지원

일반적으로 애플리케이션이 만든 스트림은 추가적인 복잡성을 감수하는 대신 기능을 더 세밀하게 제어할 수 있습니다.

스트림을 만들 때 유형을 지정합니다. 유형은 스트림에 기록된 데이터가 BigQuery에 읽을 수 있도록 표시되는 시점을 제어합니다.

대기 유형

대기 유형의 레코드는 스트림을 커밋할 때까지 대기 상태로 버퍼링됩니다. 스트림을 커밋하면 대기 중인 모든 데이터를 읽을 수 있게 됩니다. 커밋은 원자적 연산입니다. BigQuery 로드 작업의 대안으로 일괄 워크로드에 이 유형을 사용합니다. 자세한 내용은 Storage Write API를 사용한 데이터 일괄 로드를 참조하세요.

API 흐름:

  1. CreateWriteStream
  2. AppendRows(루프)
  3. FinalizeWriteStream
  4. BatchCommitWriteStreams

커밋 유형

커밋 유형의 레코드는 스트림에 쓰는 즉시 읽을 수 있습니다. 읽기 지연 시간이 최소화된 스트리밍 워크로드에 이 유형을 사용합니다. 기본 스트림은 최소 1회 형식의 커밋 유형을 사용합니다. 자세한 내용은 1회만 실행되는 시맨틱스에 커밋 유형 사용을 참조하세요.

API 흐름:

  1. CreateWriteStream
  2. AppendRows(루프)
  3. FinalizeWriteStream(선택사항)

버퍼링 유형

버퍼링 유형Apache Beam BigQuery I/O 커넥터와 함께 사용되는 경우를 제외하면 일반적으로 사용되지 않는 고급 유형입니다. 작은 배치를 항상 함께 표시해야 하는 경우 커밋 유형 사용하고 각 배치를 한 요청으로 전송합니다. 이 유형에서는 행 수준 커밋이 제공되며 스트림을 삭제하여 행이 커밋될 때까지 레코드가 버퍼링됩니다.

API 흐름:

  1. CreateWriteStream
  2. AppendRowsFlushRows(루프)
  3. FinalizeWriteStream(선택사항)

유형 선택

다음 순서도에 따라 워크로드에 가장 적합한 유형을 결정할 수 있습니다.

이미지

API 세부정보

Storage Write API를 사용할 때는 다음을 고려하세요.

AppendRows

AppendRows 메서드는 하나 이상의 레코드를 스트림에 추가합니다. AppendRows의 첫 번째 호출에는 DescriptorProto로 지정된 데이터 스키마와 함께 스트림 이름이 포함되어야 합니다. 권장사항은 각 AppendRows 호출에서 행 배치를 전송하는 것입니다. 행을 한 번에 한 개씩 전송하지 마세요.

프로토콜 버퍼 처리

프로토콜 버퍼는 언어 중립적이며 플랫폼 중립적인 확장 가능한 메커니즘을 제공하여 구조화된 데이터를 이후 버전과 호환되거나 이전 버전과 호환되도록 직렬화합니다. 빠르고 효율적인 파싱을 통해 압축 데이터 스토리지를 제공한다는 점에서 매우 유용합니다. 프로토콜 버퍼에 대해 자세히 알아보려면 프로토콜 버퍼 개요를 참조하세요.

사전 정의된 프로토콜 버퍼 메시지로 API를 직접 사용하려는 경우 프로토콜 버퍼 메시지가 package 지정자를 사용할 수 없으며 모든 중첩 또는 열거 유형이 최상위 루트 메시지 내에 정의되어 있어야 합니다. 외부 메시지에 대한 참조는 허용되지 않습니다. 예시를 보려면 sample_data.proto를 참조하세요.

클라이언트 라이브러리는 프로토콜 버퍼 스키마를 정규화하므로 Java 및 Go 클라이언트는 임의의 프로토콜 버퍼를 지원합니다.

FinalizeWriteStream

FinalizeWriteStream 메서드는 새 데이터를 추가할 수 없도록 스트림을 종결합니다. 이 메서드는 Pending 유형에서 필수이며 CommittedBuffered 유형에서는 선택사항입니다. 기본 스트림은 이 메서드를 지원하지 않습니다.

오류 처리

오류가 발생하면 반환된 google.rpc.Status오류 세부정보StorageError가 포함될 수 있습니다. 구체적인 오류 유형을 알아내려면 StorageErrorCode를 검토하세요. Google API 오류 모델에 대한 자세한 내용은 오류를 참조하세요.

연결

Storage Write API는 양방향 연결을 사용하는 gRPC API입니다. AppendRows 메서드는 스트림에 대한 연결을 만듭니다. 기본 스트림에서 여러 연결을 열 수 없습니다. 이러한 추가는 비동기식이므로 일련의 쓰기를 동시에 보낼 수 있습니다. 양방향 연결의 응답 메시지는 요청이 전송될 때와 동일한 순서로 도착합니다.

애플리케이션에서 생성된 스트림은 단일 활성 연결만 가질 수 있습니다. 활성 연결 수를 제한하고 가능한 한 많은 데이터 쓰기에 대해 하나의 연결을 사용하는 것이 좋습니다. Java 또는 Go에서 기본 스트림을 사용할 때는 Storage Write API 다중화를 통해 공유 연결을 사용하여 여러 대상 테이블에 기록할 수 있습니다.

일반적으로 단일 연결은 1MBps 이상의 처리량을 지원합니다. 상한값은 네트워크 대역폭, 데이터 스키마, 서버 부하와 같은 여러 요소에 따라 달라집니다. 연결이 처리량 한도에 도달하면 대기 중인 요청 수가 줄어들 때까지 수신 요청이 거부되거나 대기열에 추가될 수 있습니다. 처리량이 더 필요하면 연결을 더 만드세요.

BigQuery는 연결이 너무 오랫동안 유휴 상태로 유지되면 gRPC 연결을 닫습니다. 이 경우 응답 코드는 HTTP 409입니다. gRPC 연결은 서버 다시 시작 시 또는 다른 이유로 인해 닫힐 수도 있습니다. 연결 오류가 발생하면 새 연결을 만듭니다. 연결이 닫히면 Java 및 Go 클라이언트 라이브러리에서 자동으로 다시 연결합니다.

클라이언트 라이브러리 지원

Storage Write API의 클라이언트 라이브러리는 여러 프로그래밍 언어로 제공되며 기본 gRPC 기반 API 구성을 노출합니다. 이 API는 양방향 스트리밍과 같은 고급 기능을 활용하므로 이를 지원하기 위해 추가 개발 작업이 필요할 수 있습니다. 이를 위해 이 API에는 상호작용을 간소화하고 개발자의 우려사항을 줄이는 여러 상위 수준 추상화가 제공됩니다. 가능하면 이러한 다른 라이브러리 추상화를 활용하는 것이 좋습니다.

이 섹션에서는 생성된 API 외의 추가 기능이 개발자에게 제공된 언어와 라이브러리에 관한 추가 세부정보를 제공합니다.

Storage Write API와 관련된 코드 샘플을 보려면 여기를 참조하세요.

Java 클라이언트

Java 클라이언트 라이브러리는 두 명의 작성자 객체를 제공합니다.

  • StreamWriter: 프로토콜 버퍼 형식의 데이터를 허용합니다.

  • JsonStreamWriter: JSON 형식의 데이터를 허용하고 프로토콜 버퍼로 변환한 후에 전송합니다. JsonStreamWriter는 자동 스키마 업데이트도 지원합니다. 테이블 스키마가 변경되면 작성자가 새 스키마에 자동으로 다시 연결되므로 클라이언트가 새 스키마를 사용하여 데이터를 전송할 수 있습니다.

프로그래밍 모델은 두 작성기 모두 비슷합니다. 주요 차이점은 페이로드의 형식을 지정하는 방법입니다.

작성자 객체는 Storage Write API 연결을 관리합니다. 작성자 객체는 자동으로 요청을 정리하고, 요청에 리전 라우팅 헤더를 추가하고, 연결 오류 후 다시 연결합니다. gRPC API를 직접 사용하는 경우 이러한 세부 동작을 직접 처리해야 합니다.

Go 클라이언트

Go 클라이언트는 클라이언트-서버 아키텍처를 사용하여 proto2를 통해 프로토콜 버퍼 형식 내에서 메시지를 인코딩합니다. Go 클라이언트와 예시 코드를 함께 사용하는 방법에 대한 자세한 내용은 Go 문서를 참조하세요.

Python 클라이언트

Python 클라이언트는 gRPC API를 래핑하는 하위 수준 클라이언트입니다. 이 클라이언트를 사용하려면 지정된 유형의 API 흐름에 따라 데이터를 프로토콜 버퍼로 전송해야 합니다.

Python에서 프로토콜 버퍼를 사용하는 방법에 대한 자세한 내용은 Python의 프로토콜 버퍼 기본 사항 튜토리얼을 참조하세요.

Node.js 클라이언트

NodeJS 클라이언트 라이브러리는 JSON 입력을 수락하고 자동 재연결 지원을 제공합니다. 클라이언트 사용 방법에 관한 자세한 내용은 문서를 참조하세요.

데이터 유형 변환

다음 표에서는 각 BigQuery 데이터 유형에 지원되는 프로토콜 버퍼 유형을 보여줍니다.

BigQuery 데이터 유형 지원되는 프로토콜 버퍼 유형
BOOL bool, int32, int64, uint32, uint64, google.protobuf.BoolValue
BYTES bytes, string, google.protobuf.BytesValue
DATE int32(권장), int64, string

이 값은 Unix epoch(1970-01-01) 이후의 일 수입니다. 유효한 범위는 `-719162`(0001-01-01) ~ `2932896`(9999-12-31)입니다.

DATETIME, TIME string

값은 DATETIME 또는 TIME 리터럴이어야 합니다.

int64

CivilTimeEncoder 클래스를 사용하여 변환을 수행합니다.

FLOAT double, float, google.protobuf.DoubleValue, google.protobuf.FloatValue
GEOGRAPHY string

값은 WKT 또는 GeoJson 형식의 도형입니다.

INTEGER int32, int64, uint32, enum, google.protobuf.Int32Value, google.protobuf.Int64Value, google.protobuf.UInt32Value
JSON string
NUMERIC, BIGNUMERIC int32, int64, uint32, uint64, double, float, string
bytes, google.protobuf.BytesValue

BigDecimalByteStringEncoder 클래스를 사용하여 변환을 수행합니다.

STRING string, enum, google.protobuf.StringValue
TIME string

값은 TIME 리터럴이어야 합니다.

TIMESTAMP int64(권장), int32, uint32, google.protobuf.Timestamp

값은 Unix epoch(1970-01-01) 이후의 마이크로초 수로 제공됩니다.

INTERVAL string, google.protobuf.Duration

문자열 값은 INTERVAL 리터럴이어야 합니다.

RANGE<T> message

startend 필드가 있는 proto의 중첩된 메시지 유형이며, 두 필드 모두 BigQuery 데이터 유형 T에 해당하는 동일한 지원되는 프로토콜 버퍼 유형이어야 합니다. TDATE, DATETIME, TIMESTAMP 중 하나여야 합니다. proto 메시지에 필드(start 또는 end)가 설정되지 않았으면 바인딩되지 않은 경계를 나타냅니다. 다음 예시에서 f_range_dateRANGE 열을 나타냅니다. proto 메시지에 end 필드가 설정되지 않았기 때문에 이 범위의 끝 경계가 바인딩되지 않습니다.


{
  f_range_date: {
    start: 1
  }
}
REPEATED FIELD array

proto의 배열 유형은 BigQuery의 반복 필드에 해당합니다.

RECORD message

proto의 중첩된 메시지 유형은 BigQuery의 레코드 필드에 해당합니다.

사용 불능 처리

지수 백오프로 다시 시도하면 무작위 오류 및 짧은 서비스 사용 불능 기간을 완화할 수 있지만, 사용 불능 기간이 연장될 때 행이 삭제되지 않도록 하려면 더 많은 사항을 고려해야 합니다. 특히 클라이언트가 행을 지속적으로 삽입할 수 없는 경우 어떻게 해야 할까요?

답은 요구사항에 따라 다릅니다. 예를 들어 누락된 일부 행이 허용되는 운영 분석에 BigQuery가 사용되는 경우 클라이언트는 몇 번 다시 시도한 후 포기하여 데이터를 삭제할 수 있습니다. 대신 모든 행이 금융 데이터와 같이 비즈니스에 중요한 경우 나중에 삽입할 수 있을 때까지 데이터를 유지할 전략이 있어야 합니다.

지속적인 오류를 처리하는 일반적인 방법 중 하나는 나중의 평가 및 삽입을 위해 Pub/Sub 주제에 행을 게시하는 것입니다. 또 다른 일반적인 방법은 클라이언트의 데이터를 일시적으로 유지하는 것입니다. 두 방법 모두 가용성이 복원되면 모든 행을 삽입할 수 있도록 보장하는 동시에 클라이언트의 차단을 해제할 수 있습니다.

시간 단위 열로 파티션 나누기

지난 5년부터 향후 1년 이내의 DATE, DATETIME, TIMESTAMP 열을 기준으로 파티션을 나눈 테이블로 데이터를 스트리밍할 수 있습니다. 이 범위를 벗어나는 데이터는 거부됩니다.

데이터가 스트리밍되면 처음에는 __UNPARTITIONED__ 파티션에 배치됩니다. 파티션으로 나누지 않은 데이터가 충분히 수집되면 BigQuery는 데이터를 다시 파티션 나누기하여 적절한 파티션에 배치합니다. 그러나 데이터가 __UNPARTITIONED__ 파티션에서 이동하는 데 걸릴 수 있는 시간을 정의하는 서비스수준계약(SLA)이 없습니다.

Storage Write API는 파티션 데코레이터 사용을 지원하지 않습니다.

Fluent Bit Storage Write API 출력 플러그인

Fluent Bit Storage Write API 출력 플러그인은 JSON 레코드를 BigQuery로 처리하는 프로세스를 자동화하므로 코드를 작성할 필요가 없습니다. 이 플러그인을 사용하면 호환되는 입력 플러그인을 구성하고 구성 파일을 설정하기만 하면 데이터 스트리밍을 시작할 수 있습니다. Fluent Bit는 입력 및 출력 플러그인을 사용하여 다양한 유형의 데이터 소스와 싱크를 처리하는 오픈소스 크로스 플랫폼 로그 프로세서 및 전달자입니다.

이 플러그인은 다음을 지원합니다.

  • 기본 유형을 사용하는 최소 1회 실행되는 시맨틱스
  • 커밋된 유형을 사용하는 1회만 실행되는 시맨틱스
  • 백프레셔가 표시되는 경우 기본 스트림의 동적 크기 조정

Storage Write API 측정항목

서버 측 요청, 레벨 지연 시간, 동시 연결, 업로드된 바이트, 업로드된 행과 같이 Storage Write API를 사용하여 데이터 수집을 모니터링하기 위한 측정항목은 Google Cloud 측정항목을 참조하세요.

최근에 스트리밍된 데이터에 데이터 조작 언어(DML) 사용

UPDATE, DELETE, MERGE 문과 같은 데이터 조작 언어(DML)를 사용하여 BigQuery Storage Write API가 최근에 BigQuery 테이블에 기록한 행을 수정할 수 있습니다. 최근 쓰기 작업은 최근 30분 내에 발생한 작업입니다.

DML을 사용하여 스트리밍 데이터를 수정하는 방법은 DML 사용을 참조하세요.

제한사항

  • 최근에 스트리밍된 데이터에 대해 변형 DML 문을 실행하는 지원은 BigQuery Storage Write API 버퍼링된 유형을 사용하여 스트리밍된 데이터로 확장되지 않습니다.
  • 최근에 스트리밍된 데이터에 대해 변형 DML 문을 실행하는 지원은 insertAll streaming API를 사용하여 스트리밍된 데이터로 확장되지 않습니다.
  • 최근에 스트리밍된 데이터에 대해 멀티 문 트랜잭션 내에서 변형 DML 문을 실행하는 것은 지원되지 않습니다.

Storage Write API 할당량

Storage Write API 할당량 및 한도에 대한 자세한 내용은 BigQuery Storage Write API 할당량 및 한도를 참조하세요.

Google Cloud 콘솔 할당량 페이지에서 동시 연결 및 처리 할당 사용량을 모니터링할 수 있습니다.

처리량 계산

1억 개의 엔드포인트에서 로그를 수집하여 분당 1,500개의 로그 레코드를 생성한다고 가정해 보세요. 이 경우에는 100 million * 1,500 / 60 seconds = 2.5 GB per second로 처리량을 예상할 수 있습니다. 이러한 처리량을 지원하기 위해서는 할당량이 적절한지 미리 확인해야 합니다.

Storage Write API 가격 책정

가격 책정은 데이터 수집 가격 책정을 참조하세요.

사용 사례

엔드포인트 로그의 이벤트 데이터를 처리하는 파이프라인이 있다고 가정해보세요. 이벤트는 연속적으로 생성되며 최대한 빨리 BigQuery에서 쿼리할 수 있어야 합니다. 이 사용 사례에는 최신 데이터가 가장 중요하므로 BigQuery로 데이터를 수집하는 가장 좋은 방법은 Storage Write API입니다. 이러한 엔드포인트를 가볍게 만들기 위한 권장 아키텍처는 이 이벤트를 Pub/Sub로 전송하며, 이러한 이벤트는 여기서부터 BigQuery로 직접 스트리밍되는 스트리밍 Dataflow 파이프라인에서 소비됩니다.

이 아키텍처의 주요 안정성 문제는 BigQuery에 레코드를 삽입하지 못한 문제를 처리하는 방법입니다. 각 레코드가 중요하고 손실되지 않으면 데이터는 삽입되기 전에 버퍼링되어야 합니다. 위의 권장 아키텍처에서 Pub/Sub는 메시지 보관 기능을 갖춘 버퍼 역할을 수행할 수 있습니다. 잘린 지수 백오프로 BigQuery 스트리밍 삽입을 다시 시도하도록 Dataflow 파이프라인을 구성해야 합니다. 버퍼로써의 Pub/Sub의 용량이 소진되면(예: BigQuery를 장기간 사용할 수 없거나 네트워크 장애가 발생하는 경우) 클라이언트에서 데이터가 유지되어야 하며 클라이언트에는 BigQuery를 다시 사용할 수 있을 때 유지된 레코드를 다시 삽입하는 메커니즘이 필요합니다. 이러한 상황을 처리하는 방법에 대한 자세한 내용은 Google Pub/Sub 신뢰성 가이드 블로그 게시물을 참조하세요.

처리할 수 있는 또 다른 실패 사례는 악성 레코드입니다. 악성 레코드는 레코드를 재시도할 수 없는 오류와 함께 삽입하지 못했거나 최대 재시도 횟수 후에 성공적으로 삽입되지 않았기 때문에 BigQuery에서 거부된 레코드입니다. 두 가지 레코드 모두 추가 조사를 위해 Dataflow 파이프라인에 의해 '데드 레터 큐'에 저장해야 합니다.

정확히 한 번의 시맨틱스가 필요한 경우 클라이언트에서 제공된 레코드 오프셋과 함께 커밋된 유형으로 쓰기 스트림을 만듭니다. 이렇게 하면 오프셋 값이 다음 추가 오프셋과 일치하는 경우에만 쓰기 작업이 수행되므로 중복이 방지됩니다. 오프셋을 제공하지 않으면 레코드가 스트림의 현재 끝에 추가되고 실패한 추가 작업을 다시 시도하면 레코드가 스트림에 두 번 이상 표시될 수 있습니다.

정확한 1회 보장이 필요하지 않은 경우 기본 스트림에 작성하면 더 높은 처리량을 허용하고 쓰기 스트림을 만들 때 할당량 한도에 포함되지도 않습니다.

네트워크 처리량을 예측하고 처리량을 지원하기 위해 할당량이 적절한지 미리 확인합니다.

워크로드가 매우 균일하지 않은 속도로 데이터를 생성하거나 처리하는 경우 클라이언트의 부하 급증을 완화하고 일정한 처리량으로 BigQuery로 스트리밍합니다. 이렇게 하면 용량 계획을 단순화할 수 있습니다. 이 작업이 가능하지 않으면 처리량이 짧은 급증 동안 할당량을 초과하는 경우 429(리소스 소진됨) 오류를 처리할 준비가 되어 있어야 합니다.

다음 단계