diff --git a/cmd/moco-controller/cmd/root.go b/cmd/moco-controller/cmd/root.go index 36623aca1..ece5ac218 100644 --- a/cmd/moco-controller/cmd/root.go +++ b/cmd/moco-controller/cmd/root.go @@ -25,6 +25,7 @@ var ( var config struct { metricsAddr string probeAddr string + pprofAddr string leaderElectionID string webhookAddr string certDir string @@ -90,8 +91,9 @@ func Execute() { func init() { fs := rootCmd.Flags() - fs.StringVar(&config.metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to") + fs.StringVar(&config.metricsAddr, "metrics-addr", ":8080", "Listen address for metric endpoint") fs.StringVar(&config.probeAddr, "health-probe-addr", ":8081", "Listen address for health probes") + fs.StringVar(&config.pprofAddr, "pprof-addr", "", "Listen address for pprof endpoints. pprof is disabled by default") fs.StringVar(&config.leaderElectionID, "leader-election-id", "moco", "ID for leader election by controller-runtime") fs.StringVar(&config.webhookAddr, "webhook-addr", ":9443", "Listen address for the webhook endpoint") fs.StringVar(&config.certDir, "cert-dir", "", "webhook certificate directory") diff --git a/cmd/moco-controller/cmd/run.go b/cmd/moco-controller/cmd/run.go index 06c5116d5..63d03f7ea 100644 --- a/cmd/moco-controller/cmd/run.go +++ b/cmd/moco-controller/cmd/run.go @@ -12,6 +12,7 @@ import ( "github.com/cybozu-go/moco/pkg/cert" "github.com/cybozu-go/moco/pkg/dbop" "github.com/cybozu-go/moco/pkg/metrics" + "github.com/cybozu-go/moco/pkg/pprof" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -136,6 +137,13 @@ func subMain(ns, addr string, port int) error { return err } + if config.pprofAddr != "" && config.pprofAddr != "0" { + if err := mgr.Add(pprof.NewHandler(ctrl.Log.WithName("pprof"), config.pprofAddr)); err != nil { + setupLog.Error(err, "unable to set pprof handler") + return err + } + } + metrics.Register(k8smetrics.Registry) setupLog.Info("starting manager") diff --git a/docs/moco-controller.md b/docs/moco-controller.md index d992af53b..8c2ed8167 100644 --- a/docs/moco-controller.md +++ b/docs/moco-controller.md @@ -29,9 +29,10 @@ Flags: --log_file_max_size uint Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) --logtostderr log to standard error instead of files (default true) --max-concurrent-reconciles int The maximum number of concurrent reconciles which can be run (default 8) - --metrics-addr string The address the metric endpoint binds to (default ":8080") + --metrics-addr string Listen address for metric endpoint (default ":8080") --mysqld-exporter-image string The image of mysqld_exporter sidecar container --one_output If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true) + --pprof-addr string Listen address for pprof endpoints. pprof is disabled by default --skip_headers If true, avoid header prefixes in the log messages --skip_log_headers If true, avoid headers when opening log files (no effect when -logtostderr=true) --stderrthreshold severity logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=false) (default 2) diff --git a/pkg/pprof/pprof.go b/pkg/pprof/pprof.go new file mode 100644 index 000000000..774bd6fb0 --- /dev/null +++ b/pkg/pprof/pprof.go @@ -0,0 +1,65 @@ +package pprof + +import ( + "context" + "fmt" + "net/http" + "net/http/pprof" + "time" + + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +type Handler struct { + log logr.Logger + bindAddr string +} + +var ( + _ manager.Runnable = &Handler{} + _ manager.LeaderElectionRunnable = &Handler{} +) + +func NewHandler(log logr.Logger, bindAddr string) *Handler { + return &Handler{ + log: log, + bindAddr: bindAddr, + } +} + +func (h *Handler) NeedLeaderElection() bool { + return false +} + +func (h *Handler) Start(ctx context.Context) error { + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + server := &http.Server{Addr: h.bindAddr, Handler: mux} + errCh := make(chan error) + h.log.Info("starting handler", "addr", h.bindAddr) + go func() { + errCh <- server.ListenAndServe() + }() + select { + case err := <-errCh: + // ListenAndServe always returns a non-nil error. So no need for a nil check. + h.log.Error(err, "failed to listen") + return fmt.Errorf("failed to listen: %w", err) + case <-ctx.Done(): + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + h.log.Error(err, "failed to shutdown") + return fmt.Errorf("failed to shutdown: %v", err) + } + } + + h.log.Info("shutdown") + return nil +}