Konrad Kowalski (rootsher)Principal Platform & Reliability Architect010111110101011110001011100011001010111010010110

Polling: realtime przez powtarzanie requestu

data
kategoria
Frontend
także w
Backend
czytanie
2 min / 319 słów

Frontend pobiera status operacji:

http
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:

text
processing -> completed

HTTP nie ma mechanizmu, który automatycznie poinformuje o tym zakończony request.

Najprostsze rozwiązanie to wykonać następny.

Polling

js
const timer = setInterval(async () => {
  const response = await fetch("/orders/42");
  const order = await response.json();

  render(order);
}, 5000);

Powstaje sekwencja:

text
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:

text
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:

text
100 000 clients
poll every 5 s

Daje to średnio:

text
100 000 / 5 = 20 000 requests/s

nawet jeśli przez większość czasu backend odpowiada:

json
{"status":"processing"}

Problemem nie jest rozmiar pojedynczego response.

Problemem jest stosunek:

text
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:

text
00s  ███████████████
01s
02s
03s
04s
05s  ███████████████

backend dostaje okresowe piki.

Dlatego interwał często warto rozpraszać jitterem:

text
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:

http
GET /orders/42
If-None-Match: "v17"

Jeżeli reprezentacja się nie zmieniła:

http
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:

http
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:

text
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:

text
anything new?
anything new?
anything new?
anything new?

Serwer przez większość czasu odpowiada:

text
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.