스키마 진화

스키마 진화 (Schema Evolution)

시간이 지남에 따라 스키마 진화가 발생해요. 비즈니스 요구가 진화하고 데이터 형식이나 구조를 변경해야 할 때, Pinot을 사용해 스키마를 최신 상태로 유지하세요. Pinot에서 스키마를 막 시작했다면 Pinot 테이블용 새 스키마를 만드는 방법을 참고하세요.

이 튜토리얼에서는 스키마에 새 컬럼을 추가하고, 업데이트된 스키마로 데이터를 로드하고, 업데이트된 스키마를 테스트하기 위한 쿼리를 실행하고, 데이터를 백필하는 방법을 배워요.

참고: Pinot은 스키마에 컬럼 추가만 지원해요. 컬럼을 삭제하거나 컬럼 이름이나 데이터 타입을 변경하려면 새 테이블을 만들어야 해요.

출처: 문서

본문

사전 요구 사항 (Prerequisites)

시작하기 전에 Pinot 클러스터가 실행 중이고 baseballStats 테이블(Quickstart 옵션으로 Pinot 클러스터를 설정할 때 생성됨)이 있어야 해요. 자세한 내용은 Quickstart로 Pinot 실행 및 클러스터 설정 방법을 참고하세요.

스키마에 새 컬럼 추가 (Add a new column to your schema)

  1. controller API를 사용해 기존 스키마를 가져오세요:

    $ curl localhost:9000/schemas/baseballStats > baseballStats.schema
    
  2. baseballStats.schema 파일을 편집해 스키마 끝에 새 컬럼을 포함시키세요. 예를 들어 여기서는 dataType이 INT이고 defaultNullValue가 1인 yearsOfExperience라는 새 컬럼을 추가해요.

baseballStats.schema:

{
  "schemaName" : "baseballStats",
  "dimensionFieldSpecs" : [ {

    ...

    }, {
    "name" : "yearsOfExperience",
    "dataType" : "INT",
    "defaultNullValue": 1
  } ]
}
  1. 다음 명령으로 스키마를 업데이트하세요:

pinot-admin.sh

bin/pinot-admin.sh AddSchema -schemaFile baseballStats.schema -exec

curl

$ curl -F [email protected] localhost:9000/schemas

테이블 세그먼트 리로드 (Reload table segments)

스키마에 새 컬럼을 추가한 후 테이블 세그먼트를 리로드해, 완료된 세그먼트가 새 필드를 노출하도록 하고 실시간 consumer가 새 consuming 세그먼트에서 스키마를 가져오도록 하세요.

  1. (실시간 테이블) pinot.server.instance.reload.consumingSegment를 기본값 true로 유지해(Server 구성) 일관성 모드가 허용할 때 reload가 consuming 세그먼트에 대해 강제 커밋을 요청하게 하세요. 서버는 현재 mutable 세그먼트를 비동기로 봉인하고 최신 스키마/테이블 구성을 가진 새 consumer를 시작해요. POST /tables/{tableName}/forceCommit을 명시적으로 호출하고 폴링할 수도 있어요. force commit API를 참고하세요.
  2. 새 baseballStats 컬럼이 완료된 세그먼트에 나타나도록 하려면 테이블을 리로드하세요 — 상태 폴링 시 아래 샘플 reloadJobId를 자신의 것으로 교체하세요:

명령

$ curl -X POST localhost:9000/segments/baseballStats/reload

응답

{"baseballStats_OFFLINE":{"reloadJobId":"c3989a04-9fd1-46af-85e8-00f484759ef2","reloadJobMetaZKStorageStatus":"SUCCESS","numMessagesSent":"3"}}

이것은 테이블의 세그먼트를 호스팅하는 각 서버에서 리로드 연산을 트리거해요. API 응답에는 reloadJobId가 있어, 세그먼트 리로드 상태 API로 리로드 연산 상태를 모니터링할 수 있어요.

참고: 세그먼트 리로드는 진행 중인(in-flight) 쿼리에 영향을 주지 않아요. 기존 세그먼트가 진행 중인 쿼리를 서비스하지 않는 경우에만 새 세그먼트가 기존 세그먼트를 대체하도록 리로드돼요.

명령

$ curl -X GET localhost:9000/segments/segmentReloadStatus/c3989a04-9fd1-46af-85e8-00f484759ef2

응답

{
  "estimatedTimeRemainingInMinutes": 0,
  "timeElapsedInMinutes": 0.17655,
  "totalServersQueried": 3,
  "successCount": 12,
  "totalSegmentCount": 12,
  "totalServerCallsFailed": 0,
  "metadata": {
    "jobId": "c3989a04-9fd1-46af-85e8-00f484759ef2",
    "messageCount": "3",
    "submissionTimeMs": "1661753088066",
    "jobType": "RELOAD_ALL_SEGMENTS",
    "tableName": "baseballStats_OFFLINE"
  }
}

참고:

  • 실시간 consuming 세그먼트의 경우 pinot.server.instance.reload.consumingSegment가 true이면 리로드는 강제 커밋으로 수행돼요: 현재 consuming 세그먼트가 불변으로 커밋되고, 업데이트된 테이블 구성과 스키마로 새 consuming 세그먼트가 시작돼요.
  • 모든 컬럼 추가에 pauseConsumption이 필요한 것은 아니에요. 일반적인 기본값 전용 컬럼은 보통 스키마 업데이트 + 리로드/forceCommit이 필요해요. 수집 transform 변경은 consumer가 이전 transform 계획을 유지하지 않도록 일시 중지 경계나 즉시 forceCommit이 더 안전해요. 스키마 진화 결정 테이블을 참고하세요.
  • Upsert와 dedup은 테이블 수준(세그먼트 간) 메타데이터를 서버 테이블 데이터 매니저 안에 유지해요. 허용된 partial-upsert 전략 변경은 제어된 서버 재시작이 필요하며 소급 적용되지 않아요. 핵심 identity와 ordering 설정은 불변이므로, reload나 재시작에 의존하는 대신 새 테이블을 만들고 재수집하세요. full-upsert 테이블에 null 기본값 컬럼 추가는 위의 reload/forceCommit 경로를 따르지만, partial-upsert 테이블과 순서 어긋난 처리가 구성된 upsert 테이블은 consuming-segment 일관성 모드가 허용하지 않으면 강제 커밋을 제한해요.
  • 어떤 경우, 예를 들어 변환 함수 평가가 실패하거나 리로드되는 세그먼트의 일부가 아닌 컬럼을 참조하면 리로드가 변환을 성공적으로 적용하지 못할 수 있어요. 리로드 상태 API는 새 컬럼 쿼리가 실패하는 동안에도 성공을 보고할 수 있어요 — 서버 리로드 로그를 확인하세요.

데이터 쿼리 및 백필 (Query and backfill data)

  1. 세그먼트를 리로드한 후 다음을 실행해 새 컬럼을 쿼리하세요:

명령

$ bin/pinot-admin.sh PostQuery \
  -queryType sql \
  -brokerPort 8000 \
  -query "select playerID, yearsOfExperience from baseballStats limit 10" 2>/dev/null

응답

Executing command: PostQuery -brokerHost 192.168.86.234 -brokerPort 8000 -queryType sql -query select playerID, yearsOfExperience from baseballStats limit 10
Result: {"resultTable":{"dataSchema":{"columnNames":["playerID","yearsOfExperience"],"columnDataTypes":["STRING","INT"]},"rows":[["aardsda01",1],["aardsda01",1],["aardsda01",1],["aardsda01",1],["aardsda01",1],["aardsda01",1],["aardsda01",1],["aaronha01",1],["aaronha01",1],["aaronha01",1]]},"exceptions":[],"numServersQueried":1,"numServersResponded":1,"numSegmentsQueried":1,"numSegmentsProcessed":1,"numSegmentsMatched":1,"numConsumingSegmentsQueried":0,"numDocsScanned":10,"numEntriesScannedInFilter":0,"numEntriesScannedPostFilter":20,"numGroupsLimitReached":false,"totalDocs":97889,"timeUsedMs":3,"segmentStatistics":[],"traceInfo":{},"minConsumingFreshnessTimeMs":0}
  1. 보시다시피 쿼리는 새로 추가된 컬럼에 대해 defaultNullValue를 반환해요. 이 컬럼을 실제 값으로 채우려면 과거 날짜에 대한 배치 수집 작업을 다시 실행해 데이터를 백필하세요.

경고: 백필링은 실시간 테이블에서 동작하지 않아요. 같은 상대를 사용하는 오프라인 테이블을 추가해 실시간 테이블을 하이브리드 테이블로 변환한 다음, 오프라인 테이블을 백필해 새로 추가된 컬럼 값을 채울 수 있어요. 자세한 내용은 하이브리드 테이블을 참고하세요.

더 알아보기 (Learn more)