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

Log-ভিত্তিক streaming — partition, offset, consumer group আর retention

Log-based Streaming — Partitions, Offsets, Consumer Groups & Retention

Taught in Bangla, this lesson explains the append-only log as a foundation, topics and partitions with ordering only inside a partition, message keys, offsets, consumer groups and rebalancing, when to commit offsets and the at-most-once versus at-least-once trade-off, retention and log compaction, leader, follower, ISR and acks, throughput, consumer lag, partition counts, replay, queue versus log, and CDC feeding search, cache and analytics.

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

  • Log কেন এত সরল অথচ শক্তিশালী একটি data structure, আর database, replication ও streaming কীভাবে একই ধারণায় দাঁড়িয়ে
  • Topic, partition আর message key — কোন key দিলে কোন ক্রম নিশ্চিত, আর partition বাড়ালে কী ভাঙে
  • Offset, consumer group, rebalancing, আর offset commit-এর সময় বাছাইয়ে হারানো বনাম দুবার পাওয়া
  • Retention, log compaction, partition-এর replication (leader, follower, ISR, acks) আর throughput-এর কৌশল
  • Consumer lag মাপা, partition-এর সংখ্যা হিসাব, replay, queue বনাম log, আর CDC দিয়ে search, cache ও analytics হালনাগাদ রাখা

১ · Log — সবচেয়ে সরল data structure, সবচেয়ে বড় ভিত্তি

একটি কাল্পনিক ride-sharing অ্যাপে একটি trip শুরু হলে খবর দরকার ভাড়ার হিসাব, fraud ধরার সার্ভিস, notification, analytics আর search-এর। Trip সার্ভিস প্রত্যেককে আলাদা করে ডাকলে নতুন অংশ যোগ করতেই তার কোড বদলাতে হয়, আর একটি অংশ ধীর হলে পুরো trip আটকে যায়। পর্ব ৩১ — Message queue-এ দেখেছেন, মাঝে একটি সারি বসালে প্রেরক আর গ্রাহক আলাদা হয়। এই পর্বের প্রশ্ন একটু ভিন্ন: একই ঘটনা যদি অনেক দল নিজের গতিতে পড়তে চায়, আর দরকার হলে গতকালের ঘটনাও আবার?

পাড়ার মুদি দোকানের বাকির খাতার কথা ভাবুন। প্রতিটি বিক্রি নতুন লাইনে লেখা হয়, তারিখের ক্রমে; পুরোনো লাইন কখনো কাটা হয় না, ভুল হলে নতুন লাইনে সংশোধন। প্রতিটি লাইনের একটি নম্বর আছে। দোকানি মাস শেষে খাতা থেকে প্রত্যেকের বাকি হিসাব করেন; তাঁর ছেলে আলাদা করে দেখেন কোন পণ্য বেশি বিকোয় — নিজের আঙুল রেখে, নিজের গতিতে। কেউ পড়লে লাইন মুছে যায় না, আর হিসাবে গোলমাল হলে প্রথম পাতা থেকে আবার মেলানো যায়। এই খাতাটিই log.

System design-এ log মানে একটি শুধু-যোগ-করা (append-only), সম্পূর্ণ ক্রমে সাজানো record-এর সারি। এখানে log বলতে অ্যাপের debug message (“error: …”) বোঝানো হচ্ছে না; এটি ডেটার একটি কাঠামো। তিনটি গুণ একে আলাদা করে:

১ · শুধু শেষে যোগ

নতুন record সবসময় শেষে বসে; পুরোনো record বদলায় না। তাই লেখা মানে ডিস্কে টানা লেখা — খুব দ্রুত (পর্ব ১৯)।

append, কোনো update নেই

২ · ক্রম নির্দিষ্ট

প্রতিটি record একটি ক্রমিক নম্বর পায় — offset ০, ১, ২ … যে আগে লেখা হয়েছে, তার নম্বর ছোট। সব পাঠক একই ক্রম দেখেন।

offset = খাতার লাইন নম্বর

৩ · আবার পড়া যায়

পড়লে record মোছে না; প্রতিটি পাঠক নিজের অবস্থান (offset) মনে রাখেন। পেছনে গিয়ে আবার পড়া (replay) সম্ভব, retention-এর সীমা পর্যন্ত।

পাঠক অনেক, খাতা একটি

এই ধারণা নতুন নয়। প্রতিটি database ভেতরে একটি log রাখে — write-ahead log, যা crash-এর পরে ডেটা ফেরায়; বিস্তারিত পর্ব ১৯ — Storage engine-এ। Replication-এ leader সেই log follower-দের পাঠায়, আর follower একই ক্রমে প্রয়োগ করে একই অবস্থায় পৌঁছায় — পর্ব ২২ — Replication-এ। পেছনের নীতিটি সরল: একই শুরুর অবস্থা আর একই ক্রমের একই পরিবর্তন মানে একই শেষ অবস্থা। Log-ভিত্তিক streaming এই ভেতরের log-কে বাইরে এনে সবার জন্য খুলে দেয়।

২০১৩ সালের ডিসেম্বরে LinkedIn-এর engineering blog-এ Jay Kreps একটি দীর্ঘ প্রবন্ধ লেখেন — “The Log: What every software engineer should know about real-time data's unifying abstraction”. তাঁর যুক্তি: database, replication, distributed consensus আর ডেটা পাইপলাইন — সবকিছুর কেন্দ্রে একই জিনিস, একটি ক্রমে সাজানো log. এর আগেই LinkedIn-এ Kreps, Neha Narkhede আর Jun Rao Apache Kafka তৈরি করেন; প্রকাশিত ইতিহাস অনুযায়ী এটি ২০১১ সালে open-source হয়, একই বছর NetDB workshop-এ তাঁদের paper প্রকাশ পায়, আর পরের বছর এটি Apache-র top-level project হয়। খুঁটিনাটি তারিখ উৎসভেদে সামান্য আলাদা হতে পারে।

উদাহরণগুলো মূলত Kafka-ধাঁচের, কারণ এর নামগুলো সবচেয়ে প্রচলিত; ধারণাগুলো সাধারণ (অংশ ৮-এ অন্য সিস্টেম)। Setting-এর নাম, ডিফল্ট আর সীমা সংস্করণভেদে আলাদা — এখানে লেখার সময়ের documentation অনুযায়ী; নকশার আগে নিজের সংস্করণেরটি মিলিয়ে নিন।

২ · Topic আর partition — ক্রম শুধু partition-এর ভেতরে

একটি log একটি মেশিনের ডিস্ক আর নেটওয়ার্কের সীমায় আটকে থাকে। তাই log-কে ভাগ করা হয়। Topic হলো একটি নামওয়ালা stream — যেমন ride-events বা payments. প্রতিটি topic কয়েকটি partition-এ ভাগ, আর প্রতিটি partition নিজেই একটি আলাদা append-only log, নিজের offset ০ থেকে শুরু। Partition-গুলো কয়েকটি সার্ভারে (broker) ছড়ানো, আর প্রতিটির কয়েকটি কপি থাকে (অংশ ৬)। নিচের ছবিতে পুরো চিত্রটি।

Producer → topic-এর partition (replica-সহ) → consumer group

Producers append to partitions; each partition has a leader and followers on different brokers; each consumer group keeps its own offsets

Producer, তিন partition-এর topic আর দুটি consumer group Producer rider app Producer trip service hash(key) mod 3 Topic: ride-events · ৩টি partition P0P1P2 012345 012345 012345 leader: B1follower: B2, B3 leader: B2follower: B3, B1 leader: B3follower: B1, B2 নতুন record ডানে যোগ হয় · ক্রম শুধু একটি partition-এর ভেতরে Group: matching C1C2 P0, P2P1 P0=4 · P1=3 · P2=5 Group: analytics C1 P0, P1, P2 P0=1 · P1=0 · P2=2 (পিছিয়ে) প্রতিটি group নিজের committed offset রাখে, তাই একই ডেটা দুই দল আলাদা গতিতে পড়ে
ছবি ৩৩.১ — দুটি producer key-এর hash দেখে partition বাছে। তিনটি partition-এর leader তিনটি আলাদা broker-এ, তাই লেখার চাপ ভাগ হয়। Group “matching” দুজন consumer-এ partition ভাগ করে নিয়েছে; group “analytics” একজনেই সব পড়ছে আর পিছিয়ে আছে — তাতে matching-এর কিছু যায় আসে না।

Key ঠিক করে partition, আর partition ঠিক করে ক্রম

Producer প্রতিটি record-এর সাথে একটি ঐচ্ছিক key দিতে পারে। Key থাকলে partition ঠিক হয় hash(key) mod N দিয়ে (N = partition-এর সংখ্যা; Kafka-র Java client-এর ডিফল্ট partitioner key-এর murmur2 hash ব্যবহার করে)। ফলে একই key-এর সব record একই partition-এ, লেখার ক্রমেই। Key না থাকলে record-গুলো partition-এ ছড়িয়ে দেওয়া হয় — পুরোনো সংস্করণে পালা করে, নতুনগুলোতে একটি batch ভরা পর্যন্ত একটি partition-এ (“sticky”). তখন সমান ভার মেলে, কিন্তু কোনো ক্রম নেই।

মূল নিয়ম

Log-ভিত্তিক broker ক্রমের নিশ্চয়তা দেয় শুধু একটি partition-এর ভেতরে; দুই partition-এর record-এর মধ্যে কোনটি আগে, তা বলা যায় না। তাই প্রশ্নটি হলো: কোন জিনিসের ঘটনাগুলোর ক্রম ব্যবসার জন্য জরুরি? সেটিকেই key বানান — একটি trip-এর “শুরু → চলছে → শেষ” ক্রমে চাই, তাই key = trip_id; একটি অ্যাকাউন্টের জমা-খরচ ক্রমে চাই, তাই key = account_id.

Key বাছাই আসলে পর্ব ২৩ — Partitioning আর sharding-এর shard key বাছাইয়েরই আরেক রূপ, আর একই ফাঁদগুলো এখানেও আছে:

  • কম ভিন্ন মান: key = city দিলে কয়েকটি মাত্র partition কাজ পায়, আর বড় শহরের partition একাই ভারী — একটি hot partition.
  • একটি key-এ বিশাল চাপ: একটি বড় ব্যবসায়ীর অ্যাকাউন্ট বা ভাইরাল একটি পোস্ট। পর্ব ২৩-এর salting এখানে দামি: key ভাঙলে সেই key-এর ক্রম আর নিশ্চিত থাকে না।
  • Partition বাড়ানো: N বদলালে hash(key) mod N বদলায়, বেশিরভাগ key নতুন partition-এ যায় (পর্ব ২৩ আর ১৬)। পুরোনো record পুরোনো partition-এই থাকে, তাই একই key-এর পুরোনো আর নতুন record দুই partition-এ — ক্রম ভাঙতে পারে। ক্রম জরুরি হলে শুরুতেই যথেষ্ট partition নিন (অংশ ৭-এর হিসাব)।
Topic (কাল্পনিক)ভালো keyকোন ক্রম নিশ্চিত হয়সাবধান
trip-এর ঘটনাtrip_idএকটি trip-এর শুরু, অবস্থান, শেষদুই trip-এর মধ্যে ক্রম নেই — দরকারও নেই
চালকের অবস্থানdriver_idএকজন চালকের অবস্থান সময়ের ক্রমেcity দিয়ে key দিলে hot partition
লেনদেনaccount_idএকটি অ্যাকাউন্টের জমা-খরচদুই অ্যাকাউন্টের transfer দুই partition ছোঁয় (পর্ব ২৫)
click-এর গণনানেই (key ছাড়া)কোনো ক্রম নয়সমান ভার, কিন্তু একই ব্যবহারকারীর click ছড়ানো

৩ · Offset আর consumer group — কে কোন অংশ পড়বেন

