সরাসরি পর্বে যান
পর্ব ৪৭ · ৬৬-এর মধ্যে · মডিউল ৮

Monolith বনাম microservices — service boundary, service discovery আর service mesh

Monolith vs Microservices — Service Boundaries, Service Discovery & Service Mesh

Taught in Bangla, this lesson compares monoliths, modular monoliths and microservices, including the latency and availability cost of chained service calls. It covers Conway's law, bounded contexts and data ownership for drawing service boundaries, spotting a distributed monolith, sync versus async calls, the strangler fig migration, client-side and server-side service discovery, service mesh data and control planes, API versioning and contract tests.

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

  • Monolith, modular monolith আর microservices-এর লাভ-দাম, আর অনেক সেবার call-এ latency ও availability-র হিসাব
  • Conway-এর সূত্র, দলের গঠন, আর domain-driven design-এর bounded context ও aggregate দিয়ে সেবার সীমানা খোঁজা
  • Distributed monolith চেনা, sync বনাম async যোগাযোগ বাছা, আর strangler fig দিয়ে ধাপে ধাপে monolith ভাঙা
  • Client-side ও server-side service discovery, heartbeat-TTL-এর registry, DNS আর Kubernetes-ধাঁচের Service
  • Service mesh-এর data plane ও control plane, API versioning, contract test আর ইন্টারভিউতে সিদ্ধান্ত নেওয়া

১ · মডিউল ৮-এর শুরু — নকশা থেকে চালানোর দিকে

গত সাতটি মডিউলে আপনি টুকরোগুলো চিনেছেন: API, load balancer, database, cache, queue আর distributed system-এর তত্ত্ব। এই মডিউলের প্রশ্ন: সেই টুকরো দিয়ে বানানো আসল সিস্টেম দিনের পর দিন কীভাবে চলবে, বদলাবে আর বিপদে টিকবে? পথটি এরকম:

পর্বপ্রশ্ন
৪৭ (এই পর্ব)সিস্টেম কয় টুকরোয় ভাঙবেন, টুকরোগুলো কীভাবে একে অপরকে খুঁজবে
৪৮ · Resilience patternএকটি সেবা ধীর বা বিকল হলে বাকিরা কীভাবে বাঁচবে
৪৯ · Rate limitingঅতিরিক্ত অনুরোধ থেকে সেবাকে কীভাবে রক্ষা করবেন
৫০ · Observabilityভেতরে কী ঘটছে কীভাবে দেখবেন
৫১ · Disaster recoveryপুরো data center বা region হারালে কী করবেন
৫২ · নকশায় নিরাপত্তাপরিচয়, অনুমতি, এনক্রিপশন আর গোপন চাবি
৫৩ · Deployment আর releaseনতুন কোড কীভাবে নিরাপদে চালু করবেন

সবগুলোর গোড়ায় একটি সিদ্ধান্ত: সিস্টেমটি কি একটি প্রোগ্রাম, না অনেক ছোট প্রোগ্রাম যারা network-এ কথা বলে? এই উত্তরই ঠিক করে পরের পর্বগুলোর সমস্যা কত বড় — দশটি সেবা মানে timeout-retry-র প্রশ্ন দশ গুণ, pipeline-ও দশটি।

একটি বড় রেস্তোরাঁর কথা ভাবুন। এক রকম ব্যবস্থায় একটিই রান্নাঘর: তন্দুর, কাবাব আর মিষ্টির আলাদা জায়গা আছে, কিন্তু সবাই এক ছাদের নিচে, এক ভাঁড়ার থেকে মাল নেয়। আরেক রকম ব্যবস্থা শপিং মলের food court: প্রতিটি দোকান আলাদা মালিকের, নিজের ভাঁড়ার, নিজের সময়ে খোলে-বন্ধ করে। একটি দোকান বন্ধ থাকলেও বাকিরা চলে — কিন্তু একজন অতিথি তিন দোকান থেকে খাবার চাইলে তিনটি লাইনে দাঁড়াতে হয়, আর একটির দেরিতে পুরো প্লেট আটকে থাকে। প্রথমটি monolith, দ্বিতীয়টি microservices — কোনোটিই সব ক্ষেত্রে ভালো নয়।

২ · Monolith — একটি deployable, আর modular monolith কেন শক্ত শুরু

Monolith মানে পুরো অ্যাপ্লিকেশন একটি deployable — একটি প্রোগ্রাম বা package, যা একসাথে build, test আর deploy হয়। অর্ডার, পেমেন্ট, পণ্যের তালিকা, ব্যবহারকারী — সবার কোড একই process-এ চলে, আর সাধারণত একটি database ব্যবহার করে। শব্দটির আসল অর্থ শুধু “একসাথে deploy হয়” — জট পাকানো কোড নয়; ভেতরে পরিষ্কার ভাগ থাকতেই পারে।

Monolith-এর সুবিধাগুলো এত স্বাভাবিক যে চোখে পড়ে না:

  • ডাকা সস্তা: এক module আরেকটিকে ডাকে সাধারণ function call দিয়ে — কয়েক ন্যানোসেকেন্ডের ব্যাপার। একই data center-এ একটি network round trip প্রায় অর্ধ মিলিসেকেন্ড (পর্ব ৩-এর সংখ্যা), অর্থাৎ প্রায় এক লাখ গুণ বেশি — তার ওপর serialization, timeout আর ব্যর্থতার ঝুঁকি।
  • এক transaction: অর্ডার লেখা আর stock কমানো এক database transaction-এ করা যায় — পর্ব ২৪-এর ACID-এর পুরো সুবিধা। Microservices-এ একই কাজে লাগে saga আর outbox (পর্ব ২৫)।
  • সহজ debugging আর operations: একটি stack trace পুরো গল্প বলে; pipeline আর monitoring একটিই।

সমস্যা দেখা দেয় সিস্টেম আর দল বড় হলে। পঞ্চাশজন developer একই কোডে কাজ করলে প্রতিটি deploy-এ অনেকের পরিবর্তন একসাথে যায়; একজনের ভুলে সবার release আটকে থাকে। পণ্য খোঁজার অংশে দশ গুণ বেশি চাপ, অথচ scale করতে হয় পুরো প্রোগ্রাম (পর্ব ১৩)। একটি module-এর memory leak পুরো process বসিয়ে দেয় — পেমেন্টও বন্ধ। আর ভাগের নিয়ম শুধু মুখে থাকলে কয়েক বছরে সবাই সবার ভেতরের function ডাকতে শুরু করে — একে বলে “big ball of mud”.

Modular monolith — এক deploy, কিন্তু শক্ত দেয়াল

মাঝামাঝি পথটি অনেক অভিজ্ঞ প্রকৌশলীর প্রথম পছন্দ: deploy একটিই, কিন্তু module-গুলোর মাঝে নিয়মের দেয়াল শক্ত।

  • প্রতিটি module-এর একটি public interface; অন্যরা শুধু সেটিই ডাকতে পারে, ভেতরের class বা function নয় — build-এর সময় একটি নিয়ম-পরীক্ষক দিয়ে এটি জোর করে মানানো যায়।
  • প্রতিটি module-এর নিজের টেবিল (আলাদা schema); অন্য module-এর টেবিলে সরাসরি SQL নিষেধ।
  • Module-এর মাঝে যোগাযোগ যতটা পারা যায় ঘটনা (event) বা স্পষ্ট interface দিয়ে।

আজ আপনি monolith-এর সরলতা পান; কাল কোনো module-কে আলাদা সেবা বানাতে হলে তার সীমানা আর ডেটা আগেই আলাদা।

মূল ধারণা

Monolith বনাম microservices আসলে দুটি আলাদা প্রশ্ন। (১) কোড আর ডেটা কতটা ভাগ করা (modularity), আর (২) কয়টি আলাদা প্রোগ্রাম আলাদাভাবে deploy হয় (deployment). ভালো ভাগ ছাড়া microservices মানে জট-পাকানো কোড, শুধু এখন network-এর ওপারে। ভালো ভাগসহ monolith অনেক দূর পর্যন্ত যায়।

৩ · Microservices — আলাদা deploy, নিজের ডেটা, আর network-এর দাম

Microservices-এ অ্যাপটি অনেকগুলো ছোট সেবায় ভাগ, যারা network-এ API বা বার্তা দিয়ে কথা বলে। “ছোট” মানে কোডের লাইন নয়; সংজ্ঞার দুটি মূল শর্ত:

  1. আলাদাভাবে deploy করা যায় (independent deployability): পেমেন্ট দল নিজের সেবা আজ দুপুরে বদলাতে পারে, অর্ডার দলকে না জানিয়েই — যতক্ষণ API-র চুক্তি ঠিক থাকে।
  2. নিজের ডেটার মালিক (database per service): প্রতিটি সেবার ডেটা শুধু সে নিজে পড়ে-লেখে; অন্যরা তার API বা event-এর মাধ্যমে জানে।

এই দুই শর্তের একটি ভাঙলে বাকি সুবিধাও ভাঙে। ২০১৪ সালে James Lewis আর Martin Fowler-এর একটি বহুল-পঠিত লেখা নামটি জনপ্রিয় করে, যদিও ধারণাগুলো আগের service-oriented architecture-এর অভিজ্ঞতাতেও ছিল।

Monolith বনাম microservices — একই দোকান, দুই নকশা

One deployable with one database vs services behind a gateway, each with its own database and a sidecar proxy

Monolith বনাম microservices ক · Monolith — একটি প্রোগ্রাম, একটি deploy একটি process-এ সব module অর্ডার পেমেন্ট ক্যাটালগ ইউজার module-এর মাঝে function call · ন্যানোসেকেন্ড একটি database খ · Microservices — নিজের ডেটা, gateway, mesh ক্লায়েন্ট API gateway (পর্ব ১১) অর্ডার পেমেন্ট ক্যাটালগ sidecar proxy sidecar proxy sidecar proxy অর্ডার DB পেমেন্ট DB ক্যাটালগ DB control plane — sidecar-দের নিয়ম ও ঠিকানা
ছবি ৪৭.১ — (ক) Monolith: চারটি module একটি প্রোগ্রামে, একে অপরকে সাধারণ function call-এ ডাকে, ডেটা একটি database-এ। (খ) Microservices: client-এর অনুরোধ API gateway হয়ে সেবায় যায়; প্রতিটি সেবার নিজের database, আর অন্যের ডেটা লাগলে তাকে API বা event-এ জিজ্ঞেস করতে হয়। বেগুনি তীর হলো সেবা থেকে সেবার call, যা দুই sidecar proxy-র মধ্য দিয়ে যায় (অংশ ৮)। Control plane নিয়ম পাঠায়, কিন্তু নিজে অনুরোধ বহন করে না।

আসল লাভ — আর তার শর্ত

দলের স্বাধীনতা

প্রতিটি দল নিজের সেবার মালিক — কোড, ডেটা, deploy, on-call; অন্য দলের release-এর অপেক্ষা নেই। এটিই সবচেয়ে বড় লাভ, আর তা মূলত প্রতিষ্ঠানের (অংশ ৪)।

শর্ত: API-র চুক্তি স্থির

আলাদা deploy আর scaling

ছোট পরিবর্তন ছোট deploy — ঝুঁকি কম, ফেরানো সহজ। খোঁজার সেবায় ৩০টি instance, পেমেন্টে ৪টি — যার যত দরকার।

শর্ত: lock-step deploy নয়

Fault isolation

রিভিউ সেবার memory leak শুধু রিভিউ বসায়; অর্ডার চলতে থাকে, পাতায় শুধু রেটিং দেখায় না। একে বলে fault isolation.

শর্ত: timeout, fallback, async

প্রতিটি লাভের শর্ত আছে। অর্ডার সেবা রিভিউর উত্তরের জন্য timeout ছাড়া অপেক্ষা করলে, রিভিউ ধীর হলে অর্ডারের থ্রেডও আটকে যায় — isolation-এর বদলে cascading failure (প্রতিরক্ষা পর্ব ৪৮-এ)।

আসল দাম

দামকেনকোথায় সমাধান
Network callপ্রতিটি call-এ latency, serialization, আর partial failure — উত্তর না এলে কাজ হয়েছিল কি না জানা যায় নাপর্ব ৩৮, ৪৮
ছড়ানো ডেটাদুই সেবার টেবিলে join বা এক transaction নেই; ডেটা কিছুক্ষণ অমিল থাকেপর্ব ২৫, ৩৫
Operationsদশটি সেবা মানে দশটি pipeline, দশ সেট dashboard, alert, config, certificateপর্ব ৫০, ৫৩
Debuggingএকটি ভুলের কারণ পাঁচটি সেবার log-এ ছড়ানোDistributed tracing, পর্ব ৫০
সংস্করণ মেলানোপ্রতিটি API এখন একটি প্রকাশ্য চুক্তি; বদলাতে হয় পুরোনো client না ভেঙেঅংশ ৯, পর্ব ১২

Network-এর দাম সংখ্যায় — fan-out, tail latency আর composite availability

ধরা যাক একটি অনুরোধ পূরণ করতে N টি সেবাকে ডাকতে হয়, আর প্রত্যেককেই উত্তর দিতে হবে।

Availability — গুণফল. প্রতিটি সেবা স্বাধীনভাবে ai সম্ভাবনায় সচল থাকলে, সবাই একসাথে সচল থাকার সম্ভাবনা গুণফল — এটিই composite availability (পর্ব ৫-এর serial হিসাব)। পরপর ডাকুন বা একসাথে, সবাইকে লাগলে হিসাব একই:

A = a1 × a2 × … × aN = aN (সবার a সমান হলে)
a = ০.৯৯৯, N = ২০ → A ≈ ০.৯৮০২

সহজ আন্দাজ: a = 1 − ε হলে aN ≈ 1 − Nε, অর্থাৎ ব্যর্থতার অংশ মোটামুটি যোগ হয়। প্রতিটির ০.১% বিভ্রাট, ২০টি সেবা → প্রায় ২%, মাসে প্রায় ১৪ ঘণ্টা (একটির ০.১% মানে মাসে প্রায় ৪৩ মিনিট)। উল্টো দিকে, যেকোনো একটি সচল থাকলেই চললে (একই সেবার k টি copy) A = 1 − (1 − a)k — redundancy availability বাড়ায়, শিকল কমায়।

Latency — যোগফল বনাম সবচেয়ে ধীরটি. সেবাগুলোকে পরপর (sequential) ডাকলে মোট সময় সবার যোগফল। একসাথে (parallel) ডাকলে মোট সময় সবচেয়ে ধীর উত্তরের সময়। Parallel দ্রুত, কিন্তু এখানেও একটি ফাঁদ আছে — tail. ধরুন প্রতিটি call ১% সময় ধীর। N টি call-এর অন্তত একটি ধীর হওয়ার সম্ভাবনা

P(অন্তত একটি ধীর) = 1 − 0.99N
N = ৫ → ৪.৯% · N = ১০ → ৯.৫৬% · N = ২০ → ১৮.২১%

অর্থাৎ প্রতিটি সেবার p99-এর ধীরতা, ২০টি fan-out-এ প্রায় প্রতি ৫টি অনুরোধে ১টির অভিজ্ঞতা হয়ে যায় — পর্ব ৪-এর fan-out বৃদ্ধি। নিচের simulator প্রতিটি call-এর latency বানায় সাধারণত ৫–১১ ms, আর ১% ক্ষেত্রে ৫০–২৫০ ms; প্রতিটি সেবা ৯৯.৯% সময় সচল। ২০,০০০টি অনুরোধে N = ১, ৫, ১০, ২০-এর জন্য p50, p99 আর সবাই-সচল অনুরোধের অংশ মাপে।

