perf: 亲和表分片、记账按驱动并发、访问日志与原始体控量

继续上一轮性能改造的剩余项:

- 会话亲和表分 16 片(原来一个全局 mutex,每个请求都要走一遍),
  键改为内联 FNV-1a 的 64 位摘要(原先是拼接字符串 + hex,每请求两次分配),
  请求路径上零分配;新增键稳定性/无分配/分片并发测试。
- 记账落库按驱动自适应并发:Recorder 按用户分片到多个 worker(同一用户恒定
  同片,流水顺序不变)。SQLite 只有一个写者,实测 4 worker 会互相抢锁把
  p99 拖到 19–34ms,所以 SQLite 固定 1 个、PostgreSQL 用 4 个。
  新增多分片落库与顺序测试(用显式 worker 数在 SQLite 上验证语义)。
- 访问日志:每请求一次 log.Printf 直写 stderr,高 QPS 下是系统调用热点;
  改为 64 KiB 缓冲 + 每秒 flush(写/flush 互斥,-race 已验)。
- 原始体控量:LogRaw 落库的请求体截到 256 KiB、响应体截到 1 MiB 并标注原始
  体积,避免单条日志几十 MB 拖慢 usage_logs 查询与备份。
- 渠道加权随机选路从 crypto/rand + big.Int 换成 math/rand/v2 的 IntN,
  去掉每请求的系统调用与两次分配。

跳过的两项与原因:
- 「PG 下把 model 目录查询合并成一次 JOIN」:目录已缓存 10s,冷路径每模型
  10 秒才走一次,合并收益接近 0。
- 「每流一个 heartbeat goroutine」:Go 的 ticker 成本很低,千级并发流仍在
  可接受范围,改动收益不抵风险。

实测(16 并发、mock 上游、SQLite):p50 679µs→558µs、p99 2.6ms→2.2ms、
RPS 18.7k→23.9k。验证:go vet、go test ./...、-race(proxy/channel/usage)全绿。
This commit is contained in:
Sakurasan committed 2026-10-30 14:25:10 +08:00
1 parent 8c12207478
commit f1181150aa
11 files changed
+401 -69

No files matched your search

+65
View File
@@ -218,3 +218,68 @@ func TestBillingAggregatesBatch(t *testing.T) {
t.Fatalf("最后一条流水 balance_after = %v,实际余额 %v", last.BalanceAfter, user.Balance)
}
}
// 多 worker 落库:同一用户恒定落在同一分片(流水顺序与提交顺序一致),
// 不同用户并行落库,最终一条不丢。
func TestRecorderShardedByUser(t *testing.T) {
db := openTestDB(t)
var users []uint64
for i := 0; i < 3; i++ {
u := store.User{Username: fmt.Sprintf("shard-u%d", i), Email: fmt.Sprintf("shard%d@x.com", i),
PasswordHash: "x", Role: store.RoleUser, Balance: 100, Status: store.UserStatusActive}
if err := db.Create(&u).Error; err != nil {
t.Fatal(err)
}
users = append(users, u.ID)
}
// 显式用 3 个分片:SQLite 生产配置是单 worker,但分片语义本身仍要验证
rec := newRecorder(db, 3)
const per = 40
for i := 0; i < per; i++ {
for _, u := range users {
rec.Record(&store.UsageLog{
TraceID: fmt.Sprintf("u%d-%d", u, i), UserID: u, ModelName: "m1",
Cost: 0.001, Status: store.UsageStatusSuccess, CreatedAt: time.Now().UTC(),
})
}
}
rec.Close()
var n int64
db.Model(&store.UsageLog{}).Count(&n)
if n != int64(per*len(users)) {
t.Fatalf("落库 %d 条,期望 %d(一条不丢)", n, per*len(users))
}
// 每个用户的分片一致 → 流水按 id 递增的顺序应与 trace 序号一致
for _, u := range users {
var logs []store.BalanceLog
db.Where("user_id = ?", u).Order("id ASC").Find(&logs)
if len(logs) != per {
t.Fatalf("用户 %d 流水 %d 条,期望 %d", u, len(logs), per)
}
for i := 1; i < len(logs); i++ {
if logs[i].BalanceAfter > logs[i-1].BalanceAfter {
t.Fatalf("用户 %d 的流水余额应单调递减(顺序错乱):%v → %v",
u, logs[i-1].BalanceAfter, logs[i].BalanceAfter)
}
}
}
}
// 分片函数:同一用户恒定同片,且覆盖所有分片。
func TestShardOfStable(t *testing.T) {
rec := NewRecorder(openTestDB(t))
defer rec.Close()
for _, u := range []uint64{1, 2, 3, 17, 1 << 40} {
if rec.shardOf(u) != rec.shardOf(u) {
t.Fatalf("用户 %d 的分片不稳定", u)
}
}
seen := map[chan *store.UsageLog]bool{}
for u := uint64(0); u < 64; u++ {
seen[rec.shardOf(u)] = true
}
if len(seen) != rec.workers {
t.Fatalf("只用到 %d 个分片,期望 %d", len(seen), rec.workers)
}
}