⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 SQB Blog 「徐长茂」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

背景

API网关作为外部流量访问内部服务的入口,可以屏蔽内部微服务之间的差异,提供动态路由、身份认证、流量控制、协议转换、负载均衡等公共能力,在服务治理中起到非常重要的作用。

在收钱吧业务发展过程中,各业务团队基于自身需求,开发了多个API网关。这些网关使用了多种开发语言和不同的技术栈,管理平台也比较简陋。管理平台功能缺失,不方便API配置和管理,也具有一些安全隐患;API网关使用了多种开发语言和技术栈,导致维护和升级非常困难;还有重复造轮子,消耗了很大的人力和时间成本。随着业务增长,一些业务API网关也暴露出性能问题。

为了解决上述问题,统一API网关技术栈,完善API管理平台,收钱吧在经过调研和评估后,决定基于Apache APISIX[1]自研新一代网关。

为什么选择APISIX

  • APISIX是一个高性能API网关,是Apache顶级开源项目, 社区活跃
  • APISIX是在OpenResty基础上做的二次开发,OpenResty是Nginx的扩展。网络处理由Nginx负责,用户只需要关注功能组件开发
  • 使用ETCD存储服务、路由等元数据,APISIX可实时感知元数据更新
  • APISIX提供了丰富的插件和插件扩展机制
  • 插件使用lua编写,方便二次开发

以上是APISIX的优点总结,也是收钱吧选择基于APISIX开发新一代网关的原因。

二次开发

收钱吧APISIX二次开发主要围绕插件和管理平台两方面进行。

插件

APISIX官方插件虽然丰富,但是不能满足我们全部的使用需求。一些插件还存在配置过于繁琐的问题,需要对插件裁剪,简化配置。

收钱吧APISIX一部分插件是根据业务需求进行开发的,一部分插件是对APISIX官方插件进行简化和改造。相比官方插件,自研插件只使用check_schemainitaccessheader_filterbody_filter五个扩展点。

复用

收钱吧自研插件分为通用插件和业务线定制插件。相比通用插件,一些业务线插件只是在参数转换和解析响应部分有所区别。为了减少代码冗余,提高开发效率,我们在编写插件时做了一些继承和复用。

收钱吧APISIX对接口响应格式进行了定义:返回值HTTP状态码为200,格式为{code, data, msg},code=10000时表示成功响应。这就需要APISIX对上游接口响应进行二次解析和包装。

根据OpenResty lua-nginx-module[2]文档,Nginx响应是流式输出,并不会等到整个响应体都生成了才往客户端发送数据。body_filter_by_lua 在一次请求可能会被多次调用。

Nginx output filters may be called multiple times for a single request because response body may be delivered in chunks. Thus, the Lua code specified by in this directive may also run multiple times in the lifetime of a single HTTP request.

插件在解析上游接口响应时,如果每个插件都在body_filter中实现获取完整流式响应数据的代码则会过于冗余。基于这点原因,我们设计了一个名称为wosai_plugin的基础插件。wosai_plugin插件在header_filter中设置响应HTTP状态码为200,在body_filter中实现了获取完整流式响应数据的处理逻辑,并提供回调方法resolve_response由具体插件实现。这样其他插件只需要实现业务逻辑即可。

以下是插件代码复用示例。可以看到,通过代码复用,降低了代码冗余,提高了插件开发效率。

-- 基础插件wosai-plugin示例代码 --
local _M = {}

function _M.header_filter(conf, ctx)
-- access阶段调用ngx.exit返回 不需要处理返回值 --
if ngx.ctx.access_exit then
return
end

ngx.ctx.origin_http_status = ngx.status
ngx.ctx.origin_content_type = ngx.header.content_type or ''

ngx.status = 200
core.response.clear_header_as_body_modified()
ngx.header.content_type = "application/json;charset=utf-8"
end

function _M.body_filter(conf, ctx, resolve_response)
-- access阶段调用ngx.exit返回 不需要处理返回值 --
if ngx.ctx.access_exit then
return
end

local chunk, eof = ngx.arg[1], ngx.arg[2]
if ngx.ctx.buffered == nil then
ngx.ctx.buffered = {}
end

if chunk ~= "" and not ngx.is_subrequest then
table.insert(ngx.ctx.buffered, chunk)
ngx.arg[1] = nil
end

if eof then
local resp_str = table.concat(ngx.ctx.buffered)
ngx.ctx.buffered = nil

-- 处理上游接口返回值,resolve_response由具体插件实现 --
local resp = resolve_response(conf, ctx, resp_str)

ngx.arg[1] = cjson.encode(resp)
ngx.arg[2] = true
end
end

return _M
-- 插件wosai-jsonrpc示例代码 --
local _M = {}

function _M.resolve_response(conf, ctx, resp_str)
-- 解析包装返回值 --
end

function _M.header_filter(conf, ctx)
wosai_plugin.header_filter(conf, ctx)
end

-- 复用wosai-jsonrpc的插件, 可传入自定义resolve_response方法 --
function _M.body_filter(conf, ctx, resolve_response)
if not resolve_response then
resolve_response = _M.resolve_response
end

wosai_plugin.body_filter(conf, ctx, resolve_response)
end

return _M

日志

收钱吧APISIX一些接口会进行协议转换,生成日志时又需要记录接口原始请求。在插件log阶段使用ngx.req.get_body_data获取的请求数据为协议转换修改过的,基于APISIX插件机制的日志插件不能满足需求。另一方面,每一个接口配置日志插件过于重复和繁琐。

