Michail Salosin (im Folgenden â MS): â Hallo zusammen! Ich heiĂe Michail. Ich arbeite als Backend-Entwickler bei der Firma MC2 Software und ich werde ĂŒber die Verwendung von Go im Backend der mobilen Anwendung âSmotri+â berichten.

Hat jemand von den Anwesenden Lust auf Hockey?

Dann ist diese App genau das Richtige fĂŒr Sie. Sie ist fĂŒr Android und iOS und dient dazu, Live-Ăbertragungen verschiedener Sportereignisse online und im Replay anzusehen. AuĂerdem bietet die App verschiedene Statistiken, TextĂŒbertragungen, Tabellen zu Konferenzen, Turnieren und sonstige Informationen, die fĂŒr Fans nĂŒtzlich sind.

In der App gibt es auch die Möglichkeit, Videomomente anzusehen, d. h. man kann die spannenden Momente der Spiele (Tore, KĂ€mpfe, Penaltys usw.) sehen. Wenn Sie nicht die gesamte Ăbertragung sehen möchten, können Sie sich nur die interessantesten Teile anschauen.
Was haben Sie in der Entwicklung verwendet?
Der Hauptteil wurde in Go geschrieben. Die API, mit der die mobilen Clients kommunizierten, war in Go geschrieben. Auch der Service zum Versenden von Push-Benachrichtigungen an mobile GerĂ€te wurde in Go erstellt. DarĂŒber hinaus mussten wir unser eigenes ORM schreiben, ĂŒber das wir vielleicht eines Tages berichten werden. AuĂerdem wurden in Go einige kleinere Services wie das Resizing und das Hochladen von Bildern fĂŒr die Redaktion geschrieben...
Als Datenbank haben wir PostgreSQL verwendet. Die OberflĂ€che fĂŒr die Redakteure wurde mit Ruby on Rails unter Verwendung des Gems (gem) ActiveAdmin geschrieben. Der Import von Statistiken vom Statistikanbieter wurde auch in Ruby realisiert.
FĂŒr systematische API-Tests haben wir das Unittest-Framework von Python verwendet. Memcached wird zur Drosselung von API-Zahlungsanfragen eingesetzt, Chef fĂŒr das Konfigurationsmanagement, Zabbix fĂŒr die Erfassung und Ăberwachung interner statistischer Daten des Systems. Graylog2 wird zum Sammeln von Logs verwendet, Slate ist die API-Dokumentation fĂŒr die Kunden.

Protokollwahl
Das erste Problem, mit dem wir konfrontiert waren: Wir mussten ein Protokoll fĂŒr die Interaktion zwischen Backend und mobilen Clients wĂ€hlen, basierend auf den folgenden Punkten...
- Das wichtigste Kriterium: Die Daten auf den Clients mĂŒssen in Echtzeit aktualisiert werden. Das heiĂt, alle, die gerade die Ăbertragung sehen, sollten praktisch sofortige Updates erhalten.
- Zur Vereinfachung haben wir angenommen, dass die Daten, die mit den Clients synchronisiert werden, nicht gelöscht, sondern durch spezielle Flags versteckt werden.
- Verschiedene seltene Anfragen (wie Statistiken, Teamzusammensetzungen, Teamstatistiken) erfolgen ĂŒber normale GET-Anfragen.
- AuĂerdem musste das System problemlos 100.000 Benutzer gleichzeitig verkraften.
Daraus ergeben sich zwei Protokolloptionen:
- Websockets. Aber wir benötigten keine KanĂ€le vom Client zum Server. Wir mussten nur Aktualisierungen vom Server an den Client senden, daher ist Websocket eine ĂŒberflĂŒssige Wahl.
- Server-Sent Events (SSE) passten perfekt! Es ist ziemlich einfach und erfĂŒllt im Prinzip alles, was wir brauchen.
Server-Sent Events
Ein paar Worte dazu, wie dieses Ding funktioniertâŠ
Es arbeitet ĂŒber eine HTTP-Verbindung. Der Client sendet eine Anfrage, der Server antwortet mit Content-Type: text/event-stream und schlieĂt die Verbindung mit dem Client nicht, sondern schreibt weiterhin Daten in die Verbindung:

Daten können im Format gesendet werden, das mit den Clients vereinbart wurde. In unserem Fall haben wir es so gesendet: im Feld event wurde der Name der geĂ€nderten Struktur (Person, Spieler) gesendet, und im Feld data â ein JSON mit den neuen, geĂ€nderten Feldern fĂŒr den Spieler.
Jetzt zur Interaktion selbst.
- Zuerst bestimmt der Client, wann die letzte Synchronisation mit dem Dienst stattfand: Er schaut in seiner lokalen Datenbank nach und bestimmt das Datum der letzten Ănderung, die bei ihm aufgezeichnet wurde.
- Er sendet eine Anfrage mit diesem Datum.
- Als Antwort senden wir ihm alle Aktualisierungen, die seit diesem Datum erfolgt sind.
- Danach stellt er eine Verbindung zum Live-Kanal her und schlieĂt sie nicht, solange er diese Aktualisierungen benötigt:

Wir schicken ihm eine Liste von Ănderungen: Wenn jemand ein Tor schieĂt, Ă€ndern wir den Spielstand, erhĂ€lt er eine Verletzung â wird auch in Echtzeit gesendet. So erhalten die Clients in der Ereignisliste des Spiels sofort aktuelle Daten. Periodisch, damit der Client versteht, dass der Server nicht abgestĂŒrzt ist, dass ihm 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 die Live-Verbindung gewartet?
- Zuerst erstellen wir einen Kanal, in den die Aktualisierungen mit einem Puffer kommen.
- Danach abonnieren wir diesen Kanal, um Aktualisierungen zu erhalten.
- Wir setzen den richtigen Header, damit der Client weiĂ, dass alles in Ordnung ist.
- Wir senden das erste Ping. Einfach den aktuellen Zeitstempel der Verbindung aufzeichnen.
- Nach diesem Schritt lesen wir im Loop aus dem Kanal, bis der Update-Kanal geschlossen wird. In den Kanal kommt regelmĂ€Ăig entweder der aktuelle Timestamp oder Ănderungen, die wir bereits in den geöffneten Verbindungen aufnehmen.

Das erste Problem, mit dem wir konfrontiert waren, bestand darin, dass wir fĂŒr jede mit dem Client geöffnete Verbindung einen Timer erstellt haben, der alle 15 Sekunden tickte â das bedeutet, wenn wir 6000 Verbindungen zu einem Rechner (einem API-Server) offen hatten, wurden 6000 Timer erstellt. Das fĂŒhrte dazu, dass die Maschine die erforderliche Last nicht tragen konnte. Das Problem war fĂŒr uns nicht so offensichtlich, aber wir bekamen etwas UnterstĂŒtzung und haben es behoben.
In der Folge kommt unser Ping jetzt aus demselben Kanal, aus dem die Updates kommen.
Dementsprechend gibt es nur einen Timer, der alle 15 Sekunden tickt.
Hier sind einige Hilfsfunktionen â das Senden des Headers, des Pings und der Struktur selbst. Das heiĂt, hier wird der Tabellenname (person, match, season) und die Informationen ĂŒber diesen Datensatz ĂŒbergeben:

Mechanismus zum Senden von Updates
Jetzt ein wenig darĂŒber, woher die Ănderungen kommen. Wir haben mehrere Personen, Redakteure, die die Ăbertragung in Echtzeit beobachten. Sie erzeugen alle Ereignisse: jemand wurde entfernt, jemand hat sich verletzt, ein WechselâŠ
Mithilfe von CMS gelangen die Daten in die Datenbank. Danach benachrichtigt die Datenbank ĂŒber den Listen/Notify-Mechanismus die API-Server darĂŒber. Die API-Server verbreiten diese Informationen dann an die Clients. So haben wir faktisch nur einige Server, die mit der Datenbank verbunden sind, und es gibt keine besondere Last auf der Datenbank, da der Client in keiner Weise direkt mit der Datenbank interagiert:

PostgreSQL: Listen/Notify
Der Listen/Notify-Mechanismus in PostgreSQL ermöglicht es, Abonnenten ĂŒber Ănderungen an Ereignissen zu informieren â wenn ein bestimmter Datensatz in der Datenbank erstellt wurde. Dazu haben wir einen einfachen Trigger und eine Funktion geschrieben:

Bei einem Insert oder einer Ănderung des 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 der Ănderung/Aktualisierung eines Datensatzes die angegebene Funktion aufruft, die auf der Folie unten angegeben ist.
Wie meldet sich die API fĂŒr diese Ănderungen an?
Ein Fanout-Mechanismus wird erstellt â er sendet Nachrichten an die Clients. Er sammelt alle KanĂ€le der Clients und verteilt die Updates, die er ĂŒber diese KanĂ€le erhalten hat:

Hier ist die Standardbibliothek pq, die sich mit der Datenbank verbindet und angibt, dass sie den Kanal (data_updates) hören möchte, ĂŒberprĂŒft, ob die Verbindung geöffnet ist und alles in Ordnung ist. Ich lasse die FehlerschutzprĂŒfung aus, um Platz zu sparen (keine ĂberprĂŒfung kann riskant sein).
Dann legen wir asynchron einen Ticker fest, der alle 15 Sekunden ein Ping sendet, und beginnen, den Kanal zu hören, auf den wir uns angemeldet haben. Wenn ein Ping eingeht, veröffentlichen wir diesen Ping. Wenn wir einen Datensatz erhalten, veröffentlichen wir diesen Datensatz an alle Abonnenten dieses Fanouts.
Wie funktioniert Fan-out?
Auf Russisch wird es als âVerteilerâ ĂŒbersetzt. Wir haben ein Objekt, das die Abonnenten registriert, die Updates erhalten möchten. Sobald ein Update fĂŒr dieses Objekt eintrifft, verteilt es dieses Update an alle vorhandenen Abonnenten. Ganz einfach:

Wie das in Go ŃĐ”Đ°Đ»ĐžĐ·ĐŸĐČĐ°ĐœĐŸ:

Es gibt eine Struktur, die mit Hilfe von Mutexen synchronisiert wird. Sie hat ein Feld, das den Zustand der Verbindung des Fanouts zur Datenbank speichert, d. h. im Moment hört sie zu und erhĂ€lt Updates, sowie eine Liste aller vorhandenen KanĂ€le â eine Map, deren SchlĂŒssel der Kanal ist und struct als Werte (im Grunde wird es nicht genutzt).
Die beiden Methoden â Connected und Disconnected â ermöglichen es dem Fanout zu sagen, dass wir eine Verbindung zur Datenbank haben, dass sie hergestellt wurde, und dass die Verbindung zur Datenbank getrennt wurde. Im zweiten Fall mĂŒssen alle Clients trennen und ihnen mitteilen, dass sie nichts mehr hören können und sie sich neu verbinden sollten, da die Verbindung zu ihnen abgebrochen wurde.
Es gibt auch die Methode Subscribe, die einen Kanal zu den âZuhörernâ hinzufĂŒgt:

Es gibt die Methode Unsubscribe, die einen Kanal aus den Zuhörern entfernt, wenn sich der Client getrennt hat, sowie die Methode Publish, die es ermöglicht, eine Nachricht an alle Abonnenten zu senden.
Frage: â Was wird ĂŒber diesen Kanal ĂŒbertragen?
MS: â Es wird ein Modell ĂŒbertragen, das sich geĂ€ndert hat, oder ein Ping (im Grunde nur eine Zahl, Integer).
MS: â Man kann alles, jede Struktur ĂŒbermitteln, veröffentlichen â sie wird einfach in JSON umgewandelt und das war's.
MS: â Wir erhalten eine Benachrichtigung von âPostgresâ â sie enthĂ€lt den Tabellennamen und die ID. Ăber den Tabellennamen und die ID erhalten wir den benötigten Datensatz und senden diese Struktur zur Veröffentlichung.
Infrastruktur
Wie sieht das aus Sicht der Infrastruktur aus? Wir haben 7 physische Server: einer davon ist vollstĂ€ndig fĂŒr die Datenbank reserviert, auf den anderen sechs laufen virtuelle Maschinen. Es gibt 6 Kopien der API: jede virtuelle Maschine mit der API lĂ€uft auf einem separaten physischen Server â das sorgt fĂŒr ZuverlĂ€ssigkeit.

Wir haben zwei Frontends, auf denen Keepalived installiert ist, um die VerfĂŒgbarkeit zu verbessern, damit im Notfall ein Frontend das andere ersetzen kann. AuĂerdem gibt es zwei Kopien des CMS.
Es gibt auch einen Statistik-Importer. Es gibt einen DB-Slave, von dem regelmĂ€Ăig Backups erstellt werden. Und es gibt Pigeon Pusher â die Anwendung, die Push-Benachrichtigungen an Kunden sendet, sowie infrastrukturelle Dinge: Zabbix, Graylog2 und Chef.
Diese Infrastruktur ist tatsĂ€chlich ĂŒberdimensioniert, da man 100.000 auch mit weniger Servern bedienen kann. Aber die Hardware war da â wir haben sie genutzt (uns wurde gesagt, dass wir das können â warum nicht).
Vorteile von Go
Nachdem wir an dieser Anwendung gearbeitet haben, wurden einige offensichtliche Vorteile von Go deutlich.
- Tolle HTTP-Bibliothek. Damit kann man schon âout of the boxâ viel erstellen.
- AuĂerdem ermöglichen uns die KanĂ€le, den Mechanismus zum Versenden von Benachrichtigungen an Kunden sehr einfach zu implementieren.
- Das groĂartige Tool Race Detector hat uns geholfen, mehrere kritische Bugs (Staging-Infrastruktur) zu beseitigen. Alles, was auf der Staging-Umgebung lĂ€uft â wurde gestartet und mit dem SchlĂŒssel Race kompiliert; und wir können dementsprechend in der Staging-Infrastruktur sehen, welche potenziellen Probleme wir haben.
- Minimalismus und Einfachheit der Sprache.

Wir suchen Entwickler! Wenn jemand Interesse hat â bitte.
Fragen
Frage aus dem Publikum (im Folgenden â F): â Ich habe das GefĂŒhl, dass Sie einen wichtigen Punkt bezĂŒglich Fan-out ĂŒbersehen haben. Verstehe ich das richtig, dass Sie sich blockieren, wenn der Kunde nicht lesen möchte, wenn Sie ihm eine Antwort senden?
MS: â Nein, wir blockieren uns nicht. Erstens liegt alles hinter nginx, das heiĂt, es gibt keine Probleme mit langsamen Clients. Zweitens hat der Client einen gepufferten Kanal â im Grunde genommen können wir dort bis zu hundert Updates ablegen... Wenn wir in den Kanal nicht schreiben können, entfernt er ihn. Wenn wir sehen, dass der Kanal blockiert ist, schlieĂen wir einfach den Kanal und alles â der Client wird sich erneut verbinden, falls ein Problem auftritt. Daher gibt es hier grundsĂ€tzlich keine Blockierungen.
Frage: â Konnte man nicht direkt den Listen/Notify-Eintrag senden und nicht die Tabellenkennung?
MS: â Bei Listen/Notify gibt es eine BeschrĂ€nkung von 8000 Byte fĂŒr das Preload, das gesendet wird. GrundsĂ€tzlich könnte man senden, wenn wir es mit einer geringen Datenmenge zu tun hĂ€tten, aber ich glaube, dass es so [wie wir es tun] einfach zuverlĂ€ssiger ist. Die EinschrĂ€nkungen kommen vom PostgreSQL selbst.
Frage: â Bekommen die Clients Updates zu Spielen, die sie nicht interessieren?
MS: â Im Prinzip ja. Normalerweise gibt es 2-3 Spiele, die parallel laufen, und das ist auch ziemlich selten. Wenn ein Client etwas sieht, schaut er normalerweise auf das Spiel, das gerade lĂ€uft. AuĂerdem hat der Client eine lokale Datenbank, in die all diese Updates eingeliefert werden, und selbst ohne Internetverbindung kann der Client alle vergangenen Spiele ansehen, zu denen er Updates hat. Im Grunde synchronisieren wir unsere Datenbank auf dem Server mit der lokalen Datenbank des Clients, damit er auch offline arbeiten kann.
Frage: â Warum habt ihr eure eigene ORM entwickelt?
Alexei (einer der Entwickler von âSotri+â): â Zu dem Zeitpunkt (das war vor einem Jahr) gab es weniger ORMs als jetzt, wo es ziemlich viele gibt. Von den meisten bestehenden ORMs gefĂ€llt mir am wenigsten, dass die meisten von ihnen mit leeren Schnittstellen arbeiten. Das heiĂt, die Methoden, die in diesen ORMs vorhanden sind, sind bereit, alles Mögliche zu akzeptieren: Strukturen, Zeiger auf Strukturen, Zahlen, etwas, das ĂŒberhaupt nichts damit zu tun hatâŠ
Unsere ORM generiert Strukturen auf Basis des Datenmodells. SelbststÀndig. Und deswegen sind alle Methoden spezifisch, verwenden keine Reflexion usw. Sie akzeptieren Strukturen und erwarten die Verwendung jener Strukturen, die ankommen.
Frage: â Wie viele Personen haben daran teilgenommen?
MS: â In der Anfangsphase waren zwei Personen beteiligt. Irgendwann im Juni haben wir begonnen, im August war der gröĂte Teil fertig (die erste Version). Im September gab es die Veröffentlichung.
Frage: â Dort, wo Sie SSE beschreiben, verwenden Sie kein Timeout. Warum ist das so?
MS: â Um es klar zu sagen, SSE ist letztlich ein HTML5-Protokoll: Der Standard von SSE ist fĂŒr die Kommunikation mit Browsern gedacht, soweit ich verstehe. Es gibt zusĂ€tzliche Funktionen, damit sich die Browser wieder verbinden können (und Ă€hnliches), aber die brauchen wir nicht, da wir Kunden hatten, die jede Logik fĂŒr die Verbindung und den Informationsabruf implementieren konnten. Wir haben eher etwas gemacht, das SSE Ă€hnlich ist. Es ist nicht das Protokoll selbst.
Es gab keine Notwendigkeit. Soweit ich verstehe, haben die Kunden den Verbindungsmechanismus praktisch von Grund auf neu erstellt. Es war ihnen prinzipiell egal.
Frage: â Welche zusĂ€tzlichen Tools habt ihr verwendet?
MS: â Am aktivsten haben wir govet und golint verwendet, um einen einheitlichen Stil zu gewĂ€hrleisten, sowie gofmt. DarĂŒber hinaus nichts.
Frage: â Womit habt ihr debuggt?
MS: â Das Debuggen erfolgte im Wesentlichen mit Tests. Wir haben keinen Debugger, GOP verwendet.
Frage: â Könntest du die Folie zurĂŒckgeben, auf der die Publish-Funktion implementiert ist? Stören dich einbuchstabige Variablennamen?
MS: â Nein. Sie haben ein recht "enges" Sichtfeld. Sie werden nirgendwo anders verwendet, auĂer hier (auĂer den Innenleben dieser Klasse), und sie sind sehr kompakt â nehmen nur 7 Zeilen ein.
Frage: â Irgendwie ist das trotzdem nicht intuitiv...
MS: â Nein, nein, das ist echter Code! Es liegt nicht am Stil. Es ist einfach eine utilitaristische, sehr kleine Klasse â nur 3 Felder innerhalb der Klasse...

