لماذا تفضل الخدمات الصغيرة الحديثة gRPC على REST؟

لماذا تفضل الخدمات الصغيرة الحديثة gRPC على REST؟

في البنية المتجانسة، تتواصل المكونات عبر استدعاءات الأسلوب داخل الذاكرة، والتي تكون فورية وموثوقة للغاية. ومع ذلك، عند الانتقال إلى بنية الخدمات الصغيرة، يتم فصل هذه المكونات بحدود الشبكة. يصبح الاتصال عبارة عن مكالمة شبكة خارج العملية (Inter-Process Communication أو IPC).

لسنوات، كان REST (نقل الحالة التمثيلية) عبر HTTP/1.1 مع حمولات JSON هو المعيار الافتراضي لإنشاء واجهات برمجة تطبيقات الويب. على الرغم من أن REST يعد ممتازًا لخدمات الويب العامة والتفاعل بين العميل والخادم، إلا أنه يقدم اختناقات كبيرة عند استخدامه للاتصالات الداخلية بين الخدمات ذات التردد العالي وزمن الوصول المنخفض.

ولهذا السبب تتحول الخدمات الصغيرة الحديثة بسرعة نحو gRPC (استدعاء الإجراء عن بعد من Google). في هذه المقالة، سنقوم بتحليل قيود REST، ونحلل الركائز المعمارية لـ gRPC، ونستعرض التنفيذ الكامل لخدمة gRPC في Java.


1. اختناقات REST في الخدمات الصغيرة

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

أ. الحمل الزائد للنص العادي JSON

JSON قابل للقراءة من قبل الإنسان، مما يجعل تصحيح الأخطاء أمرًا سهلاً، ولكنه غير فعال للغاية للاتصال من آلة إلى آلة:

  • تكلفة التسلسل/إلغاء التسلسل: تحليل الرموز المميزة للسلسلة النصية وتحويلها إلى كائنات يستهلك دورات كبيرة من وحدة المعالجة المركزية.
  • حجم حمولة كبير: يتم تكرار مفاتيح JSON في كل طلب على حدة (على سبيل المثال، {"transactionId": "123", "amount": 99.99}). بالنسبة للأنظمة التي تعالج ملايين الطلبات في الثانية، فإن هذه البيانات التعريفية الزائدة عن الحاجة تهدر نطاقًا تردديًا هائلاً للشبكة.

ب. قيود اتصال HTTP/1.1

يتم تشغيل REST عادةً على HTTP/1.1، والذي يُظهر العديد من أوجه القصور الهيكلية:

  • الحظر من رأس الخط (HoL): في اتصال TCP واحد، يجب على العميل انتظار استجابة الطلب الحالي قبل إرسال الطلب التالي.
  • استنفاد تجمع الاتصالات: للتعامل مع الطلبات المتزامنة، يجب على العملاء فتح اتصالات TCP متعددة. وينتج عن ذلك حمل كبير لموارد نظام التشغيل، واستنفاد المقبس، وعقوبات على الأداء البطيء حيث يتم فتح الاتصالات وإغلاقها بشكل مستمر.

ج. عقود API الضعيفة

تفتقر واجهات برمجة تطبيقات REST إلى عقد مدمج لوقت الترجمة. بينما يساعد OpenAPI/Swagger في توثيق واجهات برمجة التطبيقات، إلا أنها منفصلة عن قاعدة التعليمات البرمجية. من السهل على مطور الواجهة الخلفية تغيير اسم حقل JSON وتعطيل الخدمات النهائية عن طريق الخطأ دون تحذيرات وقت الترجمة.


2. الركائز المعمارية لـ gRPC

تم تقديم gRPC بواسطة Google في عام 2015، وهو إطار عمل RPC مفتوح المصدر وعالي الأداء مصمم خصيصًا للتطبيقات السحابية الأصلية. إنه يحل اختناقات REST باستخدام ثلاث تقنيات رئيسية:

الركائز المعمارية الثلاثة لـ gRPC: المخازن المؤقتة للبروتوكول، ونقل HTTP/2، وإنشاء التعليمات البرمجية

أ. مخازن البروتوكول المؤقتة (Protobuf)

بدلاً من JSON، يستخدم gRPC مخازن البروتوكول المؤقتة كلغة تعريف الواجهة (IDL) وتنسيق تسلسل الرسائل.

  • Protobuf عبارة عن آلية تسلسل ثنائية محسنة للغاية.
  • يتم تسلسل الحقول كعلامات رقمية صغيرة بدلاً من أسماء نصية.
  • الحمولات أصغر بما يصل إلى 70-80% من JSON، والتسلسل أسرع بما يصل إلى 10x، مما يقلل بشكل كبير من استخدام وحدة المعالجة المركزية وعرض النطاق الترددي.

ب. HTTP/2 تعدد الإرسال

يستخدم gRPC HTTP/2 كبروتوكول نقل خاص به، مما يوفر العديد من تحسينات الأداء:

  • تعدد الإرسال الحقيقي: يمكن تشذير الطلبات والاستجابات المتعددة في وقت واحد عبر اتصال TCP مفرد طويل الأمد، مما يؤدي إلى التخلص تمامًا من حظر رأس الخط.
  • ضغط الرأس (HPACK): يتم ضغط رؤوس HTTP، مما يؤدي إلى تقليل حجم الطلب.
  • دعم البث: يدعم HTTP/2 في الأصل تدفق العميل أحادي الاتجاه، وتدفق الخادم، والتدفق ثنائي الاتجاه الكامل.

ج. إنشاء الكود والعقود الصارمة

من خلال تحديد بنية واجهة برمجة التطبيقات في ملف .proto، يقوم gRPC بإنشاء بذرة العميل وفئات قاعدة الخادم بعشرات اللغات (Java وGo وPython وC++ وما إلى ذلك). العقد آمن في وقت الترجمة؛ إذا قام الخادم بتحديث واجهته، فستفشل إصدارات العميل على الفور إذا لم تتطابق مع العقد الجديد.


3. تنفيذ Java: إنشاء خدمة gRPC

لنقم ببناء تطبيق Java gRPC عملي. سنقوم بتنفيذ خدمة معالجة الدفع عالية الإنتاجية حيث يرسل العميل طلب دفع ويتلقى تأكيد المعاملة.

الخطوة 1: تحديد عقد API (payment.proto)

نبدأ بتحديد واجهة الخدمة وهياكل الرسائل في ملف Protocol Buffer.

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.ghaznix.grpc.payment";
option java_outer_classname = "PaymentProto";

package payment;

// The Payment Service definition
service PaymentService {
  // Unary RPC: Sends a single payment request and receives a confirmation
  rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}

// The Payment Request message
message PaymentRequest {
  string transaction_id = 1;
  string customer_id = 2;
  double amount = 3;
  string currency = 4;
}

// The Payment Response message
message PaymentResponse {
  string transaction_id = 1;
  string status = 2;
  string message = 3;
  string timestamp = 4;
}

الخطوة 2: تنفيذ منطق جانب الخادم

بعد تجميع ملف .proto (عادةً ما تتم معالجته تلقائيًا بواسطة مكونات Maven أو Gradle gRPC الإضافية)، تقوم الأداة بإنشاء فئة PaymentServiceImplBase. نقوم بتوسيع هذه الفئة لتنفيذ منطق الأعمال.

package com.ghaznix.grpc.payment;

import io.grpc.stub.StreamObserver;
import java.time.Instant;

public class PaymentServiceImpl extends PaymentServiceGrpc.PaymentServiceImplBase {

    @Override
    public void processPayment(PaymentRequest request, StreamObserver<PaymentResponse> responseObserver) {
        System.out.println("Received payment request for customer: " + request.getCustomerId() 
                + " with amount: " + request.getAmount() + " " + request.getCurrency());

        // Process payment logic (simulation)
        String status = request.getAmount() > 0 ? "SUCCESS" : "DECLINED";
        String message = status.equals("SUCCESS") ? "Payment processed successfully." : "Invalid payment amount.";

        PaymentResponse response = PaymentResponse.newBuilder()
                .setTransactionId(request.getTransactionId())
                .setStatus(status)
                .setMessage(message)
                .setTimestamp(Instant.now().toString())
                .build();

        // Send the response to the client
        responseObserver.onNext(response);
        
        // Signal that the RPC execution is complete
        responseObserver.onCompleted();
    }
}

الخطوة 3: تمهيد خادم gRPC

بعد ذلك، نقوم بإنشاء فئة خادم لبدء خادم gRPC على منفذ معين وتسجيل تنفيذ خدمتنا.

package com.ghaznix.grpc.payment;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class PaymentServer {

    private Server server;

    public void start() throws IOException {
        int port = 50051;
        server = ServerBuilder.forPort(port)
                .addService(new PaymentServiceImpl())
                .build()
                .start();
        
        System.out.println("gRPC Server started, listening on port " + port);

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("*** Shutting down gRPC server since JVM is shutting down");
            try {
                PaymentServer.this.stop();
            } catch (InterruptedException e) {
                e.printStackTrace(System.err);
            }
            System.err.println("*** Server shut down");
        }));
    }

    public void stop() throws InterruptedException {
        if (server != null) {
            server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
        }
    }

    public void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        final PaymentServer server = new PaymentServer();
        server.start();
        server.blockUntilShutdown();
    }
}

الخطوة 4: تنفيذ العميل (الحظر وعدم المزامنة)

من المزايا الرئيسية لـ gRPC أنها تولد نوعين من بذرة العميل:

  1. BlockingStub: تنفيذ متزامن يحظر (على غرار مكالمات REST القياسية).
  2. Stub: تنفيذ غير متزامن وغير محظور باستخدام مراقبي رد الاتصال.
package com.ghaznix.grpc.payment;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;

public class PaymentClient {

    private final ManagedChannel channel;
    private final PaymentServiceGrpc.PaymentServiceBlockingStub blockingStub;

    public PaymentClient(String host, int port) {
        // Create a communication channel to the server
        this.channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext() // Disables TLS for local development
                .build();
        
        // Create the synchronous blocking stub
        this.blockingStub = PaymentServiceGrpc.newBlockingStub(channel);
    }

    public void shutdown() throws InterruptedException {
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    public void executePayment(String txId, String customerId, double amount) {
        PaymentRequest request = PaymentRequest.newBuilder()
                .setTransactionId(txId)
                .setCustomerId(customerId)
                .setAmount(amount)
                .setCurrency("USD")
                .build();

        System.out.println("Sending payment request for: " + txId);
        
        // Execute unary synchronous call
        PaymentResponse response = blockingStub.processPayment(request);
        
        System.out.println("Server Response: " + response.getStatus() + " | " + response.getMessage());
    }

    public static void main(String[] args) throws InterruptedException {
        PaymentClient client = new PaymentClient("localhost", 50051);
        try {
            client.executePayment("TX-99081", "customer-abc-123", 250.75);
        } finally {
            client.shutdown();
        }
    }
}

4. المقارنة المعمارية: gRPC مقابل REST

يوضح الجدول أدناه الخصائص الأساسية لكلا النموذجين:

الميزة المعمارية REST (نقل الحالة التمثيلية) gRPC (استدعاء الإجراء عن بعد من Google)
البروتوكول HTTP/1.1 (قياسي)، HTTP/2 (اختياري) HTTP/2 (متطلبات صارمة)
تنسيق الحمولة نص عادي JSON، XML، HTML مخازن البروتوكول الثنائية (Protobuf)
** نموذج واجهة برمجة التطبيقات ** الموارد (مسارات URI + GET/POST/PUT/DELETE) الإجراءات عن بعد (الطرق/استدعاءات الوظائف)
جودة العقد فضفاض (OpenAPI/Swagger اختياري/خارجي) صارم (محدد في ملفات .proto عند التجميع)
التعدد لا (حظر رأس الخط في HTTP/1.1) نعم (تدفقات متعددة على اتصال TCP واحد)
أنواع الدفق الأحداث المرسلة من الخادم أحادية الاتجاه (SSE) فقط العميل والخادم والتدفق ثنائي الاتجاه
** توليد الكود ** الأدوات الخارجية المطلوبة (على سبيل المثال، Swagger Codegen) مدمج عبر مترجم Protobuf (protoc)
** دعم المتصفح ** عالمي (الوصول المباشر إلى عميل الويب) محدودة (يتطلب grpc-web ترجمة الوكيل)

5. متى يتم اختيار gRPC بدلاً من REST؟

على الرغم من المزايا الهائلة التي يتمتع بها أداء gRPC، إلا أنها ليست حلاً سحريًا. يعتمد اختيار البروتوكول على السياق داخل بنية النظام الخاص بك:

  • استخدام gRPC لشبكة الخدمات الصغيرة الداخلية: بالنسبة للاتصالات ذات الحجم الكبير من خدمة إلى خدمة، فإن زمن الاستجابة المنخفض لـ gRPC، والضغط الثنائي، وإمكانيات البث تجعلها الخيار الأفضل. فهو يقلل من الحمل الداخلي لوحدة المعالجة المركزية للخادم ويسرع أوقات الاستجابة الإجمالية.
  • استخدم REST for Edge / واجهات برمجة التطبيقات العامة: نظرًا لأن متصفحات الويب لا يمكنها إجراء مكالمات gRPC بشكل أصلي بدون طبقات ترجمة، يظل REST هو الخيار الأفضل لواجهات برمجة التطبيقات العامة، وعمليات التكامل مع خطافات الويب التابعة لجهات خارجية، والاتصال المباشر من متصفحات العميل إلى حافة البنية الخاصة بك.

من خلال الجمع بين الاثنين - كشف REST على مستوى بوابة API والاستفادة من gRPC لاتصالات الخدمات الصغيرة الداخلية - يمكن للمهندسين المعماريين إنشاء أنظمة قابلة للتشغيل البيني بشكل كبير وبسرعة فائقة.


استكشف المزيد من الرؤى المتعلقة بتطوير البرامج وهندسة الواجهة الخلفية على مدونة Gaznix →