Skip to content

Commit 17be90b

Browse files
authored
fix(gRPC): connection pool leak when connection is closed and there are no more subsequent calls (#1945)
1 parent 73d007a commit 17be90b

9 files changed

Lines changed: 1269 additions & 188 deletions

File tree

pkg/remote/trans/nphttp2/conn_pool.go

Lines changed: 150 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,16 @@ import (
3030

3131
"github.com/cloudwego/kitex/pkg/klog"
3232
"github.com/cloudwego/kitex/pkg/remote"
33+
"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/codes"
3334
"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/grpc"
35+
"github.com/cloudwego/kitex/pkg/remote/trans/nphttp2/status"
3436
"github.com/cloudwego/kitex/pkg/rpcinfo"
3537
)
3638

37-
var _ remote.LongConnPool = &connPool{}
39+
const (
40+
poolOpen int32 = 0
41+
poolClosed int32 = 1
42+
)
3843

3944
func poolSize() uint32 {
4045
// One connection per processor, and need redundancy。
@@ -60,84 +65,20 @@ func NewConnPool(remoteService string, size uint32, connOpts grpc.ConnectOptions
6065
}
6166
}
6267

63-
// MuxPool manages a pool of long connections.
68+
// connPool manages a pool of gRPC long connections.
6469
type connPool struct {
6570
size uint32
6671
sfg singleflight.Group
6772
conns sync.Map // key: address, value: *transports
6873
remoteService string // remote service name
6974
connOpts grpc.ConnectOptions
75+
closed int32 // 1 means connPool has been closed
7076
}
7177

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-
109-
var _ remote.LongConnPool = (*connPool)(nil)
110-
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-
}
78+
var (
79+
_ remote.LongConnPool = (*connPool)(nil)
80+
errConnPoolClosed = status.Err(codes.Aborted, "connection pool has been closed")
81+
)
14182

14283
// Get pick or generate a net.Conn and return
14384
func (p *connPool) Get(ctx context.Context, network, address string, opt remote.ConnOption) (net.Conn, error) {
@@ -146,48 +87,74 @@ func (p *connPool) Get(ctx context.Context, network, address string, opt remote.
14687
}
14788

14889
var (
149-
trans *transports
150-
conn *clientConn
151-
err error
90+
tr grpc.ClientTransport
91+
idx uint32
92+
conn *clientConn
93+
err error
15294
)
15395

96+
// there is no need to check whether connPool has been closed
97+
// because connPool would only be closed when Kitex Client is GCed
15498
v, ok := p.conns.Load(address)
15599
if ok {
156-
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())
100+
trans := v.(*transports)
101+
tr, idx = trans.getActiveTransport()
102+
if tr != nil {
103+
// Actually new a stream, reuse the connection (grpc.ClientTransport)
104+
conn, err = newClientConn(ctx, tr, address)
105+
if err == nil {
106+
return conn, nil
107+
}
108+
109+
// when stream creations failed:
110+
// - gRPC Connection closed or draining: create a new connection
111+
// - ctx canceled: the request lifecycle has ended, exit immediately
112+
select {
113+
// ctx provided by users is canceled, we should not try to create a new gRPC connection
114+
case <-ctx.Done():
115+
return nil, err
116+
default:
165117
}
118+
klog.CtxDebugf(ctx, "KITEX: New grpc stream failed, network=%s, address=%s, error=%s", network, address, err.Error())
166119
}
167120
}
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
121+
rawTr, dErr, _ := p.sfg.Do(address, func() (i interface{}, e error) {
122+
var trans *transports
123+
var isNew bool
124+
// avoid creating duplicate transports
125+
if existTrans, ok := p.conns.Load(address); ok {
126+
trans = existTrans.(*transports)
127+
} else {
128+
trans = newTransports(p.size)
129+
isNew = true
174130
}
175-
if trans == nil {
176-
trans = &transports{
177-
size: p.size,
178-
cliTransports: make([]grpc.ClientTransport, p.size),
131+
132+
res, cErr := trans.createTransport(idx, p.remoteService, opt.Dialer, network, address, opt.ConnectTimeout, p.connOpts)
133+
if cErr != nil {
134+
return nil, cErr
135+
}
136+
137+
if isNew {
138+
// Store first, then recheck closed state to eliminate TOCTOU:
139+
// if Close() finished its Range between our earlier check and this store,
140+
// self-clean here to prevent orphaned transports.
141+
p.conns.LoadOrStore(address, trans)
142+
if p.isClosed() {
143+
if recheckV, recheckOK := p.conns.LoadAndDelete(address); recheckOK {
144+
recheckV.(*transports).close()
145+
}
146+
return nil, errConnPoolClosed
179147
}
180148
}
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
149+
150+
return res, nil
184151
})
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
152+
if dErr != nil {
153+
klog.CtxErrorf(ctx, "KITEX: New grpc client connection failed, network=%s, address=%s, error=%s", network, address, dErr.Error())
154+
return nil, dErr
188155
}
189156
klog.CtxDebugf(ctx, "KITEX: New grpc client connection succeed, network=%s, address=%s", network, address)
190-
return newClientConn(ctx, tr.(grpc.ClientTransport), address)
157+
return newClientConn(ctx, rawTr.(grpc.ClientTransport), address)
191158
}
192159

193160
// Put implements the ConnPool interface.
@@ -205,9 +172,7 @@ func (p *connPool) release(conn net.Conn) error {
205172
}
206173

207174
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)
175+
tr, err := newTransport(p.remoteService, opt.Dialer, network, address, opt.ConnectTimeout, p.connOpts, nil, nil)
211176
if err != nil {
212177
return nil, err
213178
}
@@ -224,14 +189,17 @@ func (p *connPool) Discard(conn net.Conn) error {
224189

225190
// Clean implements the LongConnPool interface.
226191
func (p *connPool) Clean(network, address string) {
227-
if v, ok := p.conns.Load(address); ok {
228-
p.conns.Delete(address)
192+
if v, ok := p.conns.LoadAndDelete(address); ok {
229193
v.(*transports).close()
230194
}
231195
}
232196

233197
// Close is to release resource of ConnPool, it is executed when client is closed.
234198
func (p *connPool) Close() error {
199+
if !p.casClosed() {
200+
return nil
201+
}
202+
235203
p.conns.Range(func(addr, trans interface{}) bool {
236204
p.conns.Delete(addr)
237205
trans.(*transports).close()
@@ -240,6 +208,19 @@ func (p *connPool) Close() error {
240208
return nil
241209
}
242210

211+
func (p *connPool) isClosed() bool {
212+
return atomic.LoadInt32(&p.closed) == poolClosed
213+
}
214+
215+
func (p *connPool) casClosed() bool {
216+
return atomic.CompareAndSwapInt32(&p.closed, poolOpen, poolClosed)
217+
}
218+
219+
type dumpEntry struct {
220+
addr string
221+
tr grpc.ClientTransport
222+
}
223+
243224
// Dump dumps the connection pool with the details of the underlying transport.
244225
func (p *connPool) Dump() interface{} {
245226
defer func() {
@@ -251,32 +232,70 @@ func (p *connPool) Dump() interface{} {
251232
// remoteAddress -> []clientTransport, where each clientTransport is a connection. Distinguish the connection via localAddress.
252233
// If mesh egress is not enabled, toAddr should be the address of the callee service.
253234
// Otherwise, toAddr will be the same, so you should check the remoteAddress in each stream, which is read from the header.
235+
236+
// sync.Map does not expose its length directly.
237+
// Since dump is a cold-path operation, performance is not a major concern here
254238
poolDump := make(map[string]interface{}, p.size)
239+
var cliTransDumps []dumpEntry
255240
p.conns.Range(func(k, v interface{}) bool {
256241
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
242+
for _, tr := range v.(*transports).loadAll() {
243+
cliTransDumps = append(cliTransDumps, dumpEntry{addr: addr, tr: tr})
274244
}
275245
return true
276246
})
247+
248+
for _, cliTransDump := range cliTransDumps {
249+
dumper, ok := cliTransDump.tr.(interface{ Dump() interface{} })
250+
if !ok {
251+
continue
252+
}
253+
var curr []interface{}
254+
if poolDump[cliTransDump.addr] == nil {
255+
curr = make([]interface{}, 0)
256+
} else {
257+
curr = poolDump[cliTransDump.addr].([]interface{})
258+
}
259+
curr = append(curr, dumper.Dump())
260+
poolDump[cliTransDump.addr] = curr
261+
}
277262
return poolDump
278263
}
279264

265+
// newTransport creates a gRPC connection
266+
func newTransport(remoteService string,
267+
dialer remote.Dialer, network, address string, connectTimeout time.Duration, opts grpc.ConnectOptions,
268+
onGoAway func(context.Context, grpc.ClientTransport, grpc.GoAwayReason),
269+
onClose func(context.Context, grpc.ClientTransport, error),
270+
) (grpc.ClientTransport, error) {
271+
conn, err := dialer.DialTimeout(network, address, connectTimeout)
272+
if err != nil {
273+
return nil, err
274+
}
275+
if opts.TLSConfig != nil {
276+
tlsConn, tErr := newTLSConn(conn, opts.TLSConfig)
277+
if tErr != nil {
278+
// release tls handshake failed connection
279+
cErr := conn.Close()
280+
if cErr != nil {
281+
klog.Warnf("KITEX: Close TLS handshake failed connection, err: %v", cErr)
282+
}
283+
return nil, tErr
284+
}
285+
conn = tlsConn
286+
}
287+
return grpc.NewClientTransportWithConfig(
288+
context.Background(), // gRPC connection does not need to be bound to a specific ctx
289+
conn,
290+
opts,
291+
grpc.ClientConfig{
292+
RemoteService: remoteService,
293+
OnGoAway: onGoAway,
294+
OnClose: onClose,
295+
},
296+
)
297+
}
298+
280299
// newTLSConn constructs a client-side TLS connection and performs handshake.
281300
func newTLSConn(conn net.Conn, tlsCfg *tls.Config) (net.Conn, error) {
282301
tlsConn := tls.Client(conn, tlsCfg)
@@ -285,3 +304,12 @@ func newTLSConn(conn net.Conn, tlsCfg *tls.Config) (net.Conn, error) {
285304
}
286305
return tlsConn, nil
287306
}
307+
308+
func checkActive(trans grpc.ClientTransport) bool {
309+
if trans == nil {
310+
return false
311+
}
312+
// grpc.ClientTransport is implemented by *http2Client in pkg/remote/trans/nphttp2/grpc
313+
// it implements grpc.IsActive
314+
return trans.(grpc.IsActive).IsActive()
315+
}

0 commit comments

Comments
 (0)