This is an automated email from the ASF dual-hosted git repository.
Alanxtl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new c150ca471 perf(remoting): share one backend client per ZK/Nacos
cluster across registry, config-center and metadata-report (#3573) (#3635)
c150ca471 is described below
commit c150ca471ffe4003f76079e39bd2fdc55db1e37b
Author: 超級の新人 <[email protected]>
AuthorDate: Sun Aug 23 21:47:53 2026 +0800
perf(remoting): share one backend client per ZK/Nacos cluster across
registry, config-center and metadata-report (#3573) (#3635)
The gost client pool is keyed by name, but each role passed a different
key, so a single application held three sessions to the same cluster:
- zookeeper: metadata-report used a fixed name with share=false while
registry/config-center already share via url.Location; align it to
the same pool key and enable sharing (3 sessions -> 1).
- nacos: all roles set share=true but role-prefixed client names never
matched; derive the pool key from the connection identity (endpoint,
address, namespace, credentials) instead (3 clients -> 2, naming and
config clients are distinct SDK types).
etcd is left as a follow-up: gost has no pool for it and callers hold
the concrete client type.
Signed-off-by: chaojixinren <[email protected]>
---
metadata/report/zookeeper/report.go | 15 +++-
remoting/nacos/builder.go | 80 ++++++++++++++++++--
remoting/nacos/builder_test.go | 145 ++++++++++++++++++++++++++++++++++++
3 files changed, 231 insertions(+), 9 deletions(-)
diff --git a/metadata/report/zookeeper/report.go
b/metadata/report/zookeeper/report.go
index f2d02b8ed..0b35bfa7d 100644
--- a/metadata/report/zookeeper/report.go
+++ b/metadata/report/zookeeper/report.go
@@ -239,11 +239,20 @@ type zookeeperMetadataReportFactory struct{}
// CreateMetadataReport creates the zookeeper-based metadata report
implementation.
func (mf *zookeeperMetadataReportFactory) CreateMetadataReport(url
*common.URL) report.MetadataReport {
+ // Join the gost shared-client pool under the same key (url.Location)
that
+ // registry and config-center use via ValidateZookeeperClient, so all
roles
+ // pointing at the same cluster reuse one ZooKeeper session. The pool is
+ // reference-counted: Close only disconnects when the last user is gone.
+ // Prefer the timeout key the other pool users read (ConfigTimeoutKey)
so
+ // the pooled client's timeout does not depend on which role creates it
+ // first; fall back to the metadata-report's historical TimeoutKey with
+ // DefaultRegTimeout as the ultimate default (consistent with registry
and config-center).
+ timeout := url.GetParamDuration(constant.ConfigTimeoutKey,
url.GetParam(constant.TimeoutKey, constant.DefaultRegTimeout))
client, err := gxzookeeper.NewZookeeperClient(
- "zookeeperMetadataReport",
+ url.Location,
strings.Split(url.Location, ","),
- false,
-
gxzookeeper.WithZkTimeOut(url.GetParamDuration(constant.TimeoutKey, "15s")),
+ true,
+ gxzookeeper.WithZkTimeOut(timeout),
)
if err != nil {
panic(err)
diff --git a/remoting/nacos/builder.go b/remoting/nacos/builder.go
index 52082d634..7fb597682 100644
--- a/remoting/nacos/builder.go
+++ b/remoting/nacos/builder.go
@@ -21,6 +21,7 @@ import (
"net"
"strconv"
"strings"
+ "sync"
"time"
)
@@ -43,6 +44,77 @@ var (
newNacosConfigClient = nacosClient.NewNacosConfigClient
)
+// credentialIDs maps each distinct credential set to a small opaque id used
+// in pool keys. The credentials themselves stay in this process-local map and
+// never become part of the key, which may end up in logs.
+var (
+ credentialIDsMu sync.RWMutex
+ credentialIDs = make(map[string]string)
+)
+
+func credentialID(url *common.URL) string {
+ tuple := strings.Join([]string{
+ url.GetParam(constant.NacosUsername, ""),
+ url.GetParam(constant.NacosPassword, ""),
+ url.GetParam(constant.NacosAccessKey, ""),
+ url.GetParam(constant.NacosSecretKey, ""),
+ }, "\n")
+
+ // Try read lock first for the common case (credential already exists)
+ credentialIDsMu.RLock()
+ id, ok := credentialIDs[tuple]
+ credentialIDsMu.RUnlock()
+ if ok {
+ return id
+ }
+
+ // Need to create new credential ID, acquire write lock
+ credentialIDsMu.Lock()
+ defer credentialIDsMu.Unlock()
+ // Double-check in case another goroutine created it while we waited
+ id, ok = credentialIDs[tuple]
+ if !ok {
+ id = "cred" + strconv.Itoa(len(credentialIDs))
+ credentialIDs[tuple] = id
+ }
+ return id
+}
+
+// nacosClientPoolKey derives the gost client-pool key from the fields that
+// distinguish one nacos connection from another: server (endpoint/address),
+// path, namespace and the full credential set. Components pointing at the same
+// cluster (registry, config-center, metadata-report) resolve to the same key
+// and share one SDK client session instead of each opening its own.
+// Role-scoped client names must not be used as the key — they would defeat
+// the sharing.
+//
+// Note on url.Location with multiple addresses: url.Location may contain
+// multiple comma-separated addresses (e.g., "host1:8848,host2:8848") which
+// are parsed into separate ServerConfig entries. The full Location string
+// participates in the pool key, so different orderings or different server
+// lists will create separate pool keys. This is intentional: the order and
+// composition of servers affects client behavior, and configurations should
+// be consistent across components that intend to share a client.
+func nacosClientPoolKey(kind string, url *common.URL) string {
+ // GetNacosConfig ignores url.Location when an endpoint is set; mirror
+ // that here so URLs resolving to the same server set share one client.
+ server := url.GetParam(constant.NacosEndpoint, "")
+ if server == "" {
+ server = url.Location
+ }
+ // Clients authenticated differently must never collapse into one pool
+ // entry, so the full credential set participates via its opaque id.
+ // Include url.Path so that nacos://host:port/pathA and
nacos://host:port/pathB
+ // create separate clients (path becomes ContextPath in ServerConfig).
+ return strings.Join([]string{
+ "dubbo-nacos", kind,
+ server,
+ url.Path,
+ url.GetParam(constant.NacosNamespaceID, ""),
+ credentialID(url),
+ }, "|")
+}
+
// NewNacosConfigClientByUrl read the config from url and build an instance
func NewNacosConfigClientByUrl(url *common.URL)
(*nacosClient.NacosConfigClient, error) {
sc, cc, err := GetNacosConfig(url)
@@ -53,7 +125,7 @@ func NewNacosConfigClientByUrl(url *common.URL)
(*nacosClient.NacosConfigClient,
if len(clientName) <= 0 {
return nil, perrors.New("nacos client name must set")
}
- return newNacosConfigClient(clientName, true, sc, cc)
+ return newNacosConfigClient(nacosClientPoolKey("config", url), true,
sc, cc)
}
// GetNacosConfig will return the nacos config
@@ -130,9 +202,5 @@ func NewNacosClientByURL(url *common.URL)
(*nacosClient.NacosNamingClient, error
return nil, perrors.New("nacos client name must set")
}
logger.Infof("[Remoting][Nacos] new nacos client, config=%+v", scs)
- namespaceID := url.GetParam(constant.NacosNamespaceID, "")
- if len(namespaceID) > 0 {
- clientName += namespaceID
- }
- return newNacosNamingClient(clientName, true, scs, cc)
+ return newNacosNamingClient(nacosClientPoolKey("naming", url), true,
scs, cc)
}
diff --git a/remoting/nacos/builder_test.go b/remoting/nacos/builder_test.go
index 03702401f..1e8d213c1 100644
--- a/remoting/nacos/builder_test.go
+++ b/remoting/nacos/builder_test.go
@@ -29,6 +29,7 @@ import (
nacosConstant "github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
import (
@@ -247,3 +248,147 @@ func TestNewNacosConfigClientByUrlMissingClientName(t
*testing.T) {
assert.Nil(t, client)
assert.Error(t, err)
}
+
+func TestNacosClientPoolKeySharedAcrossRoles(t *testing.T) {
+ var captured []string
+ oldNewNacosConfigClient := newNacosConfigClient
+ newNacosConfigClient = func(name string, share bool, sc
[]nacosConstant.ServerConfig,
+ cc nacosConstant.ClientConfig) (*nacosClient.NacosConfigClient,
error) {
+ captured = append(captured, name)
+ assert.True(t, share)
+ return &nacosClient.NacosConfigClient{}, nil
+ }
+ t.Cleanup(func() {
+ newNacosConfigClient = oldNewNacosConfigClient
+ })
+
+ mkURL := func(clientName, namespace string) *common.URL {
+ m := url.Values{}
+ m.Set(constant.NacosNamespaceID, namespace)
+ m.Set(constant.ClientNameKey, clientName)
+ u, _ := common.NewURL("registry://test.nacos.io:80",
common.WithParams(m))
+ return u
+ }
+
+ // config-center and metadata-report carry role-scoped client names, but
+ // point at the same cluster: they must resolve to one pool key (#3573).
+ _, err :=
NewNacosConfigClientByUrl(mkURL("dubbo.config-center-nacos-test.nacos.io:80",
"ns1"))
+ require.NoError(t, err)
+ _, err =
NewNacosConfigClientByUrl(mkURL("dubbo.metadata-report-nacos-test.nacos.io:80",
"ns1"))
+ require.NoError(t, err)
+ assert.Equal(t, captured[0], captured[1])
+
+ // A different namespace is a different connection identity and must not
+ // share the client.
+ _, err =
NewNacosConfigClientByUrl(mkURL("dubbo.config-center-nacos-test.nacos.io:80",
"ns2"))
+ require.NoError(t, err)
+ assert.NotEqual(t, captured[0], captured[2])
+}
+
+func TestNacosClientPoolKeyCredentialAndEndpointIdentity(t *testing.T) {
+ // Clean up global credential IDs to avoid race conditions with other
tests
+ credentialIDsMu.Lock()
+ oldCredentialIDs := credentialIDs
+ credentialIDs = make(map[string]string)
+ credentialIDsMu.Unlock()
+ t.Cleanup(func() {
+ credentialIDsMu.Lock()
+ credentialIDs = oldCredentialIDs
+ credentialIDsMu.Unlock()
+ })
+
+ key := func(params map[string]string) string {
+ m := url.Values{}
+ for k, v := range params {
+ m.Set(k, v)
+ }
+ u, _ := common.NewURL("registry://test.nacos.io:80",
common.WithParams(m))
+ return nacosClientPoolKey("config", u)
+ }
+
+ userPass := map[string]string{constant.NacosUsername: "alice",
constant.NacosPassword: "s3cret-A"}
+
+ // Same server and same full credential set share one key.
+ assert.Equal(t, key(userPass),
key(map[string]string{constant.NacosUsername: "alice", constant.NacosPassword:
"s3cret-A"}))
+
+ // A different password (or secretKey) is a differently authenticated
+ // client and must never collapse into the same pool entry.
+ assert.NotEqual(t, key(userPass),
key(map[string]string{constant.NacosUsername: "alice", constant.NacosPassword:
"s3cret-B"}))
+ assert.NotEqual(t,
+ key(map[string]string{constant.NacosAccessKey: "AKID",
constant.NacosSecretKey: "SK-1"}),
+ key(map[string]string{constant.NacosAccessKey: "AKID",
constant.NacosSecretKey: "SK-2"}))
+
+ // Credentials must not appear in the key verbatim (keys can be logged).
+ assert.NotContains(t, key(userPass), "alice")
+ assert.NotContains(t, key(userPass), "s3cret-A")
+
+ // When an endpoint is set, GetNacosConfig ignores url.Location; the
pool
+ // key mirrors that, so differing (ignored) locations still share.
+ withEndpoint := func(location string) string {
+ m := url.Values{}
+ m.Set(constant.NacosEndpoint, "acm.aliyun.com")
+ u, _ := common.NewURL("registry://"+location,
common.WithParams(m))
+ return nacosClientPoolKey("config", u)
+ }
+ assert.Equal(t, withEndpoint("a.nacos.io:80"),
withEndpoint("b.nacos.io:80"))
+}
+
+func TestNacosNamingAndConfigPoolKeysDistinct(t *testing.T) {
+ var namingKey, configKey string
+ oldNaming := newNacosNamingClient
+ oldConfig := newNacosConfigClient
+ newNacosNamingClient = func(name string, share bool, sc
[]nacosConstant.ServerConfig,
+ cc nacosConstant.ClientConfig) (*nacosClient.NacosNamingClient,
error) {
+ namingKey = name
+ return &nacosClient.NacosNamingClient{}, nil
+ }
+ newNacosConfigClient = func(name string, share bool, sc
[]nacosConstant.ServerConfig,
+ cc nacosConstant.ClientConfig) (*nacosClient.NacosConfigClient,
error) {
+ configKey = name
+ return &nacosClient.NacosConfigClient{}, nil
+ }
+ t.Cleanup(func() {
+ newNacosNamingClient = oldNaming
+ newNacosConfigClient = oldConfig
+ })
+
+ regURL := getRegURL()
+ _, err := NewNacosClientByURL(regURL)
+ require.NoError(t, err)
+ _, err = NewNacosConfigClientByUrl(regURL)
+ require.NoError(t, err)
+ assert.NotEqual(t, namingKey, configKey)
+}
+
+func TestNacosClientPoolKeyDistinguishesByPath(t *testing.T) {
+ // Clean up global credential IDs to avoid race conditions with other
tests
+ credentialIDsMu.Lock()
+ oldCredentialIDs := credentialIDs
+ credentialIDs = make(map[string]string)
+ credentialIDsMu.Unlock()
+ t.Cleanup(func() {
+ credentialIDsMu.Lock()
+ credentialIDs = oldCredentialIDs
+ credentialIDsMu.Unlock()
+ })
+
+ key := func(path string) string {
+ m := url.Values{}
+ m.Set(constant.ClientNameKey, "test-client")
+ m.Set(constant.NacosNamespaceID, "default")
+ u, _ := common.NewURL("registry://localhost:8848"+path,
common.WithParams(m))
+ return nacosClientPoolKey("config", u)
+ }
+
+ // URLs with different paths must generate different pool keys (#3635)
+ keyNacos := key("/nacos")
+ keyCustom := key("/custom")
+ keyEmpty := key("")
+
+ assert.NotEqual(t, keyNacos, keyCustom, "different paths should create
different pool keys")
+ assert.NotEqual(t, keyNacos, keyEmpty, "path /nacos should differ from
empty path")
+ assert.NotEqual(t, keyCustom, keyEmpty, "path /custom should differ
from empty path")
+
+ // Same path should generate same key
+ assert.Equal(t, keyNacos, key("/nacos"), "same path should create same
pool key")
+}