Skip to content

REP: Ray Active-Passive Head Architecture - #65

Merged
edoakes merged 14 commits into
ray-project:mainfrom
YoyinZyc:active-passive
May 19, 2026
Merged

edoakes merged 14 commits into
ray-project:mainfrom
YoyinZyc:active-passive

Conversation

@YoyinZyc

Copy link
Copy Markdown
Contributor

This REP proposes the transition from a single-point-of-failure GCS model to a highly available Active-Passive setup. The design aims to reduce recovery latency from minutes to seconds.

Scope:

  • Ray core: GCS native lease-based leader election client
  • KubeRay: Ability to provision multiple head nodes
  • Fault-tolerance: Align with existing GCS FT scope, targeting KubeRay based cluster.

Requested reviewers: @andrewsykim @edoakes @MengjinYan

Signed-off-by: Yuchen Zhou <yczhou@google.com>
Signed-off-by: Yuchen Zhou <yczhou@google.com>
Comment thread reps/2026-04-23-active-passive-head.md Outdated
#### 5. KubeRay Changes
1. **API change**: specifying the number of desired head replicas.
2. **Configuration changes**:
- Readiness Probe for passive heads to fail.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, probes are immutable. So, probably the change should be in GCS?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was originally reaching to k8s APIServer directly to fetch the lease status for readiness check but it could be part of the gcs_health endpoint. In this way, no need to update the readiness probe. Updated.

Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Outdated
3. **Fencing Token**: The last barrier to prevent data inconsistency. It is also one of the best practices for implementing lease-based leader election in production. At the end of the day, we must make sure the data stored in redis is pure.
- **The Token**: Lease transition, it is updated when a new leader takes over the lease
- **Write to Redis**: the leader GCS attaches the token with the query
- **Fencing**: The Redis rejects the request if the request_epoch is smaller than its current_epoch

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YoyinZyc how exactly does redis reject requests based on the token? Is it a feature in Redis or something else?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be implemented with redis lua script, but I am wondering if doing fencing in redis is enough. Shouldn't we also have this fencing in all GCS -> raylet communication?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is with lua script, added more details and the example lua script. GCS -> raylet communication involves more design and consideration, I added it as the a phase 2 item which should fall into later iteration.

Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Outdated
The latter two are Ray-specific optimizations for the GCS HA scenario. Techenically, they could provide additional data consistency guarantees. However, it may involve other potential risk and additional implementation work.(e.g. communication between heads/workers could be unreliable) We can leave it to the future iterations.
##### Metrics and Monitoring
- `gcs_is_leader`: Emits a 1 if the current instance is the leader, and a 0 if it is a passive head.
- `gcs_leader_transitions_total`: Incremented every time a leadership change is detected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where would this be exported?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the gcs process.

Comment thread reps/2026-04-23-active-passive-head.md Outdated
##### Metrics and Monitoring
- `gcs_is_leader`: Emits a 1 if the current instance is the leader, and a 0 if it is a passive head.
- `gcs_leader_transitions_total`: Incremented every time a leadership change is detected
- `gcs_recovery_latency_ms`: The time it takes for a passive head to recover the state and become the leader.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for this, where/how is it recorded?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is recorded from the gcs server. gcs_is_leader is a gauge metrics tracking local leadership status; gcs_leader_transitions_total increaments when the passive becomes the leader;
gcs_recovery_latency_ms tracks the passive head recovery latency from the moment it acquires the leadership to the moment the head is functional.

The first three mechanisms are production golden standards for leader election implementations. They are generic and must be included in the first phase of this REP.