// One user request calls N services. Each call: ~8 ms usually, 1% slow (50-250 ms),
// and each service is up 99.9% of the time. Sequential = sum, parallel = slowest one.
let seed = 47; function rand() {      // mulberry32: seeded PRNG
  seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const call = () => rand() < 0.01 ? 50 + 200 * rand() : 5 + 6 * rand();   // ms
const pct = (a, p) => a[Math.ceil(p / 100 * a.length) - 1];              // nearest rank
const R = 20000;
console.log(" N" + "seq p50".padStart(17) + "p99".padStart(9) + "par p50".padStart(15) +
  "p99".padStart(9) + "all-up formula".padStart(15) + "simulated".padStart(11));
for (const N of [1, 5, 10, 20]) {
  const seq = [], par = []; let ok = 0;
  for (let r = 0; r < R; r++) {
    let sum = 0, max = 0, up = true;
    for (let i = 0; i < N; i++) {
      const ms = call(); sum += ms; if (ms > max) max = ms;
      if (rand() >= 0.999) up = false;          // this service is down right now
    }
    seq.push(sum); par.push(max); if (up) ok++;
  }
  seq.sort((a, b) => a - b); par.sort((a, b) => a - b);
  const f = x => x.toFixed(0).padStart(5) + " ms";
  console.log(String(N).padStart(2) + "  " + f(pct(seq, 50)).padStart(15) + f(pct(seq, 99)).padStart(9) +
    f(pct(par, 50)).padStart(15) + f(pct(par, 99)).padStart(9) +
    ((0.999 ** N) * 100).toFixed(2).padStart(14) + "%" + (ok / R * 100).toFixed(2).padStart(10) + "%");
}

আউটপুট:

 N          seq p50      p99        par p50      p99 all-up formula  simulated
 1             8 ms    11 ms           8 ms    11 ms         99.90%     99.91%
 5            40 ms   241 ms          10 ms   206 ms         99.50%     99.56%
10            81 ms   306 ms          11 ms   230 ms         99.00%     99.00%
20           162 ms   420 ms          11 ms   240 ms         98.02%     97.97%

Sequential: p50 প্রায় N × ৮ ms (৮ → ৪০ → ৮১ → ১৬২); p99 N = ৫-এই ২৪১ ms, কারণ একটি ধীর হলেই পুরোটা ধীর। Parallel: p50 প্রায় স্থির (৮–১১ ms), কিন্তু p99 তবু ২০৬–২৪০ ms — N = ৫-এ অন্তত একটি ধীর call-এর সম্ভাবনা ৪.৯%, ১%-এর অনেক ওপরে। (N = ১-এ ধীর call প্রায় ঠিক ১%, তাই p99-এর সীমা দ্রুত অংশের কিনারায়, ১১ ms.) Availability: simulation আর 0.999N প্রায় হুবহু মিলেছে — ২০টি সেবায় ৯৮.০২% বনাম ৯৭.৯৭%.

এই সূত্রের অনুমান: সেবাগুলো স্বাধীনভাবে বিকল হয়। বাস্তবে একই database বা একই config-ভুল কয়েকটি সেবাকে একসাথে ফেলে দেয়, তখন হিসাব আশাবাদী। আবার critical path থেকে একটি সেবা সরালে (যেমন নোটিফিকেশন async করা) সেটি গুণফলেই আসে না — নকশার বড় কাজ এটিই ঠিক করা যে কোন call সত্যিই এখনই লাগবে।

৪ · Service boundary — Conway-এর সূত্র, bounded context আর ডেটার মালিকানা

Conway-এর সূত্র — সিস্টেম দলের ছবি আঁকে

১৯৬৮ সালে Melvin Conway একটি প্রবন্ধে (প্রকাশিত হয় Datamation পত্রিকায়, এমনটাই সাধারণত উল্লেখ করা হয়) একটি পর্যবেক্ষণ লেখেন, যা পরে Conway-এর সূত্র নামে পরিচিত হয়: যে প্রতিষ্ঠান একটি সিস্টেম নকশা করে, তার নকশা সেই প্রতিষ্ঠানের যোগাযোগের কাঠামোর মতো হতে বাধ্য। প্রচলিত একটি উদাহরণে: চারটি দল একটি compiler বানালে তাতে চারটি ধাপ (pass) থাকবে। Fred Brooks তাঁর ১৯৭৫ সালের বই The Mythical Man-Month-এ এটিকে “Conway’s law” নামে উল্লেখ করে আরও পরিচিত করেন।

কারণটি সহজ: দুটি module-এর মাঝে interface মানে দুই দলের মাঝে চুক্তি। রোজ একসাথে বসা দুই দলের কোডে সীমানা ঝাপসা হয়; আলাদা বিভাগের দুই দলের মাঝে সীমানা শক্ত হয়ে যায়। তাই microservices-এর সিদ্ধান্ত অনেকটাই প্রতিষ্ঠানের নকশা:

  • পাঁচজনের একটি দল বিশটি সেবা বানালে, প্রত্যেকের মাথায় চারটি সেবার deploy, alert আর on-call — দলের কাঠামোর সাথে সিস্টেমের মিল নেই, তাই ঘর্ষণ।
  • উল্টো দিকে, দশটি দল এক monolith-এ কাজ করলে release-এর দিন সবাই একে অপরের পথে।
  • Inverse Conway maneuver: যে architecture চান, আগে সেই আকারে দল সাজান — প্রতিটি bounded context-এর জন্য একটি দীর্ঘস্থায়ী দল, যারা নিজের সেবার শুরু থেকে শেষ পর্যন্ত মালিক।

২০১৯ সালের একটি প্রভাবশালী বই Team Topologies (Matthew Skelton ও Manuel Pais) এই চিন্তাকে আরও এগিয়ে নেয়: দলের ওপর মানসিক ভার (cognitive load) সীমিত রাখা, আর প্রতিটি সেবার মালিক ঠিক একটি দল। একটি সেবার দুই মালিক মানে কার্যত কোনো মালিক নেই।

সহজ পরীক্ষা: প্রতিটি সেবার পাশে একটি দলের নাম লিখুন। কোনো সেবার দুটি দল, বা কোনো দলের পাঁচটি সেবা? তাহলে সীমানা বা দলের গঠন ভুল।

কোথায় কাটবেন — কাজ ধরে, স্তর ধরে নয়

সবচেয়ে প্রচলিত ভুল হলো প্রযুক্তির স্তর ধরে কাটা — “UI সেবা”, “logic সেবা”, “database সেবা”; তখন প্রতিটি নতুন feature-এ তিনটিই বদলাতে আর একসাথে deploy করতে হয়। কাটতে হয় ব্যবসার কাজ ধরে, যাতে একটি সাধারণ পরিবর্তন একটি সেবাতেই সীমিত থাকে — ভেতরে ঘনিষ্ঠতা (cohesion) বেশি, বাইরে নির্ভরতা (coupling) কম।

Bounded context — একই শব্দ, ভিন্ন অর্থ

২০০৩ সালে Eric Evans-এর বই Domain-Driven Design (DDD) এর জন্য একটি ভাষা দেয়। একটি কাল্পনিক অনলাইন দোকানে “পণ্য” শব্দটি ভাবুন। ক্যাটালগ দলের কাছে পণ্য মানে নাম, ছবি আর বর্ণনা; গুদাম দলের কাছে তাকের নম্বর, ওজন আর stock; দাম দলের কাছে মূল্য, ছাড় আর VAT. তিনটি এক বিশাল “Product” টেবিলে ঠাসলে প্রতিটি দল অন্যদের কলাম নিয়ে ভয়ে থাকে।

Bounded context হলো একটি সীমানা, যার ভেতরে প্রতিটি শব্দের একটিই নির্দিষ্ট অর্থ — দলের আর কোডের একই ubiquitous language. দুই context-এর মাঝে অনুবাদ হয় স্পষ্ট interface-এ; পুরোনো বা বাইরের সিস্টেমের বিদঘুটে মডেল ঠেকাতে মাঝে একটি অনুবাদক-স্তর — anti-corruption layer.

পাইকারি আড়তের খাতা আর খুচরা দোকানের খাতা — দুটিতেই “চাল” লেখা। আড়তের খাতায় চাল মানে বস্তা, ট্রাকের চালান আর মহাজনের বাকি। খুচরা দোকানের খাতায় চাল মানে কেজি, খদ্দেরের নাম আর আজকের দাম। দুই খাতা আলাদাই থাকে; মাঝে শুধু একটি চালান-কাগজ চলে — “৫ বস্তা পাঠানো হলো।” সেই চালানই দুই context-এর মাঝের চুক্তি।

Aggregate — একসাথে বদলাতে হয় এমন জিনিস

Evans-এর আরেকটি ধারণা aggregate: কয়েকটি বস্তুর গুচ্ছ, যার নিয়ম (invariant) একসাথে সত্য রাখতে হয়, তাই যা একটি transaction-এ বদলায়। অর্ডার আর তার লাইনগুলো একটি aggregate (“মোট = লাইনগুলোর যোগফল”); অর্ডার আর গ্রাহকের wallet আলাদা aggregate, তাদের মাঝে নিয়ম রক্ষা করে saga (পর্ব ২৫)।

তাই নিয়ম: একটি aggregate কখনো দুই সেবায় ভাগ হবে না। একটি সেবায় এক বা একাধিক aggregate থাকতে পারে; সাধারণত একটি bounded context একটি সেবা দিয়ে শুরু হয়।

ডেটার মালিকানা — database per service

প্রতিটি ডেটার একজনই মালিক সেবা, যে একাই সেটি লেখে। অন্য সেবার সেই ডেটা লাগলে তিনটি পথ:

  1. API-তে জিজ্ঞেস করা — সর্বশেষ মান, কিন্তু মালিক বিকল হলে আপনিও আটকে যান।
  2. ঘটনা শুনে নিজের কপি রাখা — মালিক প্রকাশ করে “PriceChanged”, আর অর্ডার সেবা নিজের ছোট টেবিলে দাম রাখে (event-carried state transfer, পর্ব ৩২)। দ্রুত আর স্বাধীন, কিন্তু কপি একটু পুরোনো হতে পারে।
  3. রিপোর্টের জন্য আলাদা জায়গা — সব সেবার ঘটনা বা CDC থেকে একটি data warehouse (DBMS কোর্সের data warehousing পর্ব)।

যে পথটি বন্ধ: অন্য সেবার database-এ সরাসরি ঢুকে পড়া বা লেখা। তাতে টেবিলের schema দুই সেবার গোপন চুক্তি হয়ে যায় — একটি কলামের নাম বদলালে অন্যটি ভাঙে, আর কেউ জানেই না কে কী পড়ছে।

৫ · Distributed monolith — দুই জগতের খারাপটুকু, আর sync বনাম async

সীমানা ভুল হলে যা তৈরি হয় তার নাম distributed monolith: দেখতে microservices — অনেক সেবা, অনেক container — কিন্তু আচরণে monolith, শুধু সব call এখন network-এ। Monolith-এর সরলতা নেই, microservices-এর স্বাধীনতাও নেই। চেনার তিনটি প্রধান চিহ্ন:

  • বাচাল call (chatty): একটি অনুরোধে কয়েক ডজন call, আর সংখ্যাটি ডেটার আকারের সাথে বাড়ে — যেমন কার্টের প্রতিটি পণ্যের জন্য আলাদা call (database-এর N+1 সমস্যার network-রূপ)।
  • ভাগের database: একটি টেবিলে কয়েকটি সেবা লেখে। এক সেবার schema বদলালে অন্যদের একসাথে বদলাতে হয়।
  • Lock-step deploy: “পেমেন্ট আর অর্ডার একই দিনে, এই ক্রমে deploy করতে হবে।” আলাদা deploy-ই যদি না হয়, microservices-এর মূল লাভই নেই।

আরও চিহ্ন: লম্বা synchronous শিকল (A → B → C → D), আর সব সেবায় ব্যবসার নিয়মসহ একটি ভাগ করা library.

নিচের কোড একটি কাল্পনিক অনলাইন দোকানের “অর্ডার দেওয়া” অনুরোধের call graph আর কে কোন টেবিলে লেখে তা নিয়ে চিহ্নগুলো গোনে — আগের নকশা আর ঠিক করা নকশা পাশাপাশি। কার্টে ১ থেকে ৮টি পণ্য, ১,০০০টি অর্ডার।

// Made-up shop, "place order": a>b sync call, a>b* one call per cart item, a~b async event
let seed = 8; function rand() {      // mulberry32: seeded PRNG
  seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const designs = {
  before: { edges: "gateway>order order>user order>catalog* order>pricing* pricing>catalog " +
      "order>inventory* order>payment payment>user order>notify",
    writers: { products: "catalog pricing inventory", users: "user payment", orders: "order notify" } },
  after: { edges: "gateway>order order>user order>pricing order>inventory order>payment order~notify",
    writers: { products: "catalog", prices: "pricing", stock: "inventory", users: "user", orders: "order" } },
};
function walk(edges, node, mult, k, depth, acc) {        // count sync network calls
  for (const e of edges.split(" ")) {
    const [a, b] = e.replace("*", "").split(/[>~]/); if (a !== node) continue;
    if (e.includes("~")) { acc.events++; continue; }      // fire-and-forget event
    const m = mult * (e.endsWith("*") ? k : 1);
    acc.calls += m; acc.depth = Math.max(acc.depth, depth + 1); walk(edges, b, m, k, depth + 1, acc);
  }
}
for (const [name, d] of Object.entries(designs)) {
  const runs = [];
  for (let r = 0; r < 1000; r++) {                        // 1000 orders, cart size 1-8
    const acc = { calls: 0, events: 0, depth: 0 }, items = 1 + Math.floor(rand() * 8);
    walk(d.edges, "gateway", 1, items, 0, acc); runs.push(acc);
  }
  const calls = runs.map(a => a.calls).sort((x, y) => x - y), { depth, events } = runs[0];
  const shared = Object.entries(d.writers).filter(([, w]) => w.includes(" "));
  console.log(name + ": calls/request avg " + (calls.reduce((s, x) => s + x) / 1000).toFixed(1) +
    ", max " + calls[999] + ", sync depth " + depth + ", async events " + events);
  if (calls[999] > 10) console.log("  SMELL chatty: per-item calls grow with cart size");
  if (depth >= 3) console.log("  SMELL sync chain of " + depth + " hops: latency and failures add up");
  for (const [t, w] of shared) console.log("  SMELL table '" + t + "' written by " + w + " -> lock-step deploys");
  if (!shared.length && calls[999] <= 10) console.log("  ok: one owner per table, calls stay flat");
}

আউটপুট:

before: calls/request avg 23.1, max 37, sync depth 3, async events 0
  SMELL chatty: per-item calls grow with cart size
  SMELL sync chain of 3 hops: latency and failures add up
  SMELL table 'products' written by catalog pricing inventory -> lock-step deploys
  SMELL table 'users' written by user payment -> lock-step deploys
  SMELL table 'orders' written by order notify -> lock-step deploys
after: calls/request avg 5.0, max 5, sync depth 2, async events 1
  ok: one owner per table, calls stay flat

আগে: প্রতি অর্ডারে call = ৫ + ৪k, যেখানে k কার্টের পণ্যসংখ্যা — প্রতি পণ্যে ক্যাটালগ, দাম, দামের ভেতর থেকে আবার ক্যাটালগ, আর stock. গড়ে ২৩.১, আর ৮টি পণ্যে ৩৭টি network call! শিকলের গভীরতা ৩ (gateway → order → pricing → catalog), আর তিনটি টেবিলে একাধিক লেখক — products টেবিলে তিনটি সেবা। পরে: batch API (পুরো কার্টের দাম একবারে), প্রতিটি টেবিলের একজন মালিক (দাম prices টেবিলে, stock আলাদা), আর নোটিফিকেশন একটি async event. Call স্থির ৫টি, কার্ট যত বড়ই হোক।

সেবার মাঝে কথা — sync না async?

Synchronous call (REST বা gRPC, পর্ব ৮, ৯) সরল: প্রশ্ন, তারপর উত্তর। কিন্তু উত্তর না আসা পর্যন্ত ডাকা সেবাকে সচল থাকতে হয় — অংশ ৩-এর গুণফল এখানেই। Asynchronous বার্তা (পর্ব ৩১-এর queue, পর্ব ৩২-এর pub/sub) সময়ের বাঁধন খুলে দেয়: অর্ডার সেবা “OrderPlaced” প্রকাশ করে চলে যায়, নোটিফিকেশন সেবা বিকল থাকলেও বার্তা broker-এ অপেক্ষা করে — একে বলে temporal decoupling. দাম: ফল সঙ্গে সঙ্গে জানা যায় না, ক্রম আর duplicate সামলাতে হয় (পর্ব ৩৪), debugging কঠিন।

প্রয়োজনসাধারণ পছন্দউদাহরণ
এখনই উত্তর লাগবেSync, ছোট timeout ও fallback-সহকার্টের দাম দেখানো
“এটি ঘটেছে” জানানোAsync eventঅর্ডারের পর SMS, analytics
কয়েক সেবা জুড়ে দীর্ঘ প্রবাহSaga — choreography বা orchestrationপেমেন্ট → stock → ডেলিভারি (পর্ব ২৫, ৩৭)
অন্যের ডেটা বারবার পড়াEvent শুনে নিজের কপিঅর্ডার সেবায় পণ্যের নাম আর দামের কপি

৬ · Strangler fig — monolith থেকে ধীরে ধীরে বের হওয়া

পুরো monolith একবারে নতুন করে লেখা (big-bang rewrite) একটি পরিচিত ফাঁদ: বছরের পর বছর দুটি সিস্টেম চলে, পুরোনোটির অর্ধেক আচরণ লেখা নেই, আর চালুর দিন এক ঝুঁকিতে সব। Martin Fowler ২০০৪ সালের একটি লেখায় বিকল্পটির নাম দেন strangler fig application — অস্ট্রেলিয়ার বনে দেখা এক ধরনের ডুমুর-লতার নামে, যা একটি গাছকে ঘিরে ধীরে ধীরে বেড়ে ওঠে আর শেষে পুরোনো গাছের জায়গা নেয়।

কৌশলের হৃৎপিণ্ড হলো সামনে একটি facade — সাধারণত একটি reverse proxy বা API gateway (পর্ব ১১), যা প্রতিটি পথ (route) কোথায় যাবে ঠিক করে। ব্যবহারকারী একই ঠিকানায় আসেন; পেছনে একটি একটি feature নতুন সেবায় সরে যায়। নিচে একটি কাল্পনিক দোকানের “রিভিউ ও রেটিং” feature বের করা দেখুন।

কাল্পনিক দোকান · facade = API gateway · নতুন সেবা review-svc, নিজের database-সহ
ধাপ
GET /reviews
POST /reviews
সত্যের উৎস
ফেরার পথ
  1. ধাপ ১ — facade. Monolith-এর সামনে একটি gateway বসানো হলো; আপাতত সব route monolith-এই যায়, ব্যবহারকারী কিছুই টের পান না। এরপর প্রতিটি সরানো — আর ফেরানো — শুধু একটি route-এর বদল।
  2. ধাপ ২ — নতুন সেবা, ডেটা কপি। review-svc নিজের database নিয়ে চালু হলো। পুরোনো সারি backfill করে কপি, নতুন লেখা আসছে CDC দিয়ে; সত্যের উৎস এখনো monolith. কিছু পড়া নতুন সেবা থেকেও পড়ে মেলানো হয় — shadow read — কিন্তু ব্যবহারকারী পান monolith-এর উত্তর।
  3. ধাপ ৩ — ৫% পড়া নতুনে। অমিল প্রায় শূন্য হলে gateway GET-এর ৫% নতুন সেবায় পাঠায় — canary-র মতো। লেখা এখনো monolith-এ, তাই ভুল হলে ক্ষতি শুধু কিছু পড়ায়, আর route ০%-এ ফেরালেই শেষ।
  4. ধাপ ৪ — মালিকানা বদল। পড়া ১০০%, তারপর সবচেয়ে সাবধানী ধাপ: লেখাও নতুন সেবায়। এখন review-svc-ই সত্যের উৎস, আর সে “ReviewAdded” event প্রকাশ করে; monolith রেটিং নেয় API বা event থেকে। কিছুদিন উল্টো দিকের sync চালু রাখলে দরকারে ফেরা যায়।
  5. ধাপ ৫ — পুরোনো কোড মোছা। সব ঠিক থাকলে monolith থেকে রিভিউ-এর কোড আর টেবিল (backup রেখে) মোছা হলো। একটি feature “শ্বাসরুদ্ধ”, monolith একটু ছোট। পরের feature-এ একই চক্র। এটি expand–contract-এর বড় রূপ: আগে যোগ, তারপর সরানো, সবার শেষে মোছা।

কোন feature আগে? যার সীমানা পরিষ্কার, জড়ানো কম, আর আলাদা হলে সত্যিকারের লাভ আছে — যেমন আলাদা scaling বা ঘন ঘন বদল। সবচেয়ে জড়ানো, ঝুঁকির অংশ (যেমন পেমেন্টের হিসাব) দিয়ে শুরু করা সাধারণত ভুল। আর facade নিজেও ঝুঁকি — পর্ব ১১-এর SPOF আর “smart gateway”-এর সতর্কতা মনে রাখুন।

৭ · Service discovery — কে এখন কোথায় আছে

Monolith-এ module-রা একই process-এ, খোঁজার প্রশ্নই নেই। Microservices-এ পেমেন্ট সেবার হয়তো ৬টি instance, যারা autoscaling (পর্ব ১৩) আর deploy-এর সাথে জন্মায় আর মরে, প্রতিবার নতুন IP নিয়ে — config-এ ঠিকানা লিখে রাখা অচল। দরকার একটি ব্যবস্থা, যা জানে এই মুহূর্তে কোন instance জীবিত আর কোথায় — service discovery. তার দুটি অংশ: নিবন্ধন (কে আছে) আর খোঁজা (কাকে ডাকব)।

Registry — নিবন্ধন, heartbeat আর TTL

কেন্দ্রে একটি service registry: সেবার নাম থেকে জীবিত instance-এর ঠিকানার তালিকা। প্রায়ই এটি etcd, ZooKeeper বা Consul-এর মতো একটি coordination service-এর ওপর থাকে (পর্ব ৪৩)। নিবন্ধন দুই রকম। Self-registration-এ instance নিজে চালু হয়ে নিজের ঠিকানা লেখে আর নিয়মিত heartbeat দিয়ে একটি lease নবায়ন করে; heartbeat থামলে TTL শেষে entry নিজেই মুছে যায়। Third-party registration-এ platform নিজে (যেমন Kubernetes) instance চালু-বন্ধ দেখে তালিকা হালনাগাদ করে, আর readiness পরীক্ষায় পাশ করলে তবেই তালিকায় তোলে।

TTL বাছাইয়ে পর্ব ৪৪-এর টানাপোড়েন: ছোট TTL-এ মৃত instance দ্রুত বাদ, কিন্তু ধীর heartbeat-এ জীবিতও বাদ পড়তে পারে; বড় TTL-এ উল্টো। আর client তালিকা cache করলে বদল জানে আরও দেরিতে।

heartbeat প্রতি ৫ s · TTL ১৫ s · client প্রতি ১০ s-এ তালিকা পড়ে · প্রতি সেকেন্ডে ১০টি অনুরোধ, round-robin
সময়
Registry
Client-এর তালিকা
মৃত C-তে অনুরোধ
  1. t = ০ — নিবন্ধন। A, B, C নিজেদের ঠিকানা registry-তে লিখল, ১৫ s-এর lease-সহ। Client তালিকা পড়ে round-robin-এ পালা করে ডাকছে।
  2. t = ১২ — নতুন instance. D যোগ দিল, কিন্তু client-এর cache-এ এখনো A, B, C — পরের refresh t = ২০-এ, তাই নতুন instance কিছুক্ষণ অলস।
  3. t = ২০ — C মারা গেল। C-র শেষ heartbeat t = ১৯-এ, তারপর crash. ঠিক এই মুহূর্তে client তালিকা নতুন করে পড়ল — এবং পেল A, B, C, D, কারণ registry-র চোখে C এখনো জীবিত।
  4. t = ২০–৩৪ — ভুল ঠিকানা। প্রতি চারটি অনুরোধের একটি যাচ্ছে মৃত C-তে, প্রতি সেকেন্ডে প্রায় ২.৫টি ব্যর্থতা। Registry অপেক্ষা করছে TTL শেষ হওয়ার।
  5. t = ৩৫ — eviction. নীরবতা ৩৫ − ১৯ = ১৬ s, TTL ১৫ s পেরিয়েছে, তাই registry C-কে মুছল। কিন্তু client এখনো পুরোনো তালিকায় — পরের refresh t = ৪০-এ। Registry watch থাকলে client এখনই জানত।
  6. t = ৪০ — শেষ। Client-এর নতুন তালিকা A, B, D. মোট ২০ সেকেন্ড × ১০ × ¼ = ৫০টি অনুরোধ মৃত instance-এ গেছে। ক্ষতি কমানোর দুই উপায়: registry-র বদল সঙ্গে সঙ্গে জানানো (watch), আর client-এর নিজের চোখ — পরপর ব্যর্থ হলে নিজেই instance-টিকে কিছুক্ষণ বাদ রাখা, অর্থাৎ passive health check.

এবার ঠিক সেই পরিস্থিতি simulator-এ, আর সাথে একটি client যে পরপর ৩টি ব্যর্থতায় instance-টিকে ৩০ s বাদ রাখে:

// Registry: each instance heartbeats every 5 s; an entry expires after 15 s of silence.
// Client: re-reads the registry every 10 s, sends 10 req/s round-robin. C crashes at t=20.
let seed = 4747; function rand() {   // mulberry32: seeded PRNG
  seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const join = { A: 0, B: 0, C: 0, D: 12 }, phase = {}, TTL = 15, CRASH = 20, say = s => console.log("  " + s);
for (const k in join) phase[k] = Math.floor(rand() * 5);   // heartbeat offset 0-4 s
function run(eject) {          // eject: client drops an instance after 3 failures in a row
  const reg = {}, fails = {}, out = {}; let list = [], rr = 0, bad = 0;
  for (let t = 0; t <= 45; t++) {
    for (const k in join) {
      const up = !(k === "C" && t >= CRASH), d = t - join[k];
      if (up && (d === 0 || (d > 0 && d % 5 === phase[k]))) reg[k] = t;   // register / beat
    }
    for (const k in reg) if (t - reg[k] > TTL) { delete reg[k]; say("t=" + t + " registry evicts " + k); }
    if (t % 10 === 0) {
      list = Object.keys(reg).sort();
      if (!eject) say("t=" + t + " registry " + list.map(k => k + "(last " + reg[k] + ")").join(" ") +
        " -> client list " + list.join(","));
    }
    for (let i = 0; i < 10; i++) {     // 10 requests in this second
      const live = list.filter(k => !(out[k] > t)), k = live[rr++ % live.length];
      if (k !== "C" || t < CRASH) { fails[k] = 0; continue; }
      bad++; fails[k] = (fails[k] || 0) + 1;
      if (eject && fails[k] === 3) { out[k] = t + 30; say("t=" + t + " client ejects C after 3 fails"); }
    }
  }
  return bad;
}
console.log("heartbeat offsets " + JSON.stringify(phase));
for (const eject of [false, true]) {
  console.log(eject ? "--- plus passive ejection" : "--- registry + 10 s client refresh");
  say("requests sent to dead C: " + run(eject));
}

আউটপুট:

heartbeat offsets {"A":2,"B":0,"C":4,"D":0}
--- registry + 10 s client refresh
  t=0 registry A(last 0) B(last 0) C(last 0) -> client list A,B,C
  t=10 registry A(last 7) B(last 10) C(last 9) -> client list A,B,C
  t=20 registry A(last 17) B(last 20) C(last 19) D(last 17) -> client list A,B,C,D
  t=30 registry A(last 27) B(last 30) C(last 19) D(last 27) -> client list A,B,C,D
  t=35 registry evicts C
  t=40 registry A(last 37) B(last 40) D(last 37) -> client list A,B,D
  requests sent to dead C: 50
--- plus passive ejection
  t=21 client ejects C after 3 fails
  t=35 registry evicts C
  requests sent to dead C: 3

Heartbeat-এর সময় এলোমেলো, কিন্তু seed স্থির বলে প্রতিবার একই: C বাজায় t = ৪, ৯, ১৪, ১৯-এ, তাই crash-এর পর registry তাকে বাদ দেয় t = ৩৫-এ, আর client জানে t = ৪০-এ — মৃত instance-এ ৫০টি অনুরোধ. Client-এর নিজের passive ejection থাকলে মাত্র ৩টি: t = ২১-এই তৃতীয় ব্যর্থতার পর C বাদ। Registry হলো “কে সম্ভবত আছে”-র ধীর, কেন্দ্রীয় খবর; আসল অনুরোধের ফল দেখে client বা sidecar-এর দ্রুত সিদ্ধান্ত তার পরিপূরক। প্রথম ৩টির জন্য লাগে idempotent অনুরোধে অন্য instance-এ retry (পর্ব ৪৮)।

খোঁজা — client-side বনাম server-side

Client-side discovery

Client নিজে registry থেকে তালিকা আনে, cache করে, আর নিজেই instance বাছে — client-side load balancing (পর্ব ১৪)। মাঝে বাড়তি hop নেই, gRPC-র দীর্ঘ সংযোগে ভালো ভাগ হয়। দাম: প্রতিটি ভাষার প্রতিটি client-এ একই লাইব্রেরি আর যুক্তি।

Client-side: বুদ্ধি client-এ

Server-side discovery

Client একটি স্থির নাম বা ঠিকানায় ডাকে (যেমন payment.internal); মাঝের load balancer বা router registry দেখে instance বাছে। Client সরল, যেকোনো ভাষায় চলে। দাম: বাড়তি একটি hop, আর সেই router নিজে চালু রাখা।

Server-side: বুদ্ধি মাঝখানে

DNS-ভিত্তিক discovery. সবচেয়ে পুরোনো registry হলো DNS (Computer Networks-এর DNS পর্ব)। একটি নামের জন্য কয়েকটি A record, বা host আর port দুটিই জানাতে SRV record. সুবিধা: সব ভাষা আর সব যন্ত্র DNS বোঝে। দুর্বলতা: অনেক resolver, OS বা runtime উত্তর নিজের মতো বেশিক্ষণ cache করে (সংস্করণভেদে আলাদা), তাই মৃত ঠিকানা থেকে যায়; আর সাধারণ DNS জানে না কোন instance অসুস্থ। লেখার সময়ের ডকুমেন্টেশন অনুযায়ী Consul-এর মতো কিছু registry তাই health check-এ পাশ করা instance-গুলোই DNS উত্তরে দেয়।

Kubernetes-ধাঁচের Service. লেখার সময়ের Kubernetes ডকুমেন্টেশন অনুযায়ী, একটি Service object একদল pod-কে একটি স্থির নাম আর একটি স্থির virtual IP (ClusterIP) দেয়। ক্লাস্টারের DNS নামটিকে সেই IP-তে মেলায়, আর প্রতিটি node-এ kube-proxy (বা eBPF-ভিত্তিক বিকল্প) ওই IP-র সংযোগ readiness-এ পাশ করা কোনো একটি pod-এ পাঠায়। অর্থাৎ নিবন্ধন করে platform, আর খোঁজা server-side-এর মতো — “router” প্রতিটি node-এ ছড়ানো। “Headless” Service (ClusterIP ছাড়া) DNS-এ সরাসরি pod-গুলোর IP দেয়, যাতে client নিজে বাছতে পারে। বিস্তারিত network-দিক: Cloud আর container নেটওয়ার্কিং.

# every pod labelled app=payment that passes readiness joins this Service
apiVersion: v1
kind: Service
metadata:
  name: payment            # callers use http://payment (or payment.shop.svc...)
  namespace: shop
spec:
  selector:
    app: payment
  ports:
    - port: 80             # port on the stable virtual IP
      targetPort: 8080     # port the container really listens on
# clusterIP: None  -> "headless": DNS returns the pod IPs, client picks

৮ · Service mesh — sidecar, data plane আর control plane

সেবা যত বাড়ে, প্রতিটির কোডে একই কাজ ফিরে আসে: কাকে ডাকব (discovery), কতক্ষণ অপেক্ষা (timeout), আবার চেষ্টা (retry), এনক্রিপশন আর পরিচয় যাচাই (mTLS), আর প্রতিটি call-এর latency, error ও trace মাপা। পাঁচ ভাষার বিশটি সেবায় এগুলো পাঁচ রকম লাইব্রেরিতে লেখা মানে ভুল আর অমিলের কারখানা।

Service mesh-এর উত্তর: এই কাজগুলো সেবার কোড থেকে বের করে প্রতিটি instance-এর পাশে একটি ছোট proxy-তে দেওয়া — sidecar, যা একই pod বা host-এ চলে। সেবা ভাবে সে সাধারণ HTTP-তে localhost-এ কথা বলছে; বাইরে যাওয়া আর ভেতরে আসা সব traffic network-স্তরের নিয়মে (যেমন iptables) sidecar-এর মধ্য দিয়ে ঘোরানো হয়। Lyft-এ শুরু হয়ে ২০১৬ সালে open-source হওয়া Envoy এমন proxy-র সবচেয়ে পরিচিত উদাহরণ; Istio আর Linkerd পরিচিত open-source mesh (Linkerd নিজের আলাদা proxy ব্যবহার করে)।

  • Data plane: সব sidecar মিলে — যারা আসল অনুরোধ বহন করে, নিয়ম মানে আর মাপ নেয়।
  • Control plane: কেন্দ্রীয় অংশ, যা sidecar-দের পাঠায় instance-তালিকা, route, timeout-retry-র নিয়ম আর সেবা-পরিচয়ের certificate (Envoy এগুলো পায় xDS নামের একদল API দিয়ে)। সে নিজে অনুরোধ বহন করে না; কিছুক্ষণ বিকল থাকলেও sidecar-রা শেষ জানা config-এ চলে, শুধু নতুন বদল আসে না।

অর্ডার সেবা পেমেন্ট সেবাকে টাকা কাটতে বলছে — ধাপে ধাপে দেখুন।

  1. অর্ডার appPOST /chargelocalhost-এ সাধারণ HTTP
  2. বাইরের sidecartimeout 300msinstance বাছাই, mTLS
  3. NetworkTLS bytesঅন্য node-এ
  4. ভেতরের sidecarallow orderপরিচয় ও অনুমতি যাচাই
  5. পেমেন্ট app200 OKআসল কাজ
  1. ধাপ ১ — app-এর সরল call. অর্ডার সেবার কোড শুধু লেখে “http://payment/charge” — TLS, retry বা instance-তালিকা নেই। অনুরোধটি পাশের sidecar-এ ঘুরে যায়।
  2. ধাপ ২ — বাইরের sidecar-এর কাজ। Control plane-এর তালিকা থেকে সে p২ বাছল (সবচেয়ে কম চলমান অনুরোধ), ৩০০ ms-এর timeout আর সংযোগ-ব্যর্থতায় একবার retry-র নিয়ম লাগাল, আর trace-এর header যোগ করল (পর্ব ৫০)।
  3. ধাপ ৩ — mTLS-এ network. দুই sidecar একে অপরকে certificate দেখিয়ে পরিচয় প্রমাণ করল — শুধু সার্ভার নয়, client-ও। বাইটগুলো এনক্রিপ্টেড; data center-এর ভেতরেও “ভেতরে আছে বলে বিশ্বাস” নেই — zero trust, বিস্তারিত পর্ব ৫২-এ।
  4. ধাপ ৪ — ভেতরের sidecar-এর পাহারা। p২-এর sidecar দেখল certificate-এ পরিচয় “order”, আর নিয়মে order-এর POST /charge-এর অনুমতি আছে — অনুরোধ localhost-এ পেমেন্ট app-এ গেল। পরিচয়টি সেবার, IP-র নয় — service identity — তাই pod-এর ঠিকানা বদলালেও নিয়ম টেকে।
  5. ধাপ ৫ — আসল কাজ। পেমেন্ট app টাকা কাটল, ৪০ ms-এ 200 ফেরত দিল — তার কোডেও TLS-এর কোনো ঝামেলা নেই।
  6. ধাপ ৬ — ফেরার পথ আর মাপ। ধরা যাক প্রতিটি sidecar প্রায় আধা মিলিসেকেন্ড যোগ করে: ভেতরের sidecar মাপল ৪০.৪ ms, বাইরেরটি ৪১.১ ms (মাঝের network-সহ)। দুজনেই metric আর span পাঠাল। খেয়াল করুন: control plane পুরো পথে কোথাও ছিল না।

Mesh যা দেয়, আর যা নেয়

দেয়নেয়
সব সেবায় একই mTLS আর সেবা-পরিচয়ভিত্তিক অনুমতি, কোড না বদলে (পর্ব ৫২)প্রতিটি call-এ দুটি বাড়তি proxy-hop — সাধারণত এক মিলিসেকেন্ডের নিচে থেকে কয়েক মিলিসেকেন্ড (পরিবেশ আর config-ভেদে; নিজে মাপুন)
Timeout, retry, outlier বাদ দেওয়া, circuit breaking এক নিয়মে (পর্ব ৪৮)প্রতিটি pod-এ বাড়তি CPU আর memory
প্রতিটি সেবা-জোড়ার latency, error, আর trace-এর header (পর্ব ৫০)আরেকটি জটিল ব্যবস্থা চালানো আর debug করা
Traffic ভাগ — canary, mirror (পর্ব ৫৩)App-এর অর্থ বোঝে না: কোন অনুরোধ retry-নিরাপদ (idempotent), তা proxy জানে না

একটি হিসাব (ধরা যাক, সংখ্যাগুলো উদাহরণ মাত্র): একটি পাতায় ৮টি ভেতরের call পরপর, প্রতিটি দুটি sidecar পেরোয়, প্রতি sidecar ০.৫ ms — বাড়তি latency ৮ × ২ × ০.৫ = ৮ ms. ২০০টি pod-এ প্রতি sidecar ৫০ MB memory নিলে মোট ১০ GB, শুধু proxy-র জন্য। বড় সিস্টেমে এই দাম প্রায়ই পোষায়; কয়েকটি সেবার ছোট সিস্টেমে প্রায়ই না।

Retry-র গুণফল: App-এর লাইব্রেরি ৩ বার চেষ্টা করে, বাইরের sidecar-ও ৩ বার, আর নিচের সেবাও তার নিচের সেবাকে ৩ বার — একটি ব্যর্থ অনুরোধ শেষ স্তরে ৩ × ৩ × ৩ = ২৭টি চেষ্টা হয়ে যেতে পারে। এই retry amplification একটি ধীর সেবাকে বসিয়ে দেয় — retry storm. নিয়ম: retry এক স্তরে, budget-সহ, আর শুধু idempotent অনুরোধে — পর্ব ৪৮.

সাম্প্রতিক কিছু mesh প্রতি pod-এর sidecar-এর বদলে প্রতি node-এ একটি ভাগ করা proxy বা eBPF-এর মতো kernel-স্তরের কৌশল ব্যবহার করে, আর gRPC-র মতো কিছু লাইব্রেরি নিজেই xDS বুঝে proxy ছাড়াই mesh-এ যোগ দিতে পারে — বিবরণ দ্রুত বদলায়, তাই সংস্করণের ডকুমেন্টেশন দেখুন। ভিত্তি একই: data plane বহন করে, control plane নিয়ম দেয়।

৯ · আলাদা deploy-এর নিরাপত্তা-জাল আর সিদ্ধান্ত — versioning, contract test, কখন কোনটি

API versioning আর contract test

আলাদা deploy মানে যেকোনো মুহূর্তে একটি সেবার পুরোনো আর নতুন সংস্করণ একসাথে চলছে, তার client-রাও নানা সংস্করণে। তাই প্রতিটি পরিবর্তনকে backward compatible রাখতে হয় (পর্ব ৮, পর্ব ১২):

  • যোগ নিরাপদ, মোছা বা নাম বদলানো বিপজ্জনক — পুরোনো field রাখুন যতদিন কেউ পড়ে।
  • Client হোক tolerant reader: অচেনা field উপেক্ষা করে, শুধু দরকারিগুলো পড়ে।
  • ভাঙতেই হলে নতুন সংস্করণ (API versioning — /v2 বা header), কিছুদিন দুটিই চালু, ব্যবহার মেপে তারপর পুরোনোটি বন্ধ — expand–contract.

কিন্তু “কেউ এই field পড়ে কি না” জানবেন কীভাবে? এখানে আসে consumer-driven contract. Ian Robinson-এর ২০০৬ সালের একটি প্রবন্ধে বর্ণিত ধারণাটি সহজ: প্রতিটি consumer (যে সেবা ডাকে) একটি চুক্তি লেখে — “আমি এই অনুরোধ পাঠাই, আর উত্তরে এই field-গুলো পড়ি।” Provider-এর (যে সেবা উত্তর দেয়) build-এ সব consumer-এর চুক্তি চালানো হয়; কোনোটি ভাঙলে deploy আটকায়, আসল ব্যবহারকারী টের পাওয়ার আগেই। Pact-এর মতো open-source টুল এ কাজে প্রচলিত। বিশটি সেবা একসাথে চালানো end-to-end পরীক্ষার চেয়ে এটি দ্রুত আর কম ভঙ্গুর, যদিও অল্প কিছু end-to-end পরীক্ষা থাকা তবু ভালো।

Event-এর schema-ও একটি চুক্তি; সেখানে একই কাজ করে schema registry-র compatibility-র নিয়ম (পর্ব ১২)। আর “এখন কি deploy করা নিরাপদ?” প্রশ্নটি pipeline-এর একটি ধাপ হয়ে যায় — পর্ব ৫৩.

কখন কোনটি — ইন্টারভিউতে সিদ্ধান্ত নেওয়া

ইন্টারভিউতে “microservices দিয়ে বানাব” দিয়ে শুরু করা দুর্বল; প্রশ্নকর্তা শুনতে চান কেন. ধারা: requirement (পর্ব ২), তারপর সীমানা, তারপর deploy-এর একক।

সংকেতModular monolith-এর দিকেআলাদা সেবার দিকে
দলএকটি বা দুটি দলঅনেক দল, প্রত্যেকে স্পষ্ট এক অংশের মালিক
Domain-এর জ্ঞাননতুন পণ্য, সীমানা এখনো বদলাচ্ছেসীমানা স্থির, বহুদিনের চেনা
Scaling-এর চাহিদাসব অংশের চাপ মোটামুটি একসাথে বাড়েএকটি অংশের চাপ বা resource বাকিদের থেকে খুব আলাদা
Release-এর গতিসবার একই ছন্দকোনো অংশ দিনে বহুবার, কোনোটি মাসে একবার
বিচ্ছিন্নতাসব অংশের ঝুঁকি একই রকমপেমেন্টের মতো অংশকে আলাদা নিরাপত্তা বা নিয়মের ঘেরে রাখা দরকার
Operations-এর সামর্থ্যCI/CD, monitoring, tracing এখনো কাঁচাপরিণত platform — নতুন সেবা চালু করা সস্তা

ইন্টারভিউতে একটি নমুনা উত্তর: “শুরুতে একটি modular monolith — অর্ডার, ক্যাটালগ আর ব্যবহারকারী আলাদা module ও schema-সহ। দুটি অংশ আলাদা সেবা: মিডিয়া প্রসেসিং, কারণ তার CPU-চাহিদা খুব আলাদা আর কাজটি async; আর পেমেন্ট, কারণ তার নিরাপত্তার ঘের আলাদা। পড়ায় sync, ঘটনায় event; সামনে একটি gateway.” তারপর failure mode: পেমেন্ট ধীর হলে কী হবে? Case study পর্বগুলোতে (৫৪–৬১) এই ধরনের ব্যাখ্যা বারবার লাগবে।

আরও গভীরে: ২০২৩ সালের HotOS-এ Google-এর Sanjay Ghemawat ও সহকর্মীদের একটি paper (“Towards Modern Development of Cloud Applications”) প্রস্তাব করে, কোড লেখা হোক একটি logical monolith হিসেবে আর runtime ঠিক করুক কোন অংশ কোন machine-এ চলবে — অর্থাৎ modularity আর deployment আলাদা করা; এর ভিত্তিতে Service Weaver নামের একটি open-source framework প্রকাশিত হয়েছিল। আর SPIFFE নামের একটি open-source মান সেবা-পরিচয়ের নাম ও certificate-এর সাধারণ রূপ দেয়, যা অনেক mesh অনুসরণ করে বলে জানা যায়।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • Microservices-এর দুটি সংজ্ঞাগত শর্ত কী? আলাদাভাবে deploy করা যায়, আর প্রতিটি সেবা নিজের ডেটার একমাত্র মালিক।
  • ৯৯.৯%-এর ১০টি সেবা পরপর লাগলে composite availability কত? 0.99910 ≈ ৯৯.০০%, মাসে প্রায় ৭ ঘণ্টা বিভ্রাট।
  • Distributed monolith-এর তিনটি চিহ্ন? বাচাল call, কয়েক সেবার একটি টেবিলে লেখা, আর lock-step deploy.
  • Client-side আর server-side discovery-র পার্থক্য? প্রথমটিতে client registry পড়ে নিজে instance বাছে; দ্বিতীয়টিতে মাঝের load balancer বা router বাছে।

১০ · AI-এর যুগে — architecture-এর পরামর্শ যাচাই

“একটি food delivery অ্যাপের architecture চাই” — এমন প্রশ্নে AI প্রায়ই চকচকে তালিকা দেয়: বারোটি microservice, Kafka, Redis, Kubernetes, service mesh. প্রশ্নই করে না — দল কয়জনের? ব্যবহারকারী কত? কোন call critical path-এ? তিনজনের দলের জন্য এটি সমাধান নয়, বোঝা।

AI-এর সাধারণ ভুলকীভাবে ধরবেন
দল আর আকার না জেনেই “microservices + Kafka + Redis + mesh”প্রতিটি সেবার পাশে একটি দলের নাম লিখুন; modular monolith কেন যথেষ্ট নয়, তার যুক্তি চান
সবার জন্য এক database, বা এক টেবিলে কয়েকজনের লেখাপ্রতিটি টেবিলের একজন লেখক-মালিক আছে কি না দেখুন
Availability-র হিসাব ভুল — “প্রতিটি ৯৯.৯%, তাই পুরো সিস্টেম ৯৯.৯%”Critical path-এর সেবাগুলো গুণ করুন: aN
Mesh-এর latency বা পণ্যের সীমা বানিয়ে বলাঅফিসিয়াল ডকুমেন্টেশন দেখুন, আর নিজের পরিবেশে ছোট load test চালান
Failure mode আর consistency উপেক্ষা — ACID আর CAP-এর “consistency” গুলিয়ে ফেলাপ্রতিটি সেবা বিকল হলে কী হবে, আর কোন ডেটা কিছুক্ষণ অমিল থাকতে পারে, লিখে দেখুন (পর্ব ৪০)

AI-কে নকশাকার নয়, সমালোচক হিসেবে ব্যবহার করুন — আর তাকে আপনার সীমাবদ্ধতা স্পষ্ট করে দিন:

আমার একটি কাল্পনিক অনলাইন বইয়ের দোকান আছে: ৬ জনের একটি দল, দিনে প্রায় ২০,০০০ অর্ডার, ঈদের আগে চাপ প্রায় ৫ গুণ। এখন একটি monolith আর একটি PostgreSQL. ১. এই অবস্থায় modular monolith আর microservices — দুটির পক্ষে-বিপক্ষে যুক্তি দাও। দলের আকার আর operations-এর খরচ হিসাবে রাখো। ২. যদি কোনো অংশ আলাদা সেবা করার মতো হয়, কোনটি আর কেন? Bounded context আর ডেটার মালিকানা ধরে ব্যাখ্যা করো। ৩. অর্ডার দেওয়ার critical path-এ কয়টি সেবা থাকবে? প্রতিটি ৯৯.৯% ধরে composite availability হিসাব করো, ধাপসহ। ৪. Monolith থেকে সেই অংশ সরানোর strangler fig পরিকল্পনা দাও, প্রতিটি ধাপে ফেরার পথসহ। ৫. কোনো পণ্যের সীমা বা latency-র সংখ্যা বললে উৎস বলো; নিশ্চিত না হলে “নিশ্চিত নই” লেখো।

AI-এর architecture হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Conway-এর সূত্রকে “সূত্র” বলা হয় কেন — এটি কি শুধু একটি পর্যবেক্ষণ, না নকশার একটি হাতিয়ার?

Melvin Conway ১৯৬৮ সালের প্রবন্ধে যা লেখেন, তা একটি যুক্তির ফল, শুধু অভিজ্ঞতার গল্প নয়। একটি সিস্টেমকে অংশে ভাগ করে আলাদা দলকে দিলে, দুই অংশের মাঝের interface ঠিক করতে দুই দলকে কথা বলতে হয়। যে দলগুলোর মাঝে যোগাযোগ সহজ, তাদের অংশের মাঝে interface ঘন আর ঝাপসা হয়; যাদের মাঝে যোগাযোগ কঠিন, তাদের অংশের মাঝে interface সরু আর শক্ত হয়। ফলে সিস্টেমের কাঠামো প্রতিষ্ঠানের যোগাযোগের কাঠামোর একটি ছবি হয়ে দাঁড়ায়। Fred Brooks ১৯৭৫ সালে এটিকে “Conway’s law” নাম দেওয়ার পর নামটি টিকে যায়।

সূত্রটিকে উল্টো দিক থেকেও ব্যবহার করা যায়, আর এখানেই এটি হাতিয়ার। যদি চান অর্ডার আর পেমেন্ট দুটি আলাদা, স্বাধীন সেবা হোক, তাহলে দুটি আলাদা দল বানান — প্রত্যেকে নিজের অংশের শুরু থেকে শেষ পর্যন্ত মালিক। একে বলা হয় inverse Conway maneuver. উল্টোটা করলে — একটি দলকে দশটি সেবা, বা দশটি দলকে একটি সেবা — সিস্টেম ধীরে ধীরে দলের কাঠামোর দিকে গলে যায়।

২০১৯ সালের Team Topologies বই এই চিন্তায় মানসিক ভারের মাত্রা যোগ করে: একটি দল কতটা জটিলতা মাথায় রাখতে পারে, তার সীমা আছে। প্রতিটি সেবা মানে নিজস্ব deploy, alert, on-call আর ডেটা — ছোট দলের জন্য কয়েকটির বেশি সেবা প্রায়ই সেই সীমা ছাড়ায়।

তাই ইন্টারভিউ বা বাস্তব নকশায় সেবার তালিকা আঁকার পর দলের তালিকা পাশে লিখুন। দুটির আকার না মিললে নকশাটি কাগজে যত সুন্দরই হোক, প্রতিষ্ঠানের ঘর্ষণে টিকবে না।

মূল উপলব্ধি: Architecture আর প্রতিষ্ঠান একই মুদ্রার দুই পিঠ। সেবার সীমানা আঁকা মানে দলের সীমানাও আঁকা — একটিকে বদলালে অন্যটিকেও বদলাতে হয়।

প্র ০২অনেক অভিজ্ঞ প্রকৌশলী নতুন পণ্যে “monolith আগে” বলেন কেন? আর কিছু প্রতিষ্ঠান প্রকাশ্যে microservices থেকে ফিরে এসেছে — এর শিক্ষা কী?

নতুন পণ্যের সবচেয়ে বড় অনিশ্চয়তা হলো ব্যবসার আকার নিজেই। কোনটি আলাদা bounded context, কোন ডেটা কার, কোন অংশে চাপ বাড়বে — প্রথম কয়েক মাসে এগুলো প্রায়ই ভুল আন্দাজ হয়। Monolith-এ ভুল সীমানা ঠিক করা মানে কোড সরানো, যা একটি refactoring; microservices-এ ভুল সীমানা ঠিক করা মানে API বদলানো, ডেটা স্থানান্তর, দুই দলের সমন্বয় আর কয়েকটি deploy-এর ক্রম। Martin Fowler ২০১৫ সালের দিকে “MonolithFirst” নামের একটি লেখায় এই যুক্তি দেন, আর microservices-এর বাড়তি খরচকে বলেন একটি “premium” — যা শুধু যথেষ্ট জটিল সিস্টেমেই পোষায়।

উল্টো দিকের অভিজ্ঞতাও প্রকাশিত। প্রকাশিত engineering blog অনুযায়ী, ২০১৮ সালে Segment নামের একটি প্রতিষ্ঠান লেখে যে তারা শতাধিক ছোট সেবা আবার একটি সেবায় ফিরিয়ে আনে, কারণ প্রতিটি সেবার আলাদা রক্ষণাবেক্ষণ, ভাগ করা library-র সংস্করণ-অমিল আর পরীক্ষার খরচ দলটিকে থামিয়ে দিচ্ছিল। ২০২৩ সালে Amazon Prime Video-র একটি দল লেখে, তাদের একটি নির্দিষ্ট monitoring-সেবাকে ছড়ানো serverless অংশ থেকে একটি process-এ এনে তারা খরচ উল্লেখযোগ্যভাবে কমিয়েছে। দুটিই নির্দিষ্ট প্রেক্ষাপটের গল্প।

শিক্ষাটি “microservices খারাপ” নয়; শিক্ষা হলো, সেবার সীমানা একটি অর্থনৈতিক সিদ্ধান্ত। প্রতিটি network সীমানার দাম আছে — latency, ব্যর্থতা, operations — আর লাভ আসে শুধু যখন সীমানাটি সত্যিই দলের স্বাধীনতা বা আলাদা scaling দেয়। যে দুটি অংশ সবসময় একসাথে বদলায়, তাদের মাঝে network সীমানা শুধু দামই আনে।

Modular monolith এখানে ভালো মধ্যপথ: আজ সীমানা কোডে, কাল দরকার হলে network-এ।

মূল উপলব্ধি: সীমানা আগে কোডে আঁকুন, শিখুন, তারপর যেখানে লাভ প্রমাণিত সেখানে network-এ নিন। আবার একসাথে ফিরিয়ে আনা কোনো লজ্জা নয় — এটিও মেপে নেওয়া সিদ্ধান্ত।

প্র ০৩কয়েকটি সেবা একটি database ভাগ করলে সমস্যা কী? রিপোর্টের জন্য তো কয়েক সেবার ডেটা join করতেই হয় — তাহলে উপায়?

একটি ভাগের টেবিল দুই সেবার মাঝে একটি অদৃশ্য চুক্তি। API-র চুক্তি অন্তত লেখা থাকে, সংস্করণ আর contract test থাকে; টেবিলের কলামের অর্থ বা কোন মান বৈধ — কেউ লিখে রাখে না। অর্ডার দল একটি status-এর মান যোগ করলে পেমেন্ট সেবা রাতে ভেঙে যায়। ফলে schema বদলাতে দুই দলকে একসাথে deploy করতে হয় — lock-step, আলাদা deploy-এর লাভ শেষ।

আরও সূক্ষ্ম সমস্যা আছে। দুই সেবা একই টেবিলে লিখলে নিয়ম (invariant) রক্ষার দায় কার? অর্ডার সেবা stock কমানোর আগে যাচাই করে, কিন্তু গুদাম সেবা সরাসরি একই সারি বদলায় — নিয়ম দুই জায়গায়, তাই কোনো জায়গাতেই নির্ভরযোগ্য নয়। একটি সেবার ভারী query অন্যটির লেখাকে ধীর করে — fault isolation-ও থাকে না।

তাহলে রিপোর্ট? (১) API composition: ছোট প্রশ্নে একটি সেবা বা BFF কয়েকটি সেবাকে ডেকে ফল মেলায় — সহজ, কিন্তু বড় join-এ ধীর। (২) নিজের read model: যে সেবার বারবার অন্যের ডেটা লাগে, সে event শুনে নিজের দরকারি কপি রাখে (CQRS, পর্ব ৩৫)। (৩) Analytics-এর আলাদা জায়গা: সব সেবার event বা CDC-স্রোত একটি data warehouse বা lake-এ যায়, সেখানে ইচ্ছেমতো join — এটি একমুখী, কেউ সেখান থেকে লিখে ফেরত পাঠায় না। রিপোর্ট আর লেনদেনের চাহিদা এমনিতেই আলাদা, তাই এটিই সাধারণ নকশা।

মনে রাখুন: একই database server ভাগ করা আর একই টেবিল ভাগ করা এক নয় — আলাদা schema আর user নিয়ে কয়েকটি সেবা এক PostgreSQL server-এ থাকতেই পারে।

মূল উপলব্ধি: ডেটার মালিকানা হলো microservices-এর মেরুদণ্ড। প্রতিটি ডেটার একজন লেখক, আর বাকিরা API, event বা analytics-এর কপির মাধ্যমে পড়বে।

প্র ০৪Service mesh কি resilience-এর তৈরি সমাধান? Mesh-এ retry আর timeout চালু করলেই কি সিস্টেম নির্ভরযোগ্য হয়ে যায়?

Mesh একটি শক্তিশালী হাতিয়ার, কিন্তু নকশা নয়। প্রথম সমস্যা অর্থের: proxy জানে একটি HTTP অনুরোধ ব্যর্থ হয়েছে, কিন্তু জানে না অনুরোধটি আবার পাঠানো নিরাপদ কি না। “টাকা কাটুন” অনুরোধের উত্তর timeout-এ হারালে, হয়তো টাকা আসলে কাটা হয়ে গেছে; proxy যদি নিজে থেকে আবার পাঠায়, দুবার কাটা হতে পারে। তাই retry-নিরাপত্তা আসে app-এর নকশা থেকে — idempotency key (পর্ব ৮, ৩৪) — mesh-এর config থেকে নয়।

দ্বিতীয় সমস্যা স্তরের: app-এর লাইব্রেরি, sidecar আর নিচের সেবা — প্রত্যেকে নিজের মতো retry করলে চেষ্টার সংখ্যা গুণ হয়। তিন স্তরে তিনবার করে মানে একটি ব্যর্থতায় শেষ সেবায় ২৭টি চেষ্টা; একটি ধীর সেবার ওপর এটি বাড়তি ভিড় চাপিয়ে তাকে পুরো বসিয়ে দেয়। Timeout-এরও একই কথা: ওপরের স্তরের timeout নিচের স্তরের চেয়ে ছোট হলে, নিচের কাজ শেষ হওয়ার আগেই ওপরে হাল ছাড়ে, অথচ নিচের সেবা অকারণে কাজ চালিয়ে যায়। দরকার পুরো পথের একটি সময়-বাজেট (deadline propagation) আর এক জায়গায় retry-এর একটি budget.

আর mesh ব্যবসার অর্থ দেখে না — 200 উত্তরের ভেতরে ভুল দাম থাকলেও proxy খুশি; তার নিজের ভুল config-ও সব সেবাকে একসাথে ফেলতে পারে।

তবু mesh-এর মূল্য বাস্তব: বহু ভাষার বহু সেবায় mTLS আর একই রকম telemetry অন্যভাবে পাওয়া কঠিন। ভালো ব্যবহার: mesh নিয়মের বাহক, আর নিয়মগুলো (কোন অনুরোধে retry, কত timeout) app-এর অর্থ বুঝে নকশা করা — পর্ব ৪৮-এর বিষয়।

মূল উপলব্ধি: Mesh resilience-এর প্রয়োগ সহজ করে, সিদ্ধান্ত নয়। কোন অনুরোধ কতবার, কতক্ষণ, কোন শর্তে — এটি এখনো আপনার নকশার কাজ।

প্র ০৫Microservices কি আপনা থেকেই fault isolation দেয়? একটি monolith-এর তুলনায় একটি সেবা বিকল হলে ক্ষতি কি সবসময় ছোট?

না — fault isolation নকশা করে আনতে হয়, এটি বিনামূল্যে আসে না। Monolith-এ একটি module-এর memory leak বা অসীম loop পুরো process বসিয়ে দেয়, এটি সত্য। আলাদা সেবায় সেই leak শুধু একটি সেবার process-এ থাকে — যদি বাকিরা তার ওপর ঠিকভাবে নির্ভর করে।

উল্টোটা কীভাবে ঘটে দেখুন। অর্ডার সেবা প্রতিটি অনুরোধে রিভিউ সেবাকে synchronous-ভাবে ডাকে, timeout ছাড়া। রিভিউ সেবা ধীর হলো — উত্তর আসতে ৩০ সেকেন্ড। অর্ডার সেবার প্রতিটি থ্রেড এখন রিভিউর অপেক্ষায় বসে; কয়েক সেকেন্ডে থ্রেড-pool খালি, অর্ডার সেবা নতুন অনুরোধ নিতে পারে না। ব্যবহারকারীরা refresh চাপেন, retry বাড়ে — একটি অপ্রয়োজনীয় রেটিং-feature পুরো কেনাকাটা বন্ধ করে দিল। এটাই cascading failure, আর এখানে microservices monolith-এর চেয়েও খারাপ, কারণ সমস্যা বোঝা কঠিন।

অংশ ৩-এর হিসাবও একই কথা বলে: critical path-এ প্রতিটি synchronous সেবা availability-র গুণফলে যোগ হয়। দশটি সেবার শিকলে প্রতিটির ৯৯.৯% মানে পুরোটির প্রায় ৯৯.০০% — একটি monolith-এর ৯৯.৯%-এর চেয়ে কম।

Isolation আনতে কয়েকটি স্তর লাগে: প্রতিটি call-এ ছোট timeout আর fallback (রেটিং না পেলে রেটিং ছাড়াই পাতা দেখানো), dependency-প্রতি আলাদা thread বা connection pool (bulkhead), বারবার ব্যর্থ হলে কিছুক্ষণ ডাকা বন্ধ (circuit breaker), আর যা জরুরি নয় তা async-এ সরানো। এগুলো পর্ব ৪৮-এর বিষয়। Deploy-এও isolation দরকার: ভুল config সব সেবায় একসাথে না পাঠিয়ে canary-তে ধাপে ধাপে (পর্ব ৫৩)।

মূল উপলব্ধি: Microservices ব্যর্থতাকে আলাদা করার সুযোগ দেয়; সুযোগটি কাজে লাগে timeout, fallback, bulkhead আর async নকশায়। এগুলো ছাড়া সেবা যত বেশি, ব্যর্থতার পথও তত বেশি।

অনুশীলন

হাতে হিসাব করুন, তারপর উত্তর মেলান। দরকার হলে ওপরের simulator-গুলোর সংখ্যা বদলে চালিয়ে দেখুন।

  1. হিসাব — critical path: একটি কাল্পনিক টিকিট-অ্যাপে “টিকিট কেনা” অনুরোধ প্রথমে API gateway (availability ৯৯.৯৯%) পেরোয়, তারপর ৬টি সেবাকে পরপর ডাকে, প্রত্যেকের availability ৯৯.৯৫%. (ক) Composite availability কত? (খ) ৩০ দিনের মাসে প্রায় কত মিনিট এই অনুরোধ ব্যর্থ হওয়ার মতো অবস্থা থাকবে? (গ) ৬টির ২টি (নোটিফিকেশন আর loyalty পয়েন্ট) async event-এ সরালে নতুন availability আর মাসিক মিনিট কত? (ঘ) অন্য একটি পাতা ১০টি সেবাকে একসাথে (parallel) ডাকে, আর প্রতিটি call-এর ১% সময় ১০০ ms-এর বেশি লাগে। পাতার কত শতাংশ অনুরোধ ১০০ ms-এর বেশি নেবে?

    (ক) A = 0.9999 × 0.99956. 0.99956 ≈ 0.997004, তাই A ≈ 0.9999 × 0.997004 ≈ 0.996904, অর্থাৎ প্রায় ৯৯.৬৯%.

    (খ) মাসে ৪৩,২০০ মিনিট × (1 − 0.996904) ≈ ১৩৩.৭ মিনিট — দুই ঘণ্টার বেশি। আন্দাজেও কাছাকাছি: ০.০১% + ৬ × ০.০৫% = ০.৩১%.

    (গ) Critical path-এ এখন gateway আর ৪টি সেবা: 0.9999 × 0.99954 ≈ 0.997902, অর্থাৎ প্রায় ৯৯.৭৯%; মাসে প্রায় ৯০.৬ মিনিট। দুটি call পথ থেকে সরিয়ে প্রায় ৪৩ মিনিট বাঁচল — নোটিফিকেশন বিকল হলেও টিকিট কেনা আটকায় না।

    (ঘ) সবগুলো দ্রুত হওয়ার সম্ভাবনা 0.9910 ≈ 0.9044, তাই অন্তত একটি ধীর: 1 − 0.9044 ≈ ৯.৫৬%. প্রতিটি সেবার p99-এর ধীরতা পাতার প্রায় p90-এ উঠে এসেছে।

  2. নকশা সমালোচনা: একটি কাল্পনিক কোচিং-সেন্টার অ্যাপের ৬ জনের দল প্রস্তাব দিল: ১৪টি microservice (student, teacher, class, attendance, fee, payment, sms, email, report, auth, file, search, notice, admin); সব সেবা একটি PostgreSQL database-এর একই টেবিলগুলো ব্যবহার করবে, “যাতে join সহজ হয়”; প্রতিটি অনুরোধে প্রতিটি সেবা auth সেবাকে ডেকে token যাচাই করবে আর student সেবাকে ডেকে নাম আনবে; আর “নিরাপত্তার জন্য” প্রতি শুক্রবার রাতে ১৪টি সেবা একসাথে deploy হবে। সমস্যাগুলো আর একটি ভালো বিকল্প লিখুন।
    • দল বনাম সেবা: ৬ জনে ১৪টি সেবা — প্রত্যেকের মাথায় দুটির বেশি সেবার deploy, alert আর on-call; operations-এর খরচ লাভের চেয়ে অনেক বেশি।
    • ভাগের টেবিল: schema বদলাতে সবাইকে একসাথে বদলাতে হবে — distributed monolith. “Join সহজ” কথাটিই প্রমাণ যে সীমানা আসলে নেই।
    • বাচাল call: প্রতিটি অনুরোধে auth আর student-কে ডাকা — auth বিকল হলে সব বন্ধ, আর latency প্রতি স্তরে যোগ হয়। Token যাচাই প্রতিটি সেবা নিজেই করতে পারে (স্বাক্ষরিত token, পর্ব ১৫ আর ৫২) বা gateway-তে একবার; নাম দরকার হলে event থেকে নিজের কপি।
    • Lock-step deploy: ১৪টি একসাথে মানে আলাদা deploy-এর লাভ শূন্য, আর একটি ভুলে সব ফেরাতে হয় — তাও শুক্রবার রাতে, যখন দেখার লোক কম।
    • ভালো বিকল্প: একটি modular monolith — student-class-attendance, fee-payment, যোগাযোগ (sms, email, notice), রিপোর্ট — চারটি module, প্রত্যেকের নিজের schema আর public interface. SMS আর email একটি async queue-এর worker-এ, যাতে ধীর বাইরের gateway অ্যাপ আটকে না দেয়। পরে সত্যিকারের প্রয়োজনে strangler fig দিয়ে অংশ সরানো। Deploy ছোট, কাজের সময়ে, canary-সহ।
  3. সীমানা খোঁজা: একটি কাল্পনিক খাবার ডেলিভারি অ্যাপের কাজগুলো: রেস্তোরাঁর মেনু ও দাম, গ্রাহকের ঠিকানা, কার্ট, অর্ডার দেওয়া, পেমেন্ট, রাইডার বাছাই, রাইডারের লাইভ অবস্থান, ডেলিভারির অবস্থা, রেটিং, গ্রাহককে SMS. এগুলোকে ৪–৬টি bounded context-এ ভাগ করুন, প্রতিটির ডেটার মালিক বলুন, আর কোন যোগাযোগ sync আর কোনটি async হবে লিখুন।

    একটি যুক্তিসংগত ভাগ (একমাত্র সঠিক উত্তর নেই):

    • ক্যাটালগ — মেনু, দাম, রেস্তোরাঁর খোলা-বন্ধ; খুব বেশি পড়া হয়, তাই cache (পর্ব ২৮)।
    • অর্ডার — কার্ট, অর্ডার, অর্ডারের অবস্থা। অর্ডার আর তার লাইনগুলো একটি aggregate. দাম ক্যাটালগ থেকে sync-এ যাচাই, আর কেনার মুহূর্তের দাম অর্ডারের ভেতরেই লেখা।
    • পেমেন্ট — লেনদেন আর ledger, আলাদা নিরাপত্তা-ঘের; অর্ডার → পেমেন্ট একটি saga-র ধাপ।
    • ডেলিভারি — রাইডার বাছাই, লাইভ অবস্থান, ডেলিভারির অবস্থা। অবস্থানের লেখা অনেক বেশি, scaling-চাহিদা সবচেয়ে আলাদা — আলাদা সেবার সবচেয়ে শক্ত দাবিদার (বিস্তারিত পর্ব ৫৮-এর case study-তে)। “OrderPaid” event শুনে কাজ শুরু।
    • গ্রাহক — প্রোফাইল আর ঠিকানা; অর্ডারের সময় ঠিকানা অর্ডারে কপি হয়।
    • যোগাযোগ আর রেটিং — SMS সম্পূর্ণ async (event শুনে); রেটিং ডেলিভারির পর event থেকে। কোনোটি অর্ডারের critical path-এ নেই।

    খেয়াল করুন, “পণ্যের দাম” দুই জায়গায় দুই অর্থে: ক্যাটালগে আজকের দাম, অর্ডারে কেনার মুহূর্তের দাম — দুটি bounded context-এর দুটি সঠিক মান।

  4. আউটপুট অনুমান করুন: নিচের কোড একটি ছোট consumer-driven contract পরীক্ষা: provider-এর দুটি সংস্করণের উত্তর, আর তিনটি consumer যে যে field পড়ে। প্রতিটি লাইনে কী ছাপা হবে, আগে নিজে লিখুন; তারপর বলুন v2-কে নিরাপদ করতে provider কী বদলাবে (উদাহরণ — এই কোডটি পাতায় চালানো যায় না)।
    // Each consumer's contract lists the response fields it really reads.
    const provider = {
      v1: { id: "o-17", total: 450, status: "PAID" },
      v2: { id: "o-17", amount: 450, status: "PAID", currency: "BDT" },
    };
    const contracts = {
      mobileApp: ["id", "status"],
      invoice:   ["id", "total"],
      analytics: ["id", "total", "currency"],
    };
    for (const v of ["v1", "v2"])
      for (const [consumer, needs] of Object.entries(contracts)) {
        const missing = needs.filter(f => !(f in provider[v]));
        console.log(v + " " + consumer + ": " +
          (missing.length ? "FAIL missing " + missing.join(",") : "PASS"));
      }
    v1 mobileApp: PASS
    v1 invoice: PASS
    v1 analytics: FAIL missing currency
    v2 mobileApp: PASS
    v2 invoice: FAIL missing total
    v2 analytics: FAIL missing total

    (১) v1-এ mobileApp আর invoice-এর সব field আছে। (২) analytics চায় currency, যা v1-এ কখনো ছিল না — ভুল অনুমানটি contract test আগেই ধরিয়ে দিল। (৩) v2-তে total-এর নাম amount হয়েছে: mobileApp ঠিক আছে, কিন্তু invoice আর analytics দুটিই “missing total”. প্রতিকার: expand–contract — v2 কিছুদিন total আর amount দুটিই পাঠাবে; কোনো চুক্তিতে total আর না থাকলে তবেই মোছা। Contract test না থাকলে ভাঙন ধরা পড়ত production-এ, বিল তৈরির সময়।

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

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

ক্রমStrangler fig পদ্ধতিতে monolith থেকে একটি feature সরানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Monolith-এর সামনে একটি facade (gateway) বসানো, সব route আপাতত monolith-এ
  2. নিজের database-সহ নতুন সেবা চালু করা আর পুরোনো ডেটা কপি করা
  3. পড়ার অল্প অংশ নতুন সেবায় পাঠিয়ে তুলনা করা
  4. পুরো traffic আর ডেটার মালিকানা নতুন সেবায় সরানো
  5. Monolith থেকে পুরোনো কোড আর টেবিল মুছে ফেলা

একটি অনুরোধ পূরণ করতে ৮টি সেবাকে পরপর ডাকতে হয়, আর প্রতিটি সেবা স্বাধীনভাবে ৯৯.৯% সময় সচল। পুরো অনুরোধটি সফল হওয়ার মতো অবস্থা মোটামুটি কত শতাংশ সময় থাকে?

  • ৯৯.৯%
  • প্রায় ৯৯.২%
  • প্রায় ৯২%
  • প্রায় ৯৯.৯৯%

নিচের কোনটি “distributed monolith”-এর সবচেয়ে স্পষ্ট চিহ্ন?

  • সেবাগুলো আলাদা আলাদা প্রোগ্রামিং ভাষায় লেখা
  • প্রতিটি সেবার নিজস্ব database আছে
  • কয়েকটি সেবা একই টেবিলে লেখে, আর তাদের একসাথে, নির্দিষ্ট ক্রমে deploy করতে হয়
  • সেবাগুলো কিছু কাজ async event দিয়ে জানায়

Conway-এর সূত্র (১৯৬৮) অনুযায়ী কোনটি সত্য?

  • একটি সিস্টেমের কাঠামো যে প্রতিষ্ঠান সেটি নকশা করে তার যোগাযোগের কাঠামোর মতো হয়
  • দেরি হওয়া প্রকল্পে লোক বাড়ালে প্রকল্প আরও দেরি হয়
  • সেবার সংখ্যা সবসময় দলের সংখ্যার দ্বিগুণ রাখা উচিত
  • প্রতি দুই বছরে সিস্টেমের জটিলতা দ্বিগুণ হয়

Client-side service discovery-তে একটি অনুরোধের জন্য কোন instance ডাকা হবে, সেটি কে বাছে?

  • Service registry নিজে প্রতিটি অনুরোধে
  • মাঝের একটি load balancer
  • DNS server
  • Client নিজে, registry থেকে আনা তালিকা থেকে

একটি service mesh-এ control plane কিছুক্ষণের জন্য বিকল হলো, কিন্তু sidecar proxy-রা চালু আছে। সাধারণত কী ঘটে?

  • সব সেবার মাঝের traffic সঙ্গে সঙ্গে বন্ধ হয়ে যায়
  • Sidecar-রা শেষ জানা config-এ traffic চালিয়ে যায়, কিন্তু নতুন বদল পায় না
  • Sidecar-রা নিরাপত্তার জন্য mTLS বন্ধ করে সাধারণ HTTP-তে চলে
  • App-গুলো sidecar এড়িয়ে সরাসরি একে অপরকে ডাকতে শুরু করে

একটি order সেবা তার উত্তরের total field-এর নাম বদলে amount করতে চায়। কয়েকটি অন্য সেবা এই উত্তর পড়ে। Production-এ পৌঁছানোর আগেই কোন ভাঙন ধরা পড়বে, তা জানার সবচেয়ে নির্ভরযোগ্য উপায় কোনটি?

  • Order সেবার নিজের unit test-এ নতুন field পরীক্ষা করা
  • Service mesh-এ retry-এর সংখ্যা বাড়িয়ে দেওয়া
  • Consumer-driven contract test, যা provider-এর build-এ সব consumer-এর চুক্তি চালায়
  • Deploy-এর পর error log নজরে রাখা

শব্দকোষ

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

মনে রাখুন

  1. Microservices-এর দুই শর্ত: আলাদা deploy আর নিজের ডেটা; নতুন সিস্টেমের শক্ত শুরু modular monolith.
  2. Network-এর দাম সংখ্যায়: সবাইকে লাগলে availability গুণ হয় (aN); sequential-এ latency যোগ হয়, parallel-এ সবচেয়ে ধীরটি, আর fan-out-এ লেজ 1 − 0.99N হারে বাড়ে।
  3. সীমানা আঁকুন ব্যবসার ভাষায় — bounded context আর aggregate; প্রতিটি ডেটার এক লেখক। বাচাল call, ভাগের টেবিল আর lock-step deploy মানে distributed monolith; আর সিস্টেম দলের কাঠামো অনুসরণ করে — Conway-এর সূত্র।
  4. Monolith ভাঙুন strangler fig-এ: facade → নতুন সেবা আর ডেটা কপি → অল্প traffic → মালিকানা বদল → পুরোনো কোড মোছা, প্রতিটি ধাপে ফেরার পথসহ।
  5. Discovery = registry (heartbeat, TTL) বা DNS বা platform-এর Service, client-side বা server-side বাছাইসহ; mesh = sidecar-এর data plane + নিয়মের control plane, যা mTLS, retry, telemetry দেয় বাড়তি hop আর জটিলতার দামে। আলাদা deploy নিরাপদ রাখে backward compatibility আর contract test.

পরের পর্বে: সেবা ভাগ করলেই একটির ধীরতা অন্যটিতে ছড়ানোর পথ খোলে। Timeout, retry, exponential backoff আর jitter, circuit breaker আর bulkhead — বিকল সেবা থেকে বাকি সিস্টেম বাঁচানোর কৌশল।

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

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