Skip to content

فصل ۲ — دوستان نزدیک (Nearby Friends)

خلاصهٔ تحلیلی — این متن چکیدهٔ مفاهیم فصل به زبان فارسی است، نه ترجمهٔ واژه‌به‌واژه.

قابلیت «دوستان نزدیک» فهرست دوستانی را نشان می‌دهد که از نظر جغرافیایی نزدیک کاربر هستند (مشابه قابلیت فیس‌بوک). تفاوت بنیادی با فصل ۱: در سرویس مجاورت، مکان کسب‌وکارها ایستا بود؛ اینجا مکان کاربران دائماً تغییر می‌کند و مسئله عملاً «رساندن کارآمد به‌روزرسانی مکان از هر کاربر به دوستانش» است.

گام ۱ — تعیین محدودهٔ مسئله

  • «نزدیک» یعنی شعاع ۵ مایل (قابل پیکربندی)؛ فاصلهٔ خط مستقیم کافی است.
  • ۱ میلیارد کاربر که ۱۰٪ از این قابلیت استفاده می‌کنند؛ تاریخچهٔ مکان هم ذخیره می‌شود (مثلاً برای ML).
  • دوستِ غیرفعال بیش از ۱۰ دقیقه از فهرست حذف می‌شود.
  • فهرست هر چند ثانیه به‌روز می‌شود؛ هر ردیف شامل فاصله و timestamp آخرین به‌روزرسانی است.

نیازمندی‌های غیرکارکردی: تأخیر کم، پایایی (از دست رفتن گاه‌به‌گاه چند نقطهٔ داده پذیرفتنی است) و سازگاری نهایی (eventual consistency) برای مخزن مکان.

برآورد: ۱۰۰ میلیون DAU، ده میلیون کاربر همزمان، به‌روزرسانی مکان هر ۳۰ ثانیه ← QPS به‌روزرسانی ≈ ۳۳۴ هزار. با میانگین ۴۰۰ دوست و فرض آنلاین/نزدیک بودن ۱۰٪ آن‌ها، backend باید حدود ۱۴ میلیون ارسال در ثانیه انجام دهد.

گام ۲ — طراحی سطح بالا

نکتهٔ ترتیب: چون پروتکل ارتباطی ساده HTTP نیست، اول معماری و بعد API بررسی می‌شود. ایدهٔ peer-to-peer خالص (اتصال مستقیم هر کاربر به همهٔ دوستان) برای موبایل با اتصال ناپایدار و باتری محدود عملی نیست؛ پس backend مشترک لازم است که به‌روزرسانی‌ها را دریافت و فقط به دوستانِ در شعاع forward کند.

اجزای معماری

  • Load balancer: جلوی هر دو دستهٔ سرور.
  • سرورهای RESTful API: stateless؛ کارهای جانبی مثل مدیریت دوستی و پروفایل.
  • سرورهای WebSocket: stateful؛ هر کلاینت یک اتصال دائمی دارد. علاوه بر ارسال به‌روزرسانی‌ها، مقداردهی اولیهٔ کلاینت (ارسال مکان همهٔ دوستان آنلاینِ نزدیک) را هم انجام می‌دهند.
  • کش مکان (Redis): آخرین مکان هر کاربر فعال با TTL؛ هر به‌روزرسانی TTL را تمدید می‌کند. دوام (durability) لازم نیست — اگر Redis از دست برود، کش با جریان به‌روزرسانی‌های جدید دوباره گرم می‌شود.
  • پایگاه دادهٔ کاربران: پروفایل و روابط دوستی (رابطه‌ای یا NoSQL؛ شاردینگ با user ID).
  • پایگاه دادهٔ تاریخچهٔ مکان: بار نوشتن سنگین ← Cassandra گزینهٔ خوبی است؛ اسکیمای ساده (user_id, latitude, longitude, timestamp).
  • Redis Pub/Sub: لایهٔ مسیریابی پیام. هر کاربر یک کانال اختصاصی دارد؛ ساخت کانال بسیار ارزان است و کانال بدون فعالیت فقط کمی حافظه مصرف می‌کند، نه CPU.

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

۱) کلاینت مکان جدید را روی اتصال WebSocket می‌فرستد ← ۲) سرور آن را در تاریخچه و کش مکان ذخیره می‌کند (و در متغیر handler برای محاسبات بعدی) ← ۳) مکان به کانال Pub/Sub کاربر publish می‌شود ← ۴) همهٔ subscriber ها (handler های WebSocket دوستان) پیام را می‌گیرند ← ۵) هر handler فاصلهٔ جدید را محاسبه می‌کند و فقط اگر داخل شعاع باشد به کلاینت دوست می‌فرستد.

APIها (روی WebSocket)

ارسال دوره‌ای مکان، دریافت به‌روزرسانی مکان دوستان، مقداردهی اولیه (ارسال مکان خود ← دریافت مکان دوستان)، subscribe به دوست جدید و unsubscribe از دوست حذف‌شده.

