Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dynamic socket buffer sizes #1666

Merged
merged 3 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ include erlang.mk
# Don't run the examples/autobahn test suites by default.

ifndef FULL
CT_SUITES := $(filter-out examples ws_autobahn,$(CT_SUITES))
CT_SUITES := $(filter-out examples http_perf ws_autobahn ws_perf,$(CT_SUITES))
endif

# Don't run HTTP/3 test suites on Windows.
Expand Down
16 changes: 15 additions & 1 deletion doc/src/manual/cowboy_http.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ opts() :: #{
active_n => pos_integer(),
chunked => boolean(),
connection_type => worker | supervisor,
dynamic_buffer => false | {pos_integer(), pos_integer()},
http10_keepalive => boolean(),
idle_timeout => timeout(),
inactivity_timeout => timeout(),
Expand Down Expand Up @@ -53,7 +54,7 @@ Ranch functions `ranch:get_protocol_options/1` and

The default value is given next to the option name:

active_n (100)::
active_n (1)::

The number of packets Cowboy will request from the socket at once.
This can be used to tweak the performance of the server. Higher
Expand All @@ -75,6 +76,17 @@ connection_type (supervisor)::

Whether the connection process also acts as a supervisor.

dynamic_buffer ({8192, 131072})::

Cowboy will dynamically change the socket's `buffer` size
depending on the size of the data it receives from the socket.
This lets Cowboy use the optimal buffer size for the current
workload.
+
The dynamic buffer size functionality can be disabled by
setting this option to `false`. Cowboy will also disable
it by default when the `buffer` transport option is configured.

http10_keepalive (true)::

Whether keep-alive is enabled for HTTP/1.0 connections.
Expand Down Expand Up @@ -166,6 +178,8 @@ Ordered list of stream handlers that will handle all stream events.

== Changelog

* *2.13*: The `active_n` default value was changed to `1`.
* *2.13*: The `dynamic_buffer` option was added.
* *2.11*: The `reset_idle_timeout_on_send` option was added.
* *2.8*: The `active_n` option was added.
* *2.7*: The `initial_stream_flow_size` and `logger` options were added.
Expand Down
16 changes: 15 additions & 1 deletion doc/src/manual/cowboy_http2.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ opts() :: #{
connection_type => worker | supervisor,
connection_window_margin_size => 0..16#7fffffff,
connection_window_update_threshold => 0..16#7fffffff,
dynamic_buffer => false | {pos_integer(), pos_integer()},
enable_connect_protocol => boolean(),
goaway_initial_timeout => timeout(),
goaway_complete_timeout => timeout(),
Expand Down Expand Up @@ -66,7 +67,7 @@ Ranch functions `ranch:get_protocol_options/1` and

The default value is given next to the option name:

active_n (100)::
active_n (1)::

The number of packets Cowboy will request from the socket at once.
This can be used to tweak the performance of the server. Higher
Expand All @@ -91,6 +92,17 @@ The connection window will only get updated when its size
becomes lower than this threshold, in bytes. This is to
avoid sending too many `WINDOW_UPDATE` frames.

dynamic_buffer ({8192, 131072})::

Cowboy will dynamically change the socket's `buffer` size
depending on the size of the data it receives from the socket.
This lets Cowboy use the optimal buffer size for the current
workload.
+
The dynamic buffer size functionality can be disabled by
setting this option to `false`. Cowboy will also disable
it by default when the `buffer` transport option is configured.

enable_connect_protocol (false)::

Whether to enable the extended CONNECT method to allow
Expand Down Expand Up @@ -289,6 +301,8 @@ too many `WINDOW_UPDATE` frames.

== Changelog

* *2.13*: The `active_n` default value was changed to `1`.
* *2.13*: The `dynamic_buffer` option was added.
* *2.11*: Websocket over HTTP/2 is now considered stable.
* *2.11*: The `reset_idle_timeout_on_send` option was added.
* *2.11*: Add the option `max_cancel_stream_rate` to protect
Expand Down
16 changes: 15 additions & 1 deletion doc/src/manual/cowboy_websocket.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ opts() :: #{
active_n => pos_integer(),
compress => boolean(),
deflate_opts => cow_ws:deflate_opts()
dynamic_buffer => false | {pos_integer(), pos_integer()},
idle_timeout => timeout(),
max_frame_size => non_neg_integer() | infinity,
req_filter => fun((cowboy_req:req()) -> map()),
Expand All @@ -224,7 +225,7 @@ init(Req, State) ->

