Repository: pingcap/tiproxy
Pull request #807 (2025-06-03 06:23:31)

balance: never route to backends with a different label (#805)
This is an automated cherry-pick of #805

<!--

Thank you for contributing to TiProxy!

PR Title Format:
1. pkg [, pkg2, pkg3]: what's changed
2. *: what's changed

-->

### What problem does this PR solve?
<!--

Please create an issue first to describe the problem.

There MUST be one line starting with "Issue Number:  " and 
linking the relevant issues via the "close: #xxx" or "ref: #xxx".

For more info, check https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/contribute-code.html#referring-to-an-issue.

-->

Issue Number: close #804

Problem Summary:
Currently, status is at the highest priority, which means if all the TiDB instances of one business fail, the traffic will be routed to the TiDB of another business.
However, more surveys reveal that label is typically used for a hard limit. That is, the traffic of one business should NEVER affect another business.

What is changed and how it works:
- Place label in a higher priority than status
- If the idlest backend has a different label, do not route or rebalance

### Check List

Tests <!-- At least one of them must be included. -->

- [x] Unit test
- [ ] Integration test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No code

Notable changes

- [ ] Has configuration change
- [ ] Has HTTP API interfaces change
- [ ] Has tiproxyctl change
- [x] Other user behavior changes

### Release note

<!-- compatibility change, improvement, bugfix, and new feature need a release note -->

Please refer to [Release Notes Language Style Guide](https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/release-notes-style-guide.html) to write a quality release note.

```release-note
- Never route to backends with a different label
```


ti-chi-bot[bot]: [LGTM Timeline notifier]
---
Timeline:

- `2025-06-03 06:23:55.9027662 +0000 UTC m=+336522.105111460`: :ballot_box_with_check: agreed by [djshow832](https://github.com/djshow832).
ti-chi-bot[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/pingcap/tiproxy/pull/807#pullrequestreview-2890855534" title="Approved">djshow832</a>*

The full list of commands accepted by this bot can be found [here](https://prow.tidb.net/command-help?repo=pingcap%2Ftiproxy).

The pull request process is described [here](https://book.prow.tidb.net/#/workflows/pr)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/pingcap/tiproxy/blob/release-1.3/OWNERS)~~ [djshow832]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) Report
Attention: Patch coverage is `57.14286%` with `15 lines` in your changes missing coverage. Please review.
> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`release-1.3@a65bfbf`). [Learn more](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#section-missing-base-commit) about missing BASE report.

| [Files with missing lines](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | Patch % | Lines |
|---|---|---|
| [pkg/balance/factor/factor\_label.go](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&filepath=pkg%2Fbalance%2Ffactor%2Ffactor_label.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#diff-cGtnL2JhbGFuY2UvZmFjdG9yL2ZhY3Rvcl9sYWJlbC5nbw==) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) |
| [pkg/balance/factor/factor\_conn.go](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&filepath=pkg%2Fbalance%2Ffactor%2Ffactor_conn.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#diff-cGtnL2JhbGFuY2UvZmFjdG9yL2ZhY3Rvcl9jb25uLmdv) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) |
| [pkg/balance/factor/factor\_cpu.go](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&filepath=pkg%2Fbalance%2Ffactor%2Ffactor_cpu.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#diff-cGtnL2JhbGFuY2UvZmFjdG9yL2ZhY3Rvcl9jcHUuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) |
| [pkg/balance/factor/factor\_health.go](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&filepath=pkg%2Fbalance%2Ffactor%2Ffactor_health.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#diff-cGtnL2JhbGFuY2UvZmFjdG9yL2ZhY3Rvcl9oZWFsdGguZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) |
| [pkg/balance/factor/factor\_location.go](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&filepath=pkg%2Fbalance%2Ffactor%2Ffactor_location.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#diff-cGtnL2JhbGFuY2UvZmFjdG9yL2ZhY3Rvcl9sb2NhdGlvbi5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) |
| [pkg/balance/factor/factor\_memory.go](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&filepath=pkg%2Fbalance%2Ffactor%2Ffactor_memory.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#diff-cGtnL2JhbGFuY2UvZmFjdG9yL2ZhY3Rvcl9tZW1vcnkuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) |
| [pkg/balance/factor/factor\_status.go](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&filepath=pkg%2Fbalance%2Ffactor%2Ffactor_status.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#diff-cGtnL2JhbGFuY2UvZmFjdG9yL2ZhY3Rvcl9zdGF0dXMuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) |

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff               @@
##             release-1.3     #807   +/-   ##
==============================================
  Coverage               ?   66.49%           
==============================================
  Files                  ?      122           
  Lines                  ?    11326           
  Branches               ?        0           
==============================================
  Hits                   ?     7531           
  Misses                 ?     3260           
  Partials               ?      535           
```

| [Flag](https://app.codecov.io/gh/pingcap/tiproxy/pull/807/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | Coverage Δ | |
|---|---|---|
| [unit](https://app.codecov.io/gh/pingcap/tiproxy/pull/807/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `66.49% <57.14%> (?)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pingcap/tiproxy/pull/807?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/pkg/balance/factor/factor.go b/pkg/balance/factor/factor.go
index d1c0124..6aa9387 100644
--- a/pkg/balance/factor/factor.go
+++ b/pkg/balance/factor/factor.go
@@ -19,5 +19,7 @@ type Factor interface {
 	// 0 indicates the factor is already balanced.
 	BalanceCount(from, to scoredBackend) (float64, []zap.Field)
 	SetConfig(cfg *config.Config)
+	// CanBeRouted returns whether a connection can be routed or migrated to the backend with the score.
+	CanBeRouted(score uint64) bool
 	Close()
 }
diff --git a/pkg/balance/factor/factor_balance.go b/pkg/balance/factor/factor_balance.go
index ab6a2a8..1f0af70 100644
--- a/pkg/balance/factor/factor_balance.go
+++ b/pkg/balance/factor/factor_balance.go
@@ -60,11 +60,6 @@ func (fbb *FactorBasedBalance) Init(cfg *config.Config) {
 func (fbb *FactorBasedBalance) setFactors(cfg *config.Config) {
 	fbb.factors = fbb.factors[:0]
 
-	if fbb.factorStatus == nil {
-		fbb.factorStatus = NewFactorStatus(fbb.lg.Named("status"))
-	}
-	fbb.factors = append(fbb.factors, fbb.factorStatus)
-
 	if cfg.Balance.LabelName != "" {
 		if fbb.factorLabel == nil {
 			fbb.factorLabel = NewFactorLabel()
@@ -75,6 +70,11 @@ func (fbb *FactorBasedBalance) setFactors(cfg *config.Config) {
 		fbb.factorLabel = nil
 	}
 
+	if fbb.factorStatus == nil {
+		fbb.factorStatus = NewFactorStatus(fbb.lg.Named("status"))
+	}
+	fbb.factors = append(fbb.factors, fbb.factorStatus)
+
 	switch cfg.Balance.Policy {
 	case config.BalancePolicyResource, config.BalancePolicyLocation:
 		if fbb.factorLocation == nil {
@@ -177,12 +177,16 @@ func (fbb *FactorBasedBalance) BackendToRoute(backends []policy.BackendCtx) poli
 	if len(backends) == 0 {
 		return nil
 	}
+
+	scoredBackends := fbb.updateScore(backends)
+	if !fbb.canBeRouted(scoredBackends[0].scoreBits) {
+		return nil
+	}
 	if len(backends) == 1 {
 		return backends[0]
 	}
 
 	var fields []zap.Field
-	scoredBackends := fbb.updateScore(backends)
 	for _, backend := range scoredBackends {
 		fields = append(fields, zap.String(backend.Addr(), strconv.FormatUint(backend.scoreBits, 16)))
 	}
@@ -237,8 +241,7 @@ func (fbb *FactorBasedBalance) BackendsToBalance(backends []policy.BackendCtx) (
 		return
 	}
 	scoredBackends := fbb.updateScore(backends)
-	// No need to migrate to unhealthy backends.
-	if !scoredBackends[0].Healthy() {
+	if !fbb.canBeRouted(scoredBackends[0].scoreBits) {
 		return
 	}
 	if scoredBackends[0].scoreBits == scoredBackends[len(scoredBackends)-1].scoreBits {
@@ -287,6 +290,19 @@ func (fbb *FactorBasedBalance) BackendsToBalance(backends []policy.BackendCtx) (
 	return
 }
 
+func (fbb *FactorBasedBalance) canBeRouted(score uint64) bool {
+	leftBitNum := fbb.totalBitNum
+	for _, factor := range fbb.factors {
+		bitNum := factor.ScoreBitNum()
+		score := score << (maxBitNum - leftBitNum) >> (maxBitNum - bitNum)
+		if !factor.CanBeRouted(score) {
+			return false
+		}
+		leftBitNum -= bitNum
+	}
+	return true
+}
+
 func (fbb *FactorBasedBalance) SetConfig(cfg *config.Config) {
 	fbb.setFactors(cfg)
 }
diff --git a/pkg/balance/factor/factor_conn.go b/pkg/balance/factor/factor_conn.go
index f2f560b..39517db 100644
--- a/pkg/balance/factor/factor_conn.go
+++ b/pkg/balance/factor/factor_conn.go
@@ -69,5 +69,9 @@ func (fcc *FactorConnCount) BalanceCount(from, to scoredBackend) (float64, []zap
 func (fcc *FactorConnCount) SetConfig(cfg *config.Config) {
 }
 
+func (fcc *FactorConnCount) CanBeRouted(_ uint64) bool {
+	return true
+}
+
 func (fcc *FactorConnCount) Close() {
 }
diff --git a/pkg/balance/factor/factor_cpu.go b/pkg/balance/factor/factor_cpu.go
index abb7c79..ab5d106 100644
--- a/pkg/balance/factor/factor_cpu.go
+++ b/pkg/balance/factor/factor_cpu.go
@@ -272,6 +272,10 @@ func (fc *FactorCPU) BalanceCount(from, to scoredBackend) (float64, []zap.Field)
 func (fc *FactorCPU) SetConfig(cfg *config.Config) {
 }
 
+func (fc *FactorCPU) CanBeRouted(_ uint64) bool {
+	return true
+}
+
 func (fc *FactorCPU) Close() {
 	fc.mr.RemoveQueryExpr(fc.Name())
 }
diff --git a/pkg/balance/factor/factor_health.go b/pkg/balance/factor/factor_health.go
index 90fe42a..e8c6674 100644
--- a/pkg/balance/factor/factor_health.go
+++ b/pkg/balance/factor/factor_health.go
@@ -366,6 +366,10 @@ func (fh *FactorHealth) BalanceCount(from, to scoredBackend) (float64, []zap.Fie
 func (fh *FactorHealth) SetConfig(cfg *config.Config) {
 }
 
+func (fh *FactorHealth) CanBeRouted(_ uint64) bool {
+	return true
+}
+
 func (fh *FactorHealth) Close() {
 	for _, indicator := range fh.indicators {
 		fh.mr.RemoveQueryExpr(indicator.key)
diff --git a/pkg/balance/factor/factor_label.go b/pkg/balance/factor/factor_label.go
index 6b8e5ec..b53d341 100644
--- a/pkg/balance/factor/factor_label.go
+++ b/pkg/balance/factor/factor_label.go
@@ -63,5 +63,10 @@ func (fl *FactorLabel) SetConfig(cfg *config.Config) {
 	}
 }
 
+func (fl *FactorLabel) CanBeRouted(score uint64) bool {
+	// Label is used to isolate business resources. Never route to the backends of another business.
+	return score == 0
+}
+
 func (fl *FactorLabel) Close() {
 }
diff --git a/pkg/balance/factor/factor_location.go b/pkg/balance/factor/factor_location.go
index 2d3821a..fd543d9 100644
--- a/pkg/balance/factor/factor_location.go
+++ b/pkg/balance/factor/factor_location.go
@@ -53,5 +53,9 @@ func (fl *FactorLocation) BalanceCount(from, to scoredBackend) (float64, []zap.F
 func (fl *FactorLocation) SetConfig(cfg *config.Config) {
 }
 
+func (fl *FactorLocation) CanBeRouted(_ uint64) bool {
+	return true
+}
+
 func (fl *FactorLocation) Close() {
 }
diff --git a/pkg/balance/factor/factor_memory.go b/pkg/balance/factor/factor_memory.go
index c20ff3e..6d1c456 100644
--- a/pkg/balance/factor/factor_memory.go
+++ b/pkg/balance/factor/factor_memory.go
@@ -288,6 +288,10 @@ func (fm *FactorMemory) BalanceCount(from, to scoredBackend) (float64, []zap.Fie
 func (fm *FactorMemory) SetConfig(cfg *config.Config) {
 }
 
+func (fm *FactorMemory) CanBeRouted(_ uint64) bool {
+	return true
+}
+
 func (fm *FactorMemory) Close() {
 	fm.mr.RemoveQueryExpr(fm.Name())
 }
diff --git a/pkg/balance/factor/factor_status.go b/pkg/balance/factor/factor_status.go
index 9cd1569..7b8ec5f 100644
--- a/pkg/balance/factor/factor_status.go
+++ b/pkg/balance/factor/factor_status.go
@@ -107,5 +107,9 @@ func (fs *FactorStatus) BalanceCount(from, to scoredBackend) (float64, []zap.Fie
 func (fs *FactorStatus) SetConfig(cfg *config.Config) {
 }
 
+func (fl *FactorStatus) CanBeRouted(score uint64) bool {
+	return score == 0
+}
+
 func (fs *FactorStatus) Close() {
 }


Test patch:
diff --git a/pkg/balance/factor/factor_balance_test.go b/pkg/balance/factor/factor_balance_test.go
index 88e1b22..b5f7804 100644
--- a/pkg/balance/factor/factor_balance_test.go
+++ b/pkg/balance/factor/factor_balance_test.go
@@ -12,12 +12,13 @@ import (
 	"github.com/pingcap/tiproxy/lib/util/logger"
 	"github.com/pingcap/tiproxy/pkg/balance/policy"
 	"github.com/stretchr/testify/require"
+	"go.uber.org/zap"
 )
 
 func TestRouteWithOneFactor(t *testing.T) {
 	lg, _ := logger.CreateLoggerForTest(t)
 	fm := NewFactorBasedBalance(lg, newMockMetricsReader())
-	factor := &mockFactor{bitNum: 8, balanceCount: 1, threshold: 1}
+	factor := &mockFactor{bitNum: 8, balanceCount: 1, threshold: 1, canBeRouted: true}
 	fm.factors = []Factor{factor}
 	require.NoError(t, fm.updateBitNum())
 
@@ -71,8 +72,8 @@ func TestRouteWithOneFactor(t *testing.T) {
 func TestRouteWith2Factors(t *testing.T) {
 	lg, _ := logger.CreateLoggerForTest(t)
 	fm := NewFactorBasedBalance(lg, newMockMetricsReader())
-	factor1 := &mockFactor{bitNum: 4, balanceCount: 1, threshold: 1}
-	factor2 := &mockFactor{bitNum: 12, balanceCount: 1, threshold: 1}
+	factor1 := &mockFactor{bitNum: 4, balanceCount: 1, threshold: 1, canBeRouted: true}
+	factor2 := &mockFactor{bitNum: 12, balanceCount: 1, threshold: 1, canBeRouted: true}
 	fm.factors = []Factor{factor1, factor2}
 	require.NoError(t, fm.updateBitNum())
 
@@ -143,7 +144,7 @@ func TestRouteWith2Factors(t *testing.T) {
 func TestBalanceWithOneFactor(t *testing.T) {
 	lg, _ := logger.CreateLoggerForTest(t)
 	fm := NewFactorBasedBalance(lg, newMockMetricsReader())
-	factor := &mockFactor{bitNum: 2, balanceCount: 1}
+	factor := &mockFactor{bitNum: 2, balanceCount: 1, canBeRouted: true}
 	fm.factors = []Factor{factor}
 	require.NoError(t, fm.updateBitNum())
 
@@ -193,8 +194,8 @@ func TestBalanceWithOneFactor(t *testing.T) {
 func TestBalanceWith2Factors(t *testing.T) {
 	lg, _ := logger.CreateLoggerForTest(t)
 	fm := NewFactorBasedBalance(lg, newMockMetricsReader())
-	factor1 := &mockFactor{bitNum: 2, balanceCount: 2, threshold: 1}
-	factor2 := &mockFactor{bitNum: 12, balanceCount: 1}
+	factor1 := &mockFactor{bitNum: 2, balanceCount: 2, threshold: 1, canBeRouted: true}
+	factor2 := &mockFactor{bitNum: 12, balanceCount: 1, canBeRouted: true}
 	fm.factors = []Factor{factor1, factor2}
 	require.NoError(t, fm.updateBitNum())
 
@@ -280,7 +281,7 @@ func TestBalanceWith2Factors(t *testing.T) {
 func TestBalanceWith3Factors(t *testing.T) {
 	lg, _ := logger.CreateLoggerForTest(t)
 	fm := NewFactorBasedBalance(lg, newMockMetricsReader())
-	factors := []*mockFactor{{bitNum: 1}, {bitNum: 2}, {bitNum: 2}}
+	factors := []*mockFactor{{bitNum: 1, canBeRouted: true}, {bitNum: 2, canBeRouted: true}, {bitNum: 2, canBeRouted: true}}
 	fm.factors = []Factor{factors[0], factors[1], factors[2]}
 	require.NoError(t, fm.updateBitNum())
 
@@ -333,7 +334,7 @@ func TestBalanceWith3Factors(t *testing.T) {
 func TestScoreAlwaysUpdated(t *testing.T) {
 	lg, _ := logger.CreateLoggerForTest(t)
 	fm := NewFactorBasedBalance(lg, newMockMetricsReader())
-	factors := []*mockFactor{{bitNum: 1}, {bitNum: 2}}
+	factors := []*mockFactor{{bitNum: 1, canBeRouted: true}, {bitNum: 2, canBeRouted: true}}
 	fm.factors = []Factor{factors[0], factors[1]}
 	factors[0].updateScore = func(backends []scoredBackend) {
 		for i := 0; i < len(backends); i++ {
@@ -375,14 +376,14 @@ func TestSetFactors(t *testing.T) {
 			setFunc: func(balance *config.Balance) {
 				balance.LabelName = "group"
 			},
-			expectedNames: []string{"status", "label", "health", "memory", "cpu", "location", "conn"},
+			expectedNames: []string{"label", "status", "health", "memory", "cpu", "location", "conn"},
 		},
 		{
 			setFunc: func(balance *config.Balance) {
 				balance.Policy = config.BalancePolicyLocation
 				balance.LabelName = "group"
 			},
-			expectedNames: []string{"status", "label", "location", "health", "memory", "cpu", "conn"},
+			expectedNames: []string{"label", "status", "location", "health", "memory", "cpu", "conn"},
 		},
 		{
 			setFunc: func(balance *config.Balance) {
@@ -395,7 +396,7 @@ func TestSetFactors(t *testing.T) {
 				balance.Policy = config.BalancePolicyConnection
 				balance.LabelName = "group"
 			},
-			expectedNames: []string{"status", "label", "conn"},
+			expectedNames: []string{"label", "status", "conn"},
 		},
 	}
 
@@ -417,3 +418,56 @@ func TestSetFactors(t *testing.T) {
 	}
 	fm.Close()
 }
+
+func TestCanBeRouted(t *testing.T) {
+	fm := NewFactorBasedBalance(zap.NewNop(), newMockMetricsReader())
+	factor1 := &mockFactor{bitNum: 2, balanceCount: 1, canBeRouted: false}
+	factor2 := &mockFactor{bitNum: 2, balanceCount: 1, canBeRouted: true}
+	fm.factors = []Factor{factor1, factor2}
+	require.NoError(t, fm.updateBitNum())
+
+	tests := []struct {
+		scores1 []int
+		scores2 []int
+		routed  bool
+	}{
+		{
+			scores1: []int{1, 0},
+			scores2: []int{0, 0},
+			routed:  true,
+		},
+		{
+			scores1: []int{1, 1},
+			scores2: []int{1, 0},
+			routed:  false,
+		},
+		{
+			scores1: []int{0, 0},
+			scores2: []int{2, 1},
+			routed:  true,
+		},
+		{
+			scores1: []int{2, 1},
+			scores2: []int{0, 0},
+			routed:  false,
+		},
+	}
+	for tIdx, test := range tests {
+		factor1.updateScore = func(backends []scoredBackend) {
+			for i := 0; i < len(backends); i++ {
+				backends[i].addScore(test.scores1[i], factor1.bitNum)
+			}
+		}
+		factor2.updateScore = func(backends []scoredBackend) {
+			for i := 0; i < len(backends); i++ {
+				backends[i].addScore(test.scores2[i], factor2.bitNum)
+			}
+		}
+		backends := createBackends(len(test.scores1))
+		_, _, count, _, _ := fm.BackendsToBalance(backends)
+		require.Equal(t, test.routed, count > 0, "test index %d", tIdx)
+		backends = createBackends(len(test.scores1))
+		b := fm.BackendToRoute(backends)
+		require.Equal(t, test.routed, b != nil, "test index %d", tIdx)
+	}
+}
diff --git a/pkg/balance/factor/mock_test.go b/pkg/balance/factor/mock_test.go
index 668032a..ceffff6 100644
--- a/pkg/balance/factor/mock_test.go
+++ b/pkg/balance/factor/mock_test.go
@@ -69,6 +69,7 @@ type mockFactor struct {
 	balanceCount float64
 	updateScore  func(backends []scoredBackend)
 	cfg          *config.Config
+	canBeRouted  bool
 }
 
 func (mf *mockFactor) Name() string {
@@ -101,6 +102,13 @@ func (mf *mockFactor) SetConfig(cfg *config.Config) {
 	mf.cfg = cfg
 }
 
+func (mf *mockFactor) CanBeRouted(score uint64) bool {
+	if mf.canBeRouted {
+		return true
+	}
+	return score == 0
+}
+
 func (mf *mockFactor) Close() {
 }