Files
sundynix-agentix/sundynix-gateway/internal/middleware/guardrail.go
T
Blizzard 7bfea74cc0 feat(perf): 容量压测器 + 平台天花板实测曲线
cmd/loadtest:闭环加压器,阶梯并发,经 SSE 流检测完成(非轮询,避免轮询放大把网关
压成假瓶颈),输出吞吐 + 延迟分位。配套两个 benchmark 开关:
- dispatcher LLM_FORCE_STUB=1 + LLM_STUB_TTFT_MS/INTERTOKEN_MS=0:绕开真实 LLM 推理,
  量平台自身全链路天花板(不烧 token、不被模型节奏掩盖)。
- gateway RATE_LIMIT_PER_MIN 可配(缺省 120):压测放开限流。

实测(单 dispatcher、并发64、stub):
- 单任务纯平台开销 ~42ms;吞吐峰值 ~110 全链路任务/秒(饱和点 并发32–64);
  并发128 优雅降速,256 硬崩。
- 吞吐瓶颈不是 DB 连接数(池 25→80 吞吐不变),是每任务多跳管线综合成本;
  256 崩根因为 DSN 用 localhost、pgx 每连接解析 → 连接churn致 DNS 取消(易修)。
- 关键判断:平台 42ms ≪ LLM 秒级出答案,平台不是瓶颈、GPU 才是;横向拆服务喂满 GPU 集群
  的意义成立。细节与曲线见 project_analysis「容量实测」。

stub 延迟改 env 可配(默认值不变);不影响线上路径。dispatcher+gateway build/vet/test 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 11:46:50 +08:00

57 lines
2.2 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 middleware 提供 Guardrail 与限流等接入层中间件。
package middleware
import (
"bytes"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/sundynix/sundynix-gateway/internal/guardrail"
"github.com/sundynix/sundynix-gateway/internal/store"
)
// Guardrail 实现 Harness 输入护栏:拦截提示词注入 / 超大请求体。
// 只检查带 JSON 体的写请求(POST/PUT);文件上传(multipart)与 GET/SSE 不经此。
// 输出护栏不在此做 —— Token 流为 SSE 实时流,网关缓冲会破坏流式,输出过滤应在
// dispatcher 的 token 发射层(见 PROGRESS 路线图)。
func Guardrail() gin.HandlerFunc {
return func(c *gin.Context) {
if m := c.Request.Method; (m == http.MethodPost || m == http.MethodPut) &&
strings.HasPrefix(c.GetHeader("Content-Type"), "application/json") {
// 限读上限 + 1 字节以判定"过大";命中拦截则后续 handler 不执行。
body, _ := io.ReadAll(io.LimitReader(c.Request.Body, guardrail.MaxJSONBytes+1))
res := guardrail.Inspect(body)
if res.Blocked {
log.Printf("[guardrail] 拦截 %s %s%s", c.Request.Method, c.Request.URL.Path, res.Reason)
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{"error": "输入护栏拦截:" + res.Reason})
return
}
if res.Suspect { // 灰区:放行但打标,交 Dispatcher 的 LLM 分类器(Tier2)裁决
log.Printf("[guardrail] 灰区放行 %s %s:软信号 %v", c.Request.Method, c.Request.URL.Path, res.Signals)
c.Set("guardrail_suspect", true)
}
c.Request.Body = io.NopCloser(bytes.NewReader(body)) // 还原请求体供后续 handler 读取
}
c.Next()
}
}
// RateLimit 基于 Redis 的会话级限流(按客户端 IP,每分钟上限)。
// 上限经 RATE_LIMIT_PER_MIN 配置(缺省 120);压测可调高。Redis 降级时始终放行,不阻断业务。
func RateLimit(cache *store.Redis) gin.HandlerFunc {
perMinute := int64(envInt("RATE_LIMIT_PER_MIN", 120))
return func(c *gin.Context) {
ok, _ := cache.Allow(c.Request.Context(), c.ClientIP(), perMinute, time.Minute)
if !ok {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
return
}
c.Next()
}
}