Skip to content

Commit a35cad5

Browse files
authored
fix: traffic-split weight distribution and add e2e tests (#2495)
1 parent 486cc65 commit a35cad5

File tree

4 files changed

+158
-3
lines changed

4 files changed

+158
-3
lines changed

internal/provider/adc/translator/apisixroute.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,10 @@ func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service *adc
198198
var (
199199
upstreams = make([]*adc.Upstream, 0)
200200
weightedUpstreams = make([]adc.TrafficSplitConfigRuleWeightedUpstream, 0)
201-
backendErr error
202201
)
203202

204203
for _, backend := range rule.Backends {
204+
var backendErr error
205205
upstream := adc.NewDefaultUpstream()
206206
// try to get the apisixupstream with the same name as the backend service to be upstream config.
207207
// err is ignored because it does not care about the externalNodes of the apisixupstream.
@@ -223,7 +223,9 @@ func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service *adc
223223
continue
224224
}
225225
}
226-
226+
if backend.Weight != nil {
227+
upstream.Labels["meta_weight"] = strconv.FormatInt(int64(*backend.Weight), 10)
228+
}
227229
upstreams = append(upstreams, upstream)
228230
}
229231

@@ -250,7 +252,7 @@ func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service *adc
250252
}
251253

252254
// no valid upstream
253-
if backendErr != nil || len(upstreams) == 0 || len(upstreams[0].Nodes) == 0 {
255+
if len(upstreams) == 0 || len(upstreams[0].Nodes) == 0 {
254256
return
255257
}
256258

test/e2e/crds/v2/route.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"context"
2222
"fmt"
2323
"io"
24+
"math"
2425
"net"
2526
"net/http"
2627
"time"
@@ -651,6 +652,154 @@ spec:
651652
})
652653
})
653654

