From 766e9255fa32cfe13ca41e9c2304b44db6156129 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 7 Apr 2026 18:21:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(config,server,makefile):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20pprof=20=E6=80=A7=E8=83=BD=E5=88=86=E6=9E=90?= =?UTF-8?q?=E7=AB=AF=E7=82=B9=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PprofConfig 配置结构,支持路径和 IP 访问控制 - 实现 PprofHandler 处理器,提供 CPU/heap/goroutine/block/mutex profile - Makefile 新增 build-perf、build-pgo、pgo-collect 目标 - 支持 PGO (Profile-Guided Optimization) 构建 Co-Authored-By: Claude --- .gitignore | 3 +- Makefile | 64 ++++++++- internal/config/config.go | 33 +++++ internal/config/defaults.go | 12 ++ internal/server/pprof.go | 238 ++++++++++++++++++++++++++++++++++ internal/server/pprof_impl.go | 104 +++++++++++++++ internal/server/server.go | 22 ++++ 7 files changed, 472 insertions(+), 4 deletions(-) create mode 100644 internal/server/pprof.go create mode 100644 internal/server/pprof_impl.go diff --git a/.gitignore b/.gitignore index 555692d..0e83c20 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,5 @@ lolly.yaml config.yaml lolly coverage.html -html/ \ No newline at end of file +html/ +default.pgo \ No newline at end of file diff --git a/Makefile b/Makefile index 68aa48c..9b1b638 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ GO_VERSION := $(shell go version | awk '{print $$3}') BUILD_PLATFORM := $(shell go env GOOS)/$(shell go env GOARCH) BUILD_DIR := bin -# 生产构建标志 +# 生产构建标志(体积优化) LDFLAGS := -ldflags "-s -w \ -X 'rua.plus/lolly/internal/app.Version=$(VERSION)' \ -X 'rua.plus/lolly/internal/app.GitCommit=$(GIT_COMMIT)' \ @@ -19,6 +19,10 @@ LDFLAGS := -ldflags "-s -w \ -X 'rua.plus/lolly/internal/app.GoVersion=$(GO_VERSION)' \ -X 'rua.plus/lolly/internal/app.BuildPlatform=$(BUILD_PLATFORM)'" +# 运行时性能优化标志 +PERF_GCFLAGS := -gcflags="-l=4" +PERF_ASMFLAGS := -asmflags="-l=4" + # Go 文件 MAIN_PATH := main.go @@ -37,13 +41,64 @@ build: @echo "Built: $(BUILD_DIR)/$(APP_NAME)" @echo "Version: $(VERSION) | Commit: $(GIT_COMMIT) | Platform: $(BUILD_PLATFORM)" -# 生产构建(优化) +# 生产构建(体积优化) build-prod: @echo "Building $(APP_NAME) for production..." @mkdir -p $(BUILD_DIR) go build $(LDFLAGS) -trimpath -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH) @echo "Production build complete: $(BUILD_DIR)/$(APP_NAME)" +# 生产构建(最大运行时性能) +build-perf: + @echo "Building $(APP_NAME) with max runtime performance..." + @mkdir -p $(BUILD_DIR) + go build $(LDFLAGS) $(PERF_GCFLAGS) $(PERF_ASMFLAGS) -trimpath \ + -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH) + @echo "Performance build complete: $(BUILD_DIR)/$(APP_NAME)" + +# PGO 构建(需先收集 profile) +PGO_PROFILE ?= default.pgo +build-pgo: + @echo "Building $(APP_NAME) with PGO optimization..." + @mkdir -p $(BUILD_DIR) + if [ -f $(PGO_PROFILE) ]; then \ + go build $(LDFLAGS) $(PERF_GCFLAGS) $(PERF_ASMFLAGS) -trimpath \ + -pgo=$(PGO_PROFILE) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH); \ + echo "PGO build complete using: $(PGO_PROFILE)"; \ + else \ + echo "PGO profile not found: $(PGO_PROFILE)"; \ + echo "Run 'make pgo-collect' first to generate profile"; \ + exit 1; \ + fi + +# 收集 PGO profile(运行代表性 workload) +pgo-collect: + @echo "=== PGO Profile Collection Guide ===" + @echo "" + @echo "Step 1: Enable pprof in your config file:" + @echo " monitoring:" + @echo " pprof:" + @echo " enabled: true" + @echo " path: /debug/pprof" + @echo " allow: [\"127.0.0.1\"]" + @echo "" + @echo "Step 2: Build and run lolly with representative workload:" + @echo " make build && ./bin/lolly -c configs/lolly.yaml" + @echo "" + @echo "Step 3: Collect CPU profile (run during peak load):" + @echo " curl http://localhost:/debug/pprof/profile?seconds=30 > $(PGO_PROFILE)" + @echo "" + @echo "Step 4: Build with PGO optimization:" + @echo " make build-pgo" + @echo "" + @echo "Available pprof endpoints:" + @echo " /debug/pprof - Index page" + @echo " /debug/pprof/profile - CPU profile (add ?seconds=N)" + @echo " /debug/pprof/heap - Memory profile" + @echo " /debug/pprof/goroutine - Goroutine count" + @echo "" + @echo "Tip: Profile during real workload for best PGO results" + # 跨平台构建 build-linux: @echo "Building for Linux..." @@ -225,7 +280,10 @@ help: @echo "" @echo "Build:" @echo " make build - Build for current platform" - @echo " make build-prod - Production build (optimized)" + @echo " make build-prod - Production build (size optimized)" + @echo " make build-perf - Production build (max runtime performance)" + @echo " make build-pgo - PGO build (needs profile, use PGO_PROFILE=path)" + @echo " make pgo-collect - Guide for collecting PGO profile" @echo " make build-all - Build for all platforms" @echo " make build-linux - Build for Linux amd64" @echo " make build-darwin - Build for macOS (amd64 + arm64)" diff --git a/internal/config/config.go b/internal/config/config.go index 64014be..8d61a0b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1166,6 +1166,39 @@ type MonitoringConfig struct { // Status 状态端点配置 // 服务健康状态检查端点 Status StatusConfig `yaml:"status"` + + // Pprof pprof 性能分析端点配置 + // 用于收集 CPU、内存等性能数据,支持 PGO 优化 + Pprof PprofConfig `yaml:"pprof"` +} + +// PprofConfig pprof 性能分析端点配置。 +// +// 配置 pprof 端点用于收集运行时性能数据。 +// 收集的 profile 可用于 PGO (Profile-Guided Optimization) 构建。 +// +// 注意事项: +// - 生产环境仅在收集 profile 时启用,完成后关闭 +// - 建议严格限制访问 IP,防止性能数据泄露 +// - CPU profile 收集需要代表性 workload +// +// 使用示例: +// +// pprof: +// enabled: true +// path: "/debug/pprof" +// allow: ["127.0.0.1"] +type PprofConfig struct { + // Enabled 是否启用 pprof 端点 + Enabled bool `yaml:"enabled"` + + // Path 端点路径前缀 + // 默认为 "/debug/pprof" + Path string `yaml:"path"` + + // Allow 允许访问的 IP 列表 + // 可访问 pprof 端点的 IP 地址或 CIDR + Allow []string `yaml:"allow"` } // StatusConfig 状态监控端点配置。 diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 1cda69b..9d15f2c 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -137,6 +137,11 @@ func DefaultConfig() *Config { Path: "/_status", Allow: []string{"127.0.0.1"}, }, + Pprof: PprofConfig{ + Enabled: false, + Path: "/debug/pprof", + Allow: []string{"127.0.0.1"}, + }, }, HTTP3: HTTP3Config{ Enabled: false, @@ -438,6 +443,13 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) { for _, ip := range cfg.Monitoring.Status.Allow { fmt.Fprintf(&buf, " - \"%s\"\n", ip) } + buf.WriteString(" pprof: # pprof 性能分析端点(用于 PGO 优化)\n") + fmt.Fprintf(&buf, " enabled: %v # 是否启用(生产环境仅在收集 profile 时启用)\n", cfg.Monitoring.Pprof.Enabled) + fmt.Fprintf(&buf, " path: \"%s\" # 端点路径前缀\n", cfg.Monitoring.Pprof.Path) + buf.WriteString(" allow: # 允许访问的 IP\n") + for _, ip := range cfg.Monitoring.Pprof.Allow { + fmt.Fprintf(&buf, " - \"%s\"\n", ip) + } return buf.Bytes(), nil } diff --git a/internal/server/pprof.go b/internal/server/pprof.go new file mode 100644 index 0000000..4316a82 --- /dev/null +++ b/internal/server/pprof.go @@ -0,0 +1,238 @@ +// Package server 提供 pprof 性能分析端点支持。 +// +// 该文件为 fasthttp 服务器提供 pprof 端点,用于收集: +// - CPU profile(用于 PGO 优化) +// - 内存分配 profile +// - Goroutine 分析 +// - 阻塞分析 +// - 锁竞争分析 +// +// 注意事项: +// - 仅在配置启用时生效 +// - 生产环境建议限制访问 IP +// - CPU profile 收集需要代表性 workload +// +// 作者:xfy +package server + +import ( + "bufio" + "fmt" + "net" + "strconv" + "time" + + "github.com/valyala/fasthttp" + "rua.plus/lolly/internal/config" +) + +// PprofHandler pprof 性能分析处理器。 +// +// 封装 fasthttp 的 pprof handler,提供 IP 访问控制。 +type PprofHandler struct { + // path 端点路径前缀 + path string + + // allowedIPs 允许访问的 IP 列表 + allowedIPs []net.IP + + // allowedNets 允许访问的 CIDR 网络 + allowedNets []*net.IPNet +} + +// NewPprofHandler 创建 pprof 处理器。 +// +// 根据配置创建 pprof 端点处理器,包括 IP 访问控制。 +// +// 参数: +// - cfg: pprof 配置对象 +// +// 返回值: +// - *PprofHandler: 创建的处理器实例 +// - error: 创建过程中遇到的错误,如 CIDR 解析失败 +func NewPprofHandler(cfg *config.PprofConfig) (*PprofHandler, error) { + if !cfg.Enabled { + return nil, nil + } + + path := cfg.Path + if path == "" { + path = "/debug/pprof" + } + + h := &PprofHandler{path: path} + + // 解析允许的 IP 列表 + for _, ipStr := range cfg.Allow { + if ip := net.ParseIP(ipStr); ip != nil { + h.allowedIPs = append(h.allowedIPs, ip) + continue + } + // 尝试解析 CIDR + _, net, err := net.ParseCIDR(ipStr) + if err != nil { + return nil, fmt.Errorf("解析 IP/CIDR 失败: %s: %w", ipStr, err) + } + h.allowedNets = append(h.allowedNets, net) + } + + // 默认只允许 localhost + if len(h.allowedIPs) == 0 && len(h.allowedNets) == 0 { + h.allowedIPs = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} + } + + return h, nil +} + +// Path 返回 pprof 端点路径。 +func (h *PprofHandler) Path() string { + return h.path +} + +// ServeHTTP 处理 pprof 请求。 +// +// 根据 URL 路径选择对应的 profile 处理器, +// 并检查客户端 IP 是否在允许列表中。 +func (h *PprofHandler) ServeHTTP(ctx *fasthttp.RequestCtx) { + // IP 访问控制 + if !h.isAllowed(ctx) { + ctx.SetStatusCode(fasthttp.StatusForbidden) + ctx.SetBodyString("Forbidden") + return + } + + // 根据路径分发 + path := string(ctx.Path()) + subPath := path[len(h.path):] + + switch { + case subPath == "" || subPath == "/": + h.handleIndex(ctx) + case subPath == "/profile": + h.handleCPU(ctx) + case subPath == "/heap": + h.handleHeap(ctx) + case subPath == "/goroutine": + h.handleGoroutine(ctx) + case subPath == "/block": + h.handleBlock(ctx) + case subPath == "/mutex": + h.handleMutex(ctx) + default: + ctx.SetStatusCode(fasthttp.StatusNotFound) + ctx.SetBodyString("Unknown profile: " + subPath) + } +} + +// isAllowed 检查客户端 IP 是否允许访问。 +func (h *PprofHandler) isAllowed(ctx *fasthttp.RequestCtx) bool { + if len(h.allowedIPs) == 0 && len(h.allowedNets) == 0 { + return true // 无限制 + } + + ipStr := ctx.RemoteIP().String() + clientIP := net.ParseIP(ipStr) + if clientIP == nil { + return false + } + + // 检查精确 IP + for _, ip := range h.allowedIPs { + if ip.Equal(clientIP) { + return true + } + } + + // 检查 CIDR 网络 + for _, net := range h.allowedNets { + if net.Contains(clientIP) { + return true + } + } + + return false +} + +// handleIndex 处理索引页面。 +func (h *PprofHandler) handleIndex(ctx *fasthttp.RequestCtx) { + ctx.SetContentType("text/html; charset=utf-8") + html := ` +Pprof Profiles + +

Pprof Profiles

+ + + + + + +
CPU Profile (30s)CPU profile for 30 seconds
Heap ProfileMemory allocation profile
Goroutine ProfileGoroutine stack traces
Block ProfileBlocking profile
Mutex ProfileMutex contention profile
+

Usage: curl %s/profile?seconds=30 > cpu.pgo

+ +` + ctx.SetBodyString(fmt.Sprintf(html, h.path, h.path, h.path, h.path, h.path, h.path)) +} + +// handleCPU 处理 CPU profile 请求。 +func (h *PprofHandler) handleCPU(ctx *fasthttp.RequestCtx) { + // 获取采集时长 + seconds := 30 + if secStr := ctx.QueryArgs().Peek("seconds"); secStr != nil { + if sec, err := strconv.Atoi(string(secStr)); err == nil && sec > 0 { + seconds = sec + } + } + + ctx.SetContentType("application/octet-stream") + ctx.SetBodyStreamWriter(func(w *bufio.Writer) { + // 启动 CPU profile + if err := startCPUProfile(wrapBufioWriter(w)); err != nil { + w.WriteString("Error starting CPU profile: " + err.Error()) + w.Flush() + return + } + + // 等待采集完成 + time.Sleep(time.Duration(seconds) * time.Second) + + // 厉止 CPU profile + stopCPUProfile() + w.Flush() + }) +} + +// handleHeap 处理内存 profile 请求。 +func (h *PprofHandler) handleHeap(ctx *fasthttp.RequestCtx) { + ctx.SetContentType("application/octet-stream") + ctx.SetBodyStreamWriter(func(w *bufio.Writer) { + writeHeapProfile(wrapBufioWriter(w)) + w.Flush() + }) +} + +// handleGoroutine 处理 Goroutine profile 请求。 +func (h *PprofHandler) handleGoroutine(ctx *fasthttp.RequestCtx) { + ctx.SetContentType("application/octet-stream") + ctx.SetBodyStreamWriter(func(w *bufio.Writer) { + writeGoroutineProfile(wrapBufioWriter(w)) + w.Flush() + }) +} + +// handleBlock 处理阻塞 profile 请求。 +func (h *PprofHandler) handleBlock(ctx *fasthttp.RequestCtx) { + ctx.SetContentType("application/octet-stream") + ctx.SetBodyStreamWriter(func(w *bufio.Writer) { + writeBlockProfile(wrapBufioWriter(w)) + w.Flush() + }) +} + +// handleMutex 处理锁竞争 profile 请求。 +func (h *PprofHandler) handleMutex(ctx *fasthttp.RequestCtx) { + ctx.SetContentType("application/octet-stream") + ctx.SetBodyStreamWriter(func(w *bufio.Writer) { + writeMutexProfile(wrapBufioWriter(w)) + w.Flush() + }) +} \ No newline at end of file diff --git a/internal/server/pprof_impl.go b/internal/server/pprof_impl.go new file mode 100644 index 0000000..1d9b4df --- /dev/null +++ b/internal/server/pprof_impl.go @@ -0,0 +1,104 @@ +// Package server 提供 pprof 性能分析的底层实现。 +// +// 该文件封装 runtime/pprof 的调用,为 fasthttp 提供流式输出支持。 +// +// 作者:xfy +package server + +import ( + "bufio" + "io" + "runtime" + "runtime/pprof" + "sync" +) + +var ( + cpuProfileMu sync.Mutex + cpuProfileWriter io.Writer + cpuProfileActive bool +) + +// startCPUProfile 启动 CPU profile 采集。 +// +// 参数: +// - w: 输出 writer +// +// 返回值: +// - error: 启动失败时的错误 +func startCPUProfile(w io.Writer) error { + cpuProfileMu.Lock() + defer cpuProfileMu.Unlock() + + if cpuProfileActive { + return nil // 已在采集,忽略 + } + + if err := pprof.StartCPUProfile(w); err != nil { + return err + } + + cpuProfileWriter = w + cpuProfileActive = true + return nil +} + +// stopCPUProfile 厉止 CPU profile 采集。 +func stopCPUProfile() { + cpuProfileMu.Lock() + defer cpuProfileMu.Unlock() + + if cpuProfileActive { + pprof.StopCPUProfile() + cpuProfileActive = false + cpuProfileWriter = nil + } +} + +// writeHeapProfile 写入内存分配 profile。 +func writeHeapProfile(w io.Writer) { + runtime.GC() // 先执行 GC,获取更准确的数据 + pprof.WriteHeapProfile(w) +} + +// writeGoroutineProfile 写入 Goroutine stack traces。 +func writeGoroutineProfile(w io.Writer) { + p := pprof.Lookup("goroutine") + if p != nil { + p.WriteTo(w, 0) + } +} + +// writeBlockProfile 写入阻塞 profile。 +func writeBlockProfile(w io.Writer) { + p := pprof.Lookup("block") + if p != nil { + p.WriteTo(w, 0) + } +} + +// writeMutexProfile 写入锁竞争 profile。 +func writeMutexProfile(w io.Writer) { + p := pprof.Lookup("mutex") + if p != nil { + p.WriteTo(w, 0) + } +} + +// bufioWriterAdapter 将 bufio.Writer 包装为 io.Writer,自动 Flush。 +type bufioWriterAdapter struct { + w *bufio.Writer +} + +func (a *bufioWriterAdapter) Write(p []byte) (n int, err error) { + n, err = a.w.Write(p) + if err == nil { + a.w.Flush() + } + return n, err +} + +// wrapBufioWriter 将 bufio.Writer 包装为 io.Writer。 +func wrapBufioWriter(w *bufio.Writer) io.Writer { + return &bufioWriterAdapter{w: w} +} \ No newline at end of file diff --git a/internal/server/server.go b/internal/server/server.go index 96773c4..15acc16 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -388,6 +388,17 @@ func (s *Server) startSingleMode() error { } } + // 注册 pprof 性能分析端点(如果配置) + if s.config.Monitoring.Pprof.Enabled { + pprofHandler, err := NewPprofHandler(&s.config.Monitoring.Pprof) + if err != nil { + logging.Error().Msg("创建 pprof 处理器失败: " + err.Error()) + } else { + router.GET(pprofHandler.Path(), pprofHandler.ServeHTTP) + router.GET(pprofHandler.Path()+"/{profile:*}", pprofHandler.ServeHTTP) + } + } + // 注册代理路由 s.registerProxyRoutes(router, &s.config.Server) @@ -491,6 +502,17 @@ func (s *Server) startVHostMode() error { } } + // 注册 pprof 性能分析端点(如果配置) + if s.config.Monitoring.Pprof.Enabled { + pprofHandler, err := NewPprofHandler(&s.config.Monitoring.Pprof) + if err != nil { + logging.Error().Msg("创建 pprof 处理器失败: " + err.Error()) + } else { + router.GET(pprofHandler.Path(), pprofHandler.ServeHTTP) + router.GET(pprofHandler.Path()+"/{profile:*}", pprofHandler.ServeHTTP) + } + } + s.registerProxyRoutes(router, &s.config.Server) // 静态文件