Queue-তে broker মনে রাখে কোন বার্তা কে নিল। Log-এ কাজটি অনেক সরল: প্রতিটি consumer শুধু একটি সংখ্যা মনে রাখেন — প্রতিটি partition-এ পরের যে offset পড়তে হবে। Partition P0-তে offset ৪ মানে ০ থেকে ৩ পড়া আর প্রক্রিয়া শেষ। এই সংখ্যাটি broker-এর কাছে জমা রাখাকে বলে offset commit (অংশ ৪)। Kafka-র পুরোনো সংস্করণে এটি ZooKeeper-এ থাকত; এখন থাকে একটি ভেতরের, compacted topic-এ — log দিয়েই log-এর হিসাব।

Consumer group — কাজ ভাগ, কিন্তু partition ভাগ নয়

একজন consumer সেকেন্ডে যত record সামলাতে পারেন, তার চেয়ে বেশি এলে কয়েকজন লাগে। একই কাজের consumer-রা মিলে একটি consumer group, একটি নাম নিয়ে। নিয়মটি কঠোর: group-এর ভেতরে প্রতিটি partition ঠিক একজন consumer পড়েন; একজন কয়েকটি partition পড়তে পারেন, কিন্তু একটি partition দুজনে ভাগ করেন না। এতে দুটি ফল:

  • ক্রম টেকে: একটি partition একজনই ক্রমে পড়েন, তাই একই key-এর record কখনো দুজন consumer একসাথে প্রক্রিয়া করেন না।
  • সর্বোচ্চ parallelism = partition-এর সংখ্যা: ৬টি partition-এ ৮ জন consumer দিলে ২ জন অলস বসে থাকেন। Partition-এর সংখ্যা তাই ভবিষ্যৎ consumer-সংখ্যার ছাদ।

আর আলাদা group একে অপরকে চেনে না: “matching” আর “analytics” দুটি group একই topic পড়ে, প্রত্যেকে নিজের offset নিয়ে — একটি বার্তা প্রতিটি group-এ একবার করে পৌঁছায়। এটিই log-এর ওপর pub/sub আর fan-out; event-driven নকশায় এর ব্যবহার পর্ব ৩২ — Pub/sub আর event-driven architecture-এ।

ব্যাংকের একটি শাখায় তিনটি কাউন্টার; গ্রাহকেরা অ্যাকাউন্ট নম্বরের শেষ অঙ্ক দেখে নির্দিষ্ট কাউন্টারে যান। দুপুরে নতুন একজন কর্মী এলে ম্যানেজার কাউন্টারগুলো আবার ভাগ করেন — কয়েক মিনিট কাজ থামে, তারপর আবার শুরু। নতুন কর্মী খাতা দেখে জানেন আগেরজন কোন টোকেন পর্যন্ত শেষ করেছিলেন। এই আবার-ভাগের নাম rebalance.

Rebalancing — কে কখন partition ছাড়েন, কে নেন

Group-এ কেউ যোগ দিলে, কেউ স্বেচ্ছায় বা heartbeat বন্ধ হয়ে বিদায় নিলে, অথবা topic-এ partition বাড়লে, partition-এর মালিকানা নতুন করে ভাগ হয়। একটি broker এই group-এর coordinator; প্রচলিত (classic) protocol-এ group-এরই একজন consumer নতুন ভাগ হিসাব করে coordinator-কে দেন; নতুন সংস্করণের নতুন protocol-এ এই হিসাব broker নিজেই করে (ভাবনার প্রশ্ন ৩)। পুরোনো ধাঁচে (“eager”) rebalance-এর সময় সবাই নিজের সব partition ছেড়ে দেন, ফলে কিছুক্ষণ পুরো group থেমে থাকে। নতুন ধাঁচে (cooperative বা incremental) শুধু যেসব partition সত্যিই সরবে, সেগুলোই থামে। নিচের stepper-এ একটি group-এ তৃতীয় consumer আসা আর পরে একজনের বিকল হওয়া দেখুন।

topic ride-events · ৩টি partition · group “matching”committed = পরের যে offset পড়তে হবে
কে কোন partition পড়ছেন
C1
C2
C3
Committed
Group-এর অবস্থা
  1. স্থির অবস্থা। দুজন consumer, তিনটি partition: C1 পড়েন P0 আর P2, C2 পড়েন P1. তিনটিতেই committed offset ২ — অর্থাৎ offset ০ আর ১ প্রক্রিয়া শেষ, পরেরটি ২.
  2. C3 যোগ দিতে চান। ভিড় বাড়ায় একটি নতুন consumer চালু হয়ে coordinator-কে জানাল। Coordinator rebalance-এর সংকেত দেয়; বাকিরা পরের heartbeat-এর উত্তরে খবরটি পান।
  3. Eager ধাঁচে সবাই থামেন। C1 আর C2 হাতের কাজ শেষে offset commit করে সব partition ছেড়ে দেন। এই কয়েক সেকেন্ড কোনো record প্রক্রিয়া হয় না — lag বাড়ে।
  4. নতুন ভাগ। Group-এর একজন সদস্য ভাগ হিসাব করেন: C1 → P0, C2 → P1, C3 → P2. P2-এর আগের মালিক C1 যেখানে থেমেছিলেন, C3 শুরু করেন ঠিক সেই committed offset থেকে।
  5. আবার কাজ। C3 P2-এর offset ২ থেকে ৫ পড়ে commit করলেন ৬. C1 P0-এর offset ২ শেষ করে commit করলেন ৩. C2 P1-এর offset ২ প্রক্রিয়া শেষ করেছেন — যেমন একটি SMS পাঠিয়েছেন — কিন্তু commit পাঠানোর আগেই…
  6. C2 বিকল, P1 সরল C1-এর কাছে। নির্দিষ্ট সময় heartbeat না আসায় coordinator C2-কে বাদ দেয়। Group eager হলে এখানেও সবাই কিছুক্ষণ থামতেন; cooperative ধাঁচে শুধু P1 সরে, P0 আর P2 থামে না। C1 P1 শুরু করেন committed offset ২ থেকে — তাই offset ২ দ্বিতীয়বার প্রক্রিয়া হলো, SMS গেল দুবার। Rebalance-এ ডুপ্লিকেট এভাবেই আসে, পরের অংশের বিষয়।

Rebalance কমানোর কৌশল (ভাবনার প্রশ্ন ৩): consumer-কে স্থায়ী পরিচয় দেওয়া (“static membership”), একটি poll-এ বেশি কাজ না নেওয়া, আর cooperative assignment.

নিচের প্রোগ্রামে চারটি trip-এর তিনটি করে অবস্থান-হালনাগাদ (v1, v2, v3) key ধরে ৩টি partition-এ যায়; দুজনের group প্রতি partition থেকে সর্বোচ্চ ২টি পড়ে; তারপর তৃতীয়জন এসে rebalance, আর সবাই committed offset থেকে বাকিটা পড়েন। শেষে যাচাই — প্রতিটি key-এর হালনাগাদ ক্রমে এসেছে কি না। Hash FNV-1a 32-bit; ভাগের নিয়ম সরল round robin (partition p যায় সদস্য p mod n-এর কাছে)।

// A topic with 3 partitions: keyed messages, then a consumer group of 2 -> 3 consumers.
function fnv1a(s) {               // FNV-1a 32-bit hash
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
  return h >>> 0;
}
const P = 3, log = [[], [], []], keys = ["ride-10", "ride-11", "ride-13", "ride-17"];
let rr = 0; const noKey = [];
for (let v = 1; v <= 3; v++)      // every ride sends 3 location updates, in order v1, v2, v3
  for (const k of keys) {
    log[fnv1a(k) % P].push(k + ":v" + v);
    if (k === "ride-10") noKey.push("v" + v + "->P" + (rr % P)); rr++;   // what a key-less round robin would do
  }
log.forEach((m, p) => console.log("P" + p + "  " + m.map((x, o) => o + "=" + x).join("  ")));
console.log("ride-10 without a key: " + noKey.join(" ") + "  (spread out, no order guarantee)");
const committed = [0, 0, 0], seen = {};
function run(members, limit) {    // round-robin assignment: partition p -> member p % n
  for (let i = 0; i < members.length; i++) {
    const mine = [0, 1, 2].filter((p) => p % members.length === i), done = [];
    for (const p of mine) {
      const end = Math.min(log[p].length, committed[p] + limit);
      for (let o = committed[p]; o < end; o++) {
        const [k, v] = log[p][o].split(":"); (seen[k] = seen[k] || []).push(v); done.push("P" + p + "@" + o);
      }
      committed[p] = end;         // commit after processing
    }
    console.log("  " + members[i] + " owns " + mine.map((p) => "P" + p).join(",").padEnd(6) + " read " + (done.join(" ") || "-"));
  }
}
console.log("group of 2 (at most 2 messages per partition):"); run(["c1", "c2"], 2);
console.log("c3 joins -> rebalance; resume from committed offsets P0=" + committed[0] + " P1=" + committed[1] + " P2=" + committed[2]);
run(["c1", "c2", "c3"], 99);
for (const k of keys) console.log(k + " seen " + seen[k].join(" ") + (seen[k].join() === "v1,v2,v3" ? "  in order" : "  OUT OF ORDER"));

আউটপুট:

P0  0=ride-11:v1  1=ride-11:v2  2=ride-11:v3
P1  0=ride-13:v1  1=ride-13:v2  2=ride-13:v3
P2  0=ride-10:v1  1=ride-17:v1  2=ride-10:v2  3=ride-17:v2  4=ride-10:v3  5=ride-17:v3
ride-10 without a key: v1->P0 v2->P1 v3->P2  (spread out, no order guarantee)
group of 2 (at most 2 messages per partition):
  c1 owns P0,P2  read P0@0 P0@1 P2@0 P2@1
  c2 owns P1     read P1@0 P1@1
c3 joins -> rebalance; resume from committed offsets P0=2 P1=2 P2=2
  c1 owns P0     read P0@2
  c2 owns P1     read P1@2
  c3 owns P2     read P2@2 P2@3 P2@4 P2@5
ride-10 seen v1 v2 v3  in order
ride-11 seen v1 v2 v3  in order
ride-13 seen v1 v2 v3  in order
ride-17 seen v1 v2 v3  in order

এক: ride-10 আর ride-17 দুটিই P2-তে, তাই সেখানে ৬টি record, বাকি দুটিতে ৩টি করে — অল্প key-এ সমান ভাগ আশা করা যায় না। দুই: rebalance-এর পরে P2 গেল C1 থেকে C3-এর হাতে, আর C3 শুরু করলেন ঠিক offset ২ থেকে — কিছু বাদ পড়েনি, দুবারও আসেনি, কারণ commit প্রক্রিয়ার পরে আর crash নেই। তিন: P2 দুজন ভিন্ন consumer পড়লেও প্রতিটি key-এর v1, v2, v3 ক্রমে এসেছে — partition একসময় একজনের হাতে, হাতবদল committed offset ধরে। Key না দিলে ride-10-এর হালনাগাদ তিন partition-এ ছড়াত, আর v3 আগে প্রক্রিয়া হতে পারত।

৪ · Offset commit — কখন “পড়েছি” বলবেন

Consumer-এর জীবন একটি চক্র: poll (কয়েকটি record আনা) → প্রক্রিয়া (database-এ লেখা, SMS পাঠানো) → commit (broker-কে জানানো “এই offset পর্যন্ত শেষ”)। Crash হলে নতুন মালিক শুরু করেন শেষ committed offset থেকে। তাই প্রক্রিয়া আর commit-এর মাঝে যা ঘটে, সেটিই নির্ধারণ করে বার্তা হারাবে, না দুবার আসবে।

Auto commit

Client নিজেই পেছনে নির্দিষ্ট বিরতিতে (Kafka-র Java client-এ ডিফল্ট সাধারণত ৫ সেকেন্ড) সর্বশেষ poll করা offset commit করে। সহজ, কিন্তু commit আর প্রক্রিয়ার সম্পর্ক আপনার হাতে নেই।

ঝুঁকি: দুই দিকেই

Manual commit

