Skip to content

Commit 6ead596

Browse files
committed
optimize: fix gRPC conn pool leak
Previously a closed/broken transport stayed in the nphttp2 client pool indefinitely, because only a later put() could overwrite the slot. A new OnClose callback now evicts the transport via transports.remove() as soon as it closes, and all cliTransports accesses are serialized by a new sync.RWMutex. To avoid a lock-order inversion between transports.mu (which doPut holds while calling IsActive -> http2Client.mu) and the onClose -> remove path (which takes transports.mu while http2Client.Close historically held t.mu), http2Client.Close now releases t.mu before invoking onClose. The invariant transports.mu -> http2Client.mu is documented on doPut, remove, close, and http2Client.Close. GracefulClose is now invoked outside transports.mu in both put() and close() to avoid re-entering the mutex through onClose -> remove on transports with zero active streams. Dump() snapshots (addr, transport) refs under RLock and calls each transport's Dump() after releasing the lock. The pool-side callback signatures now take (ctx, trans, err) instead of (). A new grpc.ClientConfig + NewClientTransportWithConfig expose the new shape; the deprecated NewClientTransport is preserved as a thin adapter. Note: the deprecated NewClientTransport path has a visible timing change - onClose is now invoked after state is set to closing and after http2Client.mu is released, whereas it previously ran before the state change while the mutex was held. Callers relying on the old ordering must migrate to NewClientTransportWithConfig.
1 parent a86ecf4 commit 6ead596

9 files changed

Lines changed: 938 additions & 154 deletions

File tree

pkg/remote/trans/nphttp2/conn_pool.go

Lines changed: 90 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"runtime"
2424
"runtime/debug"
2525
"sync"
26-
"sync/atomic"
2726
"time"
2827

2928
"golang.org/x/sync/singleflight"
@@ -60,7 +59,7 @@ func NewConnPool(remoteService string, size uint32, connOpts grpc.ConnectOptions
6059
}
6160
}
6261

