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 无漏洞
This commit is contained in:
163
internal/api/middleware/gzip.go
Normal file
163
internal/api/middleware/gzip.go
Normal file
@@ -0,0 +1,163 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user