The default value is given next to the option name:

active_n (100)::
active_n (1)::

The number of packets Cowboy will request from the socket at once.
This can be used to tweak the performance of the server. Higher
Expand All @@ -248,6 +249,17 @@ options and the zlib compression options. The
defaults optimize the compression at the expense
of some memory and CPU.

dynamic_buffer ({8192, 131072})::

Cowboy will dynamically change the socket's `buffer` size
depending on the size of the data it receives from the socket.
This lets Cowboy use the optimal buffer size for the current
workload.
+
The dynamic buffer size functionality can be disabled by
setting this option to `false`. Cowboy will also disable
it by default when the `buffer` transport option is configured.

idle_timeout (60000)::

Time in milliseconds that Cowboy will keep the
Expand Down Expand Up @@ -287,6 +299,8 @@ normal circumstances if necessary.

== Changelog

* *2.13*: The `active_n` default value was changed to `1`.
* *2.13*: The `dynamic_buffer` option was added.
* *2.13*: The `max_frame_size` option can now be set dynamically.
* *2.11*: Websocket over HTTP/2 is now considered stable.
* *2.11*: HTTP/1.1 Websocket no longer traps exits by default.
Expand Down
43 changes: 35 additions & 8 deletions src/cowboy.erl
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,26 @@

start_clear(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0),
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts1),
ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
{TransOpts2, DynamicBuffer} = ensure_dynamic_buffer(TransOpts1, ProtoOpts0),
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts2),
ProtoOpts = ProtoOpts0#{
connection_type => ConnectionType,
dynamic_buffer => DynamicBuffer
},
ranch:start_listener(Ref, ranch_tcp, TransOpts, cowboy_clear, ProtoOpts).

-spec start_tls(ranch:ref(), ranch:opts(), opts())
-> {ok, pid()} | {error, any()}.

start_tls(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0),
SocketOpts = maps:get(socket_opts, TransOpts1, []),
TransOpts2 = TransOpts1#{socket_opts => [
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|SocketOpts]},
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts2),
ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
{TransOpts2, DynamicBuffer} = ensure_dynamic_buffer(TransOpts1, ProtoOpts0),
TransOpts3 = ensure_alpn(TransOpts2),
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts3),
ProtoOpts = ProtoOpts0#{
connection_type => ConnectionType,
dynamic_buffer => DynamicBuffer
},
ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).

%% @todo Experimental function to start a barebone QUIC listener.
Expand All @@ -77,6 +82,7 @@ start_tls(Ref, TransOpts0, ProtoOpts0) ->
-spec start_quic(ranch:ref(), #{socket_opts => [{atom(), _}]}, cowboy_http3:opts())
-> {ok, pid()}.

%% @todo Implement dynamic_buffer for HTTP/3 if/when it applies.
start_quic(Ref, TransOpts, ProtoOpts) ->
{ok, _} = application:ensure_all_started(quicer),
Parent = self(),
Expand Down Expand Up @@ -139,11 +145,32 @@ port_0() ->
end,
Port.

ensure_alpn(TransOpts) ->
SocketOpts = maps:get(socket_opts, TransOpts, []),
TransOpts#{socket_opts => [
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|SocketOpts]}.

