news 2026/7/6 14:48:29

极简 API 网关:手写一个支持限流和断路器的轻量反向代理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
极简 API 网关:手写一个支持限流和断路器的轻量反向代理

极简 API 网关:手写一个支持限流和断路器的轻量反向代理

一、网关的肥胖症:为什么 Nginx + 插件不足以说服你另起炉灶

Nginx 是优秀的反向代理,Kong 是强大的 API 网关。但有一种场景会让你动摇:你的后端只有三个微服务,每天几万次调用。这时候引入 Kong(需要 PostgreSQL + 配置管理 + 插件生态),运维复杂度超过了业务价值。

极简主义的回答是:如果需求足够明确,200 行 Go 代码写一个网关比配 Nginx 插件更快、更可控、更易维护。

核心需求通常就三个:反向代理(把请求路由到正确的后端)、限流(防止某个客户端打爆服务)、断路器(后端挂了就不再转发,避免雪崩)。加上健康检查和基本的日志,这就是一个够用的网关。

graph LR C1[客户端A] --> GW[API 网关] C2[客户端B] --> GW C3[客户端C] --> GW GW -->|限流器检查| RL[令牌桶] GW -->|断路器检查| CB[熔断器] CB -->|闭合| S1[用户服务 :8081] CB -->|熔断| FB[快速失败 503] CB -->|闭合| S2[订单服务 :8082] S1 -->|失败计数| CB style FB fill:#ff6b6b,color:#fff style GW fill:#4dabf7,color:#fff

本文将实现一个不到 300 行的 Go API 网关,覆盖反向代理、令牌桶限流和断路器三个核心能力。

二、反向代理的深层机制:ReverseProxy 的请求改写与错误传播

Go 标准库的net/http/httputil.ReverseProxy是网关的基础。它的核心逻辑是:接收*http.Request,构造一个新的*http.Request发送到后端,然后将后端的*http.Response写回给客户端。

在使用ReverseProxy时,有三个容易出错的点:

请求头改写:默认情况下,ReverseProxy会复制客户端的 Host 头到后端。如果后端服务依赖 Host 头做路由,这会导致请求被路由到错误的服务。用Director函数可以覆盖。

SSE/流式响应ReverseProxy默认会缓冲后端的响应体。对于 SSE(Server-Sent Events)这种流式响应,需要设置FlushInterval为负数或较小正数以关闭缓冲。

错误响应格式统一:后端返回 4xx/5xx 时,网关不应该原样透传。应该用ModifyResponse统一错误响应格式,把后端的内部错误信息替换为对客户端友好的提示。

三、完整实现:限流器 + 断路器 + 反向代理的组合

