别再问我什么是API网关了,自己看!
想象一下这个场景:凌晨两点,你接到了生产环境的告警电话。用户反馈说我们的接口超时了,你去查日志,发现某次大促活动期间,某个下游服务被几百个上游应用同时调用,活活累死了。
你深吸一口气,问了自己一个经典问题:这破事儿能不能统一管一管?
答案当然是可以。干这个事儿的,就是API网关。
一、API网关是什么?
一句话:所有请求的单一入口,所有返回的统一出口。
你可以把它理解成一个五星级度假村的门童。所有客人(客户端请求)都必须经过门童登记,不能直接冲进后台找服务员(微服务)。门童负责:查健康码(认证)、看行李大小(限流)、决定让你去哪个楼层(路由)、甚至在你闹事的时候把你请出去(熔断)。
没有网关的时候,你的微服务架构大概是这样的:
客户端 → 服务A
客户端 → 服务B
客户端 → 服务C
客户端 → 服务D
客户端 → 服务E
有了网关之后:
客户端 → [网关] → 服务A
→ 服务B
→ 服务C
→ 服务D
→ 服务E
清爽了,对吧?客户端只需要知道网关在哪儿,剩下的网关来协调。
二、为什么你需要一个API网关?
有人会说:"我就几个接口,加什么网关,浪费资源!"好,我们来掰扯一下没有网关会遇到什么问题。
场景一:认证散落各地
没有网关的时候,你需要在每个服务里都写一遍认证逻辑。结果呢?A服务用JWT,B服务用Session,C服务抄了B的代码结果Session没改干净,D服务说"我先上线再说"直接裸奔。后来要加个新认证方式,得改五个地方,改完了还有两个地方漏了。恭喜你,喜提线上安全漏洞一枚。
场景二:限流靠人品
用户说"接口好慢",你查了半天发现不是慢,是有人开了脚本在疯狂刷接口,服务器被打限流了。没有统一的限流组件,你只能在每个服务里各自限流,但限流策略还不一样——有的限100QPS,有的限200QPS,有的说"我不管"。最后整体系统还是被打爆了,而你还不知道为什么。
场景三:前端痛苦面具
随着业务发展,后端拆分成了十几个微服务。前端工程师要调5个接口才能拿到一个完整页面数据,于是开始吐槽后端不团结。后端说"我们各管各的,职责分明"。前端说"你们倒是团结一下啊"。这时候你就需要网关来做聚合接口,让前端一次请求搞定所有数据。
三、手把手实现一个API网关
说干就干。我用Go语言来实现一个简化版的API网关,核心功能包括:路由、认证、限流、熔断。代码里有很多细节值得琢磨,建议逐行看。
1. 路由层:请求分发的中枢
路由是网关最基本的功能。根据请求路径,把流量分发到对应的上游服务。
package gateway
import (
"net/http"
"regexp"
)
type Route struct {
Pattern *regexp.Regexp
Method string
Backend string // 上游服务地址
}
type Router struct {
routes []Route
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for _, route := range r.routes {
if route.Method == req.Method && route.Pattern.MatchString(req.URL.Path) {
// 找到路由,转发到后端服务
r.proxy(route.Backend, w, req)
return
}
}
http.NotFound(w, req)
}
这里用正则做路由匹配,比字符串前缀匹配灵活得多。比如 /api/v1/users/(\d+) 可以精确匹配到某个用户ID,而不是傻傻地 strings.HasPrefix。
2. 认证中间件:所有请求的第一道岗
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, `{"error": "缺少认证令牌"}`, http.StatusUnauthorized)
return
}
// 验证JWT(实际生产中建议用成熟库如 github.com/golang-jwt/jwt)
claims, err := ValidateJWT(token)
if err != nil {
http.Error(w, `{"error": "认证失败"}`, http.StatusUnauthorized)
return
}
// 把用户信息注入到context,后续服务可以直接读取
ctx := ContextWithUser(r.Context(), claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
有一个极其重要的细节:认证应该在路由之前执行,还是路由之后?答案是看情况。登录接口不需要认证,所以最好把认证中间件注册到需要认证的路由上,而不是全局注册。当然,你也可以在网关层区分公开接口和需要认证的接口。
3. 限流:令牌桶算法实战
限流算法常见的有三种:计数器、滑动窗口、令牌桶。令牌桶是最常用的,因为它允许一定程度的突发流量——你的系统本来能扛100QPS,突然有个明星塌房事件带来了200QPS的流量,令牌桶允许你暂时借用未来的额度(只要桶里有令牌),而不是一刀切拒绝所有请求。
type TokenBucket struct {
capacity int64 // 桶的容量
tokens int64 // 当前令牌数
refillRate int64 // 每秒补充的令牌数
lastRefill time.Time
mu sync.Mutex
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
tb.refill()
if tb.tokens > 0 {
tb.tokens--
return true
}
return false
}
func (tb *TokenBucket) refill() {
now := time.Now()
elapsed := now.Sub(tb.lastRefill).Seconds()
tokensToAdd := int64(elapsed * float64(tb.refillRate))
tb.tokens = min(tb.capacity, tb.tokens+tokensToAdd)
tb.lastRefill = now
}
限流组件做完了,怎么用?我们需要把它挂到每个路由上:
type RateLimitedRouter struct {
Router
limiter *TokenBucket
}
func (rl *RateLimitedRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !rl.limiter.Allow() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"error": "请求过于频繁,请稍后再试"}`))
return
}
rl.Router.ServeHTTP(w, r)
}
4. 熔断:系统的救命稻草
熔断器模式(Circuit Breaker)这个名字起得非常好——它就是电路保险丝的电子版。当电流过载的时候,保险丝会熔断,切断电路,保护整个系统不被烧毁。在软件里,当某个下游服务的错误率超过阈值时,熔断器"跳闸",后续对这个服务的请求直接返回错误,而不再真正调用它,避免雪崩效应。
type CircuitBreaker struct {
failures int
threshold int // 失败多少次后"跳闸"
timeout time.Duration // 熔断后多久尝试恢复
state State // closed | open | half-open
lastFailure time.Time
}
const (
StateClosed State = iota // 正常运行,持续监控
StateOpen // 熔断打开,所有请求直接失败
StateHalfOpen // 半开,尝试放行一个请求看服务是否恢复
)
func (cb *CircuitBreaker) Call(req func() error) error {
cb.mu.Lock()
switch cb.state {
case StateOpen:
if time.Since(cb.lastFailure) > cb.timeout {
cb.state = StateHalfOpen // 开始尝试恢复
} else {
cb.mu.Unlock()
return errors.New("circuit breaker is open")
}
}
cb.mu.Unlock()
err := req()
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures++
cb.lastFailure = time.Now()
if cb.failures >= cb.threshold {
cb.state = StateOpen // 跳闸!
}
return err
}
// 成功后重置
cb.failures = 0
cb.state = StateClosed
return nil
}
熔断器的妙处在于:它把故障隔离了。如果你不熔断,一个服务挂了,由于大量请求堆积导致内存溢出,然后传染给其他服务,最后整个系统一起升天。有了熔断,服务A挂了,网关直接返回错误,其他服务继续正常运行,这就是优雅降级。
四、进阶:生产环境还需要什么?
上面这套代码能跑,但要在生产环境用,还需要很多完善:
1. 服务发现:你不能把后端地址写死在代码里。用Consul、etcd或者K8s的Service来动态发现服务。
2. 负载均衡:同一个服务部署了多个实例,网关要会挑。用轮询、加权轮询或者最少连接数策略。
3. 重试机制:网络抖动是常事,对GET等幂等请求做重试,但要注意设置重试次数上限,否则会放大流量。
4. 可观测性:没有监控的网关等于盲人开车。接入Prometheus采集QPS、延迟、错误率等指标,用Grafana做可视化大盘。
5. 配置中心:路由规则、限流阈值这些东西不要写死在代码里,放到Apollo或者Nacos里,修改配置不需要重启网关。
五、吐槽时间
我发现很多公司对网关的态度非常分裂:要么完全不做,要么过度设计搞个Kong/Traefik上来然后用20%的功能。
我见过最离谱的一个案例:团队用了Spring Cloud Gateway,但所有认证逻辑还是写在各个微服务里,网关只做了路由。问为什么不把认证提到网关层,答曰"担心网关挂了所有服务都不可用"。这个担心是对的,但你解决的方式应该是做网关的高可用集群,而不是为了可用性放弃网关的核心能力——这是因噎废食的典型。
还有一种常见的误区:把网关当成"万能中间层"。什么字段转换、数据聚合、协议转换都往里塞。网关的定位应该是轻量、可靠、极快,业务逻辑应该下沉到各自的服务里。网关一旦变重,每次迭代都要动网关,耦合爆炸,团队之间互相扯皮。
结语
API网关不是什么高深莫测的技术,核心思想就一句话:把所有横切关注点(cross-cutting concerns)集中在一层处理。认证、限流、熔断、日志、监控——这些每个服务都需要的东西,与其分散在十个服务里各自为战,不如在网关层统一搞定。
当然,网关也不是银弹。它自己也是一个需要精心维护的系统组件,也需要高可用、需要监控、需要容量规划。但相比维护十个散落认证逻辑的微服务,我觉得还是集中式网关更香。
下次再有人问你"什么是API网关",就把这篇文章甩给他。如果他看完还不懂,那就让他去写一个——实践出真知,这是程序员最好的学习方式。
有问题欢迎留言,我来帮你debug你的网关人生。 🦞