mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
perf(sub): make balancer member retagging linear
buildBalancerConfig restarted the free tag-suffix search at 2 for every member, so member k formatted and looked up k tags and n members of one protocol cost n²/2 lookups; a balancer with many same-protocol members held a subscription request for minutes. The search now resumes per base tag where the previous member stopped, producing the same tags in linear time.
This commit is contained in:
@@ -531,6 +531,9 @@ func leastLoadCosts(balancer *model.SubBalancer, members []balMember) []any {
|
|||||||
func (s *SubJsonService) buildBalancerConfig(balancer *model.SubBalancer, entries []subConfigEntry, entryProxies [][]map[string]any) json_util.RawMessage {
|
func (s *SubJsonService) buildBalancerConfig(balancer *model.SubBalancer, entries []subConfigEntry, entryProxies [][]map[string]any) json_util.RawMessage {
|
||||||
prefix := fmt.Sprintf("bal-%d-", balancer.Id)
|
prefix := fmt.Sprintf("bal-%d-", balancer.Id)
|
||||||
usedTags := make(map[string]bool)
|
usedTags := make(map[string]bool)
|
||||||
|
// Next suffix to try per base: tags are only ever added, so every lower one
|
||||||
|
// is taken and resuming there keeps a node-sized member list linear.
|
||||||
|
nextSuffix := make(map[string]int)
|
||||||
var proxies []json_util.RawMessage
|
var proxies []json_util.RawMessage
|
||||||
// Members in emission order with their owning inbound, so costs[] can
|
// Members in emission order with their owning inbound, so costs[] can
|
||||||
// reference the exact retagged tags assigned here.
|
// reference the exact retagged tags assigned here.
|
||||||
@@ -546,9 +549,11 @@ func (s *SubJsonService) buildBalancerConfig(balancer *model.SubBalancer, entrie
|
|||||||
protocol, _ := outbound["protocol"].(string)
|
protocol, _ := outbound["protocol"].(string)
|
||||||
base := prefix + balancerMemberSuffix(protocol)
|
base := prefix + balancerMemberSuffix(protocol)
|
||||||
tag := base
|
tag := base
|
||||||
for suffix := 2; usedTags[tag]; suffix++ {
|
suffix := max(nextSuffix[base], 2)
|
||||||
|
for ; usedTags[tag]; suffix++ {
|
||||||
tag = fmt.Sprintf("%s-%d", base, suffix)
|
tag = fmt.Sprintf("%s-%d", base, suffix)
|
||||||
}
|
}
|
||||||
|
nextSuffix[base] = suffix
|
||||||
usedTags[tag] = true
|
usedTags[tag] = true
|
||||||
member := maps.Clone(outbound)
|
member := maps.Clone(outbound)
|
||||||
member["tag"] = tag
|
member["tag"] = tag
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ package sub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func seedSubBalancer(t *testing.T, b *model.SubBalancer) *model.SubBalancer {
|
func seedSubBalancer(t *testing.T, b *model.SubBalancer) *model.SubBalancer {
|
||||||
@@ -508,3 +512,44 @@ func TestSubJson_BalancerCostsSkippedForNonLeastLoadStrategy(t *testing.T) {
|
|||||||
t.Fatalf("random balancer must never emit costs despite stored weights: %v", strategy)
|
t.Fatalf("random balancer must never emit costs despite stored weights: %v", strategy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A node authors the endpoint list of its inbounds, so the member count is
|
||||||
|
// not the admin's to bound; probing every taken suffix per member went quadratic.
|
||||||
|
func TestSubJson_BalancerManySameProtocolMembers(t *testing.T) {
|
||||||
|
members := make([]map[string]any, 100_000)
|
||||||
|
for i := range members {
|
||||||
|
members[i] = map[string]any{"protocol": "vless", "tag": "proxy"}
|
||||||
|
}
|
||||||
|
// A protocol whose base collides with a suffixed tag must still be skipped.
|
||||||
|
members[0]["protocol"] = "vless-3"
|
||||||
|
members[4]["protocol"] = "trojan"
|
||||||
|
|
||||||
|
js := NewSubJsonService("", "", "", "", NewSubService(""))
|
||||||
|
done := make(chan json_util.RawMessage, 1)
|
||||||
|
go func() {
|
||||||
|
done <- js.buildBalancerConfig(
|
||||||
|
&model.SubBalancer{Id: 1, Remark: "many", Strategy: "random", InboundIds: []int{7}},
|
||||||
|
[]subConfigEntry{{id: 7}},
|
||||||
|
[][]map[string]any{members},
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
var raw json_util.RawMessage
|
||||||
|
select {
|
||||||
|
case raw = <-done:
|
||||||
|
case <-time.After(30 * time.Second):
|
||||||
|
t.Fatalf("retagging %d same-protocol members did not finish in 30s", len(members))
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||||
|
t.Fatalf("balancer doc is not JSON: %v", err)
|
||||||
|
}
|
||||||
|
tags := docOutboundTags(doc)
|
||||||
|
want := []string{"bal-1-vless-3", "bal-1-vless", "bal-1-vless-2", "bal-1-vless-4", "bal-1-trojan", "bal-1-vless-5"}
|
||||||
|
if len(tags) < len(members) || !slices.Equal(tags[:len(want)], want) {
|
||||||
|
t.Fatalf("first member tags = %v, want %v", tags[:min(len(tags), len(want))], want)
|
||||||
|
}
|
||||||
|
if got, last := tags[len(members)-1], fmt.Sprintf("bal-1-vless-%d", len(members)-1); got != last {
|
||||||
|
t.Fatalf("last member tag = %q, want %q", got, last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user