מדוע שירותי מיקרו מודרניים מעדיפים gRPC על פני REST

מדוע שירותי מיקרו מודרניים מעדיפים gRPC על פני REST

בארכיטקטורה מונוליטית, רכיבים מתקשרים באמצעות קריאות בשיטת הזיכרון, שהן מיידיות ואמינות ביותר. עם זאת, כאשר עוברים לארכיטקטורת שירותי מיקרו, רכיבים אלה מופרדים על ידי גבולות רשת. תקשורת הופכת לשיחת רשת מחוץ לתהליך (Inter-Process Communication, או IPC).

במשך שנים, REST (Representational State Transfer) על HTTP/1.1 עם עומסי JSON היה תקן ברירת המחדל לבניית ממשקי API באינטרנט. בעוד ש-REST מצוין עבור שירותי אינטרנט הפונה לציבור ואינטראקציה בין לקוח לשרת, הוא מציג צווארי בקבוק משמעותיים כאשר משתמשים בו לתקשורת פנימית בין שירות לשירות בתדירות גבוהה, בהשהייה נמוכה.

זו הסיבה ששירותי מיקרו מודרניים עוברים במהירות לכיוון gRPC (שיחת Google Remote Procedure). במאמר זה, ננתח את המגבלות של REST, ננתח את עמודי התווך הארכיטקטוניים של gRPC ונעבור על יישום מלא של שירות gRPC ב-Java.


1. צווארי הבקבוק של REST בשירותי מיקרו

REST הניע את האינטרנט במשך יותר משני עשורים. עם זאת, הטכנולוגיות הבסיסיות שלה אינן מותאמות לארכיטקטורות מבוזרות פנימיות:

א. התקורה של JSON בטקסט פשוט

JSON הוא קריא אנושי, מה שמקל על איתור באגים, אבל הוא מאוד לא יעיל לתקשורת מכונה למכונה:

  • עלות סריאליזציה/דה-סריאליזציה: ניתוח אסימוני מחרוזת טקסט והמרתם לאובייקטים גוזל מחזורי CPU משמעותיים.
  • גודל מטען גדול: מפתחות JSON חוזרים על עצמם בכל בקשה בודדת (למשל, {"transactionId": "123", "amount": 99.99}). עבור מערכות המעבדות מיליוני בקשות בשנייה, מטא נתונים מיותרים אלו מבזבזים רוחב פס עצום של הרשת.

ב. מגבלות חיבור HTTP/1.1

REST פועל בדרך כלל על HTTP/1.1, אשר מציג מספר חוסר יעילות מבנית:

  • חסימת ראש קו (HoL): בחיבור TCP יחיד, לקוח חייב להמתין לתגובת הבקשה הנוכחית לפני שליחת הבקשה הבאה.
  • מיצוי מאגר חיבורים: כדי לטפל בבקשות במקביל, לקוחות חייבים לפתוח מספר חיבורי TCP. זה גורם לתקורה משמעותית של משאבי מערכת ההפעלה, מיצוי שקעים וקנסות על ביצועים בהתחלה איטית, כאשר החיבורים נפתחים ונסגרים ללא הרף.

ג. חוזי API חלשים

ממשקי API של REST חסרים חוזה מובנה בזמן הידור. בעוד ש-OpenAPI/Swagger עוזר לתעד ממשקי API, הם נפרדים מבסיס הקוד. למפתח עורפי קל לשנות שם שדה של JSON ולשבור בטעות שירותים במורד הזרם ללא אזהרות בזמן הידור.


2. העמודים האדריכליים של gRPC

הוצגה על ידי Google ב-2015, gRPC היא מסגרת RPC בעלת קוד פתוח עם ביצועים גבוהים שתוכננה במיוחד עבור יישומים מקוריים בענן. זה פותר את צווארי הבקבוק של REST באמצעות שלוש טכנולוגיות מפתח:

שלושת העמודים האדריכליים של gRPC: מאגרי פרוטוקול, העברה של HTTP/2 ויצירת קוד

א. חוצצי פרוטוקול (פרוטובוף)

במקום JSON, gRPC משתמש במאגרי פרוטוקול כשפת הגדרת הממשק (IDL) ופורמט הסדרת הודעות.

  • Protobuf הוא מנגנון סריאליזציה בינארי אופטימלי במיוחד.
  • שדות מסודרים כתגים מספריים קטנים במקום שמות טקסט.
  • המטענים קטנים עד 70-80% מ-JSON, והסדרה מהירה עד פי 10, מה שמפחית משמעותית את השימוש במעבד וברוחב הפס.

ב. ריבוי HTTP/2

gRPC משתמש ב-HTTP/2 כפרוטוקול התחבורה שלו, מה שמביא מספר אופטימיזציות של ביצועים:

  • ריבוי אמיתי: ניתן לשלב בקשות ותגובות מרובות בו-זמנית באמצעות חיבור יחיד ארוך טווח של TCP, ולבטל לחלוטין חסימת Head-of-Line.
  • דחיסת כותרת (HPACK): כותרות HTTP נדחסות, מה שמקטין עוד יותר את גודל הבקשה.
  • תמיכה בסטרימינג: HTTP/2 תומך באופן טבעי בהזרמת לקוח חד-כיוונית, הזרמת שרת וזרימה דו-כיוונית מלאה.

ג. יצירת קוד וחוזים קפדניים

על ידי הגדרת מבנה ה-API בקובץ .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 Remote Procedure)
פרוטוקול HTTP/1.1 (סטנדרטי), HTTP/2 (אופציונלי) HTTP/2 (דרישה קפדנית)
פורמט מטען טקסט פשוט JSON, XML, HTML חוצצי פרוטוקול בינאריים (פרוטובוף)
פרדיגמת API משאבים (נתיבי 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, הקומפקטיות הבינארית ויכולות הסטרימינג של gRPC הופכות אותו לבחירה המעולה. זה מקטין את תקורה של מעבד שרת פנימי ומאיץ את זמני התגובה הכוללים.
  • השתמש ב-REST עבור Edge / APIs ציבוריים: מכיוון שדפדפני אינטרנט אינם יכולים לבצע קריאות gRPC באופן מקורי ללא שכבות תרגום, REST נשאר הבחירה הטובה ביותר עבור ממשקי API ציבוריים, אינטגרציות עם webhooks של צד שלישי ותקשורת ישירה מדפדפני לקוח לקצה הארכיטקטורה שלך.

על ידי שילוב של השניים - חשיפת REST ברמת API Gateway ומינוף gRPC לתקשורת מיקרו-שירותים פנימית - אדריכלים יכולים לבנות מערכות שהן ניתנות להפעלה הדדית גבוהה וגם מהירות.


גלה עוד פיתוח תוכנה ותובנות הנדסיות עורפיות בבלוג Ghaznix →