|
| 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