Skip to content

Commit 002e3d8

Browse files
committed
📖 Add a design for supporting warm replicas.
1 parent c4c31bb commit 002e3d8

File tree

1 file changed

+151
-0
lines changed

1 file changed

+151
-0
lines changed

designs/warmreplicas.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
Add support for warm replicas
2+
===================
3+
4+
## Motivation
5+
Controllers reconcile all objects during startup / leader election failover to account for changes
6+
in the reconciliation logic. For certain sources, the time to serve the initial list can be
7+
significant in the order of minutes. This is problematic because by default controllers (and by
8+
extension watches) do not start until they have won leader election. This implies guaranteed
9+
downtime as even after leader election, the controller has to wait for the initial list to be served
10+
before it can start reconciling.
11+
12+
## Proposal
13+
A warm replica is a replica with a queue pre-filled by sources started even when leader election is
14+
not won so that it is ready to start processing items as soon as the leader election is won. This
15+
proposal aims to add support for warm replicas in controller-runtime.
16+
17+
### Context
18+
Mostly written to confirm my understanding, but also to provide context for the proposal.
19+
20+
Controllers are a monolithic runnable with a `Start(ctx)` that
21+
1. Starts the watches [here](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.20.2/pkg/internal/controller/controller.go#L196-L213)
22+
2. Starts the workers [here](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.20.2/pkg/internal/controller/controller.go#L244-L252)
23+
There needs to be a way to decouple the two so that the watches can be started before the workers
24+
even as part of the same Runnable.
25+
26+
If a runnable implements the `LeaderElectionRunnable` interface, the return value of the
27+
`NeedLeaderElection` function dictates whether or not it gets binned into the leader election
28+
runnables group [code](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.20.2/pkg/manager/runnable_group.go).
29+
30+
Runnables in the leader election group are started only after the manager has won leader election,
31+
and all controllers are leader election runnables by default.
32+
33+
### Design
34+
1. Add a new interface `WarmupRunnable` that allows for leader election runnables to specify
35+
behavior when manager is not in leader mode. This interface should be as follows:
36+
```go
37+
type WarmupRunnable interface {
38+
NeedWarmup() bool
39+
GetWarmupRunnable() Runnable
40+
}
41+
```
42+
43+
2. Controllers will implement this interface to specify behavior when the manager is not the leader.
44+
Add a new controller option `ShouldWarmupWithoutLeadership`. If set to true, then the main
45+
controller runnable will not start sources, and instead rely on the warmup runnable to start sources
46+
The option will be used as follows:
47+
```go
48+
type Controller struct {
49+
// ...
50+
51+
// ShouldWarmupWithoutLeadership specifies whether the controller should start its sources
52+
// when the manager is not the leader.
53+
// Defaults to false, which means that the controller will wait for leader election to start
54+
// before starting sources.
55+
ShouldWarmupWithoutLeadership *bool
56+
57+
// ...
58+
}
59+
60+
type runnableWrapper struct {
61+
startFunc func (ctx context.Context) error
62+
}
63+
64+
func(rw runnableWrapper) Start(ctx context.Context) error {
65+
return rw.startFunc(ctx)
66+
}
67+
68+
// NeedWarmup implements WarmupRunnable
69+
func (c *Controller[request]) NeedWarmup() bool {
70+
if c.ShouldWarmupWithoutLeadership == nil {
71+
return false
72+
}
73+
return c.ShouldWarmupWithoutLeadership
74+
}
75+
76+
// GetWarmupRunnable implements WarmupRunnable
77+
func (c *Controller[request]) GetWarmupRunnable() Runnable {
78+
return runnableWrapper{
79+
startFunc: func (ctx context.Context) error {
80+
if !c.ShouldWarmupWithoutLeadership {
81+
return nil
82+
}
83+
84+
// pseudocode
85+
for _, watch := c.startWatches {
86+
watch.Start()
87+
// handle syncing sources
88+
}
89+
}
90+
}
91+
}
92+
```
93+
94+
3. Add a separate runnable category for warmup runnables to specify behavior when the
95+
manager is not the leader. [ref](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.20.2/pkg/manager/runnable_group.go#L55-L76)
96+
```go
97+
type runnables struct {
98+
// ...
99+
100+
LeaderElection *runnableGroup
101+
Warmup *runnableGroup
102+
103+
// ...
104+
}
105+
106+
func (r *runnables) Add(fn Runnable) error {
107+
switch runnable := fn.(type) {
108+
// ...
109+
case WarmupRunnable:
110+
if runnable.NeedWarmup() {
111+
r.Warmup.Add(runnable.GetWarmupRunnable(), nil)
112+
}
113+
114+
// fallthrough to ensure that a runnable that implements both LeaderElection and
115+
// NonLeaderElection interfaces is added to both groups
116+
fallthrough
117+
case LeaderElectionRunnable:
118+
if !runnable.NeedLeaderElection() {
119+
return r.Others.Add(fn, nil)
120+
}
121+
return r.LeaderElection.Add(fn, nil)
122+
// ...
123+
}
124+
}
125+
```
126+
127+
4. Start the non-leader runnables during manager startup.
128+
```go
129+
func (cm *controllerManager) Start(ctx context.Context) (err error) {
130+
// ...
131+
132+
// Start the warmup runnables
133+
if err := cm.runnables.Warmup.Start(cm.internalCtx); err != nil {
134+
return fmt.Errorf("failed to start other runnables: %w", err)
135+
}
136+
137+
// ...
138+
}
139+
```
140+
141+
## Concerns/Questions
142+
1. Controllers opted into this feature will break the workqueue.depth metric as the controller will
143+
have a pre filled queue before it starts processing items.
144+
2. Ideally, non-leader runnables should block readyz and healthz checks until they are in sync. I am
145+
not sure what the best way of implementing this is, because we would have to add a healthz check
146+
that blocks on WaitForSync for all the sources started as part of the non-leader runnables.
147+
3. An alternative way of implementing the above is to moving the source starting / management code
148+
out into their own runnables instead of having them as part of the controller runnable and
149+
exposing a method to fetch the sources. I am not convinced that that is the right change as it
150+
would introduce the problem of leader election runnables potentially blocking each other as they
151+
wait for the sources to be in sync.

0 commit comments

Comments
 (0)