7bfea74cc0
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>
57 lines
2.2 KiB
Go
57 lines
2.2 KiB
Go
// 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()
|
||
}
|
||
}
|