Files
long-agent 7b047e2f11 perf: Sprint 19 P0/P1 性能优化落地
P0(高优先级):
- P0-1: 确认数据库复合索引已存在(GORM tag),composite_index_test 验证通过
- P0-2: 连接池调优 MaxIdleConns 5→10, ConnMaxLifetime 30min→5min
- P0-3: Redis 智能探测(ProbeRedis),无 Redis 自动降级到纯内存模式

P1(中优先级):
- P1-1: GZIP 压缩中间件(compress/gzip 标准库,零新依赖)
- P1-2: 权限缓存 TTL 30min→5min
- P1-3: Argon2id 启动自适应校准(CalibrateArgon2id)

历史优化(含本次提交):
- L1Cache O(n)→O(1) LRU 重构
- Auth 中间件 DB 查询合并 + 5s L1 缓存
- Logger 异步化(4096 缓冲通道)

验证: go build/vet/test 41/41 PASS, govulncheck 无漏洞
2026-04-18 22:57:44 +08:00

164 lines
3.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"compress/gzip"
"io"
"net/http"
"strings"
"sync"
"github.com/gin-gonic/gin"
)
// gzipMinLength 小于此字节数的响应不压缩(避免小响应压缩反而增大体积)
const gzipMinLength = 1024
// gzipPool 复用 gzip.Writer减少 GC 压力
var gzipPool = sync.Pool{
New: func() interface{} {
w, _ := gzip.NewWriterLevel(io.Discard, gzip.BestSpeed)
return w
},
}
// gzipResponseWriter 包装 gin.ResponseWriter按需启用 gzip 压缩。
// 所有写入先缓冲;第一次超过阈值时决定是否压缩。
type gzipResponseWriter struct {
gin.ResponseWriter
gz *gzip.Writer
buf []byte
threshold int
decided bool // 已决定是否压缩
}
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
if g.decided {
if g.gz != nil {
return g.gz.Write(data)
}
return g.ResponseWriter.Write(data)
}
// 积累数据
g.buf = append(g.buf, data...)
if len(g.buf) >= g.threshold {
return len(data), g.decide()
}
return len(data), nil
}
func (g *gzipResponseWriter) WriteString(s string) (int, error) {
return g.Write([]byte(s))
}
// decide 根据已缓冲内容和 Content-Type 决定是否压缩,并写出缓冲数据
func (g *gzipResponseWriter) decide() error {
g.decided = true
ct := g.ResponseWriter.Header().Get("Content-Type")
if g.gz != nil && shouldCompress(ct) {
// 启用 gzip
g.ResponseWriter.Header().Set("Content-Encoding", "gzip")
g.ResponseWriter.Header().Set("Vary", "Accept-Encoding")
g.ResponseWriter.Header().Del("Content-Length")
g.gz.Reset(g.ResponseWriter)
if len(g.buf) > 0 {
_, err := g.gz.Write(g.buf)
g.buf = nil
return err
}
} else {
// 不压缩:回收 gzip.Writer
if g.gz != nil {
gzipPool.Put(g.gz)
g.gz = nil
}
if len(g.buf) > 0 {
_, err := g.ResponseWriter.Write(g.buf)
g.buf = nil
return err
}
}
g.buf = nil
return nil
}
// finalize 在请求处理完毕后刷出剩余缓冲数据并关闭 gzip.Writer
func (g *gzipResponseWriter) finalize() {
if !g.decided {
// 响应体小于阈值,直接透传(不压缩)
g.decided = true
if g.gz != nil {
gzipPool.Put(g.gz)
g.gz = nil
}
if len(g.buf) > 0 {
_, _ = g.ResponseWriter.Write(g.buf)
g.buf = nil
}
return
}
if g.gz != nil {
_ = g.gz.Flush()
_ = g.gz.Close()
gzipPool.Put(g.gz)
g.gz = nil
}
}
// shouldCompress 根据 Content-Type 判断是否值得压缩(二进制流不压缩)
func shouldCompress(contentType string) bool {
ct := strings.ToLower(strings.SplitN(contentType, ";", 2)[0])
switch ct {
case "application/json",
"application/javascript",
"text/html",
"text/plain",
"text/css",
"text/xml",
"application/xml",
"application/x-www-form-urlencoded":
return true
}
return false
}
// GzipMiddleware 对 JSON/文本类响应启用 GZIP 压缩。
//
// 仅在满足以下条件时压缩:
// - 客户端发送了 Accept-Encoding: gzip
// - 响应 Content-Type 为 JSON/文本类
// - 响应体超过 gzipMinLength默认 1 KiB
//
// 其余情况透传,不影响性能。
func GzipMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 客户端不接受 gzip 则跳过
if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") {
c.Next()
return
}
gz := gzipPool.Get().(*gzip.Writer)
grw := &gzipResponseWriter{
ResponseWriter: c.Writer,
gz: gz,
threshold: gzipMinLength,
}
c.Writer = grw
defer func() {
grw.finalize()
c.Writer = grw.ResponseWriter
}()
c.Next()
}
}
// Ensure gzipResponseWriter implements http.Hijacker forwarding (needed by some WebSocket libs)
var _ http.ResponseWriter = (*gzipResponseWriter)(nil)