Day 13 / 共 20 天 · 第 3 周 插件系统

插件配置合并

Day 04/08 说过路由可引用 service/consumer 复用配置。今天看这个"合并"具体怎么做:merge_service_route / merge_consumer_route 的规则、优先级、以及合并结果如何缓存以避免每请求重算。

📍 你在整条链的位置(第 3 周 插件系统)
run_plugin 执行 D12 配置合并(分层覆盖) 鉴权/consumer D14 典型插件 D15
L01

为什么要合并

🤔 痛点:100 条路由都要开同一套"限流 + 监控" 难道每条路由都把这套插件配置抄一遍?改一次限流阈值就要改 100 处,漏改一处就出 bug。太痛苦了。
💡 本质:分层配置 + 运行时合并 配置合并就像公司报销制度:公司有默认规则(service)、部门可以覆盖(route)、个人还有特批(consumer)。真报销时把这几层"叠加"算出"你这单按什么规则报"。APISIX 运行时同理——把 service / route / consumer 几层合并成"这个请求的最终配置"。100 条路由共享一个 service 的配置,改 service 一处,100 条全变。
"复用 + 定制"的矛盾靠合并解决 你想复用(100 条路由共享一个 service 的配置),又想定制(某条路由单独加个插件、某个 consumer 有专属限流)。这就需要"分层配置 + 合并":service 是基础层、route 覆盖它、consumer 再覆盖。运行时把这些层合并成一份"当前请求的最终配置"。合并的核心问题是:同一个插件在多层都配了,用谁的?——答案是"越具体的层优先"(下面细讲)。
L02

merge_service_route

plugin.lua:647-656:把 service 的配置合并进 route。Day 04 access 阶段调它。

function _M.merge_service_route(service_conf, route_conf)
    local route_service_key = route_conf.value.id .. "#"
        .. route_conf.modifiedIndex .. "#" .. service_conf.modifiedIndex
    return merged_route(route_service_key, service_conf,
                        merge_service_route, service_conf, route_conf)
end
读法:route_service_key 是缓存键(下一节)——由 route id + 两者的 modifiedIndex(etcd 版本)拼成。merged_route 用这个 key 缓存合并结果。真正的合并逻辑在内部的 merge_service_route 函数里。
L03

合并缓存

merged_route 用 lrucache 缓存合并结果——同一个 (route, service) 组合只合并一次,之后直接取缓存。

合并不能每请求都做 合并涉及深拷贝、遍历插件——如果每个请求都合并一次,性能损失大。所以缓存:第一次请求合并并缓存,后续相同 (route,service) 直接用缓存结果。缓存键带 modifiedIndex(版本号)——route 或 service 一改,版本变、缓存键变、自动重新合并。"缓存 + 版本号失效"是 APISIX 里反复出现的模式(Day 05 变量缓存、Day 09 schema 编译缓存)——用版本号保证缓存和源数据一致。
L04

合并规则

下层只填补上层没有的;同名插件冲突时,越具体的层赢 Service(通用底座):limit-count(100) · prometheus Route(本路由):limit-count(20) Consumer(alice 专属):limit-count(5) 最终配置(alice 的请求): limit-count(5) ← consumer 赢 · prometheus ← service 补
三层叠加:limit-count 三层都配了 → consumer(5) 最具体赢;prometheus 只有 service 配 → 补充进来。

merge_service_stream_routeplugin.lua:659-686,规则清晰)体会合并思路:

local new_conf = core.table.deepcopy(route_conf)   -- 以 route 为基础
if service_conf.value.plugins then
    for name, conf in pairs(service_conf.value.plugins) do
        if not new_conf.value.plugins[name] then
            new_conf.value.plugins[name] = conf   -- ★ route 没配的插件,才用 service 的
        end
    end
end
if not new_conf.value.upstream and service_conf.value.upstream then
    new_conf.value.upstream = service_conf.value.upstream  -- route 没上游才用 service 的
end
核心规则:route 优先,service 补充 以 route 为基础,service 只"填补 route 没有的"——route 配了 limit-count 就用 route 的,route 没配 service 有就用 service 的。上游同理。这就是"具体覆盖通用":route(具体)优先于 service(通用)。注意是"插件级"覆盖(按插件名),不是"整个 plugins 字段"覆盖——两边的插件会合并到一起,冲突时 route 赢。
📝 举个例子 service 配 limit-count(count=100) + prometheus;route 自己配 limit-count(count=20)
合并 → limit-countroute 的 20(同名冲突 route 赢)、prometheusservice 的(route 没配、补进来)。最终这条路由跑 limit-count(20) + prometheus

👶 小白:consumer 优先级最高,那我在 route 上配的限流不就白配了?

👨‍🏫 老师:不会。是按插件名覆盖——只有 consumer 也配了同一个插件(比如两边都配 limit-count),才用 consumer 的;route 上那些 consumer 没配的插件照样生效。合并是"叠加 + 同名冲突时具体层赢",不是"整包替换"。

L05

merge_consumer_route

plugin.lua:741:识别出 consumer 后,把 consumer(和 consumer group)的插件配置合并进 route。返回 route, changed——changed 表示合并后是否有变化(Day 04 据此决定要不要补跑 rewrite)。

consumer 的配置优先级最高 consumer 是"针对具体调用方"的定制——比 route/service 更具体,所以优先级最高。给 alice 单独配的限流会覆盖 route 上的通用限流。合并后如果插件集变了(changed),Day 04 会重新 filter + 补跑 rewrite_in_consumer(Day 12)。consumer group 是"一组 consumer 共享的配置",介于 consumer 和 route 之间。
L06

优先级链

最终配置 = 合并(从低到高优先级覆盖):
  Plugin Config(可复用插件套餐)
    < Service(插件+上游套餐)
      < Route(本路由)
        < Consumer Group(消费者组)
          < Consumer(具体调用方,最高优先级)
读法:越往右越具体、优先级越高、越后合并(覆盖前面的)。这是 APISIX 配置系统的"层叠"模型——像 CSS 的选择器优先级、面向对象的方法覆盖。理解这条链,你就知道"为什么我在 consumer 配的限流生效了、route 的没生效"。Day 04 的合并顺序正是按这条链走的。
L07

conf_version 缓存键

Day 04 里合并 service 后设 api_ctx.conf_version = route.modifiedIndex .. "&" .. service.modifiedIndexplugin.conf_version(conf)plugin.lua:876)用它做各种缓存的键。

版本号是缓存一致性的钥匙 合并结果、编译的插件配置、picker 等都以 conf_version 为缓存键。只要 route 或 service 在 etcd 里改了,modifiedIndex 变、conf_version 变、所有相关缓存自动失效重建。这保证了"改配置立即生效"——热更新(Day 07)+ 版本化缓存的组合拳。不用手动清缓存,版本号一变全自动。这是 APISIX 又快又动态的深层机制。
⚠️ 常见误解:小白常以为"合并结果被缓存了,那我改 consumer 配置岂不是要等缓存过期才生效"。其实缓存键里带了 modifiedIndex——你一改配置,etcd 的 modifiedIndex 就变,缓存键跟着变,等于自动换了一把新钥匙,旧缓存直接作废、立刻重新合并。缓存快 + 版本号保证不脏,两全。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 为什么要合并配置?解决什么矛盾?
  • 合并的核心规则(route 优先、service 补充)?
  • 优先级链从低到高是什么?consumer 为什么最高?
  • 合并结果怎么缓存?conf_version 怎么保证一致性?

✋ 动手

cd /Users/bitmart/work/codes/github/apisix
sed -n '647,690p' apisix/plugin.lua
sed -n '741,800p' apisix/plugin.lua
grep -n "conf_version\|modifiedIndex" apisix/init.lua | head
明天预告 · Day 14鉴权与 consumer——鉴权插件(key-auth)怎么在 rewrite 阶段识别出 consumer、find_consumer 怎么按 key 查找、attach_consumer 怎么把身份挂到 ctx 上供后续用。
← Day 12 Day 14 · 鉴权 →