Mikhail Salosin (im Folgenden – MS): – Hallo zusammen! Mein Name ist Mikhail. Ich arbeite als Backend-Entwickler bei MC2 Software und werde über den Einsatz von Go im Backend der mobilen App „Smotri+“ berichten.

Hat jemand hier eine Vorliebe für Hockey?

Dann ist diese App genau das Richtige für Sie. Sie ist für Android und iOS verfügbar und dient zum Ansehen von Übertragungen verschiedener Sportereignisse, sowohl live als auch in der Aufzeichnung. Die App bietet zudem verschiedene Statistiken, Textübertragungen, Tabellen zu Konferenzen, Turnieren und weitere nützliche Informationen für Fans.

Außerdem bietet die App eine Funktion namens Videomomente, d. h. Sie können sich die besten Momente der Spiele (Tore, Kämpfe, Penaltys usw.) ansehen. Wenn Sie nicht die gesamte Übertragung anschauen möchten, können Sie sich nur das Interessanteste ansehen.
Welche Technologien wurden in der Entwicklung verwendet?
Der Hauptteil wurde in Go geschrieben. Die API, mit der die mobilen Clients kommunizierten, wurde ebenfalls in Go erstellt. Zusätzlich wurde ein Service zur Versendung von Push-Benachrichtigungen für mobile Geräte in Go entwickelt. Außerdem mussten wir unser eigenes ORM schreiben, worüber wir vielleicht irgendwann berichten werden. Und in Go wurden einige kleinere Dienste geschrieben: das Resizing und der Upload von Bildern für die Redakteure…
Als Datenbank verwendeten wir PostgreSQL. Die Benutzeroberfläche für die Redakteure wurde mit Ruby on Rails und dem Gem ActiveAdmin entwickelt. Auch der Import von Statistiken von unserem Statistikdienstleister wurde in Ruby geschrieben.
Für die systematischen API-Tests haben wir Unittest von Python verwendet. Memcached kommt zum Throttling der API-Zahlungsanfragen zum Einsatz, Chef wird für die Konfigurationskontrolle genutzt, Zabbix übernimmt das Sammeln und Überwachen interner Systemstatistiken. Graylog2 wird zur Protokollsammlung verwendet, Slate ist die API-Dokumentation für Kunden.

Wahl des Protokolls
Das erste Problem, mit dem wir konfrontiert waren: Wir mussten ein Protokoll für die Interaktion zwischen dem Backend und den mobilen Clients auswählen, basierend auf den folgenden Punkten…
- Das wichtigste Kriterium ist, dass die Daten der Kunden in Echtzeit aktualisiert werden müssen. Das bedeutet, dass alle, die gerade einen Live-Stream ansehen, fast sofortige Updates erhalten sollten.
- Zur Vereinfachung haben wir angenommen, dass die Daten, die mit den Kunden synchronisiert werden, nicht gelöscht, sondern durch spezielle Flags verborgen werden.
- Seltene Anfragen (wie Statistiken, Teamzusammenstellungen, Teamstatistiken) werden durch normale GET-Anfragen abgerufen.
- Außerdem musste das System problemlos 100.000 gleichzeitige Nutzer bewältigen können.
Ausgehend davon hatten wir zwei Protokolloptionen:
- Websockets. Aber wir benötigten keine Verbindungen vom Client zum Server. Wir mussten nur Updates vom Server an den Client senden, daher war Websocket überflüssig.
- Server-Sent Events (SSE) erfüllten genau unsere Anforderungen! Es ist einfach genug und entspricht im Grunde allen unseren Bedürfnissen.
Server-Sent Events
Ein paar Worte darüber, wie dieses Konzept funktioniert...
Es funktioniert über eine HTTP-Verbindung. Der Client sendet eine Anfrage, auf die der Server mit Content-Type: text/event-stream antwortet und die Verbindung zum Client nicht schließt, sondern weiterhin Daten in die Verbindung schreibt:

Daten können in einem Format gesendet werden, das mit den Kunden abgestimmt ist. In unserem Fall haben wir es so gesendet: Im Feld event wurde der Name der geänderten Struktur (Person, Spieler) angegeben, und im Feld data – ein JSON mit den neuen, geänderten Feldern für den Spieler.
Nun zum eigentlichen Interaktionsablauf.
- Zunächst bestimmt der Kunde, wann zuletzt eine Synchronisierung mit dem Dienst stattgefunden hat: Er schaut in seine lokale Datenbank und ermittelt das Datum der letzten Änderung, die bei ihm aufgezeichnet wurde.
- Er sendet eine Anfrage mit diesem Datum.
- Daraufhin senden wir ihm alle Updates, die seit diesem Datum aufgetreten sind.
- Anschließend stellt er eine Verbindung zum Live-Kanal her und hält diese offen, solange er diese Updates benötigt:

Wir senden ihm eine Liste der Änderungen: Wenn jemand ein Tor erzielt hat – wird der Spielstand angepasst, wenn jemand verletzt wurde – wird dies ebenfalls in Echtzeit gesendet. So erhalten die Kunden in der Ereignisliste des Spiels sofort aktuelle Daten. Periodisch, um dem Kunden zu zeigen, dass der Server noch aktiv ist und nichts passiert ist, senden wir alle 15 Sekunden einen Zeitstempel – damit er weiß, dass alles in Ordnung ist und er sich nicht neu verbinden muss.
Wie wird eine Live-Verbindung betreut?
- Zunächst erstellen wir einen Kanal, über den die Aktualisierungen mit einem Puffer eintreffen.
- Danach abonnieren wir diesen Kanal, um die Aktualisierungen zu erhalten.
- Wir setzen den richtigen Header, damit der Kunde weiß, dass alles in Ordnung ist.
- Wir senden den ersten Ping. Dabei nehmen wir einfach den aktuellen Timestamp der Verbindung auf.
- Anschließend lesen wir in einer Schleife aus dem Kanal, bis der Aktualisierungskanal geschlossen wird. Periodisch kommen entweder der aktuelle Timestamp oder Änderungen in den Kanal, die wir bereits in den offenen Verbindungen festhalten.

Das erste Problem, mit dem wir konfrontiert waren, war folgendes: Für jede geöffnete Verbindung mit einem Kunden haben wir einen Timer erstellt, der einmal alle 15 Sekunden tickte – das bedeutete, wenn wir 6000 Verbindungen zu einer Maschine (einem API-Server) offen hatten, wurden 6000 Timer erstellt. Das führte dazu, dass die Maschine die erforderliche Last nicht bewältigen konnte. Das Problem war für uns nicht so offensichtlich, aber wir erhielten etwas Hilfe und konnten es lösen.
Jetzt kommt der Ping aus demselben Kanal, aus dem die Aktualisierung kommt.
Dementsprechend gibt es nun nur noch einen Timer, der alle 15 Sekunden tickt.
Hier sind einige Hilfsfunktionen – das Senden von Headern, Pings und der Struktur selbst. Das bedeutet, dass der Name der Tabelle (person, match, season) und die Informationen zu diesem Eintrag übergeben werden:

Der Mechanismus zum Versenden von Updates
Jetzt etwas dazu, woher die Änderungen kommen. Wir haben mehrere Personen, Redakteure, die die Live-Übertragung in Echtzeit verfolgen. Sie erstellen alle Ereignisse: Jemand wurde vom Platz gestellt, jemand hat sich verletzt, ein Austausch fand statt…
Durch das CMS gelangen die Daten in die Datenbank. Danach informiert die Datenbank über den Listen/Notify-Mechanismus die API-Server darüber. Die API-Server verteilen diese Informationen an die Kunden. So sind im Grunde nur einige Server mit der Datenbank verbunden, und es gibt keine besondere Belastung für die Datenbank, da der Kunde nicht direkt mit der Datenbank interagiert:

PostgreSQL: Listen/Notify
Der Listen/Notify-Mechanismus in PostgreSQL ermöglicht es, Abonnenten über Ereignisse zu informieren, wenn ein Ereignis geändert wurde – wenn ein Datensatz in der Datenbank erstellt wurde. Dafür haben wir einen einfachen Trigger und eine Funktion geschrieben:

Beim Einfügen oder Ändern eines Datensatzes rufen wir die Funktion notify auf dem Kanal data_updates auf, übergeben den Tabellennamen und die ID des Datensatzes, der geändert oder eingefügt wurde.
Für alle Tabellen, die mit dem Client synchronisiert werden sollen, definieren wir einen Trigger, der nach einer Änderung / Aktualisierung des Datensatzes die auf der Folie unten angegebene Funktion aufruft.
Wie abonniert die API diese Änderungen?
Es wird ein Fanout-Mechanismus erstellt – er verteilt Nachrichten an die Clients. Er sammelt alle Clientkanäle und sendet die Updates, die er über diese Kanäle erhalten hat:

