Skip to content

Commit af04d3b

Browse files
feat(clickhouse): add s3_tier storage policy with renamed disks
- Add `s3_tier` storage policy: local NVMe as hot tier, S3 as cold tier - Rename disks for clarity: `default` → `local`, `s3_cache` → `s3` - Add `--s3-tier-move-factor` option to `clickhouse init` (default: 0.2) - Store `s3TierMoveFactor` in ClickHouseConfig and wire through manifest builder - Inject `CLICKHOUSE_S3_TIER_MOVE_FACTOR` env var into ClickHouse pods - Update Event.ClickHouse.ConfigSaved to include s3TierMoveFactor - Add end-to-end test: creates table with s3_tier policy, inserts data, forces move to S3, verifies bucket is non-empty Closes #582 Co-authored-by: Jon Haddad <rustyrazorblade@users.noreply.github.com>
1 parent 4ec42d2 commit af04d3b

File tree

12 files changed

+243
-10
lines changed

12 files changed

+243
-10
lines changed

bin/end-to-end-test

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,51 @@ EOF
892892
clickhouse-query "INSERT INTO test (id) VALUES (1)"
893893
}
894894

895+
step_clickhouse_s3_tier_test() {
896+
if [ "$ENABLE_CLICKHOUSE" != true ]; then
897+
echo "=== Skipping ClickHouse S3 tier test (use --clickhouse to enable) ==="
898+
return 0
899+
fi
900+
echo "=== Testing ClickHouse S3 tier storage policy ==="
901+
902+
local data_bucket
903+
data_bucket=$(jq -r '.dataBucket // empty' state.json 2>/dev/null)
904+
if [ -z "$data_bucket" ]; then
905+
echo "ERROR: dataBucket not found in state.json"
906+
return 1
907+
fi
908+
echo "Data bucket: $data_bucket"
909+
910+
echo "--- Creating table with s3_tier storage policy ---"
911+
clickhouse-query <<'EOF'
912+
CREATE OR REPLACE TABLE test_s3_tier (
913+
id UInt64,
914+
ts DateTime DEFAULT now()
915+
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/default/test_s3_tier', '{replica}')
916+
ORDER BY id
917+
SETTINGS storage_policy = 's3_tier'
918+
EOF
919+
920+
echo "--- Inserting 10000 rows ---"
921+
clickhouse-query "INSERT INTO test_s3_tier (id) SELECT number FROM numbers(10000)"
922+
923+
echo "--- Forcing data move to S3 disk ---"
924+
clickhouse-query "ALTER TABLE test_s3_tier MOVE PARTITION tuple() TO DISK 's3'"
925+
926+
echo "--- Verifying data exists in S3 bucket ---"
927+
local s3_object_count
928+
s3_object_count=$(aws s3 ls "s3://${data_bucket}/clickhouse/" --recursive 2>/dev/null | wc -l)
929+
echo "S3 object count under clickhouse/: $s3_object_count"
930+
if [ "$s3_object_count" -gt 0 ]; then
931+
echo "S3 tier test PASSED: $s3_object_count objects found in s3://${data_bucket}/clickhouse/"
932+
else
933+
echo "ERROR: No objects found in s3://${data_bucket}/clickhouse/ after moving data to S3 tier"
934+
return 1
935+
fi
936+
937+
echo "=== ClickHouse S3 tier test completed ==="
938+
}
939+
895940
step_clickhouse_stop() {
896941
if [ "$ENABLE_CLICKHOUSE" != true ]; then
897942
echo "=== Skipping ClickHouse stop (use --clickhouse to enable) ==="
@@ -1245,6 +1290,7 @@ STEPS_WORKDIR=(
12451290
"step_cassandra_start_stop:Cassandra start/stop cycle"
12461291
"step_clickhouse_start:Start ClickHouse"
12471292
"step_clickhouse_test:Test ClickHouse"
1293+
"step_clickhouse_s3_tier_test:Test ClickHouse S3 tier policy"
12481294
"step_clickhouse_stop:Stop ClickHouse"
12491295
"step_opensearch_start:Start OpenSearch"
12501296
"step_opensearch_test:Test OpenSearch"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Design: ClickHouse S3 Tier Storage Policy
2+
3+
## Data Flow
4+
5+
```
6+
clickhouse init --s3-tier-move-factor 0.1
7+
→ ClickHouseConfig.s3TierMoveFactor = 0.1
8+
→ saved to state.json
9+
10+
clickhouse start
11+
→ ClickHouseManifestBuilder.buildClusterConfigMap(s3TierMoveFactor = 0.1)
12+
→ ConfigMap key: "s3-tier-move-factor" = "0.1"
13+
→ ClickHouseManifestBuilder.buildServerContainer()
14+
→ env var: CLICKHOUSE_S3_TIER_MOVE_FACTOR from ConfigMap
15+
→ Pod reads env var → config.xml reads <move_factor from_env="CLICKHOUSE_S3_TIER_MOVE_FACTOR"/>
16+
```
17+
18+
## config.xml Changes
19+
20+
### Disk renames
21+
- `default``local` (explicit `type=local`, path `/mnt/db1/clickhouse/`)
22+
- `s3_cache``s3` (cache layer over `s3_disk`, path `/mnt/db1/clickhouse/disks/s3/`)
23+
24+
### New policy
25+
```xml
26+
<s3_tier>
27+
<volumes>
28+
<hot><disk>local</disk></hot>
29+
<cold><disk>s3</disk></cold>
30+
</volumes>
31+
<move_factor from_env="CLICKHOUSE_S3_TIER_MOVE_FACTOR"/>
32+
</s3_tier>
33+
```
34+
35+
## Kotlin Changes
36+
37+
### Constants.ClickHouse
38+
```kotlin
39+
const val DEFAULT_S3_TIER_MOVE_FACTOR = 0.2
40+
```
41+
42+
### ClickHouseConfig (ClusterState.kt)
43+
```kotlin
44+
data class ClickHouseConfig(
45+
...
46+
val s3TierMoveFactor: Double = Constants.ClickHouse.DEFAULT_S3_TIER_MOVE_FACTOR,
47+
)
48+
```
49+
50+
### ClickHouseInit
51+
New option: `--s3-tier-move-factor`
52+
53+
### ClickHouseManifestBuilder
54+
- `buildClusterConfigMap`: adds `"s3-tier-move-factor"` key
55+
- `buildServerContainer`: adds `CLICKHOUSE_S3_TIER_MOVE_FACTOR` env var from ConfigMap
56+
- `buildAllResources` + `buildClusterConfigMap`: accept `s3TierMoveFactor` param
57+
- `buildServerInitDataDirContainer`: creates `/mnt/db1/clickhouse/disks/s3` (renamed from `s3_cache`)
58+
59+
### ClickHouseStart
60+
Passes `clickHouseConfig.s3TierMoveFactor` to `buildAllResources`.
61+
62+
### Event.ClickHouse.ConfigSaved
63+
New field: `s3TierMoveFactor: Double`
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Proposal: ClickHouse S3 Tier Storage Policy
2+
3+
## Problem
4+
5+
ClickHouse supports two storage modes today:
6+
- `local`: NVMe only
7+
- `s3_main`: S3 with local cache (data lives primarily in S3)
8+
9+
There is no policy for a tiered approach where data starts on fast local NVMe and migrates to S3 only when local disk fills up. This is the classic "hot/cold" tiering pattern that maximises write speed while keeping storage costs low.
10+
11+
## Proposed Solution
12+
13+
Add a new `s3_tier` storage policy to ClickHouse with:
14+
- **Hot volume**: local NVMe disk (`local` disk)
15+
- **Cold volume**: S3 with local cache (`s3` disk)
16+
- **Automatic migration**: controlled by `move_factor` — when local disk is `(1 - move_factor)` full, ClickHouse moves the oldest parts to the cold volume
17+
18+
The move factor is configurable via `--s3-tier-move-factor` on `clickhouse init` (default: `0.2`, meaning data moves when local disk reaches 80% capacity).
19+
20+
## Disk Rename
21+
22+
As part of this change, disk names are made more descriptive:
23+
- `default``local` (explicit local disk definition)
24+
- `s3_cache``s3` (the cache-over-S3 disk)
25+
26+
This makes storage policies self-documenting: `local` policy uses `local` disk, `s3_main` policy uses `s3` disk, `s3_tier` policy uses both.
27+
28+
## User Experience
29+
30+
```bash
31+
# Use default move factor (0.2)
32+
easy-db-lab clickhouse init
33+
34+
# Use custom move factor
35+
easy-db-lab clickhouse init --s3-tier-move-factor 0.1
36+
37+
easy-db-lab clickhouse start
38+
39+
# Create a table with tiered storage
40+
clickhouse-query "CREATE TABLE events ... SETTINGS storage_policy = 's3_tier'"
41+
```
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Tasks: ClickHouse S3 Tier Storage Policy
2+
3+
## Implementation
4+
5+
- [x] Add `DEFAULT_S3_TIER_MOVE_FACTOR = 0.2` to `Constants.ClickHouse`
6+
- [x] Add `s3TierMoveFactor: Double` field to `ClickHouseConfig`
7+
- [x] Add `--s3-tier-move-factor` option to `ClickHouseInit`
8+
- [x] Update `Event.ClickHouse.ConfigSaved` to include `s3TierMoveFactor`
9+
- [x] Rename disks in `config.xml`: `default``local`, `s3_cache``s3`
10+
- [x] Add `s3_tier` policy to `config.xml`
11+
- [x] Update `ClickHouseManifestBuilder.buildClusterConfigMap` to store `s3-tier-move-factor`
12+
- [x] Update `ClickHouseManifestBuilder.buildServerContainer` to inject `CLICKHOUSE_S3_TIER_MOVE_FACTOR` env var
13+
- [x] Update `ClickHouseManifestBuilder.buildAllResources` signature
14+
- [x] Update `buildServerInitDataDirContainer` to create `/mnt/db1/clickhouse/disks/s3`
15+
- [x] Wire `ClickHouseStart` to pass `s3TierMoveFactor` from config to builder
16+
17+
## Tests
18+
19+
- [x] Update `ClickHouseManifestBuilderTest` to pass `s3TierMoveFactor`
20+
- [x] Add assertion for `s3-tier-move-factor` in cluster config ConfigMap test
21+
- [x] Add test asserting `s3_tier` policy appears in `config.xml`
22+
- [x] Add `step_clickhouse_s3_tier_test` to `bin/end-to-end-test`:
23+
- Creates table with `s3_tier` policy
24+
- Inserts 10,000 rows
25+
- Forces move to S3 disk via `ALTER TABLE ... MOVE PARTITION tuple() TO DISK 's3'`
26+
- Verifies S3 bucket contains objects under `clickhouse/` prefix

src/main/kotlin/com/rustyrazorblade/easydblab/Constants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ object Constants {
237237
const val DEFAULT_S3_CACHE_SIZE = "10Gi"
238238
const val DEFAULT_S3_CACHE_ON_WRITE = "true"
239239
const val DEFAULT_REPLICAS_PER_SHARD = 3
240+
const val DEFAULT_S3_TIER_MOVE_FACTOR = 0.2
240241
}
241242

242243
// YACE (Yet Another CloudWatch Exporter) configuration

src/main/kotlin/com/rustyrazorblade/easydblab/commands/clickhouse/ClickHouseInit.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,20 @@ class ClickHouseInit : PicoBaseCommand() {
4040
)
4141
var replicasPerShard: Int = Constants.ClickHouse.DEFAULT_REPLICAS_PER_SHARD
4242

43+
@Option(
44+
names = ["--s3-tier-move-factor"],
45+
description = ["Fraction of local disk free space that triggers data move to S3 tier (default: \${DEFAULT-VALUE})"],
46+
)
47+
var s3TierMoveFactor: Double = Constants.ClickHouse.DEFAULT_S3_TIER_MOVE_FACTOR
48+
4349
override fun execute() {
4450
val state = clusterStateManager.load()
4551
val config =
4652
ClickHouseConfig(
4753
s3CacheSize = s3CacheSize,
4854
s3CacheOnWrite = s3CacheOnWrite,
4955
replicasPerShard = replicasPerShard,
56+
s3TierMoveFactor = s3TierMoveFactor,
5057
)
5158
state.updateClickHouseConfig(config)
5259
clusterStateManager.save(state)
@@ -56,6 +63,7 @@ class ClickHouseInit : PicoBaseCommand() {
5663
replicasPerShard = replicasPerShard,
5764
s3CacheSize = s3CacheSize,
5865
s3CacheOnWrite = s3CacheOnWrite,
66+
s3TierMoveFactor = s3TierMoveFactor,
5967
),
6068
)
6169
}

src/main/kotlin/com/rustyrazorblade/easydblab/commands/clickhouse/ClickHouseStart.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import picocli.CommandLine.Option
2626
* Storage policies available for tables:
2727
* - 'local': Local disk storage (default)
2828
* - 's3_main': S3 storage with local cache
29+
* - 's3_tier': Local NVMe as hot tier, S3 as cold tier (automatic data migration)
2930
*
3031
* Example creating a distributed replicated table:
3132
* ```sql
@@ -197,6 +198,7 @@ class ClickHouseStart : PicoBaseCommand() {
197198
replicasPerShard = replicasPerShard,
198199
s3CacheSize = clickHouseConfig.s3CacheSize,
199200
s3CacheOnWrite = clickHouseConfig.s3CacheOnWrite,
201+
s3TierMoveFactor = clickHouseConfig.s3TierMoveFactor,
200202
)
201203

202204
for (resource in resources) {

src/main/kotlin/com/rustyrazorblade/easydblab/configuration/ClusterState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ data class ClickHouseConfig(
156156
val s3CacheSize: String = Constants.ClickHouse.DEFAULT_S3_CACHE_SIZE,
157157
val s3CacheOnWrite: String = Constants.ClickHouse.DEFAULT_S3_CACHE_ON_WRITE,
158158
val replicasPerShard: Int = Constants.ClickHouse.DEFAULT_REPLICAS_PER_SHARD,
159+
val s3TierMoveFactor: Double = Constants.ClickHouse.DEFAULT_S3_TIER_MOVE_FACTOR,
159160
)
160161

161162
/**

src/main/kotlin/com/rustyrazorblade/easydblab/configuration/clickhouse/ClickHouseManifestBuilder.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,20 @@ class ClickHouseManifestBuilder(
8080
* @param replicasPerShard Number of replicas per shard
8181
* @param s3CacheSize S3 cache size (e.g. "10Gi")
8282
* @param s3CacheOnWrite Whether to cache on write operations
83+
* @param s3TierMoveFactor Fraction of local disk free space that triggers move to S3 tier
8384
* @return List of all K8s resources
8485
*/
8586
fun buildAllResources(
8687
totalReplicas: Int,
8788
replicasPerShard: Int,
8889
s3CacheSize: String,
8990
s3CacheOnWrite: String,
91+
s3TierMoveFactor: Double = Constants.ClickHouse.DEFAULT_S3_TIER_MOVE_FACTOR,
9092
): List<HasMetadata> =
9193
listOf(
9294
buildKeeperConfigMap(),
9395
buildServerConfigMap(totalReplicas, replicasPerShard),
94-
buildClusterConfigMap(replicasPerShard, s3CacheSize, s3CacheOnWrite),
96+
buildClusterConfigMap(replicasPerShard, s3CacheSize, s3CacheOnWrite, s3TierMoveFactor),
9597
buildKeeperService(),
9698
buildServerHeadlessService(),
9799
buildServerClientService(),
@@ -143,6 +145,7 @@ class ClickHouseManifestBuilder(
143145
replicasPerShard: Int,
144146
s3CacheSize: String,
145147
s3CacheOnWrite: String,
148+
s3TierMoveFactor: Double = Constants.ClickHouse.DEFAULT_S3_TIER_MOVE_FACTOR,
146149
): HasMetadata =
147150
ConfigMapBuilder()
148151
.withNewMetadata()
@@ -153,6 +156,7 @@ class ClickHouseManifestBuilder(
153156
.addToData("replicas-per-shard", replicasPerShard.toString())
154157
.addToData("s3-cache-size", s3CacheSize)
155158
.addToData("s3-cache-on-write", s3CacheOnWrite)
159+
.addToData("s3-tier-move-factor", s3TierMoveFactor.toString())
156160
.build()
157161

158162
/**
@@ -518,7 +522,7 @@ class ClickHouseManifestBuilder(
518522
mkdir -p /mnt/db1/clickhouse/user_files
519523
mkdir -p /mnt/db1/clickhouse/format_schemas
520524
mkdir -p /mnt/db1/clickhouse/disks/s3_disk
521-
mkdir -p /mnt/db1/clickhouse/disks/s3_cache
525+
mkdir -p /mnt/db1/clickhouse/disks/s3
522526
mkdir -p /mnt/db1/clickhouse/logs
523527
chown -R 101:101 /mnt/db1/clickhouse
524528
""".trimIndent(),
@@ -596,6 +600,17 @@ class ClickHouseManifestBuilder(
596600
.build(),
597601
).build(),
598602
).build(),
603+
EnvVarBuilder()
604+
.withName("CLICKHOUSE_S3_TIER_MOVE_FACTOR")
605+
.withValueFrom(
606+
EnvVarSourceBuilder()
607+
.withConfigMapKeyRef(
608+
ConfigMapKeySelectorBuilder()
609+
.withName("clickhouse-cluster-config")
610+
.withKey("s3-tier-move-factor")
611+
.build(),
612+
).build(),
613+
).build(),
599614
),
600615
).withEnvFrom(
601616
io.fabric8.kubernetes.api.model

src/main/kotlin/com/rustyrazorblade/easydblab/events/Event.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4462,13 +4462,15 @@ sealed interface Event {
44624462
val replicasPerShard: Int,
44634463
val s3CacheSize: String,
44644464
val s3CacheOnWrite: String,
4465+
val s3TierMoveFactor: Double,
44654466
) : ClickHouse {
44664467
override fun toDisplayString(): String =
44674468
"""
44684469
ClickHouse configuration saved.
44694470
Replicas per shard: $replicasPerShard
44704471
S3 cache size: $s3CacheSize
44714472
S3 cache on write: $s3CacheOnWrite
4473+
S3 tier move factor: $s3TierMoveFactor
44724474
""".trimIndent()
44734475
}
44744476
}

0 commit comments

Comments
 (0)