پرش به محتوای اصلی
OpenAI

۱۴ اردیبهشت ۱۴۰۵

مهندسی

OpenAI چگونه AI صوتی کم‌تأخیر را در مقیاس بزرگ ارائه می‌کند

نوشته‌ی Yi Zhang و William McDonald، اعضای کادر فنی

AI سخنگو فقط زمانی طبیعی به نظر می‌رسد که مکالمه با سرعت گفتار پیش برود. وقتی شبکه مانع ایجاد می‌کند، افراد بلافاصله آن را به شکل مکث‌های ناخوشایند، قطع‌شدن‌های ناگهانی، یا تأخیر در قطع‌کردن صحبت و ورود به مکالمه احساس می‌کنند. این موضوع برای ChatGPT سخنگو، برای توسعه‌دهندگانی که با Realtime API کار می‌کنند، برای عامل‌هایی که در گردش‌کارهای تعاملی فعالیت دارند، و برای مدل‌هایی که باید صدا را در حالی پردازش کنند که کاربر هنوز در حال صحبت است، اهمیت دارد.

در مقیاس OpenAI، این موضوع به سه نیاز مشخص تبدیل می‌شود:

  • پوشش جهانی برای بیش از ۹۰۰ میلیون کاربر فعال هفتگی
  • برقراری سریع اتصال تا کاربر به‌محض شروع نشست بتواند صحبت کند
  • زمان رفت‌وبرگشت رسانه‌ای کم و پایدار، با نوسان تأخیر و از دست رفتن بسته‌ی کم، تا نوبت‌گیری در گفتگو روان بماند

تیم OpenAI که مسئول تعاملات بلادرنگ AI است، اخیراً پشته‌ی WebRTC ما را بازطراحی کرده تا به سه محدودیتی بپردازد که در مقیاس گسترده شروع به تداخل با یکدیگر کرده بودند: خاتمه‌ی رسانه با مدل یک پورت برای هر نشست با زیرساخت OpenAI سازگار نیست، نشست‌های حالت‌مند ICE (Interactive Connectivity Establishment / برقراری تعاملی اتصال) و DTLS (Datagram Transport Layer Security / امنیت لایه انتقال دیتاگرام) به مالکیت پایدار نیاز دارند، و مسیریابی جهانی باید تأخیر اولین پرش را پایین نگه دارد. در این مطلب، معماری تفکیک‌شده‌ی رله به‌همراه فرستنده‌ـ‌گیرنده را مرور می‌کنیم که آن را ساختیم تا رفتار استاندارد WebRTC برای کلاینت‌ها حفظ شود، در حالی که نحوه‌ی مسیریابی بسته‌ها در داخل زیرساخت OpenAI تغییر می‌کند.

WebRTC به ما امکان می‌دهد محصولات بلادرنگ AI بسازیم

WebRTC یک استاندارد باز برای ارسال صدا، ویدئو و داده با تأخیر پایین میان مرورگرها، اپلیکیشن‌های موبایل و سرورها است. این فناوری اغلب با تماس‌های همتا‌به‌همتا شناخته می‌شود، اما برای سامانه‌های بلادرنگِ کلاینت‌به‌سرور نیز یک پایه‌ی عملی محسوب می‌شود؛ زیرا بخش‌های دشوار رسانه‌های تعاملی را استاندارد می‌کند: ICE برای برقراری اتصال و عبور از NAT (Network Address Translation / ترجمه‌ی نشانی شبکه)، DTLS و SRTP (Secure Real-time Transport Protocol / پروتکل امن انتقال بلادرنگ) برای انتقال رمزگذاری‌شده، مذاکره‌ی کُدِک برای فشرده‌سازی و رمزگشایی صدا، RTCP (Real-time Transport Control Protocol / پروتکل کنترل انتقال بلادرنگ) برای کنترل کیفیت، و قابلیت‌های سمت کلاینت مانند حذف پژواک و بافر کردن نوسان تأخیر.

