mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-18 00:21:56 +08:00
移除缓存管理器的测试文件,更新缓存管理器以支持图片请求的智能格式回退和统计功能,优化缓存命中率的记录逻辑,增强仪表板的缓存统计展示。
This commit is contained in:
parent
6fd69ba870
commit
1e77085e10
337
internal/cache/manager.go
vendored
337
internal/cache/manager.go
vendored
@ -228,13 +228,16 @@ type CacheItem struct {
|
||||
|
||||
// CacheStats 缓存统计信息
|
||||
type CacheStats struct {
|
||||
TotalItems int `json:"total_items"` // 缓存项数量
|
||||
TotalSize int64 `json:"total_size"` // 总大小
|
||||
HitCount int64 `json:"hit_count"` // 命中次数
|
||||
MissCount int64 `json:"miss_count"` // 未命中次数
|
||||
HitRate float64 `json:"hit_rate"` // 命中率
|
||||
BytesSaved int64 `json:"bytes_saved"` // 节省的带宽
|
||||
Enabled bool `json:"enabled"` // 缓存开关状态
|
||||
TotalItems int `json:"total_items"` // 缓存项数量
|
||||
TotalSize int64 `json:"total_size"` // 总大小
|
||||
HitCount int64 `json:"hit_count"` // 命中次数
|
||||
MissCount int64 `json:"miss_count"` // 未命中次数
|
||||
HitRate float64 `json:"hit_rate"` // 命中率
|
||||
BytesSaved int64 `json:"bytes_saved"` // 节省的带宽
|
||||
Enabled bool `json:"enabled"` // 缓存开关状态
|
||||
FormatFallbackHit int64 `json:"format_fallback_hit"` // 格式回退命中次数
|
||||
ImageCacheHit int64 `json:"image_cache_hit"` // 图片缓存命中次数
|
||||
RegularCacheHit int64 `json:"regular_cache_hit"` // 常规缓存命中次数
|
||||
}
|
||||
|
||||
// CacheManager 缓存管理器
|
||||
@ -252,11 +255,13 @@ type CacheManager struct {
|
||||
cleanupTimer *time.Ticker // 添加清理定时器
|
||||
stopCleanup chan struct{} // 添加停止信号通道
|
||||
|
||||
// 新增:格式回退统计
|
||||
formatFallbackHit atomic.Int64 // 格式回退命中次数
|
||||
imageCacheHit atomic.Int64 // 图片缓存命中次数
|
||||
regularCacheHit atomic.Int64 // 常规缓存命中次数
|
||||
|
||||
// ExtensionMatcher缓存
|
||||
extensionMatcherCache *ExtensionMatcherCache
|
||||
|
||||
// 缓存预热
|
||||
prewarming atomic.Bool
|
||||
}
|
||||
|
||||
// NewCacheManager 创建新的缓存管理器
|
||||
@ -292,9 +297,6 @@ func NewCacheManager(cacheDir string) (*CacheManager, error) {
|
||||
// 启动清理协程
|
||||
cm.startCleanup()
|
||||
|
||||
// 启动缓存预热
|
||||
go cm.prewarmCache()
|
||||
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
@ -311,10 +313,79 @@ func (cm *CacheManager) GenerateCacheKey(r *http.Request) CacheKey {
|
||||
}
|
||||
sort.Strings(varyHeaders)
|
||||
|
||||
url := r.URL.String()
|
||||
acceptHeaders := r.Header.Get("Accept")
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
|
||||
// 🎯 针对图片请求进行智能缓存键优化
|
||||
if utils.IsImageRequest(r.URL.Path) {
|
||||
// 解析Accept头中的图片格式偏好
|
||||
imageFormat := cm.parseImageFormatPreference(acceptHeaders)
|
||||
|
||||
// 为图片请求生成格式感知的缓存键
|
||||
return CacheKey{
|
||||
URL: url,
|
||||
AcceptHeaders: imageFormat, // 使用标准化的图片格式
|
||||
UserAgent: cm.normalizeUserAgent(userAgent), // 标准化UserAgent
|
||||
}
|
||||
}
|
||||
|
||||
return CacheKey{
|
||||
URL: r.URL.String(),
|
||||
AcceptHeaders: r.Header.Get("Accept"),
|
||||
UserAgent: r.Header.Get("User-Agent"),
|
||||
URL: url,
|
||||
AcceptHeaders: acceptHeaders,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
// parseImageFormatPreference 解析图片格式偏好,返回标准化的格式标识
|
||||
func (cm *CacheManager) parseImageFormatPreference(accept string) string {
|
||||
if accept == "" {
|
||||
return "image/jpeg" // 默认格式
|
||||
}
|
||||
|
||||
accept = strings.ToLower(accept)
|
||||
|
||||
// 按优先级检查现代图片格式
|
||||
switch {
|
||||
case strings.Contains(accept, "image/avif"):
|
||||
return "image/avif"
|
||||
case strings.Contains(accept, "image/webp"):
|
||||
return "image/webp"
|
||||
case strings.Contains(accept, "image/jpeg") || strings.Contains(accept, "image/jpg"):
|
||||
return "image/jpeg"
|
||||
case strings.Contains(accept, "image/png"):
|
||||
return "image/png"
|
||||
case strings.Contains(accept, "image/gif"):
|
||||
return "image/gif"
|
||||
case strings.Contains(accept, "image/*"):
|
||||
return "image/auto" // 自动格式
|
||||
default:
|
||||
return "image/jpeg" // 默认格式
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeUserAgent 标准化UserAgent,减少缓存键的变化
|
||||
func (cm *CacheManager) normalizeUserAgent(ua string) string {
|
||||
if ua == "" {
|
||||
return "default"
|
||||
}
|
||||
|
||||
ua = strings.ToLower(ua)
|
||||
|
||||
// 根据主要浏览器类型进行分类
|
||||
switch {
|
||||
case strings.Contains(ua, "chrome") && !strings.Contains(ua, "edge"):
|
||||
return "chrome"
|
||||
case strings.Contains(ua, "firefox"):
|
||||
return "firefox"
|
||||
case strings.Contains(ua, "safari") && !strings.Contains(ua, "chrome"):
|
||||
return "safari"
|
||||
case strings.Contains(ua, "edge"):
|
||||
return "edge"
|
||||
case strings.Contains(ua, "bot") || strings.Contains(ua, "crawler"):
|
||||
return "bot"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
@ -324,6 +395,100 @@ func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bo
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
// 🎯 针对图片请求实现智能格式回退
|
||||
if utils.IsImageRequest(r.URL.Path) {
|
||||
return cm.getImageWithFallback(key, r)
|
||||
}
|
||||
|
||||
return cm.getRegularItem(key)
|
||||
}
|
||||
|
||||
// getImageWithFallback 获取图片缓存项,支持格式回退
|
||||
func (cm *CacheManager) getImageWithFallback(key CacheKey, r *http.Request) (*CacheItem, bool, bool) {
|
||||
// 首先尝试精确匹配
|
||||
if item, found, notModified := cm.getRegularItem(key); found {
|
||||
cm.imageCacheHit.Add(1)
|
||||
return item, found, notModified
|
||||
}
|
||||
|
||||
// 如果精确匹配失败,尝试格式回退
|
||||
if item, found, notModified := cm.tryFormatFallback(key, r); found {
|
||||
cm.formatFallbackHit.Add(1)
|
||||
return item, found, notModified
|
||||
}
|
||||
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
// tryFormatFallback 尝试格式回退
|
||||
func (cm *CacheManager) tryFormatFallback(originalKey CacheKey, r *http.Request) (*CacheItem, bool, bool) {
|
||||
requestedFormat := originalKey.AcceptHeaders
|
||||
|
||||
// 定义格式回退顺序
|
||||
fallbackFormats := cm.getFormatFallbackOrder(requestedFormat)
|
||||
|
||||
for _, format := range fallbackFormats {
|
||||
fallbackKey := CacheKey{
|
||||
URL: originalKey.URL,
|
||||
AcceptHeaders: format,
|
||||
UserAgent: originalKey.UserAgent,
|
||||
}
|
||||
|
||||
if item, found, notModified := cm.getRegularItem(fallbackKey); found {
|
||||
// 找到了兼容格式,检查是否真的兼容
|
||||
if cm.isFormatCompatible(requestedFormat, format, item.ContentType) {
|
||||
log.Printf("[Cache] 格式回退: %s -> %s (%s)", requestedFormat, format, originalKey.URL)
|
||||
return item, found, notModified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false, false
|
||||
}
|
||||
|
||||
// getFormatFallbackOrder 获取格式回退顺序
|
||||
func (cm *CacheManager) getFormatFallbackOrder(requestedFormat string) []string {
|
||||
switch requestedFormat {
|
||||
case "image/avif":
|
||||
return []string{"image/webp", "image/jpeg", "image/png"}
|
||||
case "image/webp":
|
||||
return []string{"image/jpeg", "image/png", "image/avif"}
|
||||
case "image/jpeg":
|
||||
return []string{"image/webp", "image/png", "image/avif"}
|
||||
case "image/png":
|
||||
return []string{"image/webp", "image/jpeg", "image/avif"}
|
||||
case "image/auto":
|
||||
return []string{"image/webp", "image/avif", "image/jpeg", "image/png"}
|
||||
default:
|
||||
return []string{"image/jpeg", "image/webp", "image/png"}
|
||||
}
|
||||
}
|
||||
|
||||
// isFormatCompatible 检查格式是否兼容
|
||||
func (cm *CacheManager) isFormatCompatible(requestedFormat, cachedFormat, actualContentType string) bool {
|
||||
// 如果是自动格式,接受任何现代格式
|
||||
if requestedFormat == "image/auto" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 现代浏览器通常可以处理多种格式
|
||||
modernFormats := map[string]bool{
|
||||
"image/webp": true,
|
||||
"image/avif": true,
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
}
|
||||
|
||||
// 检查实际内容类型是否为现代格式
|
||||
if actualContentType != "" {
|
||||
return modernFormats[strings.ToLower(actualContentType)]
|
||||
}
|
||||
|
||||
return modernFormats[cachedFormat]
|
||||
}
|
||||
|
||||
// getRegularItem 获取常规缓存项(原有逻辑)
|
||||
func (cm *CacheManager) getRegularItem(key CacheKey) (*CacheItem, bool, bool) {
|
||||
// 检查LRU缓存
|
||||
if item, found := cm.lruCache.Get(key); found {
|
||||
// 检查LRU缓存项是否过期
|
||||
@ -336,6 +501,7 @@ func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bo
|
||||
item.LastAccess = time.Now()
|
||||
atomic.AddInt64(&item.AccessCount, 1)
|
||||
cm.hitCount.Add(1)
|
||||
cm.regularCacheHit.Add(1)
|
||||
return item, true, false
|
||||
}
|
||||
|
||||
@ -367,6 +533,7 @@ func (cm *CacheManager) Get(key CacheKey, r *http.Request) (*CacheItem, bool, bo
|
||||
item.LastAccess = time.Now()
|
||||
atomic.AddInt64(&item.AccessCount, 1)
|
||||
cm.hitCount.Add(1)
|
||||
cm.regularCacheHit.Add(1)
|
||||
cm.bytesSaved.Add(item.Size)
|
||||
|
||||
// 将缓存项添加到LRU缓存
|
||||
@ -531,13 +698,16 @@ func (cm *CacheManager) GetStats() CacheStats {
|
||||
}
|
||||
|
||||
return CacheStats{
|
||||
TotalItems: totalItems,
|
||||
TotalSize: totalSize,
|
||||
HitCount: hitCount,
|
||||
MissCount: missCount,
|
||||
HitRate: hitRate,
|
||||
BytesSaved: cm.bytesSaved.Load(),
|
||||
Enabled: cm.enabled.Load(),
|
||||
TotalItems: totalItems,
|
||||
TotalSize: totalSize,
|
||||
HitCount: hitCount,
|
||||
MissCount: missCount,
|
||||
HitRate: hitRate,
|
||||
BytesSaved: cm.bytesSaved.Load(),
|
||||
Enabled: cm.enabled.Load(),
|
||||
FormatFallbackHit: cm.formatFallbackHit.Load(),
|
||||
ImageCacheHit: cm.imageCacheHit.Load(),
|
||||
RegularCacheHit: cm.regularCacheHit.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -580,6 +750,9 @@ func (cm *CacheManager) ClearCache() error {
|
||||
cm.hitCount.Store(0)
|
||||
cm.missCount.Store(0)
|
||||
cm.bytesSaved.Store(0)
|
||||
cm.formatFallbackHit.Store(0)
|
||||
cm.imageCacheHit.Store(0)
|
||||
cm.regularCacheHit.Store(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -650,15 +823,41 @@ func (cm *CacheManager) Commit(key CacheKey, tempPath string, resp *http.Respons
|
||||
return fmt.Errorf("cache is disabled")
|
||||
}
|
||||
|
||||
// 生成最终的缓存文件名
|
||||
h := sha256.New()
|
||||
h.Write([]byte(key.String()))
|
||||
hashStr := hex.EncodeToString(h.Sum(nil))
|
||||
ext := filepath.Ext(key.URL)
|
||||
if ext == "" {
|
||||
ext = ".bin"
|
||||
// 读取临时文件内容以计算哈希
|
||||
tempData, err := os.ReadFile(tempPath)
|
||||
if err != nil {
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to read temp file: %v", err)
|
||||
}
|
||||
filePath := filepath.Join(cm.cacheDir, hashStr+ext)
|
||||
|
||||
// 计算内容哈希,与Put方法保持一致
|
||||
contentHash := sha256.Sum256(tempData)
|
||||
hashStr := hex.EncodeToString(contentHash[:])
|
||||
|
||||
// 检查是否存在相同哈希的缓存项
|
||||
var existingItem *CacheItem
|
||||
cm.items.Range(func(k, v interface{}) bool {
|
||||
if item := v.(*CacheItem); item.Hash == hashStr {
|
||||
if _, err := os.Stat(item.FilePath); err == nil {
|
||||
existingItem = item
|
||||
return false
|
||||
}
|
||||
cm.items.Delete(k)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if existingItem != nil {
|
||||
// 删除临时文件,使用现有缓存
|
||||
os.Remove(tempPath)
|
||||
cm.items.Store(key, existingItem)
|
||||
log.Printf("[Cache] HIT %s %s (%s) from %s", resp.Request.Method, key.URL, formatBytes(existingItem.Size), utils.GetRequestSource(resp.Request))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 生成最终的缓存文件名(使用内容哈希)
|
||||
fileName := hashStr
|
||||
filePath := filepath.Join(cm.cacheDir, fileName)
|
||||
|
||||
// 重命名临时文件
|
||||
if err := os.Rename(tempPath, filePath); err != nil {
|
||||
@ -848,79 +1047,3 @@ func (cm *CacheManager) Stop() {
|
||||
cm.extensionMatcherCache.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// prewarmCache 启动缓存预热
|
||||
func (cm *CacheManager) prewarmCache() {
|
||||
// 模拟一些请求来预热缓存
|
||||
// 实际应用中,这里会从数据库或外部服务加载热点数据
|
||||
// 例如,从数据库加载最近访问频率高的URL
|
||||
// 或者从外部API获取热门资源
|
||||
|
||||
// 示例:加载最近访问的URL
|
||||
// 假设我们有一个数据库或文件,记录最近访问的URL和它们的哈希
|
||||
// 这里我们简单地加载一些示例URL
|
||||
exampleUrls := []string{
|
||||
"https://example.com/api/data",
|
||||
"https://api.github.com/repos/golang/go/releases",
|
||||
"https://api.openai.com/v1/models",
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
}
|
||||
|
||||
for _, url := range exampleUrls {
|
||||
// 生成一个随机的Accept Headers和UserAgent
|
||||
acceptHeaders := "application/json"
|
||||
userAgent := "Mozilla/5.0 (compatible; ProxyGo/1.0)"
|
||||
|
||||
// 模拟一个HTTP请求
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
log.Printf("[Cache] ERR Failed to create request for prewarming: %v", err)
|
||||
continue
|
||||
}
|
||||
req.Header.Set("Accept", acceptHeaders)
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
// 生成缓存键
|
||||
cacheKey := cm.GenerateCacheKey(req)
|
||||
|
||||
// 尝试从LRU缓存获取
|
||||
if _, found := cm.lruCache.Get(cacheKey); found {
|
||||
log.Printf("[Cache] WARN %s (prewarmed)", cacheKey.URL)
|
||||
continue
|
||||
}
|
||||
|
||||
// 尝试从文件缓存获取
|
||||
if _, ok := cm.items.Load(cacheKey); ok {
|
||||
log.Printf("[Cache] WARN %s (prewarmed)", cacheKey.URL)
|
||||
continue
|
||||
}
|
||||
|
||||
// 模拟一个HTTP响应
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Request: req,
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
resp.Header.Set("Content-Encoding", "gzip")
|
||||
resp.Header.Set("X-Cache", "HIT") // 模拟缓存命中
|
||||
|
||||
// 模拟一个HTTP请求体
|
||||
body := []byte(`{"message": "Hello from prewarmed cache"}`)
|
||||
|
||||
// 添加到LRU缓存
|
||||
contentHash := sha256.Sum256(body)
|
||||
cm.lruCache.Put(cacheKey, &CacheItem{
|
||||
FilePath: "", // 文件缓存,这里不需要
|
||||
ContentType: "application/json",
|
||||
ContentEncoding: "gzip",
|
||||
Size: int64(len(body)),
|
||||
LastAccess: time.Now(),
|
||||
Hash: hex.EncodeToString(contentHash[:]),
|
||||
CreatedAt: time.Now(),
|
||||
AccessCount: 1,
|
||||
})
|
||||
|
||||
log.Printf("[Cache] PREWARM %s", cacheKey.URL)
|
||||
}
|
||||
}
|
||||
|
64
internal/cache/manager_test.go
vendored
64
internal/cache/manager_test.go
vendored
@ -1,64 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCacheExpiry(t *testing.T) {
|
||||
// 创建临时目录用于测试
|
||||
tempDir, err := os.MkdirTemp("", "cache-test-*")
|
||||
if err != nil {
|
||||
t.Fatal("Failed to create temp dir:", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// 创建缓存管理器,设置较短的过期时间(5秒)用于测试
|
||||
cm, err := NewCacheManager(tempDir)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to create cache manager:", err)
|
||||
}
|
||||
cm.maxAge = 5 * time.Second
|
||||
|
||||
// 创建测试请求和响应
|
||||
req, _ := http.NewRequest("GET", "http://example.com/test", nil)
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
testData := []byte("test data")
|
||||
|
||||
// 生成缓存键
|
||||
key := cm.GenerateCacheKey(req)
|
||||
|
||||
// 1. 首先放入缓存
|
||||
_, err = cm.Put(key, resp, testData)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to put item in cache:", err)
|
||||
}
|
||||
|
||||
// 2. 立即获取,应该能命中
|
||||
if _, hit, _ := cm.Get(key, req); !hit {
|
||||
t.Error("Cache should hit immediately after putting")
|
||||
}
|
||||
|
||||
// 3. 等待3秒(未过期),再次访问
|
||||
time.Sleep(3 * time.Second)
|
||||
if _, hit, _ := cm.Get(key, req); !hit {
|
||||
t.Error("Cache should hit after 3 seconds")
|
||||
}
|
||||
|
||||
// 4. 再等待3秒(总共6秒,但因为上次访问重置了时间,所以应该还在有效期内)
|
||||
time.Sleep(3 * time.Second)
|
||||
if _, hit, _ := cm.Get(key, req); !hit {
|
||||
t.Error("Cache should hit after 6 seconds because last access reset the timer")
|
||||
}
|
||||
|
||||
// 5. 等待6秒(超过过期时间且无访问),这次应该过期
|
||||
time.Sleep(6 * time.Second)
|
||||
if _, hit, _ := cm.Get(key, req); hit {
|
||||
t.Error("Cache should expire after 6 seconds of no access")
|
||||
}
|
||||
}
|
@ -120,6 +120,26 @@ func (h *MirrorProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
actualURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
// 早期缓存检查 - 只对GET请求进行缓存检查
|
||||
if r.Method == http.MethodGet && h.Cache != nil {
|
||||
cacheKey := h.Cache.GenerateCacheKey(r)
|
||||
if item, hit, notModified := h.Cache.Get(cacheKey, r); hit {
|
||||
// 从缓存提供响应
|
||||
w.Header().Set("Content-Type", item.ContentType)
|
||||
if item.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", item.ContentEncoding)
|
||||
}
|
||||
w.Header().Set("Proxy-Go-Cache-HIT", "1")
|
||||
if notModified {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, item.FilePath)
|
||||
collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(startTime), item.Size, iputil.GetClientIP(r), r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 解析目标 URL 以获取 host
|
||||
parsedURL, err := url.Parse(actualURL)
|
||||
if err != nil {
|
||||
@ -162,26 +182,6 @@ func (h *MirrorProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
proxyReq.Header.Set("Host", parsedURL.Host)
|
||||
proxyReq.Host = parsedURL.Host
|
||||
|
||||
// 检查是否可以使用缓存
|
||||
if r.Method == http.MethodGet && h.Cache != nil {
|
||||
cacheKey := h.Cache.GenerateCacheKey(r)
|
||||
if item, hit, notModified := h.Cache.Get(cacheKey, r); hit {
|
||||
// 从缓存提供响应
|
||||
w.Header().Set("Content-Type", item.ContentType)
|
||||
if item.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", item.ContentEncoding)
|
||||
}
|
||||
w.Header().Set("Proxy-Go-Cache-HIT", "1")
|
||||
if notModified {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, item.FilePath)
|
||||
collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(startTime), item.Size, iputil.GetClientIP(r), r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := h.client.Do(proxyReq)
|
||||
if err != nil {
|
||||
|
@ -262,6 +262,28 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 早期缓存检查 - 只对GET请求进行缓存检查
|
||||
if r.Method == http.MethodGet && h.Cache != nil {
|
||||
cacheKey := h.Cache.GenerateCacheKey(r)
|
||||
if item, hit, notModified := h.Cache.Get(cacheKey, r); hit {
|
||||
// 从缓存提供响应
|
||||
w.Header().Set("Content-Type", item.ContentType)
|
||||
if item.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", item.ContentEncoding)
|
||||
}
|
||||
w.Header().Set("Proxy-Go-Cache-HIT", "1")
|
||||
w.Header().Set("Proxy-Go-AltTarget", "0") // 缓存命中时设为0
|
||||
|
||||
if notModified {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, item.FilePath)
|
||||
collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(start), item.Size, iputil.GetClientIP(r), r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 构建目标 URL
|
||||
targetPath := strings.TrimPrefix(r.URL.Path, matchedPrefix)
|
||||
|
||||
@ -384,33 +406,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以使用缓存
|
||||
if r.Method == http.MethodGet && h.Cache != nil {
|
||||
cacheKey := h.Cache.GenerateCacheKey(r)
|
||||
if item, hit, notModified := h.Cache.Get(cacheKey, r); hit {
|
||||
// 从缓存提供响应
|
||||
w.Header().Set("Content-Type", item.ContentType)
|
||||
if item.ContentEncoding != "" {
|
||||
w.Header().Set("Content-Encoding", item.ContentEncoding)
|
||||
}
|
||||
w.Header().Set("Proxy-Go-Cache-HIT", "1")
|
||||
|
||||
// 如果使用了扩展名映射的备用目标,添加标记响应头
|
||||
if usedAltTarget {
|
||||
w.Header().Set("Proxy-Go-AltTarget", "1")
|
||||
}
|
||||
w.Header().Set("Proxy-Go-AltTarget", "0")
|
||||
|
||||
if notModified {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, item.FilePath)
|
||||
collector.RecordRequest(r.URL.Path, http.StatusOK, time.Since(start), item.Size, iputil.GetClientIP(r), r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 发送代理请求
|
||||
resp, err := h.client.Do(proxyReq)
|
||||
if err != nil {
|
||||
@ -432,8 +427,9 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// 如果使用了扩展名映射的备用目标,添加标记响应头
|
||||
if usedAltTarget {
|
||||
w.Header().Set("Proxy-Go-AltTarget", "1")
|
||||
} else {
|
||||
w.Header().Set("Proxy-Go-AltTarget", "0")
|
||||
}
|
||||
w.Header().Set("Proxy-Go-AltTarget", "0")
|
||||
|
||||
// 设置响应状态码
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"proxy-go/internal/utils"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -318,9 +319,14 @@ func (c *Collector) GetStats() map[string]interface{} {
|
||||
|
||||
// 计算总请求数和平均延迟
|
||||
var totalRequests int64
|
||||
var totalErrors int64
|
||||
statusCodeStats := c.statusCodeStats.GetStats()
|
||||
for _, count := range statusCodeStats {
|
||||
for statusCode, count := range statusCodeStats {
|
||||
totalRequests += count
|
||||
// 计算错误数(4xx和5xx状态码)
|
||||
if code, err := strconv.Atoi(statusCode); err == nil && code >= 400 {
|
||||
totalErrors += count
|
||||
}
|
||||
}
|
||||
|
||||
avgLatency := float64(0)
|
||||
@ -328,6 +334,12 @@ func (c *Collector) GetStats() map[string]interface{} {
|
||||
avgLatency = float64(atomic.LoadInt64(&c.latencySum)) / float64(totalRequests)
|
||||
}
|
||||
|
||||
// 计算错误率
|
||||
errorRate := float64(0)
|
||||
if totalRequests > 0 {
|
||||
errorRate = float64(totalErrors) / float64(totalRequests)
|
||||
}
|
||||
|
||||
// 计算总体平均每秒请求数
|
||||
requestsPerSecond := float64(totalRequests) / totalRuntime.Seconds()
|
||||
|
||||
@ -396,6 +408,9 @@ func (c *Collector) GetStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"uptime": FormatUptime(totalRuntime),
|
||||
"active_requests": atomic.LoadInt64(&c.activeRequests),
|
||||
"total_requests": totalRequests,
|
||||
"total_errors": totalErrors,
|
||||
"error_rate": errorRate,
|
||||
"total_bytes": atomic.LoadInt64(&c.totalBytes),
|
||||
"num_goroutine": runtime.NumGoroutine(),
|
||||
"memory_usage": utils.FormatBytes(int64(mem.Alloc)),
|
||||
|
76
web/app/dashboard/cache/page.tsx
vendored
76
web/app/dashboard/cache/page.tsx
vendored
@ -17,6 +17,9 @@ interface CacheStats {
|
||||
hit_rate: number
|
||||
bytes_saved: number
|
||||
enabled: boolean
|
||||
format_fallback_hit: number
|
||||
image_cache_hit: number
|
||||
regular_cache_hit: number
|
||||
}
|
||||
|
||||
interface CacheConfig {
|
||||
@ -325,6 +328,45 @@ export default function CachePage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 智能缓存汇总 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>智能缓存汇总</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center" title="所有常规文件的精确缓存命中总数">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{(stats?.proxy.regular_cache_hit ?? 0) + (stats?.mirror.regular_cache_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">常规缓存命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="所有图片文件的精确格式缓存命中总数">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{(stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">图片精确命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片格式回退命中总数,提高了缓存效率">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{(stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">格式回退命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="格式回退在所有图片请求中的占比,显示智能缓存的效果">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{(() => {
|
||||
const totalImageRequests = (stats?.proxy.image_cache_hit ?? 0) + (stats?.mirror.image_cache_hit ?? 0) + (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)
|
||||
const fallbackHits = (stats?.proxy.format_fallback_hit ?? 0) + (stats?.mirror.format_fallback_hit ?? 0)
|
||||
return totalImageRequests > 0 ? ((fallbackHits / totalImageRequests) * 100).toFixed(1) : '0.0'
|
||||
})()}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">格式回退率</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* 代理缓存 */}
|
||||
<Card>
|
||||
@ -370,6 +412,23 @@ export default function CachePage() {
|
||||
<dt className="text-sm font-medium text-gray-500">节省带宽</dt>
|
||||
<dd className="text-sm text-gray-900">{formatBytes(stats?.proxy.bytes_saved ?? 0)}</dd>
|
||||
</div>
|
||||
<div className="border-t pt-2 mt-2">
|
||||
<div className="text-xs font-medium text-gray-600 mb-1">智能缓存统计</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="text-center" title="常规文件的精确缓存命中">
|
||||
<div className="text-blue-600 font-medium">{stats?.proxy.regular_cache_hit ?? 0}</div>
|
||||
<div className="text-gray-500">常规命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片文件的精确格式缓存命中">
|
||||
<div className="text-green-600 font-medium">{stats?.proxy.image_cache_hit ?? 0}</div>
|
||||
<div className="text-gray-500">图片命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片格式回退命中(如请求WebP但提供JPEG)">
|
||||
<div className="text-orange-600 font-medium">{stats?.proxy.format_fallback_hit ?? 0}</div>
|
||||
<div className="text-gray-500">格式回退</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
{renderCacheConfig("proxy")}
|
||||
</CardContent>
|
||||
@ -419,6 +478,23 @@ export default function CachePage() {
|
||||
<dt className="text-sm font-medium text-gray-500">节省带宽</dt>
|
||||
<dd className="text-sm text-gray-900">{formatBytes(stats?.mirror.bytes_saved ?? 0)}</dd>
|
||||
</div>
|
||||
<div className="border-t pt-2 mt-2">
|
||||
<div className="text-xs font-medium text-gray-600 mb-1">智能缓存统计</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="text-center" title="常规文件的精确缓存命中">
|
||||
<div className="text-blue-600 font-medium">{stats?.mirror.regular_cache_hit ?? 0}</div>
|
||||
<div className="text-gray-500">常规命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片文件的精确格式缓存命中">
|
||||
<div className="text-green-600 font-medium">{stats?.mirror.image_cache_hit ?? 0}</div>
|
||||
<div className="text-gray-500">图片命中</div>
|
||||
</div>
|
||||
<div className="text-center" title="图片格式回退命中(如请求WebP但提供JPEG)">
|
||||
<div className="text-orange-600 font-medium">{stats?.mirror.format_fallback_hit ?? 0}</div>
|
||||
<div className="text-gray-500">格式回退</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
{renderCacheConfig("mirror")}
|
||||
</CardContent>
|
||||
|
@ -40,7 +40,7 @@ interface Metrics {
|
||||
error_count: number
|
||||
avg_latency: string
|
||||
bytes_transferred: number
|
||||
last_access_time: number // 添加最后访问时间字段
|
||||
last_access_time: number
|
||||
}>
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ export default function DashboardPage() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchMetrics()
|
||||
const interval = setInterval(fetchMetrics, 3000)
|
||||
const interval = setInterval(fetchMetrics, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchMetrics])
|
||||
|
||||
@ -146,6 +146,18 @@ export default function DashboardPage() {
|
||||
<div className="text-sm font-medium text-gray-500">当前活跃请求</div>
|
||||
<div className="text-lg font-semibold">{metrics.active_requests}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-500">总请求数</div>
|
||||
<div className="text-lg font-semibold">{metrics.total_requests || Object.values(metrics.status_code_stats || {}).reduce((a, b) => a + (b as number), 0)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-500">总错误数</div>
|
||||
<div className="text-lg font-semibold text-red-600">{metrics.total_errors || 0}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-500">错误率</div>
|
||||
<div className="text-lg font-semibold text-red-600">{((metrics.error_rate || 0) * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-500">总传输数据</div>
|
||||
<div className="text-lg font-semibold">{formatBytes(metrics.total_bytes)}</div>
|
||||
@ -154,6 +166,10 @@ export default function DashboardPage() {
|
||||
<div className="text-sm font-medium text-gray-500">每秒传输数据</div>
|
||||
<div className="text-lg font-semibold">{formatBytes(metrics.bytes_per_second)}/s</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-500">平均每秒请求数</div>
|
||||
<div className="text-lg font-semibold">{metrics.requests_per_second.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -177,10 +193,8 @@ export default function DashboardPage() {
|
||||
<div className="text-lg font-semibold">{metrics.avg_response_time}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-500">平均每秒请求数</div>
|
||||
<div className="text-lg font-semibold">
|
||||
{metrics.requests_per_second.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-500">当前带宽</div>
|
||||
<div className="text-lg font-semibold">{metrics.current_bandwidth}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
Loading…
x
Reference in New Issue
Block a user