گام ۳ — بررسی عمیق (مقیاس‌دهی هر جزء)

  • سرورهای API: stateless؛ auto-scale عادی.
  • سرورهای WebSocket: auto-scale ساده است اما چون stateful هستند، حذف نود باید با حالت «draining» در load balancer انجام شود تا اتصال‌های موجود تخلیه شوند؛ انتشار نسخهٔ جدید هم همین دقت را می‌خواهد.
  • مقداردهی اولیهٔ کلاینت: به‌روزرسانی کش، بارگیری فهرست دوستان، fetch دسته‌ای مکان‌ها از کش (دوستان غیرفعال به لطف TTL اصلاً در کش نیستند)، محاسبهٔ فاصله و ارسال موارد در شعاع، و subscribe به کانال همهٔ دوستان — حتی غیرفعال‌ها، چون کانال بی‌استفاده تقریباً هزینه‌ای ندارد و این کار منطق subscribe/unsubscribe هنگام فعال/غیرفعال شدن دوستان را حذف می‌کند (مبادلهٔ حافظهٔ بیشتر در برابر معماری ساده‌تر).
  • کش مکان: ۱۰ میلیون × ~۱۰۰ بایت از نظر حافظه در یک سرور جا می‌شود، اما ۳۳۴K write/s زیاد است ← شاردینگ بر اساس user ID + replica آماده برای هر شارد.

مقیاس‌دهی Redis Pub/Sub — قلب فصل

محاسبات نشان می‌دهد گلوگاه CPU است، نه حافظه:

  • حافظه: ۱۰۰ میلیون کانال × ~۱۰۰ دوست × ~۲۰ بایت ≈ ۲۰۰ گیگابایت ← فقط ۲ سرور ۱۰۰ گیگابایتی.
  • CPU: ~۱۴ میلیون push در ثانیه با فرض محافظه‌کارانهٔ ۱۰۰ هزار push بر سرور ← حدود ۱۴۰ سرور.

پس کلاستر توزیع‌شده لازم است: کانال‌ها مستقل‌اند و با consistent hashing روی سرورها پخش می‌شوند. حلقهٔ هش در یک کامپوننت service discovery (مثل etcd یا ZooKeeper) نگهداری می‌شود و سرورهای WebSocket به تغییرات آن subscribe می‌کنند و نسخهٔ محلی را کش می‌کنند.

نکتهٔ ظریف: Pub/Sub را باید مثل کلاستر stateful اداره کرد. پیام‌ها ماندگار نیستند، اما فهرست subscriber های هر کانال state واقعی است؛ جابه‌جایی کانال (تغییر اندازهٔ حلقه) یعنی موج بزرگ resubscribe و احتمال از دست رفتن چند به‌روزرسانی. بنابراین: کلاستر را over-provision کنید، resize را در کم‌ترافیک‌ترین ساعت انجام دهید، و تعویض یک سرور خراب (جایگزینی نود در همان جایگاه حلقه) بسیار کم‌ریسک‌تر از تغییر اندازهٔ حلقه است.

موارد تکمیلی

  • افزودن/حذف دوست: کلاینت callback ثبت می‌کند که به سرور WebSocket پیام subscribe/unsubscribe بفرستد.
  • کاربران پر-دوست: با سقف دوستی (مثلاً ۵٬۰۰۰ در فیس‌بوک؛ رابطهٔ دوطرفه، نه follower)، subscriber ها بین سرورهای متعدد پخش می‌شوند و hotspot ایجاد نمی‌شود.
  • افراد غریبهٔ نزدیک (امتیاز اضافه): به‌جای کانال به‌ازای کاربر، کانال به‌ازای سلول geohash بسازید؛ هر کاربر به سلول خود و ۸ همسایه subscribe می‌کند.
  • جایگزین Pub/Sub — Erlang: پردازه‌های Erlang فوق‌سبک‌اند (~۳۰۰ بایت، میلیون‌ها پردازه در یک سرور) و می‌توان هر کاربر را یک پردازه مدل کرد که پیام مکان را مستقیم به پردازه‌های دوستان می‌فرستد. از نظر فنی راه‌حل بهتری است، اما نیروی متخصص Erlang کمیاب است.

نکات کلیدی فصل

  • تفاوت دادهٔ ایستا و پویا کل معماری را عوض می‌کند: از «کوئری ایندکس جغرافیایی» به «مسیریابی پیام real-time».
  • WebSocket برای push دوطرفه، Redis با TTL برای «آخرین مکان»، و Pub/Sub برای fan-out — هر قطعه دقیقاً یک مسئولیت دارد.
  • در برآورد ظرفیت، جنس گلوگاه (CPU در برابر حافظه) تعیین‌کنندهٔ تعداد سرورهاست.
  • سیستم‌هایی که فهرست subscriber نگه می‌دارند stateful هستند؛ حتی اگر خودِ پیام‌ها ناپایدار باشند، عملیات resize را باید مثل کلاستر storage مدیریت کرد.
  • پذیرش از دست رفتن گاه‌به‌گاه داده (نیازمندی‌های نرم‌تر) طراحی را به‌طور چشمگیری ساده و ارزان می‌کند.

یادداشت‌های مترجم

  • اصطلاحات WebSocket، Pub/Sub، TTL، consistent hashing و geohash به شکل لاتین حفظ شده‌اند.