63-
// MuxPool manages a pool of long connections.
62+
// connPool manages a pool of gRPC long connections.
6463
type connPool struct {
6564
size uint32
6665
sfg singleflight.Group
@@ -69,76 +68,8 @@ type connPool struct {
6968
connOpts grpc.ConnectOptions
7069
}
7170

72-
type transports struct {
73-
index uint32
74-
size uint32
75-
cliTransports []grpc.ClientTransport
76-
}
77-
78-
// get connection from the pool, load balance with round-robin.
79-
func (t *transports) get() grpc.ClientTransport {
80-
idx := atomic.AddUint32(&t.index, 1)
81-
return t.cliTransports[idx%t.size]
82-
}
83-
84-
// put find the first empty position to put the connection to the pool.
85-
func (t *transports) put(trans grpc.ClientTransport) {
86-
for i := 0; i < int(t.size); i++ {
87-
cliTransport := t.cliTransports[i]
88-
if cliTransport == nil {
89-
t.cliTransports[i] = trans
90-
return
91-
}
92-
if !cliTransport.(grpc.IsActive).IsActive() {
93-
t.cliTransports[i].GracefulClose()
94-
t.cliTransports[i] = trans
95-
return
96-
}
97-
}
98-
}
99-
100-
// close all connections of the pool.
101-
func (t *transports) close() {
102-
for i := range t.cliTransports {
103-
if c := t.cliTransports[i]; c != nil {
104-
c.GracefulClose()
105-
}
106-
}
107-
}
108-
10971
var _ remote.LongConnPool = (*connPool)(nil)
11072

111-
func (p *connPool) newTransport(ctx context.Context, dialer remote.Dialer, network, address string,
112-
connectTimeout time.Duration, opts grpc.ConnectOptions,
113-
) (grpc.ClientTransport, error) {
114-
conn, err := dialer.DialTimeout(network, address, connectTimeout)
115-
if err != nil {
116-
return nil, err
117-
}
118-
if opts.TLSConfig != nil {
119-
tlsConn, err := newTLSConn(conn, opts.TLSConfig)
120-
if err != nil {
121-
return nil, err
122-
}
123-
conn = tlsConn
124-
}
125-
return grpc.NewClientTransport(
126-
ctx,
127-
conn,
128-
opts,
129-
p.remoteService,
130-
func(grpc.GoAwayReason) {
131-
// remove connection from the pool.
132-
// we do not need to close this grpc transport manually
133-
// since grpc client is responsible for doing this.
134-
p.conns.Delete(address)
135-
},
136-
func() {
137-
// do nothing
138-
},
139-
)
140-
}
141-
14273
// Get pick or generate a net.Conn and return
14374
func (p *connPool) Get(ctx context.Context, network, address string, opt remote.ConnOption) (net.Conn, error) {
14475
if p.connOpts.ShortConn {
@@ -147,47 +78,47 @@ func (p *connPool) Get(ctx context.Context, network, address string, opt remote.
14778

14879
var (
14980
trans *transports
81+
tr grpc.ClientTransport
82+
idx uint32
15083
conn *clientConn
15184
err error
15285
)
15386

15487
v, ok := p.conns.Load(address)
15588
if ok {
15689
trans = v.(*transports)
157-
if tr := trans.get(); tr != nil {
158-
if tr.(grpc.IsActive).IsActive() {
159-
// Actually new a stream, reuse the connection (grpc.ClientTransport)
160-
conn, err = newClientConn(ctx, tr, address)
161-
if err == nil {
162-
return conn, nil
163-
}
164-
klog.CtxDebugf(ctx, "KITEX: New grpc stream failed, network=%s, address=%s, error=%s", network, address, err.Error())
90+
tr, idx = trans.getActiveTransport()
91+
if tr != nil {
92+
// Actually new a stream, reuse the connection (grpc.ClientTransport)
93+
conn, err = newClientConn(ctx, tr, address)
94+
if err == nil {
95+
return conn, nil
16596
}
97+
klog.CtxDebugf(ctx, "KITEX: New grpc stream failed, network=%s, address=%s, error=%s", network, address, err.Error())
16698
}
16799
}
168-
tr, err, _ := p.sfg.Do(address, func() (i interface{}, e error) {
169-
// Notice: newTransport means new a connection, the timeout of connection cannot be set,
170-
// so using context.Background() but not the ctx passed in as the parameter.
171-
tr, err := p.newTransport(context.Background(), opt.Dialer, network, address, opt.ConnectTimeout, p.connOpts)
172-
if err != nil {
173-
return nil, err
100+
rawTr, dErr, _ := p.sfg.Do(address, func() (i interface{}, e error) {
101+
// avoid creating duplicate transports
102+
if existTrans, ok := p.conns.Load(address); ok {
103+
trans = existTrans.(*transports)
104+
} else if trans == nil {
105+
trans = newTransports(p.size)
174106
}
175-
if trans == nil {
176-
trans = &transports{
177-
size: p.size,
178-
cliTransports: make([]grpc.ClientTransport, p.size),
179-
}
107+
108+
res, cErr := trans.createTransport(idx, p.remoteService, opt.Dialer, network, address, opt.ConnectTimeout, p.connOpts)
109+
if cErr != nil {
110+
return nil, cErr
180111
}
181-
trans.put(tr) // the tr (connection) maybe not in the pool, but can be recycled by keepalive.
182-
p.conns.Store(address, trans)
183-
return tr, nil
112+
113+
p.conns.LoadOrStore(address, trans)
114+
return res, nil
184115
})
185-
if err != nil {
186-
klog.CtxErrorf(ctx, "KITEX: New grpc client connection failed, network=%s, address=%s, error=%s", network, address, err.Error())
187-
return nil, err
116+
if dErr != nil {
117+
klog.CtxErrorf(ctx, "KITEX: New grpc client connection failed, network=%s, address=%s, error=%s", network, address, dErr.Error())
118+
return nil, dErr
188119
}
189120
klog.CtxDebugf(ctx, "KITEX: New grpc client connection succeed, network=%s, address=%s", network, address)
190-
return newClientConn(ctx, tr.(grpc.ClientTransport), address)
121+
return newClientConn(ctx, rawTr.(grpc.ClientTransport), address)
191122
}
192123

193124
// Put implements the ConnPool interface.
@@ -205,9 +136,7 @@ func (p *connPool) release(conn net.Conn) error {
205136
}
206137

207138
func (p *connPool) createShortConn(ctx context.Context, network, address string, opt remote.ConnOption) (net.Conn, error) {
208-
// Notice: newTransport means new a connection, the timeout of connection cannot be set,
209-
// so using context.Background() but not the ctx passed in as the parameter.
210-
tr, err := p.newTransport(context.Background(), opt.Dialer, network, address, opt.ConnectTimeout, p.connOpts)
139+
tr, err := newTransport(p.remoteService, opt.Dialer, network, address, opt.ConnectTimeout, p.connOpts, nil, nil)
211140
if err != nil {
212141
return nil, err
213142
}
@@ -224,8 +153,7 @@ func (p *connPool) Discard(conn net.Conn) error {
224153

225154
// Clean implements the LongConnPool interface.
226155
func (p *connPool) Clean(network, address string) {
227-
if v, ok := p.conns.Load(address); ok {
228-
p.conns.Delete(address)
156+
if v, ok := p.conns.LoadAndDelete(address); ok {
229157
v.(*transports).close()
230158
}
231159
}
@@ -240,6 +168,11 @@ func (p *connPool) Close() error {
240168
return nil
241169
}
242170

171+
type dumpEntry struct {
172+
addr string
173+
tr grpc.ClientTransport
174+
}
175+
243176
// Dump dumps the connection pool with the details of the underlying transport.
244177
func (p *connPool) Dump() interface{} {
245178
defer func() {
@@ -252,31 +185,61 @@ func (p *connPool) Dump() interface{} {
252185
// If mesh egress is not enabled, toAddr should be the address of the callee service.
253186
// Otherwise, toAddr will be the same, so you should check the remoteAddress in each stream, which is read from the header.
254187
poolDump := make(map[string]interface{}, p.size)
188+
var cliTransDumps []dumpEntry
255189
p.conns.Range(func(k, v interface{}) bool {
256190
addr := k.(string)
257-
ts := v.(*transports)
258-
for _, t := range ts.cliTransports {
259-
if t == nil {
260-
continue
261-
}
262-
dumper, ok := t.(interface{ Dump() interface{} })
263-
if !ok {
264-
continue
265-
}
266-
var curr []interface{}
267-
if poolDump[addr] == nil {
268-
curr = make([]interface{}, 0)
269-
} else {
270-
curr = poolDump[addr].([]interface{})
271-
}
272-
curr = append(curr, dumper.Dump())
273-
poolDump[addr] = curr
191+
for _, tr := range v.(*transports).loadAll() {
192+
cliTransDumps = append(cliTransDumps, dumpEntry{addr: addr, tr: tr})
274193
}
275194
return true
276195
})
196+
197+
for _, cliTransDump := range cliTransDumps {
198+
dumper, ok := cliTransDump.tr.(interface{ Dump() interface{} })
199+
if !ok {
200+
continue
201+
}
202+
var curr []interface{}
203+
if poolDump[cliTransDump.addr] == nil {
204+
curr = make([]interface{}, 0)
205+
} else {
206+
curr = poolDump[cliTransDump.addr].([]interface{})
207+
}
208+
curr = append(curr, dumper.Dump())
209+
poolDump[cliTransDump.addr] = curr
210+
}
277211
return poolDump
278212
}
279213

214+
// newTransport created a gRPC connection
215+
func newTransport(remoteService string,
216+
dialer remote.Dialer, network, address string, connectTimeout time.Duration, opts grpc.ConnectOptions,
217+
onGoAway func(context.Context, grpc.ClientTransport, grpc.GoAwayReason),
218+
onClose func(context.Context, grpc.ClientTransport, error),
219+
) (grpc.ClientTransport, error) {
220+
conn, err := dialer.DialTimeout(network, address, connectTimeout)
221+
if err != nil {
222+
return nil, err
223+
}
224+
if opts.TLSConfig != nil {
225+
tlsConn, err := newTLSConn(conn, opts.TLSConfig)
226+
if err != nil {
227+
return nil, err
228+
}
229+
conn = tlsConn
230+
}
231+
return grpc.NewClientTransportWithConfig(
232+
context.Background(), // gRPC connection does not need to be bound to a specific ctx
233+
conn,
234+
opts,
235+
grpc.ClientConfig{
236+
RemoteService: remoteService,
237+
OnGoAway: onGoAway,
238+
OnClose: onClose,
239+
},
240+
)
241+
}
242+
280243
// newTLSConn constructs a client-side TLS connection and performs handshake.
281244
func newTLSConn(conn net.Conn, tlsCfg *tls.Config) (net.Conn, error) {
282245
tlsConn := tls.Client(conn, tlsCfg)
@@ -285,3 +248,12 @@ func newTLSConn(conn net.Conn, tlsCfg *tls.Config) (net.Conn, error) {
285248
}
286249
return tlsConn, nil
287250
}
251+
252+
func checkActive(trans grpc.ClientTransport) bool {
253+
if trans == nil {
254+
return false
255+
}
256+
// grpc.ClientTransport is implemented by *http2Client in pkg/remote/trans/nphttp2/grpc
257+
// it has implements grpc.IsActive so we do not assert type
258+
return trans.(grpc.IsActive).IsActive()
259+
}

0 commit comments

Comments
 (0)