Files
Cyrene_v2/internal/runtime/memory/preinsert_recall_test.go
T
AskaEth f3fb097630 perf(embedding,memory): 批上限 4→8 削掉请求放大,审查预算 20s→90s 覆盖实测链长
实测(2026-10-13,本机空闲,活库 cyrene/user 分区 201 条、均长 34.7 汉字):
- 一条入库审查召回链 = 51 个「4 条」请求 = 23.070 / 21.860 s;旧审查预算 20s
  比链长还短 ⇒ 每一条候选满额的审查都超时,且超时后 **一行留痕都不写**
  (7 天里 92.1% 的审查 recall_mode 为空、2274 条超时降级的根因)
- 同一条链改成「8 条」= 26 个请求 = 21.435 / 21.597 s:请求数减半,墙钟几乎不变
  —— 链路耗时由「条数 × 每条成本」主导,批次只改请求数
- 「16 条」在**真实代码路径**上 3 次里 2 次让单请求越过 5s 单次超时(1.818 /
  5.016 超时 / 5.001 超时)⇒ 批上限不越过 8;隔离测量里 12 条 0.996~1.017 s 仍在界内

改动:
- embedding: perItemBudget 500ms→300ms(实测摊销 70~107ms/条的约 3 倍)
  ⇒ DefaultBatchLimit 由 (5s/2−100ms)/300ms = **8**,同一套推演、两维都成立
  (条数 100ms+8×300ms=2.5s;字符 100ms+480×5ms=2.5s,均在超时的一半内)
- embedding: 新增 maxSafeBatchLimit=12(实测外沿)+ 编译期断言(越界即 uint 溢出、
  编译失败),与 tidy_config.go 那条同手法
- memory: defaultReviewTimeout 20s→**90s**(> 实测最坏链:空闲 21.6s⇒4.2 倍余量、
  与整理链并发 56.7s + T1 8s ⇒ 1.4 倍;不再大的理由=单 worker 有界队列 256 的
  最坏积压 256×90s≈6.4h、服务真挂死时第一片 5s 就降级用不到这条预算)

测试(不留过时断言):budget_test.go 加「钉值 8/480 + 两维不等式 + ≤12 外沿 +
16 条必须仍判越界」四层;api_url_test.go 的 reviewBudgetRef 20s→90s;
preinsert_recall_test.go 新增跨包断言「预算 > 实测最坏链 + defaultT1Timeout」。
变异自证:perItemBudget 回 500ms ⇒ 钉值断言红;reviewTimeout 回 20s ⇒ 跨包断言红;
perItemBudget 150ms(⇒16)⇒ 编译失败(constant -4 overflows uint)。三者均已逐字还原。

遗留(如实登记,未修):① 整理轮每轮重嵌整个分区(user 一轮仍 250 个请求),
解法方向是复用已入库的 vec_items 或给整理轮一个 embedding 请求预算;② 链路对单点
慢分片没有韧性 —— 一个分片失败即整条 Embed 失败、丢掉其余分片的工作量,且实测
任何批次形状都会偶发 >5s 尖峰(4 条 7.139s / 8 条 9.711s,与批次无关),方向是
重试/分片级容错,调常量解决不了。
2026-10-13 21:59:31 +08:00

71 lines
3.8 KiB
Go
Raw 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 memory
import (
"testing"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding"
)
// measuredReviewChain* 是**实测参照值**(2026-10-13,本机空闲;正文取自活库
// cyrene/user 分区 201 条,均长 34.7 汉字;完整曲线与复跑命令见
// internal/runtime/embedding/budget.go 的文件头)。它们只用于本文件的断言 ——
// 与 embedding 侧 api_url_test.go 的 reviewBudgetRef 同一手法:把「拿实测数字做的
// 决定」留成可复核的常量,而不是消失在注释里。
//
// 链的形状 = ⌈(ScanLimit + 1) / embedding.DefaultBatchLimit⌉ 个请求:
//
// 改前 4 条/片:51 个请求 ⇒ 墙钟 23.070 / 21.860 s
// 改后 8 条/片:26 个请求 ⇒ 墙钟 21.435 / 21.597 s
//
// loaded 那一档取自 M14 只读勘测报告(同一条链与整理链并发时测得 56.7s)——
// 它是按**改前**的 4 条/片形状测的;链路墙钟由「条数 × 每条成本」主导、与批次
// 形状几乎无关,因此拿它当改后代码的最坏参照是**偏保守**的一侧。
const (
measuredReviewChainIdle = 22 * time.Second
measuredReviewChainLoaded = 57 * time.Second
)
// TestEmbeddingTimeoutFitsReviewBudget 钉住两条**跨包**口径:
//
// 1. embedding 单次调用的独立超时必须严格小于入库前审查的单条总预算;
// 2. 反过来,审查的单条总预算必须严格大于「实测最坏召回链长 + T1 自己的超时」。
//
// 第 1 条的理由(2026-10-07 实测的故障形态):端点主机写成 localhost → 先试 IPv6
// ::1 → 连接等系统超时(Windows 上约 21s),一次 embedding 失败正好吃光预算,
// 整条审查超时(`memory/review: context deadline exceeded`),排在后面的 T1
// 灰区判定连执行机会都没有。地址规范化(embedding.normalizeEndpointURL)治的是根,
// 第 1 条守的是「就算再有新的卡死来源,也得先把预算留给后续步骤」。
//
// 第 2 条是本次修复(2026-10-13)的核心断言,**变异自证**:把 defaultReviewTimeout
// 调回 20s ⇒ 这一条必红。它守的是「预算 ≥ 链长 + T1」这条不可让步的下界 ——
// 链长随 ScanLimit / 批上限漂移,T1 超时随 defaultT1Timeout 漂移,任一侧动到把
// 预算吃掉都会被它抓到。旧值 20s 正是踩在这条线上的:实测链长 21~23s(空闲),
// 于是每一条候选满额的审查都必然超时,7 天里 2274 条审查因此降级为空 recall_mode。
//
// embedding 侧同一口径也有一条钉值测试(api_url_test.go),两边互为护栏。
func TestEmbeddingTimeoutFitsReviewBudget(t *testing.T) {
t.Parallel()
if embedding.DefaultRequestTimeout <= 0 {
t.Fatalf("embedding.DefaultRequestTimeout = %s, 必须为正", embedding.DefaultRequestTimeout)
}
if embedding.DefaultRequestTimeout >= defaultReviewTimeout {
t.Fatalf("embedding 单次超时 %s 必须严格小于审查总预算 %s",
embedding.DefaultRequestTimeout, defaultReviewTimeout)
}
// 第 2 条:预算必须盖得住实测最坏链长,还要留得下 T1 那一步自己的超时
// (T1 在召回**之后**跑,同一条 ctx)。空闲余量倍数由 budget.go 的注释列明,
// 这里只钉住「盖得住」这条下界。
worst := measuredReviewChainLoaded + defaultT1Timeout
if defaultReviewTimeout <= worst {
t.Fatalf("审查单条预算 %s 必须严格大于「实测最坏召回链 %s + T1 超时 %s = %s」—— "+
"否则链还没跑完预算就用光(2026-10-13 的 2274 条超时降级正是这条)",
defaultReviewTimeout, measuredReviewChainLoaded, defaultT1Timeout, worst)
}
if measuredReviewChainIdle >= defaultReviewTimeout {
t.Fatalf("空闲实测链长 %s 已不小于审查预算 %s(余量必须为正)",
measuredReviewChainIdle, defaultReviewTimeout)
}
}