From 9824ad5b57a021fe70f58fa33ab9fa91b156b154 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 11 Jun 2026 14:43:12 +0800 Subject: [PATCH] perf(netutil): cache RemoteAddr string formatting Add netutil.FormatRemoteAddr() to avoid repeated net.TCPAddr.String() allocations on the hot logging path. - IPv4 addresses use a zero-allocation fast path (custom uint8->ASCII). - IPv6 falls back to addr.String() with a 1024-entry LRU cache. - Update logging.LogAccess and variable $remote_addr/$remote_port getters to use the shared helper. Eliminates net.JoinHostPort and net.IP.String from top allocators when access logging is active. --- internal/logging/logging.go | 3 +- internal/netutil/ip.go | 85 ++++++++++++++++++++++++++++++++++++ internal/variable/builtin.go | 11 ++--- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 80609e0..652b3ca 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -27,6 +27,7 @@ import ( "github.com/rs/zerolog" "github.com/valyala/fasthttp" "rua.plus/lolly/internal/config" + "rua.plus/lolly/internal/netutil" "rua.plus/lolly/internal/variable" ) @@ -142,7 +143,7 @@ func (l *Logger) LogAccess(ctx *fasthttp.RequestCtx, status int, size int64, dur // JSON 格式或空格式:输出结构化 JSON if l.accessFormat == formatJSON || l.accessFormat == "" { l.accessLog.Info(). - Str("remote_addr", ctx.RemoteAddr().String()). + Str("remote_addr", netutil.FormatRemoteAddr(ctx)). Bytes("request", append(append(ctx.Method(), ' '), ctx.Path()...)). Int("status", status). Int64("body_bytes_sent", size). diff --git a/internal/netutil/ip.go b/internal/netutil/ip.go index 8d5c184..5c7123a 100644 --- a/internal/netutil/ip.go +++ b/internal/netutil/ip.go @@ -9,6 +9,7 @@ package netutil import ( "net" "strings" + "sync" "github.com/valyala/fasthttp" ) @@ -110,3 +111,87 @@ func GetRemoteAddrIP(ctx *fasthttp.RequestCtx) net.IP { } return nil } + +// remoteAddrCache 缓存 RemoteAddr 字符串化结果,避免重复的 net.TCPAddr.String() 分配。 +type remoteAddrCache struct { + mu sync.RWMutex + entries map[string]string + maxSize int +} + +var globalRemoteAddrCache = &remoteAddrCache{ + entries: make(map[string]string, 1024), + maxSize: 1024, +} + +// FormatRemoteAddr 使用缓存格式化 RemoteAddr,避免重复的地址字符串分配。 +// 优先使用 ctx.RemoteIP() 获取 IP,对 IPv4 直接零分配格式化,IPv6 回退到 addr.String()。 +func FormatRemoteAddr(ctx *fasthttp.RequestCtx) string { + ip := ctx.RemoteIP() + if ip == nil { + addr := ctx.RemoteAddr() + if addr == nil { + return "-" + } + return addr.String() + } + + // 优先尝试 IPv4 快速路径(零分配) + if ipv4 := ip.To4(); ipv4 != nil { + return formatIPv4(ipv4) + } + + // IPv6:尝试缓存 + ipStr := ip.String() + globalRemoteAddrCache.mu.RLock() + if cached, ok := globalRemoteAddrCache.entries[ipStr]; ok { + globalRemoteAddrCache.mu.RUnlock() + return cached + } + globalRemoteAddrCache.mu.RUnlock() + + // 未命中缓存,回退到 addr.String() + addr := ctx.RemoteAddr() + if addr == nil { + return "-" + } + result := addr.String() + + globalRemoteAddrCache.mu.Lock() + if len(globalRemoteAddrCache.entries) < globalRemoteAddrCache.maxSize { + globalRemoteAddrCache.entries[ipStr] = result + } + globalRemoteAddrCache.mu.Unlock() + return result +} + +// formatIPv4 将 4 字节 IPv4 地址格式化为字符串(零分配)。 +func formatIPv4(ip net.IP) string { + var buf [15]byte + n := 0 + for i := 0; i < 4; i++ { + if i > 0 { + buf[n] = '.' + n++ + } + n += writeUint8(buf[n:], ip[i]) + } + return string(buf[:n]) +} + +// writeUint8 将 uint8 写入 buf,返回写入的字节数。 +func writeUint8(buf []byte, v byte) int { + if v >= 100 { + buf[0] = byte('0' + v/100) + buf[1] = byte('0' + (v/10)%10) + buf[2] = byte('0' + v%10) + return 3 + } + if v >= 10 { + buf[0] = byte('0' + v/10) + buf[1] = byte('0' + v%10) + return 2 + } + buf[0] = byte('0' + v) + return 1 +} diff --git a/internal/variable/builtin.go b/internal/variable/builtin.go index 30b8de5..0cfc6e2 100644 --- a/internal/variable/builtin.go +++ b/internal/variable/builtin.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" "github.com/valyala/fasthttp" + "rua.plus/lolly/internal/netutil" ) // 内置变量常量 @@ -81,11 +82,7 @@ func init() { Name: VarRemoteAddr, Description: "客户端 IP 地址", Getter: func(ctx *fasthttp.RequestCtx) string { - addr := ctx.RemoteAddr() - if addr == nil { - return "-" - } - return addr.String() + return netutil.FormatRemoteAddr(ctx) }, }) @@ -98,8 +95,8 @@ func init() { if addr == nil { return "-" } - // 解析地址获取端口 - s := addr.String() + // 解析地址获取端口,优先用 FormatRemoteAddr 缓存的结果 + s := netutil.FormatRemoteAddr(ctx) for i := len(s) - 1; i >= 0; i-- { if s[i] == ':' { return s[i+1:]