Posts for: #L7Gateway

网关的限流方案

Note:本文由 Google Gemini DeepResearch 生成。

1.网关限流服务的战略必要性与流量控制架构

1.1. 网关在流量管理中的控制点角色与核心必要性

API 网关在现代微服务架构中扮演着至关重要的流量控制点角色,负责管理北向(Ingress)和南向(Egress)的流量。对流量实施限制(即限流)是保障上游服务稳定性、维持系统可用性和实现资源公平分配的核心防御机制。

限流服务的战略必要性主要体现在以下几个方面:

首先,限流是防止系统过载和雪崩效应的关键。当面临突发流量高峰时,如不加限制,后台服务器可能因资源耗尽而被压垮。通过限制客户端在指定时间窗内的请求次数,网关可以有效地保护应用编程接口(API)及其背后的微服务。其次,限流是抵御恶意攻击的有效手段,尤其是分布式拒绝服务攻击(DDoS)或防止未受限制的网络爬虫过度消耗计算资源。如果任由客户端无限制地访问上游服务,系统的可用性将受到负面影响。

从架构设计的角度看,网关作为请求进入系统的唯一入口,是实施全局限流策略的最佳位置,可以根据 IP、API 密钥、特定消费者(Consumer)或其他定制标准来应用限流规则。

1.2. 限流策略的粒度、层次结构与定制化响应

在实际应用中,灵活的限流服务需要支持精细的粒度和层次化配置模型,以满足不同业务和资源保护的需求。这种配置的优先级机制体现了一种 “微观精确控制”优于“宏观默认保护” 的设计哲学。

网关通常支持多层次的限流配置,包括:

  1. 特定 API 或消费者限流: 这是优先级最高的限流配置。例如,用户可以对单个关键 API 进行限流设置,或者对特定的消费者群体施加限制。这种配置会覆盖所有默认设置。这意味着在限流值冲突时,系统优先执行最明确定义或最严格的限制,确保关键资源的防护级别不受宏观策略的稀释。
  2. API 默认限流: 作用于应用下大部分 API 的通用配置。当没有针对单个 API 配置限流值时,将采用此默认值。
  3. 应用总限流 (App Total Limit): 这是对当前应用下所有 API 请求的总和设置的上限。应用总限流是防止资源被“虹吸”的最后防线。即使单个 API 的限流都没有达到,一旦应用的总请求量超出限流总和值,所有请求都将被限制。这种设计解决了在微服务架构中,攻击者通过对大量低 QPS API 进行频繁调用,最终耗尽整个应用共享资源预算的“长尾攻击”问题。

网关在触发限流时,必须向客户端返回明确的、用户友好的响应。例如,默认的限流响应可能包含 {"resultStatus":1002, "tips":"顾客太多,客官请稍候"} 。为了实现友好的降级体验,网关需要支持定制化的响应格式,允许用户配置 JSON 格式的返回内容,如通过 resultStatus 字段(例如,1002 表示限流)和 tips 字段向用户提供定制化的限流提示。

2. 常见限流算法的原理、适用性与分布式性能剖析

选择合适的限流算法是构建高性能网关服务的基石。不同的算法在精确度、突发处理能力和分布式系统中的扩展性方面存在显著差异。

2.1. 固定窗口计数器 (Fixed Window Counter, FWC)

固定窗口计数器是最简单、资源消耗最低的限流算法。它在一个固定的时间周期内(例如每 60 秒)累积请求计数,当周期结束时,计数器重置。

优势与局限性: FWC 的实现非常简单,查询和递增操作计算复杂度为 $O(1)$,因此延迟极低。然而,其主要局限在于边界突发问题。如果客户端在窗口结束前的一刻发出大量请求,并在下一个窗口开始后的第一时间再次发出同等量的请求,系统在短时间内实际放行的请求量可能高达阈值的两倍。这使得 FWC 难以应对流量的瞬间爆发,可能导致上游服务过载。

APISIX 插件体系

背景知识

APISIX 基于 Openresty 和 Nginx 开发,了解 APISIX 的插件体系也有必要先了解下 Nginx 和 Openresty 的基础知识。

Nginx 请求处理阶段

Nginx 将一个 HTTP 请求的处理过程精心划分为一系列有序的阶段(Phases),就像一个工厂的流水线。每个阶段都有特定的任务,不同的模块可以将自己的处理程序(handler)注册到感兴趣的阶段。这种设计使得 Nginx 的功能高度模块化、可扩展,并且处理流程非常清晰高效。

一个请求在 Nginx 中主要会经过以下 11 个处理阶段

1. NGX_HTTP_POST_READ_PHASE (请求读取后阶段)

  • 任务: 接收到完整的请求头(Request Header)之后,第一个被执行的阶段。
  • 常用模块ngx_http_realip_module
  • 作用举例: 在这个阶段,realip 模块会根据 X-Forwarded-For 或 X-Real-IP 等请求头,将客户端的真实 IP 地址替换掉代理服务器的 IP 地址。这对后续的访问控制和日志记录至关重要。

2. NGX_HTTP_SERVER_REWRITE_PHASE (Server 级别地址重写阶段)

  • 任务: 在 server 配置块中执行 URL 重写。
  • 常用模块/指令rewrite 指令(当它定义在 server 块中时)。
  • 作用举例: 在请求进入具体的 location 匹配之前,对 URL 进行全局的、初步的改写。例如,将所有 http 请求强制重定向到 https
server {
    listen 80;
    server_name example.com;
    # 这个 rewrite 就工作在 SERVER_REWRITE 阶段
    rewrite ^/(.*)$ https://example.com/$1 permanent; 
}

3. NGX_HTTP_FIND_CONFIG_PHASE (配置查找阶段)

  • 任务: 根据上个阶段处理完的 URI,查找并匹配对应的 location 配置块。
  • 核心功能: 这是 Nginx 路由的核心。Nginx 会用请求的 URI 与 location 指令定义的规则进行匹配,找到最合适的 location。这个阶段没有模块可以注册 handler,是 Nginx 核心自己完成的。

4. NGX_HTTP_REWRITE_PHASE (Location 级别地址重写阶段)

  • 任务: 在上一步匹配到的 location 块内部,执行 URL 重写。
  • 常用模块/指令rewrite 指令(当它定义在 location 块中时)、set 指令。
  • 作用举例: 对特定 location 的请求进行更精细的 URL 改写。这个阶段的 rewrite 可能会导致 Nginx 重新回到 FIND_CONFIG 阶段去匹配新的 location,可能会有循环,最多执行 10 次以防止死循环。
location /app/ {
    # 这个 rewrite 工作在 REWRITE 阶段
    rewrite ^/app/(.*)$ /v2/app/$1 break; 
}

5. NGX_HTTP_POST_REWRITE_PHASE (地址重写后阶段)

  • 任务: 防止 rewrite 阶段的重写指令导致死循环。如果 URI 在上一个阶段被重写,这个阶段会把重写后的 URI 交给 Nginx,让其重新开始 FIND_CONFIG 阶段,查找新的 location。这是一个内部阶段,用户通常不直接感知。

6. NGX_HTTP_PREACCESS_PHASE (访问权限控制前置阶段)

  • 任务: 在正式的访问权限检查之前,做一些准备工作。
  • 常用模块ngx_http_limit_conn_module (连接数限制), ngx_http_limit_req_module (请求速率限制)。
  • 作用举例: 在检查用户名密码之前,先检查客户端的请求速率是否过快,如果过快,直接拒绝,不必再执行后面的阶段。

7. NGX_HTTP_ACCESS_PHASE (访问权限控制阶段)

  • 任务: 对请求进行权限验证。
  • 常用模块/指令ngx_http_access_module (allowdeny), ngx_http_auth_basic_module (HTTP 基本认证)。
  • 作用举例: 检查客户端 IP 是否在允许列表中,或者验证用户提供的用户名和密码是否正确。如果这个阶段有任何模块拒绝了请求,处理就会立即中断并返回错误(如 403 Forbidden)。

8. NGX_HTTP_POST_ACCESS_PHASE (访问权限控制后置阶段)

  • 任务: 主要用于配合 ACCESS 阶段的 satisfy 指令。
  • 作用举例: 当 satisfy any; 被使用时,如果 ACCESS 阶段有模块允许了访问(例如 IP 匹配成功),POST_ACCESS 阶段的处理就可以被跳过。如果 ACCESS 阶段没有明确允许,这个阶段的处理(例如 HTTP 基本认证)就会被执行。

9. NGX_HTTP_TRY_FILES_PHASE (Try_files 阶段)

  • 任务try_files 指令专属的特殊阶段。
  • 常用模块/指令try_files
  • 作用举例: 按顺序检查文件或目录是否存在,如果找到,则内部重定向到该文件,如果都找不到,则执行最后一个参数(通常是内部重定向到一个 location 或返回一个状态码)。
location / {
    # 这个指令工作在 TRY_FILES 阶段
    try_files $uri $uri/ /index.html;
}

10. NGX_HTTP_CONTENT_PHASE (内容生成阶段)

  • 任务核心阶段,负责生成最终的响应内容并发送给客户端。
  • 重要特性: 每个 location 只有一个模块能成为“内容处理模块”(Content Handler)。
  • 常用模块:
    • ngx_http_static_module: 处理静态文件。
    • ngx_http_proxy_module: 将请求反向代理到后端服务器 (proxy_pass)。
    • ngx_http_fastcgi_module: 将请求转发给 FastCGI 应用(如 PHP-FPM)。
    • ngx_http_index_module: 处理目录索引文件。
    • return: 直接返回指定的状态码或内容。
  • 执行逻辑: Nginx 会调用在 location 中找到的内容处理模块。例如,如果配置了 proxy_pass,那么 proxy_module 就会接管请求。如果没有特定的内容处理模块,默认会由 static_module 尝试提供静态文件服务。

11. NGX_HTTP_LOG_PHASE (日志记录阶段)

  • 任务: 请求处理完成后,记录访问日志。
  • 常用模块/指令ngx_http_log_module (access_log)。
  • 作用举-例: 无论请求成功还是失败,这个阶段都会被执行(除非有严重错误),用于将请求的相关信息(如客户端IP、请求时间、状态码等)写入到日志文件中。

过滤器模块 (Filter Modules)

CONTENT 阶段生成了响应内容后,在发送给客户端之前,响应数据还会经过一系列的 过滤器(Filter) 链处理。过滤器负责对响应头和响应体进行修改或加工。

APISIX balancer 支持 domain

最近在做网关开发时遇到了一个场景:业务在做多实例部署时需要通过 Ingress 将服务注册给 APISIX,由于每个实例属于一个单独的 namespace,导致在 APISIX 和 Ingress 上出现流量覆盖的问题:

  1. Ingress 通过 Host 和 URI 匹配入向流量,同一个服务的 Host 和 URI 是完全一致的(Host 用 ip:port 表示),因此注册多个 Ingress 流量只会引流到一个实例上。
  2. APISIX 侧在 balance 阶段对于相同 ip:port 的 upstream node 会做去重,导致跨集群多实例时流量分配并不符合预期。

举个例子,有 A,B 两个集群,A 集群部署了 2 个实例,B 集群部署了一个实例。预期这 3 个实例应该平均分配流量(假设采用 rr),A 集群和 B 集群流量比例应该是 2:1,但实际上 A 集群和 B 集群在 APISIX 分配时流量是 1:1,而在 A 集群内一个实例占据了全部流量。

从业务上来说这个可以通过重新规划 namespace 和实例的关系来解决。同一个集群同一个服务的所有实例分布在同一个 namespace 中。这样注册的 Ingress 和 APISIX upstream node 就只会有一个。

抛开这个点,我们来看看从网关的角度如何解决这两个问题。

第一个问题比较好解决,每个注册的实例采用独立的 Domain,Ingress 通过 Host Header 来区分流量。在 APISIX 采用域名注册,域名解析到 Ingress 地址。如果域名没法解析(我们遇到的场景),也可以将 Domain 信息放到 node 的 metadata 内,写一个插件将其注入到 node.domain 内来完成。