Skip to content

Commit fbd6b94

Browse files
authored
Merge pull request #1943 from zqzten/pprof
✨ Introduce pprof server to manager
2 parents a33d038 + 482ed05 commit fbd6b94

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
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"net"
2424
"net/http"
25+
"net/http/pprof"
2526
"sync"
2627
"sync/atomic"
2728
"time"
@@ -105,6 +106,9 @@ type controllerManager struct {
105106
// Healthz probe handler
106107
healthzHandler *healthz.Handler
107108

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

@@ -326,6 +330,24 @@ func (cm *controllerManager) serveHealthProbes() {
326330
go cm.httpServe("health probe", cm.logger, server, cm.healthProbeListener)
327331
}
328332

333+
func (cm *controllerManager) addPprofServer() error {
334+
mux := http.NewServeMux()
335+
srv := httpserver.New(mux)
336+
337+
mux.HandleFunc("/debug/pprof/", pprof.Index)
338+
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
339+
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
340+
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
341+
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
342+
343+
return cm.add(&server{
344+
Kind: "pprof",
345+
Log: cm.logger,
346+
Server: srv,
347+
Listener: cm.pprofListener,
348+
})
349+
}
350+
329351
func (cm *controllerManager) httpServe(kind string, log logr.Logger, server *http.Server, ln net.Listener) {
330352
log = log.WithValues("kind", kind, "addr", ln.Addr())
331353

@@ -423,6 +445,13 @@ func (cm *controllerManager) Start(ctx context.Context) (err error) {
423445
cm.serveHealthProbes()
424446
}
425447

448+
// Add pprof server
449+
if cm.pprofListener != nil {
450+
if err := cm.addPprofServer(); err != nil {
451+
return fmt.Errorf("failed to add pprof server: %w", err)
452+
}
453+
}
454+
426455
// First start any webhook servers, which includes conversion, validation, and defaulting
427456
// webhooks that are registered.
428457
//

pkg/manager/manager.go

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

274+
// PprofBindAddress is the TCP address that the controller should bind to
275+
// for serving pprof.
276+
// It can be set to "" or "0" to disable the pprof serving.
277+
// Since pprof may contain sensitive information, make sure to protect it
278+
// before exposing it to public.
279+
PprofBindAddress string
280+
274281
// Port is the port that the webhook server serves at.
275282
// It is used to set webhook.Server.Port if WebhookServer is not set.
276283
//
@@ -347,6 +354,7 @@ type Options struct {
347354
newResourceLock func(config *rest.Config, recorderProvider recorder.Provider, options leaderelection.Options) (resourcelock.Interface, error)
348355
newMetricsListener func(addr string) (net.Listener, error)
349356
newHealthProbeListener func(addr string) (net.Listener, error)
357+
newPprofListener func(addr string) (net.Listener, error)
350358
}
351359

352360
// BaseContextFunc is a function used to provide a base Context to Runnables
@@ -458,6 +466,13 @@ func New(config *rest.Config, options Options) (Manager, error) {
458466
return nil, err
459467
}
460468

469+
// Create pprof listener. This will throw an error if the bind
470+
// address is invalid or already in use.
471+
pprofListener, err := options.newPprofListener(options.PprofBindAddress)
472+
if err != nil {
473+
return nil, fmt.Errorf("failed to new pprof listener: %w", err)
474+
}
475+
461476
errChan := make(chan error)
462477
runnables := newRunnables(options.BaseContext, errChan)
463478

@@ -481,6 +496,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
481496
healthProbeListener: healthProbeListener,
482497
readinessEndpointName: options.ReadinessEndpointName,
483498
livenessEndpointName: options.LivenessEndpointName,
499+
pprofListener: pprofListener,
484500
gracefulShutdownTimeout: *options.GracefulShutdownTimeout,
485501
internalProceduresStop: make(chan struct{}),
486502
leaderElectionStopped: make(chan struct{}),
@@ -626,6 +642,19 @@ func defaultHealthProbeListener(addr string) (net.Listener, error) {
626642
return ln, nil
627643
}
628644

645+
// defaultPprofListener creates the default pprof listener bound to the given address.
646+
func defaultPprofListener(addr string) (net.Listener, error) {
647+
if addr == "" || addr == "0" {
648+
return nil, nil
649+
}
650+
651+
ln, err := net.Listen("tcp", addr)
652+
if err != nil {
653+
return nil, fmt.Errorf("error listening on %s: %w", addr, err)
654+
}
655+
return ln, nil
656+
}
657+
629658
// defaultBaseContext is used as the BaseContext value in Options if one
630659
// has not already been set.
631660
func defaultBaseContext() context.Context {
@@ -686,6 +715,10 @@ func setOptionsDefaults(options Options) Options {
686715
options.newHealthProbeListener = defaultHealthProbeListener
687716
}
688717

718+
if options.newPprofListener == nil {
719+
options.newPprofListener = defaultPprofListener
720+
}
721+
689722
if options.GracefulShutdownTimeout == nil {
690723
gracefulShutdownTimeout := defaultGracefulShutdownPeriod
691724
options.GracefulShutdownTimeout = &gracefulShutdownTimeout

pkg/manager/manager_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ var _ = Describe("manger.Manager", func() {
302302
LeaderElectionID: "test-leader-election-id-2",
303303
HealthProbeBindAddress: "0",
304304
MetricsBindAddress: "0",
305+
PprofBindAddress: "0",
305306
})
306307
Expect(err).To(BeNil())
307308

@@ -347,6 +348,7 @@ var _ = Describe("manger.Manager", func() {
347348
LeaderElectionID: "test-leader-election-id-3",
348349
HealthProbeBindAddress: "0",
349350
MetricsBindAddress: "0",
351+
PprofBindAddress: "0",
350352
})
351353
Expect(err).To(BeNil())
352354

@@ -381,6 +383,7 @@ var _ = Describe("manger.Manager", func() {
381383
},
382384
HealthProbeBindAddress: "0",
383385
MetricsBindAddress: "0",
386+
PprofBindAddress: "0",
384387
})
385388
Expect(err).ToNot(HaveOccurred())
386389
Expect(m1).ToNot(BeNil())
@@ -401,6 +404,7 @@ var _ = Describe("manger.Manager", func() {
401404
},
402405
HealthProbeBindAddress: "0",
403406
MetricsBindAddress: "0",
407+
PprofBindAddress: "0",
404408
})
405409
Expect(err).ToNot(HaveOccurred())
406410
Expect(m2).ToNot(BeNil())
@@ -1478,6 +1482,99 @@ var _ = Describe("manger.Manager", func() {
14781482
})
14791483
})
14801484

