Skip to content

Commit 3820d0e

Browse files
bugfix: buffer bloat and CPU 100% when download large file was filtered by body_filter_by_lua. (#1909)
When http body was change by lua script, ngx_http_lua_body_filter_param_set will mark the origin buf as consumed with code "cl->buf->pos = cl->buf->last". Because the buf was marked as consumed, the function ngx_output_chain will continue to write data to the output filter. When kernel socket send buffer is full, write to the socket will return EAGAIN. And the output buf will be cached in the output chain. The output chain becomes longer and longer. Function ngx_chain_update_chains do a linear iteration of the output chain to put the new buf to the end of the chain. In the end, there are thousands of buffs in the output chain which causes the CPU utilization becomes 100% and memory continue to grow up. nginx.conf: location /download { alias download; header_filter_by_lua_block { ngx.header.content_length = nil } body_filter_by_lua_block { ngx.arg[1]=ngx.arg[1] } } create a 2G file with the following cmd: mkdir -p /usr/local/openresty/nginx/download dd if=/dev/zero of=/usr/local/openresty/nginx/download/2G count=4096000 reproduce with the following cmd: curl -o /dev/null http://127.0.0.1/download/2G typical call stacks: 42706f: ngx_chain_update_chains[14] 47e993: ngx_http_chunked_body_filter[14] 483024: ngx_http_gzip_body_filter[14] 4864d3: ngx_http_ssi_body_filter[14] 489668: ngx_http_charset_body_filter[14] 48b86c: ngx_http_addition_body_filter[14] 48bf9c: ngx_http_gunzip_body_filter[14] 48d75e: ngx_http_trailers_filter[14] 4fb63c: ngx_http_lua_capture_body_filter[14] 509cb9: ngx_http_lua_body_filter[14] 42739e: ngx_output_chain[14] 48e1aa: ngx_http_copy_filter[14] 45ea2b: ngx_http_output_filter[14] 496569: ngx_http_static_handler[14] 45ec6e: ngx_http_core_content_phase[14] 459a25: ngx_http_core_run_phases[14] 463ed3: ngx_http_process_request[14] 4643ff: ngx_http_process_request_headers[14] 4647a6: ngx_http_process_request_line[14] 44c1fe: ngx_epoll_process_events[14] 4431c3: ngx_process_events_and_timers[14] 44a65a: ngx_worker_process_cycle[14] 449074: ngx_spawn_process[14] 44aaac: ngx_start_worker_processes[14] 44b213: ngx_master_process_cycle[14] 422df2: main[14] 23493: __libc_start_main[2] 422e5e: _start[14] 42706f: ngx_chain_update_chains[14] 509ce7: ngx_http_lua_body_filter[14] 42739e: ngx_output_chain[14] 48e1aa: ngx_http_copy_filter[14] 45ea2b: ngx_http_output_filter[14] 496569: ngx_http_static_handler[14] 45ec6e: ngx_http_core_content_phase[14] 459a25: ngx_http_core_run_phases[14] 463ed3: ngx_http_process_request[14] 4643ff: ngx_http_process_request_headers[14] 4647a6: ngx_http_process_request_line[14] 44c1fe: ngx_epoll_process_events[14] 4431c3: ngx_process_events_and_timers[14] 44a65a: ngx_worker_process_cycle[14] 449074: ngx_spawn_process[14] 44aaac: ngx_start_worker_processes[14] 44b213: ngx_master_process_cycle[14] 422df2: main[14] 23493: __libc_start_main[2] 422e5e: _start[14] 47d51f: ngx_http_write_filter[14] 47e973: ngx_http_chunked_body_filter[14] 483024: ngx_http_gzip_body_filter[14] 4864d3: ngx_http_ssi_body_filter[14] 489668: ngx_http_charset_body_filter[14] 48b86c: ngx_http_addition_body_filter[14] 48bf9c: ngx_http_gunzip_body_filter[14] 48d75e: ngx_http_trailers_filter[14] 4fb63c: ngx_http_lua_capture_body_filter[14] 509cb9: ngx_http_lua_body_filter[14] 42739e: ngx_output_chain[14] 48e1aa: ngx_http_copy_filter[14] 45ea2b: ngx_http_output_filter[14] 496569: ngx_http_static_handler[14] 45ec6e: ngx_http_core_content_phase[14] 459a25: ngx_http_core_run_phases[14] 463ed3: ngx_http_process_request[14] 4643ff: ngx_http_process_request_headers[14] 4647a6: ngx_http_process_request_line[14] 44c1fe: ngx_epoll_process_events[14] 4431c3: ngx_process_events_and_timers[14] 44a65a: ngx_worker_process_cycle[14] 449074: ngx_spawn_process[14] 44aaac: ngx_start_worker_processes[14] 44b213: ngx_master_process_cycle[14] 422df2: main[14] 23493: __libc_start_main[2] 422e5e: _start[14]
1 parent 86514c1 commit 3820d0e

5 files changed

+148
-24
lines changed

.travis.yml

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
dist: xenial
22

3+
branches:
4+
only:
5+
- "master"
6+
37
os: linux
48

59
language: c

src/ngx_http_lua_bodyfilterby.c

+81-24
Original file line numberDiff line numberDiff line change
@@ -235,18 +235,15 @@ ngx_http_lua_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
235235
uint16_t old_context;
236236
ngx_http_cleanup_t *cln;
237237
ngx_chain_t *out;
238+
ngx_chain_t *cl, *ln;
238239
ngx_http_lua_main_conf_t *lmcf;
239240

240241
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
241242
"lua body filter for user lua code, uri \"%V\"", &r->uri);
242243