The latter two are Ray-specific optimizations for the GCS HA scenario. Techenically, they could provide additional data consistency guarantees. However, it may involve other potential risk and additional implementation work.(e.g. communication between heads/workers could be unreliable) We can leave it to the future iterations.
##### Metrics and Monitoring

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any other metrics around API server latency / health that we should observe?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIServer has a bunch of metrics but those are pretty generic like apiserver_request_total, apiserver_request_duration_seconds and apiserver_current_inflight_requests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably worth also exporting similar metrics from our lease client

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What similar metrics are you referring to? These are k8s APIServer metrics, how could the lease client export them?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean our lease client should also export metrics like the latency for its requests to the API server and a failed request count

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, added two more metrics for lease renew latency and failure count.

Comment thread reps/2026-04-23-active-passive-head.md Outdated
Comment on lines +181 to +182
- **Raylet**
Safe to run only if launched with `--num-cpus=0` and `--num-gpus=0`, ensuring the active primary never schedules user tasks onto the standby head node.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting these is not strong enough -- actors/tasks with no resource requests can still be scheduled there.

Can we gate the Raylet from joining the cluster until after the GCS is elected the leader? This is similar to the problem w/ the autoscaler that I pointed out above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, autoscaler must be suppressed. Raylet may be fine since it anyway only talks to the local gcs but we should restrict its registration which incurs the redis write. Added more detail about components behavior.

Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Resolved
Signed-off-by: Yuchen Zhou <yczhou@google.com>
YoyinZyc and others added 5 commits May 4, 2026 21:44
Signed-off-by: Yuchen Zhou <yczhou@google.com>
Signed-off-by: Yuchen Zhou <yczhou@google.com>
Signed-off-by: Yuchen Zhou <yczhou@google.com>
Signed-off-by: Gang Li <ganglica@google.com>
Update autoscaler section in Active-Passive REP
Comment thread reps/2026-04-23-active-passive-head.md Outdated
Comment on lines +190 to +197
- **Raylet**: it is required to be alive to answer the readiness and liveness probes.
- **Startup**: Upon boot, the local standby Raylet targets the local GCS and sends a RegisterNode gRPC request. On passive head, the PUT request should not reach to Redis.
- **Passive State**: The Raylet continuously sends heartbeats to the local GCS to keep its node liveness.
- **Promotion**: During promotion, the Raylet should register itself and physically writes it to Redis.
- **Autoscaler**: it is a critical, high-risk background component that must be suppressed on passive head.
- **Startup**: Upon boot, the local autoscaler targets the local GCS and periodically sends a GetClusterResourceState gRPC request to check if the cluster resource state in GCS is ready. The cluster resource state should only be ready when the GCS is in active mode.
- **Active State**: Autoscaler gets the cluster resource state from GCS and continues with the normal autoscaling logic including updating KubeRay CRDs, reporting the autoscaling state back to GCS.
- **Passive State**: Autoscaler gets the cluster resource state from GCS, which indicates the cluster resource state is not ready. The autoscaler skips the rest of the logic.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will each of these also be monitoring that its GCS is the leader and fast-failing when it loses the lease?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gangli113 helped adding more details about autoscaler, let us know if there is any other concerns. Thanks!

@edoakes edoakes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Largely looks good. Primary open feedback:

  • Please document the full scope of KubeRay changes (especially any user-facing changes).
  • We will need the same safety guarantees for the autoscaler process. It will either need to fast fail when the GCS fails or also directly check w/ a lease client.

