|
| 1 | +package merger |
| 2 | + |
| 3 | +// compare_bench_test.go — SAME-MACHINE, SAME-WORKLOAD throughput comparison of |
| 4 | +// the merger's decode-free central ingest against the path Thanos Receive uses: |
| 5 | +// the Prometheus TSDB head Appender (parse-done -> Append encodes into head XOR |
| 6 | +// chunks + indexes each series -> Commit writes the head WAL). This is the |
| 7 | +// per-sample CPU the merger OFFLOADS to the edge (the edge pre-encodes the XOR |
| 8 | +// chunk; the merger ingests it decode-free and only pays the per-series index at |
| 9 | +// block-build time). |
| 10 | +// |
| 11 | +// The Prometheus path decodes the SAME XOR frags the merger ingests, so both |
| 12 | +// systems ingest byte-identical samples. Run all on tmpfs for a CPU-bound, |
| 13 | +// apples-to-apples number (the fsync-bearing stages are IO-bound on spinning |
| 14 | +// disk and would otherwise measure the disk, not the engine): |
| 15 | +// |
| 16 | +// TMPDIR=/dev/shm GOPRIVATE='github.com/ProjectASAP/*' go test ./internal/merger/ \ |
| 17 | +// -run '^$' -bench 'IngestFragmentBatch|WindowToPendingBlock|PrometheusTSDBAppendCommit' \ |
| 18 | +// -benchmem -benchtime=10x -count=3 |
| 19 | +// |
| 20 | +// Fair comparison metric = "central CPU per sample to make data queryable+indexed": |
| 21 | +// - Prometheus/Thanos: BenchmarkPrometheusTSDBAppendCommit (append+commit; head |
| 22 | +// is indexed & queryable immediately). |
| 23 | +// - Merger: BenchmarkIngestFragmentBatch + BenchmarkWindowToPendingBlock |
| 24 | +// (ingest is decode-free and NOT yet queryable; block-build is where the |
| 25 | +// merger pays the per-series index — that sum is the honest queryable gate, |
| 26 | +// NOT the raw ingest figure alone). |
| 27 | +// |
| 28 | +// === MEASURED, SAME MACHINE (Xeon Gold 5512U), SAME WORKLOAD (1000 series x 120 |
| 29 | +// samples = 120k samples/window), SINGLE CORE, tmpfs (/dev/shm) === |
| 30 | +// |
| 31 | +// path (per core) per 120k samples/s/core basis |
| 32 | +// ASAP merger — decode-free ingest 23.8 ms 5.04 M buffer pre-encoded XOR chunks + WAL (no per-sample parse/encode) |
| 33 | +// VictoriaMetrics v1.145 remote_write — 3.20 M snappy/proto parse + AddRows (bg part-merge deferred); SERVER-CONFIRMED 6.0M rows, 1000 series queryable |
| 34 | +// Prometheus TSDB v0.308 (Thanos core) 87.0 ms 1.38 M per-sample encode + index + WAL commit (head queryable; input pre-decoded, so this is the pure TSDB-core cost) |
| 35 | +// ASAP merger — window->pending block ~225 ms ~0.53 M full on-disk TSDB block per 2-min window (deferred/background; the merger's heaviest per-sample stage) |
| 36 | +// |
| 37 | +// Read (honest): |
| 38 | +// - Single-core ingest-to-queryable-buffer: merger 5.0M > VM 3.2M > Prometheus |
| 39 | +// 1.4M. The merger leads because the EDGE pre-encoded the XOR chunk (offloaded |
| 40 | +// ~1.38 us/sample edge cost, Fig 11), so central ingest is decode-free. VM |
| 41 | +// beats Prometheus-TSDB because its ingest path is more optimised, not because |
| 42 | +// of any ASAP work. |
| 43 | +// - Persisting on-disk blocks is a DEFERRED/background cost for all three (VM |
| 44 | +// part-merge, Prometheus 2h head->block, merger per-window block-build). The |
| 45 | +// merger's block-build (~0.53M/s/core here) is where its deferred per-series |
| 46 | +// index lands and is its slowest stage — a real perf target, not a win. |
| 47 | +// - VM CAVEAT: remote_write timestamps MUST be within -retentionPeriod or VM |
| 48 | +// parses-then-DROPS them (an early run timestamped at the corpus's 2023 base |
| 49 | +// showed a bogus 11.5 M/s "accept" rate with 0 series stored). The 3.20 M/s |
| 50 | +// above is with near-now timestamps and is server-confirmed queryable. |
| 51 | +// - allocs/window: merger ingest 14.1k, Prometheus append 22.3k. |
| 52 | + |
| 53 | +import ( |
| 54 | + "context" |
| 55 | + "testing" |
| 56 | + |
| 57 | + "github.com/prometheus/prometheus/model/labels" |
| 58 | + "github.com/prometheus/prometheus/storage" |
| 59 | + "github.com/prometheus/prometheus/tsdb" |
| 60 | + "github.com/prometheus/prometheus/tsdb/chunkenc" |
| 61 | +) |
| 62 | + |
| 63 | +// decodedSeries is the corpus expressed as raw (labels, samples) — the form a |
| 64 | +// remote_write/scrape ingester (the Prometheus TSDB head that Thanos Receive |
| 65 | +// embeds) consumes. Decoded ONCE, outside the timed loop, from the same XOR |
| 66 | +// frags the merger ingests. |
| 67 | +type decodedSeries struct { |
| 68 | + ls labels.Labels |
| 69 | + ts []int64 |
| 70 | + val []float64 |
| 71 | +} |
| 72 | + |
| 73 | +func decodeCorpus(tb testing.TB, c benchCorpus) []decodedSeries { |
| 74 | + tb.Helper() |
| 75 | + out := make([]decodedSeries, 0, len(c.frags)) |
| 76 | + for fi := range c.frags { |
| 77 | + f := &c.frags[fi] |
| 78 | + chk, err := chunkenc.FromData(chunkenc.EncXOR, f.Data) |
| 79 | + if err != nil { |
| 80 | + tb.Fatal(err) |
| 81 | + } |
| 82 | + ds := decodedSeries{ls: labelsFor(f.MetricName, f.Attributes)} |
| 83 | + it := chk.Iterator(nil) |
| 84 | + for it.Next() == chunkenc.ValFloat { |
| 85 | + t, v := it.At() |
| 86 | + ds.ts = append(ds.ts, t) |
| 87 | + ds.val = append(ds.val, v) |
| 88 | + } |
| 89 | + if err := it.Err(); err != nil { |
| 90 | + tb.Fatal(err) |
| 91 | + } |
| 92 | + out = append(out, ds) |
| 93 | + } |
| 94 | + return out |
| 95 | +} |
| 96 | + |
| 97 | +// BenchmarkPrometheusTSDBAppendCommit — the Thanos Receive central ingest path |
| 98 | +// (Prometheus TSDB head Appender). Per 120k-sample window: Append every |
| 99 | +// (series,t,v) (encodes into head XOR chunks + indexes each series) then Commit |
| 100 | +// (head WAL). Same workload/machine/disk as the merger benches. |
| 101 | +func BenchmarkPrometheusTSDBAppendCommit(b *testing.B) { |
| 102 | + corpus := buildBenchCorpus(benchNumSeries, 1, benchPerChunk) |
| 103 | + series := decodeCorpus(b, corpus) |
| 104 | + ctx := context.Background() |
| 105 | + b.SetBytes(int64(corpus.numSamples) * 16) // 16 B/sample (8B ts + 8B f64) raw |
| 106 | + b.ReportAllocs() |
| 107 | + b.ResetTimer() |
| 108 | + for i := 0; i < b.N; i++ { |
| 109 | + b.StopTimer() |
| 110 | + dir := b.TempDir() |
| 111 | + db, err := tsdb.Open(dir, nil, nil, tsdb.DefaultOptions(), nil) |
| 112 | + if err != nil { |
| 113 | + b.Fatal(err) |
| 114 | + } |
| 115 | + // keep each iteration's window strictly forward of the last so the head |
| 116 | + // never rejects out-of-order (fresh DB per iter also guarantees this). |
| 117 | + off := int64(i+1) * int64(benchPerChunk) * 1000 * 2 |
| 118 | + b.StartTimer() |
| 119 | + |
| 120 | + app := db.Appender(ctx) |
| 121 | + for si := range series { |
| 122 | + s := &series[si] |
| 123 | + var ref storage.SeriesRef |
| 124 | + for k := range s.ts { |
| 125 | + var aerr error |
| 126 | + ref, aerr = app.Append(ref, s.ls, s.ts[k]+off, s.val[k]) |
| 127 | + if aerr != nil { |
| 128 | + b.Fatal(aerr) |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + if err := app.Commit(); err != nil { |
| 133 | + b.Fatal(err) |
| 134 | + } |
| 135 | + |
| 136 | + b.StopTimer() |
| 137 | + _ = db.Close() |
| 138 | + b.StartTimer() |
| 139 | + } |
| 140 | +} |
0 commit comments