Skip to content

Commit 9314462

Browse files
committed
roachtest: add splits perturbation test
Reframe "how many splits can the cluster sustain" as a perturbation test that runs concurrently with a foreground KV workload and reports impact in roachperf-comparable terms (throughput ratio, density in replicas/vcpu/node). Supersedes the legacy kv/splits/*/quiesce=*/lease=* binary tests dropped earlier in this PR. Two flavors are registered together: asleep=true measures total replica capacity (followers park via store liveness quiescence), asleep=false measures active-replica capacity (every replica costs full per-tick CPU, the relevant ceiling during outages where followers can't sleep). asleep=true targets ~9,400 replicas/vcpu (40% over the production telemetry cluster's ~6,700 replicas/vcpu non-quiescent density) to stress-test the primary quiescence path beyond current production density; asleep=false targets ~6,250 replicas/vcpu to match the telemetry cluster's observed non-quiescent density. asleep=false also disables store-liveness follower sleep via the kv.raft.store_liveness.quiescence.enabled cluster setting; leader leases keep leaseholders awake unconditionally so no additional knob is needed. Design (see the struct-level comment in splits.go for the long form): - Partition the cluster's stores into disjoint groups of three on distinct nodes; pin one database per group with voter_constraints that one-to-one map each voter to a store. With this every split's children inherit their parent's correct placement, so no SCATTER and no rebalance churn during the split storm. - Issue splits per group in parallel batches; descending keys per batch to keep each split landing on the leftmost (stable, single-leader) range rather than chaining new raft groups together. - After batch 0 (and again at end-of-perturbation), do an explicit deterministic round-robin lease rebalance via ALTER RANGE RELOCATE LEASE. Plain SCATTER stalls at 1.5-3x spread under per-store voter_constraints because the allocator's lease scorer picks the same biased target on every re-scatter. Round-robin converges in one pass; dispatch is parallelized within a group (concurrency 16) and across groups (one task per group). - Per-call ALTER RANGE failures are tolerated up to 10% (catches systemic problems like a node down while shrugging off transient range-merge races); a 100%-failure floor catches small-batch pathologies that the percentage gate would skip. First error is surfaced in the failure message and in a warn log when any failure happens below the threshold. Knobs in splits.setup(): - asleep=true: targetSplits=600k, batchSize=10k (full); the optional devSizer / metamorphicSizer interfaces (added in this commit) let the dev and metamorphic variants shrink to 4k and 60k respectively. - asleep=false: targetSplits=400k, batchSize=5k (full); 40k metamorphic; shares the 4k dev with asleep=true. - v.splits=500 + v.scatter=true: enough kv ranges to avoid a single-range hotspot for the foreground workload, scattered so leases distribute evenly. - v.timeout=5h: full 600k storm + end-of-perturbation rebalance runs for over an hour; the default 3h budget is too tight. asleep=false shares the budget since the rebalance phase dominates either way. - v.tokenReturnTime=1h: real outstanding flow-control tokens drain within seconds of workload cancel, but ValidateTokensReturned's SQL query races against background flow-control activity (replicate queue, lease queue, jobs) at 1-2M replicas and observed transient non-zero diffs at the 10m default. A longer window gives the retry loop a better chance to land on a clean snapshot. - impact=10x, recoveryImpact=3x: during the split storm the cluster is expected to absorb significant throughput loss; the meaningful pass/fail signal is whether it returns to baseline afterwards. Both flavors share these thresholds; the asleep=false gates may be tightened in a follow-up now that the flavor lands at production density. - SkipPostValidations=PostValidationReplicaDivergence: at 1.2M-1.8M ranges the post-test consistency check can't scan a meaningful fraction within the framework's 20m budget (observed: 0 ranges in 20m). The framework tolerates the timeout but logs it as a post-assertion failure; skipping explicitly keeps the artifact clean. addDev now also zeros v.recoveryImpact, v.timeout, and v.tokenReturnTime, all of which splits.setup() sets to non-zero values. Without these clears the dev variant would inherit a 3x recovery throughput gate (too tight for noisy local processes), a 5h test timeout (would hide hangs behind a long wait), and a 1h token-return wait (wildly overshoots what's meaningful on a 5-node local cluster). Test fixtures: - waitForVoterPlacement polls per-range placement against the group's expected store set and nudges non-conforming ranges into the replicate queue via crdb_internal.kv_enqueue_replica (must run on the leaseholder). - waitForVoterPlacement budget defaults to 10 minutes and scales up to v.timeout/3 (capped at 90 minutes) when the test sets a per-test timeout. The 10-minute default suits the dev variant; the lift makes the nightly variant tolerant of slow up-replication. - reportDensity / reportBalance / summarize give per-node and per-store range and lease distribution at every batch boundary, with deterministic min/max tiebreaks so log diffs are stable across runs. Epic: none Release note: None
1 parent d74b408 commit 9314462

3 files changed

Lines changed: 1136 additions & 1 deletion

File tree

pkg/cmd/roachtest/tests/perturbation/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ go_library(
1414
"network_partition.go",
1515
"restart_node.go",
1616
"slow_disk.go",
17+
"splits.go",
1718
],
1819
importpath = "github.com/cockroachdb/cockroach/pkg/cmd/roachtest/tests/perturbation",
1920
visibility = ["//visibility:public"],
@@ -23,6 +24,7 @@ go_library(
2324
"//pkg/cmd/roachtest/registry",
2425
"//pkg/cmd/roachtest/roachtestutil",
2526
"//pkg/cmd/roachtest/roachtestutil/artifactsutil",
27+
"//pkg/cmd/roachtest/roachtestutil/task",
2628
"//pkg/cmd/roachtest/spec",
2729
"//pkg/cmd/roachtest/test",
2830
"//pkg/roachprod/install",
@@ -34,6 +36,7 @@ go_library(
3436
"//pkg/workload/cli",
3537
"//pkg/workload/histogram",
3638
"@com_github_cockroachdb_errors//:errors",
39+
"@com_github_lib_pq//:pq",
3740
"@com_github_stretchr_testify//require",
3841
],
3942
)

pkg/cmd/roachtest/tests/perturbation/framework.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ type variations struct {
123123
profileOptions []roachtestutil.ProfileOptionFunc
124124
specOptions []spec.Option
125125
clusterSettings map[string]string
126+
// skipPostValidations is forwarded to TestSpec.SkipPostValidations. The
127+
// zero value runs all post-test validations. Perturbations that
128+
// inflate the cluster (large range counts, heavy fills) can set this
129+
// to skip checks that exceed the framework's per-check budget.
130+
skipPostValidations registry.PostValidation
126131
}
127132

128133
const NUM_REGIONS = 3
@@ -395,6 +400,9 @@ func setup(p perturbation, impact acceptableImpact) variations {
395400
roachtestutil.ProfMultipleFromP99(10),
396401
}
397402
v.impact = impact
403+
// recoveryImpact left at zero value — falls back to impact at the
404+
// runTest use site. Tests that want a different threshold for the
405+
// recovery interval set it explicitly.
398406
v.clusterSettings = make(map[string]string)
399407
// Having the io_load_listener logs makes it easier to debug failures.
400408
v.clusterSettings["server.debug.default_vmodule"] = "io_load_listener=1"
@@ -416,6 +424,13 @@ func RegisterTests(r registry.Registry) {
416424
register(r, restart{}, notSkipped)
417425
addLong(r, restart{})
418426
register(r, backup{}, notSkipped)
427+
// splits runs in two flavors: asleep=true exercises total replica
428+
// capacity (followers park via store liveness quiescence), asleep=false
429+
// exercises active-replica capacity (every replica costs full per-tick
430+
// CPU). See splits.go for the long-form rationale.
431+
for _, asleep := range []bool{true, false} {
432+
register(r, splits{asleep: asleep}, notSkipped)
433+
}
419434

420435
// TODO(ssd): We skipped the majority of these tests so that we can focus on
421436
// one at a time. These are vaguely ordered by their previous pass rate
@@ -571,6 +586,13 @@ func addMetamorphic(r registry.Registry, p perturbation, skipReason string) {
571586
rng, seed := randutil.NewPseudoRand()
572587
v := p.setupMetamorphic(rng)
573588
v.seed = seed
589+
// Some perturbations need to scale down for the metamorphic variant
590+
// (the randomized clusters this suite picks are smaller than the
591+
// dedicated nightly cluster). They implement metamorphicSizer to
592+
// return a shrunk version of themselves.
593+
if ms, ok := v.perturbation.(metamorphicSizer); ok {
594+
v.perturbation = ms.sizeForMetamorphic()
595+
}
574596
v = v.finishSetup()
575597
spec := registry.TestSpec{
576598
Name: fmt.Sprintf("perturbation/metamorphic/%s", v.perturbationName()),
@@ -581,6 +603,7 @@ func addMetamorphic(r registry.Registry, p perturbation, skipReason string) {
581603
Cluster: v.makeClusterSpec(),
582604
Leases: v.leaseType,
583605
Randomized: true,
606+
SkipPostValidations: v.skipPostValidations,
584607
PostProcessPerfMetrics: perturbationDefaultProcessFunction,
585608
Run: v.runTest,
586609
}
@@ -600,6 +623,7 @@ func addFull(r registry.Registry, p perturbation, skipReason string) {
600623
Cluster: v.makeClusterSpec(),
601624
Leases: v.leaseType,
602625
Benchmark: true,
626+
SkipPostValidations: v.skipPostValidations,
603627
PostProcessPerfMetrics: perturbationDefaultProcessFunction,
604628
Run: v.runTest,
605629
}
@@ -673,8 +697,22 @@ func addLong(r registry.Registry, p perturbation) {
673697

674698
func addDev(r registry.Registry, p perturbation) {
675699
v := p.setup()
676-
// Dev tests never fail on latency increases.
700+
// Dev tests never fail on latency or throughput changes — local
701+
// processes on a developer machine are too noisy for either to be
702+
// meaningful. Zero out both intervals' thresholds. recoveryImpact
703+
// must be zeroed explicitly even though impact is also zero: a
704+
// perturbation setup that set recoveryImpact to a non-zero value
705+
// would otherwise survive and gate the dev run.
677706
v.impact = noImpactThresholds()
707+
v.recoveryImpact = acceptableImpact{}
708+
// Clear any per-test timeout override; the registry default is fine
709+
// for a 30-second perturbation on a 5-node local cluster, and an
710+
// override sized for the nightly variant (e.g. several hours) would
711+
// hide hangs in the dev run. Same reasoning for tokenReturnTime:
712+
// the nightly override (e.g. 1h) wildly overshoots what's
713+
// meaningful on a 5-node local cluster.
714+
v.timeout = 0
715+
v.tokenReturnTime = 0
678716
// Make the tests faster for development.
679717
v.splits = 1
680718
v.numNodes = 5
@@ -696,6 +734,12 @@ func addDev(r registry.Registry, p perturbation) {
696734

697735
// Allow the test to run on dev machines.
698736
v.cloud = registry.AllClouds
737+
// Some perturbations need to scale down for the dev variant (smaller
738+
// cluster, faster wall time). They implement devSizer to return a
739+
// shrunk version of themselves.
740+
if ds, ok := v.perturbation.(devSizer); ok {
741+
v.perturbation = ds.sizeForDev()
742+
}
699743
v = v.finishSetup()
700744
spec := registry.TestSpec{
701745
Name: fmt.Sprintf("perturbation/dev/%s", v.perturbationName()),
@@ -747,6 +791,18 @@ type perturbationNamer interface {
747791
nameSuffix() string
748792
}
749793

794+
// devSizer is an optional interface for perturbations that need to shrink
795+
// themselves for the dev variant (small cluster, fast wall time). addDev
796+
// invokes sizeForDev() if the perturbation implements it.
797+
type devSizer interface {
798+
sizeForDev() perturbation
799+
}
800+
801+
// metamorphicSizer is the same idea as devSizer for the metamorphic variant.
802+
type metamorphicSizer interface {
803+
sizeForMetamorphic() perturbation
804+
}
805+
750806
func prettyPrint(title string, stats map[string]aggregatedStat) string {
751807
var outputStr strings.Builder
752808
outputStr.WriteString(title + "\n")

0 commit comments

Comments
 (0)