সরাসরি পর্বে যান
পর্ব ০৯ · ৬৬-এর মধ্যে · মডিউল ২

gRPC আর GraphQL — Protocol Buffers, streaming আর schema-ভিত্তিক API

gRPC & GraphQL — Protocol Buffers, HTTP/2 Streaming, Deadlines, Resolvers, N+1 & Choosing an API Style

Taught in Bangla, this lesson explains why API styles beyond REST exist, counting bytes and round trips for over-fetching and chatty mobile apps. It covers RPC, gRPC with Protocol Buffers and code generation, HTTP/2 streams, four call types, status codes, metadata and deadline propagation, then GraphQL schemas, queries, mutations, subscriptions and resolvers, the N+1 problem with DataLoader batching, caching and query-cost limits, and compares all three for one home screen.

এই পর্বে যা শিখবেন

  • Over-fetching, under-fetching আর “আলাপী” মোবাইল অ্যাপের সমস্যা — বাইট আর round trip গুনে
  • RPC-র ধারণা, আর দূরের call-কে কাছের মতো ভাবার বিপদ
  • gRPC-র .proto, code generation, HTTP/2 frame, চার ধরনের call, status code, metadata আর deadline
  • GraphQL-এর schema, query, mutation, subscription, resolver, N+1 আর DataLoader, caching আর নিরাপত্তার সীমা
  • REST, gRPC আর GraphQL-এর তুলনা, আর চার সার্ভিসের হোম স্ক্রিনের জন্য API style বাছাই

১ · REST-এর পরেও কেন নতুন API ধারা — over-fetching, under-fetching আর আলাপী অ্যাপ

পর্ব ০৮-এ (REST API নকশা) দেখেছেন, REST-এ প্রতিটি জিনিস একটি resource, প্রতিটির নিজের URL, আর HTTP-র method দিয়ে কাজ। পাবলিক API-র জন্য এটি চমৎকার: যেকোনো ভাষা, ব্রাউজার আর curl দিয়ে চালানো যায়, CDN সহজে cache করে। তবু দুটি জায়গায় REST-এর ঘর্ষণ চোখে পড়ে।

প্রথম জায়গা: সার্ভিসের সাথে সার্ভিসের কথা। একটি বড় অ্যাপের ভেতরে কয়েক ডজন microservice একে অপরকে প্রতি সেকেন্ডে হাজারো বার ডাকে। সেখানে প্রতিবার JSON লেখা আর পড়ার CPU খরচ, হাতে লেখা client কোড, আর “এই field-টা string না number?” জাতীয় ভুল জমতে জমতে বড় হয়। দরকার একটি টাইপ-করা চুক্তি — যা দুই পক্ষের কোড নিজে থেকেই মেনে চলে।

দ্বিতীয় জায়গা: মোবাইল অ্যাপের একটি পাতা। ধরুন একটি খাবার-ডেলিভারি অ্যাপের হোম স্ক্রিন (কাল্পনিক)। তাতে দেখাতে হবে ব্যবহারকারীর নাম আর wallet-এর টাকা, সাম্প্রতিক ৩টি অর্ডার, সুপারিশ করা ১০টি পণ্য, আর ৩টি অফার। চারটি তথ্য আসে চারটি আলাদা সার্ভিস থেকে। Resource ধরে বানানো REST API-তে দুটি সমস্যা দেখা দেয়:

  • Over-fetching — দরকারের চেয়ে বেশি ডেটা আনা। GET /orders পুরো ২০টি অর্ডারের সব খুঁটিনাটি দেয়, অথচ পর্দায় লাগে ৩টির তিনটি field.
  • Under-fetching — এক অনুরোধে যথেষ্ট না পাওয়া। GET /recommendations দেয় শুধু ১০টি পণ্যের id; নাম আর দাম জানতে আরও ১০টি অনুরোধ। একের ফলের ওপর অন্যটি নির্ভর করে বলে round trip বাড়ে।

মুদি দোকানে ফোন করে বললেন, “চাল আছে?” দোকানি বললেন, “আছে, পাঁচ রকম।” আবার ফোন — “মিনিকেটের দাম?” আবার ফোন — “আধা কেজি ডাল আর এক হালি ডিম দিতে পারবেন?” প্রতিবার ফোন ধরা আর রাখার সময়টাই আসল খরচ; এই অ্যাপটিও তেমনই আলাপী (chatty) স্বভাবের। উল্টো ছবিও আছে: শুধু ডিমের দাম জানতে চাইলেন, দোকানি পুরো মাসের তালিকা পড়ে শোনালেন — over-fetching. ভালো সমাধান হলো একবারের ফোনে পুরো ফর্দ বলা, আর দোকানির কাছ থেকে ঠিক ততটুকু উত্তর নেওয়া।

সংখ্যায় দেখা যাক। মোবাইল নেটওয়ার্কে একটি round trip (RTT) ধরা যাক ১২০ ms; ডেটা সেন্টারের ভেতরে মাত্র ১ ms. নিচের হিসাবে পাঁচটি উপায়ের তুলনা — প্রতিটি সংখ্যা ধরা যাক, বদলে চালিয়ে দেখুন:

// Home screen = 4 services. Bytes and round trips under different API styles.
// EVERY number below is an assumption — change one and run again.
const RTT = 120;     // ms, phone <-> server round trip (mobile network)
const DC = 1;        // ms, round trip inside the data centre
const SVC = 30;      // ms, one service's own work per call
const BPMS = 500;    // bytes per ms the phone downloads (about 4 Mbit/s)
const HDR = 500;     // header bytes per request + response
const need = { profile: 300, orders: 3 * 120, products: 10 * 150, offers: 300 };
const useful = Object.values(need).reduce((a, b) => a + b, 0);   // what the screen shows

// waves = phone round trips one after another; inner = service waves behind one call
const restBody = 2400 + 20 * 850 + 400 + 10 * 1600 + 1800;  // full objects + id list
const styles = [
  ["REST one by one",   14, 14, 0, restBody],
  ["REST in parallel",  14,  2, 0, restBody],
  ["REST /home (BFF)",   1,  1, 2, useful + 100],
  ["GraphQL query",      1,  1, 2, useful + 100 + 700],   // + query text in the POST
  ["GraphQL persisted",  1,  1, 2, useful + 100 + 64],    // + a 64-char hash only
];
const pad = (s, n) => String(s).padStart(n);
console.log("useful bytes on screen: " + useful);
console.log("style              reqs  waves   bytes  useful%   time ms");
for (const [name, reqs, waves, inner, body] of styles) {
  const bytes = body + reqs * HDR;
  const ms = waves * (RTT + (inner ? 0 : SVC)) + inner * (DC + SVC) + bytes / BPMS;
  console.log(name.padEnd(18) + pad(reqs, 5) + pad(waves, 7) + pad(bytes, 8) +
    pad((100 * useful / bytes).toFixed(1), 9) + pad(ms.toFixed(0), 10));
}

আউটপুট:

useful bytes on screen: 2460
style              reqs  waves   bytes  useful%   time ms
REST one by one      14     14   44600      5.5      2189
REST in parallel     14      2   44600      5.5       389
REST /home (BFF)      1      1    3060     80.4       188
GraphQL query         1      1    3760     65.4       190
GraphQL persisted     1      1    3124     78.7       188

চারটি শিক্ষা। এক, পর্দায় লাগে মাত্র ২,৪৬০ বাইট, অথচ resource ধরে আনলে আসে ৪৪,৬০০ বাইট — দরকারি মাত্র ৫.৫%; বাকিটা over-fetching আর ১৪টি অনুরোধের header. দুই, সবচেয়ে বড় খরচ বাইট নয়, round trip: একটার পর একটা ১৪টি অনুরোধে প্রায় ২,১৮৯ ms; সমান্তরালে পাঠালেও id-র তালিকার পরে পণ্যের অনুরোধ যেতে হয় বলে দুই দফা — ৩৮৯ ms. তিন, সার্ভারের দিকে একটি জায়গা যদি চারটি সার্ভিস নিজে ডেকে এক উত্তরে সাজিয়ে দেয় (BFF বা GraphQL), ফোনকে একবারই যেতে হয় — প্রায় ১৮৮–১৯০ ms; ভেতরের দুই দফা call-এ network-এর খরচ দফাপ্রতি মাত্র ১ ms. চার, GraphQL-এর query-র লেখাটিও প্রতিবার যায় (৭০০ বাইট); আগে থেকে নিবন্ধিত query-র শুধু hash পাঠালে (persisted query) বাইট প্রায় BFF-এর সমান। RTT ৩০০ করে দেখুন — দুর্বল নেটওয়ার্কে round trip আরও নির্মম।

মূল ধারণা

দুই সমস্যা, দুই ধারা। ভেতরের সার্ভিসগুলোর দ্রুত, টাইপ-করা আলাপের জন্য তৈরি হয়েছে gRPC; বহু রকম ক্লায়েন্ট যাতে নিজের দরকারমতো ডেটা এক অনুরোধে চাইতে পারে, তার জন্য GraphQL. দুটিই REST-এর বদলি নয় — প্রায়ই তিনটি একই সিস্টেমে পাশাপাশি থাকে।

২ · RPC-র ধারণা — দূরের function যেন হাতের কাছের

REST ভাবে “জিনিস” (resource) দিয়ে; RPC (Remote Procedure Call) ভাবে “কাজ” দিয়ে। RPC-র স্বপ্নটা সরল: অন্য যন্ত্রে থাকা একটি function ডাকা হবে ঠিক নিজের প্রোগ্রামের function-এর মতো — order = orders.GetOrder("A-1029"). Network, বাইট আর সংযোগের ঝামেলা লুকিয়ে রাখে মাঝখানের একটি stub: সে argument-কে বাইটে বদলায় (serialization), পাঠায়, উত্তরের বাইট আবার object বানিয়ে ফেরত দেয়।

ধারণাটি পুরোনো। ১৯৮৪ সালে Xerox PARC-এর Andrew Birrell আর Bruce Nelson-এর “Implementing Remote Procedure Calls” গবেষণাপত্র RPC-কে একটি পূর্ণাঙ্গ, ব্যবহারযোগ্য ব্যবস্থা হিসেবে দাঁড় করায়। এরপর এসেছে Sun RPC (যার ওপর NFS চলত), CORBA, Java RMI, SOAP, আর ২০০৭ সালের দিকে Facebook-এর তৈরি Thrift. আজকের gRPC এই দীর্ঘ ধারারই উত্তরসূরি।

কিন্তু “কাছের মতো” ভাবাটাই বিপদ। ১৯৯৪ সালে Sun Microsystems-এর Jim Waldo ও সহকর্মীরা “A Note on Distributed Computing” লেখায় দেখান, দূরের call আর কাছের call মৌলিকভাবে আলাদা, আর পার্থক্য লুকালে ভুল নকশা হয়:

দিকনিজের প্রোগ্রামের functionNetwork-এর ওপারের call
সময়ন্যানোসেকেন্ড থেকে মাইক্রোসেকেন্ডভেতরের network-এ প্রায় মিলিসেকেন্ড, মোবাইলে শত মিলিসেকেন্ড — হাজার থেকে লক্ষ গুণ ধীর
ব্যর্থতাহয় চলে, নয় পুরো প্রোগ্রাম থামেআংশিক ব্যর্থতা: উত্তর এল না — অনুরোধ পৌঁছায়নি, নাকি কাজ হয়ে উত্তর হারিয়েছে, বোঝার উপায় নেই
Retryপ্রশ্নই ওঠে নাআবার পাঠালে কাজ দুবার হতে পারে — idempotency লাগে
Argumentpointer বা reference দেওয়া যায়সব কপি করে বাইটে পাঠাতে হয়; বড় object মানে বড় খরচ
সংস্করণএকসাথে compile হয়দুই পক্ষ আলাদা সময়ে deploy হয়; পুরোনো আর নতুন কোড একসাথে চলে

লুপের ভেতরে RPC সবচেয়ে চেনা ফাঁদ। for লুপে ১,০০০ বার একটি দূরের function ডাকলে, প্রতিটি ১ ms হলেও মোট এক সেকেন্ড — কোডে দেখতে নিরীহ। আধুনিক RPC তাই ইচ্ছা করে দূরত্বটা চোখে রাখে: প্রতিটি call-এ deadline, ব্যর্থতার নির্দিষ্ট কোড, আর batch বা streaming-এর উপায়। Network-এর এই কঠিন সত্যগুলো — “network নির্ভরযোগ্য”, “latency শূন্য” জাতীয় ভ্রান্ত ধারণার তালিকাসহ — বিস্তারিত আসবে পর্ব ৩৮-এ (distributed system-এর কঠিন সত্য)।

৩ · gRPC আর Protocol Buffers — আগে চুক্তি, তারপর কোড

gRPC একটি open-source RPC framework. Google ২০১৫ সালের দিকে এটি প্রকাশ্যে আনে; gRPC-র প্রকাশিত নকশা-নীতি অনুযায়ী এর পেছনে ছিল Google-এর ভেতরে বহু বছর ব্যবহৃত Stubby নামের RPC ব্যবস্থার অভিজ্ঞতা। এখন এটি Cloud Native Computing Foundation (CNCF)-এর একটি প্রকল্প। তিনটি জিনিস মিলে gRPC: চুক্তির ভাষা Protocol Buffers, চুক্তি থেকে code generation, আর পরিবহনে HTTP/2.

Protocol Buffers (সংক্ষেপে Protobuf) একটি IDL (Interface Definition Language) — সার্ভিস কী কী কাজ দেয় আর প্রতিটি বার্তার গঠন কী, তা ভাষা-নিরপেক্ষভাবে লেখার নিয়ম। Google-এর ভেতরে বহু বছর ব্যবহারের পর ২০০৮ সালে এটি open-source হয়। নিচে একটি কাল্পনিক খাবার-ডেলিভারি অ্যাপের অর্ডার সার্ভিসের চুক্তি:

syntax = "proto3";
package shop.v1;

import "google/protobuf/timestamp.proto";

service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);              // unary
  rpc WatchOrder (GetOrderRequest) returns (stream OrderUpdate); // server streaming
}

message GetOrderRequest {
  string order_id = 1;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;   // proto3: the first value must be 0
  ORDER_STATUS_PLACED = 1;
  ORDER_STATUS_COOKING = 2;
  ORDER_STATUS_ON_THE_WAY = 3;
  ORDER_STATUS_DELIVERED = 4;
}

message Order {
  string order_id = 1;
  OrderStatus status = 2;
  int64 total_paisa = 3;          // money as whole paisa, never a float
  repeated LineItem items = 4;
  google.protobuf.Timestamp placed_at = 5;
}

message LineItem {
  string name = 1;
  int32 quantity = 2;
}

message OrderUpdate {
  OrderStatus status = 1;
  google.protobuf.Timestamp at = 2;
}

ফাইলটি পড়ার চাবি কয়েকটি:

  • service আর rpc: সার্ভিস কোন কোন method দেয়, প্রতিটি কোন বার্তা নেয় আর কোনটি ফেরত দেয়। stream শব্দটি থাকলে সেই দিকে একাধিক বার্তা যাবে।
  • Field number (= 1, = 2 …): তারে field-এর নাম যায় না, যায় এই নম্বর। তাই নাম বদলানো যায়, কিন্তু একবার ব্যবহৃত নম্বর আর কখনো অন্য অর্থে নয়; মুছে ফেলা field-এর নম্বর reserved করে রাখা ভালো অভ্যাস।
  • টাকা পয়সায়, পূর্ণসংখ্যায়: int64 total_paisa — দশমিক ভগ্নাংশের (float) গোলমাল এড়াতে।

এবার protoc নামের compiler (সাথে ভাষাভিত্তিক plugin) এই একটি ফাইল থেকে Go, Java, Python, C++, Kotlin, TypeScript ইত্যাদি ভাষায় তৈরি করে দেয় বার্তার class, ক্লায়েন্টের stub আর সার্ভারের ফাঁকা কাঠামো। Go-তে লেখা অর্ডার সার্ভিসকে Kotlin-এর অ্যাপ সার্ভার বা Python-এর সুপারিশ সার্ভিস ডাকতে পারে, কারও হাতে client লিখতে হয় না। ভুল ধরনের field পাঠালে ভুলটা ধরা পড়ে compile-এর সময়েই, production-এ নয়।

ব্যাংকের টাকা জমার স্লিপের কথা ভাবুন। ঘরগুলো ছাপানো আর নম্বর দেওয়া — হিসাব নম্বর, টাকার অঙ্ক, তারিখ। গ্রাহক যে ভাষাতেই কথা বলুন, কাউন্টারের কর্মকর্তা ঘরের নম্বর দেখেই বোঝেন কোনটি কী। .proto ফাইল সেই ছাপানো স্লিপ; field number হলো ঘরের নম্বর। নতুন ঘর যোগ করা যায়, কিন্তু পুরোনো ঘরের নম্বর অন্য কাজে লাগালে সব শাখায় গোলমাল বাঁধে।

তারে Protobuf বার্তা ছোট আর binary. ওপরের Order-এ order_id = "A-1029", status ৩ আর total ৪৫,০০০ পয়সা থাকলে Protobuf-এ লাগে মাত্র ১৪ বাইট; একই তথ্য JSON-এ — {"order_id":"A-1029","status":"ORDER_STATUS_ON_THE_WAY","total_paisa":45000} — ৭৬ বাইট। কারণ JSON প্রতিবার field-এর নাম লেখে, Protobuf শুধু ছোট নম্বর। দাম হলো চোখে পড়া যায় না; বাইটগুলো বুঝতে .proto ফাইল লাগে। বাইট কীভাবে সাজানো থাকে (varint, wire type), আর পুরোনো-নতুন সংস্করণ একসাথে চালানোর নিয়ম (schema evolution) — দুটিই বিস্তারিত আছে পর্ব ১২-এ (Data format আর serialization পর্বে)।

৪ · gRPC-র ভেতরে — HTTP/2 stream, চার ধরনের call, status code আর metadata

gRPC চলে HTTP/2-এর ওপর। HTTP/2-এ একটি TCP (সাথে TLS) সংযোগের — যাদের মূল কথা পর্ব ০৭-এ — ভেতরে অনেকগুলো স্বাধীন stream একসাথে চলে — একে বলে multiplexing; frame আর header compression-এর বিস্তারিত Computer Networks কোর্সের HTTP/2 আর HTTP/3 পর্বে। gRPC-র নিয়ম সরল: একটি call মানে একটি stream. নিচে একটি unary call-এর পুরো যাত্রা, তারের আসল বাইটসহ:

  1. অ্যাপের কোডGetOrder()deadline ৩০০ ms
  2. Client stubprotobufতৈরি করা কোড
  3. HTTP/2stream 1একটি সংযোগ
  4. সার্ভারhandlerঅর্ডার সার্ভিস
অ্যাপ সার্ভারের কোডছদ্মকোড — দেখতে সাধারণ function
const order = await orderClient.getOrder(
  { orderId: "A-1029" },
  { deadline: Date.now() + 300 }   // ms
);
Serialization৮ বাইটের বার্তা
GetOrderRequest { order_id: "A-1029" }
0a 06 41 2d 31 30 32 39
0a = field 1, wire type 2 (দৈর্ঘ্যসহ)
06 = দৈর্ঘ্য ৬, তারপর "A-1029"
HEADERS frameনতুন stream 1
:method       POST
:scheme       https
:path         /shop.v1.OrderService/GetOrder
:authority    orders.internal
content-type  application/grpc
te            trailers
grpc-timeout  300m
x-request-id  7f3a9c
DATA frameEND_STREAM: ক্লায়েন্টের কথা শেষ
00            compressed? না
00 00 00 08   বার্তার দৈর্ঘ্য = 8
0a 06 41 2d 31 30 32 39
সার্ভারের gRPC librarypath দেখে handler বাছাই
path    → OrderService.GetOrder
bytes   → GetOrderRequest{order_id:"A-1029"}
timeout → এখন + 300 ms, handler-এর context-এ
handler → DB থেকে অর্ডার পড়ল
উত্তরHEADERS → DATA → trailers
HEADERS  :status 200
         content-type application/grpc
DATA     00 00 00 00 0e
         0a 06 41 2d 31 30 32 39
         10 03 18 c8 df 02
HEADERS  grpc-status 0   (trailers, END_STREAM)
Client stubবাইট থেকে object
grpc-status 0 = OK
Order { order_id: "A-1029",
        status: ON_THE_WAY, total_paisa: 45000 }
stream 1 বন্ধ; সংযোগ খোলা থাকে
পরের call: stream 3, 5, 7 …
  1. ধাপ ১ — দেখতে স্থানীয় function. অ্যাপ সার্ভারের কোড তৈরি করা client-এর getOrder ডাকল, সাথে একটি deadline: ৩০০ ms-এর মধ্যে উত্তর চাই। Network-এর কোনো চিহ্ন কোডে নেই — RPC-র সুবিধা আর বিপদ, দুটোই এখানে।
  2. ধাপ ২ — Stub বার্তাটিকে বাইটে বদলাল। GetOrderRequest হলো মাত্র ৮ বাইট: 0a বলছে “field 1, দৈর্ঘ্যসহ ধরন”, 06 বলছে দৈর্ঘ্য ৬, তারপর "A-1029"-এর ছয়টি অক্ষর। Field-এর নাম তারে যায়নি।
  3. ধাপ ৩ — HTTP/2-এ নতুন stream, HEADERS frame. Method সবসময় POST; :path-এ সার্ভিস আর method-এর পুরো নাম। grpc-timeout: 300m মানে ৩০০ মিলিসেকেন্ড — deadline এভাবেই সার্ভারে পৌঁছায়। x-request-id হলো metadata, নিচে দেখবেন।
  4. ধাপ ৪ — DATA frame-এ বার্তা। প্রতিটি বার্তার আগে ৫ বাইটের ভূমিকা: ১ বাইট (compressed কি না) আর ৪ বাইটে দৈর্ঘ্য (এখানে ৮)। এই length-prefixed কাঠামোর জন্যই এক stream-এ পরপর অনেক বার্তা পাঠানো যায় — streaming-এর ভিত্তি।
  5. ধাপ ৫ — সার্ভার handler চালাল। সার্ভারের gRPC library :path দেখে ঠিক handler বাছল, বাইট থেকে object বানাল, আর grpc-timeout থেকে deadline বসিয়ে দিল handler-এর context-এ। Handler চাইলে দেখতে পারে কত সময় বাকি।
  6. ধাপ ৬ — উত্তর: HEADERS, DATA, তারপর trailers. HTTP status প্রায় সবসময় 200; আসল ফল থাকে শেষের trailers-এ — grpc-status 0, অর্থাৎ OK. ১৪ বাইটের Order বার্তার আগে 00 00 00 00 0e (দৈর্ঘ্য ১৪)।
  7. ধাপ ৭ — Stub object ফেরত দিল। ক্লায়েন্টের stub trailers দেখে বুঝল সফল, বাইট থেকে Order বানিয়ে কোডকে দিল। Stream 1 বন্ধ হলো, কিন্তু সংযোগ খোলা থাকে; পরের call যাবে stream 3-এ (ক্লায়েন্টের খোলা stream-এর নম্বর বিজোড়)। ব্যর্থ হলে stub একটি status code-সহ error তোলে।

চার ধরনের call

Length-prefixed বার্তা আর দীর্ঘস্থায়ী stream মিলে gRPC চার রকম call দেয়। .proto-তে কোন দিকে stream লেখা, তা-ই ঠিক করে ধরন:

ধরন.proto-তেকী ঘটেউদাহরণ (কাল্পনিক)
Unaryrpc A(Req) returns (Res)একটি অনুরোধ, একটি উত্তরঅর্ডারের বিবরণ আনা
Server streamingreturns (stream Res)একটি অনুরোধ, সার্ভার একের পর এক বার্তা পাঠায়অর্ডারের অবস্থা বদলালেই জানানো
Client streamingrpc A(stream Req)ক্লায়েন্ট অনেক বার্তা পাঠায়, শেষে একটি উত্তরসারাদিনের sensor রিডিং জমা দিয়ে একটি সারাংশ
Bidirectionalদুই দিকেই streamদুই পক্ষ স্বাধীনভাবে যখন খুশি পাঠায়চালকের অবস্থান পাঠানো আর সার্ভারের নির্দেশ পাওয়া একসাথে

Status code — ব্যর্থতার নির্দিষ্ট নাম

gRPC-র নিজের status code আছে, মোট ১৭টি (০ থেকে ১৬)। HTTP-র 404 বা 503-এর সাথে এদের গুলিয়ে ফেলবেন না — HTTP status সাধারণত 200-ই থাকে, আসল ফল grpc-status-এ। সবচেয়ে বেশি দেখা কয়েকটি:

কোডনামঅর্থআবার চেষ্টা?
0OKসফল—
1CANCELLEDcall বাতিল, সাধারণত ডাকা পক্ষই বাতিল করেছেসাধারণত না
3INVALID_ARGUMENTঅনুরোধটাই ভুল, যেমন ফাঁকা order_idনা — আগে অনুরোধ ঠিক করুন
4DEADLINE_EXCEEDEDসময়সীমা পেরিয়ে গেছে। সাবধান: অবস্থা বদলানো কাজ সার্ভারে হয়েও থাকতে পারেশুধু idempotent হলে, নতুন সময়সীমা দিয়ে
5NOT_FOUNDচাওয়া জিনিস নেইনা
8RESOURCE_EXHAUSTEDquota বা সীমা শেষ, যেমন rate limitঅপেক্ষা করে, backoff দিয়ে
14UNAVAILABLEসার্ভিস সাময়িকভাবে পাওয়া যাচ্ছে না — প্রায়ই ক্ষণস্থায়ীহ্যাঁ, backoff দিয়ে (অ-idempotent কাজে সাবধানে)
16UNAUTHENTICATEDবৈধ পরিচয়পত্র (credential) নেইনা — আগে login বা token

Metadata — চিঠির খামের ওপরের লেখা

বার্তার বাইরে কিছু বাড়তি তথ্য যায় metadata-তে: key-value জোড়া, যা আসলে HTTP/2 header (শুরুতে) বা trailer (শেষে)। Key ছোট হাতের অক্ষরে; binary মান হলে key-এর শেষে -bin. সাধারণ ব্যবহার: পরিচয়ের token (authorization), প্রতিটি অনুরোধের id, আর distributed tracing-এর header (observability, পর্ব ৫০)। ডাকঘরের চিঠির মতো — ভেতরের চিঠি (message) পড়ে প্রাপক, খামের ওপরের ঠিকানা আর সিল (metadata) দেখে ডাকঘরের প্রতিটি হাত।

৫ · gRPC-কে production-এ টেকানো — deadline, cancellation, load balancing আর ব্রাউজার

Deadline আর cancellation: সময়সীমা পুরো শিকলে

Timeout বলে “আমি কতক্ষণ অপেক্ষা করব”; deadline বলে “কোন মুহূর্তের পরে উত্তরের আর দাম নেই”। পার্থক্যটা বোঝা যায় শিকলে। ক্লায়েন্ট ৩০০ ms অপেক্ষা করবে; A ডাকে B-কে, B ডাকে C-কে। ক্লায়েন্ট হাল ছাড়ার পরেও যদি B আর C কাজ করে যায়, সেই কাজের ফল কেউ নেবে না। পর্ব ০৪-এ (Latency, throughput আর percentile) যে deadline propagation-এর কথা এসেছিল, gRPC তা প্রায় নিজে থেকেই করে: প্রতিটি সার্ভিস বাকি সময়টা পরের call-এর grpc-timeout-এ পাঠায়। আর ক্লায়েন্ট call বাতিল করলে বা সময় ফুরালে সার্ভার সেই খবর পায় (cancellation); handler-এর context বাতিল হয়, আর সেই context দিয়ে করা নিচের call-গুলোও বাতিল হয়। Go-তে context, Java-তে Context এ কাজ সহজ করে; ভাষা আর library-ভেদে খুঁটিনাটি আলাদা।

ডিফল্টে কোনো deadline নেই। gRPC-র অফিশিয়াল নির্দেশিকা অনুযায়ী, ক্লায়েন্ট deadline না দিলে call কার্যত অনন্তকাল অপেক্ষা করতে পারে। একটি আটকে যাওয়া নিচের সার্ভিস তখন ওপরের সব thread আর সংযোগ ধরে রাখে — cascading failure-এর সোজা পথ। নিয়ম: প্রতিটি call-এ deadline, আর সার্ভিসের ভেতরে আসা context-ই নিচের call-এ ব্যবহার।

নিচের simulator একটি শিকল চালায়: ক্লায়েন্ট → A (BFF) → B (অর্ডার) → C (stock সার্ভিস)। C ধীর — তার ২৫০ ms লাগে। তিনটি নীতি: none — সার্ভিসগুলো ক্লায়েন্টের সময়সীমা জানে না; propagate — সবাই জানে, সময় ফুরালেই থামে; early — জানে, আর হাতে যথেষ্ট সময় না থাকলে কাজ শুরুই করে না।

// client -> A -> B -> C, 300 ms client deadline. Model: one shared absolute deadline, 5 ms hops.
const DEADLINE = 300, HOP = 5;
const svc = [{ name: "A (BFF)", pre: 40, post: 20 }, { name: "B (orders)", pre: 60, post: 30 },
             { name: "C (stock)", pre: 250, post: 0 }];   // pre = work before calling the next
function run(policy) {             // "none" | "propagate" | "early" (= propagate + early reject)
  const work = [0, 0, 0], notes = []; let late = 0;   // late = work after the client gave up
  const busy = (i, from, ms) => {
    let end = from + ms;
    if (policy !== "none") end = Math.min(end, Math.max(from, DEADLINE));   // stop at deadline
    work[i] += end - from; late += Math.max(0, end - Math.max(from, DEADLINE));
    return end;
  };
  const expired = (i, t) => {
    if (policy === "none" || t < DEADLINE) return false;
    notes.push(`${svc[i].name} DEADLINE_EXCEEDED at ${t}`); return true;
  };
  const call = (i, t) => {         // the request reaches service i at time t
    const s = svc[i], left = DEADLINE - t;
    if (policy === "early" && left < s.pre + s.post) {
      notes.push(`${s.name} rejects at ${t} (needs ${s.pre + s.post}, has ${left})`); return { t, ok: false };
    }
    let now = busy(i, t, s.pre);
    if (expired(i, now)) return { t: now, ok: false };
    if (i + 1 === svc.length) return { t: now, ok: true };
    const r = call(i + 1, now + HOP);
    if (!r.ok) return { t: Math.min(r.t + HOP, DEADLINE), ok: false };
    now = busy(i, r.t + HOP, s.post);
    return expired(i, now) ? { t: now, ok: false } : { t: now, ok: true };
  };
  const r = call(0, HOP), at = Math.min(r.t + HOP, DEADLINE), ok = r.ok && r.t + HOP <= DEADLINE;
  console.log(`${policy.padEnd(9)} client ${ok ? "OK" : "DEADLINE_EXCEEDED"} at ${ok ? r.t + HOP : at} ms`
    + ` | work A/B/C ${work.join("/")} = ${work[0] + work[1] + work[2]} | after give-up ${late}`);
  notes.forEach((n) => console.log("          " + n));
}
["none", "propagate", "early"].forEach(run);

আউটপুট:

none      client DEADLINE_EXCEEDED at 300 ms | work A/B/C 60/90/250 = 400 | after give-up 115
propagate client DEADLINE_EXCEEDED at 300 ms | work A/B/C 40/60/185 = 285 | after give-up 0
          C (stock) DEADLINE_EXCEEDED at 300
early     client DEADLINE_EXCEEDED at 130 ms | work A/B/C 40/60/0 = 100 | after give-up 0
          C (stock) rejects at 115 (needs 250, has 185)

তিন নীতিতেই ক্লায়েন্ট সফল হয়নি — উত্তর পৌঁছাতো ৪৩০ ms-এ, সীমা ৩০০. পার্থক্য হলো সার্ভারের দিকে কতটা কাজ নষ্ট হলো। none: ক্লায়েন্ট ৩০০ ms-এ হাল ছাড়ল, কিন্তু C আরও ৬৫ ms, B ৩০ ms আর A ২০ ms কাজ চালিয়ে গেল — হাল ছাড়ার পরে মোট ১১৫ ms অকারণ কাজ, আর পুরো ৪০০ ms-ই বৃথা। propagate: ঠিক ৩০০ ms-এ C-র call DEADLINE_EXCEEDED পেল, সবাই থামল; হাল ছাড়ার পরে কাজ শূন্য। early: C পৌঁছানোর মুহূর্তে দেখল হাতে ১৮৫ ms, লাগবে ২৫০ — শুরুই করল না। মোট কাজ ১০০ ms, আর ক্লায়েন্ট খবর পেল ১৩০ ms-এ, অপেক্ষার বদলে দ্রুত অন্য ব্যবস্থা নিতে পারল। ভিড়ের সময়ে প্রতি সেকেন্ডে এমন ১,০০০টি অনুরোধ এলে none-এ প্রতি সেকেন্ডে ১১৫ সেকেন্ড-সমান অকারণ কাজ — প্রায় ১১৫টি worker সারাক্ষণ কারও কাজে না লেগে ব্যস্ত। মডেলটি সরল: সবার একটি সাধারণ সময়রেখা ধরা হয়েছে; বাস্তবে gRPC আপেক্ষিক timeout পাঠায় আর গ্রহীতা নিজের ঘড়িতে গোনে, তাই প্রতি hop-এ network সময়ের সমান সামান্য ঢিল থাকে। Retry আর timeout-এর পূর্ণ কৌশল আসবে resilience pattern-এর পর্বে (পর্ব ৪৮)।

দীর্ঘস্থায়ী HTTP/2 সংযোগ আর load balancing-এর ফাঁদ

HTTP/2-এর শক্তি — একটি সংযোগে হাজারো call — load balancing-এ সমস্যা তৈরি করে। একটি L4 (TCP-স্তরের) load balancer ভাগ করে সংযোগ, call নয়। ক্লায়েন্ট একবার সংযোগ খুলে রাখলে তার সব call চিরকাল একই সার্ভারে যায়; নতুন সার্ভার যোগ হলেও পুরোনো সংযোগের ভার সরে না। Kubernetes-এর সাধারণ Service-এও একই কারণে একটি gRPC সংযোগ একটি Pod-এ আটকে থাকে (Computer Networks কোর্সের Cloud আর container নেটওয়ার্কিং পর্বে আছে)। প্রচলিত তিনটি সমাধান:

  • L7 proxy: HTTP/2 বোঝে এমন proxy (যেমন Envoy) প্রতিটি call আলাদাভাবে বিভিন্ন সার্ভারে পাঠায়।
  • Client-side load balancing: ক্লায়েন্ট নিজেই সব সার্ভারের ঠিকানা জেনে প্রত্যেকের সাথে সংযোগ রাখে আর call ঘুরিয়ে পাঠায় (gRPC-তে round_robin নীতি; ডিফল্ট নীতি সাধারণত pick_first, যা একটিতেই যায়)।
  • সংযোগের বয়সসীমা: সার্ভার নির্দিষ্ট সময় পরপর ক্লায়েন্টকে নতুন সংযোগ খুলতে বলে (max connection age), যাতে ভার আবার ছড়ায়।

L4 আর L7-এর বিস্তারিত পর্ব ১৪-এ (load balancing), আর service discovery ও service mesh পর্ব ৪৭-এ (monolith বনাম microservices পর্বে)।

ব্রাউজার আর gRPC-Web

ব্রাউজারের JavaScript সরাসরি HTTP/2-এর frame নিয়ন্ত্রণ করতে পারে না, আর response-এর trailer পড়ার নির্ভরযোগ্য উপায়ও নেই — অথচ gRPC-র ফল থাকে trailer-এ। তাই ব্রাউজারের জন্য আছে gRPC-Web: একটু ভিন্ন একটি protocol, যা সাধারণত মাঝখানের একটি proxy (যেমন Envoy-এর gRPC-Web filter) আসল gRPC-তে বদলে দেয়। লেখার সময়ে gRPC-Web-এ unary আর server streaming চলে; client streaming আর bidirectional চলে না (সংস্করণভেদে আলাদা হতে পারে, ডকুমেন্টেশন দেখে নিন)। ব্রাউজারের দিকে দুই দিকের real-time যোগাযোগের প্রচলিত পথ WebSocket — পর্ব ১০-এ (Real-time যোগাযোগ)।

gRPC বাছবেন যখন

ডেটা সেন্টারের ভেতরে সার্ভিস থেকে সার্ভিস; বহু ভাষার দল; কম latency আর ছোট বার্তা দরকার; streaming লাগে; শক্ত টাইপ-করা চুক্তি আর deadline চাই।

ভেবে দেখবেন যখন

পাবলিক API, যা অচেনা developer curl দিয়ে চালাবেন; ব্রাউজার সরাসরি ডাকবে; CDN-এ HTTP caching জরুরি; দলের debugging-এর টুল শুধু JSON বোঝে।

৬ · GraphQL — ক্লায়েন্ট নিজেই বলে দেয় ঠিক কী চাই

GraphQL একটি query ভাষা আর API-র নিয়ম। Facebook-এ ২০১২ সালের দিকে মোবাইল অ্যাপের news feed-এর জন্য এর জন্ম, আর ২০১৫ সালে এর spec আর একটি reference implementation প্রকাশ্যে আসে; এখন এটি Linux Foundation-এর অধীনের GraphQL Foundation-এ। মূল ভাবনা: সার্ভার একটি টাইপ-করা schema-তে জানায় কী কী পাওয়া যায়, আর ক্লায়েন্ট একটি query-তে লিখে দেয় ঠিক কোন field চাই — উত্তরের আকার হুবহু query-র আকারের মতো।

type Query {
  me: User!
  recommendations(first: Int = 10): [Product!]!
  offers(first: Int = 3): [Offer!]!
}

type Mutation {
  placeOrder(input: PlaceOrderInput!): Order!
}

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

type User {
  id: ID!
  name: String!
  avatarUrl: String
  walletPaisa: Int!
  recentOrders(first: Int = 3): [Order!]!
}

type Order   { id: ID!  status: OrderStatus!  totalPaisa: Int! }
type Product { id: ID!  name: String!  pricePaisa: Int!  thumbnailUrl: String }
type Offer   { id: ID!  title: String!  discountPercent: Int! }
enum OrderStatus { PLACED COOKING ON_THE_WAY DELIVERED }

input PlaceOrderInput {
  restaurantId: ID!
  productIds: [ID!]!
  idempotencyKey: String!
}

! মানে মান কখনো ফাঁকা (null) হবে না; [Product!]! মানে পণ্যের একটি তালিকা, যা নিজেও null নয়, ভেতরের কোনোটিও নয়। তিনটি বিশেষ টাইপ তিন রকম কাজের দরজা: Query — পড়া; mutation — লেখা বা বদলানো; subscription — কিছু বদলালেই সার্ভার থেকে খবর, সাধারণত WebSocket-এর ওপর (পর্ব ১০)। এবার হোম স্ক্রিনের পুরো চাহিদা একটি query-তে:

query HomeScreen {
  me {
    name
    walletPaisa
    recentOrders(first: 3) { id status totalPaisa }
  }
  recommendations(first: 10) { id name pricePaisa }
  offers { title discountPercent }
}

# answer (trimmed): same shape as the query, nothing extra
{
  "data": {
    "me": {
      "name": "Nusrat",
      "walletPaisa": 125000,
      "recentOrders": [ { "id": "A-1029", "status": "ON_THE_WAY", "totalPaisa": 45000 } ]
    },
    "recommendations": [ { "id": "p7", "name": "Kacchi biryani", "pricePaisa": 32000 } ],
    "offers": [ { "title": "Free delivery", "discountPercent": 100 } ]
  }
}

এক অনুরোধ, এক round trip, আর কোনো বাড়তি field নেই — অংশ ১-এর over-fetching আর under-fetching দুটোরই উত্তর। নতুন একটি স্ক্রিনের ভিন্ন field লাগলে সার্ভারে নতুন endpoint লাগে না; ক্লায়েন্ট শুধু query বদলায়। লেখার উদাহরণ: mutation { placeOrder(input: {...}) { id status } } — এখানেও ফেরত কোন field আসবে, ক্লায়েন্ট বলে দেয়। Mutation-এ retry নিরাপদ রাখতে idempotencyKey-এর মতো field দেওয়া ভালো (পর্ব ০৮ আর পর্ব ৩৪)।

Resolver — প্রতিটি field-এর পেছনে একটি ছোট function

সার্ভার query পেলে প্রথমে schema মিলিয়ে যাচাই করে, তারপর গাছের মতো ওপর থেকে নিচে নামে। প্রতিটি field-এর জন্য একটি resolver function চলে: Query.me প্রোফাইল সার্ভিস থেকে ব্যবহারকারী আনে, User.recentOrders অর্ডার সার্ভিস ডাকে, Query.recommendations সুপারিশ সার্ভিস ডাকে। ফলে GraphQL সার্ভার হয়ে ওঠে অনেক সার্ভিসের সামনের একটি সমন্বয়ের জায়গা — অনেকটা রেস্তোরাঁর ওয়েটারের মতো, যিনি একটি অর্ডার-স্লিপ নিয়ে রান্নাঘরের তিন কাউন্টার থেকে তিনটি পদ এনে এক থালায় সাজান।

ত্রুটির ধরন আলাদা। GraphQL-এ একটি অংশ ব্যর্থ হলেও বাকি ডেটা আসতে পারে: "offers": null, সাথে "errors" তালিকায় কারণ। প্রচলিত অনেক সার্ভার তখনও HTTP 200 দেয়, তাই শুধু HTTP status দেখে monitoring করলে ভুল ছবি পাবেন। নতুন GraphQL-over-HTTP নিয়মে কিছু ক্ষেত্রে 4xx/5xx-এর প্রস্তাব আছে — সার্ভার ও টুলভেদে আচরণ আলাদা।

৭ · N+1 query সমস্যা আর DataLoader-এর batching

Resolver-এর সরলতা একটি লুকানো ফাঁদ তৈরি করে। Query: “৫টি post আর প্রতিটির লেখকের নাম।” Query.posts একটি query চালায়; তারপর প্রতিটি post-এর জন্য Post.author আলাদা করে চলে, প্রতিবার নিজের query নিয়ে। মোট ১ + ৫ = ৬ — একে বলে N+1 query সমস্যা. ১০০টি post হলে ১০১টি query. ধাপে ধাপে দেখুন:

সাধারণ resolverপ্রতি post-এ একটি query
Query: { posts: () => db.posts(5) }
Post:  { author: (post) =>
           db.user(post.authorId) }
DataLoader-ধাঁচের resolverkey জমা, পরে একসাথে
Query: { posts: () => db.posts(5) }
Post:  { author: (post, args, ctx) =>
           ctx.userLoader.load(post.authorId) }
// userLoader = new DataLoader(ids => db.usersIn(ids))
Executor-এর অবস্থা
এখন চলছে
DB query
অপেক্ষমাণ key
  1. ধাপ ১ — তালিকা আনা। Query.posts একটি query-তে ৫টি post আনল: ১০১ থেকে ১০৫, লেখক যথাক্রমে ১, ২, ১, ৩, ২. এখন পর্যন্ত DB query ১টি। এটাই সেই “১”।
  2. ধাপ ২ — প্রথম post-এর লেখক। Post ১০১-এর জন্য Post.author চলল, লেখক ১-এর জন্য আলাদা query. মোট ২টি।
  3. ধাপ ৩ — দ্বিতীয় post-এর লেখক। Post ১০২, লেখক ২ — আরেকটি query. মোট ৩টি। প্রতিটি resolver নিজের কাজ ঠিকই করছে; কিন্তু কেউ জানে না অন্যরা কী চাইছে।
  4. ধাপ ৪ — বাকি তিনটি। Post ১০৩–১০৫-এর লেখক ১, ৩, ২ — আরও ৩টি query. মোট ১ + ৫ = ৬টি, অথচ আলাদা লেখক মাত্র ৩ জন; লেখক ১ আর ২-কে দুবার করে আনা হলো। প্রতিটি query-র একটি round trip আছে; ১০০টি post হলে ১০১টি।
  5. ধাপ ৫ — DataLoader: শুধু key জমা। এবার Post.author সরাসরি query চালায় না, userLoader.load(id) ডাকে। Loader key-গুলো টুকে রাখে আর একই key একবারই রাখে: পাঁচবার ডাকা হলেও অপেক্ষমাণ key ৩টি — ১, ২, ৩. DB query এখনো ১টি।
  6. ধাপ ৬ — একসাথে পাঠানো। চলমান কাজের পালা (event loop-এর tick) শেষ হতেই loader একটি batch query চালাল: WHERE id IN (1, 2, 3). মোট query ২টি — post যতই হোক।
  7. ধাপ ৭ — ফল ভাগ করে দেওয়া। Loader প্রতিটি অপেক্ষমাণ resolver-কে তার লেখক বুঝিয়ে দিল, ঠিক যে ক্রমে key চাওয়া হয়েছিল। ১০০টি post আর ৪০ জন লেখক হলেও query ২টি। Loader প্রতিটি অনুরোধের জন্য নতুন বানাতে হয়, যাতে এক ব্যবহারকারীর cache-এ রাখা ডেটা অন্যজনের উত্তরে না যায়।

একই ঘটনা কোডে, সত্যিকারের query log-সহ:

// query { posts(first: 5) { title author { name } } } — two ways to resolve it.
const users = { 1: "Rahim", 2: "Nusrat", 3: "Tanvir" };
const posts = [[101, "Metro rail tips", 1], [102, "Ferry schedule", 2],
  [103, "Rickshaw fares", 1], [104, "Eid bus tickets", 3], [105, "Load-shedding", 2]];
let log = [];
const db = {
  posts: (n) => { log.push(`SELECT * FROM posts LIMIT ${n}`); return posts.slice(0, n); },
  user: (id) => { log.push(`SELECT * FROM users WHERE id = ${id}`); return { name: users[id] }; },
  usersIn: (ids) => { log.push(`SELECT * FROM users WHERE id IN (${ids.join(", ")})`);
    return ids.map((id) => ({ name: users[id] })); },   // same order as ids
};
// Naive resolvers: Post.author runs once per post, each with its own query.
const naive = () => db.posts(5).map(([, title, aid]) => ({ title, author: db.user(aid).name }));
// DataLoader-style: load() only notes the key; dispatch() runs ONE batch for all keys.
function makeLoader(batchFn) {
  const waiting = new Map();                      // key -> callbacks (duplicates merge)
  return {
    load(key, cb) { if (!waiting.has(key)) waiting.set(key, []); waiting.get(key).push(cb); },
    dispatch() { const keys = [...waiting.keys()], rows = batchFn(keys);
      keys.forEach((k, i) => waiting.get(k).forEach((cb) => cb(rows[i]))); waiting.clear(); },
  };
}
function batched() {
  const authors = makeLoader(db.usersIn);         // a new loader for every request
  const out = db.posts(5).map(([, title, aid]) => {
    const row = { title, author: null };
    authors.load(aid, (u) => { row.author = u.name; }); return row;
  });
  authors.dispatch(); return out;    // real DataLoader dispatches at the end of the tick
}
for (const [name, fn] of [["naive resolvers", naive], ["DataLoader-style", batched]]) {
  log = []; const result = fn();
  console.log(`${name}: ${log.length} queries`); log.forEach((q) => console.log("   " + q));
  console.log("   result[3] = " + JSON.stringify(result[3]));
}

আউটপুট:

naive resolvers: 6 queries
   SELECT * FROM posts LIMIT 5
   SELECT * FROM users WHERE id = 1
   SELECT * FROM users WHERE id = 2
   SELECT * FROM users WHERE id = 1
   SELECT * FROM users WHERE id = 3
   SELECT * FROM users WHERE id = 2
   result[3] = {"title":"Eid bus tickets","author":"Tanvir"}
DataLoader-style: 2 queries
   SELECT * FROM posts LIMIT 5
   SELECT * FROM users WHERE id IN (1, 2, 3)
   result[3] = {"title":"Eid bus tickets","author":"Tanvir"}

সাধারণ resolver-এ ৬টি query, যার দুটি সরাসরি পুনরাবৃত্তি (id = 1 আর id = 2 দুবার করে)। DataLoader-ধাঁচে ২টি: তালিকা আর এক batch-এ সব লেখক। দুই ক্ষেত্রেই result[3] একই — “Eid bus tickets”-এর লেখক Tanvir — অর্থাৎ ফল বদলায়নি, শুধু কাজ কমেছে। এখানে dispatch() হাতে ডাকা হয়েছে যাতে ক্রমটি চোখে পড়ে; Facebook-এর প্রকাশ করা JavaScript library DataLoader (আর অন্য ভাষায় তার অনুকরণ) নিজেই tick-এর শেষে এটা করে। মনে রাখবেন: DataLoader অনুরোধের ভেতরের পুনরাবৃত্তি সরায়; বহু অনুরোধ জুড়ে cache-এর কাজ আলাদা (পর্ব ২৮–৩০)। N+1 শুধু GraphQL-এর সমস্যা নয় — ORM দিয়ে লুপে query চালালেও একই ঘটনা; DBMS ও SQL কোর্সের অ্যাপ থেকে ডেটাবেস পর্বে তার রূপ দেখবেন।

৮ · GraphQL-এর দাম — caching, query-র খরচ আর federation

Caching কেন কঠিন

REST-এ GET /products/p7-এর উত্তর URL ধরে CDN, proxy আর ব্রাউজার নিজে থেকেই cache করে। GraphQL-এ সাধারণত সব অনুরোধ যায় একটিই ঠিকানায় — POST /graphql — আর আসল প্রশ্ন থাকে body-তে। HTTP cache URL দেখে চেনে, body নয়; আবার একই ডেটার দুটি query সামান্য ভিন্ন হলেই উত্তরও ভিন্ন। প্রচলিত উপায়গুলো:

  • Persisted query: query-গুলো আগে থেকে সার্ভারে নিবন্ধিত থাকে; ক্লায়েন্ট পাঠায় শুধু তার id বা hash, প্রায়ই GET দিয়ে, যেমন GET /graphql?id=HomeScreen_v3&variables=…. এখন URL দিয়েই চেনা যায়, তাই CDN cache করতে পারে; বাইটও কমে। ঠিক রূপ টুলভেদে আলাদা (যেমন Apollo-র ধরনে query-র SHA-256 hash যায়)।
  • ক্লায়েন্টের ভেতরের cache: Apollo Client বা Relay-এর মতো library উত্তর ভেঙে টাইপ আর id ধরে সাজিয়ে রাখে, তাই অন্য স্ক্রিনে একই অর্ডার আবার আনতে হয় না।
  • সার্ভারের ভেতরে: resolver-এর পেছনে সাধারণ cache (পর্ব ২৮–৩০)।

নিরাপত্তা: একটি query কতটা ভারী হতে পারে

REST-এ একটি endpoint কতটা কাজ করবে, সার্ভার জানে। GraphQL-এ ক্লায়েন্ট যা খুশি জোড়া লাগাতে পারে — “প্রতিটি বন্ধুর প্রতিটি বন্ধুর প্রতিটি post-এর সব মন্তব্য” — এক লাইনের একটি query সার্ভারের লাখো সারি টেনে আনতে পারে। এটা ইচ্ছাকৃত হোক বা ভুলে, সুরক্ষা সার্ভারের দায়িত্ব:

সুরক্ষাকী করে
Depth limitquery কত স্তর গভীরে যেতে পারবে তার সীমা, যেমন সর্বোচ্চ ৮
Cost analysisচালানোর আগেই হিসাব: প্রতিটি field-এর একটি খরচ, তালিকা হলে first দিয়ে গুণ; বাজেট ছাড়ালে প্রত্যাখ্যান
তালিকায় বাধ্যতামূলক সীমাfirst না দিলে বা ১০০-র বেশি চাইলে প্রত্যাখ্যান, পাতায় পাতায় আনা
Persisted query-র allowlistশুধু নিবন্ধিত query চলে; নিজেদের অ্যাপের জন্য সবচেয়ে কড়া সুরক্ষা
Timeout আর rate limitপ্রতিটি query-র সময়সীমা; ক্লায়েন্টপ্রতি খরচভিত্তিক সীমা (পর্ব ৪৯)

উদাহরণ: GitHub-এর পাবলিক GraphQL API-র প্রকাশিত ডকুমেন্টেশন অনুযায়ী প্রতিটি query-র একটি point-খরচ হিসাব হয়, আর প্রতিটি ব্যবহারকারীর নির্দিষ্ট সময়ে point-এর বাজেট থাকে — ঠিক এই cost analysis-এর বাস্তব রূপ (সীমার সংখ্যা সময়ে সময়ে বদলায়)।

অনেক দল, একটি graph — federation

একটি GraphQL সার্ভারে সব schema রাখলে সেটি একটি বড় বাধা হয়ে দাঁড়ায় — প্রতিটি দলের পরিবর্তন একই কোডে। Federation-এ প্রতিটি দল নিজের অংশের schema (subgraph) নিজের সার্ভিসে রাখে, আর সামনের একটি gateway বা router সব মিলিয়ে একটি graph দেখায় আর query ভাগ করে ঠিক সার্ভিসে পাঠায়। Apollo ২০১৯ সালের দিকে এই ধারণা জনপ্রিয় করে। সামনের দরজার এই ভূমিকা — gateway, BFF — পর্ব ১১-এ (Reverse proxy, API gateway আর BFF), আর দলের সীমানা অনুযায়ী সার্ভিস ভাগ পর্ব ৪৭-এ।

৯ · তিনটির তুলনা আর একটি নকশা — চার সার্ভিসের হোম স্ক্রিন

দিকRESTgRPCGraphQL
পরিবহনHTTP/1.1 বা HTTP/2; প্রতিটি resource-এর নিজের URLHTTP/2; প্রতি call একটি streamসাধারণত HTTP POST, একটি endpoint; subscription WebSocket-এ
চুক্তিঐচ্ছিক (OpenAPI)বাধ্যতামূলক .proto, তা থেকে কোডবাধ্যতামূলক schema, introspection দিয়ে পড়া যায়
ডেটার রূপসাধারণত JSON (লেখা)Protobuf (binary, ছোট)JSON, ঠিক চাওয়া field-গুলো
Performanceভালো; resource-ভিত্তিক হলে অনেক round tripছোট বার্তা, কম serialization খরচ, multiplexingক্লায়েন্টের round trip কম; সার্ভারের খরচ query-ভেদে অনিশ্চিত
Streamingনেই (আলাদা SSE বা WebSocket)চার ধরনের call-এ পূর্ণ সমর্থনsubscription
HTTP cachingসবচেয়ে সহজ: GET, Cache-Control, CDNপ্রায় নেই; অ্যাপের ভেতরে cacheকঠিন; persisted query আর ক্লায়েন্টের cache
ব্রাউজারসরাসরিসরাসরি নয়; gRPC-Web আর proxyসরাসরি
টুল আর debuggingসবখানে: ব্রাউজার, curlcodegen, grpcurl; binary চোখে পড়া কঠিনGraphiQL-এর মতো explorer, টাইপের codegen
ভুলের ভাষাHTTP status (404, 503…)grpc-status, ১৭টি কোডপ্রায়ই 200 আর errors তালিকা, আংশিক ডেটা
সবচেয়ে মানানসইপাবলিক API, সরল CRUD, cache-নির্ভর পড়াভেতরের সার্ভিস, কম latency, বহু ভাষা, streamingবহু রকম ক্লায়েন্ট, ঘনঘন বদলানো UI, অনেক সার্ভিসের ডেটা এক পাতায়

এবার অংশ ১-এর হোম স্ক্রিনে ফিরি এবং simulator-এর সংখ্যা ধরে একটি সিদ্ধান্তে পৌঁছাই। প্রতিটি ধাপে কোন বিকল্প বিবেচনায়, তা আলোকিত:

চাহিদা৪ সার্ভিস · ২,৪৬০ বাইট
REST, একটার পর একটা১৪ অনুরোধ
REST, সমান্তরাল২ দফা
BFF: GET /home১ অনুরোধ
GraphQL query১ অনুরোধ
ভেতরে gRPC৪ সার্ভিস, deadline
হিসাব (অংশ ১-এর অনুমান: মোবাইল RTT ১২০ ms)
অনুরোধ · দফা
বাইট
সময় (প্রায়)
মূল ঝুঁকি
  1. ধাপ ১ — চাহিদা। প্রোফাইল, অর্ডার, সুপারিশ আর অফার — চার দলের চার সার্ভিস। পর্দায় লাগে ২,৪৬০ বাইট; ব্যবহারকারী মোবাইল নেটওয়ার্কে, RTT ধরা যাক ১২০ ms. লক্ষ্য: হোম স্ক্রিন দ্রুত, আর কোনো একটি সার্ভিস ধীর হলেও পাতা খুলবে।
  2. ধাপ ২ — অ্যাপ সরাসরি REST, একটার পর একটা। ১৪টি অনুরোধ, ১৪টি round trip — প্রায় ২,১৮৯ ms. প্রতিটি অনুরোধে রেডিও জেগে থাকে, ব্যাটারিও যায়। বাতিল।
  3. ধাপ ৩ — REST, সমান্তরাল। দফা নেমে ২, সময় প্রায় ৩৮৯ ms; কিন্তু ৪৪,৬০০ বাইটের মাত্র ৫.৫% দরকারি। তার ওপর ভেতরের চারটি সার্ভিসকেই ইন্টারনেটের দিকে খুলে দিতে হয় — নিরাপত্তার বড় পরিধি।
  4. ধাপ ৪ — BFF: একটি GET /home. সার্ভারের দিকে মোবাইলের জন্য একটি BFF (Backend for Frontend, পর্ব ১১) চারটি সার্ভিস ডেকে ঠিক দরকারি ডেটা দেয়: ১ অনুরোধ, ৩,০৬০ বাইট, প্রায় ১৮৮ ms. সরল আর HTTP cache-বান্ধব। দাম: প্রতিটি নতুন স্ক্রিন বা নতুন ধরনের ক্লায়েন্টে BFF-এর কোড বদলাতে হয়।
  5. ধাপ ৫ — GraphQL. একই এক round trip (৩,৭৬০ বাইট; persisted query হলে ৩,১২৪), প্রায় ১৯০ ms. সুবিধা নমনীয়তা: Android, iOS আর web নিজের মতো query লেখে, নতুন স্ক্রিনে সার্ভার বদলাতে হয় না। দাম: DataLoader, cost limit, persisted query আর schema-র দেখভাল।
  6. ধাপ ৬ — ভেতরে gRPC, আর সিদ্ধান্ত। BFF বা GraphQL স্তর থেকে চার সার্ভিসে gRPC: টাইপ-করা চুক্তি, ছোট বার্তা, আর ৩০০ ms-এর deadline পুরো শিকলে। অফার সার্ভিস সময়মতো না দিলে offers ফাঁকা রেখে বাকি পাতা দেখানো — graceful degradation (পর্ব ১৮)। ক্লায়েন্টের ধরন কম আর স্ক্রিন স্থির হলে BFF যথেষ্ট; বহু ক্লায়েন্ট আর ঘনঘন বদলালে GraphQL.

বাইরে GraphQL বা BFF, ভেতরে gRPC

Clients → one edge layer → internal gRPC services, each with its own data

বাইরে GraphQL বা BFF, ভেতরে gRPC সার্ভিস Android iOS ব্রাউজার GraphQL server বা BFF Profile Orders Recommend Offers অ্যাপ অ্যাপ web একটি query · persisted query depth ও cost সীমা · DataLoader · ৩০০ ms প্রোফাইল অর্ডার সুপারিশ অফার DB DB cache DB কঠিন রেখা: অনুরোধ-উত্তর · ভাঙা রেখা: streaming ভেতরে প্রতিটি call-এ বাকি deadline যায় HTTPS · JSON gRPC HTTP/2 Protobuf stream ডেটা সেন্টারের ভেতরে
ছবি ৯.১ — ক্লায়েন্ট একটিই দরজা চেনে; ভেতরের সার্ভিসগুলো ইন্টারনেট থেকে আড়ালে। অর্ডারের অবস্থা server streaming-এ এসে ক্লায়েন্টে subscription হয়ে যায় (পর্ব ১০)। বাস্তবে সামনে আরও থাকে load balancer আর gateway (পর্ব ১১, ১৪)।

আরও গভীরে: gRPC-র প্রকাশিত নকশা-নীতিতে (২০১৫) Google লিখেছে, Stubby-র অভিজ্ঞতা থেকেই deadline, cancellation আর flow control-কে প্রথম শ্রেণির বৈশিষ্ট্য করা হয়েছে। Envoy আর gRPC-র xDS API মিলে এখন “proxyless” service mesh সম্ভব — ক্লায়েন্ট library নিজেই control plane থেকে সার্ভারের তালিকা আর load balancing-এর নীতি নেয়। GraphQL-এর দিকে Relay-এর ধারণা — প্রতিটি UI component নিজের দরকারি field (fragment) পাশেই লিখে রাখে, আর build-এর সময় সেগুলো persisted query হয়ে যায় — বড় অ্যাপে over-fetching আর নিরাপত্তা একসাথে সামলায়।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • REST, gRPC আর GraphQL — কখন কোনটি? — পাবলিক আর cache-নির্ভর হলে REST, ভেতরের দ্রুত টাইপ-করা call আর streaming-এ gRPC, বহু ক্লায়েন্টের নমনীয় ডেটা চাহিদায় GraphQL বা BFF.
  • gRPC কেন HTTP/2 নেয়? — এক সংযোগে বহু stream (multiplexing), দুই দিকের streaming, header compression আর trailer-এ ফল পাঠানোর সুযোগ।
  • GraphQL-এ N+1 কী, সমাধান কী? — প্রতিটি parent-এর জন্য child resolver-এর আলাদা query; DataLoader দিয়ে key জমিয়ে এক batch, বা দরকারে JOIN.
  • gRPC-তে L4 load balancer কেন যথেষ্ট নয়? — সংযোগ দীর্ঘস্থায়ী আর multiplexed, তাই L4 সব call এক সার্ভারে পাঠায়; L7 proxy বা client-side load balancing লাগে।
  • Deadline propagation না থাকলে কী হয়? — ক্লায়েন্ট হাল ছাড়ার পরেও নিচের সার্ভিসগুলো কাজ চালায়; ভিড়ে এই অপচয়ই overload আর cascading failure ডেকে আনে।

১০ · AI-এর যুগে API style বাছাই — খসড়া AI-র, যাচাই আপনার

“আমাদের অ্যাপের জন্য gRPC না GraphQL?” — AI-কে জিজ্ঞেস করলে আত্মবিশ্বাসী উত্তর আসে, প্রায়ই সুন্দর তুলনার টেবিলসহ। কিন্তু এই বিষয়ে কয়েকটি ভুল বারবার দেখা যায়:

AI-এর ভুলকেমন দেখায়কীভাবে যাচাই করবেন
সব জায়গায় একই প্রেসক্রিপশন“GraphQL gateway, gRPC, Kafka আর service mesh যোগ করুন” — তিনজনের দলের একটি CRUD অ্যাপেপ্রতিটি অংশের পেছনে কোন requirement বা সংখ্যা, জিজ্ঞেস করুন; সরলতম বিকল্পটি আগে
বানানো performance-দাবি“gRPC সবসময় ১০ গুণ দ্রুত”নিজের payload দিয়ে ছোট benchmark; বেশিরভাগ সময় যায় ডেটাবেস আর network-এ, serialization-এ নয়
হিসাবের ভুলround trip গোনায় নির্ভরশীল call-কে সমান্তরাল ধরাঅংশ ১-এর মতো দফা গুনে আবার হিসাব
Failure mode বাদdeadline, cancellation বা আংশিক ব্যর্থতার কোনো কথা নেইপ্রতিটি call-এ জিজ্ঞেস করুন: ধীর হলে? না এলে? দুবার এলে?
Status code গুলিয়ে ফেলাgRPC-তে “404 ফেরত দিন”, বা DEADLINE_EXCEEDED মানেই কাজ হয়নিgRPC-র অফিশিয়াল status code তালিকা
পণ্যের সীমা বানিয়ে বলাmessage-এর সর্বোচ্চ আকার বা gRPC-Web-এ কী চলে, ভুল বা পুরোনো তথ্যব্যবহৃত library-র সংস্করণের ডকুমেন্টেশন, তারিখসহ
GraphQL-এর দাম লুকানোN+1, caching আর query-র খরচের সীমার কথা নেইresolver-এর query log দেখুন; depth আর cost limit আছে কি না

AI-কে সিদ্ধান্তদাতা নয়, কড়া reviewer বানান। নিজের খসড়া আগে লিখুন, তারপর এভাবে যাচাই করান:

তুমি একজন অভিজ্ঞ backend স্থপতি আর কড়া design reviewer. আমাদের একটি খাবার-ডেলিভারি অ্যাপের (কাল্পনিক) হোম স্ক্রিন চারটি সার্ভিস থেকে ডেটা নেয়: প্রোফাইল, অর্ডার, সুপারিশ, অফার। ক্লায়েন্ট: Android, iOS আর web. আমার প্রস্তাব: [এখানে আপনার নকশা পেস্ট করুন — বাইরে কোন API style, ভেতরে কোনটি, কেন] ১. প্রতিটি পছন্দের পেছনে কোন requirement বা সংখ্যা আছে বলো; কারণ দুর্বল হলে সরলতর বিকল্প দাও। ২. মোবাইল RTT ১২০ ms ধরে round trip আর বাইটের হিসাব ধাপে ধাপে আবার করো; কোন call কোনটির ওপর নির্ভরশীল, আলাদা করে দেখাও। ৩. deadline কীভাবে পুরো শিকলে যাবে, আর একটি সার্ভিস ধীর হলে পাতা কী দেখাবে, বলো। ৪. GraphQL নিলে N+1, caching আর query-র খরচের সীমা কীভাবে সামলাব; gRPC নিলে load balancing কীভাবে। ৫. কোনো library-র সীমা বা আচরণ বললে উৎস আর সংস্করণ দাও; নিশ্চিত না হলে স্পষ্ট লেখো।

AI-এর প্রস্তাব নেওয়ার আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১দূরের call-কে কাছের মতো দেখানোর স্বপ্ন চল্লিশ বছরে বারবার হোঁচট খেয়েছে কেন? gRPC কী শিখেছে?

১৯৮৪ সালে Birrell আর Nelson যখন RPC-র পূর্ণাঙ্গ রূপ প্রকাশ করেন, লক্ষ্য ছিল প্রোগ্রামারের কাছে network-কে অদৃশ্য করা — দূরের function ডাকা হবে কাছের মতোই। ১৯৯০-এর দশকে CORBA আর পরে Java RMI এই স্বপ্নকে আরও এগিয়ে নেয়: দূরের যন্ত্রের object-কে নিজের প্রোগ্রামের object-এর মতো ব্যবহার। ফল প্রায়ই হতাশাজনক। লুপের ভেতরে নিরীহ দেখতে call আসলে network-এ হাজারো যাত্রা; একটি সার্ভার থেমে গেলে ক্লায়েন্ট অনির্দিষ্টকাল ঝুলে থাকে; আর “উত্তর আসেনি” মানে কাজ হয়েছে না হয়নি — বোঝার উপায় নেই।

১৯৯৪ সালে Sun Microsystems-এর Jim Waldo, Geoff Wyant, Ann Wollrath আর Sam Kendall “A Note on Distributed Computing” লেখায় সমস্যার মূল ধরিয়ে দেন: latency, memory-র আলাদা জগৎ, আংশিক ব্যর্থতা আর concurrency — এই চারটি পার্থক্য interface-এর আড়ালে লুকানো যায় না, লুকালে নকশা ভুল হয়। প্রায় একই সময়ে Sun-এর L Peter Deutsch ও সহকর্মীদের নামে প্রচলিত “distributed computing-এর ভ্রান্ত ধারণা”-র তালিকা ছড়ায় — network নির্ভরযোগ্য, latency শূন্য, bandwidth অসীম ইত্যাদি (পর্ব ৩৮)।

gRPC-র নকশায় এই শিক্ষা স্পষ্ট। প্রতিটি call-এ deadline রাখা যায়, আর তা নিচের সার্ভিসে ছড়ায়; cancellation পুরো শিকলে যায়; ব্যর্থতার ১৭টি নির্দিষ্ট কোড আছে, যার একটি (DEADLINE_EXCEEDED) সরাসরি বলে “কাজ হয়েও থাকতে পারে”; আর streaming ও async API লুপের বদলে প্রবাহের ভাষায় ভাবতে শেখায়। অর্থাৎ gRPC দূরত্বকে লুকায় না, বরং ব্যবহারযোগ্য করে।

মূল উপলব্ধি: ভালো RPC network-কে অদৃশ্য করে না, বরং তার খরচ আর ব্যর্থতাকে API-র অংশ করে তোলে — deadline, status code আর idempotency ছাড়া কোনো দূরের call সম্পূর্ণ নয়।

প্র ০২Protobuf আর gRPC Google-এর ভেতর থেকে বেরিয়ে এত জনপ্রিয় হলো কেন? Binary ফরম্যাটের দাম কী?

Google-এর ভেতরে Protocol Buffers ব্যবহার হচ্ছিল বহু বছর ধরে; ২০০৮ সালে এটি open-source হয়। ভেতরের RPC ব্যবস্থা Stubby তখনো প্রকাশ্য ছিল না। ২০১৫ সালে, যে বছর HTTP/2-এর মান (RFC 7540) প্রকাশিত হয়, Google gRPC প্রকাশ করে — Stubby-র অভিজ্ঞতা, কিন্তু সবার জন্য খোলা মানের ওপর দাঁড়ানো। ২০১৬ সালে এর 1.0 সংস্করণ আসে, আর ২০১৭ সালে প্রকল্পটি CNCF-এ যোগ দেয়। Kubernetes-কেন্দ্রিক cloud-native জগতে etcd-এর মতো অনেক open-source প্রকল্প gRPC ব্যবহার করে।

জনপ্রিয়তার তিনটি কারণ। প্রথম, একটি চুক্তি থেকে বহু ভাষার কোড — বড় প্রতিষ্ঠানে দল ভিন্ন ভাষায় কাজ করলে এটি বিশাল সুবিধা। দ্বিতীয়, দক্ষতা: ছোট binary বার্তা আর এক সংযোগে বহু call মানে কম CPU আর কম bandwidth; প্রতি সেকেন্ডে লাখো ভেতরের call-এ এই সাশ্রয় বিলের অঙ্কে দেখা যায়। তৃতীয়, deadline, streaming আর status code-এর মতো production-এর দরকারি জিনিস framework-এর ভেতরেই।

দামও আছে। বাইট চোখে পড়া যায় না; একটি ভুল উত্তর বুঝতে grpcurl আর .proto ফাইল লাগে, যেখানে JSON-এ ব্রাউজারই যথেষ্ট। চুক্তি বদলাতে শৃঙ্খলা লাগে: field number পুনর্ব্যবহার করলে পুরোনো আর নতুন কোড নীরবে ভুল অর্থ করবে (পর্ব ১২)। ব্রাউজার সরাসরি পারে না, proxy লাগে। আর HTTP caching-এর সহজ সুবিধা হারায়। Facebook-এর Thrift (২০০৭) আর Apache Avro-র মতো বিকল্পও একই সমস্যার ভিন্ন সমাধান; কোনোটিই সব ক্ষেত্রে সেরা নয়।

মূল উপলব্ধি: binary আর টাইপ-করা চুক্তি যন্ত্রের জন্য সাশ্রয়, মানুষের জন্য একটু কম সুবিধাজনক — তাই ভেতরের সার্ভিসে gRPC আর বাইরের অচেনা developer-এর জন্য REST, এই ভাগ এত প্রচলিত।

প্র ০৩Facebook কেন GraphQL বানাল, আর কেন এটি সবার জন্য ঠিক উত্তর নয়?

২০১২ সালের দিকে Facebook তার মোবাইল অ্যাপ নতুন করে native কোডে লিখছিল। প্রকাশ্য বক্তৃতা আর লেখায় GraphQL-এর স্রষ্টারা — Lee Byron, Nick Schrock আর Dan Schafer — যে সমস্যার কথা বলেছেন, তা অংশ ১-এর হোম স্ক্রিনের মতোই: news feed-এর একটি পাতায় post, লেখক, ছবি, মন্তব্য, লাইক — বহু রকম ডেটা, ধীর মোবাইল নেটওয়ার্কে বহু round trip, আর প্রতিটি নতুন ফিচারে সার্ভারের endpoint বদলানোর ঝামেলা। সমাধান হলো ক্লায়েন্টকে একটি টাইপ-করা ভাষা দেওয়া, যাতে সে নিজের দরকার নিজেই বলে। ২০১৫ সালে spec আর reference implementation প্রকাশ্যে আসে, আর ২০১৮ সালে Linux Foundation-এর অধীনে GraphQL Foundation গঠিত হয়।

কিন্তু Facebook-এর সমস্যা সবার সমস্যা নয়। GraphQL-এর দাম: resolver-এর N+1 সামলাতে DataLoader-এর শৃঙ্খলা; একটি URL-এ POST বলে CDN caching-এর সহজ পথ বন্ধ; ক্লায়েন্ট যেকোনো আকারের query লিখতে পারে বলে depth আর cost limit লাগে; monitoring-এ HTTP status যথেষ্ট নয়, প্রতিটি query আলাদা করে মাপতে হয়; আর schema-টাই একটি পণ্য, যার মালিকানা আর পরিবর্তনের নিয়ম লাগে। এজন্যই Facebook নিজে persisted query ব্যবহার করে বলে প্রকাশিত লেখায় এসেছে — ক্লায়েন্ট পুরো query নয়, শুধু id পাঠায়।

একটি ছোট দল, একটি web অ্যাপ আর স্থির কয়েকটি পাতা হলে সুনির্দিষ্ট REST endpoint বা একটি BFF প্রায়ই সরল আর যথেষ্ট। অন্যদিকে বহু রকম ক্লায়েন্ট, দ্রুত বদলানো UI আর ডজনখানেক সার্ভিসের ডেটা এক পাতায় দরকার হলে GraphQL-এর নমনীয়তা তার জটিলতার দাম শোধ করে।

মূল উপলব্ধি: GraphQL জটিলতাকে ক্লায়েন্ট থেকে সার্ভারে সরিয়ে আনে। সার্ভারের দিকে সেই জটিলতা সামলানোর দল আর শৃঙ্খলা থাকলে এটি শক্তিশালী; না থাকলে সমস্যা শুধু জায়গা বদলায়।

প্র ০৪নতুন সার্ভার যোগ করলাম, অথচ পুরোনোগুলোর ভার কমল না — gRPC-তে এমন কেন হয়, আর বড় সিস্টেম কী করে?

পরিচিত ঘটনা: একটি gRPC সার্ভিসের তিনটি সার্ভার একটি TCP-স্তরের load balancer-এর পেছনে। ভিড় বাড়ায় আরও তিনটি সার্ভার যোগ হলো, কিন্তু graph-এ দেখা গেল নতুনগুলো প্রায় অলস, পুরোনোগুলো আগের মতোই ব্যস্ত। কারণ HTTP/2-এর মূল সুবিধাতেই লুকানো: ক্লায়েন্ট একবার সংযোগ খুলে তাতে মাসের পর মাস হাজারো call চালাতে পারে। L4 load balancer সিদ্ধান্ত নেয় শুধু নতুন সংযোগ খোলার মুহূর্তে; পুরোনো সংযোগের call কখনো নতুন সার্ভারে যায় না। HTTP/1.1-এর ছোট ছোট সংযোগের যুগে এই সমস্যা কম চোখে পড়ত।

বড় সিস্টেম তিন পথে এগোয়। প্রথম, HTTP/2 বোঝে এমন L7 proxy: Lyft-এ তৈরি হয়ে ২০১৬ সালে open-source হওয়া Envoy-এর মতো proxy প্রতিটি call আলাদা করে ভাগ করে; service mesh-এ (Istio, Linkerd) প্রতিটি সার্ভিসের পাশে এমন একটি proxy চলে (পর্ব ৪৭)। দ্বিতীয়, client-side load balancing: ক্লায়েন্ট library নিজেই DNS বা service registry থেকে সব সার্ভারের তালিকা নেয়, প্রত্যেকের সাথে সংযোগ রাখে আর call ঘুরিয়ে দেয় — মাঝে কোনো বাড়তি hop নেই, কিন্তু প্রতিটি ভাষার library-কে এই বুদ্ধি রাখতে হয়। gRPC-র xDS সমর্থন এই তালিকা আর নীতি একটি কেন্দ্রীয় control plane থেকে আনতে দেয়। তৃতীয়, সার্ভার মাঝে মাঝে সংযোগের বয়সসীমা টেনে ক্লায়েন্টকে নতুন সংযোগ খুলতে বাধ্য করে, যাতে ভার ধীরে ধীরে নতুন সার্ভারে ছড়ায়।

একই কারণে Kubernetes-এর সাধারণ Service দিয়ে gRPC চালালে একটি সংযোগ একটি Pod-এ আটকে থাকে; headless Service আর client-side load balancing, বা mesh — এ দুটিই প্রচলিত সমাধান।

মূল উপলব্ধি: load balancing-এর একক হলো সংযোগ না অনুরোধ — এই প্রশ্নটি দীর্ঘস্থায়ী সংযোগের যুগে নকশার প্রথম প্রশ্নগুলোর একটি।

প্র ০৫DEADLINE_EXCEEDED পেলে ধরে নেওয়া যায় কি যে কাজটি হয়নি? Deadline আর retry কীভাবে একসাথে নকশা করবেন?

না — আর gRPC-র অফিশিয়াল status code-এর বর্ণনাতেই এই সতর্কতা আছে: অবস্থা বদলানো কাজে DEADLINE_EXCEEDED ফেরত আসতে পারে এমনকি কাজটি সফলভাবে শেষ হলেও। কারণ সহজ: সার্ভার কাজ শেষ করে উত্তর পাঠিয়েছে, কিন্তু উত্তর পৌঁছানোর আগেই ক্লায়েন্টের সময় ফুরিয়েছে। একটি টাকা পাঠানোর call-এ এর অর্থ — অন্ধভাবে আবার চেষ্টা করলে টাকা দুবার যেতে পারে। তাই অবস্থা বদলানো প্রতিটি call-এ একটি idempotency key (পর্ব ০৮, ৩৪): একই key-এর দ্বিতীয় অনুরোধে সার্ভার আগের ফলই ফেরত দেবে।

Deadline আর retry-র সম্পর্কও সূক্ষ্ম। Retry করতে হবে মূল deadline-এর ভেতরে, নতুন পূর্ণ সময় নিয়ে নয়; নইলে প্রতিটি স্তর নিজের মতো তিনবার চেষ্টা করলে তিন স্তরের শিকলে নিচের সার্ভিসে ৩ × ৩ × ৩ = ২৭ গুণ অনুরোধ পৌঁছাতে পারে — ঠিক যখন সে আগে থেকেই ধীর। Google-এর Site Reliability Engineering বই (২০১৬) cascading failure-এর অধ্যায়ে deadline ছড়িয়ে দেওয়া আর retry-র বাজেট সীমিত রাখার পরামর্শ দেয়। Jeff Dean আর Luiz André Barroso-র “The Tail at Scale” (২০১৩) দেখায়, fan-out-এ সবচেয়ে ধীর অংশই পুরো অনুরোধের সময় ঠিক করে (পর্ব ০৪) — তাই ধীর অংশকে সময়মতো বাদ দিয়ে আংশিক উত্তর দেওয়াও একটি কৌশল।

একটি ভালো নকশার রূপ: প্রবেশদ্বারে ব্যবহারকারীর অভিজ্ঞতা থেকে deadline ঠিক (যেমন ৩০০ ms); প্রতিটি স্তর বাকি সময় নিচে পাঠায়; হাতে যথেষ্ট সময় না থাকলে কাজ শুরুই না করা (simulator-এর early নীতি); retry শুধু UNAVAILABLE-এর মতো ক্ষণস্থায়ী কোডে, backoff আর jitter দিয়ে, একটি স্তরেই; আর ঐচ্ছিক অংশ (অফার) না এলে বাকি পাতা দেখানো। পূর্ণ কৌশল পর্ব ৪৮-এ।

মূল উপলব্ধি: timeout মানে “আমি আর অপেক্ষা করছি না”, “কাজ হয়নি” নয়। Deadline, idempotency আর সীমিত retry — তিনটি একসাথে নকশা করলেই ব্যর্থতা নিরাপদ হয়।

অনুশীলন

আগে নিজে খাতায় লিখুন, তারপর উত্তরের সাথে মেলান। নকশার প্রশ্নে একটিমাত্র সঠিক উত্তর নেই — যুক্তিটাই আসল।

  1. একটি .proto লিখুন: একটি আন্তঃজেলা বাস-টিকিট সেবার (কাল্পনিক) seat সার্ভিস তিনটি কাজ দেয় — (ক) তারিখ আর রুট দিয়ে trip খোঁজা, (খ) একটি trip-এ নির্দিষ্ট seat সাময়িকভাবে ধরে রাখা (hold), যা নিরাপদে আবার চেষ্টা করা যায়, (গ) একটি trip-এর seat খালি বা ভরা হলেই সাথে সাথে খবর পাওয়া। Service, message আর call-এর ধরন লিখুন।
    syntax = "proto3";
    package bus.v1;
    
    import "google/protobuf/timestamp.proto";
    
    service SeatService {
      rpc SearchTrips (SearchTripsRequest) returns (SearchTripsResponse); // unary
      rpc HoldSeat (HoldSeatRequest) returns (HoldSeatResponse);          // unary, idempotent
      rpc WatchSeats (WatchSeatsRequest) returns (stream SeatChange);     // server streaming
    }
    
    message SearchTripsRequest {
      string from_city = 1;
      string to_city = 2;
      string travel_date = 3;       // "2026-11-05"
      int32 page_size = 4;
      string page_token = 5;
    }
    message Trip {
      string trip_id = 1;
      google.protobuf.Timestamp departs_at = 2;
      int64 fare_paisa = 3;
      int32 seats_left = 4;
    }
    message SearchTripsResponse {
      repeated Trip trips = 1;
      string next_page_token = 2;
    }
    
    message HoldSeatRequest {
      string trip_id = 1;
      string seat_no = 2;           // "B4"
      string idempotency_key = 3;   // same key again = same hold, never a second one
    }
    message HoldSeatResponse {
      string hold_id = 1;
      google.protobuf.Timestamp expires_at = 2;
    }
    
    message WatchSeatsRequest { string trip_id = 1; }
    message SeatChange {
      string seat_no = 1;
      bool available = 2;
    }
    • ধরন বাছাই: খোঁজা আর hold — unary; খবর পাওয়া — server streaming, কারণ ক্লায়েন্ট একবার চায় আর সার্ভার বারবার পাঠায়।
    • Idempotency: idempotency_key থাকায় DEADLINE_EXCEEDED পেয়ে আবার চেষ্টা করলেও দ্বিতীয় seat ধরা হয় না। একই seat অন্য কেউ ধরে রাখলে সার্ভার ALREADY_EXISTS বা FAILED_PRECONDITION দিতে পারে — চুক্তির নথিতে লিখে রাখুন।
    • বাকি খুঁটিনাটি: ভাড়া পয়সায় int64; বড় ফলাফলে page_token; hold-এর মেয়াদ expires_at-এ। ক্লায়েন্ট প্রতিটি call-এ deadline দেবে, যেমন খোঁজায় ৫০০ ms.
  2. একটি N+1 resolver ঠিক করুন: একটি GraphQL সার্ভারে নিচের resolver আছে। Query { orders(first: 50) { id restaurant { name } } }-এ ৫০টি অর্ডার, যাদের রেস্তোরাঁ মোট ১২টি। এখন কতটি DB query হয়? কোডটি কীভাবে বদলাবেন, আর বদলের পর কতটি হবে?
    Query:  { orders: (_, { first }) => db.query("SELECT * FROM orders LIMIT ?", first) }
    Order:  { restaurant: (order) =>
                db.query("SELECT * FROM restaurants WHERE id = ?", order.restaurantId) }

    এখন: ১টি query অর্ডারের তালিকায় + ৫০টি, প্রতি অর্ডারে একটি রেস্তোরাঁ = ৫১টি; তার মধ্যে ৫০ − ১২ = ৩৮টি একই রেস্তোরাঁর পুনরাবৃত্তি।

    // one loader per request: created in the request context
    const context = () => ({
      restaurantLoader: new DataLoader(async (ids) => {
        const rows = await db.query("SELECT * FROM restaurants WHERE id IN (?)", ids);
        const byId = new Map(rows.map((r) => [r.id, r]));
        return ids.map((id) => byId.get(id) ?? null);   // same order and length as ids
      }),
    });
    
    const resolvers = {
      Order: { restaurant: (order, _args, ctx) => ctx.restaurantLoader.load(order.restaurantId) },
    };
    • বদলের পর: ১টি অর্ডারের query + ১টি batch (১২টি আলাদা id) = ২টি।
    • তিনটি জরুরি খুঁটিনাটি: batch function-কে ঠিক ids-এর ক্রমে আর সমান সংখ্যায় ফল দিতে হবে; না পাওয়া id-র জায়গায় null (বা error); আর loader প্রতিটি অনুরোধে নতুন, যাতে এক ব্যবহারকারীর ডেটা অন্যের উত্তরে না যায়।
    • বিকল্প: resolver query-র গঠন দেখে (look-ahead) অর্ডার আর রেস্তোরাঁ একটি JOIN-এ আনতে পারে; তবে DataLoader সহজ আর সার্ভিস জুড়েও কাজ করে — রেস্তোরাঁর তথ্য আলাদা সার্ভিসে থাকলে batch gRPC call (GetRestaurants(ids)).
  3. API style বাছাই, যুক্তিসহ: তিনটি পরিস্থিতিতে বাইরের API আর ভেতরের যোগাযোগে কী নেবেন? (ক) একটি পেমেন্ট সেবা (কাল্পনিক), যার API অচেনা তৃতীয় পক্ষের developer-রা নিজেদের ওয়েবসাইটে জুড়বেন; (খ) একটি ride-sharing অ্যাপের ভেতরে ৩০টি সার্ভিস, যেখানে চালকের অবস্থান প্রতি কয়েক সেকেন্ডে আসে আর matching সার্ভিসকে খুব কম latency-তে ডেটা লাগে; (গ) একটি অনলাইন বইয়ের দোকানের Android, iOS আর web অ্যাপ, যাদের স্ক্রিন প্রতি মাসে বদলায় আর প্রতিটি স্ক্রিন ৫–৬টি সার্ভিসের ডেটা দেখায়।
    • (ক) REST (JSON), সাথে webhook. অচেনা developer, যেকোনো ভাষা আর curl — REST-এর ঘর্ষণ সবচেয়ে কম; OpenAPI-তে নথি; টাকা পাঠানোর প্রতিটি POST-এ idempotency key; ফলাফল জানাতে webhook. gRPC এখানে অচেনা ক্লায়েন্টের ওপর codegen আর HTTP/2 চাপিয়ে দেয়; GraphQL-এর নমনীয় query পেমেন্টের মতো সুনির্দিষ্ট, নিরাপত্তা-কঠিন কাজে কম দরকারি। ভেতরে চাইলে gRPC.
    • (খ) ভেতরে gRPC, অবস্থানের জন্য streaming. সব দল নিয়ন্ত্রিত, ভাষা আলাদা হতে পারে, latency কঠিন — Protobuf আর টাইপ-করা চুক্তি। চালকের অ্যাপ থেকে অবস্থান client streaming বা bidirectional stream-এ (মোবাইল অ্যাপ native হওয়ায় gRPC চলে; ব্রাউজার নয়)। প্রতিটি call-এ deadline, L7 বা client-side load balancing জরুরি। খুব বেশি অবস্থান-ঘটনা হলে অনেক নকশায় মাঝে একটি message queue বা log-ভিত্তিক stream থাকে (পর্ব ৩৩, ৫৮)।
    • (গ) GraphQL (বা একাধিক BFF), ভেতরে gRPC. তিন রকম ক্লায়েন্ট, ঘনঘন বদলানো স্ক্রিন আর বহু সার্ভিস — GraphQL-এর মূল ক্ষেত্র। শর্ত: persisted query, depth আর cost limit, DataLoader, আর বইয়ের মতো প্রায়-স্থির ডেটার জন্য cache. দল ছোট হলে প্রতিটি অ্যাপের জন্য একটি BFF-ও যুক্তিসংগত, তবে স্ক্রিন বদলালেই BFF বদলাতে হবে।
  4. আউটপুট অনুমান আর হিসাব: অংশ ৫-এর deadline_chain.js-এ সব একই রেখে শুধু C-র কাজ ২৫০ থেকে ১৫০ ms করা হলো, অর্থাৎ pre: 150 লেখা হলো। (ক) তিন নীতিতে ক্লায়েন্ট কী পাবে, কখন? (খ) কোন সার্ভিসের call DEADLINE_EXCEEDED পাবে? (গ) none-এ ক্লায়েন্ট হাল ছাড়ার পরে কত ms কাজ হবে? (ঘ) আসল কোডে (C ২৫০ ms) শুধু DEADLINE ৪৫০ করলে কী হবে? আগে খাতায় হিসাব করুন, তারপর কোড বদলে মিলিয়ে নিন।

    সময়রেখা (C = ১৫০): A পৌঁছায় ৫-এ, কাজ ৪০ → ৪৫; B পৌঁছায় ৫০, কাজ ৬০ → ১১০; C পৌঁছায় ১১৫, কাজ ১৫০ → ২৬৫; B উত্তর পায় ২৭০, বাকি কাজ ৩০ → ৩০০; A উত্তর পেত ৩০৫-এ — সীমার পরে।

    আসল আউটপুট:

    none      client DEADLINE_EXCEEDED at 300 ms | work A/B/C 60/90/150 = 300 | after give-up 20
    propagate client DEADLINE_EXCEEDED at 300 ms | work A/B/C 40/90/150 = 280 | after give-up 0
              B (orders) DEADLINE_EXCEEDED at 300
    early     client DEADLINE_EXCEEDED at 300 ms | work A/B/C 40/90/150 = 280 | after give-up 0
              B (orders) DEADLINE_EXCEEDED at 300
    • (ক) তিন নীতিতেই ক্লায়েন্ট পায় DEADLINE_EXCEEDED, ৩০০ ms-এ। early-এও না — প্রতিটি সার্ভিস শুধু নিজের কাজের সময় দেখে (C-র দরকার ১৫০, হাতে ১৮৫), পুরো শিকলের নয়।
    • (খ) propagate আর early-এ সময় ফুরায় B-তে, ঠিক ৩০০ ms-এ, তার শেষের ৩০ ms কাজের সময়। ধীর অংশ C নয়, অথচ ব্যর্থতা দেখা দিল B-তে — DEADLINE_EXCEEDED বলে কোথায় সময় ফুরাল, কে ধীর ছিল তা নয়। কে কতটা সময় খেল, তা বুঝতে distributed tracing লাগে (পর্ব ৫০)।
    • (গ) none-এ হাল ছাড়ার পরে শুধু A-র শেষের কাজ: ৩০৫ থেকে ৩২৫ = ২০ ms (B-র কাজ ঠিক ৩০০-তে শেষ)। মোট কাজ ৬০ + ৯০ + ১৫০ = ৩০০ ms, সবটাই বৃথা; propagate-এ ২৮০ ms, কারণ A-র শেষের ২০ ms বাঁচে।
    • (ঘ) শিকলের স্বাভাবিক সময় ৪৩০ ms, যা ৪৫০-এর মধ্যে — তিন নীতিতেই client OK at 430 ms, কাজ ৬০/৯০/২৫০ = ৪০০, হাল ছাড়ার পরে ০। শিক্ষা: deadline নির্ধারণ করুন পুরো পথের p99 মেপে, অনুমানে নয়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি gRPC unary call-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. অ্যাপের কোড deadline দিয়ে client stub-এর method ডাকে
  2. Stub request বার্তাটিকে Protobuf বাইটে serialize করে
  3. HTTP/2 সংযোগে নতুন stream-এ HEADERS frame যায় (:path, grpc-timeout)
  4. DATA frame-এ length-prefixed বার্তা যায়
  5. সার্ভার বাইট decode করে handler চালায়
  6. উত্তরের DATA আর শেষে trailers-এ grpc-status আসে

একটি ride-sharing অ্যাপে চালকের অ্যাপ একটি call খুলে প্রতি ৩ সেকেন্ডে অবস্থান পাঠায়, আর সার্ভার একই call-এ যখন খুশি নতুন রাইডের প্রস্তাব পাঠায়। gRPC-র কোন ধরনের call এর জন্য সবচেয়ে মানানসই?

  • Unary
  • Server streaming
  • Client streaming
  • Bidirectional streaming

তিনটি gRPC সার্ভার একটি L4 (TCP-স্তরের) load balancer-এর পেছনে। একটি ক্লায়েন্ট একটিমাত্র HTTP/2 সংযোগ খুলে তাতে প্রতি সেকেন্ডে ৩,০০০ call পাঠায়। সাধারণত কী ঘটে?

  • প্রতিটি সার্ভার প্রায় ১,০০০টি করে call পায়
  • ৩,০০০টি call-ই একটি সার্ভারে যায়, বাকি দুটি প্রায় অলস
  • HTTP/2 নিজেই তিন সার্ভারে তিনটি সংযোগ খুলে ভার ভাগ করে
  • Load balancer gRPC বোঝে না বলে call-গুলো প্রত্যাখ্যান করে

একটি GraphQL query ২০টি post আর প্রতিটির লেখকের নাম চায়; ২০টি post-এর লেখক মোট ৮ জন। লেখকের resolver প্রতি post-এ আলাদা DB query চালালে, আর DataLoader দিয়ে এক batch-এ আনলে মোট DB query যথাক্রমে কত?

  • ২১ আর ২
  • ২০ আর ৮
  • ২১ আর ৯
  • ৮ আর ১

ক্লায়েন্টের deadline ৩০০ ms. সার্ভিস A নিজের ৮০ ms কাজ করে B-কে ডাকে; B নিজের ১২০ ms কাজ করে C-কে ডাকে। Deadline propagation থাকলে, network-এর সময় বাদ দিয়ে, C সর্বোচ্চ কত ms সময় পায়?

  • ৩০০ ms
  • ১০০ ms
  • ১৮০ ms
  • ২২০ ms

একটি GraphQL API-র সব অনুরোধ POST /graphql-এ যায়, query থাকে body-তে। সাধারণ CDN এদের উত্তর সহজে cache করতে পারে না কেন, আর প্রচলিত সমাধান কী?

  • JSON উত্তর cache করা যায় না; সমাধান Protobuf
  • GraphQL-এর উত্তর সবসময় খুব বড়; সমাধান compression
  • সব অনুরোধের URL এক, আসল প্রশ্ন body-তে — সমাধান persisted query আর GET
  • GraphQL HTTPS ছাড়া চলে না; সমাধান HTTP

একটি টাকা পাঠানোর gRPC call DEADLINE_EXCEEDED ফেরত দিল। ক্লায়েন্টের সবচেয়ে নিরাপদ ধারণা আর পদক্ষেপ কোনটি?

  • টাকা যায়নি; একই অনুরোধ নতুন করে পাঠানো যায়
  • টাকা নিশ্চয়ই গেছে; কিছু করার নেই
  • কাজ হয়েও থাকতে পারে; একই idempotency key দিয়ে আবার চেষ্টা বা অবস্থা যাচাই
  • deadline দ্বিগুণ করে অন্য একটি নতুন অনুরোধ পাঠানো

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Resource-ভিত্তিক REST-এ মোবাইলের একটি পাতায় over-fetching, under-fetching আর অনেক round trip; বাইটের চেয়ে round trip বেশি দামি।
  2. gRPC = .proto চুক্তি + code generation + HTTP/2; একটি call একটি stream; unary আর তিন রকম streaming; ফল trailer-এর grpc-status-এ।
  3. প্রতিটি call-এ deadline, আর তা নিচে ছড়ান; DEADLINE_EXCEEDED মানে “কাজ হয়নি” নয় — idempotency লাগে।
  4. দীর্ঘস্থায়ী HTTP/2 সংযোগে L4 load balancer call ভাগ করে না; L7 proxy বা client-side load balancing লাগে। ব্রাউজারে gRPC-Web আর proxy.
  5. GraphQL-এ ক্লায়েন্ট ঠিক field চায়; দাম N+1 (সমাধান DataLoader), কঠিন caching (persisted query) আর query-র খরচের সীমা। প্রচলিত নকশা: বাইরে REST, BFF বা GraphQL, ভেতরে gRPC.

পরের পর্বে: পর্ব ১০, real-time যোগাযোগ — polling, long polling, SSE আর WebSocket. GraphQL-এর subscription আর অর্ডারের অবস্থার খবর আসলে কোন পথে ব্রাউজার আর ফোনে পৌঁছায়, সেটাই দেখব।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান