This is an automated email from the ASF dual-hosted git repository.

AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git


The following commit(s) were added to refs/heads/develop by this push:
     new 6a498838d perf: cut gRPC/Triple/Dubbo proxy latency via connection 
reuse and descriptor caching (fix #820) (#1017)
6a498838d is described below

commit 6a498838d50d88fe978748e302c25d8d67e9f98a
Author: Tsukikage <[email protected]>
AuthorDate: Wed Aug 26 21:20:31 2026 -0700

    perf: cut gRPC/Triple/Dubbo proxy latency via connection reuse and 
descriptor caching (fix #820) (#1017)
    
    * perf: reduce gRPC, Triple, and Dubbo proxy latency
---
 pkg/adapter/dubboregistry/registry/registry.go     |   4 +
 .../dubboregistry/registry/registry_test.go        |  20 +
 pkg/client/dubbo/call_test.go                      |  24 +-
 pkg/client/dubbo/dubbo.go                          | 171 ++++---
 pkg/client/dubbo/typeconv.go                       |  30 +-
 pkg/client/dubbo/typeconv_test.go                  |   6 +
 pkg/cmd/gateway.go                                 |  27 +-
 pkg/cmd/gateway_test.go                            |  23 +
 pkg/common/constant/key.go                         |   1 +
 pkg/common/constant/pixiu.go                       |   1 +
 pkg/common/extension/filter/filter_chain.go        |  19 +
 pkg/common/extension/filter/filter_manager.go      | 146 +++++-
 pkg/common/extension/filter/filter_manager_test.go |  30 ++
 pkg/common/http/manager.go                         |  11 +
 pkg/filter/http/grpcproxy/connection_manager.go    | 456 +++++++++++++++++
 .../http/grpcproxy/connection_manager_test.go      | 553 +++++++++++++++++++++
 pkg/filter/http/grpcproxy/descriptor.go            | 150 +++++-
 pkg/filter/http/grpcproxy/grpc.go                  | 158 ++++--
 pkg/listener/http/http_listener.go                 | 145 +++++-
 pkg/listener/http/http_listener_test.go            |  94 ++++
 pkg/server/cluster_manager.go                      | 282 ++++++++++-
 pkg/server/cluster_manager_test.go                 |  17 +
 pkg/server/pixiu_start.go                          |   3 +
 pkg/tracing/driver.go                              |   2 +
 tools/benchmark/README.md                          |  58 ++-
 tools/benchmark/README_CN.md                       |  90 ++--
 .../benchmark/protocol/grpc/pixiu/conf/config.yaml |   2 +-
 .../protocol/triple/go-server/cmd/server.go        |   4 +-
 .../protocol/triple/pixiu/conf/config.yaml         |   4 +-
 tools/benchmark/test/dubbo_suite/dubbo_test.go     |   2 +-
 tools/benchmark/test/grpc_suite/grpc_test.go       |   4 +-
 tools/benchmark/test/test_tool.go                  |   5 +-
 .../test/triple_suite/proto_suite/proto_test.go    |  10 +-
 33 files changed, 2265 insertions(+), 287 deletions(-)

diff --git a/pkg/adapter/dubboregistry/registry/registry.go 
b/pkg/adapter/dubboregistry/registry/registry.go
index b6d79355c..0ab736d16 100644
--- a/pkg/adapter/dubboregistry/registry/registry.go
+++ b/pkg/adapter/dubboregistry/registry/registry.go
@@ -24,6 +24,7 @@ import (
 
 import (
        "dubbo.apache.org/dubbo-go/v3/common"
+       dubboConstant "dubbo.apache.org/dubbo-go/v3/common/constant"
 
        "github.com/pkg/errors"
 )
@@ -95,6 +96,9 @@ func GetRegistry(name string, regConfig model.Registry, 
listener common2.Registr
 // CreateAPIConfig returns router.API struct base on the input
 func CreateAPIConfig(urlPattern, location string, dboBackendConfig 
config.DubboBackendConfig, methodString string, mappingParams 
[]config.MappingParam) router.API {
        dboBackendConfig.Method = methodString
+       if strings.TrimSpace(dboBackendConfig.Serialization) == "" {
+               dboBackendConfig.Serialization = 
dubboConstant.Hessian2Serialization
+       }
        url := strings.Join([]string{urlPattern, methodString}, 
constant.PathSlash)
        var requestType string
        switch dboBackendConfig.Protocol {
diff --git a/pkg/adapter/dubboregistry/registry/registry_test.go 
b/pkg/adapter/dubboregistry/registry/registry_test.go
index c49fd9d61..867fbfb92 100644
--- a/pkg/adapter/dubboregistry/registry/registry_test.go
+++ b/pkg/adapter/dubboregistry/registry/registry_test.go
@@ -26,6 +26,10 @@ import (
        "github.com/stretchr/testify/require"
 )
 
+import (
+       "github.com/apache/dubbo-go-pixiu/pkg/config"
+)
+
 func TestParseDubboStringPreservesSerialization(t *testing.T) {
        backend, methods, location, err := 
ParseDubboString("tri://127.0.0.1:20001/org.apache.dubbogo.samples.api.Greeter?application=BDTService&interface=org.apache.dubbogo.samples.api.Greeter&methods=SayHello&serialization=hessian2")
        require.NoError(t, err)
@@ -35,3 +39,19 @@ func TestParseDubboStringPreservesSerialization(t 
*testing.T) {
        assert.Equal(t, []string{"SayHello"}, methods)
        assert.Equal(t, "127.0.0.1:20001", location)
 }
+
+func TestCreateAPIConfigDefaultsGenericSerialization(t *testing.T) {
+       api := CreateAPIConfig(
+               "/dubbo.io/org.apache.dubbo.sample.UserProvider",
+               "127.0.0.1:20000",
+               config.DubboBackendConfig{
+                       ApplicationName: "dubbo.io",
+                       Protocol:        "dubbo",
+                       Interface:       "org.apache.dubbo.sample.UserProvider",
+               },
+               "GetUser",
+               nil,
+       )
+
+       assert.Equal(t, "hessian2", api.Method.IntegrationRequest.Serialization)
+}
diff --git a/pkg/client/dubbo/call_test.go b/pkg/client/dubbo/call_test.go
index 668ff2e85..d06782248 100644
--- a/pkg/client/dubbo/call_test.go
+++ b/pkg/client/dubbo/call_test.go
@@ -35,6 +35,7 @@ import (
 
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/propagation"
+       "go.opentelemetry.io/otel/trace"
 )
 
 import (
@@ -145,14 +146,13 @@ func TestResolveFromOutboundRegistryMode(t *testing.T) {
 func TestPreparePayloadRejectsLengthMismatch(t *testing.T) {
        dc := NewDubboClient()
 
-       types, vals, finalValues, err := 
dc.preparePayload(&DubboOutboundRequest{
+       types, vals, err := dc.preparePayload(&DubboOutboundRequest{
                Arguments:  []any{"only-one"},
                ParamTypes: []string{"java.lang.String", "int"},
        })
 
        assert.Nil(t, types)
        assert.Nil(t, vals)
-       assert.Nil(t, finalValues)
        assert.EqualError(t, err, "arguments/paramTypes length mismatch: 1 vs 
2")
 }
 
@@ -230,6 +230,26 @@ func TestCallUsesOutboundOnly(t *testing.T) {
        assert.Equal(t, "ok", res)
 }
 
+func TestWithAttachmentsPropagatesExternalSpanWhenTracingDisabled(t 
*testing.T) {
+       restorePropagator(t, propagation.TraceContext{})
+       previousTracingEnabled := tracingEnabled.Load()
+       t.Cleanup(func() { SetTracingEnabled(previousTracingEnabled) })
+       SetTracingEnabled(false)
+
+       spanContext := trace.NewSpanContext(trace.SpanContextConfig{
+               TraceID:    [16]byte{1},
+               SpanID:     [8]byte{2},
+               TraceFlags: trace.FlagsSampled,
+               Remote:     true,
+       })
+       ctx := trace.ContextWithRemoteSpanContext(context.Background(), 
spanContext)
+       ctx = withAttachments(ctx, nil)
+
+       attachments, ok := 
ctx.Value(dubboConstant.AttachmentKey).(map[string]any)
+       require.True(t, ok)
+       require.Equal(t, 
"00-01000000000000000000000000000000-0200000000000000-01", 
attachments["traceparent"])
+}
+
 func TestCallAppliesTimeout(t *testing.T) {
        dc := NewDubboClient()
        restorePropagator(t, fixedPropagator{})
diff --git a/pkg/client/dubbo/dubbo.go b/pkg/client/dubbo/dubbo.go
index 8f06db3fb..ce32a1e4b 100644
--- a/pkg/client/dubbo/dubbo.go
+++ b/pkg/client/dubbo/dubbo.go
@@ -24,6 +24,7 @@ import (
        "strconv"
        "strings"
        "sync"
+       "sync/atomic"
        "time"
 )
 
@@ -42,6 +43,7 @@ import (
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        "go.opentelemetry.io/otel/propagation"
+       "go.opentelemetry.io/otel/trace"
 )
 
 import (
@@ -70,8 +72,17 @@ var (
                Owner:        "Dubbogo Pixiu",
                Environment:  "dev",
        }
+       // tracingEnabled records whether a tracer provider was configured at 
startup.
+       // When tracing is disabled, per-request span creation and JSON payload
+       // marshaling for span attributes are skipped.
+       tracingEnabled atomic.Bool
 )
 
+// SetTracingEnabled records whether a tracer provider has been configured.
+func SetTracingEnabled(enabled bool) {
+       tracingEnabled.Store(enabled)
+}
+
 // Client client to generic invoke dubbo
 type Client struct {
        lock               sync.RWMutex
@@ -79,6 +90,10 @@ type Client struct {
        dubboProxyConfig   *DubboProxyConfig
        registries         map[string]*global.RegistryConfig
        dubboClient        *dclient.Client
+       // immutable after Apply(): cached to avoid re-computing per request
+       registryIDs      []string
+       useNacosWarmup   bool
+       consumerDefaults resolvedConsumerDefaults
 }
 
 type resolvedConsumerDefaults struct {
@@ -173,6 +188,19 @@ func (dc *Client) Apply() error {
        }
        dc.registries = registries
 
+       // Cache immutable per-request resolution data
+       dc.registryIDs = make([]string, 0, len(registries))
+       useNacosWarmup := false
+       for id, registry := range registries {
+               dc.registryIDs = append(dc.registryIDs, id)
+               if registry != nil && registry.Protocol == "nacos" {
+                       useNacosWarmup = true
+               }
+       }
+       sort.Strings(dc.registryIDs)
+       dc.useNacosWarmup = useNacosWarmup
+       dc.consumerDefaults = dc.resolveGlobalConsumerDefaults()
+
        // Create dubbo client with registries and application config
        var err error
        dc.dubboClient, err = dclient.NewClient(
@@ -203,7 +231,7 @@ func (dc *Client) Call(ctx context.Context, req 
*DubboOutboundRequest) (any, err
        }
 
        spec := dc.resolveFromOutbound(req)
-       types, vals, finalValues, err := dc.preparePayload(req)
+       types, vals, err := dc.preparePayload(req)
        if err != nil {
                return nil, err
        }
@@ -221,19 +249,26 @@ func (dc *Client) Call(ctx context.Context, req 
*DubboOutboundRequest) (any, err
                defer cancel()
        }
 
-       spanCtx, span := otel.Tracer(traceNameDubbogoClient).Start(invokeCtx, 
spanNameDubbogoClient)
-       defer span.End()
-       span.SetAttributes(
-               attribute.String(spanTagMethod, req.Method),
-               attribute.StringSlice(spanTagType, types),
-               attribute.String(spanTagValues, string(finalValues)),
-       )
+       invokeCtx = withAttachments(invokeCtx, req.Attachments)
+       var span trace.Span
+       if tracingEnabled.Load() {
+               var spanCtx context.Context
+               spanCtx, span = 
otel.Tracer(traceNameDubbogoClient).Start(invokeCtx, spanNameDubbogoClient)
+               defer span.End()
+               span.SetAttributes(
+                       attribute.String(spanTagMethod, req.Method),
+                       attribute.StringSlice(spanTagType, types),
+                       attribute.String(spanTagValues, 
string(spanValues(vals))),
+               )
+               // carry the trace context as upstream attachments
+               invokeCtx = withAttachments(spanCtx, nil)
+       }
 
-       spanCtx = context.WithValue(spanCtx, constant.AttachmentKey, 
mergeOutboundAttachments(spanCtx, req.Attachments))
-       ctxWithAttachment := withAttachments(spanCtx)
-       rst, err := gs.Invoke(ctxWithAttachment, req.Method, types, vals)
+       rst, err := gs.Invoke(invokeCtx, req.Method, types, vals)
        if err != nil {
-               span.RecordError(err)
+               if span != nil {
+                       span.RecordError(err)
+               }
                return nil, err
        }
 
@@ -241,6 +276,14 @@ func (dc *Client) Call(ctx context.Context, req 
*DubboOutboundRequest) (any, err
        return rst, nil
 }
 
+func spanValues(vals []hessian.Object) []byte {
+       finalValues, err := json.Marshal(vals)
+       if err != nil {
+               return nil
+       }
+       return finalValues
+}
+
 func (dc *Client) resolveFromOutbound(req *DubboOutboundRequest) 
resolvedReferSpec {
        spec := resolvedReferSpec{
                Interface:              req.Service,
@@ -248,7 +291,11 @@ func (dc *Client) resolveFromOutbound(req 
*DubboOutboundRequest) resolvedReferSp
                Version:                req.Version,
                EffectiveProtocol:      req.Protocol,
                EffectiveSerialization: req.Serialization,
-               ConsumerDefaults:       dc.resolveGlobalConsumerDefaults(),
+               ConsumerDefaults:       dc.consumerDefaults,
+       }
+       if spec.ConsumerDefaults.Cluster == "" && 
spec.ConsumerDefaults.RequestTimeout == 0 {
+               // fallback for clients used before Apply()
+               spec.ConsumerDefaults = dc.resolveGlobalConsumerDefaults()
        }
 
        if strings.TrimSpace(req.Address) != "" {
@@ -257,19 +304,25 @@ func (dc *Client) resolveFromOutbound(req 
*DubboOutboundRequest) resolvedReferSp
                return spec
        }
 
-       registryIDs := make([]string, 0, len(dc.registries))
-       useNacosWarmup := false
-       for id, registry := range dc.registries {
-               registryIDs = append(registryIDs, id)
-               if registry != nil && registry.Protocol == "nacos" {
-                       useNacosWarmup = true
+       spec.Mode = "registry"
+       if dc.registryIDs == nil && len(dc.registries) > 0 {
+               // fallback for clients used before Apply()
+               registryIDs := make([]string, 0, len(dc.registries))
+               useNacosWarmup := false
+               for id, registry := range dc.registries {
+                       registryIDs = append(registryIDs, id)
+                       if registry != nil && registry.Protocol == "nacos" {
+                               useNacosWarmup = true
+                       }
                }
+               sort.Strings(registryIDs)
+               spec.RegistryIDs = registryIDs
+               spec.UseNacosWarmup = useNacosWarmup
+               return spec
        }
-       sort.Strings(registryIDs)
 
-       spec.Mode = "registry"
-       spec.RegistryIDs = registryIDs
-       spec.UseNacosWarmup = useNacosWarmup
+       spec.RegistryIDs = dc.registryIDs
+       spec.UseNacosWarmup = dc.useNacosWarmup
        return spec
 }
 
@@ -297,12 +350,12 @@ func (dc *Client) resolveGlobalConsumerDefaults() 
resolvedConsumerDefaults {
        return defaults
 }
 
-func (dc *Client) preparePayload(req *DubboOutboundRequest) ([]string, 
[]hessian.Object, []byte, error) {
+func (dc *Client) preparePayload(req *DubboOutboundRequest) ([]string, 
[]hessian.Object, error) {
        if len(req.Arguments) == 0 && len(req.ParamTypes) == 0 {
-               return []string{}, []hessian.Object{}, []byte("[]"), nil
+               return []string{}, []hessian.Object{}, nil
        }
        if len(req.Arguments) != len(req.ParamTypes) {
-               return nil, nil, nil, errors.Errorf("arguments/paramTypes 
length mismatch: %d vs %d", len(req.Arguments), len(req.ParamTypes))
+               return nil, nil, errors.Errorf("arguments/paramTypes length 
mismatch: %d vs %d", len(req.Arguments), len(req.ParamTypes))
        }
 
        types := append([]string(nil), req.ParamTypes...)
@@ -311,15 +364,19 @@ func (dc *Client) preparePayload(req 
*DubboOutboundRequest) ([]string, []hessian
                vals[i] = arg
        }
 
-       finalValues, err := json.Marshal(vals)
-       if err != nil {
-               return nil, nil, nil, errors.Wrap(err, "marshal dubbo 
arguments")
-       }
-
-       return types, vals, finalValues, nil
+       return types, vals, nil
 }
 
-func mergeOutboundAttachments(ctx context.Context, outbound map[string]any) 
map[string]any {
+func withAttachments(ctx context.Context, outbound map[string]any) 
context.Context {
+       // The internal tracing switch does not describe the caller's context. 
Keep
+       // the fast path only when there is no outbound state and no externally
+       // supplied span context that the global propagator must inject.
+       if !tracingEnabled.Load() &&
+               len(outbound) == 0 &&
+               ctx.Value(constant.AttachmentKey) == nil &&
+               !trace.SpanContextFromContext(ctx).IsValid() {
+               return ctx
+       }
        attachments := make(map[string]any, len(outbound))
        if attaRaw := ctx.Value(constant.AttachmentKey); attaRaw != nil {
                switch userAtta := attaRaw.(type) {
@@ -336,7 +393,15 @@ func mergeOutboundAttachments(ctx context.Context, 
outbound map[string]any) map[
        for key, val := range outbound {
                attachments[key] = val
        }
-       return attachments
+
+       carrier := propagation.MapCarrier{}
+       // Carry tracing headers as Dubbo attachments for the upstream 
invocation.
+       otel.GetTextMapPropagator().Inject(ctx, carrier)
+       for key, val := range carrier {
+               attachments[key] = val
+       }
+
+       return context.WithValue(ctx, constant.AttachmentKey, attachments)
 }
 
 func prepareInvokeContext(ctx context.Context, timeout time.Duration) 
(context.Context, context.CancelFunc) {
@@ -355,15 +420,6 @@ func (dc *Client) get(key string) *generic.GenericService {
        return dc.GenericServicePool[key]
 }
 
-func (dc *Client) check(key string) bool {
-       dc.lock.RLock()
-       defer dc.lock.RUnlock()
-       if _, ok := dc.GenericServicePool[key]; ok {
-               return true
-       }
-       return false
-}
-
 func (spec resolvedReferSpec) validate() error {
        switch spec.Mode {
        case "registry":
@@ -420,8 +476,8 @@ func (dc *Client) Get(spec resolvedReferSpec) 
(*generic.GenericService, error) {
        if err != nil {
                return nil, err
        }
-       if dc.check(key) {
-               return dc.get(key), nil
+       if service := dc.get(key); service != nil {
+               return service, nil
        }
 
        return dc.create(spec)
@@ -587,28 +643,3 @@ func loadBalanceReferenceOption(loadBalance string) 
dclient.ReferenceOption {
                return dclient.WithLoadBalance(loadBalance)
        }
 }
-
-func withAttachments(ctx context.Context) context.Context {
-       attachments := make(map[string]any)
-       if attaRaw := ctx.Value(constant.AttachmentKey); attaRaw != nil {
-               switch userAtta := attaRaw.(type) {
-               case map[string]any:
-                       for key, val := range userAtta {
-                               attachments[key] = val
-                       }
-               case map[string]string:
-                       for key, val := range userAtta {
-                               attachments[key] = val
-                       }
-               }
-       }
-
-       carrier := propagation.MapCarrier{}
-       // Carry tracing headers as Dubbo attachments for the upstream 
invocation.
-       otel.GetTextMapPropagator().Inject(ctx, carrier)
-       for key, val := range carrier {
-               attachments[key] = val
-       }
-
-       return context.WithValue(ctx, constant.AttachmentKey, attachments)
-}
diff --git a/pkg/client/dubbo/typeconv.go b/pkg/client/dubbo/typeconv.go
index a0a3934fa..276a7cf0e 100644
--- a/pkg/client/dubbo/typeconv.go
+++ b/pkg/client/dubbo/typeconv.go
@@ -98,25 +98,35 @@ func CoerceDirectInvokeValue(parameterType string, value 
any) (any, error) {
                if !ok {
                        return value, nil
                }
-
-               result := make([]any, len(items))
-               for i, item := range items {
-                       mapped, err := MapTypes(elementType, item)
-                       if err != nil {
-                               return nil, err
-                       }
-                       result[i] = mapped
-               }
-               return result, nil
+               return coerceScalarSlice(elementType, items)
        }
 
        if _, ok := cst.JTypeMapper[normalizeJavaTypeName(trimmed)]; ok {
+               // The legacy generic HTTP contract uses the element type for a
+               // collection argument, for example types="string" with
+               // values=[["003", "002"]]. Preserve that contract while still
+               // coercing each scalar element to the declared type.
+               if items, ok := value.([]any); ok {
+                       return coerceScalarSlice(trimmed, items)
+               }
                return MapTypes(trimmed, value)
        }
 
        return value, nil
 }
 
+func coerceScalarSlice(parameterType string, items []any) ([]any, error) {
+       result := make([]any, len(items))
+       for i, item := range items {
+               mapped, err := MapTypes(parameterType, item)
+               if err != nil {
+                       return nil, err
+               }
+               result[i] = mapped
+       }
+       return result, nil
+}
+
 // NormalizeReferenceProtocol canonicalizes dubbo reference protocols.
 func NormalizeReferenceProtocol(protocol string) string {
        normalized := strings.ToLower(strings.TrimSpace(protocol))
diff --git a/pkg/client/dubbo/typeconv_test.go 
b/pkg/client/dubbo/typeconv_test.go
index d1199c1d1..c091f85a4 100644
--- a/pkg/client/dubbo/typeconv_test.go
+++ b/pkg/client/dubbo/typeconv_test.go
@@ -68,6 +68,12 @@ func TestCoerceDirectInvokeValueWrapperFQNArray(t 
*testing.T) {
        assert.Equal(t, []any{1, 2}, val)
 }
 
+func TestCoerceDirectInvokeValueLegacyGenericArrayWithScalarType(t *testing.T) 
{
+       val, err := CoerceDirectInvokeValue("string", []any{"003", "002"})
+       require.NoError(t, err)
+       assert.Equal(t, []any{"003", "002"}, val)
+}
+
 func TestNormalizeReferenceProtocol(t *testing.T) {
        assert.Equal(t, "tri", NormalizeReferenceProtocol("triple"))
        assert.Equal(t, "tri", NormalizeReferenceProtocol("  TRI  "))
diff --git a/pkg/cmd/gateway.go b/pkg/cmd/gateway.go
index 2be1a3b17..7f203ca26 100644
--- a/pkg/cmd/gateway.go
+++ b/pkg/cmd/gateway.go
@@ -50,6 +50,9 @@ var (
        logFormat string
 
        limitCpus string
+       // port for the reload HTTP server, configurable so multiple
+       // gateway instances can run side by side without port conflicts
+       reloadPort int
 )
 
 var (
@@ -92,6 +95,7 @@ func init() {
        startGatewayCmd.PersistentFlags().StringVarP(&logLevel, 
constant.LogLevelKey, "l", os.Getenv(constant.EnvDubbogoPixiuLogLevel), 
"dubbogo pixiu log level, trace|debug|info|warning|error|critical")
        startGatewayCmd.PersistentFlags().StringVarP(&limitCpus, 
constant.LimitCpusKey, "m", os.Getenv(constant.EnvDubbogoPixiuLimitCpus), 
"dubbogo pixiu schedule threads count")
        startGatewayCmd.PersistentFlags().StringVarP(&logFormat, 
constant.LogFormatKey, "f", os.Getenv(constant.EnvDubbogoPixiuLogFormat), 
"dubbogo pixiu log format, currently useless")
+       startGatewayCmd.PersistentFlags().IntVarP(&reloadPort, 
constant.ReloadPortKey, "r", constant.DefaultReloadPort, "dubbogo pixiu reload 
HTTP server port")
 
        GatewayCmd.AddCommand(startGatewayCmd)
 }
@@ -122,8 +126,8 @@ func (d *DefaultDeployer) initialize() error {
        // Set config path for hot reload
        hotreload.SetConfigPath(configPath)
 
-       // Start HTTP reload endpoint on port 18380
-       if err := hotreload.StartReloadServer(18380, ""); err != nil {
+       // Start HTTP reload endpoint, port configurable via --reload-port
+       if err := hotreload.StartReloadServer(reloadPort, ""); err != nil {
                logger.Warnf("[startGatewayCmd] failed to start reload server: 
%s", err.Error())
        }
 
@@ -169,17 +173,16 @@ func initDefaultValue() {
 
 // initLog initializes logger according to log config file and log level
 func initLog() error {
-       err := logger.InitLog(logConfigPath)
-       if err != nil {
-               // cause `logger.InitLog` already handle init failed, so just 
use logger to log
-               return err
-       }
-
+       initErr := logger.InitLog(logConfigPath)
        lvl := logger.ParseLogLevel(logLevel)
-       if ok := logger.SetLoggerLevel(lvl); !ok {
-               err = fmt.Errorf("set logLevel failed")
-       }
-       return err
+       if ok := logger.SetLoggerLevel(lvl); !ok && initErr == nil {
+               return fmt.Errorf("set logLevel failed")
+       }
+       // Apply the requested level even when the optional log config file is
+       // missing or invalid. InitLog already installs the default logger in 
that
+       // case, and leaving it at development/debug level adds avoidable 
hot-path
+       // logging during gateway startup and request handling.
+       return initErr
 }
 
 func initLogWithConfig(boot *model.Bootstrap) {
diff --git a/pkg/cmd/gateway_test.go b/pkg/cmd/gateway_test.go
index 8e1821bd3..2a5d31c92 100644
--- a/pkg/cmd/gateway_test.go
+++ b/pkg/cmd/gateway_test.go
@@ -26,6 +26,12 @@ import (
        "github.com/spf13/cobra"
 
        "github.com/stretchr/testify/assert"
+
+       "go.uber.org/zap"
+)
+
+import (
+       "github.com/apache/dubbo-go-pixiu/pkg/logger"
 )
 
 // MockDeployer for testing
@@ -198,6 +204,23 @@ func TestInitDefaultValue(t *testing.T) {
        // logFormat can be empty as DefaultLogFormat is ""
 }
 
+func TestInitLogAppliesLevelWhenConfigIsMissing(t *testing.T) {
+       previousPath := logConfigPath
+       previousLevel := logLevel
+       defer func() {
+               logConfigPath = previousPath
+               logLevel = previousLevel
+               logger.InitLogger(nil)
+       }()
+
+       logConfigPath = "/tmp/dubbo-go-pixiu-missing-log-config.yml"
+       logLevel = "info"
+
+       err := initLog()
+       assert.Error(t, err)
+       assert.False(t, 
logger.GetLogger().Desugar().Core().Enabled(zap.DebugLevel))
+}
+
 func TestGatewayCmdAddedToRootCmd(t *testing.T) {
        // Check that GatewayCmd is properly initialized
        assert.NotNil(t, GatewayCmd)
diff --git a/pkg/common/constant/key.go b/pkg/common/constant/key.go
index 721361481..0e8dac662 100644
--- a/pkg/common/constant/key.go
+++ b/pkg/common/constant/key.go
@@ -80,6 +80,7 @@ const (
        LogLevelKey      = "log-level"
        LimitCpusKey     = "limit-cpus"
        LogFormatKey     = "log-format"
+       ReloadPortKey    = "reload-port"
 )
 
 const (
diff --git a/pkg/common/constant/pixiu.go b/pkg/common/constant/pixiu.go
index 464e5ed8d..f8a35107d 100644
--- a/pkg/common/constant/pixiu.go
+++ b/pkg/common/constant/pixiu.go
@@ -73,4 +73,5 @@ const (
        DefaultLogLevel      = "info"
        DefaultLimitCpus     = "0"
        DefaultLogFormat     = ""
+       DefaultReloadPort    = 18380
 )
diff --git a/pkg/common/extension/filter/filter_chain.go 
b/pkg/common/extension/filter/filter_chain.go
index 07d8108da..c4fe08daf 100644
--- a/pkg/common/extension/filter/filter_chain.go
+++ b/pkg/common/extension/filter/filter_chain.go
@@ -17,6 +17,10 @@
 
 package filter
 
+import (
+       "sync"
+)
+
 import (
        "github.com/apache/dubbo-go-pixiu/pkg/context/http"
 )
@@ -36,6 +40,21 @@ type defaultFilterChain struct {
 
        encodeFilters      []HttpEncodeFilter
        encodeFiltersIndex int
+       release            func()
+       releaseOnce        sync.Once
+}
+
+func (c *defaultFilterChain) setRelease(release func()) {
+       c.release = release
+}
+
+// Release releases resources leased by this request's filter chain.
+func (c *defaultFilterChain) Release() {
+       c.releaseOnce.Do(func() {
+               if c.release != nil {
+                       c.release()
+               }
+       })
 }
 
 func NewDefaultFilterChain() FilterChain {
diff --git a/pkg/common/extension/filter/filter_manager.go 
b/pkg/common/extension/filter/filter_manager.go
index 3a87b2dd4..4129b5a1f 100644
--- a/pkg/common/extension/filter/filter_manager.go
+++ b/pkg/common/extension/filter/filter_manager.go
@@ -40,7 +40,11 @@ type FilterManager struct {
        filtersArray  []*HttpFilterFactory
        filterConfigs []*model.HTTPFilter
 
-       mu sync.RWMutex
+       mu          sync.RWMutex
+       lifecycleMu sync.Mutex
+       factoryRefs map[*HttpFilterFactory]int
+       retired     map[*HttpFilterFactory]bool
+       closed      bool
 }
 
 // NewFilterManager create filter manager
@@ -57,7 +61,14 @@ func NewEmptyFilterManager() *FilterManager {
 func (fm *FilterManager) CreateFilterChain(ctx *http.HttpContext) FilterChain {
        chain := NewDefaultFilterChain()
 
-       for _, f := range fm.GetFactory() {
+       fm.mu.RLock()
+       defer fm.mu.RUnlock()
+       factories := append([]*HttpFilterFactory(nil), fm.filtersArray...)
+       fm.leaseFactories(factories)
+       chain.(*defaultFilterChain).setRelease(func() {
+               fm.releaseFactories(factories)
+       })
+       for _, f := range factories {
                _ = (*f).PrepareFilterChain(ctx, chain)
        }
        return chain
@@ -68,7 +79,7 @@ func (fm *FilterManager) GetFactory() []*HttpFilterFactory {
        fm.mu.RLock()
        defer fm.mu.RUnlock()
 
-       return fm.filtersArray
+       return append([]*HttpFilterFactory(nil), fm.filtersArray...)
 }
 
 // Load the filter from config
@@ -78,22 +89,130 @@ func (fm *FilterManager) Load() {
 
 // ReLoad filter configs
 func (fm *FilterManager) ReLoad(filters []*model.HTTPFilter) {
+       fm.mu.RLock()
+       closed := fm.closed
+       fm.mu.RUnlock()
+       if closed {
+               return
+       }
+
        tmp := make(map[string]HttpFilterFactory)
-       filtersArray := make([]*HttpFilterFactory, len(filters))
-       for i, f := range filters {
+       filtersArray := make([]*HttpFilterFactory, 0, len(filters))
+       for _, f := range filters {
                apply, err := fm.Apply(f.Name, f.Config)
                if err != nil {
                        logger.Errorf("apply [%s] init fail, %s", f.Name, 
err.Error())
+                       continue
                }
                tmp[f.Name] = apply
-               filtersArray[i] = &apply
+               filtersArray = append(filtersArray, &apply)
        }
        // avoid filter inconsistency
        fm.mu.Lock()
-       defer fm.mu.Unlock()
-
+       if fm.closed {
+               fm.mu.Unlock()
+               fm.closeAndLog(filtersArray)
+               return
+       }
+       oldFilters := fm.filtersArray
        fm.filters = tmp
        fm.filtersArray = filtersArray
+       ready := fm.retireFactories(oldFilters)
+       fm.mu.Unlock()
+       fm.closeAndLog(ready)
+}
+
+func (fm *FilterManager) leaseFactories(factories []*HttpFilterFactory) {
+       fm.lifecycleMu.Lock()
+       defer fm.lifecycleMu.Unlock()
+       if fm.factoryRefs == nil {
+               fm.factoryRefs = make(map[*HttpFilterFactory]int)
+       }
+       for _, factory := range factories {
+               if factory != nil {
+                       fm.factoryRefs[factory]++
+               }
+       }
+}
+
+func (fm *FilterManager) retireFactories(factories []*HttpFilterFactory) 
[]*HttpFilterFactory {
+       fm.lifecycleMu.Lock()
+       defer fm.lifecycleMu.Unlock()
+       if fm.retired == nil {
+               fm.retired = make(map[*HttpFilterFactory]bool)
+       }
+       var ready []*HttpFilterFactory
+       for _, factory := range factories {
+               if factory == nil {
+                       continue
+               }
+               fm.retired[factory] = true
+               if fm.factoryRefs[factory] == 0 {
+                       delete(fm.retired, factory)
+                       ready = append(ready, factory)
+               }
+       }
+       return ready
+}
+
+func (fm *FilterManager) releaseFactories(factories []*HttpFilterFactory) {
+       fm.lifecycleMu.Lock()
+       var ready []*HttpFilterFactory
+       for _, factory := range factories {
+               if factory == nil {
+                       continue
+               }
+               fm.factoryRefs[factory]--
+               if fm.factoryRefs[factory] == 0 {
+                       delete(fm.factoryRefs, factory)
+                       if fm.retired[factory] {
+                               delete(fm.retired, factory)
+                               ready = append(ready, factory)
+                       }
+               }
+       }
+       fm.lifecycleMu.Unlock()
+       fm.closeAndLog(ready)
+}
+
+func (fm *FilterManager) closeAndLog(factories []*HttpFilterFactory) {
+       if err := fm.closeFactories(factories); err != nil {
+               logger.Warnf("failed to close retired HTTP filter factory: %v", 
err)
+       }
+}
+
+func (fm *FilterManager) closeFactories(factories []*HttpFilterFactory) error {
+       var firstErr error
+       for _, factory := range factories {
+               if factory == nil || *factory == nil {
+                       continue
+               }
+               closer, ok := (*factory).(interface{ Close() error })
+               if !ok {
+                       continue
+               }
+               if err := closer.Close(); err != nil && firstErr == nil {
+                       firstErr = err
+               }
+       }
+       return firstErr
+}
+
+// Close releases resources held by HTTP filter factories that expose an
+// optional Close method. Keeping this optional preserves compatibility with
+// existing HTTP filter implementations.
+func (fm *FilterManager) Close() error {
+       fm.mu.Lock()
+       if fm.closed {
+               fm.mu.Unlock()
+               return nil
+       }
+       fm.closed = true
+       factories := fm.filtersArray
+       fm.filtersArray = nil
+       ready := fm.retireFactories(factories)
+       fm.mu.Unlock()
+       return fm.closeFactories(ready)
 }
 
 // Apply return a new filter factory by name & conf
@@ -108,16 +227,27 @@ func (fm *FilterManager) Apply(name string, conf 
map[string]any) (HttpFilterFact
        if err != nil {
                return nil, errors.New("plugin create filter error")
        }
+       if filter == nil {
+               return nil, errors.New("plugin returned nil filter factory")
+       }
+       closeFilter := func() {
+               if closer, ok := filter.(interface{ Close() error }); ok {
+                       _ = closer.Close()
+               }
+       }
 
        factoryConf := filter.Config()
        if err := yaml.ParseConfig(factoryConf, conf); err != nil {
+               closeFilter()
                return nil, errors.Wrap(err, "config error")
        }
        if err = defaults.Set(factoryConf); err != nil {
+               closeFilter()
                return nil, err
        }
        err = filter.Apply()
        if err != nil {
+               closeFilter()
                return nil, errors.Wrap(err, "create fail")
        }
        return filter, nil
diff --git a/pkg/common/extension/filter/filter_manager_test.go 
b/pkg/common/extension/filter/filter_manager_test.go
index e0bce7020..d729fc685 100644
--- a/pkg/common/extension/filter/filter_manager_test.go
+++ b/pkg/common/extension/filter/filter_manager_test.go
@@ -19,6 +19,7 @@ package filter
 
 import (
        "fmt"
+       "sync"
        "testing"
 )
 
@@ -144,6 +145,35 @@ func TestLoad(t *testing.T) {
        runFilter(t, fm, filtersConf)
 }
 
+func TestReloadSkipsFailedFactoriesAndDoesNotReopenAfterClose(t *testing.T) {
+       fm := NewEmptyFilterManager()
+       fm.ReLoad([]*model.HTTPFilter{{Name: "missing-filter"}})
+       assert.Empty(t, fm.GetFactory())
+       assert.NotPanics(t, func() { 
fm.CreateFilterChain(&contexthttp.HttpContext{}) })
+
+       fm.ReLoad([]*model.HTTPFilter{{Name: DEMO}})
+       assert.Len(t, fm.GetFactory(), 1)
+       assert.NoError(t, fm.Close())
+       fm.ReLoad([]*model.HTTPFilter{{Name: DEMO}})
+       assert.Empty(t, fm.GetFactory())
+}
+
+func TestReloadAndCloseDoNotReopenManager(t *testing.T) {
+       fm := NewEmptyFilterManager()
+       filters := []*model.HTTPFilter{{Name: DEMO}}
+       var wg sync.WaitGroup
+       wg.Go(func() {
+               for range 20 {
+                       fm.ReLoad(filters)
+               }
+       })
+       wg.Go(func() {
+               assert.NoError(t, fm.Close())
+       })
+       wg.Wait()
+       assert.Empty(t, fm.GetFactory())
+}
+
 func runFilter(t *testing.T, fm *FilterManager, filtersConf 
[]*model.HTTPFilter) {
        fm.ReLoad(filtersConf)
 
diff --git a/pkg/common/http/manager.go b/pkg/common/http/manager.go
index 5177bcad4..7eaf5c651 100644
--- a/pkg/common/http/manager.go
+++ b/pkg/common/http/manager.go
@@ -92,6 +92,14 @@ func (hcm *HttpConnectionManager) Handle(hc 
*pch.HttpContext) error {
        return nil
 }
 
+// Close releases resources held by the HTTP filter chain.
+func (hcm *HttpConnectionManager) Close() error {
+       if hcm.filterManager == nil {
+               return nil
+       }
+       return hcm.filterManager.Close()
+}
+
 func (hcm *HttpConnectionManager) ServeHTTP(w stdHttp.ResponseWriter, r 
*stdHttp.Request) {
        hc := hcm.pool.Get().(*pch.HttpContext)
        defer hcm.pool.Put(hc)
@@ -109,6 +117,9 @@ func (hcm *HttpConnectionManager) ServeHTTP(w 
stdHttp.ResponseWriter, r *stdHttp
 // handleHTTPRequest handle http request
 func (hcm *HttpConnectionManager) handleHTTPRequest(c *pch.HttpContext) {
        filterChain := hcm.filterManager.CreateFilterChain(c)
+       if releaser, ok := filterChain.(interface{ Release() }); ok {
+               defer releaser.Release()
+       }
 
        // recover any err when filterChain run
        defer func() {
diff --git a/pkg/filter/http/grpcproxy/connection_manager.go 
b/pkg/filter/http/grpcproxy/connection_manager.go
new file mode 100644
index 000000000..e6c7796ee
--- /dev/null
+++ b/pkg/filter/http/grpcproxy/connection_manager.go
@@ -0,0 +1,456 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package grpcproxy
+
+import (
+       "container/list"
+       "context"
+       "fmt"
+       "sync"
+       "time"
+)
+
+import (
+       "golang.org/x/sync/singleflight"
+
+       "google.golang.org/grpc"
+       "google.golang.org/grpc/connectivity"
+       "google.golang.org/grpc/credentials/insecure"
+)
+
+const (
+       defaultGRPCDialTimeout = 5 * time.Second
+       // Endpoint tombstones only need to cover recently delivered lifecycle
+       // events. Active connections and in-flight dials are pinned separately 
and
+       // are never evicted by this bound.
+       maxEndpointTombstones = 1024
+)
+
+type grpcConnectionDialer func(context.Context, string) (*grpc.ClientConn, 
error)
+
+// grpcConnectionManager owns long-lived backend connections for the HTTP gRPC
+// proxy. A grpc.ClientConn is safe for concurrent use and multiplexes calls
+// over HTTP/2, so a sync.Pool is both unnecessary and incorrect here.
+type grpcConnectionManager struct {
+       connections sync.Map
+       creates     singleflight.Group
+       dial        grpcConnectionDialer
+       dialTimeout time.Duration
+       onRemove    func(*grpc.ClientConn)
+
+       mu                  sync.Mutex
+       closed              bool
+       endpointGenerations map[string]uint64
+       endpointEventVers   map[string]uint64
+       endpointRefs        map[string]int
+       endpointRemoved     map[string]bool
+       endpointTombstones  map[string]*list.Element
+       tombstoneOrder      *list.List
+       // endpointPresent is an optional authoritative snapshot check used 
after
+       // bounded tombstone metadata has been evicted. It is intentionally kept
+       // outside the manager so direct-endpoint users do not need a cluster 
store.
+       endpointPresent func(key, endpoint string) bool
+}
+
+func newGRPCConnectionManager() *grpcConnectionManager {
+       return &grpcConnectionManager{
+               dial:                dialGRPCConnection,
+               dialTimeout:         defaultGRPCDialTimeout,
+               endpointGenerations: make(map[string]uint64),
+               endpointEventVers:   make(map[string]uint64),
+               endpointRefs:        make(map[string]int),
+               endpointRemoved:     make(map[string]bool),
+               endpointTombstones:  make(map[string]*list.Element),
+               tombstoneOrder:      list.New(),
+       }
+}
+
+func (m *grpcConnectionManager) initEndpointStateLocked() {
+       if m.endpointGenerations == nil {
+               m.endpointGenerations = make(map[string]uint64)
+       }
+       if m.endpointEventVers == nil {
+               m.endpointEventVers = make(map[string]uint64)
+       }
+       if m.endpointRefs == nil {
+               m.endpointRefs = make(map[string]int)
+       }
+       if m.endpointRemoved == nil {
+               m.endpointRemoved = make(map[string]bool)
+       }
+       if m.endpointTombstones == nil {
+               m.endpointTombstones = make(map[string]*list.Element)
+       }
+       if m.tombstoneOrder == nil {
+               m.tombstoneOrder = list.New()
+       }
+}
+
+func (m *grpcConnectionManager) discardEndpointTombstoneLocked(key string) {
+       if element, ok := m.endpointTombstones[key]; ok {
+               m.tombstoneOrder.Remove(element)
+               delete(m.endpointTombstones, key)
+       }
+}
+
+func (m *grpcConnectionManager) discardEndpointStateLocked(key string) {
+       m.discardEndpointTombstoneLocked(key)
+       delete(m.endpointGenerations, key)
+       delete(m.endpointEventVers, key)
+       delete(m.endpointRemoved, key)
+}
+
+func (m *grpcConnectionManager) rememberEndpointTombstoneLocked(key string) {
+       m.initEndpointStateLocked()
+       if element, ok := m.endpointTombstones[key]; ok {
+               m.tombstoneOrder.MoveToFront(element)
+               return
+       }
+       element := m.tombstoneOrder.PushFront(key)
+       m.endpointTombstones[key] = element
+
+       for len(m.endpointTombstones) > maxEndpointTombstones {
+               var evict *list.Element
+               for element := m.tombstoneOrder.Back(); element != nil; element 
= element.Prev() {
+                       candidate := element.Value.(string)
+                       if m.endpointRefs[candidate] == 0 && 
!m.hasConnection(candidate) {
+                               evict = element
+                               break
+                       }
+               }
+               if evict == nil {
+                       return
+               }
+               candidate := evict.Value.(string)
+               m.tombstoneOrder.Remove(evict)
+               delete(m.endpointTombstones, candidate)
+               delete(m.endpointGenerations, candidate)
+               delete(m.endpointEventVers, candidate)
+               delete(m.endpointRemoved, candidate)
+       }
+}
+
+func (m *grpcConnectionManager) hasConnection(key string) bool {
+       _, ok := m.connections.Load(key)
+       return ok
+}
+
+func (m *grpcConnectionManager) pinEndpoint(key string) (uint64, error) {
+       m.mu.Lock()
+       defer m.mu.Unlock()
+       m.initEndpointStateLocked()
+       if m.closed {
+               return 0, fmt.Errorf("grpc connection manager is closed")
+       }
+       if m.endpointRemoved[key] {
+               return 0, fmt.Errorf("grpc endpoint was removed")
+       }
+       m.endpointRefs[key]++
+       return m.endpointGenerations[key], nil
+}
+
+func (m *grpcConnectionManager) isEndpointPresent(key, endpoint string) bool {
+       if m.endpointPresent == nil {
+               return true
+       }
+       return m.endpointPresent(key, endpoint)
+}
+
+func (m *grpcConnectionManager) unpinEndpoint(key string) {
+       m.mu.Lock()
+       defer m.mu.Unlock()
+       if m.endpointRefs[key] > 1 {
+               m.endpointRefs[key]--
+               return
+       }
+       delete(m.endpointRefs, key)
+       if m.endpointRemoved[key] && !m.hasConnection(key) {
+               m.rememberEndpointTombstoneLocked(key)
+       } else if !m.endpointRemoved[key] && m.endpointEventVers[key] == 0 && 
!m.hasConnection(key) {
+               // Get may create a temporary generation entry for a direct 
endpoint
+               // before the cluster manager has delivered a lifecycle event. 
Do not
+               // retain that request-only state after a failed or canceled 
dial.
+               m.discardEndpointStateLocked(key)
+       }
+}
+
+func (m *grpcConnectionManager) finalizeRemovedEndpoint(key string) {
+       m.mu.Lock()
+       defer m.mu.Unlock()
+       if m.endpointRemoved[key] && m.endpointRefs[key] == 0 && 
!m.hasConnection(key) {
+               m.rememberEndpointTombstoneLocked(key)
+       }
+}
+
+func dialGRPCConnection(ctx context.Context, endpoint string) 
(*grpc.ClientConn, error) {
+       return grpc.DialContext( //nolint:staticcheck // SA1019: the context is 
required to enforce the dial timeout.
+               ctx,
+               endpoint,
+               grpc.WithTransportCredentials(insecure.NewCredentials()),
+       )
+}
+
+func (m *grpcConnectionManager) Get(ctx context.Context, key, endpoint string) 
(*grpc.ClientConn, error) {
+       if key == "" || endpoint == "" {
+               return nil, fmt.Errorf("grpc connection key and endpoint must 
not be empty")
+       }
+       if ctx == nil {
+               ctx = context.Background()
+       }
+       if !m.isEndpointPresent(key, endpoint) {
+               return nil, fmt.Errorf("grpc endpoint was removed")
+       }
+
+       if conn, ok := m.loadHealthy(key); ok {
+               if !m.isEndpointPresent(key, endpoint) {
+                       m.remove(key, conn)
+                       return nil, fmt.Errorf("grpc endpoint was removed")
+               }
+               return conn, nil
+       }
+
+       endpointGeneration, err := m.pinEndpoint(key)
+       if err != nil {
+               return nil, err
+       }
+       createKey := fmt.Sprintf("%s\x00%d", key, endpointGeneration)
+       result := m.creates.DoChan(createKey, func() (any, error) {
+               if !m.isEndpointPresent(key, endpoint) {
+                       return nil, fmt.Errorf("grpc endpoint was removed")
+               }
+               m.mu.Lock()
+               currentGeneration := m.endpointGenerations[key]
+               removed := m.endpointRemoved[key]
+               m.mu.Unlock()
+               if removed || currentGeneration != endpointGeneration {
+                       return nil, fmt.Errorf("grpc endpoint was removed while 
connecting")
+               }
+               if conn, ok := m.loadHealthy(key); ok {
+                       if !m.isEndpointPresent(key, endpoint) {
+                               m.remove(key, conn)
+                               return nil, fmt.Errorf("grpc endpoint was 
removed")
+                       }
+                       return conn, nil
+               }
+
+               m.mu.Lock()
+               if m.closed {
+                       m.mu.Unlock()
+                       return nil, fmt.Errorf("grpc connection manager is 
closed")
+               }
+               dial := m.dial
+               dialTimeout := m.dialTimeout
+               m.mu.Unlock()
+
+               dialCtx, cancel := context.WithTimeout(context.Background(), 
dialTimeout)
+               defer cancel()
+               conn, err := dial(dialCtx, endpoint)
+               if err != nil {
+                       return nil, err
+               }
+
+               m.mu.Lock()
+               closed := m.closed
+               removed = m.endpointRemoved[key] || m.endpointGenerations[key] 
!= endpointGeneration
+               if !closed && !removed {
+                       m.connections.Store(key, conn)
+               }
+               m.mu.Unlock()
+               if closed || removed || !m.isEndpointPresent(key, endpoint) {
+                       removedFromManager := false
+                       if !closed && !removed {
+                               removedFromManager = m.remove(key, conn)
+                       }
+                       if !removedFromManager {
+                               _ = conn.Close()
+                       }
+                       if closed {
+                               return nil, fmt.Errorf("grpc connection manager 
closed while dialing")
+                       }
+                       return nil, fmt.Errorf("grpc endpoint was removed while 
connecting")
+               }
+
+               return conn, nil
+       })
+
+       select {
+       case <-ctx.Done():
+               go func() {
+                       <-result
+                       m.unpinEndpoint(key)
+               }()
+               return nil, ctx.Err()
+       case result := <-result:
+               m.unpinEndpoint(key)
+               if result.Err != nil {
+                       return nil, result.Err
+               }
+               return result.Val.(*grpc.ClientConn), nil
+       }
+}
+
+func (m *grpcConnectionManager) loadHealthy(key string) (*grpc.ClientConn, 
bool) {
+       m.mu.Lock()
+       removed := m.endpointRemoved[key]
+       m.mu.Unlock()
+       if removed {
+               return nil, false
+       }
+       value, ok := m.connections.Load(key)
+       if !ok {
+               return nil, false
+       }
+
+       conn, ok := value.(*grpc.ClientConn)
+       if ok && m.isHealthy(conn) {
+               return conn, true
+       }
+       if ok {
+               m.remove(key, conn)
+       }
+       return nil, false
+}
+
+func (m *grpcConnectionManager) isHealthy(conn *grpc.ClientConn) bool {
+       if conn == nil {
+               return false
+       }
+       state := conn.GetState()
+       return state != connectivity.Shutdown
+}
+
+// Invalidate removes a connection only when its transport is known to be
+// unusable. Application-level RPC errors must not cause healthy connections to
+// churn.
+func (m *grpcConnectionManager) Invalidate(key string, conn *grpc.ClientConn) {
+       if conn == nil || m.isHealthy(conn) {
+               return
+       }
+       m.remove(key, conn)
+}
+
+// RemoveEndpoint closes and removes the connection for a deleted endpoint.
+func (m *grpcConnectionManager) RemoveEndpoint(clusterName, endpoint string) {
+       key := grpcConnectionKey(clusterName, endpoint)
+       m.mu.Lock()
+       m.initEndpointStateLocked()
+       if m.closed {
+               m.mu.Unlock()
+               return
+       }
+       m.endpointGenerations[key]++
+       m.endpointRemoved[key] = true
+       m.mu.Unlock()
+       value, ok := m.connections.Load(key)
+       if !ok {
+               m.finalizeRemovedEndpoint(key)
+               return
+       }
+       conn, ok := value.(*grpc.ClientConn)
+       if ok {
+               m.remove(key, conn)
+       }
+       m.finalizeRemovedEndpoint(key)
+}
+
+// UpdateEndpointState applies an ordered endpoint lifecycle event. Additions
+// advance the same generation as removals so a delayed removal cannot delete
+// a connection created after the endpoint was re-added.
+func (m *grpcConnectionManager) UpdateEndpointState(clusterName, endpoint 
string, present bool, eventVersion uint64) {
+       key := grpcConnectionKey(clusterName, endpoint)
+       m.mu.Lock()
+       m.initEndpointStateLocked()
+       if m.closed {
+               m.mu.Unlock()
+               return
+       }
+       if eventVersion <= m.endpointEventVers[key] {
+               m.mu.Unlock()
+               return
+       }
+       m.endpointEventVers[key] = eventVersion
+       m.endpointGenerations[key]++
+       m.endpointRemoved[key] = !present
+       if present {
+               m.discardEndpointTombstoneLocked(key)
+       }
+       var conn *grpc.ClientConn
+       if value, ok := m.connections.Load(key); ok {
+               conn, _ = value.(*grpc.ClientConn)
+       }
+       m.mu.Unlock()
+       if conn != nil {
+               // A present event starts a new endpoint incarnation. Remove any
+               // connection left over from the previous incarnation before a 
new Get
+               // can reuse it.
+               m.remove(key, conn)
+       }
+       if !present {
+               m.finalizeRemovedEndpoint(key)
+       }
+}
+
+func (m *grpcConnectionManager) remove(key string, expected *grpc.ClientConn) 
bool {
+       value, ok := m.connections.Load(key)
+       if !ok || value != expected {
+               return false
+       }
+       if m.connections.CompareAndDelete(key, expected) {
+               _ = expected.Close()
+               if m.onRemove != nil {
+                       m.onRemove(expected)
+               }
+               return true
+       }
+       return false
+}
+
+func (m *grpcConnectionManager) Close() error {
+       m.mu.Lock()
+       if m.closed {
+               m.mu.Unlock()
+               return nil
+       }
+       m.closed = true
+       m.endpointGenerations = nil
+       m.endpointEventVers = nil
+       m.endpointRefs = nil
+       m.endpointRemoved = nil
+       m.endpointTombstones = nil
+       m.tombstoneOrder = nil
+       var connections []*grpc.ClientConn
+       m.connections.Range(func(key, value any) bool {
+               m.connections.Delete(key)
+               if conn, ok := value.(*grpc.ClientConn); ok {
+                       connections = append(connections, conn)
+               }
+               return true
+       })
+       m.mu.Unlock()
+
+       var firstErr error
+       for _, conn := range connections {
+               if err := conn.Close(); err != nil && firstErr == nil {
+                       firstErr = err
+               }
+               if m.onRemove != nil {
+                       m.onRemove(conn)
+               }
+       }
+       return firstErr
+}
diff --git a/pkg/filter/http/grpcproxy/connection_manager_test.go 
b/pkg/filter/http/grpcproxy/connection_manager_test.go
new file mode 100644
index 000000000..905fc067c
--- /dev/null
+++ b/pkg/filter/http/grpcproxy/connection_manager_test.go
@@ -0,0 +1,553 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package grpcproxy
+
+import (
+       "context"
+       "fmt"
+       "net"
+       "sync"
+       "sync/atomic"
+       "testing"
+       "time"
+)
+
+import (
+       "github.com/jhump/protoreflect/desc"            //nolint:staticcheck // 
legacy descriptor API used by grpcproxy.
+       "github.com/jhump/protoreflect/desc/protoparse" //nolint:staticcheck // 
legacy parser used by grpcproxy.
+
+       "github.com/stretchr/testify/require"
+
+       "google.golang.org/grpc"
+       "google.golang.org/grpc/codes"
+       "google.golang.org/grpc/connectivity"
+       "google.golang.org/grpc/credentials/insecure"
+       reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
+       "google.golang.org/grpc/status"
+)
+
+import (
+       ct "github.com/apache/dubbo-go-pixiu/pkg/context"
+)
+
+func startTestGRPCServer(t *testing.T) string {
+       t.Helper()
+
+       listener, err := net.Listen("tcp", "127.0.0.1:0")
+       require.NoError(t, err)
+
+       server := grpc.NewServer()
+       go func() {
+               _ = server.Serve(listener)
+       }()
+       t.Cleanup(func() {
+               server.Stop()
+               _ = listener.Close()
+       })
+       return listener.Addr().String()
+}
+
+func testConnectionManager(t *testing.T, calls *atomic.Int32) 
*grpcConnectionManager {
+       t.Helper()
+       return &grpcConnectionManager{
+               dial: func(ctx context.Context, endpoint string) 
(*grpc.ClientConn, error) {
+                       calls.Add(1)
+                       return grpc.DialContext(ctx, endpoint, 
grpc.WithTransportCredentials(insecure.NewCredentials())) //nolint:staticcheck 
// the test verifies context-bounded dialing.
+               },
+               dialTimeout: time.Second,
+       }
+}
+
+func TestGRPCConnectionManagerSharesConcurrentConnection(t *testing.T) {
+       endpoint := startTestGRPCServer(t)
+       var dialCalls atomic.Int32
+       manager := testConnectionManager(t, &dialCalls)
+       t.Cleanup(func() { require.NoError(t, manager.Close()) })
+
+       const requests = 64
+       connections := make([]*grpc.ClientConn, requests)
+       errs := make(chan error, requests)
+       var wg sync.WaitGroup
+       for i := 0; i < requests; i++ {
+               wg.Add(1)
+               go func(index int) {
+                       defer wg.Done()
+                       conn, err := manager.Get(context.Background(), 
grpcConnectionKey("cluster", endpoint), endpoint)
+                       if err != nil {
+                               errs <- err
+                               return
+                       }
+                       connections[index] = conn
+               }(i)
+       }
+       wg.Wait()
+       close(errs)
+       for err := range errs {
+               require.NoError(t, err)
+       }
+
+       require.Equal(t, int32(1), dialCalls.Load())
+       for _, conn := range connections[1:] {
+               require.Same(t, connections[0], conn)
+       }
+}
+
+func TestGRPCConnectionManagerRecreatesUnhealthyConnection(t *testing.T) {
+       endpoint := startTestGRPCServer(t)
+       var dialCalls atomic.Int32
+       manager := testConnectionManager(t, &dialCalls)
+       t.Cleanup(func() { require.NoError(t, manager.Close()) })
+
+       key := grpcConnectionKey("cluster", endpoint)
+       first, err := manager.Get(context.Background(), key, endpoint)
+       require.NoError(t, err)
+       require.NoError(t, first.Close())
+       manager.Invalidate(key, first)
+
+       second, err := manager.Get(context.Background(), key, endpoint)
+       require.NoError(t, err)
+       require.NotSame(t, first, second)
+       require.Equal(t, int32(2), dialCalls.Load())
+}
+
+func TestGRPCConnectionManagerRetainsTransientFailureConnection(t *testing.T) {
+       listener, err := net.Listen("tcp", "127.0.0.1:0")
+       require.NoError(t, err)
+       endpoint := listener.Addr().String()
+       require.NoError(t, listener.Close())
+
+       conn, err := grpc.NewClient("passthrough:///"+endpoint, 
grpc.WithTransportCredentials(insecure.NewCredentials()))
+       require.NoError(t, err)
+       conn.Connect()
+       require.Eventually(t, func() bool {
+               return conn.GetState() == connectivity.TransientFailure
+       }, time.Second, time.Millisecond)
+
+       manager := newGRPCConnectionManager()
+       key := grpcConnectionKey("cluster", endpoint)
+       manager.connections.Store(key, conn)
+       t.Cleanup(func() { require.NoError(t, manager.Close()) })
+
+       got, ok := manager.loadHealthy(key)
+       require.True(t, ok)
+       require.Same(t, conn, got)
+       manager.Invalidate(key, conn)
+       _, stillCached := manager.connections.Load(key)
+       require.True(t, stillCached)
+}
+
+func TestGRPCConnectionManagerHonorsCallerTimeoutWhileCreating(t *testing.T) {
+       var dialCalls atomic.Int32
+       manager := &grpcConnectionManager{
+               dial: func(ctx context.Context, _ string) (*grpc.ClientConn, 
error) {
+                       dialCalls.Add(1)
+                       <-ctx.Done()
+                       return nil, ctx.Err()
+               },
+               dialTimeout: time.Second,
+       }
+
+       ctx, cancel := context.WithTimeout(context.Background(), 
10*time.Millisecond)
+       defer cancel()
+       _, err := manager.Get(ctx, "cluster\x00endpoint", "endpoint")
+       require.ErrorIs(t, err, context.DeadlineExceeded)
+       require.Equal(t, int32(1), dialCalls.Load())
+       require.NoError(t, manager.Close())
+}
+
+func TestGRPCConnectionManagerDoesNotPublishRemovedEndpointAfterDial(t 
*testing.T) {
+       endpoint := startTestGRPCServer(t)
+       dialStarted := make(chan struct{})
+       releaseDial := make(chan struct{})
+       manager := &grpcConnectionManager{
+               dial: func(ctx context.Context, endpoint string) 
(*grpc.ClientConn, error) {
+                       close(dialStarted)
+                       <-releaseDial
+                       return grpc.DialContext(ctx, endpoint, 
grpc.WithTransportCredentials(insecure.NewCredentials())) //nolint:staticcheck 
// the test verifies endpoint lifecycle.
+               },
+               dialTimeout: time.Second,
+       }
+       t.Cleanup(func() { require.NoError(t, manager.Close()) })
+
+       key := grpcConnectionKey("cluster", endpoint)
+       result := make(chan error, 1)
+       go func() {
+               _, err := manager.Get(context.Background(), key, endpoint)
+               result <- err
+       }()
+       <-dialStarted
+       manager.RemoveEndpoint("cluster", endpoint)
+       close(releaseDial)
+
+       require.Error(t, <-result)
+       _, ok := manager.connections.Load(key)
+       require.False(t, ok, "a removed endpoint must not be published after 
dialing")
+}
+
+func TestGRPCConnectionManagerBoundsEndpointTombstones(t *testing.T) {
+       manager := newGRPCConnectionManager()
+       t.Cleanup(func() { require.NoError(t, manager.Close()) })
+
+       const churn = maxEndpointTombstones * 4
+       for i := 0; i < churn; i++ {
+               manager.UpdateEndpointState(
+                       "cluster",
+                       fmt.Sprintf("127.0.0.1:%d", 20000+i),
+                       false,
+                       uint64(i+1),
+               )
+       }
+
+       manager.mu.Lock()
+       defer manager.mu.Unlock()
+       require.LessOrEqual(t, len(manager.endpointGenerations), 
maxEndpointTombstones)
+       require.LessOrEqual(t, len(manager.endpointEventVers), 
maxEndpointTombstones)
+       require.LessOrEqual(t, len(manager.endpointTombstones), 
maxEndpointTombstones)
+}
+
+func TestGRPCConnectionManagerRejectsEvictedRemovedEndpointFromSnapshot(t 
*testing.T) {
+       endpoint := "127.0.0.1:20000"
+       current := make(map[string]bool)
+       var currentMu sync.Mutex
+       var dialCalls atomic.Int32
+       manager := &grpcConnectionManager{
+               dial: func(context.Context, string) (*grpc.ClientConn, error) {
+                       dialCalls.Add(1)
+                       return nil, fmt.Errorf("unexpected dial")
+               },
+               dialTimeout: time.Second,
+               endpointPresent: func(_, address string) bool {
+                       currentMu.Lock()
+                       defer currentMu.Unlock()
+                       return current[address]
+               },
+       }
+       t.Cleanup(func() { require.NoError(t, manager.Close()) })
+
+       current[endpoint] = true
+       manager.UpdateEndpointState("cluster", endpoint, true, 1)
+       current[endpoint] = false
+       manager.UpdateEndpointState("cluster", endpoint, false, 2)
+       for i := 0; i < maxEndpointTombstones+1; i++ {
+               address := fmt.Sprintf("127.0.0.1:%d", 21000+i)
+               manager.UpdateEndpointState("cluster", address, false, 
uint64(i+3))
+       }
+
+       _, err := manager.Get(context.Background(), 
grpcConnectionKey("cluster", endpoint), endpoint)
+       require.EqualError(t, err, "grpc endpoint was removed")
+       require.Zero(t, dialCalls.Load())
+}
+
+func TestGRPCConnectionManagerReclaimsRequestOnlyEndpointState(t *testing.T) {
+       manager := &grpcConnectionManager{
+               dial: func(context.Context, string) (*grpc.ClientConn, error) {
+                       return nil, fmt.Errorf("dial failed")
+               },
+               dialTimeout: time.Second,
+       }
+       t.Cleanup(func() { require.NoError(t, manager.Close()) })
+
+       _, err := manager.Get(context.Background(), "cluster\x00endpoint", 
"endpoint")
+       require.EqualError(t, err, "dial failed")
+
+       manager.mu.Lock()
+       defer manager.mu.Unlock()
+       require.Empty(t, manager.endpointGenerations)
+       require.Empty(t, manager.endpointEventVers)
+}
+
+func TestGRPCConnectionManagerClosePreventsNewConnections(t *testing.T) {
+       var dialCalls atomic.Int32
+       manager := testConnectionManager(t, &dialCalls)
+       require.NoError(t, manager.Close())
+
+       _, err := manager.Get(context.Background(), 
grpcConnectionKey("cluster", "endpoint"), "endpoint")
+       require.Error(t, err)
+       require.Equal(t, int32(0), dialCalls.Load())
+}
+
+func TestDescriptorSourceHonorsRequestTimeout(t *testing.T) {
+       listener, err := net.Listen("tcp", "127.0.0.1:0")
+       require.NoError(t, err)
+
+       server := grpc.NewServer()
+       reflectpb.RegisterServerReflectionServer(server, 
&blockingReflectionServer{})
+       go func() {
+               _ = server.Serve(listener)
+       }()
+       t.Cleanup(func() {
+               server.Stop()
+               _ = listener.Close()
+       })
+
+       conn, err := grpc.NewClient(listener.Addr().String(), 
grpc.WithTransportCredentials(insecure.NewCredentials()))
+       require.NoError(t, err)
+       t.Cleanup(func() { require.NoError(t, conn.Close()) })
+
+       descriptor := &Descriptor{}
+       ctx, cancel := context.WithTimeout(context.Background(), 
100*time.Millisecond)
+       defer cancel()
+       source, err := descriptor.getServerDescriptorSourceCtx(
+               context.WithValue(ctx, ct.ContextKey(GrpcClientConnKey), conn),
+               &Config{},
+       )
+       require.NoError(t, err)
+
+       _, err = source.FindSymbol("test.SlowService")
+       require.Error(t, err)
+       if st, ok := status.FromError(err); ok {
+               require.Equal(t, codes.DeadlineExceeded, st.Code())
+       } else {
+               require.ErrorIs(t, err, context.DeadlineExceeded)
+       }
+}
+
+type blockingReflectionServer struct{}
+
+func (blockingReflectionServer) ServerReflectionInfo(stream 
reflectpb.ServerReflection_ServerReflectionInfoServer) error {
+       <-stream.Context().Done()
+       return stream.Context().Err()
+}
+
+type countingDescriptorSource struct {
+       descriptor desc.Descriptor
+       findCalls  atomic.Int32
+}
+
+func (s *countingDescriptorSource) ListServices() ([]string, error) {
+       return []string{"test.Greeter"}, nil
+}
+
+func (s *countingDescriptorSource) FindSymbol(string) (desc.Descriptor, error) 
{
+       s.findCalls.Add(1)
+       return s.descriptor, nil
+}
+
+func (s *countingDescriptorSource) AllExtensionsForType(string) 
([]*desc.FieldDescriptor, error) {
+       return nil, nil
+}
+
+func TestDescriptorCachesMethodLookupPerConnection(t *testing.T) {
+       files, err := (protoparse.Parser{
+               Accessor: protoparse.FileContentsFromMap(map[string]string{
+                       "test.proto": `syntax = "proto3";
+package test;
+
+service Greeter {
+  rpc Hello(Request) returns (Response);
+}
+
+message Request {}
+message Response {}
+`,
+               }),
+       }).ParseFiles("test.proto")
+       require.NoError(t, err)
+
+       source := &countingDescriptorSource{descriptor: 
files[0].FindSymbol("test.Greeter")}
+       descriptor := &Descriptor{}
+       conn := &grpc.ClientConn{}
+       _, err = descriptor.getMethodDescriptor(source, conn, "test.Greeter", 
"Hello")
+       require.NoError(t, err)
+
+       const requests = 32
+       methods := make([]*desc.MethodDescriptor, requests)
+       errs := make([]error, requests)
+       var wg sync.WaitGroup
+       for i := 0; i < requests; i++ {
+               wg.Add(1)
+               go func(index int) {
+                       defer wg.Done()
+                       methods[index], errs[index] = 
descriptor.getMethodDescriptor(source, conn, "test.Greeter", "Hello")
+               }(i)
+       }
+       wg.Wait()
+
+       for index, method := range methods {
+               require.NoError(t, errs[index])
+               require.NotNil(t, method)
+               require.Equal(t, "Hello", method.GetName())
+       }
+       require.Equal(t, int32(1), source.findCalls.Load())
+}
+
+func TestDescriptorLookupDoesNotShareRequestBoundSource(t *testing.T) {
+       files, err := (protoparse.Parser{
+               Accessor: protoparse.FileContentsFromMap(map[string]string{
+                       "test.proto": `syntax = "proto3";
+package test;
+
+service Greeter {
+  rpc Hello(Request) returns (Response);
+}
+
+message Request {}
+message Response {}
+`,
+               }),
+       }).ParseFiles("test.proto")
+       require.NoError(t, err)
+
+       firstSource := &blockingDescriptorSource{
+               descriptor: files[0].FindSymbol("test.Greeter"),
+               started:    make(chan struct{}),
+               release:    make(chan struct{}),
+       }
+       secondSource := &countingDescriptorSource{descriptor: 
files[0].FindSymbol("test.Greeter")}
+       descriptor := &Descriptor{}
+       conn := &grpc.ClientConn{}
+
+       firstResult := make(chan error, 1)
+       go func() {
+               _, lookupErr := descriptor.getMethodDescriptor(firstSource, 
conn, "test.Greeter", "Hello")
+               firstResult <- lookupErr
+       }()
+       <-firstSource.started
+
+       secondResult := make(chan error, 1)
+       go func() {
+               _, lookupErr := descriptor.getMethodDescriptor(secondSource, 
conn, "test.Greeter", "Hello")
+               secondResult <- lookupErr
+       }()
+
+       select {
+       case lookupErr := <-secondResult:
+               require.NoError(t, lookupErr)
+       case <-time.After(200 * time.Millisecond):
+               close(firstSource.release)
+               t.Fatal("request-bound descriptor lookup was shared with a 
canceled/slow caller")
+       }
+
+       close(firstSource.release)
+       require.NoError(t, <-firstResult)
+}
+
+type blockingDescriptorSource struct {
+       descriptor desc.Descriptor
+       started    chan struct{}
+       release    chan struct{}
+}
+
+func (s *blockingDescriptorSource) ListServices() ([]string, error) {
+       return []string{"test.Greeter"}, nil
+}
+
+func (s *blockingDescriptorSource) FindSymbol(string) (desc.Descriptor, error) 
{
+       close(s.started)
+       <-s.release
+       return s.descriptor, nil
+}
+
+func (s *blockingDescriptorSource) AllExtensionsForType(string) 
([]*desc.FieldDescriptor, error) {
+       return nil, nil
+}
+
+func TestDescriptorRemovalForOneConnectionDoesNotInvalidateAnother(t 
*testing.T) {
+       files, err := (protoparse.Parser{
+               Accessor: protoparse.FileContentsFromMap(map[string]string{
+                       "test.proto": `syntax = "proto3";
+package test;
+
+service Greeter {
+  rpc Hello(Request) returns (Response);
+}
+
+message Request {}
+message Response {}
+`,
+               }),
+       }).ParseFiles("test.proto")
+       require.NoError(t, err)
+
+       source := &blockingDescriptorSource{
+               descriptor: files[0].FindSymbol("test.Greeter"),
+               started:    make(chan struct{}),
+               release:    make(chan struct{}),
+       }
+       descriptor := &Descriptor{}
+       connA := &grpc.ClientConn{}
+       connB := &grpc.ClientConn{}
+       _, err = descriptor.getMethodDescriptor(
+               &countingDescriptorSource{descriptor: 
files[0].FindSymbol("test.Greeter")},
+               connB, "test.Greeter", "Hello",
+       )
+       require.NoError(t, err)
+
+       result := make(chan error, 1)
+       go func() {
+               _, lookupErr := descriptor.getMethodDescriptor(source, connA, 
"test.Greeter", "Hello")
+               result <- lookupErr
+       }()
+       <-source.started
+
+       descriptor.removeConnection(connB)
+       close(source.release)
+
+       require.NoError(t, <-result)
+}
+
+func TestDescriptorRemovalSeparatesNewLookupOnSameConnection(t *testing.T) {
+       files, err := (protoparse.Parser{
+               Accessor: protoparse.FileContentsFromMap(map[string]string{
+                       "test.proto": `syntax = "proto3";
+package test;
+
+service Greeter {
+  rpc Hello(Request) returns (Response);
+}
+
+message Request {}
+message Response {}
+`,
+               }),
+       }).ParseFiles("test.proto")
+       require.NoError(t, err)
+
+       oldSource := &blockingDescriptorSource{
+               descriptor: files[0].FindSymbol("test.Greeter"),
+               started:    make(chan struct{}),
+               release:    make(chan struct{}),
+       }
+       newSource := &countingDescriptorSource{descriptor: 
files[0].FindSymbol("test.Greeter")}
+       descriptor := &Descriptor{}
+       conn := &grpc.ClientConn{}
+
+       oldResult := make(chan error, 1)
+       go func() {
+               _, lookupErr := descriptor.getMethodDescriptor(oldSource, conn, 
"test.Greeter", "Hello")
+               oldResult <- lookupErr
+       }()
+       <-oldSource.started
+       descriptor.removeConnection(conn)
+
+       newResult := make(chan error, 1)
+       go func() {
+               _, lookupErr := descriptor.getMethodDescriptor(newSource, conn, 
"test.Greeter", "Hello")
+               newResult <- lookupErr
+       }()
+       select {
+       case lookupErr := <-newResult:
+               require.NoError(t, lookupErr)
+       case <-time.After(200 * time.Millisecond):
+               close(oldSource.release)
+               t.Fatal("new lookup joined the removed connection's in-flight 
lookup")
+       }
+
+       close(oldSource.release)
+       require.Error(t, <-oldResult)
+}
diff --git a/pkg/filter/http/grpcproxy/descriptor.go 
b/pkg/filter/http/grpcproxy/descriptor.go
index 40cc8e7aa..495b371d6 100644
--- a/pkg/filter/http/grpcproxy/descriptor.go
+++ b/pkg/filter/http/grpcproxy/descriptor.go
@@ -23,6 +23,7 @@ import (
        "os"
        "path/filepath"
        "strings"
+       "sync"
 )
 
 import (
@@ -42,7 +43,27 @@ import (
 )
 
 type Descriptor struct {
-       fileSource *fileSource
+       fileSource  *fileSource
+       methodMu    sync.RWMutex
+       methodDescs map[*grpc.ClientConn]map[string]*desc.MethodDescriptor
+       // connectionStates invalidates only lookups for the connection that
+       // was removed. closeGeneration invalidates all in-flight lookups on 
close.
+       connectionStates  map[*grpc.ClientConn]*descriptorConnectionState
+       closeGeneration   uint64
+       nextStateSequence uint64
+       closed            bool
+}
+
+type descriptorConnectionState struct {
+       generation uint64
+}
+
+type serviceNotExposedError struct {
+       service string
+}
+
+func (e *serviceNotExposedError) Error() string {
+       return fmt.Sprintf("service not exposed: %s", e.service)
 }
 
 func (dr *Descriptor) GetCurrentDescriptorSource(ctx context.Context) 
(DescriptorSource, error) {
@@ -90,7 +111,7 @@ func (dr *Descriptor) getDescriptorCompose(ctx 
context.Context, cfg *Config) (De
 
        cs := &compositeSource{}
        cs.reflection, err = dr.getServerDescriptorSourceCtx(ctx, cfg)
-       cs.file = dr.fileSource
+       cs.file = dr.getFileSource()
 
        return cs, err
 }
@@ -117,24 +138,121 @@ func (dr *Descriptor) 
getServerDescriptorSourceCtx(refCtx context.Context, cfg *
        default:
                err = errors.Errorf("found a value of type %s, which is not 
*grpc.ClientConn, ", t)
        }
-       return &serverSource{client: grpcreflect.NewClient(refCtx, 
reflectpb.NewServerReflectionClient(cc))}, err
+       if err != nil {
+               return nil, err
+       }
+
+       // The reflection client is created per lookup and bound to the request
+       // context so every remote reflection RPC honors the request timeout.
+       // It must not be cached connection-scoped: grpcreflect reuses the root
+       // context for every RPC, and a cached client would lose the deadline 
and
+       // keep the per-request timeout from applying. The method descriptor
+       // cache in getMethodDescriptor below is what avoids repeating the
+       // reflection RPC after the first lookup.
+       return &serverSource{client: grpcreflect.NewClientV1Alpha(refCtx, 
reflectpb.NewServerReflectionClient(cc))}, nil
 }
 
 // nolint
 func (dr *Descriptor) getServerDescriptorSource(refCtx context.Context, cc 
*grpc.ClientConn) DescriptorSource {
-       return &serverSource{client: grpcreflect.NewClient(refCtx, 
reflectpb.NewServerReflectionClient(cc))}
+       return &serverSource{client: grpcreflect.NewClientV1Alpha(refCtx, 
reflectpb.NewServerReflectionClient(cc))}
 }
 
-func (dr *Descriptor) getFileDescriptorCompose(ctx context.Context, cfg 
*Config) (DescriptorSource, error) {
-       if dr.fileSource == nil {
-               dr.initFileDescriptorSource(cfg)
+func (dr *Descriptor) removeConnection(cc *grpc.ClientConn) {
+       if cc == nil {
+               return
+       }
+       dr.methodMu.Lock()
+       delete(dr.methodDescs, cc)
+       state := dr.connectionStates[cc]
+       if state != nil {
+               dr.nextStateSequence++
+               state.generation = dr.nextStateSequence
+               // Keep the state alive for any in-flight lookup, but do not 
retain the
+               // removed connection in the descriptor's long-lived map.
+               delete(dr.connectionStates, cc)
        }
-       return dr.fileSource, nil
+       dr.methodMu.Unlock()
 }
 
-func (dr *Descriptor) initFileDescriptorSource(cfg *Config) *Descriptor {
+func (dr *Descriptor) Close() {
+       dr.methodMu.Lock()
+       dr.closed = true
+       dr.methodDescs = nil
+       dr.connectionStates = nil
+       dr.closeGeneration++
+       dr.methodMu.Unlock()
+}
+
+func (dr *Descriptor) getMethodDescriptor(source DescriptorSource, cc 
*grpc.ClientConn, service, method string) (*desc.MethodDescriptor, error) {
+       key := service + "\x00" + method
+       dr.methodMu.Lock()
+       if dr.closed {
+               dr.methodMu.Unlock()
+               return nil, errors.New("descriptor is closed")
+       }
+       if methods := dr.methodDescs[cc]; methods != nil {
+               if descriptor, ok := methods[key]; ok {
+                       dr.methodMu.Unlock()
+                       return descriptor, nil
+               }
+       }
+       state := dr.connectionStates[cc]
+       if state == nil {
+               if dr.connectionStates == nil {
+                       dr.connectionStates = 
make(map[*grpc.ClientConn]*descriptorConnectionState)
+               }
+               dr.nextStateSequence++
+               state = &descriptorConnectionState{generation: 
dr.nextStateSequence}
+               dr.connectionStates[cc] = state
+       }
+       connectionGeneration := state.generation
+       closeGeneration := dr.closeGeneration
+       dr.methodMu.Unlock()
+       // Do not singleflight this lookup across requests. DescriptorSource 
carries
+       // the request context used by server reflection, so sharing the first
+       // caller's source would let its timeout or cancellation fail other 
callers.
+       // The per-connection method cache below still removes repeated 
reflection
+       // calls after the first successful lookup.
+       dscp, err := source.FindSymbol(service)
+       if err != nil {
+               return nil, err
+       }
+       svcDesc, ok := dscp.(*desc.ServiceDescriptor)
+       if !ok {
+               return nil, &serviceNotExposedError{service: service}
+       }
+       descriptor := svcDesc.FindMethodByName(method)
+       if descriptor == nil {
+               return nil, fmt.Errorf("method not found: %s/%s", service, 
method)
+       }
 
-       if dr.fileSource != nil {
+       dr.methodMu.Lock()
+       defer dr.methodMu.Unlock()
+       if dr.closed || dr.closeGeneration != closeGeneration || 
state.generation != connectionGeneration {
+               return nil, errors.New("descriptor cache invalidated")
+       }
+       if methods := dr.methodDescs[cc]; methods != nil {
+               if cached, ok := methods[key]; ok {
+                       return cached, nil
+               }
+       }
+       if dr.methodDescs == nil {
+               dr.methodDescs = 
make(map[*grpc.ClientConn]map[string]*desc.MethodDescriptor)
+       }
+       if dr.methodDescs[cc] == nil {
+               dr.methodDescs[cc] = make(map[string]*desc.MethodDescriptor)
+       }
+       dr.methodDescs[cc][key] = descriptor
+       return descriptor, nil
+}
+
+func (dr *Descriptor) getFileDescriptorCompose(ctx context.Context, cfg 
*Config) (DescriptorSource, error) {
+       dr.initFileDescriptorSource(cfg)
+       return dr.getFileSource(), nil
+}
+
+func (dr *Descriptor) initFileDescriptorSource(cfg *Config) *Descriptor {
+       if dr.getFileSource() != nil {
                return dr
        }
 
@@ -145,11 +263,21 @@ func (dr *Descriptor) initFileDescriptorSource(cfg 
*Config) *Descriptor {
                return dr
        }
 
-       dr.fileSource = descriptor
+       dr.methodMu.Lock()
+       if dr.fileSource == nil {
+               dr.fileSource = descriptor
+       }
+       dr.methodMu.Unlock()
 
        return dr
 }
 
+func (dr *Descriptor) getFileSource() *fileSource {
+       dr.methodMu.RLock()
+       defer dr.methodMu.RUnlock()
+       return dr.fileSource
+}
+
 func loadFileSource(gc *Config) (*fileSource, error) {
 
        var fsrc fileSource
diff --git a/pkg/filter/http/grpcproxy/grpc.go 
b/pkg/filter/http/grpcproxy/grpc.go
index ca43e3177..9148dc862 100644
--- a/pkg/filter/http/grpcproxy/grpc.go
+++ b/pkg/filter/http/grpcproxy/grpc.go
@@ -40,7 +40,6 @@ import (
 
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
-       "google.golang.org/grpc/credentials/insecure"
        "google.golang.org/grpc/metadata"
        "google.golang.org/grpc/status"
 )
@@ -90,21 +89,24 @@ type (
                cfg *Config
                // grpc descriptor source factory
                descriptor *Descriptor
-               // hold grpc.ClientConns, key format: cluster name + "." + 
endpoint
-               pools map[string]*sync.Pool
+               // hold grpc.ClientConns, key format: cluster name + NUL + 
endpoint
+               connections           *grpcConnectionManager
+               removeEndpointHandler func()
 
-               extReg     *dynamic.ExtensionRegistry
-               registered map[string]bool
+               extReg      *dynamic.ExtensionRegistry
+               registered  map[string]bool
+               extensionMu *sync.RWMutex
        }
        Filter struct {
                cfg *Config
                // grpc descriptor source factory
                descriptor *Descriptor
-               // hold grpc.ClientConns, key format: cluster name + "." + 
endpoint
-               pools map[string]*sync.Pool
+               // hold grpc.ClientConns, key format: cluster name + NUL + 
endpoint
+               connections *grpcConnectionManager
 
-               extReg     *dynamic.ExtensionRegistry
-               registered map[string]bool
+               extReg      *dynamic.ExtensionRegistry
+               registered  map[string]bool
+               extensionMu *sync.RWMutex
        }
 
        // Config describe the config of AccessFilter
@@ -148,12 +150,40 @@ func (p *Plugin) Kind() string {
 }
 
 func (p *Plugin) CreateFilterFactory() (filter.HttpFilterFactory, error) {
-       return &FilterFactory{cfg: &Config{DescriptorSourceStrategy: AUTO}, 
descriptor: &Descriptor{}}, nil
+       descriptor := &Descriptor{}
+       connections := newGRPCConnectionManager()
+       connections.onRemove = descriptor.removeConnection
+       var removeEndpointHandler func()
+       if clusterManager := server.GetClusterManager(); clusterManager != nil {
+               connections.endpointPresent = func(key, endpoint string) bool {
+                       clusterName, _, ok := strings.Cut(key, "\x00")
+                       return ok && 
clusterManager.HasEndpointAddress(clusterName, endpoint)
+               }
+               removeEndpointHandler = 
clusterManager.AddEndpointStateHandler(func(clusterName, endpoint string, 
present bool, version uint64) {
+                       connections.UpdateEndpointState(clusterName, endpoint, 
present, version)
+               })
+       }
+       return &FilterFactory{
+               cfg:                   &Config{DescriptorSourceStrategy: AUTO},
+               descriptor:            descriptor,
+               connections:           connections,
+               removeEndpointHandler: removeEndpointHandler,
+               extReg:                &dynamic.ExtensionRegistry{},
+               registered:            make(map[string]bool),
+               extensionMu:           &sync.RWMutex{},
+       }, nil
 }
 
 func (factory *FilterFactory) PrepareFilterChain(ctx *http.HttpContext, chain 
filter.FilterChain) error {
        // Deep copy config to avoid pointer sharing (factory.cfg may change at 
runtime)
-       f := &Filter{cfg: factory.cfg.DeepCopy(), descriptor: 
factory.descriptor, pools: factory.pools, extReg: factory.extReg, registered: 
factory.registered}
+       f := &Filter{
+               cfg:         factory.cfg.DeepCopy(),
+               descriptor:  factory.descriptor,
+               connections: factory.connections,
+               extReg:      factory.extReg,
+               registered:  factory.registered,
+               extensionMu: factory.extensionMu,
+       }
        chain.AppendDecodeFilters(f)
        return nil
 }
@@ -199,21 +229,13 @@ func (f *Filter) Decode(c *http.HttpContext) 
filter.FilterStatus {
        defer cancel()
        ep := e.Address.GetAddress()
 
-       p, ok := f.pools[strings.Join([]string{re.Cluster, ep}, ".")]
-       if !ok {
-               p = &sync.Pool{}
-       }
-
-       clientConn, ok = p.Get().(*grpc.ClientConn)
-       if !ok || clientConn == nil {
-               // TODO(Kenway): Support Credential and TLS
-               clientConn, err = grpc.DialContext(ctx, ep, 
grpc.WithTransportCredentials(insecure.NewCredentials()))
-               if err != nil || clientConn == nil {
-                       logger.Errorf("%s err {failed to connect to grpc 
service provider}", loggerHeader)
-                       errResp := 
http.ServiceUnavailable.WithError(fmt.Errorf("endpoint not found: %w", err))
-                       c.SendLocalReply(errResp.Status, errResp.ToJSON())
-                       return filter.Stop
-               }
+       connectionKey := grpcConnectionKey(re.Cluster, ep)
+       clientConn, err = f.connections.Get(ctx, connectionKey, ep)
+       if err != nil || clientConn == nil {
+               logger.Errorf("%s err {failed to connect to grpc service 
provider}: %v", loggerHeader, err)
+               errResp := 
http.ServiceUnavailable.WithError(fmt.Errorf("endpoint not found: %w", err))
+               c.SendLocalReply(errResp.Status, errResp.ToJSON())
+               return filter.Stop
        }
 
        // get DescriptorSource, contain file and reflection
@@ -227,24 +249,20 @@ func (f *Filter) Decode(c *http.HttpContext) 
filter.FilterStatus {
        //put DescriptorSource concurrent, del if no need
        ctx = context.WithValue(ctx, ct.ContextKey(DescriptorSourceKey), source)
 
-       dscp, err := source.FindSymbol(svc)
+       mthDesc, err := f.descriptor.getMethodDescriptor(source, clientConn, 
svc, mth)
        if err != nil {
-               logger.Errorf("%s err {%s}", loggerHeader, "request path 
invalid")
+               if _, ok := err.(*serviceNotExposedError); ok {
+                       logger.Errorf("%s err {service not expose, %s}", 
loggerHeader, svc)
+                       errResp := http.BadRequest.WithError(err)
+                       c.SendLocalReply(errResp.Status, errResp.ToJSON())
+                       return filter.Stop
+               }
+               logger.Errorf("%s err {request path invalid, service: %s, 
method: %s, cause: %v}", loggerHeader, svc, mth, err)
                errResp := http.MethodNotAllowed.New()
                c.SendLocalReply(errResp.Status, errResp.ToJSON())
                return filter.Stop
        }
 
-       svcDesc, ok := dscp.(*desc.ServiceDescriptor)
-       if !ok {
-               logger.Errorf("%s err {service not expose, %s}", loggerHeader, 
svc)
-               errResp := http.BadRequest.WithError(fmt.Errorf("service not 
exposed: %s", svc))
-               c.SendLocalReply(errResp.Status, errResp.ToJSON())
-               return filter.Stop
-       }
-
-       mthDesc := svcDesc.FindMethodByName(mth)
-
        err = f.registerExtension(source, mthDesc)
        if err != nil {
                logger.Errorf("%s err {%s}", loggerHeader, "register extension 
failed")
@@ -281,6 +299,7 @@ func (f *Filter) Decode(c *http.HttpContext) 
filter.FilterStatus {
                        logger.Errorf("%s err {gRPC client error, code: %s, 
msg: %s}", loggerHeader, st.Code(), st.Message())
                        errResp := http.BadGateway.WithError(fmt.Errorf("gRPC 
client error: %w", err))
                        c.SendLocalReply(errResp.Status, errResp.ToJSON())
+                       f.connections.Invalidate(connectionKey, clientConn)
                        return filter.Stop
                }
                // Handle server-side gRPC errors
@@ -289,11 +308,13 @@ func (f *Filter) Decode(c *http.HttpContext) 
filter.FilterStatus {
                                logger.Errorf("%s err {failed to invoke grpc 
service provider because timeout, err:%s}", loggerHeader, err.Error())
                                errResp := 
http.GatewayTimeout.WithError(fmt.Errorf("upstream timeout: %w", err))
                                c.SendLocalReply(errResp.Status, 
errResp.ToJSON())
+                               f.connections.Invalidate(connectionKey, 
clientConn)
                                return filter.Stop
                        }
                        logger.Errorf("%s err {failed to invoke grpc service 
provider, %s}", loggerHeader, err.Error())
                        errResp := 
http.ServiceUnavailable.WithError(fmt.Errorf("gRPC invoke error: %w", err))
                        c.SendLocalReply(errResp.Status, errResp.ToJSON())
+                       f.connections.Invalidate(connectionKey, clientConn)
                        return filter.Stop
                }
        } else if err != nil {
@@ -301,6 +322,7 @@ func (f *Filter) Decode(c *http.HttpContext) 
filter.FilterStatus {
                logger.Errorf("%s err {failed to invoke grpc service provider, 
%s}", loggerHeader, err.Error())
                errResp := http.ServiceUnavailable.WithError(fmt.Errorf("gRPC 
invoke error: %w", err))
                c.SendLocalReply(errResp.Status, errResp.ToJSON())
+               f.connections.Invalidate(connectionKey, clientConn)
                return filter.Stop
        }
 
@@ -323,19 +345,48 @@ func (f *Filter) Decode(c *http.HttpContext) 
filter.FilterStatus {
                Trailer:    th,
                Request:    c.Request,
        }
-       p.Put(clientConn)
        return filter.Continue
 }
 
+func grpcConnectionKey(cluster, endpoint string) string {
+       return cluster + "\x00" + endpoint
+}
+
 func (f *Filter) registerExtension(source DescriptorSource, mthDesc 
*desc.MethodDescriptor) error {
-       err := RegisterExtension(source, f.extReg, mthDesc.GetInputType(), 
f.registered)
-       if err != nil {
-               return perrors.New("register extension failed")
+       inputDesc := mthDesc.GetInputType()
+       outputDesc := mthDesc.GetOutputType()
+       if f.extensionMu == nil || f.extReg == nil || f.registered == nil {
+               if err := RegisterExtension(source, f.extReg, inputDesc, 
f.registered); err != nil {
+                       return perrors.New("register extension failed")
+               }
+               if err := RegisterExtension(source, f.extReg, outputDesc, 
f.registered); err != nil {
+                       return perrors.New("register extension failed")
+               }
+               return nil
        }
 
-       err = RegisterExtension(source, f.extReg, mthDesc.GetOutputType(), 
f.registered)
-       if err != nil {
-               return perrors.New("register extension failed")
+       inputName := inputDesc.GetFullyQualifiedName()
+       outputName := outputDesc.GetFullyQualifiedName()
+       f.extensionMu.RLock()
+       registered := f.registered[inputName] && f.registered[outputName]
+       f.extensionMu.RUnlock()
+       if registered {
+               return nil
+       }
+
+       f.extensionMu.Lock()
+       defer f.extensionMu.Unlock()
+       if !f.registered[inputName] {
+               if err := RegisterExtension(source, f.extReg, inputDesc, 
f.registered); err != nil {
+                       return perrors.New("register extension failed")
+               }
+               f.registered[inputName] = true
+       }
+       if !f.registered[outputName] {
+               if err := RegisterExtension(source, f.extReg, outputDesc, 
f.registered); err != nil {
+                       return perrors.New("register extension failed")
+               }
+               f.registered[outputName] = true
        }
        return nil
 }
@@ -426,6 +477,23 @@ func (factory *FilterFactory) Apply() error {
        return nil
 }
 
+// Close releases all backend connections owned by this filter factory.
+func (factory *FilterFactory) Close() error {
+       var firstErr error
+       if factory.connections != nil {
+               if err := factory.connections.Close(); err != nil {
+                       firstErr = err
+               }
+       }
+       if factory.descriptor != nil {
+               factory.descriptor.Close()
+       }
+       if factory.removeEndpointHandler != nil {
+               factory.removeEndpointHandler()
+       }
+       return firstErr
+}
+
 func configCheck(cfg *Config) error {
        if len(cfg.DescriptorSourceStrategy.Val()) == 0 {
                return perrors.Errorf("grpc descriptor source config 
`descriptor_source_strategy` is `%s`, maybe set it `%s`", 
cfg.DescriptorSourceStrategy.String(), AUTO)
diff --git a/pkg/listener/http/http_listener.go 
b/pkg/listener/http/http_listener.go
index 005bbaaac..5a65e631b 100644
--- a/pkg/listener/http/http_listener.go
+++ b/pkg/listener/http/http_listener.go
@@ -49,7 +49,9 @@ type (
        // ListenerService the facade of a listener
        HttpListenerService struct {
                listener.BaseListenerService
-               srv *http.Server
+               srv         *http.Server
+               filterMu    sync.Mutex
+               filterState *httpFilterChainState
        }
 
        // DefaultHttpListener
@@ -58,6 +60,73 @@ type (
        }
 )
 
+type httpFilterChainState struct {
+       chain     *filterchain.NetworkFilterChain
+       mu        sync.Mutex
+       refs      int
+       retired   bool
+       done      chan struct{}
+       closeErr  error
+       closeOnce sync.Once
+}
+
+func newHTTPFilterChainState(chain *filterchain.NetworkFilterChain) 
*httpFilterChainState {
+       return &httpFilterChainState{chain: chain, done: make(chan struct{})}
+}
+
+func (state *httpFilterChainState) acquire() bool {
+       if state == nil {
+               return false
+       }
+       state.mu.Lock()
+       defer state.mu.Unlock()
+       if state.retired || state.chain == nil {
+               return false
+       }
+       state.refs++
+       return true
+}
+
+func (state *httpFilterChainState) release() {
+       if state == nil {
+               return
+       }
+       state.mu.Lock()
+       state.refs--
+       closeNow := state.retired && state.refs == 0
+       state.mu.Unlock()
+       if closeNow {
+               state.close()
+       }
+}
+
+func (state *httpFilterChainState) retire(wait bool) error {
+       if state == nil {
+               return nil
+       }
+       state.mu.Lock()
+       state.retired = true
+       closeNow := state.refs == 0
+       state.mu.Unlock()
+       if closeNow {
+               state.close()
+       }
+       if wait {
+               <-state.done
+               return state.closeErr
+       }
+       return nil
+}
+
+func (state *httpFilterChainState) close() {
+       state.closeOnce.Do(func() {
+               if state.chain != nil {
+                       state.closeErr = state.chain.Close()
+               }
+               close(state.done)
+       })
+}
+
 func newHttpListenerService(lc *model.Listener, bs *model.Bootstrap) 
(listener.ListenerService, error) {
        fc := filterchain.CreateNetworkFilterChain(lc.FilterChain)
        return &HttpListenerService{
@@ -65,7 +134,8 @@ func newHttpListenerService(lc *model.Listener, bs 
*model.Bootstrap) (listener.L
                        Config:      lc,
                        FilterChain: fc,
                },
-               srv: nil,
+               srv:         nil,
+               filterState: newHTTPFilterChainState(fc),
        }, nil
 }
 
@@ -83,7 +153,15 @@ func (ls *HttpListenerService) Start() error {
 }
 
 func (ls *HttpListenerService) Close() error {
-       return ls.srv.Close()
+       serverErr := error(nil)
+       if ls.srv != nil {
+               serverErr = ls.srv.Close()
+       }
+       filterErr := ls.closeFilterChain()
+       if serverErr != nil {
+               return serverErr
+       }
+       return filterErr
 }
 
 func (ls *HttpListenerService) ShutDown(wg any) error {
@@ -96,13 +174,56 @@ func (ls *HttpListenerService) ShutDown(wg any) error {
                cancel()
                wg.(*sync.WaitGroup).Done()
        }()
-       return ls.srv.Shutdown(ctx)
+       serverErr := ls.srv.Shutdown(ctx)
+       filterErr := ls.closeFilterChainAfterShutdown()
+       if serverErr != nil {
+               return serverErr
+       }
+       return filterErr
+}
+
+func (ls *HttpListenerService) closeFilterChain() error {
+       ls.filterMu.Lock()
+       state := ls.filterState
+       if state == nil && ls.FilterChain != nil {
+               state = newHTTPFilterChainState(ls.FilterChain)
+       }
+       ls.FilterChain = nil
+       ls.filterState = nil
+       ls.filterMu.Unlock()
+       return state.retire(true)
+}
+
+// closeFilterChainAfterShutdown must not wait for a request that outlives the
+// HTTP server shutdown deadline while holding filterMu. If an active request
+// still owns a chain lease, defer the close until that request has released
+// it. The shutdown caller can then return the server timeout instead of
+// extending the timeout by the lifetime of the request.
+func (ls *HttpListenerService) closeFilterChainAfterShutdown() error {
+       ls.filterMu.Lock()
+       state := ls.filterState
+       if state == nil && ls.FilterChain != nil {
+               state = newHTTPFilterChainState(ls.FilterChain)
+       }
+       ls.FilterChain = nil
+       ls.filterState = nil
+       ls.filterMu.Unlock()
+       return state.retire(false)
 }
 
 func (ls *HttpListenerService) Refresh(c model.Listener) error {
-       // There is no need to lock here for now, as there is at most one 
NetworkFilter
        fc := filterchain.CreateNetworkFilterChain(c.FilterChain)
+       ls.filterMu.Lock()
+       old := ls.filterState
+       if old == nil && ls.FilterChain != nil {
+               old = newHTTPFilterChainState(ls.FilterChain)
+       }
        ls.FilterChain = fc
+       ls.filterState = newHTTPFilterChainState(fc)
+       ls.filterMu.Unlock()
+       if old != nil {
+               return old.retire(false)
+       }
        return nil
 }
 
@@ -175,7 +296,19 @@ func createDefaultHttpWorker(ls *HttpListenerService) 
*DefaultHttpWorker {
 
 // ServeHTTP http request entrance.
 func (s *DefaultHttpWorker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-       s.ls.FilterChain.ServeHTTP(w, r)
+       s.ls.filterMu.Lock()
+       state := s.ls.filterState
+       if state == nil && s.ls.FilterChain != nil {
+               state = newHTTPFilterChainState(s.ls.FilterChain)
+               s.ls.filterState = state
+       }
+       acquired := state.acquire()
+       s.ls.filterMu.Unlock()
+       if !acquired {
+               return
+       }
+       defer state.release()
+       state.chain.ServeHTTP(w, r)
 }
 
 func resolveInt2IntProp(currentV, defaultV int) int {
diff --git a/pkg/listener/http/http_listener_test.go 
b/pkg/listener/http/http_listener_test.go
new file mode 100644
index 000000000..ab3682c3e
--- /dev/null
+++ b/pkg/listener/http/http_listener_test.go
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package http
+
+import (
+       "testing"
+       "time"
+)
+
+import (
+       "github.com/stretchr/testify/require"
+)
+
+import (
+       "github.com/apache/dubbo-go-pixiu/pkg/filterchain"
+       listenerpkg "github.com/apache/dubbo-go-pixiu/pkg/listener"
+       "github.com/apache/dubbo-go-pixiu/pkg/model"
+)
+
+func TestCloseFilterChainAfterShutdownDoesNotWaitForActiveRequest(t 
*testing.T) {
+       listener := &HttpListenerService{
+               BaseListenerService: listenerpkg.BaseListenerService{
+                       FilterChain: &filterchain.NetworkFilterChain{},
+               },
+       }
+       listener.filterState = newHTTPFilterChainState(listener.FilterChain)
+       state := listener.filterState
+       require.True(t, state.acquire())
+
+       finished := make(chan struct{})
+       go func() {
+               _ = listener.closeFilterChainAfterShutdown()
+               close(finished)
+       }()
+
+       select {
+       case <-finished:
+       case <-time.After(100 * time.Millisecond):
+               t.Fatal("shutdown filter cleanup waited for an active request")
+       }
+
+       state.release()
+       select {
+       case <-state.done:
+       case <-time.After(time.Second):
+               t.Fatal("deferred filter cleanup did not run after the request 
released its chain lease")
+       }
+}
+
+func TestRefreshSwapsChainWithoutWaitingForActiveRequest(t *testing.T) {
+       listener := &HttpListenerService{
+               BaseListenerService: listenerpkg.BaseListenerService{
+                       FilterChain: &filterchain.NetworkFilterChain{},
+               },
+       }
+       listener.filterState = newHTTPFilterChainState(listener.FilterChain)
+       oldState := listener.filterState
+       require.True(t, oldState.acquire())
+
+       refreshed := make(chan error, 1)
+       go func() {
+               refreshed <- listener.Refresh(model.Listener{})
+       }()
+
+       select {
+       case err := <-refreshed:
+               require.NoError(t, err)
+       case <-time.After(100 * time.Millisecond):
+               oldState.release()
+               t.Fatal("refresh waited for an active request on the old filter 
chain")
+       }
+       require.NotSame(t, oldState, listener.filterState)
+       oldState.release()
+       select {
+       case <-oldState.done:
+       case <-time.After(time.Second):
+               t.Fatal("old filter chain was not closed after the active 
request released")
+       }
+}
diff --git a/pkg/server/cluster_manager.go b/pkg/server/cluster_manager.go
index c47304ef0..895bb3b20 100644
--- a/pkg/server/cluster_manager.go
+++ b/pkg/server/cluster_manager.go
@@ -44,7 +44,11 @@ type (
        ClusterManager struct {
                rw sync.RWMutex
 
-               store *ClusterStore
+               store                   *ClusterStore
+               endpointRemovalHandlers map[uint64]func(string, string)
+               endpointStateHandlers   map[uint64]func(string, string, bool, 
uint64)
+               nextEndpointHandlerID   uint64
+               nextEndpointEventID     uint64
                //cConfig []*model.ClusterConfig
        }
 
@@ -75,6 +79,49 @@ func CreateDefaultClusterManager(bs *model.Bootstrap) 
*ClusterManager {
        return &ClusterManager{store: newClusterStore(bs)}
 }
 
+// AddEndpointRemovalHandler registers a callback invoked after an endpoint is
+// removed. The returned function unregisters the callback.
+func (cm *ClusterManager) AddEndpointRemovalHandler(handler func(string, 
string)) func() {
+       if handler == nil {
+               return func() {}
+       }
+       cm.rw.Lock()
+       if cm.endpointRemovalHandlers == nil {
+               cm.endpointRemovalHandlers = make(map[uint64]func(string, 
string))
+       }
+       cm.nextEndpointHandlerID++
+       id := cm.nextEndpointHandlerID
+       cm.endpointRemovalHandlers[id] = handler
+       cm.rw.Unlock()
+       return func() {
+               cm.rw.Lock()
+               delete(cm.endpointRemovalHandlers, id)
+               cm.rw.Unlock()
+       }
+}
+
+// AddEndpointStateHandler registers a callback for endpoint additions and
+// removals. version increases for every cluster-store mutation, allowing
+// consumers to discard stale callbacks that run after a newer update.
+func (cm *ClusterManager) AddEndpointStateHandler(handler func(string, string, 
bool, uint64)) func() {
+       if handler == nil {
+               return func() {}
+       }
+       cm.rw.Lock()
+       if cm.endpointStateHandlers == nil {
+               cm.endpointStateHandlers = make(map[uint64]func(string, string, 
bool, uint64))
+       }
+       cm.nextEndpointHandlerID++
+       id := cm.nextEndpointHandlerID
+       cm.endpointStateHandlers[id] = handler
+       cm.rw.Unlock()
+       return func() {
+               cm.rw.Lock()
+               delete(cm.endpointStateHandlers, id)
+               cm.rw.Unlock()
+       }
+}
+
 func newClusterStore(bs *model.Bootstrap) *ClusterStore {
        store := &ClusterStore{
                clustersMap: map[string]*cluster.Cluster{},
@@ -87,18 +134,28 @@ func newClusterStore(bs *model.Bootstrap) *ClusterStore {
 
 func (cm *ClusterManager) AddCluster(c *model.ClusterConfig) {
        cm.rw.Lock()
-       defer cm.rw.Unlock()
-
+       old := endpointAddressSnapshot(nil)
+       version := cm.nextEndpointEventIDLocked()
        cm.store.IncreaseVersion()
        cm.store.AddCluster(c)
+       new := endpointAddressSnapshot([]*model.ClusterConfig{c})
+       cm.rw.Unlock()
+       cm.notifyEndpointChanges(endpointStateChanges(old, new, version))
 }
 
 func (cm *ClusterManager) UpdateCluster(new *model.ClusterConfig) {
        cm.rw.Lock()
-       defer cm.rw.Unlock()
-
+       oldConfig := cm.store.findClusterConfig(new.Name)
+       old := endpointAddressSnapshot([]*model.ClusterConfig{oldConfig})
+       newSnapshot := old
+       if oldConfig != nil {
+               newSnapshot = 
endpointAddressSnapshot([]*model.ClusterConfig{new})
+       }
+       version := cm.nextEndpointEventIDLocked()
        cm.store.IncreaseVersion()
        cm.store.UpdateCluster(new)
+       cm.rw.Unlock()
+       cm.notifyEndpointChanges(endpointStateChanges(old, newSnapshot, 
version))
 }
 
 // SetEndpoint registers or refreshes a single endpoint in the named
@@ -135,18 +192,168 @@ func (cm *ClusterManager) UpdateCluster(new 
*model.ClusterConfig) {
 // the whole cluster config via AddCluster/UpdateCluster.
 func (cm *ClusterManager) SetEndpoint(clusterName string, endpoint 
*model.Endpoint) {
        cm.rw.Lock()
-       defer cm.rw.Unlock()
-
+       old := 
endpointAddressSnapshot([]*model.ClusterConfig{cm.store.findClusterConfig(clusterName)})
+       version := cm.nextEndpointEventIDLocked()
        cm.store.IncreaseVersion()
        cm.store.SetEndpoint(clusterName, endpoint)
+       new := 
endpointAddressSnapshot([]*model.ClusterConfig{cm.store.findClusterConfig(clusterName)})
+       cm.rw.Unlock()
+       cm.notifyEndpointChanges(endpointStateChanges(old, new, version))
 }
 
 func (cm *ClusterManager) DeleteEndpoint(clusterName string, endpointID 
string) {
        cm.rw.Lock()
-       defer cm.rw.Unlock()
-
+       old := 
endpointAddressSnapshot([]*model.ClusterConfig{cm.store.findClusterConfig(clusterName)})
+       version := cm.nextEndpointEventIDLocked()
        cm.store.IncreaseVersion()
        cm.store.DeleteEndpoint(clusterName, endpointID)
+       new := 
endpointAddressSnapshot([]*model.ClusterConfig{cm.store.findClusterConfig(clusterName)})
+       cm.rw.Unlock()
+       cm.notifyEndpointChanges(endpointStateChanges(old, new, version))
+}
+
+func (cm *ClusterManager) notifyEndpointRemoval(clusterName, endpointAddress 
string) {
+       cm.notifyEndpointRemovals([]endpointRemoval{{clusterName: clusterName, 
address: endpointAddress}})
+}
+
+func (cm *ClusterManager) notifyEndpointRemovals(removals []endpointRemoval) {
+       cm.rw.RLock()
+       handlers := make([]func(string, string), 0, 
len(cm.endpointRemovalHandlers))
+       for _, handler := range cm.endpointRemovalHandlers {
+               handlers = append(handlers, handler)
+       }
+       cm.rw.RUnlock()
+       for _, removal := range removals {
+               if removal.address == "" {
+                       continue
+               }
+               for _, handler := range handlers {
+                       handler(removal.clusterName, removal.address)
+               }
+       }
+}
+
+func (cm *ClusterManager) notifyEndpointChanges(changes []endpointStateChange) 
{
+       cm.rw.RLock()
+       removalHandlers := make([]func(string, string), 0, 
len(cm.endpointRemovalHandlers))
+       for _, handler := range cm.endpointRemovalHandlers {
+               removalHandlers = append(removalHandlers, handler)
+       }
+       stateHandlers := make([]func(string, string, bool, uint64), 0, 
len(cm.endpointStateHandlers))
+       for _, handler := range cm.endpointStateHandlers {
+               stateHandlers = append(stateHandlers, handler)
+       }
+       cm.rw.RUnlock()
+       for _, change := range changes {
+               for _, handler := range stateHandlers {
+                       handler(change.clusterName, change.address, 
change.present, change.version)
+               }
+               if change.present {
+                       continue
+               }
+               for _, handler := range removalHandlers {
+                       handler(change.clusterName, change.address)
+               }
+       }
+}
+
+type endpointRemoval struct {
+       clusterName string
+       address     string
+}
+
+type endpointStateChange struct {
+       clusterName string
+       address     string
+       present     bool
+       version     uint64
+}
+
+func (cm *ClusterManager) nextEndpointEventIDLocked() uint64 {
+       cm.nextEndpointEventID++
+       return cm.nextEndpointEventID
+}
+
+func endpointAddressSnapshot(configs []*model.ClusterConfig) 
map[string]map[string]struct{} {
+       result := make(map[string]map[string]struct{})
+       for _, config := range configs {
+               if config == nil {
+                       continue
+               }
+               addresses := result[config.Name]
+               if addresses == nil {
+                       addresses = make(map[string]struct{})
+                       result[config.Name] = addresses
+               }
+               for _, endpoint := range config.Endpoints {
+                       if endpoint != nil {
+                               addresses[endpoint.Address.GetAddress()] = 
struct{}{}
+                       }
+               }
+       }
+       return result
+}
+
+func endpointStateChanges(old, new map[string]map[string]struct{}, version 
uint64) []endpointStateChange {
+       changes := make([]endpointStateChange, 0)
+       for clusterName, addresses := range old {
+               for address := range addresses {
+                       if _, exists := new[clusterName][address]; !exists {
+                               changes = append(changes, 
endpointStateChange{clusterName: clusterName, address: address, version: 
version})
+                       }
+               }
+       }
+       for clusterName, addresses := range new {
+               for address := range addresses {
+                       if _, exists := old[clusterName][address]; !exists {
+                               changes = append(changes, 
endpointStateChange{clusterName: clusterName, address: address, present: true, 
version: version})
+                       }
+               }
+       }
+       return changes
+}
+
+func removedEndpointAddresses(oldConfigs, newConfigs []*model.ClusterConfig) 
[]endpointRemoval {
+       newAddresses := make(map[string]map[string]struct{})
+       for _, config := range newConfigs {
+               if config == nil {
+                       continue
+               }
+               addresses := newAddresses[config.Name]
+               if addresses == nil {
+                       addresses = make(map[string]struct{})
+                       newAddresses[config.Name] = addresses
+               }
+               for _, endpoint := range config.Endpoints {
+                       if endpoint != nil {
+                               addresses[endpoint.Address.GetAddress()] = 
struct{}{}
+                       }
+               }
+       }
+
+       seen := make(map[string]struct{})
+       var removals []endpointRemoval
+       for _, config := range oldConfigs {
+               if config == nil {
+                       continue
+               }
+               for _, endpoint := range config.Endpoints {
+                       if endpoint == nil {
+                               continue
+                       }
+                       address := endpoint.Address.GetAddress()
+                       key := config.Name + "\x00" + address
+                       if _, exists := newAddresses[config.Name][address]; 
exists {
+                               continue
+                       }
+                       if _, exists := seen[key]; exists {
+                               continue
+                       }
+                       seen[key] = struct{}{}
+                       removals = append(removals, 
endpointRemoval{clusterName: config.Name, address: address})
+               }
+       }
+       return removals
 }
 
 func (cm *ClusterManager) CloneStore() (*ClusterStore, error) {
@@ -177,25 +384,29 @@ func (cm *ClusterManager) NewStore(version int32) 
*ClusterStore {
 // CompareAndSetStore swaps the store only when versions match.
 // Version mismatch must leave both stores and runtime clusters untouched.
 func (cm *ClusterManager) CompareAndSetStore(store *ClusterStore) bool {
-       swapped, replacedClusters := cm.compareAndSetStore(store)
+       swapped, replacedClusters, changes := cm.compareAndSetStore(store)
        if !swapped {
                return false
        }
 
        // Stop old runtime after publishing the swap; Stop may touch 
timers/goroutines.
        stopClusters(replacedClusters)
+       cm.notifyEndpointChanges(changes)
        return true
 }
 
-func (cm *ClusterManager) compareAndSetStore(store *ClusterStore) (bool, 
[]*cluster.Cluster) {
+func (cm *ClusterManager) compareAndSetStore(store *ClusterStore) (bool, 
[]*cluster.Cluster, []endpointStateChange) {
        cm.rw.Lock()
        defer cm.rw.Unlock()
 
        if store.Version != cm.store.Version {
-               return false, nil
+               return false, nil, nil
        }
 
        currentStore := cm.store
+       version := cm.nextEndpointEventIDLocked()
+       old := endpointAddressSnapshot(currentStore.Config)
+       new := endpointAddressSnapshot(store.Config)
        var replacedClusters []*cluster.Cluster
        if store == currentStore {
                replacedClusters = store.ensureRuntimeClusters()
@@ -207,7 +418,7 @@ func (cm *ClusterManager) compareAndSetStore(store 
*ClusterStore) (bool, []*clus
        if store != currentStore {
                replacedClusters = append(replacedClusters, 
currentStore.runtimeClustersNotIn(store)...)
        }
-       return true, replacedClusters
+       return true, replacedClusters, endpointStateChanges(old, new, version)
 }
 
 // PickEndpoint picks an endpoint from the cluster by its name and load 
balancing policy.
@@ -260,6 +471,25 @@ func (cm *ClusterManager) GetAnyEndpointByID(clusterName, 
endpointID string) *mo
        return runtimeCluster.EndpointSnapshot().EndpointByID(endpointID)
 }
 
+// HasEndpointAddress reports whether the current runtime snapshot still
+// contains the given endpoint address. Connection managers use this as an
+// authoritative check after bounded lifecycle tombstones have been evicted.
+func (cm *ClusterManager) HasEndpointAddress(clusterName, address string) bool 
{
+       cm.rw.RLock()
+       defer cm.rw.RUnlock()
+
+       runtimeCluster := cm.getRuntimeCluster(clusterName)
+       if runtimeCluster == nil {
+               return false
+       }
+       for _, endpoint := range 
runtimeCluster.EndpointSnapshot().AllEndpoints() {
+               if endpoint != nil && endpoint.Address.GetAddress() == address {
+                       return true
+               }
+       }
+       return false
+}
+
 // GetHealthyEndpointByID returns the runtime endpoint by ID only when it is
 // healthy in the current runtime snapshot.
 func (cm *ClusterManager) GetHealthyEndpointByID(clusterName, endpointID 
string) *model.Endpoint {
@@ -307,7 +537,8 @@ func (cm *ClusterManager) pickOneEndpoint(runtimeCluster 
*cluster.Cluster, polic
 
 func (cm *ClusterManager) RemoveCluster(namesToDel []string) {
        cm.rw.Lock()
-       defer cm.rw.Unlock()
+       old := endpointAddressSnapshot(cm.store.Config)
+       version := cm.nextEndpointEventIDLocked()
 
        for i, c := range cm.store.Config {
                if c == nil {
@@ -331,6 +562,9 @@ func (cm *ClusterManager) RemoveCluster(namesToDel 
[]string) {
                cm.store.Config = append(cm.store.Config[:i], 
cm.store.Config[i+1:]...)
        }
        cm.store.IncreaseVersion()
+       new := endpointAddressSnapshot(cm.store.Config)
+       cm.rw.Unlock()
+       cm.notifyEndpointChanges(endpointStateChanges(old, new, version))
 }
 
 func (cm *ClusterManager) HasCluster(clusterName string) bool {
@@ -613,10 +847,10 @@ func (s *ClusterStore) UpdateCluster(new 
*model.ClusterConfig) {
        logger.Warnf("not found modified cluster %s", new.Name)
 }
 
-func (s *ClusterStore) SetEndpoint(clusterName string, endpoint 
*model.Endpoint) {
+func (s *ClusterStore) SetEndpoint(clusterName string, endpoint 
*model.Endpoint) string {
        endpoint = model.CloneEndpoint(endpoint)
        if endpoint == nil {
-               return
+               return ""
        }
 
        clusterConfig := s.findClusterConfig(clusterName)
@@ -642,9 +876,13 @@ func (s *ClusterStore) SetEndpoint(clusterName string, 
endpoint *model.Endpoint)
                // already correct. Returning here keeps re-registration 
idempotent —
                // the LLM/Nacos path can replay the same instance event without
                // growing the endpoint slice.
-               return
+               return ""
        case setEndpointReplace:
+               oldAddress := 
clusterConfig.Endpoints[outcome.replaceIdx].Address.GetAddress()
                s.replaceEndpointAt(clusterConfig, runtimeCluster, 
outcome.replaceIdx, endpoint)
+               if oldAddress != endpoint.Address.GetAddress() && 
!clusterHasEndpointAddress(clusterConfig, oldAddress) {
+                       return oldAddress
+               }
        case setEndpointAppend:
                clusterConfig.Endpoints = append(clusterConfig.Endpoints, 
endpoint)
                s.prepareOwnedClusterConfig(clusterConfig)
@@ -652,6 +890,16 @@ func (s *ClusterStore) SetEndpoint(clusterName string, 
endpoint *model.Endpoint)
                runtimeCluster.RefreshEndpoints()
                runtimeCluster.AddEndpoint(endpoint)
        }
+       return ""
+}
+
+func clusterHasEndpointAddress(config *model.ClusterConfig, address string) 
bool {
+       for _, endpoint := range config.Endpoints {
+               if endpoint != nil && endpoint.Address.GetAddress() == address {
+                       return true
+               }
+       }
+       return false
 }
 
 // replaceEndpointAt overwrites the cluster slot at idx with the incoming
diff --git a/pkg/server/cluster_manager_test.go 
b/pkg/server/cluster_manager_test.go
index 264790350..08412f1d3 100644
--- a/pkg/server/cluster_manager_test.go
+++ b/pkg/server/cluster_manager_test.go
@@ -800,6 +800,23 @@ func 
TestClusterManager_SetEndpointExplicitSameIDDifferentAddressOverwritesInPla
        assert.Equal(t, 21101, endpoints[0].Address.Port)
 }
 
+func TestClusterManager_SetEndpointNotifiesAddressReplacement(t *testing.T) {
+       cm := testClusterManager(testCluster("endpoint-removal", 
model.LoadBalancerRoundRobin, []*model.Endpoint{
+               testEndpoint("foo", "127.0.0.1", 21120),
+       }))
+       defer stopStoreRuntimes(cm.store)
+
+       var removed []string
+       removeHandler := cm.AddEndpointRemovalHandler(func(clusterName, address 
string) {
+               removed = append(removed, clusterName+"\x00"+address)
+       })
+       defer removeHandler()
+
+       cm.SetEndpoint("endpoint-removal", testEndpoint("foo", "127.0.0.2", 
21121))
+
+       assert.Equal(t, []string{"endpoint-removal\x00127.0.0.1:21120"}, 
removed)
+}
+
 // TestClusterManager_SetEndpointExplicitSameIDSameContentIsIdempotent locks
 // the other side of the dedup contract: when two calls share the same
 // explicit ID AND the same routing-relevant content, the second call is a
diff --git a/pkg/server/pixiu_start.go b/pkg/server/pixiu_start.go
index 38b882e57..dd4837b8f 100644
--- a/pkg/server/pixiu_start.go
+++ b/pkg/server/pixiu_start.go
@@ -145,6 +145,9 @@ func GetServer() *Server {
 }
 
 func GetClusterManager() *ClusterManager {
+       if server == nil {
+               return nil
+       }
        return server.GetClusterManager()
 }
 
diff --git a/pkg/tracing/driver.go b/pkg/tracing/driver.go
index 27002159f..36d56129c 100644
--- a/pkg/tracing/driver.go
+++ b/pkg/tracing/driver.go
@@ -33,6 +33,7 @@ import (
 )
 
 import (
+       "github.com/apache/dubbo-go-pixiu/pkg/client/dubbo"
        "github.com/apache/dubbo-go-pixiu/pkg/logger"
        "github.com/apache/dubbo-go-pixiu/pkg/model"
        "github.com/apache/dubbo-go-pixiu/pkg/tracing/jaeger"
@@ -84,6 +85,7 @@ func InitDriver(bs *model.Bootstrap) *TraceDriver {
        }
        provider := newTraceProvider(exp, config)
        otel.SetTracerProvider(provider)
+       dubbo.SetTracingEnabled(true)
 
        return &TraceDriver{TracerProvider: provider}
 }
diff --git a/tools/benchmark/README.md b/tools/benchmark/README.md
index 2a92a81b4..6d4706f19 100644
--- a/tools/benchmark/README.md
+++ b/tools/benchmark/README.md
@@ -1,6 +1,6 @@
 # Benchmark Results
 
-Test environment: macOS, Apple Silicon, N=500 samples per method
+Test environment: macOS, Apple Silicon, N=500 samples per method. The three 
suites were run in parallel with `go test -count=1 ./...`.
 
 ## gRPC Protocol
 
@@ -8,19 +8,19 @@ Test environment: macOS, Apple Silicon, N=500 samples per 
method
 
 | Method | Min | Median | Mean | StdDev | Max |
 |--------|-----|--------|------|--------|-----|
-| GetUser | 100µs | 200µs | 400µs | 1.1ms | 8.2ms |
+| GetUser | 100µs | 200µs | 200µs | 400µs | 2.8ms |
 | GetUsers | 100µs | 200µs | 200µs | 100µs | 400µs |
-| GetUserByName | 100µs | 200µs | 200µs | 100µs | 600µs |
-| SayHello | 100µs | 200µs | 200µs | 100µs | 400µs |
+| GetUserByName | 100µs | 200µs | 200µs | 100µs | 400µs |
+| SayHello | 100µs | 200µs | 200µs | 0s | 500µs |
 
 ### gRPC via Pixiu
 
 | Method | Min | Median | Mean | StdDev | Max |
 |--------|-----|--------|------|--------|-----|
-| GetUser | 600µs | 1.4ms | 1.8ms | 1.8ms | 12.7ms |
-| GetUsers | 500µs | 1.4ms | 1.7ms | 1.2ms | 14.6ms |
-| GetUserByName | 500µs | 1.3ms | 2ms | 2ms | 17.1ms |
-| SayHello | 600µs | 1.3ms | 1.8ms | 2ms | 32.8ms |
+| GetUser | 200µs | 600µs | 800µs | 1.1ms | 8.3ms |
+| GetUsers | 200µs | 600µs | 700µs | 400µs | 3.6ms |
+| GetUserByName | 200µs | 400µs | 500µs | 300µs | 2ms |
+| SayHello | 200µs | 500µs | 600µs | 300µs | 1.8ms |
 
 ## Triple Protocol
 
@@ -28,19 +28,19 @@ Test environment: macOS, Apple Silicon, N=500 samples per 
method
 
 | Method | Min | Median | Mean | StdDev | Max |
 |--------|-----|--------|------|--------|-----|
-| GetUser | 200µs | 1.8ms | 2.5ms | 2.4ms | 14.5ms |
-| GetUsers | 200µs | 1.5ms | 2.0ms | 2.0ms | 12.0ms |
-| GetUserByName | 200µs | 1.2ms | 1.8ms | 1.8ms | 10.5ms |
-| SayHello | 200µs | 900µs | 1.4ms | 1.3ms | 12.5ms |
+| GetUser | 200µs | 500µs | 600µs | 300µs | 2.5ms |
+| GetUsers | 200µs | 500µs | 500µs | 200µs | 1.7ms |
+| GetUserByName | 200µs | 600µs | 800µs | 700µs | 7ms |
+| SayHello | 200µs | 500µs | 500µs | 200µs | 1.2ms |
 
 ### Triple via Pixiu
 
 | Method | Min | Median | Mean | StdDev | Max |
 |--------|-----|--------|------|--------|-----|
-| GetUser | 1ms | 2.1ms | 2.8ms | 3.1ms | 24.4ms |
-| GetUsers | 800µs | 2.0ms | 2.5ms | 2.5ms | 20.0ms |
-| GetUserByName | 700µs | 1.8ms | 2.3ms | 2.0ms | 18.0ms |
-| SayHello | 700µs | 2ms | 2.5ms | 1.4ms | 12ms |
+| GetUser | 200µs | 1ms | 1.5ms | 1.9ms | 10.7ms |
+| GetUsers | 300µs | 1.3ms | 1.4ms | 700µs | 4.1ms |
+| GetUserByName | 300µs | 900µs | 1.2ms | 1ms | 7ms |
+| SayHello | 300µs | 800µs | 900µs | 500µs | 7ms |
 
 ## Dubbo Protocol
 
@@ -48,31 +48,29 @@ Test environment: macOS, Apple Silicon, N=500 samples per 
method
 
 | Method | Min | Median | Mean | StdDev | Max |
 |--------|-----|--------|------|--------|-----|
-| GetUser | 100µs | 200µs | 600µs | 2.8ms | 20.7ms |
-| GetGender | 0s | 100µs | 200µs | 100µs | 600µs |
-| GetUser0 | 100µs | 100µs | 200µs | 0s | 400µs |
-| GetUsers | 100µs | 200µs | 200µs | 100µs | 600µs |
-| GetUser2 | 0s | 200µs | 200µs | 100µs | 500µs |
-| GetErr | 300µs | 500µs | 500µs | 200µs | 1.4ms |
+| GetUser | 100µs | 200µs | 200µs | 100µs | 500µs |
+| GetGender | 100µs | 200µs | 200µs | 100µs | 500µs |
+| GetUser0 | 100µs | 200µs | 200µs | 100µs | 900µs |
+| GetUsers | 100µs | 200µs | 200µs | 100µs | 500µs |
 
 ### Dubbo via Pixiu
 
 | Method | Min | Median | Mean | StdDev | Max |
 |--------|-----|--------|------|--------|-----|
-| GetUser | 700µs | 2ms | 3ms | 5.9ms | 44.7ms |
-| GetGender | 300µs | 800µs | 1ms | 500µs | 3.1ms |
-| GetUser0 | 300µs | 700µs | 800µs | 500µs | 4.2ms |
-| GetUsers | 100µs | 500µs | 600µs | 400µs | 3.2ms |
+| GetUser | 200µs | 400µs | 600µs | 1.5ms | 14.2ms |
+| GetGender | 200µs | 300µs | 400µs | 500µs | 4.3ms |
+| GetUser0 | 200µs | 400µs | 400µs | 200µs | 1.6ms |
+| GetUsers | 200µs | 400µs | 400µs | 200µs | 1.7ms |
 
 ## Performance Summary
 
-Pixiu proxy adds approximately 0.3-1.1ms overhead compared to direct protocol 
calls.
+Pixiu proxy adds approximately 0.2-0.4ms median overhead in this run. Results 
vary with local load and scheduling. The summary compares the same method 
(`GetUser` for Dubbo and gRPC, `SayHello` for Triple).
 
 | Protocol | Direct (Median) | via Pixiu (Median) | Overhead |
 |----------|-----------------|--------------------|-----------|
-| gRPC | ~200µs | ~1.3ms | ~1.1ms |
-| Triple | ~1.4ms | ~2ms | ~0.6ms |
-| Dubbo | ~200µs | ~700µs | ~0.5ms |
+| gRPC | ~200µs | ~600µs | ~0.4ms |
+| Triple | ~500µs | ~800µs | ~0.3ms |
+| Dubbo | ~200µs | ~400µs | ~0.2ms |
 
 # How to Run
 
diff --git a/tools/benchmark/README_CN.md b/tools/benchmark/README_CN.md
index 53cfc1477..6c716d32b 100644
--- a/tools/benchmark/README_CN.md
+++ b/tools/benchmark/README_CN.md
@@ -1,70 +1,38 @@
 # Benchmark 结果
 
-* dubbo:
+测试环境:macOS、Apple Silicon,每个方法 500 次采样。Dubbo、gRPC、Triple 三套测试并行运行。
 
-```
-dubbo protocol performance test
-      Name                 | N   | Min   | Median | Mean  | StdDev | Max   
-      =====================================================================
-      GetUser [duration]   | 500 | 100µs | 200µs  | 600µs | 2.8ms  | 20.7ms
-      ---------------------------------------------------------------------
-      GetGender [duration] | 500 | 0s    | 100µs  | 200µs | 100µs  | 600µs 
-      ---------------------------------------------------------------------
-      GetUser0 [duration]  | 500 | 100µs | 100µs  | 200µs | 0s     | 400µs 
-      ---------------------------------------------------------------------
-      GetUsers [duration]  | 500 | 100µs | 200µs  | 200µs | 100µs  | 600µs 
-      ---------------------------------------------------------------------
-      GetUser2 [duration]  | 500 | 0s    | 200µs  | 200µs | 100µs  | 500µs 
-      ---------------------------------------------------------------------
-      GetErr [duration]    | 500 | 300µs | 500µs  | 500µs | 200µs  | 1.4ms 
-
- pixiu to dubbo protocol performance test
-      Name                 | N   | Min   | Median | Mean  | StdDev | Max  
-      ====================================================================
-      GetUser [duration]   | 500 | 200µs | 600µs  | 1.8ms | 8.4ms  | 61ms 
-      --------------------------------------------------------------------
-      GetGender [duration] | 499 | 200µs | 500µs  | 600µs | 300µs  | 4.1ms
-      --------------------------------------------------------------------
-      GetUser0 [duration]  | 497 | 200µs | 500µs  | 600µs | 300µs  | 2.5ms
-      --------------------------------------------------------------------
-      GetUsers [duration]  | 495 | 200µs | 600µs  | 700µs | 300µs  | 2.9ms
-```
+## Dubbo
 
-* grpc:
+| 方法 | Min | Median | Mean | StdDev | Max |
+| --- | --- | --- | --- | --- | --- |
+| GetUser(经 Pixiu) | 200µs | **400µs** | 600µs | 1.5ms | 14.2ms |
+| GetGender(经 Pixiu) | 200µs | **300µs** | 400µs | 500µs | 4.3ms |
+| GetUser0(经 Pixiu) | 200µs | 400µs | 400µs | 200µs | 1.6ms |
+| GetUsers(经 Pixiu) | 200µs | 400µs | 400µs | 200µs | 1.7ms |
+| GetUser(直连) | 100µs | 200µs | 200µs | 100µs | 500µs |
 
-```
-grpc protocol performance test
-      Name                     | N   | Min   | Median | Mean  | StdDev | Max  
-      ========================================================================
-      GetUser [duration]       | 500 | 100µs | 300µs  | 400µs | 600µs  | 5.2ms
-      ------------------------------------------------------------------------
-      GetUsers [duration]      | 500 | 100µs | 300µs  | 300µs | 200µs  | 1.5ms
-      ------------------------------------------------------------------------
-      GetUserByName [duration] | 496 | 100µs | 200µs  | 300µs | 100µs  | 1ms  
-
-pixiu to grpc protocol performance test
-      Name                     | N   | Min   | Median | Mean  | StdDev | Max   
-      =========================================================================
-      GetUser [duration]       | 500 | 600µs | 2ms    | 3.4ms | 4.3ms  | 25.9ms
-      -------------------------------------------------------------------------
-      GetUsers [duration]      | 500 | 600µs | 1.3ms  | 2.1ms | 3.3ms  | 25.1ms
-      -------------------------------------------------------------------------
-      GetUserByName [duration] | 500 | 800µs | 1.9ms  | 2.7ms | 3.8ms  | 33.4ms
-```
+对比 issue #820 中的 Dubbo 数据:GetUser `600µs → 400µs`(约 1.5x),GetGender `500µs → 
300µs`(约 1.7x)。
 
-* triple:
+## gRPC
 
-```
-triple protocol performance test
-      Name                | N   | Min   | Median | Mean  | StdDev | Max  
-      ===================================================================
-      SayHello [duration] | 500 | 100µs | 200µs  | 300µs | 300µs  | 2.6ms
-
-pixiu to triple protocol performance test
-      Name                | N   | Min    | Median | Mean | StdDev | Max   
-      ====================================================================
-      SayHello [duration] | 490 | 10.1ms | 12.7ms | 14ms | 4.1ms  | 43.3ms
-```
+| 方法 | Min | Median | Mean | StdDev | Max |
+| --- | --- | --- | --- | --- | --- |
+| GetUser(经 Pixiu) | 200µs | **600µs** | 800µs | 1.1ms | 8.3ms |
+| GetUsers(经 Pixiu) | 200µs | **600µs** | 700µs | 400µs | 3.6ms |
+| GetUserByName(经 Pixiu) | 200µs | 400µs | 500µs | 300µs | 2ms |
+| GetUser(直连) | 100µs | 200µs | 200µs | 400µs | 2.8ms |
+
+## Triple
+
+| 方法 | Min | Median | Mean | StdDev | Max |
+| --- | --- | --- | --- | --- | --- |
+| GetUser(经 Pixiu) | 200µs | **500µs** | 600µs | 300µs | 2.5ms |
+| GetUsers(经 Pixiu) | 200µs | **500µs** | 500µs | 200µs | 1.7ms |
+| SayHello(经 Pixiu) | 300µs | **800µs** | 900µs | 500µs | 7ms |
+| GetUser(直连) | 200µs | 500µs | 600µs | 300µs | 2.5ms |
+
+相较 issue #820 中 Triple `SayHello` 的 `12.7ms`,最新结果为 `800µs`,约降低 
**15.9x**。汇总数据按同一方法对比:Dubbo/gRPC 使用 `GetUser`,Triple 使用 `SayHello`。
 
 # 运行方法
 
@@ -100,4 +68,4 @@ go test -v grpc_suite/grpc_test.go
 
 # 运行 triple 测试
 go test -v triple_suite/proto_suite/proto_test.go 
-```
\ No newline at end of file
+```
diff --git a/tools/benchmark/protocol/grpc/pixiu/conf/config.yaml 
b/tools/benchmark/protocol/grpc/pixiu/conf/config.yaml
index 62fe14036..bb8dcd258 100644
--- a/tools/benchmark/protocol/grpc/pixiu/conf/config.yaml
+++ b/tools/benchmark/protocol/grpc/pixiu/conf/config.yaml
@@ -24,7 +24,7 @@ static_resources:
       address:
         socket_address:
           address: "0.0.0.0"
-          port: 8881
+          port: 8882
       filter_chains:
           filters:
             - name: dgp.filter.httpconnectionmanager
diff --git a/tools/benchmark/protocol/triple/go-server/cmd/server.go 
b/tools/benchmark/protocol/triple/go-server/cmd/server.go
index b50bdfd11..681d3780d 100644
--- a/tools/benchmark/protocol/triple/go-server/cmd/server.go
+++ b/tools/benchmark/protocol/triple/go-server/cmd/server.go
@@ -42,7 +42,7 @@ func main() {
        // Create server using new API
        srv, err := server.NewServer(
                server.WithServerProtocol(
-                       protocol.WithPort(20000),
+                       protocol.WithPort(20010),
                        protocol.WithTriple(),
                ),
        )
@@ -62,7 +62,7 @@ func main() {
                }
        }()
 
-       fmt.Println("triple benchmark server is now running on :20000...")
+       fmt.Println("triple benchmark server is now running on :20010...")
        initSignal()
 }
 
diff --git a/tools/benchmark/protocol/triple/pixiu/conf/config.yaml 
b/tools/benchmark/protocol/triple/pixiu/conf/config.yaml
index e2ee05ccb..1f49ab696 100644
--- a/tools/benchmark/protocol/triple/pixiu/conf/config.yaml
+++ b/tools/benchmark/protocol/triple/pixiu/conf/config.yaml
@@ -24,7 +24,7 @@ static_resources:
       address:
         socket_address:
           address: "0.0.0.0"
-          port: 8881
+          port: 8883
       filter_chains:
           filters:
             - name: dgp.filter.httpconnectionmanager
@@ -52,7 +52,7 @@ static_resources:
       endpoints:
         - socket_address:
             address: 127.0.0.1
-            port: 20000
+            port: 20010
             protocol_type: "TRIPLE"
   shutdown_config:
     timeout: "60s"
diff --git a/tools/benchmark/test/dubbo_suite/dubbo_test.go 
b/tools/benchmark/test/dubbo_suite/dubbo_test.go
index 26caebb5a..4f68bfccd 100644
--- a/tools/benchmark/test/dubbo_suite/dubbo_test.go
+++ b/tools/benchmark/test/dubbo_suite/dubbo_test.go
@@ -70,7 +70,7 @@ var _ = Describe("Dubbo protocol performance test", Ordered, 
func() {
                dubboServerSession = prepareDubboServer()
                time.Sleep(5 * time.Second)
 
-               pixiuSession = test.PreparePixiu("../../dist/pixiu", 
test.CurPath+"/../../protocol/dubbo/pixiu/conf/config.yaml")
+               pixiuSession = test.PreparePixiu("../../dist/pixiu", 
test.CurPath+"/../../protocol/dubbo/pixiu/conf/config.yaml", 18380)
                time.Sleep(5 * time.Second)
        })
 
diff --git a/tools/benchmark/test/grpc_suite/grpc_test.go 
b/tools/benchmark/test/grpc_suite/grpc_test.go
index 768199c32..c82eec18f 100644
--- a/tools/benchmark/test/grpc_suite/grpc_test.go
+++ b/tools/benchmark/test/grpc_suite/grpc_test.go
@@ -65,7 +65,7 @@ var _ = Describe("gRPC protocol performance test", Ordered, 
func() {
 
                time.Sleep(1 * time.Second)
 
-               pixiuSession = test.PreparePixiu("../../dist/pixiu", 
test.CurPath+"/../../protocol/grpc/pixiu/conf/config.yaml")
+               pixiuSession = test.PreparePixiu("../../dist/pixiu", 
test.CurPath+"/../../protocol/grpc/pixiu/conf/config.yaml", 18381)
 
                time.Sleep(3 * time.Second)
 
@@ -133,7 +133,7 @@ var _ = Describe("gRPC protocol performance test", Ordered, 
func() {
                experiment := gmeasure.NewExperiment("pixiu to gRPC protocol 
performance test")
                AddReportEntry(experiment.Name, experiment)
 
-               urlPrefix := 
"http://localhost:8881/api/v1/benchmark.BenchmarkService/";
+               urlPrefix := 
"http://localhost:8882/api/v1/benchmark.BenchmarkService/";
 
                experiment.Sample(func(idx int) {
                        defer GinkgoRecover()
diff --git a/tools/benchmark/test/test_tool.go 
b/tools/benchmark/test/test_tool.go
index 7a45806a2..51cd9e965 100644
--- a/tools/benchmark/test/test_tool.go
+++ b/tools/benchmark/test/test_tool.go
@@ -20,6 +20,7 @@ package test
 import (
        "io"
        "os/exec"
+       "strconv"
        "time"
 )
 
@@ -38,8 +39,8 @@ var (
        }
 )
 
-func PreparePixiu(pixiu, path string) *gexec.Session {
-       command := exec.Command(pixiu, "gateway", "start", "-c", path)
+func PreparePixiu(pixiu, path string, reloadPort int) *gexec.Session {
+       command := exec.Command(pixiu, "gateway", "start", "-c", path, 
"--reload-port", strconv.Itoa(reloadPort))
        session, err := gexec.Start(command, io.Discard, io.Discard)
        //session, err := gexec.Start(command, os.Stdout, os.Stderr)
        gomega.Expect(err).NotTo(gomega.HaveOccurred())
diff --git a/tools/benchmark/test/triple_suite/proto_suite/proto_test.go 
b/tools/benchmark/test/triple_suite/proto_suite/proto_test.go
index 3e748f220..1d8105d34 100644
--- a/tools/benchmark/test/triple_suite/proto_suite/proto_test.go
+++ b/tools/benchmark/test/triple_suite/proto_suite/proto_test.go
@@ -62,20 +62,20 @@ var _ = Describe("triple protocol performance test", 
Ordered, func() {
                test.CurPath, err = os.Getwd()
                gomega.Expect(err).NotTo(gomega.HaveOccurred())
 
-               waitForPortAvailable("20000", 10*time.Second)
-               waitForPortAvailable("8881", 10*time.Second)
+               waitForPortAvailable("20010", 10*time.Second)
+               waitForPortAvailable("8883", 10*time.Second)
 
                tripleServerSession = prepareTripleServer()
                time.Sleep(8 * time.Second)
 
-               pixiuSession = test.PreparePixiu("../../../dist/pixiu", 
test.CurPath+"/../../../protocol/triple/pixiu/conf/config.yaml")
+               pixiuSession = test.PreparePixiu("../../../dist/pixiu", 
test.CurPath+"/../../../protocol/triple/pixiu/conf/config.yaml", 18382)
                time.Sleep(6 * time.Second)
        })
 
        It("pixiu to triple protocol performance test", func() {
                defer GinkgoRecover()
 
-               urlPrefix := 
"http://localhost:8881/dubbo.io/benchmark.BenchmarkService/%s";
+               urlPrefix := 
"http://localhost:8883/dubbo.io/benchmark.BenchmarkService/%s";
 
                experiment := gmeasure.NewExperiment("pixiu to triple protocol 
performance test")
                AddReportEntry(experiment.Name, experiment)
@@ -219,7 +219,7 @@ func prepareTripleServer() *gexec.Session {
 func prepareTripleClient() {
        // Create client using new API
        cli, err := client.NewClient(
-               client.WithClientURL("tri://127.0.0.1:20000"),
+               client.WithClientURL("tri://127.0.0.1:20010"),
        )
        gomega.Expect(err).NotTo(gomega.HaveOccurred())
 

Reply via email to