ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 149
Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 145
Abonament PDF

WebAssembly. Povestea unui Bytecode care l-a provocat pe Docker

Mircea Talu
Junior DevOps Engineer @ Accesa



PROGRAMARE


Acum câteva luni, în martie, Accesa a venit la mine cu o oportunitate interesantă. Cu cea de-a 20-a aniversare a firmei. Dar înainte de marea petrecere, pe 6 iunie urma să aibă loc un alt tip de eveniment, Tech Conference. Pentru acest eveniment, am avut ocazia să ținem o prezentare tehnică despre DevOps. Așa începe povestea noastră. 25 de minute, un PoC... trebuia să fie ceva special!

Din fericire pentru mine, în acea perioadă am aflat despre cea mai recentă carte despre Kubernetes a lui Nigel Poulton, prin amabilitatea unuia dintre colegii mei. Capitolul 9 al acestei cărți mi-a atras atenția: WebAssembly on Kubernetes.

La acea vreme nici nu auzisem de WebAssembly... așa că m-am întrebat: "Ce e chestia asta?".

WebAssembly - Explicat pe scurt

Browserul este un instrument fascinant. El ne permite să ne facem treaba, să găsim cărți online, să ne uităm la emisiunile noastre preferate și multe altele. Și ce limbi vorbește browserul? HTML, CSS și JavaScript. Noi ne vom concentra pe ultima dintre ele.

JavaScript este uimitor la ceea ce face cu toate acestea, dar nu este grozav când vine vorba de sarcini de lucru computaționale de înaltă performanță. Acesta este unul dintre motivele pentru care ne este foarte dificil să ne jucăm jocurile noastre video preferate în browser.

Acum să ne imaginăm o lume în care am putea rula aplicații de jocuri video scrise în C/C++ sau Rust în browser. Destul de dificil, nu? Aceste limbaje se compilează în cod mașină și, prin urmare, nu sunt portabile.

Niște oameni foarte deștepți s-au gândit la aceeași lume în urmă cu șapte ani și nu au fost mulțumiți de situație, așa că s-au gândit la o soluție. Ce ar fi dacă am compila codul nostru scris în C/C++ sau Rust într-un low-level bytecode, nu direct în cod mașină?

Mai mult decât atât, ce ar fi dacă acel bytecode ar putea fi interpretat de browser? Asta ar însemna că putem să ne luăm implementarea din C/C++ sau Rust, să compilăm în acel bytecode și apoi să o rulăm în browser la o performanță aproape nativă.

Formatul de bytecode pe care l-am menționat este acum cunoscut sub numele de WebAssembly (abreviat ca WASM). De la începuturile sale, au avut loc multe progrese și în prezent există compilatoare care pot viza WASM din mai multe limbaje.

Docker și containere

Acum vom lua o decizie radicală și vom abandona WASM (pentru moment).

Dacă suntem ingineri software, sunt șanse mari să fi folosit Docker pentru a ne containeriza aplicația cel puțin o dată. Cum am făcut asta? Probabil așa: am scris un Dockerfile, am construit imaginea cu docker build -t my-cool-image:latest și apoi am rulat-o cu docker run my-cool-image:latest. Iată prima noastră aplicație containerizată!

Totuși, pe măsură ce Tech Conference-ul se apropia cu pași repezi, am început să sap mai adânc pentru a mă pregăti și curând mi-am dat seama că, de fapt, nu știu nimic despre containere...

Unul dintre cele mai mari mituri este că Docker este ceea ce ne oferă containere. De fapt, Docker nu ne-a oferit niciodată containere. Adevărul este că prietenul nostru, Linux, este cel care o face.

Sursă img. Fermyon Developer

În următoarele câteva minute, vreau să uitați tot ceea ce știți despre Docker și containere.

Primul lucru de care trebuie să ne amintim este că totul este un proces! Jocul video, browserul și aplicațiile care rulează în containere sunt toate procese. Dar atunci, ce face containerele să difere de procesele normale care rulează pe host? Izolarea.

Procesele care rulează în interiorul containerelor sunt izolate! Deci, cum realizează Kernelul Linux această izolare atunci când creează un container? Există doar două concepte despre care trebuie să știți: control groups și namespaces.

Namespaces

Când rulăm containerul, noi specificăm un ENTRYPOINT sau un CMD, care este ceea ce va executa noul nostru proces.

FROM openjdk:23-slim
COPY my-application-1.0-SNAPSHOT.jar 
  /home/my-application-1.0-SNAPSHOT.jar

CMD ["java","-jar",
  "/home/my-application-1.0-SNAPSHOT.jar"]

Dar cum este creat acest nou proces? Răspunsul este: la fel ca orice alt proces, folosind fork().

Acum, după fork(), avem un proces părinte și un proces copil (care este procesul containerizat). Ce se întâmplă dacă procesul copil apelează getppid() (obține ID-ul procesului părinte)? Apelul de sistem va returna identificatorul de proces al părintelui. Asta e rău! Nu dorim ca procesul nostru containerizat să știe despre nimic din afara containerului. Prin urmare, trebuie să facem ca procesul copil să creadă că nu are părinte, așa că setăm ID-ul de proces părinte la NULL folosind prctl(). De asemenea, în imagine puteți vedea că procesul copil are 2 ID-uri (8 și 1). Acest lucru îi este datorat contextului. În interiorul containerului este procesul 1, dar din punctul de vedere al gazdei, este procesul 8.

Acesta este tipul de problemă de care sunt responsabile namespace-urile. Ele ne spun ce poate vedea procesul!

Control groups (cgroups)

Grupurile de control ne spun cât de mult poate folosi procesul. În exemplul de mai jos, parte a unui manifest YAML al unui Deployment Kubernetes, putem vedea limitele de resurse ale containerului meu.

containers:
  - name: myapp
    image: campionulclujului/resource-app:latest
    command: ["/"]
      resources:
        limits:
          cpu: "0.5"
          memory: "256Mi"

Există destul de multe grupuri de control și dacă doriți să vă aprofundați în fiecare dintre ele, urmăriți acest videoclip grozav de pe YouTube.

Docker

Dacă Kernelul Linux ne oferă tot ce avem nevoie pentru a crea containere (namespaces și cgroups), atunci ce face Docker? Deși este adevărat că putem crea containere folosind doar apeluri de sistem Linux, este extrem de dificil. Docker oferă o modalitate foarte ușoară de a crea containere Linux. Nu numai asta, dar Docker vine și cu un CLI, un serviciu de autentificare (la Docker Hub) și multe alte utilități.

De aceea, a devenit atât de popular și și-a depășit toți concurenții la început! A fost foarte ușor de utilizat pentru ingineri software. Tot ce trebuie să facem este să rulăm o mică comandă și dintr-odată avem o aplicație containerizată care rulează pe host.

Cu toate acestea, pe măsură ce Cloud Native Computing Foundation (CNCF) a evoluat și Kubernetes a fost adoptat pe scară largă, au început să apară preocupări în ceea ce privește Docker. Deși a fost un instrument excelent pentru dezvoltatori pentru a rula local, avea o mulțime de funcționalități inutile în producție. Când facem deploymentul aplicației noastre containerizate în Kubernetes, ne pasă doar de container runtime, care era doar o mică parte din întregul pachet Docker.

Container runtime

Pe măsură ce cursa pentru containerizare s-a intensificat, oamenii au început să-și dorească runtime-uri diferite. Până atunci, Kubernetes era strâns cuplat cu Docker. Soluția a fost simplă: creăm un Container Runtime Interface (CRI) și lăsăm pe oricine să vină cu propria implementare.

Ca urmare a acestor evoluții, Docker a trebuit să reacționeze. Așadar, cei de la Docker și-au împărțit arhitectura monolitică pentru a se potrivi cu specificațiile Open Container Initiative (OCI). Container runtime-ul rezultat a devenit cunoscut sub numele de containerd.

Putem vedea câteva componente despre care nu am vorbit în imagine. Să începem cu runc. Scopul runc este de a configura mediul containerului (cgroups, namespaces), de a începe procesul de container și apoi de a ieși (exit).

Ce se întâmplă după ce runc iese? Cine va gestiona containerul acum? Responsabilitățile sale sunt trecute la containerd-shim. După cum putem vedea, ciclul de viață al acestei componente se potrivește cu ciclul de viață al containerului. Când containerul este ucis, shimul moare odată cu el.

Motivul pentru care avem un shim este că oferă un strat suplimentar de decuplare. Putem folosi diferite tipuri de shim. În imagine, două containere folosesc containerd-shim implicit, în timp ce al treilea folosește ceva diferit (poate că numele îi sună familiar).

Nu în ultimul rând, la cel mai înalt nivel, avem demonul containerd care are rolul de runtime al containerului de nivel înalt pe acel nod.

Containere WASM

În sfârșit, am ajuns la finalul jocului. Am promis că ne vom întoarce la WASM și iată-ne.

Știm că containerele sunt foarte eficiente. Ele ne permit să izolăm aplicația noastră cu dependențele sale. Prin urmare, să luăm, de exemplu, o aplicație Java.

FROM openjdk:23-slim
COPY my-application-1.0-SNAPSHOT.jar /home/my-application-1.0-SNAPSHOT.jar
CMD ["java","-jar",  "/home/my-application-1.0-SNAPSHOT.jar"]

Acest Dockerfile poate părea familiar, asta pentru că este același pe care l-am folosit anterior. Dar de data aceasta, vreau să ne concentrăm pe prima linie. Ce face acea instrucțiune FROM? Specifică o imagine de bază. De ce avem nevoie de o imagine de bază? Deoarece bytecode-ul nostru Java trebuie interpretat și are nevoie de un mediu de rulare. Asta ne oferă acel openjdk:23-slim.

Ok, e grozav. Cu toate acestea, ce se întâmplă atunci când trebuie să ne extindem aplicația containerizată? Vom avea 100 de instanțe ale containerului nostru, fiecare dintre ele având propriul său JDK. Acesta este overhead! Practic, trebuie să creăm același mediu de rulare de 100 de ori.

Acum ce-ar fi să ne imaginăm o lume în care nu avem nevoie de un mediu de rulare în interiorul containerului, o lume în care putem configura mediul de rulare în afara containerului, unde poate fi utilizat de toate containerele noastre?

"Dar, Mircea... asta e imposibil! Acest lucru ar necesita ca toate aplicațiile să fie scrise în același limbaj, același format."

Dar dacă am putea într-adevăr să facem asta? Într-un final, înțelegem de ce WebAssembly a fost atât de important în toată această poveste. Dacă ne putem compila toate aplicațiile, indiferent de limbajul în care sunt scrise, în bytecode WASM, atunci putem standardiza mediul de rulare. Exact asta au gândit un alt grup de oameni inteligenți când au venit cu ideea unui WebAssembly System Interface (WASI), o interfață pentru runtime-uri de WebAssembly.

În această imagine, putem vedea cum containerele nu mai au nevoie să își creeze propriul mediu de rulare (biblioteci, dependențe, bins). Tot ce rămâne în containerul (modulul) WASM este bytecode-ul, în forma sa cea mai pură.

Acest lucru nu numai că ne scapă de overhead, dar este și mult mai sigur, deoarece reduce suprafața de atac. Dacă nu există niciun mediu în interiorul containerului (nicio mini distribuție Linux), atunci nu există nimic de atacat. Dimensiunea containerului WASM este, de asemenea, mult mai mică, ceea ce îl face mai ușor și mai simplu de pornit, ideal pentru arhitectura serverless.

Unde ne aflăm în acest moment?

Microsoft Azure a anunțat suport pentru grupurile de noduri WASI în clusterele lor Azure Kubernetes Service (AKS). Sunt încă în previzualizare, dar viitorul pare promițător.

Suportul pentru limbi suplimentare este în creștere, așa cum se poate vedea în matricea de limbi pe care am analizat-o anterior.

Interfața de sistem WebAssembly a văzut deja o serie de implementări. Unele dintre ele includ wasmedge, wasmtime, wasmer și altele.

Serviciile serverless, cum ar fi Azure Functions, nu au încă un runtime pentru WASM, dar prevedem că nu va trece mult timp până când acest lucru va deveni realitate.

Demo

Cum ne rulăm containerele WASM pe un Windows host? Recomand să instalați Docker Desktop, deoarece oferă suport Beta pentru containere WASM.

Mai întâi, accesăm Settings -> General și verificăm opțiunea Use containerd for pulling and storing images.

Apoi, accesăm Settings -> Features in development și verificați funcția Enable Wasm. Aceasta va instala următoarele runtime-uri:

Acum, suntem gata să rulăm primul nostru container WASM:

docker run --runtime=io.containerd.wasmedge.v1 --platform=wasi/wasm secondstate/rust-example-hello

Cum ne rulăm containerele WASM în Azure Kubernetes Service?

Recomand acest tutorial de la Microsoft, care trece prin toate condițiile preliminare pentru a avea un pool de noduri WASI în AKS și chiar ne arată cum să implementăm o aplicație simplă.

Cum rulez propria mea aplicație drept container WASM în Kubernetes?

În primul rând, trebuie să instalăm compilatorul potrivit, care va prelua codul scris în C/C++, Rust, Java sau orice alt limbaj acceptat și îl va compila în bytecode WASM. Apoi, singurul alt fișier de care avem nevoie este un TOML (Tom's Obvious Minimal Language), care este un fișier de configurare care îi spune runtime-ului WASM cum să ruleze modulul WASM. Asta e tot!

După ce am obținut bytecode-ul WASM, creăm un Dockerfile și copiem cele două fișiere (.wasm și .toml).

FROM scratch
COPY /target/wasm32-wasi/release/myapp_wasm.wasm .
COPY spin.toml .

Putem observa că instrucțiunea FROM nu folosește o imagine de bază tipică Linux (scratch este o imagine de bază goală)!

Acum putem construi imaginea.

docker build --platform wasi/wasm --provenance=false -t /mywasmapp:0.1 .

Apoi o încărcăm pe Docker Hub.

docker push <your_docker_hub_account>/mywasmapp:0.1

Înainte de a da deploy în Kubernetes, trebuie să creăm o clasă de rulare.

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: rc-spin
scheduling:
  nodeSelector:
    wasm: "yes"
handler: spin

Acum ne putem folosi imaginea pentru a crea un Deployment,

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mywasmapp
spec:
  runtimeClassName: rc-spin
  containers:
    - name: mywasmapp
      image: /mywasmapp:0.1
      command: ["/"]
      resources:
        limits:
          cpu: "0.1"
          memory: 128Mi

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects