Miten OpenAI tarjoaa matalaviiveistä puhetekoälyä laajamittaisesti
Yi Zhang ja William McDonald, teknisen henkilöstön jäsenet
Puhetekoäly tuntuu luonnolliselta vain, jos keskustelu etenee puheen tahdissa. Kun verkko tulee tielle, ihmiset kuulevat sen heti kömpelöinä taukoina, katkonaisina keskeytyksinä tai viivästyneenä sisäänpuheena. Tällä on merkitystä ChatGPT Voicelle, Realtime API:n kanssa rakentaville kehittäjille, vuorovaikutteisissa työnkuluissa toimiville agenteille ja malleille, joiden on käsiteltävä ääntä käyttäjän vielä puhuessa.
OpenAI:n mittakaavassa tämä tarkoittaa kolmea konkreettista vaatimusta:
- Globaali kattavuus yli 900 miljoonalle viikoittain aktiiviselle käyttäjälle
- Nopea yhteydenmuodostus, jotta käyttäjä voi alkaa puhua heti istunnon alkaessa
- Matala ja vakaa median edestakainen viive sekä vähäinen jitteri ja pakettihävikki, jotta vuorottelu tuntuu napakalta
OpenAI:n reaaliaikaisista tekoälyvuorovaikutuksista vastaava tiimi suunnitteli hiljattain WebRTC-pinomme uudelleen ratkaistakseen kolme rajoitetta, jotka alkoivat törmätä toisiinsa mittakaavan kasvaessa: yksi portti per istunto -mallinen median päättäminen ei sovi hyvin OpenAI:n infrastruktuuriin, tilalliset ICE- (Interactive Connectivity Establishment) ja DTLS-istunnot (Datagram Transport Layer Security) tarvitsevat vakaan omistajuuden, ja globaalin reitityksen on pidettävä ensimmäisen hypyn viive alhaisena. Tässä kirjoituksessa käymme läpi rakentamamme jaetun rele + lähetinvastaanotin -arkkitehtuurin, jolla säilytämme asiakkaille standardin WebRTC-käyttäytymisen mutta muutamme sitä, miten paketit reititetään OpenAI:n infrastruktuurin sisällä.
WebRTC on avoin standardi matalaviiveisen äänen, videon ja datan lähettämiseen selainten, mobiilisovellusten ja palvelinten välillä. Se yhdistetään usein vertaisyhteyksiin perustuviin puheluihin, mutta se on myös käytännöllinen perusta asiakas-palvelin-tyyppisille reaaliaikaisille järjestelmille, koska se standardoi interaktiivisen median vaikeat osat: ICE:n yhteyden muodostamiseen ja NAT-läpikulkuun (Network Address Translation), DTLS:n ja SRTP:n (Secure Real-time Transport Protocol) salattuun siirtoon, koodekkineuvottelun äänen pakkaamiseen ja purkamiseen, RTCP:n (Real-time Transport Control Protocol) laadunvalvontaan sekä asiakaspään ominaisuudet, kuten kaiunpoiston ja jitteripuskuroinnin.
Tämä standardointi on tärkeää tekoälytuotteille. Ilman WebRTC:tä jokainen asiakas tarvitsisi eri ratkaisun siihen, miten yhteys muodostetaan NAT:ien yli, miten media salataan, miten koodekeista neuvotellaan (siirtoon ja purkuun valitut kooderit-dekooderit) ja miten sopeudutaan muuttuviin verkko-olosuhteisiin. WebRTC:n avulla voimme rakentaa protokollapinon päälle, joka on jo toteutettu selaimissa ja mobiilialustoilla, ja keskittää oman työmme infrastruktuuriin, joka yhdistää reaaliaikaisen median malleihin.
Rakennamme myös itse WebRTC-ekosysteemin varaan, mukaan lukien kypsät avoimen lähdekoodin toteutukset ja standardointityö, joka pitää selaimet, mobiilisovellukset ja palvelimet yhteentoimivina. Justin Ubertin (yhden WebRTC:n alkuperäisistä arkkitehdeista) ja Sean DuBoisin (Pionin luoja ja ylläpitäjä) perustavanlaatuinen työ mahdollisti sen, että meidän kaltaisemme tiimit voivat rakentaa taistelussa testatun mediainfrastruktuurin päälle sen sijaan, että keksisivät uudelleen matalan tason siirron, salauksen ja ruuhkansäädön käyttäytymisen. Olemme onnekkaita, että sekä Justin että Sean ovat nyt kollegoitamme täällä OpenAI:ssa ja auttavat ohjaamaan sitä, miten tuomme WebRTC:n ja reaaliaikaisen tekoälyn lähemmäs toisiaan.
Tekoälyn kannalta tärkein ominaisuus on, että ääni saapuu jatkuvana virtana. Puhuva agentti voi alkaa litteroida, tehdä päättelyä, kutsua työkaluja tai tuottaa puhetta samalla, kun käyttäjä vielä puhuu, sen sijaan että se odottaisi koko lähetyksen valmistumista. Tämä erottaa keskustelevalta tuntuvan järjestelmän push-to-talk-tyyppisestä järjestelmästä.
Kun olimme valinneet WebRTC:n, seuraava kysymys oli, missä se päätetään (missä hyväksyisimme ja omistaisimme WebRTC-yhteyden — esimerkiksi reunalla) ja miten nämä istunnot yhdistetään inferenssitaustajärjestelmään. Päättäminen on tärkeää, koska se määrittää, miten käsittelemme reaaliaikaista istuntotilaa, median siirtoa, reititystä, viivettä ja vikojen eristämistä.
SFU eli selective forwarding unit on mediapalvelin, joka vastaanottaa yhden WebRTC-virran jokaiselta osallistujalta ja välittää valikoidusti virtoja muille. Tässä mallissa SFU päättää erillisen WebRTC-yhteyden jokaiselle osallistujalle, ja tekoäly liittyy istuntoon yhtenä lisäosallistujana. Tämä voi sopia hyvin tuotteisiin, jotka ovat luonteeltaan monen osapuolen tuotteita, kuten ryhmäpuheluihin, luokkahuoneisiin tai yhteistoiminnallisiin kokouksiin. Se pitää äänikoodekit, RTCP-viestit, datakanavat, tallennuksen ja per-virta-käytännöt yhdessä paikassa.1
Myös asiakas-tekoäly-tuotteissa SFU on usein oletuslähtökohta, koska sen avulla tiimit voivat käyttää uudelleen yhtä hyväksi todettua järjestelmää signalointiin, median reititykseen, tallennukseen, havainnoitavuuteen ja tuleviin laajennuksiin, kuten ihmisen tekemään siirtoon tai useampien osallistujien lisäämiseen.
Meidän kuormamme on erilainen. Useimmat istunnot ovat 1:1 — yksi käyttäjä puhuu yhdelle mallille tai yksi sovellus yhdelle reaaliaikaiselle agentille — ja jokainen vuoro on viiveherkkä. Tämänmuotoiselle liikenteelle valitsimme lähetinvastaanotin-mallin: reunan WebRTC-palvelu päättää asiakasyhteyden ja muuntaa sitten median ja tapahtumat yksinkertaisemmiksi sisäisiksi protokolliksi mallin inferenssiä, litterointia, puheen tuottamista, työkalujen käyttöä ja orkestrointia varten.
Tässä suunnitelmassa lähetinvastaanotin on ainoa palvelu, joka omistaa WebRTC-istunnon tilan, mukaan lukien ICE-yhteystarkistukset, DTLS-kättelyn, SRTP-salausavaimet ja istunnon elinkaaren. Tässä “päättäminen” tarkoittaa, että lähetinvastaanotin on endpoint, joka saattaa nämä kättelyt päätökseen ja salaa tai purkaa median salauksen. Tämän tilan pitäminen yhdessä paikassa helpotti istunnon omistajuuden hahmottamista, ja se antoi taustapalveluille mahdollisuuden skaalautua tavallisten palvelujen tavoin sen sijaan, että ne itse toimisivat WebRTC-vertaisina.
Valittuamme lähetinvastaanotinmallin ensimmäinen toteutuksemme oli yksi Go-palvelu, joka oli rakennettu Pionin päälle ja joka hoiti sekä signaloinnin että median päättämisen. Se pyörittää ChatGPT Voicea, Realtime API:n WebRTC-endpointia ja useita tutkimusprojekteja.
Operatiivisesti lähetinvastaanotinpalvelulla on kaksi tehtävää:
- Signalointi: SDP-neuvottelu, koodekkivalinta, ICE-tunnistetiedot ja istunnon käyttöönotto
- Media: alavirran WebRTC-yhteyksien päättäminen ja ylävirran yhteyksien ylläpito taustapalveluihin inferenssiä ja orkestrointia varten
Halusimme palvelun toimivan kuten muu infrastruktuurimme: Kubernetesissa, jossa kuormat voivat skaalautua ylös ja alas ja siirtyä hostien välillä kysynnän muuttuessa. Mutta tavanomainen yksi portti per istunto -WebRTC-malli sopii tähän ympäristöön huonosti, koska se riippuu suurista julkisista UDP-porttialueista, joita on vaikea altistaa, suojata ja säilyttää, kun podeja lisätään, poistetaan tai ajoitetaan uudelleen.2
Ensimmäinen ongelma oli itse yksi portti per istunto -malli. Suurella samanaikaisuudella tämä tarkoittaa hyvin suurten UDP-porttialueiden altistamista ja hallintaa.
- Pilven kuormantasaajat ja Kubernetes-palvelut eivät ole suunniteltu kymmenientuhansien julkisten UDP-porttien ympärille per palvelu. Jokainen lisäalue lisää operatiivista monimutkaisuutta kuormantasaajan asetuksiin, terveystarkistuksiin, palomuurikäytäntöihin ja käyttöönoton turvallisuuteen.3
- Laajoja UDP-porttialueita on vaikea suojata, koska ne laajentavat ulkoisesti tavoitettavaa hyökkäyspinta-alaa ja vaikeuttavat verkkokäytäntöjen auditointia.
- Ne sopivat huonosti myös automaattiseen skaalaamiseen. Podeja lisätään, poistetaan ja ajoitetaan jatkuvasti uudelleen Kubernetesissa. Jos jokaisen podin on varattava ja ilmoitettava suuri vakaa porttialue, tämä elastisuus muuttuu hauraaksi.4
Siksi monet WebRTC-järjestelmät siirtyvät kohti yhtä UDP-porttia per palvelin, ja sovellustason demultipleksointi tapahtuu tuon portin takana.5
Yksi portti per palvelin -suunnitelmat ratkaisevat porttimäärän, mutta ne tuovat mukanaan toisen ongelman: kunkin istunnon omistajuuden säilyttämisen palvelinlaivueessa.
ICE ja DTLS ovat tilallisia protokollia. Session luoneen prosessin on edelleen vastaanotettava kyseisen session paketit, jotta se voi validoida yhteystarkistukset, saattaa DTLS-kättelyn päätökseen, purkaa SRTP:n salauksen ja käsitellä myöhempiä istuntomuutoksia, kuten ICE-uudelleenkäynnistyksiä. Jos saman session paketit päätyvät eri prosessiin, käyttöönotto voi epäonnistua tai media voi rikkoutua.
Tämä antoi meille täsmällisen tavoitteen: altistaa julkiseen internetiin pieni, kiinteä UDP-pinta, mutta silti reitittää jokainen paketti sille lähetinvastaanottimelle, joka omistaa vastaavan WebRTC-istunnon.
Arvioimme useita tapoja päästä tähän, mukaan lukien TURNin (Traversal Using Relays around NAT), jossa reunan rele päättää asiakasallokaatiot ja välittää liikennettä niiden puolesta.2
Lähestymistapa | Edut | Haitat |
Yksilöllinen IP:portti per istunto (tunnetaan myös nimellä natiivi suora UDP) | Suora asiakas-palvelin-mediapolku Ei välityskerrosta datapolulla | Vaatii yhden julkisen UDP-portin per istunto Suuria porttialueita on vaikea altistaa ja suojata Sopii huonosti Kubernetesiin ja pilven kuormantasaajiin |
Yksilöllinen IP:portti per palvelin | Paljon pienempi julkinen UDP-jalanjälki kuin istuntokohtaisessa altistuksessa Yksi jaettu socket per palvelin voi demultipleksoida monta istuntoa | Toimii siististi yhdellä hostilla, mutta ei yksinään jaetussa kuormantasatussa laivueessa Istuntojen demultipleksointi yhdellä hostilla auttaa vasta sen jälkeen, kun paketti saavuttaa kyseisen hostin; kuormantasatussa laivueessa ensimmäinen paketti voi silti päätyä väärään instanssiin, joten tarvitset edelleen deterministisen tavan ohjata kukin istunto sitä omistavaan prosessiin |
TURN-rele (protokollan päättävä) | Asiakkaiden tarvitsee tavoittaa vain TURN-releen osoite ja portti Käytäntöjä voidaan keskittää reunalle | TURN-allokaatiot lisäävät käyttöönoton edestakaisia kierroksia Allokaatioiden siirtäminen tai palauttaminen TURN-palvelimien välillä on edelleen vaikeaa |
Tilaton välittäjä + tilallinen päättäjä (OpenAI:n rele + lähetinvastaanotin) | Pieni julkinen UDP-jalanjälki Lähetinvastaanotin omistaa edelleen koko WebRTC-istunnon | Lisää yhden välityshypyn ennen kuin media saavuttaa omistavan lähetinvastaanottimen Vaatii mukautettua koordinaatiota releen ja lähetinvastaanottimen välillä |
Käyttöön ottamamme arkkitehtuuri erottaa pakettien reitityksen protokollan päättämisestä. Signalointi saavuttaa edelleen lähetinvastaanottimen istunnon käyttöönottoa varten, kun taas media saapuu ensin releen kautta. Rele on kevyt UDP-välityskerros, jolla on pieni julkinen jalanjälki, ja lähetinvastaanotin on sen takana oleva tilallinen WebRTC-endpoint.
Rele ei pura median salausta, suorita ICE-tilakoneita eikä osallistu koodekkineuvotteluun. Se lukee paketin metatietoja juuri sen verran, että voi valita kohteen, ja välittää sitten paketin lähetinvastaanottimelle, joka omistaa istunnon. Lähetinvastaanotin näkee edelleen normaalin WebRTC-virran ja omistaa edelleen kaiken protokollatilan. Asiakkaan näkökulmasta mikään WebRTC-istunnossa ei muutu.
Ensimmäisen paketin reititys on tämän rakenteen avainvaihe. Releen on reititettävä asiakkaan ensimmäinen paketti ennen kuin itse pakettipolulla on olemassa mitään istuntoa, eikä pysähtymällä ulkoisen hakupalvelun kohdalle.
Jokaisessa WebRTC-istunnossa on jo protokollaan kuuluva reitityskoukku: ICE-käyttäjätunnistefragmentti eli ufrag, lyhyt tunniste, joka vaihdetaan istunnon käyttöönoton aikana ja toistetaan STUN-yhteystarkistuksissa. Luomme palvelinpuolen ufragin siten, että se sisältää juuri riittävästi reititysmetatietoa, jotta rele voi päätellä kohdeklusterin ja omistavan lähetinvastaanottimen.
Signaloinnin aikana lähetinvastaanotin varaa istuntotilan ja palauttaa SDP-vastauksessa jaetun rele-VIP:n ja UDP-portin. VIP on virtuaalinen IP-osoite, joka on relelaivueen edessä; yhdistettynä porttiin se antaa asiakkaalle yhden vakaan kohteen, kuten `203.0.113.10:3478`, vaikka sen takana on monta rele-instanssia. Asiakkaan ensimmäinen mediapolun paketti on yleensä STUN-sidontapyyntö (Session Traversal Utilities for NAT), jota ICE käyttää varmistaakseen, että paketit pääsevät ilmoitettuun osoitteeseen.
Rele jäsentää ensimmäisestä STUN-paketista juuri sen verran, että se voi lukea palvelimen ufragin, purkaa reititysvihjeen ja välittää paketin omistavalle lähetinvastaanottimelle. Kukin lähetinvastaanotin kuuntelee jaettua UDP-socketia, eli yhtä käyttöjärjestelmän endpointia, joka on sidottu sisäiseen IP:porttiin, ei yhtä socketia per istunto. Kun rele on luonut session asiakkaan lähde-IP:portista tuohon lähetinvastaanotinkohteeseen, myöhemmät DTLS-, RTP- ja RTCP-paketit kulkevat session sisällä ilman ufragin uudelleenpurkua.
Releen sessio on tarkoituksella minimaalinen: se koostuu vain muistissa olevasta sessiosta pakettien välityksen ohjaamiseksi sekä tarvittavista laskureista valvontaa varten ja ajastimista session vanhenemista ja siivousta varten. Tämä suunnitteluratkaisu pitää pakettien reitityksen suoraan pakettipolulla. Jos rele käynnistyy uudelleen ja menettää session, seuraava STUN-paketti rakentaa session uudelleen ufragin reititysvihjeestä. Vielä luotettavamman ratkaisun saamiseksi käytämme Redis-välimuistia pitämään yllä <asiakkaan IP + portti, lähetinvastaanottimen IP + portti> -kartoitusta, kun reitti on muodostettu, jotta se voidaan palauttaa paljon aiemmin, ennen seuraavan STUN-paketin saapumista.
Kun olimme pienentäneet julkisen UDP-pinnan pieneen määrään vakaita osoitteita ja portteja, pystyimme ottamaan saman relemallin käyttöön globaalisti. Global Relay on maantieteellisesti hajautettujen releiden laivueemme, joka toteuttaa kaikki saman pakettien välityskäyttäytymisen.
Laaja maantieteellinen sisääntulo lyhentää ensimmäistä asiakas–OpenAI-hyppyä, koska paketti voi tulla verkkoomme käyttäjää lähellä olevan releen kautta sekä maantieteellisesti että verkkotopologian kannalta, sen sijaan että se ylittäisi ensin julkisen internetin kaukaiseen alueeseen. Käytännössä tämä tarkoittaa pienempää viivettä, vähemmän jitteriä ja vähemmän vältettävissä olevia häviöpiikkejä ennen kuin liikenne saavuttaa runkoverkkomme.6
Käytämme signalointiin Cloudflaren maantieteellistä ja läheisyyteen perustuvaa ohjausta, jotta alkuperäinen HTTP- tai WebSocket-pyyntö saavuttaa lähellä olevan lähetinvastaanotinklusterin. Pyynnön konteksti määrittää session sijainnin ja sen, mikä Global Relayn sisääntulopiste ilmoitetaan asiakkaalle. SDP-vastaus tarjoaa Global Relay -osoitteen, kun taas ufrag sisältää riittävästi tietoa, jotta Global Relay voi reitittää median määritettyyn klusteriin ja rele voi reitittää sen kohdelähetinvastaanottimelle.
Yhdessä maantieteellisesti ohjattu signalointi ja Global Relay tuovat sekä käyttöönoton että median läheiselle sisääntuloreitille samalla, kun sessio pysyy ankkuroituna yhteen lähetinvastaanottimeen. Tämä vähentää signaloinnin ja ensimmäisen ICE-yhteystarkistuksen edestakaista aikaa, mikä lyhentää suoraan sitä aikaa, jonka käyttäjä odottaa ennen kuin puhe voi alkaa.
Kirjoitimme relepalvelun Golla ja pidimme toteutuksen tarkoituksella kapeana. Linuxissa ytimen verkkopino vastaanottaa UDP-paketit koneen verkkoliitännästä ja toimittaa ne socketille, käyttöjärjestelmän endpointille, jota prosessi lukee sidottuaan IP:portin. Rele toimii käyttäjätilassa, joten tavallinen Go-prosessi lukee pakettiotsikot tältä socketilta, päivittää pienen määrän virtaustilaa ja välittää paketit päättämättä WebRTC:tä. Emme tarvinneet mitään ytimen ohituskehystä, joka antaisi käyttäjätilan prosessin kysellä verkkojonoja suoraan suurempia pakettinopeuksia varten mutta lisäisi samalla operatiivista monimutkaisuutta.
Keskeiset suunnitteluvalinnat:
- Ei protokollan päättämistä: Rele jäsentää vain STUN-otsikot/ufragin; myöhempiin DTLS-, RTP- ja RTCP-paketteihin se käyttää välimuistissa olevaa tilaa, jolloin paketit pysyvät läpinäkymättöminä.
- Ohimenevä tila: Se ylläpitää pientä, lyhyen aikakatkaisun muistissa olevaa karttaa asiakkaan osoitteesta lähetinvastaanottimen kohteeseen virtaustilaa ja havainnoitavuutta varten.
- Vaakasuuntainen skaalautuvuus: Useita rele-instansseja toimii rinnakkain kuormantasaajan takana. Tila ei ole kovaa WebRTC-tilaa, joten uudelleenkäynnistykset aiheuttavat vain minimaalisia liikennekatkoja ja nopean virtausten palautumisen.
Tehokkuustoimenpiteet:
SO_REUSEPORTon Linuxin socket-asetus, joka sallii useiden reletyöntekijöiden samalla koneella sitoa saman UDP-portin. Ydin jakaa sitten saapuvat paketit näiden työntekijöiden kesken, mikä välttää yhden lukusilmukan pullonkaulan.runtime.LockOSThreadkiinnittää jokaisen UDP:tä lukevan goroutinen tiettyyn OS-säikeeseen. YhdistettynäSO_REUSEPORT:iin tämä pyrkii pitämään saman virran paketit (lähde- ja kohde-IP:portti sekä protokolla) samalla CPU-ytimellä, mikä parantaa välimuistin paikallisuutta ja vähentää kontekstinvaihtoja.- Esivaratut puskurit ja minimaalinen kopiointi pitävät jäsentämisen ja allokoinnin kuorman pienenä, jotta Gossa vältetään roskienkeruu.
Tämä toteutus käsitteli globaalia reaaliaikaista medialiikennettämme suhteellisen pienellä relejalanjäljellä, joten pidimme kiinni yksinkertaisemmasta suunnittelusta sen sijaan, että olisimme ottaneet käyttöön ytimen ohituksen.
Tämän arkkitehtuurin avulla voimme käyttää WebRTC-mediaa Kubernetesissa altistamatta tuhansia UDP-portteja. Tämä on tärkeää, koska pienempi ja kiinteä UDP-pinta on helpompi suojata ja kuormantasata, ja se antaa infrastruktuurin skaalautua ilman suurten julkisten porttialueiden varaamista. Kubernetesin paremman infrastruktuurituen ja pienemmän hyökkäyspinta-alan tuoman paremman turvallisuuden ansiosta tämä suunnittelu säilyttää myös asiakkaille standardin WebRTC-käyttäytymisen ja vahvistaa, että SFU:ton suunnittelu oli oikea oletus työkuormallemme. Suurin osa istunnoistamme on pisteestä pisteeseen, viiveherkkiä ja helpommin skaalattavia, kun inferenssipalvelujen ei tarvitse käyttäytyä kuin WebRTC-vertaisina.
Laajempi opetus on, että monimutkaisuutta kannattaa lisätä ohueen reitityskerrokseen, ei jokaiseen taustapalveluun eikä asiakaskohtaiseen mukautettuun käyttäytymiseen. Reititysmetatietojen koodaaminen protokollan natiiviin kenttään antoi meille deterministisen ensimmäisen paketin reitityksen, pienen julkisen UDP-jalanjäljen ja riittävän joustavuuden sijoittaa sisääntulopisteitä lähelle käyttäjiä ympäri maailmaa.
Muutama valinta oli erityisen tärkeä:
- Säilytä protokollasemantiikka reunalla. Asiakkaat puhuvat edelleen standardia WebRTC:tä, mikä pitää selainten ja mobiilin yhteentoimivuuden ennallaan.
- Pidä vaikea istuntotila yhdessä paikassa. Lähetinvastaanotin omistaa ICE:n, DTLS:n, SRTP:n ja session elinkaaren; rele vain välittää paketit.
- Reititä käyttöönotossa jo olemassa olevan tiedon perusteella. ICE:n ufrag antoi meille ensimmäisen paketin reitityskoukun ilman kuuman polun hakuriippuvuutta.
- Optimoi yleisintä tapausta varten ennen ytimen ohituksen käyttöönottoa. Kapea Go-toteutus, jossa käytettiin huolellisesti
SO_REUSEPORT:ia, säiekiinnitystä ja vähäallokointista jäsentämistä, riitti työkuormallemme.
Reaaliaikainen puhetekoäly toimii vain, kun infrastruktuuri tekee viiveestä käytännössä näkymättömän. Meille tämä tarkoitti WebRTC-käyttöönoton muodon muuttamista muuttamatta sitä, mitä asiakkaat odottavat itse WebRTC:ltä.
Tekijä
Viitteet
2. GitHub - l7mp/stunner: Kubernetesin mediayhdyskäytävä WebRTC:lle(avautuu uudessa ikkunassa)
3. WebRTC-portit pähkinänkuoressa [Esimerkkejä] - BlogGeek.me(avautuu uudessa ikkunassa)
4. Käyttöönotto Kubernetesiin - LiveKit-dokumentaatio(avautuu uudessa ikkunassa)
6. Cloudflare Calls: miljoonia kaskadoituvia puita pohjaan asti(avautuu uudessa ikkunassa)