আপনি ঠিক করেন কখন commit — সাধারণত প্রক্রিয়া শেষ হওয়ার পরে। Synchronous commit নিশ্চিত কিন্তু ধীর; asynchronous দ্রুত, কিন্তু ব্যর্থ হলে আবার চেষ্টায় সাবধান।

প্রচলিত: প্রক্রিয়ার পরে

Auto commit-এর দুটি জানালা: commit হয়ে গেছে কিন্তু প্রক্রিয়া বাকি — crash-এ record হারায় (যেমন record অন্য thread-এ পাঠিয়ে পরের poll ডাকলে; Java client-এর documentation অনুযায়ী প্রতিটি poll-এর সব record পরের poll-এর আগে শেষ করলে auto commit at-least-once দেয়); প্রক্রিয়া শেষ কিন্তু commit বাকি — crash-এ শেষ কয়েক সেকেন্ডের record আবার আসে। নিচের প্রোগ্রাম দুটি নীতি পাশাপাশি চালায়: offset ০ থেকে ৯, প্রতিবার ৪টি poll, আর offset ৫-এ একবার crash.

// When to commit the offset? One partition, offsets 0..9, polls of 4, one crash mid-batch.
const LOG = 10, BATCH = 4, CRASH_AT = 5;     // the consumer dies while handling offset 5

function consume(mode) {
  let committed = 0, crashed = false; const effects = [];
  while (committed < LOG) {
    const from = committed, to = Math.min(LOG, from + BATCH);   // poll: next batch from the committed offset
    if (mode === "before") committed = to;                     // commit first, then process
    let died = false;
    for (let o = from; o < to; o++) {
      if (!crashed && o === CRASH_AT) { crashed = died = true; break; }   // process dies, memory is lost
      effects.push(o);                                         // the side effect, e.g. send an SMS
    }
    if (died) { console.log("  crash at offset " + CRASH_AT + "; restart from committed offset " + committed); continue; }
    if (mode === "after") committed = to;                      // commit only after the whole batch is done
  }
  const count = {}; effects.forEach((o) => (count[o] = (count[o] || 0) + 1));
  const lost = [], dup = [];
  for (let o = 0; o < LOG; o++) { if (!count[o]) lost.push(o); else if (count[o] > 1) dup.push(o); }
  console.log("  effects: " + effects.join(" "));
  console.log("  lost: [" + lost.join(" ") + "]  duplicated: [" + dup.join(" ") + "]");
}
console.log("commit BEFORE processing (at-most-once):"); consume("before");
console.log("commit AFTER processing (at-least-once):"); consume("after");

আউটপুট:

commit BEFORE processing (at-most-once):
  crash at offset 5; restart from committed offset 8
  effects: 0 1 2 3 4 8 9
  lost: [5 6 7]  duplicated: []
commit AFTER processing (at-least-once):
  crash at offset 5; restart from committed offset 4
  effects: 0 1 2 3 4 4 5 6 7 8 9
  lost: []  duplicated: [4]

আগে commit: দ্বিতীয় poll-এ ৪ থেকে ৭ আনা হলো, আর সাথে সাথে commit ৮. Offset ৪ শেষ, ৫-এ crash; নতুন শুরু ৮ থেকে — ৫, ৬, ৭ চিরতরে হারাল। এটি at-most-once: কোনো record দুবার নয়, কিন্তু কিছু একবারও নয়। পরে commit: ৪ থেকে ৭-এর batch শেষ হওয়ার আগে crash, তাই commit তখনও ৪; নতুন শুরু ৪ থেকে — offset ৪-এর effect দুবার। এটি at-least-once: কিছু হারায় না, কিন্তু কিছু দুবার। ডুপ্লিকেটের সংখ্যা নির্ভর করে crash-এর আগে batch-এর কতগুলো শেষ হয়েছিল তার ওপর — বড় batch মানে বড় জানালা।

Log কোনো জাদুতে “ঠিক একবার” দেয় না। বেশিরভাগ সিস্টেম বেছে নেয় প্রক্রিয়ার পরে commit, আর ডুপ্লিকেট সামলায় consumer-এর দিকে: প্রতিটি record-এর একটি অনন্য id মনে রেখে দ্বিতীয়বার এলে বাদ দেওয়া — idempotent consumer. আরেকটি কৌশল: ফলাফল আর offset একই database transaction-এ লেখা, তাহলে দুটি একসাথে টেকে বা একসাথে হারায়। Kafka ০.১১ সংস্করণ (২০১৭) থেকে idempotent producer আর transaction দিয়ে “read → process → write”-এর পুরোটা Kafka-র ভেতরে থাকলে exactly-once সম্ভব; কিন্তু বাইরের SMS বা payment-এ তা আপনাআপনি পৌঁছায় না। পুরো আলোচনা পর্ব ৩৪ — Delivery semantics-এ।

একটি বিষাক্ত record: log-এ ক্রম আছে, তাই offset ৫ বারবার ব্যর্থ হলে consumer সেখানেই আটকে থাকেন, পুরো partition পিছিয়ে পড়ে। প্রচলিত সমাধান: কয়েকবার চেষ্টার পরে record-টি আলাদা একটি retry বা dead letter topic-এ লিখে offset এগিয়ে নেওয়া (পর্ব ৩১-এর dead letter queue-এর log-রূপ)। দাম: সেই key-এর পরের record আগে প্রক্রিয়া হবে — ক্রম জরুরি হলে সেগুলোও আটকে রাখতে হয়।

৫ · Retention আর log compaction — খাতা কতদিন রাখবেন

Log শুধু বাড়তেই থাকে, তাই পুরোনো record মোছার নিয়ম লাগে। Partition-এর log ডিস্কে কয়েকটি ফাইলে ভাগ থাকে — প্রতিটি একটি segment; নতুন লেখা যায় শুধু শেষের active segment-এ। দুটি নীতি প্রচলিত:

সময় বা আকার ধরে মোছা

পুরো segment-এর সব record নির্দিষ্ট সময়ের (Kafka-তে ডিফল্ট সাধারণত ৭ দিন) চেয়ে পুরোনো হলে, বা partition নির্দিষ্ট আকার ছাড়ালে, সবচেয়ে পুরোনো segment পুরোটাই মোছা হয়। এক এক করে record মোছা হয় না — তাই খুব সস্তা।

retention: delete

Key ধরে compaction

প্রতিটি key-এর শুধু সর্বশেষ record রাখা হয়, পুরোনো মান মোছা হয় — সময় যতই যাক। Log তখন একটি table-এর পুরো বর্তমান অবস্থা বহন করে।

retention: compact

Retention নকশার সিদ্ধান্ত। একটি consumer group retention-এর চেয়ে বেশি সময় বন্ধ থাকলে তার committed offset-এর record আর নেই — তখন setting অনুযায়ী সে সবচেয়ে পুরোনো বা সবচেয়ে নতুন record থেকে শুরু করে, আর মাঝের ডেটা হারায়। তাই retention হওয়া উচিত consumer-এর সম্ভাব্য দীর্ঘতম বিরতি আর replay-এর প্রয়োজন — দুটির বড়টির চেয়ে বেশি; দাম ডিস্কে (অনুশীলন ৪)।

Log compaction — প্রতিটি key-এর শেষ মান

ধরুন একটি topic-এ প্রতিটি অ্যাকাউন্টের balance বদলালেই একটি record আসে: key = অ্যাকাউন্ট, value = নতুন balance. এক মাস পরে নতুন একটি সার্ভিসের দরকার সব অ্যাকাউন্টের বর্তমান balance — মাঝের লাখো পুরোনো মান নয়। Compaction ঠিক এই কাজ করে: পেছনের একটি cleaner closed segment-গুলো পড়ে প্রতিটি key-এর সর্বশেষ offset খুঁজে নেয়, তারপর শুধু সেগুলো রেখে নতুন segment লেখে। মুছতে চাইলে value ফাঁকা (null) রেখে একটি record পাঠাতে হয় — একটি tombstone; নির্দিষ্ট সময় পরে সেটিও সরে। পর্ব ১৯-এর LSM-tree compaction-এর সাথে মিলটি স্পষ্ট — দুটোই “শেষ মান রাখা, পুরোনো ফেলা”, তবে এখানে offset বদলায় না।

topic account-balances · key = অ্যাকাউন্ট (A, B, C, D)DEL = tombstone (value null)
Partition-এর log (offset:key=value)
Closed segment
Active segment
Cleaner-এর map
মোট record
  1. শুরুর log. চারটি অ্যাকাউন্টের দশটি record. A-এর মান চারবার বদলেছে, B-এর তিনবার; offset ৬-এ একটি tombstone দিয়ে C মোছা হয়েছে। Offset ০ থেকে ৭ বন্ধ (closed) segment-এ, আর ৮ ও ৯ এখনো active segment-এ — cleaner active segment ছোঁয় না।
  2. Map তৈরি। Cleaner closed অংশ পড়ে প্রতিটি key-এর সর্বশেষ offset লিখে রাখে: A → ৫, B → ৪, C → ৬, D → ৭. A-এর আরও নতুন মান offset ৯-এ থাকলেও সেটি active segment-এ, তাই এই হিসাবে আসে না।
  3. শুধু শেষ মান রেখে কপি। Offset ০, ১, ২, ৩ বাদ — প্রতিটির key-এর নতুনতর record আছে। থাকল ৪, ৫, ৬, ৭; সাথে active-এর ৮, ৯. Record ১০ থেকে ৬. খেয়াল করুন, offset বদলায়নি — মাঝে ফাঁক তৈরি হয়েছে মাত্র।
  4. নতুন consumer শুরু থেকে পড়লেন। তিনি প্রথম record পান offset ৪-এ, ০ থেকে ৩ না থাকায় কোনো ভুল হয় না। পুরোটা পড়ে তাঁর table: A = ৬৫০, B = ৩০০, D = ৫০, আর C নেই — পুরো log পড়লেও ঠিক এই ফলই হতো।
  5. Segment বন্ধ, দ্বিতীয় দফা। নতুন record আসায় ৮ আর ৯-এর segment বন্ধ হলো। নতুন map: A → ৯, B → ৮. এবার ৪ (B = ২৬০) আর ৫ (A = ৭০০) বাদ। থাকল ৬, ৭, ৮, ৯.
  6. Tombstone-ও সরল। Tombstone নির্দিষ্ট সময় (Kafka-তে ডিফল্ট সাধারণত এক দিন) রাখা হয়, যাতে পিছিয়ে থাকা consumer-রাও “C মোছা হয়েছে” খবরটি পান; তারপর সেটিও বাদ। শেষে ৩টি record — প্রতিটি জীবিত key-এর একটি। যে consumer এই সময়ের চেয়ে বেশি বন্ধ ছিলেন, তিনি C-এর মোছা কখনো দেখবেন না — তাঁর কাছে C-এর পুরোনো মান থেকে যাবে।

নিচের প্রোগ্রাম একই কাজ করে আর শেষে যাচাই করে: পুরো log আর compacted log — দুটি থেকে শুরু থেকে পড়ে table বানালে ফল এক কি না।

// Log compaction: keep only the latest value per key; offsets never change.
// A compacted topic of account balances (key = account, value = balance; null = deleted).
const log = [["acc-A", 500], ["acc-B", 200], ["acc-A", 450], ["acc-C", 900], ["acc-B", 260],
             ["acc-A", 700], ["acc-C", null], ["acc-D", 50], ["acc-B", 300], ["acc-A", 650]]
  .map(([key, value], offset) => ({ offset, key, value }));

const show = (title, recs) => console.log(title.padEnd(8) + recs.map((r) =>
  r.offset + ":" + r.key.slice(4) + "=" + (r.value === null ? "DEL" : r.value)).join(" "));