1485+
Context("should start serving pprof", func() {
1486+
var listener net.Listener
1487+
var opts Options
1488+
1489+
BeforeEach(func() {
1490+
listener = nil
1491+
opts = Options{
1492+
newPprofListener: func(addr string) (net.Listener, error) {
1493+
var err error
1494+
listener, err = defaultPprofListener(addr)
1495+
return listener, err
1496+
},
1497+
}
1498+
})
1499+
1500+
AfterEach(func() {
1501+
if listener != nil {
1502+
listener.Close()
1503+
}
1504+
})
1505+
1506+
It("should stop serving pprof when stop is called", func() {
1507+
opts.PprofBindAddress = ":0"
1508+
m, err := New(cfg, opts)
1509+
Expect(err).NotTo(HaveOccurred())
1510+
1511+
ctx, cancel := context.WithCancel(context.Background())
1512+
go func() {
1513+
defer GinkgoRecover()
1514+
Expect(m.Start(ctx)).NotTo(HaveOccurred())
1515+
}()
1516+
<-m.Elected()
1517+
1518+
// Check the pprof started
1519+
endpoint := fmt.Sprintf("http://%s", listener.Addr().String())
1520+
_, err = http.Get(endpoint)
1521+
Expect(err).NotTo(HaveOccurred())
1522+
1523+
// Shutdown the server
1524+
cancel()
1525+
1526+
// Expect the pprof server to shutdown
1527+
Eventually(func() error {
1528+
_, err = http.Get(endpoint)
1529+
return err
1530+
}, 10*time.Second).ShouldNot(Succeed())
1531+
})
1532+
1533+
It("should serve pprof endpoints", func() {
1534+
opts.PprofBindAddress = ":0"
1535+
m, err := New(cfg, opts)
1536+
Expect(err).NotTo(HaveOccurred())
1537+
1538+
ctx, cancel := context.WithCancel(context.Background())
1539+
defer cancel()
1540+
go func() {
1541+
defer GinkgoRecover()
1542+
Expect(m.Start(ctx)).NotTo(HaveOccurred())
1543+
}()
1544+
<-m.Elected()
1545+
1546+
pprofIndexEndpoint := fmt.Sprintf("http://%s/debug/pprof/", listener.Addr().String())
1547+
resp, err := http.Get(pprofIndexEndpoint)
1548+
Expect(err).NotTo(HaveOccurred())
1549+
defer resp.Body.Close()
1550+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1551+
1552+
pprofCmdlineEndpoint := fmt.Sprintf("http://%s/debug/pprof/cmdline", listener.Addr().String())
1553+
resp, err = http.Get(pprofCmdlineEndpoint)
1554+
Expect(err).NotTo(HaveOccurred())
1555+
defer resp.Body.Close()
1556+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1557+
1558+
pprofProfileEndpoint := fmt.Sprintf("http://%s/debug/pprof/profile", listener.Addr().String())
1559+
resp, err = http.Get(pprofProfileEndpoint)
1560+
Expect(err).NotTo(HaveOccurred())
1561+
defer resp.Body.Close()
1562+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1563+
1564+
pprofSymbolEndpoint := fmt.Sprintf("http://%s/debug/pprof/symbol", listener.Addr().String())
1565+
resp, err = http.Get(pprofSymbolEndpoint)
1566+
Expect(err).NotTo(HaveOccurred())
1567+
defer resp.Body.Close()
1568+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1569+
1570+
pprofTraceEndpoint := fmt.Sprintf("http://%s/debug/pprof/trace", listener.Addr().String())
1571+
resp, err = http.Get(pprofTraceEndpoint)
1572+
Expect(err).NotTo(HaveOccurred())
1573+
defer resp.Body.Close()
1574+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1575+
})
1576+
})
1577+
14811578
Describe("Add", func() {
14821579
It("should immediately start the Component if the Manager has already Started another Component",
14831580
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)