Protocol Buffers عملياً: تسريع نقل البيانات بين الخدمات

Protocol Buffers عملياً: تسريع نقل البيانات بين الخدمات

في الأنظمة الكبيرة التي تعتمد على خدمات متعددة (Microservices)، يصبح شكل تمثيل البيانات عاملًا أساسيًا في الأداء. أغلب المطورين يبدأون باستخدام JSON لأنه بسيط وقابل للقراءة، لكن مع زيادة عدد الطلبات وحجم البيانات، تظهر الحاجة إلى حل أسرع وأكثر كفاءة. هنا يأتي دور Protocol Buffers من جوجل.

في هذا Protocol Buffers Tutorial سنشرح بشكل عملي كيف تستخدم Protobuf لتسريع الاتصال بين الخدمات، وما الفرق بينه وبين JSON، وكيف يمكن دمجه في تصميم الأنظمة الكبيرة مع REST أو gRPC.

ما هو Protocol Buffers (Protobuf)؟

Protocol Buffers هو نظام ترميز بيانات ثنائي (Binary Serialization) تم تطويره بواسطة جوجل. فكر فيه كبديل لـ JSON أو XML لكن:

  • أصغر في الحجم عند الإرسال عبر الشبكة
  • أسرع في التشفير/فك التشفير (Serialization/Deserialization)
  • يملك تعريفًا ثابتًا للرسالة (Schema) من خلال ملفات .proto
  • يدعم عددًا كبيرًا من لغات البرمجة (Java, C#, Python, Go, C++, Node.js وغيرها)

الفكرة الأساسية: بدل إرسال نص مثل JSON، يتم إرسال بيانات ثنائية مضغوطة مبنية على تعريف رسالة مشترك بين العميل والخادم.

لماذا Protobuf أسرع من JSON؟

لفهم أهمية Protobuf، علينا فهم تأثير تمثيل البيانات على أداء الأنظمة، خصوصًا في بيئات System Design في الأنظمة الكبيرة:

  • JSON نصي (Text-based): كل قيمة تتحول لنص، بما في ذلك الأرقام والـ booleans، ثم يتم تحويلها مرة أخرى في الطرف الآخر.
  • Protobuf ثنائي (Binary): البيانات تُخزن في شكل قريب من التمثيل الداخلي في الذاكرة، ما يقلل من تكلفة التحويل.

النتيجة:

  • حجم الرسالة بـ Protobuf غالبًا أصغر 2-10 مرات من JSON حسب نوع البيانات.
  • التحويل (Serialization) أسرع لأن مكتبات Protobuf تعمل مباشرة على الـ bytes.
  • فك التشفير كذلك أسرع وأسهل على الـ CPU، وهذا مهم جدًا عند وجود ملايين الطلبات في الثانية أو في حالات التوسّع الأفقي للأنظمة.

بنية ملفات Protobuf: ما هو ملف .proto؟

ملف .proto هو المكان الذي تعرّف فيه شكل الرسائل التي ستتبادلها الخدمات. يمكنك اعتباره مشابهًا لتعريف الـ DTO أو Models في لغتك، لكن بشكل مستقل عن اللغة.

مثال بسيط لتعريف مستخدم:

syntax = "proto3";

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  bool is_active = 4;
}

ملاحظات مهمة في هذا المثال:

  • syntax = "proto3": تحدد أننا نستخدم الإصدار الثالث من Protobuf.
  • message User: تعريف رسالة (Struct/Model) باسم User.
  • int32, string, bool: أنواع البيانات المدعومة من Protobuf.
  • = 1, 2, 3...: رقم الحقل (Field Number) وهو عنصر أساسي في Protobuf يستخدم لتمييز الحقول داخل الرسالة.

أهمية أرقام الحقول (Field Numbers)

أرقام الحقول لا تُستخدم فقط للترتيب؛ بل هي جزء من البروتوكول نفسه. عند إرسال البيانات، لا يُرسل اسم الحقل نصًا، بل يُستخدم رقم الحقل لتحديده. لهذا:

  • يجب أن تكون فريدة داخل نفس الرسالة.
  • تحتاج إلى الحرص عند تعديل الرسالة حتى لا تغيّر معاني الحقول القديمة.
  • القيم بين 1 و 15 تُشفر بحجم أصغر؛ لذا من الأفضل استخدامها للحقول الأكثر استخدامًا.

أنواع البيانات المدعومة في Protobuf

Protobuf يدعم مجموعة كبيرة من الأنواع الأساسية:

  • عددية صحيحة: int32, int64, uint32, uint64, sint32, sint64
  • أعداد عشرية: float, double
  • نصيّة: string
  • ثنائية: bytes لتخزين بيانات خام (مثل صور صغيرة، أو بيانات مشفرة).
  • منطقية: bool.