// activeFrom: offsets from here are in the active segment, which the cleaner never touches
function compact(recs, activeFrom, dropTombstones) {
  const latest = {};                                  // key -> latest offset in the closed segments
  for (const r of recs) if (r.offset < activeFrom) latest[r.key] = r.offset;
  return recs.filter((r) => r.offset >= activeFrom ||
    (r.offset === latest[r.key] && !(dropTombstones && r.value === null)));
}
function state(recs) {                                // replay from the start into a table
  const t = {};
  for (const r of recs) { if (r.value === null) delete t[r.key]; else t[r.key] = r.value; }
  return Object.keys(t).sort().map((k) => k + "=" + t[k]).join(" ");
}
show("before", log);
const c1 = compact(log, 8, false);    // first pass: offsets 8-9 still in the active segment
const c2 = compact(c1, 10, true);     // later: segment rolled, tombstone retention has passed
show("pass 1", c1);
show("pass 2", c2);
console.log("records: " + log.length + " -> " + c1.length + " -> " + c2.length +
  "; offsets kept: " + c2.map((r) => r.offset).join(","));
console.log("replay full log : " + state(log));
console.log("replay compacted: " + state(c2));

আউটপুট:

before  0:A=500 1:B=200 2:A=450 3:C=900 4:B=260 5:A=700 6:C=DEL 7:D=50 8:B=300 9:A=650
pass 1  4:B=260 5:A=700 6:C=DEL 7:D=50 8:B=300 9:A=650
pass 2  7:D=50 8:B=300 9:A=650
records: 10 -> 6 -> 3; offsets kept: 7,8,9
replay full log : acc-A=650 acc-B=300 acc-D=50
replay compacted: acc-A=650 acc-B=300 acc-D=50

প্রথম দফায় active segment-এর ৮, ৯ ছোঁয়া হয়নি, তাই A-এর ৫ আর B-এর ৪ টিকে গেছে; দ্বিতীয় দফায় (stepper-এর ধাপ ৫ আর ৬ একসাথে) সেগুলো আর tombstone সরেছে। দশটি থেকে তিনটি record, অথচ শেষ দুই লাইন হুবহু এক — compaction ইতিহাস হারায়, বর্তমান অবস্থা নয়। তাই একটি নতুন cache বা সার্ভিস compacted topic থেকে পুরো অবস্থা নিয়ে শুরু করতে পারে; কিন্তু “গত মাসে A-এর balance কত ছিল”-র উত্তর আর নেই। পূর্ণ ইতিহাস লাগলে compaction নয় — পর্ব ৩৫ — Event sourcing আর CQRS দেখুন।

৬ · Partition-এর replication — leader, follower, ISR আর acks

একটি broker-এর disk নষ্ট হলে তার partition-এর ডেটা শেষ। তাই প্রতিটি partition-এর কয়েকটি কপি (replication factor, সাধারণত ৩), আলাদা broker-এ, সম্ভব হলে আলাদা zone-এ। একটি কপি leader — সব লেখা আর সাধারণত সব পড়া তার কাছে; বাকিরা follower, leader থেকে নতুন record টেনে আনে — পর্ব ২২-এর leader-follower replication, partition ধরে।

দুটি শব্দ জরুরি। ISR (in-sync replicas): যেসব কপি leader-এর সাথে তাল রাখছে — নির্দিষ্ট সময়ের বেশি পিছিয়ে পড়লে একটি follower ISR থেকে বাদ পড়ে, আবার ধরে ফেললে ফেরে। High watermark: যে offset পর্যন্ত record ISR-এর সব কপিতে পৌঁছেছে; consumer শুধু এর আগ পর্যন্ত পড়তে পান। ফলে leader বদলালেও consumer এমন কিছু দেখেননি যা পরে উধাও হতে পারে। নিচের stepper-এ একটি record-এর পুরো যাত্রা — producer থেকে consumer পর্যন্ত।

  1. Producerkey=ride-10hash → P2
  2. Leader B3P2 offset 6শেষে append
  3. Follower B1, B2fetchকপি টেনে আনে
  4. Consumer C1group matchingHW পর্যন্ত পড়ে
P2-এর অবস্থা (LEO = পরের লেখার offset)
Producer
Leader B3
Follower B1
Follower B2
High watermark
C1-এর committed
  1. Produce আর partition বাছাই। Producer record-টিকে byte-এ রূপ দেয় (schema-সহ, পর্ব ১২), key ride-10-এর hash থেকে partition P2 বের করে, আর কয়েক millisecond অপেক্ষা করে একই partition-এর আরও record জমায় — একটি batch. P2-এর leader broker B3; তিনটি কপিতেই এখন offset ০ থেকে ৫, তাই LEO ৬.
  2. Leader-এ append. Batch পৌঁছাল B3-এ; leader record-টি log-এর শেষে offset ৬-এ লিখল, LEO হলো ৭. কিন্তু high watermark এখনো ৬ — follower-দের কাছে কপি না পৌঁছানো পর্যন্ত consumer offset ৬ দেখবেন না।
  3. Follower কপি টানে। B1 পরের fetch-এ offset ৬ পেয়ে নিজের log-এ লিখল; B2 একটু ধীর, এখনো ৬-এর আগে। দুটিই ISR-এ, কারণ B2 এখনো অনুমোদিত সময়ের ভেতরে পিছিয়ে আছে।
  4. সবার কাছে পৌঁছাল — ack. B2-ও offset ৬ পেল। ISR-এর সবার কাছে record আছে, তাই high watermark হলো ৭, আর acks=all-এ এখনই producer সফলতার খবর পায়। acks=1 হলে ধাপ ২-এই ack যেত; acks=0 হলে producer কোনো উত্তরের অপেক্ষাই করত না।
  5. Consumer পড়লেন। Group matching-এ P2-এর মালিক C1; তাঁর committed offset ৬, তাই fetch করে offset ৬ পেলেন — high watermark ৭-এর আগ পর্যন্ত। প্রক্রিয়া চলছে (যেমন চালকের অবস্থান হালনাগাদ)।
  6. Commit. প্রক্রিয়া শেষে C1 offset ৭ commit করলেন — “পরের পড়া ৭ থেকে”. এই commit-ও একটি ভেতরের topic-এ লেখা হয়, একইভাবে replicate হয়ে। এখন যে-ই P2-এর মালিক হোন, শুরু করবেন ৭ থেকে।
Producer-এর acksAck কখনLatencyকখন ডেটা হারাতে পারে
acks=0কখনো না — পাঠিয়েই শেষসবচেয়ে কমনেটওয়ার্ক বা leader-এর যেকোনো সমস্যায়; producer জানবেও না
acks=1Leader নিজের log-এ লেখার পরেকমFollower কপি নেওয়ার আগেই leader মারা গেলে, আর নতুন leader-এ সেই record না থাকলে
acks=allISR-এর সবার কাছে পৌঁছানোর পরেসবচেয়ে বেশি (সবচেয়ে ধীর ISR কপি)ISR-এর সব কপি একসাথে হারালে; ISR যদি শুধু leader-এ নেমে আসে, তাহলে এটি কার্যত acks=1

শেষ সারির ফাঁদটি গুরুত্বপূর্ণ: ISR-এ শুধু leader থাকলে acks=all মানে “শুধু leader লিখেছে”. তাই একটি সঙ্গী setting লাগে — min.insync.replicas: ISR এর চেয়ে ছোট হলে broker লেখা প্রত্যাখ্যান করে। প্রচলিত টেকসই সমন্বয়: replication factor ৩, min.insync.replicas=2, acks=all — একটি broker বিকল হলেও লেখা চলে, দুটি বিকল হলে লেখা থামে কিন্তু নিশ্চিত হওয়া ডেটা হারায় না। এটি একটি সচেতন বাছাই: availability-র বদলে durability; CAP-এর ভাষায় এই আলোচনা পর্ব ৪০-এ। Kafka-র লেখার সময়ের documentation অনুযায়ী ৩.০ সংস্করণ থেকে producer-এর ডিফল্ট acks=all, আগে ছিল ১ — পুরোনো client-এ নিজে যাচাই করুন।

Leader মারা গেলে: cluster-এর controller ISR-এর একটি follower-কে নতুন leader করে। ISR-এর বাইরের, পিছিয়ে থাকা কপিকে leader বানানো (unclean leader election) partition দ্রুত চালু করে, কিন্তু নিশ্চিত record-ও হারাতে পারে — সাধারণত এটি বন্ধ থাকে (ভাবনার প্রশ্ন ৪)। Controller আর metadata-র জন্য Kafka আগে ZooKeeper ব্যবহার করত, এখন নিজের Raft-ভিত্তিক KRaft; লেখার সময়ের সর্বশেষ বড় সংস্করণে ZooKeeper আর নেই — consensus পর্ব ৪২-এ।

৭ · Throughput আর consumer lag — গতি কোথা থেকে, আর কী মাপবেন

Log-ভিত্তিক broker প্রায়ই একটি সাধারণ মেশিনেও সেকেন্ডে কয়েকশো MB পর্যন্ত লেখা নিতে পারে — হার্ডওয়্যার আর setting-ভেদে অনেক আলাদা। গোপন রহস্য নেই; কয়েকটি সরল নকশার সিদ্ধান্ত:

  • শুধু শেষে লেখা: log-এ লেখা মানে sequential I/O — random লেখার চেয়ে বহু গুণ দ্রুত, index হালনাগাদের ঝামেলাও নেই (পর্ব ১৯)।
  • OS-এর page cache: সদ্য লেখা ডেটা operating system-এর memory-তে থাকে, তাই সময়মতো চলা consumer প্রায়ই disk না ছুঁয়েই পড়েন।
  • Zero-copy: Linux-এর sendfile-এর মতো system call দিয়ে ডেটা application-এর memory-তে না এনেই socket-এ পাঠানো যায়; TLS চালু থাকলে encryption-এর জন্য আনতেই হয়, তাই সুবিধা কমে।
  • Batching আর compression: producer একই partition-এর record কয়েক millisecond জমিয়ে পুরো batch একসাথে compress করে পাঠায় (gzip, snappy, lz4, zstd); broker batch প্রায় অপরিবর্তিত রাখে, consumer-ও batch ধরে পড়েন। প্রতি record-এর স্থির খরচ ভাগ হয়; দাম কয়েক millisecond বাড়তি latency.
  • Partition-এ parallelism: প্রতিটি partition আলাদা broker-এ, আলাদা consumer-এর হাতে — throughput মোটামুটি partition-এর সংখ্যার সাথে বাড়ে।
  • Pull মডেল: consumer নিজের গতিতে টানেন; log নিজেই ভিড় শুষে নেয় — পর্ব ১৮ — Overload সামলানো-এর backpressure-এর একটি স্বাভাবিক রূপ।

Consumer lag — সবচেয়ে জরুরি একটি সংখ্যা

প্রতিটি partition-এ lag = সর্বশেষ offset (log end) − group-এর committed offset; group-এর মোট lag সব partition-এর যোগফল। ছবি ৩৩.১-এ ধরুন সব partition-এ LEO ৬: group matching-এর lag (৬ − ৪) + (৬ − ৩) + (৬ − ৫) = ৬, আর analytics-এর (৬ − ১) + (৬ − ০) + (৬ − ২) = ১৫. Record-এর সংখ্যার চেয়েও অর্থবহ হলো সময়ে lag: সবচেয়ে নতুন record প্রক্রিয়া হতে কত সেকেন্ড বাকি — মোটামুটি lag ÷ consumer-এর প্রক্রিয়ার হার; পর্ব ৪ — Latency, throughput আর percentile-এর Little-এর সূত্রের আত্মীয়।

কেন CPU-র বদলে lag? Consumer-এর CPU ৩০% হলেও lag বাড়তে পারে — হয়তো তাঁরা একটি ধীর database-এর অপেক্ষায়। Lag সরাসরি ব্যবহারকারীর অভিজ্ঞতা মাপে: “চালকের অবস্থান কত পুরোনো”. সতর্কসংকেত দিন lag একটি সীমা ছাড়ালে, আর কয়েক মিনিট ধরে ক্রমাগত বাড়তে থাকলে (observability পর্ব ৫০-এ)। নিচের প্রোগ্রাম একটি কাল্পনিক notification topic-এ ৫ মিনিটের ভিড় নিয়ে হিসাব করে।

