cockroachdb / cockroachdb/cockroach

schemachanger: C-SPANN HNSW backfill stalls indefinitely under default partition sizing at 9M+ vectors due to single-coordinator merge pinning

Open
#171,851 1 comment 0 reactions 0 assignees View on GitHub
A-jobs C-bug O-community T-specialized-indexing X-blathers-triaged
Dominant language
Go
Stars
32.5k
Forks
4.1k
PR merge metrics
PR metrics pending

Description

## Describe the problem

A `CREATE VECTOR INDEX` on a 9M-row, 1024-dim table aborted at 31.6% completion after 6h 52m wall-clock, with no possibility of forward progress. The build was using **default partition sizing** (`min_partition_size = 16`, `max_partition_size = 128`) plus the full set of nine max-aggressive build-time cluster settings recommended for vector backfills. Once the build reached the merge phase, a single coordinator node was pinned at ~96% CPU on partition merge while the other two nodes idled at ~32%. The `fraction_completed` gauge then decelerated from 4.8%/hr to 0%/hr and froze for the final 18.7 minutes before we cancelled.

The same workload (same cluster, same dataset, same nine build-time tunings) completes in **4h 12m** with all three nodes balanced when the index is created with `WITH (min_partition_size = 64, max_partition_size = 1024)`. So the bottleneck is partition-config-dependent, not intrinsic to HNSW or to the dataset.

## Environment

- **CockroachDB:** v26.1.3 (release build, linux-amd64), insecure mode
- **Cluster:** 3 nodes, `m6a.2xlarge` (8 vCPU / 32 GiB), 500 GB gp3 EBS each, single AZ `us-east-1a`
- **Schema:**
```sql
CREATE TABLE embeddings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
review_id STRING NOT NULL,
embedding_type STRING NOT NULL,
vector VECTOR(1024) NOT NULL,
...
);
```
- **Dataset:** 9,022,122 rows, 1024-dim float32 vectors (DS1-fukuro, L2-normalized)
- **Cluster settings active before the build:**
```sql
SET CLUSTER SETTING bulkio.index_backfill.ingest_concurrency = 16; -- default 2
SET CLUSTER SETTING bulkio.index_backfill.vector_merge_batch_size = 1000; -- default 3
SET CLUSTER SETTING bulkio.index_backfill.distributed_merge.mode = 'declarative'; -- default 'disabled'
SET CLUSTER SETTING bulkio.index_backfill.batch_size = 100000; -- default 30000
SET CLUSTER SETTING bulkio.index_backfill.merge_num_workers = 8; -- default 4
SET CLUSTER SETTING bulkio.index_backfill.initial_splits_per_processor = 8; -- default 3
SET CLUSTER SETTING schemachanger.backfiller.buffer_size = '128 MiB'; -- default 32 MiB
SET CLUSTER SETTING schemachanger.backfiller.max_buffer_size = '2 GiB'; -- default 512 MiB
SET CLUSTER SETTING bulkio.index_backfill.checkpoint_interval = '10s'; -- default 30s
```

## Steps to reproduce

1. Load ~9M 1024-dim float32 vectors into a table with a `VECTOR(1024)` column.
2. Apply the nine cluster settings above.
3. Run:
```sql
CREATE VECTOR INDEX ON embeddings (vector vector_cosine_ops);
```
(no `WITH` clause, so partition sizes default to 16/128)
4. Watch `[SHOW JOBS]` and per-node CPU.

## Expected behavior

The build completes in roughly the same wall-clock as the `WITH (64, 1024)` variant (~4h 12m), or, if defaults are genuinely slower, at least continues making forward progress with merge work distributed across all nodes.

## Actual behavior

### Phase 1 (ingest, ~first 6h)
Three nodes share work near-equally, writes ~1,300/s per node, `fraction_completed` advances roughly linearly.

### Phase 2 (merge, last ~50m before abort)
- **Node n2** (coordinator): `sys.cpu.combined.percent-normalized` ≈ 0.96 sustained, almost entirely on partition-merge goroutines.
- **Nodes n1, n3:** ≈ 0.32 each, idle on the merge work.
- **`admission.elastic_cpu.utilization` = 0** on all nodes (no admission throttling).
- L0 files = 0, L0 sublevels = 0 (no compaction backlog; storage healthy).
- `bulkio.index_backfill.elastic_control.enabled = false` (so backfill isn't subject to elastic CPU throttling either).

### Progress deceleration
| T+ (h) | fraction_completed | Δ%/hr |
|---|---|---|
| 0 | 0.00 | n/a |
| 4 | 19.2% | 4.8 |
| 5 | 24.0% | 4.8 |
| 6 | 25.0% | 1.0 |
| 6.5 | 25.0% | 0.0 |
| 6.87 | 31.6% (frozen for last 18.7 min) | 0.0 |

Job cancelled at T+6h 52m.

## Comparison run (same cluster, same data, same nine tunings)

```sql
CREATE VECTOR INDEX ON embeddings (vector vector_cosine_ops)
WITH (min_partition_size = 64, max_partition_size = 1024);
```

Completed in **252.6 min (4h 12m)**, all three nodes balanced (writes within 4% per node), 595 v/s per-vector rate. No coordinator pinning observed.

## Hypothesis

Default `max_partition_size = 128` produces roughly 8x more leaf partitions than `WITH (64, 1024)`. The declarative schema changer parallelizes the ingest phase across all nodes, but the merge phase appears to concentrate per-partition merge work on a single coordinator node regardless of `bulkio.index_backfill.distributed_merge.mode = 'declarative'`. At 9M vectors with the default-sized partition count, merge work on one node exceeds practical wall-clock budget.

Possibly related:
- `bulkio.index_backfill.distributed_merge.mode` is documented as flipping merge to distributed under `declarative`, but at this scale with these partition sizes the distribution does not appear to happen.
- `bulkio.index_backfill.merge_num_workers = 8` should provide eight merge workers but the observed work stays on one node.

## Workaround

Use `WITH (min_partition_size = 64, max_partition_size = 1024)` for any `CREATE VECTOR INDEX` on tables >= 1M rows. This converges with balanced load distribution at 9M vectors in 4h 12m on the same hardware.

## Additional context

- Same scaling matrix was verified at 100K (~510K vectors, defaults, 8 min) and 1M (~2M vectors, defaults, 5h 48m with intermittent multi-minute single-coordinator freeze cycles that eventually resolved via handoff). The 1M-defaults build did complete, suggesting the freeze behaviour begins emerging between ~2M and ~9M vectors under defaults.
- The build was launched via the standard `cockroach sql` client. Job state was inspected via `SHOW JOBS` and `crdb_internal.jobs`. `fraction_completed` reporting was inconsistent during the build but its movement (and lack thereof) matched what `crdb_internal.distsql_flows` and per-node CPU showed.
- I'm happy to share full per-node CPU traces, `SHOW JOBS` snapshots, and the job's `last_modified` timestamps if useful.

Jira issue: CRDB-64987

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the CREATE VECTOR INDEX workload with default partition sizing and with min_partition_size=64, max_partition_size=1024, then inspect SHOW JOBS, crdb_internal.jobs, crdb_internal.distsql_flows, and per-node CPU. Read the schemachanger backfiller and bulkio index-backfill merge paths, especially distributed_merge.mode and merge_num_workers. Done means large default-sized builds continue progressing with merge work balanced across nodes.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
databases, distributed-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.