چرا میکروسرویس های مدرن gRPC را به REST ترجیح می دهند

چرا میکروسرویس های مدرن gRPC را به REST ترجیح می دهند

در یک معماری یکپارچه، اجزاء از طریق فراخوانی متد درون حافظه، که آنی و بسیار قابل اعتماد هستند، ارتباط برقرار می کنند. با این حال، هنگام انتقال به معماری میکروسرویس، این اجزا با مرزهای شبکه از هم جدا می شوند. ارتباط به یک تماس شبکه خارج از فرآیند تبدیل می شود (ارتباط بین فرآیندی یا IPC).

برای سال‌ها، REST (انتقال وضعیت نمایندگی) از طریق HTTP/1.1 با بارهای JSON استاندارد پیش‌فرض برای ساخت APIهای وب بوده است. در حالی که REST برای خدمات وب عمومی و تعامل مشتری به سرور بسیار عالی است، هنگامی که برای ارتباطات داخلی سرویس به سرویس با فرکانس بالا، تأخیر پایین استفاده می شود، تنگناهای قابل توجهی ایجاد می کند.

به همین دلیل است که میکروسرویس های مدرن به سرعت به سمت gRPC (تماس رویه از راه دور Google) تغییر می کنند. در این مقاله، محدودیت‌های REST را تجزیه و تحلیل می‌کنیم، ستون‌های معماری gRPC را تشریح می‌کنیم و اجرای کامل یک سرویس gRPC در جاوا را طی می‌کنیم.


1. تنگناهای REST در میکروسرویس ها

REST بیش از دو دهه است که وب را تقویت کرده است. با این حال، فناوری های زیربنایی آن برای معماری های توزیع شده داخلی بهینه نشده اند:

A. سربار JSON متن ساده

JSON قابل خواندن توسط انسان است، که اشکال زدایی را آسان می کند، اما برای ارتباط ماشین به ماشین بسیار ناکارآمد است:

  • هزینه سریال‌سازی/سریال‌زدایی: تجزیه توکن‌های رشته متن و تبدیل آن‌ها به اشیا، چرخه‌های CPU قابل توجهی را مصرف می‌کند.
  • اندازه بار بزرگ: کلیدهای JSON در هر درخواست تکرار می شوند (به عنوان مثال، {"transactionId": "123", "amount": 99.99}). برای سیستم‌هایی که میلیون‌ها درخواست را در ثانیه پردازش می‌کنند، این ابرداده اضافی پهنای باند شبکه را هدر می‌دهد.

ب. محدودیت های اتصال HTTP/1.1

REST معمولاً روی HTTP/1.1 اجرا می‌شود که چندین ناکارآمدی ساختاری را نشان می‌دهد:

  • Blocking Head-of-Line (HoL): در یک اتصال TCP، یک کلاینت باید قبل از ارسال درخواست بعدی منتظر پاسخ درخواست فعلی باشد.
  • خستگی ادغام اتصال: برای رسیدگی به درخواست های همزمان، کلاینت ها باید چندین اتصال TCP را باز کنند. این منجر به سربار منابع سیستم عامل قابل توجه، فرسودگی سوکت و جریمه های عملکرد کند شروع می شود زیرا اتصالات به طور مداوم باز و بسته می شوند.

ج. قراردادهای ضعیف API

API های REST فاقد قرارداد داخلی و زمان کامپایل هستند. در حالی که OpenAPI/Swagger به مستندسازی APIها کمک می کند، آنها از پایگاه کد جدا هستند. تغییر نام فیلد JSON و شکست تصادفی سرویس های پایین دستی بدون اخطار زمان کامپایل برای یک توسعه دهنده باطن آسان است.


2. ستون های معماری gRPC

gRPC که توسط گوگل در سال 2015 معرفی شد، یک چارچوب RPC منبع باز و با کارایی بالا است که به طور خاص برای برنامه های کاربردی ابری طراحی شده است. با استفاده از سه فناوری کلیدی، تنگناهای REST را حل می کند:

3 ستون معماری gRPC: بافرهای پروتکل، حمل و نقل HTTP/2 و تولید کد

الف. بافرهای پروتکل (Protobuf)

به جای JSON، gRPC از پروتکل بافر به عنوان زبان تعریف رابط (IDL) و قالب سریال سازی پیام خود استفاده می کند.

  • Protobuf یک مکانیسم سریال سازی باینری بسیار بهینه شده است.
  • فیلدها به‌جای نام متنی، به‌عنوان تگ‌های عددی کوچک سریال‌سازی می‌شوند.
  • بارهای پرداختی تا 70-80٪ کوچکتر از JSON هستند، و سریال سازی تا 10 برابر سریعتر است که به طور قابل توجهی مصرف CPU و پهنای باند را کاهش می دهد.

ب. HTTP/2 Multiplexing

gRPC از HTTP/2 به عنوان پروتکل انتقال خود استفاده می کند و چندین بهینه سازی عملکرد را به همراه دارد:

  • True Multiplexing: درخواست ها و پاسخ های متعدد را می توان به طور همزمان روی یک تک اتصال طولانی مدت TCP در هم آمیخت و مسدود کردن Head-of-Line را کاملاً از بین می برد.
  • فشرده سازی سرصفحه (HPACK): سرصفحه های HTTP فشرده می شوند و اندازه درخواست را بیشتر کاهش می دهند.
  • **پشتیبانی از جریان **: HTTP/2 به طور بومی از جریان یک طرفه مشتری، پخش سرور و پخش دو طرفه کامل پشتیبانی می کند.

ج. تولید کد و قراردادهای دقیق

با تعریف ساختار API در یک فایل .proto، gRPC کلان مشتری و کلاس های پایه سرور را در ده ها زبان (جاوا، Go، Python، C++ و غیره) تولید می کند. قرارداد در زمان کامپایل امن است. اگر سرور رابط خود را به‌روزرسانی کند، ساخت‌های کلاینت در صورتی که با قرارداد جدید مطابقت نداشته باشند، فوراً با شکست مواجه می‌شوند.


3. پیاده سازی جاوا: ساخت یک سرویس gRPC

بیایید یک برنامه کاربردی gRPC جاوا بسازیم. ما یک سرویس پردازش پرداخت پرتوان را اجرا خواهیم کرد که در آن مشتری درخواست پرداخت را ارسال می کند و تأیید تراکنش را دریافت می کند.

مرحله 1: تعریف قرارداد API (payment.proto)

ما با تعریف رابط سرویس و ساختار پیام خود در یک فایل بافر پروتکل شروع می کنیم.

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: Client را پیاده سازی کنید (Blocking and Async Stubs)

یکی از مزایای اصلی 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)
پارادایم 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 آن را به انتخاب برتر تبدیل می‌کند. سربار CPU سرور داخلی را کاهش می دهد و زمان پاسخگویی کلی را سرعت می بخشد.
  • از REST برای Edge/Public API استفاده کنید: از آنجایی که مرورگرهای وب نمی‌توانند به صورت بومی تماس‌های gRPC را بدون لایه‌های ترجمه انجام دهند، REST بهترین انتخاب برای APIهای عمومی، ادغام با وب هوک‌های شخص ثالث و ارتباط مستقیم از مرورگرهای مشتری به لبه معماری شما است.

با ترکیب این دو - افشای REST در سطح API Gateway و استفاده از gRPC برای ارتباطات میکروسرویس داخلی - معماران می‌توانند سیستم‌هایی بسازند که هم کارایی بالایی دارند و هم سریع.


درباره توسعه نرم‌افزار بیشتر و بینش‌های مهندسی باطن در Ghaznix Blog →