واجهات REST النصية أصبحت عنق الزجاجة: كيف ضاعفتُ سرعة التواصل بين خدماتي المصغرة باستخدام gRPC؟

يا جماعة الخير، السلام عليكم ورحمة الله. معكم أخوكم أبو عمر.

قبل فترة، كنت شغال على نظام كبير شوي، عبارة عن منصة تحليل بيانات بتستقبل كم هائل من الأحداث (events) في الثانية الواحدة. النظام مبني على معمارية الخدمات المصغرة (Microservices)، وكل خدمة إلها وظيفتها الخاصة: خدمة لاستقبال البيانات، خدمة لمعالجتها، خدمة لتخزينها، وخدمة للتنبيهات… وشبكة معقدة من الخدمات اللي بتحكي مع بعضها طول الوقت.

في البداية، الأمور كانت “عال العال”. استخدمنا واجهات REST مع صيغة JSON للتواصل بين الخدمات، وهذا هو المتعارف عليه والسهل. لكن مع زيادة الضغط على النظام، بدأت ألاحظ شغلة غريبة: بطء شديد وتأخير في معالجة البيانات. قضينا أيام وليالي واحنا بنحلل الأداء، وبنعمل profiling للكود، وبنفلي كل سطر. بعد حفر وتنقيب، اكتشفنا إن المشكلة مش في منطق المعالجة نفسه، لا، المشكلة كانت “عنق الزجاجة” الحقيقي… كانت في شبكة التواصل بين الخدمات نفسها! كل طلب REST كان يحمل معه “شنطة سفر” من الـ Headers، وكل رسالة JSON كان لازم تتحول من نص لـ object والعكس (Serialization/Deserialization)، وهذا الأشي كان يستهلك وقت ومعالج بشكل “بيجلط” مع آلاف الطلبات في الثانية.

هنا وقفت مع حالي وقلت: “يا أبو عمر، لازم يكون في حل أحسن”. وبدأ مشوار البحث اللي وصلني لعالم جديد اسمه gRPC، واللي غير طريقة تفكيري في بناء الأنظمة الموزعة. خلوني أحكيلكم القصة بالتفصيل.

لماذا أصبحت واجهات REST “عنق الزجاجة” في نظامي؟

الحكي بينا، أنا من عشاق REST. بساطتها وسهولة استخدامها مع أي لغة برمجة بتخليها الخيار الأول لأي مشروع جديد. لكن لكل تقنية حدودها، وفي حالتي، وصلت REST لحدودها القصوى لعدة أسباب:

1. مشكلة البيانات النصية (JSON Overhead)

الـ JSON مصمم ليكون سهل القراءة للبشر، وهذا ممتاز للمطورين وللواجهات العامة (Public APIs). لكن في التواصل الداخلي بين خدمة وخدمة (service-to-service)، الآلات هي اللي بتحكي مع بعضها، وهي ما بتهتم بالمسافات وأسماء الحقول الطويلة. كل بايت زيادة في رسالة JSON بيتم ضربه في آلاف أو ملايين الطلبات، مما يؤدي إلى استهلاك كبير في الشبكة.

تخيل رسالة بسيطة زي هاي بالـ JSON:

{
  "user_id": 12345,
  "event_type": "user_login",
  "timestamp": "2023-10-27T10:00:00Z"
}

هذه الرسالة، رغم بساطتها، تحتوي على أسماء حقول وعلامات ترقيم تزيد من حجمها. في المقابل، الصيغ الثنائية (Binary formats) بتخزن هاي البيانات بشكل مضغوط جدًا وبدون أي “زوايد”.

2. ثقل بروتوكول HTTP/1.1

معظم تطبيقات REST التقليدية بتشتغل فوق بروتوكول HTTP/1.1. هذا البروتوكول عنده مشكلة اسمها “Head-of-Line Blocking”، يعني لو بعتت 5 طلبات على نفس الاتصال، لازم تستنى جواب الأول عشان يوصلك جواب الثاني، وهكذا. هذا الأشي بيخلق طوابير انتظار غير ضرورية بين خدماتك.

