From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: Kefu Chai <k.chai@proxmox.com>, pve-devel@lists.proxmox.com
Subject: Re: [PATCH http-server 1/1] fix #7483: apiserver: add backpressure to websocket proxy
Date: Mon, 13 Apr 2026 09:38:27 +0200 [thread overview]
Message-ID: <177606590735.45911.9684650018695214160@yuna.proxmox.com> (raw)
In-Reply-To: <20260412111209.3960421-2-k.chai@proxmox.com>
Quoting Kefu Chai (2026-04-12 13:12:09)
> During PDM cross-cluster migration to LVM-thin storage, pveproxy can
> be OOM-killed on the destination host when disk writes are slower than
> the incoming network transfer rate.
I don't think this is limited to that particular combination, in principle all
remote migrations are affected. Might just be easier to trigger with LVM thin
because of its zero-on-alloc behaviour?
Might make sense to adapt the commit message either when applying or sending a
v2.
> The existing wbuf_max on the backend handle turns out not to help:
> AnyEvent::Handle only checks it inside the `if (!$self->{_ww})` guard
> in _drain_wbuf. Once the first EAGAIN installs a write watcher, all
> subsequent push_write calls return immediately without ever reaching
> the check, so wbuf grows without bound.
nice find! though it's a good thing it doesn't I guess, since AFAICT it would
trigger a fatal error on the connection instead of applying back pressure?
> Instead, follow the same approach response_stream() already takes:
> pause reading from the source handle when the backend write buffer
> exceeds the limit, and resume via an on_drain callback once it empties.
I guess there isn't enough overlap to make this a shared helper? the variant
there does handle EOF correctly ;)
>
> Signed-off-by: Kefu Chai <k.chai@proxmox.com>
> Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=7483
> ---
> src/PVE/APIServer/AnyEvent.pm | 45 +++++++++++++++++++++++++++++------
> 1 file changed, 38 insertions(+), 7 deletions(-)
>
> diff --git a/src/PVE/APIServer/AnyEvent.pm b/src/PVE/APIServer/AnyEvent.pm
> index 915d678..5a4b449 100644
> --- a/src/PVE/APIServer/AnyEvent.pm
> +++ b/src/PVE/APIServer/AnyEvent.pm
> @@ -581,10 +581,11 @@ sub websocket_proxy {
>
> $self->dprint("CONNECTed to '$remhost:$remport'");
>
> + my $wbuf_limit = $max_payload_size * 5;
> +
> $reqstate->{proxyhdl} = AnyEvent::Handle->new(
> fh => $fh,
> rbuf_max => $max_payload_size,
> - wbuf_max => $max_payload_size * 5,
> timeout => 5,
> on_eof => sub {
> my ($hdl) = @_;
> @@ -604,7 +605,30 @@ sub websocket_proxy {
> },
> );
>
> - my $proxyhdlreader = sub {
> + # Stop reading from $read_hdl until $write_hdl drains its write
> + # buffer, then re-register $on_read_cb. Returns true if
> + # backpressure was applied. We cannot rely on AnyEvent::Handle's
> + # wbuf_max for this because its check in _drain_wbuf is skipped
> + # when a write watcher is already active.
> + my $apply_backpressure = sub {
> + my ($read_hdl, $write_hdl, $on_read_cb, $alive_key) = @_;
> + return if length($write_hdl->{wbuf}) <= $wbuf_limit;
> +
> + $read_hdl->on_read();
AFAICT (but I haven't tried to actually trigger this) this is not handling EOF
conditions correctly now?
if you remove the on_read callback here without having an on_eof callback set,
then nothing will read and handle potentially buffered data.. the way
websockets work that shouldn't be a problem IIRC, except when a client is not
waiting for the ack when closing the socket? still probably doesn't hurt to try
to handle it?
> + my $prev_on_drain = $write_hdl->{on_drain};
> + $write_hdl->on_drain(sub {
> + my ($wrhdl) = @_;
> + $read_hdl->on_read($on_read_cb) if $reqstate->{$alive_key};
> + if ($prev_on_drain) {
> + $wrhdl->on_drain($prev_on_drain);
> + $prev_on_drain->($wrhdl);
> + }
> + });
> + return 1;
> + };
> +
> + my $proxyhdlreader;
> + $proxyhdlreader = sub {
> my ($hdl) = @_;
>
> my $len = length($hdl->{rbuf});
> @@ -614,10 +638,15 @@ sub websocket_proxy {
>
> my $string = $encode->(\$data);
>
> - $reqstate->{hdl}->push_write($string) if $reqstate->{hdl};
> + my $clienthdl = $reqstate->{hdl};
> + return if !$clienthdl;
> +
> + $clienthdl->push_write($string);
> + $apply_backpressure->($hdl, $clienthdl, $proxyhdlreader, 'proxyhdl');
> };
>
> - my $hdlreader = sub {
> + my $hdlreader;
> + $hdlreader = sub {
> my ($hdl) = @_;
>
> while (my $len = length($hdl->{rbuf})) {
> @@ -672,7 +701,11 @@ sub websocket_proxy {
> }
>
> if ($opcode == 1 || $opcode == 2) {
> - $reqstate->{proxyhdl}->push_write($payload) if $reqstate->{proxyhdl};
> + my $proxyhdl = $reqstate->{proxyhdl};
> + if ($proxyhdl) {
> + $proxyhdl->push_write($payload);
> + return if $apply_backpressure->($hdl, $proxyhdl, $hdlreader, 'hdl');
> + }
doesn't this also mean that the server stops responding to pings with pongs if
we apply backpressure.. though I don't see a way to avoid that except for
adding custom control frames that apply the pressure client-side.. which
probably causes more harm than good.
> } elsif ($opcode == 8) {
> my $statuscode = unpack("n", $payload);
> $self->dprint("websocket received close. status code: '$statuscode'");
> @@ -700,8 +733,6 @@ sub websocket_proxy {
> $reqstate->{proxyhdl}->on_read($proxyhdlreader);
> $reqstate->{hdl}->on_read($hdlreader);
>
> - # todo: use stop_read/start_read if write buffer grows to much
> -
> # FIXME: remove protocol in PVE/PMG 8.x
> #
> # for backwards, compatibility, we have to reply with the websocket
> --
> 2.47.3
>
>
>
>
>
next prev parent reply other threads:[~2026-04-13 7:38 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-12 11:12 [PATCH http-server 0/1] fix pveproxy OOM during PDM cross-cluster migration to LVM-thin Kefu Chai
2026-04-12 11:12 ` [PATCH http-server 1/1] fix #7483: apiserver: add backpressure to websocket proxy Kefu Chai
2026-04-13 7:38 ` Fabian Grünbichler [this message]
2026-04-13 7:47 ` [PATCH http-server 0/1] fix pveproxy OOM during PDM cross-cluster migration to LVM-thin Fabian Grünbichler
2026-04-13 13:00 ` superseded: " Kefu Chai
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=177606590735.45911.9684650018695214160@yuna.proxmox.com \
--to=f.gruenbichler@proxmox.com \
--cc=k.chai@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox