Mikhail Salosin (hierna – MS): – Hallo allemaal! Mijn naam is Mikhail. Ik werk als backend-ontwikkelaar bij MC2 Software en ik zal het hebben over het gebruik van Go in de backend van de mobiele toepassing ‘Bekijk+’.

Is er iemand in het publiek die van hockey houdt?

Dan is deze app iets voor jou. Het is beschikbaar voor Android en iOS en biedt de mogelijkheid om live en opgenomen uitzendingen van verschillende sportevenementen te bekijken. Daarnaast bevat de app verschillende statistieken, tekstverslagen, tabellen voor conferenties, toernooien en andere nuttige informatie voor fans.

De app beschikt ook over een functie genaamd videomomenten, dit betekent dat je hoogtepunten van wedstrijden (doelpunten, vechtpartijen, penalties, enz.) kunt bekijken. Als je geen zin hebt om de hele uitzending te bekijken, kun je alleen het meest interessante zien.
Wat hebben we gebruikt voor de ontwikkeling?
Het grootste deel is geschreven in Go. De API waarmee de mobiele cliënten communiceerden, was geschreven in Go. Ook de service voor het verzenden van pushmeldingen naar mobiele apparaten was in Go geschreven. Verder moesten we ons eigen ORM ontwikkelen, waar we misschien ooit meer over zullen vertellen. En er zijn nog een paar kleinere services in Go geschreven: resizing en uploaden van afbeeldingen voor de redacteurszijde…
Als database gebruikten we PostgreSQL. De interface voor de redacteurs was geschreven in Ruby on Rails met behulp van de gem ActiveAdmin. Ook werd de statistiek geïmporteerd vanuit de leverancier van statistieken in Ruby.
Voor system tests van de API gebruikten we de unittest van Python. Memcached wordt gebruikt voor throttling van API-aanroepen voor betalingen, Chef voor configuratiebeheer, Zabbix voor het verzamelen en monitoren van interne statistische gegevens van het systeem. Graylog2 wordt gebruikt voor het verzamelen van logs, Slate is de API-documentatie voor klanten.

Keuze van protocol
Het eerste probleem waarmee we werden geconfronteerd: we moesten een protocol kiezen voor de interactie tussen de backend en de mobiele cliënten, gebaseerd op de volgende punten…
- Het belangrijkste vereiste: de gegevens op de cliënten moeten in realtime worden bijgewerkt. Dit betekent dat iedereen die op dat moment de uitzending kijkt, bijna onmiddellijk updates moet ontvangen.
- Voor de versimpeling gingen we ervan uit dat de gegevens die met de cliënten worden gesynchroniseerd, niet worden verwijderd, maar worden verborgen met behulp van speciale vlaggen.
- Zeldzame verzoeken (zoals statistieken, teamopstellingen, teamstatistieken) worden ontvangen als gewone GET-verzoeken.
- Bovendien moest het systeem moeiteloos 100.000 gelijktijdige gebruikers aankunnen.
Daarop gebaseerd hadden we twee protocolopties:
- Websockets. Maar we hadden geen kanalen van de client naar de server nodig. We moesten alleen updates van de server naar de client sturen, dus was websocket een overbodige optie.
- Server-Sent Events (SSE) was perfect! Het is eenvoudig genoeg en voldoet in principe aan alles wat we nodig hebben.
Server-Sent Events
Een paar woorden over hoe dit systeem werkt...
Het werkt bovenop een http-verbinding. De client stuurt een verzoek, de server reageert met Content-Type: text/event-stream en sluit de verbinding met de client niet, maar blijft gegevens naar de verbinding schrijven:

Gegevens kunnen worden verzonden in een formaat dat met de clients is afgestemd. In ons geval verzonden we het in de volgende vorm: in het veld event werd de naam van de gewijzigde structuur (persoon, speler) geplaatst, en in het veld data – JSON met de nieuwe, gewijzigde velden voor de speler.
Laten we nu bespreken hoe de interactie zelf werkt.
- Als eerste bepaalt de client wanneer de laatste synchronisatie met de service heeft plaatsgevonden: hij kijkt in zijn lokale database en bepaalt de datum van de laatste wijziging die hij heeft geregistreerd.
- Hij stuurt een verzoek met deze datum.
- In antwoord sturen we hem alle updates die zich sinds deze datum hebben voorgedaan.
- Daarna maakt hij verbinding met het live-kanaal en sluit het niet tot hij deze updates nodig heeft:

We sturen hem een lijst met wijzigingen: als iemand een doelpunt heeft gescoord - wordt de score van de wedstrijd aangepast, als iemand geblesseerd is geraakt - wordt dit ook in realtime verzonden. Zo ontvangen klanten onmiddellijk actuele gegevens in de live-feed van de wedstrijd. Periodiek, zodat de client begrijpt dat de server niet is overleden, dat er niets met hem aan de hand is, sturen we om de 15 seconden een timestamp – zodat hij weet dat alles in orde is en dat een herverbinding niet nodig is.
Hoe wordt de live-verbinding onderhouden?
- Allereerst creëren we een kanaal waar updates met een buffer naartoe komen.
- Vervolgens abonneren we dit kanaal op het ontvangen van updates.
- We stellen de juiste header in, zodat de client weet dat alles in orde is.
- We sturen de eerste ping. Gewoon de huidige timestamp van de verbinding opschrijven.
- Daarna lezen we in een lus uit het kanaal totdat het update-kanaal is gesloten. Er komt periodiek een actuele timestamp of wijzigingen binnen in het kanaal die we al in de open verbindingen opslaan.

Het eerste probleem waarmee we geconfronteerd werden, was het volgende: voor elke open verbinding met de klant creëerden we een timer die elke 15 seconden tikte – dat betekent dat als we 6000 verbindingen met één machine (met één API-server) hadden, er 6000 timers werden aangemaakt. Dit leidde tot een onhoudbare belasting voor de machine. Het probleem was niet zo voor de hand liggend voor ons, maar we kregen wat hulp en hebben het opgelost.
Uiteindelijk komt de ping nu uit hetzelfde kanaal als de update.
Daarom is er nu slechts één timer die elke 15 seconden tikt.
Hier zijn een paar hulpfuncties – het verzenden van de header, de ping en de structuur zelf. Dat wil zeggen, hier wordt de naam van de tabel doorgegeven (persoon, wedstrijd, seizoen) en de informatie over deze record:

Het mechanisme voor het verzenden van updates
Nu een beetje over waar de wijzigingen vandaan komen. We hebben verschillende redacteuren die in real time de uitzending volgen. Zij creëren alle gebeurtenissen: iemand is verwijderd, iemand heeft een blessure, er is een wissel…
Met behulp van de CMS komen de gegevens in de database. Daarna meldt de database via het Listen/Notify-mechanisme dit aan de API-servers. De API-servers verspreiden deze informatie naar de klanten. Op deze manier zijn er in principe maar een paar servers verbonden met de database en is er geen bijzondere belasting op de database, omdat de klant op geen enkele manier direct met de database interacteert:

PostgreSQL: Listen/Notify
Het Listen/Notify-mechanisme in PostgreSQL stelt abonnees op gebeurtenissen in staat om te worden geïnformeerd wanneer er een record in de database is aangemaakt of gewijzigd. Hiervoor hebben we een eenvoudige trigger en functie geschreven:

Bij een insert of wijziging van een record roepen we de notify-functie aan op het kanaal data_updates, waarbij we de naam van de tabel en de identificatie van het record dat is gewijzigd of ingevoegd doorgeven.
Voor alle tabellen die gesynchroniseerd moeten worden met de klant, definiëren we een trigger die na wijziging/update van een record de functie aanroept die op de dia hieronder is aangegeven.
Hoe abonneert de API zich op deze wijzigingen?
Er wordt een Fanout-mechanisme aangemaakt - het verstuurt berichten naar klanten. Het verzamelt alle kanalen van klanten en verspreidt updates die het via deze kanalen heeft ontvangen:

Hier is de standaard bibliotheek pq, die verbinding maakt met de database en aangeeft dat het het kanaal (data_updates) wil afluisteren, en controleert of de verbinding open is en alles in orde is. Ik laat de foutcontrole weg om ruimte te besparen (het niet controleren kan riskant zijn).
Vervolgens stellen we asynchroon een Ticker in die elke 15 seconden een ping verzendt, en beginnen we met het afluisteren van het kanaal waarop we zijn geabonneerd. Als we een ping ontvangen, publiceren we deze ping. Als we een record ontvangen, publiceren we dit record naar alle abonnees van deze Fanout.
Hoe werkt Fan-out?
In het Nederlands betekent dit 'splitter'. We hebben één object dat abonnees registreert die updates willen ontvangen. Zodra een update voor dit object binnenkomt, verspreidt het deze update naar alle abonnees die het heeft. Het is vrij eenvoudig:

Hoe dit is geïmplementeerd in Go:

Er is een structuur die gesynchroniseerd wordt met behulp van Mutexen. Deze heeft een veld dat de status van de verbinding van Fanout met de database opslaat, dat wil zeggen, op dit moment luistert het en zal het updates ontvangen, evenals de lijst van alle beschikbare kanalen – een map, waarbij de sleutel het kanaal is en de struct bestaat uit waarden (in feite wordt dit verder niet gebruikt).
Twee methoden - Connected en Disconnected - stellen Fanout in staat om aan te geven dat we verbinding hebben met de database, dat deze is tot stand gekomen en dat de verbinding met de database is verbroken. In het tweede geval moeten alle klanten worden losgekoppeld en geïnformeerd dat ze niets meer kunnen afluisteren en dat ze opnieuw moeten verbinden, aangezien de verbinding met hen is gesloten.
Daarnaast is er een Methode Subscribe, die een kanaal toevoegt aan de 'luisteraars':

Er is een Methode Unsubscribe, die een kanaal verwijdert uit de luisteraars als de klant is losgekoppeld, en een Methode Publish, die het mogelijk maakt om een bericht naar alle abonnees te verzenden.
Vraag: - Wat wordt er via dit kanaal verzonden?
MS: - Er wordt een model verzonden dat is gewijzigd of een ping (in wezen gewoon een getal, integer).
MS: - Je kunt alles verzenden, elke structuur publiceren - het wordt gewoon omgezet in JSON en dat is het.
MS: – We receive a notification from "Postgres" – it contains the table name and identifier. Using the table name, we retrieve the desired record by its identifier, and then we send this structure for publication.
Infrastructuur
How does this look from an infrastructure perspective? We have 7 physical servers: one is entirely dedicated to the database, while the other six run virtual machines. There are 6 copies of the API: each virtual machine with the API runs on a separate physical server for reliability.

We have two frontends, on which Keepalived is installed to improve availability, so that if necessary, one frontend can replace the other. Additionally, there are two copies of the CMS.
There is also a statistics importer. There is a DB Slave from which backups are periodically made. There is Pigeon Pusher – the application that sends push notifications to clients, as well as infrastructure components: Zabbix, Graylog2, and Chef.
In reality, this infrastructure is redundant because 100,000 can be serviced with fewer servers. But we had the hardware – we used it (we were told it was possible – why not).
Advantages of Go
After we worked on this application, some obvious advantages of Go became apparent.
- A great HTTP library. With it, you can create quite a lot right "out of the box."
- Plus, the channels allowed us to easily implement a mechanism for sending notifications to clients.
- The wonderful Race detector helped us eliminate several critical bugs (staging infrastructure). Everything that runs on staging is launched, compiled with the Race flag; thus, we can see the potential issues we have in the staging infrastructure.
- Minimalism and simplicity of the language.

We are looking for developers! If anyone is interested – please.
Vragen
Audience question (following – Q): – It seems to me that you missed an important point regarding Fan-out. Am I correct in understanding that when you send a response to the client, you block if the client does not wish to read?
MS: – Nee, we blokkeren niet. Ten eerste ligt alles achter nginx, dus we hebben geen problemen met langzame clients. Ten tweede heeft de klant een bufferkanaal - in principe kunnen we daar tot honderd updates naartoe sturen... Als we niet in het kanaal kunnen schrijven, verwijdert het het. Als we zien dat het kanaal geblokkeerd is, sluiten we het gewoon af, en dat is het - de klant maakt opnieuw verbinding als er een probleem optreedt. Daarom ontstaan er hier in principe geen blokkades.
V: – Kon je niet meteen de Listen/Notify-opname sturen, in plaats van de tabel-identificator?
MS: – Listen/Notify heeft een beperking van 8000 bytes op de preload die het verstuurt. In principe zou het mogelijk zijn om te versturen als we met een klein aantal gegevens te maken hebben, maar ik denk dat het zoals wij het doen gewoon betrouwbaarder is. De beperkingen zitten in PostgreSQL zelf.
V: – Krijgen klanten updates over wedstrijden waarin ze niet geïnteresseerd zijn?
MS: – Over het algemeen ja. Meestal zijn er 2-3 wedstrijden parallel, en dat komt vrij zelden voor. Als een klant iets kijkt, is het meestal de wedstrijd die momenteel bezig is. Bovendien heeft de klant een lokale database waarin al deze updates worden opgeslagen, en zelfs zonder internetverbinding kan de klant alle voorbije wedstrijden bekijken waarvoor hij updates heeft. In principe synchroniseren we onze database op de server met de lokale database van de klant, zodat hij ook offline kan werken.
V: – Waarom hebben jullie je eigen ORM gemaakt?
Alexey (een van de ontwikkelaars van 'Smatri+'): – Op dat moment (dat is een jaar geleden) waren er minder ORM's dan nu, toen er behoorlijk veel zijn. Van de meeste bestaande ORM's vind ik het vervelend dat de meeste werken met lege interfaces. Dat wil zeggen, de methoden in deze ORM's zijn bereid om van alles te accepteren: een structuur, een pointer naar een structuur, een getal, iets dat helemaal niet relevant is...
Onze ORM genereert structuren op basis van het datamodel. Automatisch. En daarom zijn alle methoden specifiek, maken geen gebruik van reflectie, enz. Ze accepteren structuren en verwachten de structuren te gebruiken die binnenkomen.
V: – Hoeveel mensen hebben eraan deelgenomen?
MS: – In de beginfase waren er twee mensen bij betrokken. Rond juni zijn we begonnen, in augustus was het grootste deel klaar (de eerste versie). In september was de release.
V: – Waar jullie SSE beschrijven, gebruiken jullie geen timeout. Waarom niet?
MS: Als we eerlijk zijn, is SSE toch een HTML5-protocol: de SSE-standaard is bedoeld voor communicatie met browsers, voor zover ik begrijp. Het heeft extra functies zodat browsers opnieuw kunnen verbinden (en zo verder), maar die hebben we niet nodig omdat we klanten hadden die elke verbinding- en informatie-oproeplogica konden implementeren. We hebben eerder wat gemaakt dat lijkt op SSE, maar niet het protocol zelf.
Er was geen noodzaak. Voor zover ik begrijp, hebben de klanten het verbindingsmechanisme praktisch vanaf nul geïmplementeerd. Ze maakten zich er in principe niet echt druk om.
V: Welke extra hulpmiddelen hebben jullie gebruikt?
MS: We hebben voornamelijk govet en golint gebruikt om de stijl consistent te houden, evenals gofmt. Verder hebben we niets anders gebruikt.
V: Waarmee hebben jullie gedebugged?
MS: De debugging gebeurde in grote lijnen met behulp van tests. We hebben geen debugger of GOP gebruikt.
V: Kun je de dia teruggeven waar de functie Publish wordt geïmplementeerd? Stoort eenlettergrepseudoniem je niet?
MS: Nee. Ze hebben een behoorlijk 'smalle' scope. Ze worden nergens anders gebruikt (behalve in de interne structuur van deze klasse), en deze klasse is zeer compact – slechts 7 regels.
V: Het is niet intuïtief...
MS: Nee, nee, dit is echte code! Het gaat niet om de stijl. Het is gewoon een utilitaire, heel kleine klasse – slechts 3 velden binnen de klasse...

MS: Over het algemeen veranderen alle gegevens die gesynchroniseerd worden met de klanten (seizoenswedstrijden, spelers) niet. Grofweg gezegd, als we een andere sporttak zouden maken waarin we de wedstrijd moeten wijzigen, zouden we dat gewoon in de nieuwe versie van de client verwerken, en de oude versies van de client zouden worden geblokkeerd.
V: Zijn er externe pakketten voor afhankelijkheidsbeheer?
MS: We hebben go dep gebruikt.
V: Er stond iets over video in het onderwerp van de lezing, maar er is geen video in de lezing.
MS: Nee, ik heb niets over video in mijn onderwerp. Het heet 'Soot+'' – zo heet de app.
V: Je zei dat het gestreamd wordt naar klanten?
MS: We hebben ons niet beziggehouden met streaming video. Dat deed volledig 'MegaFon'. Ja, ik heb niet gezegd dat het een MegaFon-app is.
MS: – Go – voor het verzenden van alle gegevens – over de account, over wedstrijdevenementen, statistieken… Go – is volledig de backend voor de applicatie. De klant moet ergens vandaan weten welke link te gebruiken voor de speler, zodat de gebruiker de wedstrijd kan bekijken. We hebben links naar video en streams die voorbereid zijn.

Een beetje reclame 🙂
Bedankt dat je bij ons blijft. Houd je van onze artikelen? Wil je meer interessante inhoud zien? Ondersteun ons door een bestelling te plaatsen of ons aan vrienden aan te bevelen, , een unieke variant van entry-level servers, die we voor jou hebben bedacht: (opties beschikbaar met RAID1 en RAID10, tot 24 cores en tot 40GB DDR4).
Dell R730xd is 2 keer goedkoper in datacenter Equinix Tier IV in Amsterdam? Alleen bij ons in Nederland! Dell R420 — 2x E5-2430 2.2Ghz 6C 128GB DDR3 2x960GB SSD 1Gbps 100TB — vanaf $99! Lees over hoe
Bron: habr.com
