26M15f

Young-Kyoo Kim·2일 전

아래 방향을 권장합니다.

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)


1. 전체 아키텍처 권장안

[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)


2. Inventory Job 구성 시 고려사항

2.1 Inventory 출력 포맷

대규모 후처리 목적이면 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)

2.2 fast vs strict

10PiB 이상, 수십억 객체 환경에서는 우선 fast로 시작하고, 결과 신뢰도와 실행 시간을 본 뒤 일부 중요 bucket에만 strict를 적용하는 방식이 현실적입니다. 다만 Inventory 문서상 faststrict 모두 job 실행 중 수정된 object가 report에 포함되지 않을 수 있으므로, 결과는 “정확한 transaction snapshot”이라기보다 “일 단위 운영 통계 snapshot”으로 보는 것이 맞습니다. (MinIO AIStor Documentation)

2.3 스케줄 시간

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

2.4 prefix filter 사용 여부

전체 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 소수” 정도로 시작하는 편이 좋습니다.


3. AIStor Table 설계 원칙

핵심 원칙

Raw table만으로 사용자가 prefix 사용량을 조회하게 만들면 안 됩니다. 수십억 object table에서 매번 WHERE key LIKE 'abc/%' 또는 regex를 수행하면 비용이 큽니다. Raw table은 감사, 검증, 재처리용으로 두고, 실제 서비스 조회는 prefix_usage_daily, pattern_usage_daily 같은 aggregate table을 보게 해야 합니다.

권장 table 역할은 다음과 같습니다.

Table목적보관 기간
inventory_runInventory 실행 이력, manifest path, scanned/matched/records 검증장기
object_inventory_daily_rawobject 단위 raw snapshot. 재처리, 정합성 검증, 상세 추적7~30일 또는 90일
prefix_usage_dailyprefix별 object count, byte, version/delete marker 통계1~3년
prefix_usage_by_attr_dailyprefix + 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

4. Table Schema 상세 설계

4.1 inventory_run

Inventory 실행 단위의 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)


4.2 object_inventory_daily_raw

Inventory 결과를 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)


4.3 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 때문에 실제 저장소를 차지하는 용량”을 동시에 설명할 수 있습니다.


4.4 prefix_usage_by_attr_daily

StorageClass, 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_typeattr_keyattr_value
storage_classnullSTANDARD
tiernullWARM
tiering_statusnulltransitioned
replication_statusnullCOMPLETED
tagprojectabc
metadatax-amz-meta-ownerteam-a

4.5 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)


4.6 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"
  }
}

5. Prefix 집계 방식

5.1 Prefix canonicalization 규칙

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 최대값을 제한

5.2 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 구조가 더 안정적입니다.


6. ETL 처리 흐름

6.1 실행 단위

  1. Inventory status API 또는 mc inventory status로 job 상태 확인
  2. manifestPath 획득
  3. manifest.json read
  4. partialResultsAvailable=false 확인
  5. manifest의 files[] 목록만 read
  6. raw table append
  7. prefix aggregate 생성
  8. run table에 success 기록
  9. bucket metric과 root prefix 통계 비교

Inventory API는 bucket-scoped이고, configuration 생성/조회/삭제/status 조회 API를 제공합니다. status 응답에는 state, startTime, endTime, scannedCount, matchedCount, recordsWritten, outputFilesCount, manifestPath 등이 포함됩니다. (MinIO AIStor Documentation)

6.2 정합성 체크

매일 다음 검증을 넣는 것이 좋습니다.

-- 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 = '';

7. 조회 예시

7.1 특정 prefix의 일별 사용량

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;

7.2 특정 bucket에서 큰 prefix Top 100

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;

7.3 small files가 많은 prefix

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;

7.4 등록 pattern별 사용량

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;

8. 운영상 주의사항

8.1 Raw table 보관 기간을 짧게 가져가기

수십억 object라면 daily raw snapshot을 장기 보관하는 순간 table 자체가 너무 커집니다. 추천은 다음과 같습니다.

object_inventory_daily_raw:
  7~30일 보관
  장애 분석/재처리 기간만 유지

prefix_usage_daily:
  1~3년 보관
  사용자 조회, 추이 분석, 비용 배부에 사용

inventory_run:
  장기 보관
  감사/정합성/장애 분석에 사용

8.2 AIStor bucket metric과 차이가 날 수 있는 이유를 문서화

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_bytesstored_size_bytes를 반드시 분리해야 운영팀/사용자 간 해석 충돌을 줄일 수 있습니다.

8.3 Table 접근 제어

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)

8.4 Trino/Spark 연동

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

9. 최종 권장 구성

가장 현실적인 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 환경에서 조회 성능, 정합성 검증, 운영 권한 분리, 장기 보관 비용을 가장 균형 있게 가져갈 수 있습니다.

0개의 댓글