Polling: realtime przez powtarzanie requestu
Frontend pobiera status operacji:
GET /orders/42
HTTP/1.1 200 OK
Content-Type: application/json
{"status":"processing"}
Request się kończy.
Pięć sekund później backend zmienia stan:
processing -> completed
HTTP nie ma mechanizmu, który automatycznie poinformuje o tym zakończony request.
Najprostsze rozwiązanie to wykonać następny.
Polling
const timer = setInterval(async () => {
const response = await fetch("/orders/42");
const order = await response.json();
render(order);
}, 5000);
Powstaje sekwencja:
t=0 GET -> processing
t=5 GET -> processing
t=10 GET -> processing
t=15 GET -> completed
To nadal zwykły request/response.
„Realtime” powstaje wyłącznie przez częste powtarzanie requestu.
Latency jest funkcją interwału
Jeżeli polling odbywa się co T, zmiana może zostać zauważona od niemal natychmiast do prawie T później.
Przy zmianach równomiernie rozłożonych w czasie średnie dodatkowe opóźnienie wynosi około:
T / 2
Polling co 10 sekund oznacza więc średnio około 5 sekund opóźnienia wynikającego wyłącznie z harmonogramu klienta.
Zmniejszenie T poprawia freshness, ale zwiększa koszt.
Koszt istnieje nawet bez zmian
Załóżmy:
100 000 clients
poll every 5 s
Daje to średnio:
100 000 / 5 = 20 000 requests/s
nawet jeśli przez większość czasu backend odpowiada:
{"status":"processing"}
Problemem nie jest rozmiar pojedynczego response.
Problemem jest stosunek:
number of requests
------------------
number of actual changes
Jeżeli stan zmienia się raz na kilka minut, większość ruchu istnieje tylko po to, aby potwierdzić brak zmiany.
Klienci potrafią się zsynchronizować
Jeżeli tysiące instancji UI uruchamia polling w podobnym momencie:
00s ███████████████
01s
02s
03s
04s
05s ███████████████
backend dostaje okresowe piki.
Dlatego interwał często warto rozpraszać jitterem:
next_poll = base_interval + random_jitter
To ten sam powód, dla którego jitter pojawia się w retry/backoff.
Polling nie musi zwracać pełnych danych
Możemy wykorzystać semantykę HTTP.
Przykład z ETag:
GET /orders/42
If-None-Match: "v17"
Jeżeli reprezentacja się nie zmieniła:
HTTP/1.1 304 Not Modified
ETag: "v17"
Zmniejszamy transfer body, ale nie liczbę requestów.
Można też pytać o zmiany od konkretnego punktu:
GET /events?after=1842
To pomaga uniknąć pobierania całego stanu, ale nadal klient decyduje, kiedy zapytać.
Polling ma ważną zaletę
Jest operacyjnie prosty.
Każdy request jest niezależny:
client
|
v
load balancer
|
v
any backend instance
Nie potrzebujemy wielominutowej relacji klienta z konkretnym połączeniem.
Działają normalne:
- timeouts,
- HTTP caching,
- autoscaling,
- stateless backend instances,
- klasyczne reverse proxy.
Dlatego polling bywa właściwym wyborem.
Jeżeli dashboard może mieć dane starsze o 30 sekund, WebSocket może być kosztowniejszą odpowiedzią na problem, którego nie mamy.
Gdzie jest prawdziwe ograniczenie?
Klient pyta:
anything new?
anything new?
anything new?
anything new?
Serwer przez większość czasu odpowiada:
no
no
no
yes
Ale serwer wie, kiedy pojawi się yes.
Możemy więc zmienić jedną rzecz:
zamiast odpowiadać natychmiast „nic nowego”, nie odpowiadajmy, dopóki coś się nie wydarzy.
To jest long polling.