০০ যে প্রশ্নগুলোর উত্তর খুঁজবো
সেশন ০৫ এ আমরা transaction দেখেছিলাম। কয়েকটা কাজকে একসাথে বেঁধে ফেলা হয়, যাতে হয় সবগুলো হয়, নয়তো একটাও না। কিন্তু সেখানে সব কাজ ছিলো একটাই database এর ভেতরে।
বড় system এ একটা কাজ প্রায়ই কয়েকটা service আর কয়েকটা database ছুঁয়ে যায়। তখন “হয় সব, নয়তো কিছুই না” নিয়মটা কে রক্ষা করবে? এই সেশনে আমরা একটা ছোট travel booking এর গল্প ধরে এই প্রশ্নের উত্তর খুঁজবো। শেষে আপনি বুঝবেন, বাস্তব system অর্ধেক হওয়া কাজ কীভাবে নিরাপদে ফিরিয়ে আনে।
- ১ Flight, payment আর hotel তিনটা আলাদা service এ। একটা local transaction কেন যথেষ্ট নয়? (distributed transaction)
- ২ সবাইকে আগে "পারবেন তো?" জিজ্ঞেস করে তারপর commit করলে কেমন হয়? (2PC, coordinator)
- ৩ 2PC এর blocking problem টা কী, আর বাস্তবে 2PC কেন কম চলে?
- ৪ Saga কী? (local transaction এর chain)
- ৫ মাঝপথে একটা step fail করলে আগের কাজগুলো কীভাবে ফেরানো হয়? (compensating transaction)
- ৬ Saga র step গুলো কে চালাবে? (choreography বনাম orchestrator)
- ৭ Database এ লেখা আর event পাঠানো, দুটো একসাথে নিশ্চিত করবো কীভাবে? (outbox pattern)
- ৮ এই পুরো সময় user screen এ কী দেখবে? (pending, confirmed, refunded)
০১ একটা local transaction কেন যথেষ্ট নয়?
ডিসেম্বরের এক বৃহস্পতিবার, রাত সাড়ে এগারোটা। শিরিন অনেক দিন ধরে বাচ্চাদের কথা দিয়ে রেখেছেন, এবার শীতের ছুটিতে সমুদ্র দেখাবেন। “চলো ঘুরি” site এ তিনি চারজনের জন্য ঢাকা থেকে কক্সবাজারের flight ticket বাছলেন, সাথে তিন রাতের জন্য একটা sea-view room। Card দিয়ে ৳৪২,০০০ pay করলেন। কয়েক সেকেন্ড পর ব্যাংকের SMS এলো: টাকা কেটেছে। ঠিক তখনই screen এ লাল অক্ষরে লেখা উঠলো, “Hotel booking failed”।
রাতেই support এ ticket এলো: “টাকা কেটে নিলেন, অথচ hotel নেই। এখন বাচ্চাদের কী বলবো?” Ticket টা গিয়ে পড়লো backend engineer রাইসার কাছে। রাইসা log খুলে দেখলো, কোনো server crash করেনি। প্রতিটা service ঠিক সেটাই করেছে, যা তাকে করতে বলা হয়েছিলো। তবু একটা পরিবারের ৳৪২,০০০ আটকে আছে।
তিনটা service, তিনটা database
“চলো ঘুরি” একটা বড় application নয়, কয়েকটা ছোট service এর সমষ্টি। প্রতিটা service এর নিজের database আছে, আর অন্য কেউ সেই database এ সরাসরি হাত দেয় না। এই নিয়মকে বলে database per service (সেশন ১১)।
প্রতিটা service নিজের database এ খুব ভালো transaction চালাতে পারে। Flight service চারটা seat একসাথে hold করে, হয় চারটাই, নয়তো একটাও না। কিন্তু এই transaction এর সীমানা ঐ একটা database পর্যন্তই। Flight এর database জানে না payment এর database এ কী হচ্ছে। তিনটাকে একসাথে বাঁধার মতো কোনো BEGIN ... COMMIT নেই।
সেই রাতে আসলে কী হয়েছিলো
রাইসা log থেকে ঘটনাটা মিনিট-সেকেন্ড ধরে সাজালো:
Booking service আগে flight service কে ডাকলো, seat hold হলো। তারপর payment service কে ডাকলো, টাকা কাটা হলো। দুটোই নিজের database এ commit হয়ে গেলো। এরপর hotel service কে ডাকতেই উত্তর এলো, শেষ sea-view room টা দুই সেকেন্ড আগে অন্য কেউ নিয়ে নিয়েছে।
Hotel এর fail করা স্বাভাবিক ঘটনা। আসল সমস্যা হলো, আগের দুটো commit করা কাজ নিজে থেকে ফিরে আসে না। কাজটা তিন ভাগের দুই ভাগ হয়ে থেমে গেছে।
একাধিক service বা database জুড়ে ছড়ানো একটা কাজকে বলে distributed transaction। আমরা চাই এই কাজটাও “হয় সব, নয়তো কিছুই না” নিয়ম মানুক। কিন্তু একটা database এর transaction অন্য database পর্যন্ত পৌঁছায় না। তাই এই নিয়ম রক্ষা করার দায়িত্ব এখন আমাদের নিজেদের design এর।
এই সমস্যার দুটো বিখ্যাত সমাধান আছে। একটা হলো সবাইকে আগে থেকে রাজি করিয়ে একসাথে commit করা (2PC)। অন্যটা হলো একটা একটা করে commit করা, আর দরকার হলে উল্টো কাজ করে ফেরানো (saga)। দুটোই দেখবো।
০২ সবাইকে আগে “পারবেন তো?” জিজ্ঞেস করে তারপর commit করলে কেমন হয়?
বন্ধুদের picnic এর analogy
ধরুন, আপনি পাঁচ বন্ধুর picnic এর আয়োজক। বাস ভাড়া করার আগে আপনি সবাইকে ফোন করে জিজ্ঞেস করেন, শুক্রবার আসা যাবে কিনা। সাথে বলে দেন, যে আসতে পারবে, সে যেন দিনটা আটকে রাখে আর অন্য কোনো plan না করে। প্রত্যেকে হয় বলে “হ্যাঁ, রাখলাম”, নয়তো বলে “না, পারবো না”।
পাঁচজনই হ্যাঁ বললে আপনি বাস ভাড়া করেন, আর সবাইকে জানান: “Final, শুক্রবার যাচ্ছি।” একজনও না বললে সবাইকে জানান: “বাতিল, শুক্রবার এখন খালি।” খেয়াল করুন, হ্যাঁ বলার পর কেউ আর ঐ শুক্রবারে অন্য কিছু ঠিক করতে পারে না। সে আপনার শেষ কথার অপেক্ষায় থাকে।
Two-phase commit (2PC)
Database এর জগতে এই কৌশলের নাম two-phase commit, সংক্ষেপে 2PC। এখানে picnic এর আয়োজকের ভূমিকা নেয় একটা coordinator। Coordinator হলো সেই component, যে পুরো distributed transaction টা চালায় আর শেষ সিদ্ধান্ত নেয়। আমাদের গল্পে booking service ই coordinator। Flight, payment আর hotel হলো participant।
- Phase 1 (prepare): Coordinator প্রতিটা participant কে “prepare” বার্তা পাঠায়। প্রত্যেকে নিজের কাজটা করে রাখে, কিন্তু commit করে না। কাজটা disk এ লিখে রাখে, যাতে crash এর পরও মনে থাকে। দরকারি row গুলোতে lock ধরে রাখে। Lock মানে, এই row টা এখন অন্য কোনো transaction বদলাতে পারবে না। তারপর coordinator কে vote দেয়: YES বা NO।
- Phase 2 (commit বা abort): সবাই YES দিলে coordinator নিজের log এ “COMMIT” লিখে রাখে, তারপর সবাইকে commit করতে বলে। একজনও NO দিলে, বা সময়মতো উত্তর না দিলে, coordinator সবাইকে abort করতে বলে। দুই ক্ষেত্রেই participant রা কাজ শেষ করে lock ছেড়ে দেয়।
2PC থাকলে শিরিনের সেই রাতটা অন্যরকম হতো। Prepare এর সময়েই hotel NO vote দিতো, কারণ room নেই। Coordinator তখন সবাইকে abort করতে বলতো। Payment কখনো commit হতো না, তাই শিরিনের টাকাও কাটতো না।
YES vote মানে একটা প্রতিশ্রুতি: “Coordinator যা-ই বলুক, আমি সেটা করতে পারবো।” সবার প্রতিশ্রুতি হাতে না পাওয়া পর্যন্ত কেউ commit করে না। তাই হয় সবাই commit করে, নয়তো কেউই করে না।
বাস্তবে অনেক database 2PC সমর্থন করে। যেমন PostgreSQL এ PREPARE TRANSACTION আর COMMIT PREPARED command আছে। একাধিক database কে একসাথে 2PC তে চালানোর একটা পুরোনো standard ও আছে, নাম XA। তাহলে সব service এ 2PC চালিয়ে দিলেই তো হয়? রাইসাও প্রথমে তাই ভেবেছিলো।
০৩ 2PC এর blocking problem টা কী, আর বাস্তবে 2PC কেন কম চলে?
আয়োজকের ফোন বন্ধ হয়ে গেলে
Picnic এর গল্পে ফিরে যান। পাঁচজনই হ্যাঁ বলেছে, সবাই শুক্রবার আটকে রেখেছে। ঠিক তখন আয়োজকের ফোনের charge শেষ হয়ে গেলো। বন্ধুরা এখন কী করবে? Picnic কি final হয়েছে, নাকি বাতিল? কেউ জানে না। কেউ নিজে থেকে “বাতিল” ধরে নিলো, অথচ আয়োজক বাস ভাড়া করে ফেলেছে। তখন বাসে একজন কম যাবে। তাই সবাই শুক্রবার আটকে রেখে অপেক্ষা করে।
2PC তে ঠিক এটাই হয়:
YES vote দেওয়ার পর একজন participant আর একা সিদ্ধান্ত নিতে পারে না। কারণ coordinator হয়তো এর মধ্যে অন্য কাউকে commit করতে বলে দিয়েছে। আবার হয়তো বলেনি। Participant এর জানার কোনো উপায় নেই। তাই সে lock ধরে বসে থাকে, যতক্ষণ না coordinator ফেরে। এটাকেই বলে 2PC এর blocking problem।
সংখ্যায় দেখুন। ডিসেম্বরের ছুটির রাতে “চলো ঘুরি” তে প্রতি মিনিটে প্রায় ৩০০টা booking আসে। Coordinator crash করে আবার চালু হতে ৩ মিনিট লাগলে প্রায় ৯০০টা booking এর সময় নষ্ট হয়। আগে থেকে চলা booking গুলো lock ধরে আটকে থাকে। একই flight বা একই hotel এর নতুন booking গুলো সেই lock এর পেছনে দাঁড়িয়ে timeout পায়।
বাস্তবে 2PC আরো তিন জায়গায় আটকে যায়
- সবাইকে “prepare” বুঝতে হয়: শিরিনের card এর টাকা কাটে একটা payment gateway। Hotel এর room আসে partner hotel এর API থেকে। এরা অন্য company র system। এরা আপনার coordinator এর “prepare” বার্তা চেনে না, আর আপনার জন্য lock ধরে বসেও থাকবে না। ৩টার মধ্যে একটা participant ও 2PC না বুঝলে পুরো পদ্ধতিটাই চলে না।
- Lock অনেকক্ষণ ধরা থাকে: Phase 1 আর phase 2 মিলিয়ে কমপক্ষে দুইবার আসা-যাওয়া লাগে। সবচেয়ে ধীর participant এর উত্তর ৮০০ms এ এলে সবার lock অন্তত ১.৬ সেকেন্ড ধরা থাকে। ভিড়ের রাতে এই সময়টা অনেক বড়।
- একজন down হলে সবাই থামে: তিনজনের যেকোনো একজন unavailable হলে কোনো booking ই হয় না। Participant যত বাড়ে, পুরো system এর availability তত কমে।
2PC ভুল কিছু নয়। একই company র ভেতরে, একই রকম কয়েকটা database এর মধ্যে, যেখানে network দ্রুত আর নির্ভরযোগ্য, সেখানে 2PC ভালো কাজ করে। Google Spanner এর মতো system coordinator কে নিজেই একটা Raft বা Paxos group (সেশন ১৬) বানিয়ে চালায়, যাতে coordinator এর crash এ সব আটকে না যায়। কিন্তু অন্য company র API, ধীর network আর অনেক service এর জগতে 2PC খুব কমই চলে।
রাইসার system এ payment gateway আর hotel API দুটোই বাইরের। তাই 2PC তার জন্য পথ নয়। তাকে এমন কিছু খুঁজতে হবে, যেখানে কেউ কারো জন্য lock ধরে বসে থাকে না।
০৪ Saga কী?
2PC এর ভাবনা ছিলো আগে সবাইকে রাজি করানো, তারপর একসাথে commit করা। Saga উল্টো পথে হাঁটে। প্রতিটা step নিজের local transaction এ এখনই commit করে, তারপর পরের step কে শুরু করতে বলে। কোনো step fail করলে আগের commit হওয়া step গুলোকে একটা করে উল্টো কাজ দিয়ে ফেরানো হয়।
অর্থাৎ saga হলো local transaction এর একটা chain। ১৯৮৭ সালে Hector Garcia-Molina আর Kenneth Salem একটা গবেষণাপত্রে এই নাম দেন। তাঁরা লম্বা সময় ধরে চলা transaction নিয়ে কাজ করছিলেন, যেখানে এতক্ষণ lock ধরে রাখা সম্ভব নয়।
খেয়াল করুন, প্রতিটা lock এখন মাত্র কয়েক millisecond এর। Flight service seat hold করে commit করে, সাথে সাথে lock ছেড়ে দেয়। Payment service কী করছে, সেটা নিয়ে flight service এর আর কোনো মাথাব্যথা নেই। কেউ crash করলেও অন্যরা lock ধরে আটকে থাকে না।
বিনিময়ে একটা জিনিস ছাড়তে হয়। Saga চলার মাঝখানে বাইরের লোক অর্ধেক কাজ দেখতে পায়। যেমন seat hold হয়ে গেছে, টাকা কাটা হয়েছে, কিন্তু hotel এখনো হয়নি। একটা database transaction এ এই মাঝের অবস্থা কেউ দেখতে পায় না। এই বৈশিষ্ট্যের নাম isolation, আর saga তে সেটা থাকে না। এর ফল কী, আর কীভাবে সামলাতে হয়, সেটা সেশনের শেষ অংশে (প্রশ্ন ৮) দেখবো।
| 2PC | Saga | |
|---|---|---|
| Commit কখন হয় | সবাই YES দিলে, একসাথে | প্রতিটা step নিজের মতো, সাথে সাথে |
| Lock কতক্ষণ | পুরো transaction জুড়ে, coordinator crash এ অনির্দিষ্টকাল | প্রতিটা local transaction এর কয়েক millisecond |
| Fail হলে | Database নিজে rollback করে | আমাদের লেখা উল্টো কাজ চালাতে হয় |
| মাঝের অবস্থা | বাইরে থেকে দেখা যায় না | দেখা যায়, তাই সামলাতে হয় |
| বাইরের API | Prepare না বুঝলে চলে না | যেকোনো API এর সাথে চলে |
০৫ মাঝপথে একটা step fail করলে আগের কাজগুলো কীভাবে ফেরানো হয়?
Saga র প্রতিটা step এর জন্য আগে থেকেই একটা জোড়া কাজ লিখে রাখা হয়। এই উল্টো কাজকে বলে compensating transaction, বা সংক্ষেপে compensation। Flight এর seat hold এর compensation হলো seat release। Payment এর টাকা কাটার compensation হলো refund।
শিরিনের booking এ saga ঠিক এভাবে চলতো:
তিনটা নিয়ম এখানে খেয়াল করার মতো।
- উল্টো ক্রমে চলে: যে step সবার শেষে commit হয়েছে, তার compensation সবার আগে চলে। তাই আগে refund, তারপর seat release। একটার উপর একটা রাখা বইয়ের স্তূপের মতো: সবার উপরের বইটা আগে নামে।
- শুধু commit হওয়া step এরই compensation লাগে: Hotel এর step commit হয়নি, তাই hotel এর জন্য কিছুই ফেরাতে হয় না।
- Compensation কোনো rollback নয়: Rollback এ database এমনভাবে কাজ মুছে দেয়, যেন কিছুই হয়নি। Compensation হলো একটা নতুন কাজ, যেটা আগের কাজের প্রভাব কাটায়। শিরিনের card statement এ টাকা কাটার লাইনটা থেকেই যায়, তার নিচে refund এর একটা নতুন লাইন যোগ হয়। আর “টাকা কেটেছে” SMS টা তো আর ফেরত আনা যায় না। তাই compensation এর অংশ হিসেবে আরেকটা SMS যায়: “দুঃখিত, booking হয়নি, টাকা ফেরত যাচ্ছে।”
Compensation কে idempotent হতেই হবে
এবার একটা কঠিন case ভাবুন। Refund worker gateway কে ৳৪২,০০০ ফেরত দেওয়ার request পাঠালো। Gateway টাকা ফেরত দিলো। কিন্তু worker নিজের database এ “refund হয়েছে” লেখার আগেই crash করলো। Queue এর নিয়মে (সেশন ০৮) job টা আবার ফিরে আসবে, আর আরেকজন worker refund চালাবে। Refund idempotent না হলে শিরিন ৳৮৪,০০০ ফেরত পাবেন। শিরিন খুশি হবেন, কিন্তু company র ক্ষতি ৳৪২,০০০।
সেশন ০৮ এ শিখেছিলাম, একটা কাজ একাধিকবার করলেও একবার করার মতো ফল হলে তাকে বলে idempotent। Compensation এর বেলায় এটা ঐচ্ছিক নয়, বাধ্যতামূলক। কারণ compensation retry হবেই।
function refundPayment(bookingId):
key = bookingId + ":refund" # "B-2071:refund"
if refunds.exists(key):
return "already done" # দ্বিতীয়বার এলে কিছুই করে না
# একই key gateway কেও পাঠানো হয়, তাই gateway ও দুইবার টাকা ফেরত দেয় না
gateway.refund(paymentId, 42000, idempotencyKey = key)
refunds.insert(key, status = "DONE")
return "done"
এখানে booking ID থেকে বানানো B-2071:refund হলো idempotency key। Worker নিজে check করে, আবার gateway কেও একই key পাঠায়। ফলে crash যেখানেই হোক, টাকা একবারই ফেরত যায়।
আরেকটা কথা। সাধারণ step fail করতে পারে, কিন্তু compensation কে শেষ পর্যন্ত সফল হতেই হবে। Hotel না পাওয়া মেনে নেওয়া যায়, কিন্তু refund না হওয়া মেনে নেওয়া যায় না। তাই compensation fail করলে বারবার retry হয়। কয়েকবার চেষ্টার পরও না হলে job টা DLQ (সেশন ০৮) তে যায়, আর একজন মানুষ হাতে ঠিক করেন।
যে step ফেরানো সবচেয়ে কঠিন বা সবচেয়ে কষ্টের, সেটা saga র একদম শেষে রাখুন। রাইসার team পরে ক্রমটা বদলালো। এখন শুরুতে card এ শুধু ৳৪২,০০০ আটকে রাখা হয় (authorization hold), টাকা কাটা হয় না। তারপর flight, তারপর hotel। সব হয়ে গেলে একদম শেষে আটকে রাখা টাকাটা আসলে কাটা হয়। কোনো step fail করলে hold টা ছেড়ে দিলেই হয়, ৫-৭ দিনের refund এর ঝামেলা থাকে না।
০৬ Saga র step গুলো কে চালাবে?
Saga র প্রতিটা step কে কেউ না কেউ শুরু করতে হয়, আর fail হলে compensation চালাতে হয়। এই কাজটা দুইভাবে ভাগ করা যায়।
- Choreography: কোনো পরিচালক নেই। প্রতিটা service আগের service এর event শুনে নিজের কাজ করে, তারপর নিজের event পাঠায়। সেশন ১০ এ এই পদ্ধতি দেখেছেন। Flight service
FlightHeldevent পাঠায়, payment service সেটা শুনে টাকা কাটে আরPaymentDoneপাঠায়। Hotel fail করলেHotelFailedপাঠায়, payment সেটা শুনে refund করে, আর flight serviceRefundedশুনে seat ছেড়ে দেয়। - Orchestration: একটা আলাদা component পুরো saga চালায়। একে বলে orchestrator। Orchestrator জানে কোন step এর পর কোন step। সে প্রতিটা service কে command পাঠায়, উত্তরের অপেক্ষা করে, আর প্রতিটা ধাপ নিজের database এ লিখে রাখে। কোনো step fail করলে সে নিজেই compensation গুলো উল্টো ক্রমে চালায়।
| Choreography | Orchestration | |
|---|---|---|
| সুবিধা | কোনো কেন্দ্রীয় component নেই; ২-৩ step এর ছোট flow তে সহজ | পুরো flow এক জায়গায় পড়া যায়; booking কোথায় আটকে আছে তা এক query তে জানা যায় |
| Downside | Step বাড়লে কে কার event শোনে, তা বোঝা কঠিন; fail এর পথ খুঁজে বের করা কষ্টের | Orchestrator নিজে একটা বাড়তি service, তাকেও চালাতে আর সামলাতে হয় |
| কখন বেছে নেবেন | কয়েকটা step, সহজ নিয়ম | অনেক step, টাকার লেনদেন, জটিল compensation |
রাইসার team orchestration বেছে নিলো। কারণটা খুব সাধারণ। Support এর মানুষ রাতে জানতে চান, “Booking B-2071 এখন কোন ধাপে?” Orchestrator এর database এ প্রতিটা saga র অবস্থা লেখা থাকে, তাই এক query তেই উত্তর মেলে।
একটা ভয় মনে আসতে পারে। Orchestrator ও তো crash করতে পারে, তাহলে কি 2PC এর মতোই সব আটকে যাবে? না। Orchestrator কারো lock ধরে রাখে না। প্রতিটা service নিজের কাজ commit করে ফেলেছে। Orchestrator restart হয়ে নিজের database থেকে দেখে নেয় কোন saga কোন ধাপে ছিলো, তারপর সেখান থেকে আবার চালায়। বাস্তবে Temporal, AWS Step Functions আর Camunda এর মতো tool এই কাজটাই করে।
০৭ Database এ লেখা আর event পাঠানো, দুটো একসাথে নিশ্চিত করবো কীভাবে?
নতুন design চালুর দুই সপ্তাহ পর রাইসা আরেকটা অদ্ভুত booking পেলো। টাকা কাটা হয়েছে, কিন্তু saga hotel এর ধাপে কখনো পৌঁছায়নি। Booking টা চুপচাপ PENDING হয়ে পড়ে আছে।
খুঁজে দেখা গেলো, payment service এর কাজ দুই ধাপের। প্রথমে নিজের database এ payment row commit করে। তারপর message broker এ PaymentDone event পাঠায়। (Message broker মানে Kafka বা RabbitMQ এর মতো software, যেটা event এক service থেকে অন্য service এ পৌঁছে দেয়।) সেদিন ঠিক এই দুই ধাপের মাঝখানে একটা deploy হয়েছিলো, আর process টা বন্ধ হয়ে গিয়েছিলো।
দুটো আলাদা system এ আলাদা করে লেখাকে বলে dual write। Database আর broker এর মধ্যে কোনো যৌথ transaction নেই। তাই যে ক্রমেই লিখুন, মাঝখানে crash হলে একটা হবে, অন্যটা হবে না। আগে database এ লিখলে event হারায়। আগে event পাঠালে এমন event চলে যায়, যার পেছনে আসলে কোনো কাজ হয়নি।
সমাধান: outbox pattern
বুদ্ধিটা সহজ। Event টা broker এ না পাঠিয়ে আগে নিজের database এই লিখে রাখুন। Payment service এর database এ outbox নামে একটা table থাকে। Payment row আর outbox row একই local transaction এ লেখা হয়:
BEGIN;
UPDATE payments
SET status = 'PAID'
WHERE booking_id = 'B-2071';
INSERT INTO outbox (event_id, event_type, payload, sent)
VALUES ('evt-9f3a', 'PaymentDone',
'{"booking": "B-2071", "amount": 42000}', false);
COMMIT; -- দুটো লেখা একসাথে টেকে, নয়তো একসাথে বাতিল হয়
একটাই database, একটাই transaction। তাই commit হলে দুটো row ই থাকে, rollback হলে একটাও থাকে না। “টাকা কাটা হলো অথচ event নেই” অবস্থা আর তৈরি হতে পারে না।
এরপর একটা আলাদা process, যাকে বলে outbox relay, outbox table থেকে না-পাঠানো event গুলো পড়ে broker এ publish করে। তারপর row টাকে “sent” চিহ্ন দেয়।
Relay দুইভাবে outbox পড়তে পারে:
- Polling: প্রতি ৫০০ms এ একবার query চালায়: “কোন row এখনো sent হয়নি?” সহজ, কিন্তু database এ বাড়তি চাপ পড়ে।
- CDC (change data capture): Database নিজের প্রতিটা পরিবর্তন একটা log এ লেখে। CDC tool সেই log পড়ে নতুন row এর খবর সাথে সাথে পায়, আলাদা query চালাতে হয় না। Debezium এমন একটা জনপ্রিয় tool।
একটা জিনিস মাথায় রাখুন। Relay event publish করলো, কিন্তু “sent” চিহ্ন দেওয়ার আগেই crash করলো। Restart এর পর সে একই event আবার পাঠাবে। অর্থাৎ outbox এর delivery ও at-least-once (সেশন ০৮)। তাই hotel service কে idempotent consumer হতে হবে, যাতে একই PaymentDone দুইবার এলেও দুইটা room book না হয়। Consumer এর দিকে এই কাজ গুছিয়ে করার কৌশল (inbox pattern) সেশন ১৮ তে দেখবো।
দুটো system এ একসাথে লেখার চেষ্টা করবেন না। একটাই database এ, একটাই transaction এ দুটো লিখুন। বাইরের দুনিয়ায় পাঠানোর কাজটা পরে আলাদা একজন করবে, আর দরকার হলে বারবার চেষ্টা করবে।
০৮ এই পুরো সময় user screen এ কী দেখবে?
2PC তে মাঝের অবস্থা কেউ দেখতো না। Saga তে দেখে, কারণ প্রতিটা step আলাদা সময়ে commit হয়। এক step থেকে আরেক step এ যেতে কয়েক শ millisecond লাগে। Refund এ লাগে আরো বেশি। তাই user কে মিথ্যা না বলে, প্রতিটা অবস্থাকে একটা সৎ নাম দিতে হয়।
- PENDING: Saga চলছে। Screen এ লেখা থাকে, “আপনার booking হচ্ছে, একটু অপেক্ষা করুন।” এই অবস্থায় ticket বা hotel voucher দেখানো হয় না।
- CONFIRMED: তিনটা step ই সফল। এখন, এবং শুধু এখনই, SMS যায় আর ticket download করা যায়।
- REFUNDING: কোনো step fail করেছে, compensation চলছে। User জানে কী হচ্ছে, তাকে অন্ধকারে রাখা হয় না।
- REFUNDED: সব compensation শেষ। ব্যাংক থেকে টাকা পৌঁছাতে সাধারণত ৫-৭ কর্মদিবস লাগে, আর screen এ সেটা পরিষ্কার লেখা থাকে।
সবচেয়ে বড় ভুল হলো saga শেষ হওয়ার আগেই “Booking confirmed!” দেখিয়ে ফেলা। তাহলে compensation চললে user এর কাছে মনে হবে, system নিশ্চিত করা booking কেড়ে নিয়েছে। “এখনো হচ্ছে” বলা অনেক বেশি সম্মানের।
নতুন design এ শিরিনের রাত
নতুন design চালুর পর একই রকম একটা রাতে শিরিন আবার কক্সবাজারের booking দিলেন। এবারও শেষ room টা অন্য কেউ নিয়ে নিলো। কিন্তু এবার card এ টাকা শুধু আটকে ছিলো, কাটা হয়নি। দুই সেকেন্ডের মধ্যে orchestrator flight এর seat ছেড়ে দিলো, card এর hold তুলে নিলো, আর SMS পাঠালো: “দুঃখিত, এই hotel এ room নেই। আপনার কোনো টাকা কাটা হয়নি। কাছাকাছি আরো তিনটা hotel দেখবেন?” শিরিন পাশের একটা hotel বেছে নিলেন। Support এ এবার কোনো ticket আসেনি।
০৯ মগজে প্রেশার দিন
আগে নিজে ভাবুন। তারপর উত্তর খুলে দেখুন: বেশিরভাগ মানুষ কীভাবে ভাবে, সেই চিন্তায় কী বাদ পড়ে, আর আসল কথাটা কোথায়।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“2PC একটু slow, কারণ দুইবার আসা-যাওয়া লাগে। Speed এর জন্য মানুষ saga ব্যবহার করে।”
Speed একটা কারণ ঠিকই, কিন্তু সবচেয়ে বড় কারণ নয়।
যে জায়গাটা যোগ করা দরকার
আসল সমস্যা দুটো। প্রথমত, blocking: YES দেওয়ার পর coordinator crash করলে participant রা lock ধরে অনির্দিষ্টকাল অপেক্ষা করে, আর পেছনের সব request আটকে যায়। দ্বিতীয়ত, বাইরের system: payment gateway বা partner hotel এর API “prepare” বোঝে না, তাই তাদের 2PC তে আনাই যায় না। Saga প্রতিটা step এ সাথে সাথে commit করে, কারো জন্য lock ধরে না, আর যেকোনো API এর সাথে চলে। বিনিময়ে compensation লিখতে হয়, আর মাঝের অবস্থা সামলাতে হয়।
একই company র কয়েকটা database এর মধ্যে, দ্রুত network এ 2PC চলতে পারে। অনেক service, বাইরের API আর লম্বা সময়ের কাজে saga বেছে নিন।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“হ্যাঁ। কোনো step fail করলে আগের step গুলো undo করা হয়। এটাই rollback।”
কাজের দিক থেকে কাছাকাছি, কিন্তু তিনটা গুরুত্বপূর্ণ পার্থক্য বাদ পড়েছে।
যে জায়গাটা যোগ করা দরকার
Rollback এ database কাজটা এমনভাবে মুছে দেয়, যেন কখনো হয়নি। Compensation হলো একটা নতুন, আলাদা কাজ। আগের কাজটা ঘটে গেছে, আর বাইরের দুনিয়া সেটা দেখেও ফেলেছে: card statement এ লাইন আছে, SMS চলে গেছে। তাই compensation আগের প্রভাব কাটায়, ইতিহাস মোছে না।
দ্বিতীয় পার্থক্য: compensation retry হবেই, তাই সেটাকে idempotent হতে হয়। তৃতীয় পার্থক্য: compensation কে শেষ পর্যন্ত সফল হতেই হয়। Retry, তারপর DLQ, তারপর মানুষের হাত, কিন্তু refund বাদ দেওয়া চলে না।
Rollback হলো খাতার পাতা ছিঁড়ে ফেলা। Compensation হলো খাতায় নতুন লাইনে উল্টো হিসাব লেখা।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“Publish এর চারপাশে একটা retry loop দিলেই হলো। Broker কিছুক্ষণ down থাকলেও retry তে event চলে যাবে।”
Broker down থাকার case টা এতে সামলানো যায়। কিন্তু সবচেয়ে বিপজ্জনক case টা বাদ পড়েছে।
যে জায়গাটা যোগ করা দরকার
Retry loop টা চলে process এর memory তে। DB commit আর publish এর মাঝখানে process টা নিজেই crash করলে, বা deploy এর সময় বন্ধ হলে, retry করার মতো কেউ আর থাকে না। Event টা কোথাও লেখা ছিলো না, তাই চিরদিনের জন্য হারিয়ে যায়। রাইসার PENDING হয়ে পড়ে থাকা booking টা ঠিক এভাবেই তৈরি হয়েছিলো।
Outbox event টাকে payment row এর সাথে একই transaction এ disk এ লিখে ফেলে। Process crash করলেও event টা database এ থাকে। Relay পরে সেটা খুঁজে পায়, আর পাঠিয়ে দেয়।
যে event হারালে টাকা বা data এর গরমিল হয়, সেটা আগে নিজের database এ, একই transaction এ লিখুন। পাঠানোর কাজ পরে।
১০ যে টার্মগুলো শিখলাম
| টার্ম | মানে |
|---|---|
| Distributed transaction | একাধিক service বা database জুড়ে ছড়ানো একটা কাজ, যেটাকে “হয় সব, নয়তো কিছুই না” রাখতে হয়। |
| Two-phase commit (2PC) | আগে সবাইকে prepare করিয়ে vote নেওয়া, সবাই YES দিলে তবেই একসাথে commit করার পদ্ধতি। |
| Coordinator | 2PC তে যে component সবাইকে prepare আর commit/abort বার্তা পাঠায়, আর শেষ সিদ্ধান্ত নেয়। |
| Participant | 2PC তে যে database বা service নিজের অংশের কাজ করে আর vote দেয়। |
| Lock | একটা row এর উপর আটকানো চিহ্ন। Lock থাকা অবস্থায় অন্য transaction সেই row বদলাতে পারে না। |
| Blocking problem | YES দেওয়ার পর coordinator crash করলে participant রা lock ধরে অনির্দিষ্টকাল অপেক্ষা করে। |
| Saga | Local transaction এর একটা chain। প্রতিটা step সাথে সাথে commit করে, fail হলে আগের step গুলো উল্টো কাজে ফেরানো হয়। |
| Compensating transaction | Saga র একটা commit হওয়া step এর প্রভাব কাটানোর নতুন কাজ, যেমন টাকা কাটার বিপরীতে refund। |
| Choreography | Saga চালানোর যে পদ্ধতিতে কোনো পরিচালক নেই, প্রতিটা service event শুনে নিজে চলে (সেশন ১০)। |
| Orchestrator | যে component saga র step গুলো ক্রমে চালায়, অবস্থা লিখে রাখে, আর fail হলে compensation চালায়। |
| Dual write | Database আর message broker এর মতো দুটো আলাদা system এ আলাদা করে লেখা, যেখানে মাঝখানে crash হলে একটা হয়, অন্যটা হয় না। |
| Outbox pattern | Event কে broker এ না পাঠিয়ে আগে business data র সাথে একই local transaction এ outbox table এ লেখা। |
| Outbox relay | যে process outbox table থেকে event পড়ে broker এ publish করে। |
| CDC (change data capture) | Database এর নিজের change log পড়ে প্রতিটা পরিবর্তনের খবর সাথে সাথে পাওয়ার কৌশল। |
| Authorization hold | Card এ টাকা কাটা নয়, শুধু আটকে রাখা। পরে কাটা হয়, অথবা ছেড়ে দেওয়া হয়। |
১১ শর্ট সামারি
- একটা database এর transaction অন্য database পর্যন্ত পৌঁছায় না। তাই কয়েকটা service জুড়ে চলা কাজ (distributed transaction) মাঝপথে fail করলে টাকা কাটা অথচ room নেই, এমন অর্ধেক অবস্থা তৈরি হয়।
- 2PC আগে সবাইকে prepare করিয়ে vote নেয়, সবাই YES দিলে তবেই commit করে। কিন্তু coordinator crash করলে সবাই lock ধরে আটকে থাকে, আর বাইরের API গুলো 2PC বোঝেই না।
- Saga প্রতিটা step এ সাথে সাথে local commit করে। কোনো step fail করলে আগের step গুলোর compensation উল্টো ক্রমে চলে, আর সেই compensation কে idempotent হতে হয়।
- Outbox pattern event কে business data র সাথে একই transaction এ লিখে রাখে, যাতে crash এ event না হারায়। আর user কে PENDING, CONFIRMED, REFUNDED এর মতো সৎ অবস্থা দেখাতে হয়।
১২ মনে রাখুন
- একটা transaction এর সীমানা একটা database। কয়েকটা service জুড়ে “হয় সব, নয়তো কিছুই না” রাখার দায়িত্ব আপনার design এর।
- 2PC এর YES একটা প্রতিশ্রুতি, আর সেটাই তার দুর্বলতা। Coordinator না ফেরা পর্যন্ত participant lock ধরে আটকে থাকে।
- Saga তে প্রতিটা step এর জোড়া compensation আগে থেকে লিখে রাখুন। Compensation উল্টো ক্রমে চলে, আর শুধু commit হওয়া step এর জন্যই চলে।
- Compensation কে idempotent আর শেষ পর্যন্ত সফল হতে হবে। Retry হবেই, আর refund বাদ দেওয়ার কোনো সুযোগ নেই।
- ফেরানো সবচেয়ে কঠিন step টা saga র শেষে রাখুন। শুরুতে টাকা কাটার বদলে শুধু আটকে রাখুন।
- Database আর broker এ আলাদা করে লিখবেন না। Event আগে outbox table এ, একই transaction এ লিখুন।
- সব step শেষ না হওয়া পর্যন্ত CONFIRMED দেখাবেন না। “এখনো হচ্ছে” বলা সবসময় নিরাপদ।