带你玩玩接口网关
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." .. nameif is_stream_plugin thenpkg_name = "apisix.stream.plugins." .. nameendpkg_loaded[pkg_name] = nillocal ok, plugin = pcall(require, pkg_name)if not ok thencore.log.error("failed to load plugin [", name, "] err: ", plugin)returnendif not plugin.priority thencore.log.error("invalid plugin [", name,"], missing field: priority")returnendif not plugin.version thencore.log.error("invalid plugin [", name, "] missing field: version")returnendplugin.name = namecore.table.insert(plugins_list, plugin)if plugin.init thenplugin.init()endreturnend
看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 thenreturn false, errendreturn trueend
从代码schema表中看出主要有3个配置:
1、type 声明校验的类型
2、propertise 校验参数列表,拿rate配置来讲,type限制参数类型,并且mininum限制最小值
3、Required 必传的参数
最后check_schema完成校验工作。
上面只是展示了jsoschema一小部分校验规则,实际上还支持更多校验方式。其思想无语言的边界,以一种规范形式替代纯粹的if-else。
第四、完成限流限速功能
function _M.rewrite(conf, ctx)-- TODOendfunction _M.access(conf, ctx)-- TODOendfunction _M.balancer(conf, ctx)-- TODOend
插件主要工作在两个阶段,分别是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)endfunction _M.access(conf, ctx)-- 实例化限流库并将对象缓存local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx,create_limit_obj, conf)if not lim thencore.log.error("failed to instantiate a resty.limit.req object: ", err)return 500endlocal key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_versioncore.log.info("limit key: ", key)-- 调用限流并返回是否触发local delay, err = lim:incoming(key, true)if not delay then-- 超过阈值,就返回指定的状态码if err == "rejected" thenreturn conf.rejected_codeendcore.log.error("failed to limit req: ", err)return 500end-- 达到并发数,缓冲队列延迟处理if delay >= 0.001 thensleep(delay)endend
核心代码主要有2个,实例化resty-limit-req对象完成调用并做对应处理,代码中我也做了相应的注释。除上述三个工作区外,还有一些不常用工作区。这些方法可以不去实现它,但在你需要时能让你写出更合理的插件逻辑。
function init()-- TODOendfunction header_filter(conf, ctx)-- TODOendfunction body_filter(conf, ctx)-- TODOendfunction log(conf, ctx)-- TODOend
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