package gateway import ( "context" "fmt" "log" "net/http" "net/http/httputil" "net/url" "sync" "time" ) // ========== 令牌桶限流器 ========== type TokenBucket struct { rate float64 // 每秒生成的令牌数 burst int // 桶容量 tokens float64 lastRefill time.Time mu sync.Mutex } func NewTokenBucket(rate float64, burst int) *TokenBucket { return &TokenBucket{ rate: rate, burst: burst, tokens: float64(burst), lastRefill: time.Now(), } } func (tb *TokenBucket) Allow() bool { tb.mu.Lock() defer tb.mu.Unlock() now := time.Now() elapsed := now.Sub(tb.lastRefill).Seconds() tb.tokens += elapsed * tb.rate if tb.tokens > float64(tb.burst) { tb.tokens = float64(tb.burst) } tb.lastRefill = now if tb.tokens >= 1.0 { tb.tokens -= 1.0 return true } return false } // ========== 断路器 ========== type CircuitState int const ( CircuitClosed CircuitState = iota CircuitOpen CircuitHalfOpen ) type CircuitBreaker struct { failureThreshold int cooldownPeriod time.Duration failureCount int state CircuitState lastFailureTime time.Time mu sync.Mutex } func NewCircuitBreaker(threshold int, cooldown time.Duration) *CircuitBreaker { return &CircuitBreaker{ failureThreshold: threshold, cooldownPeriod: cooldown, state: CircuitClosed, } } func (cb *CircuitBreaker) Allow() bool { cb.mu.Lock() defer cb.mu.Unlock() switch cb.state { case CircuitClosed: return true case CircuitOpen: if time.Since(cb.lastFailureTime) > cb.cooldownPeriod { cb.state = CircuitHalfOpen return true } return false case CircuitHalfOpen: return true } return false } func (cb *CircuitBreaker) RecordSuccess() { cb.mu.Lock() defer cb.mu.Unlock() cb.failureCount = 0 cb.state = CircuitClosed } func (cb *CircuitBreaker) RecordFailure() { cb.mu.Lock() defer cb.mu.Unlock() cb.failureCount++ cb.lastFailureTime = time.Now() if cb.failureCount >= cb.failureThreshold { cb.state = CircuitOpen } } // ========== 网关核心 ========== type Gateway struct { routes map[string]*Route mu sync.RWMutex } type Route struct { path string target *url.URL proxy *httputil.ReverseProxy limiter *TokenBucket breaker *CircuitBreaker } func NewGateway() *Gateway { return &Gateway{ routes: make(map[string]*Route), } } // AddRoute 注册一个后端服务 func (g *Gateway) AddRoute(pathPrefix, targetURL string, rate float64, burst int, cbThreshold int, cbCooldown time.Duration) error { target, err := url.Parse(targetURL) if err != nil { return fmt.Errorf("parse target URL: %w", err) } proxy := httputil.NewSingleHostReverseProxy(target) // 自定义 Director:修改请求路径 originalDirector := proxy.Director proxy.Director = func(req *http.Request) { originalDirector(req) // 去掉前缀后转发 req.URL.Path = req.URL.Path[len(pathPrefix):] } // 统一的错误处理 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { log.Printf("proxy error for %s: %v", r.URL.Path, err) http.Error(w, `{"error":"backend_unavailable"}`, http.StatusBadGateway) } g.mu.Lock() defer g.mu.Unlock() g.routes[pathPrefix] = &Route{ path: pathPrefix, target: target, proxy: proxy, limiter: NewTokenBucket(rate, burst), breaker: NewCircuitBreaker(cbThreshold, cbCooldown), } return nil } func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { route := g.matchRoute(r.URL.Path) if route == nil { http.Error(w, `{"error":"route_not_found"}`, http.StatusNotFound) return } // Step 1: 限流检查 if !route.limiter.Allow() { http.Error(w, `{"error":"rate_limit_exceeded"}`, http.StatusTooManyRequests) return } // Step 2: 断路器检查 if !route.breaker.Allow() { http.Error(w, `{"error":"circuit_open"}`, http.StatusServiceUnavailable) return } // Step 3: 记录响应用于断路器状态更新 recorder := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK} route.proxy.ServeHTTP(recorder, r) // Step 4: 根据响应状态更新断路器 if recorder.statusCode >= 500 { route.breaker.RecordFailure() } else { route.breaker.RecordSuccess() } } func (g *Gateway) matchRoute(path string) *Route { g.mu.RLock() defer g.mu.RUnlock() // 最长前缀匹配 var bestMatch *Route for prefix, route := range g.routes { if len(path) >= len(prefix) && path[:len(prefix)] == prefix { if bestMatch == nil || len(prefix) > len(bestMatch.path) { bestMatch = route } } } return bestMatch } type statusRecorder struct { http.ResponseWriter statusCode int } func (r *statusRecorder) WriteHeader(code int) { r.statusCode = code r.ResponseWriter.WriteHeader(code) } // 使用示例 func main() { gw := NewGateway() gw.AddRoute("/api/users", "http://localhost:8081", 100, 20, 5, 30*time.Second) gw.AddRoute("/api/orders", "http://localhost:8082", 50, 10, 3, 20*time.Second) log.Println("Gateway listening on :8080") http.ListenAndServe(":8080", gw) }

关键设计:statusRecorder拦截 WriteHeader 调用记录状态码,用于断路器状态更新。路由匹配使用最长前缀匹配,保证/api/users/profile优先匹配/api/users而非/api

四、边界分析:这个网关不能做什么

不支持动态路由:路由在启动时注册,不能运行时热更新。如果你的路由需要频繁变更,考虑引入 etcd/Consul 做服务发现。

