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

Batch আর stream processing — MapReduce, window আর watermark

Batch and Stream Processing — MapReduce, Windows & Watermarks

Taught in Bangla, this lesson contrasts batch processing of bounded data with stream processing of unbounded streams, walks through MapReduce map, shuffle and reduce with a word-count example and combiners, data locality, stragglers and failures, dataflow engines, batch joins and ETL versus ELT, event time versus processing time, tumbling, sliding and session windows, watermarks and late data in a simulator, state, checkpoints, stream joins and lambda versus kappa.

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

  • কখন batch আর কখন stream — latency, সম্পূর্ণতা আর খরচের trade-off
  • MapReduce-এর map, shuffle, reduce, combiner, data locality আর straggler সামলানো, হাতে-কলমে word count
  • Spark-ধাঁচের dataflow engine, batch-এ join-এর দুই কৌশল আর ETL বনাম ELT
  • Event time, tumbling-sliding-session window, watermark আর দেরিতে আসা ডেটা — simulator-এ গুনে দেখা
  • State, checkpoint, stream join, lambda বনাম kappa, আর AI-এর দেওয়া pipeline নকশা যাচাই

১ · Batch আর stream — সীমিত ডেটা বনাম অসীম ধারা

পাড়ার একটি মুদি দোকানের কথা ভাবুন। দোকানি দুইভাবে হিসাব রাখেন। প্রতি মাসের শেষে খাতা খুলে পুরো মাসের বিক্রি যোগ করেন — কোন পণ্য বেশি চলল, কার কাছে কত বাকি। এই হিসাব নিখুঁত, কারণ মাস শেষ, আর কোনো বিক্রি যোগ হবে না। কিন্তু উত্তর আসে মাসে একবার। আবার দিনের মধ্যেও তিনি ক্যাশবাক্সে চোখ রাখেন: “গত এক ঘণ্টায় চিনি অনেক বিক্রি হলো, আড়তে এখনই ফোন দিই।” এই হিসাব তাৎক্ষণিক, কিন্তু কখনো পুরো নিশ্চিত নয় — কোনো খদ্দের হয়তো বাকিতে নিয়েছেন, টাকা দেবেন পরে।

প্রথমটি batch processing: একটি সীমিত, শেষ হয়ে যাওয়া ডেটার স্তূপ (bounded data) নিয়ে একবারে হিসাব। দ্বিতীয়টি stream processing: যে ডেটা কখনো শেষ হয় না (unbounded data) — অ্যাপের click, পেমেন্ট, GPS-এর অবস্থান — তার ওপর চলমান হিসাব। দুটোরই কাজ কাঁচা ঘটনা থেকে কাজের তথ্য বানানো; পার্থক্য হলো কখন উত্তর চাই আর উত্তর কতটা সম্পূর্ণ হতে হবে।

মূল ধারণা

অসীম ধারার ওপর হিসাবে latency (কত দ্রুত উত্তর) আর completeness (উত্তর কতটা সম্পূর্ণ) একসাথে সর্বোচ্চ করা যায় না। সব ডেটা আসার জন্য অপেক্ষা করলে উত্তর দেরিতে আসে; না করলে কিছু ঘটনা বাদ পড়ে বা পরে ফল বদলাতে হয়। Batch বেছে নেয় সম্পূর্ণতা, stream বেছে নেয় গতি — আর ভালো নকশা এই দুইয়ের মাঝে নিজের জায়গা স্পষ্ট করে বলে।

দিকBatchStream
ইনপুটসীমিত: একটি দিনের ফাইল, একটি টেবিলের snapshotঅসীম: log বা broker থেকে আসতে থাকা ঘটনা
কখন চলেনির্দিষ্ট সময়ে (প্রতি রাতে, প্রতি ঘণ্টায়), শেষ হয়সারাক্ষণ চলে, থামে না
উত্তরের দেরিমিনিট থেকে ঘণ্টাসাধারণত মিলিসেকেন্ড থেকে কয়েক সেকেন্ড
সম্পূর্ণতাইনপুট বন্ধ, তাই ফল চূড়ান্তদেরিতে আসা ডেটার জন্য অনুমান (watermark) লাগে
ভুল হলেকোড ঠিক করে আবার চালানো সহজচলমান state ঠিক করা কঠিন; log আবার চালাতে হয়
উদাহরণ কাজমাসিক বেতন, রাতের report, search index পুনর্গঠন, ML প্রশিক্ষণfraud সতর্কতা, লাইভ dashboard, ride-এর ভাড়া বাড়ানো, ট্রেন্ডিং হ্যাশট্যাগ

একটি গুরুত্বপূর্ণ উপলব্ধি: batch আসলে stream-এর একটি বিশেষ রূপ। গতকালের log হলো একটি stream, যার শেষ আছে। তাই আধুনিক অনেক engine একই API দিয়ে দুটোই চালায়। উল্টোটা সত্য নয় — অসীম ডেটাকে batch-এ চালাতে হলে কৃত্রিমভাবে টুকরো (যেমন প্রতি ঘণ্টা) করতে হয়, আর টুকরোর সীমায় পড়া ঘটনা (রাত ১১:৫৯-এ শুরু হওয়া একটি session) ভেঙে যায়। এই কথাটি অংশ ৫-এ আবার আসবে।

নিচের ছবিতে একটি সাধারণ বড় সিস্টেমের দুই পথ পাশাপাশি দেখুন। ঘটনাগুলো প্রথমে একটি log-এ জমে (পর্ব ৩৩); সেখান থেকে stream processor সেকেন্ডে উত্তর দেয়, আর একই ডেটা data lake-এ জমে রাতের batch কাজে যায়।

একই ঘটনা, দুই পথ — stream আর batch

Sources → log → stream processor (window, state) → sinks · log → data lake → batch job → warehouse

Stream আর batch — দুই পথের architecture উৎস · sources অ্যাপের click, পেমেন্ট, database-এর CDC Log · Kafka-ধাঁচের broker partition + offset, ক্রমিক আর replay করা যায় Stream processor window state event time + watermark checkpoint store Sink dashboard, alert, cache Data lake object storage-এ ফাইল Batch job MapReduce/Spark · রাতে Warehouse report, বিশ্লেষণ stream · সেকেন্ডে batch · ঘণ্টায়
ছবি ৩৬.১ — দুই পথের উৎস একই log. বাঁ দিকের পথ দ্রুত কিন্তু অনুমাননির্ভর উত্তর দেয়; ডান দিকের পথ ধীর কিন্তু পুরো ডেটা দেখে। অংশ ৮-এ দেখবেন, দুটো পথ রাখা (lambda) নাকি একটিতেই সব (kappa) — এটি নিজেই একটি নকশার সিদ্ধান্ত।

বাস্তবে কোন পথ লাগবে, সেটি আসে requirement থেকে (পর্ব ২)। “মাসিক বিক্রির report” — batch যথেষ্ট, সস্তাও। “সন্দেহজনক লেনদেন ২ সেকেন্ডের মধ্যে আটকাতে হবে” — stream ছাড়া উপায় নেই।

২ · MapReduce — map, shuffle আর reduce

ডাকঘরে চিঠি বাছাইয়ের দৃশ্য মনে করুন। সারা দেশ থেকে লাখো চিঠি আসে। এক দল কর্মী প্রতিটি চিঠি হাতে নিয়ে শুধু জেলার নামটি পড়েন আর একটি চিহ্ন দেন — এটি map. তারপর চিঠিগুলো জেলা অনুযায়ী বস্তায় ভরে আলাদা আলাদা টেবিলে পাঠানো হয়: “রাজশাহীর সব চিঠি তিন নম্বর টেবিলে” — এটি shuffle. শেষে প্রতিটি টেবিলের কর্মী নিজের জেলার চিঠিগুলো গুনে বা সাজিয়ে পরের ধাপে পাঠান — এটি reduce. কেউ পুরো স্তূপ একা দেখেন না, তবু শেষে সব চিঠি ঠিক জায়গায়।

২০০৪ সালে Google-এর Jeffrey Dean ও Sanjay Ghemawat OSDI সম্মেলনে “MapReduce: Simplified Data Processing on Large Clusters” প্রবন্ধে ঠিক এই ভাবনাটিকে একটি প্রোগ্রামিং মডেল হিসেবে প্রকাশ করেন। প্রোগ্রামার লেখেন শুধু দুটি ফাংশন; ভাগ করা, হাজার মেশিনে চালানো, মেশিন বিকল হলে আবার চালানো — সব করে framework. প্রবন্ধে বর্ণিত MapReduce মডেলটি এরকম:

map(k1, v1) → list(k2, v2)
shuffle: একই k2-এর সব v2 এক reducer-এ, partition = hash(k2) mod R
reduce(k2, list(v2)) → list(v2)

Map ধাপে (map phase) প্রতিটি map task ইনপুটের একটি টুকরো (split) পড়ে, আর প্রতিটি রেকর্ড থেকে শূন্য বা বেশি (key, value) জোড়া বানায়। Shuffle ধাপে প্রতিটি জোড়া hash দেখে R-টি reducer-এর একটিতে যায়, আর প্রতিটি reducer নিজের জোড়াগুলো key অনুযায়ী sort করে দল বাঁধে। Reduce ধাপে (reduce phase) প্রতিটি key-এর সব value একসাথে পেয়ে ফল লেখা হয়। সবচেয়ে চেনা উদাহরণ — কোন শব্দ কতবার এসেছে (word count). নিচের stepper-এ দুটি map task আর দুটি reducer দিয়ে পুরো পথ দেখুন:

  1. Split2 টুকরোইনপুট ভাগ
  2. Map(word, 1)প্রতিটি শব্দে একটি জোড়া
  3. Combinelocal sumনিজের মেশিনেই যোগ
  4. Shufflehash mod 2key অনুযায়ী পাঠানো
  5. Sort, groupkey → listreducer-এর ভেতরে
  6. Reducepart-0000rফল লেখা
ইনপুটদুটি split, প্রতিটিতে দুটি লাইন
split 0 → map task 0
  "ঢাকা rain ঢাকা traffic"
  "rain ঢাকা rain"
split 1 → map task 1
  "traffic jam ঢাকা"
  "jam jam rain"
Map outputমোট 7 + 6 = 13টি জোড়া
task 0: (ঢাকা,1) (rain,1) (ঢাকা,1) (traffic,1)
        (rain,1) (ঢাকা,1) (rain,1)
task 1: (traffic,1) (jam,1) (ঢাকা,1)
        (jam,1) (jam,1) (rain,1)
Combiner-এর পরে13 থেকে 7টি জোড়া
task 0: (ঢাকা,3) (rain,3) (traffic,1)
task 1: (traffic,1) (jam,3) (ঢাকা,1) (rain,1)
ShuffleFNV-1a(word) mod 2
reducer 0 ← traffic, ঢাকা   (দুই task থেকেই)
reducer 1 ← jam, rain
একই শব্দ সবসময় একই reducer-এ
Sort আর groupপ্রতিটি key-এর সব value একসাথে
reducer 0: traffic [1,1]   ঢাকা [3,1]
reducer 1: jam [3]         rain [3,1]
Outputপ্রতিটি reducer নিজের ফাইলে
part-00000: traffic=2  ঢাকা=4
part-00001: jam=3      rain=4
মোট শব্দ 13 = 2 + 4 + 3 + 4
  1. ধাপ ১ — ইনপুট ভাগ। চারটি লাইন দুটি split-এ ভাগ হলো, প্রতিটি একটি map task-এর কাজ। বাস্তবে একটি split সাধারণত distributed file system-এর একটি block — কয়েক ডজন থেকে শতাধিক MB.
  2. ধাপ ২ — Map. প্রতিটি শব্দের জন্য একটি (শব্দ, 1) জোড়া। task 0 দিল ৭টি, task 1 দিল ৬টি — মোট ১৩টি। Map একে অপরের কথা জানে না, তাই হাজারটি একসাথে চলতে পারে।
  3. ধাপ ৩ — Combine. পাঠানোর আগে নিজের মেশিনেই একই শব্দের 1-গুলো যোগ: task 0-এ ঢাকা ৩, rain ৩, traffic ১। নেটওয়ার্কে যাবে ১৩-এর বদলে ৭টি জোড়া।
  4. ধাপ ৪ — Shuffle. প্রতিটি জোড়ার key-এর hash নিয়ে mod 2: traffic আর ঢাকা যায় reducer 0-এ, jam আর rain যায় reducer 1-এ। দুই task-এর “ঢাকা” একই reducer-এ পৌঁছায় — reduce সঠিক হওয়ার শর্ত এটিই।
  5. ধাপ ৫ — Sort আর group. Reducer নিজের জোড়াগুলো key অনুযায়ী সাজিয়ে দল বাঁধে: ঢাকা পেল [3,1], rain পেল [3,1], traffic পেল [1,1], jam পেল [3].
  6. ধাপ ৬ — Reduce. প্রতিটি তালিকা যোগ: traffic=2, ঢাকা=4, jam=3, rain=4. মোট ১৩, ইনপুটের শব্দ-সংখ্যার সমান। প্রতিটি reducer নিজের আলাদা ফাইলে লেখে, তাই লেখার সময় কারও সাথে ঝগড়া নেই।

এবার একই কাজটি নিজে চালিয়ে দেখুন। কোডে একটি প্রক্রিয়ার ভেতরে দুটি map task আর দুটি reducer সাজানো হয়েছে; partition ঠিক হয় FNV-1a hash দিয়ে (FNV-1a, পর্ব ১৬-এ দেখেছেন)।

// MapReduce word count on 2 machines (map tasks) and 2 reducers, in one process.
const splits = [                                  // each split = one map task's input
  ["ঢাকা rain ঢাকা traffic", "rain ঢাকা rain"],
  ["traffic jam ঢাকা", "jam jam rain"] ];
const R = 2;                                      // number of reduce tasks
function fnv1a(s) {                               // FNV-1a 32-bit over UTF-16 units
  let h = 0x811c9dc5;
  for (const ch of s) { h ^= ch.charCodeAt(0); h = Math.imul(h, 0x01000193) >>> 0; }
  return h;
}
const part = (w) => fnv1a(w) % R;                 // partition function: hash(key) mod R
const mapFn = (line) => line.split(" ").map((w) => [w, 1]);   // emit (word, 1)
const combine = (pairs) => {                      // local reduce on the map machine
  const m = new Map(); for (const [w, c] of pairs) m.set(w, (m.get(w) || 0) + c);
  return [...m];
};
let sentPlain = 0, sentComb = 0;
const buckets = Array.from({ length: R }, () => new Map());
splits.forEach((lines, i) => {
  const out = lines.flatMap(mapFn), comb = combine(out);
  sentPlain += out.length; sentComb += comb.length;
  console.log(`map task ${i}: ` + out.map(([w, c]) => `(${w},${c})`).join(" "));
  console.log(`  after combiner: ` + comb.map(([w, c]) => `(${w},${c})`).join(" "));
  for (const [w, c] of comb) {                    // shuffle: send to reducer part(w)
    const b = buckets[part(w)]; if (!b.has(w)) b.set(w, []); b.get(w).push(c);
  }
});
buckets.forEach((b, r) => {
  const keys = [...b.keys()].sort();              // each reducer sees its keys sorted
  console.log(`\nreducer ${r} gets: ` + keys.map((k) => `${k} [${b.get(k)}]`).join("  "));
  console.log(`  output: ` + keys.map((k) => `${k}=${b.get(k).reduce((a, x) => a + x, 0)}`).join("  "));
});
console.log(`\npairs shuffled: ${sentPlain} without combiner, ${sentComb} with combiner`);

আউটপুট:

map task 0: (ঢাকা,1) (rain,1) (ঢাকা,1) (traffic,1) (rain,1) (ঢাকা,1) (rain,1)
  after combiner: (ঢাকা,3) (rain,3) (traffic,1)
map task 1: (traffic,1) (jam,1) (ঢাকা,1) (jam,1) (jam,1) (rain,1)
  after combiner: (traffic,1) (jam,3) (ঢাকা,1) (rain,1)

reducer 0 gets: traffic [1,1]  ঢাকা [3,1]
  output: traffic=2  ঢাকা=4

reducer 1 gets: jam [3]  rain [3,1]
  output: jam=3  rain=4

pairs shuffled: 13 without combiner, 7 with combiner

Map output-এ প্রতিটি শব্দ একবার করে: task 0-এ ৭টি, task 1-এ ৬টি জোড়া। Combiner একই মেশিনে যোগ করায় পাঠানো জোড়া ১৩ থেকে ৭ — প্রায় ৪৬% কম। Reducer 0 পেয়েছে ঢাকা [3,1]: একটি 3 এসেছে task 0 থেকে, একটি 1 task 1 থেকে। খেয়াল করুন, “traffic” আগে আর “ঢাকা” পরে সাজানো — কারণ sort হয় Unicode code point দিয়ে, আর Latin অক্ষরের code point বাংলার চেয়ে ছোট। বাস্তবে একই শব্দ লাখোবার আসে, তাই combiner-এর সাশ্রয় প্রায়ই অনেক গুণ বেশি (অনুশীলন ১)।

Combiner সব কাজে চলে না। Combiner আসলে একই reduce ফাংশন আগেভাগে map-এর মেশিনে চালানো, তাই ফাংশনটি associative আর commutative হতে হবে — যোগ, সর্বোচ্চ, সর্বনিম্ন চলে। গড় সরাসরি চলে না: (২, ৪)-এর গড় ৩ আর (৯)-এর গড় ৯-এর গড় ৬, কিন্তু ২, ৪, ৯-এর আসল গড় ৫। সমাধান: combiner পাঠাবে (যোগফল, সংখ্যা) — (৬, ২) আর (৯, ১) — আর reducer শেষে ভাগ করবে: ১৫ ÷ ৩ = ৫। Framework combiner-কে শূন্য, এক বা একাধিকবার চালাতে পারে; ফল যেন তাতে না বদলায়।

৩ · হাজার মেশিনে MapReduce — data locality, straggler আর ব্যর্থতা

মডেলটি সরল, কিন্তু হাজার মেশিনে চালাতে গেলে তিনটি বাস্তব সমস্যা আসে: ডেটা নড়াচড়ার খরচ, ধীর মেশিন, আর বিকল মেশিন। মূল প্রবন্ধের অনেকটাই এই তিনটির সমাধান।

Data locality — কাজকে ডেটার কাছে পাঠানো

ইনপুট থাকে distributed file system-এ — Google-এ GFS, open-source জগতে HDFS (দুটোই পর্ব ২৬-এ দেখেছেন)। প্রতিটি block-এর সাধারণত ৩টি কপি তিনটি মেশিনে। Scheduler তাই map task সেই মেশিনেই চালাতে চায় যেখানে block-এর একটি কপি আছে; না পারলে একই rack-এর মেশিনে। কয়েক MB কোড পাঠানো, কয়েকশো GB ডেটা টানার চেয়ে অনেক সস্তা। এর নাম data locality.

কিন্তু shuffle-এ locality নেই: প্রতিটি map task-এর output প্রতিটি reducer-এর কাছে যেতে পারে, অর্থাৎ M × R জোড়া সংযোগ। তাই বড় কাজে সবচেয়ে ব্যয়বহুল ধাপ সাধারণত shuffle — disk-এ লেখা, নেটওয়ার্কে পাঠানো, আবার sort করা। Combiner, compression আর ভালো partition — সবই shuffle ছোট করার চেষ্টা।

Straggler আর speculative execution

রেলস্টেশনের দশটি টিকিট কাউন্টারের নয়টিতে লাইন শেষ, একটিতে কম্পিউটার ধীর — পুরো যাত্রীদল সেই একটির জন্য দাঁড়িয়ে। MapReduce-এ কাজ শেষ হয় তখনই, যখন সবচেয়ে ধীর task শেষ হয়। খারাপ disk, ব্যস্ত CPU বা ভুল configuration-এর একটি মেশিন — একটি straggler — পুরো কাজ আটকে রাখে। সমাধান: কাজ শেষের দিকে এলে বাকি চলমান task-গুলোর একটি করে বাড়তি কপি (backup task) অন্য মেশিনে চালানো; যেটি আগে শেষ হয়, তার ফল নেওয়া, অন্যটি বাতিল। Hadoop-এ এর নাম speculative execution. প্রবন্ধের একটি পরীক্ষায় এই ব্যবস্থা বন্ধ করলে sort কাজ প্রায় ৪৪% বেশি সময় নেয় — কয়েক শতাংশ বাড়তি কাজের দামে বড় সাশ্রয়। এটি পর্ব ৪-এর hedged request-এর একই চিন্তা।

Speculative execution সব straggler সারায় না। কোনো key-তে অস্বাভাবিক বেশি ডেটা থাকলে — যেমন word count-এ “the” বা “এবং”, অথবা একটি ভাইরাল পোস্টের id — সেই reducer-টি ধীর হয় মেশিনের দোষে নয়, ডেটার ভারে। বাড়তি কপিও একই ভার টানবে। এটি data skew বা hot key-এর সমস্যা (পর্ব ২৩)। প্রতিকার: combiner, key-কে লবণ মিশিয়ে কয়েক টুকরো করা (key salting) আর শেষে টুকরোগুলো মেলানো।

ব্যর্থতা — আবার চালানোই সমাধান

হাজার মেশিনে প্রতিদিনই কিছু মেশিন বিকল হয়। MapReduce-এর উত্তর সরল: map আর reduce ফাংশন deterministic আর পার্শ্বপ্রতিক্রিয়াহীন হলে, বিকল মেশিনের task অন্য মেশিনে আবার চালালেই একই ফল আসবে। একজন master নিয়মিত ping করে বিকল worker চেনে। Map-এর output থাকে map মেশিনের local disk-এ, তাই মেশিন বিকল হলে শেষ হওয়া map task-ও আবার চালাতে হয়; reduce-এর output থাকে GFS-এ, তাই শেষ হওয়া reduce আর চালাতে হয় না। আর output ফাইল প্রথমে অস্থায়ী নামে লেখা হয়, শেষে atomic rename — তাই একই task দুবার চললেও চূড়ান্ত ফাইল একটিই। এই কৌশলটি পর্ব ৩৪-এর idempotent লেখার batch রূপ।

সীমা কোথায়: প্রতিটি MapReduce কাজ শেষে ফল disk-এর file system-এ লেখে, আর পরের কাজ আবার সেখান থেকে পড়ে। দশ ধাপের একটি pipeline মানে দশবার disk-এ লেখা আর পড়া। ML-এর মতো একই ডেটার ওপর বারবার ঘোরা (iterative) কাজে এটি খুব ধীর। পরের প্রজন্মের engine ঠিক এই জায়গাটিই বদলেছে।

৪ · Dataflow engine, batch join আর ETL

MapReduce-এর পরে আসে dataflow engine — Apache Spark তার সবচেয়ে পরিচিত উদাহরণ (UC Berkeley-তে গবেষণা প্রকল্প হিসেবে শুরু; এর মূল ধারণা RDD নিয়ে প্রবন্ধ NSDI ২০১২-তে)। মূল পরিবর্তন তিনটি:

  • পুরো কাজ একটি DAG: শুধু map আর reduce নয় — filter, join, group, sort-এর মতো অনেক operator, যারা একটি directed acyclic graph-এ জোড়া। Engine পুরো graph দেখে বুঝতে পারে কোন ধাপগুলো একসাথে চালানো যায়, আর কোথায় shuffle অবশ্যই লাগবে।
  • মাঝের ফল memory-তে: ধাপের মাঝে সব ফল disk-এর file system-এ লেখার বদলে যতটা সম্ভব memory-তে বা local disk-এ রাখা। পুনরাবৃত্ত কাজে এতে বড় গতি আসে; তবে কতটা — সেটি ডেটার আকার, memory আর কাজের ধরনের ওপর নির্ভর করে, আর memory না কুলালে ডেটা disk-এ নামেই (spill).
  • ব্যর্থতায় lineage: কোন ডেটা কোন ধাপে কী থেকে তৈরি — সেই রেসিপি মনে রাখা। একটি টুকরো হারালে শুধু সেই টুকরোটি রেসিপি মেনে আবার হিসাব করা হয়, পুরো কাজ নয়।

Spark-ধাঁচের engine DAG-কে stage-এ ভাগ করে: যে ধাপে প্রতিটি টুকরো শুধু নিজের ইনপুট দেখে (filter, map), সেগুলো এক stage-এ পাইপের মতো চলে; যে ধাপে অন্য টুকরোর ডেটা লাগে (group by key, join), সেখানে shuffle আর নতুন stage. তাই নকশা করার সময় প্রশ্নটি একই থাকে — কতবার shuffle হচ্ছে, আর প্রতিবার কত ডেটা?

Batch-এ join — sort-merge আর broadcast

ধরা যাক একটি কাল্পনিক ই-কমার্স সেবার ২ TB অর্ডার টেবিলকে ৫০ MB-র শহর টেবিলের সাথে join করতে হবে (city_id দিয়ে)। দুই কৌশল:

🔀 Sort-merge join

দুই টেবিলকেই join key দিয়ে partition আর sort করা (দুটোরই shuffle). তারপর প্রতিটি partition-এ দুটি সাজানো তালিকা পাশাপাশি হেঁটে মিল খোঁজা — ঠিক MapReduce-এর reduce-side join. দুই টেবিলই বিশাল হলে এটিই নিরাপদ পথ।

sort-merge join

📡 Broadcast (hash) join

ছোট টেবিলটি প্রতিটি worker-এ পুরো কপি পাঠানো, সেখানে memory-তে একটি hash table বানানো। বড় টেবিল নিজের জায়গাতেই থাকে, প্রতিটি সারি hash table-এ খুঁজে মেলে। বড় টেবিলের কোনো shuffle নেই।

broadcast join

এখানে broadcast স্পষ্ট জয়ী: ১০০টি worker-এ ৫০ MB পাঠাতে মোট ৫ GB, আর sort-merge-এ ২ TB-র পুরোটাই নেটওয়ার্কে ঘুরত। কিন্তু ছোট টেবিল worker-এর memory ছাড়ালে broadcast ব্যর্থ হয়। অনেক engine টেবিলের আকার দেখে নিজেই বেছে নেয়; সেই সীমা configuration-এ থাকে আর সংস্করণভেদে আলাদা। আর join key-তে skew থাকলে (একটি শহরে অর্ধেক অর্ডার) sort-merge-এর একটি partition বিশাল হয় — অংশ ৩-এর সেই hot key.

ETL, ELT, data lake আর warehouse

Batch-এর সবচেয়ে বড় ব্যবহার বিশ্লেষণের ডেটা তৈরি। পুরোনো ধারা ETL: উৎস থেকে Extract, আলাদা সার্ভারে Transform (পরিষ্কার, যোগ, সারসংক্ষেপ), তারপর warehouse-এ Load. নতুন ধারা ELT: কাঁচা ডেটা আগে সস্তা storage-এ বা warehouse-এ তুলে ফেলা, transform পরে warehouse-এর নিজের শক্তিশালী engine-এ, SQL দিয়ে। কাঁচা ফাইলের সেই বিশাল ভান্ডারকে বলে data lake — সাধারণত object storage-এ (পর্ব ২৬), column-ভিত্তিক ফাইল ফরম্যাটে (column store, পর্ব ১৯)। আর সাজানো, schema-মানা, report-এর জন্য তৈরি অংশ হলো data warehouse. Star schema, slowly changing dimension আর incremental load-এর বিস্তারিত পাবেন DBMS কোর্সের data warehouse পর্বে।

৫ · Stream processing-এর মূল — event time আর window

এবার অসীম ধারায় আসি। Stream processor একটি log বা broker থেকে (পর্ব ৩১, পর্ব ৩৩) ঘটনা পড়ে, প্রতিটির ওপর হিসাব করে, আর ফল পাঠায় sink-এ — একটি database, cache, dashboard বা আরেকটি topic-এ। কিছু কাজে কোনো স্মৃতি লাগে না: “প্রতিটি পেমেন্ট ৫ লাখ টাকার বেশি হলে সতর্কতা” — প্রতিটি ঘটনা আলাদাভাবে দেখা যায়। কিন্তু “গত ১ মিনিটে একই কার্ডে ৫টির বেশি লেনদেন” বলতে গেলে সময় ধরে গুনতে হয়। আর সেখানেই প্রশ্ন: কোন সময়?

Event time বনাম processing time

একজন ব্যবহারকারী মেট্রোরেলে বসে অ্যাপে তিনটি বোতাম চাপলেন; সুড়ঙ্গে নেটওয়ার্ক নেই, তাই অ্যাপ ঘটনাগুলো জমিয়ে রাখল, আর ২ মিনিট পরে স্টেশনে পৌঁছে একসাথে পাঠাল। প্রতিটি ঘটনার দুটি সময়: ফোনে চাপার মুহূর্ত — event time, আর processor-এর হাতে পৌঁছানোর মুহূর্ত — processing time. নেটওয়ার্ক, retry, broker-এর জমা, processor-এর বিরতি — সব মিলিয়ে দুটির ফারাক মিলিসেকেন্ড থেকে ঘণ্টা, আর ঘটনাগুলো পৌঁছায় এলোমেলো ক্রমে।

Processing time ধরে গোনা সহজ — processor-এর নিজের ঘড়ি দেখলেই হলো। কিন্তু তখন “১২:০০–১২:০১-এ কতজন কার্ট-এ পণ্য রেখেছেন”-এর উত্তর নির্ভর করে নেটওয়ার্কের মেজাজের ওপর, আর একই ডেটা আবার চালালে উত্তর বদলায়। ব্যবসার প্রায় সব প্রশ্ন আসলে event time-এর প্রশ্ন। অংশ ৬-এর simulator-এ দেখবেন ফারাকটি কত বড়।

তিন রকম window

🧱 Tumbling window

নির্দিষ্ট দৈর্ঘ্যের, পাশাপাশি, না-ঢাকা খোপ: ১২:০০–১২:০১, ১২:০১–১২:০২ … প্রতিটি ঘটনা ঠিক একটি window-এ। “প্রতি মিনিটে কত অর্ডার।”

tumbling window

🪟 Sliding window

দৈর্ঘ্য L, প্রতি S পরপর নতুন window শুরু (S < L), তাই window-গুলো একে অপরকে ঢাকে। প্রতিটি ঘটনা L ÷ S-টি window-এ পড়ে। “গত ১০ মিনিটের গড়, প্রতি মিনিটে হালনাগাদ।”

sliding window

💬 Session window

দৈর্ঘ্য নির্দিষ্ট নয়: একজন ব্যবহারকারীর কাজ চলতে থাকলে window চলতে থাকে; নির্দিষ্ট সময় (gap) কোনো কাজ না হলে বন্ধ। প্রতিটি key-এর নিজের window. “একটি ভিজিটে কতক্ষণ, কটি পাতা।”

session window

নামের গোলমাল: বিভিন্ন system একই জিনিসকে ভিন্ন নামে ডাকে। Flink-এর “sliding window”-কে Kafka Streams বলে “hopping window”; আর Kafka Streams-এ “sliding window” বলতে বোঝায় দুটি ঘটনার সময়ের ব্যবধান-ভিত্তিক একটি ভিন্ন ধারণা (লেখার সময়ে, সংস্করণভেদে বদলাতে পারে)। Documentation পড়ার সময় সংজ্ঞা মিলিয়ে নিন, নাম দেখে নয়।

Sliding window-এর খরচ একটু হিসাব করুন। দৈর্ঘ্য ১০ মিনিট, slide ১ মিনিট, আর সেকেন্ডে ৫০,০০০ ঘটনা। প্রতিটি ঘটনা ১০ ÷ ১ = ১০টি window-এ পড়ে, তাই সাদামাটা বাস্তবায়নে সেকেন্ডে ৫০,০০০ × ১০ = ৫,০০,০০০টি state হালনাগাদ। চালাক বাস্তবায়ন সময়কে ১ মিনিটের টুকরোয় (slice বা pane) ভাগ করে: প্রতিটি ঘটনা একটিই টুকরোয় যোগ হয় (সেকেন্ডে ৫০,০০০ হালনাগাদ), আর প্রতি মিনিটে শেষ ১০টি টুকরো মিলিয়ে ফল বানানো হয়। শর্ত আবার সেই combiner-এর — হিসাবটি যোগ করা যায় এমন হতে হবে।

Session window ধারায় সবচেয়ে মজার, কারণ দেরিতে আসা একটি ঘটনা দুটি আলাদা session-কে জুড়ে দিতে পারে। নিচের কোডে দুজন ব্যবহারকারীর click, gap ৫ মিনিট। নিয়ম: একটি click আগের session-এর প্রথম click-এর ৫ মিনিট আগে থেকে শেষ click-এর ৫ মিনিট পর পর্যন্ত (দুই প্রান্তসহ) পড়লে সেই session-এ যোগ হয়; session-এর window হলো [প্রথম click, শেষ click + gap). রিনার একটি click মোবাইল offline থাকায় দেরিতে পৌঁছাচ্ছে।

// Session windows: a session ends after GAP minutes with no click from that user.
const GAP = 5;
const clicks = [                       // [user, event time in minutes], in arrival order
  ["rina", 0], ["tanvir", 1], ["rina", 2], ["rina", 3], ["tanvir", 9], ["tanvir", 10],
  ["tanvir", 11], ["rina", 12], ["rina", 14], ["tanvir", 25], ["rina", 30],
  ["rina", 7] ];                       // the last click was made offline and arrives late
const sessions = new Map();            // user -> list of [first click, last click, clicks]
function add(user, t) {
  const list = sessions.get(user) || [];
  let s = [t, t, 1];
  for (const o of list.filter((o) => t >= o[0] - GAP && t <= o[1] + GAP)) { // touches o?
    s = [Math.min(s[0], o[0]), Math.max(s[1], o[1]), s[2] + o[2]];
    list.splice(list.indexOf(o), 1);   // merge: the old session is absorbed
  }
  list.push(s); list.sort((a, b) => a[0] - b[0]); sessions.set(user, list);
}
const show = () => { for (const [u, list] of sessions)
  console.log(`  ${u.padEnd(7)}` + list.map(([a, b, n]) => `[${a}-${b + GAP}) ${n} clicks`).join("   ")); };
clicks.forEach(([u, t], i) => {
  add(u, t);
  if (i === clicks.length - 2) { console.log(`before the late click (gap ${GAP} min):`); show(); }
});
console.log(`after the late click at minute ${clicks.at(-1)[1]}:`); show();

আউটপুট:

before the late click (gap 5 min):
  rina   [0-8) 3 clicks   [12-19) 2 clicks   [30-35) 1 clicks
  tanvir [1-6) 1 clicks   [9-16) 3 clicks   [25-30) 1 clicks
after the late click at minute 7:
  rina   [0-19) 6 clicks   [30-35) 1 clicks
  tanvir [1-6) 1 clicks   [9-16) 3 clicks   [25-30) 1 clicks