این استانداردسازی برای محصولات AI اهمیت دارد. بدون WebRTC، هر کلاینت برای برقراری اتصال از میان NATها، رمزگذاری رسانه، مذاکره‌ی کُدِک‌ها (یعنی کُدگذارها-کُدگشاهایی که برای انتقال و از حالت فشرده خارج کردن داده انتخاب می‌شوند)، و سازگاری با شرایط متغیر شبکه، به راهکار متفاوتی نیاز داشت. با WebRTC، می‌توانیم بر پشته‌ی پروتکلی تکیه کنیم که از پیش در مرورگرها و پلتفرم‌های موبایل پیاده‌سازی شده است و کار خودمان را بر زیرساختی متمرکز کنیم که رسانه‌ی بلادرنگ را به مدل‌ها متصل می‌کند.

ما همچنین بر خودِ اکوسیستم WebRTC نیز تکیه می‌کنیم؛ از جمله پیاده‌سازی‌های متن‌بازِ تثبیت‌شده و کارهای استانداردسازی‌ای که باعث می‌شوند مرورگرها، اپلیکیشن‌های موبایل و سرورها با یکدیگر سازگار و قابل‌تعامل باقی بمانند. کارهای بنیادین Justin Uberti، یکی از معماران اولیه‌ی WebRTC، و Sean DuBois، خالق و نگه‌دارنده‌ی Pion، این امکان را برای تیم‌هایی مانند ما فراهم کرد که به‌جای بازاختراع سازوکارهای سطح پایینِ انتقال، رمزگذاری و کنترل ازدحام، بر زیرساخت رسانه‌ای آزموده‌شده و قابل‌اعتماد بنا کنند. خوش‌شانسیم که اکنون هم Justin و هم Sean همکاران ما در OpenAI هستند و کمک می‌کنند مسیر نزدیک‌تر کردن WebRTC و AI بلادرنگ را هدایت کنیم.

برای AI، مهم‌ترین ویژگی این است که صدا به‌صورت یک جریان پیوسته دریافت می‌شود. یک عامل صوتی می‌تواند در حالی که کاربر هنوز در حال صحبت است، رونویسی، استدلال، فراخوانی ابزارها یا تولید گفتار را آغاز کند، به‌جای آنکه منتظر بماند کل فایل بارگذاری شود. همین تفاوت است که باعث می‌شود یک سامانه حس مکالمه‌ی طبیعی داشته باشد، نه حس ارتباط دکمه‌ای و نوبتی.

انتخاب معماری رسانه

پس از آنکه WebRTC را انتخاب کردیم، پرسش بعدی این بود که اتصال آن را در کجا خاتمه دهیم (یعنی کجا اتصال WebRTC را بپذیریم و مالکیت آن را بر عهده بگیریم،—برای مثال در نقطه ورودی‌ی شبکه)، و چگونه این نشست‌ها را به بک‌اند استنتاج متصل کنیم. محل خاتمه‌ی اتصال اهمیت دارد، چون تعیین می‌کند وضعیت نشست‌های بلادرنگ، انتقال رسانه، مسیریابی، تأخیر و جداسازی خطاها را چگونه مدیریت کنیم.

گزینه‌ی ۱: رویکرد SFU، AI را به‌عنوان یکی از شرکت‌کنندگان WebRTC در نظر می‌گیرد

یک SFU یا واحد ارسال انتخابی، سرور رسانه‌ای‌ای است که از هر شرکت‌کننده یک جریان WebRTC دریافت می‌کند و جریان‌ها را به‌صورت انتخابی برای دیگران ارسال می‌کند. در این مدل، SFU برای هر شرکت‌کننده یک اتصال WebRTC جداگانه را خاتمه می‌دهد و AI نیز به‌عنوان یک شرکت‌کننده‌ی دیگر وارد نشست می‌شود. این معماری می‌تواند برای محصولاتی که ذاتاً چندنفره هستند، مانند تماس‌های گروهی، کلاس‌ها یا جلسات مشارکتی، مناسب باشد. همچنین کُدِک‌های صوتی، پیام‌های RTCP، کانال‌های داده، ضبط و سیاست‌گذاریِ مخصوص هر جریان را در یک نقطه متمرکز نگه می‌دارد.۱

حتی در محصولات کلاینت‌به‌هوش‌مصنوعی نیز SFU اغلب نقطه‌ی شروع پیش‌فرض است، زیرا به تیم‌ها اجازه می‌دهد از یک سامانه‌ی آزموده‌شده برای سیگنالینگ، مسیریابی رسانه، ضبط، مشاهده‌پذیری و توسعه‌های آینده، مانند واگذاری مکالمه به انسان یا افزودن شرکت‌کنندگان بیشتر، دوباره استفاده کنند.

گزینه‌ی ۲: رویکرد فرستنده‌ـ‌گیرنده، WebRTC را در نقطه‌ی ورودی شبکه خاتمه می‌دهد و آن را به یک پروتکل بک‌اند تبدیل می‌کند

بار کاری ما متفاوت است. بیشتر نشست‌ها یک‌به‌یک هستند؛ یک کاربر با یک مدل صحبت می‌کند، یا یک اپلیکیشن با یک عامل بلادرنگ در ارتباط است، در حالی که هر نوبت از مکالمه به تأخیر بسیار حساس است. برای این الگوی ترافیک، ما مدل فرستنده‌ـ‌گیرنده را انتخاب کردیم: یک سرویس نقطه ورودی WebRTC اتصال کلاینت را خاتمه می‌دهد و سپس رسانه و رویدادها را به پروتکل‌های داخلی ساده‌تری برای استنتاج مدل، رونویسی، تولید گفتار، استفاده از ابزارها و هماهنگ‌سازی فرایندها تبدیل می‌کند.

در این طراحی، فرستنده‌ـ‌گیرنده تنها سرویسی است که مالک وضعیت نشست WebRTC است؛ از جمله بررسی‌های اتصال ICE، تبادل اولیه‌ی DTLS، کلیدهای رمزگذاری SRTP و چرخه‌ی عمر نشست. منظور از «خاتمه» در اینجا این است که فرستنده‌ـ‌گیرنده همان نقطه‌ی پایانی‌ای است که این تبادل اولیه‌ها را کامل می‌کند و رسانه را رمزگذاری یا رمزگشایی می‌کند. نگه‌داشتن این وضعیت در یک نقطه باعث شد درک و مدیریت مالکیت نشست ساده‌تر شود و به سرویس‌های بک‌اند اجازه داد مانند سرویس‌های معمولی مقیاس‌پذیر شوند، نه اینکه خودشان نقش همتای WebRTC را ایفا کنند.

مسئله‌ی اصلی استقرار: WebRTC در برابر Kubernetes

پس از انتخاب مدل فرستنده‌ـ‌گیرنده، نخستین پیاده‌سازی ما یک سرویس واحد Go مبتنی بر Pion بود که هم سیگنالینگ را مدیریت می‌کرد و هم خاتمه‌ی رسانه را بر عهده داشت. این سرویس ChatGPT سخنگو، نقطه‌ی پایانی WebRTC در Realtime API و تعدادی از پروژه‌های پژوهشی را پشتیبانی می‌کند.

از نظر عملیاتی، سرویس فرستنده‌ـ‌گیرنده دو وظیفه انجام می‌دهد:

  • سیگنالینگ: مذاکره‌ی SDP، انتخاب کُدِک، اطلاعات احراز اتصال ICE و راه‌اندازی نشست
  • رسانه: خاتمه‌دادن به اتصال‌های WebRTC در مسیر پایین‌دست و حفظ اتصال‌های بالادست به سرویس‌های بک‌اند برای استنتاج و هماهنگ‌سازی

ما می‌خواستیم این سرویس مانند سایر بخش‌های زیرساخت‌مان اجرا شود: روی Kubernetes، جایی که بارهای کاری می‌توانند با تغییر تقاضا افزایش یا کاهش یابند و میان میزبان‌ها جابه‌جا شوند. اما مدل مرسوم WebRTC با یک پورت برای هر نشست با چنین محیطی سازگاری کمی دارد، چون به بازه‌های بزرگی از پورت‌های عمومی UDP وابسته است که در معرض‌قراردادن، ایمن‌سازی و حفظ آن‌ها هنگام اضافه‌شدن، حذف‌شدن یا زمان‌بندیِ مجدد پادها دشوار است.۲

اتمام پورت‌ها

