Skip to content

Commit fac48d2

Browse files
committed
add a local tlsTransportCache to available_controller
The available_controller creates short-lived clients to sync remote APIService objects. These clients are constructed with HTTP transports that cannot be cached by client-go (because client-go won't know whether the TLS configs have dynamic functions or not), which may spam idle connections. A local cache works because we know all the configs share the same dialer function, and can only vary on the dynamic cert/key.
1 parent 0765ba8 commit fac48d2

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed

staging/src/k8s.io/kube-aggregator/pkg/controllers/status/available_controller.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,53 @@ type AvailableConditionController struct {
8686
cache map[string]map[string][]string
8787
// this lock protects operations on the above cache
8888
cacheLock sync.RWMutex
89+
90+
// TLS config with customized dialer cannot be cached by the client-go
91+
// tlsTransportCache. Use a local cache here to reduce the chance of
92+
// the controller spamming idle connections with short-lived transports.
93+
// NOTE: the cache works because we assume that the transports constructed
94+
// by the controller only vary on the dynamic cert/key.
95+
tlsCache *tlsTransportCache
96+
}
97+
98+
type tlsTransportCache struct {
99+
mu sync.Mutex
100+
transports map[tlsCacheKey]http.RoundTripper
101+
}
102+
103+
func (c *tlsTransportCache) get(config *rest.Config) (http.RoundTripper, error) {
104+
// If the available controller doesn't customzie the dialer (and we know from
105+
// the code that the controller doesn't customzie other functions i.e. Proxy
106+
// and GetCert (ExecProvider)), the config is cacheable by the client-go TLS
107+
// transport cache. Let's skip the local cache and depend on the client-go cache.
108+
if config.Dial == nil {
109+
return rest.TransportFor(config)
110+
}
111+
c.mu.Lock()
112+
defer c.mu.Unlock()
113+
// See if we already have a custom transport for this config
114+
key := tlsConfigKey(config)
115+
if t, ok := c.transports[key]; ok {
116+
return t, nil
117+
}
118+
restTransport, err := rest.TransportFor(config)
119+
if err != nil {
120+
return nil, err
121+
}
122+
c.transports[key] = restTransport
123+
return restTransport, nil
124+
}
125+
126+
type tlsCacheKey struct {
127+
certData string
128+
keyData string
129+
}
130+
131+
func tlsConfigKey(c *rest.Config) tlsCacheKey {
132+
return tlsCacheKey{
133+
certData: string(c.TLSClientConfig.CertData),
134+
keyData: string(c.TLSClientConfig.KeyData),
135+
}
89136
}
90137

91138
// NewAvailableConditionController returns a new AvailableConditionController.
@@ -115,6 +162,7 @@ func NewAvailableConditionController(
115162
workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 30*time.Second),
116163
"AvailableConditionController"),
117164
proxyCurrentCertKeyContent: proxyCurrentCertKeyContent,
165+
tlsCache: &tlsTransportCache{transports: make(map[tlsCacheKey]http.RoundTripper)},
118166
}
119167

