Wykład 3: Współbieżność, skalowalność i wdrażanie

Termin: sobota, 24 października 2026 r. — druga część wspólnego bloku 4 × 45 minut.

Wykłady 2 i 3 odbywają się podczas jednego spotkania. Każdy moduł obejmuje 2 × 45 minut. Materiał na stronie rozwija przykłady omawiane na wykładzie; samodzielna praca nad implementacją będzie realizowana na laboratoriach.

Od poprawnej operacji do działającej usługi

Kontrakt API określa znaczenie żądania i odpowiedzi. System musi jeszcze zachować poprawność, gdy żądania przychodzą równocześnie, proces zostaje uruchomiony ponownie albo odpowiedź ginie w sieci. Rozbudowa infrastruktury nie usuwa tych problemów automatycznie.

Druga część sobotniego bloku rozwija aplikację rezerwacji ośrodka „Sosna”. Punktem odniesienia jest jedna aplikacja FastAPI i jeden plik SQLite. Kolejki, pamięć podręczna i wiele serwerów są wariantami rozwoju, a nie usługami już uruchomionymi w demonstratorze.

Dwa żądania i jeden wolny termin

R1 zajmuje P1 od 08:00 do 10:00. Dwa żądania próbują zarezerwować P1 od 10:00 do 11:00. Każde z osobna ma poprawne dane i nie koliduje z R1. Problem powstaje między nowymi rezerwacjami.

Niepoprawny podział kontroli i zapisu może dopuścić taki przebieg:

Krok Żądanie A Żądanie B
1 Odczyt: brak konfliktu —
2 — Odczyt: brak konfliktu
3 Zapis nowej rezerwacji —
4 — Zapis drugiej rezerwacji

Jest to race condition — wyścig, w którym wynik zależy od przeplotu operacji. Pojedynczy odczyt „wolne” nie rezerwuje terminu na czas późniejszego zapisu. Sprawdzenie i zmiana muszą stanowić odpowiednio chronioną całość.

Co robi obecny demonstrator

W aplikacji „Sosna” operacja zapisu otwiera transakcję SQLite poleceniem BEGIN IMMEDIATE przed odczytem konfliktów. Dopiero potem sprawdza obiekt, jednostkę i terminy, a następnie dodaje rezerwację i zatwierdza transakcję.

BEGIN IMMEDIATE
    sprawdzenie obiektu i jednostki
    sprawdzenie konfliktów
    INSERT rezerwacji, jeśli termin jest wolny
COMMIT

To schemat działania, nie kompletny skrypt SQL. SQLite dopuszcza jednego piszącego naraz. Drugie połączenie może zaczekać lub otrzymać błąd zajęcia; po uzyskaniu możliwości zapisu musi wykonać kontrolę wobec aktualnego stanu.

Dla krótkich operacji oczekiwany wynik próby to jedna odpowiedź 201 i jedna 409. Gdy oczekiwanie na bazę przekroczy limit demonstratora, możliwe jest 503. Kryterium poprawności jest stan bazy: najwyżej jedna nowa kolidująca rezerwacja, nie założenie, że każde równoczesne żądanie zawsze skończy się jednym konkretnym kodem.

Ochrona zależy od stosowania tej ścieżki przez wszystkich zapisujących. Sam schemat obecnej bazy nie zawiera ograniczenia wykluczającego wszystkie nakładające się przedziały. Bezpośredni zapis inną ścieżką może ominąć regułę.

Ponowienie żądania a wynik operacji

Brak odpowiedzi nie jest dowodem braku zapisu. Możliwy jest przebieg:

sequenceDiagram
    participant K as Klient
    participant A as API
    participant B as Baza danych
    K->>A: POST /rezerwacje
    A->>B: Zapis i COMMIT
    B-->>A: Zatwierdzono
    A--xK: Odpowiedź nie dociera
    Note over K: Wynik jest nieznany klientowi

