Skip to content

Commit b215aaf

Browse files
committed
fix(metrics): 重构路径统计重置功能以支持前缀匹配并修复缓存命中率统计
修改 ResetPathStats 方法以支持路径前缀匹配,确保能正确重置所有匹配前缀的路径统计。同时修复前端重置后刷新统计数据的逻辑。添加相关文档说明缓存命中率和路径统计重置功能的修复细节。
1 parent be32999 commit b215aaf

File tree

3 files changed

+257
-25
lines changed

3 files changed

+257
-25
lines changed

CACHE_HIT_RATE_FIX.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# 缓存命中率和路径统计重置功能修复
2+
3+
## 问题总结
4+
5+
### 问题1:缓存命中率始终显示为 0
6+
7+
**根本原因**:所有请求记录都使用了 `RecordRequest()` 方法,而不是 `RecordRequestWithCache()` 方法,导致缓存命中和未命中的计数器从未被更新。
8+
9+
**影响范围**
10+
- 路径统计中的缓存命中率显示为 0
11+
- 缓存节省字节数统计为 0
12+
- 无法准确评估缓存效果
13+
14+
**修复方案**
15+
将所有请求记录改为使用 `RecordRequestWithCache()` 并传递正确的缓存状态:
16+
17+
1. **缓存命中时**
18+
- 参数:`cacheHit=true, bytesSaved=item.Size`
19+
- 修改位置:`internal/handler/proxy.go``internal/handler/mirror_proxy.go`
20+
21+
2. **缓存未命中时**
22+
- 参数:`cacheHit=false, bytesSaved=0`
23+
- 修改位置:主代理流程和重试流程
24+
25+
**修改文件**
26+
- `internal/handler/proxy.go` (4处)
27+
- `internal/handler/mirror_proxy.go` (2处)
28+
29+
### 问题2:路径统计重置功能不生效
30+
31+
**根本原因**
32+
1. **后端问题**:重置接口只重置了精确匹配的路径,但实际统计数据按具体请求路径存储(例如 `/b2/file1.jpg`),而前端传递的是路径前缀(例如 `/b2`
33+
2. **前端问题**:重置后调用了错误的刷新函数(`fetchConfig` 而不是刷新统计数据)
34+
35+
**修复方案**
36+
37+
#### 后端修复
38+
39+
修改 `ResetPathStats`**路径前缀匹配**模式:
40+
41+
```go
42+
// 修改前:只重置精确匹配的路径
43+
if stats, ok := c.pathStats.Load(path); ok {
44+
// 重置该路径...
45+
}
46+
47+
// 修改后:重置所有匹配前缀的路径
48+
c.pathStats.Range(func(key string, stats *models.PathMetrics) bool {
49+
if strings.HasPrefix(key, pathPrefix) {
50+
// 确保完整路径段匹配
51+
if len(key) == len(pathPrefix) || key[len(pathPrefix)] == '/' {
52+
// 重置所有计数器...
53+
}
54+
}
55+
return true
56+
})
57+
```
58+
59+
**精确匹配逻辑**
60+
- `/b2` 会匹配 `/b2`, `/b2/file.jpg`, `/b2/dir/file.jpg`
61+
- `/b2` 不会匹配 `/b2x``/b2345`(确保路径段完整)
62+
63+
#### 前端修复
64+
65+
修改 `handleResetPathStats` 函数,重置后正确刷新统计数据:
66+
67+
```typescript
68+
// 修改前
69+
await fetchConfig() // 错误:刷新配置而不是统计
70+
71+
// 修改后
72+
const statsResponse = await fetch("/admin/api/path-stats", {
73+
headers: {
74+
'Authorization': `Bearer ${token}`,
75+
'Content-Type': 'application/json'
76+
}
77+
})
78+
if (statsResponse.ok) {
79+
const statsData = await statsResponse.json()
80+
setPathStats(statsData.path_stats || [])
81+
}
82+
```
83+
84+
## 测试验证
85+
86+
### 缓存命中率测试
87+
88+
1. **访问资源** - 第一次访问某个路径的资源(缓存未命中)
89+
- 预期:`cache_misses` +1,命中率保持低值
90+
91+
2. **再次访问** - 第二次访问相同资源(缓存命中)
92+
- 预期:`cache_hits` +1,命中率上升
93+
94+
3. **查看统计** - 在配置页面查看路径统计
95+
- 预期:缓存命中率正确显示(例如 50%, 70%)
96+
97+
### 路径统计重置测试
98+
99+
1. **查看当前统计** - 记录当前的请求数、缓存命中率等数据
100+
101+
2. **点击重置按钮** - 在统计卡片右上角点击"重置"按钮
102+
103+
3. **验证结果**
104+
- ✅ 后端日志显示:`[Collector] 已重置路径前缀 /xxx 的统计,共 N 条`
105+
- ✅ 前端显示:toast提示"统计数据已重置"
106+
- ✅ 统计数据归零:总请求数、缓存命中数、流量等全部变为0
107+
- ✅ 持久化:`data/path_stats.json` 中对应路径的统计数据清零
108+
109+
4. **新请求测试** - 访问该路径的资源
110+
- 预期:统计数据从0开始重新累计
111+
112+
## 日志说明
113+
114+
### 正常运行日志
115+
116+
```
117+
[Collector] 已重置路径前缀 /cdnjs 的统计,共 X 条
118+
[PathStatsStorage] 路径统计数据已保存: 20592 条路径记录
119+
```
120+
121+
**解释**
122+
- 第一行:成功重置了 `/cdnjs` 路径前缀下的 X 条统计记录
123+
- 第二行:将所有路径统计(20592条)持久化到磁盘
124+
- 注意:这个数字是**全部路径**的统计记录数,不是重置的数量
125+
- 包含已重置的路径(计数器为0)和其他路径(保持原值)
126+
127+
## 相关文件
128+
129+
### 后端修改
130+
131+
1. **`internal/handler/proxy.go`**
132+
- 主代理流程缓存统计记录 (4处)
133+
134+
2. **`internal/handler/mirror_proxy.go`**
135+
- Mirror代理缓存统计记录 (2处)
136+
137+
3. **`internal/metrics/collector.go`**
138+
- 添加 `strings` 导入
139+
- 重构 `ResetPathStats` 为路径前缀匹配
140+
141+
4. **`internal/handler/path_stats.go`**
142+
- `ResetPathStats` API处理器
143+
144+
5. **`internal/router/admin_router.go`**
145+
- 注册重置API路由
146+
147+
### 前端修改
148+
149+
1. **`web/app/dashboard/config/PathStatsCard.tsx`**
150+
- 添加重置按钮
151+
- 实现重置逻辑和状态管理
152+
153+
2. **`web/app/dashboard/config/PathMappingItem.tsx`**
154+
- 传递 `onResetStats` prop
155+
156+
3. **`web/app/dashboard/config/page.tsx`**
157+
- 实现 `handleResetPathStats` 函数
158+
- 修复重置后的数据刷新逻辑
159+
160+
## API接口
161+
162+
### POST /admin/api/path-stats/reset
163+
164+
重置指定路径前缀的统计数据
165+
166+
**请求**
167+
```json
168+
{
169+
"path": "/b2"
170+
}
171+
```
172+
173+
**响应**
174+
```json
175+
{
176+
"success": true,
177+
"message": "路径统计已重置"
178+
}
179+
```
180+
181+
**行为**
182+
- 重置所有以 `/b2` 开头的路径统计
183+
- 立即持久化到 `data/path_stats.json`
184+
- 如果配置了S3,自动同步到云端
185+
186+
## 注意事项
187+
188+
1. **路径前缀匹配**:重置 `/b2` 会重置所有以 `/b2` 开头的路径,包括 `/b2/file1.jpg`, `/b2/dir/file2.jpg`
189+
190+
2. **不可恢复**:重置操作不可撤销,统计数据将永久清除
191+
192+
3. **持久化延迟**:重置后立即持久化到本地,但S3同步可能有几秒延迟
193+
194+
4. **配置保留**:只重置统计数据,路径映射配置(URL、扩展名规则、缓存配置等)不受影响
195+
196+
5. **多节点环境**:如果配置了S3,重置操作会通过持久化同步到其他节点
197+
198+
## 后续优化建议
199+
200+
- [ ] 添加重置前确认对话框(防止误操作)
201+
- [ ] 显示重置影响的路径数量
202+
- [ ] 支持批量重置多个路径
203+
- [ ] 记录重置历史(谁在何时重置了哪个路径)

internal/metrics/collector.go

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime"
1212
"sort"
1313
"strconv"
14+
"strings"
1415
"sync"
1516
"sync/atomic"
1617
"time"
@@ -1006,33 +1007,48 @@ func (c *Collector) startPersistenceTask() {
10061007
}()
10071008
}
10081009

