上一篇我写的是,企业级限流别一上来就把所有东西堆在 Route 上。那篇先把入口防护和套餐层拆开了,但真往下配时,另一个问题很快会冒出来:套餐既然不放在 Route,上哪一层才顺手?租户身份挂哪?凭证又该怎么算?

这件事表面上像 APISIX 配置题,做久了你会发现不是。把人拖住的,往往是这些很具体的动作:新租户注册,要改几处对象;免费版升到付费版,要不要碰业务路由;同一个客户要多发一把 key,是复制 Consumer,还是只补一个凭证。

我后来会把这篇文章写出来,不是因为 key-authlimit-count 有多复杂,是因为多租户网关一旦往企业场景走,卡人的通常是对象模型。

先别急着谈插件参数

大部分教程喜欢先讲插件怎么配,这当然有用,只是一进真实业务,问题很快就会变成另外一种样子。

比如:

  • 新 API 上线时,路由里到底该带哪些东西,哪些又不该带。
  • 某个租户升级套餐时,变更应该落在身份对象,还是套餐对象。
  • 同一档套餐下,某个大客户只是多一把访问 key,这算身份变化还是套餐变化。

这些都是建模题,不是参数题。

如果对象边界一开始就糊了,后面再去谈“怎么治理配置”,大概率只是在给一套本来就缠在一起的东西继续补手工约定。

越补越乱。

我后来更愿意这样拆

我现在更倾向把 APISIX 里的几层对象按下面这个方式看:

Route

它负责 API 功能层。什么 URI、什么上游、开哪些认证插件,这些是它的工作。Route 可以带少量确实属于接口本身的规则,但不该变成租户策略和商业套餐的大仓库。

Consumer

它负责回答“是谁在调用”。认证通过以后,APISIX 才能知道当前请求属于哪个调用方,所以 Consumer 更像租户身份对象,而不是某条路由的附件。

Consumer Group

它负责一组 Consumer 共享的策略。免费版、付费版、企业版这类东西,本质上都是一组调用方共享同一套额度和插件配置,这正好就是 Consumer Group 擅长承载的层。

凭证管理

它负责凭证生命周期。一个租户不一定永远只有一个 key,更别说还有轮换、多个客户端、临时发证这些事。按我们当时那条时间线,更稳的做法不是硬把它写成 APISIX 现成对象,而是让租户服务统一管 key 的申请、轮换和同步,再把当前生效的认证配置回写到 Consumer。后面如果升级到支持 Credential 资源的版本,再把这一层显式拆出来会更顺。

这样看以后,套餐该挂哪层就不绕了——它更像 Consumer Group 这层共享策略。

画出来大概是这个关系:

flowchart TD
    Route["Route\n管 API:URI + 上游 + 认证插件"]
    Upstream["Upstream\n转发目标"]
    Consumer["Consumer\n租户身份"]
    CG["Consumer Group\n套餐策略:limit-count"]
    Cred["凭证管理流程\nkey 申请·轮换·同步"]

    Route -->|"转发"| Upstream
    Route -->|"key-auth 识别调用方"| Consumer
    Consumer -->|"group_id 归属套餐"| CG
    Cred -->|"回写认证配置"| Consumer

一个最小链路,差不多应该长这样

先让 Route 保持克制,只负责把认证和转发接起来:

curl -X PUT "http://127.0.0.1:9180/apisix/admin/routes/data_route" \
  -H "X-API-KEY: ${admin_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/api/v1/data",
    "plugins": {
      "key-auth": {}
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {
        "httpbin.org:80": 1
      }
    }
  }'

然后把套餐层放到 Consumer Group:

curl -X PUT "http://127.0.0.1:9180/apisix/admin/consumer_groups/paid_plan" \
  -H "X-API-KEY: ${admin_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "plugins": {
      "limit-count": {
        "count": 100000,
        "time_window": 3600,
        "rejected_code": 429,
        "policy": "redis",
        "redis_host": "127.0.0.1",
        "redis_port": 6379,
        "redis_password": "password",
        "redis_database": 0,
        "group": "paid_plan_quota"
      }
    }
  }'

接着给租户建 Consumer:

curl -X PUT "http://127.0.0.1:9180/apisix/admin/consumers" \
  -H "X-API-KEY: ${admin_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "tenant_enterprise_01",
    "group_id": "paid_plan"
  }'

如果只是最简单的场景,把 key 直接挂在 Consumer 上就能跑。再往前走,真正要补的是“凭证管理流程”,不是急着把它说成某个在当时已经现成可用的 APISIX 对象。对那条时间线来说,更现实的做法是:租户服务统一管理 key 的申请、轮换和同步,再把最终生效的认证配置回写到 Consumer;如果后续升级到支持 Credential 资源的版本,再把凭证从 Consumer 中单独拆出来。

还有一个经常会被省略的细节:套餐如果要表达多节点 APISIX 集群上的统一额度,这里的 limit-count 也不能继续吃默认 policy=local。默认 local 只在当前节点内存计数,示例里我直接写成 Redis 共享计数,是为了让“套餐额度”真的对应到整个网关集群,而不是每个节点各记各的。

这套链路里,每个对象的职责都比较单一:

  • Route 管 API。
  • Consumer 管身份。
  • 凭证管理流程管 key 的申请、轮换和同步。
  • Consumer Group 管套餐。

你会发现,很多原本说不清的事情一下子有了落点。客户升级套餐,改 group_id 就够了。凭证轮换,走租户服务同步认证配置就行。新增一个 API,不需要把历史套餐再复制一轮。

为什么我不想把这些对象都塞回一个 YAML 仓库

这里我不是说 GitOps 不好。相反,我觉得 Route 这层很适合跟服务发布绑在一起,走声明式配置、代码评审和 CI/CD。

不过 Consumer、凭证管理、Consumer Group 这一层,更像业务状态。

新租户注册、套餐升级、补发凭证,这些动作往往是后台操作或业务事件触发的。你硬把它们和 Route 一起装进一个大 YAML 仓库,早期会觉得统一,后面就会开始感到别扭:

  • 路由变更和租户状态变更混在一起,噪音很大。
  • 业务侧一有批量套餐调整,配置仓库会被状态型数据刷屏。
  • 本来只想换一把 key,最后却要走一遍和发布接口同等级别的流程。

我更认可的边界是:

  • Route 以及路由侧共享配置,继续按功能层治理。
  • Consumer、认证配置、group_id 这类租户对象,交给后台服务或租户管理流程驱动。

每个对象按自己的节奏变,管起来才不打架。

这一篇最后留下来的判断

我不太愿意把 APISIX 理解成“插件很多的网关”,原因就在这儿。企业场景里,决定这套东西后面会不会越来越乱的,不只是插件够不够,更关键的是对象边界有没有先立住。

如果套餐、身份、凭证、路由一开始就揉在一起,后面每次接新客户、升套餐、换 key,都会顺手把系统再缠紧一点。相反,只要这几层先拆开,很多动作就会自然收敛:

  • 套餐升级,不碰 Route。
  • 凭证轮换,不改套餐。
  • 新接口上线,不复制租户策略。

我最后把套餐从 Route 上拿下来,就是因为它不该替套餐层背这个模型。Route 能挂限流,但那不是套餐该待的地方。