1. APISIX 轻量级"探针"实战:serverless-pre-function与serverless-post-function组合应用
在微服务架构的日常运维中,最让人头疼的莫过于那些突然出现的499/504超时问题。作为一线运维人员,我经常遇到这样的场景:监控系统疯狂报警,业务方不断催促,而你却像盲人摸象一样,连问题发生在网关层、网络层还是上游服务都难以快速定位。更棘手的是,当上游是第三方服务(比如支付回调或SSO认证)时,我们连加个日志埋点的机会都没有。
这就是我今天要介绍的APISIX serverless插件组合的价值所在——它们就像给API网关装上了"听诊器",不需要动手术(修改代码)就能诊断出系统的"病灶"。
1.1 为什么需要网关层探针?
去年我们接入了一个第三方支付服务,回调接口频繁超时。由于对方是银行系统,我们既不能要求他们加日志,也不能控制他们的超时设置。传统的解决方案要么是搭建一个代理层,要么是在业务代码里加各种try-catch,这些方法要么太重,要么需要发布新版本。
APISIX的serverless-pre-function和serverless-post-function这对组合插件完美解决了这个痛点。它们允许我们在请求生命周期的关键节点注入诊断逻辑,就像在API的"前门"和"后门"各装了一个摄像头,全程记录请求的"进出"情况。
关键优势:零侵入性。不需要修改上游代码,不需要重启服务,只需要更新路由配置就能开启或关闭诊断功能,特别适合生产环境临时排查问题。
2. 插件核心机制解析
2.1 插件执行阶段对比
让我们用一张表格清晰对比两个插件的工作机制:
| 插件名称 | 执行阶段 | 典型应用场景 | 可访问的上下文数据 |
|---|---|---|---|
| serverless-pre-function | 请求转发到上游之前 | 参数校验、请求篡改、开始计时 | 请求头、查询参数、客户端IP、路由信息 |
| serverless-post-function | 收到上游响应后 | 响应日志、耗时计算、错误处理 | 响应头、响应体、上游IP、耗时数据 |
2.2 技术实现原理
这两个插件本质上都是APISIX插件系统的扩展点实现。当你在路由中启用它们时:
- APISIX会在对应的生命周期阶段(pre或post)创建一个隔离的Lua协程
- 你的自定义函数会被加载到这个协程中执行
- 执行环境提供了完整的请求/响应上下文访问能力
- 所有操作都在网关层面完成,完全不触及上游服务
这种设计带来了几个重要特性:
- 热加载:修改插件配置后立即生效,不需要重启网关
- 沙箱安全:自定义代码运行在受限环境中,不会影响网关稳定性
- 低开销:相比全链路追踪方案,这种轻量级探针几乎不增加系统负载
3. 实战:支付回调超时诊断
假设你有一个电商平台,支付回调接口/api/payment/callback近期频繁超时。由于上游是第三方支付网关,我们无法修改其代码。下面展示如何用这对插件组合进行诊断。
3.1 配置pre-function记录请求信息
首先创建一个包含pre-function插件的路由:
bash复制curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: your-admin-key' -X PUT -d '
{
"uri": "/api/payment/callback/*",
"plugins": {
"serverless-pre-function": {
"phase": "rewrite",
"functions": [
"return function(conf, ctx)
local core = require('apisix.core')
local start_time = ngx.now()
ctx.start_time = start_time
-- 记录关键请求信息
core.log.warn('PRE: ',
'client: ', ngx.var.remote_addr, ' | ',
'method: ', ngx.req.get_method(), ' | ',
'uri: ', ngx.var.request_uri, ' | ',
'headers: ', core.json.encode(ngx.req.get_headers()))
-- 如果需要读取body(注意性能影响)
ngx.req.read_body()
local body = ngx.req.get_body_data()
if body then
core.log.warn('Request body: ', body)
end
end"
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"payment-gateway.com:443": 1
}
}
}'
注意事项:读取请求体(body)会带来额外的性能开销,在高压环境下建议只记录必要字段而非完整body。
3.2 配置post-function计算耗时
接着添加post-function插件来记录响应情况:
bash复制curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: your-admin-key' -X PUT -d '
{
"plugins": {
"serverless-post-function": {
"phase": "log",
"functions": [
"return function(conf, ctx)
local core = require('apisix.core')
local end_time = ngx.now()
local elapsed = (end_time - ctx.start_time) * 1000
core.log.warn('POST: ',
'status: ', ngx.status, ' | ',
'upstream: ', ngx.var.upstream_addr, ' | ',
'time: ', string.format('%.2fms', elapsed), ' | ',
'response: ', ngx.var.upstream_response_length, ' bytes')
-- 关键错误识别
if ngx.status >= 500 then
core.log.error('Upstream error: ', ngx.var.upstream_status)
elseif elapsed > 1000 then -- 超过1秒视为慢请求
core.log.warn('Slow request: ', elapsed, 'ms')
end
end"
]
}
}
}'
3.3 诊断数据分析方法
配置生效后,你可以在APISIX的error.log中看到如下格式的日志:
code复制2023/08/15 14:30:22 [warn] PRE: client: 192.168.1.100 | method: POST | uri: /api/payment/callback/order123 | headers: {"Host":"example.com","Content-Type":"application/json"}
2023/08/15 14:30:23 [warn] POST: status: 504 | upstream: 10.0.0.1:443 | time: 1250.32ms | response: 0 bytes
通过分析这些日志,我们可以:
-
定位瓶颈阶段:
- 如果PRE日志和POST日志时间差很小,但客户端收到504,说明是网络连接问题
- 如果时间差接近网关超时设置,说明上游处理超时
-
识别异常模式:
- 特定客户端IP的请求是否总是慢?
- 特定参数的请求是否更容易失败?
- 响应时间是否呈现周期性波动?
-
数据关联分析:
bash复制# 统计各上游节点的平均响应时间 grep 'POST:' error.log | awk -F '|' '{print $3}' | awk '{sum+=$4; count++} END {print "Avg:", sum/count, "ms"}' # 找出最慢的5个请求 grep 'Slow request:' error.log | sort -k5 -nr | head -5
4. 高级应用场景
4.1 分布式追踪增强
虽然这不是完整的分布式追踪方案,但我们可以利用这两个插件补充关键信息:
lua复制-- pre-function中生成追踪ID
local trace_id = ngx.var.request_id or require('resty.jit-uuid').generate_v4()
ctx.trace_id = trace_id
ngx.req.set_header('X-Trace-ID', trace_id)
-- post-function中记录完整追踪信息
core.log.warn('TRACE: ', trace_id, ' | ',
'gateway_time: ', elapsed, 'ms | ',
'upstream_status: ', ngx.var.upstream_status)
4.2 动态采样控制
在高流量环境下,可以通过条件判断实现采样:
lua复制-- 只记录1%的请求
if math.random(100) == 1 then
core.log.warn('Sampled request: ', ngx.var.request_uri)
end
4.3 异常请求拦截
在pre-function中实现简单防护:
lua复制-- 拦截异常Content-Length
local content_length = tonumber(ngx.req.get_headers()['Content-Length'])
if content_length and content_length > 10 * 1024 * 1024 then -- 10MB
return ngx.exit(413)
end
5. 性能优化建议
-
日志级别控制:
- 生产环境建议使用
core.log.warn而非core.log.info - 可以通过环境变量动态调整日志级别
- 生产环境建议使用
-
避免内存泄漏:
- 不要在函数中累积全局状态
- 大型临时变量使用后显式置为nil
-
错误处理:
lua复制local ok, err = pcall(function() -- 你的代码 end) if not ok then core.log.error('plugin failed: ', err) end -
性能关键路径:
- 避免在pre-function中进行耗时的JSON解析
- 考虑使用FFI优化热点代码
我在实际使用中发现,这套方案最大的价值不在于它的技术复杂度,而在于它提供的"即时可见性"。曾经有个案例,我们通过这种方式发现某个第三方服务的响应时间与请求体中的某个字段长度成正比,最终帮助对方定位到了他们数据库的索引问题——而这一切都不需要对方配合修改任何代码。
这种轻量级探针特别适合以下场景:
- 第三方服务问题诊断
- 生产环境临时问题排查
- 无法修改的上游系统监控
- 快速验证性能优化效果
最后分享一个实用技巧:你可以为诊断路由设置单独的日志文件,避免污染主日志流:
nginx复制http {
log_format diagnostics '[$time_local] $remote_addr "$request" $status $body_bytes_sent';
server {
location /diagnostic-log {
access_log logs/diagnostic.log diagnostics;
}
}
}