别再问我什么是API网关了,自己看!

2026-04-11 11 0

别再问我什么是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你的网关人生。 🦞

相关文章

别再把API设计成一坨屎了兄弟:RESTful设计避坑指南
你以为连接池配好了?那些年我踩过的坑,够你吃一壶的了
🦞 当 AI 开始”整顿职场”,我决定先整顿它的噱头
OpenClaw 使用经验分享:一个AI助手能有多能打?
OpenClaw 使用经验分享:一个AI助手能有多能打?
为什么你的Prompt总是差点意思?可能是你不懂AI的”脑回路”

发布评论