WebSockets и стриминг
Как только продукту нужен живой апдейт - чат, нотификации, дашборд - HTTP-модель «запрос-ответ» перестаёт хватать, и на собесе спрашивают, чем ты это закроешь. Ценят не знание слова WebSocket, а умение выбрать между ним, SSE и стримингом и понимание, что всё это ломается на масштабе.
Типовые формулировки: «WebSocket или SSE?», «как отдать большой файл, не съев память?», «как масштабировать вебсокеты на несколько воркеров».
WebSocket, SSE, StreamingResponse
WebSocket - постоянный двунаправленный канал: обе стороны шлют сообщения в любой момент, в отличие от «запрос-ответ». Это для настоящей двусторонности: чаты, живые дашборды. Но он требует ASGI (asynchronous server gateway interface) - WSGI (web server gateway interface) постоянный канал не держит.
SSE (server-sent events) - односторонний поток сервер→клиент поверх обычного HTTP (text/event-stream): проще WebSocket, есть авто-reconnect, спокойно проходит прокси. Если данные текут только в одну сторону (лента событий, прогресс) - бери SSE, не усложняй вебсокетом.
StreamingResponse отдаёт тело кусками из генератора, не держа весь ответ в памяти - так отдают большие файлы и данные на лету, не раздувая RAM.
// Правило выбора: двусторонность → WebSocket; поток в одну сторону → SSE; большой ответ телом → StreamingResponse.
- WebSocket
- постоянный двусторонний канал поверх одного соединения
- SSE
- односторонний поток событий сервер→клиент по HTTP
Масштаб: где всё ломается
Соединения живут в памяти конкретного воркера. Значит два клиента на разных воркерах друг друга не видят - рассылку между воркерами делают через общий backend, обычно Redis pub/sub: воркер публикует сообщение, все подписанные воркеры доставляют его своим соединениям.
Два продовых врага. Медленный клиент, который читает медленнее, чем сервер шлёт - буфер отправки растёт до OOM (out of memory), если нет backpressure (сдерживания). И мёртвые соединения, которые сервер не заметил - их ловят ping/pong-хартбитом, иначе копятся зомби.
// И база: не пересоздавай соединение или клиента на каждое сообщение - весь смысл в переиспользовании одного канала.
- StreamingResponse
- отдача тела частями без буферизации целиком
- backpressure
- сдерживание, когда клиент читает медленнее, чем сервер шлёт
Как отвечать: «WebSocket или SSE - что выбрать?»
Смотрю на направление данных. Если поток нужен только от сервера к клиенту - лента событий, прогресс задачи, живые метрики - беру SSE: он проще, работает поверх обычного HTTP, сам переподключается и спокойно проходит прокси. WebSocket достаю, когда нужна настоящая двусторонность в реальном времени: чат, коллаборативный редактор, где клиент тоже постоянно шлёт. За удобство WebSocket платит тем, что требует ASGI и сложнее масштабируется. Поэтому не усложняю: одностороннему кейсу - SSE, двустороннему - WebSocket.
Дан чёткий критерий выбора (направление данных), названы сильные стороны SSE (прокси, reconnect) и честная цена WebSocket это выбор по требованиям, а не по хайпу.
На чём валят
- −Ждать WebSocket на WSGI-сервере: постоянный двусторонний канал требует ASGI.
- −Пересоздавать соединение/клиент на каждое сообщение вместо переиспользования одного канала.
- −Игнорировать медленных клиентов: буфер отправки растёт до OOM без backpressure.
- −Держать вебсокеты на нескольких воркерах без общего backend (Redis pub/sub) - клиенты не видят друг друга.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Чем websocket-соединение отличается от обычного HTTP-запроса?A)Websocket работает исключительно в одну сторону: только сервер шлёт клиентуB)Это постоянный двунаправленный канал — сервер и клиент шлют данные в любой моментC)Websocket — тот же HTTP-запрос, только с более длинным допустимым таймаутом ответаD)Разница лишь в формате: websocket шлёт данные бинарными кадрами
показать ответ и разбор
+B)Это постоянный двунаправленный канал — сервер и клиент шлют данные в любой момент// разбор: HTTP — модель «запрос-ответ»: клиент спрашивает, сервер отвечает, соединение по смыслу завершено. Websocket после апгрейда держит постоянное TCP-соединение, по которому обе стороны шлют сообщения в произвольные моменты. Это нужно для реального времени: чаты, нотификации, живые дашборды, игры.
- Нужны серверные обновления в реальном времени, но только в одну сторону (лента событий). Что выбрать?A)Обычный GET-запрос, который сервер будет держать открытым бесконечно долгоB)Обязательно полноценный websocket, ведь другого способа слать данные с сервера нетC)SSE (Server-Sent Events): однонаправленный поток сервер→клиент поверх HTTPD)Частый опрос эндпоинта клиентом через setInterval — это и есть правильный realtime
показать ответ и разбор
+C)SSE (Server-Sent Events): однонаправленный поток сервер→клиент поверх HTTP// разбор: Если данные текут только с сервера (нотификации, прогресс, котировки), websocket избыточен. SSE — стандарт поверх обычного HTTP (Content-Type text/event-stream): сервер держит соединение и шлёт события, клиент слушает через EventSource. Проще websocket, легко проходит прокси, авто-reconnect из коробки. Для двусторонности всё же нужен websocket.
- Эндпоинт формирует гигантский CSV. Почему лучше StreamingResponse, а не собрать строку и вернуть?A)Обычный ответ вообще не способен вернуть данные объёмом больше пары мегабайтB)Разницы по памяти нет: обычный ответ и стриминг расходуют её одинаковоC)Стриминг отдаёт данные кусками, не держа весь ответ в памяти сразуD)StreamingResponse автоматически сжимает ответ, а обычный ответ отдаётся без сжатия
показать ответ и разбор
+C)Стриминг отдаёт данные кусками, не держа весь ответ в памяти сразу// разбор: Собрать весь CSV в строку — значит держать его целиком в памяти воркера, что при больших объёмах и многих клиентах опасно. StreamingResponse принимает генератор и отдаёт тело порциями по мере готовности: память под один чанк, а не под весь ответ. Так же стримят файлы и результаты, вычисляемые на лету.
- Почему websocket-эндпоинт нельзя обслужить на WSGI-сервере (gunicorn sync)?A)Можно, если увеличить число sync-воркеров gunicorn под количество клиентовB)gunicorn запрещает websockets в целях безопасности передаваемого трафикаC)Websocket требует отдельного порта, а gunicorn умеет слушать только одинD)Постоянный двусторонний канал требует ASGI — WSGI знает лишь запрос-ответ
показать ответ и разбор
+D)Постоянный двусторонний канал требует ASGI — WSGI знает лишь запрос-ответ// разбор: Websocket живёт в событийной модели с постоянным соединением и обменом в обе стороны — это спецификация ASGI. WSGI устроен как единичный синхронный вызов на запрос и такой канал выразить не может. Поэтому websocket-эндпоинты FastAPI обслуживает ASGI-сервер (uvicorn), а не sync-gunicorn.
- Как выглядит жизненный цикл websocket-обработчика в FastAPI?A)Клиент сам управляет соединением, а серверный обработчик не участвует в обменеB)accept подтверждает соединение, дальше цикл receive/send до отключенияC)Один вызов функции: принял сообщение, вернул ответ, соединение сразу закрылосьD)Соединение открывается и закрывается автоматически на каждое отдельное сообщение
показать ответ и разбор
+B)accept подтверждает соединение, дальше цикл receive/send до отключения// разбор: Websocket-обработчик FastAPI: сначала await websocket.accept() завершает рукопожатие, затем обычно бесконечный цикл, где await websocket.receive_text()/receive_json() читает входящее, а send_* отправляет исходящее. Выход из цикла (или WebSocketDisconnect) закрывает соединение. Всё это в рамках одного долгоживущего вызова обработчика.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.