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.
This commit is contained in:
xfy 2026-06-11 14:43:12 +08:00
parent 148f43fcb3
commit 9824ad5b57
3 changed files with 91 additions and 8 deletions

View File

@ -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).

View File

@ -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
}

View File

@ -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:]