1. APISIX自定义插件开发概述
APISIX作为云原生API网关,其插件机制是其核心能力之一。通过自定义插件开发,我们可以灵活扩展网关功能,满足特定业务场景需求。不同于传统Nginx模块开发需要重新编译的繁琐流程,APISIX插件采用Lua语言编写,支持热加载,极大提升了开发效率。
在实际业务中,我们经常遇到需要定制化认证逻辑、特殊流量控制、请求/响应改写等场景。比如最近我们团队就遇到一个需求:需要根据请求头中的特定字段进行动态路由,这正是通过开发自定义插件完美解决的。下面我将结合这个实战案例,详细介绍APISIX自定义插件的完整开发流程。
2. 开发环境准备与插件结构
2.1 基础环境配置
在开始开发前,需要确保APISIX运行环境已正确配置。关键配置项位于conf/config.yaml:
apisix: extra_lua_path: "/usr/local/apisix/plugins/?.lua" # 自定义插件路径 lua_module_hook: "custom_hook" # 可选:全局方法钩子建议的插件目录结构如下:
/usr/local/apisix/ ├── plugins/ │ ├── custom_hook.lua # 全局钩子文件(可选) │ └── apisix/ │ └── plugins/ │ ├── my-plugin.lua # 自定义插件主文件 │ └── ... # 其他依赖文件注意:目录权限需要确保APISIX进程有读取权限,建议设置为755
2.2 插件基础模板
每个插件都需要遵循基本结构模板:
local core = require("apisix.core") local plugin_name = "my-plugin" local schema = { type = "object", properties = { -- 插件配置参数定义 key = {type = "string", default = "value"} }, required = {"key"} -- 必填参数 } local _M = { version = 1.0, priority = 500, -- 执行优先级(避免与内置插件冲突) name = plugin_name, schema = schema, } function _M.check_schema(conf) return core.schema.check(schema, conf) end function _M.access(conf, ctx) -- 主要业务逻辑 core.log.info("Hit my-plugin with conf: ", core.json.encode(conf)) return 200, "Hello from my-plugin" end return _M3. 插件核心开发详解
3.1 插件生命周期与执行阶段
APISIX插件支持OpenResty的所有阶段,最常用的包括:
| 阶段 | 描述 | 典型用途 |
|---|---|---|
| rewrite | 请求重写阶段 | 认证、请求改写 |
| access | 访问控制阶段 | 限流、权限检查 |
| header_filter | 响应头处理 | 添加/修改响应头 |
| body_filter | 响应体处理 | 响应内容修改 |
| log | 日志记录阶段 | 请求日志记录 |
以认证插件为例,典型阶段实现如下:
function _M.rewrite(conf, ctx) -- 1. 获取认证信息(如JWT Token) local token = core.request.header(ctx, "Authorization") -- 2. 验证逻辑 if not token or not validate_token(token) then return 401, {message = "Unauthorized"} end -- 3. 设置上下文信息 ctx.auth_info = decode_token(token) end3.2 配置管理与Schema验证
完善的Schema定义能确保配置的正确性:
local schema = { type = "object", properties = { redis_host = {type = "string", default = "127.0.0.1"}, redis_port = {type = "integer", minimum = 1, maximum = 65535}, timeout = {type = "number", minimum = 0, default = 1000}, whitelist = { type = "array", items = {type = "string"}, minItems = 1 } }, required = {"redis_host", "redis_port"}, encrypt_fields = {"redis_password"} -- 加密字段 }验证函数示例:
function _M.check_schema(conf, schema_type) -- 支持多种schema类型校验 if schema_type == core.schema.TYPE_METADATA then return core.schema.check(metadata_schema, conf) end return core.schema.check(schema, conf) end3.3 上下文对象关键属性
ctx对象包含丰富的请求上下文信息:
function _M.access(conf, ctx) -- 请求信息 local method = ctx.var.request_method local uri = ctx.var.uri -- 路由匹配信息 local route_id = ctx.matched_route and ctx.matched_route.value.id -- 上游信息 local upstream = ctx.matched_upstream -- 自定义变量 core.ctx.register_var("my_var", function(ctx) return ctx.var.arg_foo or "default" end) end4. 高级功能实现
4.1 外部依赖集成
当插件需要连接Redis/MySQL等外部服务时:
local redis = require("resty.redis") function _M.init() -- 初始化共享字典 local ok, err = ngx.shared.my_dict:set("init", true) if not ok then core.log.error("failed to init shared dict: ", err) end end function _M.access(conf, ctx) local red = redis:new() red:set_timeout(conf.redis_timeout) local ok, err = red:connect(conf.redis_host, conf.redis_port) if not ok then core.log.error("failed to connect redis: ", err) return 500, {message = "Internal Error"} end -- 业务逻辑... end需要在Nginx配置中添加共享内存:
nginx_config: http_configuration_snippet: | lua_shared_dict my_dict 10m;4.2 自定义API暴露
插件可以注册两种类型的API:
- 公共API(通过public-api插件暴露):
function _M.api() return { { methods = {"POST"}, uri = "/my-plugin/operation", handler = function(ctx) -- 处理逻辑 end } } end- 控制API(仅限本地访问):
function _M.control_api() return { { methods = {"GET"}, uris = {"/v1/plugin/my-plugin/status"}, handler = function(ctx) return 200, {status = "ok"} end } } end5. 插件测试与部署
5.1 单元测试示例
测试用例通常放在t/plugin目录下:
=== TEST 1: basic schema validation --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.my-plugin") local ok, err = plugin.check_schema({ redis_host = "127.0.0.1", redis_port = 6379 }) if not ok then ngx.say(err) end ngx.say("passed") } } --- request GET /t --- response_body passed --- no_error_log [error]5.2 插件启用配置
在conf/config.yaml中启用插件:
plugins: - my-plugin # 自定义插件 - key-auth # 内置插件 - limit-req # 内置插件路由配置示例:
{ "uri": "/api/*", "plugins": { "my-plugin": { "redis_host": "10.0.0.1", "redis_port": 6379 } }, "upstream": { "type": "roundrobin", "nodes": { "backend:8080": 1 } } }6. 实战经验与排错指南
6.1 常见问题排查
插件未生效:
- 检查
plugins列表是否包含插件名 - 确认插件文件路径正确
- 查看错误日志
logs/error.log
- 检查
优先级冲突:
- 使用
GET /v1/schema查看所有插件优先级 - 确保自定义插件priority不与内置插件冲突
- 使用
内存泄漏:
- 使用
ngx.shared.DICT管理共享数据 - 确保外部连接(如Redis)正确释放
- 使用
6.2 性能优化建议
- 缓存热点数据:
local cached_data = ngx.shared.my_cache:get("key") if not cached_data then cached_data = fetch_data() ngx.shared.my_cache:set("key", cached_data, 60) -- 缓存60秒 end减少阻塞操作:
- 使用cosocket进行非阻塞IO
- 耗时操作放到定时器或单独阶段
合理设置优先级:
- 认证类插件设置高优先级(1000+)
- 日志类插件设置低优先级(100-)
7. 插件开发进阶技巧
7.1 动态配置加载
实现配置热更新:
local last_update = 0 local cached_config function _M.access(conf, ctx) if ngx.now() - last_update > 60 then -- 60秒更新一次 cached_config = fetch_latest_config() last_update = ngx.now() end -- 使用cached_config... end7.2 跨插件通信
通过ctx共享数据:
-- 插件A设置数据 ctx.shared_data = {key = "value"} -- 插件B读取数据 local data = ctx.shared_data7.3 自定义日志格式
在插件中记录结构化日志:
function _M.log(conf, ctx) core.log.warn(core.json.encode({ time = ngx.localtime(), client_ip = ctx.var.remote_addr, request_id = ctx.var.request_id, custom_field = ctx.custom_value })) end我在实际开发中发现,良好的日志设计能极大提升问题排查效率。建议为每个重要操作添加DEBUG级别日志,生产环境再调整日志级别。