MS: â Im GroĂen und Ganzen Ă€ndern sich die Daten, die mit den Kunden synchronisiert werden (saisonale Spiele, Spieler), nicht. Grob gesagt, wenn wir eine weitere Sportart machen, bei der das Spiel geĂ€ndert werden muss, werden wir einfach in der neuen Version des Clients alles berĂŒcksichtigen, und die alten Versionen des Clients werden gesperrt.
Frage: â Gibt es irgendwelche Drittanbieter-Pakete zur Verwaltung von AbhĂ€ngigkeiten?
MS: â Wir haben go dep verwendet.
Frage: â Im Vortrag gab es etwas ĂŒber Video, aber im Bericht gibt es kein Video.
MS: â Nein, ich habe im Thema zum Video nichts. Es heiĂt âSotri+â â so heiĂt die App.
Frage: â Ihr habt gesagt, dass ihr an die Kunden streamt?..
MS: â Mit Streaming-Video haben wir uns nicht beschĂ€ftigt. Das hat vollstĂ€ndig âMegafonâ gemacht. Ja, ich habe nicht gesagt, dass die App von Megafon ist.
MS: â Go â zum Versenden aller Daten â zu Konten, zu Spielereignissen, Statistiken⊠Go â ist die komplette Backend-Lösung fĂŒr die Anwendung. Der Client muss irgendwo erfahren, welchen Link er fĂŒr den Player verwenden soll, damit der Benutzer das Spiel sehen kann. Wir haben Links zu Videos und Streams, die vorbereitet sind.

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 Freunden empfehlen, , ein einzigartiges Ăquivalent zu Einsteigerservern, das wir fĂŒr Sie entwickelt haben: (es sind Optionen mit RAID1 und RAID10 bis zu 24 Kernen und bis zu 40GB DDR4 verfĂŒgbar).
Dell R730xd ist im Equinix Tier IV Rechenzentrum in Amsterdam doppelt so gĂŒnstig? Nur bei uns in den Niederlanden! Dell R420 â 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB â ab $99! Lesen Sie, wie
Quelle: habr.com
