跳到內容

核心概念

Gortexa 的設計建立在四個想法上:Protobuf 是唯一事實來源、一個 h2c port 多工三種協定、固定八段的攔截器鏈,以及一張 gRPC、HTTP 與 MCP 共用的錯誤對應表。以下逐一說明。

Protobuf 是單一事實來源。你只寫兩種東西:proto contract 和 logic 實作。gRPC stubs、HTTP/JSON gateway、OpenAPI spec、MCP tool schema 全部由 gortexa regen 從 proto 產生,流程固定是 buf lint → buf breaking → buf generate。

  • gen/ 下全是產生碼:進版控(CI 檢查 drift)、不手動編輯。
  • breaking-change gate 內建在流程裡:會破壞相容性的 proto 修改直接被擋下,additive 修改放行。

一個 cleartext HTTP/2 port,依 Content-Type 與路徑分派:

請求 去向
Content-Type: application/grpc gRPC(grpc-go 的 ServeHTTP 模式)
路徑 /mcp MCP bridge(Streamable HTTP)
其他 grpc-gateway(HTTP/JSON)與內建路由(/healthz、/openapi.json)

h2c 用的是 Go 原生 http.Server.Protocols,不是已棄用的 x/net/h2c。cleartext 表示部署時 TLS 必須在前面終結,見部署。

八段、順序固定:

recover → request-id → logger → load-shed → rate-limit → circuit-breaker → auth → validation

鏈不完整時啟動直接失敗(fail-loud),不會默默少掛一段;整條鏈缺席也一樣被拒絕——kernel.New 要求 WithInterceptors 與 WithoutInterceptors 恰好給一個,沒有 recover/auth/rate limit/validation 的伺服器不可能因為「忘了寫」而產生。OTel 以 gRPC StatsHandler 掛載,unary 與 streaming 都涵蓋。recover 段復原的 panic 會以 request_id 記錄(呼叫端提供或鏈鑄造、與回應標頭 X-Request-Id 一致)——那是一個 panic 的 RPC 唯一會留下的紀錄。

順序固定,但 auth 段可插拔:預設是 HS256 JWT(interceptor.Config.Verifier),而 auth 段實際跑的是一個 auth.Authenticator——非 JWT 方案(static bearer/mTLS/API-key)改設 Config.Authenticator 即可,八段順序不變。要在鏈上加自己的 interceptor,用 kernel.WithServerOptions(...)(gRPC 會接在框架鏈之後、handler 之前,仍在 recover 之內),而非改動這八段。注意 interceptor 與 StatsHandler 選項如述生效,但傳輸層選項——KeepaliveParams、KeepaliveEnforcementPolicy、MaxStreams、InitialWindowSize——在共用的 h2c 埠上會被默默忽略:對外 gRPC 在那裡跑的是 grpc-go 的 ServeHTTP handler 模式,傳輸由 http.Request 建構。它們對 in-process loopback 仍有效。公開 gRPC 表面需要 keepalive 強制時,得把 gRPC 拆到自己的 listener。

鏈上三段治理的預設值定義在 cmd/server/main.go(不在 config.yaml——要調整就改 interceptor.NewSet 的參數重新編譯):

段 預設值 超限行為
rate-limit 每 peer 200 RPS、burst 100、追蹤 TTL 10 分鐘 RESOURCE_EXHAUSTED(HTTP 429)
circuit-breaker 連續 5 次失敗開路 10 秒、half-open 放 2 個探測 UNAVAILABLE(HTTP 503)
load-shed in-flight 上限 1,024(gRPC health 檢查豁免;reflection 開啟時 /grpc.reflection. 同樣豁免) RESOURCE_EXHAUSTED(HTTP 429)

circuit-breaker 只計伺服端失敗:Unavailable、Internal,以及 request context 仍存活時 handler 產生的 DeadlineExceeded。呼叫端造成的結果——request context 已取消或逾時,以及 Canceled/InvalidArgument/Unauthenticated/PermissionDenied(auth 與 validation 在斷路器之內回的)——是中性的:既不觸發也不重置計數。half-open 的探測若超過 OpenInterval 都沒回報,該回合直接放棄、下一個呼叫者開新回合,長串流不會把方法卡死在 half-open。要讓慢的依賴被看見,依賴的 timeout 要短於 client deadline。

量測數字(框架 README 的基準測試,Apple M1/go1.26.5):通過認證的 unary 走完八段是 80 allocs(ChainUnaryJWT)——容量規劃要看這個數字;auth 被略過的 health/reflection 路徑則約 1,552 ns、27 allocs(ChainUnaryAuthSkipped),兩者差在 HS256 驗簽、metadata 查找與 JWT 解析。rate-limiter Allow 熱路徑 0 allocs(allocs 機器無關)。

一張對應表決定同一個錯誤在三個協定的輸出:gRPC status、HTTP status、MCP error envelope。原則是內部原因不外洩——只有 invalid-argument 與 unauthenticated 兩類會把訊息轉發給呼叫端,其餘類別一律回 registry 中的安全訊息。handler 以 %w 包裝 status 時,也只轉發最內層 status 自己的訊息——包裝層的上下文(host、DSN、SQL)與 grpc 的 rpc error: code = … 樣板一律換成 registry 的安全訊息。

錯誤解析熱路徑約 70 ns、2 allocs(用 Go 1.26 的 errors.AsType)。

元件 說明
Config 分層載入、fail-loud、secrets 遮罩。見設定
Health 三態健康對應 gRPC Health;app.Health().Register(...) 接真實依賴
Logging/Observability slog 結構化日誌 + OTLP 匯出
Database PgBouncer-safe 的 pgx pool + sqlc
Cache 預設 process-local in-memory;cache.driver: redis 切換分散式(零依賴 RESP client)
MQ NATS(core 或 JetStream);mq.group_id 空=fanout、非空=load-balance