Hier ist die Standardbibliothek pq, die sich mit der Datenbank verbindet und anzeigt, dass sie den Kanal (data_updates) hören möchte, überprüft, dass die Verbindung geöffnet ist und alles in Ordnung ist. Ich lasse die Fehlerüberprüfung weg, um Platz zu sparen (das Unterlassen von Prüfungen kann problematisch sein).
Anschließend setzen wir asynchron einen Ticker, der alle 15 Sekunden ein Ping sendet, und beginnen, den Kanal zu hören, auf den wir uns angemeldet haben. Wenn ein Ping empfangen wird, veröffentlichen wir dieses Ping. Wenn ein Datensatz empfangen wird, veröffentlichen wir diesen Datensatz an alle Abonnenten dieses Fanouts.
Wie funktioniert Fan-out?
Auf Deutsch wird das als „Verteiler“ übersetzt. Wir haben ein Objekt, das Abonnenten registriert, die Updates erhalten möchten. Sobald ein Update für dieses Objekt eintrifft, verteilt es dieses Update an alle bestehenden Abonnenten. Ganz einfach:

So wird es in Go umgesetzt:

Es gibt eine Struktur, die mit Mutexen synchronisiert wird. Diese hat ein Feld, das den Verbindungsstatus des Fanouts zur Datenbank speichert, d. h. derzeit hört es zu und wird Updates erhalten, sowie eine Liste aller verfügbaren Kanäle – eine Map, deren Schlüssel der Kanal ist und der Wert aus einer Struktur besteht (die eigentlich nicht verwendet wird).
Zwei Methoden – Connected und Disconnected – zeigen dem Fanout an, dass eine Verbindung zur Datenbank besteht, dass sie hergestellt wurde, und dass die Verbindung zur Datenbank unterbrochen wurde. Im letzteren Fall müssen alle Clients getrennt und darüber informiert werden, dass sie nichts mehr empfangen können und sich neu verbinden sollen, da die Verbindung zu ihnen geschlossen wurde.
Es gibt auch die Methode Subscribe, die einen Kanal zu den „Zuhörern“ hinzufügt:

Es gibt eine Unsubscribe-Methode, die den Kanal aus den Abonnenten entfernt, wenn der Kunde sich abmeldet, sowie eine Publish-Methode, die es ermöglicht, eine Nachricht an alle Abonnenten zu senden.
Frage: – Was wird über diesen Kanal übertragen?
MS: – Es wird entweder das Modell, das sich geändert hat, oder ein Ping (im Grunde einfach eine Zahl, Integer) übertragen.
MS: – Man kann alles Mögliche übermitteln, jede Struktur kann veröffentlicht werden – sie wird einfach in JSON umgewandelt und das war's.
MS: – Wir erhalten eine Benachrichtigung aus PostgreSQL – darin ist der Tabellenname und die ID enthalten. Anhand des Tabellennamens und der ID rufen wir den benötigten Datensatz ab und senden diese Struktur zur Veröffentlichung.
Infrastruktur
Wie sieht das aus infrastrukturell? Wir haben 7 physische Server: einer davon ist vollständig für die Datenbank reserviert, auf den anderen sechs laufen die virtuellen Maschinen. Es gibt 6 Kopien des API: Jede virtuelle Maschine mit API läuft auf einem separaten physischen Server – das dient der Zuverlässigkeit.

