在介绍Kong之前,我们可以先来了解一下什么是API网关。
API网关
网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问(The role of a Gateway in anAPI architecture is to protect, enrich and control access to API services.)。
API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。
网关的职能
一般来说,API 网关有四大职能。
- 请求接入:作为所有 API 接口服务请求的接入点,管理所有的接入请求。
- 业务聚合:作为所有后端业务服务的聚合点,所有的业务服务都可以在这里被调用。
- 中介策略:实现安全、验证、路由、过滤、流控、缓存等策略,进行一些必要的中介处理。
- 统一管理:提供配置管理工具,对所有 API 服务的调用生命周期和相应的中介策略进行统一管理。
目前常见的开源网关大致上按照语言分类有如下几类。
例如基于openrestry的Kong、Apisix以及其他语言相关的spring cloud gateway、grpc-gateway等等,具体可以参考GitHub。
Kong网关
Kong是一个云原生、快速的、可扩展的、分布式的微服务抽象层(也称为API网关)框架。更确切地说,Kong是一个在Nginx中运行的Lua应用程序,并且可以通过lua-nginx模块实现。Kong不是用这个模块编译Nginx,而是与OpenResty一起发布。
这为可插拔架构奠定了基础,可以在运行时启用和执行Lua脚本(称为“插件”)。插件可以存在于单独的代码库中,并且可以在几行代码中注入到请求生命周期的任何位置。Kong作为开源项目在2015年推出,它的核心价值是高性能和可扩展性。
关于Kong相关的service、route、consumer等概念,可以进一步阅读konggetting-started-guide。
Kong插件
插件提供了用于修改和控制Kong Gateway功能的模块化系统,目的是为了集成技术人员编写的一些业务相关的通用能力,提高网关整体的可扩展性。例如,为了保护服务API,可能需要一个访问密钥,可以使用key-auth插件进行设置。插件提供了广泛的功能,包括访问控制,缓存,速率限制,日志记录等等,KongHub上有丰富的插件可以直接在业务中使。在Kong的实际使用过程中,我们也经常需要自己编写插件来为特定的场景服务。
插件执行流程
由于Kong是运行在OpenResty,我们先来看一下OpenResty执行流程
- init_by_lua: master进程启动后的回调。
- init_worker_by_lua:nginx worker进程启动后的回调。
- rewrite_by_lua:request进来后首先经过rewrite,可以改写URL等。
- access_by_lua:request路由完成之后,进一步处理之前,可以修改request。
- header_filter_by_lua:response准备返回之前,可以修改header。
- body_filter_by_lua:response准备返回之前,可以修改body。
- log_by_lua:用于请求的日志记录。
Kong通过在 openresty 各个阶段加入了lua代码来实现插件的注入和执行,Kong 的 nginx 配置:
init_by_lua_block {
kong = require 'kong'
kong.init() // 完成 Kong 的初始化,路由创建,插件预加载等
}
init_worker_by_lua_block {
kong.init_worker() // 初始化 Kong 事件, worker 之间的事件,由 worker_events 来处理, cluster 节点之间的事件,由 cluster_events 来处理,缓存机制
}
upstream kong_upstream {
server 0.0.0.1;
balancer_by_lua_block {
kong.balancer() //负载均衡
}
keepalive 60;
}
...
location / {
rewrite_by_lua_block {
kong.rewrite() //插件生效策略的筛选,并执行对应的操作,只能处理全局插件(kong插件级别,全局(作用于所有请求),route(作用于当前路由),service(作用于匹配到当前service的所有请求)),路由匹配未开始。
}
access_by_lua_block {
kong.access() //1.完成路由匹配,2.确认加载的插件(并加入缓存) 3.进入balancer阶段
}
header_filter_by_lua_block {
kong.header_filter() //遍历在缓存中的插件列表,并执行
}
body_filter_by_lua_block {
kong.body_filter() //遍历在缓存中的插件列表,并执行
}
log_by_lua_block {
kong.log() //遍历在缓存中的插件列表,并执行
}
}
插件加载
for plugin, plugin_conf in plugins_iterator(singletons.loaded_plugins, true) do
plugin.handler:rewrite(plugin_conf)
end
加载过程可以简单的解读为:通过对符合规则的插件进行遍历,然后执行这个节点插件对应的方法。
插件编写
插件编可以参考Kong官方插件项目模板:https://github.com/Kong/kong-plugin,主要分为三个部分:
- *_handler.lua:实现kong各个生命期的钩子函数,会被kong加载使用,是插件核心逻辑所在。
- schema.lua:定义plugin支持哪些配置,每一个字段的类型,这样kong可以校验。
- .rockspec:luarocks包管理工具的描述文件,kong利用luarocks把plugin源码安装到lua的系统查找路径下面。(当然我们也可以不用.rockspec,手动把源码放到lua系统路径下,以便openresty可以加载到)
handler.lua范例:
local plugin = {
PRIORITY = 1000, -- set the plugin priority, which determines plugin execution order
VERSION = "0.1",
}
-- constructor
function plugin:new()
-- do initialization here, runs in the 'init_by_lua_block', before worker processes are forked
end
---------------------------------------------------------------------------------------------
-- In the code below, just remove the opening brackets; `[[` to enable a specific handler
--
-- The handlers are based on the OpenResty handlers, see the OpenResty docs for details
-- on when exactly they are invoked and what limitations each handler has.
---------------------------------------------------------------------------------------------
-- handles more initialization, but AFTER the worker process has been forked/created.
-- It runs in the 'init_worker_by_lua_block'
function plugin:init_worker()
-- your custom code here
end --]]
-- runs in the ssl_certificate_by_lua_block handler
function plugin:certificate(plugin_conf)
-- your custom code here
end --]]
-- runs in the 'rewrite_by_lua_block'
-- IMPORTANT: during the `rewrite` phase neither the `api` nor the `consumer` will have
-- been identified, hence this handler will only be executed if the plugin is
-- configured as a global plugin!
function plugin:rewrite(plugin_conf)
-- your custom code here
end --]]
--- runs in the 'access_by_lua_block'
function plugin:access(plugin_conf)
-- your custom code here
-- ngx.req.set_header("Hello-World", "this is on a request")
kong.ctx.plugin.source_ip = kong.client.get_ip()
end --]]
---[[ runs in the 'header_filter_by_lua_block'
function plugin:header_filter(plugin_conf)
-- your custom code here, for example;
-- ngx.header["Bye-World"] = "this is on the response"
kong.response.set_header("tag", plugin_conf["tag"])
end --]]
-- runs in the 'body_filter_by_lua_block'
function plugin:body_filter(plugin_conf)
-- your custom code here
end --]]
-- runs in the 'log_by_lua_block'
function plugin:log(plugin_conf)
-- your custom code here
kong.log.debug("my-plugin", kong.ctx.plugin.source_ip)
end --]]
-- return our plugin object
return plugin
项目实战
拍客中台是我们最近在做的攻坚项目,服务于OGC/PGC拍客群体,勾连起从拍客上传到作品发布与数据查看的全流程,并协同多个中台,实现未来为CP进行工业化体系赋能,提升拍客创作效率。
攻坚更侧重于迭代速度,要求能快速上线并验证收益。特别是在即研发新服务的同时又需要协同数十个团队复用现有服务能力,网关在这里担负起了快速打通上游业务接入与下游服务接入的重担。
系统架构
- 应用层:负责接入拍客服务的业务线,有PC后台、APP、H5等等。
- 接入层:肩负流量接入、服务接入和业务接入的职能,流量接入主要由负载均衡负责,Kong作为业务网关,负责下游服务接入和业务接入。
- 服务层:负责核心能力实现,包含账号、组织服务、素材相关服务、作品相关服务,以及依赖的其他中台服务。
- 存储层:负责持久化数据、静态资源、缓存等的存储,包含账号、组织库、素材库、源站、缓存、消息队列等。
拍客自身会依赖多个下游服务,每个服务有自己团队的研发风格,接口协议也千差万别,Kong除了负责基础的服务治理、路由、流控等,还要负责登录态转换、协议标准化等工作。
登录态转换
受限于各个业务线还没有统一的SSO账号,需要打通各个业务线与拍客的账号体系,目前通过关联的内容中台UID做转换,如果后续需要支持非内容中台用户使用拍客,则可以通过统一的Openid来做支持。
转换功能由自研bussiness-to-paikeid插件来完成,主要包含两方面:
- 复用现有业务线的身份鉴权,验证登录态用户身份。复用现有能力,可以使业务线前端专注在业务逻辑研发上,对拍客后续快速推行也非常有利。
- 通过关联的内容中台UID,将业务线的UID转换为拍客的UID,并透传给下游服务。透传方式和参数的配置化,可以很好的适配多个下游服务协议。
- 业务线前端透传用户身份标识(cookie/token…)。
- 插件把身份标识透传到业务线服务端做身份验证,并查询绑定的内容中台UID(mediaid)。
- 通过内容中台UID查拍客系统对应的用户信息,验证是否为拍客账号。
- 透传拍客UID到服务层,透传方式可以为header头、query参数、body参数,参数名可以自定义。
协议标准化
提供给前端统一的API请求协议和返回协议,这些可以由单独的接入层来完成,但是需要一定的人力投入,并且大多是类似包接口这样的逻辑,还要花费大量的时间对接上下游,十分低效。所以我们可以通过请求转换、服务器函数、rpc解析等插件来完成业务接入的角色。
- 请求协议的标准化:在插件处实现下游服务鉴权逻辑,将公网授权转换为内部授权,也就是登录态验证后的请求直接转化为内部请求。
- 返回协议标准化:修改下游服务的回包结构,兼容不同服务的返回体。我们使用统一的{code, msg, data}结构的json结构,屏蔽服务差别。
- 作品数据场景通过request-transformer插件,透传pass中台鉴权参数给到数据中台。
- 素材相关场景通过pre-function插件,编写鉴权severless函数,并透传签名到素材库中台。
- 对服务层返回的rpc错误进行解析,提取错误码和错误信息,转换为对应的{code, msg}返回给前端。
- 对服务层返回的非标准化协议body做修改,统一返回结构{code, msg, data}。
结语
最后,提一下Kong vs Apisix,他们都是基于OpenResty研发的非常优秀的API网关,这是一份来自Apisix官方的对比。
Both of them have been covered core features of API gateway
Features | Apache APISIX | KONG |
---|---|---|
Dynamic upstream | Yes | Yes |
Dynamic router | Yes | Yes |
Health check | Yes | Yes |
Dynamic SSL | Yes | Yes |
L4 and L7 proxy | Yes | Yes |
Opentracing | Yes | Yes |
Custom plugin | Yes | Yes |
REST API | Yes | Yes |
CLI | Yes | Yes |
The advantages of Apache APISIX
Features | Apache APISIX | Kong |
---|---|---|
Belongs to | Apache Software Foundation | Kong Inc. |
Tech Architecture | Nginx + etcd | Nginx + postgres |
Communication channels | Mail list, Wechat group, QQ group, GitHub, meetup | GitHub, freenode, forum |
Single-core CPU, QPS(enable limit-count and prometheus plugins) | 18000 | 1700 |
Latency | 0.2 ms | 2 ms |
Dubbo | Yes | No |
Configuration rollback | Yes | No |
Route with TTL | Yes | No |
Plug-in hot loading | Yes | No |
Custom LB and route | Yes | No |
REST API <--> gRPC transcoding | Yes | No |
Tengine | Yes | No |
MQTT | Yes | No |
Configuration effective time | Event driven, < 1ms | polling, 5 seconds |
Dashboard | Yes | No |
IdP | Yes | No |
Configuration Center HA | Yes | No |
Speed limit for a specified time window | Yes | No |
Support any Nginx variable as routing condition | Yes | No |
Apisix作为后起之秀,在设计上的确可以避免Kong自身的一些弊端,特别是在性能和新特性上,但是自身的稳定性和社区生态上还有待时间的进一步检验。实际业务中的选型,还是应该根据具体场景综合考量。
参考文章
Using API gateways in microservices
百亿流量微服务网关的设计与实现
Kong-如何编写插件
Kong-Gateway
拍客系统:你好,想你请教一下关于kong做api网关的一些问题