Skip to content

Commit 37138d6

Browse files
authored
Add pprof debug endpoint (#1064)
Allow users to opt-in to exposing pprof debugging endpoints on the operator's metrics port. Users may add the `ENABLE_DEBUG_PPROF` env variable to the operator Pod to enable the pprof endpoints. This is an opt-in pattern, as memory/CPU profiling can have a non-insignificant impact on runtime perf, and could be a denial of service vector. These endpoints should not be enabled in production. With this feature enabled, users can access the pprof output at `<OPERATOR_URL>:<METRICS_PORT>/debug/pprof`. For example, after port-forwarding to the operator Pod: ``` go tool pprof -inuse_space -web http://localhost:9782/debug/pprof/heap ```
1 parent 6d5295b commit 37138d6

File tree

6 files changed

+149
-1
lines changed

6 files changed

+149
-1
lines changed

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ COPY main.go main.go
1313
COPY api/ api/
1414
COPY controllers/ controllers/
1515
COPY internal/ internal/
16+
COPY pkg/ pkg/
1617

1718
# Build
1819
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -tags timetzdata -o manager main.go

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ kubebuilder-assets: $(KUBEBUILDER_ASSETS)
2525

2626
.PHONY: unit-tests
2727
unit-tests: install-tools $(KUBEBUILDER_ASSETS) generate fmt vet manifests ## Run unit tests
28-
ginkgo -r --randomize-all api/ internal/
28+
ginkgo -r --randomize-all api/ internal/ pkg/
2929

3030
.PHONY: integration-tests
3131
integration-tests: install-tools $(KUBEBUILDER_ASSETS) generate fmt vet manifests ## Run integration tests

main.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package main
1111
import (
1212
"flag"
1313
"fmt"
14+
"github.com/rabbitmq/cluster-operator/pkg/profiling"
1415
"os"
1516
"strconv"
1617
"time"
@@ -113,6 +114,17 @@ func main() {
113114
os.Exit(1)
114115
}
115116

117+
if enableDebugPprof, ok := os.LookupEnv("ENABLE_DEBUG_PPROF"); ok {
118+
pprofEnabled, err := strconv.ParseBool(enableDebugPprof)
119+
if err == nil && pprofEnabled {
120+
mgr, err = profiling.AddDebugPprofEndpoints(mgr)
121+
if err != nil {
122+
log.Error(err, "unable to add debug endpoints to manager")
123+
os.Exit(1)
124+
}
125+
}
126+
}
127+
116128
clusterConfig := config.GetConfigOrDie()
117129

118130
err = (&controllers.RabbitmqClusterReconciler{

pkg/profiling/pprof.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package profiling
2+
3+
import (
4+
"net/http"
5+
"net/http/pprof"
6+
ctrl "sigs.k8s.io/controller-runtime"
7+
)
8+
9+
func AddDebugPprofEndpoints(mgr ctrl.Manager) (ctrl.Manager, error) {
10+
pprofEndpoints := map[string]http.HandlerFunc{
11+
"/debug/pprof": http.HandlerFunc(pprof.Index),
12+
"/debug/pprof/allocs": http.HandlerFunc(pprof.Index),
13+
"/debug/pprof/block": http.HandlerFunc(pprof.Index),
14+
"/debug/pprof/cmdline": http.HandlerFunc(pprof.Cmdline),
15+
"/debug/pprof/goroutine": http.HandlerFunc(pprof.Index),
16+
"/debug/pprof/heap": http.HandlerFunc(pprof.Index),
17+
"/debug/pprof/mutex": http.HandlerFunc(pprof.Index),
18+
"/debug/pprof/profile": http.HandlerFunc(pprof.Profile),
19+
"/debug/pprof/symbol": http.HandlerFunc(pprof.Symbol),
20+
"/debug/pprof/threadcreate": http.HandlerFunc(pprof.Index),
21+
"/debug/pprof/trace": http.HandlerFunc(pprof.Trace),
22+
}
23+
for path, handler := range pprofEndpoints {
24+
err := mgr.AddMetricsExtraHandler(path, handler)
25+
if err != nil {
26+
return mgr, err
27+
}
28+
}
29+
return mgr, nil
30+
}

pkg/profiling/pprof_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package profiling_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
"github.com/rabbitmq/cluster-operator/pkg/profiling"
9+
"io"
10+
"net/http"
11+
ctrl "sigs.k8s.io/controller-runtime"
12+
)
13+
14+
var _ = Describe("Pprof", func() {
15+
16+
var (
17+
opts ctrl.Options
18+
mgr ctrl.Manager
19+
err error
20+
metricsEndpoint string
21+
)
22+
23+
BeforeEach(func() {
24+
metricsEndpoint, err = getFreePort()
25+
opts = ctrl.Options{
26+
MetricsBindAddress: metricsEndpoint,
27+
}
28+
mgr, err = ctrl.NewManager(cfg, opts)
29+
Expect(err).NotTo(HaveOccurred())
30+
mgr, err = profiling.AddDebugPprofEndpoints(mgr)
31+
Expect(err).NotTo(HaveOccurred())
32+
33+
})
34+
35+
It("should serve extra endpoints", func() {
36+
ctx, cancel := context.WithCancel(context.Background())
37+
defer cancel()
38+
go func() {
39+
defer GinkgoRecover()
40+
Expect(mgr.Start(ctx)).NotTo(HaveOccurred())
41+
}()
42+
<-mgr.Elected()
43+
endpoint := fmt.Sprintf("http://%s/debug/pprof", metricsEndpoint)
44+
resp, err := http.Get(endpoint)
45+
Expect(err).NotTo(HaveOccurred())
46+
defer resp.Body.Close()
47+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
48+
49+
body, err := io.ReadAll(resp.Body)
50+
Expect(err).NotTo(HaveOccurred())
51+
Expect(string(body)).NotTo(BeEmpty())
52+
})
53+
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package profiling_test
2+
3+
import (
4+
"k8s.io/client-go/kubernetes"
5+
"k8s.io/client-go/rest"
6+
"net"
7+
"sigs.k8s.io/controller-runtime/pkg/envtest"
8+
logf "sigs.k8s.io/controller-runtime/pkg/log"
9+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
10+
"testing"
11+
12+
. "github.com/onsi/ginkgo/v2"
13+
. "github.com/onsi/gomega"
14+
)
15+
16+
var testenv *envtest.Environment
17+
var cfg *rest.Config
18+
var clientset *kubernetes.Clientset
19+
20+
func TestProfiling(t *testing.T) {
21+
RegisterFailHandler(Fail)
22+
RunSpecs(t, "Profiling Suite")
23+
}
24+
25+
var _ = BeforeSuite(func() {
26+
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
27+
28+
testenv = &envtest.Environment{}
29+
30+
var err error
31+
cfg, err = testenv.Start()
32+
Expect(err).NotTo(HaveOccurred())
33+
34+
})
35+
36+
var _ = AfterSuite(func() {
37+
Expect(testenv.Stop()).To(Succeed())
38+
})
39+
40+
func getFreePort() (string, error) {
41+
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
42+
if err != nil {
43+
return "", err
44+
}
45+
46+
l, err := net.ListenTCP("tcp", addr)
47+
if err != nil {
48+
return "", err
49+
}
50+
defer l.Close()
51+
return l.Addr().String(), nil
52+
}

0 commit comments

Comments
 (0)