ensure_connection_type(TransOpts=#{connection_type := ConnectionType}) ->
{TransOpts, ConnectionType};
ensure_connection_type(TransOpts) ->
{TransOpts#{connection_type => supervisor}, supervisor}.

%% Dynamic buffer was set; accept transport options as-is.
%% Note that initial 'buffer' size may be lower than dynamic buffer allows.
ensure_dynamic_buffer(TransOpts, #{dynamic_buffer := DynamicBuffer}) ->
{TransOpts, DynamicBuffer};
%% Dynamic buffer was not set; define default dynamic buffer
%% only if 'buffer' size was not configured. In that case we
%% set the 'buffer' size to the lowest value.
ensure_dynamic_buffer(TransOpts=#{socket_opts := SocketOpts}, _) ->
case proplists:get_value(buffer, SocketOpts, undefined) of
undefined ->
{TransOpts#{socket_opts => [{buffer, 8192}|SocketOpts]}, {8192, 131072}};
_ ->
{TransOpts, false}
end.

-spec stop_listener(ranch:ref()) -> ok | {error, not_found}.

stop_listener(Ref) ->
Expand Down
80 changes: 80 additions & 0 deletions src/cowboy_dynamic_buffer.hrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
%% Copyright (c) 2025, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

%% These functions are common to cowboy_http, cowboy_http2 and
%% cowboy_websocket. It requires the options and the state
%% to use the same field names.

%% Experiments have shown that the size of the 'buffer' can greatly
%% impact performance: a buffer too small leads to more messages
%% being handled and typically more binary appends; and a buffer
%% too large results in inefficient use of memory which in turn
%% reduces the throughput, presumably because large binary appends
%% are not as efficient as smaller ones, and because while the
%% buffer gets allocated only when there is data, the allocated
%% size remains until the binary is GC and so under-use hurts.
%%
%% The performance of a given 'buffer' size will also depend on
%% how the client is sending data, and on the protocol. For example,
%% HTTP/1.1 doesn't need a very large 'buffer' size for reading
%% request headers, but it does need one for reading large request
%% bodies. At the same time, HTTP/2 performs best reading large
%% request bodies when the 'buffer' size is about half that of
%% HTTP/1.1.
%%
%% It therefore becomes important to resize the buffer dynamically
%% depending on what is currently going on. We do this based on
%% the size of data packets we received from the transport. We
%% maintain a moving average and when that moving average is
%% 90% of the current 'buffer' size, we double the 'buffer' size.
%% When things slow down and the moving average falls below
%% 40% of the current 'buffer' size, we halve the 'buffer' size.
%%
%% To calculate the moving average we do (MovAvg + DataLen) div 2.
%% This means that the moving average will change very quickly when
%% DataLen increases or decreases rapidly. That's OK, we want to
%% be reactive, but also setting the buffer size is a pretty fast
%% operation. The formula could be changed to the following if it
%% became a problem: (MovAvg * N + DataLen) div (N + 1).
%%
%% Note that this works best when active,N uses low values of N.
%% We don't want to accumulate too much data because we resize
%% the buffer.

init_dynamic_buffer_size(#{dynamic_buffer_initial_size := DynamicBuffer}) ->
DynamicBuffer;
init_dynamic_buffer_size(#{dynamic_buffer := {LowDynamicBuffer, _}}) ->
LowDynamicBuffer;
init_dynamic_buffer_size(_) ->
false.

maybe_resize_buffer(State=#state{dynamic_buffer_size=false}, _) ->
State;
maybe_resize_buffer(State=#state{transport=Transport, socket=Socket,
opts=#{dynamic_buffer := {LowDynamicBuffer, HighDynamicBuffer}},
dynamic_buffer_size=BufferSize0, dynamic_buffer_moving_average=MovingAvg0}, Data) ->
DataLen = byte_size(Data),
MovingAvg = (MovingAvg0 + DataLen) div 2,
if
BufferSize0 < HighDynamicBuffer andalso MovingAvg > BufferSize0 * 0.9 ->
BufferSize = min(BufferSize0 * 2, HighDynamicBuffer),
ok = maybe_socket_error(State, Transport:setopts(Socket, [{buffer, BufferSize}])),
State#state{dynamic_buffer_moving_average=MovingAvg, dynamic_buffer_size=BufferSize};
BufferSize0 > LowDynamicBuffer andalso MovingAvg < BufferSize0 * 0.4 ->
BufferSize = max(BufferSize0 div 2, LowDynamicBuffer),
ok = maybe_socket_error(State, Transport:setopts(Socket, [{buffer, BufferSize}])),
State#state{dynamic_buffer_moving_average=MovingAvg, dynamic_buffer_size=BufferSize};
true ->
State#state{dynamic_buffer_moving_average=MovingAvg}
end.
Loading