Skip to content

Commit 9a9baad

Browse files
authored
Merge pull request #311 from andrewjjenkins/mirror
Add traffic mirroring for Istio service mesh
2 parents 0c60cf3 + a21e53f commit 9a9baad

22 files changed

Lines changed: 430 additions & 74 deletions

File tree

artifacts/flagger/crd.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ spec:
4141
type: string
4242
JSONPath: .spec.canaryAnalysis.interval
4343
priority: 1
44+
- name: Mirror
45+
type: boolean
46+
JSONPath: .spec.canaryAnalysis.mirror
47+
priority: 1
4448
- name: StepWeight
4549
type: string
4650
JSONPath: .spec.canaryAnalysis.stepWeight
@@ -183,6 +187,9 @@ spec:
183187
stepWeight:
184188
description: Canary incremental traffic percentage step
185189
type: number
190+
mirror:
191+
description: Mirror traffic to canary before shifting
192+
type: boolean
186193
match:
187194
description: A/B testing match conditions
188195
anyOf:

charts/flagger/templates/crd.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ spec:
4242
type: string
4343
JSONPath: .spec.canaryAnalysis.interval
4444
priority: 1
45+
- name: Mirror
46+
type: boolean
47+
JSONPath: .spec.canaryAnalysis.mirror
48+
priority: 1
4549
- name: StepWeight
4650
type: string
4751
JSONPath: .spec.canaryAnalysis.stepWeight
@@ -184,6 +188,9 @@ spec:
184188
stepWeight:
185189
description: Canary incremental traffic percentage step
186190
type: number
191+
mirror:
192+
description: Mirror traffic to canary before shifting
193+
type: boolean
187194
match:
188195
description: A/B testing match conditions
189196
anyOf:

docs/gitbook/faq.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,50 @@ The above configuration will run an analysis for five minutes.
102102
Flagger starts the load test for the canary service (green version) and checks the Prometheus metrics every 30 seconds.
103103
If the analysis result is positive, Flagger will promote the canary (green version) to primary (blue version).
104104

105+
**When can I use traffic mirroring?**
106+
107+
Traffic Mirroring is a pre-stage in a Canary (progressive traffic shifting) or
108+
Blue/Green deployment strategy. Traffic mirroring will copy each incoming
109+
request, sending one request to the primary and one to the canary service. The
110+
response from the primary is sent back to the user. The response from the canary
111+
is discarded. Metrics are collected on both requests so that the deployment will
112+
only proceed if the canary metrics are healthy.
113+
114+
Mirroring is supported by Istio only.
115+
116+
In Istio, mirrored requests have `-shadow` appended to the `Host` (HTTP) or
117+
`Authority` (HTTP/2) header; for example requests to `podinfo.test` that are
118+
mirrored will be reported in telemetry with a destination host
119+
`podinfo.test-shadow`.
120+
121+
Mirroring must only be used for requests that are **idempotent** or capable of
122+
being processed twice (once by the primary and once by the canary). Reads are
123+
idempotent. Before using mirroring on requests that may be writes, you should
124+
consider what will happen if a write is duplicated and handled by the primary
125+
and canary.
126+
127+
To use mirroring, set `spec.canaryAnalysis.mirror` to `true`. Example for
128+
traffic shifting:
129+
130+
```yaml
131+
apiVersion: flagger.app/v1alpha3
132+
kind: Canary
133+
spec:
134+
provider: istio
135+
canaryAnalysis:
136+
interval: 30s
137+
mirror: true
138+
stepWeight: 20
139+
maxWeight: 50
140+
metrics:
141+
- interval: 29s
142+
name: request-success-rate
143+
threshold: 99
144+
- interval: 29s
145+
name: request-duration
146+
threshold: 500
147+
```
148+
105149
### Kubernetes services
106150

107151
**How is an application exposed inside the cluster?**

kustomize/base/flagger/crd.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ spec:
4141
type: string
4242
JSONPath: .spec.canaryAnalysis.interval
4343
priority: 1
44+
- name: Mirror
45+
type: boolean
46+
JSONPath: .spec.canaryAnalysis.mirror
47+
priority: 1
4448
- name: StepWeight
4549
type: string
4650
JSONPath: .spec.canaryAnalysis.stepWeight
@@ -183,6 +187,9 @@ spec:
183187
stepWeight:
184188
description: Canary incremental traffic percentage step
185189
type: number
190+
mirror:
191+
description: Mirror traffic to canary before shifting
192+
type: boolean
186193
match:
187194
description: A/B testing match conditions
188195
anyOf:

