perf: 鉴权缓存让命中请求零 SQL,去掉两个无用索引
profile(16 并发)显示请求路径的 CPU 有 ~70% 花在鉴权上:每个代理请求都要用 密钥哈希查 api_keys、再用 user_id 查 users,两条随机读。 - 新增鉴权缓存(OT_PROXY_AUTH_CACHE_TTL,默认 2s,0=关闭):缓存密钥与用户行, 但密钥状态、用户状态、密钥过期仍按当前时间逐请求判定;管理端写操作与用户的 密钥写操作经中间件立即清空缓存。余额随用户行一起缓存,只影响「≤0 就 402」 这道软闸,最长滞后一个 TTL(请求照样计费)。 - last_used_at 节流改由缓存内的 touch 表记账(原先按 DB 行时间戳判断,行被缓存后 时间戳不再变化,会导致每请求都写一次库)。touch 用 sync.Map:早先与缓存共用一把 排他锁,16 并发下互斥饥饿把吞吐从 31k 拖到 3.2k(p99 7ms),换 sync.Map 后消失。 - 去掉 usage_logs 上从未被查询过滤的 key_id / trace_id 索引(没有查询按它们过滤, 每个索引都是每行一次 B-tree 插入);schema v8 迁移里显式 DROP 旧库上的索引。 实测(16 并发、mock 上游、SQLite): - 鉴权缓存开:p50 131–177µs、p99 0.6–1.2ms、RPS 56k–81k - 鉴权缓存关:p50 535–631µs、p99 1.6–2.8ms、RPS 21k–27k (4 并发下同样受益:30k→41k) 测试:新增缓存命中零 SQL、状态/过期重判、失效、TTL 到期、缓存关闭、并发七项; go vet、go test ./...、-race(proxy/usage/store/api)全绿。
This commit is contained in:
1 parent
117e03673b
commit
8e5fe16611
12 files changed
+391
-16
No files matched your search
@@ -54,9 +54,11 @@ type Gateway struct {
|
||||
affinityTTL time.Duration
|
||||
// 渠道并发满载时每个候选最多排队多久(0=立刻跳过)。
|
||||
channelAcquireWait time.Duration
|
||||
aff *affinityTable
|
||||
tpMu sync.Mutex
|
||||
tps map[time.Duration]*http.Transport
|
||||
// 鉴权缓存(密钥 + 用户行),0=关闭。
|
||||
auth *authCache
|
||||
aff *affinityTable
|
||||
tpMu sync.Mutex
|
||||
tps map[time.Duration]*http.Transport
|
||||
|
||||
policyMu sync.Mutex
|
||||
policy modelPolicy
|
||||
@@ -102,6 +104,9 @@ func (g *Gateway) modelByName(name string) (store.Model, bool) {
|
||||
return e.m, e.ok
|
||||
}
|
||||
|
||||
// InvalidateAuth 清空鉴权缓存(密钥/用户/密钥白名单等写操作后调用)。
|
||||
func (g *Gateway) InvalidateAuth() { g.auth.clear() }
|
||||
|
||||
// InvalidateCatalog 清空模型与渠道目录缓存(管理端改动、健康状态变化后调用)。
|
||||
func (g *Gateway) InvalidateCatalog() {
|
||||
g.modelMu.Lock()
|
||||
@@ -221,6 +226,7 @@ type Options struct {
|
||||
FirstTokenWait time.Duration // 流式放行前等内容的最长时间(0=不限)
|
||||
AffinityTTL time.Duration // 会话亲和 TTL(0=关闭)
|
||||
ChannelAcquireWait time.Duration // 渠道满载排队时长(0=立刻跳过)
|
||||
AuthCacheTTL time.Duration // 鉴权缓存 TTL(0=关闭)
|
||||
}
|
||||
|
||||
func NewGateway(db *gorm.DB, enc *crypto.Encryptor, rec *usage.Recorder, lim *ratelimit.Limiter, o Options) *Gateway {
|
||||
@@ -239,6 +245,7 @@ func NewGateway(db *gorm.DB, enc *crypto.Encryptor, rec *usage.Recorder, lim *ra
|
||||
firstTokenWait: o.FirstTokenWait,
|
||||
affinityTTL: o.AffinityTTL,
|
||||
channelAcquireWait: o.ChannelAcquireWait,
|
||||
auth: newAuthCache(o.AuthCacheTTL),
|
||||
aff: newAffinityTable(),
|
||||
modelCat: map[string]modelEntry{},
|
||||
tps: map[time.Duration]*http.Transport{},
|
||||
@@ -317,14 +324,28 @@ func (g *Gateway) Auth(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
hash := apikey.Hash(key)
|
||||
var k store.APIKey
|
||||
if err := g.db.Where("key_hash = ? AND status = ?", hash, store.KeyStatusActive).First(&k).Error; err != nil {
|
||||
// 命中缓存时省掉两条随机读;状态/过期仍在下面按当前时间判定
|
||||
k, u, ok := g.auth.get(hash)
|
||||
if !ok {
|
||||
if err := g.db.Where("key_hash = ? AND status = ?", hash, store.KeyStatusActive).First(&k).Error; err != nil {
|
||||
apiError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if err := g.db.First(&u, k.UserID).Error; err != nil {
|
||||
apiError(c, http.StatusForbidden, "user_disabled", "User account is disabled")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
g.auth.put(hash, k, u)
|
||||
}
|
||||
// 缓存值可能是 TTL 内的旧状态:每次请求都按当前时间重判
|
||||
if k.Status != store.KeyStatusActive {
|
||||
apiError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
var u store.User
|
||||
if err := g.db.First(&u, k.UserID).Error; err != nil || u.Status != store.UserStatusActive {
|
||||
if u.Status != store.UserStatusActive {
|
||||
apiError(c, http.StatusForbidden, "user_disabled", "User account is disabled")
|
||||
c.Abort()
|
||||
return
|
||||
@@ -359,8 +380,9 @@ func (g *Gateway) Auth(c *gin.Context) {
|
||||
c.Set(CtxKeyID, k.ID)
|
||||
c.Set(CtxKeyModel, k.AllowedModels)
|
||||
c.Set(CtxTrace, newTraceID())
|
||||
// last_used_at 按分钟节流:纯展示字段,不值得每请求一次 DB 写(高 QPS 下是可观开销)
|
||||
if k.LastUsedAt == nil || time.Since(*k.LastUsedAt) >= time.Minute {
|
||||
// last_used_at 按分钟节流:纯展示字段,不值得每请求一次 DB 写(高 QPS 下是可观开销)。
|
||||
// 密钥行可能来自缓存(时间戳不会自己变),所以节流按缓存里的 touch 记录判断。
|
||||
if g.auth.shouldTouch(k.ID, time.Minute) {
|
||||
g.db.Model(&store.APIKey{}).Where("id = ?", k.ID).Update("last_used_at", time.Now())
|
||||
}
|
||||
c.Next()
|
||||
|
||||
Reference in new issue
Block a user