כיצד בנינו את הפייפליין של tracio.ai עם השהיה מתחת ל-30ms
מאיסוף אותות ועד מזהה מבקר בפחות מ-30ms: הארכיטקטורה שלנו מבוססת Go, ClickHouse, Redis ועיבוד מבוזר.
כשיצאנו לבנות את מנוע זיהוי המכשירים של tracio.ai, הייתה לנו דרישה אחת שאינה נתונה למשא ומתן: הפייפליין כולו — מקבלת האותות המוצפנים ועד החזרת מזהה מבקר — חייב להסתיים בפחות מ-30 מילישניות באחוזון ה-95. מאמר זה הוא סקירה מפורטת של הארכיטקטורה שבנינו כדי לעמוד ביעד הזה.
סקירת הפייפליין
פייפליין הזיהוי כולל חמישה שלבים: פענוח אותות, נרמול אותות, חישוב hash, פתרון זהות וסריאליזציה של התגובה. כל שלב עובר אופטימיזציה בנפרד, ושלבים שיכולים לרוץ במקביל אכן רצים כך. התקציב הכולל הוא 30ms, המחולק בקירוב כך: פענוח 2ms, נרמול 3ms, hashing 2ms, פתרון זהות 20ms, סריאליזציה 1ms. ה-2ms הנותרים הם מרווח ביטחון.
פענוח האותות הופך את התעבורה המוצפנת בצד הלקוח. אנו משתמשים בחבילות ה-crypto של Go עם האצה חומרתית, המשלימות פענוח של מטען טיפוסי בגודל 4KB בפחות מ-1ms. הנרמול מפרסר את ה-JSON של האות, מאמת טיפוסים ומיישם טרנספורמציות ספציפיות לפלטפורמה — למשל, נרמול מחרוזות user agent כדי להסיר רעש התלוי בגרסה.
פתרון זהות מבוזר
פתרון זהות — קביעה האם המכשיר הזה נראה בעבר — הוא השלב הרגיש ביותר להשהיה. אנו מאחסנים פרופילי מכשירים ב-Redis, מחולקים ל-shards על פני אשכול באמצעות שכבת ניתוב מפתחות מבוזרת. הניתוב מחלק את המפתחות על בסיס טביעת האצבע ברמת החומרה, מה שמבטיח שחיפושים עבור אותו מכשיר תמיד יגיעו לאותו צומת Redis.
מימוש ה-sharding שלנו משתמש בצמתים וירטואליים (150 לכל צומת פיזי) כדי להבטיח חלוקה אחידה. כאשר צומת מתווסף או מוסר, יש למפות מחדש רק 1/N מהמפתחות, כאשר N הוא מספר הצמתים. מימשנו את שכבת הניתוב ב-Go עם זמן חיפוש של O(log n) וללא הקצאות זיכרון.
Redis כמאגר הזהות
בחרנו ב-Redis על פני החלופות (Memcached, ScyllaDB, DynamoDB) בזכות זמני התגובה העקביים שלו מתחת למילישנייה והתמיכה במבני נתונים מורכבים. כל פרופיל מכשיר מאוחסן כ-hash של Redis עם שדות עבור ה-hash של כל שכבת אות, מזהה המבקר, חותמת הזמן של הראייה האחרונה ומטא-נתוני רמת ודאות.
שאילתת פתרון הזהות היא קריאת HGETALL בודדת ואחריה השוואה של ה-hashes של האותות הנכנסים מול ה-hashes המאוחסנים. אם שכבת החומרה תואמת, אנו מחזירים את מזהה המבקר הקיים ברמת ודאות גבוהה. אם רק שכבת התוכנה תואמת, אנו מבצעים השוואת דמיון של הנתונים ברמת האות כדי לקבוע האם מדובר באותו מכשיר עם דפדפן מעודכן. אם דבר אינו תואם, אנו מייצרים מזהה מבקר חדש.
ClickHouse לאחסון אירועים
כל אירוע זיהוי נכתב ל-ClickHouse באופן אסינכרוני. אנו משתמשים ב-writer עם באפר המקבץ הכנסות באצווה — אוסף אירועים במשך 100ms או עד שמצטברים 1,000 אירועים, המוקדם מביניהם. קיבוץ זה קריטי משום ש-ClickHouse מתפקד בצורה הטובה ביותר עם הכנסות גדולות (אלפי שורות בכל פעם) ולא עם הכנסות של שורה בודדת.
הסכמה שלנו ב-ClickHouse עברה אופטימיזציה עבור שני דפוסי השאילתה הנפוצים ביותר: חיפוש כל האירועים עבור מזהה מבקר מסוים, וצבירת אירועים לאורך פרקי זמן. אנו משתמשים במנוע MergeTree עם מפתח ראשי של (visitor_id, timestamp), המספק חיפושי נקודה מהירים וסריקות טווח יעילות. תצוגות מגובשות (materialized views) מתחזקות מטריקות יומיות ושעתיות מצוברות מראש.
השגת השהיה מתחת ל-30ms בקנה מידה
שלוש החלטות ארכיטקטוניות היו קריטיות לעמידה ביעד ההשהיה שלנו. ראשית, הפייפליין הוא streaming מלא — אנו מתחילים לעבד אותות לפני שגוף בקשת ה-HTTP כולו התקבל. שנית, חיפושי Redis משתמשים ב-connection pooling עם חיבורים מתמשכים, מה שמבטל את התקורה של לחיצת היד של TCP. שלישית, כתיבות ל-ClickHouse אסינכרוניות לחלוטין ולעולם אינן חוסמות את נתיב התגובה.
בבדיקות עומס עם 50K בקשות בשנייה, השהיית ה-p50 שלנו היא 12ms, ה-p95 היא 24ms וה-p99 היא 38ms. ה-p99 חורג מדי פעם מיעד ה-30ms שלנו במהלך איזון מחדש של אשכול Redis, אך ה-p95 נשאר באופן עקבי מתחת ל-30ms. עבור לקוחות עם דרישות השהיה מחמירות יותר, אנו מציעים אשכולות Redis ייעודיים המבטלים תחרות רב-דיירתית.