工作中经常会遇到需要“数据支撑”决策的时候,那么可曾想过这些数据从何而来呢?如果业务涉及 Web 服务,那么这些数据的来源之一便是服务器上各种服务器的请求数据,如果我们将专门用于统计的数据进行服务器区分,有一些服务器专注于接收“统计类型”的请求,那么产生的这些日志便是“打点日志”。

本文将介绍如何在容器中使用 Nginx 简单搭建一个支持前端使用的统计(打点采集)服务,避免引入过多的技术栈,徒增维护成本。

写在前面

不知你是否想过一个问题,当一个页面中的打点事件比较多的时候,页面打开的瞬间将同时发起无数请求,此刻非宽带环境下用户体验将不复存在,打点服务器也将面临来自友军的业务 DDoS 行为。

所以这几年中,不断有公司将数据统计方案由 GET 切换为 POST 方案,结合自研定制的 SDK,对客户端的数据统计进行进行“打包合并”,并进行有一定频率的增量日志上报,极大的解决了前端性能问题、以及降低了服务器的压力。

五年前,我曾分享过如何构建易于扩展的前端统计脚本,感兴趣可以进行关联阅读。

POST 请求在 Nginx 环境下的问题

看到这个小节的标题,你或许会感到迷惑,日常对 Nginx 进行 POST 交互司空见惯,会有什么问题呢?

我们不妨做一个小实验,使用容器启动一个 Nginx 服务:

然后使用 curl 模拟日常业务中的 POST 请求:

你将看到下面的返回结果:

按图索骥,查看 Nginx 模块 modules/ngx_http_stub_status_module.chttp/ngx_http_special_response.c的源码可以看到下面的实现:

没错,默认情况下,NGINX 并不支持记录 POST 请求,会根据 RFC7231 展示错误码405。所以一般情况下,我们会借助 Lua /Java / PHP / Go / Node 等动态语言进行辅助解析。

那么如何来解决这个问题呢?能否单纯的使用性能好、又轻量的 Nginx 来完成对 POST 请求的支持,而不借助外力吗?

让 Nginx “原生”支持 POST 请求

为了更清晰的展示配置,我们接下来使用 compose 来启动 Nginx 进行实验,在编写脚本之前,我们需要先获取配置文件,使用下面的命令行将指定版本的 Nginx 的配置文件保存到当前目录中。

默认的配置文件内容如下:

稍作精简,我们会得到一个更简单的配置文件,并在其中添加一行 error_page 405 =200 $uri;

将本小节开始部分的命令改写为 docker-compose.yml 并添加 volumes,把刚刚导出的配置文件映射到容器内,方便使用后续使用 compose 启动容器进行验证。

使用 docker-compose up 启动服务,然后使用前面的 curl 模拟 POST 验证请求是否正常。

执行完毕,除了得到 “soulteary” 这个字符串返回之外, Nginx 日志记录也会多一条看起来正常的记录:

但是,如果你细心的话,你会发现日志中并未包含我们发送的数据,那么这个问题该如何解决呢?

解决 Nginx 日志中丢失的 POST 数据

这个问题其实是老生常谈,默认 Nginx 服务器记录日志格式并不包含 POST Body(性能考虑),并且在没有 proxy_pass 的情况下,是不会解析 POST Body的。

先执行下面的命令:

可以看到默认的 log_format 配置规则中确实并没有任何关于 POST Body 中的数据。

所以解决这个问题的方案也不难,新增一个日志格式,添加 POST Body 变量(request_body),然后添加一个 proxy_pass 路径,激活 Nginx 解析 POST Body 的处理逻辑。

考虑到维护问题,我们前文中的配置文件与这个配置进行合并,并定义一个名为 /internal-api-path 的路径:

将新配置文件保存为 nginx.conf 后,调整 compose 中的 volumes 配置信息,再次使用 docker-compose up 启动服务。

再次使用 curl 模拟之前的 POST 请求,会看到 Nginx 日志多了两条记录,第一条记录中包含了我们所需要的 POST 数据:

但是这里不完美的地方还有很多:

  • 服务器可以正常接收 GET 请求,我们在日志处理的时候需要进行大量“抛弃动作”,并且在暂存的时候,磁盘空间也存在不必要的浪费。
  • 用于激活 Nginx POST Body 解析能力的路径可以被随意调用,产生无意义日志,同样存在上面的问题。
  • 更关键的,日志中的数据看起来还需要额外加工处理,进行转码,解析效率会有不必要的性能损耗。

接下来我们来继续解决这些问题。

改进 Nginx 配置,优化日志记录

首先,在日志格式中添加 escape=json 参数,要求 Nginx 解析日志请求中的 JSON 数据:

然后,在不需要记录日志的路径中,添加 access_log off; 指令,避免不必要的日志进行记录。

接着使用 Nginx map 指令,和 Nginx 中的条件判断,过滤非 POST 请求的日志记录,以及拒绝处理非 POST 请求。

再次使用 curl 请求,会看到日志已经能够正常解析,不会出现两条日志了。

同时,也不会再记录任何非 POST 请求,使用 POST 请求的时候,会提示 405 错误状态。

这个时候,你或许会好奇,为什么这个 405 和前文中不同,不会被重定向为 200 呢?这是因为这个 405 是我们根据触发条件“手动设置”的,而非 Nginx 逻辑运行过程中判断出新的结果。

当前的 Nginx 配置如下:

但是到这里就真的结束了吗?

模拟前端客户端常见跨域请求

我们打开熟悉的“百度”,在控制台中输入下面的代码,模拟一次常见的业务跨域请求。

代码执行完毕后,你会看到一个经典的提示信息:

观察 Network 网络面板,会看到有两条失败的新请求:

  • Request URL: http://localhost:3000/
    • Request Method: OPTIONS
    • Status Code: 405 Not Allowed
  • Request URL: http://localhost:3000/
    • Request Method: POST
    • 没有响应结果

让我们继续调整配置,解决这个常见的问题吧。

使用 Nginx 解决前端跨域问题

我们首先调整之前的过滤规则,允许 OPTIONS 请求的处理。

跨域请求是前端常见场景,许多人会偷懒使用 “*”来解决问题,但是 Chrome 等现代浏览器在新版本中有些场景不能使用这样宽松的规则,而且为了业务安全,一般情况,我们会在服务端设置允许进行跨域请求的域名白名单,参考上文中的方式,我们可以很容易的定义出类似下面的 Nginx map 配置,来谢绝所有前端非授权跨域请求:

这里有一个 trick 的地方,Nginx 的路由内的规则编写,并不完全类似级编程语言一样,可以顺序执行,是具备“优先级/覆盖”关系的,所以为了能够让前端正常调用接口进行数据提交,这里需要这样书写规则,存在四行代码冗余。

再次在网页中执行前面的 JavaScript 代码,会发现请求已经可以正常执行了,前端数据会返回:

而 Nginx 日志,则会多一条符合预期的记录:

而使用 curl 执行之前的命令,继续模拟纯接口调用,则会发现出现了 405 错误响应,这是因为我们的请求中不包含 origin 请求头,无法表明我们的来源身份,在请求中使用 -H 参数补全这个数据,即可拿到符合预期的返回:

相对完整的 Nginx 配置

到现在为止,我们基本实现一般的采集功能,满足基本诉求的 Nginx 配置信息如下:

如果我们结合容器使用,只需要在其中添加一段额外的路由定义,单独用于健康检查,就能够实现一个简单稳定的采集服务。继续对接后续的数据转存、处理程序。

而 compose 配置文件,相比较之前,不过多了几行健康检查定义罢了:

结合 Traefik ,可以轻松进行实例的水平扩展,处理更多的请求。感兴趣可以翻阅我之前的文章

最后

本文仅介绍了数据采集的皮毛,更多的内容或许后续有时间会细细道来。要给我家毛孩子付猫粮尾款啦,先写到这里吧。

–EOF