@fjall/components-infrastructure 16.0.0 → 17.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.
- package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +103 -8
- package/dist/lib/patterns/aws/clickhouseDatabase.js +51 -12
- package/dist/lib/patterns/aws/database.d.ts +1 -1
- package/dist/lib/patterns/aws/database.js +1 -1
- package/dist/lib/resources/aws/database/clickhouseBackupScript.d.ts +103 -0
- package/dist/lib/resources/aws/database/clickhouseBackupScript.js +123 -0
- package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +132 -20
- package/dist/lib/resources/aws/database/clickhouseConstants.js +126 -15
- package/dist/lib/resources/aws/database/clickhouseSchemas.d.ts +33 -1
- package/dist/lib/resources/aws/database/clickhouseSchemas.js +31 -0
- package/dist/lib/resources/aws/database/clickhouseStorage.d.ts +58 -0
- package/dist/lib/resources/aws/database/clickhouseStorage.js +87 -0
- package/dist/lib/resources/aws/database/clickhouseTuning.d.ts +5 -6
- package/dist/lib/resources/aws/database/clickhouseTuning.js +39 -16
- package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +11 -0
- package/dist/lib/resources/aws/database/clickhouseUserData.js +22 -6
- package/dist/lib/resources/aws/database/clickhouseXmlRenderer.js +22 -1
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.d.ts +54 -5
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.js +106 -10
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FrozenProfileSpec } from "./clickhouseSchemas.js";
|
|
2
2
|
/**
|
|
3
3
|
* Server-level tuning values derived from the instance type. Consumed by
|
|
4
4
|
* `generateServerConfigXml` — every value here lands in the rendered
|
|
@@ -50,10 +50,9 @@ export declare const SYSTEM_LOG_TTL = "event_date + INTERVAL 14 DAY DELETE";
|
|
|
50
50
|
export declare function deriveClickHouseServerTuning(instanceType: string): ClickHouseServerTuning;
|
|
51
51
|
/**
|
|
52
52
|
* Derive the four workload-class default profiles from the instance type.
|
|
53
|
-
* Memory caps, external-spill thresholds and
|
|
54
|
-
* instance memory; thread caps scale with vCPUs.
|
|
55
|
-
*
|
|
56
|
-
* (pinned by clickhouseTuning.test.ts).
|
|
53
|
+
* Memory caps, external-spill thresholds and constraint ceilings scale with
|
|
54
|
+
* instance memory; thread caps scale with vCPUs. The full m7g.medium
|
|
55
|
+
* shape is pinned by clickhouseTuning.test.ts.
|
|
57
56
|
*
|
|
58
57
|
* `high_throughput_ingest.maxConcurrentQueriesForUser` deliberately does
|
|
59
58
|
* NOT scale: it mirrors the webapp's client-side FIFO gate
|
|
@@ -61,4 +60,4 @@ export declare function deriveClickHouseServerTuning(instanceType: string): Clic
|
|
|
61
60
|
* parity test on the webapp side enforces the pair, so the value is a
|
|
62
61
|
* client contract, not an instance-capacity knob.
|
|
63
62
|
*/
|
|
64
|
-
export declare function deriveClickHouseDefaultProfiles(instanceType: string): Readonly<Record<string,
|
|
63
|
+
export declare function deriveClickHouseDefaultProfiles(instanceType: string): Readonly<Record<string, FrozenProfileSpec>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLICKHOUSE_INSTANCE_SPECS } from "./clickhouseConstants.js";
|
|
1
|
+
import { clickHouseInstanceSpec, CLICKHOUSE_INSTANCE_SPECS } from "./clickhouseConstants.js";
|
|
2
2
|
/** 4 GiB / 1 vCPU (m7g.medium) baseline the scaling formulas anchor to.
|
|
3
3
|
* These are the proven prod values the construct shipped hand-tuned before
|
|
4
4
|
* derivation existed — `deriveClickHouseServerTuning("m7g.medium")` must
|
|
@@ -47,7 +47,7 @@ export const MAX_SERVER_MEMORY_RAM_RATIO = 0.75;
|
|
|
47
47
|
* change is a single edit. */
|
|
48
48
|
export const SYSTEM_LOG_TTL = "event_date + INTERVAL 14 DAY DELETE";
|
|
49
49
|
function resolveSpec(instanceType) {
|
|
50
|
-
const spec =
|
|
50
|
+
const spec = clickHouseInstanceSpec(instanceType);
|
|
51
51
|
if (spec === undefined) {
|
|
52
52
|
throw new Error(`ClickHouseDatabase: unknown instance type '${instanceType}' — cannot ` +
|
|
53
53
|
"derive server tuning. Supported: " +
|
|
@@ -110,10 +110,9 @@ export function deriveClickHouseServerTuning(instanceType) {
|
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
112
|
* Derive the four workload-class default profiles from the instance type.
|
|
113
|
-
* Memory caps, external-spill thresholds and
|
|
114
|
-
* instance memory; thread caps scale with vCPUs.
|
|
115
|
-
*
|
|
116
|
-
* (pinned by clickhouseTuning.test.ts).
|
|
113
|
+
* Memory caps, external-spill thresholds and constraint ceilings scale with
|
|
114
|
+
* instance memory; thread caps scale with vCPUs. The full m7g.medium
|
|
115
|
+
* shape is pinned by clickhouseTuning.test.ts.
|
|
117
116
|
*
|
|
118
117
|
* `high_throughput_ingest.maxConcurrentQueriesForUser` deliberately does
|
|
119
118
|
* NOT scale: it mirrors the webapp's client-side FIFO gate
|
|
@@ -124,12 +123,27 @@ export function deriveClickHouseServerTuning(instanceType) {
|
|
|
124
123
|
export function deriveClickHouseDefaultProfiles(instanceType) {
|
|
125
124
|
const spec = resolveSpec(instanceType);
|
|
126
125
|
const scale = memoryScale(spec);
|
|
126
|
+
// The four unraisable ceilings — one source for both the ingest profile's
|
|
127
|
+
// scalar defaults and its <constraints> block, so a retuned default can
|
|
128
|
+
// never drift from (or exceed) its own ceiling.
|
|
129
|
+
const ingestCeilings = Object.freeze({
|
|
130
|
+
maxThreads: spec.vcpus,
|
|
131
|
+
maxMemoryUsage: 1610612736 * scale, // 1.5 GiB baseline
|
|
132
|
+
maxMemoryUsageForUser: 2147483648 * scale, // 2 GiB baseline
|
|
133
|
+
maxExecutionTime: 30
|
|
134
|
+
});
|
|
127
135
|
return Object.freeze({
|
|
128
136
|
high_throughput_ingest: Object.freeze({
|
|
129
|
-
maxThreads:
|
|
137
|
+
maxThreads: ingestCeilings.maxThreads,
|
|
130
138
|
maxInsertThreads: spec.vcpus,
|
|
131
139
|
maxConcurrentQueriesForUser: 6,
|
|
132
|
-
|
|
140
|
+
// MUST stay 0: any non-zero value drops ALL ExceptionBeforeStart rows
|
|
141
|
+
// (quota / concurrency / limit refusals) and every QueryStart row from
|
|
142
|
+
// system.query_log unconditionally (executeQuery.cpp gates on elapsed
|
|
143
|
+
// time, which refused queries never accrue) — failed product queries
|
|
144
|
+
// become invisible. If log volume ever needs a lever it is
|
|
145
|
+
// log_queries_min_type=QUERY_FINISH, never a duration floor.
|
|
146
|
+
logQueriesMinQueryDurationMs: 0,
|
|
133
147
|
optimizeMoveToPrewhere: true,
|
|
134
148
|
useQueryConditionCache: true,
|
|
135
149
|
useSkipIndexesIfFinal: true,
|
|
@@ -146,16 +160,25 @@ export function deriveClickHouseDefaultProfiles(instanceType) {
|
|
|
146
160
|
inputFormatParallelParsing: false,
|
|
147
161
|
outputFormatParallelFormatting: false,
|
|
148
162
|
queryPlanOptimizeLazyMaterialization: true,
|
|
149
|
-
maxMemoryUsage:
|
|
150
|
-
maxMemoryUsageForUser:
|
|
163
|
+
maxMemoryUsage: ingestCeilings.maxMemoryUsage,
|
|
164
|
+
maxMemoryUsageForUser: ingestCeilings.maxMemoryUsageForUser,
|
|
151
165
|
maxBytesBeforeExternalSort: 536870912 * scale, // 512 MiB baseline
|
|
152
166
|
maxBytesBeforeExternalGroupBy: 536870912 * scale,
|
|
153
|
-
maxExecutionTime:
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
167
|
+
maxExecutionTime: ingestCeilings.maxExecutionTime,
|
|
168
|
+
// Deliberately no maxRowsToRead: the planning-time estimate refuses
|
|
169
|
+
// legitimate in-order LIMIT reads, and any statement can override it.
|
|
170
|
+
// Per-box protection is the unraisable ceilings below; per-tenant
|
|
171
|
+
// fairness is the org-keyed quota (webapp clickhouse-init/030).
|
|
172
|
+
constraints: Object.freeze({
|
|
173
|
+
maxExecutionTime: Object.freeze({
|
|
174
|
+
max: ingestCeilings.maxExecutionTime
|
|
175
|
+
}),
|
|
176
|
+
maxMemoryUsage: Object.freeze({ max: ingestCeilings.maxMemoryUsage }),
|
|
177
|
+
maxMemoryUsageForUser: Object.freeze({
|
|
178
|
+
max: ingestCeilings.maxMemoryUsageForUser
|
|
179
|
+
}),
|
|
180
|
+
maxThreads: Object.freeze({ max: ingestCeilings.maxThreads })
|
|
181
|
+
})
|
|
159
182
|
}),
|
|
160
183
|
audit_append: Object.freeze({
|
|
161
184
|
maxThreads: 1,
|
|
@@ -18,6 +18,17 @@ export interface BuildClickHouseUserDataOptions {
|
|
|
18
18
|
* as `clickHouseTaskMemoryMiB`).
|
|
19
19
|
*/
|
|
20
20
|
instanceType: string;
|
|
21
|
+
/**
|
|
22
|
+
* Declared data-volume size in GiB, embedded into the emitted script as a
|
|
23
|
+
* comment line. Load-bearing, not documentation: user data is part of the
|
|
24
|
+
* launch template, so embedding the size means a `storageGb` change
|
|
25
|
+
* produces a new launch-template version — which is what triggers the ASG
|
|
26
|
+
* instance refresh whose boot runs the `resize2fs` below. Without it, a
|
|
27
|
+
* volume increase modifies the EBS device in place, nothing relaunches,
|
|
28
|
+
* and the grown capacity is inert until some unrelated deploy happens to
|
|
29
|
+
* replace the instance. Same apply mechanism `instanceType` rides.
|
|
30
|
+
*/
|
|
31
|
+
dataVolumeSizeGb: number;
|
|
21
32
|
/**
|
|
22
33
|
* Optional S3 cold-tier configuration.
|
|
23
34
|
* When omitted, single-tier hot storage on the EC2 EBS volume is used.
|
|
@@ -362,9 +362,14 @@ chmod 644 "$MOUNT_POINT/server-certs/client.xml"
|
|
|
362
362
|
`
|
|
363
363
|
: "";
|
|
364
364
|
// The CWAgent-namespace metrics have no producer without this — no
|
|
365
|
-
// CloudWatch Agent is installed. disk_used_percent feeds the
|
|
366
|
-
//
|
|
367
|
-
//
|
|
365
|
+
// CloudWatch Agent is installed. disk_used_percent feeds the disk WARN
|
|
366
|
+
// alarm (trajectory) and disk_free_gib the disk CRITICAL alarm (imminent
|
|
367
|
+
// breakage — merges need free bytes, not free percent, so the paging signal
|
|
368
|
+
// is absolute and the warn one is not); mem_used_percent is dashboards-only
|
|
369
|
+
// (the CWAgent memory alarm is retired — memory alarming lives on ECS
|
|
370
|
+
// MemoryUtilization). Free space is read in MiB and divided rather than
|
|
371
|
+
// taken from `--block-size=1G`, whose integer truncation would quantise the
|
|
372
|
+
// metric to 1 GiB steps against a floor measured in tens. The ASG name
|
|
368
373
|
// (the alarms' dimension) is unknowable at synth (LaunchTemplate → ASG is a
|
|
369
374
|
// CFN cycle), so it resolves at run time: IMDS instance tags first, with a
|
|
370
375
|
// describe-auto-scaling-instances fallback. AL2023 ECS AMIs ship no cron;
|
|
@@ -375,10 +380,10 @@ chmod 644 "$MOUNT_POINT/server-certs/client.xml"
|
|
|
375
380
|
// timer never re-triggers a still-activating unit, so ONE wedged run (hung
|
|
376
381
|
// IMDS or AWS call) would silence the publisher forever — every curl carries
|
|
377
382
|
// --max-time and the unit a TimeoutStartSec under the 60s cadence. The disk
|
|
378
|
-
//
|
|
383
|
+
// metrics publish only when the data volume is actually mounted: `df` on
|
|
379
384
|
// an unmounted path reports the ROOT filesystem, a false-healthy number
|
|
380
385
|
// that would defeat the disk-critical alarm's missing-data=BREACHING flip.
|
|
381
|
-
const { namespace, memoryMetric, diskMetric, asgDimension } = CLICKHOUSE_HOST_METRICS;
|
|
386
|
+
const { namespace, memoryMetric, diskMetric, diskFreeMetric, asgDimension } = CLICKHOUSE_HOST_METRICS;
|
|
382
387
|
const metricsPublisher = `
|
|
383
388
|
cat > /usr/local/bin/fjall-ch-metrics.sh << 'METRICSEOF'
|
|
384
389
|
#!/bin/bash
|
|
@@ -401,8 +406,10 @@ METRICS=("MetricName=${memoryMetric},Dimensions=[{Name=${asgDimension},Value=$AS
|
|
|
401
406
|
if mountpoint -q ${CLICKHOUSE_DATA_MOUNT_PATH}; then
|
|
402
407
|
DISK=$(df --output=pcent ${CLICKHOUSE_DATA_MOUNT_PATH} | tail -n 1 | tr -dc '0-9')
|
|
403
408
|
METRICS+=("MetricName=${diskMetric},Dimensions=[{Name=${asgDimension},Value=$ASG}],Value=$DISK,Unit=Percent")
|
|
409
|
+
FREE=$(df --output=avail --block-size=1M ${CLICKHOUSE_DATA_MOUNT_PATH} | tail -n 1 | tr -dc '0-9' | awk '{printf "%.2f", $1/1024}')
|
|
410
|
+
METRICS+=("MetricName=${diskFreeMetric},Dimensions=[{Name=${asgDimension},Value=$ASG}],Value=$FREE,Unit=Gigabytes")
|
|
404
411
|
else
|
|
405
|
-
echo "fjall-ch-metrics: ${CLICKHOUSE_DATA_MOUNT_PATH} not mounted, skipping disk
|
|
412
|
+
echo "fjall-ch-metrics: ${CLICKHOUSE_DATA_MOUNT_PATH} not mounted, skipping disk metrics" >&2
|
|
406
413
|
fi
|
|
407
414
|
aws cloudwatch put-metric-data --region "$REGION" --namespace ${namespace} --metric-data "\${METRICS[@]}"
|
|
408
415
|
METRICSEOF
|
|
@@ -490,6 +497,15 @@ fi
|
|
|
490
497
|
mkdir -p "$MOUNT_POINT"
|
|
491
498
|
mount "$DEVICE" "$MOUNT_POINT"
|
|
492
499
|
|
|
500
|
+
# The declared size below is load-bearing, and it MUST be a statement, not a
|
|
501
|
+
# comment — compactUserDataScript strips comments from the emitted script.
|
|
502
|
+
# User data is part of the launch template, so the size appearing verbatim in
|
|
503
|
+
# the emitted script is what turns a storageGb change into a new template
|
|
504
|
+
# version -> instance refresh -> the boot-time resize2fs below. Without it a
|
|
505
|
+
# volume increase never relaunches anything and the grown capacity stays
|
|
506
|
+
# unusable until an unrelated deploy replaces the instance.
|
|
507
|
+
DECLARED_DATA_VOLUME_GIB=${options.dataVolumeSizeGb}
|
|
508
|
+
#
|
|
493
509
|
# Grow the filesystem to fill the volume. Raising the CDK volume size issues a
|
|
494
510
|
# ModifyVolume against the block device only — ext4 keeps its old size until
|
|
495
511
|
# resize2fs runs, so without this line every capacity increase is INERT and the
|
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
function camelToSnakeCase(input) {
|
|
2
2
|
return input.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase());
|
|
3
3
|
}
|
|
4
|
+
function renderConstraintsBlock(constraints) {
|
|
5
|
+
const settings = Object.entries(constraints).map(([key, constraint]) => {
|
|
6
|
+
const xmlName = camelToSnakeCase(key);
|
|
7
|
+
const children = [];
|
|
8
|
+
if (constraint.min !== undefined) {
|
|
9
|
+
children.push(` <min>${String(constraint.min)}</min>`);
|
|
10
|
+
}
|
|
11
|
+
if (constraint.max !== undefined) {
|
|
12
|
+
children.push(` <max>${String(constraint.max)}</max>`);
|
|
13
|
+
}
|
|
14
|
+
if (constraint.readonly === true) {
|
|
15
|
+
children.push(` <readonly/>`);
|
|
16
|
+
}
|
|
17
|
+
return ` <${xmlName}>\n${children.join("\n")}\n </${xmlName}>`;
|
|
18
|
+
});
|
|
19
|
+
return ` <constraints>\n${settings.join("\n")}\n </constraints>`;
|
|
20
|
+
}
|
|
4
21
|
function renderProfileSpec(spec) {
|
|
22
|
+
const { constraints, ...scalars } = spec;
|
|
5
23
|
const lines = [];
|
|
6
|
-
for (const [key, value] of Object.entries(
|
|
24
|
+
for (const [key, value] of Object.entries(scalars)) {
|
|
7
25
|
if (value === undefined)
|
|
8
26
|
continue;
|
|
9
27
|
const xmlName = camelToSnakeCase(key);
|
|
10
28
|
const xmlValue = typeof value === "boolean" ? (value ? "1" : "0") : String(value);
|
|
11
29
|
lines.push(` <${xmlName}>${xmlValue}</${xmlName}>`);
|
|
12
30
|
}
|
|
31
|
+
if (constraints !== undefined && Object.keys(constraints).length > 0) {
|
|
32
|
+
lines.push(renderConstraintsBlock(constraints));
|
|
33
|
+
}
|
|
13
34
|
return lines.join("\n");
|
|
14
35
|
}
|
|
15
36
|
function renderDefaultUserBlock(access) {
|
|
@@ -23,10 +23,39 @@ export interface ClickHouseAlarmThresholds {
|
|
|
23
23
|
* CWAgent host-memory alarm is retired (its threshold meant a different
|
|
24
24
|
* headroom per instance size; the timer metric remains for dashboards). */
|
|
25
25
|
memoryThreshold?: number;
|
|
26
|
-
/** ClickHouse data volume disk % used. Default 70
|
|
26
|
+
/** ClickHouse data volume disk % used. Default 70. The TRAJECTORY signal —
|
|
27
|
+
* "this volume is filling up, plan growth" — which is the one disk
|
|
28
|
+
* question a percentage answers well at any volume size. The imminent-
|
|
29
|
+
* breakage question is answered in GiB by `diskFreeCriticalGib`.
|
|
30
|
+
*
|
|
31
|
+
* The nightly restore-verify transiently doubles the database's on-disk
|
|
32
|
+
* footprint, so a database above ~35 % of the volume crosses a 70 % warn
|
|
33
|
+
* during every verify window. That is the knob's honest reading — the
|
|
34
|
+
* volume genuinely is that full while the scratch copy exists — but when
|
|
35
|
+
* the nightly warn becomes noise, the cures are a larger `storageGb`, a
|
|
36
|
+
* higher threshold here, or `backupVerify: false`; the critical floor is
|
|
37
|
+
* protected from the same transient by the verify gate's ×2 margin. */
|
|
27
38
|
diskWarnThreshold?: number;
|
|
28
|
-
/** ClickHouse data volume
|
|
29
|
-
|
|
39
|
+
/** Free space on the ClickHouse data volume, in GiB, below which the
|
|
40
|
+
* critical alarm pages. Defaults to `defaultDiskFreeCriticalGib(storageGb)`
|
|
41
|
+
* — the larger of an absolute floor and a share of the volume.
|
|
42
|
+
*
|
|
43
|
+
* Absolute rather than a percentage BECAUSE the failure it guards is
|
|
44
|
+
* absolute: a merge needs free bytes proportional to the parts it is
|
|
45
|
+
* merging, not to the volume, and the server stops accepting inserts on a
|
|
46
|
+
* fixed reserve. A percentage critical was either redundant (on a small
|
|
47
|
+
* volume the free-space floor is crossed first) or noise (on a large one
|
|
48
|
+
* 85 % used still leaves hundreds of usable GiB) — never uniquely useful.
|
|
49
|
+
*
|
|
50
|
+
* Must stay under a fifth of `storageGb`, so the floor is a slice of the
|
|
51
|
+
* disk rather than most of it; `resolveClickHouseStorage` enforces the
|
|
52
|
+
* pair together, since neither value is checkable alone.
|
|
53
|
+
*
|
|
54
|
+
* Replaces `diskCriticalThreshold` (a percentage of the volume used). The
|
|
55
|
+
* rename carries a unit change, so there is no value that means the same
|
|
56
|
+
* thing in both — a declaration on the old key fails to compile rather
|
|
57
|
+
* than deploying a threshold read as the wrong quantity. */
|
|
58
|
+
diskFreeCriticalGib?: number;
|
|
30
59
|
/** `<Error>`/`<Fatal>` server-log lines per 5 min. Default 30. */
|
|
31
60
|
serverErrorsPer5Min?: number;
|
|
32
61
|
/** Failed `executeQuery:` error lines per 5 min. Default 10 — absolute, not
|
|
@@ -52,7 +81,7 @@ export interface ClickHouseAlarmThresholds {
|
|
|
52
81
|
* backup heartbeat window. The remaining fields are consumed by the pattern's
|
|
53
82
|
* service-alarm and logAlarms wiring (see `ClickHouseAlarmThresholds`).
|
|
54
83
|
*/
|
|
55
|
-
export type ClickHouseHostAlarmThresholds = Pick<ClickHouseAlarmThresholds, "cpuThreshold" | "diskWarnThreshold" | "
|
|
84
|
+
export type ClickHouseHostAlarmThresholds = Pick<ClickHouseAlarmThresholds, "cpuThreshold" | "diskWarnThreshold" | "backupHeartbeatWindowHours">;
|
|
56
85
|
export declare const BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS = 26;
|
|
57
86
|
/**
|
|
58
87
|
* Ceiling derivation — an ORDERING bound, not CloudWatch expressibility.
|
|
@@ -97,7 +126,25 @@ export interface ClickHouseAlarmsProps {
|
|
|
97
126
|
* Omitted when `backupSchedule: false` — no backup task, no log group.
|
|
98
127
|
*/
|
|
99
128
|
backupTaskLogGroup?: ILogGroup;
|
|
129
|
+
/**
|
|
130
|
+
* Whether the backup task runs the restore-verify (the pattern's
|
|
131
|
+
* `backupVerify` prop, already resolved). `false` withholds the two verify
|
|
132
|
+
* alarms so a deliberately disabled verify never leaves marker-counting
|
|
133
|
+
* alarms armed against a script that emits no markers — the alarm half and
|
|
134
|
+
* the script half of the contract disable together. The backup-failure
|
|
135
|
+
* alarm and the success heartbeat stay: they monitor the backup itself.
|
|
136
|
+
*/
|
|
137
|
+
backupVerifyEnabled?: boolean;
|
|
100
138
|
config?: ClickHouseHostAlarmThresholds;
|
|
139
|
+
/**
|
|
140
|
+
* Critical free-space floor in GiB, ALREADY RESOLVED. Not read off `config`
|
|
141
|
+
* because its default depends on the volume size and its legality depends
|
|
142
|
+
* on the volume size — both of which live on the pattern, not here. The
|
|
143
|
+
* pattern resolves the pair once (`resolveClickHouseStorage`) and hands the
|
|
144
|
+
* answer down; re-deriving it here would be a second source for a number
|
|
145
|
+
* that must agree with the volume it guards.
|
|
146
|
+
*/
|
|
147
|
+
diskFreeCriticalGib: number;
|
|
101
148
|
/** Application ID for webhook-to-application alarm mapping. */
|
|
102
149
|
applicationId?: string;
|
|
103
150
|
}
|
|
@@ -107,7 +154,9 @@ export interface ClickHouseAlarmsProps {
|
|
|
107
154
|
* (never installed on these hosts) but by the lightweight put-metric-data
|
|
108
155
|
* timer the user-data installs (see `buildClickHouseUserData`), which publishes
|
|
109
156
|
* `mem_used_percent` / `disk_used_percent` under the same names and
|
|
110
|
-
* `AutoScalingGroupName` dimension the agent would use
|
|
157
|
+
* `AutoScalingGroupName` dimension the agent would use, plus `disk_free_gib`,
|
|
158
|
+
* which has no CloudWatch Agent equivalent and exists because the disk
|
|
159
|
+
* question that pages must be answered in bytes rather than percent — plus the
|
|
111
160
|
* backup-failure log alarm when a backup-task log group is supplied:
|
|
112
161
|
*
|
|
113
162
|
* - **Backup failures** — `AccessDenied` or `S3Exception` from the backup
|
|
@@ -5,7 +5,7 @@ import { Metric } from "aws-cdk-lib/aws-cloudwatch";
|
|
|
5
5
|
import { FilterPattern, MetricFilter } from "aws-cdk-lib/aws-logs";
|
|
6
6
|
import { ALARM_DEFAULTS, registerAlarm, tagAlarmsWithApplicationId, buildAlarmDescription } from "./alarmDefaults.js";
|
|
7
7
|
import { METRIC_NAMESPACE, stackScopedMetricNamespace } from "./metricNamespaces.js";
|
|
8
|
-
import { CLICKHOUSE_HOST_METRICS } from "../database/clickhouseConstants.js";
|
|
8
|
+
import { CLICKHOUSE_BACKUP_VERIFY_FAILED_MARKER, CLICKHOUSE_BACKUP_VERIFY_SKIPPED_MARKER, CLICKHOUSE_HOST_METRICS } from "../database/clickhouseConstants.js";
|
|
9
9
|
export const BACKUP_HEARTBEAT_DEFAULT_WINDOW_HOURS = 26;
|
|
10
10
|
/**
|
|
11
11
|
* Ceiling derivation — an ORDERING bound, not CloudWatch expressibility.
|
|
@@ -55,7 +55,9 @@ export function validateClickHouseAlarmThresholds(config) {
|
|
|
55
55
|
* (never installed on these hosts) but by the lightweight put-metric-data
|
|
56
56
|
* timer the user-data installs (see `buildClickHouseUserData`), which publishes
|
|
57
57
|
* `mem_used_percent` / `disk_used_percent` under the same names and
|
|
58
|
-
* `AutoScalingGroupName` dimension the agent would use
|
|
58
|
+
* `AutoScalingGroupName` dimension the agent would use, plus `disk_free_gib`,
|
|
59
|
+
* which has no CloudWatch Agent equivalent and exists because the disk
|
|
60
|
+
* question that pages must be answered in bytes rather than percent — plus the
|
|
59
61
|
* backup-failure log alarm when a backup-task log group is supplied:
|
|
60
62
|
*
|
|
61
63
|
* - **Backup failures** — `AccessDenied` or `S3Exception` from the backup
|
|
@@ -79,7 +81,7 @@ export function validateClickHouseAlarmThresholds(config) {
|
|
|
79
81
|
* see (the EC2 host and the backup task).
|
|
80
82
|
*/
|
|
81
83
|
export function createClickHouseAlarms(props) {
|
|
82
|
-
const { scope, instanceRole, asgName, alarmTopic, backupTaskLogGroup, config = {}, applicationId } = props;
|
|
84
|
+
const { scope, instanceRole, asgName, alarmTopic, backupTaskLogGroup, backupVerifyEnabled = true, config = {}, diskFreeCriticalGib, applicationId } = props;
|
|
83
85
|
validateClickHouseAlarmThresholds(config);
|
|
84
86
|
const alarms = [];
|
|
85
87
|
const snsAction = new SnsAction(alarmTopic);
|
|
@@ -116,24 +118,28 @@ export function createClickHouseAlarms(props) {
|
|
|
116
118
|
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
117
119
|
});
|
|
118
120
|
registerAlarm(diskWarnAlarm, snsAction, alarms);
|
|
119
|
-
const diskCriticalThreshold = config.diskCriticalThreshold ?? 85;
|
|
120
121
|
const diskCriticalAlarm = new Alarm(scope, "ClickHouseDiskCriticalAlarm", {
|
|
121
|
-
alarmDescription: buildAlarmDescription(`ClickHouse data volume
|
|
122
|
+
alarmDescription: buildAlarmDescription(`ClickHouse data volume under ${diskFreeCriticalGib} GiB free — merges and inserts are about to fail`, applicationId),
|
|
122
123
|
metric: new Metric({
|
|
123
124
|
namespace: CLICKHOUSE_HOST_METRICS.namespace,
|
|
124
|
-
metricName: CLICKHOUSE_HOST_METRICS.
|
|
125
|
+
metricName: CLICKHOUSE_HOST_METRICS.diskFreeMetric,
|
|
125
126
|
dimensionsMap: { [CLICKHOUSE_HOST_METRICS.asgDimension]: asgName },
|
|
126
127
|
period: Duration.minutes(5),
|
|
127
|
-
|
|
128
|
+
// Minimum, not Average: a single sample under the floor is the event,
|
|
129
|
+
// and averaging a dip against the surrounding minute hides exactly the
|
|
130
|
+
// moment a large merge claims the last of the disk.
|
|
131
|
+
statistic: "Minimum"
|
|
128
132
|
}),
|
|
129
|
-
threshold:
|
|
133
|
+
threshold: diskFreeCriticalGib,
|
|
130
134
|
evaluationPeriods: 2,
|
|
131
135
|
datapointsToAlarm: 2,
|
|
132
|
-
comparisonOperator: ComparisonOperator.
|
|
136
|
+
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
|
|
133
137
|
// BREACHING, unlike the other host alarms: a dead metrics publisher looks
|
|
134
138
|
// identical to a full disk (no datapoints either way), and this alarm's
|
|
135
139
|
// whole job is to fire before inserts start failing. The 2×5min window
|
|
136
|
-
// absorbs the boot gap on instance replacement
|
|
140
|
+
// absorbs the boot gap on instance replacement — and, on the deploy that
|
|
141
|
+
// first introduces the metric, the instance refresh that starts
|
|
142
|
+
// publishing it. Expect one transient ALARM on that deploy.
|
|
137
143
|
treatMissingData: TreatMissingData.BREACHING
|
|
138
144
|
});
|
|
139
145
|
registerAlarm(diskCriticalAlarm, snsAction, alarms);
|
|
@@ -163,6 +169,16 @@ export function createClickHouseAlarms(props) {
|
|
|
163
169
|
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
164
170
|
});
|
|
165
171
|
registerAlarm(backupFailureAlarm, snsAction, alarms);
|
|
172
|
+
if (backupVerifyEnabled) {
|
|
173
|
+
createBackupVerifyAlarms({
|
|
174
|
+
scope,
|
|
175
|
+
backupTaskLogGroup,
|
|
176
|
+
namespace: backupFailureNamespace,
|
|
177
|
+
snsAction,
|
|
178
|
+
applicationId,
|
|
179
|
+
alarms
|
|
180
|
+
});
|
|
181
|
+
}
|
|
166
182
|
createBackupHeartbeat({
|
|
167
183
|
scope,
|
|
168
184
|
backupTaskLogGroup,
|
|
@@ -176,6 +192,86 @@ export function createClickHouseAlarms(props) {
|
|
|
176
192
|
tagAlarmsWithApplicationId(alarms, applicationId);
|
|
177
193
|
return alarms;
|
|
178
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Restore-verify alarms — the half of the backup story `BACKUP_CREATED`
|
|
197
|
+
* cannot tell.
|
|
198
|
+
*
|
|
199
|
+
* The success marker the heartbeat reads means the BACKUP statement returned.
|
|
200
|
+
* It says nothing about whether the bytes can be read back, and the cheap
|
|
201
|
+
* check that looks like it would say so does not: a `structure_only` restore
|
|
202
|
+
* of a backup whose entire `data/` tree had been deleted returns `RESTORED`
|
|
203
|
+
* on 26.3.17.56, because it reads `metadata/` and never opens a part. So the
|
|
204
|
+
* backup task restores in full into a scratch database and emits its own
|
|
205
|
+
* verdict, and these two alarms read it.
|
|
206
|
+
*
|
|
207
|
+
* - **Verify failed** (`BACKUP_VERIFY_FAILED`, Sum >= 1 per hour) — the
|
|
208
|
+
* backup that was just written could not be restored, or restored without
|
|
209
|
+
* the tables the live database has, or restored empty. This is the alarm
|
|
210
|
+
* that means the backups are not backups.
|
|
211
|
+
* - **Verify skipped for size** (`BACKUP_VERIFY_SKIPPED_FOR_SIZE`, Sum >= 1
|
|
212
|
+
* per DAY) — the restore would not have left the critical free-space floor
|
|
213
|
+
* clear, so the task declined to run it. Slower than its sibling because
|
|
214
|
+
* it is not an emergency on the day it first fires; it is nonetheless a
|
|
215
|
+
* page, because from that day on nothing is checking that the backups
|
|
216
|
+
* restore, and a degradation nobody is told about is the failure mode this
|
|
217
|
+
* whole mechanism exists to remove. The cure is a larger `storageGb` or
|
|
218
|
+
* verification moved off-box.
|
|
219
|
+
*
|
|
220
|
+
* Both treat missing data as NOT_BREACHING: a run that never happened is the
|
|
221
|
+
* heartbeat's question, not this one's, and double-paging one outage from two
|
|
222
|
+
* alarms buys nothing.
|
|
223
|
+
*/
|
|
224
|
+
function createBackupVerifyAlarms(props) {
|
|
225
|
+
const { scope, backupTaskLogGroup, namespace, snsAction, applicationId, alarms } = props;
|
|
226
|
+
const verifyFailedMetricName = "ClickHouseBackupVerifyFailedCount";
|
|
227
|
+
new MetricFilter(scope, "ClickHouseBackupVerifyFailedMetricFilter", {
|
|
228
|
+
logGroup: backupTaskLogGroup,
|
|
229
|
+
metricNamespace: namespace,
|
|
230
|
+
metricName: verifyFailedMetricName,
|
|
231
|
+
filterPattern: FilterPattern.allTerms(CLICKHOUSE_BACKUP_VERIFY_FAILED_MARKER),
|
|
232
|
+
metricValue: "1",
|
|
233
|
+
defaultValue: 0
|
|
234
|
+
});
|
|
235
|
+
const verifyFailedAlarm = new Alarm(scope, "ClickHouseBackupVerifyFailedAlarm", {
|
|
236
|
+
alarmDescription: buildAlarmDescription("ClickHouse backup could not be restored — the backup completed but does not read back; treat the retained backups as unproven", applicationId),
|
|
237
|
+
metric: new Metric({
|
|
238
|
+
namespace,
|
|
239
|
+
metricName: verifyFailedMetricName,
|
|
240
|
+
period: Duration.hours(1),
|
|
241
|
+
statistic: "Sum"
|
|
242
|
+
}),
|
|
243
|
+
threshold: 1,
|
|
244
|
+
evaluationPeriods: 1,
|
|
245
|
+
datapointsToAlarm: 1,
|
|
246
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
247
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
248
|
+
});
|
|
249
|
+
registerAlarm(verifyFailedAlarm, snsAction, alarms);
|
|
250
|
+
const verifySkippedMetricName = "ClickHouseBackupVerifySkippedCount";
|
|
251
|
+
new MetricFilter(scope, "ClickHouseBackupVerifySkippedMetricFilter", {
|
|
252
|
+
logGroup: backupTaskLogGroup,
|
|
253
|
+
metricNamespace: namespace,
|
|
254
|
+
metricName: verifySkippedMetricName,
|
|
255
|
+
filterPattern: FilterPattern.allTerms(CLICKHOUSE_BACKUP_VERIFY_SKIPPED_MARKER),
|
|
256
|
+
metricValue: "1",
|
|
257
|
+
defaultValue: 0
|
|
258
|
+
});
|
|
259
|
+
const verifySkippedAlarm = new Alarm(scope, "ClickHouseBackupVerifySkippedAlarm", {
|
|
260
|
+
alarmDescription: buildAlarmDescription("ClickHouse backup restore-verify skipped — the database no longer fits a verify restore alongside itself; raise storageGb or move verification off-box", applicationId),
|
|
261
|
+
metric: new Metric({
|
|
262
|
+
namespace,
|
|
263
|
+
metricName: verifySkippedMetricName,
|
|
264
|
+
period: Duration.days(1),
|
|
265
|
+
statistic: "Sum"
|
|
266
|
+
}),
|
|
267
|
+
threshold: 1,
|
|
268
|
+
evaluationPeriods: 1,
|
|
269
|
+
datapointsToAlarm: 1,
|
|
270
|
+
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
271
|
+
treatMissingData: TreatMissingData.NOT_BREACHING
|
|
272
|
+
});
|
|
273
|
+
registerAlarm(verifySkippedAlarm, snsAction, alarms);
|
|
274
|
+
}
|
|
179
275
|
/**
|
|
180
276
|
* Backup success heartbeat — the complete-but-slow half of the backup alarm
|
|
181
277
|
* pair (the AccessDenied/S3Exception failure alarm above is the fast half).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.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": "^
|
|
84
|
-
"@fjall/util": "^
|
|
83
|
+
"@fjall/generator": "^17.0.0",
|
|
84
|
+
"@fjall/util": "^17.0.0",
|
|
85
85
|
"constructs": "^10.7.2"
|
|
86
86
|
},
|
|
87
87
|
"overrides": {
|