feat(scheduler): 1000+ node scaling with HA, sharding, and O(nodes) reconciliation - #40
Merged
Merged
Conversation
… handling improvements
… snapshot prefetching
…proving concurrency
…and add deregistration on shutdown
…sk, and DeleteDisk functions
… GetBucket, DeleteBucket, and GetBucketAccess functions
…ith certmanager integration
…ields for improved configuration
…er event management
…uthentication process
…ilability and Redis event streaming
…und loop handling
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.
Scheduler Optimization for 1000+ Node Scaling with High Availability
This release is a comprehensive architectural overhaul enabling persys-scheduler to run at 1,000-5,000+ node fleet scales with high availability, sharding, and significantly reduced agent communication overhead. It implements leader election, active-active failover modes, a live node cache for placement, weighted placement scoring with in-flight reservations, etcd compare-and-swap on all concurrent writes, cluster-wide event streaming to Redis, connection pooling to agents, and bounded-concurrency reconciliation.
TL;DR:
Major Features
1. High Availability & Leader Election
Multiple scheduler replicas can run against the same etcd cluster with automatic failover:
Failover mode (default,
SCHEDULER_HA_MODE=failover): Exactly one active replica cluster-wide drives all singleton background loops (reconciliation, drift detection, monitoring, node watch). Others are hot standbys that take over automatically if the leader dies or its etcd lease expires (typically within 15s TTL). Preserves existing single-active-instance behavior exactly.Active-Active mode (
SCHEDULER_HA_MODE=active-active): Nodes are partitioned acrossSCHEDULER_SHARD_COUNTshards by stable hash of node ID. Each replica is assigned aSCHEDULER_SHARD_INDEXand only drives reconciliation/monitoring/drift-detection for nodes in its shard. Multiple shards process workloads concurrently instead of one replica doing all the work. Run more than one replica per shard for HA within a shard.In both modes, the gRPC API runs unconditionally on every replica (stateless, protected by etcd CAS); only the background singleton loops are gated by leadership. Safe to route agent traffic to any replica via load balancer.
Implementation:
leader.go(etcd-lease-based election),sharding.go(node partitioning)SCHEDULER_HA_MODE,SCHEDULER_SHARD_COUNT,SCHEDULER_SHARD_INDEXLeaderElected,LeaderLostStartReconciliation(ctx)withStartLeaderElectedBackgroundLoops(ctx)in main.go2. Live Node Watch Cache for Placement
A leader-elected singleton background loop maintains a continuously updated in-memory node cache via etcd watch:
Why: Without this optimization, every placement decision would require a full etcd scan. At 1000+ nodes, that's prohibitively expensive for a high-frequency placement path.
Implementation:
node_watch.go(watch loop + cache management)scheduler.go:StartNodeWatch(ctx), in-memory node cache,candidateNodeSnapshot()with fallback3. Reconciliation Fan-Out: O(workloads) → O(nodes)
Reconciliation now batches workload queries per node instead of querying each workload individually:
GetWorkloadsonce per node (batch RPC) at start of cycle = O(nodes) fan-outgetActualWorkloadStateconsults snapshot before falling back to live per-workload RPCBounded Concurrency:
SCHEDULER_RECONCILE_CONCURRENCYworkloads concurrently (default 64)Applied same bounded-concurrency optimization to
MonitorNodes,MonitorWorkloads, anddetectDriftOnce.Implementation:
nodeSnapshotEntryinreconciler.goprefetchNodeSnapshots(ctx)batches GetWorkloads callsgetActualWorkloadState()consults snapshot firstSCHEDULER_RECONCILE_CONCURRENCY(default 64)4. etcd Compare-and-Swap on All Concurrent Write Paths
All etcd mutations now use atomic compare-and-swap instead of unconditional put, eliminating silent overwrites:
RetryableEtcdCASPut(key, value)(with conflict retry)UpdateWorkloadStatus,UpdateWorkloadLogs,UpdateWorkloadMetadata,UpdateWorkloadRuntimeDetailsFixes:
Usagefield) was silently dropped from status projection on every read → now preserved via CASImplementation:
RetryableEtcdCASPut(key, value)inetcd.go5. Weighted Placement Algorithm with In-Flight Reservations
Placement scoring replaced single-factor "lowest utilization" with multi-factor weighted score:
Weighted Score:
In-Flight Reservations:
Node
AvailableCPUandAvailableMemoryonly update via heartbeat, which lags placement decisions. When multiple placement decisions happen concurrently (reconciliation with bounded concurrency, monitoring, multiple replicas in active-active mode), they all read stale "available" numbers and pick the same node, causing oversubscription before heartbeats catch up.In-flight reservations track resources committed to just-assigned workloads and subtract them from effective capacity during scoring. They self-expire after 90s (multiple heartbeat intervals), trading small scoring precision loss for not needing to hook every status-confirmation path.
Implementation:
placement.go(scoring functions, in-flight reservation tracking)reservePlacement(nodeID, workloadID, cpu, memory),pendingReservationFor(nodeID)pendingReservationwith TTL-based expiryselectNodeForWorkload()to use weighted scoring6. Agent Connection Pooling
gRPC connections to agents are now pooled and reused:
map[nodeID]*grpc.ClientConn(keyed by node ID)Impact: Eliminates TLS handshake from every apply/delete/status/list call to every node every cycle. At 1000 nodes with 5s reconciliation interval, that's 200 TLS handshakes/second removed.
Implementation:
agentConnEntry(wraps *grpc.ClientConn)agentConns map[string]*agentConnEntryin SchedulergetAgentConn(nodeID),closeAgentConns()sched.SetCertManager(certMgr)for cert-aware retry on TLS errors7. Cluster-Wide Event Streaming (Redis-Backed)
Events are now stored in a Redis Stream, not etcd, and streamed via gRPC:
Why Redis instead of etcd:
Event Types:
NodeJoined,NodeLost,NodeLeftWorkloadScheduled,WorkloadFailed,DriftDetected,RetryTriggered,Rescheduled,RelocatedNodeDraining,NodeReady,NodeTainted,NodeUntainted,NodeLabelSet,NodeLabelDeletedLeaderElected,LeaderLostSchedulerModeChangedgRPC API:
ListEvents(type, workload_id, node_id, limit)- Historical replay (oldest-first)WatchEvents(type, workload_id, node_id)- Server-streaming, replays recent history then follows live eventsImplementation:
events.go(event emitting, watching, filtering)redis_store.goextensions for event stream (XADD, XREAD, XLEN, XTRIM)SchedulerEventView,ListEventsRequest,ListEventsResponse,WatchEventsRequestemitEvent(),ListEvents(),WatchEvents(),readEventsFromStream(),watchEventStream()emitEvent()after successful writeREDIS_EVENT_TTL(default 24h),REDIS_EVENT_MAX_ENTRIES(default 1000)8. Standalone Disk/Volume Management
New gRPC API for managing block volumes and managed volumes:
CreateDisk(name, driver, size_gb, fs_type, retain_policy)- Provision standalone volumeListDisks()- List all volumesGetDisk(disk_id)- Get volume detailsDeleteDisk(disk_id, force)- Delete volume (respects retain policy)Drivers:
local,nfs,ceph-rbdState: Stored in etcd (
/volumes/*,/attachments/*)Implementation:
disk.go(disk CRUD operations)persistence.go(workload storage classification helpers)DiskView,CreateDiskRequest, etc.CreateDisk(),ListDisks(),GetDisk(),DeleteDisk()9. Ceph RGW S3-Compatible Object Storage Proxying
New gRPC API for managing Ceph RGW buckets (no etcd bucket inventory):
CreateBucket(name, region, versioning)- Create S3 bucket on RGWListBuckets()- List all bucketsGetBucket(name)- Get bucket details (size, object count, versioning, owner)DeleteBucket(name)- Delete bucket on RGWGetBucketAccess(name)- Generate S3 access key/secret (stored in Vault)ListBucketObjects(bucket, prefix)- List objects in bucketArchitecture:
Implementation:
bucket.go(RGW API proxying, S3 signing, Vault integration)BucketView,CreateBucketRequest,GetBucketAccessRequest, etc.CreateBucket(),ListBuckets(),GetBucket(),DeleteBucket(),GetBucketAccess(),ListBucketObjects()PERSYS_RGW_ENDPOINT,PERSYS_RGW_REGION,PERSYS_RGW_ACCESS_KEY,PERSYS_RGW_SECRET_KEY,PERSYS_RGW_ADMIN_PATH10. CoreDNS Multi-Replica Fix
Scheduler self-registration for gateway → scheduler discovery now works with multiple replicas:
Before: Single fixed etcd key; every replica overwrote others on startup. Gateway could only resolve one replica.
After: Per-instance etcd key (mirroring agent node registration pattern). CoreDNS returns one A/SRV record per running replica. Deregistration on clean shutdown prevents lingering dead records.
Note: This DNS mechanism is for gateway → scheduler discovery only. Agents do not use CoreDNS to reach the scheduler (they use direct IPs from registration).
Implementation:
RegisterSchedulerSelfInCoreDNS(),DeregisterSchedulerSelfFromCoreDNS()to use per-instance keyConfiguration Changes
New environment variables (all have safe defaults matching prior behavior):