AlinsRan commented on issue #11244:
URL: https://github.com/apache/apisix/issues/11244#issuecomment-4784923234

   ### Root cause
   
   The growth comes from how `core.response.hold_body_chunk` accumulates the 
response body when a logger has `include_resp_body = true`.
   
   For every `body_filter` chunk it appends the chunk to a Lua table and, once 
`max_resp_body_bytes` is reached (or on eof), concatenates the whole table with 
`table.concat`:
   
   ```lua
   body_buffer = { chunk, n = 1, bytes = #chunk }   -- first chunk
   ...
   body_buffer[n] = chunk                            -- subsequent chunks
   ...
   local body_data = concat_tab(body_buffer, "", 1, body_buffer.n)
   body_data = str_sub(body_data, 1, max_resp_body_bytes)
   ```
   
   Two things make this expensive for large bodies (the ~128K in this report, 
and worse under concurrency):
   
   1. At the moment of `concat`, both the **table of all chunks** and the 
**full concatenated string** are alive at the same time — roughly 2x the body 
size per in-flight request.
   2. `concat` + `str_sub` each allocate a fresh string, so the truncation path 
allocates the whole concatenated body and then a second truncated copy.
   
   Under load (e.g. `wrk -c100`), many requests hold these buffers 
simultaneously, so the transient allocations pile up and the LuaJIT/GC arena 
does not return the memory to the OS — which matches the "grows and never 
drops" behavior reported here. It is allocation/retention pressure rather than 
a true unbounded leak.
   
   ### Suggested fix
   
   Accumulate into a LuaJIT `string.buffer` instead of a Lua table:
   
   ```lua
   local str_buffer = require("string.buffer")
   ...
   body_buffer = { buf = str_buffer.new(), bytes = 0 }
   ...
   body_buffer.buf:put(chunk)
   body_buffer.bytes = body_buffer.bytes + #chunk
   ...
   -- truncation: get() returns exactly the first N bytes, buffer is dropped 
after
   local body_data = body_buffer.buf:get(max_resp_body_bytes)
   ```
   
   `buffer:put` appends without keeping every chunk reference, and 
`buffer:get(n)` produces the final (optionally truncated) string in one step, 
so the chunk table + intermediate concatenated copy are no longer held at once. 
All existing semantics (`hold_the_copy`, `max_resp_body_bytes` truncation, the 
`done` flag, the single-chunk eof path) are preserved.
   
   Note: #12035 touched the same function but was closed without landing. I can 
put up a PR with the `string.buffer` approach above plus tests covering 
multi-chunk accumulation, byte truncation across chunks, the `done` flag and 
small-body passthrough.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to