Remote Storage (Apache Iceberg on object store)
Korvet includes a built-in storage worker that offloads sealed Redis stream segments to Apache Iceberg tables backed by Parquet files on S3.
|
In Korvet tiered storage terminology, the local tier uses Redis Streams and the remote tier is a set of Apache Iceberg tables, one per topic. Korvet uses local/remote in its configuration and APIs. |
Overview
The remote tier holds one Apache Iceberg table per topic, named after the topic with separator characters (., , :) rewritten to (topic orders.created.v1 becomes table orders_created_v1). All partitions of a topic share its table. The storage worker:
-
Reads sealed LOCAL segments from each topic-partition’s segment metadata in Redis
-
Streams the segment contents from Redis (one page resident at a time) into an Iceberg data file
-
Appends and commits the data file to the Iceberg table, then marks the Redis segment as offloaded
-
Runs in-process as part of the Korvet server; there is no separate archive daemon
Each topic’s table is a standard Iceberg table: external query engines (Spark, Trino, Athena, DuckDB, …) can read it directly. Iceberg manages its own metadata and manifest files under the table location. Each offloaded write commits as one Iceberg append; if the storage worker crashes before the commit, the Redis segment stays sealed-but-not-offloaded and the next scan re-streams the (still untouched) Redis segment.
Configuration
Enable remote storage by setting korvet.storage.remote.path in your application.yml:
korvet:
storage:
remote:
path: s3://my-bucket/korvet
s3:
region: us-west-1
worker:
tick-interval: 1m
For local development and tests, point the cold tier at a local directory instead of an object store. No s3.* settings are needed:
korvet:
storage:
remote:
path: file:///var/lib/korvet/cold
Setting korvet.storage.remote.path makes the cold tier available. The leader-locked storage worker is enabled by default and rolls eligible segments, offloads sealed segments, and enforces local and remote retention. Topics are archived only when they also have remote.storage.enable=true.
|
Storage Properties
| Property | Default | Description |
|---|---|---|
|
required |
Cold-tier root URI. Supports |
|
unset |
AWS region for the S3 object store. |
|
unset |
Optional endpoint URL for S3-compatible stores such as MinIO or LocalStack. |
|
unset |
Use path-style addressing. Required for most non-AWS S3-compatible stores. |
|
unset |
Static access-key id. Prefer IAM roles in production. |
|
unset |
Static secret access key. Prefer IAM roles in production. |
Maintenance Properties
| Property | Default | Description |
|---|---|---|
|
|
Enables the storage worker in this JVM. |
|
|
Tick cadence for the storage worker loop. |
|
|
Redis leader-lock lease duration. Must exceed |
Per-Topic Retention Configuration
Control when data moves from the local tier to the remote tier using topic-level configuration:
| Configuration | Default | Description |
|---|---|---|
|
|
Enable tiered storage for this topic (Kafka KIP-405 standard). |
|
|
Time to keep in the local tier. |
|
|
Size to keep in the local tier. |
|
|
Total retention across all tiers. |
Remote-tier retention is implicit: retention.ms - local.retention.ms.
Example: Keep 1 day local and the rest remote (1 year total):
kafka-configs --bootstrap-server localhost:9092 \
--entity-type topics --entity-name my-topic --alter \
--add-config remote.storage.enable=true,retention.ms=31536000000,local.retention.ms=86400000
Storage Format
Table Layout
The remote tier holds one Iceberg table per topic, under an Iceberg namespace named after
korvet.namespace (default korvet) so instances sharing a warehouse keep separate tables.
Each table is partitioned by stream_key
(identity) — so each topic partition’s rows land in their own Iceberg partition — and sorted by
message_ts. Iceberg owns the on-disk layout under korvet.storage.remote.path: a metadata/
directory for table metadata and manifests, plus Parquet data files. Each offloaded segment is written
as one data file named:
segment-<segmentId>-<uuid>.parquet
The trailing UUID keeps each write unique so a retried offload never collides with a file left by a prior failed attempt.
Iceberg Schema
Each row is one message. The hot-tier stream entry is decoded into analytics-friendly columns so external engines can query the key, value, headers, and timestamps directly. The Iceberg schema is:
required STRING stream_key;
required LONG segment_id;
required STRING message_id;
required LONG message_ts;
required LONG kafka_timestamp;
optional BINARY key;
optional BINARY value;
optional LIST<STRUCT< // headers
required STRING header_key,
optional BINARY header_value>>;
| Column | Type | Description |
|---|---|---|
|
STRING |
Local stream key of the source partition (the table partition column). |
|
LONG |
Sealed segment number the message came from. |
|
STRING |
Full Redis stream message id (e.g. |
|
LONG |
Redis stream-id millisecond component (append-time ordering); the table sort key. |
|
LONG |
Producer record timestamp in epoch milliseconds, or |
|
BINARY |
Record key, decoded from the stream entry. Null when the record has no key. |
|
BINARY |
Record value, decoded from the stream entry. Null for a tombstone. |
|
LIST<STRUCT> |
Record headers, preserving order and duplicate keys. Each entry has a required |
AWS Authentication
The cold tier writes through Iceberg’s S3FileIO. Configure common S3 settings under korvet.storage.remote.s3; otherwise the AWS SDK default credential provider chain is used.
Credential providers
Common production choices:
| Provider | Use when |
|---|---|
EC2, ECS, or EKS node roles |
Leave static credentials unset and let the AWS SDK use instance metadata. |
EKS IAM Roles for Service Accounts (IRSA) |
Leave static credentials unset. |
Static access key + secret |
Use |
IRSA (EKS IAM Roles for Service Accounts)
korvet:
storage:
remote:
path: s3://my-bucket/korvet
s3:
region: us-east-1
When IRSA is configured on the cluster, AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE are injected into the pod and picked up automatically.
Performance
The storage worker achieves high throughput when archiving to same-region S3:
| Configuration | Throughput | Notes |
|---|---|---|
Single stream |
~32,000 msg/s |
Baseline |
4 streams (parallel) |
~115,000 msg/s |
Near-linear scaling |
See Remote Storage Benchmarks for detailed results.
Performance tips
-
Same-region S3: deploy Korvet in the same AWS region as your bucket.
-
Multiple partitions: archival parallelism is per (topic, partition); more partitions = more archive concurrency.
-
Segment size: larger sealed segments produce larger Parquet files with better compression and fewer object-store operations.