Kong 插件加载机制概述

以请求的视角回顾自己的一生

Posted by qmsheng on May 14, 2018

概述

插件可以认为是 Kong 管理 API 的核心,其模块化和可扩张性做得很好,尤其是其灵活的加载机制使得 Kong 能够针对不同 API 启用、组合任意插件。Kong 默认自带的插件集,按照功能的不同大致可以分为六大类:Authentication 认证、Security 安全、Traffic Control 流量控制、Analytics & Monitoring 分析监控、Transformations 请求报文处理、Logging 日志等

无论是为了理解这些插件的工作原理,亦或者是定制开发属于自己的插件,熟悉插件的加载机制无疑都是一个关键的前提。

Kong 从 0.11.0 版本开始区分了社区版和商业版,节点之间的消息通信也改为了数据库轮训机制(原先是通过 serf 实现的),通过最终一致性实现了节点的无状态,任何时候节点只需连上数据库即可工作。之前的版本都相对来说太重,部署过于复杂。所以我这里将基于 Kong 0.12.3 版本分析其插件加载机制。

我一般研究一门新技术,倾向于研究更新更早期的代码。 因为非常成熟有名的代码往往已经过度设计,对于阅读代码入门不一定是好的选择。而一些出于项目早期的代码,倒是更容易阅读理解其核心原理。


1. 插件的应用方式

Kong 按照插件的不同应用方式,大致可以分为两大类四小类

  • 全局插件 → GLOBAL

    既不独自应用于 API,又不独自应用于 Consumer 的插件,而是应用于所有 API 和 Consumer 的插件。

  • 局部插件 → LOCAL

    • 应用于 API 的插件
      1. 仅仅应用于 API 的插件 → api
      2. 应用于 API 且指定 Consumer 的插件 → api & consumer

        特定用户且特定 API 需要执行的插件。这个貌似不太好理解,我这里来举个例子:

        假设现在有两个 API: /foo, /bar; 两个 Consumer: c1, c2
        
        如果想让 c1 在调用 /foo 时启用插件 rate-limit,这里只需要为 /foo 添加 rate-limit 插件并指定 c1 Consumer 即可。
        这里并不能单独为 c1 配置 consumer 插件,因为这样会使 c1 消费 /bar 时也调用 rate-limit 插件,显然是不符合需求的。
        
    • 应用于 Consumer 的插件 → consumer

这四种方式插件的组合将伴随在 API 请求响应生命周期不同阶段中逐个被执行。同时 Kong 也将严格约束这四种方式在启用插件时的行为。比如:同一种方式只能添加同一个插件一次、不同方式之间可以添加同一个插件

2. 插件的生效策略

所谓生效策略就是 Kong 组织上述提到的四种不同的插件应用方式的策略。结果是:API 最终要执行的插件等于 LOCAL 插件和 GLOBAL 插件的并集。也就是说:

API 最终要运行的插件 = api & consumer + consumer + api + GLOBAL = LOCAL + GLOBAL

但是这里还有一个问题没有解决,就是虽然在同一种方式上同一插件只能应用一次,但是由于有上述四种不同的插件应用方式的存在。那么完全可能有同一插件在不同方式上均应用的情况,比如:rate-limit 插件既应用于 consumer 上,又应用于 api 上,那么这时候哪个生效?

答案是:consumer 上的 rate-limit 生效。Kong 在处理上述四种方式插件冲突的优先级是:

注意:这里并不是插件的执行顺序,而是处理插件冲突的优先级

3. 插件的执行顺序

插件的执行顺序由插件自身的优先级唯一确定(既和插件应用的四种方式无关,也无关于插件的生效策略),其并不会随 API 的不同而改变。待确定插件执行的顺序之后,插件将随着 API 请求响应生命周期中的不同阶段逐个执行其相应的 hook

上图并不能视为插件的执行顺序,而是请求生命周期不同阶段的执行顺序,这里可以理解为插件的执行阶段。在不同的阶段中,插件均需按顺序执行其对应的 hook

值得一提的是,目前 Kong 默认自带的插件均运行在 access 以及之后的阶段。

4. 一个请求的一生

当一个请求在自己生命周期的不同阶段时,均需要按顺序(自身的优先级)遍历所有已安装插件(包括自己自定义的),以检查自己是否被启用(属于 GLOBAL 插件或者是 LOCAL 插件),并执行其对应的 hook。我把它称之为 「phase 循环」。

理解「phase 循环」对于掌握 Kong 插件机制至关重要!比如:

  • rewrite 循环

    当一个请求进入到 rewrite 阶段时,所有已安装插件(包括自己自定义的)将会按照顺序(自身的优先级)检查自己是否属于 GLOBAL 插件。如果属于则执行其 rewrite 方法,否则检查下一个插件,直到检查完全部插件为止。

  • access 循环

    接下来进入到 access 阶段,在这个阶段将完成插件生效策略的筛选。同样所有已安装插件继续按照顺序(当然还是自身的优先级)检查自己是否属于 api 插件,如果属于并且恰巧还是 auth 插件(auth 插件拥有较高的优先级执行都比较早),那么接下来将依次检查自己是否属于 api & consumerconsumerapiGLOBAL 插件并执行其 access 方法;否则将直接检查自己是否属于 apiGLOBAL 插件。

  • filter 循环

    经过上面两个阶段之后,就已经完成了插件生效策略的筛选。当前请求应该被执行的插件已经确定,并被缓存在自身中,并随着生命周期的结束而被销毁。当然这是一步很重的操作。不过也从这个阶段开始,Kong 在遍历所有插件时将直接从上面的缓存中查找,并执行相应的 filter 方法,而不再经过生效策略的筛选,这当然也是出于性能上的考量。

结语

通过理解上面概念,我现在来回答这个终极问题:到底是 LOCAL 插件先被执行,还是 GLOBAL 插件先被执行?

答案是:乱序的。因为插件的执行顺序由插件自身的优先级唯一确定。注意这里需要和插件的生效策略区分开来,后者的生效顺序总是 LOCAL 优先于 GLOBAL

我曾经和 Kong 的技术人员聊过,他说目前社区反映 Kong 在长时间运行之后,内存碎片严重,我相信这里少不了插件的生效策略的锅。