Konrad Kowalski (rootsher)Principal Platform & Reliability Architect000100100000001101100101111110110110110010001001

Long Polling: kiedy serwer opóźnia odpowiedź

data
kategoria
Backend
także w
Frontend
czytanie
1 min / 295 słów

Polling ma prosty problem:

text
client -> anything new?
server <- no

client -> anything new?
server <- no

Serwer odpowiada natychmiast, mimo że klient nie potrzebuje odpowiedzi „brak zmian”.

Long polling zmienia czas odpowiedzi.

Request pozostaje otwarty

Klient wysyła:

http
GET /events?after=1842

Jeżeli nie ma nowych danych, backend nie odpowiada od razu.

text
client -> request ────────────────┐
                                  │
                             wait │
                                  │
                          event!  │
client <- response ───────────────┘

Przykładowa odpowiedź:

json
{
  "id": 1843,
  "type": "order.completed",
  "orderId": 42
}

Po jej odebraniu klient natychmiast rozpoczyna kolejny request:

js
async function listen(after) {
  while (true) {
    const response =
      await fetch(`/events?after=${after}`);

    const event = await response.json();

    handle(event);
    after = event.id;
  }
}

To nadal jest HTTP request/response.

Nie powstał trwały kanał wiadomości. Po prostu odpowiedź może nadejść dużo później.

Timeout jest częścią protokołu

Żaden request nie powinien wisieć bez końca.

Reverse proxy, load balancer albo sam backend może mieć limit długości requestu.

Dlatego typowy model wygląda tak:

text
request
  |
  v
event appears -> respond

or

timeout -> empty response

Klient w obu przypadkach rozpoczyna następny request.

Przykład:

text
t=0    request A
t=25   timeout
t=25   request B
t=41   event
t=41   response B
t=41   request C

Long polling jest więc ciągiem długich requestów, a nie jednym nieskończonym requestem.

Musimy wiedzieć, co już widzieliśmy

Reconnect tworzy istotny problem.

Załóżmy:

text
event 1843 generated
response sent
network breaks

Czy klient otrzymał event?

Backend nie może tego pewnie wywnioskować tylko z faktu wysłania odpowiedzi.

Dlatego klient często przekazuje cursor:

http
GET /events?after=1842

Po odebraniu 1843 następny request będzie:

http
GET /events?after=1843

To wprowadza pojęcia, które wrócą później:

text
event identity
cursor
resume
duplicate delivery

Transport i delivery semantics to dwie różne rzeczy.

Mniej requestów, ale więcej otwartych requestów

Polling:

text
few open requests
many total requests

Long polling:

text
many open requests
fewer total requests

Przy dużej liczbie klientów backend i infrastruktura muszą dobrze obsługiwać dużą liczbę jednocześnie oczekujących operacji I/O.

W modelu thread-per-request może to być kosztowne.

W event-driven/non-blocking I/O tysiące oczekujących socketów są znacznie bardziej naturalne.

Mechanizm komunikacji wpływa więc bezpośrednio na model wykonania backendu.

Dlaczego to działało tak dobrze?

Long polling daje serwerowi możliwość niemal natychmiastowego dostarczenia zmiany bez wymagania osobnego protokołu realtime.

Przy prawidłowo zestawionym keep-alive kolejne requesty mogą wykorzystywać istniejące połączenie transportowe.

To ważne rozróżnienie:

text
HTTP request lifetime
!=
TCP connection lifetime

Kończymy request, niekoniecznie fizyczne połączenie.

Ale nadal kończymy response

Jeżeli serwer ma trzy zdarzenia:

text
event A
event B
event C

long polling zwykle wykonuje cykl:

text
request
<- event A

request
<- event B

request
<- event C

To prowadzi do prostego pytania:

skoro HTTP response body może być przesyłane stopniowo, dlaczego musimy kończyć response po pierwszym zdarzeniu?

Nie musimy.

Jedna odpowiedź może być strumieniem danych.

To jest HTTP streaming.