为了解决这个问题,收钱吧APISIX在OpenResty的access_by_lua_blockbody_filter_by_lua_blocklog_by_lua_block进行埋点,分别记录原始请求、接口响应和生成日志。

access_by_lua_block {
wosai_logger.log_request() --- 记录原始请求
apisix.http_access_phase()
}

body_filter_by_lua_block {
apisix.http_body_filter_phase()
wosai_logger.log_response() --- 记录接口响应
}

log_by_lua_block {
wosai_logger.log() --- 生成日志
apisix.http_log_phase()
}

APISIX在初始化阶段使用ngx_tpl.lua生成nginx.conf,日志埋点是通过修改ngx_tpl.lua完成的。一些深度定制开发需求,可通过此种方式进行,这样对APISIX的代码侵入会很小。

管理平台

APISIX虽然自带了Dashboard,但是其没有用户权限控制,无法动态新增和修改插件配置,也不能进行多网关管理,无法满足我们的使用需求。

基于上述原因,收钱吧基于APISIX Admin API开发了网关管理平台,自研管理平台开发工作主要围绕以下几方面进行:

  • 多网关管理
  • 严格的用户权限设计
  • 完整的操作日志记录
  • 动态添加和修改插件
  • 同时支持测试和生产环境配置

在动态管理插件方面,前端使用了XRender[3]根据插件schema动态渲染生成UI,这样极大简化了前端的开发工作。

版本升级

├── conf                     # apisix配置文件, 构建镜像时复制到/usr/local/apisix/conf目录
│ ├── README.md
│ ├── config-beta.yaml
│ ├── config-default.yaml
│ ├── config-prod.yaml
│ └── config.yaml
├── lua
│ ├── cli # 修改源码ngx_tpl.lua, 此模板文件用于生成nginx.conf, 在此文件中添加了wosai_logger相关代码
│ ├── plugins # 自研插件,构建镜像时复制到/usr/local/apisix/apisix/plugins目录
│ └── constants.lua # 修改源码constants.lua, 构建镜像时覆盖原文件
├── CHANGELOG.md
├── Dockerfile
└── README.md

上面为收钱吧APISIX的代码结构。收钱吧APISIX自研插件代码与APISIX核心代码是分离的,这样的代码结构比较干净,既方便APISIX版本升级,也方便我们的自研插件进行迭代。

下面为收钱吧APISIX项目的Dockerfile。在构建镜像时, 把自研插件、配置文件等复制到APISIX源码中,这样就形成了完整的收钱吧APISIX代码。

FROM apache/apisix:2.14.0-alpine

WORKDIR /usr/local/apisix

ENV TZ Asia/Shanghai

COPY conf conf
COPY lua/plugins apisix/plugins
COPY lua/cli/ngx_tpl.lua apisix/cli
COPY lua/constants.lua apisix

EXPOSE 9080 9443

CMD ["sh", "-c", "/usr/bin/apisix init && /usr/bin/apisix init_etcd && /usr/local/openresty/bin/openresty -p /usr/local/apisix -c /usr/local/apisix/conf/nginx.conf -g 'daemon off;'"]

STOPSIGNAL SIGQUIT

在版本升级时,只需要本地安装APISIX新版本或者从GitHub获取新版本代码,然后根据新版本同步更新项目中ngx_tpl.lua、config-defaullt.yaml等文件, 再更新Dockerfile中的APISIX镜像版本,就可以完成升级了。

部署

收钱吧APISIX采用了共享代码,资源隔离的方式进行部署:

  • 共享代码:各业务线使用同一份APISIX代码和同一个管理平台
  • 资源隔离:各业务线APISIX部署在独立的服务器上,拥有独立的域名,对外单独提供服务

共享代码,使收钱吧API网关技术栈得到统一,既方便管理,也方便维护。资源隔离,可以防止某一业务线API网关出现故障影响到其他业务,提高了网关的可靠性。

总结

收钱吧APISIX二次开发是从去年5月份开始的。到目前为止,收钱吧已经把超过一半的业务线API迁移到了APISIX网关,其他业务线API正在进行迁移测试。在使用过程中,APISIX网关基本上没有出现过故障,APISIX的性能和可靠性经受住了生产环境的验证。

通过使用APISIX,统一了收钱吧API网关技术栈和管理平台,把业务团队从网关开发和维护这项繁重的工作中解放出来,使业务团队专注业务开发,提高了业务开发和上线效率。

在APISIX二次开发过程中,我们体会到APISIX设计和架构非常优秀,插件开发也比较简单。基于Apache APISIX,用户可以快速DIY自己的高性能API网关。

关于作者

徐长茂,来自技术平台部

参考资料

[1]Apache APISIX: https://github.com/apache/apisix

[2]lua-nginx-module: https://github.com/openresty/lua-nginx-module#body_filter_by_lua_block

[3]XRender: https://xrender.fun

文章目录
  1. 1. 背景
  2. 2. 为什么选择APISIX
  3. 3. 二次开发
    1. 3.1. 插件
    2. 3.2. 复用
    3. 3.3. 日志
    4. 3.4. 管理平台
    5. 3.5. 版本升级
  4. 4. 部署
  5. 5. 总结
  6. 6. 关于作者
    1. 6.1. 参考资料