يا أهلاً وسهلاً فيكم يا جماعة. معكم أخوكم أبو عمر، من قلب فلسطين الحبيبة، جاي أحكيلكم قصة صارت معي ومع فريقي، قصة فيها سهر وتعب، بس نهايتها كانت سعيدة وعلّمتنا كثير. قصة عن كيف كنا غرقانين في بحر من الـ JSON البطيء، وكيف ظهرلنا طوق نجاة اسمه gRPC.
من ذاكرة أبو عمر: ليلة لا تُنسى مع وحش الـ JSON
بتذكرها زي كأنها مبارح. كانت ليلة خميس، وأنا وفريقي كنا بنشتغل على إطلاق ميزة جديدة في نظامنا المبني على الخدمات المصغرة (Microservices). كل شي كان ماشي تمام في بيئة التطوير، الاختبارات كلها ناجحة، والقهوة شغالة. أطلقنا الميزة الجديدة، وبعدها بكم ساعة، بلشت توصلنا التنبيهات… “System is slow”، “High latency detected”، “Users are complaining”.
يا لطيف! نزل علينا الخبر زي الصاعقة. فتحنا الـ dashboards، وشفنا الكارثة بعيوننا. وحدة من خدماتنا الأساسية، خلينا نسميها “خدمة المستخدمين”، كانت بتستجيب ببطء شديد جداً. والأسوأ، إنها كانت بتسبب تأثير الدومينو، وكل الخدمات اللي بتعتمد عليها صارت بطيئة كمان.
قعدنا ساعات طويلة نحلل المشكلة. بالأول فكرنا المشكلة في قاعدة البيانات، بعدين فكرنا في الكود نفسه. بس بعد حفر وتنقيب، اكتشفنا إنه المشكلة الحقيقية كانت في الاتصال بين “خدمة الطلبات” و”خدمة المستخدمين”. خدمة الطلبات كانت بتطلب بيانات عدد كبير من المستخدمين دفعة واحدة، وخدمة المستخدمين كانت بترجّعلها ملف JSON ضخم جداً. حجمه كان بيوصل للميغابايتات في بعض الأحيان!
كنا زي اللي بحاول يبعت حمولة شاحنة كبيرة من خلال ماسورة مي صغيرة. عملية تحويل البيانات لـ JSON (Serialization) على السيرفر، ونقلها عبر الشبكة، وبعدين تحليلها (Deserialization) على العميل… كل هاد كان بياكل وقت وموارد بشكل مش طبيعي. وقتها قلت للفريق، “يا جماعة، الحكي هاد ما بنفع. لازم نلاقي حل جذري لمشكلة الاتصال هاي، وإلا رح نضل نطفّي حرايق”. ومن هون بلشت رحلتنا مع gRPC.
ما المشكلة أصلاً في REST و JSON؟
قبل ما نحكي عن المنقذ gRPC، خلينا نفهم عدونا الأول. بصراحة، REST و JSON مش أعداء، هم أصدقاء خدمونا سنين طويلة، ولسه بنستخدمهم في أماكن كثيرة. لكن لكل شي حدود. لما يتعلق الأمر بالاتصال عالي الأداء بين عشرات أو مئات الخدمات المصغرة داخل شبكتك الخاصة (East-West traffic)، بتبدأ مشاكلهم تظهر.
الثرثرة الزائدة (Verbose & Chatty)
الـ JSON مصمم ليكون مقروء للبشر، وهذا شيء ممتاز. لكن هاي الميزة بتصير عيب لما الآلات تحكي مع بعض. كل طلب واستجابة بتحتوي على أسماء الحقول (keys) مكررة. تخيل عندك قائمة من 1000 مستخدم، وكل مستخدم عنده حقل “user_id”. كلمة “user_id” رح تتكرر 1000 مرة في نفس الرسالة! هذا هدر كبير لحجم البيانات المنقولة.
الـ Serialization والـ Deserialization البطيء
تحويل كائن (object) في لغة البرمجة إلى نص JSON، والعكس، هي عملية تستهلك من وقت المعالج (CPU). صحيح إنها سريعة في الأحجام الصغيرة، لكن لما تتعامل مع بيانات ضخمة ومعقدة، هاي العملية بتصير عنق زجاجة (bottleneck) واضح ومؤثر على الأداء.
انعدام العقود الصارمة (Lack of Strict Contracts)
في عالم REST، العقد بين العميل والسيرفر غالباً ما يكون مجرد توثيق (documentation) مكتوب على جنب (باستخدام أدوات مثل OpenAPI/Swagger). من السهل جداً على مطور في فريق معين إنه يغير اسم حقل أو نوعه، ويكسر كل الخدمات اللي بتعتمد عليه بدون ما يحس. ما في شي بيجبر الطرفين على الالتزام بنفس “اللغة” بشكل صارم على مستوى الكود.
مرحباً بـ gRPC: المنقذ الذي لم نكن نعلم أننا بحاجته
بعد ليلتنا الطويلة مع وحش الـ JSON، بلشنا نبحث عن بدائل. وهون تعرفنا على gRPC. في البداية، كان في شوية تردد من الفريق، “شو هاد الأشي الجديد؟”، “رح ياخد وقت نتعلمه”، “خلينا على اللي بنعرفه”. بس أنا كنت مصرّ، وعملنا إثبات مفهوم (Proof of Concept) صغير، والنتائج كانت مذهلة لدرجة أقنعت الجميع.
ما هو gRPC؟ ببساطة يا جماعة
gRPC هو اختصار لـ “gRPC Remote Procedure Call”. هو إطار عمل (framework) مفتوح المصدر وعالي الأداء طورته جوجل. باختصار، بيسمحلك تستدعي دالّة (function) موجودة على سيرفر بعيد كأنها دالّة محلية عندك في الكود، وبشكل فعال جداً.
الأسلحة السرية: Protocol Buffers و HTTP/2
قوة gRPC الحقيقية تكمن في طبقتين أساسيتين بيعتمد عليهم:
- Protocol Buffers (Protobuf): هاي هي لغة gRPC. بدل ما تستخدم JSON النصي، بتستخدم Protobuf، وهو صيغة ثنائية (binary) لعملية الـ serialization. بتعرّف شكل البيانات (schema) في ملف خاص اسمه
.proto، ومنه بتقدر تولّد كود العميل والسيرفر بلغات برمجة مختلفة (Go, Java, Python, C#, …). لأنه ثنائي، حجمه أصغر بكثير من JSON وسرعة تحليله فائقة. - HTTP/2: بينما تستخدم معظم واجهات REST بروتوكول HTTP/1.1، يعمل gRPC فوق HTTP/2. هذا بيعطيه مزايا خارقة مثل:
- Multiplexing: إمكانية إرسال عدة طلبات واستجابات على نفس الاتصال (connection) في نفس الوقت، بدون ما واحد يblok التاني.
- Streaming: دعم أصيل للبث الثنائي الاتجاه (bidirectional streaming)، يعني السيرفر والعميل بيقدروا يبعتوا لبعض سيل من الرسائل بشكل مستمر.
- Header Compression: ضغط الـ headers لتقليل حجم البيانات المنقولة.
‘
‘
لنُشمّر عن سواعدنا: مثال عملي (Go و gRPC)
الحكي النظري حلو، بس خلينا نشوف الكود. لنفترض بدنا نبني خدمة بسيطة بترسل تحية. هاي هي الخطوات:
الخطوة الأولى: تعريف الخدمة بملف .proto
أول شي، بنعمل ملف اسمه greeting/greeting.proto وبنعرّف فيه شكل الرسائل والخدمة نفسها.
// greeting/greeting.proto
syntax = "proto3";
package greeting;
option go_package = "greetingpb";
// الخدمة اللي بتقدم التحية
service Greeter {
// دالّة بسيطة بترسل تحية
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// الرسالة اللي بيحتوي عليها الطلب
message HelloRequest {
string name = 1;
}
// الرسالة اللي بيحتوي عليها الرد
message HelloResponse {
string message = 1;
}
لاحظ كيف العقد (contract) واضح ومحدد بشكل صارم. نوع البيانات، أسماء الحقول، كل شي معرف. ما في مجال للخطأ.
الخطوة الثانية: توليد الكود (السحر يبدأ هنا)
باستخدام مترجم Protobuf (protoc)، بنقدر نولّد كود السيرفر والعميل بلغة Go (أو أي لغة أخرى تدعمها).
# أمر لتوليد كود Go من ملف الـ proto
protoc --go_out=. --go_opt=paths=source_relative
--go-grpc_out=. --go-grpc_opt=paths=source_relative
greeting/greeting.proto
هذا الأمر رح يولّد ملفات Go بتحتوي على كل الكود اللازم للـ serialization والـ networking. إنت ما عليك إلا إنك تكتب المنطق البرمجي للخدمة.
الخطوة الثالثة: كتابة السيرفر (Server)
الآن، نكتب المنطق الفعلي لدالّة SayHello في ملف server/main.go.
// server/main.go
package main
import (
"context"
"fmt"
"log"
"net"
pb "path/to/your/greetingpb" // استيراد الكود المولّد
"google.golang.org/grpc"
)
// تعريف الـ struct الخاص بالسيرفر
type server struct{
pb.UnimplementedGreeterServer // للتوافقية المستقبلية
}
// تطبيق دالّة SayHello من الـ interface المولّد
func (*server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
log.Printf("Received a request with name: %v", req.GetName())
message := "أهلاً وسهلاً يا " + req.GetName()
res := &pb.HelloResponse{
Message: message,
}
return res, nil
}
func main() {
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
log.Println("Server is running on port 50051...")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
الخطوة الرابعة: كتابة العميل (Client)
وأخيراً، نكتب العميل اللي رح يستدعي هاي الخدمة في ملف client/main.go.
// client/main.go
package main
import (
"context"
"log"
pb "path/to/your/greetingpb"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("Could not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
req := &pb.HelloRequest{Name: "أبو عمر"}
res, err := c.SayHello(context.Background(), req)
if err != nil {
log.Fatalf("Error while calling SayHello RPC: %v", err)
}
log.Printf("Response from server: %s", res.GetMessage())
}
شوف ما أبسط الموضوع! العميل استدعى c.SayHello(...) كأنها دالّة محلية عادية، وكل تعقيدات الشبكة والـ serialization تمت معالجتها خلف الكواليس بواسطة gRPC.
النتائج على أرض الواقع: قبل وبعد gRPC
بعد ما طبقنا gRPC على الخدمتين الحساستين في نظامنا، النتائج كانت مبهرة بكل معنى الكلمة:
- سرعة الاستجابة (Latency): متوسط زمن الاستجابة بين الخدمتين نزل من حوالي 200ms في أوقات الذروة إلى أقل من 30ms. تحسن بنسبة تفوق 85%!
- استخدام الشبكة (Bandwidth): حجم البيانات المنقولة انخفض بشكل كبير. رسالة كانت بحجم 1MB بصيغة JSON، صارت أقل من 200KB بصيغة Protobuf.
- استهلاك المعالج (CPU): لاحظنا انخفاض في استهلاك الـ CPU على كلا الخدمتين، لأن عملية الـ serialization/deserialization الثنائية أكفأ بكثير.
نصائح من قلب الميدان (من أبو عمر شخصياً)
بعد ما خضنا هاي التجربة، تعلمت كم شغلة بحب أشاركها معكم:
- لا ترمي كل شيء دفعة واحدة: الانتقال لـ gRPC لا يعني إنك لازم تعيد كتابة كل خدماتك. ابدأ بالخدمات الأكثر حساسية للأداء، اللي فيها اتصال كثير (chatty services). ابدأ صغير، اثبت النجاح، وبعدين توسع.
- الـ API Gateway هو صديقك: gRPC ممتاز للاتصال الداخلي بين الخدمات. لكن المتصفحات والتطبيقات الخارجية لسه بتحكي REST/JSON. استخدم API Gateway مثل Envoy أو Traefik اللي بيقدر يستقبل طلبات HTTP/JSON من الخارج ويترجمها لطلبات gRPC للخدمات الداخلية.
- استثمر في الـ Tooling: تعلم كيف تستخدم أدوات مثل
grpcurl(زيcurlبس لـ gRPC) و BloomRPC لتجربة واجهات gRPC واختبارها بسهولة. - فكر بالـ Streaming: قوة gRPC الحقيقية تظهر في الـ streaming. إذا عندك حالة استخدام تحتاج إرسال أو استقبال تدفق من البيانات (مثل تحديثات حية، رفع ملفات كبيرة)، gRPC هو الخيار الأمثل.
الخلاصة: هل gRPC هو الحل لكل شيء؟
لا، وبكل صراحة. gRPC ليس الرصاصة الفضية اللي بتحل كل المشاكل. REST/JSON ما زال خيار ممتاز جداً، وربما الأفضل، للواجهات العامة (Public APIs) اللي بيتعامل معها مطورين من خارج شركتك، أو للتطبيقات البسيطة اللي ما بتحتاج أداء فائق.
الفكرة هي أن تعرف أدواتك جيداً، وتعرف متى تستخدم كل أداة. gRPC أداة قوية جداً في صندوق عدة المطور، خصوصاً في عالم الخدمات المصغرة المعقد والمتشابك. لما يكون الأداء والفعالية هما الأولوية القصوى في الاتصال الداخلي بين خدماتك، gRPC هو صديقك الذي لن يخذلك.
أتمنى تكون القصة والتفاصيل هاي فادتكم. لا تخافوا من تجربة التقنيات الجديدة، لأنها ممكن تكون هي طوق النجاة اللي بتدوروا عليه. يلا، شدّوا حيلكم! 🚀