দেরির click আসার আগে রিনার তিনটি session: মিনিট ০–৩ (৩টি click, window শেষ ৩ + ৫ = ৮-এ), ১২–১৪ আর ৩০। মিনিট ৩ আর ১২-এর মাঝে ৯ মিনিটের ফাঁক, তাই আলাদা। তারপর মিনিট ৭-এর click এল: ৭ ≤ ৩ + ৫, তাই প্রথম session-এ পড়ে; আবার ৭ ≥ ১২ − ৫, তাই দ্বিতীয়টিতেও। দুটি session জুড়ে একটি হলো — [0-19), ৬টি click. তানভীরের session-এ কোনো বদল নেই, কারণ session window প্রতিটি key-এর আলাদা। এই জোড়া লাগার জন্যই session window-এর ফল চূড়ান্ত বলা যায় শুধু তখন, যখন নিশ্চিত হওয়া যায় যে মাঝের ফাঁকে আর কোনো দেরির ঘটনা আসবে না — অর্থাৎ watermark দেখে।

৬ · Watermark আর দেরিতে আসা ডেটা

Event time ধরে ১২:০০–১২:০১ window গুনছি। কিন্তু কখন বলব “এই মিনিটের গণনা শেষ, এবার ফল পাঠানো যাক”? ১২:০১ বাজলেই নয় — ১২:০০:৫৯-এর একটি ঘটনা হয়তো এখনো মেট্রোর সুড়ঙ্গে। অনন্তকাল অপেক্ষাও করা যায় না। এখানে আসে watermark: event time-এর একটি চলমান রেখা, যার অর্থ “আমার ধারণা, এর চেয়ে পুরোনো event time-এর ঘটনা আর আসবে না।” Watermark কোনো window-এর শেষ সময় পেরোলে সেই window বন্ধ হয়, ফল বেরোয়।

সবচেয়ে প্রচলিত হিসাব: watermark = এ পর্যন্ত দেখা সবচেয়ে বড় event time − একটি নির্দিষ্ট দেরি (bounded out-of-orderness). দেরি বড় রাখলে ফল সম্পূর্ণতর, কিন্তু দেরিতে; ছোট রাখলে দ্রুত, কিন্তু কিছু ঘটনা বাদ। অংশ ১-এর latency বনাম completeness এখানে একটি সংখ্যায় নেমে এল। নিচের stepper-এ ১ মিনিটের tumbling window আর watermark = সর্বোচ্চ event time − ১০ সেকেন্ড:

Tumbling window ১ মিনিট · watermark = সর্বোচ্চ event time − 10 s
যে ঘটনা এল (event time)
এ পর্যন্ত সর্বোচ্চ event time
Watermark
Window [0:00–1:00)
Window [1:00–2:00)
Sink-এ যা গেল
  1. A এল, event time 0:12. সর্বোচ্চ event time 0:12, তাই watermark 0:12 − 10 s = 0:02. A পড়ল [0:00–1:00) window-এ, গণনা ১।
  2. B এল, 0:41. Watermark এগিয়ে 0:31. প্রথম window-এ গণনা ২। Watermark এখনো 1:00-এর অনেক পেছনে, তাই window খোলা।
  3. C এল, 1:05. নতুন window [1:00–2:00) খুলল, গণনা ১। Watermark 0:55 — প্রথম window-এর শেষ (1:00) এখনো পেরোয়নি, তাই প্রথম window-ও খোলা থাকল, গণনা ২।
  4. D এল ক্রম ভেঙে — event time 0:58, কিন্তু C-এর পরে। সর্বোচ্চ event time বদলায়নি (1:05), watermark তাই 0:55-এই। প্রথম window খোলা ছিল বলে D বাঁচল: গণনা ৩। Watermark-এর ১০ সেকেন্ডের ছাড় ঠিক এই কাজেই।
  5. E এল, 1:14. Watermark 1:04 — এবার 1:00 পেরোল। প্রথম window বন্ধ, sink-এ গেল [0:00–1:00) = 3. দ্বিতীয় window-এ গণনা ২। এখন যদি 0:50-এর একটি ঘটনা আসে? সেটি পরের stepper-এ।

এবার বড় পরিসরে চালান। নিচের simulator-এ ২৪টি click, প্রায় প্রতি ১০ সেকেন্ডে একটি, ৪ মিনিট ধরে — অর্থাৎ প্রতি মিনিটে ঠিক ৬টি। প্রায় ২০% click খুব দেরিতে পৌঁছায় (৬০–১১৯ সেকেন্ড), বাকিরা ০–১৪ সেকেন্ডে। Watermark-এর ছাড় ৩০ সেকেন্ড। একই ঘটনা তিনভাবে গোনা হয়েছে: আসল (event time, সব ঘটনা), watermark-সহ event time, আর processing time.

