كيف بنينا خط معالجة tracio.ai بزمن أقل من 30 مللي ثانية
من جمع الإشارات إلى معرّف الزائر في أقل من 30 مللي ثانية: بنيتنا المعمارية باستخدام Go وClickHouse وRedis والمعالجة الموزّعة.
عندما شرعنا في بناء محرّك التعرّف على الأجهزة الخاص بـ tracio.ai، كان لدينا متطلّب واحد غير قابل للتفاوض: يجب أن يكتمل خط المعالجة بأكمله — من استقبال الإشارات المشفّرة إلى إعادة معرّف الزائر — في أقل من 30 مللي ثانية عند المئين الخامس والتسعين. هذه المقالة شرح تفصيلي للبنية المعمارية التي بنيناها لتحقيق ذلك الهدف.
نظرة عامة على خط المعالجة
يتألّف خط معالجة التعرّف من خمس مراحل: فك تشفير الإشارة، وتطبيع الإشارة، وحساب التجزئة، وحل الهوية، وتسلسل الاستجابة. تُحسَّن كل مرحلة على نحو مستقل، وتُنفَّذ المراحل التي يمكن تشغيلها بالتوازي معًا. الميزانية الإجمالية هي 30 مللي ثانية، موزّعة تقريبًا على النحو التالي: فك التشفير 2 مللي ثانية، والتطبيع 3 مللي ثانية، والتجزئة 2 مللي ثانية، وحل الهوية 20 مللي ثانية، والتسلسل 1 مللي ثانية. أمّا الـ 2 مللي ثانية المتبقية فهي احتياطي.
يعكس فك تشفير الإشارة النقل المشفّر من جهة العميل. نستخدم حزم التشفير في Go مع التسريع العتادي، ما يُكمل فك تشفير حمولة نموذجية بحجم 4 كيلوبايت في أقل من 1 مللي ثانية. يحلّل التطبيع بنية JSON للإشارة، ويتحقّق من الأنواع، ويطبّق تحويلات خاصة بكل منصّة — على سبيل المثال، تطبيع سلاسل وكيل المستخدم لإزالة الضوضاء المرتبطة بالإصدار.
حل الهوية الموزّع
حل الهوية — أي تحديد ما إذا كان هذا الجهاز قد شوهد من قبل — هو أكثر المراحل حساسيةً للكمون. نخزّن ملفّات تعريف الأجهزة في Redis، مُوزَّعة بالتشظية عبر عنقود باستخدام طبقة توجيه مفاتيح موزّعة. يوزّع التوجيه المفاتيح استنادًا إلى بصمة الطبقة العتادية، ما يضمن أن تُصيب عمليات البحث عن الجهاز نفسه عقدة Redis نفسها دائمًا.
يستخدم تنفيذنا للتشظية عُقدًا افتراضية (150 لكل عقدة فيزيائية) لضمان توزيع متساوٍ. عند إضافة عقدة أو إزالتها، لا يحتاج سوى 1/N من المفاتيح إلى إعادة تخطيط، حيث N هو عدد العُقد. نفّذنا طبقة التوجيه في Go بزمن بحث بمقدار O(log n) وبدون أي تخصيصات ذاكرة.
Redis بوصفه مخزن الهوية
اخترنا Redis بدلًا من البدائل (Memcached وScyllaDB وDynamoDB) بسبب أزمنة استجابته المتّسقة دون المللي ثانية ودعمه لبنى البيانات المعقّدة. يُخزَّن كل ملف تعريف جهاز على هيئة تجزئة Redis تتضمّن حقولًا لتجزئة كل طبقة إشارة، ومعرّف الزائر، والطابع الزمني لآخر ظهور، وبيانات وصفية للثقة.
استعلام حل الهوية هو استدعاء HGETALL واحد يتبعه مقارنة بين تجزئات الإشارة الواردة والتجزئات المخزّنة. إذا تطابقت الطبقة العتادية، نُعيد معرّف الزائر الموجود بثقة عالية. وإذا تطابقت الطبقة البرمجية فقط، نجري مقارنة تشابه لبيانات مستوى الإشارة لتحديد ما إذا كان هذا هو الجهاز نفسه بمتصفّح محدَّث. وإذا لم يتطابق شيء، نولّد معرّف زائر جديدًا.
ClickHouse لتخزين الأحداث
يُكتب كل حدث تعرّف إلى ClickHouse على نحو غير متزامن. نستخدم كاتبًا مؤقتًا يجمّع عمليات الإدراج دفعيًا — يجمع الأحداث لمدة 100 مللي ثانية أو حتى يتراكم 1000 حدث، أيّهما يأتي أولًا. هذا التجميع الدفعي بالغ الأهمية لأن أداء ClickHouse يكون أفضل مع عمليات الإدراج الكبيرة (آلاف الصفوف في المرّة الواحدة) بدلًا من إدراج الصفوف فرادى.
مخطّط ClickHouse لدينا مُحسَّن لأكثر نمطي استعلام شيوعًا: البحث عن جميع الأحداث لمعرّف زائر محدّد، وتجميع الأحداث على مدى فترات زمنية. نستخدم محرّك MergeTree بمفتاح أساسي هو (visitor_id, timestamp)، ما يوفّر عمليات بحث نقطي سريعة ومسحًا نطاقيًا فعّالًا. وتحافظ العروض المتحقّقة على مقاييس يومية وساعية مُجمَّعة مسبقًا.
تحقيق زمن أقل من 30 مللي ثانية على نطاق واسع
كانت ثلاثة قرارات معمارية بالغة الأهمية لبلوغ هدف الكمون لدينا. أولًا، خط المعالجة انسيابي بالكامل — نبدأ معالجة الإشارات قبل استقبال كامل جسم طلب HTTP. ثانيًا، تستخدم عمليات البحث في Redis تجميع الاتصالات مع اتصالات دائمة، ما يلغي زمن مصافحة TCP الإضافي. ثالثًا، عمليات الكتابة إلى ClickHouse غير متزامنة تمامًا ولا تحجب مسار الاستجابة أبدًا.
تحت اختبار الحِمل بمعدّل 50 ألف طلب في الثانية، يبلغ كمون p50 لدينا 12 مللي ثانية، وp95 يبلغ 24 مللي ثانية، وp99 يبلغ 38 مللي ثانية. يتجاوز p99 أحيانًا هدف الـ 30 مللي ثانية أثناء إعادة موازنة عنقود Redis، لكن p95 يظل باستمرار دون 30 مللي ثانية. وللعملاء ذوي متطلّبات الكمون الأكثر صرامة، نوفّر عناقيد Redis مخصّصة تلغي التنازع متعدّد المستأجرين.