সেশন ১২

Service-to-Service Communication - এক service যখন আরেকজনকে ডাকে

এই সেশনের লক্ষ্য: Microservice গুলো একে অপরের সাথে কীভাবে কথা বলে, আর সেই কথাবার্তা কোথায় কোথায় ভাঙতে পারে, তা বোঝা। Sync আর async call, REST আর gRPC, API contract আর versioning, latency chain আর fan-out, প্রতিটা call এ timeout, cascading failure, নিরাপদ retry, আর service mesh: একটা ride-sharing app এর একটা screen দিয়ে সব কিছু দেখা।

০০ যে প্রশ্নগুলোর উত্তর খুঁজবো

সেশন ১১ এ আমরা দেখেছি, কেন একটা বড় application কে ছোট ছোট service এ ভাগ করা হয়। ভাগ করার পর একটা নতুন বাস্তবতা সামনে আসে। আগে যে কাজটা ছিলো একই program এর ভেতরে একটা function call, এখন সেটা network এর উপর দিয়ে আরেকটা machine এ যাওয়া একটা request।

Function call প্রায় কখনো হারায় না, আর মাঝপথে আটকেও থাকে না। Network call হারাতে পারে, ধীর হতে পারে, আবার অর্ধেক কাজ করে থেমেও যেতে পারে। এই সেশনে আমরা দেখবো, service গুলো কীভাবে একে অপরের সাথে কথা বলে, আর একটা service এর ছোট সমস্যা কীভাবে পুরো system কে থামিয়ে দিতে পারে। শেষে আপনি জানবেন, এই বিপদ ঠেকাতে প্রতিটা call এ কোন নিয়মগুলো মানতে হয়।

  • ১ একটা screen এর জন্য service গুলো একে অপরকে কীভাবে ডাকে? (sync RPC বনাম async message)
  • ২ REST নাকি gRPC? JSON নাকি Protobuf?
  • ৩ API contract কী, আর API versioning ছাড়া একটা field বদলালে কী ভাঙে?
  • ৪ Call গুলো series এ চললে latency chain কত লম্বা হয়?
  • ৫ Call গুলো parallel এ fan-out করলে কতটা লাভ, আর tail latency কেন ভোগায়?
  • ৬ একটা service ধীর হলে পুরো app কেন আটকে যায়? (partial failure, cascading failure)
  • ৭ প্রতিটা call এ timeout কেন লাগে, আর মানটা কত হবে?
  • ৮ Retry কখন নিরাপদ? (retry storm, idempotency)
  • ৯ সব service এ একই নিয়ম কীভাবে চালু রাখবেন? (service mesh, sidecar)

০১ একটা screen এর জন্য service গুলো একে অপরকে কীভাবে ডাকে?

শুক্রবার সন্ধ্যা ছয়টা। অফিস ছুটির ভিড়ে ঢাকার রাস্তায় হাজার হাজার মানুষ ride খুঁজছে। সাকিব একটা ride-sharing app এর backend engineer। সে নিজেও বাড়ি ফেরার জন্য একটা bike ডাকতে যাচ্ছিলো, তখনই ফোনে alert বাজলো। Support team এর group এ একের পর এক message আসছে: “App খুলছে না”, “ভাড়া দেখাচ্ছে না”, “ride book হচ্ছে না”। সাকিব dashboard খুলে অবাক হলো। প্রায় সব service ঠিকঠাক চলছে, শুধু Pricing service টা একটু ধীর। একটা service একটু ধীর হলে পুরো app কেন বন্ধ হয়ে যাবে?

এই প্রশ্নের উত্তর পেতে আগে বুঝতে হবে, service গুলো একে অপরের সাথে কথা বলে কীভাবে। সাকিবের দলের সবচেয়ে ব্যস্ত screen টা দিয়েই শুরু করা যাক।

Trip Summary screen: একটা screen, ছয়টা service

Ride শেষ হলে rider একটা summary screen দেখে। সেখানে থাকে পথের map, ভাড়ার হিসাব, driver এর নাম আর ছবি, payment হয়েছে কিনা, rating দেওয়ার জায়গা, আর পরের ride এর একটা promo। এই প্রতিটা তথ্য আলাদা আলাদা service এর কাছে থাকে।

Rider App Trip Summary screen ১টা request Summary Service সব তথ্য জোগাড় করে একসাথে দেয় Trip 30ms Driver 40ms Pricing 80ms Payment 50ms Map 60ms Promo 40ms প্রতিটা তীর একটা network call: ধীর হতে পারে, fail করতে পারে
Rider একবার screen খুললে একটাই request যায়। কিন্তু সেই উত্তর বানাতে Summary service কে ছয়টা আলাদা service কে ডাকতে হয়। স্বাভাবিক দিনে প্রতিটা call কত সময় নেয়, সেটা box এর ভেতরে লেখা। Pricing আলাদা রঙে আছে, কারণ গল্পের বিপদটা ওখান থেকেই শুরু হবে।

Monolith এর যুগে (সেশন ১১) এগুলো একই program এর ভেতরের function ছিলো। pricing.getFare(48213) লিখলে উত্তর আসতো কয়েক microsecond এ। এখন Pricing আলাদা machine এ চলা আলাদা একটা service। তাই একই লাইনের পেছনে এখন একটা network request যায়, আর উত্তর আসে network পেরিয়ে।

RPC: দেখতে function call, আসলে network call

অন্য machine এর একটা function কে নিজের code এর function এর মতো করে ডাকার কৌশলকে বলে RPC (Remote Procedure Call)। Code এ দেখতে সাধারণ function call, কিন্তু পেছনে request টা network দিয়ে যায়, আর উত্তরও network দিয়ে ফেরে।

এই মিলটাই সবচেয়ে বড় ফাঁদ। Function call এর ফল দুই রকম: উত্তর আসে, অথবা error আসে। Network call এর আরো একটা ফল আছে: কিছুই আসে না। Request টা পৌঁছেছে কিনা, কাজটা হয়েছে কিনা, caller সেটা জানতেও পারে না। এই সেশনের প্রায় সব সমস্যা এই তৃতীয় অবস্থা থেকেই আসে।

দুই রকম কথা বলা: sync আর async

Service গুলো দুইভাবে কথা বলতে পারে। পার্থক্যটা এক প্রশ্নে: যে ডাকছে, সে কি উত্তরের জন্য অপেক্ষা করে?

SYNC (RPC) ASYNC (MESSAGE) Summary ভাড়া এখনই লাগবে getFare() ৳285 Pricing হিসাব করে উত্তর দেয় Summary উত্তরের অপেক্ষায় থাকে উত্তর না এলে screen আটকে থাকে Trip Service trip শেষ হলো trip_completed QUEUE Email রিসিট পাঠায় Earnings driver এর আয় Trip Service message রেখেই নিজের কাজে ফেরে
বামে sync call: Summary প্রশ্ন করে, তারপর উত্তর না আসা পর্যন্ত অপেক্ষা করে। ডানে async message: Trip Service শুধু জানিয়ে দেয় "trip শেষ", তারপর নিজের কাজে ফিরে যায়। Email আর Earnings পরে নিজেদের সময়মতো কাজটা সারে।
Sync (RPC)Async (message)
Caller কি অপেক্ষা করে?হ্যাঁ, উত্তর না আসা পর্যন্তনা, message রেখেই নিজের কাজে ফেরে
কখন মানায়উত্তর ছাড়া পরের ধাপ এগোয় নাশুধু জানিয়ে দিলেই যথেষ্ট
সাকিবের app এ উদাহরণSummary screen এর জন্য ভাড়া জানাTrip শেষে রিসিট email, driver এর আয় আপডেট করা
অন্য পক্ষ down থাকলেCaller এর কাজও আটকে যায়Message queue তে অপেক্ষা করে, পরে process হয়