1009-
// ResetPathStats 重置指定路径的统计数据
1010-
func (c *Collector) ResetPathStats(path string) error {
1011-
if stats, ok := c.pathStats.Load(path); ok {
1012-
// 重置所有计数器
1013-
stats.RequestCount.Store(0)
1014-
stats.ErrorCount.Store(0)
1015-
stats.BytesTransferred.Store(0)
1016-
stats.TotalLatency.Store(0)
1017-
stats.Status2xx.Store(0)
1018-
stats.Status3xx.Store(0)
1019-
stats.Status4xx.Store(0)
1020-
stats.Status5xx.Store(0)
1021-
stats.CacheHits.Store(0)
1022-
stats.CacheMisses.Store(0)
1023-
stats.BytesSaved.Store(0)
1024-
stats.LastAccessTime.Store(time.Now().Unix())
1025-
1026-
log.Printf("[Collector] 已重置路径统计: %s", path)
1027-
1028-
// 立即持久化
1029-
if err := c.savePathStats(); err != nil {
1030-
log.Printf("[Collector] 重置后持久化失败: %v", err)
1010+
// ResetPathStats 重置指定路径前缀的统计数据
1011+
// 会重置所有以该前缀开头的路径
1012+
func (c *Collector) ResetPathStats(pathPrefix string) error {
1013+
resetCount := 0
1014+
1015+
// 遍历所有路径统计
1016+
c.pathStats.Range(func(key string, stats *models.PathMetrics) bool {
1017+
// 检查路径是否以指定前缀开头
1018+
if strings.HasPrefix(key, pathPrefix) {
1019+
// 确保匹配的是完整的路径段(避免 /abc 匹配到 /abcd)
1020+
if len(key) == len(pathPrefix) || (len(key) > len(pathPrefix) && key[len(pathPrefix)] == '/') {
1021+
// 重置所有计数器
1022+
stats.RequestCount.Store(0)
1023+
stats.ErrorCount.Store(0)
1024+
stats.BytesTransferred.Store(0)
1025+
stats.TotalLatency.Store(0)
1026+
stats.Status2xx.Store(0)
1027+
stats.Status3xx.Store(0)
1028+
stats.Status4xx.Store(0)
1029+
stats.Status5xx.Store(0)
1030+
stats.CacheHits.Store(0)
1031+
stats.CacheMisses.Store(0)
1032+
stats.BytesSaved.Store(0)
1033+
stats.LastAccessTime.Store(time.Now().Unix())
1034+
resetCount++
1035+
}
10311036
}
1037+
return true
1038+
})
10321039

1033-
return nil
1040+
if resetCount == 0 {
1041+
return fmt.Errorf("路径前缀 %s 下没有找到统计数据", pathPrefix)
1042+
}
1043+
1044+
log.Printf("[Collector] 已重置路径前缀 %s 的统计,共 %d 条", pathPrefix, resetCount)
1045+
1046+
// 立即持久化
1047+
if err := c.savePathStats(); err != nil {
1048+
log.Printf("[Collector] 重置后持久化失败: %v", err)
10341049
}
1035-
return fmt.Errorf("路径 %s 的统计数据不存在", path)
1050+
1051+
return nil
10361052
}
10371053

10381054
// ResetAllPathStats 重置所有路径的统计数据

web/app/dashboard/config/page.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,20 @@ export default function ConfigPage() {
522522
}
523523

524524
// 重新获取统计数据
525-
await fetchConfig()
525+
try {
526+
const statsResponse = await fetch("/admin/api/path-stats", {
527+
headers: {
528+
'Authorization': `Bearer ${token}`,
529+
'Content-Type': 'application/json'
530+
}
531+
})
532+
if (statsResponse.ok) {
533+
const statsData = await statsResponse.json()
534+
setPathStats(statsData.path_stats || [])
535+
}
536+
} catch (error) {
537+
console.error("刷新路径统计失败:", error)
538+
}
526539
}
527540

528541
const updateSecurity = (field: keyof SecurityConfig['IPBan'], value: boolean | number) => {

0 commit comments

Comments
 (0)