// Consumer lag during a traffic spike, and how many partitions the peak needs.
// Assumed: each consumer handles 150 msg/s; a partition is read by one consumer of the group.
const PER_CONSUMER = 150, PARTITIONS = 6;
const rateAt = (min) => (min >= 2 && min <= 6 ? 1500 : 600);      // msg/s; spike in minutes 2-6

function simulate(consumers) {
  const active = Math.min(consumers, PARTITIONS);                  // extra consumers sit idle
  const cap = active * PER_CONSUMER;
  let lag = 0, peak = 0, drainedAt = null; const rows = [];
  for (let min = 0; min < 20; min++) {
    lag = Math.max(0, lag + (rateAt(min) - cap) * 60);              // messages behind after this minute
    peak = Math.max(peak, lag);
    if (min > 6 && lag === 0 && drainedAt === null) drainedAt = min + 1;
    rows.push([min, rateAt(min), lag, Math.round(lag / cap)]);
  }
  return { active, cap, peak, drainedAt, rows };
}
const a = simulate(6);
console.log("minute  in/s     lag  behind(s)    capacity " + a.cap + " msg/s");
for (const [m, r, lag, s] of a.rows)
  if (m <= 8 || m % 4 === 0 || m === a.drainedAt - 1)
    console.log(String(m).padStart(6) + String(r).padStart(6) + String(lag).padStart(8) + String(s).padStart(11));
console.log("peak lag " + a.peak + " messages; back to 0 at the end of minute " + (a.drainedAt - 1));
const b = simulate(10);
console.log("10 consumers on " + PARTITIONS + " partitions: active " + b.active + ", capacity " + b.cap +
  " msg/s, peak lag " + b.peak);
const need = Math.ceil(1500 / PER_CONSUMER);
console.log("no lag at the peak needs ceil(1500 / " + PER_CONSUMER + ") = " + need + " consumers -> at least " + need + " partitions");
console.log("with 2x growth headroom: " + Math.ceil(2 * 1500 / PER_CONSUMER) + " partitions");

আউটপুট:

minute  in/s     lag  behind(s)    capacity 900 msg/s
     0   600       0          0
     1   600       0          0
     2  1500   36000         40
     3  1500   72000         80
     4  1500  108000        120
     5  1500  144000        160
     6  1500  180000        200
     7   600  162000        180
     8   600  144000        160
    12   600   72000         80
    16   600       0          0
peak lag 180000 messages; back to 0 at the end of minute 16
10 consumers on 6 partitions: active 6, capacity 900 msg/s, peak lag 180000
no lag at the peak needs ceil(1500 / 150) = 10 consumers -> at least 10 partitions
with 2x growth headroom: 20 partitions

৬ জন consumer মিলে সেকেন্ডে ৯০০. ভিড়ের সময় আসে ১,৫০০, তাই প্রতি মিনিটে lag বাড়ে (১,৫০০ − ৯০০) × ৬০ = ৩৬,০০০; পাঁচ মিনিটে ১,৮০,০০০ — তখন নতুন একটি notification প্রায় ২০০ সেকেন্ড দেরিতে যায়। ভিড় থামলে প্রতি মিনিটে কমে (৯০০ − ৬০০) × ৬০ = ১৮,০০০, তাই শূন্যে ফিরতে ১,৮০,০০০ ÷ ১৮,০০০ = ১০ মিনিট — ১৬ নম্বর মিনিটের শেষে। সবচেয়ে বড় শিক্ষা পরের লাইনে: ঘাবড়ে consumer ১০ জন করলেও ৬টি partition-এ সক্রিয় থাকেন ৬ জন, ক্ষমতা সেই ৯০০ — বাকি ৪ জন অলস। ভিড়ে lag না চাইলে অন্তত ১০টি partition লাগত, আর বৃদ্ধির জায়গা রাখলে ২০. Partition-এর সংখ্যা তাই আগেই ভাবতে হয়।

Partition ≥ max( লক্ষ্য throughput ÷ প্রতি partition-এ লেখার ক্ষমতা , লক্ষ্য throughput ÷ প্রতি consumer-এর প্রক্রিয়ার ক্ষমতা )
তারপর বৃদ্ধির জায়গা — কিন্তু অকারণে অনেক বেশি নয়

এটি প্রচলিত একটি মোটা হিসাব (Confluent-এর engineering blog-এ এমন নিয়মের আলোচনা আছে); দুটি ক্ষমতাই নিজের হার্ডওয়্যার আর কোডে মেপে নিতে হয়। বেশি partition-এরও দাম আছে — ভাবনার প্রশ্ন ২।

৮ · Replay, queue বনাম log, CDC আর অন্য সিস্টেম

Replay — অতীত আবার চালানো

Log-এর সবচেয়ে শক্তিশালী গুণ: consumer-এর offset পেছনে সরিয়ে (সময় দিয়েও — “গতকাল সকাল ১০টা থেকে”) একই record আবার প্রক্রিয়া করা। কাজে লাগে বাগ ঠিক করায় (ভুল ভাড়ার হিসাব কোড ঠিক করে আবার), নতুন সার্ভিসে (নতুন fraud model পুরোনো ঘটনা থেকে অবস্থা বানায়) আর নতুন view-তে — একই ঘটনা থেকে ভিন্ন আকারের একটি materialized view তৈরি করায়। শর্ত: retention যথেষ্ট লম্বা, effect idempotent (নইলে দ্বিতীয়বার SMS), আর replay-এর গতিতে নিচের database যেন না ডোবে।

Kreps ২০১৪ সালের একটি প্রবন্ধে (“Questioning the Lambda Architecture”) প্রস্তাব দেন: batch আর stream-এর আলাদা দুটি pipeline-এর বদলে একটি stream pipeline, আর আবার হিসাব লাগলে log থেকে replay — পরে যার নাম হয় “Kappa architecture”. পুরো তুলনা পর্ব ৩৬ — Batch আর stream processing-এ।

Queue বনাম log — কখন কোনটি

পর্ব ৩১-এর message queue আর এই পর্বের log বাইরে থেকে একই রকম দেখায় — দুটোতেই producer লেখে, consumer পড়েন। ভেতরের মডেল আলাদা:

দিকMessage queue (RabbitMQ বা SQS-ধাঁচের)Log (Kafka-ধাঁচের)
পড়ার পরে বার্তাAck পেলে মুছে যায়থাকে, retention পর্যন্ত
কে কোথায়, হিসাব রাখেBroker, প্রতিটি বার্তা ধরেConsumer, প্রতি partition-এ একটি offset
Consumer বাড়ানোযত খুশি; প্রত্যেকে পরের খালি বার্তা নেনসর্বোচ্চ partition-এর সংখ্যা পর্যন্ত
ক্রমঅনেক consumer-এ সাধারণত দুর্বলPartition-এর ভেতরে কঠোর
একটি বার্তা ব্যর্থ হলেশুধু সেটি আবার বা dead letter queue-তে; বাকিরা চলেPartition আটকে যায়, যদি না আলাদা topic-এ সরান
Replay আর নতুন পাঠকসাধারণত নেইস্বাভাবিক কাজ
যেখানে মানায়আলাদা আলাদা কাজ: email পাঠানো, ছবি ছোট করাঘটনার stream: অনেক পাঠক, ক্রম, ইতিহাস, CDC

সীমারেখা ঝাপসা হয়ে আসছে, তবু প্রশ্নটি স্থির: বার্তাটি কি একটি কাজ (একজন করলেই শেষ), নাকি একটি ঘটনা (অনেকে জানবেন, ক্রমে)?

CDC — database-এর log থেকে stream

একটি অর্ডার সার্ভিস database-এ অর্ডার লেখে, আর search, cache ও analytics-কেও খবর দিতে চায়। সরল চিন্তা “dual write” — কোডে প্রথমে database, তারপর topic-এ লেখা। কিন্তু দুই লেখার মাঝে crash হলে একটিতে আছে, অন্যটিতে নেই; আর দুটি request একই সারি প্রায় একসাথে বদলালে দুই জায়গায় উল্টো ক্রমে পৌঁছাতে পারে — চিরস্থায়ী অমিল (ভাবনার প্রশ্ন ৫)।

Change data capture সমস্যাটি উল্টো দিক থেকে ধরে: database নিজেই প্রতিটি commit তার log-এ ক্রমে লেখে — PostgreSQL-এ WAL, MySQL-এ binlog. একটি CDC টুল সেই log replication-এর পথে পড়ে (পর্ব ২২-এর logical replication), আর প্রতিটি সারির পরিবর্তন একটি event হিসেবে topic-এ লেখে — সাধারণত প্রতিটি table-এর জন্য একটি topic, key = সারির primary key. Open-source Debezium এই ধাঁচের একটি পরিচিত টুল, যা Kafka Connect-এর ওপর চলে; সমর্থিত database আর খুঁটিনাটি সংস্করণভেদে আলাদা।

  • শুরুতে snapshot, তারপর stream: নতুন CDC চালু হলে আগে পুরো table-এর একটি consistent snapshot, আর সেই মুহূর্তের log-এর অবস্থান থেকে পরিবর্তন — যাতে কিছু বাদ না পড়ে।
  • Key = primary key: একটি সারির সব পরিবর্তন এক partition-এ, ক্রমে। Topic-টি compacted রাখলে নতুন যে কেউ পুরো table-এর বর্তমান অবস্থা পান।
  • ব্যবহার: search index হালনাগাদ (পর্ব ২৭ — Search-এ CDC থেকে index); cache-এর key মুছে দেওয়া বা নতুন করা (পর্ব ২৯ — Eviction আর invalidation); data warehouse আর analytics-এ টানা ডেটা; আর পুরোনো সিস্টেম থেকে নতুনে সরানো (পর্ব ২৩-এর resharding).
  • Schema: table-এর একটি column বদলালে event-এর গঠনও বদলায় — consumer ভাঙা ঠেকাতে schema registry আর compatibility-র নিয়ম, পর্ব ১২ — Data format আর serialization-এ।
  • কাঁচা table না ঘটনা? CDC database-এর ভেতরের কাঠামো বাইরে ফাঁস করে — column-এর নাম বদলালে বাইরের দশটি সার্ভিস ভাঙে। ব্যবসার অর্থবহ event চাইলে outbox pattern: event-টি ব্যবসার সারির সাথে একই transaction-এ একটি outbox table-এ, আর CDC শুধু সেই table পড়ে — পর্ব ২৫ — Distributed transaction-এ।

অন্য log-ভিত্তিক সিস্টেম

Apache Pulsar

Yahoo-তে শুরু, পরে Apache project. Broker আর storage আলাদা — ডেটা থাকে Apache BookKeeper-এ, তাই broker বাড়ানো-কমানো সহজ; queue-ধাঁচের subscription-ও আছে।

compute আর storage আলাদা

Kinesis-ধাঁচের managed stream

Cloud-এর পরিচালিত সেবা (যেমন AWS Kinesis Data Streams): partition-এর নাম shard, offset-এর নাম sequence number; প্রতিটি shard-এর সীমা আর retention documentation-এ দেওয়া, যা সময়ের সাথে বদলায়।

server চালানোর ঝামেলা নেই

Redis Streams

Redis ৫.০ (২০১৮) থেকে একটি stream data type: XADD দিয়ে শেষে যোগ, সময়ভিত্তিক id, consumer group আর XACK, ack না পাওয়া বার্তার তালিকা। ছোট থেকে মাঝারি চাপে সহজ, কিন্তু ডেটা মূলত memory-তে — দীর্ঘ retention-এর জন্য নয়।

memory-তে হালকা stream

