在业务开发中,我们经常需要通过“数据驱动”做决策。前端页面中的各类打点事件产生大量请求,如何高效、稳定地进行数据采集,成为后端服务设计的重要课题。相比引入复杂的多语言服务,我们可以巧妙利用 Nginx 的轻量高性能特性,搭建一个具备 CORS 支持,既能处理 GET 请求,也能优雅接收 POST 请求体的打点采集服务,满足生产级的需求。
本文将结合实践,详细介绍如何通过配置 Nginx,解决 Nginx 默认不支持 POST 请求且无法记录 POST Body 的问题,设计支持跨域,日志格式友好,并能便于后续离线分析的打点收集服务。
问题背景与挑战
- 数据来源:业务 Web 服务的请求日志是数据采集的重要来源,为了降低对业务的影响,通常会设计专门用于统计的打点服务器。
- 打点并发压力:前端页面瞬间会发起大量打点请求,容易导致友军 DDoS,影响用户体验和服务器稳定。
- Nginx的局限:
- 默认不支持 POST 请求访问指定路径,返回 405 错误。
- 即使支持部分动态解析,默认 access_log 也不会记录 POST 的请求体。
- 跨域(CORS)限制导致前端 POST 请求受限。
- 如何用原生 Nginx 实现完整流程,成为值得探讨的重点。
Nginx 默认 POST 请求的限制
通过实验在无任何配置的 Nginx 容器中使用 curl -d '{...}' -X POST
访问,会收到 HTTP 405 Not Allowed
错误。
这是因为诸如 ngx_http_stub_status_module
等模块只允许 GET/HEAD 方法。Nginx 默认作为静态文件服务器,不解析 POST 请求体,也不支持写入日志。
使 Nginx 支持原生 POST 请求及日志记录
1. 利用 error_page 405 =200 $uri
绕过默认限制
添加以下配置可使 Nginx 返回 200,而非 405:
nginx代码解读复制代码error_page 405 =200 $uri;
调试验证,客户端 POST 请求能拿到响应。但是,日志里仍然不记录 POST Body。
2. 自定义日志格式,记录请求体
默认 access_log
不包含请求体 $request_body
,需要定义新的日志格式:
nginx代码解读复制代码log_format main escape=json '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" $request_body';
此处使用 escape=json
让请求体中的转义字符得到处理,方便后续日志解析。
3. 通过 proxy_pass
激活 Nginx POST Body 解析
为了让 Nginx 读取和记录请求体,必须将请求代理到内部路径:
nginx代码解读复制代码location / { proxy_pass http://127.0.0.1/internal-api-path; } location /internal-api-path { access_log off; default_type application/json; return 200 '{"code":0,"data":"success"}'; }
这里一个小技巧:通过代理触发 Nginx 解析 POST Body,且内部接口返回固定 JSON,方便前端调用及后端排查。
过滤非 POST 请求,避免日志污染及接口滥用
nginx代码解读复制代码map $request_method $loggable { default 0; POST 1; } server { location / { if ( $request_method !~ ^(POST|OPTIONS)$ ) { return 405; } access_log /var/log/nginx/access.log main if=$loggable; proxy_pass http://127.0.0.1/internal-api-path; } }
- 利用
map
变量,只有 POST 请求被记录日志。 - 非 POST/OPTIONS 请求直接返回 405。
- 避免日志记录无用的 GET 请求,更节省资源。
解决跨域问题,支持前端跨站访问
现代浏览器发起跨域 POST 请求时,会首先做一个 OPTIONS
预检请求,如果服务器不响应,前端请求失败。
完整配置如下:
nginx代码解读复制代码map $http_origin $corsHost { default 0; "~(.*).soulteary.com" 1; "~(.*).baidu.com" 1; } server { location / { if ( $request_method !~ ^(POST|OPTIONS)$ ) { return 405; } if ( $corsHost = 0 ) { return 405; } if ( $corsHost = 1 ) { add_header 'Access-Control-Allow-Credentials' 'false'; add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma'; add_header 'Access-Control-Allow-Methods' 'POST,OPTIONS'; add_header 'Access-Control-Allow-Origin' '$http_origin'; } if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Credentials' 'false'; add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma'; add_header 'Access-Control-Allow-Methods' 'POST,OPTIONS'; add_header 'Access-Control-Allow-Origin' '$http_origin'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain charset=UTF-8'; add_header 'Content-Length' 0; return 204; } access_log /var/log/nginx/access.log main if=$loggable; proxy_pass http://127.0.0.1/internal-api-path; } }
- 利用
map
定义白名单域名,拒绝非法跨域。 - 预检请求返回 HTTP 204,无响应体。
- 动态添加
Access-Control-Allow-Origin
,更安全。
支持 GET 请求的“打点埋点GIF方案”
出于兼容一些老设备或轻量采集考虑,仍然需要支持经典的通过 image 请求的 GET 打点:
nginx代码解读复制代码log_format slr_event_format '{"eventTime":"$msec","channel":"$arg_channel","targetField":"$arg_targetField","targetValue":"$arg_targetValue","eventField":"$arg_eventField","eventValue":"$arg_eventValue","eventAttrMap":"$arg_eventAttrMap"}'; map $time_iso8601 $log_date { '~^(?
\d{4}-\d{2}-\d{2})' $ymd; default 'date-not-found'; } location /slr_event_local.gif { log_subrequest on; access_log /var/log/nginx/slr_event_local_${log_date}.json slr_event_format; add_header Expires "Fri, 01 Jan 1980 00:00:00 GMT"; add_header Pragma "no-cache"; add_header Cache-Control "no-cache, max-age=0, must-revalidate"; empty_gif; }
- 日志格式以 JSON 字符串格式记录 URL 参数。
- 按日期分文件保存,方便后续切割和归档。
- 返回一个透明 1x1 像素 GIF,前端 img 标签发起请求即可。
- 关闭浏览器缓存,确保每次请求都到后端。
完整且实用的 Nginx 配置示例
以下是一份集成上述功能,满足生产使用的 Nginx 配置节选(略去冗余注释),供参考:
nginx代码解读复制代码user nginx; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main escape=json '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" $request_body'; log_format slr_event_format '{"eventTime":"$msec","channel":"$arg_channel","targetField":"$arg_targetField","targetValue":"$arg_targetValue","eventField":"$arg_eventField","eventValue":"$arg_eventValue","eventAttrMap":"$arg_eventAttrMap"}'; map $time_iso8601 $log_date { '~^(?
\d{4}-\d{2}-\d{2})' $ymd; default 'date-not-found'; } map $request_method $loggable { default 0; POST 1; } map $http_origin $corsHost { default 0; "~(.*).soulteary.com" 1; "~(.*).baidu.com" 1; } server { listen 48881; server_name localhost; charset utf-8; client_max_body_size 10m; # GET埋点接口(本地环境) location /slr_event_local.gif { log_subrequest on; access_log /var/log/nginx/slr_event_local_${log_date}.json slr_event_format; add_header Expires "Fri, 01 Jan 1980 00:00:00 GMT"; add_header Pragma "no-cache"; add_header Cache-Control "no-cache, max-age=0, must-revalidate"; empty_gif; } # POST埋点接口(本地环境) location /batch_slr_event_local { if ( $request_method !~ ^(POST|OPTIONS)$ ) { return 405; } if ( $corsHost = 0 ) { return 405; } if ( $corsHost = 1 ) { add_header 'Access-Control-Allow-Credentials' 'false'; add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma'; add_header 'Access-Control-Allow-Methods' 'POST,OPTIONS'; add_header 'Access-Control-Allow-Origin' $http_origin; } if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Credentials' 'false'; add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma'; add_header 'Access-Control-Allow-Methods' 'POST,OPTIONS'; add_header 'Access-Control-Allow-Origin' $http_origin; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain charset=UTF-8'; add_header 'Content-Length' 0; return 204; } access_log /var/log/nginx/slr_event_local_${log_date}.json main if=$loggable; proxy_pass http://127.0.0.1:48881/internal-api-path-local; } location /internal-api-path-local { access_log off; default_type application/json; return 200 '{"code": 0, "data": "success"}'; } error_page 405 =200 $uri; } }
注意:
- 这里定义了两个入口:
/slr_event_local.gif
负责 GET 打点日志(兼容旧方案),/batch_slr_event_local
允许 POST 请求体日志打点。- 采用动态日志文件切割:每天生成不同日期文件,便于后续集中处理。
- 通过严格的 CORS 白名单保护接口不被滥用。
- 预检 OPTIONS 请求配置完备,避免了前端跨域阻断。
实战经验与后续优化建议
- 日志接入和存储:采集日志后,结合日志聚合平台(比如 ELK, ClickHouse)针对
request_body
进行分析和挖掘。 - 上游代理支持:结合 Traefik 等反向代理,实现打点服务的水平扩展,提高并发和稳定性。
- 打点SDK设计优化:前端通过自定义 SDK 做合并打包,减少请求量,降低服务器压力。
- 安全防护:设置请求大小限制 (
client_max_body_size
) 和白名单限制,防止恶意请求。 - 日志格式:自定义 JSON 友好格式,便于后续流式解析、在线统计。
- 健康检查:添加
/health
接口,方便容器编排与自动化运维。
总结
通过本文示范的 Nginx 配置,我们有效解决了:
- Nginx 默认无法处理 POST 请求体和记录日志的问题
- 避免了跨域请求阻断,支持现代前端跨域采集
- 灵活支持 GET 图像请求和 POST JSON 批量上报
- 满足生产刚需的高性能、低运维成本和稳健可扩展
这种“轻量且强大”的打点采集服务足以支撑绝大多数中小业务线的需求,且极易集成入现有运维体系。
评论记录:
回复评论: