Skip to content

Broker: avoid EPOLLOUT add/remove churn per write (+60-90% throughput) - #3722

Open
dgprivate wants to merge 2 commits into
eclipse-mosquitto:masterfrom
dgprivate:perf-avoid-epollout-toggle
Open

dgprivate wants to merge 2 commits into
eclipse-mosquitto:masterfrom
dgprivate:perf-avoid-epollout-toggle

Conversation

@dgprivate

Copy link
Copy Markdown

Broker: avoid EPOLLOUT add/remove churn on every write

Summary

packet__write() (broker build) registers EPOLLOUT via mux__add_out() at the
top of the function and removes it again via mux__remove_out() once the out
queue drains. In the common case — the socket accepts the write synchronously —
this costs two epoll_ctl() syscalls per write cycle that the event loop
never needed
.

This changes it to the standard level-triggered pattern: only register EPOLLOUT
when a write actually blocks (EAGAIN with data still pending). The outgoing
bridge connect_pending case still arms EPOLLOUT explicitly, and packet__queue
already attempts an eager write for every queued packet, so nothing can be left
waiting for an EPOLLOUT that was never armed.

Deterministic effect (strace -c -e epoll_ctl, fixed 200k-message run)

build epoll_ctl calls per message
before 334,728 ~1.67
after 2 ~0

perf before: epoll_ctl = ~9.6% of broker CPU; after: absent from the profile.

Throughput (interleaved A/B, broker pinned to one core, plain TCP, 64B QoS0)

scenario before after delta
1 pub → 1 sub 304k/s 547k/s +80%
8 pub → 2 sub 129k/s 249k/s +93%
4 pub → 4 sub 50k/s 86k/s +72%
2 pub → 8 sub 24k/s 39k/s +64%
8 pub → 8 sub 36k/s 58k/s +60%

(Trials alternated between two fixed binaries built from the parent commit and
this commit, to control for host drift.)

Correctness

  • Backpressure — 100 KB QoS1 messages to a deliberately slow reader, all
    delivered intact and in order over plain TCP, TLS (SSL_write WANT_WRITE),
    and builtin WebSockets
    .
  • Fan-out 1→40 subscribers: every subscriber received all messages, no
    corruption.
  • poll backend (WITH_EPOLL=no) exercised as well as epoll.
  • No busy-loop: broker CPU stays low while backpressured.
  • Broker test suite: unchanged pass/fail versus the parent commit (including the
    bridge tests that cover the connect_pending path).

Second commit (optional)

The second commit adds a small throughput benchmark under misc/bench/ used to
produce the numbers above. Happy to drop it if you'd rather keep it out of tree.

dgprivate and others added 2 commits August 24, 2026 00:17
packet__write() unconditionally registered EPOLLOUT via mux__add_out() before
attempting to write, then removed it again once the out queue drained. In the
common case where the kernel accepts the write immediately this cost two
epoll_ctl() syscalls per write cycle that the event loop never needed.

Only register EPOLLOUT when a write actually blocks (EAGAIN with data still
pending); the bridge connect-pending case still arms it explicitly. On a
single-subscriber QoS0 routed workload this roughly doubled throughput
(~295k -> ~548k msg/s here) and removed epoll_ctl (~9.6% of broker CPU) from
the profile, with no change to the broker test suite results.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Dean Gostiša <dean.gostisa@black.si>
Adds a small, self-contained throughput benchmark under misc/bench: a
single-connection QoS-0 publisher and a multi publisher/subscriber driver,
with a README documenting how to A/B two builds without being fooled by
host drift (interleave trials; verify the two binaries actually differ;
prefer the deterministic `strace -c -e epoll_ctl` count).

Used to substantiate the EPOLLOUT-toggle change: epoll_ctl on the write
path drops from ~1.67 per message to ~0, and single-core throughput rises
by roughly 60-90% across a range of publisher/subscriber counts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Dean Gostiša <dean.gostisa@black.si>
@dgprivate dgprivate changed the title Broker: avoid EPOLLOUT add/remove churn on every write Broker: avoid EPOLLOUT add/remove churn per write (+60-90% throughput) Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant