Nginx 源代码笔记 - HTTP 模块 - mirror
流量复制工具
It can be used to increase confidence in code deployments, configuration changes and infrastrucure changes.
—Goreplay Project
在服务程序开发流程里,性能测试是很重要的一个环节。通过这个环节可以掌握服务程序的响应时间、并发数、吞 吐量等等性能指标,从而可预估系统的服务质量、可承载用户和需要的硬件资源等等。
我们可以使用诸如 ab, wrk, httperf, locust, JMeter 等工具模拟用户请求,但是这些模拟请求不 足以还原真实场景,同时它们的请求模式也过于单一和理想化。
为了更贴近真实场景,我们可以使用诸如 httperf, vanishreplay, tcpreplay, log-replay 等「离线 回放」工具重放生产环境请求日志,也可以使用 goreplay ,tcpcopy 等「流量复制」工具,实时捕捉生产环境 流量并导向目标测试系统。同时,这些「流量复制」工具甚至可以对真实流量进行放大或缩小。
流量复制工具一般分成两类:基于应用层的流量复制工具和基于网络栈的流量复制工具。前者实现简单,但会挤 占线上应用的资源(比如连接资源,内存资源等),还可能会因为耦合度高而影响正常业务。而基于网络栈的流 量复制工具,直接从链路层抓取数据包,对应用影响较小,但是其实现也就相对复杂一些。
我们本篇要分析的模块 ngx_http_mirror_module 在 Nginx 1.13.4 中引入,它是一种应用层的流量复制 工具。该模块 [1] 目前只实现了两个配置指令,用法相当简单:
location / { mirror /mirror; proxy_pass } location /mirror { internal; proxy_pass http://test_backend$request_uri; }
每一条 mirror 配置项对应用户请求的一个副本,我们就可以通过配置多次 mirror 指令来实现 “流量 放大” 的效果。当然,你也可以将多个副本转发给不同的后端目标系统。
接下来,我们从源代码角度分析一下该模块是如何实现的,以及它可能存在有哪些问题。
模块代码分析
Nginx 在主请求的 PRECONTENT 阶段创建「后台子请求」(更多对「子请求」的介绍,请移步 这里 ),然后这些「后台子请求」使 用同一虚拟主机的其它 location {} 将请求数据副本发送给目标系统。
配置指令解析
ngx_http_mirror_module 模块提供的两个配置指令 mirror 和 mirror_request_body 可以用在配 置文件的 http {} 、 server {} 和 location {} 三类作用域中。
mirror 配置指令可以在某个配置作用域中出现多次,它属于 「数组类配置项」 。 当内层作用域没有显式使用 mirror 配置项时,会从外层继承相关配置。
static char * ngx_http_mirror(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ... if (mlcf->mirror == NGX_CONF_UNSET_PTR) { mlcf->mirror = ngx_array_create(cf->pool, 4, sizeof(ngx_str_t)); ... } s = ngx_array_push(mlcf->mirror); ... *s = value[1]; ... } static char * ngx_http_mirror_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ... ngx_conf_merge_ptr_value(conf->mirror, prev->mirror, NULL); ... }
mirror_request_body 配置指令是一个 「一般配置项」 。 Nginx 根据它的值决定是否将主请求的请求包体转发给目标服务。它的解析和继承流程和 mirror 相似,此 处不再赘述。
注册处理函数
该模块工作于 *PRECONTENT* 阶段,这个阶段本身也是 1.13.4 版本刚刚加入到 Nginx 中的。模块提供的 phase handler ngx_http_mirror_handler 在 Nginx 进程启动阶段被注册到 PRECONTENT 阶段:
static ngx_int_t ngx_http_mirror_init(ngx_conf_t *cf) { ... h = ngx_array_push(&cmcf->phases[NGX_HTTP_PRECONTENT_PHASE].handlers); ... *h = ngx_http_mirror_handler; ... }
复制用户请求
接下来进入关键环节:当主请求的处理流程进行到 PRECONTENT 阶段时,Nginx 会调用 ngx_http_mirror_handler 检查是否需要复制当前请求,决定是否复制请求包体和创建「后台子请求」开始流 量复制流程。
我们按照 phase handler 的执行流程逐段分析相关代码。
当前请求非主请求,或者当前作用域并未配置 mirror 指令的话,不处理当前请求。
# ngx_http_mirror_handler if (r != r->main) { return NGX_DECLINED; } mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mirror_module); if (mlcf->mirror == NULL) { return NGX_DECLINED; }
如果需要连同请求包体一起复制,那么在创建「后台子请求」之前,Nginx 需要接收完整请求包体。
- Nginx 使用函数 ngx_http_read_client_request_body 读取请求包体,它会开启一个新的异步流程 (增加主请求引用计数)。同时,因为当前我们除了接收请求包体外,该主请求上并无其它任务进行,所以 Nginx 可以暂时停止主请求处理流程( ngx_http_finalize_request(r, NGX_DONE) )。待接收包体的 异步函数完成后,Nginx 再在函数 ngx_http_mirror_body_handler 中恢复主请求处理流程。
- 如果并不需要复制请求包体,Nginx 则直接调用函数 ngx_http_mirror_handler_internal 创建「后 台子请求」开始请求复制流程,并恢复主请求正常处理流程。
# ngx_http_mirror_handler if (mlcf->request_body) { ... rc = ngx_http_read_client_request_body(r, ngx_http_mirror_body_handler); ... ngx_http_finalize_request(r, NGX_DONE); return NGX_DONE; } return ngx_http_mirror_handler_internal(r);
请求包体收取完成后,Nginx 调用函数 ngx_http_mirror_body_handler 创建「后台子请求」,并恢复原 主请求处理流程。
- 模块通过设置主请求的 r->preserve_body = 1 防止主请求处理完成后删除请求包体所在的临时文 件,避免还未完成的「后台子请求」无请求包体可用。
- 函数 ngx_http_mirror_handler 的返回值 NGX_DONE,会让主请求再被再次调度时(由下面的函 数 ngx_http_core_run_phases 触发),仍旧从 PRECONTENT 阶段恢复处理流程。
# ngx_http_mirror_body_handler ctx->status = ngx_http_mirror_handler_internal(r); r->preserve_body = 1; r->write_event_handler = ngx_http_core_run_phases; ngx_http_core_run_phases(r);
函数 ngx_http_mirror_handler_internal 创建「后台子请求」,开始流量复制流程。
- 它为每个 mirror 配置指令创建一个对应的「后台子请求」。子请求使用和主请求相同的 HTTP 方法。
- 为「后台子请求」设置 r->header_only,这样会带来一个问题:如果「后台子请求」是所在连接上的最 后一个请求的话,Nginx 一旦接收完它的响应包头就会关闭上下游连接,这样可能会造成目标系统因无 法正常发送响应数据而报错(比如响应数据包体较大时)。
# ngx_http_mirror_handler_internal mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mirror_module); name = mlcf->mirror->elts; for (i = 0; i < mlcf->mirror->nelts; i++) { if (ngx_http_subrequest(r, &name[i], &r->args, &sr, NULL, NGX_HTTP_SUBREQUEST_BACKGROUND) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } sr->header_only = 1; sr->method = r->method; sr->method_name = r->method_name; }
以上就是模块 ngx_http_mirror_module 进行流量复制的主要逻辑了。
目前实现限制
从上面的分析,我们可以看到,模块 ngx_http_mirror_module 的使用和实现都很简单,并且其流量复制过 程也不会影响当前主请求的响应时间。但是它还是有一些局限性的:
- 模块 ngx_http_mirror_module 属于应用层流量复制,它不可避免的会占用 Nginx 连接资源。同时,虽 然它不会阻塞主请求,但是「后台子请求」依然会保持对主请求的引用,就可能会造成主请求占用内存不能被 及时回收。更严重的问题是,如果主请求使用了 keepalive 包头开启了长连接模式时,「后台子请求」对主请 求的引用可能会造成复用此连接的下一个主请求不能被及时接收处理,从而会降低 Nginx 整体吞吐率。
- 由于 Nginx 不会为「后台子请求」接收上游完整响应数据,可能会造成上游数据发送失败,从而造成目标业务 处理异常。
总结下来,目前并不建议在流量、负载比较高的生产环境中使用该模块。如果在这些系统中有实时流量复制需 求的话,还是使用 goreplay 和 tcpcopy 这样的网络栈系工具比较妥当。
Comments
不要轻轻地离开我,请留下点什么...