From 11e22c80b89d525544b2f541958a038cba2c7702 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 28 Apr 2026 20:11:20 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E9=9B=B6=E5=88=86=E9=85=8D=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=B8=8E=20Dial=20timeout=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 b2s/s2b 零分配字节-字符串转换工具函数 - WebSocket 数据转发使用 sync.Pool 复用 32KB buffer - 条件化 Debug 日志避免非 Debug 级别的字符串分配 - 缓存键哈希计算直接写入 []byte 避免 string 转换 - 使用 bytes.EqualFold 替代 strings.ToLower 进行大小写不敏感比较 - generateETag 使用 strconv.AppendInt 避免 fmt.Sprintf - 支持 Dial timeout 配置,区分 TCP 连接建立和总连接超时 - MaxConnsPerHost 默认值改为 512(fasthttp 推荐) Co-Authored-By: Claude Opus 4.7 --- docs/prompts.md | 2 + internal/adapter/common.go | 6 +- internal/cache/purge.go | 1 - internal/config/defaults.go | 2 +- internal/config/defaults_test.go | 4 +- internal/config/proxy_config.go | 11 +++- internal/handler/static.go | 11 +++- internal/http3/adapter.go | 2 +- internal/proxy/cache_handler.go | 39 ++++-------- internal/proxy/header_modifier.go | 5 +- internal/proxy/health.go | 4 +- internal/proxy/proxy.go | 100 +++++++++++++++++++++--------- internal/proxy/utils.go | 37 +++++++++++ internal/proxy/websocket.go | 14 ++++- 14 files changed, 164 insertions(+), 74 deletions(-) create mode 100644 internal/proxy/utils.go diff --git a/docs/prompts.md b/docs/prompts.md index 2db380b..2b0c753 100644 --- a/docs/prompts.md +++ b/docs/prompts.md @@ -35,6 +35,8 @@ ulw 完善性能基准测试 ulw 深度分析下代码质量 +ulw 深度分析下代码架构 + ulw 分析下 lib/fasthttp/ 的源码,然后看下 lolly 的用法合不合理,有没有性能可以提升的地方 ## 兼容性 diff --git a/internal/adapter/common.go b/internal/adapter/common.go index 9e864fa..ecf77b5 100644 --- a/internal/adapter/common.go +++ b/internal/adapter/common.go @@ -9,9 +9,9 @@ // // 关键设计决策: // -// 1. bufferPool 使用 singleton 模式,ctxPool 保持独立 -// 2. CommonAdapter 不包含 ConvertResponse(HTTP/2/HTTP/3 行为不同) -// 3. 阈值常量统一,避免 HTTP/2 inline 和 HTTP/3 constant 不一致 +// 1. bufferPool 使用 singleton 模式,ctxPool 保持独立 +// 2. CommonAdapter 不包含 ConvertResponse(HTTP/2/HTTP/3 行为不同) +// 3. 阈值常量统一,避免 HTTP/2 inline 和 HTTP/3 constant 不一致 // // 作者:xfy package adapter diff --git a/internal/cache/purge.go b/internal/cache/purge.go index 75b16b4..e4213fc 100644 --- a/internal/cache/purge.go +++ b/internal/cache/purge.go @@ -266,4 +266,3 @@ func MatchPattern(pattern, path string) bool { func matchPattern(pattern, path string) bool { return MatchPattern(pattern, path) } - diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 41b984f..eeca0cc 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -182,7 +182,7 @@ func DefaultConfig() *Config { }, Transport: TransportConfig{ IdleConnTimeout: 90 * time.Second, - MaxConnsPerHost: 0, // 0 表示不限制 + MaxConnsPerHost: 512, // fasthttp 推荐值 }, }, Monitoring: MonitoringConfig{ diff --git a/internal/config/defaults_test.go b/internal/config/defaults_test.go index de837c9..594880e 100644 --- a/internal/config/defaults_test.go +++ b/internal/config/defaults_test.go @@ -171,8 +171,8 @@ func TestDefaultConfigPerformance(t *testing.T) { if cfg.Performance.Transport.IdleConnTimeout != 90*time.Second { t.Errorf("Transport.IdleConnTimeout 期望 90s, 实际 %v", cfg.Performance.Transport.IdleConnTimeout) } - if cfg.Performance.Transport.MaxConnsPerHost != 0 { - t.Errorf("Transport.MaxConnsPerHost 期望 0 (不限制), 实际 %d", cfg.Performance.Transport.MaxConnsPerHost) + if cfg.Performance.Transport.MaxConnsPerHost != 512 { + t.Errorf("Transport.MaxConnsPerHost 期望 512 (fasthttp 推荐), 实际 %d", cfg.Performance.Transport.MaxConnsPerHost) } } diff --git a/internal/config/proxy_config.go b/internal/config/proxy_config.go index fdda253..bb64dcc 100644 --- a/internal/config/proxy_config.go +++ b/internal/config/proxy_config.go @@ -261,12 +261,17 @@ type HealthMatchConfig struct { // 使用示例: // // timeout: -// connect: 5s +// dial: 5s # TCP 连接建立超时 +// connect: 30s # 总连接超时(含 DNS/TLS) // read: 30s // write: 30s type ProxyTimeout struct { - // Connect 连接超时 - // 建立到后端服务器的连接超时 + // Dial TCP 连接建立超时 + // 建立 TCP 连接的超时时间(不含 DNS 和 TLS) + Dial time.Duration `yaml:"dial"` + + // Connect 总连接超时 + // 从开始连接到连接可用的总超时时间(含 DNS 和 TLS) Connect time.Duration `yaml:"connect"` // Read 读取超时 diff --git a/internal/handler/static.go b/internal/handler/static.go index abea74c..2c35c2d 100644 --- a/internal/handler/static.go +++ b/internal/handler/static.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "time" @@ -771,8 +772,16 @@ func (h *StaticHandler) validateSymlink(filePath string) error { } // generateETag 基于 ModTime 和 Size 生成 ETag。 +// 使用 strconv.AppendInt 避免 fmt.Sprintf 分配。 func generateETag(modTime time.Time, size int64) string { - return fmt.Sprintf("\"%x-%x\"", modTime.Unix(), size) + var buf [32]byte + b := buf[:0] + b = append(b, '"') + b = strconv.AppendInt(b, modTime.Unix(), 16) + b = append(b, '-') + b = strconv.AppendInt(b, size, 16) + b = append(b, '"') + return string(b) } // isNotModified 检查条件请求是否匹配(返回 true 表示应返回 304)。 diff --git a/internal/http3/adapter.go b/internal/http3/adapter.go index b780ca1..a2c8484 100644 --- a/internal/http3/adapter.go +++ b/internal/http3/adapter.go @@ -16,8 +16,8 @@ import ( "net" "net/http" - "rua.plus/lolly/internal/adapter" "github.com/valyala/fasthttp" + "rua.plus/lolly/internal/adapter" ) // Adapter 将 fasthttp.RequestHandler 适配为 http.Handler。 diff --git a/internal/proxy/cache_handler.go b/internal/proxy/cache_handler.go index fc4b821..db1a9e3 100644 --- a/internal/proxy/cache_handler.go +++ b/internal/proxy/cache_handler.go @@ -9,33 +9,18 @@ import ( "rua.plus/lolly/internal/loadbalance" ) -// buildCacheKey 构建缓存键字符串。 -// -// 使用请求方法和完整请求 URI 作为缓存键。 -// 该函数保留用于日志记录和调试场景。 -// -// 参数: -// - ctx: FastHTTP 请求上下文 -// -// 返回值: -// - string: 缓存键(格式 "METHOD:URI") -func (p *Proxy) buildCacheKey(ctx *fasthttp.RequestCtx) string { - // 使用请求方法和路径作为缓存键 - return string(ctx.Request.Header.Method()) + ":" + string(ctx.Request.URI().RequestURI()) -} - // buildCacheKeyHash 使用 FNV-64a 计算缓存键的 uint64 哈希值。 -// 返回哈希值和原始字符串键。 -// 注意:此函数会先构建字符串键再哈希,存在双重分配。 -// 对于只需要哈希值的场景,使用 buildCacheKeyHashValue 代替。 +// 使用零分配方式构建哈希,避免 []byte(origKey) 转换。 func (p *Proxy) buildCacheKeyHash(ctx *fasthttp.RequestCtx) (uint64, string) { - // 构建原始 key - origKey := p.buildCacheKey(ctx) - - // 使用 FNV-64a 计算哈希 h := fnv.New64a() - h.Write([]byte(origKey)) - return h.Sum64(), origKey + h.Write(ctx.Request.Header.Method()) + h.Write([]byte(":")) + h.Write(ctx.Request.URI().RequestURI()) + hash := h.Sum64() + + // 仅在需要 origKey 时构建字符串 + origKey := b2s(ctx.Request.Header.Method()) + ":" + b2s(ctx.Request.URI().RequestURI()) + return hash, origKey } // buildCacheKeyHashValue 直接计算缓存键的哈希值,零字符串分配。 @@ -111,12 +96,12 @@ func (p *Proxy) backgroundRefresh(ctx *fasthttp.RequestCtx, target *loadbalance. // 处理 304 Not Modified 响应 if resp.StatusCode() == 304 { - newHeaders := make(map[string]string) + newHeaders := make(map[string]string, 5) // 预分配,通常只有 Last-Modified 和 ETag if lm := resp.Header.Peek("Last-Modified"); len(lm) > 0 { - newHeaders["Last-Modified"] = string(lm) + newHeaders["Last-Modified"] = b2s(lm) } if et := resp.Header.Peek("ETag"); len(et) > 0 { - newHeaders["ETag"] = string(et) + newHeaders["ETag"] = b2s(et) } p.cache.RefreshTTL(hashKey, origKey, newHeaders) return diff --git a/internal/proxy/header_modifier.go b/internal/proxy/header_modifier.go index 06ed394..7148fa5 100644 --- a/internal/proxy/header_modifier.go +++ b/internal/proxy/header_modifier.go @@ -75,8 +75,9 @@ func (p *Proxy) modifyResponseHeaders(ctx *fasthttp.RequestCtx) { if len(passSet) > 0 { var toDelete []string for key := range respHeaders.All() { - if !passSet[string(key)] { - toDelete = append(toDelete, string(key)) + // 不在白名单中的应该删除 + if !isInWhitelist(key, passSet) { + toDelete = append(toDelete, b2s(key)) } } for _, k := range toDelete { diff --git a/internal/proxy/health.go b/internal/proxy/health.go index 6e47cdd..c29d79f 100644 --- a/internal/proxy/health.go +++ b/internal/proxy/health.go @@ -234,8 +234,8 @@ func (h *HealthChecker) checkTarget(target *loadbalance.Target) { return } - // 提取响应头(小写 key) - headers := make(map[string]string) + // 提取响应头(小写 key,预分配容量) + headers := make(map[string]string, 20) for key, value := range resp.Header.All() { headers[string(key)] = string(value) } diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 4e6b5f4..ad1d951 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -276,7 +276,7 @@ func createHostClient(targetURL string, timeout config.ProxyTimeout, transportCf // 默认值 maxIdleConnDuration := 90 * time.Second - maxConns := 100 + maxConns := 512 // fasthttp 推荐值 DefaultMaxConnsPerHost // 应用 Transport 配置 if transportCfg != nil { @@ -301,17 +301,25 @@ func createHostClient(targetURL string, timeout config.ProxyTimeout, transportCf SecureErrorLogMessage: false, } - // ProxyBind:使用指定本地地址作为出站连接源 - if proxyBind != "" { - localAddr := proxyBind - dialTimeout := client.MaxConnWaitTimeout - if dialTimeout <= 0 { - dialTimeout = 30 * time.Second - } + // Dial timeout:如果配置了 Dial,使用它作为 TCP 连接建立超时 + // 否则使用 Connect 作为向后兼容 + dialTimeout := timeout.Dial + if dialTimeout <= 0 { + dialTimeout = timeout.Connect + } + if dialTimeout <= 0 { + dialTimeout = 30 * time.Second // 最终默认值 + } + + // 设置自定义 Dial 函数以使用 Dial timeout + // 如果有 ProxyBind 或需要自定义 Dial timeout + if proxyBind != "" || timeout.Dial > 0 { client.Dial = func(addr string) (net.Conn, error) { dialer := &net.Dialer{ - LocalAddr: &net.TCPAddr{IP: net.ParseIP(localAddr)}, - Timeout: dialTimeout, + Timeout: dialTimeout, + } + if proxyBind != "" { + dialer.LocalAddr = &net.TCPAddr{IP: net.ParseIP(proxyBind)} } return dialer.Dial("tcp", addr) } @@ -456,9 +464,14 @@ func FinalizeUpstreamVars(vc *variable.Context, upstreamAddr string, upstreamSta // 如果没有可用的健康目标,返回 502 Bad Gateway。 // 如果后端请求失败,根据 next_upstream 配置尝试下一个目标。 func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { - // DEBUG: 打印请求信息 - logging.Debug().Msgf("[PROXY] 收到请求: path=%s, host=%s, method=%s", - string(ctx.Path()), string(ctx.Host()), string(ctx.Method())) + // DEBUG: 打印请求信息(条件化避免非 Debug 级别的 string() 分配) + if logging.Debug().Enabled() { + logging.Debug(). + Str("path", b2s(ctx.Path())). + Str("host", b2s(ctx.Host())). + Str("method", b2s(ctx.Method())). + Msg("[PROXY] 收到请求") + } // 上游变量捕获 var upstreamAddr string @@ -517,8 +530,13 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { attemptedTargets = append(attemptedTargets, target) - // DEBUG: 打印选中的目标 - logging.Debug().Msgf("[PROXY] 选中目标: url=%s, healthy=%v", target.URL, target.Healthy.Load()) + // DEBUG: 打印选中的目标(条件化避免分配) + if logging.Debug().Enabled() { + logging.Debug(). + Str("url", target.URL). + Bool("healthy", target.Healthy.Load()). + Msg("[PROXY] 选中目标") + } // 获取所选目标的客户端 client := p.getClient(target.URL) @@ -531,8 +549,13 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { continue } - // DEBUG: 打印客户端信息 - logging.Debug().Msgf("[PROXY] client 信息: Addr=%s, IsTLS=%v", client.Addr, client.IsTLS) + // DEBUG: 打印客户端信息(条件化避免分配) + if logging.Debug().Enabled() { + logging.Debug(). + Str("addr", client.Addr). + Bool("isTLS", client.IsTLS). + Msg("[PROXY] client 信息") + } // 增加连接计数(用于最少连接数负载均衡) loadbalance.IncrementConnections(target) @@ -598,9 +621,14 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { } req.SetRequestURIBytes(targetURI) - // DEBUG: 打印请求头 - logging.Debug().Msgf("[PROXY] 请求准备完成: Host=%s, URI=%s, targetURI=%s", - string(req.Header.Host()), string(req.RequestURI()), targetURI) + // DEBUG: 打印请求头(条件化避免分配) + if logging.Debug().Enabled() { + logging.Debug(). + Str("host", b2s(req.Header.Host())). + Str("uri", b2s(req.RequestURI())). + Str("targetURI", b2s(targetURI)). + Msg("[PROXY] 请求准备完成") + } // 尝试从缓存获取(如果启用) if p.cache != nil && attempt == 0 { @@ -679,7 +707,12 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { if err != nil { logging.Error().Msgf("[PROXY] 请求失败: url=%s, err=%v, errType=%T", target.URL, err, err) } else { - logging.Debug().Msgf("[PROXY] 请求成功: url=%s, status=%d", target.URL, ctx.Response.StatusCode()) + if logging.Debug().Enabled() { + logging.Debug(). + Str("url", target.URL). + Int("status", ctx.Response.StatusCode()). + Msg("[PROXY] 请求成功") + } } if err != nil { @@ -805,17 +838,25 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) { var lastModified, etag string for key, value := range ctx.Response.Header.All() { - headerName := strings.ToLower(string(key)) - if ignoreSet[headerName] { + // 使用 bytes.EqualFold 进行大小写不敏感比较,避免 strings.ToLower 分配 + shouldIgnore := false + for _, h := range p.config.Cache.CacheIgnoreHeaders { + if bytes.EqualFold(key, s2b(h)) { + shouldIgnore = true + break + } + } + if shouldIgnore { continue } - headers[string(key)] = string(value) + // 使用 b2s 零分配转换 + headers[b2s(key)] = b2s(value) - switch headerName { - case "last-modified": - lastModified = string(value) - case "etag": - etag = string(value) + // 检查特定头部(使用 bytes.EqualFold) + if bytes.EqualFold(key, []byte("last-modified")) { + lastModified = b2s(value) + } else if bytes.EqualFold(key, []byte("etag")) { + etag = b2s(value) } } p.cache.Set(hashKey, origKey, ctx.Response.Body(), headers, statusCode, p.getCacheDuration(statusCode)) @@ -1010,4 +1051,3 @@ func extractHostFromURL(urlStr string) string { return host } - diff --git a/internal/proxy/utils.go b/internal/proxy/utils.go new file mode 100644 index 0000000..dbcc4b3 --- /dev/null +++ b/internal/proxy/utils.go @@ -0,0 +1,37 @@ +package proxy + +import ( + "bytes" + "unsafe" +) + +// b2s converts byte slice to string without allocation. +// WARNING: The returned string shares memory with the original slice. +// Do not modify the slice after calling this function. +func b2s(b []byte) string { + if len(b) == 0 { + return "" + } + return unsafe.String(&b[0], len(b)) +} + +// s2b converts string to byte slice without allocation. +// WARNING: The returned slice shares memory with the original string. +// Do not modify the slice contents. +func s2b(s string) []byte { + if s == "" { + return nil + } + return unsafe.Slice(unsafe.StringData(s), len(s)) +} + +// isInWhitelist checks if a header key is in the whitelist. +// Uses bytes.EqualFold for case-insensitive comparison without allocation. +func isInWhitelist(key []byte, whitelist map[string]bool) bool { + for wKey := range whitelist { + if bytes.EqualFold(key, s2b(wKey)) { + return true + } + } + return false +} \ No newline at end of file diff --git a/internal/proxy/websocket.go b/internal/proxy/websocket.go index 0dd2590..4404224 100644 --- a/internal/proxy/websocket.go +++ b/internal/proxy/websocket.go @@ -36,6 +36,15 @@ import ( "rua.plus/lolly/internal/netutil" ) +// wsBufPool WebSocket 数据转发 buffer pool。 +// 复用 32KB buffer 避免每次 copyData 调用分配。 +var wsBufPool = sync.Pool{ + New: func() interface{} { + buf := make([]byte, 32*1024) + return &buf + }, +} + // WebSocketBridge WebSocket 桥接器。 // // 在客户端和后端服务器之间建立双向数据通道,透明转发 WebSocket 数据帧。 @@ -118,7 +127,10 @@ func (b *WebSocketBridge) Bridge() error { // 返回值: // - error: 读写错误,连接正常关闭返回 nil func (b *WebSocketBridge) copyData(dst, src net.Conn, direction string) error { - buf := make([]byte, 32*1024) // 32KB 缓冲区 + bufPtr := wsBufPool.Get().(*[]byte) + buf := *bufPtr + defer wsBufPool.Put(bufPtr) + for { n, err := src.Read(buf) if err != nil {