Review: Laravel Websockets Breaks Our Alert System & Socket.IO Fixes It

...
profile picture of ghostzero
GhostZero
Software Developer
Published at April 4, 2021

In diesem Betrag geht es darum unsere Fehler zu reflektieren und unsere Erfahrungen von Websockets und diversen Produkten zu präsentieren.

Wie einige OWN3D Pro Nutzer mitbekommen haben, hatten wir in den letzten Wochen ein starkes Performance Problem unserer Alerts aufgrund der steigenden Nutzerzahlen. Vor allem zum Abend zwischen 18:00 und 04:00 Uhr CET war unser System extrem ausgelastet, da viele Streamer gleichzeitig unser Plugin nutzen.

Reason 1 — Performance

Da Laravel Websockets auf PHP basiert, und PHP hat per-default kein Thread System eingebaut. Deshalb verwendet Laravel Websockets, ReactPHP, dass ein Event Loop verwendet. In diesem Loop laufen alle Aktionen. Connections akzeptiere, Daten verarbeiten, Statistiks erstellen, Verbindungen schließen.

Der große Nachteil dabei ist, dass die Tasks sich blockieren, sprich, wenn Statistiken erstellt werden, dann ist der ganze Event Loop blockiert und es werden keine Verbindungen verarbeitet. Dadurch stauen sich Notifications oder gar Verbindungen.

In unserem Case hat sich herausgestellt, dass ab 1K verbindungen der Event Loop überlastet war und der Statistics Handler jede Minute den Loop für mehr als 10 Sekunden blockiert hat.

Reason 2 — Clustering

Laravel Websockets in der aktuellen Version 1.9 kommt nicht mit nativen Cluster Möglichkeiten. Dieses sorgt dafür, dass die Websockets nicht skalierbar sind und alle Verbindungen auf einem Host gehen.

Clustering ist zwar bei Laravel Websockets seit 2018 für die v2.0 geplant und hat es mittlerweile auch im v2.0 Branch geschafft. Dennoch gibt es zu der Konfiguration keinerlei Dokumentation und Konzepte für ein Production betrieb.

Socket.IO wiederum unterstützt seit der v3.0 (Release Date: Nov 4, 2020) natives Clustering. Vorrausgesetzt dafür ist ein Redis-Server. Wenn man auch noch HTTP long-polling als Fallback-Protokoll aktivieren möchte, dann wird auch ein Loadbalancer für Sticky-Sessions benötigt (wir verwenden dafür HaProxy).

Passing events between nodes — Source: socket.io

Durch unsere neue Infrastruktur haben wir eine komplette Horizontale Skalierbarkeit, da wir nicht mehr auf einer Single Application angewiesen sind. Somit können wir in der Theorie extrem stark wachsen. Themen die für uns in Zukunft relevant werden, sind zum Beispiel geographische Skalierbarkeit, sprich das Routing zu optimieren um hohe Pings zu veringern.

Reason 3 — Fallback Strategy

In unserem neuen System haben wir zwei Technologien um Alerts in OBS auszuliefern, diese sind Websockets (Sockets die eine Aktive Verbindung haben) sowie HXR-Polling. Dieses ermöglicht uns nun auch das Zustellen von Alerts, auch wenn keine Websocket verbindung aufgebaut werden kann.

Reason 4— Overhead

Das coole an unserer neuen Infrastruktur ist, dass durch die Umstellung der ganze Quellcode sich auf insgesamt 93 Zeilen begrenzt. Natürlich werden wir in Zukunft weitere Optimierungen und Features einbauen. Aber durch den wenigen Overhead konnten wir die Infrastruktur in unter 24 Stunden mit einem Entwickler komplett austauschen.

Der Socket.IO Server

Unser aktueller Socket.IO Server besteht gerade mal aus unter 50 Zeilen Code. Mit dem Ziel Nachrichten von unserem Notify Backend zu akzeptieren und dann an die jeweiligen OBS Studio Clients zu verschicken.

require('dotenv').config();

const express = require('express');
const app = express();
const basicAuth = require('express-basic-auth');
const bodyParser = require('body-parser');

app.use(bodyParser.json());

const auth = basicAuth({
    users: {'own3d-socket': process.env.AUTH_PASSWORD},
});

// Health check
app.head('/health', function (req, res) {
    res.sendStatus(200);
});

// HTTP Endpoint to emit data into rooms
app.post('/emit', auth, (req, res) => {
    io.in(req.body.room).emit(req.body.event, req.body.data);
    res.send(req.body);    // echo the result back
});

const server = app.listen(3000, () => {
    console.log('listening on *:3000');
});

const io = require('socket.io')(server, {
    cors: {
        origin: true,
        credentials: true,
        methods: ["GET", "POST"]
    }
});

const redis = require('socket.io-redis');
io.adapter(redis({host: '10.1.0.2', port: 6379}));

// handle incoming connections from clients
io.on('connection', (socket) => {
    console.log('a user connected');
    // once a client has connected, we expect to get a ping from them saying what room they want to join
    socket.on('room', function(room) {
        socket.join(room);
    });
});

Der Socket.IO Frontend Client

Mit gerade mal 12 Zeilen Code kann man sich zum Socket.IO Cluster verbinden und alle Events eines Streamers empfangen.

const socket = io('https://socket-hel1-1.own3d.dev', {
    withCredentials: true,
});

socket.on("connect", () => {
  document.write(socket.id + '<br>');
  socket.emit('room', 'twitch.106415581');
});

socket.on('notifysub', (data) => {
  document.write(JSON.stringify(data.subscription) + '<br>');
});

Der Socket.IO Backend Client

Der Backend Client ermöglicht es uns Nachrichten an den Socket.IO Server zu senden.

<?php

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;

class Own3dSocket
{
    public function emit(string $platform, string $id, array $data): bool
    {
        $response = $this->getClient()->post('emit', [
            RequestOptions::JSON => [
                'room' => sprintf('%s.%s', $platform, $id),
                'event' => 'notifysub',
                'data' => $data
            ],
            RequestOptions::AUTH => [
                config('services.own3d-socket.username'),
                config('services.own3d-socket.password'),
            ],
        ]);

        return in_array($response->getStatusCode(), [200, 204]);
    }

    private function getClient(): Client
    {
        return new Client([
            'base_uri' => 'https://socket-hel1-1.own3d.dev',
            'http_errors' => false,
        ]);
    }
}
Follow me!

Related stories

You liked Review: Laravel Websockets Breaks Our Alert System & Socket.IO Fixes It? You may also be interested in these following articles...
...

With SUBtember right around the corner, you might be wondering what all the fuss is about. What's the difference between a subathon and SUBtember? Let's take a closer look.

GhostZero
1 month ago
...

Düsseldorf. 70,000 visitors came to the DoKomi for manga fans and cosplay in Düsseldorf. This means that DoKomi set a new record for visitor numbers and a growth of +27 percent (compared to 2019).

GhostZero
3 months ago
...

Mit YAFS (Yet Another Firmware Selector) ist es nun möglich die Freifunk Ense Firmware für unterstützte Router zu finden und herunterzuladen.

GhostZero
6 months ago
GhostZero is live on Twitch!
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy.