Edge rendering: renderowanie bliżej użytkownika
- data
- kategoria
- Frontend
- także w
- Cloud · System Design
- czytanie
- 3 min / 613 słów
Do tej pory przesuwaliśmy rendering głównie w czasie.
Mógł odbywać się:
podczas builda
przy pierwszym requestcie
przy każdym requestcie
albo fragmentami przez streaming
Ale jest jeszcze druga oś:
gdzie fizycznie wykonuje się kod?
Jeśli użytkownik jest w Warszawie, aplikacja działa w regionie us-east-1, a baza stoi w Wirginii, każdy dynamiczny request ma koszt geograficzny.
Naturalny pomysł brzmi:
przenieśmy compute bliżej użytkownika.
Origin nie musi być jedynym miejscem wykonania
Klasyczny model:
browser
|
v
CDN
|
v
origin
|
v
database
CDN świetnie radzi sobie ze statycznymi zasobami.
Ale dynamiczny HTML nadal często wymaga round tripu do originu.
Edge rendering próbuje przesunąć część wykonania:
browser
|
v
edge runtime
|
v
response
Jeżeli użytkownik znajduje się kilka milisekund od edge node'a, pierwsza część requestu może być znacznie krótsza.
„Bliżej użytkownika” brzmi lepiej niż jest
Załóżmy:
user -> edge 10 ms
user -> origin 120 ms
Na pierwszy rzut oka edge wygrywa bez dyskusji.
Ale render zwykle potrzebuje danych.
Jeżeli baza nadal znajduje się przy originie:
user
| 10 ms
v
edge
| 110 ms
v
database
| 110 ms
v
edge
| 10 ms
v
user
to przenieśliśmy compute bliżej użytkownika, ale oddaliliśmy go od danych.
W praktyce najważniejsze pytanie nie brzmi:
gdzie jest renderer?
Tylko:
gdzie znajduje się cały dependency graph potrzebny do odpowiedzi?
Komercyjny przykład: globalny storefront
Wyobraźmy sobie sklep działający w Europie, USA i Azji.
Część strony jest wspólna:
layout
opis produktu
zdjęcia
content marketingowy
Część zależy od regionu:
waluta
podatki
promocje
dostępność
język
Edge runtime może bardzo dobrze nadawać się do lekkiej logiki:
request
|
v
detect region
|
v
select locale
|
v
read edge cache
|
v
compose response
Jeżeli potrzebne dane są już lokalnie dostępne, można uniknąć drogiego round tripu do centralnego originu.
To właśnie dlatego edge dobrze łączy się z:
CDN cache
KV storage
geolokalizacja
routing
prosta personalizacja
Edge runtime to często inny runtime
W praktyce „edge” nie zawsze oznacza po prostu:
Node.js, tylko bliżej użytkownika
Często kod działa w bardziej ograniczonym środowisku.
Na przykład w modelu opartym o isolate'y:
request
|
v
existing lightweight isolate
|
v
execute handler
zamiast:
request
|
v
start process/container
|
v
initialize runtime
|
v
execute handler
To może zmniejszyć startup overhead, ale zwykle ogranicza dostępne API i sposób korzystania z natywnych zależności.
Framework może ukrywać sporą część tej różnicy, ale runtime pozostaje inny.
Ciekawostka
V8 isolate nie jest osobnym procesem.
To odseparowany kontekst wykonania JavaScriptu wewnątrz procesu V8, posiadający własny heap i stan runtime'u.
Dzięki temu wiele isolate'ów może współdzielić jeden proces, zamiast uruchamiać osobny proces systemowy dla każdego requestu.
To daje bardzo dobry profil startupu i gęstości uruchomień.
Ale granica isolate'a nie daje automatycznie takiej samej izolacji jak granica procesu.
To jeden z powodów, dla których edge runtimes mają własny zestaw ograniczeń i mechanizmów sandboxingu.
Data locality wygrywa z marketingiem
Załóżmy dwa warianty.
Origin rendering
user -> origin: 100 ms
origin -> database: 2 ms
render: 15 ms
Edge rendering
user -> edge: 10 ms
edge -> database: 90 ms
render: 15 ms
Edge nie daje tutaj spektakularnej przewagi.
Jeżeli render wykonuje kilka sekwencyjnych query:
edge
|
v
DB query A
|
v
DB query B
|
v
DB query C
problem może być wręcz gorszy.
Każde połączenie z bazą dokłada kolejne RTT.
Dlatego edge architecture bardzo często wymusza zmianę nie tylko miejsca wykonania frontendu, ale też sposobu dostępu do danych.
Replikacja danych zmienia grę
Jeżeli dane możemy replikować:
US database
EU replica
Asia replica
wtedy compute i dane mogą znajdować się blisko siebie.
Ale natychmiast pojawia się nowy problem:
consistency.
Jeżeli użytkownik zmieni adres dostawy w Europie, kiedy aktualizacja będzie widoczna w replice w Tokio?
Dla contentu marketingowego opóźnienie kilku sekund może być nieistotne.
Dla stanu konta albo płatności już nie.
Edge przesuwa więc frontend w świat klasycznych problemów systemów rozproszonych:
latency
consistency
replication
data ownership
Edge świetnie nadaje się do pewnej klasy pracy
Nie wszystko trzeba przenosić.
Dobre kandydatury to zwykle operacje:
krótkie
request-specific
zależne od lokalizacji
mocno cache'owalne
niewymagające wielu odległych query
Przykładowo:
redirect
locale detection
A/B assignment
authentication check
cache lookup
response composition
Znacznie trudniejszy przypadek:
complex ORM query
+
multiple internal APIs
+
transaction
+
large server render
Sam deployment „na edge” nie naprawia architektury zależności.
Edge i SSR to różne decyzje
To ważne rozróżnienie.
SSR odpowiada:
kiedy powstaje HTML?
Edge odpowiada:
gdzie wykonuje się kod?
Możemy mieć:
SSR on origin
SSR on edge
static HTML on edge CDN
client rendering with edge API
To niezależne osie.
Współczesne frameworki często prezentują je razem, dlatego łatwo zacząć traktować „edge rendering” jak osobny typ renderowania.
Architektonicznie to przede wszystkim decyzja o lokalizacji compute.
I znowu wracamy do klienta
Do tej pory próbowaliśmy poprawić pierwsze dostarczenie HTML-a.
Przesuwaliśmy rendering:
build
request
stream
edge
Ale nadal pozostaje druga połowa problemu.
Jeżeli wysyłamy użytkownikowi dużą aplikację JavaScriptową, browser może później wykonać pełną hydration niezależnie od tego, jak szybko powstał HTML.
Możemy więc zoptymalizować serwer do granic możliwości, a potem oddać main threadowi ogromne drzewo komponentów do aktywacji.
Naturalne pytanie brzmi:
czy cała strona naprawdę musi stać się interaktywna od razu?
To prowadzi do kolejnej klasy rozwiązań: Selective i Progressive Hydration.