[PATCH] HTTP/2: fixed sendfile() aio handling

Sergey Kandaurov pluknet at nginx.com
Thu Nov 25 13:15:09 UTC 2021


> On 24 Nov 2021, at 22:06, Maxim Dounin <mdounin at mdounin.ru> wrote:
> 
> Hello!
> 
> On Wed, Nov 24, 2021 at 06:50:10PM +0300, Sergey Kandaurov wrote:
> 
>>> On 24 Nov 2021, at 06:46, Maxim Dounin <mdounin at mdounin.ru> wrote:
>>> 
>>> On Tue, Nov 23, 2021 at 10:44:00AM +0300, Sergey Kandaurov wrote:
>>> 
>>>>> On 23 Nov 2021, at 06:12, Maxim Dounin <mdounin at mdounin.ru> wrote:
>>>>> 
>>>>> On Thu, Nov 18, 2021 at 07:46:48PM +0300, Sergey Kandaurov wrote:
>>>>> 
>>>>>>> On 16 Nov 2021, at 17:41, Maxim Dounin <mdounin at mdounin.ru> wrote:
>>>>>>> 
>>>>>>> On Tue, Nov 16, 2021 at 02:59:46PM +0300, Sergey Kandaurov wrote:
>>>>>>> 
>>>>>>>>> On 11 Nov 2021, at 06:10, Maxim Dounin <mdounin at mdounin.ru> wrote:
>>>>>>>>> 
>>>>>>>>> # HG changeset patch
>>>>>>>>> # User Maxim Dounin <mdounin at mdounin.ru>
>>>>>>>>> # Date 1636599377 -10800
>>>>>>>>> #      Thu Nov 11 05:56:17 2021 +0300
>>>>>>>>> # Node ID 76e072a6947a221868705c13973de15319c0d921
>>>>>>>>> # Parent  82b750b20c5205d685e59031247fe898f011394e
>>>>>>>>> HTTP/2: fixed sendfile() aio handling.
>>>>>>>>> 
>>>>>>>>> With sendfile() in threads ("aio threads; sendfile on;"), client connection
>>>>>>>>> can block on writing, waiting for sendfile() to complete.  In HTTP/2 this
>>>>>>>>> might result in the request hang, since an attempt to continue processig
>>>>>>>> 
>>>>>>>> processing
>>>>>>> 
>>>>>>> Fixed, thnx.
>>>>>>> 
>>>>>>>>> in thread event handler will call request's write event handler, which
>>>>>>>>> is usually stopped by ngx_http_v2_send_chain(): it does nothing if there
>>>>>>>>> are no additional data and stream->queued is set.  Further, HTTP/2 resets
>>>>>>>>> stream's c->write->ready to 0 if writing blocks, so just fixing
>>>>>>>>> ngx_http_v2_send_chain() is not enough.
>>>>>>>>> 
>>>>>>>>> Can be reproduced with test suite on Linux with:
>>>>>>>>> 
>>>>>>>>> TEST_NGINX_GLOBALS_HTTP="aio threads; sendfile on;" prove h2*.t
>>>>>>>>> 
>>>>>>>>> The following tests currently fail: h2_keepalive.t, h2_priority.t,
>>>>>>>>> h2_proxy_max_temp_file_size.t, h2.t, h2_trailers.t.
>>>>>>>>> 
>>>>>>>>> Similarly, sendfile() with AIO preloading on FreeBSD can block as well,
>>>>>>>>> with similar results.  This is, however, harder to reproduce, especially
>>>>>>>>> on modern FreeBSD systems, since sendfile() usually do not return EBUSY.
>>>>>>>> 
>>>>>>>> does not
>>>>>>> 
>>>>>>> Fixed, thnx.
>>>>>>> 
>>>>>>>>> Fix is to post a write event on HTTP/2 connection in the thread event
>>>>>>>>> handler (and aio preload handler).  This ensures that sendfile() will be
>>>>>>>>> completed and stream processing will be resumed by HTTP/2 code.
>>>>>>>>> 
>>>>>>>>> diff --git a/src/http/ngx_http_copy_filter_module.c b/src/http/ngx_http_copy_filter_module.c
>>>>>>>>> --- a/src/http/ngx_http_copy_filter_module.c
>>>>>>>>> +++ b/src/http/ngx_http_copy_filter_module.c
>>>>>>>>> @@ -250,6 +250,21 @@ ngx_http_copy_aio_sendfile_event_handler
>>>>>>>>> r->aio = 0;
>>>>>>>>> ev->complete = 0;
>>>>>>>>> 
>>>>>>>>> +#if (NGX_HTTP_V2)
>>>>>>>>> +
>>>>>>>>> +    if (r->stream) {
>>>>>>>>> +        /*
>>>>>>>>> +         * for HTTP/2, trigger a write event on the main connection
>>>>>>>>> +         * to handle sendfile() preload
>>>>>>>>> +         */
>>>>>>>>> +
>>>>>>>>> +        ngx_post_event(r->stream->connection->connection->write,
>>>>>>>>> +                       &ngx_posted_events);
>>>>>>>>> +        return;
>>>>>>>>> +    }
>>>>>>>>> +
>>>>>>>>> +#endif
>>>>>>>>> +
>>>>>>>>> r->connection->write->handler(r->connection->write);
>>>>>>>>> }
>>>>>>>>> 
>>>>>>>>> @@ -323,6 +338,20 @@ ngx_http_copy_thread_event_handler(ngx_e
>>>>>>>>> r->main->blocked--;
>>>>>>>>> r->aio = 0;
>>>>>>>>> 
>>>>>>>>> +#if (NGX_HTTP_V2)
>>>>>>>>> +
>>>>>>>>> +    if (r->stream) {
>>>>>>>>> +        /*
>>>>>>>>> +         * for HTTP/2, trigger a write event on the main connection
>>>>>>>>> +         * to handle sendfile() in threads
>>>>>>>>> +         */
>>>>>>>>> +
>>>>>>>>> +        ngx_post_event(r->stream->connection->connection->write,
>>>>>>>>> +                       &ngx_posted_events);
>>>>>>>>> +    }
>>>>>>>>> +
>>>>>>>>> +#endif
>>>>>>>>> +
>>>>> 
>>>>> [...]
>>>>> 
>>>>> For the record, while testing this patch Sergey found another 
>>>>> issue with sendfile() in threads and HTTP/2: since HTTP/2 might 
>>>>> call sendfile() within the main connection, bypassing request 
>>>>> filter chain, normal r->aio flag checking to prevent multiple 
>>>>> operations do not work, and this eventually results in "task 
>>>>> already active" alerts due to duplicate operations being posted.  
>>>>> With the above patch this issue is much more likely to happen, 
>>>>> since it intentionally triggers write events on the main HTTP/2 
>>>>> connection.
>>>>> 
>>>>> Below are two patches: the first one addresses the issue with 
>>>>> duplicate operations by additionally checking file->thread_task 
>>>>> before sendfile(), and the second one is a better alternative to 
>>>>> the above patch which doesn't post additional events on the main 
>>>>> connection.
>>>>> 
>>>>> # HG changeset patch
>>>>> # User Maxim Dounin <mdounin at mdounin.ru>
>>>>> # Date 1637635671 -10800
>>>>> #      Tue Nov 23 05:47:51 2021 +0300
>>>>> # Node ID 8a18b0bff1266db221fe35dc08f4483044ea0f86
>>>>> # Parent  82b750b20c5205d685e59031247fe898f011394e
>>>>> HTTP/2: fixed "task already active" with sendfile in threads.
>>>>> 
>>>>> With sendfile in threads, "task already active" alerts might appear in logs
>>>>> if a write event happens on the main HTTP/2 connection, triggering a sendfile
>>>>> in threads while another thread operation is already running.  Observed
>>>>> with "aio threads; aio_write on; sendfile on;" and with thread event handlers
>>>>> modified to post a write event to the main HTTP/2 connection (though can
>>>>> happen without any modifications).
>>>>> 
>>>>> Fix is to avoid starting a sendfile operation if file->thread_task indicates
>>>>> that another thread operation is active.
>>>>> 
>>>>> diff --git a/src/os/unix/ngx_linux_sendfile_chain.c b/src/os/unix/ngx_linux_sendfile_chain.c
>>>>> --- a/src/os/unix/ngx_linux_sendfile_chain.c
>>>>> +++ b/src/os/unix/ngx_linux_sendfile_chain.c
>>>>> @@ -324,6 +324,18 @@ ngx_linux_sendfile_thread(ngx_connection
>>>>>                  "linux sendfile thread: %d, %uz, %O",
>>>>>                  file->file->fd, size, file->file_pos);
>>>>> 
>>>>> +    task = file->file->thread_task;
>>>>> +
>>>>> +    if (task && task->event.active) {
>>>>> +        /*
>>>>> +         * with HTTP/2, another thread operation might be already running
>>>>> +         * if sendfile() is called as a result of a write event on the main
>>>>> +         * connection
>>>>> +         */
>>>>> +
>>>>> +        return NGX_DONE;
>>>>> +    }
>>>>> +
>>>>>   task = c->sendfile_task;
>>>>> 
>>>>>   if (task == NULL) {
>>> 
>>> After looking once again into it, I tend to think this patch is 
>>> incomplete.  In particular, the particular check won't stop 
>>> additional sendfile() if there are multiple files and different 
>>> files use different thread tasks.  While this is not something 
>>> possible with standard modules, but nevertheless.
>> 
>> Could you please clarify?
>> Is it something rather theoretical (and irrelevant to subrequests)?
>> Because there is a room for only one task in c->sendfile_task.
> 
> The code above checks file->file->thread_task, not 
> c->sendfile_task, so there can be many different tasks.
> 

Got it.

> This is not something about subrequests: multiple thread/aio 
> operations are allowed with subrequests as long as they are within 
> different subrequests (the r->aio flag is within a particular 
> subrequest).
> 
> The case I'm talking about can happen if a module returns multiple 
> buffers with different files, and these files use different 
> thread tasks (for non-sendfile thread operations).  This is not 
> something which can happen with the standard modules, but might 
> happen in theory.

Agreed.

> 
>>> Further, the same problem seems to apply to aio preloading (though 
>>> unlikely to happen in practice).
>>> 
>>> The following patch checks r->aio in relevant handlers to prevent 
>>> sendfile() when another operation is running:
>> 
>> As a positive effect, moving the check down to thread handlers allows
>> to complete a sendfile operation, within http2 write handler, after
>> thread task completion, since now the check is performed after the
>> "if (task->event.complete) {" logic in ngx_linux_sendfile_thread().
>> E.g.:
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 http2 write handler
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 http2 frame out: 000055BA978A5E28 sid:1 bl:0 len:8192
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 http2 frame out: 000055BA978A5D20 sid:1 bl:1 len:8192
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 linux sendfile thread: 15, 8192, 8794
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 linux sendfile thread: complete:1 err:0 sent:8192
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 no tcp_nodelay
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 tcp_nopush
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 writev: 9 of 9
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 linux sendfile thread: 15, 8192, 16986
>> 2021/11/24 15:16:43 [debug] 474666#474666: *1 linux sendfile thread: complete:0
> 
> While the side effect is expected, I don't think this is not 
> something important.  As far as I understand, you are looking at 
> what happens with the previous version of the initial patch with 
> ngx_post_event(), but in real world such a write event is 
> unlikely.  (Not to mention that this won't happen with HTTP/1.x, 
> and only applies to HTTP/2 without SSL.)

Correct.
I was not able to see any difference with the current series.

> 
>>> # HG changeset patch
>>> # User Maxim Dounin <mdounin at mdounin.ru>
>>> # Date 1637723745 -10800
>>> #      Wed Nov 24 06:15:45 2021 +0300
>>> # Node ID 3450841798597536d17ced29b35d1d90ce06ce0d
>>> # Parent  3443c02ca1d183fe52bf8af66627c94be2b2f785
>>> HTTP/2: fixed "task already active" with sendfile in threads.
>>> 
>>> With sendfile in threads, "task already active" alerts might appear in logs
>>> if a write event happens on the main HTTP/2 connection, triggering a sendfile
>>> in threads while another thread operation is already running.  Observed
>>> with "aio threads; aio_write on; sendfile on;" and with thread event handlers
>>> modified to post a write event to the main HTTP/2 connection (though can
>>> happen without any modifications).
>>> 
>>> Similarly, sendfile() with AIO preloading on FreeBSD can trigger duplicate
>>> aio operation, resulting in "second aio post" alerts.  This is, however,
>>> harder to reproduce, especially on modern FreeBSD systems, since sendfile()
>>> usually does not return EBUSY.
>>> 
>>> Fix is to avoid starting a sendfile operation if other thread operation
>>> is active by checking r->aio in the thread handler (and, similarly, in
>>> aio preload handler).
>>> 
>>> diff --git a/src/http/ngx_http_copy_filter_module.c b/src/http/ngx_http_copy_filter_module.c
>>> --- a/src/http/ngx_http_copy_filter_module.c
>>> +++ b/src/http/ngx_http_copy_filter_module.c
>>> @@ -219,6 +219,22 @@ ngx_http_copy_aio_sendfile_preload(ngx_b
>>>    ngx_http_request_t      *r;
>>>    ngx_output_chain_ctx_t  *ctx;
>>> 
>>> +#if (NGX_HTTP_V2)
>>> +
>>> +    r = file->file->aio->data;
>>> +
>>> +    if (r->aio) {
>>> +        /*
>>> +         * with HTTP/2, another thread operation might be already running
>>> +         * if sendfile() is called as a result of a write event on the main
>>> +         * connection
>>> +         */
>>> +
>>> +        return NGX_OK;
>>> +    }
>>> +
>>> +#endif
>>> +
>> 
>> Shouldn't this also check for r->stream to narrow it down to HTTP/2 ?
>> At least, it looks inconsistent taking the code is under NGX_HTTP_V2.
>> On the contrary, shouldn't this expand to non-HTTP/2 protocols as well?
> 
> I tend to think that correct approach would be to expand this to 
> non-HTTP/2 as well.  Without the HTTP/2 check, it will also catch 
> duplicate sendfile calls due to subrequests (currently not handled 
> by the aio preloading, and handled separately in sendfile in 
> threads).
> 
> Updated patch below.
> 
>>>    n = ngx_file_aio_read(file->file, buf, 1, file->file_pos, NULL);
>>> 
>>>    if (n == NGX_AGAIN) {
>>> @@ -270,6 +286,23 @@ ngx_http_copy_thread_handler(ngx_thread_
>>> 
>>>    r = file->thread_ctx;
>>> 
>>> +#if (NGX_HTTP_V2)
>>> +
>>> +    if (r->aio
>>> +        && r->stream
>>> +        && r->stream->connection->connection->sendfile_task == task)
>>> +    {
>> 
>> I'm not quite sure how the last part of condition is related (if at all)
>> to stop additional sendfile() for different files using different tasks,
>> as outlined above.
>> 
>> Also, looking at ngx_linux_sendfile_thread(), where c->sendfile_task is
>> initially allocated, I cannot imagine how the tasks won't match there.
>> I guess it is enough to check for a non-NULL sendfile_task.
> 
> The same thread handler can be used for different thread 
> operations, notably sendfile(), read(), or write().  The idea is 
> to check if the operation we are called for is actually sendfile() 
> in threads, and not thread read or write (which will use different 
> task).  With sendfile(), calls with r->aio set are expected, and 
> we can simply return NGX_OK to properly handle them.  With thread 
> read or write, simply returning NGX_OK likely will silently broke 
> things, so it's better to let it instead fail in 
> ngx_thread_task_post().

Ok.

> 
>>> +        /*
>>> +         * with HTTP/2, another thread operation might be already running
>>> +         * if sendfile() is called as a result of a write event on the main
>>> +         * connection
>>> +         */
>>> +
>>> +        return NGX_OK;
>>> +    }
>>> +
>>> +#endif
>>> +
>>>    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
>>>    tp = clcf->thread_pool;
>>> 
>>> diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c
>>> --- a/src/http/ngx_http_upstream.c
>>> +++ b/src/http/ngx_http_upstream.c
>>> @@ -3854,6 +3854,23 @@ ngx_http_upstream_thread_handler(ngx_thr
>>>    r = file->thread_ctx;
>>>    p = r->upstream->pipe;
>>> 
>>> +#if (NGX_HTTP_V2)
>>> +
>>> +    if (r->aio
>>> +        && r->stream
>>> +        && r->stream->connection->connection->sendfile_task == task)
>>> +    {
>>> +        /*
>>> +         * with HTTP/2, another thread operation might be already running
>>> +         * if sendfile() is called as a result of a write event on the main
>>> +         * connection
>>> +         */
>>> +
>>> +        return NGX_OK;
>>> +    }
>>> +
>>> +#endif
>>> +
>>>    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
>>>    tp = clcf->thread_pool;
>>> 
> 
> Updated patch:
> 
> # HG changeset patch
> # User Maxim Dounin <mdounin at mdounin.ru>
> # Date 1637774456 -10800
> #      Wed Nov 24 20:20:56 2021 +0300
> # Node ID 80c8892153bef7edec591cd1af37237b22d6d0c5
> # Parent  3443c02ca1d183fe52bf8af66627c94be2b2f785
> HTTP/2: fixed "task already active" with sendfile in threads.
> 
> With sendfile in threads, "task already active" alerts might appear in logs
> if a write event happens on the main HTTP/2 connection, triggering a sendfile
> in threads while another thread operation is already running.  Observed
> with "aio threads; aio_write on; sendfile on;" and with thread event handlers
> modified to post a write event to the main HTTP/2 connection (though can
> happen without any modifications).
> 
> Similarly, sendfile() with AIO preloading on FreeBSD can trigger duplicate
> aio operation, resulting in "second aio post" alerts.  This is, however,
> harder to reproduce, especially on modern FreeBSD systems, since sendfile()
> usually does not return EBUSY.
> 
> Fix is to avoid starting a sendfile operation if other thread operation
> is active by checking r->aio in the thread handler (and, similarly, in
> aio preload handler).
> 
> The added check also makes duplicate calls protection in sendfile() in
> threads redundant, so it is removed.  Further, now there is a corresponding
> duplicate calls protection in AIO preloading.
> 
> diff --git a/src/http/ngx_http_copy_filter_module.c b/src/http/ngx_http_copy_filter_module.c
> --- a/src/http/ngx_http_copy_filter_module.c
> +++ b/src/http/ngx_http_copy_filter_module.c
> @@ -219,13 +219,25 @@ ngx_http_copy_aio_sendfile_preload(ngx_b
>     ngx_http_request_t      *r;
>     ngx_output_chain_ctx_t  *ctx;
> 
> +    aio = file->file->aio;
> +    r = aio->data;
> +
> +    if (r->aio) {
> +        /*
> +         * tolerate sendfile() calls if another operation is already
> +         * running; this can happen due to subrequests, multiple calls
> +         * of the next body filter from a filter, or in HTTP/2 due to
> +         * a write event on the main connection
> +         */
> +
> +        return NGX_OK;
> +    }
> +
>     n = ngx_file_aio_read(file->file, buf, 1, file->file_pos, NULL);
> 
>     if (n == NGX_AGAIN) {
> -        aio = file->file->aio;
>         aio->handler = ngx_http_copy_aio_sendfile_event_handler;
> 
> -        r = aio->data;
>         r->main->blocked++;
>         r->aio = 1;
> 
> @@ -263,6 +275,7 @@ static ngx_int_t
> ngx_http_copy_thread_handler(ngx_thread_task_t *task, ngx_file_t *file)
> {
>     ngx_str_t                  name;
> +    ngx_connection_t          *c;
>     ngx_thread_pool_t         *tp;
>     ngx_http_request_t        *r;
>     ngx_output_chain_ctx_t    *ctx;
> @@ -270,6 +283,27 @@ ngx_http_copy_thread_handler(ngx_thread_
> 
>     r = file->thread_ctx;
> 
> +    if (r->aio) {
> +        /*
> +         * tolerate sendfile() calls if another operation is already
> +         * running; this can happen due to subrequests, multiple calls
> +         * of the next body filter from a filter, or in HTTP/2 due to
> +         * a write event on the main connection
> +         */
> +
> +        c = r->connection;
> +
> +#if (NGX_HTTP_V2)
> +        if (r->stream) {
> +            c = r->stream->connection->connection;
> +        }
> +#endif
> +
> +        if (task == c->sendfile_task) {
> +            return NGX_OK;
> +        }
> +    }
> +
>     clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
>     tp = clcf->thread_pool;
> 
> diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c
> --- a/src/http/ngx_http_upstream.c
> +++ b/src/http/ngx_http_upstream.c
> @@ -3847,6 +3847,7 @@ ngx_http_upstream_thread_handler(ngx_thr
> {
>     ngx_str_t                  name;
>     ngx_event_pipe_t          *p;
> +    ngx_connection_t          *c;
>     ngx_thread_pool_t         *tp;
>     ngx_http_request_t        *r;
>     ngx_http_core_loc_conf_t  *clcf;
> @@ -3854,6 +3855,27 @@ ngx_http_upstream_thread_handler(ngx_thr
>     r = file->thread_ctx;
>     p = r->upstream->pipe;
> 
> +    if (r->aio) {
> +        /*
> +         * tolerate sendfile() calls if another operation is already
> +         * running; this can happen due to subrequests, multiple calls
> +         * of the next body filter from a filter, or in HTTP/2 due to
> +         * a write event on the main connection
> +         */
> +
> +        c = r->connection;
> +
> +#if (NGX_HTTP_V2)
> +        if (r->stream) {
> +            c = r->stream->connection->connection;
> +        }
> +#endif
> +
> +        if (task == c->sendfile_task) {
> +            return NGX_OK;
> +        }
> +    }
> +
>     clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
>     tp = clcf->thread_pool;
> 
> diff --git a/src/os/unix/ngx_linux_sendfile_chain.c b/src/os/unix/ngx_linux_sendfile_chain.c
> --- a/src/os/unix/ngx_linux_sendfile_chain.c
> +++ b/src/os/unix/ngx_linux_sendfile_chain.c
> @@ -379,15 +379,6 @@ ngx_linux_sendfile_thread(ngx_connection
>         return ctx->sent;
>     }
> 
> -    if (task->event.active && ctx->file == file) {
> -        /*
> -         * tolerate duplicate calls; they can happen due to subrequests
> -         * or multiple calls of the next body filter from a filter
> -         */
> -
> -        return NGX_DONE;
> -    }
> -
>     ctx->file = file;
>     ctx->socket = c->fd;
>     ctx->size = size;
> 

It's hard to say about preload handler, generally it looks good.
Also, I can confirm that the check in ngx_linux_sendfile_thread() is now
redundant and can be removed, a test case for 6422:768e287a6f36 shows
that duplicate calls in subrequests are now handled in thread handlers
(both in HTTP/1.x and HTTP/2), also can be tested with proxy_store.t.
BTW, what about a similar check in ngx_freebsd_sendfile_chain()
used to catch duplicate calls? Both were added in 6422:768e287a6f36.
Anyway, it is removed in the sendfile(SF_NODISKIO) rework.

-- 
Sergey Kandaurov



More information about the nginx-devel mailing list