mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
perf: 40% LESS CPU usage: cut per-instance CPU cost of shared socket.io Redis pub/sub channel (#27282)
* perf: cut per-instance CPU cost of shared socket.io Redis pub/sub channel Profiling a multi-instance deployment (py-spy --gil) showed ~44% of worker CPU in the socket.io pub/sub listener. Two causes, two fixes: - Add hiredis so redis-py parses the RESP protocol in C instead of pure Python (redis/_parsers/resp3.py alone accounted for ~28% of GIL samples; redis-py auto-selects the hiredis parser when importable). - Subclass AsyncRedisManager to drop emits whose target room has no local participants before upstream _handle_emit re-encodes the full packet. Every instance receives every emit published on the shared channel, so with N instances all but the hosting one were paying full packet re-serialization per message just to deliver it to nobody. Broadcasts (room=None) are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9CQ9qnp3sZGYQQwztsCJT * fix: restrict pub/sub emit early-out to string rooms Adversarial review against python-socketio 5.16.2 found one divergence from upstream: for a degenerate empty-sequence room (emit to room=[]) on an instance whose namespace has no local clients, the filter's get_participants probe raises IndexError from room[0] where upstream returns silently at the namespace guard and still publishes to Redis. Open WebUI only ever emits to scalar string rooms or room=None, so the case is unreachable today; guard on isinstance(room, str) anyway so any non-string room shape passes through to upstream behavior unchanged. Every open-webui emit uses a string room, so the fast path still covers all real traffic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9CQ9qnp3sZGYQQwztsCJT * Update requirements.txt * Update pyproject.toml * Update requirements-min.txt --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,29 @@ REDIS = None
|
||||
# Configure CORS for Socket.IO
|
||||
SOCKETIO_CORS_ORIGINS = '*' if CORS_ALLOW_ORIGIN == ['*'] else CORS_ALLOW_ORIGIN
|
||||
|
||||
class LocalFilteredRedisManager(socketio.AsyncRedisManager):
|
||||
"""AsyncRedisManager that drops pub/sub emits with no local recipients.
|
||||
|
||||
Every instance subscribed to the shared Socket.IO channel receives every
|
||||
emit published by the whole fleet. Upstream ``_handle_emit`` re-encodes
|
||||
the full packet before discovering the target room has no participants on
|
||||
this instance, so each instance burns CPU serializing payloads addressed
|
||||
to sessions it does not host — a cost that grows with instance count.
|
||||
Bail out before that work when the room is empty here. Only string
|
||||
rooms take the fast path; broadcasts (``room=None``) and any other
|
||||
room shape (e.g. lists, which upstream indexes before validating)
|
||||
always pass through unchanged.
|
||||
"""
|
||||
|
||||
async def _handle_emit(self, message):
|
||||
room = message.get('room')
|
||||
if isinstance(room, str):
|
||||
namespace = message.get('namespace') or '/'
|
||||
if next(self.get_participants(namespace, room), None) is None:
|
||||
return
|
||||
await super()._handle_emit(message)
|
||||
|
||||
|
||||
if WEBSOCKET_MANAGER == 'redis':
|
||||
sentinel_hosts = WEBSOCKET_SENTINEL_HOSTS or ''
|
||||
ws_redis_url = (
|
||||
@@ -64,7 +87,7 @@ if WEBSOCKET_MANAGER == 'redis':
|
||||
if sentinel_hosts
|
||||
else WEBSOCKET_REDIS_URL
|
||||
)
|
||||
redis_manager = socketio.AsyncRedisManager(ws_redis_url, redis_options=WEBSOCKET_REDIS_OPTIONS)
|
||||
redis_manager = LocalFilteredRedisManager(ws_redis_url, redis_options=WEBSOCKET_REDIS_OPTIONS)
|
||||
sio = socketio.AsyncServer(
|
||||
cors_allowed_origins=SOCKETIO_CORS_ORIGINS,
|
||||
async_mode='asgi',
|
||||
|
||||
@@ -33,6 +33,7 @@ alembic==1.18.4
|
||||
|
||||
pycrdt==0.13.1
|
||||
redis
|
||||
hiredis
|
||||
|
||||
APScheduler==3.11.2
|
||||
RestrictedPython==8.2
|
||||
|
||||
@@ -30,7 +30,8 @@ psycopg[binary]==3.3.4
|
||||
alembic==1.18.4
|
||||
|
||||
pycrdt==0.13.1
|
||||
redis==8.0.0
|
||||
redis==8.0.1
|
||||
hiredis==3.4.0
|
||||
|
||||
APScheduler==3.11.2
|
||||
RestrictedPython==8.2
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@ dependencies = [
|
||||
"alembic==1.18.4",
|
||||
|
||||
"pycrdt==0.13.1",
|
||||
"redis==8.0.0",
|
||||
"redis==8.0.1",
|
||||
"hiredis==3.4.0",
|
||||
# "valkey-glide-sync==2.3.1", # optional: install manually if VECTOR_DB=valkey
|
||||
|
||||
"pytz==2026.2",
|
||||
|
||||
Reference in New Issue
Block a user