Skip to content

Commit 482ed05

Browse files
committed
introduce pprof server to manager
1 parent 2271827 commit 482ed05

File tree

4 files changed

+220
-0
lines changed

4 files changed

+220
-0
lines changed

pkg/manager/internal.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"net"
2525
"net/http"
26+
"net/http/pprof"
2627
"sync"
2728
"sync/atomic"
2829
"time"
@@ -106,6 +107,9 @@ type controllerManager struct {
106107
// Healthz probe handler
107108
healthzHandler *healthz.Handler
108109

110+
// pprofListener is used to serve pprof
111+
pprofListener net.Listener
112+
109113
// controllerConfig are the global controller options.
110114
controllerConfig config.Controller
111115

@@ -343,6 +347,24 @@ func (cm *controllerManager) serveHealthProbes() {
343347
go cm.httpServe("health probe", cm.logger, server, cm.healthProbeListener)
344348
}
345349

350+
func (cm *controllerManager) addPprofServer() error {
351+
mux := http.NewServeMux()
352+
srv := httpserver.New(mux)
353+
354+
mux.HandleFunc("/debug/pprof/", pprof.Index)
355+
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
356+
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
357+
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
358+
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
359+
360+
return cm.add(&server{
361+
Kind: "pprof",
362+
Log: cm.logger,
363+
Server: srv,
364+
Listener: cm.pprofListener,
365+
})
366+
}
367+
346368
func (cm *controllerManager) httpServe(kind string, log logr.Logger, server *http.Server, ln net.Listener) {
347369
log = log.WithValues("kind", kind, "addr", ln.Addr())
348370

@@ -440,6 +462,13 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) {
440462
cm.serveHealthProbes()
441463
}
442464

465+
// Add pprof server
466+
if cm.pprofListener != nil {
467+
if err := cm.addPprofServer(); err != nil {
468+
return fmt.Errorf("failed to add pprof server: %w", err)
469+
}
470+
}
471+
443472
// First start any webhook servers, which includes conversion, validation, and defaulting
444473
// webhooks that are registered.
445474
//

pkg/manager/manager.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ type Options struct {
233233
// Liveness probe endpoint name, defaults to "healthz"
234234
LivenessEndpointName string
235235

236+
// PprofBindAddress is the TCP address that the controller should bind to
237+
// for serving pprof.
238+
// It can be set to "" or "0" to disable the pprof serving.
239+
// Since pprof may contain sensitive information, make sure to protect it
240+
// before exposing it to public.
241+
PprofBindAddress string
242+
236243
// Port is the port that the webhook server serves at.
237244
// It is used to set webhook.Server.Port if WebhookServer is not set.
238245
Port int
@@ -308,6 +315,7 @@ type Options struct {
308315
newResourceLock func(config *rest.Config, recorderProvider recorder.Provider, options leaderelection.Options) (resourcelock.Interface, error)
309316
newMetricsListener func(addr string) (net.Listener, error)
310317
newHealthProbeListener func(addr string) (net.Listener, error)
318+
newPprofListener func(addr string) (net.Listener, error)
311319
}
312320

313321
// BaseContextFunc is a function used to provide a base Context to Runnables
@@ -417,6 +425,13 @@ func New(config *rest.Config, options Options) (Manager, error) {
417425
return nil, err
418426
}
419427

428+
// Create pprof listener. This will throw an error if the bind
429+
// address is invalid or already in use.
430+
pprofListener, err := options.newPprofListener(options.PprofBindAddress)
431+
if err != nil {
432+
return nil, fmt.Errorf("failed to new pprof listener: %w", err)
433+
}
434+
420435
errChan := make(chan error)
421436
runnables := newRunnables(options.BaseContext, errChan)
422437

@@ -444,6 +459,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
444459
healthProbeListener: healthProbeListener,
445460
readinessEndpointName: options.ReadinessEndpointName,
446461
livenessEndpointName: options.LivenessEndpointName,
462+
pprofListener: pprofListener,
447463
gracefulShutdownTimeout: *options.GracefulShutdownTimeout,
448464
internalProceduresStop: make(chan struct{}),
449465
leaderElectionStopped: make(chan struct{}),
@@ -574,6 +590,19 @@ func defaultHealthProbeListener(addr string) (net.Listener, error) {
574590
return ln, nil
575591
}
576592

593+
// defaultPprofListener creates the default pprof listener bound to the given address.
594+
func defaultPprofListener(addr string) (net.Listener, error) {
595+
if addr == "" || addr == "0" {
596+
return nil, nil
597+
}
598+
599+
ln, err := net.Listen("tcp", addr)
600+
if err != nil {
601+
return nil, fmt.Errorf("error listening on %s: %w", addr, err)
602+
}
603+
return ln, nil
604+
}
605+
577606
// defaultBaseContext is used as the BaseContext value in Options if one
578607
// has not already been set.
579608
func defaultBaseContext() context.Context {
@@ -634,6 +663,10 @@ func setOptionsDefaults(options Options) Options {
634663
options.newHealthProbeListener = defaultHealthProbeListener
635664
}
636665

666+
if options.newPprofListener == nil {
667+
options.newPprofListener = defaultPprofListener
668+
}
669+
637670
if options.GracefulShutdownTimeout == nil {
638671
gracefulShutdownTimeout := defaultGracefulShutdownPeriod
639672
options.GracefulShutdownTimeout = &gracefulShutdownTimeout

pkg/manager/manager_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ var _ = Describe("manger.Manager", func() {
148148
LeaderElectionID: "test-leader-election-id-2",
149149
HealthProbeBindAddress: "0",
150150
MetricsBindAddress: "0",
151+
PprofBindAddress: "0",
151152
})
152153
Expect(err).To(BeNil())
153154

@@ -193,6 +194,7 @@ var _ = Describe("manger.Manager", func() {
193194
LeaderElectionID: "test-leader-election-id-3",
194195
HealthProbeBindAddress: "0",
195196
MetricsBindAddress: "0",
197+
PprofBindAddress: "0",
196198
})
197199
Expect(err).To(BeNil())
198200

@@ -227,6 +229,7 @@ var _ = Describe("manger.Manager", func() {
227229
},
228230
HealthProbeBindAddress: "0",
229231
MetricsBindAddress: "0",
232+
PprofBindAddress: "0",
230233
})
231234
Expect(err).ToNot(HaveOccurred())
232235
Expect(m1).ToNot(BeNil())
@@ -247,6 +250,7 @@ var _ = Describe("manger.Manager", func() {
247250
},
248251
HealthProbeBindAddress: "0",
249252
MetricsBindAddress: "0",
253+
PprofBindAddress: "0",
250254
})
251255
Expect(err).ToNot(HaveOccurred())
252256
Expect(m2).ToNot(BeNil())
@@ -1280,6 +1284,99 @@ var _ = Describe("manger.Manager", func() {
12801284
})
12811285
})
12821286

1287+
Context("should start serving pprof", func() {
1288+
var listener net.Listener
1289+
var opts Options
1290+
1291+
BeforeEach(func() {
1292+
listener = nil
1293+
opts = Options{
1294+
newPprofListener: func(addr string) (net.Listener, error) {
1295+
var err error
1296+
listener, err = defaultPprofListener(addr)
1297+
return listener, err
1298+
},
1299+
}
1300+
})
1301+
1302+
AfterEach(func() {
1303+
if listener != nil {
1304+
listener.Close()
1305+
}
1306+
})
1307+
1308+
It("should stop serving pprof when stop is called", func() {
1309+
opts.PprofBindAddress = ":0"
1310+
m, err := New(cfg, opts)
1311+
Expect(err).NotTo(HaveOccurred())
1312+
1313+
ctx, cancel := context.WithCancel(context.Background())
1314+
go func() {
1315+
defer GinkgoRecover()
1316+
Expect(m.Start(ctx)).NotTo(HaveOccurred())
1317+
}()
1318+
<-m.Elected()
1319+
1320+
// Check the pprof started
1321+
endpoint := fmt.Sprintf("http://%s", listener.Addr().String())
1322+
_, err = http.Get(endpoint)
1323+
Expect(err).NotTo(HaveOccurred())
1324+
1325+
// Shutdown the server
1326+
cancel()
1327+
1328+
// Expect the pprof server to shutdown
1329+
Eventually(func() error {
1330+
_, err = http.Get(endpoint)
1331+
return err
1332+
}, 10*time.Second).ShouldNot(Succeed())
1333+
})
1334+
1335+
It("should serve pprof endpoints", func() {
1336+
opts.PprofBindAddress = ":0"
1337+
m, err := New(cfg, opts)
1338+
Expect(err).NotTo(HaveOccurred())
1339+
1340+
ctx, cancel := context.WithCancel(context.Background())
1341+
defer cancel()
1342+
go func() {
1343+
defer GinkgoRecover()
1344+
Expect(m.Start(ctx)).NotTo(HaveOccurred())
1345+
}()
1346+
<-m.Elected()
1347+
1348+
pprofIndexEndpoint := fmt.Sprintf("http://%s/debug/pprof/", listener.Addr().String())
1349+
resp, err := http.Get(pprofIndexEndpoint)
1350+
Expect(err).NotTo(HaveOccurred())
1351+
defer resp.Body.Close()
1352+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1353+
1354+
pprofCmdlineEndpoint := fmt.Sprintf("http://%s/debug/pprof/cmdline", listener.Addr().String())
1355+
resp, err = http.Get(pprofCmdlineEndpoint)
1356+
Expect(err).NotTo(HaveOccurred())
1357+
defer resp.Body.Close()
1358+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1359+
1360+
pprofProfileEndpoint := fmt.Sprintf("http://%s/debug/pprof/profile", listener.Addr().String())
1361+
resp, err = http.Get(pprofProfileEndpoint)
1362+
Expect(err).NotTo(HaveOccurred())
1363+
defer resp.Body.Close()
1364+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1365+
1366+
pprofSymbolEndpoint := fmt.Sprintf("http://%s/debug/pprof/symbol", listener.Addr().String())
1367+
resp, err = http.Get(pprofSymbolEndpoint)
1368+
Expect(err).NotTo(HaveOccurred())
1369+
defer resp.Body.Close()
1370+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1371+
1372+
pprofTraceEndpoint := fmt.Sprintf("http://%s/debug/pprof/trace", listener.Addr().String())
1373+
resp, err = http.Get(pprofTraceEndpoint)
1374+
Expect(err).NotTo(HaveOccurred())
1375+
defer resp.Body.Close()
1376+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1377+
})
1378+
})
1379+
12831380
Describe("Add", func() {
12841381
It("should immediately start the Component if the Manager has already Started another Component",
12851382
func() {

pkg/manager/server.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manager
18+
19+
import (
20+
"context"
21+
"errors"
22+
"net"
23+
"net/http"
24+
25+
"github.com/go-logr/logr"
26+
)
27+
28+
// server is a general purpose HTTP server Runnable for a manager
29+
// to serve some internal handlers such as health probes, metrics and profiling.
30+
type server struct {
31+
Kind string
32+
Log logr.Logger
33+
Server *http.Server
34+
Listener net.Listener
35+
}
36+
37+
func (s *server) Start(ctx context.Context) error {
38+
log := s.Log.WithValues("kind", s.Kind, "addr", s.Listener.Addr())
39+
40+
serverShutdown := make(chan struct{})
41+
go func() {
42+
<-ctx.Done()
43+
log.Info("shutting down server")
44+
if err := s.Server.Shutdown(context.Background()); err != nil {
45+
log.Error(err, "error shutting down server")
46+
}
47+
close(serverShutdown)
48+
}()
49+
50+
log.Info("starting server")
51+
if err := s.Server.Serve(s.Listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
52+
return err
53+
}
54+
55+
<-serverShutdown
56+
return nil
57+
}
58+
59+
func (s *server) NeedLeaderElection() bool {
60+
return false
61+
}

0 commit comments

Comments
 (0)