不支持 TLS 终止:本实现只处理 HTTP。生产环境建议在网关前面加一层 Nginx 或 Caddy 处理 TLS,网关本身处理业务逻辑。

没有请求体改写能力:如果需要在网关层修改请求体(如注入认证信息),需要实现ModifyResponse

断路器的简单假设:本实现使用简单的失败计数模式。更精细的做法是监控错误率而非绝对计数,但复杂度会翻倍。在微服务数量 < 5 的场景下,简单方案已经够用。

性能上限:单进程 Go 网关可以支撑数万 QPS。但如果后端的延迟 > 100ms,网关的并发连接数会快速膨胀。配置合理的连接池和超时可以缓解这个问题。

五、总结

当你只有三个微服务时,300 行 Go 代码的网关比任何"全家桶"方案都更值得。核心就三个组件:令牌桶限流(防过度调用)、断路器(防雪崩)、反向代理(请求路由)。

每个组件的代码不超过 50 行,阅读和维护成本极低。以后需求增长时,你可以选择逐步替换组件(如把令牌桶换成 Redis 分布式限流),而不是上来就引入整个 Kong 生态。

少即是多。工程化的本质不是堆砌工具,而是在合适的时候,用合适的复杂度解决问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/6 14:47:41

专业级Windows系统美化革命:DWMBlurGlass深度解析与实战指南

专业级Windows系统美化革命&#xff1a;DWMBlurGlass深度解析与实战指南 【免费下载链接】DWMBlurGlass Add custom effect to global system title bar, support win10 and win11. 项目地址: https://gitcode.com/gh_mirrors/dw/DWMBlurGlass DWMBlurGlass作为一款革命…

作者头像 李华
网站建设 2026/7/6 14:46:52

投票模式:多数决 + 加权 + 辩论

投票模式&#xff1a;多数决 加权 辩论 概念速查 投票模式&#xff08;Voting Pattern&#xff09;是 Multi-Agent 系统中最基础的共识机制&#xff1a;多个 Agent 各自输出对同一问题的回答&#xff0c;通过聚合函数选出一个最终答案。常见的变体有三种&#xff1a;模式聚合…

作者头像 李华
网站建设 2026/7/6 14:45:11

【QT】国产化编译指南6

rmp系统能用yum吗在 Linux 系统中&#xff0c;通常是 “RPM 包管理系统”&#xff08;使用.rpm 格式包的系统&#xff0c;如 CentOS、RHEL、Fedora 等&#xff09;&#xff0c;这类系统可以使用 yum。yum&#xff08;Yellowdog Updater Modified&#xff09;是基于 RPM 包管理的…

作者头像 李华
网站建设 2026/7/6 14:42:37

项目编号:project50575|Express组团旅游平台:旅游线路、用户报名、订单支付与后台运营全流程技术栈:Express + Node.js + MySQL

摘要&#xff1a;Express组团旅游平台围绕用户操作、后台管理、业务流程和数据维护展开&#xff0c;本文从项目概述、用例分析、功能结构、流程设计、运行效果和数据库设计几个方面进行完整拆解。一、项目概述Express组团旅游平台的核心价值在于把具体业务对象和管理流程串联起…

作者头像 李华
网站建设 2026/7/6 14:40:05

低成本创业做GEO优化代理合适吗

低成本创业者——启动资金有限、团队只有一两个人甚至只有自己、希望以最小风险验证一个方向的可行性。GEO代理适不适合这类创业者&#xff1f;低成本创业与全案托管代理高度匹配低成本创业的核心诉求是低风险启动。全案托管代理几乎是为这个诉求量身定做的。找一个好的GEO服务…

作者头像 李华
网站建设 2026/7/6 14:39:31

【AAAI 2026】VQAThinker:通过RL进行可解释VQA训练

note 本文提出了VQAThinker&#xff0c;一种基于推理的VQA框架&#xff0c;通过结合大型多模态模型和强化学习&#xff0c;实现了可泛化和可解释的视频质量评估。它把视频质量评估从“直接回归一个分数”&#xff0c;升级成“先像人一样分析视频有哪些质量问题&#xff0c;再给…

作者头像 李华