- 添加 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 <noreply@anthropic.com>
183 lines
5.1 KiB
Go
183 lines
5.1 KiB
Go
package proxy
|
||
|
||
import (
|
||
"hash/fnv"
|
||
"time"
|
||
|
||
"github.com/valyala/fasthttp"
|
||
"rua.plus/lolly/internal/cache"
|
||
"rua.plus/lolly/internal/loadbalance"
|
||
)
|
||
|
||
// buildCacheKeyHash 使用 FNV-64a 计算缓存键的 uint64 哈希值。
|
||
// 使用零分配方式构建哈希,避免 []byte(origKey) 转换。
|
||
func (p *Proxy) buildCacheKeyHash(ctx *fasthttp.RequestCtx) (uint64, string) {
|
||
h := fnv.New64a()
|
||
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 直接计算缓存键的哈希值,零字符串分配。
|
||
// 用于只需要哈希值而不需要原始键的场景。
|
||
func (p *Proxy) buildCacheKeyHashValue(ctx *fasthttp.RequestCtx) uint64 {
|
||
h := fnv.New64a()
|
||
h.Write(ctx.Request.Header.Method())
|
||
h.Write([]byte(":"))
|
||
h.Write(ctx.Request.URI().RequestURI())
|
||
return h.Sum64()
|
||
}
|
||
|
||
// writeCachedResponse 将缓存的响应写入 FastHTTP 响应上下文。
|
||
//
|
||
// 设置响应体、状态码、响应头,并添加 X-Cache: HIT 头标记缓存命中。
|
||
//
|
||
// 参数:
|
||
// - ctx: FastHTTP 请求上下文
|
||
// - entry: 缓存条目,包含响应数据和元数据
|
||
func (p *Proxy) writeCachedResponse(ctx *fasthttp.RequestCtx, entry *cache.ProxyCacheEntry) {
|
||
ctx.Response.SetBody(entry.Data)
|
||
ctx.Response.SetStatusCode(entry.Status)
|
||
for key, value := range entry.Headers {
|
||
ctx.Response.Header.Set(key, value)
|
||
}
|
||
ctx.Response.Header.Set("X-Cache", "HIT")
|
||
}
|
||
|
||
// backgroundRefresh 在后台异步刷新缓存条目。
|
||
//
|
||
// 向对应的上游目标发送请求,获取最新响应并更新缓存。
|
||
// 该方法在独立 goroutine 中运行,不阻塞主请求流程。
|
||
//
|
||
// 参数:
|
||
// - ctx: 原始 FastHTTP 请求上下文(仅用于复制请求信息)
|
||
// - target: 要刷新的后端目标
|
||
// - hashKey: 缓存哈希键
|
||
// - origKey: 缓存原始键
|
||
func (p *Proxy) backgroundRefresh(ctx *fasthttp.RequestCtx, target *loadbalance.Target, hashKey uint64, origKey string) {
|
||
// 创建新的请求上下文副本
|
||
req := fasthttp.AcquireRequest()
|
||
resp := fasthttp.AcquireResponse()
|
||
defer fasthttp.ReleaseRequest(req)
|
||
defer fasthttp.ReleaseResponse(resp)
|
||
|
||
// 复制原始请求
|
||
ctx.Request.CopyTo(req)
|
||
|
||
// 如果启用 Revalidate,添加条件请求头
|
||
if p.config.Cache.Revalidate {
|
||
if entry, ok, _ := p.cache.Get(hashKey, origKey); ok {
|
||
if entry.LastModified != "" {
|
||
req.Header.Set("If-Modified-Since", entry.LastModified)
|
||
}
|
||
if entry.ETag != "" {
|
||
req.Header.Set("If-None-Match", entry.ETag)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取客户端
|
||
client := p.getClient(target.URL)
|
||
if client == nil {
|
||
return
|
||
}
|
||
|
||
// 执行请求
|
||
err := client.Do(req, resp)
|
||
if err != nil {
|
||
p.cache.ReleaseLock(hashKey, err)
|
||
return
|
||
}
|
||
|
||
// 处理 304 Not Modified 响应
|
||
if resp.StatusCode() == 304 {
|
||
newHeaders := make(map[string]string, 5) // 预分配,通常只有 Last-Modified 和 ETag
|
||
if lm := resp.Header.Peek("Last-Modified"); len(lm) > 0 {
|
||
newHeaders["Last-Modified"] = b2s(lm)
|
||
}
|
||
if et := resp.Header.Peek("ETag"); len(et) > 0 {
|
||
newHeaders["ETag"] = b2s(et)
|
||
}
|
||
p.cache.RefreshTTL(hashKey, origKey, newHeaders)
|
||
return
|
||
}
|
||
|
||
// 提取响应头(使用 pool 复用 map)
|
||
headers, ok := headersPool.Get().(map[string]string)
|
||
if !ok {
|
||
headers = make(map[string]string, 20)
|
||
}
|
||
for k := range headers {
|
||
delete(headers, k)
|
||
}
|
||
for key, value := range resp.Header.All() {
|
||
headers[string(key)] = string(value)
|
||
}
|
||
|
||
// 更新缓存
|
||
p.cache.Set(hashKey, origKey, resp.Body(), headers, resp.StatusCode(), p.getCacheDuration(resp.StatusCode()))
|
||
}
|
||
|
||
// GetCache 返回代理的 ProxyCache 实例(用于 purge handler)。
|
||
// 如果缓存未启用,返回 nil。
|
||
func (p *Proxy) GetCache() *cache.ProxyCache {
|
||
return p.cache
|
||
}
|
||
|
||
// GetCacheStats 返回代理缓存的统计信息。
|
||
// 如果缓存未启用,返回 nil。
|
||
func (p *Proxy) GetCacheStats() *cache.ProxyCacheStats {
|
||
if p.cache == nil {
|
||
return nil
|
||
}
|
||
stats := p.cache.Stats()
|
||
return &stats
|
||
}
|
||
|
||
// getCacheDuration 根据状态码获取缓存时间。
|
||
// 优先级:CacheValid 配置 > MaxAge
|
||
//
|
||
// 映射规则:
|
||
// - 200-299: CacheValid.OK(0 时继承 MaxAge)
|
||
// - 301/302: CacheValid.Redirect
|
||
// - 404: CacheValid.NotFound
|
||
// - 400-499(除 404): CacheValid.ClientError
|
||
// - 500-599: CacheValid.ServerError
|
||
// - 其他: 不缓存(返回 0)
|
||
func (p *Proxy) getCacheDuration(statusCode int) time.Duration {
|
||
// 无 CacheValid 配置,使用 MaxAge
|
||
if p.config.CacheValid == nil {
|
||
return p.config.Cache.MaxAge
|
||
}
|
||
|
||
cv := p.config.CacheValid
|
||
|
||
switch {
|
||
case statusCode >= 200 && statusCode < 300:
|
||
if cv.OK > 0 {
|
||
return cv.OK
|
||
}
|
||
return p.config.Cache.MaxAge // 0 表示继承 MaxAge
|
||
|
||
case statusCode == 301 || statusCode == 302:
|
||
return cv.Redirect // 0 表示不缓存
|
||
|
||
case statusCode == 404:
|
||
return cv.NotFound
|
||
|
||
case statusCode >= 400 && statusCode < 500:
|
||
return cv.ClientError
|
||
|
||
case statusCode >= 500:
|
||
return cv.ServerError
|
||
|
||
default:
|
||
return 0 // 不缓存
|
||
}
|
||
}
|