تصميم أنظمة الخدمات الخلفية
أنماط التوسع والبنية التحتية للبيانات
كل مقابلة تصميم أنظمة تصل في النهاية إلى السؤال: "كيف يتوسع هذا لـ 10 أضعاف أو 100 ضعف حركة المرور؟" يغطي هذا الدرس أنماط التوسع واستراتيجيات قواعد البيانات والبنية التحتية المتخصصة للبيانات التي تحتاجها للإجابة بثقة.
التوسع الأفقي مقابل الرأسي
| الجانب | الرأسي (توسيع لأعلى) | الأفقي (توسيع للخارج) |
|---|---|---|
| الطريقة | آلة أكبر (معالج أقوى، ذاكرة أكثر، قرص أكبر) | آلات أكثر |
| الحد | سقف العتاد (أكبر مثيل متاح) | غير محدود عمليًا |
| التعقيد | بسيط (بدون تغييرات في الكود) | معقد (حالة موزعة، تنسيق) |
| منحنى التكلفة | أسي (2x معالج ≠ 2x تكلفة) | خطي (2x خوادم ≈ 2x تكلفة) |
| التوقف | يتطلب إعادة تشغيل لتغيير الحجم | بدون توقف مع موزع الأحمال |
| الأفضل لـ | قواعد البيانات (قبل التجزئة)، الخدمات الصغيرة | خوادم API عديمة الحالة، النسخ المتماثلة للقراءة |
قاعدة عامة للمقابلة: ابدأ رأسيًا للبساطة، ثم انتقل أفقيًا عند الوصول للحدود. الخدمات عديمة الحالة (خوادم API) تتوسع أفقيًا بسهولة. الخدمات ذات الحالة (قواعد البيانات) تحتاج تخطيطًا دقيقًا.
النسخ المتماثلة لقراءة قواعد البيانات
أبسط نمط توسع لأعباء العمل كثيفة القراءة. خادم أساسي (رئيسي) واحد يعالج كل الكتابات، ونسخ متماثلة متعددة تعالج القراءات.
┌──────────────┐
│ الأساسي │
الكتابات ────▶│ (الرئيسي) │
└──────┬───────┘
│ النسخ المتماثل
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ نسخة 1 │ │ نسخة 2 │ │ نسخة 3 │
└──────────┘ └──────────┘ └──────────┘
▲ ▲ ▲
└────────────┼────────────┘
القراءات (موزعة الأحمال)
تأخر النسخ المتماثل
النسخ المتماثلة متسقة نهائيًا. التأخر النموذجي 10-100 مللي ثانية، لكن قد يرتفع تحت حمل كتابة ثقيل. تعامل مع هذا في تطبيقك:
def get_user_profile(user_id: str, just_updated: bool = False) -> dict:
if just_updated:
# القراءة من الأساسي لضمان رؤية آخر كتابة
return primary_db.query("SELECT * FROM users WHERE id = %s", user_id)
else:
# القراءات العادية تذهب للنسخة المتماثلة
return replica_db.query("SELECT * FROM users WHERE id = %s", user_id)
سجل الكتابة المسبقة (WAL)
WAL هو الآلية التي تستخدمها قواعد البيانات لضمان المتانة واسترداد الأعطال. فهمه يظهر معرفة عميقة بقواعد البيانات في المقابلات.
كيف يعمل WAL:
- قبل تعديل صفحة بيانات، تكتب قاعدة البيانات التغيير في ملف سجل تسلسلي (WAL)
- كتابة WAL تُفرغ إلى القرص (fsync)
- فقط بعد ذلك تُعدل صفحة البيانات الفعلية في الذاكرة
- صفحات البيانات تُفرغ إلى القرص لاحقًا على دفعات (نقاط التفتيش)
إذا تعطلت قاعدة البيانات:
- التغييرات غير المؤكدة في الذاكرة تُفقد، لكن WAL على القرص
- عند إعادة التشغيل، تعيد قاعدة البيانات تشغيل WAL لإعادة بناء الحالة
- لا تُفقد أبدًا أي بيانات مؤكدة (WAL مُفرغ)
استخدامات WAL في PostgreSQL: استرداد الأعطال، النسخ المتماثل المتدفق للنسخ، الاسترداد في نقطة زمنية (PITR)، والنسخ المتماثل المنطقي (Debezium CDC).
استراتيجيات التجزئة (Sharding)
عندما لا يستطيع خادم قاعدة بيانات واحد التعامل مع حجم الكتابة أو حجم البيانات، تقسم البيانات عبر خوادم متعددة (أجزاء). هذا أحد أعقد القرارات في تصميم الأنظمة.
مقارنة الاستراتيجيات
| الاستراتيجية | كيف تعمل | المزايا | العيوب |
|---|---|---|---|
| قائمة على النطاق | تجزئة بنطاق المفتاح (مثلاً A-M على الجزء 1، N-Z على الجزء 2) | استعلامات نطاق بسيطة، سهلة الفهم | أقسام ساخنة (بعض النطاقات أكثر نشاطًا) |
| قائمة على التجزئة | shard = hash(key) % num_shards |
توزيع متساوٍ | استعلامات النطاق تتعطل (يجب التشتت والتجميع) |
| قائمة على الدليل | جدول بحث يربط كل مفتاح بالجزء الخاص به | إعادة تعيين مرنة | البحث يضيف زمنًا، الدليل نقطة فشل واحدة |
مثال التجزئة القائمة على Hash
import hashlib
NUM_SHARDS = 8
def get_shard(user_id: str) -> int:
"""تحديد أي جزء يحتوي على بيانات هذا المستخدم."""
hash_value = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
return hash_value % NUM_SHARDS
def query_user(user_id: str) -> dict:
shard_id = get_shard(user_id)
connection = shard_connections[shard_id]
return connection.query("SELECT * FROM users WHERE id = %s", user_id)
اكتشاف الأقسام الساخنة والتخفيف
بعض المفاتيح تتلقى حركة مرور غير متناسبة (ملف شخصية مشهور، منشور فيروسي). استراتيجيات التخفيف:
- التجزئة المتسقة مع العقد الافتراضية — توزع الحمل بشكل أكثر تساويًا عبر الخوادم الفعلية
- تمليح المفاتيح الساخنة — إضافة لاحقة عشوائية (
celebrity_123_0،celebrity_123_1، ...celebrity_123_9) والتشتيت عبر 10 أجزاء، ثم تجميع القراءات - جزء مخصص للبيانات الساخنة — نقل المفاتيح الساخنة المكتشفة إلى جزء معزول وأقوى
- طبقة تخزين مؤقت — تخزين المفاتيح الساخنة في Redis حتى لا تصل لقاعدة البيانات أبدًا
تقسيم البيانات متعدد المستأجرين
تطبيقات SaaS تخدم مستأجرين (عملاء) متعددين. استراتيجية العزل تعتمد على الحجم والامتثال والتكلفة:
| الاستراتيجية | الوصف | العزل | التكلفة | الأفضل لـ |
|---|---|---|---|---|
| DB مشتركة، مخطط مشترك | كل المستأجرين في نفس الجداول، عمود tenant_id |
الأقل | الأرخص | مستأجرون صغار، شركات ناشئة |
| DB مشتركة، مخططات منفصلة | كل مستأجر يحصل على مخططه الخاص في DB واحدة | متوسط | منخفض | مستأجرون متوسطون، بيانات معتدلة |
| مخطط مخصص لكل مستأجر | مخطط منفصل، خادم مشترك | جيد | متوسط | الصناعات المنظمة |
| DB مخصصة لكل مستأجر | كل مستأجر يحصل على خادم قاعدة بيانات خاص | الأعلى | الأغلى | المؤسسات، الامتثال الصارم |
-- نهج المخطط المشترك: كل استعلام يتضمن tenant_id
-- استخدم Row Level Security (RLS) في PostgreSQL للتطبيق
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- التطبيق يعيّن سياق المستأجر لكل طلب
SET app.current_tenant = 'tenant-abc-123';
SELECT * FROM orders; -- يرى فقط طلبات tenant-abc-123
تخزين الكائنات الثنائية الكبيرة
الكائنات الثنائية الكبيرة (الصور، الفيديوهات، المستندات) يجب ألا تُخزن أبدًا في قاعدة البيانات العلائقية. استخدم تخزين الكائنات.
| الخدمة | المزود | الميزات الرئيسية |
|---|---|---|
| S3 | AWS | متانة 11 تسعات، قواعد دورة الحياة، إشعارات الأحداث |
| Cloud Storage | GCP | طبقات Nearline/Coldline، اتساق قوي |
| Azure Blob | Azure | طبقات Hot/Cool/Archive، تخزين غير قابل للتغيير |
نمط الروابط المُوقّعة مسبقًا
import boto3
def generate_upload_url(file_key: str, content_type: str) -> str:
"""إنشاء رابط مُوقّع مسبقًا لرفع مباشر من العميل إلى S3."""
s3 = boto3.client("s3")
url = s3.generate_presigned_url(
"put_object",
Params={
"Bucket": "my-app-uploads",
"Key": file_key,
"ContentType": content_type,
},
ExpiresIn=300, # 5 دقائق
)
return url
# التدفق:
# 1. العميل يطلب رابط رفع من خادم API
# 2. خادم API يُنشئ رابطًا مُوقّعًا مسبقًا ويرجعه
# 3. العميل يرفع مباشرة إلى S3 (يتجاوز خادم API)
# 4. S3 يُطلق إشعار حدث ← معالجة/صورة مصغرة/CDN
هذا النمط يبعد الملفات الكبيرة عن خوادم API، مما يمنع اختناقات عرض النطاق وضغط الذاكرة.
البنية التحتية للبحث
عندما يكون SQL LIKE '%search_term%' بطيئًا جدًا (وهو دائمًا بطيء على نطاق واسع لأنه لا يستطيع استخدام الفهارس)، تحتاج محرك بحث مخصصًا.
المفاهيم الأساسية لـ Elasticsearch
| المفهوم | الوصف |
|---|---|
| الفهرس المقلوب | يربط كل كلمة فريدة بقائمة المستندات التي تحتويها — بحث O(1) |
| التعيين (Mapping) | تعريف المخطط: أي الحقول text (قابلة للبحث الكامل) مقابل keyword (مطابقة تامة) |
| المُحلل (Analyzer) | مُرمّز + مرشحات: "Running quickly" ← ["run", "quick"] (تجذير + تصغير) |
| تسجيل الصلة | TF-IDF أو BM25: المستندات ذات المصطلحات النادرة المطابقة تحصل على درجة أعلى |
| شبه فوري | المستندات قابلة للبحث خلال ~1 ثانية من الفهرسة (فترة التحديث) |
متى تستخدم Elasticsearch مقابل SQL
| استخدم Elasticsearch عندما | ابقَ مع SQL عندما |
|---|---|
| بحث نصي كامل عبر حقول متعددة | بحث دقيق بالمفتاح الأساسي |
| مطابقة ضبابية، مرادفات، تجذير | استعلامات WHERE column = value بسيطة |
| بحث مُصنّف (تصفية حسب الفئة + السعر + التقييم) | البيانات تتسع في الذاكرة، أقل من مليون سجل |
| اقتراحات الإكمال التلقائي | الاتساق القوي مطلوب |
| تجميع وتحليل السجلات | لا ميزانية لبنية تحتية إضافية |
بيانات السلاسل الزمنية
المقاييس، بيانات أجهزة IoT، سجلات التطبيقات، والتداول المالي كلها تشترك في النمط: كثيفة الكتابة، إلحاق فقط، يُستعلم عنها بنطاق زمني.
| قاعدة البيانات | النوع | الميزة الرئيسية |
|---|---|---|
| InfluxDB | مصممة خصيصًا للسلاسل الزمنية | لغة استعلام InfluxQL/Flux، خفض العينات مدمج |
| TimescaleDB | امتداد PostgreSQL | دعم SQL كامل، جداول فائقة تُقسّم تلقائيًا بالوقت |
| ClickHouse | تحليلات عمودية التوجه | استعلامات تجميع سريعة للغاية |
الاحتفاظ وخفض العينات
البيانات الخام بدقة ثانية واحدة تستهلك تخزينًا هائلًا. استخدم احتفاظًا متدرجًا:
البيانات الخام (دقة 1 ثانية): احتفظ 7 أيام
تجميعات 5 دقائق: احتفظ 30 يومًا
تجميعات ساعة: احتفظ سنة واحدة
تجميعات يوم: احتفظ للأبد
-- TimescaleDB: إنشاء تجميع مستمر لملخصات 5 دقائق
CREATE MATERIALIZED VIEW metrics_5min
WITH (timescaledb.continuous) AS
SELECT
time_bucket('5 minutes', timestamp) AS bucket,
sensor_id,
AVG(value) AS avg_value,
MAX(value) AS max_value,
MIN(value) AS min_value,
COUNT(*) AS sample_count
FROM raw_metrics
GROUP BY bucket, sensor_id;
-- إضافة سياسة احتفاظ لحذف البيانات الخام بعد 7 أيام
SELECT add_retention_policy('raw_metrics', INTERVAL '7 days');
تصميم خط أنابيب ETL
خطوط أنابيب ETL (استخراج، تحويل، تحميل) تنقل البيانات من قواعد البيانات التشغيلية إلى مستودعات البيانات للتحليلات.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ قاعدة │───▶│ استخراج │───▶│ تحويل │───▶│ تحميل │
│ البيانات │ │ (Debezium │ │ (Spark / │ │(BigQuery / │
│ (PostgreSQL) │ │ CDC) │ │ dbt) │ │ Snowflake / │
└──────────────┘ └──────────────┘ └──────────────┘ │ Redshift) │
└──────────────┘
CDC (التقاط تغييرات البيانات) مع Debezium
بدلاً من التصديرات الدورية على دفعات (التي تفقد البيانات بين التشغيلات وتُرهق قاعدة البيانات المصدر)، يلتقط CDC كل INSERT و UPDATE و DELETE فور حدوثها بقراءة WAL لقاعدة البيانات.
WAL قاعدة البيانات ← موصّل Debezium ← موضوع Kafka ← المستهلك ← مستودع البيانات
الفوائد:
- شبه فوري (ثوانٍ، وليس ساعات)
- بدون حمل على قاعدة البيانات المصدر (يقرأ WAL، وليس الجداول)
- يلتقط الحذف (التصدير الدوري يفقدها)
- يحافظ على الترتيب الدقيق للتغييرات
طبقة التحويل
| الأداة | النوع | الأفضل لـ |
|---|---|---|
| dbt | تحويلات SQL أولاً | المحللون الذين يعرفون SQL، تحويلات تصريحية |
| Apache Spark | حوسبة موزعة | بيانات واسعة النطاق (TB+)، تحويلات معقدة |
| Apache Flink | معالجة تدفقية | تحويلات فورية، تجميعات نافذية |
نصيحة للمقابلة: عند السؤال عن خطوط أنابيب البيانات، اذكر CDC بدلاً من ETL الدوري لإظهار تفكير عصري. أبرز المقايضة: CDC أكثر تعقيدًا في الإعداد لكنه يوفر بيانات أحدث وحملاً أقل على قاعدة البيانات المصدر.
لقد أكملت جميع الدروس الثلاثة في هذه الوحدة. خذ الاختبار لاختبار فهمك لأُطر تصميم الأنظمة والمسائل الكلاسيكية وأنماط التوسع. :::