feat(proxy): 架构与算法优化二轮——sense 运行时单例激活 T-X3、打码密钥泄漏修复、账本热路径 2.4x

- gateway/proxy/routes.py:sense 运行时(SenseStore+Grader+晋升表)按配置单例化,
  原每请求重建导致:每请求跑全量 DDL、重读工件文件,且 T-X3 60s 决策缓存随
  Grader 丢弃、命中率恒为 0(本轮最大收益,激活既有已测组件);
  T1 审计抽样修复原 NameError×3 被 except 吞掉(load_config/get_review/request_id
  均未定义、§9.4 从未入队)——request_id 上提、review 队列经 review_getter 注入
  (api.py 反向依赖解除)、sample_rate 走 load_config
- gateway/model_pool.py:新增 usable_entries() 未打码内通道;routes 的降级链/
  预算降档/能力位重定向/档位映射四处改走该通道——修复打码 api_key 流入上游
  Bearer 头导致带密钥条目重定向必 401 的隐性缺陷(HTTP 管理面仍用打码 list())
- gateway/proxy/ledger.py + billing.py:共享持久连接(原每操作新建,全链路
  每请求 3-5 次 connect)+ 日重置 UPDATE 每实例每日一次短路(原每请求全表扫描
  抢写锁);check_and_count A/B 0.75→0.31 ms/op(2.4x);新增 usage_stats()
  SQL 聚合,/admin/stats 从 list_usage(limit=50 万) Python 四遍扫描改为下推聚合
  (微基准 ~28x,随流水线性扩大)
- gateway/sense/grader.py:决策留痕 insert_decision 移入 asyncio.to_thread
  (原同步 sqlite 写直接跑在事件循环线程,高并发阻塞网关;与观察写批量队列同等保护)
- 全量 474 项两轮复核全绿(基线 474)
This commit is contained in:
tzt
2026-09-18 23:59:44 +08:00
parent 781f8aaadf
commit e00e2cd2b1
6 changed files with 156 additions and 49 deletions
+33
View File
@@ -179,6 +179,39 @@ class BillingMixin:
(request_id,)).fetchone()
return dict(row) if row else None
def usage_stats(self) -> Dict[str, Any]:
"""全局流水聚合(§5.2 看板口径,2026-09 优化:SQL 下推替代全量拉取)。
返回 {requests, gateway_cached, in_hit_tok, in_miss_tok, revenue_milli,
cost_milli, by_bucket}by_bucket 键为 bucket 名。
"""
with self._lock, self._connect() as conn:
row = conn.execute(
"SELECT COUNT(*) AS requests,"
" COALESCE(SUM(gateway_cached), 0) AS gateway_cached,"
" COALESCE(SUM(in_hit_tok), 0) AS in_hit_tok,"
" COALESCE(SUM(in_miss_tok), 0) AS in_miss_tok,"
" COALESCE(SUM(charged_milli), 0) AS revenue_milli,"
" COALESCE(SUM(upstream_cost_milli), 0) AS cost_milli"
" FROM usage_ledger").fetchone()
buckets = conn.execute(
"SELECT bucket, COUNT(*) AS requests,"
" COALESCE(SUM(charged_milli), 0) AS revenue_milli,"
" COALESCE(SUM(upstream_cost_milli), 0) AS cost_milli"
" FROM usage_ledger GROUP BY bucket").fetchall()
return {
"requests": row["requests"],
"gateway_cached": row["gateway_cached"],
"in_hit_tok": row["in_hit_tok"],
"in_miss_tok": row["in_miss_tok"],
"revenue_milli": row["revenue_milli"],
"cost_milli": row["cost_milli"],
"by_bucket": {r["bucket"]: {"requests": r["requests"],
"revenue_milli": r["revenue_milli"],
"cost_milli": r["cost_milli"]}
for r in buckets},
}
def list_usage(self, student_id: Optional[int] = None,
limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
"""流水分页(可按学生过滤,经其名下 key);按学生过滤走联表常量语句。"""
+25 -9
View File
@@ -8,8 +8,8 @@
工程纪律:
- D-P1 毫元整数,本模块不做任何浮点运算(元换算只发生在入口参数转换)。
- D-P10:同步实现 + 全局锁每操作连接ReviewQueue 模式),异步调用方经
asyncio.to_thread 包装;WAL 模式(§3)。
- D-P10:同步实现 + 全局锁串行访问ReviewQueue 模式2026-09 起复用单条
持久连接,语义不变),异步调用方经 asyncio.to_thread 包装;WAL 模式(§3)。
- 全部数据库访问使用占位符参数化语句,语句为常量,零拼接。
"""
from __future__ import annotations
@@ -74,6 +74,8 @@ class Ledger(BillingMixin):
self.db_path = Path(db_path)
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.Lock()
self._conn: Optional[sqlite3.Connection] = None
self._reset_date: Optional[str] = None # 日重置短路标记(2026-09 优化)
self._init_db()
# ---------- 初始化 ----------
@@ -83,9 +85,17 @@ class Ledger(BillingMixin):
return cls(db_path)
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
"""共享持久连接(2026-09 优化)。
所有操作本就串行于 self._lock 之下,复用单连接语义与逐操作新建等价,
省去每请求 3-5 次 sqlite3.connectWindows 上开销显著);
`with conn` 只做提交/回滚不关闭,连接持续可用。
"""
if self._conn is None:
conn = sqlite3.connect(self.db_path, check_same_thread=False)
conn.row_factory = sqlite3.Row
self._conn = conn
return self._conn
def _init_db(self) -> None:
with self._lock, self._connect() as conn:
@@ -168,12 +178,18 @@ class Ledger(BillingMixin):
# ---------- 日限额 ----------
def check_and_count(self, key_id: int, student_id: int, now: float) -> bool:
"""日请求限额双检:跨日重置(注入日期)+ 原子计数;超限返回 False(429)。"""
"""日请求限额双检:跨日重置(注入日期)+ 原子计数;超限返回 False(429)。
2026-09 优化:重置 UPDATE 每实例每日只执行一次(原每请求全表扫
students 并抢写锁);单进程部署(D-P9)下语义等价。
"""
today = _today(now)
with self._lock, self._connect() as conn:
conn.execute(
"UPDATE students SET spent_today_milli = 0, spent_date = ?"
" WHERE spent_date IS NOT ?", (today, today))
if self._reset_date != today:
conn.execute(
"UPDATE students SET spent_today_milli = 0, spent_date = ?"
" WHERE spent_date IS NOT ?", (today, today))
self._reset_date = today
row = conn.execute(
"SELECT req_today, req_date, day_cap_req FROM proxy_keys"
" WHERE id = ?", (key_id,)).fetchone()
+79 -36
View File
@@ -46,11 +46,14 @@ def install_error_handlers(app) -> None:
headers={"WWW-Authenticate": "Bearer"} if exc.status_code == 401 else None)
def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRouter:
def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None,
review_getter=None) -> APIRouter:
"""组装代理面路由(唯一组装点)。
settings_provider:返回设置对象的回调(api.py 注入 settings_store)。
未注入时语义分析器分流不启用(None 安全,既有测试不受影响)。
review_getter:返回人工检验队列的回调(api.py 注入 get_review);
未注入时 T1 审计抽样跳过(不报错)。
"""
router = APIRouter(prefix="/proxy")
ledger = __import__("gateway.proxy.ledger", fromlist=["Ledger"]).Ledger.init_db(cfg.db_path)
@@ -87,19 +90,16 @@ def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRou
# 语义分析器 live 分流(D-G7:mode=live 才启用;任何异常不影响代理可用性)。
# T-X4 修复:settings_provider 由 api.py 注入(原实现引用未导入的
# settings_storeNameError 被 except 吞掉,分流从未实际生效)。
# 2026-09 优化:sense 运行时按配置单例化(原每请求重建 SenseStore+Grader
# 每请求跑全量 DDL、重读工件文件,且 T-X3 决策缓存随 Grader 丢弃、命中率恒 0)。
tier_used = None
request_id = "px" + uuid.uuid4().hex[:12]
if settings_provider is not None:
try:
from gateway.sense.config import build_sense_config
from gateway.sense.grader import Grader
from gateway.sense.observer import get_observer
from gateway.sense.promotion import PromotionTable
from gateway.sense.store import SenseStore
scfg = build_sense_config(settings_provider().to_dict())
if scfg.enabled and scfg.mode == "live":
sstore = SenseStore.init_db(scfg.db_path)
grader = Grader(scfg, sstore, get_observer(sstore),
promotion=PromotionTable(sstore))
_sstore, grader = _get_sense_runtime(scfg)
qtext = "\n".join(str(m.get("content") or "")
for m in (body.get("messages") or []))
d = await grader.decide(qtext or str(body.get("model") or ""),
@@ -113,17 +113,22 @@ def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRou
except Exception:
pass # D-G4:分级故障不影响代理可用性
try:
resp = await _run_chat(body, dict(request.headers), ctx, cfg, ledger, pool)
resp = await _run_chat(body, dict(request.headers), ctx, cfg, ledger, pool,
request_id=request_id)
if tier_used:
resp.headers["x-campus-tier"] = tier_used
# T1 审计抽样(§9.4):按 review.sample_rate 入队人工核;
# promo 标签供人工 verdict 回填晋升表(T-X5
# promo 标签供人工 verdict 回填晋升表(T-X5
# 2026-09 修复:原块引用未定义的 load_config/get_review/request_id
# 三个 NameError 被 except 吞掉,抽样从未入队;review 队列现由
# api.py 经 review_getter 注入(避免 proxy -> api 反向依赖)。
try:
import random as _random
from router_system.config import load_config
from gateway.sense.promotion import promotion_label
rate = float(load_config().get("review", {}).get("sample_rate", 0.1))
if _random.random() < rate:
get_review().enqueue(
if review_getter is not None and _random.random() < rate:
review_getter().enqueue(
request_id + "-sense", str(body.get("model") or "proxy"),
"(sense T1 审计抽样)",
tags=["sense_t1", "promo:" + promotion_label("proxy", "")],
@@ -136,7 +141,7 @@ def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRou
def _entry_by_tier_hint(pool, hint: str) -> Optional[Dict[str, Any]]:
"""按档位名选池条目(tier_hint 优先,缺省按 tier 字段映射)。"""
entries = pool.list().get("entries", [])
entries = _pool_entries(pool)
for e in entries:
if not e.get("enabled") or e.get("backend") == "mock":
continue
@@ -191,32 +196,27 @@ def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRou
@router.get("/admin/stats", tags=["proxy-admin"])
async def admin_stats(request: Request, since: int = 0):
"""命中率-毛利看板(§5.2 口径):h_g/h_p/revenue/cost/margin/by_bucket。"""
"""命中率-毛利看板(§5.2 口径):h_g/h_p/revenue/cost/margin/by_bucket。
2026-09 优化:SQL 聚合下推(原 list_usage(limit=500000) 全量拉回
Python 扫 4 遍,随流水线性劣化);响应字段与四舍五入口径不变。
"""
_guard_admin(request)
from gateway.proxy.ledgerutil import _today
import time as _t
now = _t.time()
today = _today(now)
# list_usage 返回全列(含 stats 所需字段),复用既有参数化查询
rows = ledger.list_usage(limit=500000)
n = len(rows)
cached = sum(1 for r in rows if r["gateway_cached"])
s = ledger.usage_stats()
n = s["requests"]
cached = s["gateway_cached"]
h_g = cached / n if n else 0.0
in_hit = sum(r["in_hit_tok"] for r in rows)
in_miss = sum(r["in_miss_tok"] for r in rows)
in_hit = s["in_hit_tok"]
in_miss = s["in_miss_tok"]
h_p = in_hit / (in_hit + in_miss) if (in_hit + in_miss) else 0.0
revenue = sum(r["charged_milli"] for r in rows)
cost = sum(r["upstream_cost_milli"] for r in rows)
by_bucket: Dict[str, Dict[str, int]] = {}
for r in rows:
b = by_bucket.setdefault(r["bucket"], {"requests": 0, "revenue_milli": 0,
"cost_milli": 0})
b["requests"] += 1
b["revenue_milli"] += r["charged_milli"]
b["cost_milli"] += r["upstream_cost_milli"]
return {"requests": n, "h_g": round(h_g, 4), "h_p": round(h_p, 4),
"revenue_milli": revenue, "cost_milli": cost,
"margin_milli": revenue - cost, "by_bucket": by_bucket,
"revenue_milli": s["revenue_milli"], "cost_milli": s["cost_milli"],
"margin_milli": s["revenue_milli"] - s["cost_milli"],
"by_bucket": s["by_bucket"],
"today": today,
"upstream_failover": upstream_failover_stats()}
@@ -271,6 +271,15 @@ def _limits():
return _AUTH_SINGLETON_LIMITS
def _pool_entries(pool) -> List[Dict[str, Any]]:
"""池条目快照(转发链内部专用):优先未打码通道 usable_entries
回退打码 list()(兼容测试注入的简化 pool)。HTTP 响应一律用 list()。"""
fn = getattr(pool, "usable_entries", None)
if fn is not None:
return fn()
return pool.list().get("entries", [])
def _estimate_hold_milli(body: dict, cfg: ProxyConfig, model: str) -> int:
"""预扣估算(宁可高估,D-P11):in 按字符/3out 按 min(max_tokens,4096)。
@@ -352,7 +361,7 @@ def _fallback_chain(pool, entry: Dict[str, Any], max_total: int = 3) -> List[Dic
"""
from gateway.model_pool import TIERS
tier_rank = {t: i for i, t in enumerate(TIERS)}
rest = [e for e in pool.list().get("entries", [])
rest = [e for e in _pool_entries(pool)
if (e.get("enabled") and e.get("id") != entry.get("id")
and e.get("backend") not in ("mock",) and e.get("base_url")
and e.get("model") != entry.get("model"))]
@@ -383,7 +392,7 @@ def _downgrade_entry(pool, entry: Dict[str, Any], mode: str) -> Optional[Dict[st
if target < 0:
return None
want = TIERS[target]
for e in pool.list().get("entries", []):
for e in _pool_entries(pool):
if (e.get("enabled") and e.get("id") != entry.get("id")
and e.get("tier") == want
and e.get("backend") not in ("mock",) and e.get("base_url")):
@@ -413,6 +422,38 @@ def reset_semcache_instances() -> None:
_semcache_instances = {}
_sense_runtimes: Dict[tuple, Any] = {}
def _get_sense_runtime(scfg):
"""sense 运行时按配置单例化(2026-09 优化,激活 T-X3 决策缓存)。
键取 SenseConfig 的完整 repr:任何设置变更都会派生新键并重建运行时,
旧实例留在注册表中随设置维度有界增长(单进程部署,D-P9)。
返回 (sstore, grader)Grader 持有的线性头/阈值/60s 决策缓存跨请求复用,
免去每请求的 SenseStore DDL 与工件文件重读。
"""
key = ("sense", repr(scfg))
rt = _sense_runtimes.get(key)
if rt is None:
from gateway.sense.grader import Grader
from gateway.sense.observer import get_observer
from gateway.sense.promotion import PromotionTable
from gateway.sense.store import SenseStore
sstore = SenseStore.init_db(scfg.db_path)
grader = Grader(scfg, sstore, get_observer(sstore),
promotion=PromotionTable(sstore))
rt = (sstore, grader)
_sense_runtimes[key] = rt
return rt
def reset_sense_runtimes() -> None:
"""测试用:清空 sense 运行时单例。"""
global _sense_runtimes
_sense_runtimes = {}
def _cached_charge(body: dict, cfg: ProxyConfig, model: str) -> Dict[str, int]:
"""缓存命中计费:成本 0,按未命中口径对入/出估 token 收售价(§7)。"""
from gateway.proxy.pricing import compute
@@ -423,14 +464,16 @@ def _cached_charge(body: dict, cfg: ProxyConfig, model: str) -> Dict[str, int]:
async def _run_chat(body: dict, headers: Dict[str, str], ctx: Dict[str, Any],
cfg: ProxyConfig, ledger, pool):
cfg: ProxyConfig, ledger, pool,
request_id: Optional[str] = None):
model = str(body.get("model") or "")
entry = _resolve_entry(pool, model, cfg)
if entry is None:
return JSONResponse({"error": {"message": f"模型不可用: {model}",
"type": "invalid_request_error"}},
status_code=400)
request_id = "px" + uuid.uuid4().hex[:12]
if request_id is None:
request_id = "px" + uuid.uuid4().hex[:12]
ts = time.time()
client_wants_usage = bool(body.get("stream_options", {}).get("include_usage")) \
if isinstance(body.get("stream_options"), dict) else False
@@ -441,7 +484,7 @@ async def _run_chat(body: dict, headers: Dict[str, str], ctx: Dict[str, Any],
if not _entry_meets(entry, needs):
from gateway.model_pool import filter_by_capabilities
cap_entries = filter_by_capabilities(
[e for e in pool.list().get("entries", [])
[e for e in _pool_entries(pool)
if e.get("enabled") and e.get("backend") not in ("mock",)
and e.get("base_url")],
need_vision=needs["vision"], need_tools=needs["tools"],