1、API网关是什么?
API网关功能相对来讲比较丰富,可以让服务只关心业务本身;而和业务实现无关的功能,就可以在独立的网关层面来解决。比如:反向代理、动态负载均衡、服务熔断、API健康检查、动态上游、动态限流限速、动态SSL证书...等等。
功能如此强大那么可以拿来替代nginx吗?答案是可以,而且api网关还具备了nginx不具备的功能。
下面带你使用开源API网关apisix框架实现一个动态限流功能。
2、apisix框架
apisix是基于openresty+etcd实现的一个api网关框架。相比其他API网关kong,无论是功能、性能或代码二次开发优势明显。具体安装方式以及数据对比可以去github查阅。
2.1 apisix启动
安装完之后开启apisix,启动时主要处理了3个事情:
local openresty_args = [[openresty -p ]] .. apisix_home .. [[ -c ]]
.. apisix_home .. [[/conf/nginx.conf]]
function _M.start(...)
init(...)
init_etcd(...)
local cmd = openresty_args
-- print(cmd)
os.execute(cmd)
end
1、初始化Nginx配置文件并检查openresty版本
2、检查etcd可用性并初始化框架默认配置
3、启动openresty并挂载生成的Nginx配置
2.2 apisix插件
Apisix的插件是框架灵魂所在,对于开发限流功能来讲,只要实现限流插件,并绑定到对应的路由上,即可完成需求。
在开发限流插件前,我们先要了解apisix工作流。
工作流可分为4部分:
1、匹配路由,根据请求的方法、uri、host、请求头等条件,去路由规则中进行匹配。如果命中了某条路由规则,就会从 etcd 中获取对应的插件列表。
2、匹配插件,本地开启的插件列表进行交集,得到最终可以运行的插件列表。
3、执行插件,根据插件的优先级,逐个运行插件。
4、分发上游,根据上游的健康检查和负载均衡算法,把这个请求发送给上游。
通过API网关的工作流不难看出,主要的核心点在于3个,匹配路由规则、匹配插件规则以及负载均衡规则。而对于我们的业务实现,基本可以从路由重写跟开发插件,这两点出发去做对应的处理。
重点讲下apisix插件热载特性,个人认为这是面向开发比较好的特性。使用过openresty都知道,只要发布了lua代码后,都需要对应服务器上openresty -s reload。有热更热载特性后,发布代码变得更加灵活并且可以自定发布机制。我们来看是apisix是怎么做到的。
拿请求工作流举例,再匹配到插件后,会从插件hash表上获取对应插件的对象并执行对应阶段的方法(如:access阶段执行access方法),插件hash表最先是在apisix启动时生成的,后续更新插件可以通过提供的http接口热载入,从而不需要openresty reload。有了热载接口后,就可以将热载的动作做在后台维护热载或自动热载。
local function load_plugin(name, plugins_list, is_stream_plugin)
local pkg_name = "apisix.plugins." .. name
if is_stream_plugin then
pkg_name = "apisix.stream.plugins." .. name
end
pkg_loaded[pkg_name] = nil
local ok, plugin = pcall(require, pkg_name)
if not ok then
core.log.error("failed to load plugin [", name, "] err: ", plugin)
return
end
if not plugin.priority then
core.log.error("invalid plugin [", name,
"], missing field: priority")
return
end
if not plugin.version then
core.log.error("invalid plugin [", name, "] missing field: version")
return
end
plugin.name = name
core.table.insert(plugins_list, plugin)
if plugin.init then
plugin.init()
end
return
end
看apisix源码具体热载插件代码,主要有2个核心点
1、pcall动态调用插件,并且将加载记录删除pkg_loaded[pkg_name]=nill
2、按插件名写入hash表,调用时从hash表读取
2.3 apisix开发插件规则
前面对apisix基本工作流以及插件热载都有了解,下面就到具体开发。
nginx本身就具备了限速limit_req模块,网关层可以利用该模块特性完成动态限流限速,可分为四步骤去现实。
第一、后台配置项
1、rate:设置并发数阈值(单位:秒)
2、burst:设置缓冲队列数,达到并发阀值后延迟处理数量
3、key:设置限流维度,如:客户端IP,请求头,服务器ip...等
4、rejected_code:请求超过(rate+burst)后 ,请求的响应值
第二、注册插件
1、文件名跟插件名保持一致,分别对应路由以及hash表规则,存放路径apisix/plugins/limit-req.lua
2、将插件名称配置在conf/config.yaml的plugins字段下配置limit-req
3、请求热载插件,地址是
http://hosts/apisix/admin/plugins/reload -X PUT
第三、插件声明基本属性
local _M = {
version = 0.1,
priority = 1001,
name = plugin_name,
schema = schema,
}
1、version 声明插件版本
2、priority 执行插件时优先级
3、name 插件名称
4、schema 校验参数表
重点讲下schema,它的作用主要是使用json schema数据格式完成对参数校验,而插件中可以使用Lua table对插件参数进行校验。
格式如下:
local schema = {
type = "object",
properties = {
rate = {type = "number", minimum = 0},
burst = {type = "number", minimum = 0},
key = {type = "string",
enum = {"remote_addr", "server_addr", "http_x_real_ip",
"http_x_forwarded_for"},
},
rejected_code = {type = "integer", minimum = 200},
},
required = {"rate", "burst", "key", "rejected_code"}
}
function _M.check_schema(conf)
local ok, err = core.schema.check(schema, conf)
if not ok then
return false, err
end
return true
end
从代码schema表中看出主要有3个配置:
1、type 声明校验的类型
2、propertise 校验参数列表,拿rate配置来讲,type限制参数类型,并且mininum限制最小值
3、Required 必传的参数
最后check_schema完成校验工作。
上面只是展示了jsoschema一小部分校验规则,实际上还支持更多校验方式。其思想无语言的边界,以一种规范形式替代纯粹的if-else。
第四、完成限流限速功能
function _M.rewrite(conf, ctx)
-- TODO
end
function _M.access(conf, ctx)
-- TODO
end
function _M.balancer(conf, ctx)
-- TODO
end
插件主要工作在两个阶段,分别是access_by_lua_block阶段以及balancer_by_lua_block阶段,并调用阶段对应的方法以及传入配置项、共享变量。
其主要的作用:
1、access_by_lua_block阶段会执行rewrite与access 方法,两个方法本质上并没有太大区别,都是处理请求并实现插件逻辑。只是rewrite处理uri重写或api鉴权会比较直观 。
2、balancer_by_lua_block阶段执行balancer方法,在这里实现负载均衡规则。
结合每个工作区的意义,限流放在access阶段处理比较合适。
local function create_limit_obj(conf)
core.log.info("create new limit-req plugin instance")
-- 实例化限流库
return limit_req_new("plugin-limit-req", conf.rate, conf.burst)
end
function _M.access(conf, ctx)
-- 实例化限流库并将对象缓存
local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx,
create_limit_obj, conf)
if not lim then
core.log.error("failed to instantiate a resty.limit.req object: ", err)
return 500
end
local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
core.log.info("limit key: ", key)
-- 调用限流并返回是否触发
local delay, err = lim:incoming(key, true)
if not delay then
-- 超过阈值,就返回指定的状态码
if err == "rejected" then
return conf.rejected_code
end
core.log.error("failed to limit req: ", err)
return 500
end
-- 达到并发数,缓冲队列延迟处理
if delay >= 0.001 then
sleep(delay)
end
end
核心代码主要有2个,实例化resty-limit-req对象完成调用并做对应处理,代码中我也做了相应的注释。除上述三个工作区外,还有一些不常用工作区。这些方法可以不去实现它,但在你需要时能让你写出更合理的插件逻辑。
function init()
-- TODO
end
function header_filter(conf, ctx)
-- TODO
end
function body_filter(conf, ctx)
-- TODO
end
function log(conf, ctx)
-- TODO
end
1、init方法在插件热载后执行,可以对插件进行初始化操作。
2、header_filter_by_lua_block阶段执行header_filter方法, 处理响应头操作。
3、body_filter_by_lua_block阶段执行body_filter方法, 处理响应体操作。
4、log_by_lua_block阶段执行log方法,处理访问日志操作。
2.4 路由匹配规则
最后将限流插件绑定对应路由配置,再路由匹配成功后会执行插件。
路由匹配规则,它是多维度匹配规则并且支持模糊匹配,传统的key-value数据结构已无法满足当前需求,而使用关系型数据库则多一层依赖关系。
apisix用的是lua的FFI库实现调用C的radixtree,使用的是基数树数据结构来存储并进行匹配。
local radix = require("resty.radixtree")
local rx = radix.new({
{
paths = "/aa",
hosts = "foo.com",
method = {"GET", "POST"},
remote_addrs = "127.0.0.1",
metadata = "1",
},
{
paths = "/bb*",
hosts = {"*.bar.com", "gloo.com"},
method = {"GET", "POST", "PUT"},
remote_addrs = "fe80:fe80::/64",
vars = {"arg_name", "jack"},
metadata = "2",
}
})
ngx.say(rx:match("/aa", {
host = "foo.com",
method = "GET",
remote_addr = "127.0.0.1"
}))
从上面的代码例子中可以看出,resty-radixtree可以支持根据uri、host、method、参数、IP地址等多个维度进行匹配,并且匹配成功将返回metadata字段值,利用该特性将匹配项以及插件ID绑定。基数树的时间复杂度是O(K),性能比“遍历+hash缓存”的方式更加高效。
3、效果展示
为了方便效果呈现,我配置访问前端地址(/test)1秒处理1个请求并且缓冲1个,触发后返回503状态码。
这是利用apisix框架搭建的在线demo,有兴趣的同学可以去体验下。
demo后台:http://47.106.211.125:9080/apisix/dashboard
前端地址:http://47.106.211.125:9080/test
负载均衡分发至本机8001、8002、8003端口,8003端口间隔1分钟断开1次
THE END
编辑:chuanrui