يا هلا بيكم يا جماعة، معكم أخوكم أبو عمر. اليوم بدي أحكي لكم قصة صارت معي قبل كم سنة، قصة علمتني درس مهم في عالم البرمجة: ما في أداة وحدة بتحل كل المشاكل. القصة عن مشروع كبير كنا شغالين عليه، نظام تجارة إلكترونية ضخم مبني على معمارية الخدمات المصغرة (Microservices).
في البداية، كان كل شيء “عال العال”. الخدمات بتتواصل مع بعضها باستخدام REST APIs المعتادة، والبيانات بتنتقل بصيغة JSON. خدمة المستخدمين بتحكي مع خدمة المنتجات، وخدمة الطلبات بتحكي مع خدمة الدفع… شغل مرتب ومنظم. لكن مع الوقت، ومع زيادة عدد المستخدمين والخدمات، بدأت المشاكل تظهر. النظام صار بطيء، بطيء بشكل مش طبيعي. الطلب الواحد اللي المفروض ياخد أجزاء من الثانية صار ياخد ثواني!
قعدنا نحلل الوضع، وشفنا إنه عنق الزجاجة الحقيقي كان في الشبكة الداخلية بين الخدمات. كل طلب REST كان بيفتح اتصال HTTP جديد، وبيرسل ويستقبل بيانات JSON كبيرة الحجم نسبياً، والعملية بتتكرر آلاف المرات في الدقيقة. صرنا زي اللي بحاول يعبي خزان مي بقشة عصير. حاولنا نزيد عدد السيرفرات ونعمل Scaling، بس المشكلة كانت أعمق. كنا بنعالج الأعراض مش المرض نفسه.
في واحد من اجتماعات الفريق اللي كلها توتر، واحد من المبرمجين الشباب، الله يجزيه الخير، حكى بهدوء: “يا جماعة، شو رأيكم نجرب gRPC؟”. في البداية، الأغلبية ومن ضمنهم أنا، كنا متشككين. “شو هاد gRPC كمان؟ بدناش نعقد الأمور زيادة!”. لكن بعد ما قرأنا عنه شوي وعملنا Proof of Concept بسيط، انصدمنا من الفرق في الأداء. كانت السرعة خرافية! وهون كانت نقطة التحول اللي خلتنا نعيد التفكير في كل شيء، واللي بدي أشارككم تفاصيلها اليوم.
لماذا REST ليس دائمًا الخيار الأمثل؟
قبل ما حدا يفهمني غلط، أنا بحب REST. هو بسيط، مفهوم، ومدعوم في كل مكان. هو العمود الفقري للويب الحديث، ولسه بستخدمه في كثير من المشاريع. لكن زي أي أداة، إله نقاط قوة ونقاط ضعف. المشكلة مش في REST نفسه، المشكلة في استخدامه في السياق الغلط.
مشكلة “الثرثرة” وحجم البيانات
واجهات REST API التقليدية تعتمد على بروتوكول HTTP/1.1، وهو بروتوكول نصي (Text-based). كل طلب يتطلب إنشاء اتصال جديد (أو إعادة استخدام اتصال بطرق غير فعالة أحيانًا)، وإرسال Headers نصية طويلة، ثم إرسال البيانات بصيغة JSON، وهي أيضًا نصية. تخيل معي هذا السيناريو في بيئة الخدمات المصغرة:
- خدمة A تطلب بيانات مستخدم من خدمة B.
- خدمة B تحتاج للتحقق من صلاحيات المستخدم من خدمة C.
- خدمة C تسجل هذا الطلب في خدمة D (Logging).
هنا، طلب واحد من المستخدم النهائي أدى إلى سلسلة من طلبات REST الداخلية. كل طلب من هدول بيحمل معه “وزن زائد” من الـ Headers والبيانات النصية. هذا هو ما أسميه “الثرثرة” (Chattiness) التي تستهلك موارد الشبكة والمعالجة بشكل كبير.
الافتقار إلى عقد صارم (Strict Contract)
في عالم REST، توثيق الـ API هو أمر حيوي. نستخدم أدوات مثل OpenAPI (Swagger) لتعريف الـ endpoints والـ payloads. لكن هذا التوثيق هو طبقة إضافية فوق الـ API، وليس جزءًا لا يتجزأ منه. كم مرة حدث خلاف بين فريق الـ Frontend والـ Backend لأن أحدهم غيّر اسم حقل في الـ JSON أو نوع بياناته دون تحديث التوثيق؟ “المفروض ترجعلي `userId` مش `user_id`!”. هذه المشاكل تضيع الوقت وتسبب أخطاء غير متوقعة.
أهلاً بالبطل: ما هو gRPC؟
gRPC هو إطار عمل للـ Remote Procedure Call (RPC) مفتوح المصدر وعالي الأداء، طورته جوجل. الفكرة بسيطة: بدل ما تفكر بـ “موارد” و “أفعال HTTP” (GET, POST, PUT)، أنت تفكر بـ “دوال” (Functions) تستدعيها عن بعد وكأنها موجودة في الكود المحلي عندك.
لكن السحر الحقيقي لـ gRPC يكمن في التقنيات التي بني عليها.
الأساس المتين: HTTP/2
بعكس REST الذي يعمل غالبًا فوق HTTP/1.1، فإن gRPC مصمم خصيصًا ليعمل فوق HTTP/2. وهذا يمنحه مزايا هائلة:
- Multiplexing: القدرة على إرسال عدة طلبات واستجابات بشكل متزامن عبر اتصال TCP واحد، مما يحل مشكلة “Head-of-line blocking” ويزيل الحاجة لفتح اتصالات متعددة.
- Streaming: يدعم gRPC تدفق البيانات بشكل أصيل. يمكنك عمل تدفق من السيرفر للعميل (Server streaming)، أو من العميل للسيرفر (Client streaming)، أو حتى في كلا الاتجاهين (Bi-directional streaming). هذا مثالي لتطبيقات الوقت الحقيقي مثل الدردشة أو تحديثات البورصة.
- Binary Protocol: بروتوكول HTTP/2 هو بروتوكول ثنائي (Binary) وليس نصيًا، مما يجعله أسرع وأقل عرضة للأخطاء في التحليل (Parsing).
القلب النابض: Protocol Buffers (Protobuf)
بدلاً من استخدام JSON النصي، يستخدم gRPC ما يسمى بـ “Protocol Buffers”. هذه آلية من جوجل لعمل Serialization للبيانات المنظمة (Structured Data). باختصار، أنت تعرف شكل بياناتك في ملف خاص بامتداد .proto، وهذا الملف يصبح هو “العقد” الرسمي بين العميل والخادم.
البيانات يتم تحويلها إلى صيغة ثنائية مضغوطة جدًا قبل إرسالها عبر الشبكة. انظر لهذا المثال البسيط:
لنفترض أن لدينا هذا الكائن في JSON (حوالي 36 بايت):
{"userName": "AbuOmar", "id": 101}نفس الكائن عند تمثيله باستخدام Protobuf قد يأخذ 8-10 بايت فقط! تخيل هذا التوفير على ملايين الطلبات.
العقود أولاً: نهج يمنع الأخطاء
مع gRPC، تبدأ دائمًا بتعريف خدمتك وبياناتك في ملف .proto. هذا الملف هو المصدر الوحيد للحقيقة (Single Source of Truth). من هذا الملف، يمكنك توليد كود الخادم والعميل تلقائيًا بلغات برمجة مختلفة (Go, Python, Java, C#, Node.js, والمزيد).
هذا يعني أن الأخطاء الناتجة عن عدم تطابق البيانات بين الخدمات شبه مستحيلة. المترجم (Compiler) سيخبرك فورًا إذا حاولت إرسال بيانات لا تتوافق مع العقد. “ما في مجال للمزح هون”، الكود إما يعمل كما هو متوقع أو لا يعمل على الإطلاق.
في الميدان: متى تختار gRPC ومتى تلتزم بـ REST؟
هذا هو السؤال الأهم. كما قلت، المبرمج الشاطر هو من يعرف متى يستخدم كل أداة.
متى يكون gRPC هو المنقذ؟ 🚀
- الاتصالات الداخلية بين الخدمات المصغرة (Microservices): هذا هو الاستخدام رقم واحد. عندما تحتاج خدماتك للتواصل مع بعضها البعض بسرعة وكفاءة عالية داخل شبكتك الخاصة، gRPC هو الخيار الأفضل بلا منازع.
- تطبيقات الوقت الحقيقي (Real-time Applications): أي تطبيق يحتاج لتدفق بيانات مستمر (Streaming) سيستفيد بشكل هائل من gRPC. فكر في: تطبيقات الدردشة، لوحات التحكم الحية (Live Dashboards)، بث بيانات أجهزة IoT.
- البيئات محدودة الموارد (Mobile & IoT): عندما تطور تطبيقًا للموبايل أو لجهاز IoT، فإن كل بايت من البيانات وكل دورة معالج (CPU cycle) تهم. صغر حجم بيانات Protobuf وكفاءة HTTP/2 تجعل gRPC مثاليًا لتوفير البطارية واستهلاك البيانات.
- البيئات متعددة اللغات (Polyglot Environments): إذا كان لديك خدمات مكتوبة بلغات مختلفة (مثلًا، خدمة بـ Go، وأخرى بـ Python، وثالثة بـ Java)، فإن قدرة gRPC على توليد الكود لكل هذه اللغات تجعل عملية التكامل سلسة جدًا.
متى يجب أن تبقى مع REST؟ 🤔
- واجهات برمجة التطبيقات العامة (Public-facing APIs): عندما تبني API ليستخدمها مطورون من خارج شركتك، REST لا يزال هو الملك. السبب بسيط: المتصفحات لا تدعم gRPC بشكل مباشر (تحتاج إلى طبقة وسيطة مثل gRPC-Web). أي مطور يمكنه اختبار REST API باستخدام متصفحه أو أداة بسيطة مثل `curl`.
- العمليات البسيطة القائمة على الموارد (Simple CRUD): إذا كان كل ما تحتاجه هو واجهة بسيطة لإنشاء وقراءة وتحديث وحذف الموارد (CRUD)، فإن تعقيد إعداد gRPC قد لا يكون مبررًا. REST يقدم نموذجًا بسيطًا وواضحًا لهذا النوع من العمليات.
- عندما تكون البساطة وسهولة الوصول هي الأولوية: لا يمكن إنكار أن REST أسهل في الفهم والبدء به. النظام البيئي حوله ضخم جدًا، من التوثيق إلى الأدوات إلى المكتبات. إذا كان فريقك غير معتاد على gRPC، قد يكون المنحنى التعليمي عاملاً يجب أخذه في الاعتبار.
مثال عملي: خدمة ترحيب بسيطة بـ gRPC
حتى تتضح الصورة، دعنا نبني خدمة “Greeter” بسيطة. سنبدأ بتعريف العقد.
1. تعريف الخدمة باستخدام Protobuf
ننشئ ملفًا اسمه greeter.proto. هذا هو العقد الذي يصف خدمتنا.
// نحدد إصدار الـ Protobuf
syntax = "proto3";
// نحدد اسم الحزمة لتجنب تضارب الأسماء
package greeter;
// تعريف الخدمة نفسها، والتي تسمى Greeter
service Greeter {
// تعريف دالة (RPC) اسمها SayHello
// تأخذ رسالة من نوع HelloRequest وترجع رسالة من نوع HelloReply
rpc SayHello (HelloRequest) returns (HelloReply);
}
// تعريف شكل رسالة الطلب
message HelloRequest {
string name = 1;
}
// تعريف شكل رسالة الرد
message HelloReply {
string message = 1;
}
من هذا الملف البسيط، يمكننا الآن توليد كود الخادم والعميل بأي لغة نريدها.
2. مثال الخادم (بلغة Go)
بعد توليد الكود من ملف .proto، يكون تطبيق الدالة بسيطًا جدًا.
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/your/generated/code/greeter" // استيراد الكود الذي تم توليده
)
// تعريف struct يطبق واجهة الخادم
type server struct {
pb.UnimplementedGreeterServer
}
// تطبيق دالة SayHello
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
// ... كود تشغيل الخادم على بورت معين
}
لاحظ كيف أن الكود نظيف ومرتب. كل ما عليك فعله هو تطبيق المنطق الخاص بدالة `SayHello`.
3. مثال العميل (بلغة Python)
العميل أيضًا بسيط جدًا بعد توليد الكود الخاص به.
import grpc
# استيراد الكود الذي تم توليده
import greeter_pb2
import greeter_pb2_grpc
def run():
# إنشاء قناة اتصال مع الخادم
with grpc.insecure_channel('localhost:50051') as channel:
# إنشاء stub (عميل)
stub = greeter_pb2_grpc.GreeterStub(channel)
# استدعاء الدالة عن بعد وكأنها دالة محلية
response = stub.SayHello(greeter_pb2.HelloRequest(name='Abu Omar'))
print("Greeter client received: " + response.message)
if __name__ == '__main__':
run()
شوف قديش الكود واضح! العميل يستدعي `stub.SayHello` وكأنها دالة عادية في Python. كل تعقيدات الشبكة والـ serialization مخفية تمامًا.
خلاصة ونصيحة من القلب 🧡
الرحلة من REST إلى gRPC في مشروعنا كانت تجربة فتحت عيوننا. لم يكن الأمر استبدالاً كاملاً، بل كان اختيارًا ذكيًا للأداة المناسبة في المكان المناسب. ما زلنا نستخدم REST لواجهاتنا العامة التي تتحدث مع المتصفحات وتطبيقات الموبايل، لكن كل الاتصالات الداخلية عالية التردد بين خدماتنا أصبحت الآن عبر gRPC، والأداء تحسن بشكل لا يصدق.
نصيحتي لك يا صديقي المبرمج: لا تقع في فخ “المطرقة الذهبية”، وهو الاعتقاد بأن الأداة التي تعرفها هي الحل لكل شيء. المبرمج المحترف هو الذي يمتلك صندوق أدوات متنوع، ويعرف متى يستخدم المطرقة، ومتى يستخدم المفك، ومتى يحتاج إلى أداة متخصصة.
افهم مشكلتك جيدًا، ثم اختر الأداة التي تحلها بأفضل طريقة. لا تخف من تجربة تقنيات جديدة مثل gRPC. قد تكون في البداية مخيفة بعض الشيء، لكنها قد تكون هي الحل السحري الذي كنت تبحث عنه لإنقاذ أداء تطبيقك. 🚀