Addressing GCP stockouts
Summary
- Architecture: gitlab.com's Linux CI fleet runs on
docker+machine. Each runner manager has one worker pinned to a single GCP project and zone viaMachineOptions, and provisions job VMs through the GCE driver (compute.instances.insert). - Shard layout: A shard spans multiple projects for blast-radius and quota isolation; each project pinned to one zone. The small-amd64 shard (our largest) has 9 projects evenly split across us-east1-{b,c,d} at 3-3-3, so a zonal stockout removes a third of the shard's capacity and the remaining zones don't absorb the load.
- Symptom:
ZONE_RESOURCE_POOL_EXHAUSTEDblocks job VM creation. Affected manager can't grow its pool, jobs queue at GitLab, no automated mitigation. - Deprecation context:
docker+machineis deprecated upstream. Long-term direction: Kubernetes with gvisor/firecracker (Hosted Runners Platform v5).docker-autoscaler+ fleeting is a backup, not the destination.
Background
Recent stockout incidents on the small-amd64 shard (production#21936 (closed), production#21939, production#21979) impacted gitlab.com CI job pickup. GCP's recommendation is Flex MIG (or instances.bulkInsert with instanceFlexibilityPolicy) — a precedence list of machine families that taps an internal obtainability mechanism unavailable to direct Instances.Insert retries.
InstanceFlexibilityPolicy.InstanceSelections[] also supports per-selection disks overrides (documented here), so a single bulkInsert can span machine families with different disk-type requirements (e.g. N2D on pd-balanced + N4 on hyperdisk-balanced). This invalidates the earlier assumption that cross-generation capacity rescue required a separate runtime fallback layer.
Options
Option 1: self-implemented spillover between workers — rejected
GCP confirmed that self-implemented retries don't tap the internal obtainability mechanism. Closed at gitlab-org/gitlab-runner!6726 (closed).
Option 2: docker-machine + RegionInstances.BulkInsert — landing
Modified the GCE docker-machine driver to provision via RegionInstances.BulkInsert with LocationPolicy + InstanceFlexibilityPolicy in the request body. Same internal-fulfillment path as Flex MIG without the external resource. Docker-machine stays the autoscaler; existing metrics and SLI (gitlab-org/gitlab-runner!6740 (merged)) keep working.
- gitlab-org/ci-cd/docker-machine!169 (merged): bulkInsert + flex policy.
- gitlab-org/ci-cd/docker-machine!173 (merged): per-selection disk override on flex policy, lets one request span pd-balanced families and hyperdisk-balanced families.
- gitlab-org/ci-cd/docker-machine!175 (merged): orphan-VM cleanup after failed bulkInsert (INC-10515).
- gitlab-org/gitlab-runner!6740 (merged): per-VM
target_*metric labels sourced from docker-machine's resolved state. - gitlab-org/gitlab-runner!6791 (merged): cap
finalizeRemovalretries.
Option 3: migrate to docker-autoscaler + MIG (contingency)
Move executor + autoscaler + plugin + IAM + helper-image distribution + chef-repo at the same time as picking up Flex MIG. Loses target-label attribution (no fleeting metrics channel), and we've never run fleeting at gitlab.com scale. Contingency if we need to move off docker+machine before K8s is ready.
Risks
The latent bug we documented during the private-1 canary materialised at fleet-wide rollout as INC-10515 (see gitlab-org/ci-cd/docker-machine#148 (closed)). When the bulkInsert path fails after GCP has already placed the VM, the driver records no zone, and the runner-manager retries cleanup forever. The incident left 10,200 stuck machines and 257 orphan VMs. Driver-side self-heal lands in gitlab-org/ci-cd/docker-machine!175 (merged). The runner-side cap on the unbounded loop lands in gitlab-org/gitlab-runner!6791 (merged) as defence in depth.
Follow-ups
- GPU shards on bulkInsert. Needs a per-selection accelerator override on
InstanceFlexibilityPolicy.InstanceSelections[], shaped like docker-machine!173 but forAcceleratorsinstead ofDisks. Until then GPU jobs can't fall back across accelerator types and stay exposed to single-SKU stockouts. - ARM shards (
saas-linux-*-arm64) on bulkInsert. The path is identical to amd64 once a flex selection family is settled (T2A / C4A). No driver changes needed, just chef-repo config in the corresponding parent roles.
Decisions
- Run a controlled stockout comparison? Stockout impact is unmeasured.
- Trigger conditions for 3 (currently contingency-only).
- Owners per track: Runner Core (driver + runner-side MRs) vs Runners Platform (chef-repo, config-mgmt).
- GCP quotas for bulkInsert: 24h Cloud Monitoring data shows current per-project peak of ~500-600/min on
heavy_weight_read_requests_per_region(us-east1), which is the closest available proxy for what'll shift toOperationReadRequestsPerMinutePerProjectPerRegionafter the switch (limit 4500/min, ~8x headroom). Inferred concurrent ops well under the 500 default bucket limit (~10x headroom). No preemptive raise; monitor on rollout and request ifoperation_read_requests_per_regionclimbs past ~50% of limit.