perf(handler): enable file info cache and fix index file cache lookup

- Router now enables FileInfoCache with 2s TTL for all static handlers
  to reduce redundant os.Stat calls.
- FileInfoCache supports negative caching: missing files are cached
  with a shorter TTL to avoid repeated stat on non-existent paths.
- Fix missing fileCache lookup for directory index files (index.html).
  Previously handleStandard/handleTryFiles skipped fileCache when
  serving index files, causing os.ReadFile on every request even
  with file_cache configured.
- Extract tryServeFromFileCache() helper to unify cache hit logic
  across file and index-file serving paths.

Verified with wrk 200 conn / 20s on static /index.html:
- Throughput: 140k -> 242k req/sec (+73%)
- alloc_space: 2.6 GB -> 4.6 MB (-99.8%)
This commit is contained in:
xfy 2026-06-11 14:43:04 +08:00
parent 047e033af5
commit 148f43fcb3

View File

@ -352,6 +352,57 @@ func (h *StaticHandler) Handle(ctx *fasthttp.RequestCtx) {
h.handleStandard(ctx, reqPath)
}
// tryServeFromFileCache 尝试从文件缓存直接响应。
// 命中缓存且文件未修改时直接写入响应并返回 true。
func (h *StaticHandler) tryServeFromFileCache(ctx *fasthttp.RequestCtx, filePath string, info os.FileInfo) bool {
if h.fileCache == nil {
return false
}
entry, ok := h.fileCache.Get(filePath)
if !ok {
return false
}
// TTL 验证cacheTTL > 0 时启用)
if h.cacheTTL > 0 && time.Since(entry.CachedAt) < h.cacheTTL {
if isNotModified(ctx, entry.ETag, info.ModTime()) {
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
ctx.Response.Header.Set("ETag", entry.ETag)
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
ctx.Response.SkipBody = true
return true
}
ctx.Response.SetBody(entry.Data)
ctx.Response.Header.SetContentType(entry.ContentType)
ctx.Response.Header.Set("ETag", entry.ETag)
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
return true
}
// TTL 过期或未启用 TTL验证文件新鲜度
if entry.ModTime.Equal(info.ModTime()) {
if isNotModified(ctx, entry.ETag, info.ModTime()) {
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
ctx.Response.Header.Set("ETag", entry.ETag)
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
ctx.Response.SkipBody = true
return true
}
if h.cacheTTL > 0 {
h.fileCache.RefreshCachedAt(filePath)
}
ctx.Response.SetBody(entry.Data)
ctx.Response.Header.SetContentType(entry.ContentType)
ctx.Response.Header.Set("ETag", entry.ETag)
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
return true
}
// 文件已修改,删除缓存
h.fileCache.Delete(filePath)
return false
}
// handleTryFiles 处理 try_files 逻辑。
//
// 按顺序尝试查找文件,支持 $uri 和 $uri/ 占位符。
@ -389,6 +440,9 @@ func (h *StaticHandler) handleTryFiles(ctx *fasthttp.RequestCtx, reqPath string)
continue
}
if !idxInfo.IsDir() {
if h.tryServeFromFileCache(ctx, idxPath, idxInfo) {
return
}
h.serveFile(ctx, idxPath, idxInfo, false)
return
}
@ -532,6 +586,9 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string)
idxPath := filepath.Join(filePath, idx)
idxInfo, idxExists, _ := h.statWithCache(idxPath)
if idxExists && !idxInfo.IsDir() {
if h.tryServeFromFileCache(ctx, idxPath, idxInfo) {
return
}
h.serveFile(ctx, idxPath, idxInfo, true)
return
}
@ -551,52 +608,9 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string)
return
}
// Phase 2: 缓存查找 + TTL 验证 // 在 serveFile 调用前检查缓存,减少 os.ReadFile 调用
// 注意: CachedAt 迁移已在 FileCache.Get() 内部完成,确保并发安全
if h.fileCache != nil {
if entry, ok := h.fileCache.Get(filePath); ok {
// TTL 验证cacheTTL > 0 时启用)
if h.cacheTTL > 0 && time.Since(entry.CachedAt) < h.cacheTTL {
// TTL 内直接返回(无需验证 ModTime
// 使用缓存的 ETag避免重新生成
if isNotModified(ctx, entry.ETag, info.ModTime()) {
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
ctx.Response.Header.Set("ETag", entry.ETag)
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
ctx.Response.SkipBody = true
return
}
ctx.Response.SetBody(entry.Data)
ctx.Response.Header.SetContentType(entry.ContentType)
ctx.Response.Header.Set("ETag", entry.ETag)
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
return
}
// TTL 过期或未启用 TTL验证文件新鲜度
if entry.ModTime.Equal(info.ModTime()) {
// 文件未修改,刷新 TTL 并返回
// 使用缓存的 ETag避免重新生成
if isNotModified(ctx, entry.ETag, info.ModTime()) {
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
ctx.Response.Header.Set("ETag", entry.ETag)
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
ctx.Response.SkipBody = true
return
}
if h.cacheTTL > 0 {
h.fileCache.RefreshCachedAt(filePath)
}
ctx.Response.SetBody(entry.Data)
ctx.Response.Header.SetContentType(entry.ContentType)
ctx.Response.Header.Set("ETag", entry.ETag)
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
return
}
// 文件已修改,删除缓存继续处理
h.fileCache.Delete(filePath)
}
// Phase 2: 缓存查找 + TTL 验证,减少 os.ReadFile 调用
if h.tryServeFromFileCache(ctx, filePath, info) {
return
}
// Phase 3: 缓存未命中,调用 serveFile 处理