// Event-time tumbling windows (60 s) with a watermark, vs processing-time windows.
function rng(seed) {                                   // mulberry32: seeded, repeatable
  return () => { 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 rand = rng(7), W = 60, DELAY = 30;              // watermark = largest event time seen - 30 s
const ev = [];
for (let i = 0; i < 24; i++) {                         // one click about every 10 s of event time
  const t = i * 10 + Math.floor(rand() * 10);
  const lag = rand() < 0.2 ? 60 + Math.floor(rand() * 60) : Math.floor(rand() * 15); // 20% very late
  ev.push({ id: "e" + i, t, arrive: t + lag });
}
ev.sort((a, b) => a.arrive - b.arrive || a.t - b.t);  // the processor sees arrival order
const fmt = (s) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
const bump = (m, k) => m.set(k, (m.get(k) || 0) + 1);
const open = new Map(), emitted = new Map(), truth = new Map(), proc = new Map(), late = [];; let maxT = -Infinity;
for (const e of ev) {
  const start = Math.floor(e.t / W) * W;
  bump(truth, start); bump(proc, Math.floor(e.arrive / W) * W);
  if (emitted.has(start)) late.push(`${e.id}(t=${fmt(e.t)}, came ${fmt(e.arrive)})`);
  else bump(open, start);
  maxT = Math.max(maxT, e.t);
  const wm = maxT - DELAY;
  for (const s of [...open.keys()].sort((a, b) => a - b))
    if (s + W <= wm) {                                 // window end passed: emit final result
      console.log(`at ${fmt(e.arrive)} watermark ${fmt(wm)} closes [${fmt(s)}-${fmt(s + W)}) count=${open.get(s)}`);
      emitted.set(s, open.get(s)); open.delete(s);
    }
}
console.log("still open: " + [...open].map(([s, c]) => `[${fmt(s)}-${fmt(s + W)}) ${c}`).join(", "));
console.log("late, dropped: " + late.join(" "));
console.log("\nwindow        true  event-time  processing-time");
for (const s of [0, 60, 120, 180, 240])
  console.log(`[${fmt(s)}-${fmt(s + W)})`.padEnd(13) + String(truth.get(s) || 0).padStart(5) +
    String(emitted.get(s) ?? open.get(s) ?? 0).padStart(12) + String(proc.get(s) || 0).padStart(17));

আউটপুট:

at 1:55 watermark 1:15 closes [0:00-1:00) count=5
at 2:48 watermark 2:04 closes [1:00-2:00) count=5
at 3:50 watermark 3:11 closes [2:00-3:00) count=5
still open: [3:00-4:00) 6
late, dropped: e0(t=0:00, came 1:58) e9(t=1:37, came 3:31) e17(t=2:53, came 4:00)

window        true  event-time  processing-time
[0:00-1:00)      6           5                4
[1:00-2:00)      6           5                5
[2:00-3:00)      6           5                6
[3:00-4:00)      6           6                7
[4:00-5:00)      0           0                2

Processing time-এর উত্তর ভুল: প্রতি মিনিটে আসল সংখ্যা ৬, কিন্তু processing-time window দেখাচ্ছে ৪, ৫, ৬, ৭, ২ — এমনকি এমন একটি মিনিটেও (4:00–5:00) ২টি, যখন কোনো click-ই হয়নি। দেরির ঘটনা পরের মিনিটে গোনা পড়েছে। মোট ঠিক আছে (২৪), কিন্তু সময়ের ছবি বিকৃত।

Watermark-সহ event time প্রায় ঠিক: ৫, ৫, ৫, ৬ (শেষ window এখনো খোলা)। প্রতিটি বন্ধ window থেকে একটি করে ঘটনা বাদ পড়েছে — e0, e9, e17 — কারণ তারা এসেছে window বন্ধ হওয়ার পরে। যেমন e0 ঘটেছে 0:00-এ, কিন্তু এসেছে 1:58-এ, আর প্রথম window বন্ধ হয়েছে 1:55-এ। তুলনা করুন: খুব দেরির ঘটনা ছিল ৫টি, বাদ পড়েছে ৩টি; বাকি দুটি (e7, e12) এসে পৌঁছেছে নিজের window বন্ধ হওয়ার আগেই — watermark-এর ৩০ সেকেন্ডের ছাড় আর সময়ের ভাগ্যে।

ফল বন্ধ হচ্ছে কখন, সেটিও দেখুন: প্রথম window-এর ফল এসেছে 1:55-এ — window শেষের প্রায় এক মিনিট পরে। এটিই সম্পূর্ণতার দাম। অনুশীলন ৪-এ DELAY বদলে দুটি দিক মেপে দেখবেন।

দেরির ডেটা — allowed lateness আর আলাদা পথ

Window বন্ধের পরে আসা ঘটনাকে বলে late data. তিনটি বিকল্প: ফেলে দেওয়া (simulator তাই করেছে); বন্ধ window-এর state কিছুক্ষণ রেখে ফল সংশোধন করা — allowed lateness; আর সীমার পরেও এলে আলাদা একটি ধারায় পাঠানো (side output), যাতে পরে batch-এ মিলিয়ে নেওয়া যায় বা অন্তত গোনা যায় কতগুলো হারাল। নিচের stepper-এ আগের অবস্থা থেকে শুরু, allowed lateness ৩০ সেকেন্ড:

আগের stepper-এর পর · allowed lateness = 30 s (window-এর state থাকবে watermark 1:30 পর্যন্ত)
যে ঘটনা এল (event time)
Watermark
Window [0:00–1:00)
Window [1:00–2:00)
Sink-এ যা গেল
  1. F এল, event time 0:50 — window বন্ধের পরে। Watermark 1:04, কিন্তু সীমা 1:00 + 30 s = 1:30, তাই state এখনো আছে। গণনা ৪, আর sink-এ গেল সংশোধিত ফল [0:00–1:00) = 4.
  2. G এল, 1:40. Watermark 1:30-এ পৌঁছাল, তাই প্রথম window-এর state চিরতরে মুছে গেল — memory চিরকাল ধরে রাখা যায় না। দ্বিতীয় window-এ গণনা ৩, watermark 2:00-এর আগে বলে এখনো খোলা।
  3. H এল, 0:45 — সীমার পরে। সংশোধনের আর উপায় নেই। H গেল side output-এ, যাতে হারানো ঘটনা গোনা বা পরে batch-এ মেলানো যায়। চুপচাপ ফেলে দিলে কেউ জানতেই পারত না।
  4. Sink-এর দায়। একই window-এর ফল দুবার এসেছে: আগে 3, পরে 4. Sink যদি যোগ করে, লিখবে 7 — ভুল। Window-এর key ধরে upsert (আগেরটি বদলে নতুনটি) করলে থাকবে 4. দেরির ডেটা মানে ফল একাধিকবার আসতে পারে, তাই sink-কে idempotent হতে হবে (পর্ব ৩৪)।
মূল ধারণা

Watermark একটি অনুমান, প্রতিশ্রুতি নয়। উৎস যদি নিজের ক্রম জানে (যেমন একটি partition-এ সময় অনুযায়ী লেখা log), watermark প্রায় নিখুঁত হতে পারে; মোবাইল অ্যাপের মতো উৎসে সবসময় কিছু ঘটনা watermark-এর পরে আসবে। তাই নকশায় তিনটি প্রশ্নের উত্তর লিখে রাখুন: কত দেরি পর্যন্ত অপেক্ষা (watermark-এর ছাড়), কত দেরি পর্যন্ত সংশোধন (allowed lateness), আর তার পরে আসা ঘটনার কী হবে (side output বা batch-এ মেলানো)।

আরেকটি বাস্তব ফাঁদ: নিষ্ক্রিয় উৎস। বহু partition থেকে পড়লে processor-এর watermark হয় সব partition-এর watermark-এর সর্বনিম্ন। রাতে একটি partition-এ কোনো ঘটনা না এলে তার watermark থেমে থাকে, আর পুরো processor-এর সব window আটকে যায়। অনেক engine-এ তাই “idle” partition চিহ্নিত করে বাদ রাখার ব্যবস্থা থাকে; নকশায় এটি মাথায় রাখতে হয়।

৭ · State, checkpoint, stream join আর exactly-once

Window-এর গণনা, session-এর তালিকা, “এই কার্ডে গত ১ মিনিটে কটি লেনদেন” — এসবই stream processor-এর state. State থাকে key অনুযায়ী ভাগ করা (keyed state): একই কার্ডের সব ঘটনা একই processor instance-এ যায় (log-এর partition key দিয়ে, পর্ব ৩৩), তাই state স্থানীয় memory বা local disk-এ রাখা যায়, প্রতিটি ঘটনায় দূরের database-এ যেতে হয় না। বড় state-এর জন্য অনেক engine ভেতরে একটি LSM-ধাঁচের embedded store ব্যবহার করে (পর্ব ১৯)।

Checkpoint — crash-এর পরে কোথা থেকে শুরু

Processor crash করলে memory-র state হারায়। সমাধান নিয়মিত checkpoint: একটি নির্দিষ্ট মুহূর্তে সব operator-এর state আর ইনপুট log-এর কোন offset পর্যন্ত পড়া হয়েছে — দুটোই একসাথে টেকসই storage-এ লেখা। Crash-এর পরে শেষ checkpoint-এর state ফিরিয়ে আনা হয়, আর log-কে সেই offset-এ ফিরিয়ে আবার পড়া শুরু হয়। ফলে checkpoint-এর পরের ঘটনাগুলো দ্বিতীয়বার প্রক্রিয়া হয়, কিন্তু state-ও সেই মুহূর্তে ফিরে গেছে — তাই state-এর হিসাবে প্রতিটি ঘটনার প্রভাব ঠিক একবার। এর জন্য উৎসকে replay করা যায় এমন হতে হবে; সাধারণ queue থেকে পড়া বার্তা মুছে গেলে (পর্ব ৩১) এই কৌশল চলে না, log চলে।

কিন্তু শত শত operator-এর state “একই মুহূর্তে” কীভাবে? সব বন্ধ করে ছবি তুললে ধারা থেমে যাবে। Flink-ধাঁচের engine-এ উৎস থেকে ধারার ভেতরে একটি বিশেষ চিহ্ন (barrier) পাঠানো হয়; প্রতিটি operator barrier পেলে নিজের state-এর ছবি তোলে আর barrier সামনে এগিয়ে দেয়। এটি Chandy–Lamport-এর distributed snapshot (১৯৮৫) ভাবনার একটি রূপ; Flink দলের প্রকাশিত বর্ণনা (Carbone ও সহকর্মীরা, ২০১৫) একে asynchronous barrier snapshotting বলে।

Exactly-once — কোন অর্থে

Engine-এর বিজ্ঞাপনে “exactly-once” শব্দটি খুব দেখা যায়। এর সৎ অর্থ: state আর output-এর ওপর প্রতিটি ঘটনার প্রভাব একবার — ঘটনা আসলে একাধিকবার প্রক্রিয়া হলেও। ভেতরের state-এর জন্য checkpoint যথেষ্ট। বাইরের sink-এর জন্য দুটির একটি লাগে: sink idempotent (window key ধরে upsert, যেমন উপরের stepper-এ), অথবা output checkpoint-এর সাথে transaction-এ বাঁধা (Kafka-র transaction, লেখার সময়ে Kafka ০.১১ থেকে, ২০১৭)। আর SMS বা email পাঠানোর মতো ফেরানো যায় না এমন কাজে “exactly-once” প্রায় কখনো সম্ভব নয় — সেখানে idempotency key আর deduplication. পুরো আলোচনা পর্ব ৩৪-এ।

Stream join — ঘটনার সাথে টেবিল মেলানো

প্রতিটি অর্ডার-ঘটনায় শুধু user_id আছে, কিন্তু dashboard-এ চাই ব্যবহারকারীর শহর। প্রতিটি ঘটনায় database-এ query করলে সেকেন্ডে লাখো query — database ভেঙে পড়বে, আর latency বাড়বে। ভালো পথ stream-table join (enrichment): user টেবিলের প্রতিটি বদল CDC-এর মাধ্যমে একটি log-এ আসে; processor সেই log পড়ে টেবিলটির একটি স্থানীয় কপি নিজের state-এ রাখে, আর প্রতিটি অর্ডার এলে স্থানীয়ভাবে মেলায়। Kafka Streams-এ এটি KStream-KTable join নামে পরিচিত; Flink-এ lookup বা temporal join (নাম আর খুঁটিনাটি সংস্করণভেদে আলাদা)। সূক্ষ্ম প্রশ্ন: অর্ডারটি মেলানো হবে ঘটনার সময়ের শহরের সাথে, নাকি এখনকার শহরের সাথে? Event-time-ভিত্তিক temporal join প্রথমটি দেয়; সাধারণ lookup দ্বিতীয়টি।

আরেক রকম stream-stream join: দুটি ধারার ঘটনা মেলানো, যেমন “বিজ্ঞাপন দেখানো” আর “বিজ্ঞাপনে click”. দুটি ধারাই অসীম, তাই একটি সময়সীমা লাগে — “দেখানোর ৩০ মিনিটের মধ্যে click” — নইলে state অসীম হয়। সীমা পেরোলে watermark দেখে পুরোনো ঘটনা state থেকে মোছা হয়।

ধীর sink, দ্রুত উৎস: sink-এর database ধীর হলে processor-এর ভেতরে বাফার ভরে, তারপর processor উৎস থেকে পড়া কমায় — log-ভিত্তিক নকশায় এটি স্বাভাবিক backpressure, কারণ ঘটনা log-এ নিরাপদে জমে থাকে (পর্ব ১৮)। কিন্তু consumer lag বাড়তে থাকলে watermark পিছিয়ে পড়ে, ফল দেরিতে আসে, আর log-এর retention পেরোলে ডেটা হারানোর ঝুঁকি। তাই lag-এর ওপর সতর্কতা রাখা stream নকশার অংশ।

৮ · Lambda বনাম kappa — দুই পথ, নাকি এক পথ

২০১০-এর দশকের শুরুতে stream engine-গুলো দ্রুত হলেও নির্ভরযোগ্য ছিল না — ঘটনা হারাত বা দুবার গুনত, আর দেরির ডেটা সামলাত না। Nathan Marz (Apache Storm-এর স্রষ্টা) তখন একটি নকশা প্রস্তাব করেন, যার নাম lambda architecture (২০১১ সালের একটি blog লেখা থেকে শুরু; পরে James Warren-এর সাথে লেখা “Big Data” বইয়ে, ২০১৫)। ভাবনাটি: সব কাঁচা ঘটনা অপরিবর্তনীয়ভাবে জমা রাখুন; একটি batch layer পুরো ইতিহাস থেকে নির্ভুল ফল বানাক; একটি speed layer শুধু সাম্প্রতিক ঘটনা থেকে দ্রুত আনুমানিক ফল দিক; আর query দুটো মিলিয়ে উত্তর দিক। পরের batch চললে speed layer-এর ভুল আপনিই মুছে যায়।

সমস্যা: একই ব্যবসার নিয়ম দুবার লিখতে হয় — দুটি ভিন্ন system-এ, প্রায়ই দুটি ভিন্ন API-তে — আর দুটিকে সবসময় মিলিয়ে রাখতে হয়। ২০১৪ সালে LinkedIn-এ Kafka তৈরিতে জড়িত Jay Kreps “Questioning the Lambda Architecture” লেখায় বিকল্প দেন, পরে যার নাম হয় kappa architecture: শুধু একটি stream পথ। সব ঘটনা যথেষ্ট দীর্ঘ retention-সহ log-এ রাখুন। Logic বদলালে নতুন সংস্করণের job log-এর শুরু থেকে আবার চালান, নতুন output টেবিলে লিখুন; সে বর্তমানে ধরে ফেললে পাঠকদের নতুন টেবিলে সরিয়ে পুরোনো job বন্ধ করুন।

দিকLambdaKappa
পথbatch + speed, query-তে মেলানোএকটিই stream পথ
কোডএকই নিয়ম দুবার, মিলিয়ে রাখার বোঝাএকবার লেখা
পুরোনো হিসাব নতুন করেbatch আবার চালানো, সহজlog replay; retention আর replay-এর গতির ওপর নির্ভর
শর্তbatch storage সস্তা, stream দুর্বল হলেও চলেনির্ভরযোগ্য stream engine, দীর্ঘ retention-এর log
দুর্বলতাদুই system চালানো, দুই রকম bugবছরের ডেটা replay ধীর ও ব্যয়বহুল হতে পারে

আজকের বাস্তবতা প্রায়ই মাঝামাঝি: একই engine বা একই SQL দিয়ে stream আর batch দুটোই লেখা, যাতে নিয়ম একবার লিখতে হয়। নাম মুখস্থের চেয়ে জরুরি প্রশ্ন: “logic-এ bug পেলে গত তিন মাসের ফল কীভাবে ঠিক করব?” এই প্রশ্নের উত্তর না থাকলে নকশা অসম্পূর্ণ।

সবসময় নির্ভুল উত্তর লাগে না: “আজ কতজন আলাদা ব্যবহারকারী” বা “সবচেয়ে বেশি খোঁজা ১০টি শব্দ” — নিখুঁত হিসাবে প্রতিটি আলাদা id মনে রাখতে হয়, state বিশাল হয়। HyperLogLog বা count-min sketch-এর মতো probabilistic কাঠামো কয়েক KB memory-তে সামান্য ভুলসহ উত্তর দেয়, আর তাদের মেলানো (merge) যায় — window আর partition জুড়ে কাজের জন্য আদর্শ। বিস্তারিত পর্ব ৪৫-এ।

আরও গভীরে: Google-এর MillWheel (VLDB ২০১৩) আর “The Dataflow Model” (Akidau ও সহকর্মীরা, VLDB ২০১৫) প্রবন্ধ event time, watermark, trigger আর ফল জমা-বনাম-বদলানোর (accumulation) ভাষা তৈরি করে, যা Apache Beam-এর ভিত্তি। Akidau, Chernyak ও Lax-এর বই “Streaming Systems” (২০১৮) এর সহজ পূর্ণ বিবরণ। Spark-এর মূল ধারণা Zaharia ও সহকর্মীদের RDD প্রবন্ধে (NSDI ২০১২), আর Flink-এর checkpoint-এর বর্ণনা “Lightweight Asynchronous Snapshots for Distributed Dataflows” (২০১৫)-এ। এক ধাপে MapReduce-এর সমালোচনা জানতে DeWitt ও Stonebraker-এর ২০০৮ সালের বিতর্কিত লেখাটিও পড়ে দেখতে পারেন।

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

  • MapReduce-এ shuffle কেন সবচেয়ে ব্যয়বহুল? — প্রতিটি map-এর output প্রতিটি reducer-এ যেতে পারে (M × R স্থানান্তর), disk-এ লেখা, নেটওয়ার্ক আর sort একসাথে; locality সেখানে খাটে না।
  • Event time আর processing time-এর পার্থক্য, আর কোনটায় window করবেন? — ঘটনার মুহূর্ত বনাম দেখার মুহূর্ত; ব্যবসার প্রশ্নে event time, watermark দিয়ে।
  • Watermark কী, আর late data কীভাবে সামলাবেন? — “এর পুরোনো আর আসবে না”-র অনুমান; allowed lateness-এ সংশোধন, তারপর side output, আর sink-এ upsert.
  • Lambda আর kappa-র পার্থক্য? — দুই পথ (batch + speed) বনাম এক stream পথ; kappa-য় পুরোনো হিসাব log replay করে।

৯ · AI-এর যুগে batch আর stream নকশা যাচাই

AI-এর কাছে একটি real-time analytics pipeline-এর নকশা চাইলে সে কয়েক সেকেন্ডেই সুন্দর একটি ছবি দেবে: Kafka, Flink, Spark, একটি data lake, একটি warehouse, সাথে Redis. কিন্তু দিনে ২০,০০০ ঘটনা হলে এই পুরো বহর চালানোর খরচ আর জটিলতা কাজের চেয়ে বহু গুণ বেশি; একটি database টেবিল আর প্রতি ৫ মিনিটের একটি SQL job হয়তো যথেষ্ট। AI সাধারণত যেসব জায়গায় ভুল করে:

AI-এর ভুলকেন সমস্যাকীভাবে ধরবেন
প্রতিটি প্রশ্নে “Kafka + Spark + Flink”ছোট load-এ অতিরিক্ত নকশা, বেশি খরচ, বেশি ব্যর্থতার জায়গাঘটনার হার আর latency-র প্রয়োজন সংখ্যায় লিখে নিজে হিসাব
Window-এর সময় কোনটি, বলে নাচুপচাপ processing time, ফলে বিকৃত সংখ্যাজিজ্ঞেস করুন: event time কোথা থেকে, watermark কত
Late data আর idle partition-এর কথা নেইঘটনা চুপচাপ হারায়, window আটকে থাকেsimulator-এ দেরির ঘটনা দিয়ে চালান
“End-to-end exactly-once” দাবি, email বা SMS-সহবাইরের পার্শ্বপ্রতিক্রিয়া ফেরানো যায় নাপ্রতিটি sink idempotent নাকি transactional — আলাদা করে মিলান
Shuffle-এর আকার বা task-সংখ্যার হিসাবে গোলমালTB আর TiB মেশানো, combiner বাদ দেওয়াএকক মিলিয়ে নিজে আবার হিসাব
Product-এর ডিফল্ট বা সীমা বানিয়ে বলাbroadcast-এর সীমা, retention, checkpoint-এর সময় সংস্করণভেদে আলাদাআপনার সংস্করণের official documentation

নিচের prompt-এ সংখ্যা, সময়ের ধরন আর ব্যর্থতার প্রশ্ন আগেই দিয়ে রাখা হয়েছে, যাতে উত্তরটি সাধারণ ছবি না হয়ে যাচাইযোগ্য হয়:

তুমি একজন অভিজ্ঞ data engineer. একটি কাল্পনিক ride-sharing অ্যাপে প্রতি সেকেন্ডে peak-এ প্রায় ৮,০০০টি অবস্থান-ঘটনা আসে (প্রতিটি প্রায় ২০০ byte); প্রায় ৫% ঘটনা ৩০ সেকেন্ড থেকে ৫ মিনিট দেরিতে পৌঁছায় (দুর্বল নেটওয়ার্ক)। দরকার: (১) এলাকা অনুযায়ী প্রতি মিনিটে চাহিদা, ৫ সেকেন্ডের মধ্যে dashboard-এ; (২) পরদিন সকালে চূড়ান্ত, নির্ভুল দৈনিক report. ১. event time নাকি processing time — কোনটি, কেন? watermark-এর ছাড় আর allowed lateness কত রাখবে, আর তার পরে আসা ঘটনার কী হবে? ২. প্রতি মিনিটে state-এর আকার আর সেকেন্ডে কত byte আসে, ধাপে ধাপে হিসাব দেখাও। ৩. processor crash করলে কী হবে, আর dashboard-এর টেবিলে একই window দুবার লেখা হলে কী হবে? ৪. lambda নাকি kappa — logic-এ bug পেলে গত ৩০ দিনের ফল কীভাবে ঠিক করবে? ৫. কোনো product-এর ডিফল্ট সেটিং বা সীমা বললে সেটি "যাচাই করতে হবে" বলে চিহ্নিত করো।

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

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

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

প্র ০১MapReduce মাত্র দুটি ফাংশনের একটি মডেল — তবু কেন এটিকে যুগান্তকারী বলা হয়, আর পরে কেন শিল্পজগৎ Spark-ধাঁচের engine-এ সরে গেল?

MapReduce-এর অবদান নতুন কোনো algorithm নয় — map আর reduce ধারণা functional programming-এ বহু পুরোনো, আর parallel database-এ hash-ভিত্তিক partition-ও আগে থেকে ছিল। নতুন ছিল বিমূর্ততার সীমারেখা। ২০০৪ সালের প্রবন্ধে Dean ও Ghemawat দেখান, প্রোগ্রামার যদি শুধু দুটি deterministic ফাংশন দেন, তাহলে parallel করা, ডেটা ভাগ, মেশিন বিকল হলে আবার চালানো, ধীর মেশিনের জন্য backup task — সব framework নিজে করতে পারে। ফলে distributed systems-এর বিশেষজ্ঞ না হয়েও একজন সাধারণ প্রকৌশলী হাজার সস্তা মেশিনে টেরাবাইট ডেটা প্রক্রিয়া করতে পারতেন। প্রবন্ধটি জানায়, Google-এর ভেতরে বছর দেড়েকের মধ্যে কয়েকশো এমন প্রোগ্রাম লেখা হয়েছিল, আর search index তৈরির মূল কাজটিও এতে সরানো হয়েছিল।

প্রবন্ধ পড়ে Doug Cutting ও Mike Cafarella open-source Hadoop তৈরি করেন (Nutch প্রজেক্ট থেকে, ২০০৬ সালে আলাদা প্রজেক্ট, Yahoo-র বড় সমর্থনে)। এক দশক ধরে “big data” মানেই ছিল Hadoop. সমালোচনাও ছিল: ২০০৮ সালে David DeWitt ও Michael Stonebraker একটি বিতর্কিত লেখায় বলেন, MapReduce database গবেষণার schema, index আর query optimizer-এর শিক্ষা উপেক্ষা করছে।

সরে যাওয়ার মূল কারণ প্রতিটি কাজের শেষে disk-এ লেখা। দশ ধাপের pipeline মানে দশটি আলাদা কাজ, প্রতিটির মাঝে replicated file system-এ পুরো ফল লেখা। ML-এর মতো একই ডেটার ওপর বারবার ঘোরা কাজে বা ইন্টারঅ্যাকটিভ বিশ্লেষণে এটি খুব ধীর। Spark-এর RDD (NSDI ২০১২) মাঝের ফল memory-তে রাখে, পুরো কাজকে একটি DAG হিসেবে দেখে অপ্রয়োজনীয় লেখা বাদ দেয়, আর ব্যর্থতায় lineage থেকে শুধু হারানো অংশ আবার হিসাব করে। একই সাথে একটি API-তে SQL, stream আর ML.

মূল উপলব্ধি: ভালো বিমূর্ততা কঠিন অংশগুলো (ব্যর্থতা, parallelism) লুকিয়ে সাধারণ মানুষকে শক্তি দেয়; কিন্তু যে খরচ সে লুকায় (এখানে disk I/O), প্রযুক্তি বদলালে সেটিই তার সীমা হয়ে দাঁড়ায়।

প্র ০২Processing time ধরে window করা এত সহজ — তাহলে বড় সিস্টেমগুলো কেন event time আর watermark-এর জটিলতায় যায়?

কারণ processing time-এর উত্তর একটি ভিন্ন প্রশ্নের উত্তর। “এই মিনিটে processor কটি ঘটনা দেখল” — এটি সিস্টেমের স্বাস্থ্যের প্রশ্ন, ব্যবহারকারীর আচরণের নয়। অংশ ৬-এর simulator-এ প্রতি মিনিটে আসল click ৬টি, কিন্তু processing-time গণনা ৪, ৫, ৬, ৭, ২ — এমনকি একটি ফাঁকা মিনিটেও ২। বাস্তবে বিকৃতি আরও বড় হতে পারে: একটি broker কয়েক মিনিট আটকে থেকে আবার চললে processor এক মুহূর্তে হাজারো পুরোনো ঘটনা পায়, আর dashboard-এ হঠাৎ একটি “spike” দেখা দেয় যা কখনো ঘটেনি।

দ্বিতীয় সমস্যা পুনরুৎপাদনযোগ্যতা। একই ডেটা আবার চালালে (bug ঠিক করে, বা kappa-র replay-তে) processing-time-এর ফল সম্পূর্ণ বদলে যায়, কারণ replay-তে সব ঘটনা কয়েক মিনিটেই চলে আসে। Event time-এ ফল ঘটনার নিজস্ব তথ্য থেকে আসে, তাই আবার চালালেও একই — batch আর stream একই উত্তর দিতে পারে।

এই চিন্তাটি জনপ্রিয় হয় Google-এর MillWheel (২০১৩) আর “The Dataflow Model” (২০১৫) প্রবন্ধে, যারা মোবাইল আর বিজ্ঞাপনের ডেটার এলোমেলো দেরি নিয়ে কাজ করছিল। তারা প্রশ্নটিকে চার ভাগে ভাঙে: কী হিসাব (যোগ, গড়), event time-এর কোথায় (window), processing time-এর কখন ফল বেরোবে (watermark আর trigger), আর দেরির ফল আগের ফলের সাথে কীভাবে মিলবে (জমা নাকি বদল)। Apache Flink আর Apache Beam এই ভাষা গ্রহণ করে।

তবে event time-এরও দাম আছে: ফোনের ঘড়ি ভুল হতে পারে (ভবিষ্যতের timestamp!), তাই ঘটনার সময়ের যুক্তিসংগত সীমা যাচাই লাগে; watermark-এর জন্য অপেক্ষা latency বাড়ায়; state-কে দেরির ডেটার জন্য ধরে রাখতে memory লাগে।

মূল উপলব্ধি: প্রশ্নটি যে সময়ের, হিসাবও সেই সময়ে করতে হয়। Processing time মাপে সিস্টেমকে, event time মাপে জগৎকে — আর বেশিরভাগ ব্যবসার প্রশ্ন জগৎ নিয়ে।

প্র ০৩Watermark-এর ছাড় কত রাখবেন — ৫ সেকেন্ড, ১ মিনিট, নাকি ১ ঘণ্টা? এটি কি শুধুই প্রযুক্তিগত সিদ্ধান্ত?

না — এটি একটি ব্যবসার সিদ্ধান্ত, যা প্রযুক্তি দিয়ে মাপা যায়। ছাড় বাড়ালে window দেরিতে বন্ধ হয়, অর্থাৎ ফলের latency বাড়ে, আর বেশি সময় ধরে বেশি window খোলা রাখতে হয় বলে state বাড়ে। ছাড় কমালে ফল দ্রুত, কিন্তু বেশি ঘটনা late হয় — হয় বাদ পড়ে, নয় সংশোধন লাগে।

সঠিক উত্তর পেতে প্রথমে দেরির বণ্টন মাপুন: প্রতিটি ঘটনায় processing time − event time লিখে histogram বানান (পর্ব ৪-এর percentile-এর চিন্তা)। ধরা যাক p99 দেরি ২০ সেকেন্ড আর p99.9 দেরি ৪ মিনিট। তখন ছাড় ২০ সেকেন্ড রাখলে প্রায় ১% ঘটনা late হবে; ৪ মিনিট রাখলে ০.১%, কিন্তু প্রতিটি ফল ৪ মিনিট দেরিতে।

তারপর ব্যবহার অনুযায়ী ভাগ করুন। Fraud সতর্কতা — দ্রুততা মূল্যবান, কিছু ঘটনা বাদ পড়লেও চলে, কারণ পরে batch-এ মিলিয়ে দেখা হয়; ছোট ছাড়। বিলিং বা বিক্রেতার পাওনা — প্রতিটি ঘটনা জরুরি, latency কম জরুরি; বড় allowed lateness, side output, আর শেষে batch-এ চূড়ান্ত হিসাব। লাইভ dashboard — আগে দ্রুত আনুমানিক ফল, পরে সংশোধন; Dataflow মডেলের ভাষায় “early” আর “late” trigger, যাতে একই window-এর ফল কয়েকবার হালনাগাদ হয়।

আর একটি নীতি: হারানো ঘটনা গুনুন। Late বা side output-এ যাওয়া ঘটনার হার একটি metric হওয়া উচিত। কোনো নতুন অ্যাপ সংস্করণ offline-এ ঘটনা জমিয়ে পাঠানো শুরু করলে হার হঠাৎ বাড়বে — আর আপনি জানবেন watermark-এর নীতি বদলাতে হবে।

মূল উপলব্ধি: Watermark-এর ছাড় latency, সম্পূর্ণতা আর খরচের একটি দর-কষাকষি। সেটি মাপা দেরির বণ্টন আর ব্যবসার প্রয়োজন থেকে ঠিক করুন, আর যা হারাচ্ছে তা সবসময় গুনে রাখুন।

প্র ০৪Stream engine-গুলো “exactly-once” প্রতিশ্রুতি দেয়। নেটওয়ার্ক আর crash-এর জগতে এটি কি সত্যিই সম্ভব, নাকি একটি বিপণনের শব্দ?

দুটোই, শব্দটি কোন অর্থে বলা হচ্ছে তার ওপর নির্ভর করে। একটি বার্তা নেটওয়ার্কে “ঠিক একবার পৌঁছানো” সাধারণভাবে নিশ্চিত করা যায় না — ack হারালে প্রেরক জানে না বার্তা পৌঁছেছে কি না, তাই হয় আবার পাঠায় (দুবারের ঝুঁকি), নয় পাঠায় না (হারানোর ঝুঁকি)। এই কথাটি পর্ব ৩৪-এর মূল বিষয়।

Engine যা দেয় তা হলো প্রভাবের দিক থেকে একবার (অনেকে বলেন effectively-once): ঘটনা হয়তো দুবার প্রক্রিয়া হয়েছে, কিন্তু state আর output-এ তার ছাপ একবার। কৌশলটি অংশ ৭-এ দেখেছেন — state আর ইনপুট offset একসাথে checkpoint, crash-এ দুটোকেই একসাথে পেছনে নেওয়া। Flink-এর barrier-ভিত্তিক snapshot (Carbone ও সহকর্মীদের ২০১৫ সালের বর্ণনা) এভাবে কাজ করে। Kafka ০.১১ (২০১৭) idempotent producer আর transaction যোগ করে, যাতে “পড়া offset লেখা” আর “output লেখা” একটি atomic কাজ হয়; Kafka Streams এর ওপর exactly-once দেয়।

সীমা দুটি। প্রথমত, প্রতিশ্রুতি শুধু সেই সিস্টেমের ভেতরে, যারা এই protocol-এ অংশ নেয়। বাইরের একটি REST API ডাকা, email পাঠানো, বা একটি সাধারণ database-এ insert — engine এগুলো ফেরাতে পারে না; replay-তে আবার ঘটবে। তাই sink-কে নিজেই idempotent হতে হয় (window key ধরে upsert, idempotency key, deduplication টেবিল), অথবা দুই-ধাপের commit-এ অংশ নিতে হয়। দ্বিতীয়ত, দাম: checkpoint-এর মাঝে transactional output প্রকাশ হয় শুধু commit-এর পরে, তাই নিচের পাঠকেরা ফল দেখে checkpoint-এর সময়সীমা পরিমাণ দেরিতে।

মূল উপলব্ধি: exactly-once delivery নয়, exactly-once effect — আর সেটি পাওয়া যায় replay করা যায় এমন উৎস, সমন্বিত checkpoint আর idempotent বা transactional sink মিলিয়ে। এর একটিও বাদ পড়লে প্রতিশ্রুতি ভাঙে।

প্র ০৫Kappa architecture যদি সরল হয়, তাহলে lambda এখনো কোথাও টিকে আছে কেন? একটি নতুন দলের জন্য আপনি কোনটি সুপারিশ করবেন?

Lambda-র জন্ম একটি নির্দিষ্ট সময়ের বাস্তবতায়। ২০১১ সালের দিকে stream engine (Storm-এর প্রথম দিকের সংস্করণসহ) সাধারণত at-least-once দিত, state-এর ভালো checkpoint ছিল না, দেরির ডেটা সামলানোর ভাষা ছিল না। Nathan Marz-এর সমাধান ছিল বিনয়ী: stream থেকে দ্রুত কিন্তু আনুমানিক ফল নিন, আর অপরিবর্তনীয় কাঁচা ডেটা থেকে batch-এ নির্ভুল ফল বানিয়ে প্রতিবার আগেরটি বদলে দিন। ভুল হলে batch আবার চালালেই সব ঠিক — মানুষের ভুলের প্রতি সহনশীলতা ছিল এর বড় যুক্তি।

২০১৪ সালে Jay Kreps যুক্তি দেন, দুই codebase মিলিয়ে রাখাই আসল সমস্যা, আর stream engine যথেষ্ট নির্ভরযোগ্য হলে log replay-ই batch-এর কাজ করতে পারে। পরের বছরগুলোতে Flink-এর checkpoint, Kafka-র transaction আর event-time window সেই নির্ভরযোগ্যতা এনেছে।

তবু lambda-ধাঁচ টিকে আছে কয়েকটি কারণে। (১) Replay-এর খরচ: কয়েক বছরের ডেটা stream engine-এ ক্রমানুসারে আবার চালানো batch engine-এর বিশাল parallel scan-এর চেয়ে ধীর ও দামি হতে পারে। (২) ভিন্ন প্রশ্ন: ML প্রশিক্ষণ, জটিল বহু-টেবিল join বা বছরভিত্তিক report স্বভাবতই batch-এর। (৩) নিয়ন্ত্রক বা হিসাবের প্রয়োজন: আর্থিক হিসাবের দিন-শেষের মিলানো (reconciliation) প্রায়ই আলাদা batch-এ নিরীক্ষাযোগ্যভাবে করা হয়, stream-এর ফলের পাশাপাশি।

একটি নতুন, ছোট দলের জন্য সাধারণ পরামর্শ: আগে প্রশ্ন করুন stream লাগবে কি না — প্রতি ৫ মিনিটের batch অনেক সময় যথেষ্ট আর অনেক সহজ। লাগলে একটি engine বা একটি SQL ভাষা বেছে নিন যা batch আর stream দুটোই চালায়, যাতে নিয়ম একবার লিখতে হয়; কাঁচা ঘটনা সস্তা object storage-এ চিরকাল রাখুন; আর “logic-এ bug হলে পুরোনো ফল কীভাবে ঠিক হবে” — এর উত্তর নকশায় লিখে রাখুন।

মূল উপলব্ধি: Lambda আর kappa দুটি নাম, আসল প্রশ্ন একটি — নির্ভুলতা, দ্রুততা আর পুনঃহিসাবের খরচের মধ্যে কোথায় দাঁড়াবেন, আর কটি system চালানোর সামর্থ্য আপনার দলের আছে।

অনুশীলন

হিসাবগুলো কাগজে করুন, তারপর উত্তর মিলিয়ে নিন। কোড বদলের কাজে আগে অনুমান লিখুন, তারপর চালান।

  1. Shuffle-এর হিসাব: একটি word count কাজে ২ TiB টেক্সট, block বা split ১২৮ MiB. ক্লাস্টারে ২০০টি মেশিন, প্রতিটিতে একসাথে ৮টি map task চলতে পারে। (ক) কতটি map task, আর কত “ঢেউয়ে” (wave) চলবে? (খ) প্রতিটি map task গড়ে ৩২০ MiB জোড়া বানায়; combiner দিলে প্রতিটির output নামে ৩.২ MiB-তে। দুই ক্ষেত্রে মোট shuffle কত? (গ) ধরা যাক পুরো ক্লাস্টার shuffle-এ সেকেন্ডে মোট ২০ GB (দশমিক) সামলাতে পারে। দুই ক্ষেত্রে শুধু shuffle-এ কত সময়?

    (ক) ২ TiB ÷ ১২৮ MiB = (২ × ১০২৪ × ১০২৪ MiB) ÷ ১২৮ MiB = ১৬,৩৮৪টি map task. একসাথে চলে ২০০ × ৮ = ১,৬০০টি। ১৬,৩৮৪ ÷ ১,৬০০ = ১০.২৪, তাই ১১টি ঢেউ — শেষ ঢেউটি প্রায় খালি (মাত্র ৩৮৪টি task), আর সেখানেই straggler-এর প্রভাব সবচেয়ে বেশি চোখে পড়ে।

    (খ) Combiner ছাড়া: ১৬,৩৮৪ × ৩২০ MiB = ৫২,৪২,৮৮০ MiB = ৫,১২০ GiB = ৫ TiB — ইনপুটের আড়াই গুণ! কারণ প্রতিটি ছোট শব্দের সাথে একটি “1” আর key-এর বাড়তি খরচ। Combiner-সহ: ১৬,৩৮৪ × ৩.২ MiB = ৫২,৪২৮.৮ MiB = ৫১.২ GiB — ১০০ গুণ কম।

    (গ) ৫ TiB = ৫ × ১০২৪⁴ byte ≈ ৫,৪৯৭.৬ GB; ÷ ২০ GB/s ≈ ২৭৫ সেকেন্ড, প্রায় ৪.৬ মিনিট। ৫১.২ GiB ≈ ৫৪.৯৮ GB; ÷ ২০ ≈ ২.৭৫ সেকেন্ড. খেয়াল করুন TiB (১০২৪⁴) আর TB (১০০০⁴)-এর ফারাক প্রায় ১০% — একক না মেলালে AI-এর মতো আপনিও ভুল করবেন (পর্ব ৩)। বাস্তবে disk-এ লেখা, sort আর skew-এর কারণে সময় আরও বেশি লাগবে; এই হিসাব শুধু নিচের সীমা।

  2. নকশা সমালোচনা: একটি কাল্পনিক mobile banking সেবার fraud দলের প্রস্তাব: “প্রতিটি লেনদেন এলে processor-এর ঘড়ি ধরে ১ মিনিটের window-এ গুনব; কোনো অ্যাকাউন্টে এক window-এ ৫টির বেশি লেনদেন হলে অ্যাকাউন্ট আটকে SMS পাঠাব। State থাকবে processor-এর memory-তে, checkpoint লাগবে না — crash হলে নতুন করে শুরু করব।” অন্তত চারটি সমস্যা আর তার সমাধান লিখুন।
    • Processing time: broker কিছুক্ষণ আটকে থেকে আবার চললে কয়েক মিনিটের লেনদেন এক মিনিটে আসবে — নিরীহ গ্রাহকের অ্যাকাউন্ট আটকাবে। আবার উল্টোটাও: দ্রুত ৬টি লেনদেন দুই window-এ ভাগ হয়ে ধরা পড়বে না। সমাধান: event time (লেনদেনের সার্ভার-সময়), ছোট watermark-এর ছাড়, আর tumbling-এর বদলে sliding window বা “শেষ ১ মিনিট” ধরনের গণনা, যাতে window-এর সীমায় ভাগ হয়ে ফাঁক না থাকে।
    • Checkpoint নেই: crash-এর পরে “নতুন করে শুরু” মানে state শূন্য — crash-এর আগের ৪টি লেনদেন ভুলে যাবে, প্রতারক ঠিক তখনই সুযোগ পাবে। আবার log-এর শুরু থেকে পড়লে পুরোনো সব সতর্কতা নতুন করে যাবে। সমাধান: state আর offset একসাথে checkpoint.
    • SMS-এর পার্শ্বপ্রতিক্রিয়া: replay-তে একই গ্রাহক দুবার SMS পাবেন, অ্যাকাউন্ট আটকানোর আদেশও দুবার যাবে। সমাধান: (অ্যাকাউন্ট, window) ধরে idempotency key, আর SMS আলাদা notification সেবায় (পর্ব ৫৭) যা deduplication করে।
    • Late data আর hot key: দেরির লেনদেন কোথায় যাবে, তা বলা নেই — side output আর দিন-শেষের batch যাচাই দরকার। আর একটি বড় ব্যবসায়ী অ্যাকাউন্টে স্বাভাবিকভাবেই মিনিটে শত লেনদেন — নিয়ম আর partition দুটোই আলাদা করে ভাবতে হবে।
  3. আউটপুট অনুমান: Session window-এর কোডে (session_windows.js) clicks তালিকা বদলে দিন: সুমির click মিনিট ০, ৪, ৬, ১৫, ১৮, ৪০-এ, আর শেষে দেরিতে পৌঁছানো একটি click মিনিট ১০-এ। Gap ৫ মিনিট, নিয়ম একই। দেরির click-এর আগে আর পরে কী ছাপা হবে? আগে খাতায় লিখুন, তারপর চালান।

    তালিকাটি হবে [["sumi",0],["sumi",4],["sumi",6],["sumi",15],["sumi",18],["sumi",40],["sumi",10]]. চালালে:

    before the late click (gap 5 min):
      sumi   [0-11) 3 clicks   [15-23) 2 clicks   [40-45) 1 clicks
    after the late click at minute 10:
      sumi   [0-23) 6 clicks   [40-45) 1 clicks

    প্রথমে ০, ৪, ৬ একসাথে (ফাঁক ৪ আর ২, দুটোই ≤ ৫) — window শেষ ৬ + ৫ = ১১-এ। ১৫ আর ১৮ আলাদা session (৬ থেকে ১৫-এ ফাঁক ৯), window শেষ ১৮ + ৫ = ২৩। ৪০ একা। দেরির ১০: ১০ ≤ ৬ + ৫ = ১১, তাই প্রথম session ছোঁয়; ১০ ≥ ১৫ − ৫ = ১০, তাই দ্বিতীয়টিও (প্রান্তসহ নিয়মে)। দুটি জুড়ে [0-23), ৬টি click. নিয়মটি “কঠোরভাবে ৫-এর কম” হলে ১৫ − ১০ = ৫ দ্বিতীয়টিকে ছুঁত না, ফল হতো দুটি আলাদা session — সীমারেখার নিয়মটি তাই নকশায় স্পষ্ট লিখে রাখতে হয়, আর engine-ভেদে এটি আলাদা হতে পারে।

  4. Watermark-এর দাম মাপুন: watermark_windows.js-এ DELAY = 30-কে প্রথমে DELAY = 10, তারপর DELAY = 120 করুন। প্রতিবার কতগুলো ঘটনা late হলো আর প্রথম window কখন বন্ধ হলো, লিখুন। কোন মান কোন ব্যবহারের জন্য ভালো?

    চালিয়ে পাওয়া ফল:

    DELAY = 10
    at 1:29 watermark 1:11 closes [0:00-1:00) count=5
    at 2:13 watermark 2:03 closes [1:00-2:00) count=4
    at 3:16 watermark 3:06 closes [2:00-3:00) count=4
    still open: [3:00-4:00) 6
    late, dropped: e0(t=0:00, came 1:58) e7(t=1:18, came 2:26) e12(t=2:08, came 3:18) e9(t=1:37, came 3:31) e17(t=2:53, came 4:00)
    
    DELAY = 120
    at 3:11 watermark 1:09 closes [0:00-1:00) count=6
    still open: [1:00-2:00) 6, [2:00-3:00) 6, [3:00-4:00) 6
    late, dropped: 

    ছাড় ১০ সেকেন্ড: প্রথম window বন্ধ হয় 1:29-এ (৩০ সেকেন্ডের ছাড়ে ছিল 1:55), কিন্তু late ঘটনা ৩ থেকে বেড়ে ৫টি — খুব দেরির পাঁচটির সবই (e0, e7, e12, e9, e17) বাদ, আর গণনা ৫, ৪, ৪। ছাড় ১২০ সেকেন্ড: কোনো ঘটনাই late নয়, প্রথম window-এর গণনা নিখুঁত ৬ — কিন্তু সেটি বেরোয় 3:11-এ, window শেষের ২ মিনিটেরও বেশি পরে, আর শেষ পর্যন্ত বাকি তিনটি window খোলাই থাকে (তাদের state memory-তে জমা)। অর্থাৎ ছোট ছাড় দ্রুত কিন্তু অসম্পূর্ণ — fraud সতর্কতা বা লাইভ dashboard-এর জন্য, যেখানে পরে batch-এ মেলানো হয়; বড় ছাড় সম্পূর্ণ কিন্তু দেরিতে — বিলিং বা পাওনার হিসাবের জন্য।

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

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

ক্রমএকটি MapReduce কাজের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📄 ইনপুট split-এ ভাগ, প্রতিটি split-এর জন্য একটি map task
  2. 🗂️ Map — প্রতিটি রেকর্ড থেকে (key, value) জোড়া
  3. ➕ Combiner — map-এর মেশিনেই একই key-এর আংশিক যোগ
  4. 🚚 Shuffle — hash(key) mod R দেখে জোড়া reducer-এ পাঠানো
  5. 🔤 Reducer-এ key অনুযায়ী sort আর group
  6. ✅ Reduce — প্রতিটি key-এর ফল output ফাইলে লেখা

একটি MapReduce কাজে প্রতিটি map task-এর combiner শুধু গড় (average) পাঠায়, আর reducer সেই গড়গুলোর গড় নেয়। মান ২, ৪ এক map-এ আর ৯ আরেক map-এ থাকলে কী হবে?

  • ঠিক উত্তর ৫ আসবে, কারণ গড় associative
  • Combiner একবারের বেশি চললেই শুধু ভুল হবে
  • ফল আসবে ৬, অথচ আসল গড় ৫; combiner-কে (যোগফল, সংখ্যা) পাঠাতে হবে
  • MapReduce-এ combiner শুধু word count-এ ব্যবহার করা যায়

একটি batch কাজের ৯৯৯টি task ২ মিনিটে শেষ, কিন্তু একটি task একটি খারাপ disk-এর মেশিনে ২০ মিনিট ধরে চলছে। কোন কৌশল এটি সবচেয়ে সরাসরি সামলায়?

  • Reducer-এর সংখ্যা দ্বিগুণ করা
  • Speculative execution — ধীর task-এর একটি বাড়তি কপি অন্য মেশিনে চালানো
  • Combiner যোগ করা
  • পুরো কাজ বাতিল করে আবার শুরু করা

একটি stream processor-এ ১ মিনিটের tumbling window আর watermark = এ পর্যন্ত দেখা সর্বোচ্চ event time − ১০ সেকেন্ড। এ পর্যন্ত সর্বোচ্চ event time ১২:০১:০৫. এখন event time ১২:০০:৫৮-এর একটি ঘটনা এলে কী হবে?

  • ঘটনাটি late, তাই বাদ পড়বে
  • ঘটনাটি ১২:০১–১২:০২ window-এ গোনা হবে, কারণ এখন এসেছে
  • Watermark পিছিয়ে ১২:০০:৪৮ হবে
  • ঘটনাটি ১২:০০–১২:০১ window-এ গোনা হবে, কারণ watermark ১২:০০:৫৫ — window এখনো খোলা

একজন ব্যবহারকারীর click মিনিট ০, ২, ৯, ১১ আর ২০-এ। Session window-এর gap ৫ মিনিট (পরের click আগেরটির ৫ মিনিটের মধ্যে হলে একই session). কটি session?

  • ৩টি — [০, ২], [৯, ১১] আর [২০]
  • ৫টি, প্রতিটি click-এ একটি
  • ২টি — [০, ১১] আর [২০]
  • ৩টি — [০–১০), [১০–২০), [২০–৩০)

২ TB-র একটি অর্ডার টেবিলকে ৫০ MB-র একটি শহর টেবিলের সাথে city_id দিয়ে join করতে হবে, ১০০টি worker-এ। কোন কৌশল সাধারণত সবচেয়ে কম ডেটা নড়ায়?

  • দুই টেবিলকেই city_id দিয়ে shuffle করে sort-merge join
  • ছোট টেবিলটি প্রতিটি worker-এ broadcast করে hash join
  • বড় টেবিলটি একটি মেশিনে এনে join
  • প্রতিটি অর্ডারের জন্য শহর টেবিলে আলাদা query

একটি kappa architecture-এ stream job-এর logic-এ bug পাওয়া গেছে, আর গত ৩০ দিনের ফল ভুল। ঠিক করার প্রচলিত পথ কোনটি?

  • আলাদা batch layer-এ একই logic আবার লিখে চালানো
  • শুধু কোড ঠিক করে আজ থেকে নতুন ফল নেওয়া
  • ঠিক করা job log-এর পুরোনো offset থেকে replay করে নতুন টেবিলে লেখা, তারপর পাঠকদের সেখানে সরানো
  • পুরোনো checkpoint থেকে একই job আবার চালু করা

শব্দকোষ

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

মনে রাখুন

  1. Batch সীমিত ডেটায় সম্পূর্ণ কিন্তু দেরির উত্তর; stream অসীম ধারায় দ্রুত কিন্তু অনুমাননির্ভর। Batch হলো শেষ-থাকা একটি stream.
  2. MapReduce: map → shuffle (hash(key) mod R) → reduce. সবচেয়ে দামি ধাপ shuffle; combiner (associative হিসাবে), data locality আর speculative execution খরচ কমায়, skew কমায় না।
  3. Dataflow engine পুরো DAG দেখে, মাঝের ফল memory-তে রাখে, lineage দিয়ে সারে। Join: ছোট টেবিলে broadcast, দুই বড় টেবিলে sort-merge.
  4. ব্যবসার প্রশ্নে event time; window — tumbling, sliding, session. Watermark ঠিক করে কখন window বন্ধ; দেরির ডেটা allowed lateness পর্যন্ত সংশোধন, তারপর side output; sink-এ upsert.
  5. State + offset একসাথে checkpoint = effect-এ exactly-once, শর্ত replay করা যায় এমন উৎস আর idempotent বা transactional sink. Lambda দুই পথ, kappa এক পথ আর replay.

পরের পর্বে: Stream processor চলে সারাক্ষণ, কিন্তু অনেক কাজ চলে একবার আর দীর্ঘ সময় ধরে — ভিডিও transcode, মাসিক বিল, কয়েক দিন ধরে চলা একটি অর্ডারের ধাপ। পর্ব ৩৭-এ দেখবেন background job, scheduler আর workflow engine কীভাবে এমন কাজ নির্ভরযোগ্যভাবে শেষ করে।

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

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