Podsumowanie artykułu
- Pliki użytkowników przechowuj w CDN (np. GCP), aby uniknąć problemów z replikacją i utratą danych.
- Zadania asynchroniczne realizuj przez Pub/Sub w GCP, z konsumentami w osobnych kontenerach.
- Zadania cykliczne uruchamiaj w tymczasowych kontenerach tworzonych tylko na czas wykonania.
- Chmura daje elastyczne skalowanie, ale jest droższa i wymaga wiedzy DevOps.
- Dostawcy chmurowi zapewniają wysoką dostępność i wsparcie przy awariach.
- Chmurę wybieraj przy dużym lub zmiennym ruchu; dla małych projektów może być nieopłacalna.
W przypadku tworzenia aplikacji webowych, które korzystają z zasobów chmury obliczeniowej, kluczowy jest wybór stosu technologicznego umożliwiającego skalowanie. Trzeba też pamiętać, że aplikacje działające w chmurze różnią się od tych uruchamianych w tradycyjnych środowiskach hostingowych czy na serwerach dedykowanych. w tym tekście opiszę, jak wykorzystać Google Kubernetes Engine, a także omówię typowe problemy i ich rozwiązania w podejściu serwerowym oraz chmurowym. Mam nadzieję, że moje wskazówki okażą się dla Wam pomocne przy realizacji Waszych pomysłów na aplikację webową.
Problem nr 1: przechowywanie plików przesyłanych przez użytkowników
Na pierwszy rzut oka przechowywanie plików wydaje się proste. w rozwiązaniu serwerowym tworzymy katalog na serwerze, ustawiamy odpowiednie uprawnienia (pamiętając, by nie dopuszczać do wykonywania plików) i przypisujemy dostęp użytkownikowi uruchamiającemu aplikację.
Taki sposób przestaje jednak być praktyczny, gdy w projekcie wykorzystujemy konteneryzację. Warto pamiętać, że kontenery w Kubernetes są dynamicznie replikowane. Przy dużym ruchu w aplikacji tworzymy kolejną replikę, będącą wierną kopią już działających.
Wyobraźmy sobie następującą sytuację. w aplikacji przechowujemy 100 plików przesłanych przez użytkowników (w podfolderze katalogu public, co jest typowe dla Symfony framework). w tradycyjnym rozwiązaniu serwerowym nie ma to większego znaczenia. a jak wygląda to w Kubernetesie? Załóżmy, że startujemy z jedną repliką. Mamy więc jedną kopię plików.
Klient uruchamia kampanię marketingową, a nasza aplikacja pojawia się w telewizji i radiu. Przed kampanią odnotowywaliśmy około 100 żądań na sekundę, teraz jest ich nagle 10 000. Kubernetes ma ograniczone zasoby na pod. Powiedzmy, że jest 300 MB RAM. Większa liczba zapytań obciąża pamięć, więc narzędzie Horizontal Pod Autoscaling (HPA), odpowiedzialne za horyzontalne skalowanie podów, zwiększa liczbę replik do czterech.
Aplikacja zaczyna odpowiadać w akceptowalnym czasie. Co jednak dzieje się w tle? Nasze 100 plików zostało „skopiowanych” do pozostałych replik i zostanie usunięte wraz z replikami, gdy ruch spadnie (np. w nocy). Przy 100 plikach to drobna operacja, ale co jeśli byłoby ich 100 GB? Czas tworzenia replik wydłużyłby się dramatycznie, powodując spadek wydajności i odpływ użytkowników. Zwyczajnie uznaliby oni, że aplikacja działa zbyt wolno.
Rozwiązanie: przechowywanie plików na zewnętrznej platformie
W takim przypadku sensowne jest przeniesienie plików do sieci Content Delivery Network (CDN — sieć połączonych serwerów, przyspieszająca ładowanie stron i aplikacji, zwłaszcza przy dużej ilości danych). Repliki obsługują jedynie kod aplikacji i nie zajmują się zmiennością plików przesyłanych czy usuwanych przez użytkowników. w naszym projekcie wykorzystaliśmy usługę przechowywania w chmurze dostępną w infrastrukturze GCP. Oprócz funkcji CDN, GCP oferuje automatyczne kopie zapasowe i możliwość szybkiego przywrócenia aplikacji do określonego stanu w czasie, co rozwiązuje problem znikających plików.
Pozostaje pytanie: jak uzyskać dostęp do plików w CDN, nie wskazując wprost, że korzystamy z takiej sieci? w naszej aplikacji sięgnęliśmy po pakiet flysystem-bundle. Bazuje on na bibliotece Flysystem, która zapewnia abstrakcję systemu plików dla aplikacji PHP. Konfiguracja pakietu jest prosta – określamy typ adaptera i adres URL bucketu, jak w poniższym fragmencie kodu:
flysystem:
storages:
upload.storage:
adapter: gcloud
options:
client: ‘gcloud_client_service’
bucket: ‘%env(FLYSYSTEM_GCS_DATA_COPY_BUCKET)%’
Pobieranie danych z CDN jest równie proste:
final class GetUploadedFileController extends AbstractController
{
public function __construct(
private readonly FilesystemOperator $uploadStorage,
) {}
public function __invoke(string $filename): BinaryFileResponse
{
try {
$fileStream = $this->uploadStorage->readStream($filename);
} catch (FilesystemException) {
throw new ServiceUnavailableHttpException();
}
$tmpName = (new Filesystem())->tempnam(sys_get_temp_dir(), ‘tmp_’, $filename);
$tmpFile = fopen($tmpName, ‘wb+’);
if (!is_resource($tmpFile)) {
throw new RuntimeException();
}
file_put_contents($tmpName, $fileStream);
fclose($tmpFile);
return $this->file($tmpName, $filename);
}
}
Problem nr 2: zadania asynchroniczne
Kolejny istotny obszar to zadania asynchroniczne. Niestety PHP nie jest językiem, który pozwala na uruchamianie procesów „w tle”. Znanym rozwiązaniem dla zadań asynchronicznych jest Symfony Messenger, który umożliwia delegowanie zadań w trybie asynchronicznym lub synchronicznym.
Zadania synchroniczne mogą być przydatne, gdy to samo zadanie musi zostać wykonane w wielu częściach systemu i chcesz je enkapsulować w osobnym pliku.
Prawdziwym przełomem są jednak zadania asynchroniczne. Wyobraźmy sobie sytuację dużej kampanii marketingowej. Klient chce zachęcić nowych użytkowników do skorzystania z płatnej części serwisu poprzez wygenerowanie 100 000 kodów rabatowych. Wykonanie takiego zadania synchronicznie zablokowałoby system na kilka minut. Oczywiście pojawienie się takiej sytuacji w godzinach szczytu mogłoby negatywnie wpłynąć na wizerunek marki.
Tu z pomocą przychodzą zadania asynchroniczne. Przy takich zadaniach nie interesuje nas moment ich wykonania. Kluczowy jest efekt: kody muszą zostać wygenerowane. To, czy będą dostępne za dziesięć sekund, czy za dziesięć godzin, ma mniejsze znaczenie. w końcu kampanie marketingowe planuje się z wyprzedzeniem. To właśnie tu zadania asynchroniczne znajdują idealne zastosowanie. Informujemy system, że chcemy nasze 100 tys. kodów, ale nie potrzebujemy ich natychmiast. Kolejka zadań asynchronicznych poczeka na mniej obciążony czas (np. nocą), a następnie wygeneruje to, czego potrzebujemy. Oczywiście zadaniom w kolejce można nadawać różne priorytety.
Klasyczne rozwiązanie
W tradycyjnej infrastrukturze serwerowej kolejka jest procesem działającym w systemie. Konsument kolejki, czyli część aplikacji zbierająca dane z kolejki i wykonująca zadania, to również osobny proces uruchomiony w tle. Wszystko jest w porządku, ale nadal operujemy na jednym serwerze, a więc korzystamy ze współdzielonych i skończonych zasobów.
Zalety klasycznego rozwiązania:
- Zadania nie są wykonywane synchronicznie (odciążenie serwera w czasie szczytu zadań).
Wady klasycznego rozwiązania:
- Ostatecznie wykonujemy zadania na tym samym serwerze, co może prowadzić do spadku wydajności lub niewykonania zadań przy zbyt dużym zużyciu zasobów.
- Konieczność skonfigurowania procesu kolejki.
Rozwiązanie z wykorzystaniem GCP
W naszej aplikacji zdecydowaliśmy się na usługę Pub/Sub, która umożliwia tworzenie i subskrybowanie zadań w kolejce. w rezultacie przenosimy cały proces obsługi kolejki poza zasoby aplikacji, co daje znaczący zysk wydajności. a co z konsumentem? Tu wracamy do Kubernetesa. Działając w kontenerach, możemy stworzyć kontener oparty na kodzie naszej aplikacji, który jednocześnie pełni funkcję odrębnego bytu. Przy jednym obrazie Dockera konfiguracja pozostaje identyczna, ale konsument kolejki działa jako niezależna jednostka z własnymi zasobami, dzięki czemu nie obciąża zasobów aplikacji.
Zalety takiego rozwiązania:
- Obsługa kolejki przeniesiona do zewnętrznej usługi.
- Konsument wydzielony do osobnego kontenera z własnymi zasobami.
Wady:
- Pub/Sub ma trudności z wykonywaniem zadań opóźnionych (z czym zetknęliśmy się w implementacji naszej aplikacji, co skłoniło nas do przeniesienia części zadań do zadań cyklicznych).
- Nie znamy bazowego obciążenia usługi z perspektywy konsumenta, więc nie wiemy, czy zasobochłonne operacje na bazie danych nie spowolnią systemu.
Problem nr 3: zadania cykliczne
Nie ma aplikacji bez zadań cyklicznych. Zawsze istnieją zadania, które powinny być uruchamiane w regularnych odstępach czasu. w przypadku aplikacji pracującej na danych firmowych może to być na przykład cykliczne odświeżanie danych z usługi BIR Głównego Urzędu Statystycznego (GUS). Takie zadanie zapewnia poprawność przechowywanych informacji. Zdecydowanie powinno być realizowane jako zadanie cykliczne, bo nikt nie chce klikać tysięcy przycisków przy każdej zapisanej firmie tylko po to, by odczytać i ewentualnie zaktualizować dane z bazy GUS.
Klasyczne rozwiązanie
W aplikacjach po stronie serwera problem zadań cyklicznych zwykle rozwiązuje się poprzez skonfigurowanie crontaba, czyli mechanizmu cyklicznego uruchamiania poleceń. Trzeba jednak pamiętać, że crontab działa na tym samym serwerze i korzysta z zasobów współdzielonych z aplikacją. w przypadku zadań wykonywanych często może to stać się obciążające.
Zalety:
- Sami określamy moment wykonania zadania.
Wady:
- Monitorowanie działania zadań cyklicznych jest bardzo podstawowe. Oczywiście można użyć zewnętrznych narzędzi monitorujących, ale to dodatkowe usługi instalowane na serwerze.
- Opieramy się na zasobach jednego serwera, więc musimy mieć ich odpowiedni „zapas”. Większość zasobów pozostanie niewykorzystana w normalnym trybie pracy, a mimo to generuje koszty.
Rozwiązanie z wykorzystaniem Kubernetesa
Po raz kolejny z pomocą przychodzi Kubernetes. Skoro mamy gotowy obraz Dockera, możemy go replikować dla zadań cyklicznych. Każde zadanie zostanie wywołane z własnymi zasobami, a w konsekwencji aplikacja nie odczuje ich wpływu. Gdy dane zadanie jest potrzebne, jego kontener zostanie utworzony, a po zakończeniu pracy zamknięty.
Zalety:
- Zadania mają odrębne zasoby.
- Możemy określić czas wykonania zadań.
- Nie ma potrzeby przewymiarowywania zasobów serwera.
Wady:
- Koszty, ponieważ w rozwiązaniu chmurowym płacimy za wykorzystane zasoby obliczeniowe.
Problem nr 4: koszty
Utrzymanie aplikacji zawsze wiąże się z kosztami. Tego nie da się uniknąć. Oczywiście można zdecydować się na darmowy hosting, ale bądźmy szczerzy — jego wydajność jest zazwyczaj fatalna. Co więcej w regulaminie takich usług często znajduje się zapis zabraniający komercyjnego wykorzystania.
Klasyczne rozwiązanie
W podejściu klasycznym, przed wdrożeniem aplikacji, musimy obliczyć potrzebne zasoby. Trzeba pamiętać, aby uwzględnić wspomniane wcześniej systemy kolejkowania czy zadania cykliczne. w skrajnych przypadkach może się okazać, że z wyliczeń wynika, iż na co dzień będziemy wykorzystywać jedynie 10% dostępnych zasobów, ale w okresach wzmożonej aktywności zadań cyklicznych i kolejek zasobów będzie brakować. w takiej sytuacji warto ponownie przemyśleć szczegóły implementacji lub harmonogram wykonywania zadań cyklicznych.
Zalety:
- Rozwiązania serwerowe są tańsze.
- Konfiguracja w przypadku rozwiązań serwerowych jest stosunkowo prosta.
Wady:
- W razie problemów z wydajnością mamy ograniczone możliwości skalowania. Możemy oczywiście przenieść aplikację na „mocniejszy” serwer, ale to oznacza wzrost kosztów.
Rozwiązanie chmurowe
Chmura daje znacznie większą elastyczność w zakresie zasobów. Określamy zasoby dla konkretnej usługi: czy to aplikacji, czy zadania cyklicznego.
Zalety:
- Duża elastyczność skalowania.
Wady:
- Koszty rozwiązań chmurowych są zazwyczaj wyższe niż w przypadku rozwiązań serwerowych.
- Konfiguracja rozwiązania chmurowego jest bardziej złożona — zazwyczaj wymaga specjalistycznej wiedzy o danym dostawcy usług chmurowych i nie da się jej przeprowadzić bez kompetencji DevOps.
Problem nr 5: przestoje
Nie mamy pełnej kontroli nad przestojami. Oczywiście powinniśmy minimalizować czas niedostępności i błędy spowodowane kodem aplikacji, ale bądźmy szczerzy — nie istnieje aplikacja bez błędów. Są jedynie takie, w których błędy nie zostały jeszcze odkryte (co często oznacza niską popularność).
Klasyczne rozwiązanie
W tradycyjnych rozwiązaniach zwykle wykupuje się pakiety SLA u dostawców serwerów. Alternatywnie, obsługą serwera może zająć się dział IT w firmie. Naturalnie nie mamy wpływu na zdarzenia losowe, takie jak awarie prądu, pożary w serwerowni (tak, to odniesienie do OVH) czy przerwy w dostawie internetu. „Drożsi” dostawcy hostingu oferują kopie zapasowe i gwarancję dostępności, ale problemy i tak mogą się pojawić.
Rozwiązanie chmurowe
Dostawcy usług chmurowych gwarantują bardzo wysoką dostępność usług (często na poziomie 99,9%). Warto zauważyć, że są to zazwyczaj najwięksi gracze na rynku — Google, Microsoft czy Amazon. Firmy te dysponują nieporównywalnie lepszą infrastrukturą i ogromnymi zespołami reagującymi na awarie. Mając aplikację uruchomioną w chmurze, możemy spać spokojnie.
Jak zbudować aplikację webową przy użyciu framework PHP Symfony i GCP — podsumowanie
Wykorzystanie rozwiązań chmurowych w tworzeniu aplikacji webowych daje wiele możliwości optymalizacji. Wybierając infrastrukturę, musimy oczywiście oszacować przewidywany ruch. Nie ma sensu umieszczać aplikacji, z której będzie korzystać dziesięciu użytkowników dziennie, w kosztownym środowisku chmurowym.
Decydując się na rozwiązanie chmurowe, trzeba pamiętać o różnicach wdrożeniowych wynikających z tej architektury. Nie można zapominać o replikacji, która niewątpliwie jest zaletą, ale wiąże się też z konsekwencjami dotyczącymi synchronizacji danych pomiędzy kontenerami, co sprawia, że staje się ona także „przekleństwem” już na etapie projektowania aplikacji webowej.
Jeżeli jednak uda się pokonać te wyzwania, rozwój aplikacji webowych w chmurze będzie miał więcej zalet niż klasyczne podejście serwerowe.