Wir haben zwei Frontends, auf denen Keepalived installiert ist, um die Verfügbarkeit zu verbessern, damit im Bedarfsfall ein Frontend das andere ersetzen kann. Außerdem gibt es zwei Kopien des CMS.
Es gibt auch einen Statistik-Importeur. Es gibt einen DB-Slave, von dem regelmäßig Backups gemacht werden. Und es gibt Pigeon Pusher – die Anwendung, die Push-Benachrichtigungen an die Kunden sendet, sowie wichtige Infrastrukturkomponenten: Zabbix, Graylog2 und Chef.
Tatsächlich ist diese Infrastruktur überdimensioniert, denn 100.000 Benutzer könnten auch mit weniger Servern bedient werden. Aber wir hatten die Hardware – also haben wir sie genutzt (uns wurde gesagt, dass wir es können – warum also nicht).
Die Vorteile von Go
Nachdem wir an dieser Anwendung gearbeitet haben, sind uns einige bemerkenswerte Vorteile von Go aufgefallen.
- Eine großartige HTTP-Bibliothek. Mit ihr kann man schon 'out of the box' eine Menge erstellen.
- Außerdem die Kanäle, die es uns ermöglicht haben, den Mechanismus zur Versendung von Benachrichtigungen an Kunden ganz einfach umzusetzen.
- Das großartige Tool Race Detector hat uns geholfen, mehrere kritische Bugs (in der Staging-Infrastruktur) zu beseitigen. Alles, was auf der Staging-Umgebung läuft, wird mit dem Race-Flag kompiliert; so können wir in der Staging-Umgebung potenzielle Probleme erkennen.
- Minimalismus und Einfachheit der Sprache.

Wir suchen Entwickler! Wer Interesse hat, ist herzlich eingeladen.
Fragen
Frage aus dem Publikum (im Folgenden – F): – Ich denke, Sie haben einen wichtigen Punkt bezüglich Fan-out übersehen. Verstehe ich richtig, dass Sie sich blockieren, wenn der Kunde nicht weiterlesen möchte, nachdem Sie eine Antwort gesendet haben?
MS: – Nein, wir blockieren uns nicht. Erstens befindet sich alles hinter nginx, das heißt, es gibt keine Probleme mit langsamen Kunden. Zweitens hat der Kunde einen gepufferten Kanal – im Grunde können wir dort bis zu hundert Updates ablegen… Wenn wir nicht in den Kanal schreiben können, wird dieser gelöscht. Wenn wir sehen, dass der Kanal blockiert ist, schließen wir einfach den Kanal und alles – der Kunde wird die Verbindung wiederherstellen, wenn ein Problem auftritt. Daher gibt es hier grundsätzlich keine Blockierungen.
Frage: – Könnte man nicht direkt einen Listen/Notify-Eintrag senden anstatt einer Tabellen-ID?
MS: – Listen/Notify hat eine Begrenzung von 8.000 Bytes für das Preload, das es sendet. Man könnte es theoretisch senden, wenn wir mit einer geringen Datenmenge zu tun hätten, aber ich denke, dass unsere Methode einfach zuverlässiger ist. Die Einschränkungen liegen im PostgreSQL selbst.
Frage: – Bekommen die Kunden Updates zu Spielen, die sie nicht interessieren?
MS: — Im Allgemeinen ja. In der Regel laufen 2-3 Spiele parallel, und das geschieht auch nur selten. Wenn ein Kunde etwas schaut, dann schaut er normalerweise das aktuelle Spiel. Darüber hinaus gibt es beim Kunden eine lokale Datenbank, in die all diese Updates gespeichert werden. Selbst ohne Internetverbindung kann der Kunde alle vergangene Spiele einsehen, zu denen er Updates hat. Im Grunde synchronisieren wir unsere Datenbank auf dem Server mit der lokalen Datenbank des Kunden, damit er auch offline arbeiten kann.
Frage: — Warum haben Sie Ihr eigenes ORM entwickelt?
Aleksey (einer der Entwickler von „Smotri+“): — Zu dem Zeitpunkt (es ist ungefähr ein Jahr her) gab es weniger ORM-Systeme als heute, als es bereits ziemlich viele gibt. Was ich an den meisten bestehenden ORM-Systemen am meisten nicht mag, ist, dass sie alle auf leeren Schnittstellen basieren. Das bedeutet, dass die Methoden dieser ORM bereit sind, alles Mögliche zu akzeptieren: Strukturen, Zeiger auf Strukturen, Zahlen, oder völlig irrelevante Dinge...
Unser ORM generiert Strukturen basierend auf dem Datenmodell. Automatisch. Daher sind alle Methoden konkret, nutzen keine Reflexion usw. Sie akzeptieren Strukturen und erwarten, diese Strukturen zu verwenden, die übergeben werden.
Frage: – Wie viele Personen waren beteiligt?
MS: – In der Anfangsphase waren zwei Personen beteiligt. Irgendwann im Juni haben wir angefangen, im August war der Großteil fertig (erste Version). Im September gab es die Veröffentlichung.
Frage: – In der Beschreibung von SSE verwenden Sie kein Timeout. Warum?
MS: – Um ehrlich zu sein, SSE ist letztlich ein HTML5-Protokoll: Der SSE-Standard ist für die Kommunikation mit Browsern gedacht, soweit ich weiß. Es hat zusätzliche Funktionen, um den Browsern das Wiederverbinden (und andere Dinge) zu ermöglichen, aber die benötigten wir nicht, weil wir Kunden hatten, die jede Verbindungs- und Informationsabruf-Logik umsetzen konnten. Wir haben eher etwas geschaffen, das einem SSE ähnelt. Es ist nicht das Protokoll selbst.
Es war nicht notwendig. Soweit ich verstehe, haben die Kunden den Verbindungsmechanismus praktisch von Grund auf neu implementiert. Es war ihnen prinzipiell egal.
Frage: – Welche zusätzlichen Tools haben Sie verwendet?
MS: – Am aktivsten haben wir govet und golint verwendet, um einen einheitlichen Stil zu gewährleisten, sowie gofmt. Wir haben nichts anderes verwendet.
Frage: – Mit welcher Methode haben Sie Debugging durchgeführt?
MS: – Das Debugging wurde hauptsächlich durch Tests durchgeführt. Wir haben keinen Debugger oder GOP verwendet.
Frage: – Können Sie die Folie zurückbringen, auf der die Funktion Publish implementiert ist? Stören Sie sich nicht an den einbuchstabigen Variablennamen?
MS: – Nein. Sie haben einen ziemlich «engen» Sichtbereich. Sie werden nirgendwo anders verwendet (außer in den inneren Abläufen dieser Klasse) und sind sehr kompakt – bestehen nur aus 7 Zeilen.
Frage: – Irgendwie ist das trotzdem nicht intuitiv...
MS: – Nein, das ist wirklich Code! Es liegt nicht am Stil. Es ist einfach eine sehr utilitaristische, ganz kleine Klasse – nur 3 Felder innerhalb der Klasse...

MS: – Im Großen und Ganzen ändern sich alle Daten, die mit den Clients synchronisiert werden (saisonale Spiele, Spieler), nicht. Grob gesagt, wenn wir eine andere Sportart machen, in der ein Spiel geändert werden muss, werden wir einfach in der neuen Version des Clients alles berücksichtigen, während die alten Versionen des Clients gesperrt werden.
Frage: – Gibt es irgendwelche Drittanbieter-Pakete zur Verwaltung von Abhängigkeiten?
MS: – Wir haben go dep verwendet.
Frage: – Im Thema des Berichts war etwas über Videos, aber im Bericht ist nichts über Videos.
MS: – Nein, ich habe nichts im Thema über Videos. Es heißt «Schaue+» – so heißt die Anwendung.
Frage: – Sie haben gesagt, dass es an die Clients gestreamt wird?..
MS: – Mit Streaming-Videos haben wir uns nicht beschäftigt. Das hat vollständig «Megafon» übernommen. Ja, ich habe nicht erwähnt, dass es eine Megafon-Anwendung ist.
MS: – Go – für die Verteilung aller Daten – bezüglich Abrechnungen, Spielereignissen, Statistiken… Go – das ist das gesamte Backend für die Anwendung. Der Kunde muss irgendwoher erfahren, welchen Link er für den Player verwenden soll, damit der Benutzer das Spiel sehen kann. Wir haben Links zu Videos und Streams vorbereitet.

Ein wenig Werbung 🙂
Danke, dass Sie bei uns bleiben. Gefallen Ihnen unsere Artikel? Möchten Sie mehr interessante Inhalte sehen? Unterstützen Sie uns, indem Sie eine Bestellung aufgeben oder uns Ihren Freunden empfehlen. , eine einzigartige Alternative zu Einsteiger-Servern, die wir für Sie entwickelt haben: (Verfügbar sind Optionen mit RAID1 und RAID10, bis zu 24 Kerne und bis zu 40GB DDR4).
Dell R730xd im Equinix Tier IV Rechenzentrum in Amsterdam zum halben Preis? Nur bei uns in den Niederlanden! Dell R420 — 2x E5-2430 2.2GHz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — ab 99 $! Lesen Sie darüber
Quelle: habr.com
