Skip to content

Commit b71d186

Browse files
bugfix: buffer bloat and CPU 100% when download large file was filtered by body_filter_by_lua.
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 output chain. The output chain becomes longer and longer. Function ngx_chain_update_chains do an linear iteration of the output chain to put the new buf to the end of the chain. At the end, there are thounsands of bufs in the output chain which cause the CPU utilization become 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 8981872 commit b71d186

5 files changed

+150
-24
lines changed

.travis.yml

Lines changed: 4 additions & 0 deletions
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

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

275315
if (ctx->cleanup == NULL) {
@@ -286,38 +326,57 @@ ngx_http_lua_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
286326
old_context = ctx->context;
287327
ctx->context = NGX_HTTP_LUA_CONTEXT_BODY_FILTER;
288328

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

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

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

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

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

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

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

313-
/* in != out */
314373
rc = ngx_http_next_body_filter(r, out);
315374
if (rc == NGX_ERROR) {
316375
return NGX_ERROR;
317376
}
318377

319378
ngx_chain_update_chains(r->pool,
320-
&ctx->free_bufs, &ctx->busy_bufs, &out,
379+
&ctx->free_bufs, &ctx->filter_busy_bufs, &out,
321380
(ngx_buf_tag_t) &ngx_http_lua_module);
322381

323382
return rc;

src/ngx_http_lua_common.h

Lines changed: 3 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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 ? "" : "not ");
251+
247252
dd("downstream write: %d, buf len: %d", (int) rc,
248253
(int) (b->last - b->pos));
249254

t/082-body-filter-2.t

Lines changed: 55 additions & 0 deletions
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}='world'
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)