플러그인
플러그인 (Plugins)
Pinot의 플러그 앤 플레이 아키텍처를 다루는 문서예요. 0.3.X 릴리스부터 Pinot는 플러그 앤 플레이 아키텍처를 지원해요. 0.3.0 버전부터 Pinot는 스트리밍 서비스, 저장소 시스템, 입력 형식, 메트릭 제공자 같은 새 도구를 지원하도록 쉽게 확장할 수 있어요.
출처: 문서
본문
0.3.X 릴리스부터 Pinot는 플러그 앤 플레이 아키텍처를 지원해요. 즉 0.3.0부터 Pinot는 스트리밍 서비스, 저장소 시스템, 입력 형식, 메트릭 제공자 같은 새 도구를 지원하도록 쉽게 확장할 수 있어요.
플러그인은 용도에 따라 폴더로 분류돼요. Pinot는 플러그인을 열한 개의 플러그인 패밀리로 구성하며, 각각 특정 확장성 요구를 겨냥해요. 아래 표는 모든 패밀리, 해당 SPI 모듈, Pinot와 함께 제공되는 구현을 요약해요.
한눈에 보는 플러그인 패밀리 (Plugin Families at a Glance)
| 플러그인 패밀리 | SPI 인터페이스 / 모듈 | 내장 구현 |
|---|---|---|
| 입력 형식 (Input Format) | RecordReader / StreamMessageDecoder |
Avro, CSV, JSON, ORC, Parquet, Thrift, Protobuf, Arrow, CLP-Log, Confluent Avro, Confluent JSON, Confluent Protobuf |
| 파일 시스템 (Filesystem) | PinotFS |
S3, GCS, HDFS, ADLS |
| 스트림 수집 (Stream Ingestion) | StreamConsumerFactory |
Kafka 3.0, Kafka 4.0, Kinesis, Pulsar |
| 배치 수집 (Batch Ingestion) | IngestionJobRunner |
Standalone, Hadoop, Spark 3 |
| 메트릭 (Metrics) | PinotMetricsFactory |
Dropwizard, Yammer, Compound |
| 세그먼트 라이터 (Segment Writer) | SegmentWriter |
파일 기반 |
| 세그먼트 업로더 (Segment Uploader) | SegmentUploader |
Default |
| 미니언 태스크 (Minion Tasks) | PinotTaskGenerator / PinotTaskExecutor |
MergeRollup, Purge, RealtimeToOfflineSegments, SegmentGenerationAndPush, UpsertCompaction, UpsertCompactMerge, RefreshSegment |
| 환경 (Environment) | PinotEnv |
Azure |
| 시계열 언어 (Time Series Language) | TimeSeriesLogicalPlanner |
M3QL |
| OpChain 변환기 (OpChain Converter) | OpChainConverter |
Default |
입력 형식 (Input Format)
입력 형식 플러그인은 데이터 수집 중 파일·스트림에서 데이터를 읽어요. 배치 수집은 RecordReader 구현을, 실시간 수집은 StreamMessageDecoder 구현을 사용해요.
파일 시스템 (Filesystem)
파일 시스템 플러그인은 저장소 추상화 계층을 제공해서 Pinot 세그먼트를 다양한 저장소 백엔드에 저장·가져올 수 있게 해줘요.
스트림 수집 (Stream Ingestion)
스트림 수집 플러그인은 Pinot가 실시간 스트리밍 플랫폼에서 데이터를 소비하게 해줘요.
배치 수집 (Batch Ingestion)
배치 수집 플러그인은 다양한 실행 프레임워크에서 데이터 수집 잡을 실행해요.
메트릭 (Metrics)
메트릭 플러그인은 Pinot가 JMX를 통해 내부 메트릭을 수집·노출하는 데 사용할 메트릭 라이브러리를 제어해요. Pinot는 Dropwizard(기본), Yammer, 그리고 여러 레지스트리에 동시에 팬아웃할 수 있는 Compound 구현과 함께 제공돼요.
세그먼트 라이터 (Segment Writer)
세그먼트 라이터 플러그인은 전체 배치 수집 잡을 거치지 않고 GenericRow 레코드를 프로그래밍 방식으로 모아 Pinot 세그먼트를 구축할 수 있는 API를 제공해요. 내장된 파일 기반 구현은 로컬 디스크에 행을 Avro 레코드로 버퍼링해요.
세그먼트 업로더 (Segment Uploader)
세그먼트 업로더 플러그인은 완성된 세그먼트 tar 파일을 Pinot 클러스터에 업로드하는 작업을 처리해요. 기본 구현은 테이블 구성의 batchConfigMaps로 설정된 모든 푸시 모드를 지원해요.
미니언 태스크 (Minion Tasks)
미니언 태스크 플러그인은 Pinot 미니언 노드에서 실행되는 백그라운드 처리 태스크를 정의해요. 내장 태스크에는 세그먼트 병합/롤업, purge, 실시간-오프라인 변환, upsert 컴팩션 등이 있어요.
환경 (Environment)
환경 플러그인은 Pinot가 클라우드별 기능과 구성에 통합되게 해줘요. Azure 환경 플러그인은 Azure 특정 기능을 제공해요.
시계열 언어 (Time Series Language)
시계열 언어 플러그인은 Pinot가 PromQL, M3QL 같은 커스텀 시계열 쿼리 언어를 지원하게 해줘요.
OpChain 변환기 (OpChain Converter)
OpChain 변환기 플러그인은 다중 스테이지 쿼리 엔진에서 논리적 쿼리 계획을 실행 가능한 OpChain 객체로 변환하는 커스텀 구현을 제공해요. 이를 통해 대체 실행 백엔드와 계획-실행 전략을 가능하게 해요.
플러그인 개발 (Developing Plugins)
플러그인은 제약 없이 개발할 수 있어요. 다만 따라야 할 표준이 몇 가지 있어요. 플러그인은 pinot-spi의 인터페이스를 구현해야 해요.
플래너 규칙 커스터마이저 (Planner rule customizers)
다중 스테이지 쿼리 엔진은 브로커 측 Calcite 규칙 커스터마이제이션을 위한 고급 플래너 SPI도 노출해요. Pinot의 단계별 논리적 플래닝 파이프라인에서 규칙을 추가·제거·재정렬·교체해야 할 때 pinot-query-planner-spi의 org.apache.pinot.query.planner.spi.RuleSetCustomizer를 구현해요.
발견(Discovery)은 Java ServiceLoader를 사용해요. Pinot는 먼저 브로커 애플리케이션 클래스패스에서 RuleSetCustomizer 구현을 로드한 다음, 로드된 각 플러그인 클래스로더를 스캔해요. 플러그인 JAR은 표준 Pinot 플러그인 패키징에 더해 구현 클래스를 나열하는 META-INF/services/org.apache.pinot.query.planner.spi.RuleSetCustomizer 파일을 포함해야 해요.
규칙 매칭 시점에 플래너 규칙은 Calcite 플래너 컨텍스트를 통해 쿼리별 플래너 옵션을 읽을 수 있어요. Pinot는 두 플래너 변형 모두에서 PlannerContext를 노출하므로 규칙 코드가 직접 unwrap해 getOptions()를 검사할 수 있어요:
PlannerContext plannerContext =
call.getPlanner().getContext().unwrap(PlannerContext.class);
String workerRuntime = plannerContext.getOptions().get("workerRuntime");
이 경로는 쿼리 범위 플래너 동작에 사용해요. 브로커 전체 기본값이 필요한 규칙은 같은 Calcite 컨텍스트에서 QueryEnvironment.Config를 여전히 unwrap할 수 있어요.
초기화는 일회성이에요. PinotRuleSet.defaultInstance()가 브로커 프로세스 전체 규칙 집합을 지연 빌드하고, 발견된 각 커스터마이저는 Pinot가 해당 규칙 목록을 이후 프로세스 동안 고정하기 전에 각 플래너 Phase마다 한 번 실행돼요. 브로커 시작 전에 플러그인을 로드하고, 플래너 규칙 플러그인을 추가·변경한 후에는 브로커를 재시작해요.
이 SPI는 안정적인 수집·파일 시스템·메트릭 플러그인 패밀리보다 업그레이드에 더 민감해요. Pinot는 이진 호환성을 위해 Phase를 append-only로 유지하지만, 릴리스 사이에 새 단계가 추가되고 내장 규칙 순서가 변경될 수 있어요. 특히 특정 내장 규칙 이름이나 순서에 의존한다면 매 Pinot 업그레이드마다 커스터마이저를 재검증해요.
구체화 뷰 DDL 핸들러 (Materialized view DDL handlers)
컨트롤러 관리 CREATE MATERIALIZED VIEW ... AS <query>에도 고급 확장 지점이 있어요. 다운스트림 배포가 내장된 단일 소스 MaterializedViewTask 경로와 다른 구체화 뷰 엔진 계약을 필요로 할 때 pinot-sql-ddl의 org.apache.pinot.sql.ddl.compile.MaterializedViewDdlHandler를 구현해요.
핸들러는 세 가지 결정을 소유해요:
validateDefinedQuery(...)는AS <query>형태가 대상 엔진에 유효한지 결정해요.supportsSchemaInference(...)는 DDL이 명시적 컬럼 목록을 생략할 때 Pinot가SELECT프로젝션에서 MV 컬럼을 추론할 수 있는지 결정해요.applyTaskConfig(...)는 MV 속성을TableConfigBuilder에 라우팅하고 테이블에 찍힌 태스크 타입을 반환해요.
컨트롤러 시작 시 DdlCompiler.setMaterializedViewDdlHandler(...)를 통해 핸들러를 한 번 등록해요. 핸들러가 등록되지 않으면 Pinot는 기본 동작을 유지해요: JOIN이 거부되고, 단일 소스 경로에 대한 스키마 추론이 허용되며, MV가 MaterializedViewTask로 실행돼요.
커스텀 핸들러가 내장 MaterializedViewTask가 아닌 태스크 타입을 찍으면 그 태스크 타입의 런타임 계약도 소유해요. 실제로는 커스텀 태스크 생성기/실행기, 검증 규칙, 그리고 대체 MV 구현이 요구하는 정의 메타데이터 영속성·일관성 추적을 의미해요.
기본 OSS 구체화 뷰 표면은 구체화 뷰를 보세요.
WorkerManager 리프 스테이지 세그먼트 훅 (WorkerManager leaf-stage segment hooks)
고급 다중 스테이지 라우팅 커스터마이제이션은 org.apache.pinot.query.routing.WorkerManager를 하위 클래스화할 수도 있어요. Pinot는 이미 하위 클래스가 getCandidateServers(...)와 getCandidateServersForReplicatedLeaf(...)를 통해 워커 배치에 영향을 줄 수 있게 해줘요. Pinot 1.6.0은 리프 스테이지 세그먼트 할당이 빌드된 후 실행되는 두 개의 후기 훅을 추가해요:
- 일반 리프 스테이지 할당 경로용
filterLeafStageSegments(...) - 복제 또는 브로드캐스트 리프 스테이지 할당용
filterReplicatedLeafStageSegments(...)
이 훅은 DispatchablePlanContext와 DispatchablePlanMetadata를 받으므로 하위 클래스가 getWorkerIdToSegmentsMap() 또는 getReplicatedSegments()를 검사·재작성한 다음 기존 세터로 조정된 할당을 다시 쓸 수 있어요. 쿼리 범위 라우팅 선택은 여전히 DispatchablePlanContext.getPlannerContext().getOptions()에서 요청 옵션을 읽을 수 있어요.
이것을 안정적인 독립 플러그인 패밀리가 아니라 업그레이드에 민감한 코드 수준 확장으로 취급해요. 특히 특정 리프 스테이지 플래닝 경로나 DispatchablePlanMetadata 형태에 의존한다면 매 Pinot 업그레이드마다 커스텀 WorkerManager 하위 클래스를 재검증해요.
세그먼트 서빙 수명주기 콜백 (Segment serving lifecycle callback)
커스텀 세그먼트·저장소 구현은 IndexSegment.onSegmentAdded() 또는 SegmentDirectory.onSegmentAdded()를 오버라이드해 등록 후 작업을 실행할 수 있어요. Pinot는 세그먼트가 등록되고 서버의 서빙 집합에 교체된 후 IndexSegment 콜백을 호출하므로, 쿼리가 이미 세그먼트에 도달할 수 있어요. 내장 불변 세그먼트 구현은 이 콜백을 SegmentDirectory에 전달해요.
두 메서드 모두 기본적으로 no-op이에요. Pinot는 각 세그먼트 인스턴스에 대해 콜백을 많아야 한 번, 성공적인 등록 후에만 호출해요. Best-effort로 취급해요: 예외는 기록되며 등록을 롤백하거나 Helix ONLINE 상태 전환을 실패시키지 않아요.
콜백은 세그먼트 등록 스레드(Helix 상태 전환 스레드)에서 인라인으로 실행돼요. 빨리 반환하고, 원격 I/O는 타임아웃으로 제한하거나 비동기로 수행해요. 비동기 태스크는 콜백 반환 후 세그먼트나 디렉토리가 살아 있다고 가정해서는 안 돼요.
pinot-segment-spi에 의존하는 커스텀 세그먼트·인덱스 확장은 위에서 나열한 안정적인 플러그인 패밀리와는 별개의, 더 업그레이드에 민감한 경로예요. 매 Pinot 업그레이드마다 이 확장을 재검증해요. 예를 들어 Pinot 1.6.0은 커스텀 인덱스 구현에 두 개의 필수 IndexType 메서드인 requiresDictionary(FieldSpec, C)와 shouldInvalidateOnDictionaryChange(FieldSpec, C)를 추가해요. 커스텀 세그먼트/인덱스 확장을 업그레이드하기 전에 업그레이드 노트를 보세요.
현재 latest 브랜치는 커스텀 JSON 인덱스 리더와 SPI를 직접 호출하는 코드의 JsonIndexReader 계약도 강화해요: getMatchingDocIds(...)는 이제 ImmutableRoaringBitmap을 반환하고, Pinot는 그 결과를 읽기 전용으로 취급해요. 커스텀 리더는 인덱스의 기반 저장소에서 빌린 비트맵을 반환할 수 있으므로 호출자는 이를 변경하면 안 되고, 제자리 변경 전에 toMutableRoaringBitmap()으로 복사해야 해요. 기존 리더는 공변 오버라이드를 통해 여전히 MutableRoaringBitmap을 반환할 수 있지만, 커스텀 JSON 인덱스 확장은 매 업그레이드마다 재검증해야 해요.