pkg/apis/flagger/v1alpha3/types.go

100755100644
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type CanaryAnalysis struct {
111111
Interval string `json:"interval"`
112112
Threshold int `json:"threshold"`
113113
MaxWeight int `json:"maxWeight"`
114+
Mirror bool `json:"mirror,omitempty"`
114115
StepWeight int `json:"stepWeight"`
115116
Metrics []CanaryMetric `json:"metrics"`
116117
Webhooks []CanaryWebhook `json:"webhooks,omitempty"`

pkg/controller/controller_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,9 @@ type Mocks struct {
4242
router router.Interface
4343
}
4444

45-
func SetupMocks(abtest bool) Mocks {
46-
// init canary
47-
c := newTestCanary()
48-
if abtest {
49-
c = newTestCanaryAB()
45+
func SetupMocks(c *v1alpha3.Canary) Mocks {
46+
if c == nil {
47+
c = newTestCanary()
5048
}
5149
flaggerClient := fakeFlagger.NewSimpleClientset(c)
5250

@@ -269,6 +267,12 @@ func newTestCanary() *v1alpha3.Canary {
269267
return cd
270268
}
271269

270+
func newTestCanaryMirror() *v1alpha3.Canary {
271+
cd := newTestCanary()
272+
cd.Spec.CanaryAnalysis.Mirror = true
273+
return cd
274+
}
275+
272276
func newTestCanaryAB() *v1alpha3.Canary {
273277
cd := &v1alpha3.Canary{
274278
TypeMeta: metav1.TypeMeta{APIVersion: v1alpha3.SchemeGroupVersion.String()},

pkg/controller/scheduler.go

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
155155

156156
// check if virtual service exists
157157
// and if it contains weighted destination routes to the primary and canary services
158-
primaryWeight, canaryWeight, err := meshRouter.GetRoutes(cd)
158+
primaryWeight, canaryWeight, mirrored, err := meshRouter.GetRoutes(cd)
159159
if err != nil {
160160
c.recordEventWarningf(cd, "%v", err)
161161
return
@@ -176,7 +176,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
176176
// route all traffic back to primary
177177
primaryWeight = 100
178178
canaryWeight = 0
179-
if err := meshRouter.SetRoutes(cd, primaryWeight, canaryWeight); err != nil {
179+
if err := meshRouter.SetRoutes(cd, primaryWeight, canaryWeight, false); err != nil {
180180
c.recordEventWarningf(cd, "%v", err)
181181
return
182182
}
@@ -218,7 +218,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
218218
if cd.Status.Phase == flaggerv1.CanaryPhasePromoting {
219219
if provider != "kubernetes" {
220220
c.recordEventInfof(cd, "Routing all traffic to primary")
221-
if err := meshRouter.SetRoutes(cd, 100, 0); err != nil {
221+
if err := meshRouter.SetRoutes(cd, 100, 0, false); err != nil {
222222
c.recordEventWarningf(cd, "%v", err)
223223
return
224224
}
@@ -275,7 +275,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
275275
// route all traffic back to primary
276276
primaryWeight = 100
277277
canaryWeight = 0
278-
if err := meshRouter.SetRoutes(cd, primaryWeight, canaryWeight); err != nil {
278+
if err := meshRouter.SetRoutes(cd, primaryWeight, canaryWeight, false); err != nil {
279279
c.recordEventWarningf(cd, "%v", err)
280280
return
281281
}
@@ -302,8 +302,9 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
302302
}
303303

304304
// check if the canary success rate is above the threshold
305-
// skip check if no traffic is routed to canary
306-
if canaryWeight == 0 && cd.Status.Iterations == 0 {
305+
// skip check if no traffic is routed or mirrored to canary
306+
if canaryWeight == 0 && cd.Status.Iterations == 0 &&
307+
(cd.Spec.CanaryAnalysis.Mirror == false || mirrored == false) {
307308
c.recordEventInfof(cd, "Starting canary analysis for %s.%s", cd.Spec.TargetRef.Name, cd.Namespace)
308309

309310
// run pre-rollout web hooks
@@ -328,7 +329,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
328329
if len(cd.Spec.CanaryAnalysis.Match) > 0 && cd.Spec.CanaryAnalysis.Iterations > 0 {
329330
// route traffic to canary and increment iterations
330331
if cd.Spec.CanaryAnalysis.Iterations > cd.Status.Iterations {
331-
if err := meshRouter.SetRoutes(cd, 0, 100); err != nil {
332+
if err := meshRouter.SetRoutes(cd, 0, 100, false); err != nil {
332333
c.recordEventWarningf(cd, "%v", err)
333334
return
334335
}
@@ -372,6 +373,15 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
372373
if cd.Spec.CanaryAnalysis.Iterations > 0 {
373374
// increment iterations
374375
if cd.Spec.CanaryAnalysis.Iterations > cd.Status.Iterations {
376+
// If in "mirror" mode, mirror requests during the entire B/G canary test
377+
if provider != "kubernetes" &&
378+
cd.Spec.CanaryAnalysis.Mirror == true && mirrored == false {
379+
if err := meshRouter.SetRoutes(cd, 100, 0, true); err != nil {
380+
c.recordEventWarningf(cd, "%v", err)
381+
}
382+
c.logger.With("canary", fmt.Sprintf("%s.%s", name, namespace)).
383+
Infof("Enabling mirroring for Blue/Green")
384+
}
375385
if err := c.deployer.SetStatusIterations(cd, cd.Status.Iterations+1); err != nil {
376386
c.recordEventWarningf(cd, "%v", err)
377387
return
@@ -390,7 +400,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
390400
if cd.Spec.CanaryAnalysis.Iterations == cd.Status.Iterations {
391401
if provider != "kubernetes" {
392402
c.recordEventInfof(cd, "Routing all traffic to canary")
393-
if err := meshRouter.SetRoutes(cd, 0, 100); err != nil {
403+
if err := meshRouter.SetRoutes(cd, 0, 100, false); err != nil {
394404
c.recordEventWarningf(cd, "%v", err)
395405
return
396406
}
@@ -429,16 +439,34 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
429439
if cd.Spec.CanaryAnalysis.StepWeight > 0 {
430440
// increase traffic weight
431441
if canaryWeight < maxWeight {
432-
primaryWeight -= cd.Spec.CanaryAnalysis.StepWeight
433-
if primaryWeight < 0 {
434-
primaryWeight = 0
435-
}
436-
canaryWeight += cd.Spec.CanaryAnalysis.StepWeight
437-
if canaryWeight > 100 {
438-
canaryWeight = 100
442+
// If in "mirror" mode, do one step of mirroring before shifting traffic to canary.
443+
// When mirroring, all requests go to primary and canary, but only responses from
444+
// primary go back to the user.
445+
if cd.Spec.CanaryAnalysis.Mirror && canaryWeight == 0 {
446+
if mirrored == false {
447+
mirrored = true
448+
primaryWeight = 100
449+
canaryWeight = 0
450+
} else {
451+
mirrored = false
452+
primaryWeight = 100 - cd.Spec.CanaryAnalysis.StepWeight
453+
canaryWeight = cd.Spec.CanaryAnalysis.StepWeight
454+
}
455+
c.logger.With("canary", fmt.Sprintf("%s.%s", name, namespace)).
456+
Infof("Running mirror step %d/%d/%t", primaryWeight, canaryWeight, mirrored)
457+
} else {
458+
459+
primaryWeight -= cd.Spec.CanaryAnalysis.StepWeight
460+
if primaryWeight < 0 {
461+
primaryWeight = 0
462+
}
463+
canaryWeight += cd.Spec.CanaryAnalysis.StepWeight
464+
if canaryWeight > 100 {
465+
canaryWeight = 100
466+
}
439467
}
440468

441-
if err := meshRouter.SetRoutes(cd, primaryWeight, canaryWeight); err != nil {
469+
if err := meshRouter.SetRoutes(cd, primaryWeight, canaryWeight, mirrored); err != nil {
442470
c.recordEventWarningf(cd, "%v", err)
443471
return
444472
}
@@ -489,7 +517,7 @@ func (c *Controller) shouldSkipAnalysis(cd *flaggerv1.Canary, meshRouter router.
489517
// route all traffic to primary
490518
primaryWeight = 100
491519
canaryWeight = 0
492-
if err := meshRouter.SetRoutes(cd, primaryWeight, canaryWeight); err != nil {
520+
if err := meshRouter.SetRoutes(cd, primaryWeight, canaryWeight, false); err != nil {
493521
c.recordEventWarningf(cd, "%v", err)
494522
return false
495523
}

0 commit comments

Comments
 (0)