Browse code

Support election of controllers

A master can be configured to use a leader lease to determine
whether controllers can be started up. An admin would set
controllerLeaseTTL in master-config, and then start multiple
masters (or multiple controller processes). Each process would
try to acquire a leader lease on the key
<storageprefix>/leases/controllers, setting a TTL equal to the
configured value in seconds. Whichever controller acquires the
lock will be able to begin running. When a controller cannot
renew a lease due to errors talking to etcd, the key expires,
the key is deleted, or the value of the key is changed, the controller
will exit and terminate the process (either a full master, or just the
controller process).

Because terminating a master results in API connections being terminated
(including log sessions or exec proxy sessions) it is better to run the
masters as distinct API and controller sub processes.

Clayton Coleman authored on 2015/08/14 12:12:13
Showing 7 changed files
... ...
@@ -8,11 +8,14 @@ when that change will happen.
8 8
 ## Origin 1.0.x / OSE 3.0.x
9 9
 
10 10
 1. Currently all build pods have a label named `build`. This label is being deprecated
11
-  in favor of `openshift.io/build.name` in Origin 1.0.x / OSE 3.1.x at which point both
12
-  labels will be supported. All the newly created builds will have just the new label.
13
-  In Origin 1.y / OSE 3.y the support for the old label (`build`) will be removed entirely.
11
+  in favor of `openshift.io/build.name` in Origin 1.0.x (OSE 3.0.x) - both are supported.
12
+  In Origin 1.1 we will only set the new label and remove support for the old label.
14 13
   See #3502.
15 14
 
16 15
 1. Currently `oc exec` will attempt to `POST` to `pods/podname/exec`, if that fails it will
17
-fallback to a `GET` to match older policy roles.  In Origin 1.y/ OSE 3.y the support for the
18
-old `oc exec` endpoint via `GET` will be removed.
19 16
\ No newline at end of file
17
+  fallback to a `GET` to match older policy roles.  In Origin 1.1 (OSE 3.1) the support for the
18
+  old `oc exec` endpoint via `GET` will be removed.
19
+
20
+1. The `pauseControllers` field in `master-config.yaml` is deprecated as of Origin 1.0.4 and will
21
+  no longer be supported in Origin 1.1. After that, a warning will be printed on startup if it
22
+  is set to true.
20 23
\ No newline at end of file
... ...
@@ -116,7 +116,14 @@ type MasterConfig struct {
116 116
 	Controllers string
117 117
 	// PauseControllers instructs the master to not automatically start controllers, but instead
118 118
 	// to wait until a notification to the server is received before launching them.
119
+	// TODO: will be disabled in function for 1.1.
119 120
 	PauseControllers bool
121
+	// ControllerLeaseTTL enables controller election, instructing the master to attempt to acquire
122
+	// a lease before controllers start and renewing it within a number of seconds defined by this value.
123
+	// Setting this value non-negative forces pauseControllers=true. This value defaults off (0, or
124
+	// omitted) and controller election can be disabled with -1.
125
+	ControllerLeaseTTL int
126
+	// TODO: the next field added to controllers must be added to a new controllers struct
120 127
 
121 128
 	// Allow to disable OpenShift components
122 129
 	DisabledFeatures FeatureList
... ...
@@ -99,6 +99,11 @@ type MasterConfig struct {
99 99
 	// PauseControllers instructs the master to not automatically start controllers, but instead
100 100
 	// to wait until a notification to the server is received before launching them.
101 101
 	PauseControllers bool `json:"pauseControllers,omitempty"`
102
+	// ControllerLeaseTTL enables controller election, instructing the master to attempt to acquire
103
+	// a lease before controllers start and renewing it within a number of seconds defined by this value.
104
+	// Setting this value non-negative forces pauseControllers=true. This value defaults off (0, or
105
+	// omitted) and controller election can be disabled with -1.
106
+	ControllerLeaseTTL int `json:"controllerLeaseTTL,omitempty"`
102 107
 
103 108
 	// DisabledFeatures is a list of features that should not be started.  We
104 109
 	// omitempty here because its very unlikely that anyone will want to
... ...
@@ -61,6 +61,13 @@ func ValidateMasterConfig(config *api.MasterConfig) ValidationResults {
61 61
 		validationResults.AddErrors(urlErrs...)
62 62
 	}
63 63
 
64
+	switch {
65
+	case config.ControllerLeaseTTL > 300,
66
+		config.ControllerLeaseTTL < -1,
67
+		config.ControllerLeaseTTL > 0 && config.ControllerLeaseTTL < 10:
68
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("controllerLeaseTTL", config.ControllerLeaseTTL, "TTL must be -1 (disabled), 0 (default), or between 10 and 300 seconds"))
69
+	}
70
+
64 71
 	validationResults.AddErrors(ValidateDisabledFeatures(config.DisabledFeatures, "disabledFeatures")...)
65 72
 
66 73
 	if config.AssetConfig != nil {
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"errors"
7 7
 	"fmt"
8 8
 	"net/http"
9
+	"path"
9 10
 
10 11
 	etcdclient "github.com/coreos/go-etcd/etcd"
11 12
 	"github.com/golang/glog"
... ...
@@ -21,6 +22,7 @@ import (
21 21
 	"k8s.io/kubernetes/pkg/storage"
22 22
 	etcdstorage "k8s.io/kubernetes/pkg/storage/etcd"
23 23
 	kutil "k8s.io/kubernetes/pkg/util"
24
+	kutilrand "k8s.io/kubernetes/pkg/util/rand"
24 25
 
25 26
 	"github.com/openshift/origin/pkg/api/latest"
26 27
 	"github.com/openshift/origin/pkg/auth/authenticator"
... ...
@@ -58,6 +60,7 @@ import (
58 58
 	groupstorage "github.com/openshift/origin/pkg/user/registry/group/etcd"
59 59
 	userregistry "github.com/openshift/origin/pkg/user/registry/user"
60 60
 	useretcd "github.com/openshift/origin/pkg/user/registry/user/etcd"
61
+	"github.com/openshift/origin/pkg/util/leaderlease"
61 62
 )
62 63
 
63 64
 const (
... ...
@@ -83,7 +86,8 @@ type MasterConfig struct {
83 83
 
84 84
 	TLS bool
85 85
 
86
-	ControllerPlug plug.Plug
86
+	ControllerPlug      plug.Plug
87
+	ControllerPlugStart func()
87 88
 
88 89
 	// a function that returns the appropriate image to use for a named component
89 90
 	ImageFor func(component string) string
... ...
@@ -174,6 +178,8 @@ func BuildMasterConfig(options configapi.MasterConfig) (*MasterConfig, error) {
174 174
 		return nil, err
175 175
 	}
176 176
 
177
+	plug, plugStart := newControllerPlug(options, client)
178
+
177 179
 	config := &MasterConfig{
178 180
 		Options: options,
179 181
 
... ...
@@ -191,7 +197,8 @@ func BuildMasterConfig(options configapi.MasterConfig) (*MasterConfig, error) {
191 191
 
192 192
 		TLS: configapi.UseTLS(options.ServingInfo.ServingInfo),
193 193
 
194
-		ControllerPlug: plug.NewPlug(!options.PauseControllers),
194
+		ControllerPlug:      plug,
195
+		ControllerPlugStart: plugStart,
195 196
 
196 197
 		ImageFor:            imageTemplate.ExpandOrDie,
197 198
 		EtcdHelper:          etcdHelper,
... ...
@@ -213,6 +220,27 @@ func BuildMasterConfig(options configapi.MasterConfig) (*MasterConfig, error) {
213 213
 	return config, nil
214 214
 }
215 215
 
216
+func newControllerPlug(options configapi.MasterConfig, client *etcdclient.Client) (plug.Plug, func()) {
217
+	switch {
218
+	case options.ControllerLeaseTTL > 0:
219
+		// TODO: replace with future API for leasing from Kube
220
+		id := fmt.Sprintf("master-%s", kutilrand.String(8))
221
+		leaser := leaderlease.NewEtcd(
222
+			client,
223
+			path.Join(options.EtcdStorageConfig.OpenShiftStoragePrefix, "leases/controllers"),
224
+			id,
225
+			uint64(options.ControllerLeaseTTL),
226
+		)
227
+		leased := plug.NewLeased(leaser)
228
+		return leased, func() {
229
+			glog.V(2).Infof("Attempting to acquire controller lease as %s, renewing every %d seconds", id, options.ControllerLeaseTTL)
230
+			go leased.Run()
231
+		}
232
+	default:
233
+		return plug.New(!options.PauseControllers), func() {}
234
+	}
235
+}
236
+
216 237
 func newServiceAccountTokenGetter(options configapi.MasterConfig, client *etcdclient.Client) (serviceaccount.ServiceAccountTokenGetter, error) {
217 238
 	var tokenGetter serviceaccount.ServiceAccountTokenGetter
218 239
 	if options.KubernetesMasterConfig == nil {
... ...
@@ -437,12 +437,16 @@ func StartControllers(openshiftConfig *origin.MasterConfig, kubeMasterConfig *ku
437 437
 	}
438 438
 
439 439
 	go func() {
440
+		openshiftConfig.ControllerPlugStart()
441
+		// when a manual shutdown (DELETE /controllers) or lease lost occurs, the process should exit
442
+		// this ensures no code is still running as a controller, and allows a process manager to reset
443
+		// the controller to come back into a candidate state and compete for the lease
440 444
 		openshiftConfig.ControllerPlug.WaitForStop()
441
-		glog.Fatalf("Master shutdown requested")
445
+		glog.Fatalf("Controller shutdown requested")
442 446
 	}()
443 447
 
444 448
 	openshiftConfig.ControllerPlug.WaitForStart()
445
-	glog.Infof("Master controllers starting (%s)", openshiftConfig.Options.Controllers)
449
+	glog.Infof("Controllers starting (%s)", openshiftConfig.Options.Controllers)
446 450
 
447 451
 	// Start these first, because they provide credentials for other controllers' clients
448 452
 	openshiftConfig.RunServiceAccountsController()
... ...
@@ -4,14 +4,26 @@ import (
4 4
 	"sync"
5 5
 )
6 6
 
7
+// Plug represents a synchronization primitive that holds and releases
8
+// execution for other objects.
7 9
 type Plug interface {
10
+	// Begins operation of the plug and unblocks WaitForStart().
11
+	// May be invoked multiple times but only the first invocation has
12
+	// an effect.
8 13
 	Start()
14
+	// Ends operation of the plug and unblocks WaitForStop()
15
+	// May be invoked multiple times but only the first invocation has
16
+	// an effect. Calling Stop() before Start() is undefined.
9 17
 	Stop()
18
+	// Blocks until Start() is invoked
10 19
 	WaitForStart()
20
+	// Blocks until Stop() is invoked
11 21
 	WaitForStop()
22
+	// Returns true if Start() has been invoked
12 23
 	IsStarted() bool
13 24
 }
14 25
 
26
+// plug is the default implementation of Plug
15 27
 type plug struct {
16 28
 	start   sync.Once
17 29
 	stop    sync.Once
... ...
@@ -19,7 +31,8 @@ type plug struct {
19 19
 	stopCh  chan struct{}
20 20
 }
21 21
 
22
-func NewPlug(started bool) Plug {
22
+// New returns a new plug that can begin in the Started state.
23
+func New(started bool) Plug {
23 24
 	p := &plug{
24 25
 		startCh: make(chan struct{}),
25 26
 		stopCh:  make(chan struct{}),
... ...
@@ -54,3 +67,52 @@ func (p *plug) WaitForStart() {
54 54
 func (p *plug) WaitForStop() {
55 55
 	<-p.stopCh
56 56
 }
57
+
58
+// Leaser controls access to a lease
59
+type Leaser interface {
60
+	// AcquireAndHold tries to acquire the lease and hold it until it expires, the lease is deleted,
61
+	// or we observe another party take the lease. The notify channel will be sent a value
62
+	// when the lease is held, and closed when the lease is lost.
63
+	AcquireAndHold(chan struct{})
64
+	Release()
65
+}
66
+
67
+// leased uses a Leaser to control Start and Stop on a Plug
68
+type Leased struct {
69
+	Plug
70
+
71
+	leaser Leaser
72
+}
73
+
74
+var _ Plug = &Leased{}
75
+
76
+// NewLeased creates a Plug that starts when a lease is acquired
77
+// and stops when it is lost.
78
+func NewLeased(leaser Leaser) *Leased {
79
+	return &Leased{
80
+		Plug:   New(false),
81
+		leaser: leaser,
82
+	}
83
+}
84
+
85
+// Stop releases the acquired lease
86
+func (l *Leased) Stop() {
87
+	l.leaser.Release()
88
+	l.Plug.Stop()
89
+}
90
+
91
+// Run tries to acquire and hold a lease, invoking Start()
92
+// when the lease is held and invoking Stop() when the lease
93
+// is lost.
94
+func (l *Leased) Run() {
95
+	ch := make(chan struct{}, 1)
96
+	go l.leaser.AcquireAndHold(ch)
97
+	defer l.Stop()
98
+	for {
99
+		_, ok := <-ch
100
+		if !ok {
101
+			return
102
+		}
103
+		l.Start()
104
+	}
105
+}