@fjall/components-infrastructure 10.1.2 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,10 @@
1
1
  import { CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, CLICKHOUSE_ECS_RESERVED_MEMORY_MIB, CLICKHOUSE_HOST_METRICS, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_UID, clickHousePasswordSha256Snippet } from "./clickhouseConstants.js";
2
+ import { deriveClickHouseServerTuning, MAX_SERVER_MEMORY_RAM_RATIO, SYSTEM_LOG_TTL } from "./clickhouseTuning.js";
2
3
  import { renderUsersXml } from "./clickhouseXmlRenderer.js";
3
4
  export function generateServerConfigXml(options) {
4
5
  const { backupBucketName, backupBucketRegion, coldTier } = options;
5
6
  const tlsActive = options.tls !== undefined;
7
+ const tuning = deriveClickHouseServerTuning(options.instanceType);
6
8
  const storageBlock = coldTier !== undefined
7
9
  ? ` <storage_configuration>
8
10
  <!-- Same CH 26 rule as the no-cold-tier branch: a <local_ssd> disk
@@ -15,6 +17,15 @@ export function generateServerConfigXml(options) {
15
17
  <endpoint>https://${coldTier.bucketName}.s3.${coldTier.region}.amazonaws.com/cold/</endpoint>
16
18
  <use_environment_credentials>true</use_environment_credentials>
17
19
  <metadata_path>/var/lib/clickhouse/disks/s3_cold/</metadata_path>
20
+ <!-- Blob garbage collection defaults to a 1 s wake — pure
21
+ schedule-pool churn on this workload (prod 2026-08-12:
22
+ 353,035 runs removed 1 blob). Cold-tier parts move on a
23
+ TTL measured in days, so a 60 s cadence loses nothing.
24
+ Key: DiskObjectStorage reads
25
+ <disk>.data_background_cleanup.interval_sec (CH 26.3). -->
26
+ <data_background_cleanup>
27
+ <interval_sec>60</interval_sec>
28
+ </data_background_cleanup>
18
29
  </s3_cold>
19
30
  </disks>
20
31
  <policies>
@@ -89,65 +100,67 @@ export function generateServerConfigXml(options) {
89
100
  work, but explicit beats implicit and keeps the listener identical
90
101
  across network modes. -->
91
102
  <listen_host>0.0.0.0</listen_host>
92
- <!-- m7g.medium tuning philosophy: 1 vCPU sustained / 4 GB / 3 GB container.
93
- Single-thread CPU has no headroom (raising max_threads beyond 1 just
94
- oversubscribes the kernel scheduler), so we trade RAM for CPU size
95
- caches generously so dashboard reads hit cache rather than rescanning,
96
- and let a single query use most of the budget if needed.
97
- Re-tune these in lockstep when DEFAULT_CLICKHOUSE_INSTANCE_TYPE moves
98
- to a larger box. -->
99
- <!-- 0.75 ratio gives the kernel ~750 MB cushion on the 3 GB cgroup for
100
- CH's untracked allocations (libc allocator, executable image, malloc
103
+ <!-- Small-node tuning philosophy: trade RAM for CPU size caches
104
+ generously so dashboard reads hit cache rather than rescanning, and
105
+ let a single query use most of the budget if needed. Every pool,
106
+ cache and concurrency value below is derived from the instance type
107
+ by deriveClickHouseServerTuning (clickhouseTuning.ts) the
108
+ pre-derivation shape (hand-tuned m7g.medium constants) silently
109
+ stopped fitting when the instance moved to r8g.medium. -->
110
+ <!-- 0.75 ratio gives the kernel a cushion on the cgroup for CH's
111
+ untracked allocations (libc allocator, executable image, malloc
101
112
  fragmentation, jemalloc virtual reservations on ARM64). The default
102
- 0.9 left only ~300 MB and OOM-killed the process at boot when cache
103
- + thread init pushed the cgroup over the limit (exit 137 in CW).
104
- Computed at runtime from the cgroup limit so it tracks instance size
105
- when DEFAULT_CLICKHOUSE_INSTANCE_TYPE moves to a larger box. -->
106
- <max_server_memory_usage_to_ram_ratio>0.75</max_server_memory_usage_to_ram_ratio>
113
+ 0.9 left only ~300 MB on the 4 GiB host and OOM-killed the process
114
+ at boot when cache + thread init pushed the cgroup over the limit
115
+ (exit 137 in CW). Computed at runtime from the cgroup limit so it
116
+ tracks instance size. -->
117
+ <max_server_memory_usage_to_ram_ratio>${MAX_SERVER_MEMORY_RAM_RATIO}</max_server_memory_usage_to_ram_ratio>
107
118
  <!-- Per-query caps (max_memory_usage, max_bytes_before_external_sort,
108
119
  max_bytes_before_external_group_by) live in the profile blocks of
109
120
  users.xml — ClickHouse 26.3.10 rejects them at top level with
110
121
  UNKNOWN_ELEMENT_IN_CONFIG. The default localhost user inherits
111
122
  ClickHouse's built-in defaults; workload users (app_writer,
112
123
  audit_writer, backup_reader, schema_admin) carry explicit caps. -->
113
- <!-- 384 MB mark cache + 128 MB index mark cache. CH counts these against
114
- max_server_memory_usage, so the read-cache footprint is 512 MB total
115
- (~17% of the budget). On a single-tenant dashboard workload this
116
- pays back fast — same queries repeat, mark hits avoid index re-scans. -->
117
- <mark_cache_size>402653184</mark_cache_size>
118
- <index_mark_cache_size>134217728</index_mark_cache_size>
124
+ <!-- Mark + index-mark caches scale linearly with instance memory
125
+ (384 MiB + 128 MiB at the 4 GiB baseline). CH counts these against
126
+ max_server_memory_usage. On a single-tenant dashboard workload they
127
+ pay back fast — same queries repeat, mark hits avoid index re-scans. -->
128
+ <mark_cache_size>${tuning.markCacheBytes}</mark_cache_size>
129
+ <index_mark_cache_size>${tuning.indexMarkCacheBytes}</index_mark_cache_size>
119
130
  <!-- Filesystem page cache handles uncompressed-block warmth on the
120
131
  host side; an in-CH uncompressed cache would just duplicate it. -->
121
132
  <uncompressed_cache_size>0</uncompressed_cache_size>
122
- <!-- Server-wide cap across ALL users. Raised 4→8 (2026-07-28) alongside
123
- high_throughput_ingest's per-user cap (2→6): the webapp overview
124
- fires 6-10 queries per page load and N app tasks share this cap, so
125
- 4 starved dashboard reads with TOO_MANY_SIMULTANEOUS_QUERIES (CH
126
- rejects rather than queues past the cap). On the 1 vCPU m7g.medium
127
- the extra slots time-slice rather than parallelise — acceptable for
128
- short cache-hot dashboard reads; max_threads=1 per query keeps the
129
- scheduler sane. Bump further in lockstep with instance size if
130
- DEFAULT_CLICKHOUSE_INSTANCE_TYPE moves to a >=2 vCPU host. -->
131
- <max_concurrent_queries>8</max_concurrent_queries>
132
- <background_pool_size>2</background_pool_size>
133
- <background_schedule_pool_size>2</background_schedule_pool_size>
134
- <background_merges_mutations_concurrency_ratio>2</background_merges_mutations_concurrency_ratio>
135
- <background_move_pool_size>1</background_move_pool_size>
136
- <background_fetches_pool_size>1</background_fetches_pool_size>
137
- <background_message_broker_schedule_pool_size>1</background_message_broker_schedule_pool_size>
133
+ <!-- Server-wide cap across ALL users, 8 per vCPU (raised 4→8 2026-07-28
134
+ alongside high_throughput_ingest's per-user cap 2→6: the webapp
135
+ overview fires 6-10 queries per page load and CH rejects rather than
136
+ queues past the cap). On a 1 vCPU host the slots time-slice rather
137
+ than parallelise acceptable for short cache-hot dashboard reads. -->
138
+ <max_concurrent_queries>${tuning.maxConcurrentQueries}</max_concurrent_queries>
139
+ <background_pool_size>${tuning.backgroundPoolSize}</background_pool_size>
140
+ <!-- Floored at 8 regardless of size: CH >= 25.9 caps same-type
141
+ background tasks at floor(pool * 0.8) and logs a warning on every
142
+ deferred task start — a pool of 2 (cap 1) flooded prod with ~35k
143
+ warnings/hour and serialised all background scheduling
144
+ (2026-08-12). The cap is computed once at pool creation, so this
145
+ value only changes behaviour after a server restart. -->
146
+ <background_schedule_pool_size>${tuning.backgroundSchedulePoolSize}</background_schedule_pool_size>
147
+ <background_merges_mutations_concurrency_ratio>${tuning.mergesMutationsConcurrencyRatio}</background_merges_mutations_concurrency_ratio>
148
+ <background_move_pool_size>${tuning.backgroundMovePoolSize}</background_move_pool_size>
149
+ <background_fetches_pool_size>${tuning.backgroundFetchesPoolSize}</background_fetches_pool_size>
150
+ <background_message_broker_schedule_pool_size>${tuning.backgroundMessageBrokerSchedulePoolSize}</background_message_broker_schedule_pool_size>
138
151
  <merge_tree>
139
152
  <max_suspicious_broken_parts>5</max_suspicious_broken_parts>
140
153
  <parts_to_delay_insert>150</parts_to_delay_insert>
141
154
  <parts_to_throw_insert>300</parts_to_throw_insert>
142
155
  <!-- ClickHouse 26.3.10 sanity-checks the free-pool thresholds against
143
156
  background_pool_size * background_merges_mutations_concurrency_ratio
144
- (= 2 * 2 = 4 here). The defaults (20 / 8 / 25) all exceed 4 and
145
- trip BAD_ARGUMENTS at startup on a 1 vCPU m7g.medium. Lower each
146
- to 1 so a mutation/merge/optimise can begin as soon as one pool
147
- slot is free — appropriate for the small instance footprint. -->
148
- <number_of_free_entries_in_pool_to_execute_mutation>1</number_of_free_entries_in_pool_to_execute_mutation>
149
- <number_of_free_entries_in_pool_to_lower_max_size_of_merge>1</number_of_free_entries_in_pool_to_lower_max_size_of_merge>
150
- <number_of_free_entries_in_pool_to_execute_optimize_entire_partition>1</number_of_free_entries_in_pool_to_execute_optimize_entire_partition>
157
+ and trips BAD_ARGUMENTS at startup when a threshold exceeds the
158
+ slot count (the CH defaults 20/8/25 all exceed a small pool).
159
+ Derived as max(1, slots/4) so the invariant holds by
160
+ construction at every instance size. -->
161
+ <number_of_free_entries_in_pool_to_execute_mutation>${tuning.freeEntriesToExecuteMutation}</number_of_free_entries_in_pool_to_execute_mutation>
162
+ <number_of_free_entries_in_pool_to_lower_max_size_of_merge>${tuning.freeEntriesToLowerMaxSizeOfMerge}</number_of_free_entries_in_pool_to_lower_max_size_of_merge>
163
+ <number_of_free_entries_in_pool_to_execute_optimize_entire_partition>${tuning.freeEntriesToExecuteOptimizeEntirePartition}</number_of_free_entries_in_pool_to_execute_optimize_entire_partition>
151
164
  </merge_tree>
152
165
  ${tlsActive
153
166
  ? ` <https_port>${CLICKHOUSE_HTTPS_PORT}</https_port>
@@ -170,38 +183,60 @@ ${tlsActive
170
183
  <database>system</database>
171
184
  <table>query_log</table>
172
185
  <flush_interval_milliseconds>7500</flush_interval_milliseconds>
173
- <ttl>event_date + INTERVAL 14 DAY DELETE</ttl>
186
+ <ttl>${SYSTEM_LOG_TTL}</ttl>
174
187
  </query_log>
175
188
  <!-- All system log tables need bounded retention on 80GB EBS (mirror query_log's 14-day policy). -->
176
189
  <asynchronous_metric_log>
177
190
  <database>system</database>
178
191
  <table>asynchronous_metric_log</table>
179
- <ttl>event_date + INTERVAL 14 DAY DELETE</ttl>
192
+ <ttl>${SYSTEM_LOG_TTL}</ttl>
180
193
  </asynchronous_metric_log>
181
194
  <metric_log>
182
195
  <database>system</database>
183
196
  <table>metric_log</table>
184
- <ttl>event_date + INTERVAL 14 DAY DELETE</ttl>
197
+ <ttl>${SYSTEM_LOG_TTL}</ttl>
185
198
  </metric_log>
186
199
  <part_log>
187
200
  <database>system</database>
188
201
  <table>part_log</table>
189
- <ttl>event_date + INTERVAL 14 DAY DELETE</ttl>
202
+ <ttl>${SYSTEM_LOG_TTL}</ttl>
190
203
  </part_log>
191
204
  <error_log>
192
205
  <database>system</database>
193
206
  <table>error_log</table>
194
- <ttl>event_date + INTERVAL 14 DAY DELETE</ttl>
207
+ <ttl>${SYSTEM_LOG_TTL}</ttl>
195
208
  </error_log>
209
+ <!-- CH creates these three by default with NO retention — the trace_log
210
+ family alone reached 3.5 GiB on prod before TTLs landed (2026-08-12).
211
+ Adding <ttl> changes the storage definition: on first boot after this
212
+ change CH renames each existing table to a *_N sibling and creates a
213
+ fresh one with the TTL. The renamed tables keep their data and need a
214
+ manual DROP TABLE to reclaim the disk. -->
215
+ <text_log>
216
+ <database>system</database>
217
+ <table>text_log</table>
218
+ <ttl>${SYSTEM_LOG_TTL}</ttl>
219
+ </text_log>
220
+ <trace_log>
221
+ <database>system</database>
222
+ <table>trace_log</table>
223
+ <ttl>${SYSTEM_LOG_TTL}</ttl>
224
+ </trace_log>
225
+ <background_schedule_pool_log>
226
+ <database>system</database>
227
+ <table>background_schedule_pool_log</table>
228
+ <ttl>${SYSTEM_LOG_TTL}</ttl>
229
+ </background_schedule_pool_log>
196
230
  ${storageBlock}
197
231
  ${backupEndpointBlock}
198
- <!-- 256 MB query cache. Single-tenant dashboard pattern means the same
199
- queries repeat at fixed cadences (5/15min refresh, manual reloads);
200
- a hit returns in microseconds instead of re-scanning. Sized generously
232
+ <!-- Query cache scales with instance memory (256 MiB at the 4 GiB
233
+ baseline). Single-tenant dashboard pattern means the same queries
234
+ repeat at fixed cadences (5/15min refresh, manual reloads); a hit
235
+ returns in microseconds instead of re-scanning. Sized generously
201
236
  relative to other caches because the workload pattern is uniquely
202
237
  cache-friendly. -->
203
238
  <query_cache>
204
- <max_size_in_bytes>268435456</max_size_in_bytes>
239
+ <max_size_in_bytes>${tuning.queryCacheBytes}</max_size_in_bytes>
205
240
  <max_entries>1024</max_entries>
206
241
  <max_entry_size_in_bytes>1048576</max_entry_size_in_bytes>
207
242
  </query_cache>
@@ -95,6 +95,16 @@ export function renderUsersXml(opts) {
95
95
  box that's a merge-pool starvation event. Keep TTL changes lazy: parts
96
96
  re-evaluate TTL on their next natural merge, no forced rewrite. -->
97
97
  <materialize_ttl_after_modify>0</materialize_ttl_after_modify>
98
+ <!-- Official builds sample every running query's stacks at 1 s
99
+ into system.trace_log. Nothing in the Fjall offering reads
100
+ those samples, and on a 1-vCPU node the sampling costs CPU
101
+ and disk (trace_log was the fastest-growing system table on
102
+ prod, 2026-08-12). The default profile is the server-wide
103
+ base, so this disables the profiler for every user; re-enable
104
+ per session (SET query_profiler_real_time_period_ns = 1000000000)
105
+ when actively profiling. -->
106
+ <query_profiler_real_time_period_ns>0</query_profiler_real_time_period_ns>
107
+ <query_profiler_cpu_time_period_ns>0</query_profiler_cpu_time_period_ns>
98
108
  </default>`;
99
109
  const profileBlocks = Object.entries(opts.profiles)
100
110
  .map(([name, spec]) => renderProfileBlock(name, spec))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "10.1.2",
3
+ "version": "11.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -80,8 +80,8 @@
80
80
  },
81
81
  "dependencies": {
82
82
  "@aws-sdk/client-organizations": "^3.1098.0",
83
- "@fjall/generator": "^10.1.2",
84
- "@fjall/util": "^10.1.2",
83
+ "@fjall/generator": "^11.0.0",
84
+ "@fjall/util": "^11.0.0",
85
85
  "constructs": "^10.7.2"
86
86
  },
87
87
  "overrides": {