ويمكنك تعريف:

  • رسائل متداخلة (Nested Messages).
  • قوائم باستخدام الكلمة repeated.
  • Enums لتمثيل قيم محددة.

مثال أكثر تقدمًا:

syntax = "proto3";

message Address {
  string city = 1;
  string street = 2;
  string country = 3;
}

enum UserRole {
  USER = 0;
  ADMIN = 1;
  SUPPORT = 2;
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  bool is_active = 4;
  Address address = 5;
  repeated string tags = 6;
  UserRole role = 7;
}

تثبيت Protobuf والعمل مع Compiler

الاستخدام العملي لـ Protobuf يعتمد على وجود Compiler اسمه protoc يقوم بتحويل ملفات .proto إلى كود جاهز في لغتك (Classes, Structs, Types).

الخطوة 1: تثبيت Protobuf Compiler

يمكن تثبيته من صفحة GitHub الرسمية لـ protobuf أو من مدير الحزم في نظام التشغيل (apt, brew, choco…). بعد التثبيت تأكد من أن الأمر:

protoc --version

يعطيك رقم إصدار بدون مشاكل.

الخطوة 2: تعريف ملف .proto

لنفرض أن لدينا ملفًا باسم user.proto يحتوي على التعريف السابق لـ User.

الخطوة 3: توليد الكود للغة محددة

مثال بلغة Python (لأنها منتشرة بين قرّاء المدونة ولدينا مقالات مثل البرمجة غير المتزامنة في بايثون):

protoc --python_out=. user.proto

سينتج ملف Python (مثل user_pb2.py) يحتوي على كلاس User و Address و UserRole جاهزة للاستخدام.

بنفس الطريقة، للغات أخرى:

  • Java: استخدام --java_out
  • C#: استخدام --csharp_out
  • Go: استخدام --go_out

مثال عملي: استخدام Protobuf في كود حقيقي

سنأخذ مثالًا بسيطًا بلغة Python يُظهر الفرق بين JSON و Protobuf في التمثيل والاستخدام.

الخطوة 1: تعريف الرسالة في user.proto

syntax = "proto3";

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  bool is_active = 4;
}

الخطوة 2: توليد الكود لـ Python

protoc --python_out=. user.proto

الخطوة 3: استخدام الرسالة في الكود

import user_pb2
import json

# إنشاء كائن User باستخدام Protobuf
u = user_pb2.User()
u.id = 1
u.name = "Ahmed"
u.email = "[email protected]"
u.is_active = True

# تحويل إلى bytes لإرسالها عبر الشبكة
data_protobuf = u.SerializeToString()

# للمقارنة: تمثيل نفس البيانات بـ JSON
user_json = {
    "id": 1,
    "name": "Ahmed",
    "email": "[email protected]",
    "is_active": True
}
data_json = json.dumps(user_json).encode("utf-8")

print("Protobuf size:", len(data_protobuf))
print("JSON size:", len(data_json))

# في الطرف الآخر (الخادم) نفك التشفير:
u2 = user_pb2.User()
u2.ParseFromString(data_protobuf)
print(u2.name, u2.email)

في هذا المثال، ستلاحظ غالبًا أن حجم Protobuf أصغر من JSON، وخاصة مع رسائل أكبر أو تحتوي على أعداد صحيحة كثيرة.

Protobuf مع gRPC: تسريع الاتصال بين الخدمات

الاستخدام الأشهر لـ Protobuf اليوم هو مع gRPC، وهو إطار عمل من جوجل يعتمد على HTTP/2 وProtobuf لتوفير:

  • اتصال سريع بين الخدمات (Microservices)
  • دعم Streaming ثنائي الاتجاه
  • تعريف واجهات الخدمات في ملفات .proto نفسها

مع gRPC لا تستخدم فقط الرسائل (message)، بل تعرّف أيضًا الخدمات (service) و الـ RPC methods داخل الملف.

مثال مبسط لتعريف خدمة في user.proto:

syntax = "proto3";

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
}

message GetUserRequest {
  int32 id = 1;
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  bool is_active = 4;
}

عند تشغيل protoc مع إضافات gRPC، سيتم توليد كود الخادم (Server Stub) والعميل (Client Stub)، مما يجعل استدعاء الخدمات يشبه استدعاء دوال عادية بدل التعامل مع HTTP يدويًا.

مقارنة عملية: Protobuf vs JSON في الأنظمة الكبيرة

من منظور تصميم الأنظمة وأداء الخدمات، يمكن تلخيص الفرق كالتالي:

التكلفة على الشبكة

  • JSON: حجم أكبر، استهلاك أعلى للـ Bandwidth، خاصة في أنظمة ذات عدد كبير من الطلبات.
  • Protobuf: حجم أصغر، مناسب أكثر للـ Microservices والاتصال بين الخدمات الداخلية.

أداء المعالجة (CPU)

  • JSON: يحتاج إلى Parser نصي، يستهلك وقتًا وموارد أكثر.
  • Protobuf: parsing ثنائي أسرع، وتكلفته أقل على CPU، مما يساعدك على استغلال الموارد في منطق العمل الفعلي.

تطور الـ Schema مع الوقت

  • JSON: مرن جدًا، يمكن إضافة/إزالة حقول بسهولة لكن بدون قاعدة صارمة، مما قد يسبب مشاكل توافقية غير متوقعة.
  • Protobuf: لديه قواعد واضحة لتعديل الرسائل (الاحتفاظ بأرقام الحقول، عدم تغيير النوع، إلخ) مما يساعد على الحفاظ على التوافق بين الإصدارات.

أفضل الممارسات عند استخدام Protobuf في مشروعك

لتحصل على أقصى استفادة من Protobuf، من المهم اتباع بعض الممارسات:

1. تصميم الـ Schema بحذر

  • اختر أرقام الحقول بعناية، واحتفظ بتوثيق لأي حقل لم يعد مستخدمًا.
  • تجنب إعادة استخدام رقم حقل قديم بمعنى جديد؛ هذا سيكسر التوافق مع الإصدارات السابقة.
  • استخدم repeated للقوائم بدل إرسال JSON string داخل حقل واحد.

2. موازنة القراءة البشرية مع الأداء

Protobuf ثنائي وغير قابل للقراءة المباشرة مثل JSON، لكن يمكنك استخدام أدوات لتفريغها (مثل protoc --decode) عندما تحتاج إلى Debugging.

في كثير من التصاميم، يتم استخدام JSON لواجهات REST العامة، وProtobuf للاتصال الداخلي بين الخدمات حيث الأداء أهم.

3. فهم متى تستخدم Protobuf

  • عند وجود اتصال كثيف بين الخدمات الداخلية.
  • عند الحاجة إلى سرعة استجابة عالية وتخفيض الحمل على الشبكة.
  • في الأنظمة التي تخدم ملايين المستخدمين وتحتاج إلى توسّع أفقي مستمر.

أما في التطبيقات الصغيرة أو APIs العامة المفتوحة للمطورين، قد يكون JSON خيارًا أبسط وأكثر توافقًا، خاصةً إذا لم يكن حمل الشبكة عائقًا كبيرًا في البداية.

دمج Protobuf في بيئة عملك الحالية

في مشروعات موجودة مسبقًا تعتمد على JSON وREST، يمكنك اعتماد نهج تدريجي:

  1. ابدأ بالخدمات الداخلية التي تتبادل أكبر كم من البيانات.
  2. عرّف رسائل Protobuf تعكس نفس الـ Models الحالية.
  3. أضف Layer جديد في كل خدمة لتحويل JSON <-> Protobuf إن لزم التعامل مع عملاء قدامى.
  4. مع الوقت، انتقل إلى gRPC للخدمات التي تحتاج أعلى أداء.

يمكنك ربط هذا مع مفاهيم تصميم الأنظمة التي تطرقنا لها سابقًا في مقال مقدمة في System Design للمطورين، خاصّة عندما تصمم واجهات بين الخدمات المختلفة.

الخلاصة: متى يكون Protobuf خيارًا منطقيًا؟

Protocol Buffers ليس مجرد تقنية جديدة لتجربتها؛ هو أداة مصممة خصيصًا لحل مشكلة واضحة: نقل بيانات أسرع وأصغر بين الخدمات. في المشاريع الصغيرة، لن تشعر بفارق كبير، لكن في الأنظمة الكبيرة ذات الـ Microservices الكثيفة، يمكن أن يحدث فرقًا واضحًا في:

  • زمن الاستجابة (Latency)
  • حجم البيانات المنقولة عبر الشبكة
  • استهلاك CPU في الخدمات

إذا كنت تعمل على نظام ينمو باستمرار، أو تبني بنية Microservices جديدة، أو تفكر في حلول لتسريع الاتصال بين الخدمات، فإن فهم واستخدام Protobuf عمليًا خطوة منطقية جدًا، وستساعدك على تصميم واجهات أكثر كفاءة وقابلة للتوسع.

يمكنك البدء بتجربة بسيطة، تحويل جزء صغير من الاتصال بين خدمتين من JSON إلى Protobuf، وقياس الأداء، ثم تعميم التجربة على باقي أجزاء النظام إن كانت النتائج مرضية.

حول المحتوى:

شرح عملي لكيفية استخدام Protobuf لتسريع الاتصال بين الخدمات في الأنظمة الكبيرة مقارنة بـ JSON.

هل كان هذا مفيدًا لك؟

أضف تعليقك