Senior: питання на співбесіді з теми «Трансляція подій»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
2 питання
Канали бувають трьох типів, і різниця між ними - саме в доступі.
Публічний - слухати може будь-хто, хто знає назву:
class VacancyPublished implements ShouldBroadcast
{
public function broadcastOn(): Channel
{
return new Channel('vacancies');
}
}
Годиться лише для того, що й так публічне.
Приватний - потребує авторизації:
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('user.'.$this->user->id);
}
Правило описується в routes/channels.php:
Broadcast::channel('user.{userId}', function (User $user, int $userId) {
return $user->id === $userId;
});
Клієнт спершу звертається до /broadcasting/auth, і лише отримавши підпис, підписується на канал.
Presence - приватний плюс список присутніх: хто зараз онлайн, хто друкує.
Де помиляються:
- Публічний канал для приватних даних. Назву каналу видно у фронтенді, тож
Channel('user.'.$id)слухається ким завгодно з перебором id. - Замикання авторизації, що завжди повертає
true. Це те саме, що публічний канал, але виглядає безпечно. - Забувають, що подія несе всю модель. За замовчуванням у payload потрапляють усі публічні властивості - разом із полями, яких клієнт бачити не мусить. Формуйте payload явно через
broadcastWith(). - Приватні дані у назві каналу. Email чи токен у назві видно всім, хто дивиться трафік.
Практично: усе, що стосується конкретного користувача, - PrivateChannel; payload завжди явний.
WebSocket-з'єднання довгоживучі: кожна відкрита вкладка - постійне з'єднання, яке тримає пам'ять і файловий дескриптор. Обмеження зазвичай не в процесорі, а в лімітах ОС, вебсервера й циклу подій.
1. Ліміт відкритих файлів. У Unix кожне з'єднання - файл. Типовий ліміт ulimit -n - 1024:
# /etc/security/limits.conf
forge soft nofile 10000
forge hard nofile 10000
Під Supervisor - minfds=10000 у supervisord.conf, бо процес успадковує ліміти від менеджера процесів.
2. Цикл подій. Reverb працює на ReactPHP, і за замовчуванням використовує stream_select, обмежений приблизно 1024 файлами. Для понад тисячі з'єднань потрібне розширення ext-uv (pecl install uv) - Reverb підхопить його автоматично.
3. Вебсервер перед Reverb. Nginx проксіює WebSocket із заголовками Upgrade і Connection "Upgrade", і має власні ліміти:
worker_rlimit_nofile 10000;
events {
worker_connections 10000;
}
4. Горизонтальне масштабування. Один процес Reverb однопотоковий. Коли одного сервера замало:
REVERB_SCALING_ENABLED=true
- усі сервери Reverb підключаються до спільного Redis і обмінюються повідомленнями через pub/sub: подія, отримана одним сервером, доставляється клієнтам на всіх;
- сервери стоять за балансувальником, що підтримує WebSocket;
- Redis для масштабування - центральна залежність: його відмова розриває обмін між серверами.
5. Деплой і перезапуск. reverb:restart коректно закриває з'єднання, і клієнти Echo перепідключаються. Але тисячі одночасних перепідключень - стрибок навантаження на Reverb і на ендпойнт авторизації каналів /broadcasting/auth (кожне приватне з'єднання - запит до застосунку). Варто перезапускати сервери по черзі.
6. Моніторинг. Reverb інтегрується з Pulse: картки з кількістю з'єднань і повідомлень. Слідкуйте за пам'яттю процесу й pulse:check на серверах Reverb.
7. Що навантажує систему крім самих з'єднань:
- трансляція йде через чергу - при масових подіях пропускна здатність черги стає вузьким місцем;
- розмір повідомлень - транслюйте ідентифікатори й мінімум полів, а не моделі з усіма зв'язками;
- частота - події, що генеруються сотні разів на секунду, варто об'єднувати чи дебаунсити.
Альтернатива власному масштабуванню - керований сервіс (Pusher, Ably, Laravel Cloud) з тим самим протоколом: код застосунку не змінюється.