lua-nginx-module icon indicating copy to clipboard operation
lua-nginx-module copied to clipboard

unflush buffer of main request would trigger repeatedly buffer appends into the busy buffer list from cosocket send

Open kingluo opened this issue 3 years ago • 0 comments

Version: openresty-1.21.4.1

Descritpion

If you do ngx.say without ngx.flush, and use cosockets to send messages, the message buffers would be appended to the busy buffer list and do not get released until the main request finish. The busy buffer list would become bigger and bigger, and slow down the send operation dramatically.

Reproduce this issue

nginx.conf

error_log /dev/stderr info;
worker_processes auto;

events {}

http {
    server {
        listen 50000;

        location / {
            content_by_lua_block {
                ngx.say("hello")
                --ngx.flush(true)

                local sock = ngx.socket.tcp()
                local ok, err = sock:connect("unix:/tmp/echo.sock")
                assert(ok)
                ngx.update_time()
                local t1 = ngx.now()
                for _ = 1,100000 do
                    local sent = sock:send("hello")
                    local data = sock:receive(5)
                    assert(data=="hello")
                end
                ngx.log(ngx.INFO, "uds finish")
                ngx.update_time()
                local t2 = ngx.now()
                ngx.say("uds: ", t2-t1)
                sock:close()
                ngx.exit(200)
            }
        }
    }
}

/tmp/echo.go

package main

import (
    "io"
    "log"
    "net"
    "os"
)

const SockAddr = "/tmp/echo.sock"

func echoServer(c net.Conn) {
    log.Println("* start echo io")
    n, _ := io.Copy(c, c)
    log.Println("* end echo io, copy: ", n)
    c.Close()
}

func main() {
    log.Println("start unix server")
    if err := os.RemoveAll(SockAddr); err != nil {
        log.Fatal(err)
    }

    l, err := net.Listen("unix", SockAddr)
    if err != nil {
        log.Fatal("listen error:", err)
    }
    defer l.Close()
    for {
        log.Println("accept...")
        conn, err := l.Accept()
        if err != nil {
            log.Fatal("accept error:", err)
        }

        go echoServer(conn)
    }
}

If I don't flush the response, then the unix socket client bencharmk is slow:

# run nginx
nginx -p $PWD -c nginx.conf -g "daemon off;"

# run echo server
go run /tmp/echo.go

# curl localhost:50000
hello
uds: 54.944999933243

If I uncomment ngx.flush(true), then the benchmark is correct.

# curl localhost:50000
hello
uds: 2.289999961853

Locate the reason of this issue