120168
if egressSelector != nil {
@@ -185,7 +233,12 @@ func (c *AvailableConditionController) sync(key string) error {
185233
if c.dialContext != nil {
186234
restConfig.Dial = c.dialContext
187235
}
188-
restTransport, err := rest.TransportFor(restConfig)
236+
// TLS config with customized dialer cannot be cached by the client-go
237+
// tlsTransportCache. Use a local cache here to reduce the chance of
238+
// the controller spamming idle connections with short-lived transports.
239+
// NOTE: the cache works because we assume that the transports constructed
240+
// by the controller only vary on the dynamic cert/key.
241+
restTransport, err := c.tlsCache.get(restConfig)
189242
if err != nil {
190243
return err
191244
}

staging/src/k8s.io/kube-aggregator/pkg/controllers/status/available_controller_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package apiserver
1818

1919
import (
2020
"fmt"
21+
"net"
2122
"net/http"
2223
"net/http/httptest"
2324
"net/url"
@@ -133,6 +134,7 @@ func setupAPIServices(apiServices []*apiregistration.APIService) (*AvailableCond
133134
// the maximum disruption time to a minimum, but it does prevent hot loops.
134135
workqueue.NewItemExponentialFailureRateLimiter(5*time.Millisecond, 30*time.Second),
135136
"AvailableConditionController"),
137+
tlsCache: &tlsTransportCache{transports: make(map[tlsCacheKey]http.RoundTripper)},
136138
}
137139
for _, svc := range apiServices {
138140
c.addAPIService(svc)
@@ -202,6 +204,55 @@ func TestBuildCache(t *testing.T) {
202204
})
203205
}
204206
}
207+
208+
func TestTLSCache(t *testing.T) {
209+
apiServices := []*apiregistration.APIService{newRemoteAPIService("remote.group")}
210+
services := []*v1.Service{newService("foo", "bar", testServicePort, testServicePortName)}
211+
c, _ := setupAPIServices(apiServices)
212+
// TLS configs with customized dialers are uncacheable by the client-go
213+
// TLS transport cache. The local cache will be used.
214+
c.dialContext = (&net.Dialer{
215+
Timeout: 30 * time.Second,
216+
KeepAlive: 30 * time.Second,
217+
}).DialContext
218+
for _, svc := range services {
219+
c.addService(svc)
220+
}
221+
tests := []struct {
222+
name string
223+
proxyCurrentCertKeyContent certKeyFunc
224+
expectedCacheSize int
225+
}{
226+
{
227+
name: "nil certKeyFunc",
228+
expectedCacheSize: 1,
229+
},
230+
{
231+
name: "empty certKeyFunc",
232+
proxyCurrentCertKeyContent: func() ([]byte, []byte) { return emptyCert(), emptyCert() },
233+
// the tlsCacheKey is the same, reuse existing transport
234+
expectedCacheSize: 1,
235+
},
236+
{
237+
name: "different certKeyFunc",
238+
proxyCurrentCertKeyContent: testCertKeyFunc,
239+
// the tlsCacheKey is different, create a new transport
240+
expectedCacheSize: 2,
241+
},
242+
}
243+
for _, tc := range tests {
244+
t.Run(tc.name, func(t *testing.T) {
245+
c.proxyCurrentCertKeyContent = tc.proxyCurrentCertKeyContent
246+
for _, apiService := range apiServices {
247+
c.sync(apiService.Name)
248+
}
249+
if len(c.tlsCache.transports) != tc.expectedCacheSize {
250+
t.Fatalf("%v cache size expected %v, got %v", tc.name, tc.expectedCacheSize, len(c.tlsCache.transports))
251+
}
252+
})
253+
}
254+
}
255+
205256
func TestSync(t *testing.T) {
206257
tests := []struct {
207258
name string
@@ -356,6 +407,7 @@ func TestSync(t *testing.T) {
356407
endpointsLister: v1listers.NewEndpointsLister(endpointsIndexer),
357408
serviceResolver: &fakeServiceResolver{url: testServer.URL},
358409
proxyCurrentCertKeyContent: func() ([]byte, []byte) { return emptyCert(), emptyCert() },
410+
tlsCache: &tlsTransportCache{transports: make(map[tlsCacheKey]http.RoundTripper)},
359411
}
360412
c.sync(tc.apiServiceName)
361413

@@ -420,3 +472,33 @@ func TestUpdateAPIServiceStatus(t *testing.T) {
420472
func emptyCert() []byte {
421473
return []byte{}
422474
}
475+
476+
func testCertKeyFunc() ([]byte, []byte) {
477+
return []byte(`-----BEGIN CERTIFICATE-----
478+
MIICBDCCAW2gAwIBAgIJAPgVBh+4xbGoMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
479+
BAMMEHdlYmhvb2tfdGVzdHNfY2EwIBcNMTcwNzI4MjMxNTI4WhgPMjI5MTA1MTMy
480+
MzE1MjhaMB8xHTAbBgNVBAMMFHdlYmhvb2tfdGVzdHNfY2xpZW50MIGfMA0GCSqG
481+
SIb3DQEBAQUAA4GNADCBiQKBgQDkGXXSm6Yun5o3Jlmx45rItcQ2pmnoDk4eZfl0
482+
rmPa674s2pfYo3KywkXQ1Fp3BC8GUgzPLSfJ8xXya9Lg1Wo8sHrDln0iRg5HXxGu
483+
uFNhRBvj2S0sIff0ZG/IatB9I6WXVOUYuQj6+A0CdULNj1vBqH9+7uWbLZ6lrD4b
484+
a44x/wIDAQABo0owSDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAU
485+
BggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0B
486+
AQsFAAOBgQCpN27uh/LjUVCaBK7Noko25iih/JSSoWzlvc8CaipvSPofNWyGx3Vu
487+
OdcSwNGYX/pp4ZoAzFij/Y5u0vKTVLkWXATeTMVmlPvhmpYjj9gPkCSY6j/SiKlY
488+
kGy0xr+0M5UQkMBcfIh9oAp9um1fZHVWAJAGP/ikZgkcUey0LmBn8w==
489+
-----END CERTIFICATE-----`), []byte(`-----BEGIN RSA PRIVATE KEY-----
490+
MIICWwIBAAKBgQDkGXXSm6Yun5o3Jlmx45rItcQ2pmnoDk4eZfl0rmPa674s2pfY
491+
o3KywkXQ1Fp3BC8GUgzPLSfJ8xXya9Lg1Wo8sHrDln0iRg5HXxGuuFNhRBvj2S0s
492+
Iff0ZG/IatB9I6WXVOUYuQj6+A0CdULNj1vBqH9+7uWbLZ6lrD4ba44x/wIDAQAB
493+
AoGAZbWwowvCq1GBq4vPPRI3h739Uz0bRl1ymf1woYXNguXRtCB4yyH+2BTmmrrF
494+
6AIWkePuUEdbUaKyK5nGu3iOWM+/i6NP3kopQANtbAYJ2ray3kwvFlhqyn1bxX4n
495+
gl/Cbdw1If4zrDrB66y8mYDsjzK7n/gFaDNcY4GArjvOXKkCQQD9Lgv+WD73y4RP
496+
yS+cRarlEeLLWVsX/pg2oEBLM50jsdUnrLSW071MjBgP37oOXzqynF9SoDbP2Y5C
497+
x+aGux9LAkEA5qPlQPv0cv8Wc3qTI+LixZ/86PPHKWnOnwaHm3b9vQjZAkuVQg3n
498+
Wgg9YDmPM87t3UFH7ZbDihUreUxwr9ZjnQJAZ9Z95shMsxbOYmbSVxafu6m1Sc+R
499+
M+sghK7/D5jQpzYlhUspGf8n0YBX0hLhXUmjamQGGH5LXL4Owcb4/mM6twJAEVio
500+
SF/qva9jv+GrKVrKFXT374lOJFY53Qn/rvifEtWUhLCslCA5kzLlctRBafMZPrfH
501+
Mh5RrJP1BhVysDbenQJASGcc+DiF7rB6K++ZGyC11E2AP29DcZ0pgPESSV7npOGg
502+
+NqPRZNVCSZOiVmNuejZqmwKhZNGZnBFx1Y+ChAAgw==
503+
-----END RSA PRIVATE KEY-----`)
504+
}

0 commit comments

Comments
 (0)