655+
Context("Test ApisixRoute Traffic Split", func() {
656+
It("2:1 traffic split test", func() {
657+
const apisixRouteSpec = `
658+
apiVersion: apisix.apache.org/v2
659+
kind: ApisixRoute
660+
metadata:
661+
name: default
662+
spec:
663+
ingressClassName: apisix
664+
http:
665+
- name: rule1
666+
match:
667+
hosts:
668+
- httpbin.org
669+
paths:
670+
- /get
671+
backends:
672+
- serviceName: httpbin-service-e2e-test
673+
servicePort: 80
674+
weight: 10
675+
- serviceName: %s
676+
servicePort: 9180
677+
weight: 5
678+
`
679+
By("apply ApisixRoute with traffic split")
680+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute),
681+
fmt.Sprintf(apisixRouteSpec, s.Deployer.GetAdminServiceName()))
682+
verifyRequest := func() int {
683+
return s.NewAPISIXClient().GET("/get").WithHost("httpbin.org").Expect().Raw().StatusCode
684+
}
685+
By("send requests to verify traffic split")
686+
var (
687+
successCount int
688+
failCount int
689+
)
690+
691+
s.RequestAssert(&scaffold.RequestAssert{
692+
Method: "GET",
693+
Path: "/get",
694+
Host: "httpbin.org",
695+
Check: scaffold.WithExpectedStatus(http.StatusOK),
696+
Timeout: 10 * time.Second,
697+
})
698+
for range 90 {
699+
code := verifyRequest()
700+
if code == http.StatusOK {
701+
successCount++
702+
} else {
703+
failCount++
704+
}
705+
}
706+
707+
By("verify traffic distribution ratio")
708+
ratio := float64(successCount) / float64(failCount)
709+
expectedRatio := 10.0 / 5.0 // 2:1 ratio
710+
deviation := math.Abs(ratio - expectedRatio)
711+
Expect(deviation).Should(BeNumerically("<", 0.5),
712+
"traffic distribution deviation too large (got %.2f, expected %.2f)", ratio, expectedRatio)
713+
})
714+
715+
It("zero-weight test", func() {
716+
const apisixRouteSpec = `
717+
apiVersion: apisix.apache.org/v2
718+
kind: ApisixRoute
719+
metadata:
720+
name: default
721+
spec:
722+
ingressClassName: apisix
723+
http:
724+
- name: rule1
725+
match:
726+
hosts:
727+
- httpbin.org
728+
paths:
729+
- /get
730+
backends:
731+
- serviceName: httpbin-service-e2e-test
732+
servicePort: 80
733+
weight: 10
734+
- serviceName: %s
735+
servicePort: 9180
736+
weight: 0
737+
`
738+
By("apply ApisixRoute with zero-weight backend")
739+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute),
740+
fmt.Sprintf(apisixRouteSpec, s.Deployer.GetAdminServiceName()))
741+
verifyRequest := func() int {
742+
return s.NewAPISIXClient().GET("/get").WithHost("httpbin.org").Expect().Raw().StatusCode
743+
}
744+
745+
By("wait for route to be ready")
746+
s.RequestAssert(&scaffold.RequestAssert{
747+
Method: "GET",
748+
Path: "/get",
749+
Host: "httpbin.org",
750+
Check: scaffold.WithExpectedStatus(http.StatusOK),
751+
Timeout: 10 * time.Second,
752+
})
753+
By("send requests to verify zero-weight behavior")
754+
for range 30 {
755+
code := verifyRequest()
756+
Expect(code).Should(Equal(200))
757+
}
758+
})
759+
It("valid backend is set even if other backend is invalid", func() {
760+
const apisixRouteSpec = `
761+
apiVersion: apisix.apache.org/v2
762+
kind: ApisixRoute
763+
metadata:
764+
name: default
765+
spec:
766+
ingressClassName: apisix
767+
http:
768+
- name: rule1
769+
match:
770+
hosts:
771+
- httpbin.org
772+
paths:
773+
- /get
774+
backends:
775+
- serviceName: httpbin-service-e2e-test
776+
servicePort: 80
777+
weight: 10
778+
- serviceName: invalid-service
779+
servicePort: 9180
780+
weight: 5
781+
`
782+
By("apply ApisixRoute with traffic split")
783+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute), apisixRouteSpec)
784+
verifyRequest := func() int {
785+
return s.NewAPISIXClient().GET("/get").WithHost("httpbin.org").Expect().Raw().StatusCode
786+
}
787+
788+
By("wait for route to be ready")
789+
s.RequestAssert(&scaffold.RequestAssert{
790+
Method: "GET",
791+
Path: "/get",
792+
Host: "httpbin.org",
793+
Check: scaffold.WithExpectedStatus(http.StatusOK),
794+
Timeout: 10 * time.Second,
795+
})
796+
By("send requests to verify all requests routed to valid upstream")
797+
for range 30 {
798+
code := verifyRequest()
799+
Expect(code).Should(Equal(200))
800+
}
801+
})
802+
})
654803
Context("Test ApisixRoute sync during startup", func() {
655804
const route = `
656805
apiVersion: apisix.apache.org/v2

test/e2e/scaffold/apisix_deployer.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,9 @@ func (s *APISIXDeployer) GetAdminEndpoint(svc ...*corev1.Service) string {
409409
return fmt.Sprintf("http://%s.%s:9180", svc[0].Name, svc[0].Namespace)
410410
}
411411

412+
func (s *APISIXDeployer) GetAdminServiceName() string {
413+
return s.dataplaneService.Name
414+
}
412415
func (s *APISIXDeployer) DefaultDataplaneResource() DataplaneResource {
413416
return newADCDataplaneResource(
414417
framework.ProviderType,

test/e2e/scaffold/deployer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Deployer interface {
3232
CreateAdditionalGateway(namePrefix string) (string, *corev1.Service, error)
3333
CleanupAdditionalGateway(identifier string) error
3434
GetAdminEndpoint(...*corev1.Service) string
35+
GetAdminServiceName() string
3536
DefaultDataplaneResource() DataplaneResource
3637
}
3738

0 commit comments

Comments
 (0)