Idempotencja — idempotency oznacza, że powtarzanie tej samej operacji ma zamierzony efekt taki jak wykonanie jej raz. Nie wymaga identycznych odpowiedzi HTTP przy każdym wywołaniu. Metoda POST sama nie zapewnia tej właściwości, choć konkretne API może zaprojektować idempotentne przetwarzanie.

W obecnej aplikacji powtórzony zapis tego samego terminu może zwrócić 409. Nie jest to pełny mechanizm identyfikowania ponowień: konflikt nie dowodzi, że istniejącą rezerwację utworzyło właśnie poprzednie żądanie tego klienta.

Rozszerzenie z kluczem operacji, np. Idempotency-Key, wymaga zapisania klucza, powiązania go z użytkownikiem i treścią żądania oraz zachowania wyniku. Rozpoznanie klucza i utworzenie rezerwacji muszą być skoordynowane atomowo. Ponowne użycie klucza dla innych danych powinno zostać odrzucone. Samo dodanie nagłówka po stronie klienta nie implementuje tego mechanizmu.

Trwałość: proces, kontener i plik danych

Restart procesu nie oznacza wymazania bazy, jeżeli dane znajdują się w trwałym pliku. Inaczej zachowuje się lista przechowywana wyłącznie w pamięci procesu.

W paczce demonstracyjnej konfiguracja zawiera:

services:
  api:
    build: .
    ports:
      - "127.0.0.1:8000:8000"
    volumes:
      - ./data:/data
    restart: "no"

Ścieżka ./data na komputerze jest montowana jako /data w kontenerze. Jest to bind mount — montowanie katalogu hosta, mimo użycia klucza volumes w składni Compose. Baza /data/sosna.db znajduje się dzięki temu poza zapisywalną warstwą kontenera.

Zdarzenie Oczekiwany skutek w tej konfiguracji
Zatrzymanie i uruchomienie procesu Zapisany plik pozostaje
Odtworzenie kontenera z tym samym katalogiem data Rezerwacje pozostają
Uruchomienie z innym, pustym katalogiem danych Aplikacja widzi inną bazę
Usunięcie pliku z katalogu data Utrata danych z tego pliku; potrzebne odtworzenie z kopii

Próba trwałości powinna zapisać rozpoznawalny rekord, odtworzyć kontener z zachowanym katalogiem i ponownie odczytać ten rekord. Samo uruchomienie pustego serwera nie sprawdza trwałości.

NoteTrwały katalog nie jest kopią zapasową

Błąd aplikacji lub użytkownika może zmienić także plik znajdujący się poza kontenerem. Kopia zapasowa musi być spójna i sprawdzona przez odtworzenie. Dla aktywnej bazy SQLite należy użyć odpowiedniego mechanizmu backupu; zwykłe skopiowanie jednego pliku podczas zapisu nie jest uniwersalną procedurą.

Docker: powtarzalne uruchomienie

Obraz — image zawiera aplikację i zależności przestrzeni użytkownika. Kontener — container jest uruchomioną instancją obrazu. Docker nie eliminuje różnic architektury procesora, konfiguracji ani uprawnień do katalogów. Na macOS kontenery linuksowe korzystają z maszyny wirtualnej Docker Desktop.

Poniższy Dockerfile odpowiada środowisku pierwszego etapu:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
ENV DATABASE_PATH=/data/sosna.db
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Uvicorn jest serwerem obsługującym aplikację przez ASGI — Asynchronous Server Gateway Interface. Adres 0.0.0.0 wewnątrz kontenera oznacza nasłuchiwanie na wszystkich jego interfejsach. Mapowanie 127.0.0.1:8000:8000 w Compose ogranicza opublikowany port hosta do lokalnego komputera. To odrębne ustawienia.

Zakresy wersji bibliotek z requirements.txt pozwalają instalować różne zgodne wydania podczas kolejnych budów. Pełna odtwarzalność wymaga kontroli konkretnych wersji zależności i obrazu bazowego, a nie tylko posiadania Dockerfile.

Skalowanie i współdzielony stan

