概述
插件可以认为是 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 的插件
- 仅仅应用于 API 的插件 →
api
-
应用于 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 插件,显然是不符合需求的。
- 仅仅应用于 API 的插件 →
- 应用于 Consumer 的插件 →
consumer
- 应用于 API 的插件
这四种方式插件的组合将伴随在 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 & consumer
、consumer
、api
、GLOBAL
插件并执行其access
方法;否则将直接检查自己是否属于api
、GLOBAL
插件。 -
filter 循环
经过上面两个阶段之后,就已经完成了插件生效策略的筛选。当前请求应该被执行的插件已经确定,并被缓存在自身中,并随着生命周期的结束而被销毁。当然这是一步很重的操作。不过也从这个阶段开始,Kong 在遍历所有插件时将直接从上面的缓存中查找,并执行相应的
filter
方法,而不再经过生效策略的筛选,这当然也是出于性能上的考量。
结语
通过理解上面概念,我现在来回答这个终极问题:到底是 LOCAL
插件先被执行,还是 GLOBAL
插件先被执行?
答案是:乱序的。因为插件的执行顺序由插件自身的优先级唯一确定。注意这里需要和插件的生效策略区分开来,后者的生效顺序总是 LOCAL
优先于 GLOBAL
。
我曾经和 Kong 的技术人员聊过,他说目前社区反映 Kong 在长时间运行之后,内存碎片严重,我相信这里少不了插件的生效策略的锅。