Merge pull request #492 from weaveworks/mirror-percentage

istio router: make mirrorPercentage configurable for traffic mirroring
This commit is contained in:
Takeshi Yoneda 2020-03-10 16:44:41 +09:00 committed by GitHub
commit 9a5328b507
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 136 additions and 75 deletions

View File

@ -528,6 +528,9 @@ spec:
mirror:
description: Mirror traffic to canary
type: boolean
mirrorWeight:
description: Percentage of traffic to be mirrored
type: number
match:
description: A/B testing match conditions
type: array

View File

@ -528,6 +528,9 @@ spec:
mirror:
description: Mirror traffic to canary
type: boolean
mirrorWeight:
description: Percentage of traffic to be mirrored
type: number
match:
description: A/B testing match conditions
type: array

View File

@ -324,6 +324,8 @@ spec:
iterations: 10
# enable traffic shadowing
mirror: true
# weight of the traffic mirrored to your canary (defaults to 100%)
mirrorWeight: 100
metrics:
- name: request-success-rate
thresholdRange:
@ -357,7 +359,7 @@ With the above configuration, Flagger will run a canary release with the followi
* run the acceptance tests
* abort the canary release if tests fail
* start the load tests
* mirror traffic from primary to canary
* mirror 100% of the traffic from primary to canary
* check request success rate and request duration every minute
* abort the canary release if the metrics check failure threshold is reached
* stop traffic mirroring after the number of iterations is reached

View File

@ -277,6 +277,8 @@ Istio example:
threshold: 2
# Traffic shadowing (compatible with Istio only)
mirror: true
# Weight of the traffic mirrored to your canary (defaults to 100%)
mirrorWeight: 100
```
Mirroring rollout steps for service mesh:
@ -287,7 +289,7 @@ Mirroring rollout steps for service mesh:
* run the acceptance tests
* abort the canary release if tests fail
* start the load tests
* mirror traffic from primary to canary
* mirror 100% of the traffic from primary to canary
* check request success rate and request duration every minute
* abort the canary release if the failure threshold is reached
* stop traffic mirroring after the number of iterations is reached

View File

@ -528,6 +528,9 @@ spec:
mirror:
description: Mirror traffic to canary
type: boolean
mirrorWeight:
description: Percentage of traffic to be mirrored
type: number
match:
description: A/B testing match conditions
type: array

View File

@ -175,10 +175,14 @@ type CanaryAnalysis struct {
// +optional
Iterations int `json:"iterations,omitempty"`
//Enable traffic mirroring for Blue/Green
// Enable traffic mirroring for Blue/Green
// +optional
Mirror bool `json:"mirror,omitempty"`
// Percentage of the traffic to be mirrored in the range of [0, 100].
// +optional
MirrorWeight int `json:"mirrorWeight,omitempty"`
// Max traffic percentage routed to canary
// +optional
MaxWeight int `json:"maxWeight,omitempty"`

View File

@ -325,6 +325,11 @@ type HTTPRoute struct {
// destination.
Mirror *Destination `json:"mirror,omitempty"`
// Percentage of the traffic to be mirrored by the `mirror` field.
// If this field is absent, all the traffic (100%) will be mirrored.
// Max value is 100.
MirrorPercentage *Percent `json:"mirrorPercentage,omitempty"`
// Cross-Origin Resource Sharing policy (CORS). Refer to
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
// for further details about cross origin resource sharing.
@ -334,6 +339,11 @@ type HTTPRoute struct {
Headers *Headers `json:"headers,omitempty"`
}
// Percent specifies a percentage in the range of [0.0, 100.0].
type Percent struct {
Value float64 `json:"value,omitempty"`
}
// Header manipulation rules
type Headers struct {
// Header manipulation rules to apply before forwarding a request

View File

@ -421,6 +421,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) {
*out = new(Destination)
(*in).DeepCopyInto(*out)
}
if in.MirrorPercentage != nil {
in, out := &in.MirrorPercentage, &out.MirrorPercentage
*out = new(Percent)
**out = **in
}
if in.CorsPolicy != nil {
in, out := &in.CorsPolicy, &out.CorsPolicy
*out = new(CorsPolicy)
@ -628,6 +633,22 @@ func (in *OutlierDetection) DeepCopy() *OutlierDetection {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Percent) DeepCopyInto(out *Percent) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Percent.
func (in *Percent) DeepCopy() *Percent {
if in == nil {
return nil
}
out := new(Percent)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PortSelector) DeepCopyInto(out *PortSelector) {
*out = *in

View File

@ -207,7 +207,7 @@ func newDeploymentControllerTestCanary() *flaggerv1.Canary {
Kind: "HorizontalPodAutoscaler",
}, Service: flaggerv1.CanaryService{
Port: 9898,
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,

View File

@ -249,7 +249,7 @@ func newDaemonSetTestCanary() *flaggerv1.Canary {
Kind: "DaemonSet",
}, Service: flaggerv1.CanaryService{
Port: 9898,
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,
@ -288,7 +288,7 @@ func newDaemonSetTestCanary() *flaggerv1.Canary {
func newDaemonSetTestCanaryMirror() *flaggerv1.Canary {
cd := newDaemonSetTestCanary()
cd.Spec.CanaryAnalysis.Mirror = true
cd.Spec.Analysis.Mirror = true
return cd
}
@ -306,7 +306,7 @@ func newDaemonSetTestCanaryAB() *flaggerv1.Canary {
Kind: "DaemonSet",
}, Service: flaggerv1.CanaryService{
Port: 9898,
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
Iterations: 10,
Match: []istiov1alpha3.HTTPMatchRequest{

View File

@ -55,7 +55,7 @@ func TestScheduler_DaemonSetRollback(t *testing.T) {
require.NoError(t, err)
cd := c.DeepCopy()
cd.Spec.CanaryAnalysis.Metrics = append(c.Spec.CanaryAnalysis.Metrics, flaggerv1.CanaryMetric{
cd.Spec.Analysis.Metrics = append(c.Spec.Analysis.Metrics, flaggerv1.CanaryMetric{
Name: "fail",
Interval: "1m",
ThresholdRange: &flaggerv1.CanaryThresholdRange{
@ -437,7 +437,7 @@ func TestScheduler_DaemonSetAlerts(t *testing.T) {
defer ts.Close()
canary := newDaemonSetTestCanary()
canary.Spec.CanaryAnalysis.Alerts = []flaggerv1.CanaryAlert{
canary.Spec.Analysis.Alerts = []flaggerv1.CanaryAlert{
{
Name: "slack-dev",
Severity: "info",

View File

@ -256,7 +256,7 @@ func newDeploymentTestCanary() *flaggerv1.Canary {
Kind: "HorizontalPodAutoscaler",
}, Service: flaggerv1.CanaryService{
Port: 9898,
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,
@ -295,7 +295,7 @@ func newDeploymentTestCanary() *flaggerv1.Canary {
func newDeploymentTestCanaryMirror() *flaggerv1.Canary {
cd := newDeploymentTestCanary()
cd.Spec.CanaryAnalysis.Mirror = true
cd.Spec.Analysis.Mirror = true
return cd
}
@ -318,7 +318,7 @@ func newDeploymentTestCanaryAB() *flaggerv1.Canary {
Kind: "HorizontalPodAutoscaler",
}, Service: flaggerv1.CanaryService{
Port: 9898,
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
Iterations: 10,
Match: []istiov1alpha3.HTTPMatchRequest{

View File

@ -55,7 +55,7 @@ func TestScheduler_DeploymentRollback(t *testing.T) {
c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
require.NoError(t, err)
cd := c.DeepCopy()
cd.Spec.CanaryAnalysis.Metrics = append(c.Spec.CanaryAnalysis.Metrics, flaggerv1.CanaryMetric{
cd.Spec.Analysis.Metrics = append(c.Spec.Analysis.Metrics, flaggerv1.CanaryMetric{
Name: "fail",
Interval: "1m",
ThresholdRange: &flaggerv1.CanaryThresholdRange{
@ -519,7 +519,7 @@ func TestScheduler_DeploymentAlerts(t *testing.T) {
defer ts.Close()
canary := newDeploymentTestCanary()
canary.Spec.CanaryAnalysis.Alerts = []flaggerv1.CanaryAlert{
canary.Spec.Analysis.Alerts = []flaggerv1.CanaryAlert{
{
Name: "slack-dev",
Severity: "info",

View File

@ -98,7 +98,7 @@ func newTestServiceCanary() *flaggerv1.Canary {
Service: flaggerv1.CanaryService{
Port: 9898,
},
CanaryAnalysis: &flaggerv1.CanaryAnalysis{
Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,

View File

@ -57,8 +57,8 @@ func TestContourRouter_Reconcile(t *testing.T) {
require.NoError(t, err)
cdClone = cd.DeepCopy()
cdClone.Spec.CanaryAnalysis.Iterations = 5
cdClone.Spec.CanaryAnalysis.Match = newTestABTest().Spec.CanaryAnalysis.Match
cdClone.Spec.Analysis.Iterations = 5
cdClone.Spec.Analysis.Match = newTestABTest().Spec.Analysis.Match
canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(cdClone)
require.NoError(t, err)
@ -104,8 +104,8 @@ func TestContourRouter_Routes(t *testing.T) {
// test update to A/B
cdClone := cd.DeepCopy()
cdClone.Spec.CanaryAnalysis.Iterations = 5
cdClone.Spec.CanaryAnalysis.Match = newTestABTest().Spec.CanaryAnalysis.Match
cdClone.Spec.Analysis.Iterations = 5
cdClone.Spec.Analysis.Match = newTestABTest().Spec.Analysis.Match
canary, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Update(cdClone)
require.NoError(t, err)

View File

@ -206,7 +206,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
newSpec,
virtualService.Spec,
cmpopts.IgnoreFields(istiov1alpha3.DestinationWeight{}, "Weight"),
cmpopts.IgnoreFields(istiov1alpha3.HTTPRoute{}, "Mirror"),
cmpopts.IgnoreFields(istiov1alpha3.HTTPRoute{}, "Mirror", "MirrorPercentage"),
); diff != "" {
vtClone := virtualService.DeepCopy()
vtClone.Spec = newSpec
@ -304,6 +304,10 @@ func (ir *IstioRouter) SetRoutes(
vsCopy.Spec.Http[0].Mirror = &istiov1alpha3.Destination{
Host: canaryName,
}
if mw := canary.GetAnalysis().MirrorWeight; mw > 0 {
vsCopy.Spec.Http[0].MirrorPercentage = &istiov1alpha3.Percent{Value: float64(mw)}
}
}
// fix routing (A/B testing)

View File

@ -91,69 +91,78 @@ func TestIstioRouter_SetRoutes(t *testing.T) {
err := router.Reconcile(mocks.canary)
require.NoError(t, err)
p, c, m, err := router.GetRoutes(mocks.canary)
require.NoError(t, err)
p = 60
c = 40
m = false
err = router.SetRoutes(mocks.canary, p, c, m)
require.NoError(t, err)
vs, err := mocks.meshClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
require.NoError(t, err)
pHost := fmt.Sprintf("%s-primary", mocks.canary.Spec.TargetRef.Name)
cHost := fmt.Sprintf("%s-canary", mocks.canary.Spec.TargetRef.Name)
pRoute := istiov1alpha3.DestinationWeight{}
cRoute := istiov1alpha3.DestinationWeight{}
var mirror *istiov1alpha3.Destination
for _, http := range vs.Spec.Http {
for _, route := range http.Route {
if route.Destination.Host == pHost {
pRoute = route
}
if route.Destination.Host == cHost {
cRoute = route
mirror = http.Mirror
t.Run("normal", func(t *testing.T) {
p, c := 60, 40
err := router.SetRoutes(mocks.canary, p, c, false)
require.NoError(t, err)
vs, err := mocks.meshClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
require.NoError(t, err)
var pRoute, cRoute istiov1alpha3.DestinationWeight
var mirror *istiov1alpha3.Destination
for _, http := range vs.Spec.Http {
for _, route := range http.Route {
if route.Destination.Host == pHost {
pRoute = route
}
if route.Destination.Host == cHost {
cRoute = route
mirror = http.Mirror
}
}
}
}
assert.Equal(t, p, pRoute.Weight)
assert.Equal(t, c, cRoute.Weight)
assert.Nil(t, mirror)
assert.Equal(t, p, pRoute.Weight)
assert.Equal(t, c, cRoute.Weight)
assert.Nil(t, mirror)
mirror = nil
p = 100
c = 0
m = true
})
err = router.SetRoutes(mocks.canary, p, c, m)
require.NoError(t, err)
t.Run("mirror", func(t *testing.T) {
for _, w := range []int{0, 10, 50} {
p, c := 100, 0
vs, err = mocks.meshClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
require.NoError(t, err)
// set mirror weight
mocks.canary.Spec.Analysis.MirrorWeight = w
err := router.SetRoutes(mocks.canary, p, c, true)
require.NoError(t, err)
for _, http := range vs.Spec.Http {
for _, route := range http.Route {
if route.Destination.Host == pHost {
pRoute = route
vs, err := mocks.meshClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
require.NoError(t, err)
var pRoute, cRoute istiov1alpha3.DestinationWeight
var mirror *istiov1alpha3.Destination
var mirrorWeight *istiov1alpha3.Percent
for _, http := range vs.Spec.Http {
for _, route := range http.Route {
if route.Destination.Host == pHost {
pRoute = route
}
if route.Destination.Host == cHost {
cRoute = route
mirror = http.Mirror
mirrorWeight = http.MirrorPercentage
}
}
}
if route.Destination.Host == cHost {
cRoute = route
mirror = http.Mirror
assert.Equal(t, p, pRoute.Weight)
assert.Equal(t, c, cRoute.Weight)
if assert.NotNil(t, mirror) {
assert.Equal(t, cHost, mirror.Host)
}
if w > 0 && assert.NotNil(t, mirrorWeight) {
assert.Equal(t, w, int(mirrorWeight.Value))
} else {
assert.Nil(t, mirrorWeight)
}
}
}
assert.Equal(t, p, pRoute.Weight)
assert.Equal(t, c, cRoute.Weight)
if assert.NotNil(t, mirror) {
assert.Equal(t, cHost, mirror.Host)
}
})
}
func TestIstioRouter_GetRoutes(t *testing.T) {

View File

@ -112,7 +112,7 @@ func newTestCanary() *flaggerv1.Canary {
"public-gateway.istio",
"mesh",
},
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,
@ -158,7 +158,7 @@ func newTestCanaryAppMesh() *flaggerv1.Canary {
PerTryTimeout: "gateway-error",
RetryOn: "5s",
},
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,
@ -203,7 +203,7 @@ func newTestSMICanary() *flaggerv1.Canary {
},
PortDiscovery: true,
},
CanaryAnalysis: &flaggerv1.CanaryAnalysis{
Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,
@ -247,7 +247,7 @@ func newTestABTest() *flaggerv1.Canary {
Service: flaggerv1.CanaryService{
Port: 9898,
MeshName: "global",
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
Iterations: 2,
Match: []istiov1alpha3.HTTPMatchRequest{
@ -397,7 +397,7 @@ func newTestCanaryIngress() *flaggerv1.Canary {
},
Service: flaggerv1.CanaryService{
Port: 9898,
}, CanaryAnalysis: &flaggerv1.CanaryAnalysis{
}, Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,