OpenAI

4 במאי 2026

הנדסה

איך OpenAI מספקת בינה מלאכותית קולית בהשהיה נמוכה ובהיקף נרחב

מאת יי ג'אנג וויליאם מקדונלד, חברי הצוות הטכני

בינה מלאכותית קולית מרגישה טבעית רק כשהשיחה מתנהלת בקצב הדיבור. כשהרשת מפריעה, שומעים זאת מיד: הפסקות מביכות, דיבור שנקטע או עיכוב בתגובה כשנכנסים לדברי המערכת. זה חשוב ל-ChatGPT Voice, למפתחים שבונים עם Realtime API, לסוכנים שפועלים בתהליכי עבודה אינטראקטיביים ולמודלים שצריכים לעבד אודיו בזמן שהמשתמש עדיין מדבר.

בהיקף הפעילות של OpenAI, המשמעות היא שלוש דרישות מעשיות:

  • פריסה עולמית ליותר מ-900 מיליון משתמשים פעילים בשבוע

  • הקמת חיבור מהירה, כדי שהמשתמש יוכל להתחיל לדבר ברגע שהסשן מתחיל

  • זמן הלוך־חזור קצר ויציב של המדיה, עם תנודתיות נמוכה בהשהיה ומעט אובדן חבילות, כדי שהמעבר בין הדוברים יהיה חד וחלק

הצוות ב-OpenAI שאחראי לאינטראקציות עם בינה מלאכותית בזמן אמת תכנן לאחרונה מחדש את ארכיטקטורת מערך ה-WebRTC שלנו, כדי להתמודד עם שלושה אילוצים שהחלו להתנגש ככל שהיקף הפעילות גדל: טיפול במדיה בנקודת הקצה עם פורט נפרד לכל סשן אינו מתאים היטב לתשתית של OpenAI; סשנים שומרי מצב של ICE (Interactive Connectivity Establishment) ושל DTLS (Datagram Transport Layer Security) צריכים להישאר באחריות אותו רכיב; וניתוב גלובלי חייב לשמור על השהיה נמוכה בקפיצה הראשונה. בפוסט הזה נסביר את הארכיטקטורה המפוצלת של ממסר ומשדר־מקלט שבנינו כדי לשמר את ההתנהגות הסטנדרטית של WebRTC מבחינת הלקוחות, ובו בזמן לשנות את אופן ניתוב החבילות בתוך התשתית של OpenAI.

WebRTC מאפשר לנו לבנות מוצרי בינה מלאכותית שפועלים בזמן אמת

WebRTC הוא תקן פתוח לשליחת אודיו, וידאו ונתונים בהשהיה נמוכה בין דפדפנים, אפליקציות לנייד ושרתים. לרוב מקשרים אותו לשיחות עמית־לעמית, אבל הוא גם בסיס מעשי למערכות לקוח־שרת בזמן אמת, משום שהוא מספק תקנים להיבטים המורכבים של מדיה אינטראקטיבית: ICE ליצירת קישוריות ולמעבר דרך NAT (Network Address Translation),‏ DTLS ו-SRTP (Secure Real-time Transport Protocol) לתעבורה מוצפנת, משא ומתן על רכיבי codec לדחיסה ולפענוח של אודיו, RTCP (Real-time Transport Control Protocol) לבקרת איכות, ויכולות בצד הלקוח כמו ביטול הד ואגירה לצמצום תנודתיות בהשהיה.

האחידות הזאת חשובה למוצרי בינה מלאכותית. ללא WebRTC, כל לקוח היה זקוק לפתרון אחר ליצירת קישוריות דרך מנגנוני NAT, להצפנת מדיה, למשא ומתן על רכיבי codec (המקודדים־מפענחים שנבחרים לשידור ולפריסה) ולהסתגלות לתנאי רשת משתנים. עם WebRTC, אנחנו יכולים להסתמך על מחסנית פרוטוקולים שכבר ממומשת בדפדפנים ובפלטפורמות לנייד, ולמקד את העבודה שלנו בתשתית שמחברת מדיה בזמן אמת למודלים.