Async এর কৌশল আমরা আগেই শিখেছি: queue আর ACK (সেশন ০৮), আর event দিয়ে অনেক service কে একসাথে জানানো (সেশন ১০)। এই সেশনের বেশিরভাগ জুড়ে থাকবে sync call। কারণ rider screen এর দিকে তাকিয়ে আছে, তার উত্তর এখনই দরকার।

কোনটা বেছে নেবেন

নিজেকে একটা প্রশ্ন করুন: উত্তরটা হাতে না পেলে কি আমার পরের কাজ এগোবে? না এগোলে sync call দিন। শুধু খবরটা পৌঁছে দিলেই চললে message দিন। যত বেশি কাজ async এ সরানো যায়, একটা service এর ধীরতা তত কম অন্যদের আটকায়।

০২ REST নাকি gRPC? JSON নাকি Protobuf?

Sync call করার সবচেয়ে প্রচলিত দুইটা পথ হলো REST আর gRPC।

REST আর JSON

REST আপনি আগে থেকেই চেনেন, হয়তো নামটা না জেনেই। HTTP এর পরিচিত method (GET, POST) দিয়ে একটা URL এ request পাঠানো হয়, আর উত্তর আসে JSON এ। যেমন GET /fares/48213 পাঠালে Pricing একটা JSON ফেরত দেয়। JSON হলো text, তাই মানুষ চোখে পড়তে পারে। Browser, curl, Postman, সবাই REST বোঝে।

gRPC আর Protobuf

gRPC হলো Google এর তৈরি একটা open-source RPC framework। এখানে আপনি প্রথমে একটা .proto file এ লিখে রাখেন, service টার কোন কোন function আছে আর প্রতিটা message দেখতে কেমন। যেমন: rpc GetFare(FareRequest) returns (FareReply);। তারপর একটা tool সেই file থেকে Go, Java, Python বা অন্য ভাষায় client আর server এর code বানিয়ে দেয়। Summary তখন সত্যিই একটা function এর মতো pricingClient.GetFare(...) ডাকে।

gRPC এর data যায় Protobuf (Protocol Buffers) format এ। এটা text নয়, binary format। প্রতিটা field এর নাম প্রতিবার না পাঠিয়ে শুধু তার একটা ছোট নম্বর পাঠানো হয়। নম্বর দেখে কোন field, সেটা দুই পক্ষই .proto file থেকে জানে। নিচে একই উত্তর দুই format এ দেখুন:

// JSON (REST এ সাধারণ): প্রতিবার field এর নামসহ text যায়
{"trip_id":48213,"fare_taka":285,"distance_km":7.4,"surge":1.2}
size: 63 bytes

// Protobuf (gRPC এ): shape টা একবার .proto file এ লেখা থাকে
message FareReply {
  int64 trip_id     = 1;
  int32 fare_taka   = 2;
  float distance_km = 3;
  float surge       = 4;
}
// wire এ যায় শুধু field number আর binary value, নাম যায় না
size: 17 bytes   (প্রায় ৪ গুণ ছোট)

একটা call এ ৪৬ byte কম মনে হতে পারে সামান্য। কিন্তু সাকিবের system এ সন্ধ্যার peak এ প্রতি সেকেন্ডে কয়েক হাজার internal call হয়। বাস্তবের message এ field ও থাকে অনেক বেশি। তখন ছোট payload মানে network এ কম byte, আর JSON parse করার চেয়ে কম CPU। gRPC চলে HTTP/2 এর উপর, যেটা একটাই connection দিয়ে একসাথে অনেকগুলো request চালাতে পারে। প্রতিটা call এর জন্য নতুন connection খুলতে হয় না।

REST + JSONgRPC + Protobuf
মানুষ পড়তে পারে?হ্যাঁ, textনা, binary
Payload এর আকারবড় (field এর নামসহ যায়)ছোট (শুধু নম্বর আর মান)
Contractআলাদা document লাগে (যেমন OpenAPI).proto file নিজেই contract
Browser থেকে সরাসরিসহজবাড়তি layer লাগে
যেখানে বেশি মানায়বাইরের client (mobile app, browser), public APIভেতরের service থেকে service, যেখানে call অনেক বেশি
বাস্তবে যা দেখা যায়

অনেক company দুটোই ব্যবহার করে। বাইরের দুনিয়ার সাথে কথা হয় REST আর JSON এ, কারণ সবাই সেটা বোঝে আর debug করা সহজ। ভেতরের service গুলো নিজেদের মধ্যে কথা বলে gRPC তে, কারণ সেখানে call অনেক বেশি আর গতি বেশি জরুরি। কোনটা “ভালো”, সেটা বড় প্রশ্ন নয়। দুই পক্ষ একই নিয়মে কথা বলছে কিনা, সেটাই আসল। সেই নিয়মের নাম contract।

০৩ API contract কী, আর API versioning ছাড়া একটা field বদলালে কী ভাঙে?

Contract: দুই service এর মধ্যে চুক্তি

Pricing আর Summary আলাদা দুইটা team চালায়। দুইজনের code আলাদা, আর deploy ও হয় আলাদা দিনে। তাদের মধ্যে একটা লিখিত চুক্তি থাকে: কোন endpoint এ request যাবে, request এ কী থাকবে, উত্তরে কোন field আসবে, প্রতিটা field এর type আর মানে কী, আর error হলে কী আসবে। এই চুক্তির নাম API contract।

মানে কথাটা খেয়াল করুন। "fare": 285 বললে ২৮৫ টাকা নাকি ২৮৫ পয়সা, সেটাও contract এর অংশ। gRPC তে .proto file টাই লিখিত contract। REST এ সাধারণত একটা OpenAPI file এই কাজ করে।

একটা মঙ্গলবারের ছোট বদল

Pricing team ঠিক করলো, ভাড়া এখন থেকে পয়সায় হিসাব হবে, যাতে ৳২৮৫.৫০ এর মতো ভাড়াও ঠিকঠাক দেখানো যায়। একজন developer উত্তরের fare field এর নাম বদলে fare_paisa করে দিলো, আর মঙ্গলবার দুপুরে deploy করলো। Summary team আগে থেকেই জানতো, তারা তৈরি ছিলো। কিন্তু Booking service ও Pricing কে ডাকে, আর Booking team এই খবর পায়নি। Booking এর code তখনো fare খুঁজছে। Field টা আর না থাকায় সে ধরে নিলো ভাড়া ০। সেই বিকেলে যারা ride book করলো, তারা estimate এ দেখলো ৳0।

এমন যেকোনো বদল, যেটা পুরোনো client কে ভেঙে দেয়, তাকে বলে breaking change। কোন বদল নিরাপদ আর কোনটা নয়, সেটা জানা থাকলে বেশিরভাগ দুর্ঘটনা এড়ানো যায়:

বদলপুরোনো client এর কী হয়
নতুন একটা field যোগ করাকিছুই হয় না। অচেনা field সে উপেক্ষা করে। নিরাপদ।
Field এর নাম বদলানোপুরোনো নামে খুঁজে কিছু পায় না। ভাঙে।
Field মুছে ফেলাএকই কথা। ভাঙে।
Type বা মানে বদলানো (taka থেকে paisa)সবচেয়ে বিপজ্জনক। Field এর নাম একই থাকলে কোনো error আসে না, কিন্তু client ২৮৫০০ কে টাকা ভেবে দেখায় ৳২৮,৫০০। নীরবে ভাঙে।

API versioning: পুরোনোটা রেখে নতুনটা পাশে চালু করা

Breaking change একেবারে না করে থাকা যায় না, ব্যবসার প্রয়োজন বদলায়। তাই নিয়ম হলো: পুরোনো contract ভাঙবেন না, নতুন একটা version পাশে চালু করবেন। একই service এর একাধিক contract একসাথে চালু রাখার এই কৌশলকে বলে API versioning।

Pricing Service ফেরত পায়: {"fare": 285} ফেরত পায়: {"fare_paisa": 28500} Booking Service পুরোনো code, এখনো v1 Summary Service নতুন code, v2 তে গেছে /v1/fare পুরোনো shape, এখনো চালু /v2/fare নতুন shape, নতুন client এর জন্য Version ছাড়া সরাসরি "fare" এর নাম বদলালে: Booking "fare" খুঁজে পায় না, rider দেখে ভাড়া ৳0
Pricing team ভাড়া taka থেকে paisa তে নিতে চায়। সরাসরি বদলালে যে team এখনো নিজের code বদলায়নি, তাদের service ভেঙে যায়। তাই নতুন shape যায় /v2 তে, আর /v1 আগের মতোই চলতে থাকে। /v1 এর traffic শূন্যে নামলে তবেই সেটা বন্ধ করা হয়।

ধাপগুলো সাধারণত এমন:

  1. Pricing নতুন shape টা /v2/fare এ চালু করে। /v1/fare আগের মতোই চলে।
  2. প্রতিটা client team নিজের সুবিধামতো সময়ে v2 তে সরে যায়।
  3. Pricing team মাপতে থাকে, v1 এ এখনো কারা request পাঠাচ্ছে।
  4. v1 এর traffic শূন্যে নামলে তবেই v1 বন্ধ করা হয়।

Mobile app এর ক্ষেত্রে এই অপেক্ষা অনেক লম্বা হয়। অনেকে মাসের পর মাস app update করে না, তাই public API এর পুরোনো version কয়েক বছরও চালু রাখতে হয়।

Protobuf এ একটা বাড়তি সুবিধা আছে। Wire এ field এর নাম যায় না, যায় শুধু নম্বর। তাই .proto file এ field এর নাম বদলালেও পুরোনো client ভাঙে না। কিন্তু নম্বর বা type বদলালে ভাঙে। তাই Protobuf এর নিয়ম: একবার কোনো নম্বর ব্যবহার হলে সেটা আর কখনো অন্য field কে দেবেন না।

Contract একটা প্রতিশ্রুতি

আপনার API কে যে ডাকে, সে আপনার উপর ভরসা করে নিজের code লিখেছে। তাই contract বদলানোর আগে ভাবুন, কে কে এই field পড়ে। নতুন জিনিস যোগ করুন, কিন্তু পুরোনো জিনিস জায়গামতো রেখে দিন। মানে বা type বদলাতে হলে নতুন version খুলুন।

০৪ Call গুলো series এ চললে latency chain কত লম্বা হয়?

সাকিব যখন Summary service প্রথম লিখেছিলো, তখন সে call গুলো একটার পর একটা করেছিলো। প্রথমে Trip, তারপর Driver, তারপর Pricing, এভাবে শেষে Promo। Code পড়তে সহজ, আর ঠিকঠাক কাজও করছিলো।

কিন্তু এভাবে একটার পর একটা call করলে প্রতিটা call এর সময় যোগ হয়ে যায়। একে বলে latency chain। শিকলের প্রতিটা আংটা মোট দৈর্ঘ্যে নিজের অংশ যোগ করে। প্রথম সারিতে দেখুন:

SERIES একটা শেষ হলে তবেই পরেরটা Trip 30ms Driver 40ms Pricing 80ms Payment 50ms Map 60ms Promo 40ms মোট 300ms: ছয়টা সময় যোগ হয় PARALLEL Trip থেকে id পেলেই বাকি ৫টা একসাথে Trip 30ms Driver 40ms Pricing 80ms Payment 50ms Map 60ms Promo 40ms মোট 110ms: সবচেয়ে ধীর call টাই সময় ঠিক করে
একই ছয়টা call, দুইভাবে সাজানো। উপরে series: সময়গুলো যোগ হয়ে 300ms। নিচে parallel: Trip আগে লাগে, কারণ বাকিদের trip এর id দরকার। তারপর বাকি পাঁচটা একসাথে যায়, আর মোট সময় হয় Trip (30ms) যোগ সবচেয়ে ধীর Pricing (80ms), মানে 110ms।

ছয়টা call এর সময় যোগ করলে ৩০ + ৪০ + ৮০ + ৫০ + ৬০ + ৪০, মানে ৩০০ms। এর সাথে যোগ হয় Summary এর নিজের কাজ, আর rider এর mobile network এর আসা-যাওয়া। ফলে rider প্রায় আধা সেকেন্ড পর screen দেখে।

শুধু সময় নয়, failure ও যোগ হয়

Chain লম্বা হলে শুধু সময় বাড়ে না। প্রতিটা service কখনো কখনো fail করে। ধরুন প্রতিটা service ৯৯.৯% সময় ঠিকঠাক উত্তর দেয় (SLA আর availability এর ধারণা সেশন ০১ এ দেখেছেন)। Screen টা ঠিকঠাক দেখাতে ছয়টা service কেই একসাথে ঠিক থাকতে হবে:

Chain এ কয়টা serviceসবাই একসাথে ঠিক থাকার সম্ভাবনামাসে মোটামুটি কতক্ষণ screen ভাঙা
১টা৯৯.৯%প্রায় ৪৩ মিনিট
৬টা০.৯৯৯ এর ৬ ঘাত, প্রায় ৯৯.৪%প্রায় ৪.৩ ঘণ্টা
২০টাপ্রায় ৯৮%প্রায় ১৪ ঘণ্টা

প্রতিটা service আলাদাভাবে খুব ভালো। তবু chain এ জুড়লে পুরো screen এর অবস্থা অনেক খারাপ দেখায়। Chain এর গভীরতাও গুনতে হয়। Pricing নিজেও surge হিসাবের জন্য আরো দুইটা service ডাকলে সেগুলোও এই chain এর অংশ হয়ে যায়।

মনে রাখার মতো কথা

Series এ সাজানো call এর সময় যোগ হয়, আর ঠিক থাকার সম্ভাবনা গুণ হয়। তাই একটা request এর পথে যত কম service থাকে, তত ভালো। নতুন service যোগ করার আগে জিজ্ঞেস করুন, এটা কি সত্যিই এই request এর পথেই থাকতে হবে?

০৫ Call গুলো parallel এ fan-out করলে কতটা লাভ, আর tail latency কেন ভোগায়?

আগে দেখুন, কে কার উপর নির্ভর করে

সাকিব code টা আবার পড়লো। Trip service প্রথমে লাগে, কারণ trip থেকেই driver এর id, পথের তথ্য আর payment এর id আসে। কিন্তু Trip এর উত্তর হাতে এলে বাকি পাঁচটা call কেউ কারো উপর নির্ভর করে না। Driver এর ছবি জানতে ভাড়ার দরকার নেই। Promo জানতে map এর দরকার নেই।

তাই সে বাকি পাঁচটা call একসাথে পাঠালো, আর সবগুলোর উত্তরের জন্য একসাথে অপেক্ষা করলো। একটা request থেকে এভাবে অনেকগুলো call একসাথে ছড়িয়ে দেওয়াকে বলে fan-out। উপরের diagram এর দ্বিতীয় সারিতে দেখুন। মোট সময় এখন Trip এর ৩০ms আর সবচেয়ে ধীর Pricing এর ৮০ms, মানে ১১০ms। ৩০০ms থেকে ১১০ms, শুধু call গুলোর ক্রম বদলে।

Parallel এ মোট সময় হয় সবচেয়ে ধীর call টার সমান। বাকিরা আগে শেষ হলেও Summary কে সবচেয়ে ধীরজনের জন্য অপেক্ষা করতেই হয়। এই কথাটাই একটা নতুন সমস্যা তৈরি করে।

Tail latency: দলের সবচেয়ে ধীরজন

একটা service এর গড় সময় দিয়ে তার আসল চেহারা বোঝা যায় না। তাই engineer রা p99 মাপেন। ১০০টা request কে দ্রুত থেকে ধীর ক্রমে সাজালে ৯৯তম request টা যত সময় নেয়, সেটাই p99। মানে ৯৯% request এর চেয়ে দ্রুত শেষ হয়, আর ১% এর চেয়ে বেশি সময় নেয়। এই ধীর ১% কে বলে tail latency।

একটা service এর ১% ধীর request শুনতে ছোট সমস্যা। কিন্তু fan-out এ একটা screen অনেকগুলো service এর উপর একসাথে নির্ভর করে:

ধরুন পাঁচটা service এর প্রতিটা:
  ৯৯% call  : 100ms এর মধ্যে উত্তর দেয়
   ১% call  : 1 second লাগে (GC pause, ঠান্ডা cache, ধীর disk)

Parallel এ পাঁচটা call গেলে screen দ্রুত হবে শুধু তখন, যখন পাঁচটাই দ্রুত:
  P(পাঁচটাই দ্রুত)      = 0.99 ^ 5     = 0.951
  P(অন্তত একটা ধীর)     = 1 - 0.951    = 0.049   (প্রায় ২০টা screen এ ১টা)

সন্ধ্যার peak এ 400 screen/second:
  400 × 0.049 ≈ 20 জন rider প্রতি সেকেন্ডে পুরো 1 second অপেক্ষা করে

প্রতিটা service আলাদাভাবে ৯৯% সময় দ্রুত। তবু প্রতি ২০টা screen এর মধ্যে একটা ধীর হয়। Fan-out যত চওড়া হয়, সমস্যা তত বাড়ে। একটা request ১০০টা service ছুঁয়ে গেলে প্রায় ৬৩% request এ অন্তত একটা ধীর উত্তর থাকে। Google এর engineer রা এই সমস্যা নিয়ে “The Tail at Scale” নামে একটা বিখ্যাত লেখা লিখেছিলেন। বড় system এ গড় নয়, tail ই user এর অভিজ্ঞতা ঠিক করে দেয়।

Fan-out এর দুই দিক

Parallel call মোট সময় অনেক কমায়, তাই নির্ভরতা না থাকলে call গুলো একসাথে পাঠান। কিন্তু মনে রাখুন, তখন সবচেয়ে ধীর service টাই আপনার গতি ঠিক করে। কোনো একটা service অস্বাভাবিক ধীর হলে কী হবে, সেই প্রশ্নের উত্তর আগেই ঠিক করে রাখতে হয়। পরের দুইটা section সেটা নিয়েই।

০৬ একটা service ধীর হলে পুরো app কেন আটকে যায়?

এবার শুক্রবার সন্ধ্যায় ফেরা যাক। ঠিক কী হয়েছিলো, সাকিব পরে logs ঘেঁটে বের করলো।

Partial failure: কিছু অংশ ঠিক, কিছু অংশ নয়

Monolith এ সাধারণত দুইটা অবস্থা: পুরো program চলছে, অথবা পুরোটা crash করেছে। অনেকগুলো service এর system এ একটা তৃতীয় অবস্থা খুব সাধারণ: কিছু service ঠিক আছে, আর কিছু ধীর বা down। একে বলে partial failure। অনেক machine এ ছড়ানো এমন system কে বলে distributed system। সেখানে partial failure ব্যতিক্রম নয়, প্রতিদিনের ঘটনা।

সেদিন সন্ধ্যা ছয়টা দুই মিনিটে Pricing এর surge হিসাবের একটা database query হঠাৎ ধীর হয়ে গেলো। Table টায় নতুন একটা column এ খোঁজা হচ্ছিলো, কিন্তু সেটার index ছিলো না (index এর কথা সেশন ০৭ এ দেখেছেন)। প্রতিটা fare এর উত্তর আসতে এখন ৮০ms এর বদলে ৫ সেকেন্ড লাগছে। Pricing কিন্তু down হয়নি। সে উত্তর দিচ্ছে, শুধু অনেক দেরিতে।

আটকে থাকা thread এর হিসাব

সেশন ০৮ এ দেখেছেন, server এর ভেতরে thread গুলো কর্মীর মতো, প্রত্যেকে একবারে একটা request সামলায়। Summary service এর আছে ২০০টা thread। একটা thread Pricing কে call করে উত্তরের অপেক্ষায় থাকলে সেই পুরো সময় সে আর কারো কাজ করতে পারে না। কয়টা thread একসাথে আটকে থাকবে, সেটা একটা সহজ সূত্রে বের করা যায়:

একসাথে আটকে থাকা thread = প্রতি সেকেন্ডে request × প্রতিটা request এর সময়
                           (Little's Law)

Summary Service এর আছে 200টা thread।

স্বাভাবিক সন্ধ্যা : 400 req/s × 0.11 s =   44 thread busy   (200 এর মধ্যে, আরামে চলে)
Pricing ধীর (5s)  : 400 req/s × 5 s    = 2000 thread দরকার   (আছে মাত্র 200)

200টা thread ভরে যায়: 200 ÷ 400 = মাত্র 0.5 second এ
তারপর নতুন সব request লাইনে দাঁড়ায়, যেগুলোর Pricing লাগেই না সেগুলোও

এই সূত্রের নাম Little’s Law। এটা মনে রাখলে system design interview তে অনেক প্রশ্নের হিসাব মুখে মুখে করা যায়। স্বাভাবিক দিনে Summary এর ২০০ জনের মধ্যে মাত্র ৪৪ জন ব্যস্ত থাকে। Pricing ধীর হতেই দরকার হয় ২০০০ জন। আধা সেকেন্ডের মধ্যে সব thread Pricing এর অপেক্ষায় আটকে যায়।

Cascading failure: ধীরতা উপরের দিকে ছড়ায়

এরপর যা হলো, সেটাই আসল বিপদ:

ধীরতা নিচ থেকে উপরে ছড়ায় Rider App spinner ঘুরছেই, কিছুই খোলে না Summary Service 200/200 thread আটকে Booking Service ভাড়ার estimate এ আটকে Pricing Service একটা query: 80ms থেকে 5s 18:02:00 Pricing এর একটা DB query ধীর হলো, প্রতি call এ 5s 18:02:01 Summary আর Booking এর সব thread Pricing এর অপেক্ষায় 18:02:05 App এ spinner; rider রা বারবার refresh করে, load আরো বাড়ে 18:03:00 নতুন ride book হচ্ছে না, অথচ Booking এর নিজের কোনো bug নেই
Call যায় উপর থেকে নিচে, কিন্তু ধীরতা ফেরে নিচ থেকে উপরে। Pricing ধীর হলো, তাই Summary আর Booking এর thread গুলো তার উত্তরের অপেক্ষায় আটকে গেলো। তারপর আটকালো পুরো app। একটা service এর সমস্যা এভাবে ধাপে ধাপে সবাইকে অচল করে দিলে তাকে বলে cascading failure।

Summary এর সব thread আটকে যাওয়ায় সে trip history এর মতো এমন request ও নিতে পারছিলো না, যেগুলোর Pricing লাগেই না। Booking service ও ভাড়ার estimate এর জন্য Pricing কে ডাকে। তার thread গুলোও একইভাবে আটকে গেলো, আর নতুন ride book হওয়া বন্ধ হলো। Rider রা spinner দেখে বারবার refresh করছিলো, ফলে request আরো বাড়লো।

একটা service এর সমস্যা যখন তার caller দের আটকে দেয়, আর তারা আবার নিজেদের caller দের আটকায়, তখন তাকে বলে cascading failure। সিঁড়িতে একজন হোঁচট খেলে যেমন পেছনের সবাই একে একে আটকে যায়, তেমন। সাকিবের ঘটনায় একটা index এর অভাবে পুরো app বন্ধ হয়ে গিয়েছিলো।

ধীর service, down service এর চেয়েও বিপজ্জনক

Pricing পুরোপুরি down হলে connection সাথে সাথে fail করতো। Summary কয়েক millisecond এর মধ্যে error পেয়ে পরের কাজে চলে যেতো। ধীর service error দেয় না, শুধু অপেক্ষা করায়। সেই অপেক্ষাতেই caller এর thread, memory আর connection আটকে যায়। তাই “উত্তর আসছে না” অবস্থাটাকে সবচেয়ে বেশি ভয় করুন।

Cascading failure ঠেকানোর পুরো কৌশল (circuit breaker, bulkhead, rate limiting) আমরা দেখবো সেশন ২১ এ। কিন্তু প্রথম আর সবচেয়ে জরুরি প্রতিরক্ষা খুব সহজ, আর সেটা এখনই শিখে নেওয়া যায়।

০৭ প্রতিটা call এ timeout কেন লাগে, আর মানটা কত হবে?

সেশন ০১ এ দেখেছেন, timeout মানে একটা নির্দিষ্ট সময়সীমা। তার মধ্যে উত্তর না এলে অপেক্ষা ছেড়ে দেওয়া হয়। সাকিব খুঁজে দেখলো, Summary এর Pricing call এ কোনো timeout ই দেওয়া ছিলো না। অনেক HTTP library তে default timeout থাকে না, অথবা থাকে অনেক লম্বা। যেমন Python এর জনপ্রিয় requests library তে timeout না দিলে সে উত্তরের জন্য অনির্দিষ্ট সময় অপেক্ষা করতে পারে।

সাকিব Pricing call এ ৩০০ms এর timeout বসালো। আর ঠিক করলো, সময় পার হলে কী দেখাবে:

১. Summary, Pricing কে ডাকে getFare(48213) · timeout: 300ms ২. Summary অপেক্ষা করে বড়জোর 300ms, তার বেশি নয় 300ms এর মধ্যে উত্তর এলো 300ms পার, উত্তর নেই ৩ক. ভাড়া দেখায়: ৳285 পুরো screen স্বাভাবিক ৩খ. Fallback দেখায় "ভাড়া একটু পরে দেখাচ্ছি" map, driver, rating ঠিকঠাক Thread আটকে থাকে বড়জোর 300ms, 5 second নয় 400 req/s × 0.3 s = 120 thread, 200 এর মধ্যেই থাকে
Timeout দেওয়া মানে আগেই ঠিক করে রাখা, কতক্ষণ অপেক্ষা করবো। সময়ের মধ্যে উত্তর এলে সব স্বাভাবিক। না এলে Summary অপেক্ষা ছেড়ে দেয়, আর ভাড়ার জায়গায় একটা fallback বার্তা দেখায়। Screen এর বাকি অংশ ঠিকঠাক কাজ করে, আর thread মুক্ত হয়ে পরের rider কে সামলায়।

এখন Pricing ৫ সেকেন্ড নিলেও Summary এর একটা thread বড়জোর ৩০০ms আটকে থাকে। Little’s Law দিয়ে আবার হিসাব করুন: ৪০০ × ০.৩, মানে ১২০টা thread। ২০০ এর মধ্যেই থাকে। Summary চালু থাকে, আর তার সাথে চালু থাকে বাকি app।

Fallback: পুরোটা না পারলে যতটা পারা যায়

উত্তর না এলে কী দেখাবেন, সেই বিকল্প ব্যবস্থার নাম fallback। সাকিবের ক্ষেত্রে fallback হলো ভাড়ার জায়গায় “একটু পরে দেখাচ্ছি” লেখা। আরেকটা উপায় হতে পারে cache এ রাখা শেষ জানা ভাড়াটা দেখানো (cache এর কথা সেশন ০৪ এ)। এভাবে একটা অংশ fail করলেও পুরো screen না ভেঙে কম তথ্য নিয়ে চালিয়ে যাওয়াকে বলে graceful degradation। Rider এর কাছে “ভাড়া একটু পরে আসবে” অনেক ভালো, পুরো app আটকে থাকার চেয়ে।

Timeout এর মান কত হবে

Timeout খুব ছোট হলে স্বাভাবিক call ও fail করবে। খুব বড় হলে thread আটকে থাকবে। একটা ভালো শুরু হলো: যে service কে ডাকছেন, তার p99 দেখুন, আর তার একটু উপরে timeout দিন। Pricing এর স্বাভাবিক p99 ছিলো ২০০ms, তাই সাকিব দিলো ৩০০ms।

Timeout budget: পুরো পথের জন্য একটা সময়

Timeout শুধু একটা call এর ব্যাপার নয়। পুরো request এর জন্য একটা মোট সময় থাকে, আর ভেতরের প্রতিটা call কে সেই সময়ের ভেতরে থাকতে হয়। একে বলে timeout budget। সাকিবের হিসাবটা দেখুন:

কেকতক্ষণ অপেক্ষা করে
Rider App, Summary এর জন্য২০০০ms
Summary, নিজের পুরো কাজের জন্য১০০০ms এর মধ্যে শেষ করতে চায়
Summary, Trip call এর জন্য (series)১৫০ms
Summary, বাকি পাঁচটা parallel call এর জন্যপ্রতিটা ৩০০ms
সবচেয়ে খারাপ ক্ষেত্রে Summary এর মোট অপেক্ষা১৫০ + ৩০০, মানে ৪৫০ms, budget এর ভেতরে

ভেতরের timeout সবসময় বাইরের timeout এর চেয়ে ছোট রাখতে হয়। উল্টো হলে app অপেক্ষা ছেড়ে দেওয়ার পরও Summary কাজ করে যাবে, অথচ সেই উত্তর আর কেউ নেবে না। gRPC তে এর জন্য একটা সুবিধা আছে: caller এর হাতে কত সময় বাকি, সেটা (deadline) call এর সাথেই পরের service এ চলে যায়। সময় ফুরিয়ে গেলে পরের service কাজটা আর শুরুই করে না।

নিয়মটা এক লাইনে

Network এর উপর দিয়ে যাওয়া প্রতিটা call এ timeout থাকবে, কোনো ব্যতিক্রম নেই। আর প্রতিটা timeout এর সাথে ঠিক করা থাকবে, সময় পার হলে user কী দেখবে।

০৮ Retry কখন নিরাপদ?

Timeout হলো, এখন কী করবেন? প্রথম চিন্তা আসে: আরেকবার চেষ্টা করি। অনেক সময় এটা ঠিকও। Network এ একটা packet হারিয়ে গিয়েছিলো, বা Pricing এর একটা copy ঠিক তখন restart হচ্ছিলো। এমন ক্ষণিকের সমস্যাকে বলে transient failure। দ্বিতীয়বার চেষ্টায় এগুলো প্রায়ই ঠিক হয়ে যায়। কিন্তু retry এর দুইটা লুকানো বিপদ আছে।

বিপদ ১: retry গুণ হয়ে যায়

সাকিবের system এ retry ছিলো সব জায়গায়। Rider App fail হলে তিনবার চেষ্টা করে। Summary ও Pricing call fail হলে তিনবার চেষ্টা করে। প্রত্যেকে নিজের দিক থেকে ঠিক কাজই করছে। কিন্তু সব মিলিয়ে হিসাবটা দাঁড়ায় এমন:

প্রতিটা dot একটা request, যা এই layer এ পৌঁছায় Rider App ১ বার tap, ৩ বার পর্যন্ত চেষ্টা × 3 Summary প্রতিটা request এ ৩ বার চেষ্টা × 3 Pricing (আগে থেকেই ধীর) পায় ৯টা request rider এর ১টা tap থেকে Pricing এ ৯টা request মাঝে আরেকটা layer ও ৩ বার চেষ্টা করলে সংখ্যাটা হয় ২৭
প্রতিটা layer নিজের মতো করে ভাবে: "একবার fail করলে আরো দুইবার চেষ্টা করি।" কিন্তু retry গুলো গুণ হয়ে যায়। যে Pricing আগে থেকেই ধীর, সে এখন নয় গুণ request পাচ্ছে। ধীর service এর উপর retry থেকে আসা এই বাড়তি চাপকে বলে retry storm।

Pricing যখন এমনিতেই ধীর, তখন retry গুলো তার উপর আরো চাপ বাড়ায়। ধীর service আরো ধীর হয়, আরো timeout হয়, আরো retry আসে। এই চক্রকে বলে retry storm। শুক্রবার সন্ধ্যায় Pricing এর কাছে আসলে স্বাভাবিকের চেয়ে কয়েক গুণ বেশি request এসেছিলো, যার বেশিরভাগই retry।

বিপদ ২: কাজটা হয়তো আগেই হয়ে গেছে

Timeout হলে caller জানে না, অন্য পাশে কাজটা হয়েছে কিনা। ভাড়া জানতে চাওয়ার মতো call এ এতে সমস্যা নেই। দুইবার জিজ্ঞেস করলেও উত্তর একই। কিন্তু টাকা কাটার call এ ব্যাপারটা আলাদা:

Trip Service Payment Service ১. charge ৳285 · key: trip-48213 ২. ৳285 কাটলো, OK পাঠালো ৩. OK পথেই হারালো ৪. 300ms এ উত্তর নেই, তাই retry ৫. একই request আবার, একই key key না থাকলে: আবার কাটলো rider এর গেলো ৳570 key থাকলে: আগের OK ফেরত টাকা কাটলো একবারই: ৳285
Timeout হলে caller জানে না, কাজটা হয়েছে কিনা। এখানে টাকা আসলে কাটা হয়ে গেছে, শুধু উত্তরটা পথে হারিয়েছে। Key ছাড়া retry করলে rider দুইবার টাকা দেয়। Idempotency key থাকলে Payment বুঝে ফেলে এটা একই অনুরোধ, আর আগের ফলাফলটাই আবার পাঠায়।

সমাধান আপনি সেশন ০৮ এ দেখেছেন: idempotency। প্রতিটা charge request এর সাথে একটা unique key পাঠানো হয়, এখানে trip এর id। Payment service আগে দেখে নেয়, এই key এর কাজ আগে হয়েছে কিনা। হয়ে থাকলে আবার টাকা না কেটে আগের ফলাফলটাই ফেরত দেয়।

নিরাপদ retry এর নিয়ম

CallRetry করা যায়?
ভাড়া জানা, driver এর তথ্য পড়া (শুধু পড়া)হ্যাঁ, এমনিতেই idempotent
Rating এর মান বসানো (“rating = 5”)হ্যাঁ, বারবার বসালেও একই ফল
টাকা কাটা, ride book করা, SMS পাঠানোশুধু idempotency key থাকলে
Driver এর আয়ে ৳285 “যোগ করা”Key ছাড়া কখনো না

এর সাথে আরো কয়েকটা নিয়ম মানতে হয়:

Retry একটা ওষুধ, মাত্রা মেনে খেতে হয়

ক্ষণিকের সমস্যায় retry দারুণ কাজ করে। কিন্তু ধীর বা overloaded service এর উপর বেপরোয়া retry আগুনে ঘি ঢালার মতো। Retry দিন শুধু এক জায়গায়, সংখ্যা ছোট রেখে, মাঝে অপেক্ষা করে। আর যে call দুইবার চললে ক্ষতি হয়, সেখানে idempotency key ছাড়া retry দেবেন না।

০৯ সব service এ একই নিয়ম কীভাবে চালু রাখবেন?

শুক্রবারের ঘটনার পর সাকিবের company ঠিক করলো, সব service এ timeout, retry আর idempotency এর নিয়ম মানতে হবে। কাজটা শুনতে সহজ, করতে কঠিন। Company তে এখন ৪০টা service। কোনোটা Go তে লেখা, কোনোটা Java তে, কোনোটা Node.js এ। প্রতিটা team নিজের ভাষার library দিয়ে timeout আর retry লেখে, নিজের মতো করে। কেউ ভুলে যায়, কেউ ভুল মান দেয়। তার উপর security team চায়, ভেতরের সব call encrypted হোক। প্রতিটা team কে আলাদা করে সেটাও করতে হবে।

Sidecar: প্রতিটা service এর পাশে একজন সহকারী

এর একটা সমাধান হলো network এর এই সব কাজ service এর code থেকে বের করে আনা। প্রতিটা service এর পাশে, একই machine এ, একটা ছোট আলাদা program চালানো হয়। Service এর সব আসা-যাওয়া call এই program এর ভেতর দিয়ে যায়। মোটরসাইকেলের পাশে জোড়া লাগানো ছোট বসার গাড়িটাকে ইংরেজিতে sidecar বলে। এই program টাও service এর পাশে জোড়া লাগানো থাকে, তাই এর নাম sidecar। Sidecar আসলে একটা proxy, মানে যে নিজে request টা নিয়ে আসল গন্তব্যে পৌঁছে দেয়।

Summary এখন শুধু নিজের business logic লেখে। Pricing কে ডাকলে call টা আগে যায় পাশের sidecar এ। Sidecar timeout দেয়, দরকার হলে retry করে, call টা encrypted করে, আর কত সময় লাগলো তার হিসাব রাখে।

Service mesh: সব sidecar মিলে একটা জাল

সব service এর পাশের sidecar গুলো মিলে একটা জালের মতো তৈরি হয়, যার ভেতর দিয়ে সব internal call যায়। এই পুরো ব্যবস্থাকে বলে service mesh। এর দুইটা অংশ:

Control Plane সব sidecar কে একই নিয়ম পাঠায় নিয়ম: timeout 300ms · retry ১ বার · mTLS POD 1 POD 2 mTLS Summary নিজের কাজ Sidecar proxy Sidecar proxy Pricing নিজের কাজ একই machine এ, পাশাপাশি চলে একই machine এ, পাশাপাশি চলে timeout, retry, encryption, metrics: সব sidecar সামলায় service এর code এ এগুলো আলাদা করে লিখতে হয় না
Summary সরাসরি Pricing কে ডাকে না। তার call প্রথমে যায় পাশের sidecar এ, সেখান থেকে Pricing এর sidecar এ, তারপর Pricing এ। দুই sidecar এর মাঝের পথ mTLS দিয়ে encrypted থাকে। কোন call এ কত timeout, কতবার retry, এই নিয়মগুলো control plane একসাথে সব sidecar এ পাঠিয়ে দেয়।

Diagram এ “pod” শব্দটা দেখছেন। Pod হলো Kubernetes এর সবচেয়ে ছোট চলমান একক, যেখানে এক বা একাধিক program একসাথে, একই machine এ চলে। Service আর তার sidecar একই pod এ থাকে। দুই sidecar এর মধ্যে call যায় mTLS (mutual TLS) দিয়ে। এতে দুই পক্ষই certificate দেখিয়ে নিজের পরিচয় প্রমাণ করে, আর মাঝের data encrypted থাকে।

প্রচলিত service mesh এর নাম Istio আর Linkerd। অনেক mesh এ sidecar হিসেবে Envoy নামের proxy ব্যবহার হয়। Mesh এর একটা বাড়তি লাভ হলো, সব call একই জায়গা দিয়ে যায় বলে কোন service কাকে কতবার ডাকছে আর কত সময় লাগছে, তার পুরো ছবি এক জায়গায় পাওয়া যায়।

Downside গুলো

  1. প্রতিটা call এ দুইটা বাড়তি hop: নিজের sidecar আর অন্য পাশের sidecar। প্রতি hop এ সাধারণত এক millisecond এরও কম লাগে, তবু শূন্য নয়।
  2. বাড়তি memory আর CPU: ৪০টা service এর প্রতিটা copy র পাশে একটা করে sidecar চলে।
  3. নতুন জটিলতা: Mesh নিজেই একটা বড় system। চালাতে, বুঝতে আর debug করতে আলাদা দক্ষতা লাগে।

তাই পাঁচ-ছয়টা service এর ছোট system এ mesh সাধারণত দরকার হয় না। একটা ভালো shared library দিয়েই কাজ চলে যায়। Service এর সংখ্যা আর ভাষা বাড়লে mesh এর লাভ cost কে ছাড়িয়ে যায়। কোন service কোন address এ আছে, সেটা খুঁজে বের করার কাজটাও (service discovery) প্রায়ই mesh করে। সেটা আমরা দেখবো সেশন ১৪ এ।

পরের শুক্রবার

এক সপ্তাহ পর, আবার শুক্রবার সন্ধ্যা। সাকিবের দল ততদিনে Summary এর call গুলো parallel করেছে। প্রতিটা call এ timeout দিয়েছে। Retry শুধু এক layer এ রেখেছে, আর payment এ idempotency key বসিয়েছে। সাতটার দিকে Pricing আবার দুই মিনিটের জন্য ধীর হলো। এবার কিছু rider শুধু দেখলো “ভাড়া একটু পরে দেখাচ্ছি”। Map এলো, driver এর ছবি এলো, ride book হলো। Support team এর group চুপচাপ থাকলো। সাকিব নিশ্চিন্তে একটা bike ডেকে বাড়ি ফিরলো।

সবচেয়ে গুরুত্বপূর্ণ শিক্ষা

Service গুলোর মধ্যে প্রতিটা call একটা network call, function call নয়। এটা ধীর হতে পারে, হারাতে পারে, অর্ধেক কাজ করে থামতে পারে। তাই প্রতিটা call এ চারটা প্রশ্নের উত্তর আগে থেকে ঠিক করে রাখুন: কতক্ষণ অপেক্ষা করবো (timeout), উত্তর না এলে কী দেখাবো (fallback), আবার চেষ্টা করা নিরাপদ কিনা (idempotency), আর কে retry করবে (এক layer)।

১০ মগজে প্রেশার দিন

আগে নিজে ভাবুন। তারপর উত্তর খুলে দেখুন: বেশিরভাগ মানুষ কীভাবে ভাবে, সেই চিন্তায় কী বাদ পড়ে, আর আসল কথাটা কোথায়।

প্রশ্ন ১: সব service একই data center এ, network call এ মাত্র ১ms লাগে। তাহলে service call কে সাধারণ function call এর মতো ধরে নিলে সমস্যা কী?
উত্তর দেখুন আগে নিজে ভাবুন

সবাই যেভাবে ভাবে

“১ms খুবই কম। দশটা call করলেও মাত্র ১০ms। তাই গতির দিক থেকে কোনো সমস্যা নেই, function call এর মতো ধরে নিলেই চলে।”

গড় গতির হিসাবটা ঠিক আছে, কিন্তু আসল বিপদটা গতিতে নয়।

যে জায়গাটা যোগ করা দরকার

Function call আর network call এর পার্থক্য গড় সময়ে নয়, ব্যর্থ হওয়ার ধরনে। Network call কখনো কখনো ৫ সেকেন্ড নেয় (tail latency)। কখনো একেবারেই উত্তর আসে না। আবার কখনো অন্য পাশে কাজ হয়ে যায়, কিন্তু উত্তরটা পথে হারায়। Function call এ এর কোনোটাই হয় না। তাই প্রতিটা remote call এ timeout, fallback আর নিরাপদ retry এর ব্যবস্থা লাগে।

সঠিক ভাষায়

Network call কে function call এর মতো দেখালেও তার ফল তিন রকম: সফল, ব্যর্থ, আর “জানি না”। ভালো design শুরু হয় এই তৃতীয় অবস্থাটা মেনে নিয়ে।

প্রশ্ন ২: Pricing service পুরোপুরি down, নাকি প্রতিটা call এ ৫ সেকেন্ড পর উত্তর দিচ্ছে: কোনটা বেশি বিপজ্জনক?
উত্তর দেখুন আগে নিজে ভাবুন

সবাই যেভাবে ভাবে

“Down হওয়াটাই বেশি খারাপ। ধীর হলেও অন্তত উত্তর তো আসছে, ভাড়াটা শেষ পর্যন্ত দেখানো যাচ্ছে।”

একটা request এর দিক থেকে কথাটা ঠিক। কিন্তু পুরো system এর দিক থেকে দেখলে ছবিটা উল্টে যায়।

যে জায়গাটা যোগ করা দরকার

Down service সাথে সাথে error দেয়। Caller এর thread কয়েক millisecond এ মুক্ত হয়, আর সে fallback দেখিয়ে পরের কাজে যায়। ধীর service caller এর thread আটকে রাখে। Little’s Law দিয়ে হিসাব করুন: প্রতি সেকেন্ডে ৪০০ request আর ৫ সেকেন্ড অপেক্ষা মানে ২০০০টা thread একসাথে আটকে থাকা। Caller এর thread ফুরিয়ে যায়, আর সমস্যা উপরে ছড়ায়: cascading failure। Timeout না থাকলে ধীর service আসলে down service এর চেয়েও খারাপ।

মূল pattern

দ্রুত fail করা একটা সুবিধা। Timeout দিয়ে একটা ধীর service কে caller এর চোখে “দ্রুত fail করা” service এ বদলে দিন।

প্রশ্ন ৩: "Reliability বাড়াতে প্রতিটা call fail হলে তিনবার retry করবো।" এই পরিকল্পনায় কী ঝুঁকি আছে?
উত্তর দেখুন আগে নিজে ভাবুন

সবাই যেভাবে ভাবে

“Retry করলে ক্ষণিকের network সমস্যা নিজে থেকেই ঠিক হয়ে যায়। তিনবার চেষ্টা করলে success এর সম্ভাবনা অনেক বাড়ে।”

ক্ষণিকের সমস্যার জন্য কথাটা ঠিক। কিন্তু তিনটা জিনিস বাদ পড়েছে।

যে জায়গাটা যোগ করা দরকার

প্রথমত, প্রতিটা layer তিনবার retry করলে retry গুণ হয়। তিন layer এ নিচের service ২৭ গুণ request পায়, ঠিক যখন সে এমনিতেই ধীর: retry storm। দ্বিতীয়ত, timeout হলে জানা যায় না কাজটা হয়েছে কিনা। টাকা কাটার মতো call এ idempotency key ছাড়া retry করলে দুইবার টাকা কাটা যায়। তৃতীয়ত, বিরতি ছাড়া সাথে সাথে retry করলে হাজারো client একই মুহূর্তে আবার request পাঠায়। তাই retry থাকবে এক layer এ, এক-দুইবার, backoff আর jitter সহ, আর শুধু idempotent call এ।

মাথায় গেঁথে নিন

Retry এর আগে দুইটা প্রশ্ন করুন: আর কে কে এই call টা retry করছে? আর এই call দুইবার চললে কি কোনো ক্ষতি হবে?

১১ যে টার্মগুলো শিখলাম

টার্মমানে
RPC (Remote Procedure Call)অন্য machine এর function কে নিজের function এর মতো করে ডাকা। পেছনে আসলে network request যায়।
Sync callCaller উত্তর না আসা পর্যন্ত অপেক্ষা করে।
Async messageCaller message রেখে নিজের কাজে ফেরে, অন্য service পরে কাজটা করে।
RESTHTTP method আর URL দিয়ে request, সাধারণত JSON এ উত্তর।
gRPCGoogle এর তৈরি RPC framework। .proto file থেকে client আর server এর code বানায়, HTTP/2 তে চলে।
ProtobufgRPC এর binary data format। Field এর নামের বদলে ছোট নম্বর পাঠায়, তাই payload ছোট হয়।
HTTP/2HTTP এর নতুন version, যেটা একটা connection দিয়ে একসাথে অনেক request চালাতে পারে।
API contractদুই service এর চুক্তি: কোন endpoint, কোন field, কোন type, কোন মানে।
Breaking changeএমন বদল, যেটা পুরোনো client কে ভেঙে দেয়। যেমন field এর নাম, type বা মানে বদলানো।
API versioningপুরোনো contract চালু রেখে নতুন contract আলাদা version এ (/v1, /v2) পাশাপাশি চালানো।
Latency chainSeries এ সাজানো call গুলোর সময় যোগ হয়ে মোট সময় লম্বা হওয়া।
Fan-outএকটা request থেকে অনেকগুলো call একসাথে parallel এ পাঠানো।
p99 / Tail latency১০০টা request এর মধ্যে সবচেয়ে ধীর ১% এর সময়। Fan-out এ এটাই user এর অভিজ্ঞতা ঠিক করে।
Partial failureSystem এর কিছু অংশ ঠিক, আর কিছু অংশ ধীর বা down থাকা।
Cascading failureএকটা service এর সমস্যা তার caller দের আটকে দেয়, তারা আবার তাদের caller দের, এভাবে পুরো system অচল হয়ে যায়।
Little’s Lawএকসাথে কতগুলো কাজ চলছে, তার হিসাব: প্রতি সেকেন্ডে আসা কাজের সংখ্যাকে প্রতিটা কাজের সময় দিয়ে গুণ করলেই পাওয়া যায়।
Fallback / Graceful degradationউত্তর না এলে বিকল্প কিছু দেখানো, যাতে পুরো screen না ভেঙে কম তথ্য নিয়ে চলে।
Timeout budget / Deadlineপুরো request এর জন্য মোট সময়। ভেতরের প্রতিটা call কে এর ভেতরে থাকতে হয়।
Retry stormঅনেক layer এর retry গুণ হয়ে ধীর service এর উপর বহুগুণ চাপ তৈরি করা।
Sidecarপ্রতিটা service এর পাশে চলা ছোট proxy, যে network এর কাজগুলো (timeout, retry, encryption) সামলায়।
Service meshসব sidecar (data plane) আর তাদের নিয়ম ঠিক করে দেওয়া control plane মিলে পুরো ব্যবস্থা।
mTLSদুই পক্ষই certificate দিয়ে পরিচয় প্রমাণ করে, আর মাঝের data encrypted থাকে।
Istio / Linkerd / Envoyপ্রচলিত service mesh (Istio, Linkerd) আর sidecar proxy (Envoy)।

১২ শর্ট সামারি

  1. Service থেকে service এর call দেখতে function call এর মতো হলেও আসলে network call। উত্তর এখনই দরকার হলে sync call (REST বা gRPC) দেওয়া হয়, শুধু জানালেই চললে async message। ভেতরের call এ gRPC আর Protobuf ছোট আর দ্রুত, আর দুই service এর মধ্যে API contract ভাঙতে না চাইলে নতুন version পাশাপাশি চালাতে হয়।
  2. Series এ সাজানো call এর সময় যোগ হয়, তাই নির্ভরতা না থাকলে call গুলো parallel এ fan-out করা ভালো। তখন সবচেয়ে ধীর call টাই মোট সময় ঠিক করে, আর fan-out যত চওড়া, tail latency তত বেশি ভোগায়।
  3. একটা ধীর service caller এর thread আটকে দেয়, আর সেই ধীরতা উপরে ছড়িয়ে cascading failure ঘটায়। এর প্রথম প্রতিরক্ষা হলো প্রতিটা call এ timeout আর fallback।
  4. Retry শুধু এক layer এ, অল্প সংখ্যায়, backoff সহ, আর শুধু idempotent call এ নিরাপদ। অনেক service আর অনেক ভাষার system এ এই নিয়মগুলো একসাথে চালু রাখতে service mesh আর sidecar ব্যবহার হয়।

১৩ মনে রাখুন