بالإضافة إلى ذلك، كل طلب وكل استجابة بتحمل معها مجموعة كبيرة من الترويسات (Headers)، وكثير منها مكرر، وهذا بيضيف عبء إضافي على الشبكة.

3. تكلفة الـ Serialization/Deserialization المستمرة

هذه كانت القشة التي قصمت ظهر البعير في مشروعي. كل خدمة كانت تستلم رسالة JSON نصية، وتحتاج تستهلك من قوة المعالج (CPU) عشان تحولها لكائن (Object) تقدر تتعامل معه برمجيًا. ولما بدها ترسل رد، بترجع تحول الكائن لنص JSON مرة ثانية. هذه العملية، اللي اسمها Serialization و Deserialization، مكلفة جدًا عند التعامل مع حجم بيانات ضخم، وكانت تستهلك جزء كبير من موارد السيرفرات.

gRPC: المنقذ الذي جاء في الوقت المناسب

gRPC هو إطار عمل مفتوح المصدر تم تطويره بواسطة جوجل، وهو اختصار لـ Google Remote Procedure Call. الفكرة منه بسيطة: تمكين الخدمات من استدعاء الدوال (Functions) على خدمات أخرى موجودة على شبكة مختلفة وكأنها دوال محلية موجودة في نفس الكود. لكن قوته الحقيقية تكمن في التقنيات اللي بيستخدمها تحت الغطاء.

السر الأول: Protocol Buffers (Protobuf)

بدلًا من JSON، يستخدم gRPC ما يسمى بـ Protocol Buffers. فكر فيه كبديل للـ JSON/XML لكنه أسرع وأصغر وأكثر كفاءة. الـ Protobuf هو آلية من جوجل لعمل Serialization للبيانات المهيكلة.

كيف يعمل؟

  1. تحديد العقد (Contract): أنت بتعرّف هيكل بياناتك وخدماتك في ملف خاص بامتداد .proto. هذا الملف هو “العقد” أو “مصدر الحقيقة” بين الخادم والعميل.
  2. التحويل لثنائي (Binary Serialization): عندما ترسل البيانات، يقوم Protobuf بتحويلها إلى صيغة ثنائية مضغوطة جدًا.
  3. توليد الكود (Code Generation): الأجمل من هذا كله، هو إنك بتقدر تستخدم ملف الـ .proto لتوليد كود الخادم والعميل بشكل تلقائي بأي لغة برمجة تدعمها (Go, Java, Python, C#, Dart, … والقائمة طويلة). هذا بيضمن توافق كامل بين الخدمات وبيوفر عليك وقت كتابة كود مكرر.

السر الثاني: العمل فوق HTTP/2

gRPC مبني من الأساس ليعمل فوق بروتوكول HTTP/2 الحديث، وهذا بيعطيه قدرات خارقة مقارنة بـ HTTP/1.1:

  • Multiplexing: يسمح بإرسال عدة طلبات واستجابات في نفس الوقت عبر اتصال TCP واحد، مما يقضي تمامًا على مشكلة “Head-of-Line Blocking”.
  • Streaming: يدعم gRPC تدفق البيانات (Streaming) بشكل أصيل. بتقدر تعمل خدمة ترسل سيل من البيانات للعميل (Server Streaming)، أو العكس (Client Streaming)، أو حتى الاثنين يحكوا مع بعض بسيل من البيانات في نفس الوقت (Bidirectional Streaming). هذا مثالي لتطبيقات الوقت الفعلي مثل الدردشة، أو تحديثات البورصة، أو رفع ملفات كبيرة.
  • ضغط الترويسات (Header Compression): يستخدم HTTP/2 تقنية HPACK لضغط الـ Headers، مما يقلل بشكل كبير من حجم البيانات المتبادلة.

من التنظير إلى التطبيق: مثال عملي بسيط

خلينا نشوف كيف ممكن نبني خدمة بسيطة باستخدام gRPC. لنفترض أننا نريد بناء خدمة لإدارة المستخدمين.

1. تعريف الخدمة بملف user.proto

أول خطوة هي كتابة العقد. هذا الملف يصف شكل الطلب، وشكل الاستجابة، والدالة التي تقدمها الخدمة.

// نحدد إصدار الـ syntax
syntax = "proto3";

// نحدد اسم الـ package لتنظيم الكود
package user;

// تعريف الخدمة نفسها
service UserService {
  // دالة من نوع RPC اسمها GetUser
  // تأخذ رسالة من نوع UserRequest وترجع رسالة من نوع UserResponse
  rpc GetUser (UserRequest) returns (UserResponse);
}

// تعريف رسالة الطلب
message UserRequest {
  int32 id = 1; // الرقم 1 هو معرف الحقل، وليس قيمته
}

// تعريف رسالة الاستجابة
message UserResponse {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

2. بناء الخادم (مثال بلغة Go)

بعد ما نولّد الكود من ملف الـ .proto، بنقدر نبني الخادم بسهولة. الكود التالي هو مجرد مثال توضيحي للفكرة.

package main

import (
    // ... استيراد مكتبات gRPC و Go اللازمة
    pb "path/to/your/generated/user" // استيراد الكود المولّد
)

// تعريف struct يمثل الخادم
type server struct {
    pb.UnimplementedUserServiceServer
}

// تطبيق دالة GetUser التي عرفناها في ملف الـ proto
func (s *server) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
    // في تطبيق حقيقي، هنا يتم البحث في قاعدة البيانات عن المستخدم
    // لكن للتبسيط، سنرجع بيانات ثابتة
    log.Printf("Received request for user ID: %v", req.GetId())
    return &pb.UserResponse{
        Id:    req.GetId(),
        Name:  "Abu Omar",
        Email: "abu.omar@example.com",
    }, nil
}

func main() {
    // ... كود تشغيل الخادم على منفذ معين (e.g., :50051)
}

3. بناء العميل (مثال بلغة Go)

العميل أيضًا بسيط جدًا. كل ما عليه فعله هو الاتصال بالخادم واستدعاء الدالة وكأنها دالة محلية.

package main

import (
    // ... استيراد المكتبات اللازمة
    pb "path/to/your/generated/user"
)

func main() {
    // ... كود الاتصال بالخادم على عنوانه ومنفذه

    client := pb.NewUserServiceClient(conn)

    // استدعاء الدالة عن بعد بكل بساطة!
    res, err := client.GetUser(context.Background(), &pb.UserRequest{Id: 123})
    if err != nil {
        log.Fatalf("could not get user: %v", err)
    }
    log.Printf("User Details: ID=%d, Name=%s, Email=%s", res.GetId(), res.GetName(), res.GetEmail())
}

لاحظ كيف أن العميل والخادم يتشاركان نفس “العقد” (ملف الـ proto)، مما يضمن عدم حدوث أخطاء في أنواع البيانات أو أسماء الحقول.

النتائج على أرض الواقع ونصائح من “الختيار”

بعد ما أعدنا كتابة أجزاء التواصل الحرجة في نظامنا باستخدام gRPC، كانت النتائج مذهلة بكل معنى الكلمة:

  • انخفاض زمن الاستجابة (Latency): متوسط زمن الاستجابة بين الخدمات انخفض من حوالي 120ms إلى أقل من 25ms في بعض الحالات.
  • انخفاض استهلاك المعالج (CPU): عمليات الـ Serialization/Deserialization الثنائية أصبحت أسرع بكثير، مما أدى إلى انخفاض استهلاك الـ CPU على سيرفرات الخدمات بنسبة تصل إلى 40%.
  • زيادة الإنتاجية (Throughput): النظام ككل أصبح قادرًا على معالجة ضعف عدد الأحداث في الثانية بنفس الموارد.

وهنا، اسمحولي أقدملكم كم نصيحة من خبرتي المتواضعة:

نصائح أبو عمر الذهبية:

  • متى تستخدم gRPC؟ هو الخيار الأمثل للتواصل الداخلي بين الخدمات المصغرة (service-to-service)، خاصة في الأنظمة التي تتطلب أداءً عاليًا وزمن استجابة منخفض.
  • ومتى تلتزم بـ REST؟ ابقَ على REST للواجهات البرمجية العامة (Public APIs) التي ستتعامل معها أطراف خارجية أو تطبيقات الويب مباشرة من المتصفح. دعم المتصفحات لـ gRPC لا يزال محدودًا (يتطلب بروكسي مثل gRPC-Web)، والـ JSON أسهل بكثير للمطورين الخارجيين.
  • العقد أولًا: ملف الـ .proto هو أهم شيء. صممه بعناية وفكر في تطوره المستقبلي. استخدم أرقام الحقول بحكمة ولا تغيرها بعد نشر الخدمة.
  • استكشف الـ Streaming: لا تحصر نفسك بنمط الطلب والاستجابة التقليدي. قدرات الـ Streaming في gRPC قوية جدًا ويمكن أن تبسط تصميمات معقدة كنت تحتاج فيها لـ WebSockets أو Long Polling.
  • تعلم معالجة الأخطاء: معالجة الأخطاء في gRPC تختلف عن أكواد حالة HTTP (200, 404, 500). تعلم التعامل مع أكواد الحالة الخاصة بـ gRPC (e.g., `NOT_FOUND`, `ALREADY_EXISTS`, `INTERNAL`).

الخلاصة: لكل مقامٍ مقال 👍

في النهاية، gRPC ليس “الرصاصة الفضية” التي ستحل كل المشاكل، وهو ليس بديلًا كاملًا عن REST. بل هو أداة قوية جدًا في صندوق أدوات المطور، يجب أن تعرف متى وكيف تستخدمها.

بالنسبة لي، كان gRPC هو الحل الذي أنقذ مشروعي من الاختناق وفتح أمامي آفاقًا جديدة في تصميم الأنظمة الموزعة عالية الأداء. إذا كنت تبني خدمات مصغرة والتواصل بينها أصبح يشكل تحديًا في الأداء، فأنصحك بشدة أن تعطي gRPC فرصة. قد تتفاجأ من النتائج كما تفاجأت أنا.

أتمنى أن تكون هذه التجربة مفيدة لكم. والله ولي التوفيق.

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

التوسع والأداء العالي والأحمال

قاعدة بياناتي كانت تتوسل للرحمة: كيف أنقذتني استراتيجية التخزين المؤقت (Caching) من الانهيار؟

أتذكر ذلك اليوم جيدًا، قاعدة البيانات تكاد تنهار تحت ضغط الطلبات المتزايدة. في هذه المقالة، أشارككم قصة حقيقية وكيف كانت استراتيجية التخزين المؤقت، وتحديداً Redis،...

11 مارس، 2026 قراءة المزيد
التكنلوجيا المالية Fintech

رفضنا عملاء حقيقيين وقبلنا محتالين: كيف أصلحتُ نظام ‘اعرف عميلك’ (KYC) الفاشل بالذكاء الاصطناعي

أتذكر جيدًا ذلك الاجتماع الكارثي الذي كشف أن نظام التحقق من الهوية (KYC) اليدوي لدينا كان يرفض العملاء الصادقين ويفتح الأبواب للمحتالين. في هذه المقالة،...

11 مارس، 2026 قراءة المزيد
​معمارية البرمجيات

كل خدمة تنادي الأخرى مباشرة… حتى انهار كل شيء: كيف أنقذتني المعمارية الموجهة بالأحداث (EDA) من كابوس الاقتران المحكم؟

أشارككم قصة حقيقية عن ليلة كاد فيها نظامنا أن ينهار بالكامل بسبب الاقتران المحكم بين الخدمات. سأشرح لكم كيف كانت المعمارية الموجهة بالأحداث (EDA) هي...

9 مارس، 2026 قراءة المزيد
الحوسبة السحابية

وضعت كل بيضي في سلة AWS… ثم تعطلت المنطقة بأكملها: كيف أنقذتني استراتيجية السحابة المتعددة (Multi-Cloud) من التوقف التام؟

في لحظة توقف فيها كل شيء، تعلمت الدرس الأصعب: الاعتماد على مزود سحابي واحد هو وصفة لكارثة محققة. أشارككم قصتي وكيف كانت استراتيجية السحابة المتعددة...

8 مارس، 2026 قراءة المزيد
البودكاست