全栈工程师开发手册 (作者:栾鹏)
架构系列文章
Kong 的插件使用了一个叫 Classic 的 class 机制。所有的插件都是从 base_plugin.lua 基类上继承而来。base_plugin.lua 定义了插件在各个阶段被执行的方法名:
每个Nginx worker 进程启动时执行。
function BasePlugin:init_worker()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": init_worker")
end
在SSL握手的SSL证书服务阶段执行。
function BasePlugin:certificate()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": certificate")
end
在作为重写阶段处理程序从客户端接收时针对每个请求执行。
function BasePlugin:rewrite()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": rewrite")
end
针对客户端的每个请求执行,并在代理上游服务之前执行。
function BasePlugin:access()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": access")
end
从上游服务接收到所有响应头字节时执行。
function BasePlugin:header_filter()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": header_filter")
end
从上游服务接收到的响应体的每个块执行。由于响应被流回到客户端,所以它可以超过缓冲区大小,并且通过块被流传输块。因此如果响应大,可以多次调用该方法。
function BasePlugin:body_filter()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": body_filter")
end
最后一个响应字节发送到客户端时被执行。
function BasePlugin:log()
ngx_log(DEBUG, "executing plugin \"", self._name, "\": log")
end
根据方法名也可以看出,这 7 个方法对应于 OpenResty 的不同执行阶段。也就是说插件只能对外暴露出这 7 个方法名中的一个或多个才能被 Kong 的插件机制执行,接下来 Kong 会在 OpenResty 不同的执行阶段,执行插件对应的方法。
自定义插件:
文件结构
Kong 插件的文件结构分基本插件模块和完整插件模块两种,基本插件模块结构如下:
simple-plugin
├── handler.lua
└── schema.lua
其中,handler.lua 是插件核心,它是一个接口实现,其中每个函数将在请求生命周期中的期望时刻运行。schema.lua 用于定义插件配置
完整插件模块结构如下:
complete-plugin
├── api.lua
├── daos.lua
├── handler.lua
├── migrations
│ ├── cassandra.lua
│ └── postgres.lua
└── schema.lua
其各个模块的功能如下:
Module name | Required | Description |
api.lua | No | 插件需要向 Admin API 暴露接口时使用 |
daos.lua | No | 数据层相关,当插件需要访问数据库时配置 |
handler.lua | Yes | 插件的主要逻辑,这个将会被 Kong 在不同阶段执行其对应的 handler |
migrations/*.lua | No | 插件依赖的数据表结构,启用了 daos.lua 时需要定义 |
schema.lua | Yes | 插件的配置参数定义,主要用于 Kong 参数验证 |
其中,api.lua 定义管理API操作接口;daos.lua 定义插件需要并且存储在数据库的实体的DAOs列表;migrations/*.lua 定义了给定数据存储的相应迁移,通常只有当插件必须在数据库中存储自定义实体并通过daos.lua定义的DAO进行交互时,迁移才是必要的。
其中 handler.lua 和 schema.lua 是必需的,上面提到的插件需要暴露出来的方法就定义在 handler.lua 中。
具体关于文件结构的描述参见Plugin Development - File Structure
逻辑实现
这里以request-termination熔断为例,这是一个最简单的示例.在kong/kong/plugins/request-termination文件夹里面.
该检查就是为选定的服务或路由返回指定的响应消息.响应消息包含状态码status_code, 消息类型content_type,文本消息message,消息体body
逻辑实现
实现逻辑在handler.lua中实现
-- 执行函数. 按照配置返回固定的响应
-- 引入模块(引入基类)
local BasePlugin = require "kong.plugins.base_plugin"
local singletons = require "kong.singletons"
local constants = require "kong.constants"
local meta = require "kong.meta"
-- 局部变量
local kong = kong
local server_header = meta._SERVER_TOKENS
-- 默认的response
local DEFAULT_RESPONSE = {
[401] = "Unauthorized",
[404] = "Not found",
[405] = "Method not allowed",
[500] = "An unexpected error occurred",
[502] = "Bad Gateway",
[503] = "Service unavailable",
}
-- 扩展模块(派生子类),其实这里是为了继承来自 Classic 的 __call 元方法,方便 Kong 在 init 阶段预加载插件的时候执行构造函数 new()
local RequestTerminationHandler = BasePlugin:extend()
-- 设置插件的优先级,Kong 将按照插件的优先级来确定其执行顺序(越大越优先)
-- 需要注意的是应用于 Consumer 的插件因为依赖于 Auth,所以 Auth 类插件优先级普遍比较高
RequestTerminationHandler.PRIORITY = 2
RequestTerminationHandler.VERSION = "1.0.0"
-- 插件的构造函数,用于初始化插件的 _name 属性,后面会根据这个属性打印插件名
-- 其实这个方法不是必须的,只是用于插件调试
function RequestTerminationHandler:new()
RequestTerminationHandler.super.new(self, "request-termination")
end
-- 表明需要在 access 阶段执行此插件. 也就是在接入上游服务前就直接生成响应数据.
function RequestTerminationHandler:access(conf) -- conf就是schema.lua中的config,也就是插件安装时的配置页面
-- 执行父类的 access 方法,其实就是为了调试时输出日志用的
RequestTerminationHandler.super.access(self)
-- 接下来的就是插件的主要逻辑
local status = conf.status_code
local content = conf.body
-- 如果配置了content参数
if content then
local headers = {
["Content-Type"] = conf.content_type
}
if singletons.configuration.enabled_headers[constants.HEADERS.SERVER] then
headers[constants.HEADERS.SERVER] = server_header
end
return kong.response.exit(status, content, headers)
end
-- 如果没有配置content参数,就直接生成message的消息体
return kong.response.exit(status, { message = conf.message or DEFAULT_RESPONSE[status] })
end
return RequestTerminationHandler
参数定义
Kong 插件通过schema.lua文件定义配置。类似于 JSON Schema,主要用于描述插件参数的数据格式。
schema.lua 返回一个Table类型,包含no_consumer、fields、self_check三个属性:
属性名 | Lua 类型 | 默认值 | 描述 |
no_consumer | Boolean | false | 如果为true将不能应用此插件至指定消费者,只能被应用到 Services 或者 Routes, 例如:认证插件 |
fileds | Table | {} | 插件的 schema,使用一个键值对定义可用属性和他们的规则 |
self_check | Function | nil | 如果在接受插件配置之前需要进行自定义验证,需要实现此函数 |
schema.lua 文件样本如下:
--主要用于描述插件参数的数据格式.
-- bashboard页面上创建时需要填写的内容和添加插件时进行的校验
-- 引入模块,赋值table给typedefs变量
local typedefs = require "kong.db.schema.typedefs"
-- 自定义局部函数
local is_present = function(v)
return type(v) == "string" and #v > 0 -- # 表示取长度
end
return {
-- 插件名称
name = "request-termination",
fields = {
{ run_on = typedefs.run_on_first },
{ config = {
type = "record",
-- 描述插件参数的数据格式,用于 Kong 验证参数
fields = {
{ status_code = {
type = "integer",
default = 503,
between = { 100, 599 },
}, },
{ message = { type = "string" }, },
{ content_type = { type = "string" }, },
{ body = { type = "string" }, },
},
-- 自定义更为细粒度的参数校验
custom_validator = function(config)
if is_present(config.message) and (is_present(config.content_type) or is_present(config.body))
then
return nil, "message cannot be used with content_type or body"
end
if is_present(config.content_type) and not is_present(config.body)
then
return nil, "content_type requires a body"
end
return true
end,
},
},
},
}
修改自带插件(prometheus插件为例)
这里以prometheus插件为例, 因为kong自带的prometheus插件支持的度量比较少,我们需要增加一些度量内容,
kong自带的插件在/usr/local/share/lua/5.1/kong/plugins/prometheus目录下. 我们将该文件夹copy出来,
api.lua和serv.lua文件中定义了暴露的接口, 接口中将匹配/metrics是返回收集的度量数据.
schema.lua定义的启动插件时的参数检查, 其实启动这个参数不需要任何参数, 所以这个文件的内容很少.
prometheus.lua文件中定义了基础类Metric和派生类Counter,Gauge,Histogram以及类的方法和属性
handler.lua文件定义了各阶段执行的函数, 分别调用的是exporter.lua文件中定义的各个函数.
exporter.lua文件中主要报错处理函数, 主要为init定义度量的函数, log 设置度量的值的函数 collect 搜索度量返回的函数.
我们关心的是log函数,这个函数是message为参数, 如果需要我们可以添加config也作为参数, 那就需要在调用这个函数的时候将conf参数传入.
message包含了我们需要的所有内容, 下面是他的消息体格式
message{
latencies{
request 103
kong 99
proxy 3
}
service{
host license-service.cloudai-2
created_at 1547185659
connect_timeout 60000
id 2641f0cb-c604-48e2-9d5a-0dbe13fd5274
protocol http
name license
read_timeout 60000
port 8080
updated_at 1547185659
retries 5
write_timeout 60000
}
request{
querystring{
}
size 351
uri /license/sign
url http://192.168.11.127:8443/license/sign
headers{
host 192.168.11.127:32443
content-type application/json
postman-token 00f49660-5920-432d-adcc-e98721e27e8b
accept */*
x-b3-parentspanid 0f64da2e24dd4ac6
cache-control no-cache
content-length 57
accept-encoding gzip, deflate
user-agent PostmanRuntime/7.4.0
x-b3-traceid 2aa7eebf34ea4f44ea201f10ff36da94
x-lantern-version 5.2.0
x-b3-spanid 096d7f0b78a0926a
x-b3-sampled 1
}
method POST
}
tries{
{
balancer_latency 0
port 8080
balancer_start 1547452777442
ip 10.233.56.89
}
}
client_ip 10.233.65.0
api{
}
upstream_uri /sign
response{
headers{
content-type application/json; charset=utf-8
date Mon, 14 Jan 2019 07:59:37 GMT
connection close
x-ratelimit-limit-second 1
via kong/0.14.1
x-kong-proxy-latency 100
content-length 225
x-kong-upstream-latency 3
x-ratelimit-remaining-second 0
server Python/3.6 aiohttp/3.5.1
cookie --cookie aicloud-cookie=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ2ZXNpb25ib29rIiwiaWF0IjoxNTQ3NDUyNzc3LCJuYmYiOjE1NDc0NTI3NzcsImV4cCI6MTU0NzQ1NjM3N30.fpaSSLuh7TSN9igMzMyIMLXPeAXo3aYBNhq67i3UV2E
access-control-allow-methods GET,POST
access-control-allow-origin *
}
status 200
size 824
}
route{
created_at 1547185719
strip_path true
hosts{
}
preserve_host false
regex_priority 0
updated_at 1547185719
paths{
1 /license
}
service{
id 2641f0cb-c604-48e2-9d5a-0dbe13fd5274
}
methods{
1 GET
2 POST
}
protocols{
1 http
}
id 24be26d7-40e1-487d-9f84-8cca937a02b6
}
consumer{
custom_id vesionbook
created_at 1547185392
username vesionbook
id 711171ba-817e-4a9a-8b93-2c089e5a52b0
}
}
我们按照官方的样子,模拟就可以添加自己的度量了.
修改rate-limiting插件 添加限速同时能指定黑白名单
有时我们需要对黑白名单进行限速, 而官方的插件中黑白名单, 为 完全拒绝访问的形式, 而 限速插件中, 又统一对所有的选定访问客户端, 但是有时我们想对某些ip进行限速. 因此我们需要一个有黑白名单的限速插件.
这个以后有时间再弄吧