아래 방향을 권장합니다.
Inventory Parquet는 “landing raw data”로 별도 bucket에 보관하고, AIStor Table에는 후처리된 Iceberg table을 적재하는 구조가 좋습니다. 이유는 AIStor Tables가 Iceberg table의 warehouse/table location을 AIStor가 관리하는 방식이고, table 생성 시 custom location 지정이 제한되어 있으므로 Inventory가 생성한 Parquet 파일을 “그대로 AIStor Table location으로 등록”하는 방식보다는, manifest를 읽어 검증한 뒤 managed table로 append하는 방식이 안전합니다. AIStor Tables는 외부 Hive Metastore/Glue 없이 AIStor 내부에서 Iceberg table/catalog를 제공하며, create table API는 schema와 optional partitioning을 받습니다. (MinIO AIStor Documentation)
[AIStor Source Bucket]
|
| 1) Inventory API / mc inventory
v
[Inventory Landing Bucket]
inventory-landing/
<source-bucket>/<job-id>/<run-start-time>/
manifest.json
files/file-001.parquet[.zst]
files/file-002.parquet[.zst]
|
| 2) ETL: manifest 확인, parquet read, schema normalize, prefix 추출
v
[AIStor Tables Warehouse]
inventory_mart
└── namespace: inventory
├── inventory_run
├── object_inventory_daily_raw
├── prefix_usage_daily
├── prefix_usage_by_attr_daily
├── pattern_registry
└── pattern_usage_daily
|
| 3) Trino / Spark / PyIceberg / API
v
[Prefix / Pattern 사용량 조회 서비스]
Inventory output은 실행마다 timestamped folder, files/, manifest.json 구조로 생성되고, manifest에는 output file 목록, schema, file checksum, job status, scanned/matched object 수, partial result 여부가 들어갑니다. 따라서 ETL은 반드시 manifest.json을 먼저 읽고 status=completed, partialResultsAvailable=false인 실행만 AIStor Table에 반영해야 합니다. (MinIO AIStor Documentation)
대규모 후처리 목적이면 parquet + compression on을 기본으로 두는 것이 좋습니다. MinIO AIStor Inventory는 CSV, JSON, Parquet를 지원하고, compression을 켜면 ZSTD를 사용합니다. Parquet는 Spark, Presto, Trino 같은 분석 엔진과 직접 연계하기 쉽습니다. (MinIO AIStor Documentation)
예시:
apiVersion: v1
id: daily-full-inventory-current
destination:
bucket: inventory-landing
prefix: source-bucket-name/
format: parquet
compression: on
schedule: daily
mode: fast
# 운영 목적에 따라 선택
# current: 현재 객체 기준 통계
# all: versioning 사용 시 실제 저장 용량/버전/삭제마커 분석까지 가능
versions: all
includeFields:
- ETag
- StorageClass
- IsMultipart
- EncryptionStatus
- ReplicationStatus
- Tags
- UserMetadata
- AccessTime
- Tier
- TieringStatus
versions: current는 현재 버전 중심의 “사용자가 보는 객체 수/크기”에 적합하고, versions: all은 versioning bucket에서 non-current version, delete marker, 실제 저장 사용량 분석까지 필요할 때 적합합니다. versions: all일 때 Inventory report에는 VersionID, IsDeleteMarker, IsLatest 같은 version-specific field가 포함됩니다. (MinIO AIStor Documentation)
fast vs strict10PiB 이상, 수십억 객체 환경에서는 우선 fast로 시작하고, 결과 신뢰도와 실행 시간을 본 뒤 일부 중요 bucket에만 strict를 적용하는 방식이 현실적입니다. 다만 Inventory 문서상 fast와 strict 모두 job 실행 중 수정된 object가 report에 포함되지 않을 수 있으므로, 결과는 “정확한 transaction snapshot”이라기보다 “일 단위 운영 통계 snapshot”으로 보는 것이 맞습니다. (MinIO AIStor Documentation)
daily schedule은 고정된 임의 시각 지정 방식이 아니라, scheduler 규칙에 따라 동작합니다. 문서상 daily 반복 job은 이전 실행 완료 이후 다음 UTC 00:00 기준으로 계산되므로, 한국 시간으로는 대략 오전 9시 실행 패턴이 됩니다. 특정 업무 외 시간에 정확히 맞추고 싶다면 schedule: once job을 Airflow/Jenkins/Kubernetes CronJob 등에서 원하는 시각에 생성/실행하고, job id에 날짜를 넣어 관리하는 방식이 더 통제하기 쉽습니다. (MinIO AIStor Documentation)
예:
apiVersion: v1
id: once-inventory-20260515
destination:
bucket: inventory-landing
prefix: source-bucket-name/
format: parquet
compression: on
schedule: once
mode: fast
versions: all
includeFields:
- StorageClass
- Tier
- TieringStatus
- Tags
- UserMetadata
전체 bucket의 prefix별 통계를 만들려면 bucket 전체 Inventory가 필요합니다. 다만 특정 대형 prefix만 관리 대상이라면 Inventory configuration의 filters.prefix를 사용할 수 있습니다. MinIO 문서상 prefix filter는 subset 선택에 가장 효율적인 filter type으로 설명되어 있습니다. (MinIO AIStor Documentation)
filters:
prefix:
- "team-a/"
- "team-b/project-x/"
단, prefix filter job을 너무 많이 만들면 같은 bucket에 대한 scan job이 많아져 운영 부하가 커질 수 있습니다. “전체 daily 1개 + 중요 prefix 전용 보조 job 소수” 정도로 시작하는 편이 좋습니다.
Raw table만으로 사용자가 prefix 사용량을 조회하게 만들면 안 됩니다. 수십억 object table에서 매번 WHERE key LIKE 'abc/%' 또는 regex를 수행하면 비용이 큽니다. Raw table은 감사, 검증, 재처리용으로 두고, 실제 서비스 조회는 prefix_usage_daily, pattern_usage_daily 같은 aggregate table을 보게 해야 합니다.
권장 table 역할은 다음과 같습니다.
| Table | 목적 | 보관 기간 |
|---|---|---|
inventory_run | Inventory 실행 이력, manifest path, scanned/matched/records 검증 | 장기 |
object_inventory_daily_raw | object 단위 raw snapshot. 재처리, 정합성 검증, 상세 추적 | 7~30일 또는 90일 |
prefix_usage_daily | prefix별 object count, byte, version/delete marker 통계 | 1~3년 |
prefix_usage_by_attr_daily | prefix + storage class/tier/tag 등 attribute별 통계 | 1~3년 |
pattern_registry | 관리 대상 prefix/pattern 정의 장부 | 장기 |
pattern_usage_daily | 등록된 pattern별 사용량 통계 | 1~3년 |
AIStor Tables는 warehouse → namespace → table 순서로 생성하며, warehouse는 table data/metadata 저장용 bucket을 생성하고 해당 bucket은 AIStor Tables API 또는 mc table 명령으로 관리하는 방식입니다. (MinIO AIStor Documentation)
mc table warehouse create myaistor inventory_mart
mc table namespace create myaistor inventory_mart inventory
inventory_runInventory 실행 단위의 control/audit table입니다. ETL은 이 table을 기준으로 idempotent하게 동작해야 합니다.
{
"name": "inventory_run",
"schema": {
"type": "struct",
"fields": [
{"id": 1, "name": "run_id", "type": "string", "required": true},
{"id": 2, "name": "source_bucket", "type": "string", "required": true},
{"id": 3, "name": "inventory_job_id", "type": "string", "required": true},
{"id": 4, "name": "snapshot_date", "type": "date", "required": true},
{"id": 5, "name": "run_started_at_utc", "type": "timestamp", "required": false},
{"id": 6, "name": "run_completed_at_utc", "type": "timestamp", "required": false},
{"id": 7, "name": "status", "type": "string", "required": true},
{"id": 8, "name": "manifest_path", "type": "string", "required": true},
{"id": 9, "name": "file_format", "type": "string", "required": false},
{"id": 10, "name": "file_schema", "type": "string", "required": false},
{"id": 11, "name": "scanned_count", "type": "long", "required": false},
{"id": 12, "name": "matched_count", "type": "long", "required": false},
{"id": 13, "name": "records_written", "type": "long", "required": false},
{"id": 14, "name": "output_files_count", "type": "long", "required": false},
{"id": 15, "name": "partial_results_available", "type": "boolean", "required": false},
{"id": 16, "name": "num_errors", "type": "long", "required": false},
{"id": 17, "name": "execution_time", "type": "string", "required": false},
{"id": 18, "name": "loaded_at_utc", "type": "timestamp", "required": true}
]
},
"partition-spec": [
{"name": "snapshot_date", "transform": "identity", "source-id": 4, "field-id": 1000},
{"name": "source_bucket", "transform": "identity", "source-id": 2, "field-id": 1001}
],
"properties": {
"owner": "platform-storage",
"description": "Inventory job execution metadata and manifest lineage"
}
}
Inventory status API는 scannedCount, matchedCount, recordsWritten, outputFilesCount, executionTime, manifestPath, numErrors 등을 제공하므로 이 값을 그대로 저장하면 ETL 검증에 유용합니다. (MinIO AIStor Documentation)
object_inventory_daily_rawInventory 결과를 normalized 형태로 저장하는 object 단위 raw table입니다. 이 table은 사용자 조회용이 아니라 재집계, 검증, 장애 분석, 특정 object 추적용입니다.
{
"name": "object_inventory_daily_raw",
"schema": {
"type": "struct",
"fields": [
{"id": 1, "name": "snapshot_date", "type": "date", "required": true},
{"id": 2, "name": "run_id", "type": "string", "required": true},
{"id": 3, "name": "source_bucket", "type": "string", "required": true},
{"id": 4, "name": "object_key", "type": "string", "required": true},
{"id": 5, "name": "key_depth", "type": "int", "required": false},
{"id": 6, "name": "prefix_l1", "type": "string", "required": false},
{"id": 7, "name": "prefix_l2", "type": "string", "required": false},
{"id": 8, "name": "prefix_l3", "type": "string", "required": false},
{"id": 9, "name": "prefix_l4", "type": "string", "required": false},
{"id": 10, "name": "prefix_l5", "type": "string", "required": false},
{"id": 11, "name": "prefix_l6", "type": "string", "required": false},
{"id": 12, "name": "file_ext", "type": "string", "required": false},
{"id": 13, "name": "size_bytes", "type": "long", "required": true},
{"id": 14, "name": "last_modified_at_utc", "type": "timestamp", "required": false},
{"id": 15, "name": "sequence_number", "type": "string", "required": false},
{"id": 16, "name": "version_id", "type": "string", "required": false},
{"id": 17, "name": "is_latest", "type": "boolean", "required": false},
{"id": 18, "name": "is_delete_marker", "type": "boolean", "required": false},
{"id": 19, "name": "etag", "type": "string", "required": false},
{"id": 20, "name": "storage_class", "type": "string", "required": false},
{"id": 21, "name": "tier", "type": "string", "required": false},
{"id": 22, "name": "tiering_status", "type": "string", "required": false},
{"id": 23, "name": "is_multipart", "type": "boolean", "required": false},
{"id": 24, "name": "encryption_status", "type": "string", "required": false},
{"id": 25, "name": "replication_status", "type": "string", "required": false},
{"id": 26, "name": "tags_qs", "type": "string", "required": false},
{"id": 27, "name": "user_metadata_qs", "type": "string", "required": false},
{"id": 28, "name": "access_time_utc", "type": "timestamp", "required": false},
{"id": 29, "name": "source_file", "type": "string", "required": false},
{"id": 30, "name": "loaded_at_utc", "type": "timestamp", "required": true}
]
},
"partition-spec": [
{"name": "snapshot_date", "transform": "identity", "source-id": 1, "field-id": 1000},
{"name": "source_bucket", "transform": "identity", "source-id": 3, "field-id": 1001},
{"name": "object_key_bucket", "transform": "bucket[256]", "source-id": 4, "field-id": 1002}
],
"properties": {
"owner": "platform-storage",
"description": "Daily raw object inventory snapshot from AIStor Inventory"
}
}
Inventory 기본 field는 Bucket, Key, SequenceNumber, Size, LastModifiedDate이고, optional field로 ETag, StorageClass, IsMultipart, EncryptionStatus, Tags, UserMetadata, AccessTime, ReplicationStatus, Tier, TieringStatus 등을 추가할 수 있습니다. (MinIO AIStor Documentation)
object_key_bucket partition은 prefix 조회 pruning에는 직접 도움이 되지 않지만, raw table의 data file 분산과 대규모 append/read 안정성에는 도움이 됩니다. 실제 prefix 조회는 아래 aggregate table을 사용해야 합니다. Iceberg partition transform은 identity, bucket[N], truncate[W], year, month, day, hour 등을 지원합니다. (Apache Iceberg)
prefix_usage_daily실제 사용자 조회용 핵심 table입니다. 특정 날짜, bucket, prefix에 대해 object 수와 크기를 바로 반환합니다.
{
"name": "prefix_usage_daily",
"schema": {
"type": "struct",
"fields": [
{"id": 1, "name": "snapshot_date", "type": "date", "required": true},
{"id": 2, "name": "source_bucket", "type": "string", "required": true},
{"id": 3, "name": "prefix_depth", "type": "int", "required": true},
{"id": 4, "name": "prefix_path", "type": "string", "required": true},
{"id": 5, "name": "prefix_l1", "type": "string", "required": false},
{"id": 6, "name": "prefix_l2", "type": "string", "required": false},
{"id": 7, "name": "prefix_l3", "type": "string", "required": false},
{"id": 8, "name": "prefix_l4", "type": "string", "required": false},
{"id": 9, "name": "prefix_l5", "type": "string", "required": false},
{"id": 10, "name": "prefix_l6", "type": "string", "required": false},
{"id": 11, "name": "current_object_count", "type": "long", "required": true},
{"id": 12, "name": "current_size_bytes", "type": "long", "required": true},
{"id": 13, "name": "stored_version_count", "type": "long", "required": false},
{"id": 14, "name": "stored_size_bytes", "type": "long", "required": false},
{"id": 15, "name": "delete_marker_count", "type": "long", "required": false},
{"id": 16, "name": "multipart_object_count", "type": "long", "required": false},
{"id": 17, "name": "small_object_count", "type": "long", "required": false},
{"id": 18, "name": "large_object_count", "type": "long", "required": false},
{"id": 19, "name": "min_size_bytes", "type": "long", "required": false},
{"id": 20, "name": "max_size_bytes", "type": "long", "required": false},
{"id": 21, "name": "avg_size_bytes", "type": "double", "required": false},
{"id": 22, "name": "first_modified_at_utc", "type": "timestamp", "required": false},
{"id": 23, "name": "last_modified_at_utc", "type": "timestamp", "required": false},
{"id": 24, "name": "run_id", "type": "string", "required": true},
{"id": 25, "name": "loaded_at_utc", "type": "timestamp", "required": true}
]
},
"partition-spec": [
{"name": "snapshot_date", "transform": "identity", "source-id": 1, "field-id": 1000},
{"name": "source_bucket", "transform": "identity", "source-id": 2, "field-id": 1001},
{"name": "prefix_depth", "transform": "identity", "source-id": 3, "field-id": 1002},
{"name": "prefix_path_bucket", "transform": "bucket[128]", "source-id": 4, "field-id": 1003}
],
"properties": {
"owner": "platform-storage",
"description": "Daily aggregated object and byte usage by prefix"
}
}
current_*와 stored_*를 분리하는 것이 중요합니다.
current_object_count = IsLatest=true AND IsDeleteMarker=false 기준 객체 수
current_size_bytes = IsLatest=true AND IsDeleteMarker=false 기준 크기
stored_version_count = IsDeleteMarker=false 기준 전체 version 수
stored_size_bytes = IsDeleteMarker=false 기준 전체 version size 합계
delete_marker_count = IsDeleteMarker=true 기준 delete marker 수
이렇게 분리해야 “사용자가 현재 보는 객체 수”와 “versioning 때문에 실제 저장소를 차지하는 용량”을 동시에 설명할 수 있습니다.
prefix_usage_by_attr_dailyStorageClass, Tier, ReplicationStatus, EncryptionStatus, Tag 같은 attribute별 상세 분석용입니다. prefix_usage_daily에 map column을 넣는 방법도 가능하지만, Trino/Spark 조회와 집계 편의성을 생각하면 normalized table이 더 운영하기 쉽습니다.
{
"name": "prefix_usage_by_attr_daily",
"schema": {
"type": "struct",
"fields": [
{"id": 1, "name": "snapshot_date", "type": "date", "required": true},
{"id": 2, "name": "source_bucket", "type": "string", "required": true},
{"id": 3, "name": "prefix_depth", "type": "int", "required": true},
{"id": 4, "name": "prefix_path", "type": "string", "required": true},
{"id": 5, "name": "attr_type", "type": "string", "required": true},
{"id": 6, "name": "attr_key", "type": "string", "required": false},
{"id": 7, "name": "attr_value", "type": "string", "required": true},
{"id": 8, "name": "current_object_count", "type": "long", "required": true},
{"id": 9, "name": "current_size_bytes", "type": "long", "required": true},
{"id": 10, "name": "stored_version_count", "type": "long", "required": false},
{"id": 11, "name": "stored_size_bytes", "type": "long", "required": false},
{"id": 12, "name": "run_id", "type": "string", "required": true},
{"id": 13, "name": "loaded_at_utc", "type": "timestamp", "required": true}
]
},
"partition-spec": [
{"name": "snapshot_date", "transform": "identity", "source-id": 1, "field-id": 1000},
{"name": "source_bucket", "transform": "identity", "source-id": 2, "field-id": 1001},
{"name": "attr_type", "transform": "identity", "source-id": 5, "field-id": 1002},
{"name": "prefix_path_bucket", "transform": "bucket[128]", "source-id": 4, "field-id": 1003}
],
"properties": {
"owner": "platform-storage",
"description": "Daily prefix usage broken down by storage class, tier, tag, metadata, encryption, replication"
}
}
예시 row:
| attr_type | attr_key | attr_value |
|---|---|---|
storage_class | null | STANDARD |
tier | null | WARM |
tiering_status | null | transitioned |
replication_status | null | COMPLETED |
tag | project | abc |
metadata | x-amz-meta-owner | team-a |
pattern_registry임의 regex/pattern 조회를 매번 raw table에 날리는 것은 매우 비쌉니다. 사용자가 자주 조회하는 prefix, glob, regex, tag 조건은 registry에 등록하고 daily batch에서 사전 계산하는 방식이 좋습니다.
{
"name": "pattern_registry",
"schema": {
"type": "struct",
"fields": [
{"id": 1, "name": "pattern_id", "type": "string", "required": true},
{"id": 2, "name": "pattern_name", "type": "string", "required": true},
{"id": 3, "name": "source_bucket", "type": "string", "required": true},
{"id": 4, "name": "matcher_type", "type": "string", "required": true},
{"id": 5, "name": "pattern_expr", "type": "string", "required": true},
{"id": 6, "name": "owner_team", "type": "string", "required": false},
{"id": 7, "name": "description", "type": "string", "required": false},
{"id": 8, "name": "enabled", "type": "boolean", "required": true},
{"id": 9, "name": "created_at_utc", "type": "timestamp", "required": true},
{"id": 10, "name": "updated_at_utc", "type": "timestamp", "required": false}
]
},
"partition-spec": [
{"name": "source_bucket", "transform": "identity", "source-id": 3, "field-id": 1000},
{"name": "matcher_type", "transform": "identity", "source-id": 4, "field-id": 1001}
],
"properties": {
"owner": "platform-storage",
"description": "Registered usage patterns for daily inventory aggregation"
}
}
matcher_type 예시는 다음 정도로 제한하는 것이 좋습니다.
prefix object_key starts with pattern_expr
glob Inventory name match와 유사한 glob
regex RE2 호환 regex 권장
tag tag key/value 조건
metadata user metadata key/value 조건
Inventory 자체도 name filter에서 glob, contains, regex를 지원하고, regex는 RE2 syntax를 따른다고 문서화되어 있습니다. 다만 Inventory job 단계에서 pattern별로 job을 많이 만들기보다는, 전체 Inventory를 한 번 수집한 뒤 ETL에서 registry 기반으로 pattern 집계를 만드는 방식이 일반적으로 더 관리하기 쉽습니다. (MinIO AIStor Documentation)
pattern_usage_daily{
"name": "pattern_usage_daily",
"schema": {
"type": "struct",
"fields": [
{"id": 1, "name": "snapshot_date", "type": "date", "required": true},
{"id": 2, "name": "source_bucket", "type": "string", "required": true},
{"id": 3, "name": "pattern_id", "type": "string", "required": true},
{"id": 4, "name": "pattern_name", "type": "string", "required": true},
{"id": 5, "name": "matcher_type", "type": "string", "required": true},
{"id": 6, "name": "pattern_expr", "type": "string", "required": true},
{"id": 7, "name": "current_object_count", "type": "long", "required": true},
{"id": 8, "name": "current_size_bytes", "type": "long", "required": true},
{"id": 9, "name": "stored_version_count", "type": "long", "required": false},
{"id": 10, "name": "stored_size_bytes", "type": "long", "required": false},
{"id": 11, "name": "delete_marker_count", "type": "long", "required": false},
{"id": 12, "name": "first_modified_at_utc", "type": "timestamp", "required": false},
{"id": 13, "name": "last_modified_at_utc", "type": "timestamp", "required": false},
{"id": 14, "name": "run_id", "type": "string", "required": true},
{"id": 15, "name": "loaded_at_utc", "type": "timestamp", "required": true}
]
},
"partition-spec": [
{"name": "snapshot_date", "transform": "identity", "source-id": 1, "field-id": 1000},
{"name": "source_bucket", "transform": "identity", "source-id": 2, "field-id": 1001},
{"name": "pattern_id", "transform": "identity", "source-id": 3, "field-id": 1002}
],
"properties": {
"owner": "platform-storage",
"description": "Daily usage statistics for registered prefix/glob/regex/tag patterns"
}
}
S3 object key는 실제 directory가 아니므로, prefix 규칙을 명확히 정해야 합니다.
권장 규칙:
object_key: "team-a/project-x/date=2026-05-15/file.parquet"
prefix_depth=0, prefix_path=""
prefix_depth=1, prefix_path="team-a/"
prefix_depth=2, prefix_path="team-a/project-x/"
prefix_depth=3, prefix_path="team-a/project-x/date=2026-05-15/"
주의할 점:
1. prefix_path는 항상 trailing slash 포함
2. root는 empty string 또는 "/" 중 하나로 고정
3. object_key 자체가 "/"로 끝나는 folder marker object도 object로 계산할지 정책 결정
4. "//", URL-encoded 문자, 한글/특수문자 key 처리 규칙 정의
5. prefix_depth 최대값을 제한
수십억 object에서 모든 key를 depth 10까지 explode하면 중간 데이터가 수백억 row가 될 수 있습니다. 따라서 다음 방식이 현실적입니다.
기본 precompute:
depth 0~3 또는 0~4 전체 prefix 집계
상세 precompute:
pattern_registry에 등록된 주요 prefix/pattern만 집계
ad-hoc 분석:
object_inventory_daily_raw를 Spark/Trino로 직접 분석하되, 운영자/배치 용도로 제한
즉, “모든 가능한 prefix를 모든 depth로 매일 사전 계산”하기보다는, 상위 depth 전체 + 등록형 상세 prefix/pattern 구조가 더 안정적입니다.
mc inventory status로 job 상태 확인manifestPath 획득manifest.json readpartialResultsAvailable=false 확인files[] 목록만 readInventory API는 bucket-scoped이고, configuration 생성/조회/삭제/status 조회 API를 제공합니다. status 응답에는 state, startTime, endTime, scannedCount, matchedCount, recordsWritten, outputFilesCount, manifestPath 등이 포함됩니다. (MinIO AIStor Documentation)
매일 다음 검증을 넣는 것이 좋습니다.
-- 1) run manifest records와 raw 적재 row 수 비교
SELECT
r.run_id,
r.records_written,
COUNT(*) AS raw_rows
FROM inventory.inventory_run r
JOIN inventory.object_inventory_daily_raw o
ON r.run_id = o.run_id
WHERE r.snapshot_date = DATE '2026-05-15'
GROUP BY r.run_id, r.records_written;
-- 2) root prefix와 raw 합계 비교
SELECT
p.snapshot_date,
p.source_bucket,
p.current_object_count,
raw.current_object_count AS raw_current_object_count,
p.current_size_bytes,
raw.current_size_bytes AS raw_current_size_bytes
FROM inventory.prefix_usage_daily p
JOIN (
SELECT
snapshot_date,
source_bucket,
COUNT_IF(COALESCE(is_delete_marker, false) = false
AND COALESCE(is_latest, true) = true) AS current_object_count,
SUM(CASE
WHEN COALESCE(is_delete_marker, false) = false
AND COALESCE(is_latest, true) = true
THEN size_bytes ELSE 0
END) AS current_size_bytes
FROM inventory.object_inventory_daily_raw
WHERE snapshot_date = DATE '2026-05-15'
GROUP BY snapshot_date, source_bucket
) raw
ON p.snapshot_date = raw.snapshot_date
AND p.source_bucket = raw.source_bucket
WHERE p.prefix_depth = 0
AND p.prefix_path = '';
SELECT
snapshot_date,
source_bucket,
prefix_path,
current_object_count,
current_size_bytes,
stored_version_count,
stored_size_bytes,
delete_marker_count,
ROUND(current_size_bytes / POWER(1024, 4), 2) AS current_tib,
ROUND(stored_size_bytes / POWER(1024, 4), 2) AS stored_tib
FROM inventory.prefix_usage_daily
WHERE source_bucket = 'lakehouse-prod'
AND prefix_depth = 3
AND prefix_path = 'team-a/project-x/table1/'
ORDER BY snapshot_date DESC;
SELECT
snapshot_date,
source_bucket,
prefix_depth,
prefix_path,
current_object_count,
ROUND(current_size_bytes / POWER(1024, 4), 2) AS current_tib
FROM inventory.prefix_usage_daily
WHERE snapshot_date = DATE '2026-05-15'
AND source_bucket = 'lakehouse-prod'
AND prefix_depth = 2
ORDER BY current_size_bytes DESC
LIMIT 100;
SELECT
prefix_path,
current_object_count,
small_object_count,
ROUND(100.0 * small_object_count / NULLIF(current_object_count, 0), 2) AS small_file_ratio_pct,
ROUND(current_size_bytes / POWER(1024, 3), 2) AS current_gib
FROM inventory.prefix_usage_daily
WHERE snapshot_date = DATE '2026-05-15'
AND source_bucket = 'lakehouse-prod'
AND prefix_depth = 3
ORDER BY small_file_ratio_pct DESC, small_object_count DESC
LIMIT 100;
SELECT
snapshot_date,
pattern_name,
matcher_type,
pattern_expr,
current_object_count,
ROUND(current_size_bytes / POWER(1024, 4), 2) AS current_tib
FROM inventory.pattern_usage_daily
WHERE snapshot_date = DATE '2026-05-15'
AND source_bucket = 'lakehouse-prod'
ORDER BY current_size_bytes DESC;
수십억 object라면 daily raw snapshot을 장기 보관하는 순간 table 자체가 너무 커집니다. 추천은 다음과 같습니다.
object_inventory_daily_raw:
7~30일 보관
장애 분석/재처리 기간만 유지
prefix_usage_daily:
1~3년 보관
사용자 조회, 추이 분석, 비용 배부에 사용
inventory_run:
장기 보관
감사/정합성/장애 분석에 사용
AIStor bucket metric과 Inventory 기반 prefix 통계는 다음 이유로 차이가 날 수 있습니다.
1. Inventory 실행 시각과 Prometheus metric scrape 시각 차이
2. versioning all/current 기준 차이
3. delete marker 포함/제외 기준 차이
4. ILM tiering 상태 반영 시점 차이
5. Inventory 실행 중 변경된 object 처리 차이
6. multipart, failed/incomplete upload, replication 상태 차이
특히 versioning bucket에서는 current_size_bytes와 stored_size_bytes를 반드시 분리해야 운영팀/사용자 간 해석 충돌을 줄일 수 있습니다.
Inventory raw에는 object key, tag, metadata가 들어가므로 민감 정보가 포함될 수 있습니다. 일반 사용자에게는 raw table 접근을 주지 말고, aggregate table 또는 view만 제공하는 것이 좋습니다. AIStor Tables는 warehouse, namespace, table 단위 PBAC를 사용하고, 정책 조건으로 namespace/tableName/viewName을 제한할 수 있습니다. (MinIO AIStor Documentation)
권장 권한 분리:
inventory-job-sa:
source bucket: s3:ListBucket
inventory config: s3:GetInventoryConfiguration, s3:PutInventoryConfiguration
landing bucket: write
inventory-etl-sa:
landing bucket: read
AIStor Tables: inventory namespace write
inventory-reader-sa:
prefix_usage_daily, pattern_usage_daily read
raw table read 불가
Inventory API 자체도 source bucket에 대한 s3:GetInventoryConfiguration, s3:PutInventoryConfiguration, s3:ListBucket, 그리고 admin control에는 admin:InventoryControl 권한이 필요합니다. (MinIO AIStor Documentation)
AIStor Tables는 Iceberg REST Catalog 호환 API를 제공하고, API base path는 /_iceberg/v1 형태입니다. 인증은 SigV4를 사용하며 service name은 s3tables입니다. mc table config는 Trino 또는 Spark catalog 설정 생성을 지원하므로, 운영 환경에서는 이 명령으로 query engine 설정을 표준화하는 것이 좋습니다. (MinIO AIStor Documentation)
mc table config myaistor inventory_mart --trino
mc table config myaistor inventory_mart --spark
가장 현실적인 1차 구축안은 다음입니다.
Inventory:
- bucket별 daily 또는 external once job
- format: parquet
- compression: on
- versions: all
- includeFields: StorageClass, Tier, TieringStatus, IsMultipart, Tags, UserMetadata, ReplicationStatus
Landing:
- inventory-landing/<source_bucket>/<job_id>/<run_timestamp>/manifest.json
- lifecycle로 raw inventory output 보관 기간 제한
AIStor Tables:
- warehouse: inventory_mart
- namespace: inventory
- tables:
inventory_run
object_inventory_daily_raw
prefix_usage_daily
prefix_usage_by_attr_daily
pattern_registry
pattern_usage_daily
Serving:
- 일반 조회는 prefix_usage_daily / pattern_usage_daily만 사용
- raw table은 운영자/재처리/감사용
- ad-hoc regex는 제한적으로 허용하거나 registry 등록 후 다음 daily batch에서 제공
핵심은 Inventory를 “대상 bucket의 object catalog snapshot 생성기”로 쓰고, AIStor Tables를 “운영 가능한 prefix/pattern usage mart”로 쓰는 것입니다. 이 구조가 10PiB 이상, 수십억 object 환경에서 조회 성능, 정합성 검증, 운영 권한 분리, 장기 보관 비용을 가장 균형 있게 가져갈 수 있습니다.