Comment thread reps/2026-04-23-active-passive-head.md Outdated
// +kubebuilder:default:=1
// +kubebuilder:validation:Minimum:=1
// +kubebuilder:validation:Maximum:=2
Replicas *int32 `json:"replicas,omitempty"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we introducing replicas now because we anticipate it can be more than 2 head nodes in the future?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(As opposed to something like mode: SingleReplica|ActivePassive)

@andrewsykim andrewsykim May 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think replicas is confusing because it implies a user can set it to any arbitrary value.

A field in gcsFaultToleranceOption is probably better?

spec:
  gcsFaultToleranceOptions:
    mode: ActivePassive
    ..
    ...

This is slightly more intuitive because active-passive mode also requires enabling Redis persistence anyways right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT @rueian ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was also thinking about just using a boolean or mode to control the enablement. But using replicas is more kubernetes native for controller to reconcile and can be extended if ray eventually wants to support more replicas?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But using replicas is more kubernetes native for controller to reconcile and can be extended if ray eventually wants to support more replicas?

I think we can just introduce replicas field in the future when we can support > 2 replicas. Putting replicas outside of gcsFaultToleranceOptions implies you can scale to N replicas without enabling GCS persistence, which is not the case. Putting the mode in gcsFaultToleranceOptions makes it clearer that you can only enable active-passive replica mode when GCS persistence is also enabled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok changed to using EnableActivePassiveHead under gcsFaultToleranceOptions to gate the enablement of the feature.

Comment thread reps/2026-04-23-active-passive-head.md Resolved
Signed-off-by: Yuchen Zhou <yczhou@google.com>
YoyinZyc and others added 3 commits May 12, 2026 22:36
Signed-off-by: Yuchen Zhou <yczhou@google.com>
…sive REP

Signed-off-by: Gang Li <ganglica@google.com>
Update Autoscaler section for GCS failure and promotion in Active-Passive REP
Comment thread reps/2026-04-23-active-passive-head.md Outdated
- **Active State**: Autoscaler gets the cluster resource state from GCS and continues with the normal autoscaling logic including updating KubeRay CRDs, reporting the autoscaling state back to GCS.
- **Passive State**: Autoscaler gets the cluster resource state from GCS, which indicates the cluster resource state is not ready. The autoscaler skips the rest of the logic.
- **Dashboard**: the local Dashboard process is started by KubeRay pointing to the local GCS. Because it queries the passive local GCS, the dashboard UI initially displays a single-node cluster (only itself) with no active jobs or actors. Since the standby pod is Not Ready, its dashboard API is completely unreachable by external clients with the current k8s service setup.
- **Job API**: with right k8s service configuration, the `ray job submit` request should not be able to reach the passive head.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not rely on this as the only mechanism. Users may submit job requests in a different way (e.g., exec'ing into the pod and running the job submit command there)

- **Passive State**: Autoscaler gets the cluster resource state from GCS, which indicates the cluster resource state is not ready. The autoscaler skips the rest of the logic.
- **Dashboard**: the local Dashboard process is started by KubeRay pointing to the local GCS. Because it queries the passive local GCS, the dashboard UI initially displays a single-node cluster (only itself) with no active jobs or actors. Since the standby pod is Not Ready, its dashboard API is completely unreachable by external clients with the current k8s service setup.
- **Job API**: with right k8s service configuration, the `ray job submit` request should not be able to reach the passive head.
- **Serve Controller**: The Ray Serve Controller is a standard Ray actor. Since GCS in standby mode has no active workers and no running jobs, no Serve Controller is ever spawned on the standby head.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't matter if the serve controller isn't run on the standby head, the serve API endpoints in the dashboard on a passive head could still make requests to the active head

Most likely I think we need a shared bit for API server components to check if they are the active or passive and fail requests when passive. This can be used for serve API & jobs API

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could even abstract it into a utility/decorator that performs the check & rejects requests with an appropriate status code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may need more clarification on this. what do you mean about "serve API endpoints in the dashboard on a passive head could still make requests to the active head"?

And you are suggesting that whenever there is an unexpected request hit Job API or Serve API, it should ask its local gcs to check if it is leader or passive. If it is passive, reject the request. Am I understanding correctly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also in terms of the decorator, are you saying something on gcs side like a rpc or API for leadership status checking?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are suggesting that whenever there is an unexpected request hit Job API or Serve API, it should ask its local gcs to check if it is leader or passive. If it is passive, reject the request. Am I understanding correctly?

Yes that's right.

also in terms of the decorator, are you saying something on gcs side like a rpc or API for leadership status checking?

I am saying we could have a shared utility or python decorator in the dashboard logic that automatically performs this check before accepting any requests

Comment thread reps/2026-04-23-active-passive-head.md Resolved
Comment thread reps/2026-04-23-active-passive-head.md Outdated
**1. Single-container co-located components**:
These components are running in the same container as different subprocesses. The gcs termination caused by lease lost will terminate other processes.
- **GCS Server**
Safe on Standby. Global tracking managers are instantiated empty; state hydration is deferred until promotion. The rpc server can only answer read requests in passive mode. All mutation requests should not be accepted and the rejection should be handled gracefully on the client side. (With the proper traffic routing, the requests will be sent to the active head only. But it is another layer of protection to avoid any potential split brain issue.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rpc server can only answer read requests in passive mode

Nit: It could be implementation detail, but I think it might be better just to identity the endpoints that needs to be called and allow them instead of open all the read endpoints.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least GetClusterID and healthcheck rpc have to be enabled on passive. I may uncover more during the implementation.

Comment thread reps/2026-04-23-active-passive-head.md Outdated
- **Dashboard**: the local Dashboard process is started by KubeRay pointing to the local GCS. Because it queries the passive local GCS, the dashboard UI initially displays a single-node cluster (only itself) with no active jobs or actors. Since the standby pod is Not Ready, its dashboard API is completely unreachable by external clients with the current k8s service setup.
- **Job API**: with right k8s service configuration, the `ray job submit` request should not be able to reach the passive head.
- **Serve Controller**: The Ray Serve Controller is a standard Ray actor. Since GCS in standby mode has no active workers and no running jobs, no Serve Controller is ever spawned on the standby head.
- **Dashboard Agent**: Runs locally and reports metrics to the co-located passive GCS. Upon promotion, it continues reporting to the now-promoted local GCS. Ready to initialize runtime environments if new jobs are submitted post-failover.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We might also want to disable event generation in the passive mode. There might not be many events generated but disabling it as a safe guard

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the event generation is controlled by some envs(like RAY_enable_ray_event or RAY_DASHBOARD_AGGREGATOR_AGENT_PUBLISH_EVENTS_TO_GCS ). Is it possible the re-enable the event generation at run time after promotion? Or are you referring to other approach to disable the event generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't referring to any specific mechanism but more of a high level safe guard.

You are right. The configurations are all specified at the start time. We probably need to add an additional config in the aggregator agent to control whether to emit events at the runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the most intuitive approach is to ask the dashboard agent to watch and poll the leadership status periodically (in low frequency, 5s) and emit event post-promotion only.

…assive enablement

Signed-off-by: Yuchen Zhou <yczhou@google.com>
Signed-off-by: Yuchen Zhou <yczhou@google.com>

@edoakes edoakes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me as a starting point for this feature; I'm sure there will be some more low level details to iron out as we go.

@edoakes
edoakes merged commit 564cd49 into ray-project:main May 19, 2026
1 check passed
jhasm added a commit to jhasm/ray-enhancements that referenced this pull request May 22, 2026
…ion, observability example

- Expand the Acceptance Criteria subsection to include the four criteria from @andrewsykim
  (Ray release tests, Ray Core GCS FT test equivalent, Ray scale tests, Ray Serve chaos testing)
  and incorporate @edoakes's clarification that the Redis-equivalent test should be a postmerge
  CI job using a local-file RocksDB, labeled skip-premerge with path-based triggers on
  src/ray/gcs/**.
- Add an "Interaction with Active-Passive Head (REP ray-project#65)" subsection covering the
  ReadWriteMany topology, two-layer split-brain prevention (lease + RocksDB LOCK file),
  failover sequence, and RWX latency trade-off. Update Follow-on Work item 2 to mark
  Active/Passive GCS as in flight and link to REP ray-project#65.
- Add observability data (event logs, task/actor history, log indexes) as an explicit
  example of non-critical, high-volume state that the future cold-paging optimization
  could keep primarily on disk.

Signed-off-by: Santosh Jha <santosh.m.jha@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants