feat: ClickHouse sink for query history, aggregated metrics and session snapshots#48
Open
Sanikadze wants to merge 6 commits into
Open
feat: ClickHouse sink for query history, aggregated metrics and session snapshots#48Sanikadze wants to merge 6 commits into
Sanikadze wants to merge 6 commits into
Conversation
…on snapshots Includes a small lifecycle fix in queryCompleted: archive on master Completed even when not all segments have reported. Without this, under load garbageCollect evicts queries from RunningQueriesStorage before the segment-timeout fires, so they never reach archChan and the CH sink stays empty for query_events / aggregated_metrics. Closes open-gpdb#34
- main.go: resolve config path with fallback to yagpcc.yaml in CWD when --config-path is unset, instead of reading /yagpcc.yaml from root - migrations.go: fix RenderTemplate doc comment to match missingkey=error (missing params error out, they are not rendered as <no value>) - aggregated_storage_test: save/restore package-global CurrentTime via t.Cleanup to avoid leaking into other tests - docs: correct dump_only mode (process exits, master does not continue) and drop the non-existent --dump-only flag in favour of --dump-schema
Bring the ClickHouse sink branch up to date with main's query-archival refactor (open-gpdb#35/open-gpdb#37/open-gpdb#39: explicit archive reasons, segment-refresh gating, GC forwarding). Conflict resolution in internal/master/background.go: - BackgroundStorage struct: keep both the chWriter (ClickhouseSink) field and main's segRefreshMu/segRefreshTimes. - queryCompleted: adopt main's version wholesale — it now returns (int, archiveReason) and gates archival on MinSegmentRefreshTime or the segment timeout. The sink's old "archive immediately on Completed" path is dropped; under main's logic the sink receives more complete per-segment data. The CH-sink wiring (interface, accessors, Submit/FlushAggregates) lives in untouched regions and is preserved as-is. internal/master/clickhouse_test.go: drop SegmentNodes from the RunningQuery fixture — main removed the SegmentNodes field and SegmentIndexNodes type.
query_events template_query/template_plan columns were fed from raw qi.QueryText/qi.PlanText; use qi.TemplateQueryText/TemplatePlanText instead.
- errcheck: blank-assign fmt.Fprint* returns in schema CLI - staticcheck QF1006: lift loop conditions in finalFlush and writer test - copyloopvar: drop redundant loop-var copy (Go 1.22+) - unconvert: remove unnecessary int32 conversion - SA1019: nolint template fields read in mapping.go — deprecated in proto but still sent by the collector
Sanikadze
force-pushed
the
feat/clickhouse-sink
branch
from
July 9, 2026 21:21
b3ed724 to
ac9f378
Compare
Contributor
|
Need to be rewritten as was suggested in f74a5f5 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional ClickHouse sink to the master role that streams query history, aggregated metrics and session snapshots into a ClickHouse cluster for long-term, ad-hoc SQL analysis. Complements the existing JSON archiver (which keeps working unchanged) and the Prometheus/VictoriaMetrics path.
The sink is opt-in via
clickhouse.enabled: trueinyagpcc.yaml. When disabled the orchestrator is a no-op and yagpcc behaves exactly as before.Motivation
The JSON archiver and Prometheus metrics cover the live-monitoring and short-term audit use-cases, but neither lets operators run ad-hoc SQL queries against historical query data (top-N slowest, by user/database/resgroup, plan diffs over time, etc.). ClickHouse is the natural fit for this workload — columnar storage, fast aggregation, TTL-based retention.
What it does
The sink persists three first-class tables (plus a bookkeeping table) under the
yagpccdatabase in ClickHouse:yagpcc.query_eventsArray(Tuple(...))and aggregated SystemStat/Instrumentation totals.yagpcc.aggregated_metricsSummingMergeTreerollups keyed by(user, database, query_id, plan_id, bucket_time);resource_groupis carried as a non-key column. Mirrors the in-memoryAggregatedStorageflush.yagpcc.session_snapshotspg_stat_activity(default every 10s).yagpcc._yagpcc_metaThe DDL lives in
internal/sink/clickhouse/migrations/0001_init.up.sqland is embedded into the binary via//go:embed. The retention TTL is atext/templateplaceholder rendered fromclickhouse.retention_daysat startup.Architecture
Boundaries are designed to keep the sink isolated from yagpcc's hot-path storage:
mapping.go.ClickhouseSinkinterface tointernal/master/background.go(Submit + FlushAggregates) so the master can swap a fake in tests.batchPreparerinterface (a subset ofdriver.Conn) so unit tests use hand-rolled fakes instead of standing up a real server.Data flow
archiver.goandstatwriter.gokeep working unchanged; the JSON archive becomes a manual disaster-recovery source rather than the only persistence path. Both can be enabled at the same time, only one, or neither.Configuration
Minimal master config:
When
enabled: false, the sink is constructed as a no-op and adds no overhead.Note: the config also accepts
sinks.plan_nodes(defaultfalse) — it is reserved for a future per-plan-node table and is a no-op in this PR.CLI flags
--dump-schema— print cumulative CH DDL to stdout and exit (no connection required)--dump-migration --from=N --to=M— print SQL to migrate between schema versions--migrate-only— connect, apply pending migrations, exit--verify-schema— connect, verify schema version matches binary expectations, exitSchema management
auto(default)verify_only_yagpcc_metaversion, fatal on mismatch.dump_onlyThe bookkeeping table
yagpcc._yagpcc_metatracksversion,applied_at,direction='up'|'down'for each migration.Observability
New Prometheus metrics:
yagpcc_ch_inserts_total{table, status}— counter ofINSERTbatchesyagpcc_ch_dropped_rows_total{table, reason}— counter of dropped rows (buffer_full,filter,mapping_error,insert_error)yagpcc_ch_buffer_size{table}— gauge of current buffer occupancyyagpcc_ch_batch_duration_seconds{table}— histogram of batch flush latencyyagpcc_ch_schema_mismatch— gauge, 1 when the deployed schema version disagrees with the binaryyagpcc_ch_unreachable— gauge, 1 while ClickHouse is unreachable (suitable for alerting)Backpressure
The ring buffer offers two policies:
drop_oldest(default) — drop oldest rows when buffer is full, incrementyagpcc_ch_dropped_rows_total{reason="buffer_full"}. Producer never blocks.block— block the producer until the writer drains. NOT recommended in production: a stuck CH can stall the master.Graceful shutdown
ctx.Done()triggers a final flush guarded by a 30s timeout so a stuck CH cannot block master shutdown. The flusher goroutine and the per-table writers are drained in order, then the connection is closed.Testing
*_test.gowith hand-rolled fakes forbatchPreparer/driver.Conn. Coverage ininternal/sink/clickhouse/is high (writer, tables, migrations, schema, buffer, mapping, client, metrics all have dedicated suites).internal/sink/clickhouse/integration_test.gousestestcontainers-goto spin up a real ClickHouse container, apply migrations, write rows, verify reads. Gated by-tags=integrationto keepgo test ./...fast; CI runs it as a dedicatedclickhouse-integrationjob.Files changed
internal/sink/clickhouse/cmd/server/schema_cli.go+ test,cmd/server/main.go(schema-CLI short-circuit +dump_onlystartup mode)internal/config/clickhouse.go+ test,internal/config/config.go(validation),cmd/server/yagpcc_master.yaml(example)internal/master/background.go(CH sink interface, Submit/Flush),internal/master/archiver.go(submit on archive),internal/storage/aggregated_storage.go(cycle hook)internal/app/app.go(master-only CH writer construction).github/workflows/test.yaml(newclickhouse-integrationjob)docs/clickhouse-sink-architecture.md,docs/vm-vs-ch.md, README, architecture(+ru),PROJECT_CONTEXT.md.rat-excludes(embedded SQL migrations)46 files changed, ~10200 insertions.
Backwards compatibility
clickhouse.enabled: falsekeeps existing deployments unchanged.Test plan
make buildsucceedsmake unittestpasses (excluding integration tag)make lintpasses (golangci-lint)make packageproduces a deb that installs cleanlyclickhouse.enabled: false— yagpcc behaves identically to current mainclickhouse.enabled: true, run load —yagpcc.query_eventsrows appear,yagpcc_ch_inserts_total{table="query_events"}growssystemctl stop yagpccflushes buffers within 30s, noWARN clickhouse sink close errorin logs--dump-schemaoutputs valid CH DDL--migrate-onlyapplies migrations from a clean DB and exits with code 0--verify-schemaexits with code 0 when versions match, code 1 when they don'tKnown dependencies
github.com/ClickHouse/clickhouse-go/v2(direct, native protocol client)github.com/testcontainers/testcontainers-go(test-only)Related
docs/clickhouse-sink-architecture.md— full architecture (this PR adds it)docs/vm-vs-ch.md— when to use VictoriaMetrics vs ClickHouse (this PR adds it)