אנחנו מסתמכים גם על האקוסיסטם של WebRTC עצמו, כולל מימושי קוד פתוח בשלים ועבודת התקינה שמאפשרת לדפדפנים, לאפליקציות לנייד ולשרתים לפעול יחד. העבודה החלוצית של ג'סטין אוברטי (ממתכנני הארכיטקטורה המקוריים של WebRTC) ושל שון דובואה (היוצר והמתחזק של Pion) אפשרה לצוותים כמו שלנו להתבסס על תשתית מדיה מוכחת בשטח, במקום להמציא מחדש מנגנוני תעבורה, הצפנה ובקרת גודש ברמה הנמוכה. לשמחתנו, ג'סטין ושון עובדים כיום איתנו ב-OpenAI ועוזרים להתוות את הדרך לשילוב הדוק יותר בין WebRTC לבינה מלאכותית בזמן אמת.

עבור בינה מלאכותית, התכונה החשובה ביותר היא שהאודיו מגיע כזרם רציף. סוכן קולי יכול להתחיל לתמלל, לבצע reasoning, לקרוא לכלים או להפיק דיבור בזמן שהמשתמש עדיין מדבר, במקום לחכות להעלאה מלאה. זה ההבדל בין מערכת שמרגישה כמו שיחה לבין מערכת שמרגישה כמו מכשיר קשר שצריך ללחוץ בו כדי לדבר.

בחירת ארכיטקטורת מדיה

אחרי שבחרנו ב-WebRTC, השאלה הבאה הייתה היכן למקם את נקודת הקצה שלו (היכן נקבל את חיבור ה-WebRTC ונהיה אחראים עליו — למשל, בקצה הרשת), ואיך לחבר את הסשנים האלה למערך ההסקה בצד השרת. מיקום נקודת הקצה חשוב, כי הוא קובע כיצד נטפל במצב הסשן בזמן אמת, בתעבורת המדיה, בניתוב, בהשהיה ובבידוד תקלות.

אפשרות 1: גישת ה-SFU כוללת בינה מלאכותית כמשתתפת ב-WebRTC

SFU, או יחידת העברה סלקטיבית, הוא שרת מדיה שמקבל זרם WebRTC אחד מכל משתתף ומעביר זרמים באופן סלקטיבי לאחרים. בגישה הזאת, ה-SFU משמש נקודת קצה לחיבור WebRTC נפרד עבור כל משתתף, והבינה המלאכותית מצטרפת לסשן כמשתתפת נוספת. זו יכולה להיות בחירה טובה למוצרים שמטבעם כוללים כמה משתתפים, כמו שיחות קבוצתיות, כיתות לימוד או פגישות עבודה משותפות. כך רכיבי ה-codec של האודיו, הודעות RTCP, ערוצי הנתונים, ההקלטה והמדיניות לכל זרם מרוכזים במקום אחד.1

גם במוצרים שמחברים לקוח לבינה מלאכותית, SFU הוא לעיתים קרובות נקודת המוצא המקובלת, כי הוא מאפשר לצוותים להשתמש במערכת מוכחת אחת לאיתות, לניתוב מדיה, להקלטה, לתצפיתיות ולהרחבות עתידיות, כמו העברה לנציג אנושי או הוספת משתתפים.

אפשרות 2: בגישת המשדר־מקלט, נקודת הקצה של WebRTC נמצאת בקצה הרשת, שם מתבצעת המרה לפרוטוקול של צד השרת

אופי העומס שלנו שונה. רוב הסשנים הם אחד־על־אחד — משתמש אחד שמדבר עם מודל אחד, או אפליקציה אחת שמדברת עם סוכן אחד בזמן אמת — וכל תור בשיחה רגיש להשהיה. לתעבורה מהסוג הזה בחרנו בגישת משדר־מקלט: שירות WebRTC בקצה הרשת משמש נקודת קצה לחיבור הלקוח, ואז ממיר את המדיה והאירועים לפרוטוקולים פנימיים פשוטים יותר לצורך הסקת מודל, תמלול, הפקת דיבור, שימוש בכלים ותזמור.

בתכנון הזה, המשדר־מקלט הוא השירות היחיד שמחזיק במצב של סשן ה-WebRTC, כולל בדיקות קישוריות ICE, לחיצת היד של DTLS, מפתחות הצפנת SRTP ומחזור חיי הסשן. ״סיום החיבור״ כאן פירושו שהמשדר־מקלט הוא נקודת הקצה שמשלימה את לחיצות היד האלה ומצפינה את המדיה או מפענחת אותה. ריכוז המצב במקום אחד הקל על ההבנה של חלוקת האחריות לסשנים, ואפשר לשירותי צד השרת להתרחב כמו שירותים רגילים, במקום לפעול בעצמם כעמיתי WebRTC.

אתגר הפריסה המרכזי: WebRTC פוגש את Kubernetes

לאחר שבחרנו בגישת המשדר־מקלט, המימוש הראשון שלנו היה שירות יחיד ב-Go, שנבנה על Pion וטיפל גם באיתות וגם במדיה בנקודת הקצה. הוא מפעיל את ChatGPT Voice, את ה-endpoint של WebRTC ב-Realtime API וכמה פרויקטים מחקריים.

מבחינה תפעולית, שירות המשדר־מקלט מבצע שתי משימות:

  • איתות: משא ומתן ב-SDP, בחירת רכיבי codec, פרטי אימות של ICE והקמת סשן

  • מדיה: טיפול בחיבורי WebRTC מהלקוחות כנקודת הקצה שלהם, ותחזוקת חיבורים לשירותי צד השרת לצורכי הסקה ותזמור

רצינו שהשירות ירוץ כמו שאר התשתית שלנו: על Kubernetes, שמאפשר להגדיל ולהקטין משאבים לעומסי עבודה ולהעביר אותם בין מארחים בהתאם לשינויים בביקוש. אבל הגישה המקובלת ב-WebRTC, של פורט אחד לכל סשן, לא מתאימה היטב לסביבה הזאת. היא תלויה בטווחים גדולים של פורטי UDP ציבוריים שקשה לחשוף, לאבטח ולשמר כשפודים מתווספים, מוסרים או משובצים מחדש.2

אזילת פורטים

הבעיה הראשונה הייתה עצם הגישה של פורט אחד לכל סשן. כשמספר הסשנים המקבילים גבוה, נדרש לחשוף ולנהל טווחי פורטים גדולים מאוד של UDP.

  • מאזני עומסים בענן ושירותי Kubernetes לא תוכננו לעשרות אלפי פורטי UDP ציבוריים לכל שירות. כל טווח נוסף מוסיף מורכבות תפעולית להגדרות מאזן העומסים, לבדיקות התקינות, למדיניות חומת האש ולפריסה בטוחה של עדכונים.3

  • קשה לאבטח טווחים גדולים של פורטי UDP, משום שהם מגדילים את שטח החשיפה לגישה חיצונית ומקשים על ביקורת מדיניות הרשת.

  • הם גם לא מתאימים היטב להתאמה אוטומטית של היקף המשאבים. ב-Kubernetes, פודים מתווספים, מוסרים ומשובצים מחדש כל הזמן. הדרישה שכל פוד ישריין ויפרסם טווח פורטים גדול וקבוע הופכת את הגמישות הזאת לפגיעה לשיבושים.4

לכן מערכות WebRTC רבות עוברות לפורט UDP יחיד לכל שרת, עם הפרדת התעבורה לסשנים ברמת האפליקציה מאחורי הפורט הזה.5

שמירת השיוך לרכיב שמחזיק במצב

תכנון של פורט יחיד לכל שרת פותר את בעיית מספר הפורטים, אבל יוצר בעיה שנייה: שמירה על השיוך של כל סשן לרכיב האחראי עליו במערך השרתים.

ICE ו-DTLS הם פרוטוקולים שומרי מצב. התהליך שיצר סשן צריך להמשיך לקבל את החבילות של אותו סשן, כדי שיוכל לאמת בדיקות קישוריות, להשלים את לחיצת היד של DTLS, לפענח SRTP ולטפל בשינויים מאוחרים יותר בסשן, כמו הפעלות מחדש של ICE. אם חבילות של אותו סשן מגיעות לתהליך אחר, הקמת החיבור עלולה להיכשל או שהמדיה עלולה להשתבש.

כך הגדרנו יעד ברור: לחשוף לאינטרנט הציבורי שטח חשיפה קטן וקבוע של UDP, ובמקביל להמשיך לנתב כל חבילה למשדר־מקלט שאחראי על סשן ה-WebRTC המתאים.

השוואת ארכיטקטורות מדיה של WebRTC

בחנו כמה דרכים להגיע ליעד הזה, כולל TURN (Traversal Using Relays around NAT), שבו ממסר בקצה הרשת משמש נקודת קצה להקצאות של לקוחות ומעביר תעבורה בשמם.2

גישה

יתרונות

חסרונות

צירוף IP:port ייחודי לכל סשן (מוכר גם כ-UDP ישיר מובנה)

מסלול מדיה ישיר מהלקוח לשרת

ללא שכבת העברה במסלול הנתונים

נדרש פורט UDP ציבורי אחד לכל סשן

קשה לחשוף ולאבטח טווחי פורטים גדולים

התאמה לקויה ל-Kubernetes ולמאזני עומסים בענן

צירוף IP:port ייחודי לכל שרת

חשיפה ציבורית מצומצמת בהרבה של UDP לעומת חשיפה נפרדת לכל סשן

שקע משותף אחד בכל שרת יכול להפריד את התעבורה לסשנים רבים

פועל היטב במארח יחיד, אך לא מספיק בפני עצמו למערך משותף שמאחוריו איזון עומסים

הפרדת התעבורה לסשנים במארח יחיד עוזרת רק לאחר שחבילה מגיעה לאותו מארח. במערך עם איזון עומסים, החבילה הראשונה עדיין עלולה להגיע למופע הלא נכון, ולכן עדיין נדרשת דרך דטרמיניסטית לנתב כל סשן לתהליך שאחראי עליו


ממסר TURN (משמש נקודת קצה לפרוטוקול)

הלקוחות צריכים להגיע רק לכתובת ולפורט של ממסר ה-TURN

מאפשר לרכז את המדיניות בקצה הרשת

הקצאות TURN מוסיפות סבבי הלוך־חזור בהקמת החיבור

העברה או שחזור של הקצאות בין שרתי TURN עדיין מורכבים

רכיב העברה ללא מצב + נקודת קצה שומרת מצב (הממסר והמשדר־מקלט של OpenAI)

חשיפה ציבורית מצומצמת של UDP

המשדר־מקלט עדיין אחראי על סשן ה-WebRTC במלואו

נוספת קפיצת העברה אחת לפני שהמדיה מגיעה למשדר־מקלט האחראי

נדרש מנגנון תיאום ייעודי בין הממסר למשדר־מקלט

סקירת הארכיטקטורה: ממסר + משדר־מקלט

הארכיטקטורה שהכנסנו לשימוש מפרידה בין ניתוב חבילות לבין הטיפול בפרוטוקול בנקודת הקצה. האיתות עדיין מגיע למשדר־מקלט לצורך הקמת הסשן, ואילו המדיה נכנסת תחילה דרך הממסר. הממסר הוא שכבה קלת משקל להעברת UDP, עם חשיפה ציבורית מצומצמת, והמשדר־מקלט הוא נקודת הקצה שומרת המצב של WebRTC שמאחוריו.

הממסר מעביר חבילות למשדר־מקלט ללא שמירת מצב

