Michaił Salosin (dalej – MS): - Cześć wszystkim! Mam na imię Michał. Pracuję jako backend developer w MC2 Software i opowiem o wykorzystaniu Go w backendzie aplikacji mobilnej Look+.
Czy ktoś tutaj lubi hokej?
Zatem ta aplikacja jest dla Ciebie. Jest przeznaczony na Androida i iOS i służy do oglądania transmisji różnych wydarzeń sportowych online i nagrywanych. Aplikacja zawiera także różne statystyki, transmisje tekstowe, tabele konferencji, turniejów i inne informacje przydatne fanom.
Również w aplikacji istnieje coś takiego jak momenty wideo, czyli możesz obejrzeć najważniejsze momenty meczów (gole, walki, rzuty karne itp.). Jeśli nie chcesz oglądać całej transmisji, możesz obejrzeć tylko najciekawsze.
Czego użyłeś w rozwoju?
Główna część została napisana w Go. API, z którym komunikują się klienci mobilni, zostało napisane w Go. W Go napisano także usługę wysyłania powiadomień push na telefony komórkowe. Musieliśmy także napisać własny ORM, o którym być może pewnego dnia porozmawiamy. Cóż, w Go napisano kilka małych usług: zmiana rozmiaru i ładowanie obrazów dla redaktorów...
Jako bazę danych użyliśmy PostgreSQL. Interfejs edytora został napisany w Ruby on Rails przy użyciu gem'a ActiveAdmin. Importowanie statystyk od dostawcy statystyk jest również napisane w języku Ruby.
Do testów API systemu użyliśmy testu jednostkowego Pythona. Memcached służy do ograniczania połączeń płatniczych API, „Chef” służy do kontrolowania konfiguracji, Zabbix służy do gromadzenia i monitorowania wewnętrznych statystyk systemu. Graylog2 służy do zbierania logów, Slate to dokumentacja API dla klientów.
Wybór protokołu
Pierwszy problem, jaki napotkaliśmy: musieliśmy wybrać protokół interakcji pomiędzy backendem a klientami mobilnymi, w oparciu o następujące punkty...
- Najważniejszy wymóg: dane o klientach muszą być aktualizowane w czasie rzeczywistym. Oznacza to, że każdy, kto aktualnie ogląda transmisję, powinien niemal natychmiast otrzymać aktualizacje.
- Dla uproszczenia założyliśmy, że dane synchronizowane z klientami nie są usuwane, lecz ukrywane za pomocą specjalnych flag.
- Wszelkiego rodzaju rzadkie żądania (takie jak statystyki, skład zespołu, statystyki zespołu) są uzyskiwane za pomocą zwykłych żądań GET.
- Dodatkowo system musiał bez problemu obsłużyć jednocześnie 100 tys. użytkowników.
Na tej podstawie mieliśmy dwie opcje protokołu:
- Gniazda internetowe. Ale nie potrzebowaliśmy kanałów od klienta do serwera. Musieliśmy jedynie wysłać aktualizacje z serwera do klienta, więc websocket jest opcją zbędną.
- Zdarzenia wysłane przez serwer (SSE) wypadły idealnie! Jest to dość proste i w zasadzie zaspokaja wszystko, czego potrzebujemy.
Zdarzenia wysłane przez serwer
Kilka słów o tym jak to działa...
Działa na podstawie połączenia http. Klient wysyła żądanie, serwer odpowiada Content-Type: tekst/event-stream i nie zamyka połączenia z klientem, ale kontynuuje zapisywanie danych do połączenia:
Dane mogą zostać przesłane w formacie uzgodnionym z Klientem. W naszym przypadku wysłaliśmy to w takiej formie: do pola zdarzenia przesłana została nazwa zmienionej struktury (osoba, gracz), a do pola danych JSON z nowymi, zmienionymi polami dla gracza.
Porozmawiajmy teraz o tym, jak działa sama interakcja.
- Pierwszą rzeczą, którą robi klient, jest ustalenie czasu ostatniej synchronizacji z usługą: przegląda swoją lokalną bazę danych i ustala datę ostatniej zarejestrowanej przez nią zmiany.
- Wysyła żądanie z tą datą.
- W odpowiedzi przesyłamy mu wszystkie aktualizacje, które nastąpiły od tej daty.
- Następnie nawiązuje połączenie z kanałem na żywo i nie zamyka się, dopóki nie będzie potrzebować tych aktualizacji:
Wysyłamy mu listę zmian: jeśli ktoś strzeli gola, zmieniamy wynik meczu, jeśli dozna kontuzji, jest to również przesyłane w czasie rzeczywistym. Dzięki temu klienci natychmiast otrzymują aktualne dane w kanale wydarzeń meczowych. Okresowo, aby klient zrozumiał, że serwer nie umarł, że nic mu się nie stało, co 15 sekund wysyłamy znacznik czasu - aby wiedział, że wszystko jest w porządku i nie ma potrzeby ponownego łączenia się.
W jaki sposób obsługiwane jest połączenie na żywo?
- W pierwszej kolejności tworzymy kanał, do którego będą odbierane buforowane aktualizacje.
- Następnie subskrybujemy ten kanał, aby otrzymywać aktualizacje.
- Ustawiamy prawidłowy nagłówek, aby klient wiedział, że wszystko jest w porządku.
- Wyślij pierwszy ping. Po prostu rejestrujemy aktualny znacznik czasu połączenia.
- Następnie czytamy z kanału w pętli, aż do zamknięcia kanału aktualizacji. Kanał okresowo otrzymuje albo bieżący znacznik czasu, albo zmiany, które już zapisaliśmy w otwartych połączeniach.
Pierwszy problem jaki napotkaliśmy był następujący: dla każdego połączenia otwartego z klientem tworzyliśmy timer, który tykał co 15 sekund - okazuje się, że gdybyśmy mieli otwartych 6 tysięcy połączeń na jednej maszynie (z jednym serwerem API), 6 utworzono tysiąc timerów. Doprowadziło to do tego, że maszyna nie utrzymywała wymaganego obciążenia. Problem nie był dla nas aż tak oczywisty, ale otrzymaliśmy małą pomoc i go rozwiązaliśmy.
W rezultacie teraz nasz ping pochodzi z tego samego kanału, z którego pochodzi aktualizacja.
W związku z tym istnieje tylko jeden licznik czasu, który odmierza czas co 15 sekund.
Jest tu kilka funkcji pomocniczych - wysłanie nagłówka, ping i sama struktura. Oznacza to, że nazwa tabeli (osoba, mecz, sezon) i informacje o tym wpisie są przesyłane tutaj:
Mechanizm wysyłania aktualizacji
Teraz trochę o tym, skąd biorą się zmiany. Mamy kilka osób, redaktorów, którzy oglądają transmisję w czasie rzeczywistym. To oni tworzą wszystkie zdarzenia: ktoś został wyrzucony z boiska, ktoś został kontuzjowany, ktoś miał zastąpić...
Za pomocą CMS-a dane trafiają do bazy danych. Następnie baza danych powiadamia o tym serwery API za pomocą mechanizmu Listen/Notify. Serwery API już wysyłają te informacje do klientów. Zatem zasadniczo mamy tylko kilka serwerów podłączonych do bazy danych i nie ma specjalnego obciążenia bazy danych, ponieważ klient w żaden sposób nie wchodzi w bezpośrednią interakcję z bazą danych:
PostgreSQL: Słuchaj/Powiadom
Mechanizm Listen/Notify w Postgresie umożliwia powiadamianie subskrybentów zdarzeń o zmianie jakiegoś zdarzenia - w bazie danych powstał jakiś rekord. Aby to zrobić, napisaliśmy prosty wyzwalacz i funkcję:
Wstawiając lub zmieniając rekord, wywołujemy funkcję notify na kanale data_updates, przekazując tam nazwę tabeli oraz identyfikator rekordu, który został zmieniony lub wstawiony.
Dla wszystkich tabel, które muszą być zsynchronizowane z klientem definiujemy wyzwalacz, który po zmianie/aktualizacji rekordu wywołuje funkcję wskazaną na slajdzie poniżej.
W jaki sposób interfejs API subskrybuje te zmiany?
Tworzy się mechanizm Fanout, który wysyła wiadomości do klienta. Gromadzi wszystkie kanały klientów i wysyła aktualizacje otrzymane za pośrednictwem tych kanałów:
Tutaj standardowa biblioteka pq, która łączy się z bazą danych i mówi, że chce słuchać kanału (data_updates), sprawdza, czy połączenie jest otwarte i wszystko jest w porządku. Pomijam sprawdzanie błędów, aby zaoszczędzić miejsce (nie sprawdzanie jest niebezpieczne).
Następnie asynchronicznie ustawiamy Tickera, który będzie wysyłał ping co 15 sekund i zaczniemy słuchać kanału, który subskrybowaliśmy. Jeśli otrzymamy sygnał ping, publikujemy go. Jeśli otrzymamy jakiś wpis, publikujemy go wszystkim subskrybentom tego Fanoutu.
Jak działa Fan-Out?
W języku rosyjskim oznacza to „rozdzielacz”. Mamy jeden obiekt, który rejestruje subskrybentów, którzy chcą otrzymywać aktualizacje. Gdy tylko aktualizacja dotrze do tego obiektu, dystrybuuje ją do wszystkich swoich subskrybentów. Wystarczająco proste:
Jak jest to zaimplementowane w Go:
Istnieje struktura, która jest synchronizowana za pomocą Muteksów. Posiada pole zapisujące stan połączenia Fanouta z bazą danych, czyli aktualnie nasłuchuje i będzie otrzymywał aktualizacje, a także listę wszystkich dostępnych kanałów – mapę, której kluczem jest kanał oraz struktura w postaci wartości (w zasadzie nie jest on w żaden sposób używany).
Dwie metody - Connected i Disconnected - pozwalają nam powiedzieć Fanoutowi, że mamy połączenie z bazą, pojawiło się i że połączenie z bazą zostało zerwane. W drugim przypadku musisz rozłączyć wszystkich klientów i powiedzieć im, że nie mogą już niczego słuchać i że łączą się ponownie, ponieważ połączenie z nimi zostało zamknięte.
Istnieje również metoda Subskrybuj, która dodaje kanał do „słuchaczy”:
Istnieje metoda Unsubscribe, która usuwa kanał ze słuchaczy w przypadku rozłączenia się klienta, a także metoda Publish, która pozwala wysłać wiadomość do wszystkich subskrybentów.
Pytanie: – Co jest przesyłane tym kanałem?
SM: – Przesyłany jest model, który się zmienił lub ping (w zasadzie tylko liczba, liczba całkowita).
SM: – Można wysłać wszystko, wysłać dowolną strukturę, opublikować – po prostu zamienia się w JSON i tyle.
SM: – Otrzymujemy powiadomienie od Postgres – zawiera ono nazwę tabeli oraz identyfikator. Na podstawie nazwy tabeli i identyfikatora uzyskujemy potrzebny nam rekord, a następnie wysyłamy tę strukturę do publikacji.
Infrastruktura
Jak to wygląda z punktu widzenia infrastruktury? Mamy 7 serwerów sprzętowych: jeden z nich jest w całości dedykowany dla bazy danych, na pozostałych sześciu uruchamiane są maszyny wirtualne. Istnieje 6 kopii API: każda maszyna wirtualna z API działa na oddzielnym serwerze sprzętowym – ma to na celu zapewnienie niezawodności.
Mamy dwa frontendy z zainstalowanym Keepalive, aby poprawić dostępność, tak aby w przypadku, gdy coś się stanie, jeden frontend mógł zastąpić drugi. Oraz dwie kopie CMS-a.
Istnieje również importer statystyk. Istnieje DB Slave, z którego okresowo tworzone są kopie zapasowe. Istnieje Pigeon Pusher, aplikacja wysyłająca powiadomienia push do klientów, a także elementy infrastruktury: Zabbix, Graylog2 i Chef.
Tak naprawdę ta infrastruktura jest redundantna, bo 100 tys. można obsłużyć przy mniejszej liczbie serwerów. Ale było żelazo - używaliśmy (powiedziano nam, że można - czemu nie).
Plusy Go
Po pracy nad tą aplikacją ujawniły się oczywiste zalety Go.
- Fajna biblioteka http. Dzięki niemu możesz stworzyć całkiem sporo od razu po wyjęciu z pudełka.
- Do tego kanały, które pozwoliły nam w bardzo prosty sposób zaimplementować mechanizm wysyłania powiadomień do klientów.
- Wspaniała rzecz Detektor wyścigów pozwolił nam wyeliminować kilka krytycznych błędów (infrastruktura postojowa). Uruchamiane jest wszystko, co działa na etapie inscenizacji, skompilowane za pomocą klucza Race; w związku z tym możemy przyjrzeć się infrastrukturze tymczasowej, aby zobaczyć, jakie mamy potencjalne problemy.
- Minimalizm i prostota języka.
Poszukujemy programistów! Jeśli ktoś chce to proszę.
pytania
Pytanie od publiczności (dalej – B): – Wydaje mi się, że pominąłeś jeden ważny punkt dotyczący Fan-outu. Czy dobrze rozumiem, że wysyłając odpowiedź do klienta, blokujesz ją, jeśli klient nie chce czytać?
SM: - Nie, nie blokujemy. Po pierwsze, mamy to wszystko za nginxem, czyli nie ma problemów z wolnymi klientami. Po drugie, klient posiada kanał z buforem - tak naprawdę możemy tam umieścić aż sto aktualizacji... Jeśli nie uda nam się zapisać na kanał, to go usunie. Jeśli zobaczymy, że kanał jest zablokowany, to po prostu go zamkniemy i tyle – klient ponownie połączy się, jeśli pojawi się jakiś problem. Dlatego w zasadzie nie ma tu mowy o blokowaniu.
W: – Czy nie dałoby się od razu wysłać rekordu do Listen/Notify, a nie tabeli identyfikatorów?
SM: – Funkcja Listen/Notify ma limit 8 tysięcy bajtów wysyłanego wstępnego ładowania. W zasadzie dałoby się wysłać, gdybyśmy mieli do czynienia z małą ilością danych, ale wydaje mi się, że w ten sposób [tak jak to robimy] jest po prostu bardziej niezawodny. Ograniczenia znajdują się w samym Postgresie.
W: – Czy klienci otrzymują aktualizacje dotyczące meczów, którymi nie są zainteresowani?
SM: - Generalnie tak. Z reguły równolegle toczą się 2-3 mecze, a nawet wtedy dość rzadko. Jeśli klient coś ogląda, to zazwyczaj ogląda rozgrywany mecz. Klient ma wówczas lokalną bazę danych, do której dodawane są wszystkie te aktualizacje i nawet bez połączenia z Internetem może przeglądać wszystkie przeszłe mecze, dla których posiada aktualizacje. Zasadniczo synchronizujemy naszą bazę danych na serwerze z lokalną bazą danych klienta, aby mógł on pracować offline.
W: – Dlaczego stworzyłeś własny ORM?
Alexey (jeden z twórców Look+): – Wtedy (było to rok temu) ORM-ów było mniej niż obecnie, kiedy jest ich całkiem sporo. Moją ulubioną cechą większości ORM-ów jest to, że większość z nich działa na pustych interfejsach. Oznacza to, że metody w tych ORM-ach są gotowe przyjąć wszystko: strukturę, wskaźnik struktury, liczbę, coś zupełnie nieistotnego...
Nasz ORM generuje struktury w oparciu o model danych. Ja. Dlatego wszystkie metody są konkretne, nie wykorzystują refleksji itp. Akceptują struktury i oczekują, że wykorzystają te struktury, które się pojawią.
W: – Ile osób wzięło udział?
SM: – Na początkowym etapie wzięły w nim udział dwie osoby. Zaczęliśmy gdzieś w czerwcu, a w sierpniu główna część była już gotowa (pierwsza wersja). We wrześniu była premiera.
W: – Tam, gdzie opisujesz SSE, nie używasz limitu czasu. Dlaczego?
SM: – Szczerze mówiąc, SSE jest nadal protokołem HTML5: standard SSE jest przeznaczony do komunikacji z przeglądarkami, o ile rozumiem. Ma dodatkowe funkcje, dzięki którym przeglądarki mogą się ponownie łączyć (i tak dalej), ale nie potrzebujemy ich, ponieważ mieliśmy klientów, którzy mogli wdrożyć dowolną logikę łączenia i odbierania informacji. Nie stworzyliśmy SSE, ale raczej coś podobnego do SSE. To nie jest sam protokół.
Nie było takiej potrzeby. O ile rozumiem, klienci wdrożyli mechanizm połączenia niemal od zera. Tak naprawdę nie obchodziło ich to.
W: – Z jakich dodatkowych narzędzi korzystałeś?
SM: – Najaktywniej używaliśmy govet i golint, aby ujednolicić styl, a także gofmt. Nic innego nie było używane.
W: – Czego użyłeś do debugowania?
SM: – Debugowanie odbywało się głównie za pomocą testów. Nie korzystaliśmy z żadnego debuggera ani GOP.
W: – Czy możesz zwrócić slajd, w którym zaimplementowano funkcję Publikuj? Czy jednoliterowe nazwy zmiennych są dla Ciebie mylące?
SM: - NIE. Mają dość „wąski” zakres widoczności. Nie są one używane nigdzie indziej poza tutaj (z wyjątkiem elementów wewnętrznych tej klasy) i są bardzo kompaktowe - zajmują tylko 7 linii.
W: – Jakoś to nadal nie jest intuicyjne…
SM: - Nie, nie, to jest prawdziwy kod! Tu nie chodzi o styl. To po prostu taka użyteczna, bardzo mała klasa - tylko 3 pola w klasie...
SM: – Ogólnie rzecz biorąc, wszystkie dane synchronizowane z klientami (mecze sezonowe, gracze) nie ulegają zmianie. Z grubsza rzecz biorąc, jeśli stworzymy kolejny sport, w którym będziemy musieli zmienić mecz, po prostu weźmiemy wszystko pod uwagę w nowej wersji klienta, a stare wersje klienta zostaną zbanowane.
W: – Czy istnieją pakiety do zarządzania zależnościami innych firm?
SM: – Użyliśmy go Dep.
W: – W temacie raportu było coś o wideo, ale w raporcie nie było nic o wideo.
SM: – Nie, nie mam nic w temacie dotyczącym filmu. Nazywa się „Look+” – tak nazywa się aplikacja.
W: – Mówiłeś, że jest przesyłany strumieniowo do klientów?..
SM: – Nie zajmowaliśmy się streamingiem wideo. Zostało to w całości wykonane przez Megafon. Tak, nie powiedziałem, że aplikacją jest MegaFon.
SM: – Go – do przesyłania wszystkich danych – o wynikach, wydarzeniach meczowych, statystykach… Go to cały backend aplikacji. Klient musi skądś wiedzieć, jakiego linku użyć dla gracza, aby użytkownik mógł obejrzeć mecz. Mamy linki do przygotowanych filmów i streamów.
Kilka reklam 🙂
Dziękujemy za pobyt z nami. Podobają Ci się nasze artykuły? Chcesz zobaczyć więcej ciekawych treści? Wesprzyj nas składając zamówienie lub polecając znajomym,
Dell R730xd 2 razy taniej w centrum danych Equinix Tier IV w Amsterdamie? Tylko tutaj
Źródło: www.habr.com