Hur OpenAI levererar röst-AI med låg latens i stor skala
Av Yi Zhang och William McDonald, tekniska medarbetare
Röst-AI känns bara naturlig om samtalet sker i samma takt som talet. När nätverket står i vägen upplever människor det direkt som onaturliga pauser, hackiga avbrott eller fördröjda avbrytningar. Det spelar roll för ChatGPT Voice, för utvecklare som bygger med Realtime API, för agenter som arbetar i interaktiva arbetsflöden och för modeller som behöver bearbeta ljud medan en användare fortfarande pratar.
I OpenAI:s skala innebär det tre konkreta krav:
- Global räckvidd för mer än 900 miljoner veckovisa aktiva användare
- Snabb anslutning så att användaren kan börja prata så snart en session startar
- Låg och stabil tur- och returtid (RTT) för media, med låg jitter och paketförlust, så att turtagningen känns responsiv och tydlig.
Teamet på OpenAI som ansvarar för AI-interaktioner i realtid har nyligen byggt om vår WebRTC-stack för att hantera tre begränsningar som började krocka i stor skala: medieterminering med en port per session passar inte väl i OpenAI:s infrastruktur, tillståndsbevarande ICE- (Interactive Connectivity Establishment) och DTLS-sessioner (Datagram Transport Layer Security) kräver stabilt sessionsägarskap, och global routing måste hålla låg latens till första hoppet. I det här inlägget går vi igenom den uppdelade relay- och transceiver-arkitekturen som vi byggde för att bevara standard-WebRTC-beteende för klienter, samtidigt som vi ändrade hur paket dirigeras i OpenAI:s infrastruktur.
WebRTC är en öppen standard för att skicka ljud, video och data mellan webbläsare, mobilappar och servrar. Den förknippas ofta med peer-to-peer-samtal, men är också en praktisk grund för klient–server-system i realtid eftersom den standardiserar de svåra delarna av interaktiva mediesystem: ICE för anslutningsetablering och NAT-traversering (Network Address Translation), DTLS och SRTP (Secure Real-time Transport Protocol) för krypterad transport, codec-förhandling för att komprimera och avkoda ljud, RTCP (Real-time Transport Control Protocol) för kvalitetskontroll samt klientfunktioner som ekoavbrytning och jitterbuffring.
Den här standardiseringen spelar roll för AI-produkter. Utan WebRTC skulle varje klient behöva en egen lösning för hur man etablerar anslutning genom NAT:er, krypterar media, förhandlar codec:ar (de kodare och avkodare som väljs för överföring och dekomprimering) och anpassar sig till förändrade nätverksförhållanden. Med WebRTC kan vi bygga vidare på en protokollstack som redan är implementerad i webbläsare och mobila plattformar, och fokusera vårt eget arbete på infrastrukturen som kopplar realtidsmedia till modellerna.
Vi bygger också vidare på WebRTC-ekosystemet i sig, inklusive mogna open source-implementationer och det standardiseringsarbete som gör att webbläsare, mobilappar och servrar är interoperabla. Grundläggande arbete av Justin Uberti (en av WebRTC:s ursprungliga arkitekter) och Sean DuBois (skapare och underhållare av Pion) gjorde det möjligt för team som vårt att bygga på beprövad mediainfrastruktur i stället för att återuppfinna lågnivåbeteenden för transport, kryptering och congestion control.Vi har turen att både Justin och Sean nu är våra kollegor här på OpenAI och hjälper till att vägleda hur vi för WebRTC och realtids-AI närmare varandra.
För AI är den viktigaste egenskapen att ljudet kommer in som en kontinuerlig ström. En röstagent kan börja transkribera, resonera, anropa verktyg eller generera tal medan användaren fortfarande pratar, i stället för att vänta på att hela ljudet har laddats upp. Det är skillnaden mellan ett system som känns konverserande och ett som känns som push-to-talk (PTT).
När vi väl hade valt WebRTC var nästa fråga var vi skulle terminera det (där vi skulle acceptera och äga WebRTC-anslutningen – till exempel i edge) och hur dessa sessioner skulle kopplas till inference-backend. Terminering spelar roll eftersom den avgör hur vi hanterar sessionsstatus i realtid, medietransport, routing, latens och felisolering.
En SFU, eller selective forwarding unit, är en medieserver som tar emot en WebRTC-ström från varje deltagare och vidarebefordrar utvalda strömmar till de andra. I den här modellen terminerar SFU:n en separat WebRTC-anslutning för varje deltagare, och AI:n ansluter som ytterligare en deltagare i sessionen. Det kan passa bra för produkter som i grunden är multiparty, till exempel gruppsamtal, klassrum eller samarbetsmöten. Det samlar ljud-codec:ar, RTCP-meddelanden, datakanaler, inspelning och policy per ström i en och samma komponent.1
Även i klient-till-AI-produkter är en SFU ofta det vanligaste startvalet eftersom den låter teamet återanvända ett beprövat system för signalering, media routing, inspelning, observerbarhet och framtida funktioner som handoff till människa eller att lägga till fler deltagare.
Vår arbetslast ser annorlunda ut. De flesta sessioner är 1:1 – en användare som pratar med en modell, eller en applikation som pratar med en realtidsagent – med krav på låg latens i varje tur. För den typen av trafik valde vi en transceiver-modell: en WebRTC-edge-tjänst terminerar klientanslutningen och konverterar sedan media och events till enklare interna protokoll för inference, transkribering, talgenerering, verktygsanrop och orkestrering.
I den här designen är transceivern den enda tjänsten som äger WebRTC-sessionens state, inklusive ICE connectivity checks, DTLS-handshake, SRTP-krypteringsnycklar och sessionens livscykel. ”Terminering” här betyder att transceivern är slutpunkten som slutför dessa handshakes och krypterar eller dekrypterar media. Att hålla detta state på ett ställe gjorde sessionsägarskap enklare att resonera kring, och det gjorde att backend-tjänster kunde skala som vanliga tjänster i stället för att själva agera WebRTC-peers.
Efter att ha valt transceiver-modellen var vår första implementation en enda Go-tjänst byggd på Pion som hanterade både signalering och medieterminering. Den driver ChatGPT Voice, Realtime API:s WebRTC-slutpunkt och flera forskningsprojekt.
Operativt utför transceivertjänsten två uppgifter:
- Signalering: SDP-förhandling, codec-val, ICE-credentials och sessionsetablering
- Media: terminering av nedströms WebRTC-anslutningar och underhåll av uppströmsanslutningar till backend-tjänster för inferens och orkestrering
Vi ville att tjänsten skulle köras som resten av vår infrastruktur: på Kubernetes, där arbetslaster kan skala upp och ned och flyttas mellan noder när efterfrågan förändras. Men den konventionella WebRTC-modellen med en port per session passar dåligt för den miljön, eftersom den är beroende av stora publika UDP-portintervall som är svåra att exponera, säkra och bevara när poddar läggs till, tas bort eller schemaläggs om.2
Det första problemet var själva modellen med en port per session. Vid hög samtidighet innebär det att mycket stora UDP-portintervall måste exponeras och hanteras.
- Molnbaserade lastbalanserare och Kubernetes-tjänster är inte utformade för tiotusentals publika UDP-portar per tjänst. Varje ytterligare intervall ökar den operativa komplexiteten i konfiguration av lastbalanserare, hälsokontroller, brandväggspolicyer och säkra utrullningar.3
- Stora UDP-portintervall är svåra att säkra eftersom de utökar den externt exponerade attackytan och gör nätverkspolicyer svårare att granska.
- De passar också dåligt för autoskalning. Poddar läggs ständigt till, tas bort och rescheduleras i Kubernetes. Att kräva att varje pod reserverar och exponerar ett stort stabilt portintervall gör elasticiteten mindre robust.4
Det är därför många WebRTC-system går mot en UDP-port per server, med applikationsnivå-demultiplexering bakom den porten.5
Designer med en port per server löser portantalet, men de introducerar ett andra problem: att bibehålla ägarskapet för varje session över en flotta.
ICE och DTLS är stateful protokoll. Processen som skapade en session måste fortsätta att ta emot den sessionens paket så att den kan validera ICE connectivity checks, slutföra DTLS-handskakningen, dekryptera SRTP och hantera senare sessionsändringar som ICE-omstarter. Om paket för samma session landar i en annan process kan session setup misslyckas eller så kan media brytas.
Det gav oss ett specifikt mål: att exponera en liten, fast UDP-exponering mot det publika internet, samtidigt som varje paket fortfarande dirigeras till den transceiver som äger motsvarande WebRTC-session.
Vi utvärderade flera sätt att nå dit, inklusive TURN (Traversal Using Relays around NAT), där ett edge relay terminerar klienternas allokeringar och vidarebefordrar trafik på deras vägnar.2
Metod | Fördelar | Nackdelar |
Unik IP:port per session (även kallat native direct UDP) | Direkt medieväg mellan klient och server Inget vidarebefordringslager i datapathen | Kräver en publik UDP-port per session Stora portintervall är svåra att exponera och säkra Passar dåligt för Kubernetes och molnlastbalanserare |
Unik IP:port per server | Betydligt mindre publik UDP-exponering än per session En delad socket per server kan demultiplexa många sessioner | Fungerar bra på en enskild värd, men inte över en delad lastbalanserad flotta på egen hand Sessionsdemultiplexering på en enskild värd hjälper bara efter att ett paket nått den värden; i en lastbalanserad flotta kan det första paketet fortfarande hamna på fel instans, så du behöver fortfarande ett deterministiskt sätt att styra varje session till processen som äger den |
TURN-relay (protokollterminerande) | Klienter behöver bara nå adressen och porten för TURN-relay Kan centralisera policy vid edge | TURN-allokeringar lägger till extra tur- och returer vid etablering Det är fortfarande svårt att flytta eller återställa allokeringar mellan TURN-servrar |
Stateless forwarder + stateful terminator (OpenAI:s relay + transceiver) | Liten publik UDP-exponering Transceivern äger fortfarande hela WebRTC-sessionen | Lägger till ett extra vidarebefordringshopp innan media når den transceiver som äger sessionen Kräver samordning mellan relay och transceiver |
Arkitekturen vi lanserade delar upp paketdirigering från protokollterminering. Signalering når fortfarande transceivern för sessionsetablering, medan media går in via relay först. Relay är ett lättviktigt UDP-forwarding layer med ett litet publikt fotavtryck, och transceivern är den stateful WebRTC-slutpunkten bakom det.
Relay dekrypterar inte media, kör inte ICE-tillståndsmaskiner och deltar inte i codec-förhandling. Den läser tillräckligt med paketmetadata för att välja en destination och vidarebefordrar sedan paketen till den transceiver som äger sessionen. Transceivern ser fortfarande ett normalt WebRTC-flöde och äger fortfarande allt protokolltillstånd. Ur klientens perspektiv förändras ingenting i WebRTC-sessionen.
Routning av första paketet är nyckelsteget i detta upplägg. Ett relay måste routa det första paketet från en klient innan någon session finns i själva datapathen, i stället för att förlita sig på en extern uppslagstjänst.
Varje WebRTC-session bär redan på en protokollinbyggd routing-krok: ICE-ufrag (username fragment), en kort identifierare som utbyts under sessionsetableringen och ekar i STUN connectivity checks. Vi genererar server-side ufrag så att det innehåller tillräckligt med routing-metadata för att relay ska kunna härleda destinationsklustret och den ägande transceivern.
Vid signaleringen allokerar transceivern sessionstillstånd och returnerar en delad relay-VIP och UDP-port i SDP-svaret. En VIP är en virtuell IP-adress framför relay-flottan; tillsammans med porten ger den klienten en enda stabil destination, till exempel 203.0.113.10:3478, även om många relay-instanser ligger bakom den. Klientens första paket på datapathen är vanligtvis en STUN-bindningsbegäran (Session Traversal Utilities for NAT), som ICE använder för att verifiera att paket kan nå den annonserade adressen.
Relay tolkar precis tillräckligt av det första STUN-paketet för att läsa serverns ufrag, avkoda routningsledtråden och vidarebefordra paketet till den ägande transceivern. Varje transceiver lyssnar på en delad UDP-socket, vilket innebär en operativsystemsslutpunkt bunden till en intern IP:port, inte en socket per session. Efter att relay skapat en session från klientens käll-IP:port till den transceiver-destinationen flödar efterföljande DTLS-, RTP- och RTCP-paket inom sessionen utan att ufrag behöver avkodas på nytt.
Sessionen i relay är avsiktligt minimal och består endast av en session i minnet för att styra paketvidarebefordran, tillsammans med nödvändiga räknare för övervakning och tidsutlösare för sessionens utgång och rensning. Detta designval håller paketroutning direkt i datapathen. Om ett relay startar om och förlorar sessionen bygger nästa STUN-paket upp sessionen igen från routing-hinten i ufrag. För att göra det ännu mer tillförlitligt används en Redis-cache för att lagra mappningen av <klient-IP + port, transceiver-IP + port> när rutten väl har etablerats så att den kan återställas tidigare, innan nästa STUN-paket kommer fram.
När vi hade reducerat den publika UDP-exponeringen till ett litet antal stabila adresser och portar kunde vi rulla ut samma relay-mönster globalt. Global Relay är vår flotta av geografiskt distribuerade relay-ingresspunkter som alla implementerar samma paketvidarebefordringsbeteende.
Bred geografisk ingressnärvaro förkortar det första hoppet mellan klienten och OpenAI eftersom ett paket kan gå in i vårt nätverk via ett relay nära användaren, både geografiskt och topologiskt, i stället för att först korsa det publika internet till en avlägsen region. I praktiken innebär det lägre latens, mindre jitter och färre undvikbara förlustspikar innan trafiken når vårt backbone.6
Vi använder geo- och närhetsstyrning via Cloudflare för signalering så att den initiala HTTP- eller WebSocket-begäran når ett närliggande transceiver-kluster. Kontexten för begäran avgör sessionens plats och vilken Global Relay-ingresspunkt som annonseras till klienten. SDP-svaret anger Global Relay-adressen, medan ufrag innehåller tillräcklig information för att Global Relay ska kunna routa media till det avsedda klustret och relay till den avsedda transceivern.
Tillsammans placerar geostyrd signalering och Global Relay både etablering och media på en närliggande ingressväg, samtidigt som sessionen förblir förankrad till en och samma transceiver. Det minskar tur- och returtiden (RTT) för signalering och för den första ICE connectivity check, vilket direkt förkortar hur länge en användare väntar innan hen kan börja tala.
Vi skrev relay-tjänsten i Go och höll medvetet implementationen begränsad. På Linux tar kärnans nätverksstack emot UDP-paket från maskinens nätverksinterface och levererar dem till en socket, en endpoint i operativsystemet som en process läser från efter att ha bundit till en IP:port. Relay körs i användarrymden, så en vanlig Go-process läser pakethuvuden från den socketen, uppdaterar en liten mängd flödestillstånd och vidarebefordrar paket utan att terminera WebRTC. Vi behövde inget kernel-bypass-ramverk, vilket skulle låta en process i användarrymden poll:a nätverksköer direkt för högre pakethastigheter men också öka driftskomplexiteten.
Viktiga val i designen:
- Ingen protokollterminering: Relay läser endast STUN-huvuden/ufrag; för efterföljande DTLS, RTP och RTCP använder den cachelagrat tillstånd och behandlar paketen som opaka.
- Flyktigt tillstånd: Den har en liten mappning i minnet med kort livslängd mellan klientadress och transceiver-destination för flödestillstånd och observerbarhet.
- Horisontell skalbarhet: Flera relay-instanser kör parallellt bakom en lastbalanserare. Tillståndet är inte kritiskt WebRTC-tillstånd, så omstarter orsakar minimala trafikavbrott och snabb återhämtning av trafikflöden.
Effektivitetsåtgärder:
SO_REUSEPORTär ett socketalternativ i Linux som gör att flera relay-processer på samma maskin kan binda samma UDP-port. Kärnan fördelar sedan inkommande paket mellan dessa processer, vilket undviker en flaskhals i en enda läsloop.runtime.LockOSThreadbinder varje goroutine som läser UDP till en specifik OS-tråd. I kombination medSO_REUSEPORTtenderar det att hålla paket från samma flöde (käll- och destinations-IP:portar samt protokoll) på samma CPU-kärna, vilket förbättrar cachelokalitet och minskar kontextväxling.- Förallokerade buffertar och minimal kopiering håller parsing och allokeringskostnader låga för att minimera garbage collection i Go.
Denna implementation hanterade vår globala medietrafik i realtid med en relativt liten relay-footprint, så vi behöll den enklare designen i stället för att gå över till en kernel-bypass-lösning.
Den här arkitekturen låter oss köra WebRTC-media i Kubernetes utan att exponera tusentals UDP-portar. Det är viktigt eftersom en mindre och fast UDP-exponering är enklare att säkra och lastbalansera och gör att infrastrukturen kan skala utan att reservera stora publika portintervall. Med bättre infrastrukturstöd från Kubernetes och högre säkerhet tack vare mindre exponerad yta bevarar denna design också standard WebRTC-beteende för klienter och visar att en SFU-fri design var rätt standardval för vår arbetslast. De flesta av våra sessioner är punkt-till-punkt, latenskänsliga och enklare att skala när inferenstjänster inte behöver bete sig som WebRTC-peers.
Den bredare lärdomen är att det bästa stället att lägga till komplexitet är i ett tunt routningslager, inte i varje backend-tjänst och inte i anpassat klientbeteende. Genom att koda in routningsmetadata i ett protokollinbyggt fält fick vi deterministisk routning av första paketet, en liten publik UDP-exponering och tillräcklig flexibilitet för att placera ingress nära användarna runt om i världen.
Några val var särskilt viktiga:
- Bevara protokollsemantik vid edge. Klienter använder fortfarande standard-WebRTC, vilket håller interoperabiliteten mellan webbläsare och mobila enheter intakt.
- Behåll kritiskt sessionstillstånd på ett ställe. Transceivern äger ICE, DTLS, SRTP och sessionens livscykel; relay vidarebefordrar endast paket.
- Routa baserat på information som redan finns i etableringen. ICE-ufrag gav oss en routing-krok för första paketet utan att införa ett uppslagsberoende i hot path.
- Optimera för det vanligaste fallet innan du tar till kernel-bypass. En begränsad Go-implementation med noggrann användning av
SO_REUSEPORT, trådbindning och parsing med låg allokering räckte för vår arbetslast.
Röst-AI i realtid fungerar bara när infrastrukturen får latens att kännas osynlig. För oss innebar det att ändra hur vår WebRTC-distribution är utformad utan att ändra vad klienter förväntar sig av WebRTC.
Författare
Referenser
2. GitHub - l7mp/stunner: A Kubernetes media gateway for WebRTC(öppnas i ett nytt fönster)
3. WebRTC Ports in a nutshell [Examples] - BlogGeek.me(öppnas i ett nytt fönster)
4. Deploy to Kubernetes - LiveKit docs(öppnas i ett nytt fönster)
6. Cloudflare Calls: millions of cascading trees all the way down(öppnas i ett nytt fönster)