আরও গভীরে: Martin Kleppmann-এর “Designing Data-Intensive Applications” (২০১৭)-এর stream processing অধ্যায় log-ভিত্তিক broker, CDC আর “database-কে ভেতর থেকে উল্টে দেখা” (unbundling) নিয়ে বিস্তারিত যুক্তি দেয়। Log-এর তাত্ত্বিক ভিত্তি state machine replication — একই ক্রমের একই আদেশ সব কপিকে একই অবস্থায় আনে — যা Raft-এর মতো consensus প্রোটোকলের কেন্দ্রেও (পর্ব ৪২)। আর Kafka-র transaction ও exactly-once-এর নকশা Kafka Improvement Proposal (KIP)-এর প্রকাশিত দলিলে পড়া যায়।

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

  • Kafka-ধাঁচের broker কোন ক্রমের নিশ্চয়তা দেয়? শুধু একটি partition-এর ভেতরে; ক্রম দরকার এমন জিনিসকে key বানান।
  • ৬টি partition-এ ১০ জন consumer দিলে কী হয়? একটি group-এ সর্বোচ্চ ৬ জন সক্রিয়, ৪ জন অলস।
  • Offset কখন commit করবেন? প্রক্রিয়ার পরে (at-least-once) আর consumer idempotent; আগে করলে crash-এ বার্তা হারায়।
  • Log compaction কী আর কোথায়? প্রতিটি key-এর শেষ মান রাখে, offset বদলায় না; CDC, cache বা table-এর অবস্থা ফেরাতে।
  • acks=all কি একাই যথেষ্ট? না, সাথে min.insync.replicas ≥ ২ আর replication factor ৩; নইলে ISR ছোট হয়ে acks=1-এর মতো।

৯ · AI-এর যুগে — streaming-এর নকশায় AI-কে যাচাই

“Microservice-গুলো কীভাবে যুক্ত করব?” — AI-এর প্রিয় উত্তর “মাঝে Kafka দিন”. কখনো সেটি ঠিক, কিন্তু প্রায়ই key, partition, commit আর retention নিয়ে একটি শব্দও থাকে না — অথচ সব ঝামেলা ঠিক সেখানেই। Streaming-এ AI-এর সাধারণ ভুলগুলো:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
“Kafka দিলেই ক্রম ঠিক থাকবে”ক্রম শুধু partition-এর ভেতরে, আর সেটি key-এর ওপর নির্ভর করেপ্রশ্ন করুন: কোন জিনিসের ক্রম জরুরি, key কী?
“Kafka exactly-once দেয়”বাইরের effect (SMS, payment, অন্য database)-এ আপনাআপনি নয়Commit-এর সময় আর idempotency-র পরিকল্পনা চান (পর্ব ৩৪)
Partition-এর সংখ্যা আন্দাজে, বা পরে “বাড়িয়ে নেবেন”Consumer-এর ছাদ partition; বাড়ালে key-এর partition বদলায়লক্ষ্য throughput আর মাপা ক্ষমতা দিয়ে নিজে হিসাব
Lag-এর হিসাবে ভুল, বা “আরও consumer দিন”Partition-এর বেশি consumer অলস থাকেনঅংশ ৭-এর মতো simulation চালান
Retention আর setting-এর মান বানিয়ে বলাডিফল্ট আর সীমা সংস্করণ ও প্রদানকারীভেদে বদলায়অফিসিয়াল documentation, আপনার সংস্করণের
ACID আর CAP-এর consistency গুলিয়ে ফেলাacks আর ISR-এর আলোচনা replication-এর, ACID-এর C নিয়ম রক্ষারপ্রশ্ন করুন: কোন অর্থে consistency? (পর্ব ৪০)
দরকার ছাড়াই streaming platformদিনে কয়েক হাজার কাজের জন্য একটি database table বা সরল queue যথেষ্ট হতে পারেহিসাব করুন: QPS, পাঠক কতজন, replay লাগবে কি না

AI-কে কাজটি এমনভাবে দিন যাতে প্রতিটি সিদ্ধান্তের পেছনে হিসাব আর ব্যর্থতার পরিস্থিতি দেখাতে হয়:

তুমি একজন অভিজ্ঞ backend ইঞ্জিনিয়ার। একটি ride-sharing অ্যাপের চালকের অবস্থান-হালনাগাদের জন্য log-ভিত্তিক stream নকশা করো। তথ্য (ধরে নেওয়া): ৫০,০০০ সক্রিয় চালক, প্রত্যেকে ৪ সেকেন্ডে একবার অবস্থান পাঠান, প্রতিটি record প্রায় ২০০ byte; peak-এ গড়ের ২ গুণ। পাঠক: matching সার্ভিস (সাথে সাথে), analytics (দেরি চলে), fraud (গত ৭ দিন replay লাগতে পারে)। ১. গড় ও peak-এ সেকেন্ডে কত record আর কত MB — ধাপে ধাপে হিসাব দেখাও। ২. Key কী হবে আর কেন; কোন ক্রম নিশ্চিত হবে আর কোনটি হবে না। ৩. Partition কয়টি — প্রতি consumer সেকেন্ডে ২,০০০ record প্রক্রিয়া করতে পারে ধরে, সূত্রসহ। ৪. Offset commit কখন, crash-এ কী হারাবে বা দুবার আসবে, আর consumer কীভাবে idempotent হবে। ৫. Retention কতদিন, replication factor, acks আর min.insync.replicas — ডিস্কের হিসাবসহ। ৬. একজন consumer বিকল হলে, একটি broker বিকল হলে, আর lag বাড়তে থাকলে কী ঘটবে। প্রতিটি অনুমান আলাদা করে লেখো। কোনো product-এর ডিফল্ট বা সীমা বললে সংস্করণ উল্লেখ করো; না জানলে বলো "জানি না"।

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

AI দেয় দ্রুত খসড়া, যাচাইয়ের দায় আপনার।
Key দেখুন, partition গুনুন, commit-এর জানালা মাপুন।

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। সূত্র মুখস্থ নয়, কেন কোন নকশা বেছে নেওয়া হয় সেটাই আসল।

প্র ০১Jay Kreps কেন log-কে “unifying abstraction” বললেন? LinkedIn-এর কোন সমস্যা থেকে Kafka-র জন্ম?

Kreps-এর ২০১৩ সালের প্রবন্ধ আর পরের সাক্ষাৎকারগুলোতে যে গল্পটি বলা হয়েছে, তার সারকথা এমন: একটি বড় প্রতিষ্ঠানে ডেটার উৎস অনেক — ব্যবহারকারীর কাজকর্ম, database-এর পরিবর্তন, সার্ভারের metric — আর ডেটার গন্তব্যও অনেক: search, recommendation, data warehouse, monitoring. প্রতিটি উৎস থেকে প্রতিটি গন্তব্যে আলাদা pipeline বানালে N টি উৎস আর M টি গন্তব্যে প্রায় N × M টি সংযোগ, প্রত্যেকটি নিজের format, নিজের ভুল আর নিজের দেরি নিয়ে। একটি নতুন গন্তব্য যোগ করা মানে প্রতিটি উৎসের দলের কাছে অনুরোধ।

প্রস্তাবিত সমাধানটি একটি কেন্দ্রীয় log: প্রতিটি উৎস একবার log-এ লেখে, প্রতিটি গন্তব্য log থেকে পড়ে — সংযোগ N + M. কিন্তু Kreps-এর দাবি আরও গভীর। Database-এর ভেতরে log আছে; replication মানে log পাঠানো; distributed consensus আসলে “সবাই মিলে একটি log-এর পরের entry-তে একমত হওয়া”; আর একটি table হলো log-এর একটি মুহূর্তের হিসাব, log হলো table-এর পরিবর্তনের ইতিহাস। এই দ্বৈততা (table আর stream একই জিনিসের দুই রূপ) পরে Kafka Streams-এর মতো টুলের ভিত্তি হয়।

এর আগে প্রচলিত messaging সিস্টেমগুলো মূলত queue ছিল — বার্তা পড়লেই মুছে যায়, broker প্রতিটি বার্তার অবস্থা রাখে, আর বিশাল পরিমাণ ক্রমাগত ডেটায় তাদের throughput সীমিত ছিল। NetDB ২০১১-এর Kafka paper-এ লেখকেরা সরল নকশার কথা বলেছিলেন: partition-ভিত্তিক log, ফাইলে টানা লেখা, consumer নিজের অবস্থান রাখে, broker প্রায় কোনো অবস্থা রাখে না। সেই সরলতাই গতি এনেছিল।

সতর্কতা: “সবকিছু log” আকর্ষণীয়, কিন্তু প্রতিটি সমস্যায় নয়। ছোট সিস্টেমে একটি database table আর একটি সরল queue প্রায়ই যথেষ্ট; একটি streaming platform চালানো, পর্যবেক্ষণ আর upgrade করা নিজেই একটি দলের কাজ।

মূল উপলব্ধি: log-এর শক্তি তার সরলতায় — ক্রমে সাজানো, শুধু-যোগ-করা ইতিহাস থেকে database, replication আর ডেটা pipeline সবই গড়া যায়; কিন্তু সেই শক্তি ব্যবহারের খরচও হিসাব করতে হয়।

প্র ০২একটি topic-এ কয়টি partition রাখবেন? কম আর বেশি — দুই দিকের দাম কী, আর পরে বাড়ালে কী ভাঙে?

Partition হলো একটি topic-এর parallelism-এর একক। একটি group-এ partition-এর চেয়ে বেশি consumer কাজে আসে না; আর একটি partition একটি broker-এর একটি disk আর একজন consumer-এর গতিতে বাঁধা। তাই কম partition মানে স্থায়ী একটি ছাদ: অংশ ৭-এর হিসাবে ৬টি partition-এ ভিড়ের সময় ১০ জন consumer দিয়েও লাভ নেই। আর ভারসাম্যও দুর্বল — কয়েকটি key-এর ভারে একটি partition ভারী হলে আর ভাগ করার উপায় নেই।

বেশি partition-এরও খরচ, যদিও সেগুলো কম চোখে পড়ে:

  • Broker-এর ভার: প্রতিটি partition-এর প্রতিটি কপি ডিস্কে কয়েকটি ফাইল, memory-তে index, আর follower-দের fetch-এর কাজ। Cluster-এ লাখো partition মানে বিশাল metadata.
  • বিকলতার সময়: একটি broker বিকল হলে তার সব leader partition-এর জন্য নতুন leader বাছতে হয়; সংখ্যা বেশি হলে এই সময়টুকু লম্বা, আর তখন সেগুলোতে লেখা থামে। নতুন KRaft controller এই কাজ আগের চেয়ে দ্রুত করে বলে প্রকাশিত দাবি আছে, তবু সংখ্যাটি গুরুত্বপূর্ণ।
  • ছোট batch: producer-এর লেখা অনেক partition-এ ভাগ হলে প্রতিটির batch ছোট, compression দুর্বল, প্রতি record-এর খরচ বেশি; আর acks=all-এ end-to-end latency বাড়ে।
  • Rebalance: বেশি partition মানে rebalance-এ বেশি হাতবদল।

পরে বাড়ালে কী ভাঙে: key-ওয়ালা topic-এ hash(key) mod N-এর N বদলায়, তাই অনেক key নতুন partition-এ যায়, কিন্তু তাদের পুরোনো record পুরোনো partition-এই থেকে যায়। বদলের মুহূর্তে একই key-এর দুই প্রজন্মের record দুজন ভিন্ন consumer-এর হাতে থাকতে পারে — নতুনটি আগে প্রক্রিয়া হলে ক্রম ভাঙে। Compacted topic-এ আরও খারাপ: একই key-এর দুই partition-এ দুটি “সর্বশেষ” মান। তাই key-ওয়ালা, ক্রম-নির্ভর topic-এ অনেক দল শুরুতেই হিসাবের চেয়ে কিছুটা বেশি partition নেয়, আর বড় বদল লাগলে নতুন topic বানিয়ে দুই দিকে লিখে ধাপে ধাপে সরে — পর্ব ২৩-এর resharding-এর মতো।

মূল উপলব্ধি: partition-এর সংখ্যা ভবিষ্যৎ parallelism-এর ছাদ আর আজকের overhead-এর মাঝে একটি আপস; key-নির্ভর topic-এ সেটি প্রায় স্থায়ী সিদ্ধান্ত, তাই হিসাব করে শুরুতেই নিন।

প্র ০৩Consumer group-এর rebalance কেন একসময় বড় সমস্যা ছিল, আর static membership ও cooperative rebalancing কীভাবে তা কমায়?

পুরোনো (eager) protocol-এ নিয়মটি সরল ও নিরাপদ: ভাগ বদলাতে হলে সবাই আগে নিজের সব partition ছেড়ে দেবেন, তারপর নতুন ভাগ। এতে কখনো দুজন একসাথে একটি partition পড়েন না। কিন্তু দাম: যেকোনো একটি সদস্যের আসা-যাওয়ায় পুরো group থেমে যায় — “stop the world”.

Container আর Kubernetes-এর যুগে এটি বড় আঘাত হয়ে দাঁড়ায়। একটি rolling deploy-এ ৩০ জন consumer একে একে restart হন; প্রতিটি restart-এ দুবার rebalance (একবার চলে যাওয়ায়, একবার ফেরায়)। প্রতিবার পুরো group কয়েক সেকেন্ড থেকে বেশি থামে, lag জমে, আর যাদের নিজস্ব local state আছে (যেমন stream processing-এর গণনা), তাদের নতুন partition-এর state আবার বানাতে মিনিটও লাগতে পারে। আরেকটি ফাঁদ: একটি poll-এর record প্রক্রিয়া করতে নির্ধারিত সময়ের বেশি লাগলে consumer-কে “মৃত” ধরা হয়, rebalance হয়, আর তার অসমাপ্ত কাজ অন্য কেউ আবার করে — ডুপ্লিকেট আর lag দুই-ই বাড়ে, এমনকি বারবার ঘুরতেও পারে।

Kafka-র Improvement Proposal-গুলোর মধ্য দিয়ে কয়েকটি সমাধান এসেছে (সংস্করণ আর client-ভেদে প্রাপ্যতা আলাদা):

  • Static membership: প্রতিটি consumer instance-কে একটি স্থায়ী id দেওয়া। Restart হয়ে নির্দিষ্ট সময়ের মধ্যে ফিরলে coordinator ধরে নেয় একই সদস্য ফিরেছেন, আর কোনো rebalance হয় না।
  • Cooperative (incremental) rebalancing: সবাই সব ছাড়েন না; নতুন ভাগে যেসব partition সত্যিই হাতবদল হবে, শুধু সেগুলো ছাড়া হয়, বাকিরা পড়তে থাকেন। “Sticky” assignment চেষ্টা করে আগের ভাগ যতটা সম্ভব রাখতে।
  • Broker-চালিত নতুন group protocol: ভাগের হিসাব consumer-এর হাত থেকে broker-এ সরানো, আর প্রতিটি সদস্যের সাথে আলাদা আলাদা হিসাব — যাতে এক সদস্যের দেরিতে সবার কাজ না থামে।

নকশায় এর শিক্ষা: consumer-এর প্রক্রিয়া ছোট আর পূর্বানুমেয় রাখুন, poll-এ কতটুকু নেবেন সীমা দিন, deploy-এর সময় rebalance-এর প্রভাব মাপুন, আর rebalance-কে স্বাভাবিক ঘটনা ধরে ডুপ্লিকেট সামলান।

মূল উপলব্ধি: কাজ ভাগের নিরাপত্তা আর কাজ না থামানোর মধ্যে একটি টানাপোড়েন আছে; আধুনিক protocol-গুলো শুধু যা বদলাতে হবে সেটুকুই থামিয়ে দুটিকে কাছাকাছি আনে।

প্র ০৪Replication factor ৩, acks=all আর min.insync.replicas=2 — এই সমন্বয় কোন বিকলতা সহ্য করে, কোনটি করে না? Unclean leader election কেন বিপজ্জনক?

ধরুন একটি partition-এর তিনটি কপি — leader L, follower F1 আর F2, তিনটি আলাদা zone-এ। acks=all মানে producer সফলতা পান তখনই, যখন record ISR-এর সবার কাছে; min.insync.replicas=2 মানে ISR অন্তত দুজন না হলে broker লেখা নেয় না।

  • একটি কপি বিকল: ধরুন F2. সে ISR থেকে বাদ পড়ে; ISR = {L, F1}, দুজন — লেখা চলে, প্রতিটি নিশ্চিত record দুটি জায়গায়। L নিজে বিকল হলে F1 leader হয়, কোনো নিশ্চিত record হারায় না।
  • দুটি কপি বিকল: ISR-এ একজন, যা min.insync.replicas-এর কম — broker লেখা প্রত্যাখ্যান করে। Producer error পায়; সিস্টেম লেখার দিক থেকে unavailable, কিন্তু যা নিশ্চিত হয়েছিল তা এখনো আছে, আর পড়া চলতে পারে।
  • তিনটিই বিকল: কোনো নকশাই বাঁচায় না, যদি না অন্য region-এ কপি থাকে (পর্ব ৫১)।

তুলনা করুন min.insync.replicas=1-এর সাথে। দুটি follower ধীর হয়ে ISR থেকে বাদ পড়লে leader একাই ISR, আর acks=all চুপচাপ “শুধু leader লিখেছে” হয়ে যায়। তখন leader-এর disk নষ্ট হলে নিশ্চিত বলে জানানো record-ও হারায়। এই ফাঁদ কাগজে দেখা যায় না, ঘটনার পরে ধরা পড়ে।

Unclean leader election: ধরুন ISR-এর সবাই বিকল, শুধু একটি পিছিয়ে থাকা, ISR-এর বাইরের কপি বেঁচে আছে। দুটি পথ: অপেক্ষা করা, যতক্ষণ না ISR-এর কেউ ফেরেন (partition-টি ততক্ষণ বন্ধ), অথবা পিছিয়ে থাকা কপিকে leader বানানো (partition দ্রুত চালু, কিন্তু তার পরের নিশ্চিত record চিরতরে হারায়, আর consumer-রা এমন offset দেখতে পারেন যা পরে অন্য record দিয়ে ভরে যায়)। Kafka-তে লেখার সময় এটি ডিফল্টে বন্ধ — durability-কে অগ্রাধিকার। Metric বা click-stream-এর মতো কিছু হারানো চলে এমন ডেটায় কেউ কেউ চালু রাখেন; payment-এর ডেটায় প্রায় কখনো নয়।

এটি CAP-এর একটি বাস্তব রূপ: partition বা বিকলতার সময় হয় লেখা থামাবেন (consistency আর durability), নয়তো চালু রাখবেন কিছু হারানোর ঝুঁকিতে (availability) — পর্ব ৪০-এ এর তত্ত্ব।

মূল উপলব্ধি: durability কোনো একক setting নয়; replication factor, acks, min.insync.replicas আর leader নির্বাচনের নিয়ম মিলে ঠিক করে কোন বিকলতায় আপনি ডেটা হারাবেন আর কোনটিতে শুধু সাময়িক লেখা থামবে।

প্র ০৫Search index বা cache হালনাগাদে কোডে “dual write”-এর বদলে CDC কেন নিরাপদ? CDC-র নিজের দুর্বলতা কোথায়?

Dual write-এর সমস্যা দুটি, আর দুটিই চুপচাপ ঘটে। আংশিক ব্যর্থতা: কোড database-এ অর্ডার লিখল, তারপর search-এ লেখার আগে process crash — database-এ অর্ডার আছে, search-এ নেই, আর কেউ জানেন না। উল্টো ক্রমে লিখলে উল্টো সমস্যা। Race: দুটি request একই পণ্যের দাম প্রায় একসাথে বদলাল: A দাম ১০০ করল, B ১২০. Database-এ ক্রম A তারপর B, তাই শেষ দাম ১২০; কিন্তু search-এ পৌঁছানোর পথে B আগে পৌঁছাল, A পরে — search-এ শেষ দাম ১০০. কোনো error নেই, কিন্তু দুই জায়গায় চিরস্থায়ী অমিল।

CDC-তে সত্যের উৎস একটিই — database-এর commit log, আর তার ক্রমই চূড়ান্ত। Database যে ক্রমে commit করেছে, CDC ঠিক সেই ক্রমে event লেখে; key = primary key হলে একই সারির পরিবর্তন এক partition-এ, ক্রমে। Search-এর consumer crash করলে committed offset থেকে আবার পড়েন; কিছু হারায় না, আর idempotent upsert হলে দুবার পড়াও নিরাপদ। নতুন একটি গন্তব্য (ধরুন একটি recommendation সার্ভিস) যোগ করতে মূল সার্ভিসের কোড বদলাতে হয় না। LinkedIn-এর Databus আর পরে Debezium-এর মতো open-source টুল এই ধারণাকে জনপ্রিয় করেছে।

তবে CDC-র নিজের দাম আছে:

  • ভেতরের কাঠামো ফাঁস: table-এর column বাইরের সার্ভিসের API হয়ে যায়; একটি column-এর নাম বদলালে অনেকে ভাঙেন। Outbox table আর schema registry (পর্ব ১২, ২৫) এর প্রতিকার।
  • Transaction-এর সীমা: একটি transaction কয়েকটি table বদলালে event-গুলো আলাদা topic-এ যায়; consumer-এর কাছে তারা একসাথে পৌঁছায় না।
  • Database-এর ওপর চাপ ও log ধরে রাখা: CDC পিছিয়ে পড়লে বা বন্ধ থাকলে database-কে log মুছতে না দিয়ে ধরে রাখতে হতে পারে (PostgreSQL-এর replication slot-এর মতো ব্যবস্থায়) — ডিস্ক ভরে যাওয়ার ঝুঁকি, তাই এরও lag পর্যবেক্ষণ লাগে।
  • Eventual consistency: search কয়েক millisecond থেকে কয়েক সেকেন্ড পিছিয়ে থাকে; “এইমাত্র লেখা অর্ডার খুঁজে না পাওয়া” ব্যবহারকারীকে সামলাতে হয় (পর্ব ২৭)।

মূল উপলব্ধি: দুই জায়গায় লেখার বদলে এক জায়গায় লিখে তার ক্রমে সাজানো log থেকে বাকিদের হালনাগাদ করা — CDC-র মূল যুক্তি; কিন্তু সেই log-এর গঠন আর lag-ও নকশার অংশ।

অনুশীলন

আগে নিজে হিসাব করুন বা লিখুন, তারপর উত্তর মেলান। তৃতীয় প্রশ্নে আগে অনুমান, তারপর অংশ ৪-এর প্রোগ্রাম চালিয়ে যাচাই।

  1. হিসাব — কয়টি partition লাগবে: একটি কাল্পনিক payment সেবার event topic-এ peak-এ সেকেন্ডে ৬০ MB লেখা আসবে (ধরা যাক)। পরীক্ষায় দেখা গেছে, একটি partition-এ একটি producer সেকেন্ডে প্রায় ১০ MB লিখতে পারে, আর একজন consumer (প্রতিটি event database-এ লিখে) সেকেন্ডে প্রায় ৪ MB প্রক্রিয়া করতে পারেন। (ক) কমপক্ষে কয়টি partition? (খ) দুই বছরে চাপ দ্বিগুণ হতে পারে ধরে কয়টি নেবেন? (গ) তখন একটি group-এ সর্বোচ্চ কয়জন consumer কাজে আসবেন, আর ৪০ জন দিলে কী হবে?
    • (ক) লেখার দিক: ৬০ ÷ ১০ = ৬টি partition. পড়ার দিক: ৬০ ÷ ৪ = ১৫ জন consumer, আর একটি partition একজনই পড়েন, তাই ১৫টি partition. দুটির বড়টি — ১৫টি. এখানে বাধা consumer-এর গতি, broker নয়।
    • (খ) দ্বিগুণ চাপ: ১২০ ÷ ৪ = ৩০টি partition. Key-নির্ভর topic-এ পরে বাড়ালে key-এর partition বদলায়, তাই শুরুতেই প্রায় ৩০টি নেওয়া যুক্তিসঙ্গত — কিন্তু শত শত নয়, কারণ প্রতিটির overhead আছে। প্রথম দিন ১৫ জন consumer চালালেই চলে; প্রত্যেকে দুটি করে partition পড়বেন।
    • (গ) ৩০টি partition-এ একটি group-এ সর্বোচ্চ ৩০ জন সক্রিয়। ৪০ জন দিলে ১০ জন অলস বসে থাকবেন — কোনো লাভ নেই, শুধু খরচ আর rebalance-এর ঝামেলা। আরও গতি চাইলে consumer-এর প্রক্রিয়া দ্রুত করা (database-এ batch লেখা) বা partition বাড়ানো।
  2. নকশার সমালোচনা: একটি কাল্পনিক ride-sharing অ্যাপের দল চালকের অবস্থানের topic-এ key দিয়েছে city (ঢাকা, চট্টগ্রাম, সিলেট …), ১২টি partition, consumer-এ auto commit চালু, আর consumer প্রতিটি অবস্থান পেয়ে চালকের অবস্থানের table-এ লেখে। তিনটি সমস্যা খুঁজে বের করুন আর প্রতিটির সমাধান দিন।
    • Hot partition আর কম cardinality: শহর মাত্র কয়েকটি, তাই ১২টি partition-এর অনেকগুলো খালি, আর ঢাকার partition একাই বেশিরভাগ চাপ নেয় — একজন consumer-এর গতিতে বাঁধা। সমাধান: key = driver_id; লাখো ভিন্ন মান, ভার সমান, আর যা জরুরি — একজন চালকের অবস্থানের ক্রম — ঠিক থাকে। শহর ধরে গণনা লাগলে সেটি consumer-এর দিকে হিসাব।
    • Auto commit: commit প্রক্রিয়ার সাথে বাঁধা নয় — crash-এ কিছু অবস্থান হারাতে পারে বা কয়েক সেকেন্ডের অবস্থান আবার আসতে পারে। সমাধান: প্রক্রিয়ার পরে manual commit, অর্থাৎ at-least-once নীতি।
    • ডুপ্লিকেট আর পুরোনো মান: at-least-once-এ একটি পুরোনো অবস্থান দ্বিতীয়বার এলে table-এ নতুনটির ওপর চাপা পড়তে পারে। সমাধান: প্রতিটি record-এ চালকের ফোনের ক্রমিক নম্বর বা সময়, আর লেখার শর্ত “শুধু যদি নতুনতর” — idempotent, ক্রম-সচেতন upsert. সাথে consumer lag-এর সতর্কসংকেত, কারণ কয়েক সেকেন্ড পুরোনো অবস্থানে matching ভুল হয়।
  3. আউটপুট অনুমান করুন: অংশ ৪-এর offset_commit_timing.js-এ CRASH_AT = 5-কে CRASH_AT = 7 করলে দুই নীতিতে “lost” আর “duplicated” কী হবে? আগে নিজে লিখুন, তারপর চালিয়ে মেলান। এরপর CRASH_AT = 8 হলে “commit AFTER”-এর ক্ষেত্রে কী হবে, ভাবুন।

    যুক্তি: দ্বিতীয় poll-এ offset ৪ থেকে ৭. আগে commit হলে commit ৮, তারপর ৪, ৫, ৬ শেষ আর ৭-এ crash — শুরু ৮ থেকে, তাই শুধু ৭ হারায়। পরে commit হলে ৪, ৫, ৬ শেষ কিন্তু commit তখনও ৪ — শুরু ৪ থেকে, তাই ৪, ৫, ৬ দুবার।

    আসল আউটপুট (CRASH_AT = 7):

    commit BEFORE processing (at-most-once):
      crash at offset 7; restart from committed offset 8
      effects: 0 1 2 3 4 5 6 8 9
      lost: [7]  duplicated: []
    commit AFTER processing (at-least-once):
      crash at offset 7; restart from committed offset 4
      effects: 0 1 2 3 4 5 6 4 5 6 7 8 9
      lost: []  duplicated: [4 5 6]

    শিক্ষা: একই crash-এ “আগে commit” এবার কম হারাল (১টি), আর “পরে commit” বেশি ডুপ্লিকেট দিল (৩টি) — batch-এর যত ভেতরে crash, তত বেশি দুবার। CRASH_AT = 8 হলে offset ৮ তৃতীয় batch-এর প্রথম record; “পরে commit”-এ তখন commit ৮, আর শুরু ৮ থেকে — কোনো ডুপ্লিকেট নেই, কিছু হারায়ও না (চালিয়ে দেখুন: lost: [] duplicated: [])। অর্থাৎ ডুপ্লিকেট ঘটবে কি না ভাগ্যের ব্যাপার, কিন্তু ঘটতে পারে — তাই নকশা সবসময় ডুপ্লিকেট ধরে নিয়েই।

  4. হিসাব — retention-এর ডিস্ক আর replay-এর সময়: একটি topic-এ গড়ে সেকেন্ডে ২০ MB (compression-এর পরে) আসে; retention ৭ দিন, replication factor ৩; ধরা যাক 1 TB = 106 MB. (ক) এক কপিতে আর তিন কপি মিলে কত ডিস্ক? (খ) একটি বাগ ঠিক করে নতুন একটি consumer group ৭ দিনের পুরো ডেটা শুরু থেকে replay করবে; group সব মিলিয়ে সেকেন্ডে ২০০ MB প্রক্রিয়া করতে পারে, আর এর মধ্যেও নতুন ডেটা সেকেন্ডে ২০ MB হারে আসছে। কতক্ষণে সে বর্তমানে পৌঁছাবে?
    • (ক) এক দিনে ২০ × ৮৬,৪০০ = ১৭,২৮,০০০ MB; ৭ দিনে ১,২০,৯৬,০০০ MB ≈ ১২.১ TB এক কপিতে। তিন কপিতে ৩,৬২,৮৮,০০০ MB ≈ ৩৬.৩ TB. বাস্তবে এর ওপর কিছুটা খালি জায়গা রাখতে হয় — disk ভরে গেলে broker থেমে যায়।
    • (খ) Group প্রতি সেকেন্ডে পুরোনো ডেটা কমায় (২০০ − ২০) = ১৮০ MB, কারণ ২০ MB নতুন যোগ হচ্ছে। ১,২০,৯৬,০০০ ÷ ১৮০ = ৬৭,২০০ সেকেন্ড ≈ ১৮.৭ ঘণ্টা. এই সময়ে নিচের database-এ টানা সেকেন্ডে ২০০ MB-এর চাপ যাবে — replay-এর গতি বেঁধে না দিলে চলতি সেবাই ডুবতে পারে; আর consumer idempotent না হলে প্রতিটি effect দুবার।

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

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

ক্রমacks=all-এ একটি record-এর producer থেকে consumer-এর commit পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📤 Producer key-এর hash থেকে partition বাছে আর batch-এ জমায়
  2. 📒 Partition-এর leader record-টি log-এর শেষে নতুন offset-এ লেখে
  3. 📑 ISR-এর follower-রা record-টি টেনে নিজের কপিতে লেখে
  4. ✅ High watermark এগোয় আর producer ack পায়
  5. 📥 Group-এর যে consumer partition-টির মালিক, তিনি record-টি পড়ে প্রক্রিয়া করেন
  6. 🔖 Consumer পরের offset commit করেন

একটি Kafka-ধাঁচের topic-এ ৮টি partition. Producer প্রতিটি record-এ key হিসেবে order_id দেয়। কোন ক্রমের নিশ্চয়তা পাওয়া যায়?

  • পুরো topic-এর সব record লেখার ক্রমে পড়া যাবে
  • একই customer-এর সব order ক্রমে আসবে
  • একই order_id-এর record-গুলো লেখার ক্রমে আসবে
  • কোনো ক্রমের নিশ্চয়তাই নেই

একজন consumer poll করে offset ১০ থেকে ১৯ পেলেন, সাথে সাথে offset ২০ commit করলেন, তারপর ১০ থেকে ১৪ প্রক্রিয়া শেষে crash করলেন। নতুন মালিক কোথা থেকে শুরু করবেন, আর ফল কী?

  • ২০ থেকে; offset ১৫ থেকে ১৯ হারিয়ে যাবে
  • ১০ থেকে; ১০ থেকে ১৪ দুবার প্রক্রিয়া হবে
  • ১৫ থেকে; কিছুই হারাবে না বা দুবার হবে না
  • ০ থেকে; পুরো partition আবার পড়া হবে

একটি compacted topic-এ পরপর record: (A, ১), (B, ২), (A, ৩), (C, ৪), (B, null), (A, ৫). Compaction শেষ হওয়ার আর tombstone-এর মেয়াদ পেরোনোর পরে কোন record-গুলো থাকবে?

  • সব ছয়টি, শুধু ক্রম বদলাবে
  • (A, ৫), (B, null), (C, ৪)
  • (A, ১), (B, ২), (C, ৪)
  • (C, ৪), (A, ৫)

একটি partition-এর replication factor ৩, producer-এ acks=all, আর min.insync.replicas=2. দুটি follower একসাথে বিকল হয়ে ISR-এ শুধু leader থাকলে কী ঘটবে?

  • লেখা চলবে, শুধু leader-এ নিশ্চিত হবে
  • Producer-এর লেখা প্রত্যাখ্যান হবে, যতক্ষণ না একটি follower ISR-এ ফেরে
  • সাথে সাথে একটি নতুন leader নির্বাচিত হবে
  • Partition-এর সব ডেটা মুছে যাবে

একটি consumer group সেকেন্ডে ১,০০০ record প্রক্রিয়া করতে পারে। ১০ মিনিট ধরে topic-এ সেকেন্ডে ১,২০০ record এল, শুরুতে lag ছিল ০. দশ মিনিট শেষে lag কত?

  • ২০০
  • ১২,০০০
  • ১,২০,০০০
  • ৭,২০,০০০

একটি অর্ডার সার্ভিস database-এ লেখার পরে কোডেই আলাদা করে search index-এও লেখে — একে বলে dual write. এর বদলে database-এর log থেকে CDC ব্যবহারের প্রধান লাভ কী?

  • Database-এর commit-ই একমাত্র সত্যের উৎস, আর পরিবর্তন সেই ক্রমেই পৌঁছায় — আংশিক ব্যর্থতা আর race-এর অমিল থাকে না
  • Search index সবসময় database-এর সাথে একই মুহূর্তে হালনাগাদ থাকে
  • Schema বদলালেও consumer-দের কিছু ভাবতে হয় না
  • Database-এর ওপর কোনো বাড়তি কাজ বা নির্ভরতা থাকে না

শব্দকোষ

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

মনে রাখুন

  1. Log = শুধু-যোগ-করা, ক্রমে সাজানো, আবার পড়া যায় এমন record-এর সারি; database-এর WAL, replication আর streaming একই ধারণায় দাঁড়িয়ে।
  2. Topic ভাগ হয় partition-এ; ক্রম শুধু partition-এর ভেতরে। যার ক্রম জরুরি তাকে key বানান; partition পরে বাড়ালে key-এর partition বদলায়।
  3. Group-এ প্রতিটি partition একজন consumer পড়েন — সক্রিয় consumer ≤ partition. সদস্য বদলালে rebalance; consumer committed offset থেকে শুরু করেন।
  4. প্রক্রিয়ার আগে commit = হারাতে পারে, পরে commit = দুবার আসতে পারে; তাই idempotent consumer. Retention সময় বা আকারে, compaction প্রতিটি key-এর শেষ মান রাখে।
  5. RF ৩ + acks=all + min.insync.replicas ২; গতি আসে sequential লেখা, page cache, batching আর compression থেকে; মূল মাপ consumer lag; CDC database-এর log থেকে stream বানায়।

পরের পর্বে: at-most-once, at-least-once আর exactly-once-এর পুরো ছবি — idempotency key, deduplication, idempotent producer আর transaction দিয়ে “ঠিক একবার”-এর কাছাকাছি যাওয়া।

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

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