نخستین مشکل، خودِ مدل یک پورت برای هر نشست بود. در هم‌زمانی بالا، این یعنی باید بازه‌های بسیار بزرگی از پورت‌های UDP را در معرض شبکه قرار داد و مدیریت کرد.

  • متوازن کننده‌های بار ابری و سرویس‌های Kubernetes برای ده‌ها هزار پورت عمومی UDP به‌ازای هر سرویس طراحی نشده‌اند. هر بازه‌ی اضافی، پیچیدگی عملیاتی بیشتری به پیکربندی متوازن کننده بار، بررسی سلامت، سیاست‌های فایروال و ایمنی انتشار اضافه می‌کند.۳
  • بازه‌های بزرگ پورت‌های UDP به‌سختی ایمن می‌شوند، زیرا سطح قابل‌دسترسی از بیرون را گسترش می‌دهند و ممیزی سیاست‌های شبکه را دشوارتر می‌کنند.
  • همچنین این مدل برای مقیاس‌گذاری خودکار مناسب نیست. در Kubernetes پادها دائماً اضافه، حذف و زمان‌بندی دوباره می‌شوند. اینکه هر پاد مجبور باشد یک بازه‌ی بزرگ و پایدار از پورت‌ها را رزرو و اعلان کند، این انعطاف‌پذیری را شکننده می‌کند.۴

به همین دلیل است که بسیاری از سامانه‌های WebRTC به سمت یک پورت UDP واحد برای هر سرور با چندگانه‌سازی در سطح برنامه پشت آن پورت حرکت می‌کنند.۵

چسبندگی وضعیت

طراحی‌های یک پورت برای هر سرور مشکل تعداد پورت‌ها را حل می‌کنند، اما مشکل دومی ایجاد می‌کنند: حفظ مالکیت هر نشست در میان مجموعه‌ای از سرورها.

ICE و DTLS پروتکل‌هایی حالت‌مند هستند. پردازه‌ای که یک نشست را ایجاد کرده است باید همچنان بسته‌های همان نشست را دریافت کند تا بتواند بررسی‌های اتصال را اعتبارسنجی کند، فرایند برقراری اتصال DTLS را کامل کند، SRTP را رمزگشایی کند و تغییرات بعدی نشست، مانند راه‌اندازی مجدد ICE، را پردازش کند. اگر بسته‌های مربوط به همان نشست به پردازه‌ی دیگری برسند، راه‌اندازی اتصال ممکن است شکست بخورد یا انتقال رسانه دچار اختلال شود.

این موضوع هدفی مشخص به ما داد: در عین ارائه‌ی سطحی کوچک و ثابت از UDP به اینترنت عمومی، هر بسته را همچنان به فرستنده‌-‌گیرنده‌ای مسیریابی کنیم که مالک نشست متناظر WebRTC است.

مقایسه‌ی معماری‌های رسانه‌ای WebRTC

ما چندین راهکار را برای رسیدن به این هدف ارزیابی کردیم؛ از جمله TURN (Traversal Using Relays around NAT / عبور با استفاده از رله‌ها پیرامون NAT)، که در آن یک رله‌ی نقطه ورودی، تخصیص‌های کلاینت را خاتمه می‌دهد و ترافیک را از طرف آن‌ها ارسال می‌کند.۲

رویکرد

مزایا

معایب

IP:port یکتا برای هر نشست (که با نام direct UDP بومی هم شناخته می‌شود)

مسیر مستقیم رسانه از کلاینت به سرور

بدون لایه‌ی ارسال در مسیر داده

به یک پورت UDP عمومی برای هر نشست نیاز دارد

نمایش و ایمن‌سازی بازه‌های بزرگ پورت دشوار است

برای Kubernetes و متوازن کننده‌های بار ابری مناسب نیست

IP:port یکتا برای هر سرور

ردپای عمومی UDP بسیار کوچک‌تر از حالت نمایش به‌ازای هر نشست

یک سوکت مشترک برای هر سرور می‌تواند چندین نشست را چندگانه‌سازی کند

روی یک میزبان واحد به‌خوبی کار می‌کند، اما به‌تنهایی در میان مجموعه‌ای از سرورهای مشترکِ پشت متوازن‌کننده‌ی بار کافی نیست

جداسازی نشست‌ها روی یک میزبان واحد فقط پس از رسیدن بسته به همان میزبان مفید است؛ در میان مجموعه‌ای از سرورهای پشت متوازن‌کننده‌ی بار، نخستین بسته همچنان ممکن است به نمونه‌ی اشتباه برسد بنابراین هنوز به روشی قطعی نیاز دارید تا هر نشست را به پردازه‌ای هدایت کنید که مالک آن است


رله‌ی TURN (با خاتمه پروتکل)

کلاینت‌ها فقط باید به آدرس و پورت رله‌ی TURN دسترسی داشته باشند

می‌توان سیاست‌گذاری را در نقطه ورودی متمرکز کرد

تخصیص‌های TURN باعث اضافه‌شدن رفت‌وبرگشت‌های بیشتر در مرحله‌ی راه‌اندازی می‌شوند

انتقال یا بازیابی این تخصیص‌ها میان سرورهای TURN همچنان دشوار است

ارسال‌کننده‌ی بدون‌حالت + خاتمه‌دهنده‌ی حالت‌مند (رله + فرستنده‌ـ‌گیرنده‌ی OpenAI)

ردپای عمومی UDP کوچک

فرستنده‌-‌گیرنده همچنان مالک کل نشست WebRTC است

پیش از رسیدن رسانه به فرستنده‌-‌گیرنده مالک نشست، یک گام ارسال اضافه می‌کند

به هماهنگی سفارشی میان رله و فرستنده‌-‌گیرنده نیاز دارد

نمای کلی معماری: رله + فرستنده‌-‌گیرنده

معماری‌ای که ارائه کردیم، مسیریابی بسته‌ها را از خاتمه‌ی پروتکل جدا می‌کند. سیگنالینگ همچنان برای راه‌اندازی نشست به فرستنده‌ـ‌گیرنده می‌رسد، اما رسانه ابتدا از طریق رله وارد می‌شود. رله یک لایه‌ی سبک برای ارسال UDP با ردپای عمومی کوچک است و فرستنده‌ـ‌گیرنده، نقطه‌ی پایانی حالت‌مند WebRTC در پشت آن محسوب می‌شود.

Relay بسته‌ها را بدون نگه‌داری وضعیت به فرستنده‌-‌گیرنده ارسال می‌کند

رله رسانه را رمزگشایی نمی‌کند، ماشین‌های حالت ICE را اجرا نمی‌کند و در مذاکره‌ی کُدِک مشارکت ندارد. فقط به‌اندازه‌ای فراداده‌ی بسته را می‌خواند که بتواند مقصد را انتخاب کند، سپس بسته را به فرستنده‌ـ‌گیرنده‌ای ارسال می‌کند که مالک آن نشست است. فرستنده‌ـ‌گیرنده همچنان یک جریان عادی WebRTC را می‌بیند و همچنان مالک تمام وضعیت پروتکل است. از دید کلاینت، هیچ تغییری در نشست WebRTC رخ نمی‌دهد.

مسیریابی بر اساس اعتبارنامه‌های ICE

مسیریابی نخستین بسته، گام کلیدی در این سازوکار است. رله باید نخستین بسته‌ی دریافتی از کلاینت را پیش از آنکه هیچ نشستی در مسیر بسته شکل گرفته باشد، بر اساس خودِ مسیر بسته مسیریابی کند، نه با توقف برای مراجعه به یک سرویس جست‌وجوی خارجی.

هر نشست WebRTC از قبل یک سازوکار مسیریابی بومیِ خودِ پروتکل را همراه دارد: قطعه‌ی نام کاربری ICE یا ufrag؛ شناسه‌ای کوتاه که هنگام راه‌اندازی نشست تبادل می‌شود و در بررسی‌های اتصال STUN بازتاب داده می‌شود. ما ufrag سمت سرور را طوری تولید می‌کنیم که فقط به اندازه‌ی لازم، فراداده‌ی مسیریابی در خود داشته باشد تا رله بتواند خوشه مقصد و فرستنده‌ـ‌گیرنده‌ی مالک نشست را استنباط کند.

نمودار توالی نشان می‌دهد اتصال چگونه برقرار می‌شود

در مرحله‌ی سیگنالینگ، فرستنده‌ـ‌گیرنده وضعیت نشست را اختصاص می‌دهد و در پاسخ SDP، یک VIP مشترک برای رله به‌همراه پورت UDP برمی‌گرداند. VIP یک نشانی IP مجازی است که در جلوی مجموعه‌ای از رله‌ها قرار می‌گیرد؛ این نشانی در کنار پورت، یک مقصد واحد و پایدار مانند «۲۰۳.۰.۱۱۳.۱۰:۳۴۷۸» در اختیار کلاینت می‌گذارد، حتی اگر نمونه‌های زیادی از رله پشت آن قرار داشته باشند. نخستین بسته‌ی کلاینت در مسیر رسانه معمولاً یک درخواست اتصال STUN‏ (Session Traversal Utilities for NAT / ابزارهای عبور نشست از NAT) است که ICE از آن برای تأیید این موضوع استفاده می‌کند که بسته‌ها می‌توانند به نشانی اعلام‌شده برسند.

رله فقط به‌اندازه‌ای از نخستین بسته‌ی STUN را پردازش می‌کند که بتواند ufrag سمت سرور را بخواند، نشانه‌ی مسیریابی را استخراج کند و بسته را به فرستنده‌ـ‌گیرنده‌ی مالک نشست ارسال کند. هر فرستنده‌ـ‌گیرنده روی یک سوکت UDP مشترک گوش می‌دهد؛ یعنی یک نقطه‌ی پایانی در سطح سیستم‌عامل که به یک IP و پورت داخلی متصل است، نه یک سوکت جداگانه برای هر نشست. پس از آنکه رله یک نشست را از IP و پورت مبدأِ کلاینت به مقصدِ فرستنده‌ـ‌گیرنده ایجاد کرد، بسته‌های بعدیِ DTLS، RTP و RTCP بدون نیاز به استخراج دوباره‌ی ufrag در همان نشست جریان پیدا می‌کنند.

نشستِ رله عمداً حداقلی است و فقط از یک نشست درون‌حافظه‌ای برای هدایت ارسال بسته‌ها تشکیل می‌شود، به‌همراه شمارنده‌های لازم برای پایش و تایمرهایی برای انقضای نشست و پاک‌سازی آن. این انتخاب طراحی باعث می‌شود مسیریابی بسته‌ها مستقیماً روی مسیر خودِ بسته باقی بماند. اگر یک رله دوباره راه‌اندازی شود و نشست را از دست بدهد، بسته‌ی STUN بعدی نشست را از روی نشانه‌ی مسیریابیِ موجود در ufrag دوباره می‌سازد. برای قابل‌اعتمادتر شدن این فرایند، از کش Redis استفاده می‌شود تا پس از برقرار شدن مسیر، نگاشتِ <client IP + Port, transceiver IP + Port> را نگه دارد و بتوان آن را خیلی زودتر، پیش از رسیدن بسته‌ی STUN بعدی، بازیابی کرد.

Global Relay و سیگنالینگِ هدایت‌شده بر اساس موقعیت جغرافیایی

پس از آنکه سطح عمومی UDP را به تعداد کمی نشانی و پورت پایدار کاهش دادیم، توانستیم همین الگوی رله را در سطح جهانی مستقر کنیم. Global Relay مجموعه‌ای از نقاط ورودی رله است که از نظر جغرافیایی توزیع شده‌اند و همگی همان رفتار ارسال بسته را پیاده‌سازی می‌کنند.

گستردگی جغرافیایی نقاط ورودی باعث می‌شود نخستین پرش از کلاینت به OpenAI کوتاه‌تر شود؛ زیرا بسته می‌تواند از طریق رله‌ای نزدیک به کاربر، هم از نظر جغرافیایی و هم از نظر توپولوژی شبکه، وارد شبکه‌ی ما شود، نه اینکه ابتدا از اینترنت عمومی عبور کند و به منطقه‌ای دوردست برسد. در عمل، این یعنی تأخیر کمتر، نوسان تأخیر پایین‌تر و جهش‌های قابل‌اجتناب کمتر در از‌دست‌رفتن بسته‌ها، پیش از آنکه ترافیک به بک‌بون ما برسد.6

لایه‌ی Global Relay بسته‌ها را از کلاینت دریافت می‌کند و آن‌ها را به خوشه‌ی فرستنده‌-‌گیرنده منتقل می‌کند‌

ما برای سیگنالینگ از هدایت جغرافیایی و مبتنی بر نزدیکیِ Cloudflare استفاده می‌کنیم تا درخواست اولیه‌ی HTTP یا WebSocket به خوشه فرستنده‌ـ‌گیرنده‌ی نزدیک برسد. زمینه‌ی درخواست، محل نشست و نقطه‌ی ورودی Global Relay را که باید به کلاینت اعلام شود تعیین می‌کند. پاسخ SDP نشانی Global Relay را ارائه می‌دهد، در حالی که ufrag اطلاعات کافی را در خود دارد تا Global Relay بتواند رسانه را به خوشه تعیین‌شده مسیریابی کند و رله نیز آن را به فرستنده‌ـ‌گیرنده‌ی مقصد برساند.

در کنار هم، سیگنالینگِ هدایت‌شده بر اساس موقعیت جغرافیایی و Global Relay هم راه‌اندازی اتصال و هم رسانه را در مسیر ورودی نزدیک‌تری قرار می‌دهند، در حالی که نشست همچنان به یک فرستنده‌ـ‌گیرنده‌ی مشخص متصل می‌ماند. این کار زمان رفت‌وبرگشت را برای سیگنالینگ و نخستین بررسی اتصال ICE کاهش می‌دهد و مستقیماً مدت زمانی را که کاربر پیش از شروع صحبت منتظر می‌ماند کوتاه‌تر می‌کند.

پیاده‌سازی Relay و عملکرد

ما سرویس رله را با Go نوشتیم و عمداً دامنه‌ی پیاده‌سازی آن را محدود نگه داشتیم. در لینوکس، پشته‌ی شبکه‌ی کرنل بسته‌های UDP را از واسط شبکه‌ی ماشین دریافت می‌کند و آن‌ها را به یک سوکت تحویل می‌دهد؛ یعنی همان نقطه‌ی پایانی سیستم‌عامل که یک پردازه پس از bind کردن یک IP:Port از آن می‌خواند. Relay در فضای کاربر اجرا می‌شود، بنابراین یک پردازه‌ی معمولی Go سرآیندهای بسته را از آن سوکت می‌خواند، مقدار کمی از وضعیت جریان را به‌روزرسانی می‌کند و بسته‌ها را بدون خاتمه‌دادن WebRTC ارسال می‌کند. ما به هیچ چارچوب دورزدن کرنل نیاز نداشتیم؛ چارچوبی که به یک پردازه‌ی فضای کاربر اجازه می‌دهد برای دستیابی به نرخ بالاتر پردازش بسته‌ها، صف‌های شبکه را مستقیماً پایش کند، اما در عین حال پیچیدگی عملیاتی بیشتری هم اضافه می‌کند.

انتخاب‌های اصلی طراحی:

  • بدون خاتمه‌ی پروتکل: Relay فقط سرآیندهای STUN و ufrag را پردازش می‌کند؛ برای بسته‌های بعدیِ DTLS، RTP و RTCP از وضعیت کش‌شده استفاده می‌کند و بسته‌ها را مات و غیرقابل‌مشاهده نگه می‌دارد.
  • وضعیت موقت: رله یک نگاشت کوچک، درون‌حافظه‌ای و با مهلت انقضای کوتاه از نشانی کلاینت به مقصد فرستنده‌ـ‌گیرنده نگه می‌دارد تا وضعیت جریان و مشاهده‌پذیری را مدیریت کند.
  • مقیاس‌پذیری افقی: چندین نمونه‌ی رله به‌صورت موازی پشت یک متوازن‌کننده‌ی بار اجرا می‌شوند. وضعیت نگه‌داری‌شده، وضعیت اصلی و حساس WebRTC نیست؛ بنابراین راه‌اندازی مجدد باعث افت حداقلی ترافیک و بازیابی سریع جریان می‌شود.

تمهیدات بهره‌وری:

  • SO_REUSEPORT یک گزینه‌ی سوکت در لینوکس است که به چند worker رله روی یک ماشین اجازه می‌دهد به همان پورت UDP متصل شوند. سپس کرنل بسته‌های ورودی را میان این workerها توزیع می‌کند و به این ترتیب از ایجاد گلوگاه در یک حلقه‌ی خواندن واحد جلوگیری می‌شود.
  • runtime.LockOSThread هر goroutine خواننده‌ی UDP را به یک thread مشخص در سیستم‌عامل متصل نگه می‌دارد. این کار در کنار SO_REUSEPORT معمولاً باعث می‌شود بسته‌های متعلق به یک جریان واحد (یعنی IP:Port مبدأ و مقصد به‌همراه پروتکل) روی همان هسته‌ی CPU باقی بمانند؛ در نتیجه locality کش بهتر می‌شود و جابه‌جایی context کاهش می‌یابد.
  • بافرهای ازپیش‌اختصاص‌یافته و حداقل‌سازی کپی‌کردن داده‌ها، سربار پردازش و تخصیص حافظه را پایین نگه می‌دارند تا از اجرای جمع‌آوری زباله در Go جلوگیری شود.

این پیاده‌سازی توانست ترافیک رسانه‌ی بلادرنگ جهانی ما را با ردپای نسبتاً کوچک رله مدیریت کند؛ بنابراین به‌جای رفتن سراغ مسیر دورزدن کرنل، طراحی ساده‌تر را حفظ کردیم.

نتایج و آموخته‌ها

این معماری به ما اجازه می‌دهد رسانه‌ی WebRTC را در Kubernetes اجرا کنیم، بدون آنکه هزاران پورت UDP را در معرض شبکه قرار دهیم. این موضوع مهم است، چون یک سطح UDP کوچک‌تر و ثابت، ایمن‌سازی و متوازن‌سازی بار را ساده‌تر می‌کند و به زیرساخت اجازه می‌دهد بدون رزرو بازه‌های بزرگ پورت عمومی مقیاس‌پذیر شود. با پشتیبانی بهتر زیرساختی از سوی Kubernetes و امنیت بیشتر به‌دلیل سطح تماس کوچک‌تر، این طراحی همچنین رفتار استاندارد WebRTC را برای کلاینت‌ها حفظ می‌کند و تأیید می‌کند که طراحی بدون SFU انتخاب پیش‌فرض مناسبی برای بار کاری ما بوده است. بیشتر نشست‌های ما نقطه‌به‌نقطه، حساس به تأخیر و زمانی مقیاس‌پذیرترند که سرویس‌های استنتاج نیازی نداشته باشند مانند همتای WebRTC رفتار کنند.

درس کلی‌تر این است که بهترین محل برای افزودن پیچیدگی، یک لایه‌ی نازک مسیریابی است؛ نه تک‌تک سرویس‌های بک‌اند و نه رفتار سفارشی در سمت کلاینت. قراردادن فراداده‌ی مسیریابی در یک فیلد بومیِ پروتکل، مسیریابی قطعیِ نخستین بسته، ردپای عمومی کوچک برای UDP، و انعطاف‌پذیری کافی برای قراردادن نقاط ورودی نزدیک به کاربران در سراسر جهان را برای ما فراهم کرد.

چند انتخاب به‌ویژه مهم بودند:

  • رفتار و معنای پروتکل را در نقطه‌ی ورودی شبکه حفظ کنید. کلاینت‌ها همچنان با WebRTC استاندارد ارتباط برقرار می‌کنند و این باعث می‌شود سازگاری میان مرورگرها و موبایل دست‌نخورده باقی بماند.
  • وضعیت‌های پیچیده‌ی نشست را در یک نقطه نگه دارید. فرستنده‌ـ‌گیرنده مالک ICE، DTLS، SRTP و چرخه‌ی عمر نشست است؛ رله فقط بسته‌ها را ارسال می‌کند.
  • بر اساس اطلاعاتی که از قبل در راه‌اندازی وجود دارد مسیریابی کنید. ufrag مربوط به ICE بدون افزودن وابستگی جست‌وجوی داغ در مسیر، یک سازوکار برای مسیریابی نخستین بسته به ما داد.
  • پیش از رفتن سراغ دورزدن کرنل، برای حالت رایج بهینه‌سازی کنید. یک پیاده‌سازی محدود و متمرکز با Go، همراه با استفاده‌ی دقیق از SO_REUSEPORT، اتصال threadها به هسته‌های مشخص و پردازش با تخصیص حافظه‌ی پایین، برای بار کاری ما کافی بود.

AI صوتی بلادرنگ فقط زمانی کار می‌کند که زیرساخت، تأخیر را نامرئی جلوه دهد. برای ما، این به معنای تغییر شکل استقرار WebRTC بود، بی‌آن‌که آنچه کلاینت‌ها از خودِ WebRTC انتظار دارند تغییر کند.