Skalowanie pionowe — vertical scaling polega na zwiększeniu zasobów jednego środowiska: procesora, pamięci lub wydajności dysku. Skalowanie poziome — horizontal scaling dodaje instancje obsługujące ruch. Oba podejścia mają ograniczenia, a dodatkowe instancje nie usuwają wąskiego obszaru systemu, np. kosztownego zapisu do wspólnej bazy.

Load balancer — mechanizm rozdzielania ruchu wybiera instancję obsługującą żądanie. Przykładowy układ rozbudowanego systemu:

flowchart LR
    C[Klient] --> LB[HTTPS i rozdzielanie ruchu]
    LB --> A[Instancja API A]
    LB --> B[Instancja API B]
    A --> DB[(Wspólny system danych)]
    B --> DB

Dwie niezależne kopie pliku SQLite nie tworzą wspólnej bazy. Jeżeli A zapisze rezerwację we własnym pliku, B jej nie zobaczy. Skopiowanie aplikacji i tokenu użytkownika nie rozwiązuje tego problemu.

W typowym wariancie z wieloma hostami aplikacje korzystają z usługi danych, np. PostgreSQL. Przypadkowe udostępnienie pliku SQLite przez sieciowy katalog nie zastępuje projektu współbieżności i trwałości. SQLite może natomiast obsługiwać wiele połączeń na jednym hoście — problemem nie jest sam fakt istnienia dwóch klientów, lecz model dostępu do wspólnego stanu.

Bezstanowe HTTP nie oznacza aplikacji bez danych

HTTP — Hypertext Transfer Protocol jest bezstanowe w sensie semantyki żądań. Aplikacja nadal może przechowywać rezerwacje, sesje i uprawnienia. Token JWT — JSON Web Token może ułatwić weryfikację tożsamości przez różne instancje, ale nie synchronizuje rezerwacji.

Sesje także mogą działać przy wielu instancjach, jeżeli ich stan jest dostępny w uzgodnionym wspólnym magazynie. Wybór wymaga uwzględnienia unieważniania, rotacji kluczy, dostępności magazynu i aktualności uprawnień.

Pamięć podręczna: szybszy odczyt, możliwa nieaktualność

Cache — pamięć podręczna przechowuje wynik, aby ograniczyć powtarzalną pracę. Katalog nazw i lokalizacji obiektów może być dobrym kandydatem. Informacja „termin wolny” jest znacznie bardziej wrażliwa na zmianę.

Przykład: klient odczytał wolne 10:00–11:00, po czym inny użytkownik dokonał rezerwacji. Nawet jeśli odczyt był poprawny w swojej chwili, zapis musi ponownie sprawdzić warunek w chronionej operacji. Cache nie może być ostatecznym źródłem zgody na rezerwację.

TTL — Time To Live ogranicza czas przechowywania wpisu. Krótszy TTL zmniejsza czas możliwej nieaktualności, lecz zwiększa liczbę odczytów źródła. Inne strategie obejmują unieważnianie po zapisie i wersjonowanie danych. Wyniki zależne od uprawnień wymagają klucza cache uwzględniającego właściwy kontekst dostępu.

Kolejka: rezerwacja i powiadomienie to różne wyniki

Wysłanie powiadomienia może trwać dłużej niż zapis rezerwacji. Kolejka pozwala przekazać pracę do osobnego procesu, ale wymaga określenia znaczenia sukcesu.

  • 201 Created może potwierdzać utworzenie rezerwacji, nawet jeśli wiadomość jeszcze nie została wysłana.
  • 202 Accepted oznacza przyjęcie operacji do późniejszego przetworzenia; samo w sobie nie potwierdza utworzenia docelowej rezerwacji.

Pomiędzy zatwierdzeniem rezerwacji a dodaniem zadania do kolejki może wystąpić awaria. Wzorzec transactional outbox zapisuje rezerwację i informację o zdarzeniu w jednej transakcji; osobny proces później przekazuje zdarzenie. Powtórne dostarczenie komunikatu wymaga obsługi duplikatów. Kolejka nie gwarantuje sama z siebie jednorazowego skutku w systemie odbiorcy.

W demonstratorze nie ma jeszcze kolejki ani wysyłania wiadomości. Jest to wariant architektury do analizy, nie dodatkowy warunek uruchomienia zajęć.

Monolit, mikroserwisy i CDN

Monolit jest jedną jednostką wdrożenia aplikacji; może mieć wiele modułów oraz wiele procesów lub instancji. Mikroserwisy rozdzielają odpowiedzialności na niezależnie wdrażane usługi. Taki podział może wspierać niezależne zespoły, różne profile obciążenia i izolowanie awarii, lecz dodaje komunikację sieciową, koordynację zmian i problemy spójności.

Dla obecnego projektu modułowa aplikacja FastAPI jest wystarczającym punktem wyjścia. Wydzielenie usługi powinno odpowiadać konkretnemu problemowi, a nie być celem samym w sobie.

CDN — Content Delivery Network, sieć dystrybucji treści, może obsługiwać statyczne zasoby blisko użytkowników: pliki JavaScript, arkusze CSS — Cascading Style Sheets i obrazy. Nie zastępuje logiki rezerwacji. Także strona kursu na GitHub Pages jest statyczna: publikuje materiały, ale nie uruchamia procesu FastAPI ani lokalnej bazy z demonstratora.

Wdrożenie i obserwacja działania

CI — Continuous Integration, ciągła integracja, obejmuje automatyczne sprawdzanie zmian, np. testy i budowę obrazu. CD może oznaczać Continuous Delivery — gotowość do kontrolowanego wydania — albo Continuous Deployment — automatyczne wdrożenie po spełnieniu warunków.

Logiczny przebieg wydania:

zmiana kodu → testy → budowa oznaczonego obrazu → próba uruchomienia
→ wdrożenie → kontrola odpowiedzi i danych → obserwacja lub wycofanie

Zielony wynik testów nie dowodzi, że środowisko produkcyjne ma poprawną konfigurację. Zmiana schematu bazy może również utrudnić powrót do starej wersji aplikacji, nawet jeśli jej obraz nadal istnieje.

Co powinno być widoczne po uruchomieniu

Sygnał Pytanie, na które odpowiada
Log żądania z identyfikatorem Która operacja się nie powiodła i z jakim kodem?
Odsetek odpowiedzi 5xx Czy rośnie liczba błędów serwera?
Czas odpowiedzi, np. percentyl 95 Jak długo czeka większość wolniejszych żądań?
Liczba konfliktów i zajęć bazy Czy występuje presja na zapis lub konflikt terminów?
Kontrola gotowości Czy instancja może obsługiwać wymagane zależności?

Logi nie powinny zawierać haseł, pełnych tokenów ani niepotrzebnych danych osobowych. „Proces żyje” i „usługa jest gotowa” to różne stany. Odpowiedź kontrolnego endpointu bez dostępu do bazy może potwierdzać działanie serwera, ale nie całej funkcji rezerwacji.

Przypadki do przeanalizowania

  1. Dwa żądania rezerwują ten sam termin. Należy opisać kolejność sprawdzenia i zapisu oraz sprawdzić końcową liczbę rezerwacji.
  2. Kontener zostaje odtworzony, ale rezerwacje znikają. Analiza obejmuje faktyczną ścieżkę pliku i montowanie katalogu, nie tylko kod API.
  3. Dwie instancje mają dwie kopie SQLite. Poprawne logowanie na obu nie dowodzi, że przedstawiają ten sam stan rezerwacji.
  4. Rezerwacja została zatwierdzona, a powiadomienie nie dotarło. Należy rozdzielić trwały stan rezerwacji od stanu zadania wysyłki.

Na laboratoriach samodzielna implementacja i próby odbiorcze obejmą wybrane mechanizmy. Zakres wykładu pokazuje również kierunki rozwoju, których nie trzeba wszystkich implementować, aby przygotować poprawną małą aplikację.

Test po wykładach

Otwórz wspólny test po wykładach — jedna karta z 10 pytaniami i jednym wynikiem.

Źródła