تصميم واجهات البرمجة وأنماط الخدمات المصغرة

gRPC وGraphQL

4 دقيقة للقراءة

REST ليست الطريقة الوحيدة لبناء واجهات البرمجة. gRPC تهيمن على التواصل الداخلي بين الخدمات المصغرة، بينما GraphQL تتفوق في الاستعلامات المرنة التي يحددها العميل. معرفة متى تستخدم كل منها عامل تمييز رئيسي في المقابلات.

Protocol Buffers (Protobuf)

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

// user.proto — صيغة Proto3
syntax = "proto3";

package userservice;

// تعريف الرسائل مع حقول مرقمة
message User {
  string id = 1;            // رقم الحقل 1 — لا تعد استخدامه بعد الحذف
  string name = 2;
  string email = 3;
  UserRole role = 4;
  repeated Address addresses = 5;  // repeated = قائمة/مصفوفة
  optional string phone = 6;       // حقل اختياري (proto3)
}

enum UserRole {
  USER_ROLE_UNSPECIFIED = 0;  // Proto3 يتطلب 0 كقيمة افتراضية
  USER_ROLE_ADMIN = 1;
  USER_ROLE_MEMBER = 2;
}

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

قواعد التوافق العكسي

  • لا تغيّر رقم حقل موجود أبدًا
  • لا تعد استخدام رقم حقل محذوف — استخدم reserved بدلاً من ذلك
  • إضافة حقول جديدة آمنة — العملاء القدامى يتجاهلون الحقول غير المعروفة
  • حذف الحقول آمن — العملاء الجدد يرون القيم الافتراضية للحقول المفقودة
  • إعادة تسمية الحقول آمنة — فقط رقم الحقل مهم على السلك
// حجز أرقام الحقول المحذوفة لمنع إعادة الاستخدام العرضي
message User {
  reserved 7, 8;                    // أرقام الحقول هذه متقاعدة
  reserved "legacy_username";       // اسم الحقل هذا متقاعد
  string id = 1;
  string name = 2;
}

تعريفات خدمة gRPC

تدعم gRPC أربعة أنماط تواصل، كل منها مناسب لحالات استخدام مختلفة.

// service.proto
syntax = "proto3";

package orderservice;

service OrderService {
  // 1. أحادي: طلب-استجابة بسيط (مثل REST)
  rpc GetOrder(GetOrderRequest) returns (Order);

  // 2. بث من الخادم: الخادم يرسل استجابات متعددة
  rpc WatchOrderStatus(WatchRequest) returns (stream OrderStatus);

  // 3. بث من العميل: العميل يرسل طلبات متعددة
  rpc UploadOrderDocuments(stream Document) returns (UploadSummary);

  // 4. بث ثنائي الاتجاه: كلا الجانبين يرسلان تدفقات
  rpc OrderChat(stream ChatMessage) returns (stream ChatMessage);
}

message GetOrderRequest {
  string order_id = 1;
}

message Order {
  string id = 1;
  string customer_id = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;
  double total = 5;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double price = 3;
}

message OrderStatus {
  string order_id = 1;
  string status = 2;
  string timestamp = 3;
}

message WatchRequest {
  string order_id = 1;
}

message Document {
  string filename = 1;
  bytes content = 2;
}

message UploadSummary {
  int32 files_received = 1;
  int64 total_bytes = 2;
}

message ChatMessage {
  string sender = 1;
  string content = 2;
  string timestamp = 3;
}

الأنماط الأربعة بالتفصيل

النمط حالة الاستخدام مثال
أحادي طلب-استجابة قياسي جلب طلب بالمعرف
بث من الخادم الخادم يدفع نتائج متعددة مؤشر أسعار الأسهم، تحديثات حالة الطلب الحية
بث من العميل العميل يرسل البيانات على أجزاء رفع الملفات، استيعاب بيانات دفعية
بث ثنائي الاتجاه تواصل فوري ثنائي الاتجاه المحادثة، التحرير التعاوني، الألعاب

تنفيذ خادم gRPC (مثال بلغة Go)

// server.go — تنفيذ الأحادي وبث الخادم
package main

import (
    "context"
    "log"
    "time"
    pb "myapp/proto/orderservice"
)

type orderServer struct {
    pb.UnimplementedOrderServiceServer
}

// RPC أحادي
func (s *orderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
    // جلب الطلب من قاعدة البيانات
    order, err := db.FindOrder(req.OrderId)
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "الطلب %s غير موجود", req.OrderId)
    }
    return order, nil
}

// RPC بث من الخادم
func (s *orderServer) WatchOrderStatus(req *pb.WatchRequest, stream pb.OrderService_WatchOrderStatusServer) error {
    orderID := req.OrderId
    for {
        status, err := db.GetOrderStatus(orderID)
        if err != nil {
            return err
        }
        if err := stream.Send(status); err != nil {
            return err
        }
        if status.Status == "delivered" {
            return nil // البث مكتمل
        }
        time.Sleep(2 * time.Second) // فترة الاستقصاء
    }
}

مزايا HTTP/2

تعمل gRPC على HTTP/2 الذي يوفر مزايا أداء كبيرة مقارنة بـ HTTP/1.1.

الميزة HTTP/1.1 HTTP/2
التعدد طلب واحد لكل اتصال (أو التسلسل مع حجب رأس الصف) تدفقات متعددة متزامنة على اتصال واحد
الترويسات تُرسل كنص عادي مع كل طلب مضغوطة مع HPACK، فقط الفروق تُرسل
الصيغة نصية طبقة إطارات ثنائية
دفع الخادم غير مدعوم الخادم يستطيع دفع الموارد استباقيًا
الاتصال اتصالات TCP متعددة مطلوبة اتصال TCP واحد لجميع التدفقات

مقارنة أداء gRPC مع REST

الجانب REST (JSON/HTTP 1.1) gRPC (Protobuf/HTTP 2)
حجم الحمولة أكبر بـ 2-10 مرات (نص JSON) ترميز ثنائي مضغوط
سرعة التسلسل أبطأ (تحليل نصي) أسرع بـ 5-10 مرات (ثنائي)
البث يتطلب WebSocket أو SSE بث ثنائي الاتجاه أصلي
دعم المتصفح شامل يتطلب وكيل gRPC-Web
الأدوات curl وPostman وأي عميل HTTP يحتاج مترجم protoc وأدوات gRPC
العقد OpenAPI/Swagger (اختياري) ملفات .proto (مطلوبة، محددة النوع بقوة)
توليد الكود اختياري مدمج (Go وJava وPython وغيرها)

GraphQL

تتيح GraphQL للعملاء طلب البيانات التي يحتاجونها بالضبط — لا أكثر ولا أقل. تستخدم نقطة نهاية واحدة ونظام أنواع لتعريف الاستعلامات الممكنة.

تعريف المخطط

// schema.graphql
type User {
  id: ID!
  name: String!
  email: String!
  orders(status: OrderStatus, first: Int): [Order!]!
}

type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
  items: [OrderItem!]!
  createdAt: String!
}

type OrderItem {
  product: Product!
  quantity: Int!
  price: Float!
}

type Product {
  id: ID!
  name: String!
  price: Float!
}

enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

type Query {
  user(id: ID!): User
  users(first: Int, after: String): UserConnection!
  order(id: ID!): Order
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  cancelOrder(id: ID!): Order!
}

type Subscription {
  orderStatusChanged(orderId: ID!): Order!
}

input CreateOrderInput {
  userId: ID!
  items: [OrderItemInput!]!
}

input OrderItemInput {
  productId: ID!
  quantity: Int!
}

# تصفح على نمط Relay
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

مشكلة N+1 وDataLoader

أهم مشكلة أداء في GraphQL. بدون DataLoader، جلب قائمة طلبات مع منتجاتها يُجري استعلامًا واحدًا للطلبات + N استعلام للمنتجات.

// بدون DataLoader: استعلامات N+1
// الاستعلام: { users { orders { items { product { name } } } } }
// استعلام واحد للمستخدمين
// N استعلام لطلبات كل مستخدم
// M استعلام لعناصر كل طلب
// K استعلام لمنتج كل عنصر

// مع DataLoader: استعلامات مجمعة
import DataLoader from 'dataloader';

// DataLoader تجمع التحميلات الفردية في استعلام واحد
const productLoader = new DataLoader<string, Product>(async (productIds) => {
  // استعلام واحد: SELECT * FROM products WHERE id IN (id1, id2, id3, ...)
  const products = await db.products.findMany({
    where: { id: { in: productIds as string[] } }
  });

  // الإرجاع بنفس ترتيب المعرفات المدخلة
  const productMap = new Map(products.map(p => [p.id, p]));
  return productIds.map(id => productMap.get(id) || new Error(`المنتج ${id} غير موجود`));
});

// المحلل يستخدم المحمّل
const resolvers = {
  OrderItem: {
    product: (item: OrderItem) => productLoader.load(item.productId)
    // DataLoader تجمع تلقائيًا جميع التحميلات في نفس الدورة
  }
};

متى تستخدم كلًا منها: مصفوفة القرار

المعيار REST gRPC GraphQL
الأفضل لـ الواجهات البرمجية العامة، CRUD البسيط الخدمات المصغرة الداخلية، الأداء العالي استعلامات العميل المرنة، تطبيقات الجوال
نوع العميل أي عميل HTTP عملاء مولّدون من .proto الويب/الجوال مع احتياجات بيانات متنوعة
شكل البيانات محدد من الخادم محدد بعقد .proto يختاره العميل لكل استعلام
الأداء جيد ممتاز (ثنائي، HTTP/2) جيد (لكن عبء المحللات)
البث SSE أو WebSocket (إضافي) أصلي، ثنائي الاتجاه اشتراكات (مبنية على WebSocket)
دعم المتصفح شامل يتطلب وكيل (gRPC-Web) شامل
منحنى التعلم منخفض متوسط (proto، توليد كود) متوسط-عالٍ (مخطط، محللات، تخزين مؤقت)
تطوير الواجهة يحتاج إصدارات توافق عكسي مدمج تطور المخطط (إهمال)
التخزين المؤقت تخزين HTTP مؤقت (GET) مخصص (بدون تخزين HTTP مؤقت) معقد (يعتمد على الاستعلام)
معالجة الأخطاء رموز حالة HTTP رموز حالة gRPC دائمًا 200، الأخطاء في جسم الاستجابة
العقد OpenAPI (اختياري) .proto (مطلوب) المخطط (مطلوب)

إجابة مقابلة سريعة

"سأستخدم REST لواجهة برمجية عامة حيث البساطة ودعم العملاء الواسع مهمان. للتواصل الداخلي بين الخدمات المصغرة، سأختار gRPC بسبب ترميزها الثنائي وتعدد HTTP/2 والبث الأصلي — زمن الاستجابة بين الخدمات حرج. لتطبيق جوال أو كثيف الواجهة مع متطلبات بيانات متنوعة، سأضع GraphQL كطبقة تجميع فوق الخدمات المصغرة، حتى يتمكن العملاء من طلب ما يحتاجونه بالضبط في رحلة واحدة ذهابًا وإيابًا."

التالي: سنتعمق في أنماط بنية الخدمات المصغرة — الملاحم وقواطع الدائرة وCQRS ومصادر الأحداث. :::

اختبار

اختبار الوحدة 3: تصميم واجهات البرمجة وأنماط الخدمات المصغرة

خذ الاختبار