243-
if (in == NULL) {
244-
return ngx_http_next_body_filter(r, in);
245-
}
246-
247244
llcf = ngx_http_get_module_loc_conf(r, ngx_http_lua_module);
248245

249-
if (llcf->body_filter_handler == NULL) {
246+
if (llcf->body_filter_handler == NULL || r->header_only) {
250247
dd("no body filter handler found");
251248
return ngx_http_next_body_filter(r, in);
252249
}
@@ -269,7 +266,48 @@ ngx_http_lua_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
269266
in->buf->file_pos = in->buf->file_last;
270267
}
271268

272-
return NGX_OK;
269+
in = NULL;
270+
271+
/* continue to call ngx_http_next_body_filter to process cached data */
272+
}
273+
274+
if (in != NULL
275+
&& ngx_chain_add_copy(r->pool, &ctx->filter_in_bufs, in) != NGX_OK) {
276+
return NGX_ERROR;
277+
}
278+
279+
if (ctx->filter_busy_bufs != NULL
280+
&& (r->connection->buffered
281+
& (NGX_HTTP_LOWLEVEL_BUFFERED | NGX_LOWLEVEL_BUFFERED)))
282+
{
283+
/* Socket write buffer was full on last write.
284+
* Try to write the remain data, if still can not write
285+
* do not execute body_filter_by_lua otherwise the `in` chain will be
286+
* replaced by new content from lua and buf of `in` mark as consumed.
287+
* And then ngx_output_chain will call the filter chain again which
288+
* make all the data cached in the memory and long ngx_chain_t link
289+
* cause CPU 100%.
290+
*/
291+
rc = ngx_http_next_body_filter(r, NULL);
292+
293+
if (rc == NGX_ERROR) {
294+
return rc;
295+
}
296+
297+
out = NULL;
298+
ngx_chain_update_chains(r->pool,
299+
&ctx->free_bufs, &ctx->filter_busy_bufs, &out,
300+
(ngx_buf_tag_t) &ngx_http_lua_module);
301+
if (rc != NGX_OK
302+
&& ctx->filter_busy_bufs != NULL
303+
&& (r->connection->buffered
304+
& (NGX_HTTP_LOWLEVEL_BUFFERED | NGX_LOWLEVEL_BUFFERED))) {
305+
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
306+
"waiting body filter busy buffer to be sent");
307+
return NGX_AGAIN;
308+
}
309+
310+
/* continue to process bufs in ctx->filter_in_bufs */
273311
}
274312

275313
if (ctx->cleanup == NULL) {
@@ -286,38 +324,57 @@ ngx_http_lua_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
286324
old_context = ctx->context;
287325
ctx->context = NGX_HTTP_LUA_CONTEXT_BODY_FILTER;
288326

289-
dd("calling body filter handler");
290-
rc = llcf->body_filter_handler(r, in);
327+
in = ctx->filter_in_bufs;
328+
ctx->filter_in_bufs = NULL;
291329

292-
dd("calling body filter handler returned %d", (int) rc);
330+
if (in != NULL) {
331+
dd("calling body filter handler");
332+
rc = llcf->body_filter_handler(r, in);
293333

294-
ctx->context = old_context;
334+
dd("calling body filter handler returned %d", (int) rc);
295335

296-
if (rc != NGX_OK) {
297-
return NGX_ERROR;
298-
}
336+
ctx->context = old_context;
299337

300-
lmcf = ngx_http_get_module_main_conf(r, ngx_http_lua_module);
301-
out = lmcf->body_filter_chain;
338+
if (rc != NGX_OK) {
339+
return NGX_ERROR;
340+
}
302341

303-
if (in == out) {
304-
return ngx_http_next_body_filter(r, in);
305-
}
342+
lmcf = ngx_http_get_module_main_conf(r, ngx_http_lua_module);
343+
344+
/* lmcf->body_filter_chain is the new buffer chain if
345+
* body_filter_by_lua set new body content via ngx.arg[1] = new_content
346+
* otherwise it is the original `in` buffer chain.
347+
*/
348+
out = lmcf->body_filter_chain;
349+
350+
if (in != out) {
351+
/* content of body was replaced in
352+
* ngx_http_lua_body_filter_param_set and the buffers was marked
353+
* as consumed.
354+
*/
355+
for (cl = in; cl != NULL; cl = ln) {
356+
ln = cl->next;
357+
ngx_free_chain(r->pool, cl);
358+
}
306359

307-
if (out == NULL) {
308-
/* do not forward NULL to the next filters because the input is
309-
* not NULL */
310-
return NGX_OK;
360+
if (out == NULL) {
361+
/* do not forward NULL to the next filters because the input is
362+
* not NULL */
363+
return NGX_OK;
364+
}
365+
}
366+
367+
} else {
368+
out = NULL;
311369
}
312370

313-
/* in != out */
314371
rc = ngx_http_next_body_filter(r, out);
315372
if (rc == NGX_ERROR) {
316373
return NGX_ERROR;
317374
}
318375

319376
ngx_chain_update_chains(r->pool,
320-
&ctx->free_bufs, &ctx->busy_bufs, &out,
377+
&ctx->free_bufs, &ctx->filter_busy_bufs, &out,
321378
(ngx_buf_tag_t) &ngx_http_lua_module);
322379

323380
return rc;

src/ngx_http_lua_common.h

+3
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,9 @@ typedef struct ngx_http_lua_ctx_s {
548548
ngx_chain_t *busy_bufs;
549549
ngx_chain_t *free_recv_bufs;
550550

551+
ngx_chain_t *filter_in_bufs; /* for the body filter */
552+
ngx_chain_t *filter_busy_bufs; /* for the body filter */
553+
551554
ngx_http_cleanup_pt *cleanup;
552555

553556
ngx_http_cleanup_t *free_cleanup; /* free list of cleanup records */

src/ngx_http_lua_output.c

+5
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ ngx_http_lua_ngx_echo(lua_State *L, unsigned newline)
244244
return 2;
245245
}
246246

247+
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
248+
"%s has %sbusy bufs",
249+
newline ? "lua say response" : "lua print response",
250+
ctx->busy_bufs != NULL ? "" : "no ");
251+
247252
dd("downstream write: %d, buf len: %d", (int) rc,
248253
(int) (b->last - b->pos));
249254

t/082-body-filter-2.t

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# vim:set ft= ts=4 sw=4 et fdm=marker:
2+
3+
BEGIN {
4+
$ENV{TEST_NGINX_POSTPONE_OUTPUT} = 1;
5+
$ENV{TEST_NGINX_EVENT_TYPE} = 'poll';
6+
$ENV{MOCKEAGAIN}='w'
7+
}
8+
9+
use Test::Nginx::Socket::Lua;
10+
11+
#worker_connections(1014);
12+
#master_process_enabled(1);
13+
#log_level('warn');
14+
15+
log_level('debug');
16+
17+
repeat_each(2);
18+
19+
plan tests => repeat_each() * (blocks() * 5);
20+
21+
#no_diff();
22+
no_long_string();
23+
24+
run_tests();
25+
26+
__DATA__
27+
28+
=== TEST 1: check ctx->busy_bufs
29+
--- config
30+
location /t {
31+
postpone_output 1;
32+
content_by_lua_block {
33+
for i = 1, 5 do
34+
ngx.say(i, ": Hello World!")
35+
end
36+
}
37+
38+
body_filter_by_lua_block {
39+
ngx.arg[1] = ngx.arg[1]
40+
}
41+
}
42+
--- request
43+
GET /t
44+
--- response_body
45+
1: Hello World!
46+
2: Hello World!
47+
3: Hello World!
48+
4: Hello World!
49+
5: Hello World!
50+
51+
--- error_log
52+
waiting body filter busy buffer to be sent
53+
lua say response has busy bufs
54+
--- no_error_log
55+
[error]

0 commit comments

Comments
 (0)