সেশন ১১

Microservices - কখন ভাঙবেন, আর কেন

এই সেশনের লক্ষ্য: Monolith কেন শুরুর জন্য ভালো, আর ঠিক কোন সমস্যা দেখা দিলে তাকে ভাঙার কথা ভাবা উচিত, সেটা বোঝা। Modular monolith, bounded context, database per service, distributed monolith, Conway's law আর strangler fig চিনে নেওয়া, আর microservices এর আসল cost গুলো মাথায় রাখা।

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

এখন পর্যন্ত আমরা একটা application কেই বড় করেছি। তার আরো copy চালিয়েছি, database কে replica আর shard এ ভাগ করেছি, সামনে cache আর queue বসিয়েছি। কিন্তু application এর code টা সবসময় একটাই ছিলো।

এই সেশনে প্রশ্নটা অন্য রকম। Code টাকেই কয়েকটা আলাদা program এ ভাগ করা উচিত কিনা, আর উচিত হলে কখন। এর নাম microservices। এটা নিয়ে অনেক উৎসাহ আছে, আবার অনেক দুঃখের গল্পও আছে। শেষে আপনি বুঝবেন, কোন সমস্যা দেখলে ভাগ করা ঠিক, আর কোন সমস্যা দেখলে ভাগ করলে বিপদ আরো বাড়ে।

  • ১ Monolith কী, আর শুরুতে এটা কেন ভালো পছন্দ? (monolith)
  • ২ ৯টা team একটা codebase এ কাজ করলে deploy বারবার কেন ভাঙে?
  • ৩ ভাগ করার আগে ভেতরটা গুছিয়ে নিলে কেমন হয়? (modular monolith)
  • ৪ কখন সত্যিই microservice এ ভাগ করা উচিত?
  • ৫ Service এর সীমানা কোথায় টানবো? (bounded context, database per service)
  • ৬ ভাগ করার পরও সব একসাথে ভাঙে কেন? (distributed monolith)
  • ৭ Team এর গঠন কীভাবে architecture ঠিক করে দেয়? (Conway's law)
  • ৮ চালু system কে বন্ধ না করে কীভাবে ভাঙবো? (strangler fig)
  • ৯ Microservices এর cost কী? (network call, debugging, data consistency, operations)

০১ Monolith কী, আর শুরুতে এটা কেন ভালো পছন্দ?

বৃহস্পতিবার, রাত পৌনে এগারোটা। নাদিয়া বাসায় ভাত খেতে বসেছে, ঠিক তখন ফোনটা বেজে উঠলো। Support team জানালো, checkout page এ সব order এর total দেখাচ্ছে ৳০। নাদিয়া “বাজারঘর” নামের একটা online দোকানের tech lead। রাত ৯টায় সপ্তাহের বড় deploy হয়েছে, তার পর থেকেই এই অবস্থা। প্লেট সরিয়ে রেখে সে laptop খুললো। এই মাসে এটা তৃতীয় বৃহস্পতিবার, যেদিন deploy এর পর কিছু একটা ভাঙলো।

গল্পটা অবশ্য শুরু হয়েছিলো অনেক শান্তভাবে। ২০২১ সালে বাজারঘরে engineer ছিলো মোটে চারজন। পুরো website ছিলো একটা program এ: product দেখানো, cart, payment, search, সব একসাথে। Code ছিলো একটা repository তে, database ছিলো একটাই। এই ধরনের application কে বলে monolith। অর্থাৎ পুরো application একটা codebase, একসাথে build হয়, একসাথে deploy হয়।

USERS LOAD BALANCER APPLICATION DATABASE ক্রেতা ক্রেতা ক্রেতা Load Balancer সেশন 02 MONOLITH COPY 1 Catalog Cart Payment Search MONOLITH COPY 2 Catalog Cart Payment Search MONOLITH COPY 3 Catalog Cart Payment Search Database সব table এক জায়গায়
শুরুর বাজারঘর: একটা codebase, একটা deploy, একটা database। Catalog, Cart, Payment আর Search সবাই একই program এর ভেতরে থাকে। Server গুলো stateless (সেশন ০২), তাই ভিড় বাড়লে load balancer এর পেছনে একই program এর আরো copy চালানো যায়। অর্থাৎ monolith ও scale করতে পারে।

Monolith নামটা শুনে অনেকে ভাবেন এটা পুরোনো আর খারাপ কিছু। আসলে শুরুর জন্য এটাই সবচেয়ে ভালো পছন্দ, আর তার পেছনে শক্ত কারণ আছে:

মনে রাখার কথা

Monolith মানে খারাপ design নয়। চারজন মানুষ আর একটা নতুন ব্যবসার জন্য এটাই সবচেয়ে দ্রুত আর সস্তা পথ। Shopify এর মতো বিশাল company ও আজও মূলত একটা বড় monolith এ চলে। সমস্যা monolith এ নয়, সমস্যা আসে যখন অনেক মানুষ একসাথে একই monolith বদলাতে থাকে।

০২ ৯টা team একটা codebase এ কাজ করলে deploy বারবার কেন ভাঙে?

চার বছরে বাজারঘর অনেক বড় হয়েছে। এখন engineer ৬০ জন, তারা ৯টা team এ ভাগ হয়ে কাজ করে: Catalog, Search, Cart, Checkout, Payment, Stock, Delivery, Seller আর Notification। কিন্তু code এখনো সেই একটাই monolith। প্রতি বৃহস্পতিবার রাতে সবার সপ্তাহের কাজ একসাথে deploy হয়।

আজ রাতে কী হয়েছিলো, নাদিয়া খুঁজে বের করলো। Search team দাম দেখানোর একটা shared function বদলেছিলো, যাতে search result এ discount দেখা যায়। ঐ একই function checkout page ও ব্যবহার করে, সেটা তারা জানতো না। একটা ছোট বদল, আর ভেঙে গেলো সম্পূর্ণ আলাদা একটা team এর page।

9 TEAMS · 45 CHANGES Catalog 5 change Search 1টায় bug Cart 5 change Checkout 5 change Payment 5 change Stock 5 change Delivery 5 change Seller 5 change Notify 5 change একটা codebase, একটা build পুরো test suite চলে: 40 মিনিট বৃহস্পতিবার রাত 9টা 45টা change একসাথে deploy Search এর 1টা change এ bug Checkout page এ total দেখায় ৳0 পুরো deploy rollback ভালো 44টা change ও ফেরত গেলো
৯টা team এর সব change একটা build এ মেশে, তারপর বৃহস্পতিবার রাতে একসাথে deploy হয়। Search team এর একটা ছোট ভুলে Checkout ভেঙে গেলো। Monolith এ শুধু একটা অংশ ফেরত নেওয়ার উপায় নেই, তাই বাকি ৪৪টা ভালো change ও ফেরত গেলো।

এবার হিসাবটা দেখুন। ধরুন প্রতিটা change এ bug থাকার সম্ভাবনা মাত্র ১%। Change কম থাকলে এটা প্রায় চোখেই পড়ে না। কিন্তু একসাথে অনেক change গেলে ঝুঁকি গুণ হয়ে বাড়ে:

ধরুন, প্রতিটা change এ bug থাকার সম্ভাবনা 1%   (মানে 99% change ঠিক থাকে)

2021: 4 জন engineer, এক deploy এ 5টা change
    সব ঠিক থাকার সম্ভাবনা  = 0.99^5   ≈ 0.95
    কিছু একটা ভাঙবে         = 1 - 0.95 ≈ 5%    (প্রায় 20 সপ্তাহে একবার)

2025: 60 জন engineer, এক deploy এ 45টা change
    সব ঠিক থাকার সম্ভাবনা  = 0.99^45  ≈ 0.64
    কিছু একটা ভাঙবে         = 1 - 0.64 ≈ 36%   (প্রায় 3 সপ্তাহে একবার)

চারজনের সময় প্রায় পাঁচ মাসে একবার কিছু ভাঙতো। এখন প্রায় প্রতি তিন সপ্তাহে একবার। কেউ খারাপ কাজ করছে না, শুধু মানুষ বেড়েছে। এর সাথে আরো কয়েকটা কষ্ট যোগ হয়েছে:

  1. একজনের ভুলে সবার কাজ ফেরত যায়: Monolith একটাই program, তাই শুধু Search এর অংশটা ফেরত নেওয়া যায় না। পুরো deploy rollback হয়, আর বাকি ৪৪টা ভালো change ও আটকে যায়।
  2. সবাই সবার জন্য অপেক্ষা করে: Payment team একটা জরুরি fix আজই দিতে চায়। কিন্তু deploy সপ্তাহে একবার, তাই বৃহস্পতিবার পর্যন্ত অপেক্ষা করতে হয়।
  3. Test শেষ হতে ৪০ মিনিট: একটা লাইন বদলালেও পুরো application এর সব test চালাতে হয়।
  4. Code এ কেউ নিশ্চিন্তে হাত দিতে পারে না: কোন function কে কে ব্যবহার করে, সেটা আর কারো মাথায় নেই। তাই সবাই ভয়ে ভয়ে code বদলায়।
আসল সমস্যা

বাজারঘরের traffic এর জন্য monolith এখনো যথেষ্ট। Server গুলো ঠিকই চলছে। সমস্যা হলো মানুষের: ৯টা team একটা জিনিস বদলাচ্ছে, আর একজনের কাজ অন্যজনের কাজে আটকে যাচ্ছে। এটা technical এর চেয়ে বেশি একটা team এর সমস্যা।

০৩ ভাগ করার আগে ভেতরটা গুছিয়ে নিলে কেমন হয়?

পরদিন সকালে CTO বললেন, “চলুন সব microservice বানিয়ে ফেলি। Netflix ও তো তাই করে।” নাদিয়া একটু থামলো। তার মনে হলো, code এর ভেতরে কোন অংশ কোথায় শেষ হয়, সেটাই এখন কেউ জানে না। এই অবস্থায় কেটে ভাগ করলে জট টা শুধু network এর দুই পাশে ছড়িয়ে যাবে।

তাই সে আগে একটা সহজ কাজ করলো। Program একটাই থাকলো, কিন্তু ভেতরটা গুছিয়ে নিলো। একে বলে modular monolith: deploy হয় একটা program হিসেবেই, কিন্তু ভেতরে প্রতিটা অংশ (module) পরিষ্কারভাবে আলাদা।

আগে: সব জড়ানো Catalog Cart Search Payment সবার table এক জায়গায় যে কেউ যেকোনো table পড়ে, যেকোনো table এ লেখে পরে: Modular monolith getPrice() pay() list() Catalog নিজের table Cart নিজের table Search নিজের table Payment নিজের table অন্য module এর table এ সরাসরি হাত নয় কথা হয় শুধু public function দিয়ে
বাম দিকে পুরোনো বাজারঘর: যেকোনো module যেকোনো module এর ভেতরে ঢুকে পড়ে, আর সবাই একই table এ লেখে। ডান দিকে একই program, কিন্তু ভেতরটা গোছানো। প্রতিটা module এর নিজের table আছে। অন্যের data লাগলে সে শুধু সেই module এর public function (যেমন getPrice()) call করে। Deploy এখনো একটাই।

গোছানোর নিয়ম তিনটা:

এতে আগের বৃহস্পতিবার রাতের মতো ঘটনা অনেক কমে যায়। Search team এখন আর checkout এর function এ হাত দিতে পারে না, কারণ ঐ function টা Checkout module এর ভেতরের, বাইরে থেকে দেখাই যায় না। তবে একটা জিনিস বদলায়নি। Deploy এখনো একটাই, সপ্তাহে একবার।

কেন এই ধাপটা বাদ দেওয়া যায় না

Modular monolith নিজেই অনেক company এর জন্য শেষ গন্তব্য। আর যাদের পরে সত্যিই ভাগ করতে হয়, তাদের জন্য এটা সবচেয়ে ভালো প্রস্তুতি। যে module এর সীমানা ইতিমধ্যে পরিষ্কার, তাকে আলাদা service বানানো অনেক সহজ। আগে ঘর গোছান, তারপর ঘর ভাগ করুন।

০৪ কখন সত্যিই microservice এ ভাগ করা উচিত?

প্রথমে নামটা পরিষ্কার করে নিই। Microservice হলো একটা ছোট, আলাদা program, যেটা ব্যবসার একটা নির্দিষ্ট কাজ সামলায়। এর নিজের code আছে, নিজের database আছে, আর নিজের সময়ে আলাদাভাবে deploy করা যায়। বাকিরা তার সাথে network এর উপর দিয়ে কথা বলে। “Micro” শব্দটা নিয়ে ভুল বোঝার সুযোগ আছে। এখানে ছোট মানে code এর লাইন কম নয়, মানে দায়িত্ব একটাই।

শুধু “বড় company রা করে” বলে ভাগ করা কোনো কারণ নয়। ভাগ করার পেছনে চারটা আসল কারণ থাকতে পারে।

কারণ ১: Team রা একে অপরকে আটকাচ্ছে

এটা আগের section এর গল্প। একটা service এর মালিক যখন একটা team, তখন সেই team নিজের সময়ে deploy করতে পারে। Payment team মঙ্গলবার দুপুরে fix দিতে চাইলে কারো অনুমতি লাগে না। Search team এর bug এ Payment এর deploy আর আটকায় না।

কারণ ২: আলাদা আলাদা অংশে আলাদা চাপ

গত ঈদের sale এর রাতে বাজারঘরে search এর traffic স্বাভাবিকের ১০ গুণ হয়েছিলো, প্রতি second এ ৩০০ থেকে ৩,০০০ request। কিন্তু Cart আর Payment এ চাপ বেড়েছিলো সামান্য। Admin panel এ বাড়েইনি। স্বাভাবিক দিনে monolith এর ৪টা copy চলে, আর মোট request এর প্রায় অর্ধেকই আসে search এ। তাই search ১০ গুণ হলে মোট চাপ প্রায় ৫ গুণ হয়, আর তখন ২০টা copy লাগে।

MONOLITH পুরো program এর 20টা copy আলাদা SERVICE শুধু Search এর 20টা copy বাকি সব: আগের মতোই 4টা copy 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 4 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 1 GB 3 GB 3 GB 3 GB 3 GB Cart, Payment, Admin ও 20 গুণ হলো, দরকার ছাড়াই 20 × 4 GB = 80 GB RAM Search: 20 × 1 GB, বাকি: 4 × 3 GB মোট 32 GB RAM
ঈদের sale এর রাতে Search এ traffic 10 গুণ হলো, কিন্তু Cart আর Payment এ বাড়লো সামান্য। Monolith এ শুধু Search কে বড় করার উপায় নেই, পুরো program এর copy বাড়াতে হয়। Search আলাদা service হলে শুধু তার copy বাড়ে। একই চাপ সামলাতে RAM লাগে 80 GB এর বদলে 32 GB।

Monolith এ শুধু search এর অংশটা বড় করার উপায় নেই। তাই পুরো program এর ২০টা copy চালাতে হয়েছিলো। Search আলাদা service হলে শুধু তারই copy বাড়ে। কিছু অংশের ধরনও আলাদা হয়। যেমন ছবি resize করতে CPU বেশি লাগে, আর search index রাখতে RAM বেশি লাগে। আলাদা service হলে প্রত্যেককে তার উপযুক্ত machine এ চালানো যায়।

কারণ ৩: একটার failure যেন বাকিদের না থামায়

একবার “আপনার জন্য সুপারিশ” (recommendation) অংশের একটা bug এ memory শেষ হয়ে গিয়েছিলো। Monolith এর process টাই crash করলো, আর তার সাথে checkout ও বন্ধ হয়ে গেলো। সুপারিশ না দেখালে কেউ রাগ করে না, কিন্তু checkout বন্ধ মানে বিক্রি বন্ধ। আলাদা service হলে recommendation crash করলেও checkout চলতে থাকে। শুধু সুপারিশের জায়গাটা খালি দেখায়। একে বলে fault isolation: একটা অংশের failure কে তার নিজের সীমানার ভেতরে আটকে রাখা।

কারণ ৪: আলাদা নিয়ম বা technology লাগে

Payment এর data নিয়ে কঠোর security নিয়ম মানতে হয়। পুরো monolith কে সেই নিয়মের আওতায় আনার চেয়ে শুধু Payment কে আলাদা করে সুরক্ষিত রাখা সহজ। আবার কোনো অংশ হয়তো অন্য programming language এ লিখলে অনেক ভালো চলে।

নাদিয়া এই কারণগুলো দিয়ে একটা ছোট decision chart বানালো। নতুন কোনো অংশ আলাদা করার প্রস্তাব এলে সে এই চারটা প্রশ্ন করে:

না 1. Boundary কি পরিষ্কার? নিজের data, নিজের কাজ (bounded context) আগে module গোছান এখনই ভাগ করবেন না হ্যাঁ হ্যাঁ হ্যাঁ হ্যাঁ 2. Team রা কি একে অপরকে আটকায়? merge, test আর deploy এ অপেক্ষা না 3. Scale এর চাহিদা কি খুব আলাদা? যেমন Search 10 গুণ, Admin একই না 4. এর crash কি বাকিদেরও থামায়? যেমন Recommendation এর জন্য Checkout বন্ধ না Service আলাদা করুন যেকোনো একটায় হ্যাঁ হলে Monolith এই থাকুন ভেতরটা modular রাখুন, ভাগ পরে
আলাদা service বানানোর আগে নাদিয়া এই চারটা প্রশ্ন করে। সীমানাই পরিষ্কার না হলে ভাগ করা যাবে না। সীমানা পরিষ্কার হলে বাকি তিনটা প্রশ্নের যেকোনো একটায় "হ্যাঁ" এলে service আলাদা করার কারণ আছে। তিনটাতেই "না" এলে monolith ই ঠিক জায়গা।
সংখ্যার একটা আন্দাজ

কোনো নির্দিষ্ট সংখ্যা নেই, তবে অভিজ্ঞতা থেকে একটা আন্দাজ পাওয়া যায়। দশ-পনেরো জনের একটা team এর জন্য সাধারণত monolith ই সবচেয়ে ভালো। ত্রিশ-চল্লিশ জন পেরোলে, আর team রা একে অপরের deploy আটকাতে শুরু করলে, ভাগ করার কথা ভাবার সময় আসে। মাপকাঠি হলো ব্যথা, ফ্যাশন নয়।

০৫ Service এর সীমানা কোথায় টানবো?

ভাগ করবো ঠিক হলো। এবার সবচেয়ে কঠিন প্রশ্ন: কোথায় কাটবো? ভুল জায়গায় কাটলে দুইটা service কে সারাক্ষণ একে অপরকে call করতে হয়। তখন আলাদা করে কোনো লাভ হয় না।

একই শব্দ, তিন রকম মানে

বাজারঘরের সবচেয়ে বেশি বিক্রি হওয়া জিনিসটা ধরুন, product #8841, একটা নীল পাঞ্জাবি। তিনটা team এর কাছে এই একই পাঞ্জাবি তিন রকম জিনিস:

Product #8841 একটা নীল পাঞ্জাবি Catalog context নাম, ছবি, বর্ণনা size chart, category "দেখতে কেমন?" Inventory context SKU, stock: 37টা warehouse: মিরপুর "কয়টা আছে, কোথায়?" Order context কেনার সময়ের দাম ৳1,450 qty: 2, size: L "কে, কত দামে কিনলো?" একই শব্দ "Product", কিন্তু তিন team এর কাছে তিনটা আলাদা জিনিস
Catalog এর কাছে পাঞ্জাবি মানে ছবি আর বর্ণনা। Inventory এর কাছে মানে গুদামে কয়টা আছে। Order এর কাছে মানে কেনার মুহূর্তের দাম, যেটা পরে দাম বদলালেও বদলায় না। প্রতিটা ঘর একটা bounded context, আর service এর সীমানা টানার সবচেয়ে ভালো জায়গা এই ঘরগুলোর দেয়াল।

Catalog team এর কাছে পাঞ্জাবি মানে ছবি, বর্ণনা আর size chart। Stock team এর কাছে মানে মিরপুরের গুদামে কয়টা আছে। Order team এর কাছে মানে “রহিম ভাই গত বুধবার কত টাকায় দুইটা কিনেছিলেন”। পরে পাঞ্জাবির দাম বাড়লেও ঐ পুরোনো order এর দাম বদলায় না।

এই প্রতিটা ঘরকে বলে bounded context। একটা bounded context এর ভেতরে প্রতিটা শব্দের একটাই পরিষ্কার মানে থাকে, আর এর নিজের data আর নিয়ম থাকে। ঘরের বাইরে গেলে একই শব্দের মানে বদলে যেতে পারে। Service এর সীমানা টানার সবচেয়ে ভালো জায়গা এই ঘরগুলোর দেয়াল। কারণ ঘরের ভেতরের কাজগুলো একে অপরের সাথে ঘন ঘন কথা বলে, কিন্তু অন্য ঘরের সাথে কথা হয় কম।

Database per service

সীমানা টানার পর আসে data এর প্রশ্ন। নাদিয়ার team একটা নিয়ম ঠিক করলো, যেটা পরে সবচেয়ে কাজে লেগেছে: database per service। অর্থাৎ প্রতিটা service এর নিজের database থাকে, আর সেই data এর একমাত্র মালিক সে নিজেই। অন্য কোনো service সেই database এ সরাসরি query চালাতে পারে না। Data লাগলে মালিকের API তে জিজ্ঞেস করতে হয়।

SHARED DATABASE Payment Order Stock Shared DB সবার table Stock team একটা column বদলালো Order আর Payment ও ভাঙলো DATABASE PER SERVICE API API Payment Order Stock payments orders stock অন্যের data লাগলে API তে চাইতে হয়, সরাসরি SQL এ নয়
উপরে: তিনটা service একই database এর table সরাসরি পড়ে। Stock team তাদের table এর একটা column এর নাম বদলালে বাকি দুই service এর query ও ভেঙে যায়। নিচে: প্রতিটা service তার data এর একমাত্র মালিক। Order এর stock জানা দরকার হলে সে Stock service এর API তে জিজ্ঞেস করে। Stock team ভেতরে যা খুশি বদলাতে পারে, শুধু API টা ঠিক রাখলেই হলো।

নিয়মটা কড়া শোনায়, কিন্তু এর কারণ সহজ। Database ভাগ করে রাখলে Stock team তাদের table এর গঠন যখন খুশি বদলাতে পারে। শুধু API টা আগের মতো রাখলেই চলে। Shared database থাকলে তাদের একটা column এর নাম বদলালেই অন্য team এর query ভেঙে যায়। তখন সবাইকে আবার একসাথে deploy করতে হয়, আর আমরা ফিরে যাই বৃহস্পতিবার রাতের সেই সমস্যায়।

সেশন ০৫ এর সাথে মিল

সেশন ০৫ এ data কে কয়েকটা shard এ ভাগ করার সময় দেখেছিলেন, দুই shard জুড়ে একটা transaction চালানো কঠিন। Database per service এও ঠিক একই সমস্যা আসে। Order আর Payment এখন দুইটা আলাদা database এ, তাই একটা transaction দিয়ে দুটোকে একসাথে আপডেট করা যায় না। এর সমাধান (saga) আছে সেশন ১৭ তে।

০৬ ভাগ করার পরও সব একসাথে ভাঙে কেন?

নাদিয়ার team এর আগে একবার একটা চেষ্টা হয়েছিলো, আর সেটার গল্পটা শিক্ষণীয়। দুই বছর আগে একজন senior engineer তাড়াহুড়া করে Order, Payment, Stock আর Notification কে চারটা আলাদা service বানিয়েছিলেন। Code আলাদা repository তে গেলো, আলাদা server এ চললো। কিন্তু database টা থাকলো আগের সেই একটাই।

প্রথম চেষ্টা: 4টা SERVICE, 1টা DATABASE sync sync sync Order Payment Stock Notify পুরোনো DB আগের মতোই shared এখনো সব একসাথে deploy DB তে একটা বদল, 4টা service বদলায় একজন ধীর হলে সবাই ধীর Order → Payment → Stock → Notify
বাইরে থেকে দেখলে চারটা আলাদা service। কিন্তু সবাই একই database এর table ছোঁয়, আর একটা order হতে চারজনকে পরপর অপেক্ষা করতে হয়। ফলে monolith এর সব সমস্যা রয়ে গেলো (একসাথে deploy, একজনের ভুলে সবাই আটকায়), আর তার উপর যোগ হলো network এর ধীরগতি আর failure। একেই বলে distributed monolith।

ফলাফল হলো উল্টো। Order table এ একটা নতুন column যোগ হলে চারটা service এরই code বদলাতে হতো। তাই চারটাকে একই দিনে, একই ক্রমে deploy করতে হতো। একটা order দিতে Order কে Payment এর জন্য অপেক্ষা করতে হতো, Payment কে Stock এর জন্য, Stock কে Notification এর জন্য। Notification একটু ধীর হলেই পুরো checkout ধীর হয়ে যেতো। ছয় মাস পর সবাই হাল ছেড়ে দিলো, আর চারটা service কে আবার monolith এ ফিরিয়ে আনা হলো। তাই নাদিয়া এবার এত সাবধান।

এই অবস্থার নাম distributed monolith। দেখতে microservices, কিন্তু আচরণে monolith। এটা দুই জগতের সবচেয়ে খারাপ অংশ একসাথে নিয়ে আসে। Monolith এর মতোই সবাইকে একসাথে deploy করতে হয়, আর microservices এর মতো network এর ধীরগতি আর failure ও সহ্য করতে হয়।

কয়েকটা লক্ষণ দেখলে বুঝবেন আপনি এই ফাঁদে পড়েছেন:

এক লাইনের পরীক্ষা

নিজেকে জিজ্ঞেস করুন: “আমি কি এই service টা মঙ্গলবার দুপুরে, অন্য কোনো team কে না জানিয়ে deploy করতে পারি?” উত্তর “না” হলে আপনার কাছে microservices নেই। আছে একটা monolith, যেটা এখন network দিয়ে জোড়া লাগানো।

০৭ Team এর গঠন কীভাবে architecture ঠিক করে দেয়?

নাদিয়া একদিন বাজারঘরের পুরোনো system diagram আর team এর তালিকা পাশাপাশি রাখলো। সে অবাক হয়ে দেখলো, দুটো প্রায় একই রকম দেখতে। যে দুই team রোজ একসাথে বসে, তাদের code ও সবচেয়ে বেশি জড়ানো। যে team সবার থেকে আলাদা বসে (Seller panel), তাদের code ও সবচেয়ে আলাদা।

এটা কাকতালীয় নয়। ১৯৬৮ সালে Melvin Conway নামের একজন programmer লক্ষ্য করেছিলেন: একটা প্রতিষ্ঠান যে system বানায়, তার গঠন সেই প্রতিষ্ঠানের মানুষদের যোগাযোগের গঠনের মতো হয়। এই পর্যবেক্ষণের নাম Conway’s law। সহজ কথায়, চারটা team মিলে একটা compiler বানালে সেটা প্রায় নিশ্চিতভাবে চার ধাপের compiler হবে।

TEAMS SERVICES রোজ কথা হয় রোজ কথা হয় API API মালিক মালিক মালিক Catalog team 6 জন Checkout team 7 জন Delivery team 5 জন Catalog service নিজের code, নিজের DB Checkout service নিজের code, নিজের DB Delivery service নিজের code, নিজের DB Team যে পথে কথা বলে, service গুলোও সেই পথেই কথা বলে
বাম দিকে মানুষ, ডান দিকে software। প্রতিটা team একটা service এর পুরো মালিক। Checkout team কে Catalog আর Delivery দুই team এর সাথেই রোজ কথা বলতে হয়, তাই Checkout service ও এই দুই service এর API ব্যবহার করে। Team এর মানচিত্র আর system এর মানচিত্র প্রায় একই রকম দেখায়।

এর কারণ খুব মানবিক। যাদের সাথে আপনি রোজ কথা বলেন, তাদের code এর সাথে নিজের code মেশানো সহজ। যাদের সাথে কথা হয় না, তাদের সাথে একটা পরিষ্কার চুক্তি (API) ছাড়া কাজ চলে না।

তাই নাদিয়া সিদ্ধান্ত নিলো, আগে team সাজাবে, তারপর code। প্রতিটা bounded context এর জন্য একটা team, আর সেই team তার service এর পুরো মালিক: code, database, deploy, এমনকি রাতে কিছু ভাঙলে ফোনটাও তাদের কাছেই যাবে। এই উল্টো পথে হাঁটাকে বলে inverse Conway maneuver: যে architecture চান, আগে সেই আকারে team সাজান।

Team এর আকার

Amazon এর একটা পরিচিত নিয়ম আছে, যার নাম “two-pizza team”: একটা team এ এমন কয়জন থাকবে, যাতে দুইটা pizza তেই সবার পেট ভরে যায়। মোটামুটি ছয় থেকে দশ জন। এর চেয়ে বড় হলে team এর ভেতরেই যোগাযোগ কঠিন হয়ে পড়ে। একটা service এর মালিক এমন একটা ছোট team হলে সবচেয়ে ভালো চলে।

০৮ চালু system কে বন্ধ না করে কীভাবে ভাঙবো?

CTO এর প্রথম প্রস্তাব ছিলো: “ছয় মাস নতুন feature বন্ধ রেখে পুরো system নতুন করে microservices এ লিখি।” একে বলে big bang rewrite, আর এর ইতিহাস খুব একটা সুখের নয়। ছয় মাস প্রায় সবসময় আঠারো মাস হয়ে যায়। এর মধ্যে পুরোনো system এ নতুন bug আসে, ব্যবসার চাহিদা বদলায়। আর শেষে একদিন সব traffic একবারে নতুন system এ সরাতে হয়, যেদিন ভুল হলে পুরো দোকান বন্ধ।

নাদিয়া অন্য পথ বেছে নিলো। পুরান ঢাকায় তার নানাবাড়ির পুরোনো দালানের দেয়ালে একটা অশ্বত্থ গাছ গজিয়েছিলো। প্রথমে ছোট একটা চারা। তারপর শিকড় দেয়াল বেয়ে নিচে নামলো, বছরের পর বছর মোটা হলো। একসময় দেখা গেলো দেয়ালের অনেকটা অংশ গাছের শিকড়েই ধরে আছে। বট আর অশ্বত্থ এই দলেরই গাছ, ইংরেজিতে যাদের বলে strangler fig। এরা অন্য গাছ বা দেয়ালের গায়ে জন্মায়, আর ধীরে ধীরে তার জায়গা নিয়ে নেয়।

২০০৪ সালে Martin Fowler এই গাছের নামেই একটা migration কৌশলের নাম দেন: strangler fig pattern। পুরোনো system চালু রেখে তার চারপাশে নতুন service বানানো হয়, একটা একটা feature করে। পুরোনো অংশ ধীরে ধীরে ছোট হয়, একসময় পুরোপুরি বন্ধ করে দেওয়া যায়।

ধাপ 1: সামনে একটা ROUTER সব request Router URL দেখে পাঠায় Monolith Catalog · Search · Cart · Payment · Admin ধাপ 2: SEARCH বের হলো /search বাকি সব Router URL দেখে পাঠায় Search service Monolith Catalog · Cart · Payment · Admin নতুন ধাপ 3: MONOLITH ছোট হচ্ছে Router URL দেখে পাঠায় Search service Cart service Payment service Monolith: শুধু Admin একদিন এটাও বন্ধ হবে
প্রথমে monolith এর সামনে একটা router বসে, যেটা URL দেখে request পাঠায়। User কিছুই টের পায় না। তারপর Search নতুন service এ চলে যায়, আর router শুধু /search request গুলো সেখানে পাঠায়। এভাবে একটা একটা করে feature বের হয়। Monolith প্রতি ধাপে ছোট হয়, কিন্তু কোনোদিন পুরো system বন্ধ রাখতে হয় না।

বাজারঘরে কাজটা হলো এভাবে:

  1. সামনে একটা router বসানো: Monolith এর সামনে একটা router বসলো, যেটা request এর URL দেখে ঠিক করে কোথায় পাঠাবে। শুরুতে সে সব request monolith এই পাঠায়। User কোনো পার্থক্য টের পায় না। (এই router পরে API gateway তে পরিণত হয়, সেটা সেশন ১৩ তে দেখবো।)
  2. প্রথম feature বেছে নেওয়া: নাদিয়া প্রথমে Search বেছে নিলো। এর সীমানা পরিষ্কার, ঈদে এর চাপ আলাদা, আর এটা বেশিরভাগ data শুধু পড়ে। এমন অংশ দিয়ে শুরু করলে ঝুঁকি কম।
  3. অল্প অল্প traffic সরানো: নতুন Search service তৈরি হলে router প্রথমে মাত্র ৫% search request সেখানে পাঠালো। সব ঠিক থাকায় এক সপ্তাহে ধীরে ধীরে ১০০% এ নেওয়া হলো। সমস্যা হলে router এর একটা setting বদলালেই আবার monolith এ ফেরা যেতো।
  4. পুরোনো code মুছে ফেলা: Search এর সব traffic নতুন service এ চলে যাওয়ার পর monolith থেকে পুরোনো search code ডিলিট করা হলো।
  5. পরের feature এ যাওয়া: তারপর Cart, তারপর Payment। প্রতিবার একই ধাপ।
কেন এই পথ নিরাপদ

প্রতিটা ধাপ ছোট, আর প্রতিটা ধাপ থেকে ফেরা যায়। কোনোদিন পুরো system একবারে বদলায় না। মাঝপথে থেমে গেলেও ক্ষতি নেই: যতটুকু সরানো হয়েছে, সেটা কাজ করছে, আর বাকিটা monolith এ আগের মতোই চলছে।

০৯ Microservices এর cost কী?

কয়েক মাসে Search এর পর Cart, Payment আর Stock ও আলাদা service হয়ে গেলো। তার কিছুদিন পর এক সন্ধ্যায় কয়েকজন customer অভিযোগ করলো, তাদের order “processing” এ আটকে আছে। আগে এমন হলে নাদিয়া একটা log file খুলে দশ মিনিটে কারণ বের করতো। এবার একটা order চার জায়গা ঘুরে আসে: monolith, Cart, Payment আর Stock service। চার জায়গার log মিলিয়ে কারণ খুঁজে পেতে লাগলো তিন ঘণ্টা। কারণটা ছিলো, Stock service এর একটা call মাঝে মাঝে timeout হচ্ছিলো।

সেদিন নাদিয়া বুঝলো, microservices বিনা পয়সায় আসে না। প্রথম আর সবচেয়ে বড় cost হলো network:

MONOLITH: FUNCTION CALL checkStock(8841) ~0.001 ms একই process, একই memory Order module Stock module MICROSERVICES: NETWORK CALL request: JSON বানানো, network এ পাঠানো response: আবার network, আবার JSON পড়া Order service Stock service ~1-5 ms, প্রায় 1000 গুণ বেশি সময় Network ধীর হতে পারে, timeout হতে পারে, Stock service down থাকতে পারে
একই প্রশ্ন ("এই পাঞ্জাবি stock এ আছে?") দুইভাবে করা হলো। Monolith এর ভেতরে এটা একটা সাধারণ function call, প্রায় এক microsecond লাগে আর প্রায় কখনো fail করে না। দুইটা service এর মধ্যে একই প্রশ্ন যায় network দিয়ে। সময় লাগে কয়েক millisecond, আর পথে অনেক কিছু ভুল হতে পারে।

একটা call এ কয়েক millisecond হয়তো বড় কিছু না। কিন্তু একটা checkout এ কয়েকটা service কে পরপর call করতে হয়। তখন সময় আর ঝুঁকি দুটোই যোগ হতে থাকে:

Checkout এ 4টা service কে পরপর call (Cart, Stock, Payment, Delivery):

    Monolith:       4 × 0.001 ms  ≈ 0.004 ms
    Microservices:  4 × 3 ms      ≈ 12 ms

প্রতিটা service 99.9% সময় ঠিকঠাক উত্তর দিলে:

    চারটাই ঠিক থাকার সম্ভাবনা = 0.999^4 ≈ 99.6%
    অর্থাৎ প্রতি 1,000 checkout এ প্রায় 4টা কোথাও না কোথাও আটকায়

প্রতি হাজারে চারটা checkout আটকানো শুনতে ছোট লাগে। কিন্তু ঈদের রাতে এক লাখ checkout হলে সংখ্যাটা চারশো। Monolith এ এই সমস্যাটা ছিলোই না, কারণ function call মাঝপথে হারায় না। এই ধরনের ব্যর্থতাকে বলে partial failure: system এর একটা অংশ fail করেছে, বাকিটা ঠিক আছে। Service গুলো কীভাবে কথা বলবে আর timeout কীভাবে সামলাবে, সেটা সেশন ১২ এর বিষয়।

বাকি cost গুলো একসাথে দেখে নিন:

Microservices এর downside গুলো

  • Network call: আগে যেটা function call ছিলো, এখন সেটা network এর উপর দিয়ে যায়। প্রতিটা call এ কয়েক millisecond লাগে, আর যেকোনো call fail করতে পারে।
  • Debugging কঠিন: একটা order এখন ছয়টা service ঘুরে আসে। ভুল কোথায় হলো, সেটা খুঁজতে ছয় জায়গার log মেলাতে হয়।
  • Data consistency: Order, Payment আর Stock এর data এখন তিনটা আলাদা database এ। একটা transaction দিয়ে তিনটাকে একসাথে আপডেট করার উপায় নেই।
  • Operations এর বোঝা: একটার বদলে পনেরোটা service এর deploy pipeline, monitoring আর alert চালাতে হয়। রাতে কোন team এর ফোন বাজবে, সেটাও ঠিক করে রাখতে হয়।
  • Test করা কঠিন: নিজের service একা ঠিক চললেই হয় না। বাকিদের সাথে মিলে ঠিক চলছে কিনা, সেটাও দেখতে হয়।

এগুলোর প্রতিটার জন্য আলাদা সমাধান আছে, আর পরের সেশনগুলোতে আমরা সেগুলো শিখবো। চার service জুড়ে একটা request কে খুঁজে বের করার কৌশল (distributed tracing) আছে সেশন ১৯ এ। আলাদা database জুড়ে consistency রাখার কৌশল আছে সেশন ১৭ তে। কিন্তু প্রতিটা সমাধান আবার নতুন কিছু শেখার আর চালানোর দায়িত্ব যোগ করে।

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

Microservices একটা technical সমস্যার চেয়ে বেশি একটা মানুষের সমস্যার সমাধান। অনেক team কে আলাদাভাবে, নিজের গতিতে কাজ করতে দেয়, আর আলাদা অংশকে আলাদাভাবে scale করতে দেয়। বিনিময়ে আপনি নেন network এর ধীরগতি, কঠিন debugging, ভাগ হওয়া data আর অনেক বেশি operations এর কাজ। Team এর ব্যথা এই cost এর চেয়ে বড় হলে তবেই ভাগ করুন।

ছয় মাস পরের এক বৃহস্পতিবার রাত। Payment team সেদিন বিকেলেই নিজেদের fix deploy করে বাড়ি চলে গেছে। Search team এর একটা bug এ search কিছুক্ষণ ধীর ছিলো, কিন্তু checkout একবারও থামেনি। নাদিয়া সেদিন রাতে ভাত খেতে বসলো, আর ফোনটা একবারও বাজলো না।

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

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

প্রশ্ন ১: একটা নতুন startup, তিনজন engineer। Interview এ জিজ্ঞেস করা হলো, শুরু থেকেই microservices এ বানাবেন কিনা। কী বলবেন?
উত্তর দেখুন আগে নিজে ভাবুন

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

“হ্যাঁ, শুরু থেকেই microservices। পরে monolith ভাঙা অনেক কষ্টের। এখনই ভাগ করে রাখলে পরে scale করতে সুবিধা হবে।”

কথাটা শুনতে দূরদর্শী লাগে, কিন্তু দুটো জিনিস বাদ পড়েছে।

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

প্রথমত, শুরুতে কেউ জানে না ব্যবসার সীমানাগুলো কোথায়। Product টাই হয়তো ছয় মাসে বদলে যাবে। ভুল জায়গায় ভাগ করলে পরে সেই সীমানা বদলানো monolith ভাঙার চেয়েও কঠিন, কারণ এখন সেটা network আর আলাদা database জুড়ে ছড়ানো। দ্বিতীয়ত, তিনজন মানুষের মধ্যে কেউ কাউকে আটকায় না। তাই ভাগ করার প্রধান লাভটাই এখানে নেই, কিন্তু network, debugging আর operations এর সব cost পুরোপুরি আছে।

ভালো উত্তর হলো: একটা modular monolith দিয়ে শুরু করবো। Module এর সীমানা পরিষ্কার রাখবো, প্রতিটা module এর নিজের table রাখবো। পরে team বড় হলে বা কোনো অংশের চাপ আলাদা হলে সেই module টা strangler fig দিয়ে বের করে আনবো।

Interview এ বলার মতো

Monolith first, কিন্তু ভেতরে modular। ভাগ করার সিদ্ধান্ত নিন কোনো নির্দিষ্ট ব্যথা দেখে, যেমন team রা একে অপরের deploy আটকাচ্ছে বা একটা অংশের scaling এর চাহিদা আলাদা।

প্রশ্ন ২: একটা company ৪০টা microservice বানালো, কিন্তু প্রতিটা release এ সব service একসাথে deploy করে। সমস্যাটা কোথায়?
উত্তর দেখুন আগে নিজে ভাবুন

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

“Deploy process টা ধীর। একটা ভালো CI/CD pipeline (code থেকে নিজে নিজে build, test আর deploy করার ব্যবস্থা) বানালেই ঠিক হয়ে যাবে।”

Pipeline দ্রুত হলে কষ্ট একটু কমে, কিন্তু রোগটা থেকে যায়।

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

সব service একসাথে deploy করতে হচ্ছে মানে তারা একে অপরের উপর শক্তভাবে নির্ভরশীল। এটা distributed monolith এর লক্ষণ। কারণ খুঁজতে হবে তিন জায়গায়: তারা কি একই database এর table ছোঁয়? একজনের API বদলালে কি অন্যদের code ও বদলাতে হয়? একটা request কি অনেকগুলো service এর লম্বা sync শিকলের উপর নির্ভর করে?

সমাধান pipeline এ নয়, সীমানায়। প্রতিটা service কে নিজের data এর মালিক বানাতে হবে (database per service)। API বদলাতে হলে পুরোনো version কিছুদিন চালু রাখতে হবে, যাতে অন্যরা নিজের সময়ে বদলাতে পারে। দরকার হলে ভুল জায়গায় কাটা কয়েকটা service কে আবার জোড়া লাগাতে হবে। Segment নামের একটা company ২০১৮ সালে ঠিক এটাই করেছিলো: শতাধিক ছোট service কে আবার একটা service এ ফিরিয়ে এনেছিলো, আর তাতে তাদের কাজ অনেক সহজ হয়েছিলো।

মূল কথা

Microservices এর মাপকাঠি service এর সংখ্যা নয়। মাপকাঠি হলো প্রতিটা service একা, অন্য কাউকে না জানিয়ে deploy করা যায় কিনা।

প্রশ্ন ৩: Order service এর stock জানা দরকার। Stock এর table তো পাশের database এ আছে, সরাসরি একটা SELECT চালালেই হয়। কেন API call করতে হবে, যেটা ধীরও?
উত্তর দেখুন আগে নিজে ভাবুন

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

“API call মানে network, মানে কয়েক millisecond বেশি। Table টা পড়ার অনুমতি দিলেই সবচেয়ে দ্রুত হয়। শুধু পড়ছি, লিখছি তো না, তাই ক্ষতি কী?”

দ্রুততার হিসাব ঠিক আছে। কিন্তু এখানে দ্রুততার চেয়ে বড় একটা জিনিস হারাচ্ছে।

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

Order service সরাসরি table পড়া শুরু করলে Stock এর table এর গঠন আর Stock team এর একার থাকে না। Stock team একটা column এর নাম বদলালে বা table টা দুই ভাগ করলে Order এর query ভেঙে যাবে। তাই কোনো বদলের আগে Order team এর সাথে বসতে হবে, আর দুই service একসাথে deploy করতে হবে। শুধু পড়লেও এই বাঁধন তৈরি হয়। এভাবে কয়েকটা service একই table পড়া শুরু করলে আমরা ফিরে যাই distributed monolith এ।

API হলো একটা চুক্তি: “এই প্রশ্ন করলে এই আকারে উত্তর পাবেন।” চুক্তির পেছনে Stock team যা খুশি বদলাতে পারে। আর ধীরগতির ভয় থাকলে Order service উত্তরটা কিছুক্ষণের জন্য cache করে রাখতে পারে (সেশন ০৪)।

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

অন্য service এর database মানে অন্যের ঘর। দরজায় নক করুন (API), জানালা দিয়ে ঢুকবেন না (সরাসরি SQL)।

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

টার্মমানে
Monolithপুরো application একটা codebase, একসাথে build হয় আর একসাথে deploy হয়।
Modular monolithDeploy হয় একটা program হিসেবে, কিন্তু ভেতরে প্রতিটা module এর নিজের table আর public function আছে।
Microserviceব্যবসার একটা কাজ সামলানো ছোট, আলাদা program। নিজের code, নিজের database, আর আলাদাভাবে deploy করা যায়।
Fault isolationএকটা অংশের failure কে তার নিজের সীমানার ভেতরে আটকে রাখা, যাতে বাকিরা চলতে থাকে।
Bounded contextব্যবসার এমন একটা অংশ, যার ভেতরে প্রতিটা শব্দের একটাই মানে আর যার নিজের data ও নিয়ম আছে।
Database per serviceপ্রতিটা service এর নিজের database। অন্যরা সেই data চাইলে API দিয়ে চায়, সরাসরি SQL দিয়ে নয়।
Distributed monolithদেখতে আলাদা service, কিন্তু shared database বা শক্ত নির্ভরতার কারণে সবাইকে একসাথে deploy করতে হয়।
Conway’s lawএকটা প্রতিষ্ঠান যে system বানায়, তার গঠন প্রতিষ্ঠানের মানুষদের যোগাযোগের গঠনের মতো হয়।
Inverse Conway maneuverযে architecture চান, আগে সেই আকারে team সাজানো।
Strangler figপুরোনো system চালু রেখে একটা একটা feature নতুন service এ সরানো, যতক্ষণ না পুরোনোটা বন্ধ করা যায়।
Big bang rewriteপুরো system একবারে নতুন করে লিখে একদিনে বদলে ফেলা। ঝুঁকিপূর্ণ পথ।
Partial failureSystem এর একটা অংশ fail করেছে, কিন্তু বাকি অংশ চলছে। Network এর উপর চলা system এ এটা স্বাভাবিক ঘটনা।

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

  1. Monolith শুরুর জন্য সবচেয়ে সহজ আর দ্রুত পথ, আর stateless server হলে এটাও scale করে। এর আসল সমস্যা দেখা দেয় তখন, যখন অনেকগুলো team একই codebase বদলায় আর একে অপরের deploy আটকায়।
  2. ভাগ করার আগে modular monolith দিয়ে ভেতরটা গোছান। Team রা আটকালে, কোনো অংশের scaling এর চাহিদা আলাদা হলে, বা একটার failure বাকিদের থামালে তবেই সেই অংশটা আলাদা service বানান।
  3. Service এর সীমানা টানুন bounded context ধরে, আর প্রতিটা service কে নিজের database এর মালিক বানান। Shared database বা একসাথে deploy থাকলে সেটা distributed monolith, যেখানে দুই দিকেরই খারাপটা পাওয়া যায়।
  4. Conway’s law মনে রেখে আগে team সাজান, তারপর strangler fig দিয়ে ধাপে ধাপে ভাগ করুন। আর মনে রাখুন, network call, কঠিন debugging, ভাগ হওয়া data আর বাড়তি operations হলো এই পথের নিয়মিত cost।

১৩ মনে রাখুন