Use perf top to check the hotspot: ngx_chain_update_chains.

       │    {
  0.00 │      endbr64
       │    ngx_chain_t  *cl;
       │
       │    if (*out) {
       │      mov     (%rcx),%r10
  0.01 │      mov     (%rdx),%rax
       │      test    %r10,%r10
       │    ↓ je      56
       │    if (*busy == NULL) {
       │      test    %rax,%rax
       │    ↓ je      b8
       │      nop
       │    *busy = *out;
       │
       │    } else {
       │    for (cl = *busy; cl->next; cl = cl->next) { /* void */ }
  5.95 │20:   mov     %rax,%r9
  0.02 │      mov     0x8(%rax),%rax
 93.94 │      test    %rax,%rax
       │    ↑ jne     20
       │
       │    cl->next = *out;
  0.01 │      mov     %r10,0x8(%r9)
       │    }

Use gdb to check which invokes ngx_chain_update_chains :

(gdb) bt
#0  0x00005621f736fc07 in ngx_chain_update_chains (p=0x5621f9038850, free=free@entry=0x5621f90397b8, busy=busy@entry=0x5621f90397c0, out=out@entry=0x7fa1ad4d8f68,
    tag=0x5621f75797a0 <ngx_http_lua_module>) at src/core/ngx_buf.c:195
#1  0x00005621f74710d4 in ngx_http_lua_socket_send (r=r@entry=0x5621f90388a0, u=u@entry=0x7fa1ad4d8de0) at ../ngx_lua-0.10.21/src/ngx_http_lua_socket_tcp.c:3388
#2  0x00005621f7471940 in ngx_http_lua_socket_tcp_send (L=0x7fa1ad4dc4e8) at ../ngx_lua-0.10.21/src/ngx_http_lua_socket_tcp.c:2935
#3  ngx_http_lua_socket_tcp_send (L=0x7fa1ad4dc4e8) at ../ngx_lua-0.10.21/src/ngx_http_lua_socket_tcp.c:2728
#4  0x00007fa1ae973044 in lj_BC_FUNCC () from /usr/local/openresty-debug/luajit/lib/libluajit-5.1.so.2
#5  0x00005621f7465501 in ngx_http_lua_run_thread (L=L@entry=0x7fa1ad4ea380, r=r@entry=0x5621f90388a0, ctx=ctx@entry=0x5621f90396a0, nrets=<optimized out>)
    at ../ngx_lua-0.10.21/src/ngx_http_lua_util.c:1185
#6  0x00005621f746e8b6 in ngx_http_lua_socket_tcp_resume_helper (r=0x5621f90388a0, socket_op=<optimized out>) at ../ngx_lua-0.10.21/src/ngx_http_lua_socket_tcp.c:6014
#7  0x00005621f7474c7f in ngx_http_lua_socket_handle_read_success (u=0x7fa1ad4d8de0, r=0x5621f90388a0) at ../ngx_lua-0.10.21/src/ngx_http_lua_socket_tcp.c:3511
#8  ngx_http_lua_socket_tcp_read (r=0x5621f90388a0, u=0x7fa1ad4d8de0) at ../ngx_lua-0.10.21/src/ngx_http_lua_socket_tcp.c:2522
#9  0x00005621f746c944 in ngx_http_lua_socket_tcp_handler (ev=0x5621f9085870) at ../ngx_lua-0.10.21/src/ngx_http_lua_socket_tcp.c:3265
...

The reason is: The busy buffer list is shared by the main request and cosockets created from the same main request. If the head part of the list (unflush ngx.say) keep still, the following of the list would not be cleared and get accumulated when you send more and more message via the cosocket).

How to verify the reason?

Use gdb to evaluate the breakpoint context:

b ngx_chain_update_chains
    command 1
    set $v = *busy
    set $cnt = 0
    while ($v != 0)
        set $cnt++
        set $v=$v->next
    end
    p $cnt
    continue
end

Output:

Breakpoint 1, ngx_chain_update_chains (p=0x55a53413e850, free=free@entry=0x55a53413f7b8, busy=busy@entry=0x55a53413f7c0, out=out@entry=0x7f80f353fe50, tag=0x55a5326887a0 <ngx_http_lua_module>) at src/core/ngx_buf.c:187
187     {
$11 = 80399

Breakpoint 1, ngx_chain_update_chains (p=0x55a53413e850, free=free@entry=0x55a53413f7b8, busy=busy@entry=0x55a53413f7c0, out=out@entry=0x7f80f353fe50, tag=0x55a5326887a0 <ngx_http_lua_module>) at src/core/ngx_buf.c:187
187     {
$12 = 80400

Breakpoint 1, ngx_chain_update_chains (p=0x55a53413e850, free=free@entry=0x55a53413f7b8, busy=busy@entry=0x55a53413f7c0, out=out@entry=0x7f80f353fe50, tag=0x55a5326887a0 <ngx_http_lua_module>) at src/core/ngx_buf.c:187
187     {
$13 = 80401

Breakpoint 1, ngx_chain_update_chains (p=0x55a53413e850, free=free@entry=0x55a53413f7b8, busy=busy@entry=0x55a53413f7c0, out=out@entry=0x7f80f353fe50, tag=0x55a5326887a0 <ngx_http_lua_module>) at src/core/ngx_buf.c:187
187     {
$14 = 80402

Breakpoint 1, ngx_chain_update_chains (p=0x55a53413e850, free=free@entry=0x55a53413f7b8, busy=busy@entry=0x55a53413f7c0, out=out@entry=0x7f80f353fe50, tag=0x55a5326887a0 <ngx_http_lua_module>) at src/core/ngx_buf.c:187
187     {
$15 = 80403

The busy buffer and free buffer mechanism is used to pool and reuse the buffers. But this bug seems exists for a long time. The workaround is to do ngx.flush each time you do ngx.say in my scenario.

@zhuizhuhaomeng @agentzh

kingluo avatar Sep 18 '22 09:09 kingluo