Проектаванне бэкендаў для чату у рэальным часе: камеры, зберагачча дадзейнаў і масштабаванне
Выучыце, як спроекаваць бэкенд для чату у рэальным часе з выкарыстоўванням Socket.IO, PostgreSQL і Redis, уключаючы камнаты, порядак зберагання паведамленняў, статус прысутнасці і масштабаванне на кальку сервераў.
Рэальнакі кантакт зменяе спосаб супрацоўкі сервера і кліента, пераводзячы яго з простага формата запит-адказ на аблако WebSockets, камеры, зберагчыце прывітакоў, стежэнне за наявнасцю і масштабаванне на аднойчынай базе Redis.
Большасць API следуе прыгляднаму шаблону:
Client
↓
HTTP Request
↓
Server
↓
HTTP Response
Кліент выканае запит. Сервер адправляе адказ. Гэта ўсё, што трэба для взаімадзея.
Але тепер падумайце, што патрэбна для стварэння чагосьць на кшталт:
- Slack
- Discord
- Рэальныя паведамленні
- Індыкаторы наявнасці
- Індыкаторы прадзеяў кліента
- Рэальныя панелі керування
- Функцыяны для калькі гэльдзей
У такіх случаях не хочацца, каб кліент павтараўчаса пераглядаў:
"Чы гэта ўсё як раней?"
У замен хочацца, каб сам сервер могаў абавесці:
„Осё змянілася. Ось апдэйт.
Гэта самэўсё тая проблема, якую рашае момантальная камунікацыя.
1. HTTP проты момантальнай камунікацыі
Традыцыйны метод HTTP-запытанняў выглядае так:
Client → "Any new messages?"
Server → "No"
Client → "Any new messages?"
Server → "No"Client → "Any new messages?"
Server → "Yes, here's one."
Ён працуе, але марнуюцься запытанняі і пропускна здатнасць на перагляд апдэйтаў, якіх зазвычай няма.
Пастаянны момантальны з’ѐеднанне працуе інакш:
Client ←────────────→ Server
connection
Server → New message
Server → User online
Server → Typing...
Server → Message read
Калі з’ѐеднанне вядома, сервер можа безпасцерагова адправляць запаведзінні кліенту кожны раз, калі ўтвараецца якая-небудзь змяна.
WebSockets — это шырока вжываная тэхналогія для стварэння такіх з’ѐеднанняў. У экасыстэме Node.js Socket.IO — популярная бібліятэка, створаная спецыяльна для момантальнай камунікацыі.
2. Простая архітэктура
Мінімальны прыемнік чату можа быць структураваны так:
┌──────────────┐
│ Web / Mobile │
└──────┬───────┘
│
WebSocket
│
▼
┌──────────────┐
│ Node.js │
│ Socket.IO │
└──────┬───────┘
│
┌──────────┴──────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ PostgreSQL│ │ Redis │
│ Messages │ │ Pub/Sub │
└───────────┘ └───────────┘
Кожны элемент выпалюе адзінчы ролю.
Node.js + Socket.IO
Керуе жывымі з’яўленнямі та запускаемыми через яны падзеямі.
PostgreSQL
Зберагае паведамленні та історыю размов.
Redis
Прыходзіць на дапамогу, калі вы запускаеце больш адной інстанцыі вашага прыемніка та патрэбуеце, каб гэтыя інстанцыі заставаліся сінхроннымі для рэальна-часовых падзеяў.
3. Налагоджэнне Socket.IO
Базовая наладка сервера можа выглядаць так:
import { Server } from "socket.io";
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_URL,
credentials: true,
},
});
io.on("connection", (socket) => {
console.log("User connected:", socket.id);
socket.on("disconnect", () => {
console.log("User disconnected:", socket.id);
});
});
Кожны раз, калі кліент падключаецца, Socket.IO стварае спецыяльны сокет для гэтага з’яўлення, таму кожны падключыты кліент атрымлея свой саўны сокет.
4. Падзеі — галоўная канцэпцыя
У працоўні замест апарату на адзіны запит:
"Call this endpoint"
системы рэальна-часовага типу зазвычай організаваны ўакол іменаванных падзеяў:
"user-connected"
"send-message"
"message-created"
"user-typing"
"message-read"
"user-offline"
Напрыклад, сервер можа выдаты:
socket.emit("message-created", {
id: message.id,
text: message.text,
});
А кліент слухае гэты той самы запуск:
socket.on("message-created", (message) => {
console.log("New message:", message);
});
Гэтае перахоўванне да моделі, заснованай на запусках, ёсць аднам з фундаментальных спосабоў, па якімы рэальнага часу аплікацыі разлічаюцца ад звычайных API, заснованых на REST.
5. Камеры значна спрыяюць працэўным системам чату
Уявіце сабе аднонаступны дыялог:
User A
User B
Вы не хочаце трансляваць кожны прыемленае паведамленне всім падключаным корыстнікам, а толькі тым, якія фактычна беруць участь у гэтым дыялозе.
Socket.IO дазволяе групаваць сокеты ў камеру:
socket.join(`conversation:${conversationId}`);
Потым, калі ствараецца новае паведамленне, яго выдадзяўця ў тую конкрэтную камеру:
io.to(`conversation:${conversationId}`)
.emit("message-created", message);
Толькі сокеты, якія прыўязаны да той камеры, будуць прымаць гэты запуск.
Тая ж ідея можа быць выкарыстана для групавага дыялогу. Камера, напрыклад:
conversation:123
можа мець калькі учаснікаў:
User A
User B
User C
User D
І адзін выпадзелы прыемлівы паведамлення дасягае ўсіх у той кімнаты адразу.
6. Не зберагаюце паведамленне пасля трансляцыі
Это выбор дизайну, пра які варта падумаць рэшыткальна.
Рызыкаваная последовасць будзе такой:
Receive message
↓
Broadcast message
↓
Save to database
Проблема: што будзе, якщо запіс у базе дадзеных не выйдзе пасля таго, як паведамленне вже было аправлена? Карыстальнікі пабачылі б паведамленне, якое на самай працоўнай час не было зберажана, што стварыць несуразнасць межу тым, што бачаюць людзі, і тым, што зберагаецца.
Болей надзеяны патэрн — спачатку зберагчы, а потым трансляваць:
Client
↓
send-message
↓
Validate
↓
Save to PostgreSQL
↓
Database succeeds
↓
Broadcast event
У практыцы гэта выглядае працоўна так:
socket.on("send-message", async (data) => {
const message = await saveMessage(data);
io.to(`conversation:${data.conversationId}`)
.emit("message-created", message);
});
Точныя гарантіі стойкасці, якія вам патрэбны, будуць разнымі залежна ад прыемлівы, але основны прынцып застаецца загалом тым самым: спосаб зберагчы і доставкі паведамленняяў должен быць намеровым рашэннем, а не наследкам пасля іншых дзеянняў.
7. Зберагачыце історыю чатоў у PostgreSQL
Стварэнне системы на адной з рэальных часоў не значыць, што всё павінна існаваць толькі ў памяці.
Люди спакушаныя на тое, калі яны ачынаюць размову наступнага дня, ўпэўненыя, што іх паканальныя прыемкі ўсё яшчэ там.
Спрасцаванае схематызаванне можа выглядаць так:
CREATE TABLE messages (
id UUID PRIMARY KEY,
conversation_id UUID NOT NULL,
sender_id UUID NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Потым, калі ўжо корыстувач ачынае размову, трэба запусціць ўсё такое:
SELECT *
FROM messages
WHERE conversation_id = $1
ORDER BY created_at DESC
LIMIT 50;
У гэты момент мы раздзелілі работу на два аднальныя завады:
Socket.IO
→ Real-time delivery
PostgreSQL
→ Durable message history
Раздзелэнне гэтых аспектаў дапамагае значна.
8. Дадзіце індэкс для історыі размов
Якщо ваша прыемка рэгулярна запускае запит такога формата:
WHERE conversation_id = ?
ORDER BY created_at DESC
тады схематызаванне вашай базы данных павінна быць створанае з урахоўваннем гэтага шаблону.
Напрыклад:
CREATE INDEX idx_messages_conversation_created
ON messages(conversation_id, created_at DESC);
Мета не ў тым, каб без раздумаў распасціваць індэксы па всю дыялоговую рамку.
Індэкс ёсць корыстны толькі тады, калі ён паспаўляе з запитамі, якія насправды выкананы вашым прыстроем.
І, паўтараючы тое, што вже гаворылася раней:
Заўжды меркуйце выконванасць да і пасля внесення змян.
9. Адсутнасць у інтэрнете не адно тое ж, што зберагачча паведамленняў
Спадзяймося, вы хочаце адобразыць кашто-та падобнае да гэтага:
Mit
● Online
Няма прычыны зберагаць кашто-та падобнае да гэтаго:
user.is_online = true
у PostgreSQL кожны раз, калі паўтараецца запуск корыстніка.
Чаму не?
Так как статус адсутнасці постаўляеся зменяцца стала.
Для большай часткі систем лепей будзе зберагаць такія трымканыя данні адсутнасці ў Redis.
Напрыклад:
online:user:123
TTL → 60 seconds
Кліент можа адправляць перыядычныя сигналы, каб парадкаваць, што ён яшчэ актыўны.
Калі пульсацыі серца перестану прыходзіць, ключ наявнасці проста сама выгорае.
Это не дазволяе временнаму стану з’яноўкі пацыянта сплутвацца з постояннымі дадзеннямі, якія патрэбны базе даных.
10. Акцэнтныя знакі пад час набора тэксту ўсё бол временныя
Возьмімо прыклад:
Mit is typing...
Чакаецца лінія ў PostgreSQL?
Абавязкова не.
Это чыста транзіентна інформацыя.
Для цього достатня толькі падзея сокета:
socket.to(roomId).emit("user-typing", {
userId,
});
А калі корыстнік перастае набіраць тэкст:
socket.to(roomId).emit("user-stopped-typing", {
userId,
});
Это вказывае на шырэйшую прынцыпу дизайна:
Не всё, што стежыць ваша аплікацыя, патрэбна хаваць у базе даных.
Хорашым тэстам є падзіркі, чыя інформацыя патрэбна пасля перзапуску сервера.
Якщо ні, то, верагатна, кращы варыянт — якісь транзіентныя форматы зберагача.
11. Проблема з калькамі сервераў Node.js
Хутчэй тут усё становіцца складней.
Уявіце схему з адною толькі серверам Node.js:
Client
↓
Node.js
Працюе ўсё чыста на такой масштабе.
Але потым зростае навантажэння.
Тады схема выглядае так:
Load Balancer
/ \
↓ ↓
Node.js A Node.js B
Пользователь А падключаецца да Node.js A.
Пользователь B падключаецца да Node.js B.
Тепер Пользователь А апрашоўвае паведамленне.
Як Node.js B мае дазнацца, што яму трэба доставіць гэта паведамленне Пользователю B?
Гэта самэй той тип проблемы, які вымагае спяльнага шару для абмену паведамленнямі между экземплярамі сервера.
12. Redis можа падключыць калькольве экземпляры
Аднам з распашчальных раўянняў ёсць Redis у поўнасці з адаптарам Socket.IO для Redis.
Канцэптуальна, гэта выглядае так:
Load Balancer
/ \
↓ ↓
Node.js A Node.js B
\ /
\ /
Redis
За такіх умоў паведамленне, створанае на аднам экземпляры сервера, можа быть распрацавана на іншых.
Гэта тое, што дазваляе слузе рэальнага часу расшырвацца за межы адзінага процэсу Node.js.
Важна падчыркнуць, што Redis не ўзаменяе PostgreSQL у гэтым случае.
Этыя два інструменты служа разным цілям.
PostgreSQL
→ Durable application data
Redis
→ Fast temporary/shared state + coordination
13. Аутентыка застаецца важлівай
Стварэнне з’ёднання WebSocket не значыць автаматычна, што гэта з’ёднанне можа быць надзеяным.
Працэс аутентыкація ўсё рава неабходны.
Тыповы падход — калі кліент прагчыцацца, ён прыносіць які-небудзь токен.
Перш чым надаць доступ да прыватных размов, сервер павінен пераканацца ў правамернасці гэтага токена.
Канцэптуальна, процес выглядае так:
Client
↓
Connection
↓
Authenticate
↓
Validate user
↓
Allow socket connection
Потым, калі кліент прагчыцацца да пяцеры, напрыклад:
conversation:123
сервер павінен парадактаваць, што аутентыфікаваны корыстнік дэйсна є учаснікам той размовы.
Ніколы не прыемлівайце проста так:
socket.join(conversationId);
на падставе прыпуску, што ID, які надае кліент, можна верыць без пераканальства.
14. Караце ад’ёкты
Нечаканыя перерывы у з’ўязку ёсць сталяй рэальнасцю ў системах у рэальны час.
Корыстнік можа:
- Закрыць свой браузер
- Пазбыцца з’ўязку Wi-Fi
- Змяніць сеть
- Увести тэлефон у режым сну
- Пазбыцца мобільнага сігналу
- Заканчыць роботу прыкладнення
Чэрез гэта ваш сервер патрабуе логіки на кшталт:
socket.on("disconnect", (reason) => {
console.log("Disconnected:", reason);
});
Таксама трэба врачыць логіку павторнага з’ўязку.
Короткі перерыв у сети на п’ять секунд не должен значыць, што корыстнік назаўсёды втрачае доступ да функцый у рэальны час.
Гэта ў частыні прычыны, чаму системы у рэальны час зазвычай патрабуюць болей адзяйнальнага керавання станам, чым звычайны REST API.
15. Для рэальнага часу не патрабуецца, каб усё працавала через WebSockets
Это ўсё жа адна з важных падсумков.
Няма правіла, якое вымагае, каб усё ваша прыемліка была перабудована адночасна навакола з’ўязкаў WebSocket.
Вы можете сумешваць разныя падходы:
REST API
+
WebSockets
+
PostgreSQL
+
Redis
Напрыклад:
REST
Выбірайце REST пад час обробкі:
Login
Get conversation history
Create conversation
Upload files
Search messages
WebSocket
Выбірайце запускаваннія WebSocket пад час обробкі:
New message
Typing indicator
Online status
Read receipts
Live notifications
Такая гібрыдная структура зазвычай робіць все значна простейшым, чым спроба перадаваць усё через сокеты.
16. Чаго трэба прымножваць у вырабнічых умовах
Пераход на рэальны час дадае новы слой проблем.
Вам патрэбна будзе врачыць:
Authentication
Authorization
Connection limits
Reconnection
Message ordering
Duplicate messages
Offline users
Presence
Rate limiting
Horizontal scaling
Redis
Monitoring
Database performance
А якщо вы ствараеце ўтворэнне, бліжэй да повноцэннага продукту для перадачы паведамленняў, дадзіце да гэтага ёш:
Message delivery guarantees
Idempotency
Unread counts
Read receipts
File attachments
Push notifications
Message pagination
Масштаб проблемы быстра расширваецца.
Самэ гэтая прычына, чаму першая версія не должна намагацца рашыць усё адразу.
17. Разумная пачатковая точка
Для меншага прыгу пачатковая архітэктура можа выглядаць так:
Client
│
▼
┌───────────────┐
│ Node.js │
│ REST + WS │
└───────┬───────┘
│
┌──────┴──────┐
▼ ▼
PostgreSQL Redis
Messages Cache /
Users Presence
Можна далей развіваць яе, калі ёй справачыка будзе рэальна:
Load Balancer
/ \
▼ ▼
Node.js A Node.js B
\ /
\ /
Redis
│
▼
PostgreSQL
Зберагце пачатковы варіянт простым. Прамеравайце яго работу пад рэальным выкарыстоўваннем. Лячна тады расшырюйце тыя часткі, якія паказуюцься справжніми вузькімі месцаўкамі.
18. Спіс пераканаў для бэкендаў у рэальным часе
Перш чым считаць бэкенд чату готовым да выкарыстоўвання, паўтарыце:
[ ] Authentication implemented
[ ] Authorization for conversations
[ ] WebSocket connection handling
[ ] Room management
[ ] Message persistence
[ ] Message pagination
[ ] Reconnection handling
[ ] Duplicate message handling
[ ] Online/offline presence
[ ] Typing indicators
[ ] Rate limiting
[ ] Redis for multi-instance coordination
[ ] Logging
[ ] Monitoring
[ ] Database indexes
[ ] Load testing
[ ] Failure scenarios tested
Заключныя меркі
З званэй стороны стварэнне функцыі чату здаецца простым.
Вы пішаце:
"Hello"
А іншы чалавек прымае:
"Hello"
Але за гэтымі двума словамі крыўдзіцца цэлы набор інжынерных вызоваў:
Connection management
Authentication
Authorization
Event delivery
Persistence
Ordering
Presence
Reconnection
Scaling
Саме гэтая схованая складнасць робіць системы у рэальным часе вартымі дакладнага разумеў.
Галоўны урок, які варта запамятаць, такой:
Супротставляйцеся праганню стварыць масіўна распрастранены систему ва ўжо першы дзень.
Пачніце з:
Node.js
+
Socket.IO
+
PostgreSQL
З’ясавайце, як гэта поўнага спалучанне працуе за рэальных умоў.
Увядзіце Redis і дадатковыя экземпляры толькі тады, калі рэальныя трэбаванні зробляць гэты крок неабходным.
Закольцавайце першую версію ў мінімум. Прыдзейце часу, каб зрозумець, як разлічныя элементы взаўмадзейнуюць. Спазіралі, як система працуе пад рэальным навантажэнням. Расширяйце толькі тыя частыні налаштавання, якім дасканальна патрэбны большыя можлівасці.
Самэ так простая функцыя чату ператвараецца на надзеяны рэальнага часу бэкенд.
Спаднёю літаратура
- Асаліны кэшавання ў Redis: шаблоны, проблемы і праблемы пад час адбору на работу — Дазвольце вам дакладна разумець, як функцыонуе кэшавання ў Redis у працоўных програмах на Node.js, ад методаў кэшавання збоку та TTL да захадоў протыкі спрацоўкі больш чым адной запыткі, правіл вывядзення дадзенаў з кэша і распашчальных пытанняў пад час адбору на работу.
- Аднаходжэнне сапраўднага бутлнека ў медленным канцэнтрыі Node.js — Увучыцеся систематычнам методу для выяўлення прычын затрымакаў у бэкендзе па маршруту запыткі — ад коду на Node.js да запыткаў да базы дадзенаў — па выкарыстоўванні метражавання часу і функцый EXPLAIN ANALYZE.