הממסר אינו מפענח מדיה, מפעיל מכונות מצבים של ICE או משתתף במשא ומתן על רכיבי codec. הוא קורא רק את המטא־נתונים הדרושים מתוך החבילה כדי לבחור יעד, ואז מעביר אותה למשדר־מקלט שאחראי על הסשן. המשדר־מקלט עדיין רואה תעבורת WebRTC רגילה וממשיך להחזיק בכל מצב הפרוטוקול. מבחינת הלקוח, שום דבר בסשן ה-WebRTC לא משתנה.

ניתוב לפי פרטי האימות של ICE

ניתוב החבילה הראשונה הוא השלב המרכזי במערך הזה. הממסר צריך לנתב את החבילה הראשונה מהלקוח עוד לפני שקיים סשן כלשהו, ולעשות זאת ישירות במסלול החבילה, במקום לעצור ולהמתין לשירות חיפוש חיצוני.

כל סשן WebRTC כבר נושא שדה מובנה בפרוטוקול שאפשר להשתמש בו לניתוב: מקטע שם המשתמש של ICE, או ufrag — מזהה קצר שמוחלף בעת הקמת הסשן ונשלח שוב בבדיקות קישוריות STUN. אנחנו יוצרים את ה-ufrag בצד השרת כך שיכיל בדיוק את מטא־נתוני הניתוב הדרושים לממסר כדי לזהות את אשכול היעד ואת המשדר־מקלט שאחראי על הסשן.

תרשים הרצף מראה כיצד נוצר החיבור

במהלך האיתות, המשדר־מקלט מקצה מצב לסשן ומחזיר בתשובת ה-SDP כתובת VIP משותפת של הממסר ופורט UDP. VIP היא כתובת IP וירטואלית בחזית מערך הממסרים. יחד עם הפורט, היא מספקת ללקוח יעד יחיד וקבוע, כמו `203.0.113.10:3478`, אף שמאחוריה פועלים מופעי ממסר רבים. החבילה הראשונה של הלקוח במסלול המדיה היא בדרך כלל בקשת קישור של STUN (Session Traversal Utilities for NAT), שבה ICE משתמש כדי לוודא שחבילות יכולות להגיע לכתובת שפורסמה.

הממסר מנתח רק את החלק הדרוש בחבילת ה-STUN הראשונה כדי לקרוא את ה-ufrag של השרת, לפענח את רמז הניתוב ולהעביר את החבילה למשדר־מקלט האחראי. כל משדר־מקלט מאזין בשקע UDP משותף — כלומר, נקודת קצה אחת של מערכת ההפעלה שקשורה לצירוף IP:port פנימי, ולא שקע אחד לכל סשן. לאחר שהממסר יוצר סשן מצירוף ה-IP:port של מקור הלקוח ליעד של אותו משדר־מקלט, חבילות DTLS,‏ RTP ו-RTCP הבאות עוברות במסגרת הסשן בלי לפענח שוב את ה-ufrag.

הסשן בממסר מינימלי במכוון: הוא כולל רק רשומת סשן בזיכרון שמנחה את העברת החבילות, לצד מונים הנחוצים לניטור וטיימרים לפקיעת תוקף הסשן ולניקויו. הבחירה התכנונית הזאת משאירה את ניתוב החבילות ישירות במסלול החבילה. אם ממסר מופעל מחדש ומאבד את הסשן, חבילת ה-STUN הבאה בונה אותו מחדש לפי רמז הניתוב שב-ufrag. כדי לשפר עוד יותר את האמינות, מטמון Redis שומר את המיפוי <כתובת IP + פורט של הלקוח, כתובת IP + פורט של המשדר־מקלט> לאחר קביעת הנתיב. כך אפשר לשחזר אותו הרבה קודם, עוד לפני הגעת חבילת ה-STUN הבאה.

Global Relay ואיתות בניתוב גאוגרפי

לאחר שצמצמנו את שטח החשיפה הציבורי של UDP למספר קטן של כתובות ופורטים קבועים, יכולנו לפרוס את אותו דפוס ממסר ברחבי העולם. Global Relay הוא מערך נקודות הכניסה שלנו לממסרים בפריסה גאוגרפית רחבה, שכולן מממשות את אותה התנהגות של העברת חבילות.

פריסה גאוגרפית רחבה של נקודות כניסה מקצרת את הקפיצה הראשונה מהלקוח ל-OpenAI. החבילה יכולה להיכנס לרשת שלנו דרך ממסר שקרוב למשתמש, גם גאוגרפית וגם מבחינת טופולוגיית הרשת, במקום לחצות תחילה את האינטרנט הציבורי לאזור מרוחק. בפועל, המשמעות היא השהיה נמוכה יותר, פחות תנודתיות בהשהיה ופחות רצפים מיותרים של אובדן חבילות לפני שהתעבורה מגיעה לרשת הליבה שלנו.6

שכבת Global Relay מקבלת חבילות מהלקוח ומעבירה אותן לאשכול המשדרים־מקלטים

אנחנו משתמשים בניתוב של Cloudflare לפי מיקום גאוגרפי וקרבה עבור האיתות, כך שבקשת ה-HTTP או ה-WebSocket הראשונית מגיעה לאשכול משדרים־מקלטים סמוך. הקשר הבקשה קובע את מיקום הסשן ואת נקודת הכניסה של Global Relay שתפורסם ללקוח. תשובת ה-SDP מספקת את כתובת Global Relay, ואילו ה-ufrag מכיל מספיק מידע כדי ש-Global Relay ינתב את המדיה לאשכול המיועד והממסר ינתב אותה למשדר־מקלט היעד.

יחד, איתות בניתוב גאוגרפי ו-Global Relay מכניסים גם את תעבורת ההקמה וגם את המדיה דרך נקודת כניסה קרובה, תוך שמירה על שיוך הסשן למשדר־מקלט אחד. כך מתקצר זמן ההלוך־חזור של האיתות ושל בדיקת קישוריות ה-ICE הראשונה, מה שמקצר ישירות את ההמתנה של המשתמש עד שאפשר להתחיל לדבר.

מימוש הממסר וביצועיו

כתבנו את שירות הממסר ב-Go ושמרנו במכוון על מימוש ממוקד ומצומצם. ב-Linux, מחסנית הרשת של הליבה מקבלת חבילות UDP מממשק הרשת של המכונה ומעבירה אותן לשקע — נקודת הקצה של מערכת ההפעלה שממנה תהליך קורא לאחר קשירה לצירוף IP:Port. הממסר פועל במרחב המשתמש, כך שתהליך Go רגיל קורא כותרות חבילות מהשקע הזה, מעדכן כמות קטנה של מידע על מצב הזרימה ומעביר חבילות בלי לשמש נקודת קצה של WebRTC. לא נזקקנו לתשתית לעקיפת הליבה, שהייתה מאפשרת לתהליך במרחב המשתמש לתשאל ישירות את תורי הרשת כדי להגיע לקצבי חבילות גבוהים יותר, אך גם מוסיפה מורכבות תפעולית.

בחירות תכנוניות מרכזיות:

  • ללא טיפול בפרוטוקול כנקודת קצה: הממסר מנתח רק כותרות STUN ו-ufrag; עבור חבילות DTLS,‏ RTP ו-RTCP הבאות הוא משתמש במצב שנשמר במטמון, בלי לפענח את תוכן החבילות.

  • מצב ארעי: הוא מחזיק בזיכרון מיפוי קטן, עם זמן תפוגה קצר, מכתובת הלקוח ליעד המשדר־מקלט, לצורך מעקב אחר מצב הזרימה ותצפיתיות.

  • יכולת הרחבה אופקית: כמה מופעי ממסר פועלים במקביל מאחורי מאזן עומסים. המצב הזה אינו מצב WebRTC שחייבים לשמר, ולכן הפעלות מחדש גורמות לאובדן תעבורה מזערי ולהתאוששות מהירה של הזרימה.

אמצעי ייעול:

  • SO_REUSEPORT היא אפשרות של שקע ב-Linux שמאפשרת לכמה תהליכי עבודה של הממסר באותה מכונה להיקשר לאותו פורט UDP. לאחר מכן הליבה מחלקת את החבילות הנכנסות בין תהליכי העבודה האלה, וכך נמנע צוואר בקבוק של לולאת קריאה יחידה.

  • runtime.LockOSThread מצמיד כל goroutine שקורא UDP לתהליכון מסוים של מערכת ההפעלה. בשילוב עם SO_REUSEPORT, הדבר נוטה להשאיר חבילות מאותה זרימה (צירופי IP:Port של המקור והיעד, בתוספת הפרוטוקול) על אותה ליבת מעבד, וכך משפר את מקומיות המטמון ומצמצם החלפות הקשר.

  • מאגרים שמוקצים מראש והעתקה מזערית שומרים על תקורה נמוכה של ניתוח והקצאה, כדי להימנע מאיסוף זבל ב-Go.

המימוש הזה טיפל בתעבורת המדיה העולמית שלנו בזמן אמת באמצעות מערך ממסרים קטן יחסית, ולכן נשארנו עם התכנון הפשוט יותר במקום לעבור לפתרון של עקיפת הליבה.

תוצאות ולקחים

הארכיטקטורה הזאת מאפשרת לנו להפעיל מדיית WebRTC ב-Kubernetes בלי לחשוף אלפי פורטי UDP. זה חשוב, כי קל יותר לאבטח שטח חשיפה קטן וקבוע של UDP ולאזן את העומס עליו, והוא מאפשר לתשתית להתרחב בלי לשריין טווחים גדולים של פורטים ציבוריים. לצד תמיכה טובה יותר בתשתית מצד Kubernetes ואבטחה משופרת בזכות שטח חשיפה קטן יותר, התכנון הזה גם משמר את ההתנהגות הסטנדרטית של WebRTC עבור הלקוחות, ומאשר שתכנון ללא SFU היה ברירת המחדל הנכונה לעומס שלנו. רוב הסשנים שלנו הם מנקודה לנקודה ורגישים להשהיה, וקל יותר לתמוך במספר הולך וגדל שלהם כששירותי ההסקה לא צריכים להתנהג כעמיתי WebRTC.

הלקח הרחב יותר הוא שהמקום הטוב ביותר להוסיף בו מורכבות הוא שכבת ניתוב דקה — לא כל שירות בצד השרת, ולא התנהגות מותאמת במיוחד בצד הלקוח. קידוד מטא־נתוני הניתוב בשדה מובנה בפרוטוקול נתן לנו ניתוב דטרמיניסטי של החבילה הראשונה, חשיפה ציבורית מצומצמת של UDP וגמישות מספקת למיקום נקודות כניסה קרוב למשתמשים ברחבי העולם.

כמה בחירות היו חשובות במיוחד:

  • לשמר את סמנטיקת הפרוטוקול בקצה הרשת. הלקוחות עדיין מתקשרים ב-WebRTC סטנדרטי, כך שהתאימות בין דפדפנים לפלטפורמות לנייד נשמרת.

  • לרכז במקום אחד את מצב הסשן שחייבים לשמר. המשדר־מקלט אחראי על ICE,‏ DTLS,‏ SRTP ומחזור חיי הסשן; הממסר רק מעביר חבילות.

  • לנתב על סמך מידע שכבר קיים בשלב ההקמה. ה-ufrag של ICE נתן לנו עוגן לניתוב החבילה הראשונה, בלי להוסיף תלות בחיפוש במסלול הקריטי.

  • לייעל את המקרה הנפוץ לפני שפונים לעקיפת הליבה. מימוש ממוקד ב-Go, עם שימוש מוקפד ב-SO_REUSEPORT, הצמדת תהליכונים וניתוח שממעט בהקצאות, הספיק לעומס שלנו.

בינה מלאכותית קולית בזמן אמת עובדת באמת רק כשהתשתית הופכת את ההשהיה לבלתי מורגשת. מבחינתנו, פירוש הדבר היה לשנות את מבנה פריסת ה-WebRTC שלנו, בלי לשנות את מה שהלקוחות מצפים מ-WebRTC עצמו.