按性能分析逐项落地,实测对比(16 并发、mock 上游零延迟、SQLite): - 每请求 SQL 10 → 2:模型/渠道目录(模型行、绑定、渠道行)进程内缓存 10s, 运行期状态(冷却、健康)仍每次实时判定;管理端写操作经中间件、 健康状态变化经回调立即失效。新增 TestProxyRequestSQLBudget 钉住预算。 - 端到端 p50 1.3–1.6ms → 0.66–0.70ms,RPS 3.5–8.9k → 16–18k。 - 流式正文累积:字符串 += 改 strings.Builder(原为 O(n²)), 逐行 JSON 提取改字节快路径,并加廉价的子串预过滤、拿到输出 usage 后停累积。 500 块:949µs/7.9MB 分配 → 83µs/136KB。 - 请求体定点改写:模型映射从整包 unmarshal/marshal 改为顶层字段替换 (100KB:585µs → 29µs),stream_options 注入同理;requestText 改为 只在「上游没报用量」时按需提取,亲和键改用原始体前缀。 - 渠道满载背压:新增 Acquire 有界排队(OT_PROXY_CHANNEL_ACQUIRE_WAIT, 默认 150ms);排队仍满时返回 503 + Retry-After 而不是 502。 实测渠道上限 4、16 并发:无排队 81 次 503;排队 150ms 后 400/400 成功。 - 记账按批聚合:同批同用户一次扣款、流水批量插入、日表按 (user,model,日期) 聚合,替代每行 5 条语句;幂等仍靠 ref_id 唯一索引兜底。 - gorm 开启 PrepareStmt(SQL 解析此前占 CPU 15%);tiktoken 编码器按模型复用。 - 管理端用量列表不再返回原始请求/响应体(可能几十 MB), 改为 has_raw 标记 + GET /admin/usage/:id 按需拉取,前端弹窗打开时取。 验证:go vet、go test ./...、go test -race(proxy/channel/usage/tokenizer)、 vue-tsc、eslint、vite build 全绿;本地起 mock 上游实测列表/详情接口与 管理页弹窗按需加载。
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
package proxy
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
)
|
||
|
||
// 请求体上的定点改写:只动需要改的那一段,不做整包 unmarshal → map → marshal。
|
||
// 整包往返在 agent 场景(上下文几十到几百 KB)实测约 0.5–0.6ms 且分配翻倍,
|
||
// 而这里只做一次 O(n) 扫描。
|
||
|
||
// rewriteModelField 改写请求体顶层 model 字段,其余字节原样保留。
|
||
// 嵌套出现的 "model"(比如消息内容里引用的 JSON)不会被误改:只认顶层键。
|
||
// 找不到顶层 model 或请求体不是对象时返回 (body, false)。
|
||
func rewriteModelField(body []byte, model string) ([]byte, bool) {
|
||
span, ok := topLevelValueSpan(body, "model")
|
||
if !ok {
|
||
return body, false
|
||
}
|
||
quoted, err := json.Marshal(model)
|
||
if err != nil {
|
||
return body, false
|
||
}
|
||
out := make([]byte, 0, len(body)-(span[1]-span[0])+len(quoted))
|
||
out = append(out, body[:span[0]]...)
|
||
out = append(out, quoted...)
|
||
out = append(out, body[span[1]:]...)
|
||
return out, true
|
||
}
|
||
|
||
// injectStreamOptions 在请求体顶层追加 stream_options,让流式末块带上 usage。
|
||
// 只在没有该键时插入;body 必须是 JSON 对象(以 '}' 结尾,允许尾部空白)。
|
||
func injectStreamOptions(body []byte, options string) ([]byte, bool) {
|
||
trimmed := bytes.TrimRight(body, " \t\r\n")
|
||
if len(trimmed) < 2 || trimmed[0] != '{' || trimmed[len(trimmed)-1] != '}' {
|
||
return body, false
|
||
}
|
||
if bytes.Contains(trimmed, []byte(`"stream_options"`)) {
|
||
return body, false
|
||
}
|
||
end := len(trimmed) - 1
|
||
out := make([]byte, 0, len(body)+len(options)+16)
|
||
out = append(out, trimmed[:end]...)
|
||
// 空对象 {} 不能加逗号
|
||
if last := bytes.TrimRight(trimmed[:end], " \t\r\n"); len(last) > 1 {
|
||
out = append(out, ',')
|
||
}
|
||
out = append(out, `"stream_options":`...)
|
||
out = append(out, options...)
|
||
out = append(out, '}')
|
||
return out, true
|
||
}
|
||
|
||
// topLevelValueSpan 找顶层键 key 的值在 body 中的字节区间 [start, end)。
|
||
// 只遍历顶层(depth==1)的键,跳过字符串字面量与转义。
|
||
func topLevelValueSpan(body []byte, key string) ([2]int, bool) {
|
||
var zero [2]int
|
||
trimmed := bytes.TrimLeft(body, " \t\r\n")
|
||
if len(trimmed) == 0 || trimmed[0] != '{' {
|
||
return zero, false
|
||
}
|
||
// 重新对齐到 trimmed 起点后的偏移
|
||
base := len(body) - len(trimmed)
|
||
keyPat := []byte(`"` + key + `"`)
|
||
i := base + 1
|
||
depth := 1
|
||
for i < len(body) {
|
||
switch body[i] {
|
||
case '{', '[':
|
||
depth++
|
||
i++
|
||
case '}', ']':
|
||
depth--
|
||
if depth == 0 {
|
||
return zero, false
|
||
}
|
||
i++
|
||
case '"':
|
||
start := i
|
||
i = skipJSONString(body, i)
|
||
// 顶层字符串:可能是"键",也可能是"值"
|
||
if depth == 1 && bytes.Equal(body[start:i], keyPat) {
|
||
j := i
|
||
for j < len(body) && (body[j] == ' ' || body[j] == '\t' || body[j] == '\r' || body[j] == '\n') {
|
||
j++
|
||
}
|
||
if j < len(body) && body[j] == ':' {
|
||
j++
|
||
for j < len(body) && (body[j] == ' ' || body[j] == '\t' || body[j] == '\r' || body[j] == '\n') {
|
||
j++
|
||
}
|
||
vs := j
|
||
ve, ok := jsonValueEnd(body, vs)
|
||
if !ok {
|
||
return zero, false
|
||
}
|
||
return [2]int{vs, ve}, true
|
||
}
|
||
}
|
||
default:
|
||
i++
|
||
}
|
||
}
|
||
return zero, false
|
||
}
|
||
|
||
// skipJSONString 返回从 body[i]('"')开始的下一个未转义引号之后的位置。
|
||
func skipJSONString(body []byte, i int) int {
|
||
i++ // 跳过开引号
|
||
for i < len(body) {
|
||
switch body[i] {
|
||
case '\\':
|
||
i += 2
|
||
case '"':
|
||
return i + 1
|
||
default:
|
||
i++
|
||
}
|
||
}
|
||
return i
|
||
}
|
||
|
||
// jsonValueEnd 返回从 i 开始的一个 JSON 值的结束位置(不含尾部空白)。
|
||
func jsonValueEnd(body []byte, i int) (int, bool) {
|
||
if i >= len(body) {
|
||
return 0, false
|
||
}
|
||
switch body[i] {
|
||
case '"':
|
||
return skipJSONString(body, i), true
|
||
case '{', '[':
|
||
depth := 0
|
||
for j := i; j < len(body); j++ {
|
||
switch body[j] {
|
||
case '"':
|
||
j = skipJSONString(body, j) - 1
|
||
case '{', '[':
|
||
depth++
|
||
case '}', ']':
|
||
depth--
|
||
if depth == 0 {
|
||
return j + 1, true
|
||
}
|
||
}
|
||
}
|
||
return 0, false
|
||
default:
|
||
j := i
|
||
for j < len(body) && body[j] != ',' && body[j] != '}' && body[j] != ']' &&
|
||
body[j] != ' ' && body[j] != '\t' && body[j] != '\r' && body[j] != '\n' {
|
||
j++
|
||
}
|
||
return j, j > i
|
||
}
|
||
}
|