Apache Kafka — শূন্য থেকে এক্সপার্ট
৭টা ফেজ আর ১৪টা মডিউলের একটা সম্পূর্ণ বাংলা রোডম্যাপ — Kafka-র বেসিক ভোকাবুলারি থেকে শুরু করে প্রোডিউসার/কনজিউমার ইন্টার্নালস, ডিস্ক লেআউট, রেপ্লিকেশন, KRaft, এক্সাক্টলি-ওয়ান্স সিমেন্টিকস, স্কিমা রেজিস্ট্রি, Kafka Connect/Streams হয়ে প্রোডাকশন অপারেশন আর একটা ক্যাপস্টোন সিস্টেম-ডিজাইন পর্যন্ত। প্রতিটা টপিকে What/How/Why/Do — থিওরি, ইন্টার্নালস, হ্যান্ডস-অন ল্যাব, ইন্টারভিউ প্রশ্ন আর প্রোডাকশন কর্নার কেস।
Steps
APACHE KAFKA — শূন্য থেকে এক্সপার্ট
মেন্টর ও স্টুডেন্ট
মেন্টর: Claude · স্টুডেন্ট: Rakib (Backend Software Developer & System Architect, ঢাকা)
ফরম্যাট: ৭টা Phase, ১৪টা Module। প্রতিটা মডিউলে চারটা লেয়ার:
| লেয়ার | কী শিখবেন |
|---|---|
| What | কনসেপ্টটা কী, কোন সমস্যা সমাধান করে |
| How | Kafka ভেতরে ঠিক কীভাবে করে (ডিস্ক লেআউট, প্রোটোকল, স্টেট মেশিন) |
| Why | ডিজাইনাররা কেন এই ট্রেড-অফটা নিলো (অন্য অপশন কী ছিলো) |
| Do | নিজের মেশিনে চালিয়ে দেখা, ভাঙা, ফিক্স করা |
কেন এই ম্যাপিং জরুরি
Rakib-এর আগে থেকেই PostgreSQL WAL, logical replication, Redis Streams consumer group, আর Debezium CDC নিয়ে কাজ করা আছে। Kafka-র অনেক কনসেপ্ট এগুলোরই বড় ভাই — তাই প্রতিটা মডিউলে যেখানেই সম্ভব সরাসরি তুলনা টানা হবে, নতুন করে শূন্য থেকে শেখার দরকার নেই, দরকার শুধু সঠিক ম্যাপিং।
যা লাগবে (prerequisite)
- Linux, systemd, EC2 ops — আগে থেকেই আছে ✅
- PostgreSQL WAL ও logical replication — আগে থেকেই আছে ✅
- Debezium CDC, Redis Streams consumer group model — আগে থেকেই আছে ✅
- Python (FastAPI/Django) — আগে থেকেই আছে ✅
- JVM basics (শুধু tuning বোঝার জন্য, কোড লেখার জন্য না) — নতুন
- Docker Compose (ল্যাবের জন্য) — নতুন
রেফারেন্স
- Kafka: The Definitive Guide (2nd ed.) — Narkhede, Shapira, Palino
- Designing Data-Intensive Applications — Chapter 11
- Kafka official docs — configuration reference
- KIP আর্কাইভ: KIP-98 (EOS), KIP-101 (Leader Epoch), KIP-405 (Tiered Storage), KIP-429 (Cooperative Rebalance), KIP-500/848 (KRaft ও নতুন rebalance protocol)
- LinkedIn-এর মূল পেপার: Kafka: a Distributed Messaging System for Log Processing
পরের ধাপ: Phase 0 → Module 1।
PHASE 0: ভিত্তি
Kafka শেখার আগে বোঝা দরকার Kafka আসলে কী নয় — এটা RabbitMQ-এর মতো একটা 'স্মার্ট' message broker না, এটা একটা distributed commit log। এই একটা মানসিক মডেল ঠিকমতো বসে গেলে বাকি ১৩টা মডিউলের প্রতিটা ডিজাইন-সিদ্ধান্ত স্বাভাবিক মনে হবে। Module 1-এ vocabulary, mental model, আর প্রথম hands-on ক্লাস্টার — এই তিনটাই কভার হবে।
PHASE 1: কোর মেকানিক্স
এখানে Kafka-র দৈনন্দিন ব্যবহারের তিনটা স্তম্ভ — Topic/Partition/Offset, Producer internals, আর Consumer group protocol। এই Phase শেষ করার পর প্রোডাকশনে একটা Kafka pipeline লেখা, ডিবাগ করা, আর টিউন করার মতো ব্যবহারিক দক্ষতা তৈরি হয়ে যাবে। Module 3-4-এ producer batching আর consumer rebalance-এর যে ডিটেইল আসবে, সেটাই প্রোডাকশনে সবচেয়ে বেশি ভুল হওয়া জায়গা।
PHASE 2: স্টোরেজ ইন্টার্নালস
Kafka ডিস্কে ঠিক কী ফরম্যাটে লেখে, কেন এত দ্রুত, আর পুরনো ডেটা কীভাবে সরানো/কমপ্যাক্ট করা হয় — এই Phase-এ সেই লেয়ারে নামা হবে। এখানেই বোঝা যাবে কেন Kafka-র throughput কোনো traditional message broker-এর ধারেকাছেও যেতে পারে না: sequential I/O, page cache, আর zero-copy — এই তিনটা কৌশলের সমষ্টি।
PHASE 3: ক্লাস্টার ইন্টার্নালস
একটা single broker আর একটা production cluster-এর মধ্যে আসল পার্থক্য তৈরি হয় replication আর metadata consensus-এ। এই Phase-এ ISR, High Watermark, Leader Epoch, আর KRaft (Kafka-র নিজস্ব Raft-ভিত্তিক controller) নিয়ে গভীরে যাওয়া হবে — এগুলো না বুঝলে production incident-এ (broker crash, split-brain, data loss) কী হচ্ছে বোঝা অসম্ভব।
PHASE 4: ডেলিভারি সিমেন্টিকস
At-most-once, at-least-once, exactly-once — এই তিনটা শব্দ ইন্টারভিউতে সবচেয়ে বেশি জিজ্ঞেস করা হয়, আর প্রোডাকশনে সবচেয়ে বেশি ভুল বোঝা হয়। এই Phase-এ Kafka Transactions-এর পুরো মেকানিজম (Transaction Coordinator, markers, isolation level) খুঁটিয়ে দেখা হবে।
PHASE 5: ইকোসিস্টেম
Kafka একা কিছু করে না — Schema Registry ডেটা কন্ট্র্যাক্ট ধরে রাখে, Kafka Connect (আর তার ভেতরের Debezium) সিস্টেমের মধ্যে ডেটা সরায়, আর Kafka Streams রিয়েল-টাইম প্রসেসিং করে। এই Phase আপনার (Rakib) বর্তমান CDC/notification pipeline-এর সাথে সবচেয়ে বেশি ওভারল্যাপ করে।
PHASE 6: প্রোডাকশন ও ক্যাপস্টোন
শেষ Phase — capacity planning, security, monitoring, multi-DC replication, আর একটা failure playbook। শেষ হবে একটা capstone দিয়ে: আপনার 100M+/day notification system-এর Kafka লেয়ারটা সম্পূর্ণ ডিজাইন করা, FAANG-স্টাইল mock ইন্টারভিউ ফরম্যাটে।
MODULE 1: Kafka আসলে কী, এবং কেন
এই মডিউলের লক্ষ্য একটাই বাক্য ভেতরে বসিয়ে দেওয়া: Kafka একটা distributed commit log, message queue না। এখান থেকেই বাকি সব কনসেপ্ট (ordering, replay, consumer group, retention) যুক্তিসঙ্গত হয়ে ওঠে। কভার হবে: message queue vs log, core vocabulary, পরিচিত সিস্টেমের সাথে ম্যাপিং, anti-pattern, আর প্রথম hands-on KRaft cluster।
Message Queue vs Log — মৌলিক পার্থক্য
সমস্যাটা কী
RabbitMQ/ActiveMQ-এর মতো traditional message broker একটা queue মডেল অনুসরণ করে: মেসেজ আসে, একজন কনজিউমার সেটা প্রসেস করে ACK পাঠায়, broker মেসেজটা মুছে ফেলে। মেসেজ একবার কনজিউম হয়ে গেলে সেটা আর নেই — replay করার উপায় নেই।
Kafka সম্পূর্ণ ভিন্ন মডেল অনুসরণ করে: একটা append-only log। মেসেজ পড়া মানে ডিলিট হওয়া না — শুধু একটা offset পয়েন্টার এগিয়ে যায়। মেসেজ ডিস্কে থেকেই যায়, retention policy (সময় বা সাইজ ভিত্তিক) অনুযায়ী মেয়াদ শেষ না হওয়া পর্যন্ত।
কেন RabbitMQ কে Kafka দিয়ে রিপ্লেস করা ভুল হতে পারে
- RabbitMQ-এর শক্তি: জটিল routing (topic exchange, fanout, priority queue, per-message TTL, delayed delivery)। কম latency, কম throughput ওয়ার্কলোডে এটা চমৎকার।
- Kafka-র শক্তি: বিশাল throughput, একাধিক কনজিউমার group independently পুরো history replay করতে পারে, আর ডেটা দীর্ঘদিন ধরে রাখা যায় একটা "source of truth" হিসেবে।
রিয়েল-লাইফ উদাহরণ
ধরুন আপনার (Rakib) notification pipeline-এ একটা order.placed ইভেন্ট আসে। RabbitMQ মডেলে সেই ইভেন্ট একবার notification-service কনজিউম করলে শেষ — যদি পরে analytics-service-ও এটা দরকার হয়, নতুন করে পাবে না। Kafka মডেলে notification-service আর analytics-service — দুটোই আলাদা consumer group হিসেবে একই টপিক থেকে independently পুরো স্ট্রিম পড়তে পারে, একজন আরেকজনের read-position-কে প্রভাবিত না করে।
মনে রাখার শর্টকাট
Queue = ডেটা পড়া হলে সরিয়ে ফেলা (pull-and-remove)। Log = ডেটা পড়া মানে শুধু pointer এগোনো, ডেটা থেকেই যায় (pull-and-preserve)।
Distributed Commit Log — এক কথায় Kafka
সংজ্ঞা
Commit log ডেটাবেজ ইন্টার্নালসের একটা পুরনো ধারণা: একটা append-only, ordered, immutable রেকর্ডের সিকোয়েন্স, যেখানে প্রতিটা এন্ট্রির একটা ধারাবাহিক সিকোয়েন্স নম্বর (offset) থাকে। Kafka মূলত এই ধারণাটাকেই নেটওয়ার্কে distributed আর horizontally scalable করে তুলেছে।
Distributed অংশটা আসে partitioning আর replication থেকে — একটা topic একাধিক partition-এ ভাগ হয়, প্রতিটা partition একাধিক broker-এ কপি থাকে।
আপনার পরিচিত জিনিসের সাথে সরাসরি মিল
PostgreSQL-এর WAL (Write-Ahead Log) ঠিক এই ধারণারই একটা single-node সংস্করণ: প্রতিটা write আগে WAL-এ append হয়, তারপর actual data page-এ। WAL entry-রও একটা monotonic LSN (Log Sequence Number) থাকে — Kafka-র offset-এর সমতুল্য। logical replication যখন WAL থেকে read করে subscriber-কে পাঠায়, সেটা অনেকটা Kafka consumer-এর মতোই একটা "cursor" (replication slot-এর confirmed_flush_lsn) রেখে এগোয়।
ফারাক এখানে: PostgreSQL WAL একটা single-node, internal mechanism — এটা বাইরের কনজিউমারদের জন্য ডিজাইন করা না (logical replication ছাড়া)। Kafka-র commit log-টাই তার পাবলিক API — এটাই প্রোডাক্ট।
কেন এই মডেল এত শক্তিশালী
- Replayability: offset রিসেট করে পুরনো ডেটা আবার প্রসেস করা যায় (বাগ ফিক্সের পর reprocessing, নতুন consumer-এর backfill)।
- Decoupling: producer জানে না কে বা কয়জন consume করছে।
- Ordering guarantee (per-partition): log-এর ভেতরে ক্রম কখনো বদলায় না — এটাই পরের মডিউলে ordering guarantee-র ভিত্তি।
এক লাইনে
Kafka = অনেকগুলো ordered, append-only log (partition), distributed across broker, যার প্রতিটা log স্বাধীনভাবে replicate আর consume হয়।
পরিচিত জিনিসের সাথে ম্যাপিং (WAL, Redis Streams, Debezium)
PostgreSQL WAL ≈ Kafka Partition Log
| PostgreSQL | Kafka | পার্থক্য |
|---|---|---|
| WAL segment file | Log segment file | WAL single-writer, single-node; Kafka partition distributed ও replicated |
| LSN | Offset | দুটোই monotonically increasing, ordering-এর ভিত্তি |
| Replication slot | Consumer group offset | slot না সরালে WAL জমে ডিস্ক ভরে যায় — ঠিক যেমন consumer lag বাড়লে Kafka retention-এর সাথে race শুরু হয় |
pg_wal ডিরেক্টরির ফাইল রোটেশন |
Segment rotation (segment.bytes/segment.ms) |
দুটোই বড় লগকে ছোট ফাইলে ভাগ করে পুরনোটা মুছে ফেলার সুবিধার জন্য |
Redis Streams Consumer Group ≈ Kafka Consumer Group — কিন্তু গুরুত্বপূর্ণ পার্থক্য
Redis Streams-এর XREADGROUP অনেকটা Kafka consumer group-এর মতো দেখতে — একাধিক consumer, একটা group, লোড ভাগ হয়। কিন্তু মৌলিক পার্থক্য:
- Redis Streams-এ partitioning নেই — পুরো stream-টাই একটা একক কাঠামো, ordering এবং scaling দুটোই stream-level, Kafka-র মতো partition-level parallelism নেই।
- Redis Streams-এ per-message ACK/PEL (Pending Entries List) মডেল আছে — একটা নির্দিষ্ট মেসেজ retry করা যায় স্বাধীনভাবে। Kafka-তে ACK হয় offset পর্যন্ত commit — নির্দিষ্ট একটা মেসেজ আলাদা করে retry করা যায় না, পুরো offset থেকে re-consume করতে হয়।
- Redis সাধারণত in-memory (persistence optional) — retention সীমিত। Kafka ডিজাইন করা হয়েছে দিনের পর দিন/সপ্তাহ ডেটা ধরে রাখার জন্য, ডিস্ক-ভিত্তিক।
Debezium যা Kafka-তে ঢালে, তার আসল রূপ
Debezium PostgreSQL-এর logical decoding (pgoutput প্লাগইন) ব্যবহার করে WAL থেকে row-level change event তৈরি করে, আর সেগুলো একটা Kafka topic-এ produce করে — সাধারণত প্রতি টেবিলের জন্য একটা টপিক, primary key কে message key হিসেবে ব্যবহার করে। অর্থাৎ Debezium আসলে WAL log-কে Kafka log-এ translate করে — একটা log থেকে আরেকটা log-এ ব্রিজ। এটা Module 11-এ আরও গভীরে যাবে।
সারমর্ম টেবিল
| আপনার আগের অভিজ্ঞতা | Kafka-র সমতুল্য |
|---|---|
| WAL + LSN | Partition log + Offset |
| Replication slot lag | Consumer lag |
| Redis Streams consumer group | Kafka consumer group (partition-parallel, offset-based) |
| Debezium output topic | CDC-translated commit log |
Core Vocabulary: Topic, Partition, Offset, Producer, Consumer, Broker, Cluster
সাতটা শব্দ যা সব জায়গায় ফিরে আসবে
- Topic — একটা logical নাম, যেমন
order.placedবাpayment.failed। এটা একটা category/feed — RDBMS-এর টেবিলের সাথে তুলনীয় (schema সবসময় strict না)। - Partition — একটা topic ভাঙা হয় একাধিক partition-এ। প্রতিটা partition একটা স্বাধীন, ordered, append-only log। Parallelism-এর একক (পরের মডিউলে বিস্তারিত)।
- Offset — একটা partition-এর ভেতরে প্রতিটা মেসেজের ধারাবাহিক পজিশন নম্বর (0, 1, 2, …)। শুধু partition-এর ভেতরে অর্থবহ — দুইটা ভিন্ন partition-এর offset তুলনীয় না।
- Producer — যে ক্লায়েন্ট মেসেজ পাঠায় (write করে)।
- Consumer — যে ক্লায়েন্ট মেসেজ পড়ে। consumer-রা consumer group-এ সংগঠিত হয়ে লোড ভাগ করে নেয়।
- Broker — একটা একক Kafka সার্ভার প্রসেস, যেটা কিছু partition-এর ডেটা রাখে ও সার্ভ করে।
- Cluster — একাধিক broker মিলে একটা cluster গঠন করে; একজন broker হয় controller হিসেবে metadata পরিচালনা করে (Module 8)।
একটা ছবি হিসেবে ভাবুন
Topic: order.placed (3 partitions, replication-factor=2)
Broker-1 Broker-2 Broker-3
┌────────────┐ ┌────────────┐ ┌────────────┐
│ P0 (leader)│◄───────► │ P0 (replica)│ │ P1 (leader)│
│ P2 (replica)│ │ P1 (replica)│◄───────►│ P2 (leader)│
└────────────┘ └────────────┘ └────────────┘
প্রতিটা partition-এর একজন leader থাকে (সব read/write ওখানে যায়) আর বাকি broker-এ replica (Module 7-এ বিস্তারিত)।
রিয়েল-লাইফ ম্যাপিং
আপনার notification system-এ: notification.email একটা topic, ৬টা partition, একটা NotificationSenderService তিনটা instance চালালে সেগুলো একটা consumer group গঠন করে, প্রতিটা instance গড়ে ২টা partition handle করে — এটাই horizontal scaling-এর মূল প্যাটার্ন।
Kafka কোথায় ব্যবহার করবেন না (Anti-patterns)
Kafka যা না
- RPC replacement না। সিনক্রোনাস request-response দরকার হলে (যেমন "এই ইউজারের ব্যালেন্স এখনই দাও") gRPC/REST ব্যবহার করুন। Kafka-তে request-reply করা যায় (reply topic দিয়ে), কিন্তু এটা জটিল আর latency বাড়ায় — এই জন্য ডিজাইন করা হয়নি।
- ছোট, low-latency task queue না। Celery/RabbitMQ-এর মতো per-task priority, delayed execution, বা fine-grained retry দরকার হলে Kafka এখানে দুর্বল — এতে per-message priority queue নেই, delayed delivery নেই (workaround আছে কিন্তু native না)।
- প্রাইমারি ডেটাবেজ না। যদিও Kafka log টেকসই এবং কমপ্যাক্টেড টপিক key-value store-এর মতো আচরণ করতে পারে, এটা random-access query, secondary index, বা transaction-heavy OLTP workload-এর জন্য ডিজাইন করা না।
- ছোট স্কেলে ওভার-ইঞ্জিনিয়ারিং। দিনে কয়েক হাজার মেসেজ হলে, একটা single Postgres টেবিল + polling বা Redis Streams-ই যথেষ্ট। Kafka অপারেশনাল খরচ (ZooKeeper/KRaft, monitoring, disk planning) আনার আগে scale/throughput justification থাকা উচিত।
- স্ট্রং consistency lock-step transaction-এর বিকল্প না। একাধিক সার্ভিস জুড়ে distributed transaction দরকার হলে Kafka দিয়ে সরাসরি ACID পাবেন না — Saga/Outbox প্যাটার্ন লাগবে (Module 11)।
সিদ্ধান্ত নেওয়ার প্রশ্ন
Kafka ব্যবহার করার আগে নিজেকে জিজ্ঞেস করুন:
- একাধিক independent consumer কি একই ডেটা স্ট্রিম আলাদাভাবে পড়বে?
- Replay/reprocessing দরকার হবে কি?
- Throughput কি সত্যিই high (per second-এ হাজার+ মেসেজ) নাকি এটা আসলে low-volume RPC-এর ছদ্মবেশ?
তিনটার উত্তর "হ্যাঁ, হ্যাঁ, হ্যাঁ" না হলে, আরেকবার ভাবুন — হয়তো RabbitMQ, Redis Streams, বা এমনকি একটা সাধারণ Postgres outbox টেবিলই যথেষ্ট।
ল্যাব: Docker Compose দিয়ে Single-Broker KRaft Cluster
লক্ষ্য
ZooKeeper ছাড়াই (KRaft mode) একটা single-broker Kafka cluster চালানো, একটা topic তৈরি করা, console producer/consumer দিয়ে মেসেজ পাঠানো-পড়া।
docker-compose.yml
services:
kafka:
image: apache/kafka:3.8.0
container_name: kafka
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
চালানো
docker compose up -d
# Topic তৈরি (৩ partition)
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh \
--create --topic order.placed --partitions 3 --replication-factor 1 \
--bootstrap-server localhost:9092
# Console producer
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \
--topic order.placed --bootstrap-server localhost:9092
# আরেকটা টার্মিনালে — console consumer (শুরু থেকে সব পড়বে)
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \
--topic order.placed --from-beginning --bootstrap-server localhost:9092
যা লক্ষ্য করবেন
- Producer টার্মিনালে টাইপ করা প্রতিটা লাইন consumer টার্মিনালে সাথে সাথে দেখা যাবে।
- Consumer বন্ধ করে আবার
--from-beginningদিয়ে চালালে আগের সব মেসেজ আবার দেখাবে — এটাই log model-এর replay ক্ষমতা, এটাই RabbitMQ থেকে সবচেয়ে বড় পার্থক্য। kafka-topics.sh --describe --topic order.placed --bootstrap-server localhost:9092চালিয়ে দেখুন partition, leader, replica তথ্য।
চ্যালেঞ্জ (নিজে করুন)
একটা key দিয়ে producer থেকে মেসেজ পাঠান (kafka-console-producer.sh --property "parse.key=true" --property "key.separator=:"), তারপর --partition ফ্ল্যাগ দিয়ে consumer শুধু একটা নির্দিষ্ট partition থেকে পড়ান — দেখুন একই key সবসময় একই partition-এ যাচ্ছে কিনা।
ইন্টারভিউ প্রশ্ন — Module 1
প্র.১ — Kafka-কে "distributed commit log" বলা হয় কেন, "distributed queue" না কেন? উত্তর: কারণ মেসেজ কনজিউম হওয়ার পরও ডিলিট হয় না — এটা শুধু একটা append-only, ordered log যেখানে consumer একটা offset পয়েন্টার এগিয়ে নেয়। একাধিক consumer group স্বাধীনভাবে পুরো history replay করতে পারে, যেটা traditional queue মডেলে সম্ভব না।
প্র.২ — RabbitMQ-এর বদলে আপনি কখন Kafka বেছে নেবেন? উত্তর: যখন high throughput দরকার (প্রতি সেকেন্ডে হাজার-লাখ মেসেজ), একাধিক independent consumer একই ডেটা আলাদাভাবে পড়বে, replay/reprocessing দরকার হবে, বা দীর্ঘমেয়াদী retention চাই (audit log, event sourcing)। ছোট স্কেলে জটিল routing/priority queue দরকার হলে RabbitMQ ভালো ফিট।
প্র.৩ — PostgreSQL WAL-এর সাথে Kafka partition-এর মিল ও অমিল কী? উত্তর: মিল — দুটোই append-only, monotonically increasing sequence number (LSN vs offset), sequential write। অমিল — WAL single-node internal mechanism, বাইরের কনজিউমারের জন্য না (logical replication ছাড়া); Kafka partition distributed, replicated, আর এটাই এর পাবলিক API।
প্র.৪ — Redis Streams consumer group আর Kafka consumer group-এর মূল পার্থক্য কী? উত্তর: Redis Streams-এ partitioning নেই (পুরো stream একক), per-message ACK/PEL আছে (নির্দিষ্ট মেসেজ আলাদা retry করা যায়), সাধারণত in-memory-ভিত্তিক সীমিত retention। Kafka-তে partition-level parallelism, offset-ভিত্তিক bulk commit (নির্দিষ্ট মেসেজ আলাদা retry করা যায় না), আর ডিস্ক-ভিত্তিক দীর্ঘমেয়াদী retention।
প্র.৫ — একটা টিম বলছে "আমরা সব inter-service communication Kafka দিয়ে করব, এমনকি synchronous user-facing API-ও" — আপনার প্রতিক্রিয়া কী হবে? উত্তর: এটা একটা anti-pattern red flag। Synchronous, low-latency request-response-এর জন্য Kafka ডিজাইন করা হয়নি — request-reply pattern সম্ভব কিন্তু জটিলতা ও latency বাড়ায়। User-facing সিনক্রোনাস কলের জন্য REST/gRPC রাখা উচিত, Kafka রাখা উচিত asynchronous, event-driven, বা fan-out ওয়ার্কলোডের জন্য।
প্র.৬ — Topic, Partition, আর Offset-এর সম্পর্ক এক লাইনে ব্যাখ্যা করুন। উত্তর: একটা Topic একাধিক Partition-এ ভাগ হয়; প্রতিটা Partition একটা স্বাধীন ordered log; সেই log-এর প্রতিটা মেসেজের একটা ধারাবাহিক Offset থাকে যেটা শুধু ওই partition-এর ভেতরেই অর্থবহ।
কর্নার কেস — Module 1
- একটা topic-এ ০ partition তৈরি করা যায় না। কমপক্ষে ১টা partition লাগবেই — অনেকে ভুলে ভাবেন partition অপশনাল।
--from-beginningমানে "সব সময় শুরু থেকে" না। এটা শুধু তখনই কাজ করে যখন consumer group-এর জন্য কোনো committed offset আগে থেকে সংরক্ষিত নেই। একবার offset commit হয়ে গেলে, একই group-id দিয়ে আবার চালালে--from-beginningইগনোর হয়ে যাবে — committed offset থেকেই শুরু হবে।- Replication factor cluster-এর broker সংখ্যার চেয়ে বেশি সেট করলে টপিক তৈরি ব্যর্থ হবে। single-broker ল্যাবে
replication-factor=2দিলে error — এটা প্রায়ই নতুনদের প্রথম কনফিউশন। - Console producer default-এ key ছাড়া মেসেজ পাঠায় — round-robin/sticky partitioning হয়, key-based না। যদি ordering guarantee টেস্ট করতে চান, key ছাড়া পাঠালে ভুল উপসংহারে পৌঁছাবেন ("Kafka ordering guarantee দেয় না" — ভুল, key ছাড়া partition assignment predictable না শুধু তার জন্য)।
- Docker container রিস্টার্ট করলে ডেটা হারাতে পারে যদি volume mount না থাকে — লোকাল ল্যাবে এটা প্রায়ই ভুলে যাওয়া হয়, প্রোডাকশনে persistent volume/EBS ছাড়া broker restart মানেই ডেটা লস।
- Topic নাম-এ
.আর_একসাথে ব্যবহার করলে সতর্কবার্তা আসে (কিন্তু এরর না) — কারণ Kafka ইন্টারনালি মেট্রিক নামে.কে_-এ রূপান্তর করে, ফলেorder.placedআরorder_placed— দুটো টপিক একসাথে থাকলে মেট্রিক নাম কলিশন করবে।
MODULE 2: Topic, Partition আর Offset — গভীরে
Module 1-এ vocabulary শেখা হয়েছে, এখন partition-কে গভীরে বোঝার পালা — কারণ partition-ই Kafka-র parallelism, ordering, আর scaling-এর একক। কভার হবে: partition কেন parallelism-এর একক, ordering guarantee ঠিক কতটুকু (topic-level guarantee নেই, এটা একটা সাধারণ ভুল ধারণা), offset/LEO/HW/log-start-offset-এর পার্থক্য, partition count প্ল্যানিং, আর key-based partitioning কীভাবে ordering নিশ্চিত করে।
Partition কেন Kafka-র Parallelism-এর একক
কী এবং কেন
একটা topic-এর একটাই partition থাকলে, সেই topic-এ produce/consume করা সবসময় single-threaded থেকে যাবে — একটামাত্র broker সেই লগ হোস্ট করবে, একটামাত্র consumer instance (একটা group-এ) সেটা পড়তে পারবে। partition বাড়ালে ডেটা একাধিক independent log-এ ভাগ হয়ে যায়, যেগুলো আলাদা broker-এ, আলাদা disk-এ, আলাদা thread-এ সমান্তরালে পড়া-লেখা যায়।
ভেতরে কীভাবে কাজ করে
প্রতিটা partition একটা সম্পূর্ণ স্বাধীন append-only log — নিজের leader broker, নিজের offset sequence, নিজের segment ফাইল। producer আর consumer উভয়েই partition-level এ parallelize করে: একাধিক producer thread আলাদা partition-এ সমান্তরাল write করতে পারে, আর একটা consumer group-এর একাধিক instance আলাদা partition consume করে সমান্তরাল read করতে পারে।
রিয়েল-লাইফ উদাহরণ
আপনার (Rakib) notification pipeline-এ notification.email topic-এ যদি ১টাই partition থাকে, তাহলে ১০টা NotificationSenderService instance চালালেও মাত্র ১টা কাজ করবে — বাকি ৯টা idle বসে থাকবে, কারণ consumer group-এ partition সংখ্যার বেশি active consumer কাজ পায় না। ৬টা partition করলে ৬টা instance পর্যন্ত সমান্তরাল কাজ করতে পারবে।
ড্রব্যাক / ট্রেড-অফ
- partition যত বেশি, ততই memory/file-handle overhead broker-এ (প্রতিটা partition-এর জন্য আলাদা open file handle, index ফাইল)।
- partition বেশি হলে end-to-end latency সামান্য বাড়তে পারে (producer batching কম কার্যকর হয় যখন ডেটা অনেক partition-এ ছড়িয়ে যায়)।
- controller-এর metadata propagation ভারী হয়ে যায় হাজার হাজার partition হলে (cluster-wide leader election সময় বাড়ে)।
নিয়ম: partition সংখ্যা = আপনার প্রয়োজনীয় সর্বোচ্চ parallel consumer সংখ্যা, না তার বেশি, না কম।
Ordering Guarantee ঠিক কতটুকু (Partition-level, Topic-level না)
মূল কথা
Kafka শুধু একটা partition-এর ভেতরে ordering guarantee দেয়। একটা topic-এর একাধিক partition জুড়ে কোনো global ordering guarantee নেই — এটা প্রায় সবচেয়ে বেশি ভুল বোঝা concept।
কেন এমন ডিজাইন
Global (topic-wide) ordering guarantee দিতে হলে সব produce/consume single partition দিয়ে যেতে হতো — parallelism পুরোপুরি হারিয়ে যেত। Kafka ইচ্ছাকৃতভাবে ordering-কে partition-এ সীমাবদ্ধ রেখে parallelism আর ordering-এর মধ্যে একটা ব্যবহারযোগ্য ভারসাম্য তৈরি করেছে — আপনি নিজে ঠিক করবেন কতটুকু ordering দরকার (key নির্বাচনের মাধ্যমে)।
রিয়েল-লাইফ উদাহরণ
ধরুন order.status_changed topic-এ একটা অর্ডারের status PLACED → PAID → SHIPPED — এই তিনটা ইভেন্ট যদি ভিন্ন ভিন্ন partition-এ চলে যায় (key ছাড়া produce করলে round-robin/sticky partitioning হয়), consumer হয়তো SHIPPED আগে পড়ে ফেলবে PAID-এর আগে — একটা ভয়াবহ বাগ। সমাধান: order_id-কে message key হিসেবে ব্যবহার করুন, তাহলে একটা নির্দিষ্ট অর্ডারের সব ইভেন্ট সবসময় একই partition-এ যাবে, ordering নিশ্চিত থাকবে।
ড্রব্যাক / ট্রেড-অফ
- Key-based partitioning ordering দেয়, কিন্তু একটা "hot key" (যেমন একটা অতি-সক্রিয় merchant_id) সেই partition-কে overload করে ফেলতে পারে — parallelism নষ্ট হয়ে যায় (Module 3-এ hot partition বিস্তারিত)।
- producer-এর
max.in.flight.requests.per.connection > 1হলে, retry-এর কারণে একই partition-এর ভেতরেও ordering ভাঙতে পারে (Module 3-এ বিস্তারিত)। - Consumer যদি partition একটার বেশি হ্যান্ডেল করে (স্বাভাবিক), সেই consumer-এর দৃষ্টিকোণ থেকে বিভিন্ন partition-এর মেসেজ interleaved হয়ে আসবে — per-key ordering ঠিক থাকলেও "consumer যে ক্রমে পড়ছে" সেটা partition-এর মধ্যে round-robin-এর মতো মিশ্রিত।
Offset, Log End Offset (LEO), High Watermark (HW), Log Start Offset
চারটা সংখ্যা যা প্রতিটা partition ট্র্যাক করে
- Offset — একটা নির্দিষ্ট মেসেজের পজিশন (0-ইনডেক্সড)।
- Log Start Offset — এই partition-এ এখনো যা সবচেয়ে পুরনো available মেসেজ (retention-এর কারণে পুরনো মেসেজ মুছে গেলে এটা এগোয়)।
- Log End Offset (LEO) — পরবর্তী মেসেজ যেখানে লেখা হবে; broker-এর local log-এ শেষ লেখা মেসেজের পরের পজিশন। প্রতিটা replica (leader ও follower) নিজের LEO ট্র্যাক করে।
- High Watermark (HW) — সব ISR replica-তে replicate হয়ে গেছে এমন সর্বোচ্চ offset। consumer শুধু HW পর্যন্তই পড়তে পারে — এর বেশি না, কারণ HW-এর ওপরের ডেটা এখনো durable না (সব replica-তে যায়নি)।
কেন এই distinction জরুরি
Leader-এর LEO সবসময় HW-এর সমান বা বেশি — leader প্রথমে নিজের log-এ লেখে (LEO বাড়ে), তারপর follower-রা fetch করে replicate করে, সব ISR follower catch-up করলে HW এগোয়। এই gap-টাই durability guarantee-র মূল যন্ত্র: consumer uncommitted (non-replicated) ডেটা কখনো দেখে না, ফলে leader crash হলেও consumer যা পড়েছে তা হারানোর ঝুঁকি নেই।
রিয়েল-লাইফ উদাহরণ
PostgreSQL-এ এটার সবচেয়ে কাছের সমতুল্য হলো pg_current_wal_lsn() (leader-এর লেখা সর্বশেষ LSN, অনেকটা LEO) বনাম synchronous replica-র confirmed_flush_lsn (অনেকটা HW) — synchronous_commit চালু থাকলে client শুধু তখনই commit acknowledgment পায় যখন standby সেই LSN পর্যন্ত flush করেছে, ঠিক যেমন Kafka producer acks=all-এ HW না পৌঁছানো পর্যন্ত ack পায় না (Module 7-এ বিস্তারিত)।
ড্রব্যাক / কর্নার কেস
- monitoring টুল যদি শুধু LEO দেখে lag হিসাব করে (HW না), তাহলে replication lag-জনিত সমস্যা লুকিয়ে থাকতে পারে — সবসময় consumer lag = HW - committed offset হিসাব করা উচিত, LEO না।
min.insync.replicasকম সেট থাকলে HW দ্রুত এগোয় কিন্তু durability guarantee দুর্বল হয়ে যায় — এটা একটা সরাসরি ট্রেড-অফ (Module 7)।
Partition Count কীভাবে ঠিক করবেন — বাড়ানো সহজ, কমানো অসম্ভব কেন
কেন কমানো যায় না
Kafka partition count কমানো সাপোর্ট করে না — একবার ১০টা partition তৈরি করলে সেটা কখনো ৫-এ নামানো যাবে না (টপিক ডিলিট করে নতুন করে বানানো ছাড়া)। কারণ: প্রতিটা partition একটা independent, ordered log; দুটো partition-কে merge করলে তাদের ভেতরের মেসেজগুলোর আপেক্ষিক ক্রম সংজ্ঞায়িত করার কোনো সঠিক উপায় নেই — কোন মেসেজ কার আগে গিয়েছিল তার global timestamp-ভিত্তিক ordering guarantee নেই।
partition বাড়ানো কেন "সহজ" (কিন্তু ঝুঁকিমুক্ত না)
Kafka নতুন partition যোগ করা সাপোর্ট করে (kafka-topics.sh --alter --partitions N) — কিন্তু এতে key-to-partition ম্যাপিং বদলে যায় (hash % partition_count-এর সূত্র অনুযায়ী)! ফলে partition বাড়ানোর পর একই key নতুন partition-এ চলে যেতে পারে — বিদ্যমান key-ordering guarantee ভেঙে যায় নতুন আর পুরনো মেসেজের মধ্যে।
ক্যাপাসিটি প্ল্যানিং-এর মোটামুটি সূত্র
partition_count = max(desired_throughput / per_partition_producer_throughput, desired_throughput / per_partition_consumer_throughput) — সাধারণত producer per-partition throughput ~10MB/s আর consumer per-partition ~বেশি হতে পারে workload অনুযায়ী। ভবিষ্যতের ৩-৫ গুণ growth মাথায় রেখে একটু বেশি partition রাখা common practice (over-provisioning সস্তা, পরে বাড়ানো জটিল কারণ key remapping)।
রিয়েল-লাইফ উদাহরণ
আপনি যদি শুরুতে order.placed-এ ৩টা partition দিয়ে শুরু করেন, ৬ মাস পরে throughput ৫ গুণ বাড়লে partition ১২-এ বাড়াতে পারবেন, কিন্তু ইতিমধ্যে-চলমান কোনো downstream stateful consumer (যেমন Kafka Streams KTable, Module 12) যদি partition count-এর ওপর নির্ভর করে state store বানিয়ে থাকে, সেই state rebuild করা লাগতে পারে।
ড্রব্যাক
- বেশি partition দিয়ে শুরু করলে ছোট cluster-এ unnecessary overhead।
- কম দিয়ে শুরু করলে পরে বাড়াতে গিয়ে ordering guarantee ভাঙার ঝুঁকি।
- এই দোটানার কোনো "নিখুঁত" সমাধান নেই — শুধু সচেতন ট্রেড-অফ।
Key-based Partitioning — একই Key সবসময় একই Partition-এ
কীভাবে কাজ করে
Producer একটা মেসেজ পাঠানোর সময় key দিলে, default partitioner সেই key-কে hash (murmur2) করে hash(key) % partition_count সূত্রে একটা নির্দিষ্ট partition বেছে নেয়। একই key সবসময় একই hash দেয়, ফলে একই partition-এ যায় — যতক্ষণ partition count না বদলায়।
Key না দিলে (null key), Kafka round-robin বা sticky partitioner ব্যবহার করে (Module 3-এ বিস্তারিত) — কোনো নির্দিষ্ট key-partition সম্পর্ক নেই, শুধু লোড ছড়ানোর জন্য।
রিয়েল-লাইফ উদাহরণ
Debezium যখন PostgreSQL থেকে row-change event পাঠায়, প্রতিটা মেসেজের key হয় ওই row-এর primary key। ফলে একটা নির্দিষ্ট order_id-এর সব পরিবর্তন (INSERT, UPDATE, DELETE) সবসময় একই partition-এ যায়, একই ক্রমে থাকে — downstream consumer সেই row-এর history-কে সঠিক ক্রমে দেখতে পায়। এটাই CDC pipeline-এ ordering সঠিক রাখার মূল কৌশল।
ড্রব্যাক / Hot Key সমস্যা
- যদি একটা key অস্বাভাবিক বেশি ট্রাফিক তৈরি করে (যেমন একটা celebrity user-এর সব ইভেন্ট, বা একটা flash-sale-এর সময় একটা নির্দিষ্ট product_id), সেই partition বাকিগুলোর তুলনায় অনেক বেশি লোড পায় — একে hot partition বলে। partition বাড়িয়েও এটা সমাধান হয় না যতক্ষণ না key distribution বদলায়।
- সমাধান কৌশল: composite key ব্যবহার (
user_id + random_suffix) যদি strict per-user ordering দরকার না হয়, অথবা downstream-এ aggregation লজিক আলাদাভাবে ডিজাইন করা যাতে hot key handle করতে পারে। - key null থেকে non-null-এ (বা উল্টো) বদলালে বিদ্যমান ডেটার partition assignment-এর সাথে সামঞ্জস্য থাকে না — মাইগ্রেশনের সময় সতর্ক থাকা দরকার।
ল্যাব: ৩-Partition Topic, Key দিয়ে/ছাড়া Ordering পার্থক্য
লক্ষ্য
কী দিয়ে আর ছাড়া produce করে ordering-এর পার্থক্য নিজের চোখে দেখা।
সেটআপ
kafka-topics.sh --create --topic order.status \
--partitions 3 --replication-factor 1 \
--bootstrap-server localhost:9092
ধাপ ১ — key ছাড়া produce (ordering গ্যারান্টি নেই)
kafka-console-producer.sh --topic order.status --bootstrap-server localhost:9092
> {"order_id": "A1", "status": "PLACED"}
> {"order_id": "A1", "status": "PAID"}
> {"order_id": "A1", "status": "SHIPPED"}
এখন প্রতিটা partition আলাদা করে পড়ুন (--partition 0, --partition 1, --partition 2) — দেখবেন তিনটা মেসেজ তিনটা ভিন্ন partition-এ ছড়িয়ে গেছে, ক্রম হারিয়ে গেছে।
ধাপ ২ — key দিয়ে produce (per-key ordering গ্যারান্টি)
kafka-console-producer.sh --topic order.status \
--property "parse.key=true" --property "key.separator=:" \
--bootstrap-server localhost:9092
> A1:{"status": "PLACED"}
> A1:{"status": "PAID"}
> A1:{"status": "SHIPPED"}
> B2:{"status": "PLACED"}
এখন দেখুন — A1-এর সব মেসেজ একই partition-এ, একই ক্রমে। B2 হয়তো অন্য partition-এ গেছে, কিন্তু সেটা সমস্যা না কারণ per-order ordering-ই যা দরকার, global ordering না।
চ্যালেঞ্জ
পার্টিশন সংখ্যা ৩ থেকে ৬-এ বাড়ান (kafka-topics.sh --alter), তারপর আবার A1 key দিয়ে একটা মেসেজ পাঠান — দেখুন এটা কোন partition-এ যায় (হতে পারে আগের partition-এর থেকে ভিন্ন, কারণ hash % 6 ≠ hash % 3 সাধারণত)। এটাই partition বাড়ানোর বিপদ হাতেকলমে দেখা।
ইন্টারভিউ প্রশ্ন — Module 2
প্র.১ — Kafka কি topic-level ordering guarantee দেয়? উত্তর: না। শুধু partition-level ordering guarantee দেয়। একাধিক partition জুড়ে কোনো global ordering নেই — এই ভুল ধারণাই প্রোডাকশনে সবচেয়ে বেশি ordering-related বাগের কারণ।
প্র.২ — partition count কমানো যায় না কেন? উত্তর: প্রতিটা partition একটা independent, ordered log। দুটো partition merge করলে তাদের মধ্যে মেসেজের আপেক্ষিক ক্রম সংজ্ঞায়িত করার সঠিক উপায় নেই — তাই Kafka এটা সাপোর্ট করে না। টপিক ডিলিট করে নতুন করে বানানোই একমাত্র উপায়।
প্র.৩ — High Watermark আর Log End Offset-এর পার্থক্য কী, আর consumer কোনটার ওপর নির্ভর করে? উত্তর: LEO = leader-এর নিজের log-এ শেষ লেখা মেসেজের পরের অবস্থান। HW = সব ISR replica-তে replicate হয়ে যাওয়া সর্বোচ্চ offset। Consumer শুধু HW পর্যন্ত পড়তে পারে, কারণ তার বেশি ডেটা এখনো durable না।
প্র.৪ — partition বাড়ালে key-based ordering-এ কী সমস্যা হতে পারে?
উত্তর: partition count বদলালে hash(key) % partition_count সূত্রের ফলাফলও বদলে যায় — একই key নতুন এবং পুরনো মেসেজের জন্য ভিন্ন ভিন্ন partition-এ যেতে পারে, ফলে সেই key-এর ইতিহাসের ধারাবাহিক ordering guarantee ভেঙে যায়।
প্র.৫ — hot partition সমস্যা কী, আর কীভাবে সমাধান করবেন? উত্তর: একটা নির্দিষ্ট key যদি অস্বাভাবিক বেশি ট্রাফিক তৈরি করে, সেই key-এর partition বাকিগুলোর তুলনায় বেশি লোড পায়, parallelism নষ্ট হয়। সমাধান: composite key (যদি strict ordering আবশ্যক না হয়), অথবা downstream-এ আলাদা aggregation লজিক ডিজাইন করা।
প্র.৬ — ৫ node-এর cluster-এ একটা topic-এ ২টা partition আছে, ১০টা consumer instance একই group-এ আছে — কী হবে? উত্তর: শুধু ২টা consumer instance active কাজ পাবে (প্রতি partition-এ ১টা), বাকি ৮টা idle বসে থাকবে। Kafka consumer group-এ partition সংখ্যাই max parallelism-এর সিলিং।
কর্নার কেস — Module 2
- Null key দিয়ে produce করে ordering আশা করা — সবচেয়ে সাধারণ ভুল। key null থাকলে sticky/round-robin partitioner ব্যবহার হয়, কোনো ordering guarantee নেই।
- একটা consumer instance একাধিক partition হ্যান্ডেল করলে, সেই consumer-এর দৃষ্টিতে ভিন্ন partition-এর মেসেজ interleaved দেখা যায় — এটা বাগ না, এটাই স্বাভাবিক আচরণ; per-key ordering ঠিক থাকলে এটা সমস্যা না।
- Partition বাড়ানোর সময় producer/consumer রানিং অবস্থায় থাকলে সাময়িক rebalance ও key-remapping-জনিত সাময়িক ordering ভাঙা হতে পারে — নতুন partition অ্যাড করার সময় ট্রাফিক কম থাকা windows-এ করা ভালো প্র্যাকটিস।
- LEO আর HW-এর মধ্যে গ্যাপ (under-replicated partition) থাকলে producer
acks=allদিয়ে পাঠানো মেসেজ latency বাড়তে থাকবে, কারণ HW না এগোনো পর্যন্ত ack যাবে না — এটা replication lag-এর একটা লক্ষণ। - Log Start Offset-এর নিচের offset থেকে consumer শুরু করতে চাইলে
OffsetOutOfRangeExceptionআসবে — retention expire হয়ে যাওয়া ডেটা আর reset করে পড়া যায় না,auto.offset.resetঅনুযায়ী earliest/latest-এ চলে যাবে। - একই key কিন্তু ভিন্ন serializer/producer instance ব্যবহার করলে hash ভিন্ন হতে পারে না — murmur2 hash deterministic এবং serializer-independent (byte-level key-এর ওপর কাজ করে), কিন্তু custom partitioner override করা থাকলে এই ধারণা ভেঙে যেতে পারে — এটা যাচাই করে নেওয়া উচিত।
MODULE 3: Producer — ভেতর থেকে
একটা producer.send() কল করার পর ভেতরে কী কী ধাপ ঘটে — Serializer, Partitioner, RecordAccumulator (batch buffer), Sender thread — এই পুরো পাইপলাইন বোঝা এই মডিউলের লক্ষ্য। কভার হবে: partitioner algorithm-এর বিবর্তন (sticky partitioner কেন এলো), batching-এর গণিত (batch.size vs linger.ms), acks সেটিংসের failure mode, compression কোডেক নির্বাচন, retry/ordering সম্পর্ক, আর idempotent producer কীভাবে duplicate ঠেকায়।
Producer পাইপলাইন: send() → Serializer → Partitioner → RecordAccumulator → Sender
পুরো যাত্রা
- Serializer — key/value object-কে byte array-এ রূপান্তর করে (
StringSerializer,AvroSerializer, ইত্যাদি)। - Partitioner — কোন partition-এ যাবে সিদ্ধান্ত নেয় (key-hash বা sticky, Module 2 দেখুন)।
- RecordAccumulator — মেসেজ সরাসরি network-এ যায় না; প্রথমে একটা in-memory buffer-এ (
buffer.memory), প্রতি partition-এর জন্য আলাদা batch queue-তে জমা হয়। - Sender thread — একটা ব্যাকগ্রাউন্ড thread যেটা ready batch-গুলো তুলে নিয়ে broker-এ পাঠায় (network I/O)।
send() কল asynchronous — এটা সাথে সাথে একটা Future রিটার্ন করে, actual network write পরে সেন্ডার থ্রেড করে। send().get() কল করলে সিনক্রোনাসের মতো ব্লক করে অপেক্ষা করা যায়, কিন্তু এতে throughput মারাত্মক কমে যায়।
কেন এই আর্কিটেকচার
Application thread আর network thread আলাদা করার ফলে producer high throughput পায় — application কোড batching/network delay নিয়ে ব্লক হয় না, শুধু buffer-এ রাখে আর এগিয়ে যায়। এটাই async, non-blocking design-এর মূল সুবিধা।
রিয়েল-লাইফ উদাহরণ
একটা FastAPI endpoint যদি প্রতিটা request-এ producer.send(...).get() কল করে (সিনক্রোনাস অপেক্ষা), প্রতিটা request Kafka network round-trip-এর জন্য ব্লক হবে — throughput RabbitMQ-সদৃশ পার-মেসেজ latency-তে নেমে আসবে। সঠিক প্যাটার্ন: send() কল করে callback attach করা (add_callback/add_errback), অথবা batch শেষে একবার flush() করা।
ড্রব্যাক
- Async design মানে error handling জটিল — একটা ব্যর্থ send silently miss হয়ে যেতে পারে যদি callback/errback ঠিকমতো handle না করা হয়।
buffer.memoryপূর্ণ হয়ে গেলেsend()ব্লক করা শুরু করে (max.block.msপর্যন্ত), তারপরTimeoutException— burst traffic-এ এটা একটা সাধারণ প্রোডাকশন সমস্যা।
Partitioner Algorithms: Murmur2 Hash, Round-robin, Sticky Partitioner (KIP-480)
তিনটা যুগ
- Key-based (murmur2 hash) —
hash(key) % partition_count। Deterministic, একই key সবসময় একই partition। - পুরনো round-robin (key null) — প্রতিটা মেসেজ পালাক্রমে ভিন্ন partition-এ যেত। সমস্যা: ছোট ছোট batch অনেক partition-এ ছড়িয়ে যেত, network request সংখ্যা বাড়তো, batching efficiency কমে যেত।
- Sticky Partitioner (KIP-480, Kafka 2.4+) — key null হলে একটা batch পুরোপুরি ভরে যাওয়া বা
linger.msশেষ হওয়া পর্যন্ত একই partition-এ মেসেজ জমা করে, তারপর নতুন partition বেছে নেয়। এতে batch বড়, request সংখ্যা কম, throughput বেশি।
কেন sticky দিয়ে throughput লাফ দিলো
Round-robin-এ প্রতিটা মেসেজ আলাদা partition-এ যাওয়ায় প্রতিটা partition-এর batch ছোট থেকে যেত (many small batches) — network overhead বেশি। Sticky partitioner একই সময়ে অনেক ছোট মেসেজ একই partition-এ জমিয়ে একটা বড় batch বানায় — কম network round-trip-এ বেশি ডেটা পাঠানো যায়। LinkedIn-এর বেঞ্চমার্কে এটা latency প্রায় অর্ধেক করে দিয়েছিল উচ্চ-থ্রুপুট ওয়ার্কলোডে।
রিয়েল-লাইফ উদাহরণ
একটা high-volume logging pipeline যেখানে প্রতিটা লগ লাইন আলাদা মেসেজ, কোনো meaningful key নেই (যেমন raw application log)। Sticky partitioner এখানে ideal — key না থাকলেও batch efficiency বজায় থাকে, round-robin-এর তুলনায় অনেক কম network overhead।
ড্রব্যাক
- Sticky partitioning-এ short burst-এ load distribution সাময়িকভাবে অসম হতে পারে (একটা batch ভরা পর্যন্ত একই partition-এ যায়) — কিন্তু গড়ে সময়ের সাথে ব্যালেন্স হয়ে যায়।
- Custom partitioner লিখলে এই optimization হারানোর ঝুঁকি থাকে যদি সঠিকভাবে ডিজাইন না করা হয়।
Batching-এর গণিত: batch.size vs linger.ms
দুটো নব, একটা দড়ি টানাটানি
batch.size(bytes, ডিফল্ট 16KB) — একটা batch এই সাইজে পৌঁছালে সাথে সাথে পাঠানো হয়,linger.msঅপেক্ষা না করেই।linger.ms(ডিফল্ট 0) — batch পূর্ণ না হলেও, এই সময় পর্যন্ত অপেক্ষা করবে আরও মেসেজ জমা হওয়ার জন্য, তারপর যা আছে তাই পাঠাবে।
Latency vs Throughput
linger.ms=0 মানে প্রায় সাথে সাথে পাঠানো — কম latency, কিন্তু ছোট ছোট batch, বেশি network overhead, কম throughput। linger.ms বাড়ালে (যেমন 20ms) batch বড় হওয়ার সুযোগ পায় — throughput বাড়ে, কিন্তু প্রতিটা মেসেজ কমপক্ষে সেই সময়টা অপেক্ষা করে — latency বাড়ে।
রিয়েল-লাইফ উদাহরণ
আপনার notification pipeline-এ যদি user-facing "instant push notification" পাঠান, linger.ms=0-5 রাখা উচিত (কম latency priority)। কিন্তু analytics event pipeline-এ (যেখানে কয়েক সেকেন্ড দেরি সমস্যা না) linger.ms=50-100 দিলে throughput অনেক বাড়বে, broker-এ network request সংখ্যা কমবে, cost-efficiency বাড়বে।
গণিতের একটা উদাহরণ
ধরুন প্রতি সেকেন্ডে ১০,০০০টা ১০০-বাইট মেসেজ পাঠাচ্ছেন। linger.ms=0-এ প্রতিটা প্রায় আলাদা network request হতে পারে (যদি produce rate batch.size-এর চেয়ে ধীর হয়) — হাজার হাজার request/sec। linger.ms=20-এ প্রতি ২০ms-এ একটা বড় batch (~২০০ মেসেজ = ২০KB, batch.size-এর কাছাকাছি) — request সংখ্যা প্রায় ৫০/sec-এ নেমে আসে, একই ডেটা throughput-এ।
ড্রব্যাক
linger.msবেশি সেট করলে low-traffic সময়ে (যেখানে batch কখনো পূর্ণ হয় না) প্রতিটা মেসেজ অহেতুক পুরোlinger.msসময় অপেক্ষা করবে — latency-sensitive ব্যবহারে এটা বিপজ্জনক যদি ভুলে বেশি সেট করা হয়।batch.sizeঅনেক বড় করলেbuffer.memoryদ্রুত পূর্ণ হয়ে যেতে পারে যদি broker slow হয় — backpressure তৈরি হয়।
acks=0 / 1 / all — প্রতিটার ফেইলিওর মোড
তিনটা লেভেল
acks=0— producer broker-এর response-এর অপেক্ষাই করে না, পাঠিয়ে দিয়েই পরের মেসেজে চলে যায়। সর্বোচ্চ throughput, কিন্তু broker ডাউন থাকলে বা network এরর হলে নীরবে ডেটা হারিয়ে যায় — কোনো এরর ফেরত আসে না।acks=1— শুধু leader লিখে ack পাঠায়, follower-দের replicate হওয়ার অপেক্ষা করে না। যদি leader ack পাঠানোর ঠিক পরপরই crash করে (ডেটা follower-এ replicate হওয়ার আগেই), সেই মেসেজ হারিয়ে যায় যদিও producer সফল ack পেয়েছিল।acks=all(বা-1) — সব ISR replica-তে write হওয়ার পর ack আসে (min.insync.replicas-এর সাথে মিলিয়ে, Module 7)। সর্বোচ্চ durability, কিন্তু সবচেয়ে বেশি latency।
রিয়েল-লাইফ উদাহরণ
PostgreSQL-এর synchronous_commit সেটিং-এর সাথে সরাসরি তুলনা করা যায়: synchronous_commit=off ≈ acks=1 (leader-লেখা কিন্তু standby sync নিশ্চিত না), synchronous_commit=on with sync standby ≈ acks=all। যেমন financial transaction-এর জন্য durability priority (acks=all), কিন্তু raw clickstream/metrics-এর জন্য throughput priority (acks=1 বা এমনকি 0) — যেখানে মাঝেমধ্যে একটা মেসেজ হারালে বড় সমস্যা না।
ড্রব্যাক টেবিল
| Setting | Throughput | Durability | কোথায় ব্যবহার |
|---|---|---|---|
acks=0 |
সর্বোচ্চ | সবচেয়ে ঝুঁকিপূর্ণ | metrics, non-critical telemetry |
acks=1 |
মাঝারি-বেশি | leader-crash-এ ডেটা হারানোর ঝুঁকি | সাধারণ event log, যেখানে occasional loss গ্রহণযোগ্য |
acks=all |
সবচেয়ে কম | সর্বোচ্চ (min.insync.replicas সহ) | payment, order, inventory — critical data |
Payment বা order pipeline-এ
acks=1ব্যবহার করা একটা ক্লাসিক প্রোডাকশন ভুল — মনে হয় কাজ করছে (৯৯.৯% সময়), কিন্তু leader failover-এর ঠিক মুহূর্তে ডেটা নিঃশব্দে হারিয়ে যায়।
Compression: gzip / snappy / lz4 / zstd — কোনটা কখন
কোডেকগুলোর তুলনা
| কোডেক | Compression ratio | CPU খরচ | Speed |
|---|---|---|---|
| gzip | সর্বোচ্চ | সবচেয়ে বেশি | ধীর |
| snappy | কম | কম | দ্রুত |
| lz4 | মাঝারি | সবচেয়ে কম | সবচেয়ে দ্রুত |
| zstd | gzip-এর কাছাকাছি বা ভালো | মাঝারি | দ্রুত (ভালো ট্রেড-অফ) |
Compression producer-সাইডে batch-level এ হয় (পুরো batch একসাথে compress হয়, প্রতি মেসেজ আলাদা না) — এই জন্যই বড় batch (বেশি linger.ms/batch.size) compression ratio-কে আরও ভালো করে, কারণ compress করার জন্য বেশি redundant প্যাটার্ন পাওয়া যায়।
CPU vs Network ট্রেড-অফ
Compression CPU খরচ করে network bandwidth বাঁচানোর জন্য। Network-bound ওয়ার্কলোডে (cross-region replication, ধীর লিংক) ভালো compression ratio (gzip/zstd) মূল্যবান। CPU-bound বা low-latency ওয়ার্কলোডে lz4/snappy ভালো — কম CPU খরচ, দ্রুত।
রিয়েল-লাইফ উদাহরণ
Debezium থেকে আসা JSON-ভিত্তিক CDC ইভেন্ট প্রায়ই highly repetitive (একই schema, একই ফিল্ড নাম বারবার) — এখানে zstd বা gzip চমৎকার compression ratio দেয় (৭০-৮০%+ সাইজ কমতে পারে)। কিন্তু একটা latency-critical order-matching engine-এর জন্য lz4-ই বেশি যুক্তিসঙ্গত, যেখানে প্রতি মিলিসেকেন্ড গুরুত্বপূর্ণ।
ড্রব্যাক
- Compression broker-এ CPU খরচ করায় যদি broker-সাইড decompress+recompress দরকার হয় (ভিন্ন codec version-এর মধ্যে সামঞ্জস্যতা সমস্যায়)।
- ছোট batch-এ compression overhead তার সুবিধার চেয়ে বেশি হতে পারে (compression header overhead ছোট ডেটার জন্য অপেক্ষাকৃত বড়)।
- consumer-সাইডেও decompress করতে CPU লাগে — producer আর consumer উভয় দিকেই CPU বাজেট মাথায় রাখতে হবে।
Retry, max.in.flight.requests, আর Ordering কীভাবে ভাঙে
সমস্যাটা কোথায় শুরু
Producer network এরর/timeout হলে retries অনুযায়ী আবার পাঠানোর চেষ্টা করে। কিন্তু যদি একসাথে একাধিক request "in flight" (broker-এর response-এর অপেক্ষায়) থাকে (max.in.flight.requests.per.connection > 1, ডিফল্ট 5), আর ২য় batch সফল হয়ে যায় কিন্তু ১ম batch ব্যর্থ হয়ে retry হয়, তাহলে retry করা ১ম batch ২য় batch-এর পরে broker-এ পৌঁছাতে পারে — partition-এর ভেতরেই ordering ভেঙে যায়, যদিও উভয়ই একই partition-এ (একই key)।
সমাধান
- Idempotent producer চালু করুন (
enable.idempotence=true, Kafka 3.0+ থেকে ডিফল্ট চালু যখন অন্য কনফ্লিক্টিং সেটিং নেই) — এটা broker-সাইডে sequence number দিয়ে সঠিক ক্রম নিশ্চিত করে, এমনকিmax.in.flight.requests৫ পর্যন্ত থাকলেও (idempotent producer-এ সর্বোচ্চ ৫ পর্যন্ত in-flight সাপোর্টেড with ordering safety)। - অথবা
max.in.flight.requests.per.connection=1সেট করুন — ordering নিশ্চিত হয় কিন্তু throughput কমে যায় (প্রতিটা request সিরিয়ালি যায়)।
রিয়েল-লাইফ উদাহরণ
একটা inventory-update pipeline-এ যদি ordering ভাঙে (একটা "stock -5" ইভেন্ট "stock +10" ইভেন্টের আগে চলে আসে ভুল ক্রমে), ফাইনাল stock count ভুল হয়ে যাবে। এই ধরনের critical ordering-নির্ভর pipeline-এ idempotent producer (Kafka 3.0+ ডিফল্ট) mandatory।
ড্রব্যাক
max.in.flight.requests=1দিয়ে ordering নিশ্চিত করলে throughput বড় ধাক্কা খায় (প্রতিটা batch-এর ack-এর অপেক্ষা করতে হয় পরেরটা পাঠানোর আগে)।- Idempotent producer সবসময় ডিফল্টে চালু না থাকলে (পুরনো client ভার্সনে) — legacy কনফিগারেশন migrate করার সময় এই গ্যাপ চেক করা জরুরি।
Idempotent Producer — Producer ID (PID) + Sequence Number
কীভাবে কাজ করে
enable.idempotence=true হলে, producer connect করার সময় broker একটা ইউনিক Producer ID (PID) বরাদ্দ করে। প্রতিটা মেসেজের সাথে একটা monotonically increasing sequence number (প্রতি partition-এ আলাদা) যুক্ত থাকে। Broker প্রতিটা incoming মেসেজের PID+sequence চেক করে — যদি একই PID+sequence আগেই দেখা থাকে (retry-জনিত duplicate), broker সেটা silently deduplicate করে দেয়, দ্বিতীয়বার লেখে না, কিন্তু producer-কে success ack পাঠায়।
কেন দরকার
Retry ছাড়া idempotence অর্থহীন — কিন্তু network এরর/timeout-এর পর retry না করলে ডেটা হারানোর ঝুঁকি (producer জানে না আসলে broker-এ লেখা হয়েছিল কিনা)। Idempotent producer এই দোটানা সমাধান করে: নির্দ্বিধায় retry করুন, duplicate broker নিজেই সামলাবে।
রিয়েল-লাইফ উদাহরণ
একটা পেমেন্ট ইভেন্ট পাঠানোর সময় network timeout হলো, কিন্তু আসলে broker-এ লেখা হয়ে গিয়েছিল (শুধু ack হারিয়ে গেছে network-এ)। Idempotence ছাড়া retry করলে সেই পেমেন্ট দুইবার রেকর্ড হয়ে যাবে — একটা ক্লাসিক duplicate-charge বাগ। Idempotent producer-এ broker sequence number দেখে বুঝে যায় এটা duplicate, দ্বিতীয়বার লেখে না।
সীমাবদ্ধতা / ড্রব্যাক
- Idempotence শুধু single producer session, single partition-এর ভেতরে guarantee দেয়। Producer restart হলে নতুন PID বরাদ্দ হয় — আগের session-এর সাথে deduplicate করার সম্পর্ক থাকে না।
- এটা cross-partition বা cross-topic atomicity দেয় না — সেটার জন্য দরকার পূর্ণ Transactions (Module 9)।
- সামান্য broker-সাইড overhead আছে (PID+sequence ট্র্যাক করা), কিন্তু ব্যবহারিকভাবে এত কম যে প্রায় সবসময় চালু রাখাই উচিত (Kafka 3.0+ থেকে এটাই ডিফল্ট আচরণ)।
ল্যাব: Python (confluent-kafka) Producer, linger.ms বদলে Throughput Benchmark
সেটআপ
pip install confluent-kafka
Benchmark script
import time
from confluent_kafka import Producer
def make_producer(linger_ms):
return Producer({
'bootstrap.servers': 'localhost:9092',
'linger.ms': linger_ms,
'batch.size': 16384,
'compression.type': 'lz4',
'acks': '1',
})
def benchmark(linger_ms, n=50_000):
p = make_producer(linger_ms)
start = time.time()
for i in range(n):
p.produce('bench.topic', key=str(i % 100), value=f'msg-{i}')
p.poll(0) # non-blocking, callback trigger করার জন্য
p.flush()
elapsed = time.time() - start
print(f'linger.ms={linger_ms}: {n/elapsed:.0f} msgs/sec, {elapsed:.2f}s total')
for lm in [0, 5, 20, 50, 100]:
benchmark(lm)
যা লক্ষ্য করবেন
linger.ms=0-এ throughput সবচেয়ে কম (প্রতিটা মেসেজ প্রায় আলাদা network round-trip)। linger.ms বাড়ানোর সাথে সাথে throughput উল্লেখযোগ্যভাবে বাড়বে, একটা পয়েন্টের পর (batch.size limit-এ পৌঁছালে) diminishing returns শুরু হবে।
চ্যালেঞ্জ
acks='1'থেকেacks='all'-এ বদলে একই বেঞ্চমার্ক চালান — throughput-এর পার্থক্য পরিমাপ করুন।compression.typenone,snappy,lz4,gzip,zstd— প্রতিটা দিয়ে চালিয়ে throughput আর broker-সাইড disk ব্যবহার (kafka-log-dirs.sh) তুলনা করুন।
ইন্টারভিউ প্রশ্ন — Module 3
প্র.১ — acks=1-এ কীভাবে ডেটা হারাতে পারে, যদিও producer একটা successful ack পেয়েছে?
উত্তর: acks=1-এ শুধু leader write করেই ack পাঠায়, follower replicate হওয়ার অপেক্ষা করে না। যদি leader ack পাঠানোর ঠিক পরপরই (replicate হওয়ার আগে) crash করে, নতুন leader নির্বাচিত হবে যার কাছে সেই মেসেজ নেই — producer-এর দৃষ্টিতে সফল লেখা আসলে হারিয়ে গেছে।
প্র.২ — Sticky Partitioner round-robin-এর চেয়ে ভালো কেন?
উত্তর: round-robin প্রতিটা মেসেজ আলাদা partition-এ পাঠায়, ফলে ছোট ছোট batch তৈরি হয়। Sticky partitioner একটা batch পূর্ণ না হওয়া বা linger.ms শেষ না হওয়া পর্যন্ত একই partition-এ জমা করে — বড় batch, কম network request, বেশি throughput।
প্র.৩ — max.in.flight.requests.per.connection > 1 কীভাবে ordering ভাঙতে পারে?
উত্তর: একাধিক request একসাথে in-flight থাকলে, প্রথম batch ব্যর্থ হয়ে retry হলে সেটা দ্বিতীয় (ইতিমধ্যে সফল) batch-এর পরে broker-এ পৌঁছাতে পারে — সেই partition-এর ভেতরেই মেসেজ ক্রম উল্টে যায়। Idempotent producer অথবা max.in.flight.requests=1 দিয়ে এটা এড়ানো যায়।
প্র.৪ — Idempotent producer duplicate কীভাবে ঠেকায়? উত্তর: প্রতিটা producer session-এ broker একটা PID বরাদ্দ করে, প্রতিটা মেসেজে (per-partition) একটা sequence number যুক্ত থাকে। Broker আগে-দেখা PID+sequence পেলে সেটা duplicate ধরে নিয়ে দ্বিতীয়বার লেখে না, কিন্তু success ack পাঠায় — retry নির্দ্বিধায় করা যায়।
প্র.৫ — linger.ms বাড়ানোর ট্রেড-অফ কী?
উত্তর: throughput বাড়ে (বড় batch, কম request) কিন্তু per-message latency বাড়ে (প্রতিটা মেসেজ কমপক্ষে linger.ms সময় অপেক্ষা করে batch পূর্ণ হওয়ার আগে পাঠানোর জন্য)।
প্র.৬ — একটা payment service-এর জন্য producer কনফিগ ডিজাইন করতে বললে কী সেট করবেন?
উত্তর: acks=all, enable.idempotence=true (Kafka 3.0+ ডিফল্ট), min.insync.replicas≥2 (broker-সাইড, Module 7), মাঝারি retries সহ। Throughput priority না, durability + no-duplicate priority।
কর্নার কেস — Module 3
send()-এর রিটার্ন করাFutureইগনোর করা — অনেক কোডেproducer.send(record)কল করে রেজাল্ট চেক করা হয় না; broker-সাইড এরর (যেমনRecordTooLargeException) নীরবে মিস হয়ে যায়।buffer.memoryপূর্ণ হয়ে গেলেsend()ব্লক করে (max.block.msপর্যন্ত), তারপরTimeoutException— burst traffic-এ broker slow হলে এটা cascading failure তৈরি করতে পারে (application thread-ও ব্লক হয়ে যায়)।acks=0-এ broker সম্পূর্ণ ডাউন থাকলেও কোনো এরর আসে না — producer শুধু নেটওয়ার্কে পাঠিয়ে দেয়, broker আছে কিনা তাও যাচাই করে না। মনিটরিং ছাড়া এই ডেটা লস কখনো ধরা পড়বে না।- Message size
max.request.size/broker-এরmessage.max.bytes-এর চেয়ে বড় হলে producer-সাইডেই ব্যর্থ হবে — বড় payload (যেমন attachment সহ ইভেন্ট) পাঠানোর আগে সাইজ চেক জরুরি, নাহলে প্রোডাকশনে হুট করে এরর। - Idempotent producer PID শুধু session-scoped — producer রিস্টার্ট হলে নতুন PID, পুরনো session-এর সাথে deduplication সম্পর্ক নেই। ফলে অ্যাপ্লিকেশন ক্র্যাশ করে রিস্টার্ট হলে এবং একই মেসেজ আবার পাঠালে সেটা নতুন হিসেবেই গণ্য হবে (deduplicate হবে না) — সেই কারণে end-to-end deduplication দরকার হলে application-level idempotency key ব্যবহার করা ভালো।
- Compression codec broker cluster-এ সাপোর্টেড না থাকলে (পুরনো broker ভার্সন) produce ব্যর্থ হবে — zstd তুলনামূলক নতুন কোডেক, পুরনো broker-এ কম্প্যাটিবিলিটি চেক জরুরি।
MODULE 4: Consumer — ভেতর থেকে
Producer-এর পর এবার consumer-এর ভেতরের যন্ত্র বোঝার পালা — poll() লুপ ঠিক কী করে, consumer group protocol কীভাবে কাজ করে, rebalance কেন হয় আর কীভাবে ঠিকমতো হ্যান্ডল করবেন। কভার হবে: Group Coordinator protocol, rebalance algorithm-এর বিবর্তন (Range → Sticky → Cooperative Sticky → KIP-848), offset commit strategy, timeout tuning (প্রোডাকশনে সবচেয়ে বেশি আগুন লাগার জায়গা), consumer lag, আর static membership।
Poll Loop-এর আসল রূপ
কী ঘটে একটা poll() কলে
Consumer অ্যাপ্লিকেশন কোড একটা লুপে বারবার poll(timeout) কল করে। প্রতিটা poll() কল একসাথে অনেক কিছু করে:
- Assign করা partition থেকে নতুন মেসেজ fetch করে আনে (broker-এর fetch request)।
- Group Coordinator-কে heartbeat পাঠায় ("আমি এখনো জীবিত")।
- যদি rebalance চলমান থাকে, তাতে অংশ নেয় (JoinGroup/SyncGroup call করতে পারে)।
- Auto-commit চালু থাকলে, নির্দিষ্ট বিরতিতে offset commit করে।
কেন এটা গুরুত্বপূর্ণ বোঝা
একটা সাধারণ ভুল ধারণা: heartbeat একটা আলাদা background thread-এ চলে, তাই poll() কল ধীর হলে সমস্যা নেই। কিন্তু পুরনো Kafka client-এ heartbeat poll() কলের ভেতরেই পাঠানো হতো — তাই poll() কল-এর মধ্যবর্তী সময় (মেসেজ প্রসেস করার সময়) খুব বেশি হলে heartbeat মিস হয়ে consumer group থেকে বাদ পড়তে পারতো। আধুনিক client (0.10.1+)-এ heartbeat আলাদা ব্যাকগ্রাউন্ড thread-এ চলে, কিন্তু max.poll.interval.ms-এর মধ্যে poll() না ডাকলে তাও group থেকে বাদ পড়ে (Module 4 পরের bullet)।
রিয়েল-লাইফ উদাহরণ
একটা consumer যদি প্রতিটা মেসেজ প্রসেস করতে ৫ সেকেন্ড নেয় (যেমন একটা ভারী ML inference কল), আর একবারে poll() ৫০০টা মেসেজ নিয়ে আসে, তাহলে পরের poll() কল করতে ৫০০×৫=২৫০০ সেকেন্ড লাগবে — max.poll.interval.ms (ডিফল্ট ৫ মিনিট = ৩০০ সেকেন্ড)-এর চেয়ে অনেক বেশি — consumer group থেকে বাদ পড়ে যাবে, rebalance trigger হবে।
ড্রব্যাক
- Poll-based মডেল push-based-এর তুলনায় latency-তে সামান্য পিছিয়ে (broker নিজে থেকে push করে না, consumer টেনে আনে)।
- Batch size (
max.poll.records) ভুল টিউন করলে হয় processing time বেশি হয়ে যাবে (উপরের সমস্যা), অথবা খুব ছোট batch-এ throughput কমে যাবে।
Consumer Group Protocol: Group Coordinator, JoinGroup → SyncGroup → Heartbeat
যন্ত্রপাতি
প্রতিটা consumer group-এর একটা নির্দিষ্ট broker Group Coordinator হিসেবে কাজ করে (কোন broker সেটা নির্ধারিত হয় group-id হ্যাশ করে)। Coordinator পুরো group-এর সদস্যপদ আর partition assignment ট্র্যাক করে।
প্রোটোকল ধাপ
- JoinGroup — একটা consumer group-এ যোগ দিতে চাইলে coordinator-কে জানায়। Coordinator অপেক্ষা করে সব member join করা পর্যন্ত (বা timeout), তারপর একজনকে group leader বানায়।
- SyncGroup — group leader-ই partition assignment-এর সিদ্ধান্ত নেয় (assignor অ্যালগরিদম চালিয়ে, পরের bullet দেখুন), সেটা coordinator-কে পাঠায়, coordinator সেটা সব member-কে distribute করে।
- Heartbeat — প্রতিটা member নিয়মিত heartbeat পাঠায় coordinator-কে বাঁচার প্রমাণ দিতে (
heartbeat.interval.ms)। মিস করলে (session.timeout.msপার হলে) coordinator ধরে নেয় সদস্য মারা গেছে, নতুন rebalance শুরু করে।
রিয়েল-লাইফ উদাহরণ
PostgreSQL-এ এর সরাসরি সমতুল্য নেই (single-node), কিন্তু Redis Streams consumer group-এর সাথে তুলনা করা যায়: Redis-এ কোনো central "coordinator" নেই যেটা active rebalance করে — এটা optimistic, consumer নিজেই claim করে entry (XCLAIM)। Kafka-র coordinator-চালিত মডেল অনেক বেশি centralized ও deterministic — এই জন্যই partition assignment predictable, কিন্তু coordinator একটা single point of temporary bottleneck (rebalance-এর সময়)।
ড্রব্যাক
- JoinGroup-SyncGroup প্রোটোকল stop-the-world-এর মতো আচরণ করতো পুরনো (eager) rebalance-এ — পুরো group rebalance শেষ না হওয়া পর্যন্ত কোনো consumer মেসেজ প্রসেস করতে পারতো না (পরের bullet-এ cooperative rebalance দিয়ে সমাধান)।
- Coordinator broker যদি নিজেই fail করে, নতুন coordinator নির্বাচন লাগে — এই সময় group সাময়িকভাবে অস্থিতিশীল থাকে।
Rebalance Algorithms: Range, Round-robin, Sticky, Cooperative Sticky (KIP-429), KIP-848
Assignor-এর বিবর্তন
- Range — প্রতিটা topic আলাদাভাবে partition-এর রেঞ্জ ভাগ করে consumer-দের মধ্যে। একাধিক topic থাকলে একই consumer বারবার বেশি partition পেতে পারে (imbalance)।
- Round-robin — সব topic-এর সব partition একসাথে নিয়ে round-robin ভাগ করে — বেশি ব্যালেন্সড, কিন্তু rebalance হলে পুরো assignment থেকে শুরু করে নতুন করে ভাগ হয় (কোনো sticky memory নেই)।
- Sticky — নতুন assignment করার সময় আগের assignment যতটা সম্ভব বজায় রাখে — শুধু প্রয়োজনীয় partition-ই move হয়, বাকিগুলো একই consumer-এ থেকে যায়। কম churn।
- Cooperative Sticky (KIP-429, Kafka 2.4+) — Sticky-র সাথে incremental rebalance যোগ করে: rebalance-এর সময় পুরো group একসাথে সব partition ছেড়ে দেয় না (eager), শুধু যেসব partition move হবে সেগুলোই revoke হয় — বাকি সব consumer নিজেদের অপরিবর্তিত partition নিয়ে কাজ চালিয়ে যেতে পারে rebalance চলাকালীনও।
Eager vs Incremental — কেন stop-the-world সমস্যা
Eager rebalance-এ (Range/Round-robin/Sticky-এর পুরনো protocol): rebalance শুরু হলে সব consumer সব partition ছেড়ে দেয়, তারপর নতুন assignment হয়, তারপর সবাই আবার শুরু করে। একটা বড় cluster-এ (শত শত partition) এই "সবাই থামো, আবার শুরু করো" প্রক্রিয়া কয়েক সেকেন্ড থেকে কয়েক মিনিট প্রসেসিং বন্ধ রাখতে পারে — প্রোডাকশনে এটা "rebalance storm" নামে পরিচিত সমস্যার মূল কারণ। Cooperative incremental rebalance এই downtime কমিয়ে আনে শুধু প্রভাবিত partition move করে।
KIP-848 — নতুন Server-side Rebalance Protocol (Kafka 4.x)
পুরনো protocol-এ rebalance logic client-সাইডে চলতো (client নিজেই assignment হিসাব করতো) — এতে client bug rebalance storm তৈরি করতে পারতো। KIP-848 rebalance logic broker-সাইডে (group coordinator-এ) সরিয়ে আনে — client শুধু নিজের assignment জানতে চায়, পুরো protocol অনেক সহজ ও দ্রুত হয়, incremental rebalance ডিফল্ট আচরণ হয়ে যায়।
রিয়েল-লাইফ উদাহরণ
আপনার নোটিফিকেশন pipeline-এ ৬টা partition, ৩টা consumer instance চলছে — একটা নতুন deployment-এ ৪র্থ instance যোগ হলো। Eager rebalance-এ প্রথম ৩টা instance-ও সাময়িকভাবে সব প্রসেসিং থামিয়ে দেবে (কয়েক সেকেন্ড ডেলিভারি ডিলে হবে)। Cooperative sticky-তে শুধু ১-২টা partition move হবে, বাকি consumer-রা নির্বিঘ্নে কাজ চালিয়ে যাবে।
ড্রব্যাক
- Cooperative rebalance ডিফল্ট assignor না — client কনফিগে
partition.assignment.strategy=CooperativeStickyAssignorস্পষ্টভাবে সেট করতে হয় (KIP-848 পূর্ণ চালু না হওয়া পর্যন্ত)। - Eager থেকে Cooperative-এ মাইগ্রেশন সাবধানে করতে হয় (দুই-ধাপে rolling upgrade, নাহলে protocol mismatch)।
Offset Commit: Auto vs Manual, __consumer_offsets Internal Topic
কোথায় offset সংরক্ষিত হয়
Kafka নিজেই একটা internal, compacted topic __consumer_offsets-এ প্রতিটা consumer group-এর প্রতিটা partition-এর committed offset রাখে (key = group+topic+partition, value = offset)। এটা compacted (Module 6) — প্রতিটা key-এর শুধু latest value থাকে।
Auto vs Manual
- Auto-commit (
enable.auto.commit=true, ডিফল্ট) — নির্দিষ্ট বিরতিতে (auto.commit.interval.ms) automatically শেষ পোল করা offset commit হয়। সহজ, কিন্তু বিপজ্জনক: মেসেজ প্রসেস করার আগেই পরেরpoll()কলে auto-commit হয়ে যেতে পারে — যদি প্রসেসিং-এর মাঝে ক্র্যাশ হয়, সেই মেসেজ হারিয়ে গেছে বলে গণ্য হবে (offset আগেই commit হয়ে গেছে যদিও প্রসেস হয়নি)। - Manual commit (
enable.auto.commit=false,commitSync()/commitAsync()) — প্রসেসিং সম্পূর্ণ হওয়ার পরে explicit commit করা হয় — at-least-once guarantee-এর জন্য এটাই সঠিক প্যাটার্ন।
রিয়েল-লাইফ উদাহরণ
একটা পেমেন্ট প্রসেসিং consumer-এ auto-commit চালু থাকলে: মেসেজ poll হলো, offset auto-commit-এর টাইমারে committed হয়ে গেলো, কিন্তু তারপরই application ক্র্যাশ করলো পেমেন্ট প্রসেস করার আগে — restart-এর পর consumer আগের committed offset থেকে শুরু করবে (এই মেসেজের পরে), মানে সেই পেমেন্ট কখনো প্রসেস হবে না। Manual commit ব্যবহার করলে এই সমস্যা এড়ানো যায়।
ড্রব্যাক
- Manual
commitSync()প্রতিটা batch-এর পর ব্লক করে (broker round-trip) — throughput কমায়।commitAsync()দ্রুত কিন্তু callback-এ এরর হ্যান্ডলিং জটিল। __consumer_offsetstopic-এ যদি অস্বাভাবিক বেশি consumer group + ঘন ঘন commit থাকে, এই internal topic-ই একটা hotspot হয়ে উঠতে পারে বড় cluster-এ।
session.timeout.ms, heartbeat.interval.ms, max.poll.interval.ms — প্রোডাকশনের সবচেয়ে বড় আগুন
তিনটা টাইমার, তিনটা আলাদা কাজ
heartbeat.interval.ms(ডিফল্ট 3s) — background thread কত ঘন ঘন heartbeat পাঠায়।session.timeout.ms(ডিফল্ট 45s, ভার্সনভেদে ভিন্ন) — কতক্ষণ heartbeat না পেলে coordinator ধরে নেবে consumer মারা গেছে, group থেকে বাদ দিয়ে rebalance শুরু করবে। এটা network glitch/GC pause সহ্য করার জন্য।max.poll.interval.ms(ডিফল্ট 5min) — দুইটাpoll()কলের মধ্যে সর্বোচ্চ কত সময় যেতে পারে। এই সময়ের মধ্যেpoll()না ডাকলে (মানে processing অনেক ধীর), consumer-কে group থেকে বাদ দেওয়া হয় — এমনকি heartbeat thread ঠিকমতো চললেও (আধুনিক client-এ heartbeat আলাদা thread-এ চলে, কিন্তু max.poll.interval আলাদাভাবে চেক হয়)।
কেন এখানে সবচেয়ে বেশি ভুল হয়
বেশিরভাগ প্রোডাকশন rebalance storm আসে processing time আর max.poll.interval.ms-এর মধ্যে মিসম্যাচ থেকে — একটা downstream API স্লো হয়ে গেলে, বা একটা বড় batch প্রসেস করতে বেশি সময় লাগলে, consumer group থেকে বাদ পড়ে যায়, নতুন rebalance হয়, partition অন্য consumer-এ যায়, পুরনো consumer আবার join করার চেষ্টা করে — এই চক্র বারবার ঘটলে group কখনোই স্থির হয় না ("rebalance storm")।
রিয়েল-লাইফ উদাহরণ
max.poll.records=500 আর একটা downstream Elasticsearch bulk-insert যা মাঝেমাঝে ধীর হয়ে যায় (৩০+ সেকেন্ড) — এমন একটা consumer যদি max.poll.interval.ms=5min-এর মধ্যেও মাঝে মাঝে সীমা ছাড়িয়ে যায়, এলোমেলোভাবে group থেকে ছিটকে যাবে, প্রতিবার rebalance storm ট্রিগার করবে। সমাধান: হয় max.poll.records কমান (batch ছোট করুন), অথবা max.poll.interval.ms বাড়ান processing time-এর বাস্তবতা অনুযায়ী।
ড্রব্যাক
session.timeout.msখুব কম রাখলে সামান্য GC pause বা network glitch-এই অপ্রয়োজনীয় rebalance হবে।- খুব বেশি রাখলে সত্যিকারের crash detect হতে দেরি হবে, সেই সময় ওই partition-এর মেসেজ প্রসেসিং থেমে থাকবে (কেউ claim করছে না)।
Consumer Lag — মাপা, বোঝা, ঠিক করা
সংজ্ঞা
Consumer lag = (partition-এর Log End Offset / High Watermark) − (consumer group-এর committed offset)। এটা মাপে একটা consumer group broker-এর তুলনায় কতটা "পিছিয়ে" আছে।
কীভাবে মাপবেন
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group notification-senders
এটা প্রতিটা partition-এর CURRENT-OFFSET, LOG-END-OFFSET, আর LAG দেখায়। Prometheus/Grafana-তে সাধারণত Kafka Exporter বা Burrow দিয়ে এটা মনিটর করা হয়, alert threshold সহ।
Lag বাড়ার সাধারণ কারণ
- Producer rate consumer-এর processing rate ছাড়িয়ে গেছে (পার্মানেন্ট undersizing)।
- Rebalance storm (partition বারবার হাতবদল হচ্ছে, কেউ স্থিরভাবে কাজ করতে পারছে না)।
- একটা downstream dependency (DB, external API) ধীর হয়ে গেছে — consumer সেখানে ব্লক হয়ে আছে।
- একটা "poison pill" মেসেজ যেটা বারবার প্রসেসিং exception ছুঁড়ছে, consumer আটকে আছে সেই একটা মেসেজেই (Module 14)।
রিয়েল-লাইফ উদাহরণ
আপনার notification pipeline-এ যদি ঈদের সময় ট্রাফিক ১০ গুণ বেড়ে যায় কিন্তু consumer instance সংখ্যা একই থাকে, lag ক্রমাগত বাড়তে থাকবে — ব্যবহারকারীরা নোটিফিকেশন কয়েক ঘণ্টা দেরিতে পাবে। সমাধান: partition আর consumer instance সংখ্যা autoscale করা (HPA-এর মতো), অথবা আগে থেকেই capacity planning-এ traffic spike হিসাব করা (Module 13)।
ড্রব্যাক / সতর্কতা
- শুধু raw lag (মেসেজ সংখ্যা) দেখলে বিভ্রান্তিকর হতে পারে — time-based lag (কতক্ষণ আগের ডেটা প্রসেস হচ্ছে) বেশি অর্থবহ ব্যবসায়িক প্রভাবের জন্য।
- Lag = 0 মানেই সব ঠিক আছে এমন না — consumer একেবারেই বন্ধ (dead) হয়ে গেলেও producer থামলে lag সাময়িকভাবে বাড়বে না, ভুল স্বস্তি দিতে পারে।
Static Membership (group.instance.id) — Rebalance Storm বন্ধ করা
সমস্যা যা এটা সমাধান করে
নরমাল (dynamic) membership-এ, একটা consumer instance restart হলে (deployment, pod reschedule, সাময়িক network glitch) সেটা group থেকে বাদ পড়ে যায় (session.timeout.ms পার হলে) আর নতুন সদস্য হিসেবে আবার join করে — একটা নতুন member id নিয়ে। এই প্রতিটা join/leave-ই একটা নতুন rebalance ট্রিগার করে — Kubernetes-এ rolling deployment-এর সময় ধারাবাহিকভাবে instance restart হলে, প্রতিটা restart-ই rebalance ঘটায়, পুরো group ঘন ঘন অস্থির থাকে।
কীভাবে সমাধান করে
group.instance.id সেট করলে সেই consumer একটা স্থায়ী পরিচয় পায়। Restart হলে coordinator বুঝতে পারে "এটা তো আগের একই সদস্য, শুধু সাময়িক disconnect হয়েছিল" — session.timeout.ms-এর বদলে (dynamic member যেটা দ্রুত ব্যবহার করে) একটা আলাদা, সাধারণত বড়, grace period (কনফিগারযোগ্য) দেওয়া হয় পুনরায় সংযুক্ত হওয়ার জন্য, rebalance ছাড়াই।
রিয়েল-লাইফ উদাহরণ
Kubernetes-এ একটা NotificationSenderService deployment-এ rolling update হচ্ছে — একে একে ৫টা pod restart হচ্ছে। group.instance.id (সাধারণত pod name-এর সাথে যুক্ত, StatefulSet ব্যবহার করলে সহজ) না থাকলে প্রতিটা pod restart-ই একটা rebalance ঘটাবে — মোট ৫টা rebalance, প্রতিটাতেই সাময়িক processing pause। Static membership দিয়ে এই পুরো deployment একটাও rebalance ছাড়া সম্পন্ন হতে পারে।
ড্রব্যাক
- Static membership মানে প্রতিটা instance-এর একটা predictable, persistent identity লাগে — Kubernetes-এ এটা সাধারণত StatefulSet দরকার করে (Deployment-এ pod নাম random)।
- যদি একটা static member সত্যিই permanently মারা যায় (crash loop, কখনো ফিরে আসবে না), rebalance হতে বেশি সময় লাগবে (grace period শেষ না হওয়া পর্যন্ত সেই partition-এর processing বন্ধ থাকবে) — dynamic membership-এর চেয়ে failure detection ধীর।
ল্যাব: ৩টা Consumer, Rebalance লাইভ দেখা
সেটআপ
৬-partition topic বানান:
kafka-topics.sh --create --topic order.events \
--partitions 6 --replication-factor 1 --bootstrap-server localhost:9092
ধাপ ১ — একটা consumer group-এ ৩টা console consumer চালান
তিনটা আলাদা টার্মিনালে (একই --group সহ):
kafka-console-consumer.sh --topic order.events \
--group order-processors --bootstrap-server localhost:9092
kafka-consumer-groups.sh --describe --group order-processors --bootstrap-server localhost:9092 চালিয়ে দেখুন প্রতিটা consumer ২টা করে partition পেয়েছে (6/3=2)।
ধাপ ২ — একটা consumer বন্ধ করুন (Ctrl+C)
কয়েক সেকেন্ড পর আবার --describe চালান — দেখবেন বাকি ২টা consumer-এর মধ্যে ৬টা partition পুনর্বণ্টন হয়ে গেছে (৩টা করে)। এটাই live rebalance।
ধাপ ৩ — producer দিয়ে মেসেজ পাঠান, দেখুন কোন consumer কী পায়
Rebalance-এর মাঝে producer চালিয়ে রাখুন — দেখুন rebalance চলাকালীন সময়ে সাময়িক delay হয় কিনা console-এ।
চ্যালেঞ্জ
--consumer-property partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor দিয়ে (console consumer সরাসরি সাপোর্ট নাও করতে পারে — একটা ছোট Python script দিয়ে confluent_kafka ব্যবহার করে) একই টেস্ট আবার করুন, eager আর cooperative rebalance-এর মধ্যে pause সময়ের পার্থক্য পর্যবেক্ষণ করুন।
ইন্টারভিউ প্রশ্ন — Module 4
প্র.১ — Auto-commit কেন at-least-once guarantee ভেঙে দিতে পারে? উত্তর: Auto-commit টাইমার-ভিত্তিক, মেসেজ actually প্রসেস হয়েছে কিনা তার সাথে সম্পর্কিত না। যদি offset commit হয়ে যায় প্রসেসিং শেষ হওয়ার আগেই, আর তারপর crash হয়, restart-এর পর সেই মেসেজ আর re-deliver হবে না — কার্যত মেসেজ হারিয়ে গেলো।
প্র.২ — Cooperative Sticky rebalance কীভাবে rebalance storm কমায়? উত্তর: Eager rebalance-এ পুরো group-ই সব partition ছেড়ে দেয়, নতুন করে ভাগ করে নেয়, তাতে সবার processing থেমে যায়। Cooperative sticky শুধু যে partition-গুলো move হবে সেগুলোই revoke করে — বাকি consumer-রা নিজেদের partition নিয়ে rebalance চলাকালীনও কাজ চালিয়ে যেতে পারে।
প্র.৩ — max.poll.interval.ms আর session.timeout.ms-এর পার্থক্য কী?
উত্তর: session.timeout.ms heartbeat মিস হওয়ার সীমা (broker connectivity/liveness)। max.poll.interval.ms দুই poll কলের মধ্যে সর্বোচ্চ ব্যবধান (processing liveness) — processing স্লো হলে heartbeat thread ঠিক থাকলেও এই সীমা পার হলে consumer বাদ পড়ে।
প্র.৪ — একটা consumer group ঘন ঘন rebalance করছে (rebalance storm) — সম্ভাব্য কারণ কী কী?
উত্তর: processing time max.poll.interval.ms-এর কাছাকাছি/বেশি হয়ে যাচ্ছে, dynamic membership-এ ঘন ঘন pod restart হচ্ছে (static membership না থাকায়), অথবা session.timeout.ms খুব কম সেট করা যা সামান্য network glitch-এও trigger হয়ে যাচ্ছে।
প্র.৫ — Static membership (group.instance.id) কী সমস্যা সমাধান করে, আর এর সীমাবদ্ধতা কী?
উত্তর: rolling deployment-এ প্রতিটা instance restart-এ rebalance এড়ায় — কারণ coordinator পুরনো সদস্য হিসেবে চিনতে পারে, নির্দিষ্ট grace period-এর মধ্যে ফিরে এলে rebalance লাগে না। সীমাবদ্ধতা: instance সত্যিই permanently মারা গেলে rebalance detect হতে বেশি সময় লাগে (dynamic membership-এর তুলনায়)।
প্র.৬ — __consumer_offsets topic-এর ধরন কী, আর এটা কেন compacted?
উত্তর: এটা একটা internal Kafka topic যেখানে প্রতিটা consumer group-এর প্রতিটা partition-এর committed offset রাখা হয় (key=group+topic+partition)। Compacted হওয়ার কারণ শুধু প্রতিটা key-র সর্বশেষ মান দরকার, পুরো ইতিহাস না — এটা storage সাশ্রয় করে (Module 6)।
কর্নার কেস — Module 4
- Auto-commit + দীর্ঘ processing time = নীরব ডেটা লস — যদি auto-commit ইন্টারভাল প্রসেসিং সময়ের চেয়ে ছোট হয়, offset আগেই কমিট হয়ে যায়, ক্র্যাশে সেই মেসেজ হারায় — লগে কোনো এরর দেখাবে না।
- একটা "poison pill" মেসেজ (যেটা বারবার deserialize/process এরর ছোঁড়ে) auto-retry লজিকে consumer-কে infinite loop-এ আটকে রাখতে পারে, lag শুধু ওই একটা offset-এই আটকে থাকবে, বাকি সব মেসেজ পেছনে জমা হতে থাকবে (Module 14-এ DLQ সমাধান)।
- Rebalance চলাকালীন in-flight (uncommitted) প্রসেসিং duplicate হতে পারে — একটা partition অন্য consumer-এ চলে গেলে, আগের consumer যদি প্রসেস করেছিল কিন্তু commit করার আগেই partition হারিয়ে ফেলে, নতুন consumer সেই একই মেসেজ আবার প্রসেস করবে (at-least-once-এর স্বাভাবিক আচরণ, downstream idempotency দরকার)।
auto.offset.reset=latest(ডিফল্ট)-এ নতুন consumer group প্রথমবার চালু হলে বিদ্যমান সব পুরনো ডেটা মিস করে যাবে — শুধু নতুন produced মেসেজ থেকে শুরু করবে। ব্যাকফিল দরকার হলেearliestঅথবা explicit offset seek দরকার।- Coordinator broker যদি নিজেই crash করে, একটা নতুন coordinator নির্বাচিত হওয়া পর্যন্ত heartbeat/commit ব্যর্থ হতে পারে — সাময়িক group instability, যদিও ডেটা লস হয় না (offset নিজেই একটা replicated topic-এ থাকে)।
max.poll.recordsঅনেক বড় সেট করে ধীর downstream থাকলে, একটা single batch process করতেইmax.poll.interval.msপার হয়ে যেতে পারে — batch size আর processing time-এর মধ্যে সম্পর্ক টিউন করার সময় এই গাণিতিক সম্পর্ক ভুলে গেলে চলবে না।
MODULE 5: ডিস্কে Kafka আসলে কী লেখে
Kafka-র legendary throughput-এর রহস্য কোনো ম্যাজিক না — এটা কয়েকটা সাধারণ কিন্তু নিখুঁতভাবে প্রয়োগ করা OS-লেভেল কৌশলের সমষ্টি: sequential disk I/O, OS page cache-এর ওপর নির্ভরতা, আর zero-copy network transfer। এই মডিউলে ব্রোকারের ডেটা ডিরেক্টরি খুলে দেখা হবে ঠিক কী ফরম্যাটে ডেটা ডিস্কে বসে।
Directory Layout: topic-partition/ → .log, .index, .timeindex, .snapshot
ফাইল স্ট্রাকচার
প্রতিটা partition broker-এর ডিস্কে একটা আলাদা ডিরেক্টরি — নাম হয় {topic}-{partition} (যেমন order.placed-0)। এর ভেতরে থাকে:
.log— আসল মেসেজ ডেটা, binary record batch ফরম্যাটে (পরের bullet)।.index— offset → physical file position-এর sparse index (দ্রুত lookup-এর জন্য)।.timeindex— timestamp → offset mapping,kafka-console-consumer.sh --offsetবা timestamp-ভিত্তিক seek-এর জন্য।.snapshot— producer state snapshot (idempotent/transactional producer-এর PID+sequence ট্র্যাক করতে, Module 3/9)।
রিয়েল-লাইফ উদাহরণ
এটা অনেকটা PostgreSQL-এর টেবিলের সাথে তার index ফাইলের সম্পর্কের মতো — .log হলো heap (আসল ডেটা), .index/.timeindex হলো B-tree index-এর সমতুল্য যা দ্রুত lookup সম্ভব করে, actual scan না করেই।
ls /var/lib/kafka/data/order.placed-0/
# 00000000000000000000.log
# 00000000000000000000.index
# 00000000000000000000.timeindex
# 00000000000000000000.snapshot
# leader-epoch-checkpoint
ফাইলের নাম দিয়েই বোঝা যায় সেটার শুরুর offset — 00000000000000000000 মানে এই segment offset 0 থেকে শুরু।
ড্রব্যাক
- অনেক ছোট segment (ঘন ঘন rotation) মানে অনেক ফাইল হ্যান্ডল খোলা থাকা, filesystem overhead বাড়া।
- খুব বড় segment মানে compaction/retention প্রয়োগ হতে দেরি হয় (পুরো segment expire না হওয়া পর্যন্ত সেটা মুছা যায় না, Module 6)।
Segment — কেন Log টুকরো করা হয়, segment.bytes / segment.ms
কেন একটা partition-এর পুরো log একটামাত্র ফাইল না
একটা partition-এর log ছোট ছোট segment ফাইলে ভাগ করা হয় (segment.bytes, ডিফল্ট 1GB, অথবা segment.ms, ডিফল্ট 7 দিন — যেটা আগে হয়)। শুধু সবচেয়ে নতুন (active) segment-এই write হয়; পুরনো সব segment immutable, শুধু read-only।
কেন এই ডিজাইন
- Retention/deletion সহজ — একটা পুরো segment মেয়াদোত্তীর্ণ হলে পুরো ফাইলটাই delete করা যায় (
unlink()), একটা big single file-এর ভেতর থেকে নির্বাচিত অংশ মুছার চেয়ে অনেক সস্তা। - Log compaction দক্ষ — cleaner thread একটা সময়ে একটা segment নিয়ে কাজ করে, পুরো log স্ক্যান করার দরকার নেই (Module 6)।
- Index ছোট রাখা — প্রতিটা segment-এর নিজের ছোট
.indexফাইল, memory-তে map করা সহজ।
রিয়েল-লাইফ উদাহরণ
PostgreSQL WAL সেগমেন্টেশনের সাথে সরাসরি তুলনা — pg_wal-এর ভেতরে WAL ফাইলগুলো ১৬MB (ডিফল্ট) খণ্ডে ভাগ করা থাকে, পুরনো ফাইল archive/checkpoint-এর পর recycle বা delete করা হয় — একই যুক্তি: বড় log-কে ছোট, ম্যানেজযোগ্য ইউনিটে ভাগ করলে cleanup ও management সহজ হয়ে যায়।
ড্রব্যাক
segment.bytesঅনেক ছোট রাখলে (যেমন 10MB) হাজার হাজার ফাইল তৈরি হবে high-throughput topic-এ, filesystem/OS-এর ওপর চাপ বাড়বে।segment.msঅনেক বড় রাখলে (কম্প্যাক্টেড টপিকে বিশেষভাবে গুরুত্বপূর্ণ) পুরনো ডেটা delete/compact হতে দেরি হবে, ডিস্ক ব্যবহার বেশি থাকবে প্রত্যাশার চেয়ে।
Sparse Index — O(log n) Offset Lookup
কেন "sparse"
.index ফাইল প্রতিটা মেসেজের জন্য এন্ট্রি রাখে না — এটা প্রতি index.interval.bytes (ডিফল্ট 4KB) ডেটা লেখার পর একটা এন্ট্রি রাখে (offset → byte position)। মানে হাজার হাজার মেসেজের মধ্যে হয়তো মাত্র কয়েকশো index entry।
কীভাবে lookup কাজ করে
একটা নির্দিষ্ট offset খুঁজতে হলে: sparse index-এ binary search করে সবচেয়ে কাছের ছোট offset entry খুঁজে বের করা হয়, তারপর সেই byte position থেকে .log ফাইলে sequential scan করে ঠিক মেসেজ পর্যন্ত পৌঁছানো হয় (সাধারণত কয়েক KB-এর মধ্যেই পাওয়া যায়)।
কেন এই ট্রেড-অফ বুদ্ধিমান
প্রতিটা মেসেজের জন্য পূর্ণ index রাখলে (dense index) index ফাইলগুলো বিশাল হয়ে যেত, RAM-এ রাখা কঠিন হতো (Kafka index ফাইল OS memory-map করে রাখে দ্রুত access-এর জন্য)। Sparse index সামান্য extra sequential scan-এর বিনিময়ে index-এর সাইজ বহুগুণ ছোট রাখে — memory খরচ কম, প্রায় একই লুকআপ স্পিড (কারণ sequential scan মাত্র কয়েক KB-এর মধ্যে, disk-এর জন্য এটা প্রায় বিনামূল্যে)।
রিয়েল-লাইফ উদাহরণ
PostgreSQL-এর B-tree index-এর সাথে পার্থক্য এখানেই স্পষ্ট — PostgreSQL index প্রতিটা row-এর জন্য একটা entry রাখে (dense), কারণ arbitrary WHERE ক্লজে random access দরকার। Kafka-তে access pattern predictable (sequential offset), তাই sparse index যথেষ্ট — এটা একটা ভালো উদাহরণ কীভাবে access pattern বুঝে ডেটা স্ট্রাকচার বেছে নেওয়া উচিত।
ড্রব্যাক
index.interval.bytesবড় করলে index আরও ছোট হবে কিন্তু প্রতিটা lookup-এ বেশি sequential scan লাগবে — একটা টিউনেবল ট্রেড-অফ।- Sparse index শুধু offset-based lookup-এর জন্য ভালো; arbitrary field-ভিত্তিক query (যেমন "এই user_id-এর সব মেসেজ খুঁজে দাও") Kafka নেটিভভাবে সাপোর্ট করে না — সেটার জন্য key partitioning বা external indexing দরকার।
Record Batch Binary Format (v2) — Varint, Delta Encoding, Batch-level Compression
Message ফরম্যাট নয়, Batch ফরম্যাট
Kafka v2 message format-এ (KIP-98-এর অংশ হিসেবে চালু) ডেটা ডিস্কে লেখা হয় batch হিসেবে, প্রতিটা মেসেজ আলাদাভাবে না। একটা batch-এর একটা কমন header (baseOffset, batch length, producer ID, first timestamp ইত্যাদি) থাকে, আর ভেতরের প্রতিটা রেকর্ড শুধু delta সংরক্ষণ করে (base থেকে পার্থক্য) — offset delta, timestamp delta।
কেন এটা জায়গা বাঁচায়
Varint encoding — ছোট সংখ্যা কম বাইটে এনকোড হয় (delta সাধারণত ছোট হয়, তাই কম বাইট লাগে পূর্ণ 8-byte offset/timestamp-এর বদলে)। Delta encoding — batch-এর ভেতরের প্রতিটা রেকর্ড শুধু base offset/timestamp থেকে কতটা পার্থক্য সেটাই রাখে, পূর্ণ মান না — বড় সংখ্যা বারবার লেখার বদলে ছোট পার্থক্য লেখা।
Batch-level Compression
Compression (Module 3) পুরো batch-এ একসাথে হয়, প্রতিটা রেকর্ড আলাদাভাবে compress হয় না — এতে compression ratio অনেক ভালো হয় (একসাথে অনেক similar ডেটাতে redundancy খুঁজে পাওয়া সহজ), আর compress/decompress overhead per-batch, per-message না।
রিয়েল-লাইফ উদাহরণ
Debezium-এর মতো CDC ইভেন্ট যেখানে হাজার হাজার consecutive মেসেজের schema প্রায় একই, timestamp কাছাকাছি — delta encoding + batch compression মিলে raw JSON payload-এর তুলনায় ডিস্কে অনেক কম জায়গা লাগে, network-এও কম ডেটা যায়।
ড্রব্যাক
- Batch format-এ একটা batch-এর ভেতরে একটা রেকর্ড দুর্নীতিগ্রস্ত (corrupt) হলে পুরো batch-ই সমস্যায় পড়তে পারে (checksums পুরো batch-লেভেলে)।
- পুরনো v1 format থেকে v2-এ migrate করার সময় broker/client ভার্সন কম্প্যাটিবিলিটি খেয়াল রাখা জরুরি — mixed-version cluster-এ conversion overhead হতে পারে।
Page Cache — কেন Kafka নিজের JVM Heap-এ ক্যাশ রাখে না
ডিজাইন সিদ্ধান্ত
Kafka ইচ্ছাকৃতভাবে নিজের JVM heap ছোট রাখে (সাধারণত কয়েক GB, পুরো সার্ভার RAM-এর তুলনায় ছোট) এবং ডেটা ক্যাশিং-এর দায়িত্ব সম্পূর্ণভাবে OS-এর page cache-এর ওপর ছেড়ে দেয়। যখন একটা broker ফাইলে write করে, OS সেটা page cache-এ রাখে এবং পরে asynchronously ডিস্কে flush করে। Read request এলে, যদি ডেটা page cache-এ থাকে (recent produce করা ডেটার জন্য প্রায় সবসময় থাকে), সরাসরি RAM থেকে সার্ভ হয়, ডিস্ক I/O ছাড়াই।
কেন এটা বুদ্ধিমান সিদ্ধান্ত
- GC overhead এড়ানো — বড় JVM heap মানে বড় garbage collection pause, যেটা latency spike তৈরি করে। ছোট heap + OS page cache মানে GC-জনিত latency অনেক কম।
- Duplicate caching এড়ানো — যদি Kafka নিজের heap-এ ডেটা cache করতো, একই ডেটা page cache-এ এবং heap-এ — দুইবার মেমরি খরচ হতো।
- সবচেয়ে সাম্প্রতিক ডেটা প্রায়ই সবচেয়ে বেশি পড়া হয় — নতুন produce করা ডেটা page cache-এ থাকা অবস্থাতেই সাধারণত consumer সেটা পড়ে ফেলে (real-time consumption) — cache hit rate প্রাকৃতিকভাবেই বেশি।
রিয়েল-লাইফ উদাহরণ
এটা অনেকটা PostgreSQL-এর shared_buffers ছোট রেখে OS filesystem cache-এর ওপর নির্ভর করার পুরনো wisdom-এর মতো (যদিও PostgreSQL নিজে একটা যথেষ্ট বড় shared_buffers ব্যবহার করে সাধারণত, তবুও OS cache তার নিচে আরেকটা লেয়ার হিসেবে কাজ করে) — মূল নীতি একই: OS নিজেই কোন page hot/cold তা track করতে অনেক দক্ষ, application-level caching সবসময় ভালো না।
ড্রব্যাক
- Broker রিস্টার্ট হলে page cache পুরোপুরি খালি হয়ে যায় (cold start) — প্রথম কিছু read ডিস্ক থেকে সরাসরি আসবে, ধীর হতে পারে যতক্ষণ না cache আবার "warm" হয়।
- যদি একই মেশিনে অন্য প্রসেস অনেক মেমরি খরচ করে, OS page cache-কে evict করতে পারে — Kafka broker-এর মেশিন dedicated রাখা (অন্তত মেমরি-heavy অন্য কিছু না চালানো) গুরুত্বপূর্ণ।
Zero-copy (sendfile) — Kafka-র Speed-এর আসল রহস্য
সাধারণ (non-zero-copy) ডেটা ট্রান্সফার
সাধারণত একটা ফাইল থেকে ডেটা নেটওয়ার্কে পাঠাতে ৪টা কপি আর ২টা context switch লাগে: disk → OS page cache (kernel space) → application buffer (user space) → socket buffer (kernel space) → NIC। প্রতিটা কপি CPU সাইকেল আর মেমরি bandwidth খরচ করে।
Zero-copy কীভাবে কাজ করে
Linux-এর sendfile() system call ব্যবহার করে Kafka broker page cache থেকে সরাসরি network socket-এ ডেটা পাঠায় — application (JVM) buffer-এ কপি করার ধাপটাই বাদ। ফলে মাত্র ২টা কপি লাগে (disk/cache → kernel buffer → NIC), CPU একদমই ডেটা "স্পর্শ" করে না, শুধু kernel-কে নির্দেশ দেয় কোথা থেকে কোথায় পাঠাতে হবে।
কেন এটাই Kafka-র throughput-এর মূল রহস্য
Consumer যখন historical বা real-time ডেটা fetch করে, broker-কে সেই ডেটা page cache থেকে নেটওয়ার্কে পাঠাতে হয় — এটা একটা অত্যন্ত ঘন ঘন ঘটা অপারেশন উচ্চ-থ্রুপুট সিস্টেমে। Zero-copy ছাড়া CPU এই ডেটা কপি করাতেই ব্যস্ত থাকতো, zero-copy দিয়ে CPU প্রায় মুক্ত থাকে অন্য কাজের জন্য (network handling, request processing) — এই জন্যই Kafka একই হার্ডওয়্যারে traditional broker-এর চেয়ে অনেক বেশি throughput দিতে পারে।
রিয়েল-লাইফ উদাহরণ
LinkedIn-এর মূল Kafka পেপারে বেঞ্চমার্ক দেখানো হয়েছিল zero-copy ব্যবহার করে consumer fetch path-এ CPU ব্যবহার প্রায় নিশ্চিহ্ন — broker মূলত নেটওয়ার্ক bandwidth-এই সীমাবদ্ধ থাকে, CPU-তে না। এটাই কেন Kafka broker সাধারণত network bandwidth দিয়ে সাইজ করা হয়, CPU দিয়ে না।
ড্রব্যাক / সীমাবদ্ধতা
- Zero-copy শুধু তখনই কাজ করে যখন ডেটা transformation ছাড়াই পাঠানো যায় (raw bytes)। যদি broker-সাইড ডেটা modify করতে হয় (যেমন compression format রূপান্তর — down-conversion পুরনো client-এর জন্য, বা TLS encryption করতে হলে), zero-copy path ব্যবহার করা যায় না, normal copy path-এ ফিরে যেতে হয় — performance penalty।
- TLS চালু থাকলে (Module 13-এর security) zero-copy সরাসরি কাজ করে না সব ক্ষেত্রে — এনক্রিপশনের জন্য ডেটা user-space-এ আনতে হয়, ফলে TLS-চালু cluster-এ throughput তুলনামূলক কম হতে পারে plaintext-এর চেয়ে।
Sequential I/O কেন Random I/O-র চেয়ে ১০০x দ্রুত
HDD-তে কেন
HDD-তে একটা mechanical read/write head আছে যেটা নির্দিষ্ট track/sector-এ পৌঁছাতে physically move করতে হয় (seek time, সাধারণত কয়েক মিলিসেকেন্ড)। Random I/O-তে প্রতিটা read/write আলাদা জায়গায় হয়, প্রতিবার seek লাগে। Sequential I/O-তে head একই দিকে ক্রমাগত move করে, seek প্রায় লাগেই না — throughput কয়েকশো গুণ বেশি হতে পারে।
SSD-তে কেন (mechanical seek নেই তবুও)
SSD-তে mechanical seek নেই, কিন্তু তবুও sequential I/O দ্রুত কারণ: (১) OS/filesystem বড় sequential block একসাথে read-ahead/write-behind করতে পারে কম syscall overhead-এ, (২) SSD internal-এ NAND flash-এর read/write pattern এখনো block-ভিত্তিক এলোমেলো access-এর চেয়ে contiguous access-এ ভালো কাজ করে (garbage collection, wear leveling-এর কারণে), (৩) OS page cache prefetching sequential pattern-এ predict করে এগিয়ে ডেটা লোড করতে পারে।
Kafka কীভাবে এটা কাজে লাগায়
Kafka-র append-only log design-ই মূল চাবিকাঠি: write সবসময় log-এর শেষে হয় (sequential append), কখনো মাঝখানে insert/update হয় না। Read-ও সাধারণত sequential (একটা offset থেকে ধারাবাহিকভাবে পড়া, consumer-এর normal pattern)। এই দুইয়ের মিলে Kafka disk I/O প্রায় সবসময় sequential থাকে।
রিয়েল-লাইফ উদাহরণ
একটা traditional RDBMS টেবিলে random UPDATE/DELETE খুবই common (B-tree-এর মাঝখানে ঢোকানো, পুরনো index entry মুছা) — এই random I/O pattern-ই OLTP ডেটাবেজের ডিস্ক-বাউন্ড হওয়ার একটা বড় কারণ। Kafka append-only হওয়ায় এই সমস্যাই নেই — এটাই কেন একই hardware-এ Kafka একটা RDBMS-এর চেয়ে বহুগুণ বেশি write throughput দিতে পারে।
ড্রব্যাক
- Append-only মডেলে in-place update সম্ভব না — কোনো নির্দিষ্ট মেসেজ "এডিট" করতে হলে নতুন একটা মেসেজ পাঠাতে হয় (compacted topic-এ key-wise "latest value" রাখা যায়, কিন্তু এটাও নতুন করে append, in-place modify না)।
- Random-access delete (নির্দিষ্ট একটা মেসেজ মুছে ফেলা, বাকি সব রেখে) Kafka নেটিভভাবে সাপোর্ট করে না — শুধু retention/compaction-ভিত্তিক bulk cleanup (Module 6)।
ল্যাব: kafka-dump-log.sh দিয়ে Raw Segment পড়া
লক্ষ্য
ব্রোকারের ডেটা ডিরেক্টরিতে সরাসরি গিয়ে raw segment ফাইল খুলে দেখা — বাইনারি ফরম্যাটটা চোখে দেখলে বাকি সব theory বাস্তব মনে হবে।
ধাপ ১ — কিছু মেসেজ produce করুন
for i in $(seq 1 20); do
echo "msg-$i" | kafka-console-producer.sh --topic order.placed \
--bootstrap-server localhost:9092
done
ধাপ ২ — ডেটা ডিরেক্টরি খুঁজুন
docker exec -it kafka find /var/lib/kafka/data -name "*.log" | grep order.placed
ধাপ ৩ — dump tool চালান
docker exec -it kafka /opt/kafka/bin/kafka-dump-log.sh \
--files /var/lib/kafka/data/order.placed-0/00000000000000000000.log \
--print-data-log
আউটপুটে প্রতিটা batch-এর baseOffset, lastOffset, count, CreateTime, isTransactional, producerId দেখা যাবে — এটাই batch format header যা আগের bullet-এ থিওরিতে পড়েছেন।
চ্যালেঞ্জ
--deep-iterationফ্ল্যাগ দিয়ে চালিয়ে প্রতিটা individual রেকর্ডের offset delta দেখুন।- একই টপিকে compression চালু করে (producer-সাইড
compression.type=gzip) আবার produce করুন, dump tool-এcompresscodecফিল্ড কীভাবে বদলায় দেখুন। .indexফাইলেkafka-dump-log.sh --files ....indexচালিয়ে sparse index entry-গুলো দেখুন — কয়টা মেসেজ পর একটা index entry তৈরি হচ্ছে (index.interval.bytesঅনুযায়ী)।
ইন্টারভিউ প্রশ্ন — Module 5
প্র.১ — Kafka এত দ্রুত কেন — একটা RDBMS-এর তুলনায়?
উত্তর: তিনটা কারণের সমষ্টি — (১) append-only sequential write, in-place random update নেই; (২) OS page cache-নির্ভর caching, নিজের heap-এ ডুপ্লিকেট cache না রেখে GC overhead এড়ানো; (৩) zero-copy (sendfile) দিয়ে consumer fetch-এ CPU/মেমরি কপি এড়ানো।
প্র.২ — Sparse index dense index-এর চেয়ে ভালো কেন Kafka-র ক্ষেত্রে? উত্তর: Kafka-র access pattern predictable (sequential offset-based) — তাই প্রতিটা মেসেজের জন্য index entry না রেখে প্রতি কয়েক KB-তে একটা entry রাখাই যথেষ্ট (sparse), এরপর অল্প sequential scan দিয়ে exact position পাওয়া যায়। এতে index সাইজ অনেক ছোট থাকে, memory-তে map করা সহজ হয়।
প্র.৩ — Zero-copy কীভাবে কাজ করে, আর কখন এটা ব্যবহার করা যায় না?
উত্তর: sendfile() syscall দিয়ে page cache থেকে সরাসরি network socket-এ ডেটা পাঠানো হয়, application (JVM) buffer-এ কপি না করেই। যখন broker-কে ডেটা transform করতে হয় (message format down-conversion, বা TLS encryption), zero-copy path ব্যবহার করা যায় না — ডেটা user-space-এ আনতে হয়।
প্র.৪ — Log segment-এ ভাগ করার সুবিধা কী? উত্তর: Retention/deletion সহজ (পুরনো segment পুরোপুরি delete করা যায়), log compaction একটা সময়ে একটা segment নিয়ে কাজ করতে পারে (পুরো log স্ক্যান না করে), আর index ফাইলগুলো ছোট রাখা যায়।
প্র.৫ — Kafka broker-এ JVM heap ছোট রাখার সিদ্ধান্তের পেছনে যুক্তি কী? উত্তর: বড় heap মানে বড় GC pause, latency spike। Kafka ডেটা caching-এর দায়িত্ব OS page cache-কে দিয়ে দেয় (যেটা GC-মুক্ত, এবং আসলে recent/hot ডেটা এমনিতেই cache-এ থাকবে কারণ সেটাই সবচেয়ে বেশি পড়া হয়)।
প্র.৬ — Sequential I/O SSD-তেও দ্রুত কেন, যেখানে mechanical seek নেই? উত্তর: SSD-তে mechanical seek না থাকলেও, OS বড় sequential block একসাথে read-ahead করতে পারে কম syscall overhead-এ, আর NAND flash internal garbage collection/wear-leveling contiguous access pattern-এ বেশি কার্যকর।
কর্নার কেস — Module 5
- Broker cold restart-এর পরপরই read latency বেড়ে যায় — page cache খালি থাকে, প্রথম কিছু fetch ডিস্ক থেকে সরাসরি আসে। মনিটরিং-এ এটা একটা সাময়িক, প্রত্যাশিত spike হিসেবে চিহ্নিত করা উচিত, alert না।
- TLS চালু করার পর throughput কমে যাওয়া একটা পরিচিত সারপ্রাইজ — কারণ zero-copy path আর কাজ করে না, ডেটা encryption-এর জন্য user-space-এ কপি করতে হয়। Security টিম TLS mandatory করলে capacity planning-এ এই overhead হিসাব করা জরুরি।
- একই মেশিনে Kafka broker-এর পাশে memory-heavy অন্য প্রসেস চালালে (যেমন একটা Spark executor) page cache evict হয়ে যায় — Kafka-র read latency অপ্রত্যাশিতভাবে বেড়ে যায়, কারণ নাটকীয়ভাবে বেশি ডিস্ক I/O লাগে।
- অনেক ছোট মেসেজ, compression ছাড়া, ঘন ঘন produce করলে (প্রতি মেসেজেই batch header overhead) ডিস্কে actual ডেটার তুলনায় অনেক বেশি জায়গা লাগতে পারে batch metadata-র কারণে — batching+compression সঠিকভাবে টিউন না করলে storage খরচ প্রত্যাশার চেয়ে বেশি হয়।
segment.bytesঅনেক বড় (উদাহরণ 5GB+) সেট করে low-throughput টপিকে ব্যবহার করলে একটা segment রোটেট হতেই হয়তো মাসের পর মাস লেগে যাবে — ফলে retention policy কার্যকর হতে অনেক দেরি, ডিস্ক প্রত্যাশার চেয়ে অনেক বেশি ভরে যেতে পারে।- Zero-copy আর OS page cache নির্ভরতার কারণে broker-এর
free/topকমান্ডে RAM প্রায় সবসময় "প্রায় পূর্ণ" দেখাবে — এটা স্বাভাবিক (page cache RAM ব্যবহার করছে), কিন্তু নতুন operator এটা দেখে ভুল করে memory leak/OOM সমস্যা ভাবতে পারে।availableমেমরি metric দেখা উচিত,usedনা।
MODULE 6: Retention আর Log Compaction
Kafka-র ডেটা চিরকাল থাকে না (সাধারণত) — retention policy ঠিক করে কতদিন/কত সাইজ পর্যন্ত ডেটা রাখা হবে। এই মডিউলে সময়-ভিত্তিক ও সাইজ-ভিত্তিক retention-এর পাশাপাশি একটা বিশেষ mechanism — log compaction — শেখা হবে, যেটা __consumer_offsets-এর মতো internal topic-এর ভিত্তি, এবং যেখানে শুধু "latest value per key" রাখা দরকার সেখানে ব্যবহার হয়। শেষে Tiered Storage (KIP-405) দিয়ে খরচ কমানোর নতুন পথ।
Delete Retention: Time-based, Size-based
দুইটা মেকানিজম, একসাথে কাজ করে
- Time-based (
retention.ms, ডিফল্ট 7 দিন) — একটা segment-এর সর্বশেষ মডিফিকেশন টাইম যদিretention.ms-এর চেয়ে পুরনো হয়ে যায়, পুরো segment delete হয়। - Size-based (
retention.bytes, ডিফল্ট -1 = unlimited) — partition-এর মোট সাইজ এই সীমা ছাড়ালে, সবচেয়ে পুরনো segment থেকে delete শুরু হয়, সীমার নিচে না আসা পর্যন্ত।
দুটোই একসাথে সেট থাকলে, যেটা আগে trigger হয় সেটাই কার্যকর হয় — দুটোর মধ্যে যেটা কড়া সেটাই আসলে সীমা নির্ধারণ করে।
গুরুত্বপূর্ণ: শুধু পুরো Segment মুছা যায়
দেলেশন segment-granularity-তে হয় — একটা নির্দিষ্ট মেসেজ আলাদা করে মুছা যায় না। এই জন্যই Module 5-এ শেখা segment ভাগ করার সিদ্ধান্ত এখানে গুরুত্বপূর্ণ হয়ে ওঠে: active segment (যেখানে এখনো write হচ্ছে) কখনো delete হয় না, শুধু rotate হয়ে যাওয়া পুরনো segment delete হয়।
রিয়েল-লাইফ উদাহরণ
Audit log টপিকে retention.ms=90 দিন রাখা যেতে পারে (compliance দরকার), কিন্তু raw metrics টপিকে retention.ms=1 দিন (শুধু সাম্প্রতিক ডেটাই দরকার, disk খরচ কমাতে)। একটা high-volume topic-এ retention.bytes না দিলে, একটা হঠাৎ traffic spike ডিস্ক পুরো ভরিয়ে দিতে পারে অপ্রত্যাশিতভাবে — production-এ দুটোই সেট করা নিরাপদ practice।
ড্রব্যাক
- Time-based retention শুধু segment-এর সর্বশেষ modification time দেখে, প্রতিটা মেসেজের individual timestamp না — তাই একটা segment-এ পুরনো এবং নতুন মেসেজ মিশে থাকলে, পুরো segment একসাথেই বাঁচবে বা মরবে।
retention.bytesসেট করলে peak traffic-এর সময় প্রত্যাশার চেয়ে দ্রুত পুরনো ডেটা মুছে যেতে পারে — consumer lag বেশি থাকলে সেই lagging consumer ডেটা হারানোর ঝুঁকিতে পড়ে (OffsetOutOfRangeException)।
Log Compaction Algorithm — Cleaner Thread, Dirty Ratio, Tombstone
কী সমস্যা সমাধান করে
সাধারণ delete retention পুরো log-কে সময়/সাইজ অনুযায়ী মুছে দেয় — এমনকি এমন key-ও যেটার latest value এখনো দরকার। Log compaction ভিন্ন মডেল: প্রতিটা key-এর জন্য শুধু সর্বশেষ value রাখে, পুরনো value-গুলো মুছে দেয় — key যতদিনই আগে লেখা হোক না কেন, তার latest value অনির্দিষ্টকাল বেঁচে থাকে।
কীভাবে কাজ করে (Cleaner Thread)
ব্যাকগ্রাউন্ডে একটা log cleaner thread চলে যেটা periodically প্রতিটা partition-এর "dirty ratio" (uncompacted অংশের অনুপাত, min.cleanable.dirty.ratio-র সাথে তুলনা করে) চেক করে। Dirty ratio সীমা ছাড়ালে, cleaner সেই partition-এর segment-গুলো স্ক্যান করে একটা offset map বানায় (key → সবচেয়ে সাম্প্রতিক offset), তারপর সেই ম্যাপ ব্যবহার করে পুরনো duplicate key-এন্ট্রি বাদ দিয়ে নতুন, compacted segment লেখে।
Tombstone (Deletion Marker)
কোনো key সম্পূর্ণ মুছে ফেলতে চাইলে, সেই key-এর জন্য একটা null value সহ মেসেজ পাঠানো হয় — একে tombstone বলে। Compaction-এ tombstone নিজেও একটা নির্দিষ্ট সময় (delete.retention.ms) পর্যন্ত থাকে (যাতে সব consumer সেটা দেখার সুযোগ পায়, তারপর নিজের key-কে delete হিসেবে ধরে নিতে পারে), তারপর tombstone-ও মুছে যায়।
রিয়েল-লাইফ উদাহরণ
__consumer_offsets টপিক নিজেই compacted — একটা group-topic-partition এর জন্য বারবার নতুন offset commit হতে থাকে, কিন্তু শুধু সর্বশেষ committed offset-ই গুরুত্বপূর্ণ, পুরনো ইতিহাস দরকার নেই। একইভাবে একটা "user profile latest state" টপিক (key=user_id, value=সর্বশেষ প্রোফাইল JSON) compacted হলে সেটা কার্যত একটা distributed key-value store-এর মতো আচরণ করে, যেখানে সব user-এর latest state চিরকাল পাওয়া যায় কিন্তু history না।
ড্রব্যাক
- Compaction একটা eventually consistent প্রক্রিয়া — dirty ratio threshold পার না হওয়া পর্যন্ত পুরনো duplicate key-value এখনো log-এ থাকতে পারে, সাথে সাথে মুছে যায় না।
- Consumer যদি compaction হওয়ার আগেই পড়ে, একই key-এর একাধিক পুরনো value দেখতে পারে (যেটা compaction পরে দেখাবে না) — অ্যাপ্লিকেশন লজিকে এটা মাথায় রাখা দরকার (idempotent update handling)।
Compacted Topic কখন ব্যবহার করবেন
সঠিক ব্যবহারের ক্ষেত্র
- State/latest-value storage — যেখানে শুধু "এখন কী অবস্থা" জানা দরকার, ইতিহাস না। যেমন: user profile, product catalog, feature flag configuration।
- Internal Kafka metadata —
__consumer_offsets(group offset),__transaction_state(Module 9) — দুটোই compacted, কারণ শুধু latest state দরকার। - Kafka Streams changelog topic — KTable-এর state store backup করতে (Module 12), যেখানে key-wise latest value-ই যথেষ্ট state পুনর্গঠনের জন্য।
ভুল ব্যবহার
Event history/audit trail-এর জন্য compacted topic ব্যবহার করা ভুল — সেখানে প্রতিটা ইভেন্টই গুরুত্বপূর্ণ (একটা order-এর প্রতিটা status change জানা দরকার, শুধু শেষ status না)। এখানে delete retention (Module 6 bullet 1)-ভিত্তিক normal topic ব্যবহার করা উচিত।
রিয়েল-লাইফ উদাহরণ
আপনার (Rakib) একটা user.profile.updated টপিক থাকলে, যদি downstream-এর দরকার শুধু "বর্তমান প্রোফাইল কী" (যেমন একটা caching layer বা search index sync করতে), compacted topic ব্যবহার করলে storage বহুগুণ কমে যাবে — একজন ইউজার ১০০০ বার প্রোফাইল আপডেট করলেও শুধু সর্বশেষটাই থাকবে। কিন্তু যদি "প্রতিটা প্রোফাইল পরিবর্তনের audit trail" দরকার হয় (compliance-এর জন্য), সেটা আলাদা, non-compacted টপিকে রাখতে হবে।
ড্রব্যাক
- Compacted topic-এ partition count পরে বাড়ানো বিপজ্জনক — key-to-partition mapping বদলে গেলে compaction logic এলোমেলো হয়ে যেতে পারে যদি সাবধানে না করা হয়।
- একটা টপিক ভুলে compacted হিসেবে কনফিগার করলে (
cleanup.policy=compact), গুরুত্বপূর্ণ ইতিহাস silently হারিয়ে যেতে পারে — এটা প্রায়ই খুব দেরিতে ধরা পড়ে।
Tiered Storage (KIP-405) — Hot Local + Cold S3
সমস্যা যা এটা সমাধান করে
ঐতিহ্যগতভাবে Kafka broker-এর local disk-ই একমাত্র storage — retention বাড়ানো মানে সরাসরি বেশি local disk কেনা (দামি, বিশেষ করে SSD)। বেশিরভাগ read সাম্প্রতিক ডেটার ওপর হয় ("hot" ডেটা), কিন্তু কিছু ব্যবহারিক ক্ষেত্রে (reprocessing, audit, ML retraining) পুরনো ("cold") ডেটাও মাঝে মাঝে দরকার হয়।
কীভাবে কাজ করে
KIP-405 (Tiered Storage) broker-কে সাম্প্রতিক segment local disk-এ (hot tier) রাখতে দেয়, আর পুরনো segment automatically একটা remote object store-এ (যেমন S3, cold tier) সরিয়ে দেয়। Consumer-এর দৃষ্টিতে এটা transparent — একই API দিয়ে পুরনো ডেটা fetch করা যায়, broker নিজেই বুঝে নেয় সেটা local-এ আছে না remote-এ, remote হলে সেখান থেকে fetch করে সার্ভ করে (সামান্য বেশি latency সহ)।
কেন এটা খরচ কমায়
Object storage (S3-এর মতো) local SSD-র তুলনায় প্রতি GB-তে অনেক সস্তা। Tiered storage দিয়ে retention মাসের বদলে বছরে বাড়ানো যায় বিনা বাড়তি ব্যয়ে local disk-এ, কারণ শুধু hot ডেটাই expensive local storage-এ থাকে।
রিয়েল-লাইফ উদাহরণ
একটা compliance-heavy ফিনটেক সিস্টেমে ৭ বছরের transaction log রাখতে হয় নিয়ম অনুযায়ী, কিন্তু দৈনন্দিন প্রসেসিং-এ শুধু গত ৭ দিনের ডেটাই বেশিরভাগ সময় দরকার হয়। Tiered storage ছাড়া, ৭ বছরের ডেটা রাখতে বিশাল local SSD cluster লাগতো। Tiered storage দিয়ে সাম্প্রতিক ৭ দিন local-এ, বাকি S3-এ — খরচ বহুগুণ কমে যায়, functionality একই থাকে।
ড্রব্যাক
- Cold tier থেকে ডেটা fetch করলে latency বেশি (network round-trip to S3) — random historical replay-heavy ওয়ার্কলোডে এটা লক্ষণীয় হতে পারে।
- এটা তুলনামূলক নতুন ফিচার (Kafka 3.6+ এ production-ready হওয়া শুরু) — সব ম্যানেজড সার্ভিস (MSK ইত্যাদি) সমানভাবে সাপোর্ট করে না, adopt করার আগে ভার্সন/প্রোভাইডার সাপোর্ট যাচাই করা দরকার।
ল্যাব: Compacted Topic বানিয়ে Cleaner চালিয়ে Before/After তুলনা
সেটআপ — compacted topic বানান
kafka-topics.sh --create --topic user.profile.state \
--partitions 1 --replication-factor 1 \
--config cleanup.policy=compact \
--config min.cleanable.dirty.ratio=0.01 \
--config segment.ms=5000 \
--bootstrap-server localhost:9092
(ডেমো-র জন্য segment.ms আর dirty.ratio কমিয়ে দ্রুত compaction ট্রিগার করানো হচ্ছে — প্রোডাকশনে এত আক্রমণাত্মক না।)
ধাপ ১ — একই key দিয়ে বারবার produce করুন
kafka-console-producer.sh --topic user.profile.state \
--property "parse.key=true" --property "key.separator=:" \
--bootstrap-server localhost:9092
> user1:{"name": "Rakib", "city": "Dhaka"}
> user1:{"name": "Rakib", "city": "Chittagong"}
> user1:{"name": "Rakib", "city": "Sylhet"}
> user2:{"name": "Karim"}
ধাপ ২ — Compaction-এর আগে raw segment দেখুন
kafka-dump-log.sh --files /var/lib/kafka/data/user.profile.state-0/00000000000000000000.log --print-data-log
এখানে user1-এর জন্য ৩টা এন্ট্রিই দেখা যাবে।
ধাপ ৩ — কিছুক্ষণ অপেক্ষা করে (cleaner চলার জন্য) আবার dump করুন
Default log.cleaner.backoff.ms (~15s) অনুযায়ী কিছুক্ষণ অপেক্ষা করুন, তারপর আবার dump log চালান — এবার user1-এর জন্য শুধু শেষ value ("city": "Sylhet") থাকবে।
চ্যালেঞ্জ
user1 key-এর জন্য একটা null value (tombstone) পাঠান, delete.retention.ms কমিয়ে (যেমন 100ms) দ্রুত tombstone-ও মুছে যাওয়া পর্যবেক্ষণ করুন।
ইন্টারভিউ প্রশ্ন — Module 6
প্র.১ — Delete retention আর log compaction-এর মৌলিক পার্থক্য কী? উত্তর: Delete retention সময়/সাইজ অনুযায়ী পুরো পুরনো segment মুছে দেয়, key নির্বিশেষে। Log compaction key-ভিত্তিক — প্রতিটা key-এর শুধু সর্বশেষ value রাখে, বয়স যাই হোক, পুরনো duplicate entry মুছে দেয়।
প্র.২ — Tombstone কী, আর এটা compaction-এ কীভাবে হ্যান্ডল হয়?
উত্তর: একটা key-এর জন্য null value সহ মেসেজ পাঠালে সেটা tombstone — মানে "এই key ডিলিট করো"। Compaction-এ tombstone নিজেও delete.retention.ms পর্যন্ত টিকে থাকে (সব consumer দেখার সুযোগ পাওয়ার জন্য), তারপর সেটাও মুছে যায়।
প্র.৩ — __consumer_offsets টপিক কেন compacted?
উত্তর: প্রতিটা group-topic-partition কম্বিনেশনের জন্য শুধু সর্বশেষ committed offset দরকার, পুরো commit history না। Compaction storage-কে বাউন্ডেড রাখে যদিও commit বারবার হয়।
প্র.৪ — কেন Deletion শুধু segment-granularity-তে হয়, individual message-granularity-তে না? উত্তর: performance এবং simplicity — একটা পুরো immutable segment ফাইল unlink করা সস্তা ও দ্রুত। প্রতিটা মেসেজ আলাদা track করে individually delete করতে হলে log structure-এর পুরো append-only, sequential ডিজাইন-ই ভেঙে পড়তো।
প্র.৫ — Tiered Storage (KIP-405) কী সমস্যা সমাধান করে? উত্তর: Local SSD storage ব্যয়বহুল, retention বাড়ানো মানে সরাসরি বেশি local disk কেনা। Tiered storage সাম্প্রতিক (hot) ডেটা local-এ, পুরনো (cold) ডেটা সস্তা object storage-এ (S3) রাখে transparent access সহ — retention অনেক বাড়ানো যায় খরচ না বাড়িয়ে।
প্র.৬ — একটা টিম event-sourcing audit trail-এর জন্য compacted topic ব্যবহার করছে — এটা কেন সমস্যাজনক? উত্তর: Compaction শুধু key-wise latest value রাখে, ইতিহাস মুছে দেয়। Audit trail-এ প্রতিটা ইভেন্ট গুরুত্বপূর্ণ (কী পরিবর্তন হয়েছিল সেই ইতিহাস), শুধু বর্তমান অবস্থা না — এখানে compaction ব্যবহার করলে গুরুত্বপূর্ণ ইতিহাস silently হারিয়ে যাবে। Delete-retention ভিত্তিক normal topic উপযুক্ত।
কর্নার কেস — Module 6
cleanup.policy=compact,delete(দুটো একসাথে) — compaction চলে key-wise deduplication করতে, আর delete retention চলে সময়/সাইজ অনুযায়ী পুরনো (এমনকি compacted) ডেটাও পুরোপুরি সরাতে। অনেকে ভুলে ভাবেন compacted topic মানেই ডেটা চিরকাল থাকবে —deletepolicy সাথে থাকলে তাও না।- Active segment কখনো compact হয় না — শুধু rotated (পুরনো) segment compaction-এর আওতায় আসে। একটা low-throughput compacted topic-এ যদি segment rotate হতেই অনেক দিন লাগে, ডুপ্লিকেট key অনেকদিন ধরে থেকে যেতে পারে প্রত্যাশার চেয়ে বেশি।
- Compaction consumer-এর দৃষ্টিতে asynchronous এবং eventual — একটা consumer group যদি খুব দ্রুত (compaction হওয়ার আগেই) পড়ে ফেলে, একই key-এর একাধিক পুরনো value দেখতে পারে যা পরবর্তী কোনো consumer আর দেখবে না — non-deterministic মনে হতে পারে যদি এই আচরণ না জানা থাকে।
- Tombstone-কে delete.retention.ms পার হওয়ার আগেই যদি সব consumer না পড়ে, কিছু consumer কখনোই জানবে না যে key-টা ডিলিট হয়েছে (শুধু পুরনো value দেখবে, deletion signal মিস করবে) — downstream state inconsistent থেকে যেতে পারে।
- Tiered storage চালু থাকা অবস্থায় cold tier (S3) unavailable হলে, পুরনো ডেটার জন্য read request fail বা timeout হতে পারে যদিও recent (local) ডেটা normally সার্ভ হচ্ছে — এই partial-failure মোড মনিটরিং-এ আলাদাভাবে ধরা দরকার।
min.compaction.lag.msসেট না করলে, খুব নতুন (এইমাত্র লেখা) মেসেজও যদি একই key-এর নতুন ভার্সন আসে দ্রুত compact হয়ে যেতে পারে — কিছু ব্যবহারিক ক্ষেত্রে (short-lived event consuming) এটা race condition তৈরি করতে পারে যদি consumer একটু ধীরে পড়ে।
MODULE 7: Replication আর Consistency
একটা single broker আর একটা fault-tolerant production cluster-এর মধ্যে পার্থক্যটাই replication। এই মডিউলে leader/follower/ISR মডেল, acks/min.insync.replicas-এর সঠিক combo, Leader Epoch দিয়ে data-loss ঠেকানো, আর Kafka কেন Raft/Paxos-এর মতো quorum-based consensus ব্যবহার করে না (ডেটার জন্য) — সেটা বোঝা হবে।
Leader, Follower, Replica, ISR (In-Sync Replicas)
ভোকাবুলারি
প্রতিটা partition-এর একাধিক কপি (replica) থাকে বিভিন্ন broker-এ (replication.factor অনুযায়ী)। একটা replica leader — সব produce/consume request এই broker-ই হ্যান্ডেল করে। বাকিগুলো follower — এরা leader থেকে ক্রমাগত ডেটা fetch করে নিজের local log update রাখে (Module 5-এর মতোই local segment ফাইলে)।
ISR — In-Sync Replicas
ISR হলো সেই replica-দের সেট যারা leader-এর সাথে "যথেষ্ট আপ-টু-ডেট" (replica.lag.time.max.ms-এর মধ্যে fetch করেছে)। শুধু ISR-এর মধ্যে থাকা replica-ই নতুন leader হওয়ার যোগ্য (default configuration-এ, unclean.leader.election.enable=false থাকলে)। একটা follower যদি অনেক পিছিয়ে যায় (slow disk, network issue), সেটা ISR থেকে বাদ পড়ে যায় — leader নিজে সেটা track করে।
রিয়েল-লাইফ উদাহরণ
PostgreSQL-এ synchronous replication-এর synchronous_standby_names-এর সাথে তুলনীয় — শুধু নির্দিষ্ট "in-sync" standby-ই commit acknowledge করার জন্য গণনা হয়, একটা lagging replica synchronous set-এ থাকলে পুরো write blocked হয়ে যেতে পারে। Kafka-র ISR concept একই ধারণা কিন্তু dynamic — কোনো replica lag করলে automatically ISR থেকে বাদ পড়ে (write ব্লক হওয়ার বদলে), lag কমলে আবার যোগ হয়।
ড্রব্যাক
- বেশি replica মানে বেশি storage, বেশি network bandwidth (প্রতিটা write সব replica-তে যায়) — replication.factor=3 মানে storage cost প্রায় ৩ গুণ।
- ISR shrink হয়ে গেলে (অনেক replica lag করছে)
min.insync.replicasপূরণ না হলে produceracks=all-এ write reject করবে (NotEnoughReplicasException) — availability কমে যায় consistency বজায় রাখার জন্য।
replication.factor, min.insync.replicas, acks=all — Durability-র তিন স্তম্ভ
তিনটা কনফিগ একসাথে কীভাবে কাজ করে
replication.factor=N— একটা partition-এর কতগুলো কপি থাকবে cluster-এ।min.insync.replicas=M— ISR-এ কমপক্ষে কতগুলো replica থাকতে হবে যাতে write সফল হতে পারে (M ≤ N)।acks=all(producer-সাইড) — producer শুধু তখনই ack পাবে যখন ISR-এর সব সদস্য (কমপক্ষে M-টা) write গ্রহণ করেছে।
Durability সমীকরণ
একসাথে replication.factor=3, min.insync.replicas=2, acks=all — এই combo সবচেয়ে common production সেটআপ: একটা broker হারালেও (৩ থেকে ২, এখনো ISR-এ ২টা থাকলে min.insync পূরণ হয়) সিস্টেম কাজ চালিয়ে যেতে পারে ডেটা না হারিয়ে। কিন্তু দুইটা broker একসাথে হারালে (ISR-এ ১টা থেকে যায়, min.insync=2 পূরণ হয় না), producer write reject হবে — সিস্টেম availability ছেড়ে consistency বেছে নেয়।
রিয়েল-লাইফ উদাহরণ
আপনার payment pipeline-এ replication.factor=3, min.insync.replicas=2, acks=all — একটা broker maintenance-এর জন্য নামালেও পেমেন্ট প্রসেসিং চলতে থাকবে (২টা replica যথেষ্ট)। কিন্তু একইসাথে দ্বিতীয় broker crash করলে, পেমেন্ট produce ব্যর্থ হবে (এরর রিটার্ন করবে) — এটা ইচ্ছাকৃত: ভুল ডেটা লেখার চেয়ে write ব্যর্থ হওয়া ভালো একটা financial system-এ।
ড্রব্যাক
min.insync.replicasখুব বেশি রাখলে (যেমন N=M=3) সামান্য broker instability-তেই write ব্যর্থ হতে থাকবে — over-conservative হলে availability ভুগবে।min.insync.replicas=1(কার্যতacks=1-এর মতো durability) দিয়েacks=allব্যবহার করলে মিথ্যা নিরাপত্তার অনুভূতি তৈরি হয় — মনে হয় durable, আসলে না।
High Watermark (HW) Propagation — Consumer কেন Uncommitted Data দেখে না
প্রবাহ
- Producer একটা মেসেজ leader-কে পাঠায়। Leader নিজের local log-এ লেখে, নিজের LEO এগিয়ে যায়।
- Follower-রা leader থেকে fetch request পাঠায়, নতুন ডেটা নিজের log-এ লেখে, নিজের LEO এগিয়ে যায়। এই fetch request-এর সাথে follower নিজের current LEO-ও leader-কে জানায়।
- Leader সব ISR follower-এর reported LEO-র মধ্যে সর্বনিম্নটা দেখে নিজের HW সেট করে — মানে HW = সব ISR replica-তে guaranteed replicate হয়ে যাওয়া সর্বোচ্চ offset।
- Leader তার নিজের HW পরবর্তী fetch response-এ follower-দের জানায়, তারা নিজেদের HW update করে।
কেন Consumer শুধু HW পর্যন্ত পড়ে
Consumer যদি HW-এর ওপরের (এখনো সব replica-তে না-পৌঁছানো) ডেটা পড়তে পারতো, আর তারপরই leader crash করতো (ওই ডেটা replicate হওয়ার আগেই), নতুন leader নির্বাচিত হতো যার কাছে সেই ডেটাই নেই — consumer এমন ডেটা "দেখেছে" যেটা আসলে কখনো স্থায়ীভাবে ছিলোই না (phantom read-এর মতো)। HW-বাউন্ডেড read এই সমস্যা প্রতিরোধ করে।
রিয়েল-লাইফ উদাহরণ
PostgreSQL streaming replication-এ hot_standby_feedback এবং read consistency-র ধারণার সাথে তুলনীয় — sync replica-তে read করার সময় শুধু commit-নিশ্চিত ডেটাই visible হওয়া উচিত, uncommitted বা "in-flight" ডেটা না। Kafka-র HW ঠিক এই একই গ্যারান্টি দেয় distributed log পড়ার সময়।
ড্রব্যাক
- HW propagation-এ সামান্য delay আছে (একটা network round-trip, fetch-response cycle) — মানে সদ্য-লেখা ডেটা consumer-এর কাছে available হতে সামান্য latency লাগে, এমনকি সব ISR replica সুস্থ থাকলেও।
- Follower lag বেশি হলে (slow replica এখনো ISR-এ আছে কিন্তু ধীর), HW-ও ধীরে এগোয় — সবচেয়ে ধীর ISR সদস্যের গতিতেই সীমাবদ্ধ থাকে পুরো consumer visibility।
Leader Epoch (KIP-101) — Log Divergence আর Data Loss ঠেকানো
পুরনো সমস্যা (Leader Epoch আসার আগে)
KIP-101-এর আগে, শুধু HW ব্যবহার করে log truncation হতো leader failover-এর সময় — একটা follower নতুন leader হওয়ার পর পুরনো leader (যেটা ফিরে এসেছে এখন follower হিসেবে) নিজের log-কে নতুন leader-এর সাথে মেলাতে গিয়ে নিজের HW পর্যন্ত সব ডেটা truncate করে ফেলতো, এমনকি সেই ডেটা আসলে committed/durable ছিল এমন ক্ষেত্রেও — একটা edge case-এ এটা silently ডেটা হারিয়ে ফেলতো।
Leader Epoch কীভাবে সমাধান করে
প্রতিটা নতুন leader election-এ একটা epoch number বাড়ে (0, 1, 2, …)। প্রতিটা মেসেজ শুধু offset না, তার সাথে কোন epoch-এ লেখা হয়েছিল সেটাও track করে (leader-epoch-checkpoint ফাইল, Module 5-এ dump দেখা গিয়েছিল)। একটা broker ফিরে এসে follower হওয়ার সময়, নতুন leader-কে জিজ্ঞেস করে "তোমার কাছে epoch X-এর জন্য শেষ offset কত" — এবং সেই নির্দিষ্ট তথ্যের ভিত্তিতে সঠিক truncation point বের করে, পুরো HW পর্যন্ত অন্ধভাবে truncate না করে।
রিয়েল-লাইফ উদাহরণ
PostgreSQL-এর timeline ID-র ধারণার সাথে খুব কাছের তুলনা — প্রতিবার একটা standby promote হয়ে নতুন primary হলে, একটা নতুন timeline শুরু হয় (recovery_target_timeline), যাতে WAL history-তে কোন সময় কোন সার্ভার "authoritative" ছিল তা স্পষ্টভাবে ট্র্যাক করা যায় — split-brain/log-divergence পরিস্থিতিতে সঠিক সিদ্ধান্ত নিতে। Leader Epoch ঠিক একই ভূমিকা পালন করে Kafka-তে।
ড্রব্যাক
- Epoch-based reconciliation একটা এক্সট্রা metadata layer — সামান্য জটিলতা যোগ করে broker-এর failover logic-এ, কিন্তু correctness-এর জন্য অপরিহার্য মূল্য।
- এটা শুধু broker নিজে থেকে সঠিকভাবে করে — application-level কোডে এটা নিয়ে কিছু করার নেই, শুধু বোঝা দরকার এই কারণেই failover-এর পর ডেটা loss ন্যূনতম হয় (পুরনো Kafka ভার্সনের তুলনায়)।
unclean.leader.election.enable — Availability vs Consistency-র সরাসরি সুইচ
কী এই সেটিং
যদি একটা partition-এর সব ISR replica একসাথে unavailable হয়ে যায় (উদাহরণ: ৩টা broker-ই একসাথে ক্র্যাশ), কিন্তু একটা non-ISR (lagging, out-of-sync) replica তখনও বেঁচে আছে — unclean.leader.election.enable=true হলে সেই lagging replica-কেই নতুন leader বানানো হয় (ডেটা হারিয়ে হলেও সিস্টেম চালু রাখার জন্য)। false (ডিফল্ট, সাধারণত production-এ recommended) হলে, সিস্টেম সেই partition-এর জন্য unavailable থেকে যায় যতক্ষণ না একটা প্রকৃত ISR সদস্য ফিরে আসে।
CAP থিওরেমের বাস্তব প্রয়োগ
এটা classic availability vs consistency ট্রেড-অফ। true = সিস্টেম সবসময় লেখা/পড়া গ্রহণ করবে, কিন্তু কিছু committed ডেটা হারিয়ে যেতে পারে (out-of-sync replica-র পুরনো ডেটা দিয়ে leader শুরু হয়)। false = কোনো ডেটা হারানো হয় না, কিন্তু পুরো ISR ডাউন থাকলে সিস্টেম সেই partition-এ থমকে যায়।
রিয়েল-লাইফ উদাহরণ
ফিনান্সিয়াল ট্রানজ্যাকশন topic-এ unclean.leader.election.enable=false — ডেটা হারানোর চেয়ে সাময়িক unavailability বহনযোগ্য (গ্রাহক একটু অপেক্ষা করবে, কিন্তু ভুল ব্যালেন্স দেখবে না)। কিন্তু একটা real-time metrics/analytics pipeline-এ (যেখানে সামান্য ডেটা loss গ্রহণযোগ্য কিন্তু downtime না) true যুক্তিসঙ্গত হতে পারে — ড্যাশবোর্ড চলতে থাকুক, সামান্য gap acceptable।
ড্রব্যাক
trueরাখলে, একটা multi-broker outage-এর পর সিস্টেম "স্বয়ংক্রিয়ভাবে সুস্থ" মনে হবে কিন্তু আসলে নীরবে ডেটা হারিয়ে গেছে — এই সাইলেন্ট ডেটা লস প্রায়ই অনেক পরে ধরা পড়ে (reconciliation/audit-এর সময়)।falseরাখলে, একটা বড় outage সিস্টেমকে পুরোপুরি থামিয়ে দিতে পারে যতক্ষণ না operator ম্যানুয়ালি হস্তক্ষেপ করে (kafka-leader-election.shদিয়ে জোর করে unclean election ট্রিগার করা, সচেতনভাবে) — অপারেশনাল প্রস্তুতি দরকার।
Kafka Replication vs Raft vs Paxos — কেন ডেটার জন্য Quorum না
মূল পার্থক্য
Raft/Paxos-এর মতো consensus algorithm-এ প্রতিটা write একটা quorum (majority, যেমন 3-এর মধ্যে 2) থেকে acknowledgment লাগে, এবং leader election-ও quorum ভোটে হয়। Kafka-র data replication (ISR মডেল) কিন্তু quorum-based না — এটা primary-backup with dynamic membership মডেল: leader একাই সিদ্ধান্ত নেয় কে ISR-এ থাকবে (lag দেখে), majority vote লাগে না ডেটা লেখার জন্য।
কেন Kafka এই পথ বেছে নিলো (ডেটার জন্য)
- Throughput — quorum-based write-এ প্রতিটা write-এর জন্য majority-র সাথে communication লাগে, যা latency বাড়ায়। ISR মডেলে
acksকনফিগারযোগ্য (0/1/all) — অ্যাপ্লিকেশন নিজের durability-throughput ট্রেড-অফ বেছে নিতে পারে। - Flexibility —
min.insync.replicasদিয়ে কতগুলো replica দরকার তা টিউন করা যায় quorum-এর কড়া majority নিয়ম ছাড়াই (যেমন replication.factor=5-এ min.insync=2 রাখা সম্ভব, majority হলে 3 লাগতো)।
কিন্তু Metadata-র জন্য Raft ব্যবহার করে (KRaft)!
মজার ব্যাপার — cluster metadata (কোন broker কোন partition-এর leader, topic configuration ইত্যাদি) পরিচালনার জন্য Kafka KRaft-এ (Module 8) সত্যিকারের Raft consensus ব্যবহার করে। কারণ metadata-র জন্য strict consistency আর majority-based leader election অত্যাবশ্যক (metadata-তে split-brain হলে পুরো cluster বিভ্রান্ত হয়ে যাবে) — কিন্তু actual message data-র (throughput-critical path) জন্য সেই strict quorum overhead বহন করা অপ্রয়োজনীয়, তাই সেখানে হালকা ISR মডেল।
রিয়েল-লাইফ উদাহরণ
এটা অনেকটা একটা কোম্পানির সিদ্ধান্ত নেওয়ার কাঠামোর সাথে তুলনীয় — বড়, বিরল, critical সিদ্ধান্তে (metadata/company policy) পুরো board-এর ভোট (Raft/quorum) লাগে, কিন্তু দৈনন্দিন অপারেশনাল কাজে (data write) একজন নির্দিষ্ট ম্যানেজার (leader) দ্রুত সিদ্ধান্ত নেয় যতক্ষণ তার team (ISR) তার সাথে sync আছে।
ড্রব্যাক
- ISR মডেল quorum-এর তুলনায় কম formally-proven safety guarantee দেয় কিছু edge case-এ (এই জন্যই Leader Epoch-এর মতো অতিরিক্ত মেকানিজম লাগে সমস্যা প্যাচ করতে, যেখানে pure Raft-এ এটা প্রোটোকলের অংশ হিসেবেই এমবেডেড থাকতো)।
Rack Awareness, Replica Placement
সমস্যা যা এটা সমাধান করে
Default replica placement শুধু broker সংখ্যা দেখে ছড়িয়ে দেয় — এটা জানে না কোন broker-গুলো একই physical rack/availability zone-এ আছে। যদি ৩টা replica ভুলবশত একই rack-এ পড়ে (rack ব্যর্থ হলে সব একসাথে ডাউন), replication-এর পুরো উদ্দেশ্যই ব্যর্থ হয়ে যায়।
কীভাবে কাজ করে
প্রতিটা broker-এ broker.rack কনফিগ সেট করে (যেমন us-east-1a, us-east-1b), Kafka-র replica placement algorithm নিশ্চিত করে একটা partition-এর replica-গুলো যতটা সম্ভব ভিন্ন rack-এ ছড়িয়ে থাকে — একটা single rack failure পুরো partition-কে অনুপলব্ধ করতে পারবে না।
রিয়েল-লাইফ উদাহরণ
AWS-এ একটা 3-AZ (Availability Zone) MSK/self-managed cluster-এ broker.rack=us-east-1a/1b/1c সেট করলে, replication.factor=3 স্বয়ংক্রিয়ভাবে প্রতিটা partition-এর replica তিনটা ভিন্ন AZ-এ রাখবে — একটা পুরো AZ outage (যা মাঝে মাঝে ঘটে) হলেও বাকি ২টা AZ-এর replica দিয়ে সিস্টেম চালু থাকবে।
ড্রব্যাক
- Cross-AZ network traffic-এর জন্য cloud provider-রা সাধারণত আলাদা bandwidth চার্জ করে — rack-aware replication মানে বেশিরভাগ replication traffic cross-AZ হবে, cost বাড়বে same-AZ replication-এর তুলনায়।
- Rack awareness ছাড়া deploy করা একটা cluster-এ পরে rack.id যোগ করলে বিদ্যমান replica placement স্বয়ংক্রিয়ভাবে rebalance হয় না — ম্যানুয়াল reassignment (
kafka-reassign-partitions.sh, Module 13) লাগে।
ইন্টারভিউ প্রশ্ন — Module 7
প্র.১ — replication.factor=3, min.insync.replicas=2, acks=all কনফিগারেশনে ঠিক কয়টা broker ডাউন হলে write ব্যর্থ হবে?
উত্তর: ২টা broker ডাউন হলে (ISR-এ ১টা থেকে যায়, min.insync.replicas=2 পূরণ হয় না) — producer NotEnoughReplicasException পাবে, write reject হবে।
প্র.২ — Leader Epoch কোন সমস্যা সমাধান করে? উত্তর: Leader failover-এর পর পুরনো leader (এখন follower) যখন নতুন leader-এর সাথে log মেলায়, শুধু HW ব্যবহার করে (Leader Epoch ছাড়া) কখনো কখনো committed ডেটা ভুলভাবে truncate হয়ে যেত। Leader Epoch প্রতিটা মেসেজকে তার epoch সহ ট্র্যাক করে সঠিক truncation point বের করতে সাহায্য করে, ভুলভাবে ডেটা হারানো রোধ করে।
প্র.৩ — Consumer কেন শুধু High Watermark পর্যন্ত ডেটা পড়তে পারে? উত্তর: HW-এর ওপরের ডেটা এখনো সব ISR replica-তে replicate হয়নি — এখনো durable না। যদি consumer সেটা পড়ে ফেলে আর তারপর leader crash করে (সেই ডেটা replicate হওয়ার আগেই), consumer এমন ডেটা দেখেছে যেটা কখনো স্থায়ীভাবে টিকেই থাকেনি।
প্র.৪ — unclean.leader.election.enable=true vs false — কোনটা কখন বেছে নেবেন?
উত্তর: false (ডিফল্ট, সুপারিশকৃত) যখন ডেটা হারানো একদমই গ্রহণযোগ্য না (payment, order) — সাময়িক unavailability বেছে নেওয়া ভালো। true যখন সিস্টেম সবসময় available থাকা বেশি গুরুত্বপূর্ণ ডেটা হারানোর সামান্য ঝুঁকির চেয়ে (non-critical metrics/analytics)।
প্র.৫ — Kafka ডেটা replication-এর জন্য quorum-based consensus (Raft/Paxos) ব্যবহার করে না কেন?
উত্তর: Quorum-based write-এ প্রতিটা write-এ majority-র সাথে communication লাগে, latency বাড়ায়। Kafka-র ISR মডেল (leader নিজে ISR membership নির্ধারণ করে, acks কনফিগারযোগ্য) বেশি throughput-flexible। তবে metadata management-এর জন্য (যেখানে strict consistency জরুরি) Kafka KRaft-এ প্রকৃত Raft ব্যবহার করে (Module 8)।
প্র.৬ — Rack awareness ছাড়া replication.factor=3 কেন যথেষ্ট নাও হতে পারে? উত্তর: Rack awareness ছাড়া, default placement algorithm জানে না কোন broker একই physical rack/AZ-এ — ৩টা replica ভুলবশত একই rack-এ পড়তে পারে, সেই rack ব্যর্থ হলে পুরো partition অনুপলব্ধ হয়ে যাবে, replication factor 3 থাকা সত্ত্বেও।
কর্নার কেস — Module 7
- ISR shrink হয়ে গিয়ে
min.insync.replicasপূরণ না হলে,acks=allproducer-রা এরর পাবে কিন্তুacks=1producer-রা নিরবে চলতে থাকবে — একই cluster-এ ভিন্ন producer কনফিগ থাকলে outage-এর সময় inconsistent আচরণ দেখা যাবে, যেটা ডিবাগ করা কঠিন হতে পারে যদি সব producer কনফিগ এক জায়গায় ট্র্যাক না করা থাকে। - একটা replica ISR-এ ফিরে আসতে (catch-up করতে) অনেক সময় লাগতে পারে যদি সেটা অনেকদিন ডাউন ছিল — এই সময়
min.insync.replicasকড়াভাবে সেট থাকলে সিস্টেম দুর্বল অবস্থায় (fewer effective replicas) দীর্ঘসময় চলতে পারে, availability ঝুঁকিতে থাকে। unclean.leader.election.enable=trueঅবস্থায় একটা "স্বয়ংক্রিয় রিকভারি" আসলে নীরব ডেটা লস — মনিটরিং ড্যাশবোর্ডে সব "সবুজ" দেখাবে (partition-এর leader আছে, ট্রাফিক চলছে), কিন্তু ডেটা harানোর ঘটনা কোথাও লগ না হলে কেউ জানবেই না যতক্ষণ না ডাউনস্ট্রিম reconciliation ধরে ফেলে।- Rack awareness কনফিগার করা থাকলেও broker সংখ্যা rack সংখ্যার সমানুপাতিক না হলে placement অসম হতে পারে — যেমন ২টা AZ-এ ৩টা replica রাখতে চাইলে একটা AZ-এ অনিবার্যভাবে ২টা replica পড়বে, rack-level fault tolerance আংশিক ভেঙে যাবে।
- Leader Epoch ফাইল করাপ্ট বা মুছে গেলে (ম্যানুয়াল disk operation-এর ভুলে) broker restart-এ log reconciliation ভুল হতে পারে — Kafka ডেটা ডিরেক্টরি সরাসরি হাত দিয়ে এডিট করা কখনো করা উচিত না প্রোডাকশনে।
MODULE 8: Controller, Metadata আর KRaft
Kafka ২০২৪-এর মধ্যে ZooKeeper-নির্ভরতা সম্পূর্ণ ছেড়ে দিয়েছে — এখন cluster metadata (কে leader, কী কনফিগ, কোন broker আছে) নিজেই একটা Kafka topic-এর মতো, Raft consensus দিয়ে পরিচালিত হয় (KRaft)। এই মডিউলে ZooKeeper কেন বিদায় নিলো, KRaft-এর ভেতরের যন্ত্র, controller-এর দায়িত্ব, আর Raft consensus-এর মূল ধারণা (leader election, log replication, term) শেখা হবে।
ZooKeeper যুগ কেন শেষ হলো
সমস্যাগুলো
- Dual source of truth — Kafka-র মেটাডেটার একটা অংশ ZooKeeper-এ (topic config, ACL, controller election), আরেকটা অংশ broker-এর নিজস্ব মেমরিতে — এই দুইয়ের মধ্যে sync রাখা জটিল, race condition-এর সুযোগ তৈরি করতো।
- Scalability limit — বড় cluster-এ (লাখ লাখ partition) controller-কে পুরো metadata ZooKeeper থেকে পড়ে প্রতিটা broker-এ push করতে হতো — এই full-sync প্রক্রিয়া controller failover-এর সময় মিনিটের অর্ডারে সময় নিতো, cluster সাময়িকভাবে অস্থির থাকতো।
- অপারেশনাল জটিলতা — দুইটা আলাদা distributed system (Kafka + ZooKeeper) ম্যানেজ, monitor, secure, আর আপগ্রেড করতে হতো — deployment জটিলতা প্রায় দ্বিগুণ।
KRaft কী প্রতিশ্রুতি দেয়
মেটাডেটাকে একটা নিজস্ব Kafka-স্টাইল replicated log-এ (@metadata topic) সরিয়ে, Kafka নিজেই নিজের metadata consensus চালায় — বাইরের কোনো সিস্টেম লাগে না। এতে single source of truth, দ্রুত controller failover (সেকেন্ডের অর্ডারে, আগে মিনিটের অর্ডারে ছিল), আর অনেক বড় partition count সাপোর্ট করার ক্ষমতা আসে।
রিয়েল-লাইফ উদাহরণ
এটা অনেকটা একটা মাইক্রোসার্ভিস আর্কিটেকচারে একটা আলাদা "config service" ব্যবহার করার বদলে সার্ভিস নিজেই তার configuration নিজের database-এ replicated ভাবে রাখার সিদ্ধান্তের মতো — একটা কম moving part, কম operational surface area।
ড্রব্যাক
- Migration path (ZooKeeper থেকে KRaft-এ) নিজেই জটিল একটা প্রক্রিয়া ছিল বিদ্যমান production cluster-এর জন্য — অনেক টিম কয়েক বছর দেরি করে migrate করেছে স্থিতিশীলতার জন্য অপেক্ষা করে।
- কিছু পুরনো tooling/monitoring যা ZooKeeper-এর সাথে সরাসরি ইন্টারঅ্যাক্ট করতো (custom scripts) সেগুলো KRaft-এ কাজ করে না, নতুন করে লিখতে হয়েছে।
KRaft — Raft Consensus দিয়ে Metadata Quorum
আর্কিটেকচার
KRaft mode-এ কিছু broker controller role নেয় (process.roles=controller বা broker,controller combined mode ছোট cluster-এ)। এই controller broker-গুলো মিলে একটা Raft quorum গঠন করে — একজন Raft leader (active controller), বাকিরা follower। সব metadata change (topic creation, partition reassignment, ACL update) একটা internal, replicated log-এ (__cluster_metadata topic, ইন্টারনালি "metadata log" নামে পরিচিত) লেখা হয়, ঠিক যেমন সাধারণ Kafka topic-এ মেসেজ লেখা হয়।
কেন এটা normal Kafka topic-এর মতো (কিন্তু না)
Metadata log প্রায় normal Kafka partition-এর মতোই দেখতে (segment, offset) — কিন্তু এর replication Raft protocol মেনে চলে (quorum-based, strict leader election with term/epoch), সাধারণ ISR মডেল না — কারণ metadata-র জন্য strict consistency (কোনো split-brain সহ্য করা যাবে না) দরকার, যেটা Module 7-এ শেখা relaxed ISR মডেলের চেয়ে কড়া গ্যারান্টি।
রিয়েল-লাইফ উদাহরণ
এটাকে etcd (Kubernetes-এর মেটাডেটা স্টোর, যেটাও Raft ব্যবহার করে) এর সাথে তুলনা করা যায় — Kubernetes-এর প্রতিটা pod/service definition-এর মতোই, Kafka-র প্রতিটা topic/partition/ACL definition একটা strongly-consistent, Raft-replicated log-এ থাকে যাতে পুরো cluster সবসময় একমত থাকে "সত্যিকারের অবস্থা" কী।
ড্রব্যাক
- Controller quorum-এর size (সাধারণত 3 বা 5) সাবধানে প্ল্যান করতে হয় — খুব ছোট quorum-এ fault tolerance কম, খুব বড় quorum-এ metadata write latency বাড়ে (বেশি নোডের majority agreement লাগে)।
- Combined mode (broker+controller একই প্রসেসে, ছোট cluster/dev-এ common) production-এ বড় cluster-এর জন্য recommended না — dedicated controller node আলাদা রাখাই ভালো practice বড় স্কেলে।
Controller-এর দায়িত্ব: Leader Election, Reassignment, Failure Detection
কী করে Controller
- Partition Leader Election — একটা broker ডাউন হলে, সেই broker leader ছিল এমন প্রতিটা partition-এর জন্য নতুন leader (ISR থেকে) নির্বাচন করে, cluster-wide metadata আপডেট করে সবাইকে জানায়।
- Partition Reassignment — অপারেটর যখন
kafka-reassign-partitions.shচালায় (Module 13) নতুন broker-এ replica সরাতে, controller সেই migration-এর orchestration করে (নতুন replica sync হওয়া, পুরনো replica বাদ দেওয়া)। - Broker Failure Detection — controller প্রতিটা broker-এর heartbeat/liveness ট্র্যাক করে; একটা broker অনুপস্থিত হলে (session timeout পার হলে) সেটাকে dead ঘোষণা করে, তার সব partition-এর জন্য failover trigger করে।
কেন একটা কেন্দ্রীভূত Controller
একজন authoritative সিদ্ধান্ত-গ্রহণকারী থাকলে race condition এড়ানো যায় — যদি প্রতিটা broker স্বাধীনভাবে leader election করতে চেষ্টা করতো, দুইজন broker একই সময়ে নিজেদের leader ঘোষণা করে ফেলতে পারতো (split-brain)। KRaft-এ controller নিজেই Raft-elected, তাই controller নির্বাচনও deterministic এবং consistent।
রিয়েল-লাইফ উদাহরণ
একটা ৩-broker cluster-এ broker-2 (যেটা ৫টা partition-এর leader ছিল) হঠাৎ crash করলে, active controller সাথে সাথে সেই ৫টা partition-এর জন্য ISR থেকে নতুন leader বাছাই করে, metadata log-এ পরিবর্তন লেখে, সব broker-কে notify করে — কয়েক সেকেন্ডের মধ্যে (KRaft-এ, ZooKeeper যুগের চেয়ে দ্রুত) পুরো cluster নতুন leadership জানে, client-রা নতুন leader-এর দিকে redirect হয়।
ড্রব্যাক
- Controller নিজেই যদি (Raft leader) fail করে, নতুন controller নির্বাচিত হতে (Raft leader election) সামান্য সময় লাগে — এই সংক্ষিপ্ত উইন্ডোতে নতুন metadata change (নতুন topic তৈরি ইত্যাদি) সাময়িকভাবে block থাকতে পারে, যদিও existing data plane (produce/consume) সাধারণত চলতেই থাকে।
Raft ভেতরে: Leader Election, Log Replication, Term/Epoch
মূল Raft ধারণা
- Term (epoch) — সময়কে অ-ওভারল্যাপিং periods-এ ভাগ করা, প্রতিটাতে সর্বোচ্চ একজন leader। Term সংখ্যা প্রতিটা নতুন election-এ বাড়ে।
- Leader Election — একজন node timeout-এর পর candidate হয়ে যায় (আগের leader-এর heartbeat না পেলে), অন্যদের ভোট চায়। Majority ভোট পেলে leader হয়ে যায় সেই term-এর জন্য।
- Log Replication — leader সব write গ্রহণ করে, followers-দের কাছে replicate করে পাঠায়। একটা entry "committed" ধরা হয় যখন majority node সেটা তাদের log-এ লিখে ফেলেছে — অনেকটা Kafka-র HW-এর মতোই ধারণা, কিন্তু quorum-based (majority) না যে ISR-based (dynamic membership)।
কেন Term/Epoch দরকার
একটা পুরনো leader (যেটা সাময়িকভাবে network-partition-এ বিচ্ছিন্ন ছিল, নিজেকে এখনো leader মনে করছে) ফিরে এলে, তার term number নতুন leader-এর term-এর চেয়ে ছোট হবে — বাকি সবাই তার নির্দেশ প্রত্যাখ্যান করবে (higher term জেতে) — split-brain প্রতিরোধ করা হয় এভাবেই।
রিয়েল-লাইফ উদাহরণ
Kubernetes-এর etcd, HashiCorp Consul/Vault, CockroachDB — সবগুলোই Raft (বা Raft-সদৃশ) consensus ব্যবহার করে ঠিক এই একই কারণে: strongly-consistent metadata/leader নির্বাচন দরকার এমন জায়গায়। Kafka-র KRaft controller quorum এই একই পরীক্ষিত প্যাটার্ন অনুসরণ করে, কিন্তু বাস্তবায়ন Kafka নিজেই করেছে (নিজস্ব Raft implementation, kafka-metadata-quorum.sh টুল দিয়ে পরিদর্শনযোগ্য)।
ড্রব্যাক
- Raft-এর majority-ভিত্তিক commit মানে quorum-এর অর্ধেকের বেশি node ডাউন হলে metadata plane সম্পূর্ণ থেমে যায় (নতুন leader নির্বাচিত হতে পারবে না) — এই জন্য controller quorum সংখ্যা বিজোড় (3, 5) রাখা হয়, split vote এড়াতে এবং fault tolerance স্পষ্ট রাখতে।
Cluster Metadata Propagation আর metadata.max.age.ms
কীভাবে সাধারণ Broker Metadata পায়
Controllers metadata log-এর authoritative owner, কিন্তু বাকি সব (non-controller) broker সেই log fetch করে replicate করে নিজেদের in-memory metadata cache আপডেট রাখতে — ঠিক normal Kafka consumer যেভাবে একটা topic পড়ে, তেমনি প্রতিটা broker metadata topic-এর একটা "consumer"।
Client-সাইড Metadata Caching
Producer/consumer client নিজেও metadata cache রাখে (কোন broker কোন partition-এর leader) — প্রতিটা request-এ controller-কে জিজ্ঞেস করা অত্যন্ত অদক্ষ হতো। metadata.max.age.ms (ডিফল্ট 5 মিনিট) নির্ধারণ করে client কত ঘন ঘন force-refresh করবে metadata, এমনকি কোনো এরর না হলেও।
Stale Metadata হ্যান্ডলিং
Client-এর cached metadata stale হয়ে গেলে (leader পরিবর্তন হয়ে গেছে কিন্তু client এখনো জানে না), client পুরনো broker-কে request পাঠাবে, সেই broker NOT_LEADER_OR_FOLLOWER এরর রিটার্ন করবে — client তখন immediately metadata refresh করে সঠিক নতুন leader-এ retry করে। এই self-healing mechanism-ই মানে metadata.max.age.ms অনেক বড় রাখলেও correctness নষ্ট হয় না, শুধু সাময়িক extra round-trip লাগে stale হিসাবে ধরা পড়লে।
রিয়েল-লাইফ উদাহরণ
DNS caching-এর সাথে ধারণাগতভাবে কাছাকাছি — client একটা TTL পর্যন্ত cached DNS entry ব্যবহার করে, কিন্তু stale হলে (server IP বদলে গেছে) connection error পেয়ে re-resolve করে। Kafka client metadata একইভাবে কাজ করে, শুধু error detection broker-এর explicit error code দিয়ে হয়, DNS-এর মতো connection timeout দিয়ে না।
ড্রব্যাক
metadata.max.age.msখুব ছোট রাখলে client-রা অপ্রয়োজনীয়ভাবে ঘন ঘন controller-কে metadata request পাঠাবে, সামান্য অতিরিক্ত load।- খুব বড় রাখলে, leader change ঘটলে client কিছুক্ষণ ভুল broker-এ request পাঠাতেই থাকবে (যদিও self-healing হবে stale-detected হওয়ার পর) — সামান্য বেশি latency spike হতে পারে leader change-এর ঠিক পরপর।
ল্যাব: ৩-Broker KRaft Cluster, Leader Kill করে Failover টাইমিং মাপা
সেটআপ — docker-compose দিয়ে ৩-broker, ৩-controller KRaft cluster
services:
kafka1:
image: apache/kafka:3.8.0
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka1:9093,2@kafka2:9093,3@kafka3:9093
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
# ... (kafka2, kafka3 একই প্যাটার্নে ভিন্ন NODE_ID আর পোর্ট সহ)
(সম্পূর্ণ ৩-broker কনফিগ Apache Kafka docs-এর "KRaft: Multi-node cluster" গাইডে আছে।)
ধাপ ১ — টপিক বানান, partition leader দেখুন
kafka-topics.sh --create --topic failover.test --partitions 1 --replication-factor 3 \
--bootstrap-server localhost:9092
kafka-topics.sh --describe --topic failover.test --bootstrap-server localhost:9092
দেখুন কোন broker leader (যেমন broker 1)।
ধাপ ২ — Continuous producer/consumer চালান, তারপর leader broker kill করুন
docker stop kafka1
সাথে সাথে টাইমস্ট্যাম্প নোট করুন, তারপর বারবার --describe চালিয়ে দেখুন কখন নতুন leader নির্বাচিত হয় — সময়ের ব্যবধান মাপুন (সাধারণত কয়েক সেকেন্ডের মধ্যে হওয়া উচিত KRaft-এ)।
চ্যালেঞ্জ
kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --statusচালিয়ে controller quorum-এর leader/follower স্ট্যাটাস দেখুন।- একই টেস্ট controller (broker,controller role-এর broker) kill করে করুন — broker leader kill করার তুলনায় controller kill করার recovery time-এর পার্থক্য মাপুন।
ইন্টারভিউ প্রশ্ন — Module 8
প্র.১ — ZooKeeper কেন সরিয়ে ফেলা হলো Kafka থেকে? উত্তর: Dual source of truth-এর জটিলতা (ZooKeeper + broker memory sync রাখা কঠিন), বড় cluster-এ metadata propagation স্কেলেবিলিটি সমস্যা (controller failover-এ মিনিট লাগতো), আর দুইটা আলাদা distributed system অপারেট করার জটিলতা।
প্র.২ — KRaft-এ metadata কীভাবে সংরক্ষিত ও replicate হয়?
উত্তর: একটা internal, Raft-replicated log-এ (__cluster_metadata topic-এর মতো) — normal Kafka topic-এর মতোই segment/offset আছে, কিন্তু replication ISR মডেল না, প্রকৃত Raft consensus (quorum-based, strict term/epoch সহ) মেনে চলে।
প্র.৩ — Controller-এর মূল দায়িত্ব কী কী? উত্তর: Partition leader election (broker fail হলে নতুন leader নির্বাচন), partition reassignment orchestration, আর broker failure detection (heartbeat/liveness ট্র্যাক করা)।
প্র.৪ — Raft-এ "term" বা "epoch" কেন দরকার? উত্তর: একটা পুরনো, বিচ্ছিন্ন leader ফিরে এলে তার term number নতুন leader-এর চেয়ে কম হবে — বাকি সব node তার নির্দেশ প্রত্যাখ্যান করবে (higher term সবসময় জেতে) — এভাবে split-brain প্রতিরোধ হয়।
প্র.৫ — Kafka ডেটা replication-এ ISR মডেল ব্যবহার করে, কিন্তু metadata-এ Raft — এই দ্বৈততার কারণ কী?
উত্তর: Metadata-এ strict consistency অপরিহার্য (split-brain হলে পুরো cluster বিভ্রান্ত হয়ে যাবে), তাই এখানে quorum-based Raft। Data plane-এ throughput সবচেয়ে গুরুত্বপূর্ণ, আর অ্যাপ্লিকেশন নিজেই নিজের durability ট্রেড-অফ (acks) বেছে নিতে পারে — তাই সেখানে হালকা ISR মডেল।
প্র.৬ — Client-এর metadata.max.age.ms অনেক বড় (যেমন ১ ঘণ্টা) সেট করা হলে কী রিস্ক থাকে?
উত্তর: Leader change ঘটলে client অনেকক্ষণ পুরনো (ভুল) broker-এ request পাঠাতে থাকতে পারে যতক্ষণ না সেই broker explicit NOT_LEADER_OR_FOLLOWER এরর দেয় (self-healing এখনো ঘটবে, কিন্তু নির্ভর করবে কতবার request পাঠানো হচ্ছে তার ওপর) — সামান্য বাড়তি latency/retry ঘটতে পারে সাধারণ ছোট max-age-এর তুলনায়।
কর্নার কেস — Module 8
- Controller quorum-এর majority (যেমন 3-এর মধ্যে 2) একসাথে ডাউন হলে পুরো cluster metadata plane থমকে যায় — নতুন topic তৈরি, partition reassignment, এমনকি broker join/leave — কিছুই কাজ করবে না, যদিও existing data plane (produce/consume বিদ্যমান partition-এ) কিছুক্ষণ চলতে পারে শেষ known metadata দিয়ে।
- Combined mode (broker+controller একই প্রসেস)-এ একটা broker crash মানেই সাথে সাথে একটা controller vote-ও হারানো — dev/ছোট cluster-এ এটা গ্রহণযোগ্য, কিন্তু production-এ এই coupling অপ্রত্যাশিত availability সমস্যা তৈরি করতে পারে যদি সচেতনভাবে ডিজাইন না করা হয়।
kafka-metadata-quorum.shদিয়ে quorum lag চেক না করলে, একটা lagging controller replica silently অনেক পিছিয়ে যেতে পারে — সেটা leader হয়ে গেলে (majority vote জিতলেও lag-এর কারণে) সাময়িক metadata inconsistency দেখা দিতে পারে।- ZooKeeper থেকে KRaft migration চলাকালীন (dual-write/bridge mode) একটা সাময়িক জটিল অবস্থা তৈরি হয় যেখানে উভয় সিস্টেম সচল — migration script-এর নির্দেশনা কড়াভাবে অনুসরণ না করলে metadata inconsistency ঘটার ঝুঁকি থাকে; production migration সবসময় staging environment-এ প্রথমে টেস্ট করা উচিত।
metadata.max.age.msছোট রাখলে low-traffic client (যারা কদাচিৎ produce/consume করে) অপ্রয়োজনীয়ভাবে বেশি metadata-refresh call করবে, connection overhead সামান্য বাড়বে — high-scale multi-tenant cluster-এ হাজার হাজার idle client থাকলে এটা controller-এর ওপর অপ্রত্যাশিত লোড তৈরি করতে পারে।
MODULE 9: At-most-once, At-least-once, Exactly-once
ইন্টারভিউতে সবচেয়ে বেশি জিজ্ঞাসিত আর প্রোডাকশনে সবচেয়ে বেশি ভুল বোঝা তিনটা শব্দ। এই মডিউলে দেখা হবে ঠিক কোথায় duplicate বা data-loss ঢুকে পড়ে প্রতিটা মডেলে, আর কীভাবে Kafka Transactions (Transaction Coordinator, transaction markers, isolation level) দিয়ে সত্যিকারের exactly-once অর্জন করা যায় — এবং তার দাম কতটুকু।
তিনটার আসল সংজ্ঞা — Duplicate/Loss কোথায় ঢোকে
At-most-once
মেসেজ একবার বা শূন্যবার ডেলিভার হয় — কখনো দুইবার না, কিন্তু হারিয়েও যেতে পারে। কীভাবে ঘটে: producer পাঠিয়ে ack-এর অপেক্ষা না করেই এগিয়ে যায় (acks=0), অথবা consumer মেসেজ পড়ার আগেই offset commit করে ফেলে (auto-commit, Module 4) — ক্র্যাশ হলে সেই মেসেজ কখনো প্রসেস হবে না।
At-least-once (সবচেয়ে সাধারণ ডিফল্ট)
মেসেজ একবার বা তার বেশি বার ডেলিভার হয় — কখনো হারায় না, কিন্তু duplicate হতে পারে। কীভাবে ঘটে: consumer প্রসেস করার পরে offset commit করে (manual commit) — যদি প্রসেসিং সম্পূর্ণ হওয়ার পর কিন্তু commit করার আগে ক্র্যাশ হয়, restart-এর পর একই মেসেজ আবার আসবে, আবার প্রসেস হবে — দুইবার প্রসেসড।
Exactly-once
মেসেজ ঠিক একবারই effect ফেলে — না হারায়, না duplicate করে। এটা অর্জন করা কঠিনতম — এর জন্য দরকার Kafka Transactions (পরের bullet-গুলোতে) অথবা idempotent consumer লজিক (application-level)।
রিয়েল-লাইফ উদাহরণ
একটা ব্যালেন্স-ডিডাক্ট অপারেশন (deduct_balance(user_id, amount)) যদি at-least-once সিমেন্টিক্সে দুইবার প্রসেস হয়, ইউজারের ব্যালেন্স দুইবার কমে যাবে — একটা সরাসরি financial বাগ। কিন্তু যদি এই operation স্বাভাবিকভাবেই idempotent হয় (যেমন 'set balance to X' রেকর্ড, 'deduct 50' না), তাহলে at-least-once + idempotent operation মিলে কার্যত exactly-once এর মতো ফলাফল দেয় — পুরো Transaction ব্যবহার না করেই।
ড্রব্যাক
- At-most-once সবচেয়ে সহজ implement করা কিন্তু ডেটা লসের ঝুঁকি সবচেয়ে বেশি — শুধু non-critical, loss-tolerant ওয়ার্কলোডে ব্যবহারযোগ্য।
- At-least-once সবচেয়ে common কারণ এটা সহজ এবং নিরাপদ (কখনো ডেটা হারায় না) — কিন্তু downstream-কে duplicate handle করতে জানতে হবে (idempotency)।
- Exactly-once (Transaction-ভিত্তিক) সবচেয়ে জটিল এবং latency/throughput-এর দাম দিতে হয় (পরের bullet-এ বিস্তারিত)।
Transactions: Transaction Coordinator, __transaction_state Topic, Transaction Markers
যন্ত্রপাতি
Kafka Transactions একাধিক partition জুড়ে (এমনকি একাধিক topic জুড়ে) একটা atomic write অপারেশন সম্ভব করে — হয় সব লেখা হবে, নাহলে কিছুই লেখা হবে না (all-or-nothing), Module 3-এর idempotent producer-এর single-partition guarantee-র চেয়ে বিস্তৃত।
- Transaction Coordinator — প্রতিটা transactional producer-এর (
transactional.idদিয়ে চিহ্নিত) একটা নির্দিষ্ট broker coordinator হিসেবে কাজ করে, transaction-এর অবস্থা (begin, commit, abort) ট্র্যাক করে। __transaction_state— একটা internal, compacted topic (Module 6-এর ধারণা এখানে প্রয়োগ) যেখানে প্রতিটা transaction-এর বর্তমান অবস্থা সংরক্ষিত থাকে — coordinator crash করলেও নতুন coordinator এখান থেকে state পুনর্গঠন করতে পারে।- Transaction Markers — যখন একটা transaction commit বা abort হয়, প্রতিটা প্রভাবিত partition-এ একটা বিশেষ marker রেকর্ড লেখা হয় (COMMIT বা ABORT) — consumer এই marker দেখে বুঝতে পারে কোন মেসেজগুলো actual committed transaction-এর অংশ ছিল।
রিয়েল-লাইফ উদাহরণ
PostgreSQL-এর two-phase commit (PREPARE TRANSACTION / COMMIT PREPARED) ধারণাগতভাবে কাছাকাছি — একাধিক resource-এ atomic commit নিশ্চিত করতে একটা coordinator লাগে যে সবাইকে সমন্বয় করে। Kafka Transaction Coordinator একই ভূমিকা পালন করে, কিন্তু Kafka-র নিজস্ব partition/topic-এর মধ্যে সীমাবদ্ধ (কোনো external database-এর সাথে distributed transaction না, যদি না XA-স্টাইল আলাদা ইন্টিগ্রেশন করা হয়)।
ড্রব্যাক
- Transaction Coordinator একটা এক্সট্রা network hop এবং coordination overhead যোগ করে প্রতিটা transaction-এ — non-transactional produce-এর তুলনায় latency বাড়ে।
transactional.idস্থায়ী রাখা জরুরি (একই producer instance restart হলেও একই id ব্যবহার করা উচিত) — নাহলে zombie fencing (পরের ধারণা) সঠিকভাবে কাজ নাও করতে পারে।
Two-phase Commit, Kafka-স্টাইলে
দুইটা ধাপ
- Prepare (Producer-সাইড writes) — producer transaction শুরু করে (
beginTransaction()), একাধিক partition-এ মেসেজ পাঠায়। এই মেসেজগুলো ডিস্কে লেখা হয়ে যায়, কিন্তু একটা বিশেষ 'not yet committed' মার্কার সহ — read_committed consumer এখনো এগুলো দেখতে পায় না (পরের bullet)। - Commit (Coordinator-চালিত marker লেখা) — producer
commitTransaction()কল করলে, Transaction Coordinator প্রতিটা প্রভাবিত partition-এ একটা COMMIT marker লেখে। এই marker লেখা হয়ে গেলেই transaction আনুষ্ঠানিকভাবে সম্পন্ন — সব partition জুড়ে atomic ভাবে visible হয়ে যায়।
যদি producer transaction-এর মাঝপথে ক্র্যাশ করে (prepare-এর পরে, commit-এর আগে), coordinator একটা timeout-এর পর transaction-কে abort করে দেয় — সব লেখা মেসেজে ABORT marker বসে, read_committed consumer এগুলো কখনো দেখবে না, কার্যত সেগুলো 'ছিলোই না' এর মতো আচরণ করে।
রিয়েল-লাইফ উদাহরণ
একটা order-processing pipeline যেখানে একই transaction-এ order.confirmed topic-এ একটা মেসেজ আর inventory.reserved topic-এ আরেকটা মেসেজ পাঠাতে হবে — দুটোই সফল হতে হবে একসাথে, নাহলে কোনোটাই না (নাহলে inconsistent state: অর্ডার confirmed কিন্তু inventory reserve হয়নি)। Transaction ছাড়া এই দুই-topic atomicity অর্জন করা প্রায় অসম্ভব ছিল।
ড্রব্যাক
- Prepare আর commit-এর মধ্যে সময়ে লেখা ডেটা ডিস্কে জায়গা নেয় (এমনকি পরে abort হলেও) — abort হওয়া transaction-এর ডেটা পরে delete retention/compaction-এর মাধ্যমেই cleanup হয়, সাথে সাথে না।
- Consumer-সাইডে read_uncommitted mode ব্যবহার করলে (ডিফল্ট না, কিন্তু সম্ভব) এই আধা-লেখা, potentially-aborted ডেটাও দেখা যায় — সাবধানে isolation level বেছে নেওয়া জরুরি (পরের bullet)।
isolation.level=read_committed — Consumer কীভাবে Aborted Data স্কিপ করে
দুইটা isolation level
read_uncommitted(ডিফল্ট) — consumer সব মেসেজ দেখে, committed বা aborted যাই হোক না কেন। Transactional producer ব্যবহার না করা হলে এটাতে কোনো পার্থক্য নেই (সব মেসেজ non-transactional)।read_committed— consumer শুধু সেই মেসেজ দেখে যেগুলো একটা successfully-committed transaction-এর অংশ (অথবা কখনোই কোনো transaction-এর অংশ ছিল না, plain produce)। Aborted transaction-এর মেসেজ এবং এখনো in-progress (uncommitted) transaction-এর মেসেজ — দুটোই consumer থেকে লুকানো থাকে।
কীভাবে কাজ করে ভেতরে
Consumer broker থেকে fetch করার সময়, broker transaction marker (COMMIT/ABORT) দেখে বুঝে নেয় কোন offset range আসলে committed। read_committed mode-এ consumer শুধুমাত্র committed range পর্যন্তই এগোয় — এখনো-uncommitted একটা transaction-এর মেসেজ থাকলে, সেই পয়েন্টে consumer থেমে যায় (Last Stable Offset বা LSO ধারণা — HW-এর মতোই কিন্তু transaction-aware) যতক্ষণ না সেই transaction commit/abort হয়ে যায়।
রিয়েল-লাইফ উদাহরণ
একটা analytics pipeline যদি ভুলে read_uncommitted ব্যবহার করে একটা transactional producer-এর ডেটার ওপর, সেটা মাঝেমধ্যে এমন ডেটা দেখবে যা পরে abort হয়ে যাবে (যেমন একটা payment যেটা শেষমেশ ব্যর্থ হয়েছিল) — রিপোর্ট/ড্যাশবোর্ডে ভুল সংখ্যা দেখাবে সাময়িকভাবে। read_committed ব্যবহার করলে এই সমস্যা এড়ানো যায়।
ড্রব্যাক
read_committedmode-এ একটা দীর্ঘ-চলমান, ধীর transaction (যেটা commit হতে দেরি করছে) পুরো downstream consumer-কে সেই পয়েন্টে আটকে রাখতে পারে — অন্য, পরের non-transactional মেসেজও LSO-এর কারণে দেখা যাবে না যতক্ষণ না সেই ধীর transaction resolve হয়। এটা transactional producer-এর transaction যতটা সম্ভব ছোট/দ্রুত রাখার একটা শক্তিশালী কারণ।
Consume-Process-Produce Pattern — sendOffsetsToTransaction()
সমস্যা যা এটা সমাধান করে
একটা খুবই সাধারণ stream-processing প্যাটার্ন: একটা topic থেকে consume করা, প্রসেস করা, ফলাফল আরেকটা topic-এ produce করা। এখানে দুইটা আলাদা অপারেশন atomic হওয়া দরকার: (১) output produce করা, আর (২) input-এর offset commit করা। যদি produce সফল হয় কিন্তু offset commit ব্যর্থ হয় (বা উল্টো), duplicate অথবা loss ঘটতে পারে।
সমাধান
producer.sendOffsetsToTransaction(offsets, consumerGroupId) — এই কল consumer-এর offset commit-কেও একই transaction-এর অংশ বানিয়ে দেয়, output produce-এর সাথে। এখন পুরো cycle atomic: হয় output লেখা হবে এবং input offset commit হবে একসাথে, নাহলে দুটোই abort হবে — কখনো একটা হয়ে আরেকটা না-হওয়া সম্ভব না।
রিয়েল-লাইফ উদাহরণ
একটা fraud-detection consumer যেটা transaction.raw থেকে পড়ে, ঝুঁকিপূর্ণ লেনদেন চিহ্নিত করে transaction.flagged-এ পাঠায়। Transaction ছাড়া: যদি flagged-এ produce সফল হয় কিন্তু offset commit ব্যর্থ হয়ে ক্র্যাশ করে, restart-এ একই transaction আবার প্রসেস হবে, transaction.flagged-এ duplicate flag তৈরি হবে। sendOffsetsToTransaction() ব্যবহার করলে এই পুরো cycle atomic হয়ে যায় — duplicate flagging হয় না।
ড্রব্যাক
- এই প্যাটার্ন শুধু তখনই পূর্ণ exactly-once guarantee দেয় যখন consume এবং produce দুটোই Kafka-র মধ্যে থাকে। যদি প্রসেসিং-এর মাঝে একটা external side-effect থাকে (যেমন একটা external API কল, বা database write) যেটা transaction-এর অংশ না, সেই external effect duplicate হতে পারে এমনকি Kafka transaction সঠিকভাবে কাজ করলেও — Kafka transaction শুধু Kafka-র ভেতরের atomicity guarantee দেয়, বাইরের সিস্টেমের না।
EOS-এর দাম — Latency আর Throughput কতটা কমে
কোথা থেকে খরচ আসে
- প্রতিটা transaction-এ Transaction Coordinator-এর সাথে extra round-trip (begin, commit handshake)।
- Producer batching-এ transaction boundary মেনে চলতে হয় — একটা transaction commit না হওয়া পর্যন্ত সেই batch-এর পরের ডেটা নতুন transaction-এ যেতে পারে না, যা batching efficiency সীমিত করতে পারে যদি transaction ছোট ছোট রাখা হয় (frequent commit)।
read_committedconsumer-দের LSO-বাউন্ডেড read করতে হয় (আগের bullet) — extra bookkeeping broker-সাইডেও।- Transaction marker নিজেই এক্সট্রা রেকর্ড হিসেবে ডিস্কে জায়গা নেয় প্রতিটা partition-এ।
বাস্তব সংখ্যা (আনুমানিক প্যাটার্ন, বেঞ্চমার্ক-নির্ভর)
Confluent-এর প্রকাশিত বেঞ্চমার্কে EOS চালু করলে throughput ~২০-৩০% কমতে দেখা গেছে ছোট, ঘন ঘন transaction-এ (যেমন প্রতি মেসেজে commit) — কিন্তু transaction-এর ভেতরে বেশি মেসেজ ব্যাচ করে কম ঘন ঘন commit করলে (যেমন প্রতি ১০০ms বা প্রতি কয়েকশো মেসেজে একবার) এই overhead অনেকটাই কমিয়ে আনা যায়, throughput impact কয়েক শতাংশে নেমে আসতে পারে।
রিয়েল-লাইফ উদাহরণ
একটা payment reconciliation pipeline-এ duplicate-এর কোনো সহনশীলতা নেই — এখানে ২০-৩০% throughput loss মেনে নেওয়া সহজ সিদ্ধান্ত, কারণ correctness-এর মূল্য অনেক বেশি। কিন্তু একটা high-volume clickstream pipeline-এ (যেখানে occasional duplicate acceptable, downstream aggregation approximate) EOS-এর খরচ অপ্রয়োজনীয় — সেখানে at-least-once + idempotent downstream logic ভালো ট্রেড-অফ।
ড্রব্যাক
- EOS সব জায়গায় blanket ব্যবহার করা একটা সাধারণ ওভার-ইঞ্জিনিয়ারিং ভুল — প্রতিটা pipeline-এর জন্য আলাদাভাবে বিচার করা উচিত duplicate/loss-এর ব্যবসায়িক প্রভাব কতটা গুরুতর।
Idempotent Consumer — কেন প্রায়ই Transaction-এর চেয়ে ভালো সমাধান
ধারণা
Transaction ছাড়াই at-least-once + একটা idempotent consumer লজিক দিয়ে কার্যকরভাবে exactly-once-এর মতো ফলাফল পাওয়া সম্ভব — consumer এমনভাবে ডিজাইন করা যাতে একই মেসেজ একাধিকবার প্রসেস হলেও চূড়ান্ত ফলাফল একই থাকে (duplicate প্রসেসিং নিরীহ)।
সাধারণ কৌশল
- Natural idempotency — 'set field X to value Y' টাইপ operation স্বাভাবিকভাবেই idempotent (বারবার করলেও একই ফলাফল), কিন্তু 'increment X by 1' না (Module 9 bullet 1-এর ব্যালেন্স উদাহরণ)।
- Deduplication key/table — প্রতিটা প্রসেস করা মেসেজের একটা ইউনিক id (business-level, যেমন
payment_id) একটা dedup store-এ (Redis set, ডেটাবেজ টেবিল) রেকর্ড রাখা — প্রসেস করার আগে চেক করা এই id আগে দেখা হয়েছে কিনা। - Database unique constraint — output ডেটাবেজে একটা unique constraint (যেমন
payment_idকলামে UNIQUE) ব্যবহার করলে duplicate insert স্বাভাবিকভাবেই ব্যর্থ হবে (বা upsert-এ no-op হবে), কোনো এক্সট্রা লজিক ছাড়াই।
কেন এটা প্রায়ই Transaction-এর চেয়ে ভালো
- Kafka Transaction শুধু Kafka-র ভেতরের atomicity দেয় — বাইরের সিস্টেমে (database, external API) side-effect থাকলে সেটা এমনিতেও idempotent বানাতে হবে। তাহলে অনেক ক্ষেত্রে শুরু থেকেই idempotent বানানো একমাত্র বাস্তবসম্মত সমাধান, Transaction একটা অতিরিক্ত জটিলতা যা পুরো সমস্যা সমাধান করে না।
- Idempotent consumer EOS-এর latency/throughput খরচ ছাড়াই কার্যত একই ব্যবসায়িক নিশ্চয়তা দেয়।
রিয়েল-লাইফ উদাহরণ
Debezium থেকে আসা CDC ইভেন্ট যখন Elasticsearch-এ sync হয় (Module 11), প্রতিটা ডকুমেন্ট _id = primary key দিয়ে index করা হলে, একই ইভেন্ট দুইবার প্রসেস হলেও Elasticsearch-এ শুধু একই ডকুমেন্ট overwrite হয় — স্বাভাবিকভাবেই idempotent, কোনো Kafka Transaction ছাড়াই সঠিক ফলাফল।
ড্রব্যাক
- সব operation স্বাভাবিকভাবে idempotent বানানো যায় না (যেমন 'পাঠাও একটা ইমেইল' — দুইবার পাঠালে ইউজার দুইবার ইমেইল পাবে) — সেখানে explicit deduplication store লাগবে, যা নিজেই একটা এক্সট্রা dependency এবং failure mode।
ল্যাব: Transactional Producer মাঝপথে Crash — read_committed vs read_uncommitted
সেটআপ
from confluent_kafka import Producer
import time
p = Producer({
'bootstrap.servers': 'localhost:9092',
'transactional.id': 'demo-txn-producer-1',
})
p.init_transactions()
p.begin_transaction()
p.produce('txn.demo', key=b'k1', value=b'message-1-in-transaction')
p.produce('txn.demo', key=b'k2', value=b'message-2-in-transaction')
p.flush()
print('মেসেজ পাঠানো হয়েছে কিন্তু commit করা হয়নি — এখন প্রসেস kill করুন (Ctrl+C বা আলাদা টার্মিনাল থেকে kill)')
time.sleep(60) # ইচ্ছাকৃতভাবে কমিট করার আগে থামানো, সিমুলেট করার জন্য একটা mid-transaction crash
p.commit_transaction()
ধাপ ১ — script চালান, ৬০ সেকেন্ডের sleep-এর সময় kill করুন (Ctrl+C)
এটা একটা transaction যেটা কখনো commit হয়নি সিমুলেট করে (ক্র্যাশ হয়ে গেছে ধরে নিন)।
ধাপ ২ — দুইটা ভিন্ন isolation level দিয়ে consume করুন
# read_uncommitted (ডিফল্ট) — মেসেজ দেখা যাবে
kafka-console-consumer.sh --topic txn.demo --from-beginning \
--consumer-property isolation.level=read_uncommitted \
--bootstrap-server localhost:9092
# read_committed — মেসেজ দেখা যাবে না (transaction কখনো commit হয়নি)
kafka-console-consumer.sh --topic txn.demo --from-beginning \
--consumer-property isolation.level=read_committed \
--bootstrap-server localhost:9092
চ্যালেঞ্জ
Script-টা আবার চালান, কিন্তু এবার sleep না দিয়ে সরাসরি commit_transaction() কল করুন — দুই isolation level-এই এবার মেসেজ দেখা উচিত। তারপর p.abort_transaction() দিয়ে explicit abort করে দেখুন read_committed consumer কখনোই সেই মেসেজ দেখে না।
ইন্টারভিউ প্রশ্ন — Module 9
প্র.১ — At-least-once আর exactly-once-এর মধ্যে ব্যবহারিক পার্থক্য কী? উত্তর: At-least-once-এ মেসেজ কখনো হারায় না কিন্তু duplicate হতে পারে (downstream-কে idempotent হতে হবে)। Exactly-once-এ মেসেজের effect ঠিক একবারই ঘটে — Kafka Transactions বা idempotent consumer logic দিয়ে অর্জিত হয়।
প্র.২ — sendOffsetsToTransaction() কোন সমস্যা সমাধান করে?
উত্তর: Consume-process-produce প্যাটার্নে output produce করা আর input offset commit করা দুইটা আলাদা অপারেশন — এই কল দুটোকে একই transaction-এ আবদ্ধ করে দেয়, যাতে হয় দুটোই সফল হয় বা দুটোই abort হয়, কখনো একটা হয়ে আরেকটা না-হওয়া অবস্থায় না পড়ে।
প্র.৩ — Kafka Transaction কি বাইরের সিস্টেমেও (যেমন একটা external database) atomicity guarantee দেয়? উত্তর: না। Kafka Transaction শুধু Kafka-র ভেতরের একাধিক topic/partition জুড়ে atomicity দেয়। যদি প্রসেসিং-এ কোনো external side-effect থাকে (API কল, DB write) যেটা transaction-এর অংশ না, সেটা transaction-নির্বিশেষে duplicate হতে পারে — সেটা আলাদাভাবে idempotent বানাতে হবে।
প্র.৪ — read_committed isolation level কীভাবে aborted transaction-এর ডেটা লুকায়?
উত্তর: Broker প্রতিটা transaction commit/abort হলে একটা marker রেকর্ড লেখে। read_committed consumer শুধু committed range (Last Stable Offset পর্যন্ত) পড়ে — aborted বা এখনো in-progress transaction-এর মেসেজ দেখতে পায় না।
প্র.৫ — কেন একটা টিমকে blanket ভাবে সব pipeline-এ EOS ব্যবহার করার বদলে সাবধানে বিবেচনা করতে বলবেন? উত্তর: EOS latency ও throughput-এর দাম নেয় (coordinator round-trip, transaction boundary-জনিত batching সীমাবদ্ধতা)। অনেক ক্ষেত্রে at-least-once + idempotent consumer logic একই ব্যবসায়িক নিশ্চয়তা দেয় অনেক কম খরচে — শুধু duplicate-অসহনশীল, critical pipeline-এই EOS-এর খরচ যুক্তিসঙ্গত।
প্র.৬ — একটা consumer 'increment balance by 10' টাইপ অপারেশন at-least-once সিমেন্টিক্সে প্রসেস করছে — এটা কীভাবে ভাঙতে পারে, আর কীভাবে ফিক্স করবেন? উত্তর: Duplicate delivery হলে balance দুইবার বেড়ে যাবে (ভুল ফলাফল) কারণ increment operation স্বাভাবিকভাবে idempotent না। ফিক্স: হয় operation-কে idempotent বানান ('set balance to X' স্টাইলে, একটা unique transaction id সহ dedup করে), অথবা Kafka Transaction + sendOffsetsToTransaction() ব্যবহার করে duplicate প্রসেসিং পুরোপুরি এড়ান।
কর্নার কেস — Module 9
- Auto-commit চালু রেখে 'at-least-once নিশ্চিত করছি' ভাবা একটা ক্লাসিক ভুল — auto-commit আসলে at-most-once-এর দিকে ঝুঁকে থাকতে পারে (processing শেষ হওয়ার আগেই commit হয়ে যেতে পারে) — সঠিক at-least-once-এর জন্য manual commit, প্রসেসিং শেষ হওয়ার পরে, আবশ্যক।
transactional.idপ্রতিটা producer instance-এ ইউনিক না রাখলে (দুইটা instance একই id শেয়ার করলে), দ্বিতীয় producer শুরু হওয়ার সাথে সাথে coordinator প্রথমটাকে 'zombie' হিসেবে fence করে দেয় (তার সব পরবর্তী রাইট ব্যর্থ হবে) — এটা ইচ্ছাকৃত সুরক্ষা মেকানিজম (duplicate producer instance থেকে রক্ষা), কিন্তু deployment ভুল কনফিগে অপ্রত্যাশিত producer failure হিসেবে দেখা দিতে পারে।- দীর্ঘ-চলমান transaction (
transaction.timeout.msপার হয়ে যাওয়া) স্বয়ংক্রিয়ভাবে abort হয়ে যায় — যদি একটা batch প্রসেস করতে প্রত্যাশার চেয়ে বেশি সময় লাগে (downstream slow), পুরো transaction-এর কাজ বাতিল হয়ে যেতে পারে, retry লজিক দরকার। read_committedconsumer একটা আটকে যাওয়া (never committed, never aborted) transaction-এর পেছনে অনির্দিষ্টকাল আটকে থাকতে পারে যদি producer crash করে কিন্তু coordinator timeout যথেষ্ট বড় সেট করা থাকে — এই ক্ষেত্রে পুরো downstream consumer group-এর lag বাড়তেই থাকবে যতক্ষণ না coordinator transaction-কে timeout করে abort করে।- Idempotent consumer deduplication store (Redis/DB) নিজেই যদি ব্যর্থ হয় বা reset হয়ে যায়, deduplication guarantee হারিয়ে যায় — dedup store নিজেও durable/highly-available হওয়া দরকার, নাহলে পুরো idempotency কৌশল ভেঙে পড়ে ঠিক তখনই যখন সবচেয়ে বেশি দরকার (outage recovery-র সময়)।
- Transaction marker রেকর্ড নিজেও offset ব্যবহার করে — consumer-সাইড offset গণনা করার সময় (যেমন lag হিসাব) এই marker-জনিত 'gap' consumer lag মনিটরিং-এ সামান্য বিভ্রান্তিকর সংখ্যা দেখাতে পারে যদি সেটা বিবেচনায় না রাখা হয়।
MODULE 10: Schema Registry আর ডেটা কন্ট্র্যাক্ট
Kafka নিজে schema-agnostic — যেকোনো বাইট পাঠানো যায়। কিন্তু প্রোডাকশনে producer আর consumer-দের মধ্যে একটা ডেটা কন্ট্র্যাক্ট দরকার, নাহলে একজনের schema বদল আরেকজনের কনজিউমারকে ভাঙে। এই মডিউলে Avro/Protobuf/JSON Schema-র তুলনা, wire format, compatibility mode, আর schema evolution বাস্তবে কীভাবে ভাঙে সেটা শেখা হবে।
Avro vs Protobuf vs JSON Schema
তুলনা
| ফরম্যাট | সাইজ | Schema evolution | পঠনযোগ্যতা | ইকোসিস্টেম |
|---|---|---|---|---|
| Avro | কমপ্যাক্ট (binary) | চমৎকার সাপোর্ট, Kafka ইকোসিস্টেমে সবচেয়ে বেশি ব্যবহৃত | না (binary) | Confluent Schema Registry-র মূল ফোকাস, Hadoop ইকোসিস্টেমেও জনপ্রিয় |
| Protobuf | কমপ্যাক্ট (binary) | ভালো সাপোর্ট (field number-ভিত্তিক), gRPC-তেও ব্যবহৃত | না (binary) | Google ইকোসিস্টেম, cross-language RPC-তে শক্তিশালী |
| JSON Schema | বড় (text-based) | দুর্বলতর সাপোর্ট (schema validation আছে কিন্তু evolution rule কম কড়া) | হ্যাঁ (human-readable) | Debug করা সহজ, কিন্তু network/storage খরচ বেশি |
কেন Binary ফরম্যাট (Avro/Protobuf) প্রোডাকশনে প্রাধান্য পায়
হাই-থ্রুপুট Kafka pipeline-এ প্রতি বাইট গুরুত্বপূর্ণ (Module 3-এর compression discussion মনে করুন) — JSON-এর field name বারবার text হিসেবে repeat হওয়া ({"user_id": 123, "user_id": 123, ...} প্রতিটা মেসেজে) অপচয়। Avro/Protobuf schema আলাদাভাবে সংজ্ঞায়িত থাকে, actual ডেটাতে শুধু values (এবং সংক্ষিপ্ত schema reference) থাকে।
রিয়েল-লাইফ উদাহরণ
Debezium ডিফল্টভাবে JSON আউটপুট দেয় (readable, debug সহজ) — কিন্তু হাই-ভলিউম প্রোডাকশনে Avro converter-এ পরিবর্তন করা কমন practice, কারণ ডেটা সাইজ ৫০-৭০% পর্যন্ত কমতে পারে একই তথ্যের জন্য, network এবং storage খরচ দুটোতেই সাশ্রয়।
ড্রব্যাক
- Binary format ডিবাগ করা কঠিন — একটা raw Kafka মেসেজ চোখে পড়া যায় না সরাসরি, schema ছাড়া decode করা অসম্ভব। JSON Schema এখানে debugging-friendly।
- Protobuf-এ schema versioning field number-নির্ভর — number reuse করলে (ভুল করে) সাইলেন্ট ডেটা করাপশন হতে পারে, Avro-তে এই নির্দিষ্ট ঝুঁকি নেই (নাম-ভিত্তিক)।
Wire Format: Magic Byte + Schema ID + Payload
গঠন
Schema Registry ব্যবহার করা প্রতিটা মেসেজের একটা নির্দিষ্ট বাইনারি লেআউট থাকে:
[Magic Byte (1 byte)] [Schema ID (4 bytes)] [Serialized Payload (N bytes)]
- Magic Byte — সবসময়
0x0, একটা version indicator (ভবিষ্যতে ফরম্যাট বদলালে ব্যবহারের জন্য)। - Schema ID — একটা 4-byte integer যা Schema Registry-তে রেজিস্টার্ড একটা নির্দিষ্ট schema version-কে নির্দেশ করে। পুরো schema definition (যেটা অনেক বড় হতে পারে) মেসেজে থাকে না, শুধু এই ছোট reference id।
- Payload — actual Avro/Protobuf-এনকোডেড ডেটা, ঐ schema id অনুযায়ী।
কেন এই ডিজাইন
প্রতিটা মেসেজে পুরো schema definition এমবেড করলে (যেমন Avro-র self-describing মোড) প্রতিটা মেসেজের সাইজ অনেক বেড়ে যেত। শুধু একটা ৪-বাইট reference id পাঠিয়ে, consumer সেই id দিয়ে Schema Registry থেকে (client-সাইড cache করা, রেজিস্ট্রি call প্রতিবার লাগে না) পুরো schema fetch করে নেয় — একবার fetch করলে cache-এ থেকে যায়, পরের একই schema-id মেসেজে registry call লাগে না।
রিয়েল-লাইফ উদাহরণ
একটা consumer প্রথমবার schema id 42 দেখলে Schema Registry-কে জিজ্ঞেস করে ('schema 42 কী?'), উত্তর cache করে রাখে। পরের ১০ লাখ মেসেজ যদি একই schema id 42 ব্যবহার করে, consumer আর কোনো network call করে না — শুধু cache থেকে schema নিয়ে deserialize করে। এই caching-ই Schema Registry-নির্ভর deserialization-কে দ্রুত রাখে।
ড্রব্যাক
- Schema Registry নিজেই একটা extra infrastructure dependency — এটা ডাউন থাকলে নতুন (cache-এ নেই এমন) schema-এর মেসেজ deserialize করা যাবে না, যদিও ইতিমধ্যে cache করা schema-র মেসেজ ঠিকই কাজ করবে।
- Magic byte + schema id ওভারহেড (৫ বাইট) খুব ছোট মেসেজের জন্য (কয়েক বাইট payload) আনুপাতিকভাবে উল্লেখযোগ্য হতে পারে।
Compatibility Modes: BACKWARD / FORWARD / FULL / TRANSITIVE
চারটা মোড
- BACKWARD — নতুন schema দিয়ে পুরনো ডেটা পড়া যাবে (নতুন consumer পুরনো producer-এর ডেটা পড়তে পারবে)। সবচেয়ে সাধারণ ডিফল্ট — নতুন optional field যোগ করা, বা field মুছে ফেলা (যদি default ছিল) এই মোডে সাপোর্টেড।
- FORWARD — পুরনো schema দিয়ে নতুন ডেটা পড়া যাবে (পুরনো consumer নতুন producer-এর ডেটা পড়তে পারবে, নতুন field ignore করে)।
- FULL — BACKWARD এবং FORWARD দুটোই একসাথে — সবচেয়ে কড়া, সবচেয়ে নিরাপদ, কিন্তু সবচেয়ে সীমাবদ্ধ (schema বদলানোর স্বাধীনতা কম)।
- TRANSITIVE — compatibility শুধু immediately আগের version-এর সাথে না, বরং সব আগের version-এর সাথে চেক করা হয় (
BACKWARD_TRANSITIVEইত্যাদি)। এটা নিশ্চিত করে দীর্ঘমেয়াদী evolution history-তেও কোনো version ভাঙে না।
রিয়েল-লাইফ উদাহরণ
আপনার order.placed schema-তে একটা নতুন optional field discount_code যোগ করতে চান। BACKWARD compatibility-তে এটা নিরাপদ (default value থাকলে) — পুরনো ডেটাতে এই field না থাকলেও নতুন schema সেটাকে default দিয়ে পড়তে পারবে। কিন্তু যদি একটা বিদ্যমান field (customer_email) মুছে ফেলেন কোনো default ছাড়া, এটা BACKWARD compatibility ভাঙবে — Schema Registry এই change reject করবে যদি compatibility mode enforce করা থাকে।
ড্রব্যাক
- FULL/TRANSITIVE মোড সবচেয়ে নিরাপদ কিন্তু এতে schema evolution-এর নমনীয়তা সবচেয়ে কম — কিছু legitimate পরিবর্তনও (যেমন একটা field-এর টাইপ বদলানো) এই মোডে ব্লক হয়ে যাবে, যদিও ব্যবহারিকভাবে সেটা নিরাপদ হতে পারতো নির্দিষ্ট প্রসঙ্গে।
- Compatibility checking শুধু structural — এটা business logic-level breaking change ধরতে পারে না (যেমন একটা field-এর মানে বদলে গেছে কিন্তু নাম/টাইপ একই — schema compatible কিন্তু semantically ভাঙা)।
Schema Evolution প্রোডাকশনে কীভাবে ভাঙে
সবচেয়ে সাধারণ ব্যর্থতার প্যাটার্ন
- Required field যোগ করা (default ছাড়া) — এটা BACKWARD compatibility ভাঙে, কারণ পুরনো ডেটাতে সেই field ছিলই না, নতুন schema সেটা required হিসেবে দাবি করলে deserialize ব্যর্থ হবে।
- Field-এর টাইপ বদলানো (
intথেকেstring) — এমনকি একই নামের field হলেও, এটা প্রায় সবসময় সব compatibility mode-এই breaking। - Enum-এ নতুন value যোগ করা — producer নতুন enum value পাঠালে, পুরনো consumer (যে নতুন value চেনে না) deserialize ব্যর্থ হতে পারে বা crash করতে পারে — Avro-তে এটা handle করার জন্য default/unknown-value strategy ডিজাইন করা দরকার।
- Compatibility check bypass করে ম্যানুয়ালি schema push করা (Schema Registry-র enforce না করে, অথবা compatibility mode NONE সেট করে) — সবচেয়ে বিপজ্জনক, কোনো সুরক্ষা ছাড়াই যেকোনো breaking change প্রোডাকশনে চলে যেতে পারে।
রিয়েল-লাইফ উদাহরণ
একটা টিম order.placed schema-তে customer_email: string কে customer_email: EmailObject (structured type) এ বদলে দিলো, স্থানীয় টেস্টে সব ঠিক দেখালো (নিজেদের নতুন consumer দিয়ে টেস্ট করেছিল)। কিন্তু production-এ আরেকটা টিমের পুরনো consumer (যে এখনো string আশা করছিল) deserialize ব্যর্থ হয়ে crash loop-এ পড়ে গেলো — এই ধরনের cross-team breaking change-ই Schema Registry-র compatibility enforcement দিয়ে আগে থেকেই আটকানো উচিত ছিল।
ড্রব্যাক / প্রতিরোধ
- Schema Registry-তে compatibility mode enforce করা (CI/CD pipeline-এ schema push করার আগে compatibility check বাধ্যতামূলক করা) — প্রায় সব breaking change deployment-এর আগেই ধরা পড়বে।
- একাধিক টিম একই topic-এর producer/consumer হলে, schema change-এর জন্য একটা কেন্দ্রীয় review/notification প্রক্রিয়া থাকা উচিত — শুধু automated compatibility check যথেষ্ট না, কারণ semantic breaking change (মানে বদলে যাওয়া) automated check ধরতে পারে না।
ল্যাব: Schema রেজিস্টার করে Evolve করা
সেটআপ — Schema Registry docker-এ যোগ করুন
schema-registry:
image: confluentinc/cp-schema-registry:7.6.0
ports: ["8081:8081"]
environment:
SCHEMA_REGISTRY_HOST_NAME: schema-registry
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: kafka:9092
SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081
ধাপ ১ — একটা Avro schema রেজিস্টার করুন
curl -X POST http://localhost:8081/subjects/order.placed-value/versions \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"schema": "{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"order_id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"}]}"}'
ধাপ ২ — BACKWARD-compatible পরিবর্তন যোগ করুন (একটা optional field)
discount_code field একটা default সহ যোগ করে আবার POST করুন — এটা সফল হওয়া উচিত।
ধাপ ৩ — একটা breaking change চেষ্টা করুন
order_id field-এর টাইপ string থেকে int-এ বদলে আবার POST করুন — Schema Registry এটা reject করবে (compatibility violation এরর) যদি compatibility mode BACKWARD সেট থাকে।
চ্যালেঞ্জ
curl http://localhost:8081/config/order.placed-value -X PUT -d '{"compatibility": "NONE"}' দিয়ে compatibility check বন্ধ করে দেখুন এবার breaking change-ও accept হয়ে যায় — তারপর ভাবুন এটা production-এ কেন বিপজ্জনক।
ইন্টারভিউ প্রশ্ন — Module 10
প্র.১ — Binary format (Avro/Protobuf) JSON-এর চেয়ে প্রোডাকশনে বেশি ব্যবহৃত হয় কেন? উত্তর: সাইজে অনেক কমপ্যাক্ট (field name বারবার repeat হয় না, schema আলাদা রাখা হয়) — network bandwidth ও storage দুটোতেই সাশ্রয় হাই-থ্রুপুট pipeline-এ। বিনিময়ে human-readability হারায়।
প্র.২ — Schema Registry-র wire format-এ প্রতিটা মেসেজে পুরো schema না রেখে শুধু একটা schema id রাখা হয় কেন? উত্তর: পুরো schema বারবার এমবেড করলে মেসেজের সাইজ অনেক বেড়ে যেত। Schema id একটা ছোট ৪-বাইট reference, consumer সেটা দিয়ে Schema Registry থেকে schema fetch করে client-সাইড cache করে রাখে — পরের মেসেজে আর network call লাগে না।
প্র.৩ — BACKWARD আর FORWARD compatibility-র পার্থক্য কী? উত্তর: BACKWARD মানে নতুন schema পুরনো ডেটা পড়তে পারবে (নতুন consumer পুরনো producer-এর ডেটা)। FORWARD মানে পুরনো schema নতুন ডেটা পড়তে পারবে (পুরনো consumer নতুন producer-এর ডেটা, নতুন field ignore করে)।
প্র.৪ — কোন ধরনের schema পরিবর্তন প্রায় সবসময় breaking? উত্তর: Required field যোগ করা (default ছাড়া), বিদ্যমান field-এর টাইপ বদলানো, আর একটা field সম্পূর্ণ মুছে ফেলা যদি default না থাকে — এগুলো BACKWARD/FORWARD compatibility ভাঙে।
প্র.৫ — Schema compatibility check কী ধরনের breaking change ধরতে পারে না? উত্তর: Semantic (মানে/অর্থ) পরিবর্তন — যেমন একটা field-এর নাম আর টাইপ একই থেকে গেলেও তার ব্যবসায়িক অর্থ বদলে গেলে (যেমন একটা currency field আগে USD ধরে নেওয়া হতো, এখন multi-currency) — এটা schema-level structural check ধরতে পারে না, শুধু টিমের মধ্যে যোগাযোগ/ডকুমেন্টেশন দিয়ে প্রতিরোধ করা যায়।
প্র.৬ — একটা multi-team organization-এ schema breaking change প্রোডাকশনে যাওয়া কীভাবে প্রতিরোধ করবেন? উত্তর: CI/CD pipeline-এ schema push করার আগে Schema Registry compatibility check বাধ্যতামূলক করা (schema পরিবর্তন merge/deploy হওয়ার আগেই ব্যর্থ হবে যদি ভাঙে), compatibility mode অন্তত BACKWARD রাখা ডিফল্ট হিসেবে, আর multi-team ব্যবহৃত topic-এর জন্য একটা schema-change review প্রক্রিয়া থাকা।
কর্নার কেস — Module 10
- Compatibility mode NONE (disabled) একটা টপিকে ভুলে রয়ে গেলে, কেউ না জেনেই একটা breaking schema push করে দিতে পারে — এটা সাধারণত অনেক পরে ধরা পড়ে (downstream consumer crash শুরু হলে), root cause খুঁজতে সময় লাগে যদি schema change history আলাদাভাবে audit করা না থাকে।
- Schema Registry নিজেই যদি একটা single point of failure হয় (highly-available cluster mode-এ না চালানো হলে), এটা ডাউন হলে নতুন schema-র মেসেজ deserialize করা যাবে না — যদিও ইতিমধ্যে cache করা schema-র মেসেজ ঠিকই প্রসেস হবে (সাময়িকভাবে কাজ চলবে, কিন্তু কোনো নতুন consumer instance startup-এ সমস্যা হবে যদি সেই মুহূর্তেই registry lookup লাগে)।
- Avro-তে enum-এ নতুন value যোগ করা BACKWARD-এর জন্য breaking হতে পারে যদি consumer-সাইডে unknown enum value হ্যান্ডেল করার লজিক না থাকে — অনেকে ভুলে মনে করে enum-এ value যোগ করা সবসময় নিরাপদ (নতুন field যোগ করার মতো), কিন্তু এটা field যোগ করা থেকে ভিন্ন আচরণ করে।
- Protobuf-এ field number পুনর্ব্যবহার (reuse) করলে (একটা মুছে ফেলা field-এর number নতুন field-এ ব্যবহার), পুরনো serialized ডেটা নতুন schema দিয়ে ভুলভাবে deserialize হতে পারে — কোনো explicit এরর ছাড়াই সাইলেন্ট ডেটা করাপশন, যা schema compatibility check-ও সবসময় ধরতে পারে না যদি টুলিং সঠিকভাবে সেট আপ করা না থাকে।
- একই topic-এ একাধিক ভিন্ন schema "subject" strategy (TopicNameStrategy vs RecordNameStrategy) মিশিয়ে ব্যবহার করলে confusion তৈরি হয় কোন schema কোথায় apply হচ্ছে — একটা টিমের জন্য যেটা কাজ করে সেটা আরেকটা টিমের প্রত্যাশার সাথে না মিললে integration bug দেখা দেয়।
MODULE 11: Kafka Connect আর CDC
এটা আপনার (Rakib) হোম গ্রাউন্ড — Debezium, PostgreSQL logical decoding, Outbox pattern — এসবের সাথে ইতিমধ্যেই পরিচয় আছে। এই মডিউলে Kafka Connect-এর আর্কিটেকচার, Debezium ঠিক কীভাবে ভেতরে কাজ করে (replication slot থেকে Kafka topic পর্যন্ত), আর dual-write সমস্যার সঠিক সমাধান — Outbox pattern — গভীরে দেখা হবে।
Connect Architecture: Worker, Connector, Task, Converter, Transform (SMT)
যন্ত্রাংশ
- Worker — একটা JVM প্রসেস যা connector চালায় (standalone অথবা distributed mode)।
- Connector — একটা plugin (JAR) যা source (ডেটা Kafka-তে আনা, যেমন Debezium) বা sink (Kafka থেকে বাইরে পাঠানো, যেমন Elasticsearch sink) সংজ্ঞায়িত করে — কনফিগারেশন আর task-এর কাজ ভাগ করার লজিক ধরে রাখে।
- Task — connector-এর কাজের actual execution unit — একটা connector একাধিক task-এ ভাগ হতে পারে parallelism-এর জন্য (worker-দের মধ্যে বণ্টিত হয়)।
- Converter — বাইট ↔ structured object রূপান্তর করে (JsonConverter, AvroConverter — Module 10-এর Schema Registry এখানে ব্যবহৃত হয় যদি Avro converter বেছে নেওয়া হয়)।
- Transform (Single Message Transform, SMT) — প্রতিটা মেসেজে ছোট, lightweight পরিবর্তন করে pipeline-এর মধ্যে দিয়ে যাওয়ার সময় (field mask করা, route পরিবর্তন করা, timestamp যোগ করা) — কোনো কোড লেখা ছাড়াই কনফিগ দিয়ে।
রিয়েল-লাইফ উদাহরণ
আপনার একটা Debezium PostgreSQL source connector distributed mode-এ চালানো — connector একটা টেবিলের প্রতিটা schema/table-কে একটা task হিসেবে ভাগ করতে পারে (Debezium সাধারণত single-task per connector চালায় একটা ডেটাবেজের জন্য, logical decoding-এর প্রকৃতির কারণে), আর একটা SMT ব্যবহার করে sensitive field (যেমন ssn) mask করে দিতে পারেন Kafka-তে পৌঁছানোর আগেই।
ড্রব্যাক
- SMT শুধু per-message, stateless transformation-এর জন্য উপযুক্ত — কোনো cross-message logic (aggregation, join) দরকার হলে Kafka Streams/ksqlDB লাগবে (Module 12), Connect যথেষ্ট না।
- অনেক SMT chain করলে (একাধিক transform পরপর) প্রতিটা মেসেজ প্রসেসিং-এর latency/CPU overhead বাড়ে — জটিল লজিক connector-এর বদলে downstream consumer-এ রাখা ভালো অনেক ক্ষেত্রে।
Distributed Mode, Offset Storage Topic, Rebalance
কেন Distributed Mode
Standalone mode-এ একটা single worker প্রসেস — সহজ কিন্তু কোনো fault tolerance নেই (worker crash করলে connector বন্ধ)। Distributed mode-এ একাধিক worker একটা cluster গঠন করে (Module 4-এর consumer group protocol-এর সাথে conceptually কাছাকাছি — REST API দিয়ে কনফিগার করা হয়, internal Kafka topic দিয়ে coordination হয়)।
Internal Topics
config.storage.topic(compacted) — connector configuration সংরক্ষণ করে, সব worker sync থাকার জন্য।offset.storage.topic(compacted) — প্রতিটা source connector task কতদূর পর্যন্ত সোর্স থেকে পড়েছে তার offset (যেমন Debezium-এর জন্য এটা WAL LSN-এর সমতুল্য একটা position)।status.storage.topic— connector/task-এর runtime status (running, failed, paused)।
Rebalance
Worker যোগ/বিয়োগ হলে (deployment, crash), Connect cluster নিজের মধ্যে task পুনর্বণ্টন করে — অনেকটা Module 4-এর consumer group rebalance-এর মতোই ধারণা, কিন্তু partition-এর বদলে connector task ভাগ হয়।
রিয়েল-লাইফ উদাহরণ
Debezium connector distributed mode-এ চালানো একটা ৩-worker Connect cluster-এ — একটা worker crash করলে, তার চালানো task (Debezium connector সহ) অন্য worker-এ পুনরায় শুরু হয়, offset storage topic থেকে শেষ known position পড়ে সেখান থেকে চালিয়ে যায় (WAL-এর কোনো ডেটা মিস হয় না, যতক্ষণ PostgreSQL replication slot এখনো সেই পয়েন্ট থেকে ডেটা ধরে রেখেছে)।
ড্রব্যাক
- Distributed mode অপারেশনাল জটিলতা যোগ করে (একাধিক worker manage করা, REST API-এর মাধ্যমে deploy করা) — ছোট, single-connector সেটআপে standalone mode সহজ হতে পারে।
- Rebalance চলাকালীন (Module 4-এর eager rebalance-এর মতোই পুরনো Connect version-এ) সাময়িকভাবে সব connector task থেমে যেতে পারে — নতুন Connect version-এ incremental rebalance (KIP-415-এর মতো ধারণা) এই সমস্যা কমায়।
Debezium ভেতরে: PostgreSQL Logical Decoding → Replication Slot → pgoutput → Kafka
পুরো যাত্রা
- PostgreSQL-এ একটা replication slot তৈরি হয় (
CREATE_REPLICATION_SLOT) — এটা PostgreSQL-কে বলে 'এই slot না পড়া পর্যন্ত সংশ্লিষ্ট WAL মুছো না', ঠিক Kafka consumer group-এর committed offset-এর মতো ধারণা (Module 1-এই এই তুলনা করা হয়েছিল)। - Logical decoding — PostgreSQL WAL-এর raw binary পরিবর্তনকে একটা লজিক্যাল, row-level ফরম্যাটে রূপান্তর করে একটা output plugin দিয়ে —
pgoutput(PostgreSQL native, PostgreSQL 10+) সবচেয়ে সাধারণ পছন্দ। - Debezium এই স্ট্রিম consume করে, প্রতিটা row change (INSERT/UPDATE/DELETE)-কে একটা structured event-এ রূপান্তর করে — before/after state, metadata (transaction id, LSN, timestamp) সহ।
- প্রতিটা টেবিলের জন্য একটা Kafka topic-এ produce করে (সাধারণত primary key-কে message key হিসেবে ব্যবহার করে — Module 2-এর key-based partitioning নীতি এখানে প্রয়োগ হয়, একটা row-এর সব পরিবর্তন একই partition-এ, ordering বজায় থাকে)।
রিয়েল-লাইফ উদাহরণ
আপনার একটা orders টেবিলে একটা UPDATE orders SET status='SHIPPED' WHERE id=123 চালালে — এটা WAL-এ লেখা হয়, pgoutput সেটাকে decode করে একটা logical change event বানায়, Debezium সেটা তুলে নিয়ে {"before": {...status: "PAID"...}, "after": {...status: "SHIPPED"...}} টাইপ একটা JSON/Avro event বানিয়ে dbserver1.public.orders টপিকে key=123 দিয়ে পাঠায়।
ড্রব্যাক
- Logical decoding CPU/IO overhead যোগ করে PostgreSQL primary-তে (decode প্রসেস চলে সেখানেই) — high-write-volume ডেটাবেজে এই overhead capacity planning-এ হিসাব করা দরকার।
- Replication slot lag (পরের bullet) একটা গুরুতর অপারেশনাল রিস্ক যা মনিটর না করলে ডেটাবেজ ডিস্ক ভরিয়ে দিতে পারে।
Replication Slot Lag — কেন এটা ডিস্ক ভরে দিতে পারে
সমস্যা
Replication slot PostgreSQL-কে বলে 'যে WAL এখনো slot দিয়ে consume হয়নি সেটা মুছো না' — এটা Kafka-র Module 6-এর retention/log-start-offset ধারণার সাথে সরাসরি সমতুল্য, কিন্তু একটা গুরুত্বপূর্ণ পার্থক্যসহ: Kafka-তে retention policy স্বয়ংক্রিয়ভাবে পুরনো ডেটা মুছে ফেলে সময়/সাইজ অনুযায়ী, কিন্তু PostgreSQL replication slot-এ এমন কোনো স্বয়ংক্রিয় সীমা নেই ডিফল্টে — Debezium (slot-এর consumer) যদি বন্ধ থাকে বা পিছিয়ে যায়, WAL অনির্দিষ্টকাল জমতে থাকে, ডিস্ক পূর্ণ হওয়া পর্যন্ত।
কীভাবে ঘটে বাস্তবে
Debezium connector crash করে বা অনেকক্ষণ ধরে ডাউন থাকে (deployment ভুল, Kafka Connect cluster সমস্যা), কিন্তু replication slot তখনও PostgreSQL-এ registered আছে — PostgreSQL ধরেই নেয় 'consumer ফিরে আসবে', WAL ধরে রাখতেই থাকে। কয়েক ঘণ্টা/দিনের মধ্যে ডিস্ক পূর্ণ হয়ে যেতে পারে high-write ডেটাবেজে — এবং ডিস্ক পূর্ণ হয়ে গেলে PostgreSQL primary নিজেই write ব্যর্থ করতে শুরু করে — একটা downstream CDC সমস্যা upstream primary ডেটাবেজকেই ডাউন করে দিতে পারে।
রিয়েল-লাইফ উদাহরণ
এটা ঠিক Module 4-এর consumer lag সমস্যার সমতুল্য, কিন্তু consequence অনেক বেশি গুরুতর — Kafka-তে একটা lagging consumer শুধু তার নিজের ডেটা মিস করার ঝুঁকিতে পড়ে (retention expire হলে), কিন্তু PostgreSQL-এ একটা lagging replication slot পুরো primary ডেটাবেজকেই বিপদে ফেলতে পারে ডিস্ক-ফুল সিনারিওতে।
ড্রব্যাক / প্রতিরোধ
pg_replication_slotsভিউ মনিটর করা আবশ্যক (restart_lsnকতদূর পিছিয়ে) — alert threshold সেট করা জরুরি।- একটা connector permanently বাদ দিতে চাইলে অবশ্যই
pg_drop_replication_slot()দিয়ে slot manually মুছে ফেলতে হবে — Debezium connector শুধু delete করলে slot নিজে থেকে মুছে না, একটা orphaned slot চিরকাল WAL জমাতে থাকবে।
Outbox Pattern — Dual-write সমস্যার সঠিক সমাধান
Dual-write সমস্যা
একটা সার্ভিস যদি একটা অপারেশনে ডেটাবেজে লেখে এবং সরাসরি Kafka-তে একটা ইভেন্ট produce করে (দুইটা আলাদা সিস্টেম, দুইটা আলাদা write) — এই দুইটা atomic না। ডেটাবেজ write সফল হয়ে Kafka produce ব্যর্থ হতে পারে (বা উল্টো) — একটা partial-failure state, যেখানে ডেটাবেজ আর Kafka-র মধ্যে ডেটা অসামঞ্জস্যপূর্ণ হয়ে যায়।
সমাধান: Outbox Pattern
সার্ভিস Kafka-তে সরাসরি produce করে না। বরং, একই ডেটাবেজ transaction-এ (যেটা মূল business ডেটা আপডেট করে) একটা outbox টেবিলে একটা row insert করে (event payload সহ)। যেহেতু এটা একই ডেটাবেজ transaction, এই দুইটা write atomic — নিশ্চিতভাবে দুটোই হবে বা কোনোটাই না। তারপর Debezium (CDC) সেই outbox টেবিলটাকে monitor করে, নতুন row দেখলেই সেটা Kafka-তে produce করে — CDC নিজেই এখানে reliable event publishing mechanism হয়ে যায়।
কেন এটাই 'সঠিক' সমাধান
এটা মূলত Module 9-এর Consume-Process-Produce/Transaction সমস্যার একটা ভিন্ন রূপ — কিন্তু এখানে atomicity Kafka Transaction দিয়ে না, বরং ডেটাবেজের নিজস্ব ACID transaction দিয়ে অর্জিত হয়, আর CDC (Debezium) সেই atomic write-কে reliably Kafka-তে propagate করে। ডেটাবেজ transaction ইতিমধ্যেই atomic guarantee দেয় — Outbox pattern সেটাকেই কাজে লাগায়, নতুন কোনো distributed transaction মেকানিজম আবিষ্কার না করে।
রিয়েল-লাইফ উদাহরণ
আপনার order service একটা অর্ডার তৈরি করার সময় একই DB transaction-এ orders টেবিলে row insert করে এবং outbox টেবিলে একটা {event_type: "OrderCreated", payload: {...}} row insert করে। Debezium outbox টেবিল থেকে CDC event তুলে Kafka-তে পাঠায়। যদি order insert ব্যর্থ হয়, পুরো transaction rollback হয় (outbox row-ও তৈরি হয় না) — কখনো 'order তৈরি হয়েছে কিন্তু ইভেন্ট পাঠানো হয়নি' এই অবস্থায় পড়বে না।
ড্রব্যাক
- এক্সট্রা টেবিল এবং CDC infrastructure (Debezium, Kafka Connect) দরকার — সহজ সিস্টেমের জন্য এটা ওভারহেড হতে পারে।
- Outbox টেবিল নিজেই periodically clean করা দরকার (পুরনো processed row জমতে থাকবে নাহলে) — একটা এক্সট্রা মেইনটেন্যান্স দায়িত্ব।
Snapshot Modes, Incremental Snapshot (DDD-3)
কেন Snapshot দরকার
Debezium যখন প্রথমবার একটা connector শুরু করে, replication slot শুধু এখন থেকে ভবিষ্যতের পরিবর্তন capture করবে — কিন্তু টেবিলে ইতিমধ্যেই থাকা বিদ্যমান ডেটার কী হবে? এর জন্যই snapshot — connector শুরুতে পুরো টেবিল (বা নির্দিষ্ট অংশ) স্ক্যান করে সেই বর্তমান অবস্থা Kafka-তে ইনিশিয়াল ইভেন্ট হিসেবে পাঠায়, তারপর replication slot দিয়ে ongoing পরিবর্তন capture করা শুরু হয়।
সমস্যা পুরনো Snapshot মেকানিজমে
ঐতিহ্যগত (blocking) snapshot mode-এ, snapshot চলাকালীন connector একটা lock ধরে রাখতো টেবিলে (consistency নিশ্চিত করতে) — বড় টেবিলে (কোটি কোটি row) এই lock দীর্ঘসময় ধরে রাখলে production ট্রাফিক ব্লক হয়ে যেতে পারতো, একটা বিপজ্জনক অপারেশন।
Incremental Snapshot (DDD-3) সমাধান
Debezium-এর Incremental Snapshot ফিচার টেবিলকে ছোট ছোট chunk-এ ভাগ করে স্ক্যান করে (primary key range অনুযায়ী), কোনো lock না ধরেই, আর একইসাথে ongoing streaming changes-ও চলতে থাকে। একটা watermark মেকানিজম দিয়ে snapshot chunk আর concurrent streaming change-এর মধ্যে সঠিক ordering/deduplication নিশ্চিত করা হয়।
রিয়েল-লাইফ উদাহরণ
আপনার একটা ১০ কোটি row-এর orders টেবিলে নতুন Debezium connector চালু করতে চান প্রোডাকশনে বিদ্যমান ডেটা replicate করার জন্য — blocking snapshot এখানে ঘণ্টার পর ঘণ্টা lock ধরে রাখতে পারতো (business-critical outage-এর ঝুঁকি)। Incremental snapshot দিয়ে এই একই কাজ ব্যাকগ্রাউন্ডে, লক ছাড়া, live traffic-এর পাশাপাশি চলতে পারে — connector নিজে থেকে pause/resume-ও করা যায় দরকার হলে।
ড্রব্যাক
- Incremental snapshot সময় বেশি লাগতে পারে blocking snapshot-এর তুলনায় (chunk-ভিত্তিক, careful watermark coordination-এর কারণে) — বড় টেবিলে সম্পূর্ণ snapshot শেষ হতে দিন লাগতে পারে।
- Watermark মেকানিজম নিজেই একটা জটিলতা layer — সঠিকভাবে বোঝা না থাকলে debugging কঠিন হতে পারে যদি snapshot আর streaming ডেটার মধ্যে অসঙ্গতি দেখা দেয়।
ল্যাব: Sink Connector → Elasticsearch
লক্ষ্য
একটা source (Debezium) থেকে একটা sink (Elasticsearch)-এ পুরো CDC pipeline প্রান্ত থেকে প্রান্ত চালিয়ে দেখা — আপনার বর্তমান স্ট্যাকের সাথে সরাসরি প্রাসঙ্গিক।
ধাপ ১ — Elasticsearch Sink Connector কনফিগার করুন
{
"name": "es-sink-orders",
"config": {
"connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector",
"tasks.max": "1",
"topics": "dbserver1.public.orders",
"connection.url": "http://elasticsearch:9200",
"type.name": "_doc",
"key.ignore": "false",
"schema.ignore": "true"
}
}
curl -X POST http://localhost:8083/connectors -H "Content-Type: application/json" -d @es-sink-orders.json
ধাপ ২ — PostgreSQL-এ একটা row UPDATE করুন, Elasticsearch-এ যাচাই করুন
psql -c "UPDATE orders SET status='SHIPPED' WHERE id=123"
curl http://localhost:9200/dbserver1.public.orders/_doc/123
দেখুন পরিবর্তন কয়েক সেকেন্ডের মধ্যে Elasticsearch-এ propagate হয়ে গেছে কিনা — পুরো WAL → Debezium → Kafka → Sink Connector → Elasticsearch চেইন কাজ করছে।
চ্যালেঞ্জ
Elasticsearch connector সাময়িকভাবে বন্ধ রেখে (curl -X PUT .../pause) আরও কিছু UPDATE চালান, তারপর connector আবার resume করুন — দেখুন সব মিসড ইভেন্ট catch-up হয়ে যায় (offset storage topic-এর কারণে কিছু হারায় না)।
ইন্টারভিউ প্রশ্ন — Module 11
প্র.১ — Standalone আর Distributed Kafka Connect mode-এর পার্থক্য কী? উত্তর: Standalone একটা single worker প্রসেস — সহজ কিন্তু fault tolerance নেই। Distributed mode একাধিক worker cluster গঠন করে, internal Kafka topic (config/offset/status storage) দিয়ে coordination করে, worker fail করলে task অন্য worker-এ পুনরায় শুরু হয়।
প্র.২ — Replication slot lag কেন Kafka consumer lag-এর চেয়ে বেশি বিপজ্জনক হতে পারে? উত্তর: Kafka-তে retention policy স্বয়ংক্রিয়ভাবে পুরনো ডেটা মুছে ফেলে, একটা lagging consumer শুধু নিজের ডেটা মিস করার ঝুঁকিতে থাকে। PostgreSQL replication slot-এ এমন স্বয়ংক্রিয় সীমা নেই — একটা lagging slot অনির্দিষ্টকাল WAL জমাতে থাকে, ডিস্ক পূর্ণ হয়ে গেলে পুরো primary ডেটাবেজের write ব্যর্থ হতে শুরু করতে পারে।
প্র.৩ — Outbox pattern dual-write সমস্যা কীভাবে সমাধান করে? উত্তর: সার্ভিস সরাসরি Kafka-তে produce না করে একই DB transaction-এ business ডেটা আর outbox টেবিলে event insert করে (ডেটাবেজের নিজস্ব ACID guarantee দিয়ে atomicity)। Debezium (CDC) outbox টেবিল monitor করে reliably সেই event Kafka-তে propagate করে।
প্র.৪ — Incremental Snapshot blocking snapshot-এর চেয়ে ভালো কেন বড় টেবিলে? উত্তর: Blocking snapshot টেবিলে lock ধরে রাখে পুরো স্ক্যান শেষ না হওয়া পর্যন্ত, বড় টেবিলে production ট্রাফিক ব্লক করে দিতে পারে। Incremental snapshot chunk-ভিত্তিক, কোনো lock ছাড়াই কাজ করে, streaming change-এর সাথে সমান্তরালে চলতে পারে।
প্র.৫ — একটা Debezium connector বাদ দেওয়ার (delete) পর কী গুরুত্বপূর্ণ ম্যানুয়াল ধাপ ভুলে গেলে ডিস্ক সমস্যা হতে পারে?
উত্তর: pg_drop_replication_slot() দিয়ে replication slot ম্যানুয়ালি না মুছলে, connector delete হয়ে গেলেও slot PostgreSQL-এ থেকে যায় এবং orphaned অবস্থায় WAL অনির্দিষ্টকাল জমাতে থাকে — ডিস্ক ভরে যাওয়ার ঝুঁকি তৈরি করে।
প্র.৬ — Single Message Transform (SMT) কী কাজে লাগে, আর এর সীমাবদ্ধতা কী? উত্তর: SMT প্রতিটা মেসেজে ছোট, stateless পরিবর্তন করে (field mask, routing, timestamp যোগ করা) কোনো কোড ছাড়াই কনফিগ দিয়ে। সীমাবদ্ধতা: cross-message logic (aggregation, join, windowing) SMT দিয়ে সম্ভব না — সেটার জন্য Kafka Streams/ksqlDB দরকার।
কর্নার কেস — Module 11
pg_replication_slotsমনিটর না করা সবচেয়ে বেশি ঘটা প্রোডাকশন ভুল — একটা connector সপ্তাহখানেক silently ব্যর্থ থাকলে, কেউ খেয়াল না করলে ডিস্ক ধীরে ধীরে ভরতে থাকে, একদিন হঠাৎ পুরো ডেটাবেজ write-ব্যর্থ অবস্থায় চলে যেতে পারে কোনো পূর্ব সতর্কতা ছাড়াই যদি alert সেট করা না থাকে।- Snapshot চলাকালীন যদি connector crash করে (incremental না, পুরনো blocking mode-এ), resume করলে পুরো snapshot প্রথম থেকে আবার শুরু করতে হতে পারে — একটা বড় টেবিলে এটা উল্লেখযোগ্য সময়/রিসোর্স অপচয়।
- Outbox টেবিল থেকে পুরনো processed row পরিষ্কার না করলে, টেবিল ক্রমাগত বড় হতে থাকবে, index performance কমে যেতে পারে সময়ের সাথে — একটা periodic cleanup job (বা compacted হিসেবে design) প্রয়োজন।
- Distributed Connect cluster-এ rebalance চলাকালীন (পুরনো eager rebalance protocol-এ) সব connector task সাময়িকভাবে থেমে যেতে পারে, ঠিক Module 4-এর consumer group rebalance storm-এর মতোই সমস্যা — এখানেও worker যোগ/বিয়োগের frequency কমানো (static-এর মতো স্থিতিশীল deployment প্যাটার্ন) সাহায্য করে।
- Sink connector (Elasticsearch) ডাউন থাকলে source connector (Debezium) নিজে থামে না — Kafka topic-এ ডেটা জমতে থাকে (retention অনুযায়ী), sink connector ফিরে এলে backlog catch-up করে — কিন্তু retention expire হয়ে গেলে (Module 6) সেই gap-এর ডেটা চিরতরে হারিয়ে যাবে sink-এর জন্য, একটা মনিটরিং blind spot যদি sink lag আলাদাভাবে ট্র্যাক না করা হয়।
MODULE 12: Stream Processing
Kafka Connect ডেটা সরায়, কিন্তু কখনো ডেটা transform/aggregate/join করতে হয় real-time-এ — এখানেই Kafka Streams (এবং ksqlDB) কাজে লাগে। এই মডিউলে KStream/KTable-এর duality, state store, windowing, আর watermark-এর ধারণা শেখা হবে — Kafka Streams vs Flink vs ksqlDB কখন কোনটা বেছে নেবেন সেটাসহ।
Kafka Streams: KStream, KTable, GlobalKTable
তিনটা মূল abstraction
- KStream — একটা unbounded স্ট্রিম of records, প্রতিটা রেকর্ড একটা independent event (insert-only সিমেন্টিক্স)। যেমন 'প্রতিটা ক্লিক ইভেন্ট'।
- KTable — একটা changelog stream যেটা একটা table হিসেবে interpret হয় — প্রতিটা নতুন রেকর্ড একটা key-এর আগের value update/overwrite করে (Module 6-এর compacted topic-এর ধারণার সাথে সরাসরি সম্পর্কিত)। যেমন 'প্রতিটা ইউজারের বর্তমান balance'।
- GlobalKTable — KTable-এর মতোই, কিন্তু প্রতিটা application instance-এ পুরো টেবিলের একটা সম্পূর্ণ কপি থাকে (partition-বণ্টিত না) — ছোট, reference-টাইপ ডেটার জন্য উপযুক্ত (যেমন একটা 'country code → country name' lookup টেবিল) যেখানে join করার সময় co-partitioning-এর প্রয়োজন নেই।
Stream-Table Duality
একটা KStream aggregate (যেমন group-by + count) করলে একটা KTable তৈরি হয় (প্রতিটা key-এর running total)। একটা KTable-এর changelog আবার একটা KStream হিসেবে পড়া যায়। এই duality-ই Kafka Streams-এর সবচেয়ে গভীর ধারণাগত ভিত্তি — log আর table আসলে একই ডেটার দুইটা ভিন্ন দৃষ্টিভঙ্গি (Module 1-এর commit log ধারণার সরাসরি সম্প্রসারণ)।
রিয়েল-লাইফ উদাহরণ
আপনার order.placed টপিক একটা KStream — প্রতিটা অর্ডার একটা আলাদা ইভেন্ট। কিন্তু সেই একই ডেটা থেকে groupBy(customer_id).count() করলে একটা KTable তৈরি হয় — 'প্রতিটা কাস্টমারের এখন পর্যন্ত মোট কতগুলো অর্ডার' — এটা একটা live-updating table।
ড্রব্যাক
- KTable-এর সাইজ বাউন্ডেড না — যদি key স্পেস (যেমন unique user সংখ্যা) খুব বড় হয়, KTable-এর underlying state store (পরের bullet) বিশাল হয়ে যেতে পারে, মেমরি/ডিস্ক প্ল্যানিং জরুরি।
- GlobalKTable প্রতিটা instance-এ পুরো ডেটা কপি করে — বড় reference টেবিলে এটা মেমরি অপচয় করতে পারে যদি ভুলভাবে ব্যবহার করা হয় (ছোট lookup টেবিলের জন্যই এটা ডিজাইন করা)।
State Store (RocksDB), Changelog Topic, Standby Replica
কীভাবে KTable-এর state ধরে রাখা হয়
Kafka Streams application প্রতিটা instance-এ স্থানীয়ভাবে RocksDB (একটা embedded key-value store, disk-ভিত্তিক) ব্যবহার করে KTable/aggregation-এর বর্তমান state রাখতে — দ্রুত local lookup-এর জন্য, প্রতিবার Kafka-তে query না করে।
Changelog Topic — Fault Tolerance-এর চাবিকাঠি
RocksDB local, তাই instance crash করলে সেই state হারিয়ে যেতে পারতো। এই সমস্যা সমাধানে, প্রতিটা state store-এর পরিবর্তন একটা compacted changelog topic-এ (Module 6-এর ধারণা এখানে সরাসরি প্রয়োগ) ব্যাকআপ করা হয় — instance restart হলে, RocksDB আবার তৈরি করা হয় changelog topic থেকে replay করে।
Standby Replica
num.standby.replicas কনফিগ করলে, অন্য instance-এ একই state store-এর একটা hot standby কপি maintain করা হয় (changelog থেকে ক্রমাগত আপডেট হতে থাকে) — একটা instance fail করলে, standby-যুক্ত instance অনেক দ্রুত takeover করতে পারে (RocksDB থেকে শুরু থেকে rebuild করার বদলে, যেটা বড় state-এ সময়সাপেক্ষ)।
রিয়েল-লাইফ উদাহরণ
একটা real-time fraud-detection Streams application যেটা প্রতিটা ইউজারের গত ৫ মিনিটের transaction pattern track করে (KTable state) — যদি সেই instance crash করে, standby replica না থাকলে নতুন instance-কে পুরো state আবার rebuild করতে হবে changelog থেকে (মিনিট লাগতে পারে বড় state-এ), এই সময়টাতে fraud detection কার্যত অন্ধ থাকবে। Standby replica দিয়ে এই gap সেকেন্ডে নেমে আসে।
ড্রব্যাক
- Standby replica এক্সট্রা storage এবং network bandwidth খরচ করে (changelog প্রতিটা standby-তেও replicate হয়)।
- Changelog topic নিজেই একটা এক্সট্রা Kafka topic, storage খরচ বাড়ায় — বড় state store মানে বড় changelog।
Windowing: Tumbling, Hopping, Sliding, Session
চার ধরনের Window
- Tumbling — নির্দিষ্ট আকারের, non-overlapping window (যেমন প্রতি ৫ মিনিটে একটা নতুন window, আগের window শেষ হলেই পরেরটা শুরু)। সবচেয়ে সহজ — প্রতিটা ইভেন্ট ঠিক একটা window-এর অন্তর্গত।
- Hopping — নির্দিষ্ট আকারের কিন্তু overlapping window (যেমন ৫ মিনিট আকারের window, প্রতি ১ মিনিটে নতুন window শুরু হয়) — একটা ইভেন্ট একাধিক window-এ পড়তে পারে। Moving average-টাইপ analytics-এ উপযোগী।
- Sliding — দুইটা ইভেন্টের মধ্যে সময়ের পার্থক্য একটা নির্দিষ্ট সীমার মধ্যে থাকলে একই window-এ গণনা হয় — event-driven, fixed-size না।
- Session — একটা নির্দিষ্ট 'inactivity gap'-এর ভিত্তিতে window সংজ্ঞায়িত হয় (যেমন একজন ইউজারের activity যদি ৩০ মিনিট বিরতি ছাড়া চলতে থাকে, সেটা একটা session, নির্দিষ্ট duration না)।
রিয়েল-লাইফ উদাহরণ
- Tumbling: প্রতি ঘণ্টায় মোট অর্ডার সংখ্যা রিপোর্ট (clean, non-overlapping hourly bucket)।
- Hopping: রিয়েল-টাইম ড্যাশবোর্ডে 'গত ৫ মিনিটের rolling average request rate', প্রতি ১০ সেকেন্ডে আপডেট হয় — smooth, ক্রমাগত updating metric।
- Session: একটা ইউজারের ওয়েবসাইট 'visit session' চিহ্নিত করা — activity-র মধ্যে ৩০ মিনিটের বেশি ফাঁক হলে নতুন session শুরু, fixed duration নেই কারণ ব্যবহারকারীর আচরণ variable।
ড্রব্যাক
- Hopping window-এ একই ইভেন্ট একাধিক window-এ গণনা হয় — মেমরি/CPU খরচ বেশি Tumbling-এর তুলনায়, overlap যত বেশি তত বেশি খরচ।
- Session window unbounded-এর মতো আচরণ করতে পারে যদি inactivity gap timeout ভুলভাবে সেট করা হয় (খুব বড় gap মানে session কখনো বন্ধ হয় না, state ক্রমাগত বাড়তে থাকে)।
Watermark আর Late-arriving Data
সমস্যা
Distributed সিস্টেমে ইভেন্ট সবসময় event-time অনুযায়ী ক্রমানুযায়ী আসে না — network delay, producer buffering, বা offline device sync-এর কারণে একটা ইভেন্ট তার প্রকৃত ঘটার সময়ের অনেক পরে Kafka-তে পৌঁছাতে পারে (late-arriving data)। একটা window ইতিমধ্যে 'বন্ধ' (finalized, ফলাফল emit) হয়ে যাওয়ার পর যদি সেই window-এর জন্য একটা দেরিতে-আসা ইভেন্ট আসে, কী করবেন?
Watermark কীভাবে সিদ্ধান্ত নেয়
Watermark একটা heuristic threshold — 'এই সময়ের আগের সব ইভেন্ট এসে গেছে বলে ধরে নিচ্ছি' (grace period কনফিগারযোগ্য)। Grace period শেষ না হওয়া পর্যন্ত window ফলাফল চূড়ান্ত করা হয় না, দেরিতে আসা ইভেন্ট এখনো গ্রহণ করা হয় এবং window ফলাফল আপডেট হয়। Grace period শেষ হয়ে গেলে window 'বন্ধ' হয়ে যায় — তারপর আসা ইভেন্ট বাদ দেওয়া হয় (অথবা একটা আলাদা 'late records' topic-এ পাঠানো যায় কনফিগার করলে)।
রিয়েল-লাইফ উদাহরণ
একটা mobile app যেখানে ইউজার অফলাইনে কিছু ইভেন্ট জেনারেট করে, পরে অনলাইনে এলে সেগুলো sync হয় — এই ইভেন্টগুলোর event-time (আসলে কখন ঘটেছিল) আর processing-time (Kafka-তে কখন পৌঁছালো) অনেক আলাদা হতে পারে। একটা ৫ মিনিটের tumbling window-এ ১০ মিনিটের grace period রাখলে, ৫ মিনিট পর্যন্ত দেরিতে আসা ইভেন্টও সঠিক window-এ গণনা করা সম্ভব হয়, তার বেশি দেরি হলে সেটা miss হয়ে যাবে।
ড্রব্যাক
- Grace period বাড়ালে window ফলাফল চূড়ান্ত (এবং downstream-এ emit) হতে বেশি দেরি হয় — real-time-এর সাথে correctness-এর ট্রেড-অফ।
- Grace period-এর পরেও আসা ইভেন্ট নীরবে বাদ যেতে পারে যদি late-records handling সেট আপ করা না থাকে — ডেটা লস, কিন্তু কোনো এরর ছাড়াই, মনিটরিং না থাকলে অলক্ষিত থেকে যায়।
Kafka Streams vs Flink vs ksqlDB — কখন কোনটা
তুলনা
| Kafka Streams | Apache Flink | ksqlDB | |
|---|---|---|---|
| ডিপ্লয়মেন্ট | নিজের অ্যাপ্লিকেশনের ভেতরে লাইব্রেরি (কোনো আলাদা cluster লাগে না) | আলাদা cluster (JobManager/TaskManager) | আলাদা সার্ভার (Kafka-র ওপর) |
| ইন্টারফেস | Java/Scala কোড (DSL বা Processor API) | Java/Scala/Python কোড, বা SQL (Flink SQL) | শুধু SQL |
| শক্তি | Kafka-নেটিভ, deploy সহজ (শুধু আপনার অ্যাপ), microservice-friendly | সবচেয়ে বেশি feature-rich, বহু সোর্স/সিঙ্ক সাপোর্ট, complex event processing-এ শক্তিশালী | দ্রুত prototyping, non-programmer-বান্ধব, ops কম |
| সীমাবদ্ধতা | শুধু Kafka source/sink (নেটিভভাবে), জটিল multi-stream join কিছুটা কঠিন | আলাদা cluster অপারেট করার জটিলতা | SQL-এর expressiveness সীমা, খুব জটিল custom logic কঠিন |
সিদ্ধান্ত নেওয়ার নীতি
- Kafka Streams — যখন আপনার ইতিমধ্যে একটা microservice আছে এবং সেটাতেই stream processing embed করতে চান, আলাদা infrastructure ছাড়াই।
- Flink — যখন জটিল, বহু-সোর্স (শুধু Kafka না, অন্য সিস্টেমও), বড় স্কেলের event processing দরকার, আর একটা ডেডিকেটেড platform টিম আছে ম্যানেজ করার জন্য।
- ksqlDB — যখন দ্রুত একটা stream transformation/aggregation দরকার SQL দিয়ে, প্রোডাকশন কোড না লিখে (analyst/data engineer-দের জন্য উপযোগী)।
রিয়েল-লাইফ উদাহরণ
আপনার notification service-এ যদি একটা simple windowed aggregation দরকার হয় ('গত ৫ মিনিটে কতগুলো নোটিফিকেশন পাঠানো হয়েছে প্রতি ইউজারে', rate-limiting-এর জন্য), Kafka Streams সবচেয়ে যুক্তিসঙ্গত — এটা আপনার existing FastAPI/Django সার্ভিসের পাশে একটা লাইব্রেরি হিসেবে যোগ করা যায়। কিন্তু যদি একটা বড় ডেটা টিম একাধিক সোর্স (Kafka + S3 + database) থেকে জটিল, বহু-ঘণ্টার windowed analytics করতে চায়, Flink বেশি উপযুক্ত।
ড্রব্যাক
- Kafka Streams দিয়ে non-JVM ভাষায় (Python) stream processing করা সহজ না — সরাসরি সাপোর্ট নেই (kafka-python দিয়ে নিজে থেকে অনেক কিছু implement করতে হবে, অথবা ksqlDB/Flink বেছে নিতে হবে যদি JVM এড়াতে চান)।
ল্যাব: Windowed Aggregation + State Store Inspect
সেটআপ — একটা সাধারণ Kafka Streams টপোলজি (Java/Kotlin, বা Python-এ faust লাইব্রেরি দিয়ে conceptually সমতুল্য)
StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> orders = builder.stream("order.placed");
KTable<Windowed<String>, Long> orderCounts = orders
.groupBy((key, value) -> extractCustomerId(value))
.windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(5)))
.count();
orderCounts.toStream().foreach((windowedKey, count) ->
System.out.println(windowedKey.key() + " @ " + windowedKey.window() + " = " + count));
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();
ধাপ ১ — চালান, order.placed-এ কয়েকটা customer_id দিয়ে মেসেজ পাঠান
দেখুন প্রতি ৫ মিনিট window-এ প্রতিটা customer-এর count কীভাবে আপডেট হয় কনসোলে।
ধাপ ২ — Changelog topic দেখুন
kafka-console-consumer.sh --topic <application-id>-KSTREAM-AGGREGATE-STATE-STORE-changelog \
--from-beginning --bootstrap-server localhost:9092
দেখুন প্রতিটা count update এখানে একটা compacted রেকর্ড হিসেবে যাচ্ছে (Module 6-এর ধারণা বাস্তবে)।
চ্যালেঞ্জ
Application মাঝপথে kill করে আবার চালু করুন — দেখুন state store changelog থেকে rebuild হয়ে আগের counts থেকেই continue করে, শূন্য থেকে শুরু করে না।
ইন্টারভিউ প্রশ্ন — Module 12
প্র.১ — KStream আর KTable-এর মৌলিক পার্থক্য কী? উত্তর: KStream প্রতিটা রেকর্ডকে independent ইভেন্ট হিসেবে treat করে (insert-only)। KTable প্রতিটা নতুন রেকর্ডকে একটা key-এর জন্য update হিসেবে treat করে (Module 6-এর compacted topic-এর মতো) — শুধু latest state ধরে রাখে।
প্র.২ — Changelog topic কেন দরকার Kafka Streams-এ? উত্তর: State store (RocksDB) local, instance crash করলে হারিয়ে যেতে পারে। প্রতিটা state পরিবর্তন একটা compacted changelog topic-এ ব্যাকআপ থাকে, instance restart-এ সেখান থেকে replay করে state rebuild করা হয়।
প্র.৩ — Standby replica কী সমস্যা সমাধান করে? উত্তর: শুধু changelog থেকে state rebuild করতে বড় state-এ অনেক সময় লাগতে পারে। Standby replica অন্য instance-এ একটা hot copy maintain রাখে, failover-এর সময় দ্রুত takeover সম্ভব হয়, rebuild-এর অপেক্ষা ছাড়াই।
প্র.৪ — Watermark/grace period কী সমস্যা সমাধান করে? উত্তর: Late-arriving data (network delay, offline sync-এর কারণে event-time আর processing-time-এর ফারাক) সঠিক window-এ গণনা করার সুযোগ দেয়, window ফলাফল সাথে সাথে finalize না করে একটা নির্দিষ্ট গ্রেস পিরিয়ড পর্যন্ত অপেক্ষা করে।
প্র.৫ — Tumbling আর Hopping window-এর পার্থক্য কী, একটা উদাহরণ সহ? উত্তর: Tumbling non-overlapping, fixed-size window (প্রতি ৫ মিনিটে একটা নতুন window)। Hopping overlapping window (৫ মিনিট সাইজ, প্রতি ১ মিনিটে নতুন window শুরু) — একটা ইভেন্ট একাধিক window-এ পড়তে পারে, যেমন rolling average metric-এ ব্যবহৃত হয়।
প্র.৬ — একটা টিমের দরকার একটা simple stream aggregation, কোনো নতুন infrastructure চালু না করে — Kafka Streams, Flink, নাকি ksqlDB? উত্তর: Kafka Streams — এটা একটা লাইব্রেরি, existing অ্যাপ্লিকেশনের ভেতরেই embed করা যায়, আলাদা cluster ডিপ্লয় করার দরকার নেই। Flink/ksqlDB দুটোই আলাদা infrastructure/server চালানো দাবি করে।
কর্নার কেস — Module 12
- State store disk-এ পূর্ণ হয়ে গেলে (unbounded key space, যেমন প্রতিটা unique session id একটা নতুন key), Kafka Streams application crash করতে পারে আচমকা — key space-এর বৃদ্ধির হার আগে থেকে হিসাব করা জরুরি capacity planning-এ।
num.standby.replicasনা রেখে বড় state store-সহ একটা instance হারানো মানে rebuild করতে যথেষ্ট সময় লাগতে পারে (GB-স্কেল RocksDB rebuild), এই সময় সেই partition-এর processing পুরোপুরি থেমে থাকে — high-availability দরকার হলে standby replica অপরিহার্য।- Grace period শেষ হওয়ার পর আসা late data নীরবে বাদ পড়ে যায় যদি explicit late-record handling (আলাদা topic-এ route করা) সেট আপ না করা থাকে — মনিটরিং-এ 'কতগুলো রেকর্ড দেরিতে বাদ গেছে' এই মেট্রিক ট্র্যাক না করলে ডেটা loss পুরোপুরি অলক্ষিত থেকে যায়।
- GlobalKTable ব্যবহার করে একটা বড় (কোটি-row) reference টেবিল লোড করার চেষ্টা করলে, প্রতিটা application instance-এ পুরো টেবিল কপি হবে — মেমরি চাপ বহুগুণ বেড়ে যেতে পারে instance সংখ্যার সমানুপাতিকভাবে, ছোট lookup টেবিলের জন্যই এটা ডিজাইন করা মনে রাখা জরুরি।
- একই application-id দুইবার ভুলবশত ভিন্ন topology-সহ চালালে (deployment ভুল), changelog topic-এর সাথে নতুন topology-র schema/state অসামঞ্জস্যপূর্ণ হতে পারে — অদ্ভুত, অস্পষ্ট রানটাইম এরর দেখা দিতে পারে যা সরাসরি root cause নির্দেশ করে না।
MODULE 13: Operations, Security আর Tuning
থিওরি থেকে প্রোডাকশনে পার্থক্য তৈরি হয় এই মডিউলে — capacity planning, broker/JVM tuning, monitoring metrics, partition reassignment, security (TLS/SASL/ACL), আর multi-DC replication। এই মডিউল শেষে একটা Kafka cluster দিনের পর দিন নির্ভরযোগ্যভাবে চালানোর জন্য যা যা জানা দরকার তার একটা কাঠামো তৈরি হবে।
Capacity Planning: Partition Count, Throughput, Retention, Disk Sizing-এর ফর্মুলা
মূল হিসাব
- Partition count (Module 2-এ বিস্তারিত) —
max(desired_throughput / per_partition_producer_cap, desired_consumer_parallelism)। - Disk sizing —
daily_ingest_rate × retention_days × replication_factor। যেমন প্রতিদিন ১০০GB নতুন ডেটা, ৭ দিন retention, replication factor ৩ — মোট প্রয়োজন100 × 7 × 3 = 2100GBcluster-wide, broker সংখ্যা দিয়ে ভাগ করে per-broker disk আকার। - Network bandwidth — প্রতিটা মেসেজ producer থেকে leader-এ একবার, তারপর leader থেকে প্রতিটা follower-এ (replication.factor-1 বার) — মানে effective write bandwidth =
ingest_rate × replication_factor। এটা প্রায়ই ভুলে যাওয়া হিসাব। - Memory — Module 5-এর page cache নীতি অনুযায়ী, RAM যত বেশি hot ডেটা কভার করতে পারবে (সাম্প্রতিক কয়েক ঘণ্টা/দিনের ডেটা RAM-এ ফিট করা উচিত ভালো read performance-এর জন্য) তত ভালো — 'RAM ≥ সাম্প্রতিক active dataset' একটা ভালো starting rule।
রিয়েল-লাইফ উদাহরণ
আপনার notification pipeline প্রতিদিন ৫০GB ইভেন্ট জেনারেট করলে, ১৪ দিন retention আর replication factor ৩ সহ — 50 × 14 × 3 = 2100GB প্রয়োজন। ৩টা broker-এ ভাগ করলে প্রতিটাতে ~৭০০GB+ ডিস্ক (কিছু headroom সহ, সাধারণত ৭০-৮০% এর বেশি ভরাট না রাখা ভালো practice)।
ড্রব্যাক
- এই ফর্মুলাগুলো starting point, বাস্তব traffic pattern (spike, seasonal variation) অনেক সময় স্থির average-based হিসাবের চেয়ে বেশি হয় — peak traffic-এর জন্য headroom (সাধারণত ৩-৫x) রাখা নিরাপদ।
- Under-provisioning করলে (হিসাব কম করে) disk-full incident-এর ঝুঁকি (Module 14), over-provisioning করলে অপ্রয়োজনীয় খরচ — সঠিক balance করা একটা চলমান, পুনরাবৃত্ত প্রক্রিয়া, একবারের হিসাব না।
Broker Config যেগুলো আসলে ম্যাটার করে
গুরুত্বপূর্ণ কনফিগ
num.io.threads— disk I/O request হ্যান্ডেল করা thread সংখ্যা। ডিফল্ট (৮) ছোট cluster-এ ঠিক আছে, কিন্তু high-throughput, বেশি disk (RAID/JBOD)-এর cluster-এ বাড়ানো দরকার — disk সংখ্যার সমানুপাতিক রাখা একটা সাধারণ rule।num.network.threads— network request গ্রহণ/পাঠানো handle করা thread সংখ্যা। High connection count (অনেক producer/consumer client) থাকলে বাড়ানো দরকার।socket.send.buffer.bytes/socket.receive.buffer.bytes— OS socket buffer সাইজ, বড় করলে high-latency নেটওয়ার্কে (cross-region) throughput ভালো হতে পারে (TCP window scaling সুবিধা)।num.replica.fetchers— follower broker কতগুলো parallel thread দিয়ে leader থেকে fetch করে replicate করবে — বেশি হলে replication দ্রুত catch-up করে, কিন্তু network/CPU খরচ বাড়ে।
রিয়েল-লাইফ উদাহরণ
আপনার একটা broker-এ ৬টা আলাদা disk mount (JBOD সেটআপ) থাকলে, num.io.threads ডিফল্ট ৮ রাখলে সব disk-এ সমানভাবে I/O thread বণ্টন হবে না দক্ষভাবে — disk সংখ্যার ২-৩ গুণ (~১২-১৮) রাখা বেশি কার্যকর হতে পারে বেঞ্চমার্ক অনুযায়ী।
ড্রব্যাক
- Thread সংখ্যা প্রয়োজনের চেয়ে বেশি বাড়ালে context-switching overhead বাড়ে, কার্যত throughput কমেও যেতে পারে একটা পয়েন্টের পর — 'যত বেশি thread তত ভালো' সবসময় সত্যি না।
- এই কনফিগগুলো ভুল টিউন করলে সমস্যা সাথে সাথে স্পষ্ট নাও হতে পারে — শুধু load বাড়লে বা peak traffic-এ ধরা পড়ে, তাই load-testing দিয়ে যাচাই করা জরুরি প্রোডাকশনে যাওয়ার আগে।
JVM Tuning — Heap ছোট রাখুন, Page Cache বড়
মূলনীতি (Module 5-এর সরাসরি সম্প্রসারণ)
Kafka broker-এর JVM heap সাধারণত ৬GB-এর আশেপাশে রাখা হয় recommended (-Xmx6g -Xms6g টাইপ সেটিং), মেশিনের মোট RAM যতই হোক (৩২GB, ৬৪GB, বা তার বেশি)। বাকি RAM ইচ্ছাকৃতভাবে OS page cache-এর জন্য ছেড়ে দেওয়া হয় (Module 5)।
কেন এই নির্দিষ্ট সীমা
বড় heap মানে বড় GC (Garbage Collection) pause — Java-র G1GC (Kafka-র recommended collector) বড় heap-এ (২০GB+) কয়েকশো মিলিসেকেন্ড থেকে সেকেন্ড-স্কেল pause তৈরি করতে পারে occasionally, যেটা broker-কে সাময়িকভাবে unresponsive করে দিতে পারে — client timeout, ISR shrink (follower fetch করতে দেরি) ইত্যাদি cascading সমস্যা তৈরি করতে পারে একটা GC pause থেকেই।
GC Tuning প্র্যাকটিস
- G1GC ব্যবহার recommended (
-XX:+UseG1GC), ডিফল্ট আধুনিক JVM-এ। -XX:MaxGCPauseMillis=20টাইপ target সেট করা যায়, কিন্তু এটা একটা goal, guarantee না।- GC log পর্যবেক্ষণ করা (
-Xlog:gc*) monitoring-এর একটা গুরুত্বপূর্ণ অংশ — একটা pause spike-ই অনেক downstream সমস্যার root cause হতে পারে।
রিয়েল-লাইফ উদাহরণ
একটা টিম ভুল করে broker-এর heap ৩২GB সেট করেছিল ('বেশি মেমরি মানে বেশি পারফরম্যান্স' ভেবে) — ফলাফল উল্টো হলো: occasional ২-৩ সেকেন্ড GC pause broker-কে unresponsive করে দিতো, যার ফলে ISR shrink হতো (follower সময়মতো fetch করতে পারতো না), এবং client timeout বেড়ে যেতো। Heap ৬GB-এ কমিয়ে আনার পর pause সমস্যা প্রায় সম্পূর্ণ চলে গেলো।
ড্রব্যাক
- Heap খুব ছোট রাখলে (যেমন ২GB একটা high-throughput broker-এ) OutOfMemoryError-এর ঝুঁকি থাকে যদি internal buffer/cache (request queue, replica fetcher buffer) সেই মেমরির মধ্যে ফিট না করে — সঠিক sweet spot workload অনুযায়ী বেঞ্চমার্ক করে বের করা উচিত, অন্ধভাবে 6GB copy-paste করা না।
Monitoring: JMX Metrics, UnderReplicatedPartitions, RequestHandlerAvgIdlePercent, Consumer Lag
সবচেয়ে গুরুত্বপূর্ণ metrics
UnderReplicatedPartitions— কতগুলো partition-এর ISR সংখ্যা replication.factor-এর চেয়ে কম (Module 7-এর ISR shrink)। এটা 0-এর বেশি হলেই তাৎক্ষণিক তদন্ত দরকার — replication বা broker health সমস্যার প্রাথমিক সংকেত।RequestHandlerAvgIdlePercent— broker-এর request handler thread কতটা idle (ব্যস্ত না)। এটা কম হতে থাকলে (যেমন ২০%-এর নিচে) broker CPU-bound হয়ে যাচ্ছে, capacity বাড়ানোর সংকেত।- Consumer Lag (Module 4-এ বিস্তারিত) — প্রতিটা consumer group-এর lag ট্র্যাক করা, alert threshold সহ।
ActiveControllerCount— cluster-এ ঠিক একটা active controller থাকা উচিত সবসময়; 0 বা 1-এর বেশি মানে metadata plane সমস্যায় আছে (Module 8)।RequestQueueSize/ResponseQueueSize— request backlog জমছে কিনা, broker overload হচ্ছে কিনা তার সংকেত।
Prometheus + Grafana সেটআপ
Kafka JMX metrics expose করে (JMX_PORT কনফিগ), একটা JMX Exporter (Prometheus-এর জন্য) এই metrics scrape করে Prometheus format-এ রূপান্তর করে, Grafana dashboard দিয়ে visualize করা হয়। Kafka community-তে রেডিমেড Grafana dashboard template আছে (Confluent, Datadog ইত্যাদি প্রকাশিত) যেগুলো starting point হিসেবে ব্যবহার করা যায়।
রিয়েল-লাইফ উদাহরণ
আপনার Django/FastAPI backend-এর monitoring stack-এর সাথে একই Prometheus/Grafana infrastructure পুনরায় ব্যবহার করে Kafka metrics যোগ করা সহজ — একটা unified dashboard-এ database query latency, API response time, আর Kafka consumer lag পাশাপাশি দেখা সম্ভব, incident-এর সময় correlation বের করা সহজ হয়।
ড্রব্যাক
- JMX metrics ভারী হতে পারে (হাজার হাজার partition-এ প্রতিটা partition-level metric) — scrape interval আর metric cardinality সাবধানে ম্যানেজ করা দরকার, নাহলে monitoring system নিজেই overload হয়ে যেতে পারে।
Partition Reassignment, kafka-reassign-partitions.sh, Throttling
কখন দরকার
- নতুন broker যোগ করার পর বিদ্যমান partition সেখানে সরানো (load balance)।
- Rack awareness (Module 7) retroactively প্রয়োগ করা।
- একটা broker decommission করার আগে তার সব partition অন্য broker-এ সরানো।
কীভাবে কাজ করে
kafka-reassign-partitions.sh একটা JSON প্ল্যান জেনারেট করে (--generate) যেটা বলে কোন partition কোন broker-এ যাবে, তারপর --execute করলে controller (Module 8) সেই migration orchestrate করে — নতুন replica তৈরি হয়, ডেটা catch-up হয় (leader থেকে fetch করে, ঠিক normal replication-এর মতো), সব sync হয়ে গেলে পুরনো replica বাদ দেওয়া হয়।
Throttling — কেন জরুরি
Reassignment একটা bulk ডেটা কপি অপারেশন — throttle ছাড়া এটা network bandwidth-এর একটা বড় অংশ দখল করে ফেলতে পারে, normal produce/consume traffic-কে প্রভাবিত করে (latency spike)। --throttle ফ্ল্যাগ দিয়ে reassignment-এর bandwidth সীমাবদ্ধ করা যায় (bytes/sec), যাতে production traffic-এর ওপর প্রভাব কম থাকে, migration ধীরে কিন্তু নিরাপদে সম্পন্ন হয়।
রিয়েল-লাইফ উদাহরণ
আপনার cluster-এ একটা নতুন ৪র্থ broker যোগ করার পর, বিদ্যমান ৩টা broker-এর কিছু partition নতুন broker-এ সরাতে চান load balance করতে। Peak traffic hour-এ throttle ছাড়া reassignment চালালে production latency spike হতে পারে — off-peak সময়ে, throttled রেটে (যেমন 50MB/s) চালানো নিরাপদ practice।
ড্রব্যাক
- Throttle খুব কম রাখলে reassignment সম্পূর্ণ হতে দিন লেগে যেতে পারে বড় dataset-এ — একটা ট্রেড-অফ migration speed বনাম production impact-এর মধ্যে।
- Reassignment চলাকালীন সাময়িকভাবে extra disk space লাগে (পুরনো এবং নতুন replica দুটোই একসাথে থাকে যতক্ষণ না migration সম্পূর্ণ) — capacity planning-এ এই headroom হিসাব করা দরকার।
Security: TLS, SASL (PLAIN/SCRAM/OAUTHBEARER), ACL, Quota
চারটা স্তম্ভ
- TLS — network-এ ডেটা এনক্রিপ্ট করে (encryption in transit)। Module 5-এ আলোচিত হয়েছিল zero-copy এর সাথে এর সাংঘর্ষিকতা — TLS চালু থাকলে throughput কিছুটা কমে।
- SASL — authentication (ক্লায়েন্ট কে, তা যাচাই)। PLAIN (সাধারণ username/password, TLS-এর সাথে ব্যবহার করা উচিত নাহলে plaintext credential যায়), SCRAM (salted challenge-response, PLAIN-এর চেয়ে নিরাপদ, password hash নেটওয়ার্কে যায় না), OAUTHBEARER (OAuth token-ভিত্তিক, enterprise identity provider-এর সাথে ইন্টিগ্রেশনের জন্য উপযুক্ত)।
- ACL (Access Control List) — authorization (কে কী করতে পারে) — কোন principal কোন topic-এ produce/consume/describe করতে পারবে, নির্দিষ্টভাবে নিয়ন্ত্রণ করে।
- Quota — একটা client/user কতটা bandwidth/request-rate ব্যবহার করতে পারবে সীমাবদ্ধ করে — একটা misbehaving client পুরো cluster-এর জন্য bottleneck হয়ে ওঠা থেকে রক্ষা করে (noisy neighbor সমস্যা প্রতিরোধ)।
রিয়েল-লাইফ উদাহরণ
একটা multi-team organization-এ, payment-service principal-কে শুধু payment.* টপিকে produce/consume ACL দেওয়া হবে, analytics-service-কে শুধু read-only ACL দেওয়া হবে বিভিন্ন টপিকে — একটা compromised বা buggy analytics service ভুলবশত payment টপিকে লিখতে পারবে না, ACL সেটা প্রতিরোধ করবে এমনকি কোড bug থাকলেও।
ড্রব্যাক
- TLS + SASL + ACL সব একসাথে চালু করলে অপারেশনাল জটিলতা উল্লেখযোগ্যভাবে বাড়ে (certificate rotation, credential management, ACL maintenance) — dev/staging-এ এই জটিলতা এড়িয়ে শুধু production-এ কড়া security রাখা common practice, কিন্তু তাতে dev-prod parity কমে যায় (Module 14-এর 'staging-এ কাজ করলো, production-এ না' সমস্যার একটা উৎস হতে পারে)।
- Quota ভুলভাবে সেট করলে legitimate high-throughput client অপ্রত্যাশিতভাবে throttled হয়ে যেতে পারে, latency spike দেখা দিতে পারে যা প্রথমে network/broker সমস্যা মনে হতে পারে।
Multi-DC: MirrorMaker 2, Active-Active, Cluster Linking
কেন Multi-DC দরকার
- Disaster recovery — একটা পুরো datacenter/region ডাউন হলে অন্য region থেকে সার্ভিস চালু রাখা।
- Geo-locality — বিভিন্ন region-এর ইউজারদের কাছাকাছি broker থেকে সার্ভ করা, latency কমাতে।
- Regulatory compliance — কিছু ডেটা নির্দিষ্ট geography-র বাইরে যেতে না পারার নিয়ম (data residency)।
MirrorMaker 2 (MM2)
Kafka Connect-ভিত্তিক (Module 11-এর architecture এখানে পুনর্ব্যবহৃত) একটা টুল যেটা এক cluster থেকে আরেক cluster-এ topic replicate করে। এটা offset translation-ও হ্যান্ডেল করে (source cluster-এর offset আর target cluster-এর offset ভিন্ন হতে পারে, কারণ target-এ ডেটা fresh produce হয় mirroring-এর মাধ্যমে, মূল offset সংরক্ষিত থাকে না সরাসরি)।
Active-Active vs Active-Passive
- Active-Passive — একটা cluster primary (সব ট্রাফিক), আরেকটা শুধু replica/standby (disaster recovery-র জন্য প্রস্তুত)।
- Active-Active — দুটো cluster-ই সক্রিয়ভাবে ট্রাফিক নেয় (সাধারণত ভিন্ন topic-এ বা ভিন্ন geo-এর ইউজারের জন্য), একে অপরের ডেটা mirror করে। জটিলতা বেশি (conflict resolution, ordering guarantee cross-region-এ দুর্বল হয়ে যায়) কিন্তু resource utilization ভালো এবং latency কম (স্থানীয় cluster ব্যবহার করা যায়)।
Cluster Linking (Confluent-specific)
MirrorMaker 2-এর একটা alternative, broker-level সরাসরি replication (আলাদা Connect infrastructure ছাড়াই) — offset preserve থাকে (source আর target-এ একই offset), যা MM2-এর তুলনায় একটা বড় সুবিধা কিছু ব্যবহারিক ক্ষেত্রে (consumer migration সহজ হয়)।
রিয়েল-লাইফ উদাহরণ
আপনার একটা primary cluster ঢাকায়, disaster recovery cluster সিঙ্গাপুরে — MM2 ক্রমাগত সব critical topic mirror করে রাখে। ঢাকা datacenter আউটেজ হলে, applications সিঙ্গাপুর cluster-এ failover করতে পারে (সামান্য ডেটা lag সহ, MM2 asynchronous replication)।
ড্রব্যাক
- Cross-DC replication network bandwidth costly এবং latency-bound — real-time synchronous cross-region replication ব্যবহারিকভাবে প্রায় অসম্ভব (speed of light limit), তাই সবসময় কিছুটা lag/eventual-consistency মেনে নিতে হয়।
- Active-Active-এ conflict resolution জটিল একটা সমস্যা যদি একই key দুই দিকেই আপডেট হতে পারে — careful application design (geo-partitioned key ownership) দরকার।
AWS MSK vs Self-managed EC2 — খরচ, কন্ট্রোল, অপারেশনাল ব্যথা
তুলনা
| AWS MSK (Managed) | Self-managed EC2 | |
|---|---|---|
| অপারেশনাল বোঝা | কম — AWS broker patching, monitoring baseline, scaling সাহায্য করে | বেশি — সব নিজে ম্যানেজ (upgrade, patching, monitoring setup, disaster recovery) |
| খরচ | সাধারণত বেশি (per-broker-hour premium + storage), কিন্তু hidden ops খরচ কম | Raw EC2/EBS খরচ কম, কিন্তু engineer সময়ের খরচ (opportunity cost) যোগ করলে প্রায়ই সমান বা বেশি |
| কন্ট্রোল | সীমিত — কিছু low-level config/tuning MSK-তে exposed না, নির্দিষ্ট Kafka ভার্সনে আটকে থাকতে হতে পারে | সম্পূর্ণ — যেকোনো config, patch, custom broker plugin, নির্দিষ্ট ভার্সন choice |
| KRaft সাপোর্ট | Provider-নির্ভর, সবসময় সর্বশেষ ফিচার দ্রুত না-ও পাওয়া যেতে পারে | নিজে upgrade করলে সর্বশেষ ফিচার দ্রুত পাওয়া যায় |
সিদ্ধান্ত নেওয়ার নীতি
- MSK — যখন টিমের ডেডিকেটেড Kafka expertise নেই, দ্রুত শুরু করতে চান, অপারেশনাল ঝুঁকি AWS-এ ছেড়ে দিতে চান।
- Self-managed — যখন cost optimization critical (বড় স্কেলে), কাস্টম tuning/config দরকার, অথবা multi-cloud/on-prem flexibility চাই।
রিয়েল-লাইফ উদাহরণ
একটা ছোট স্টার্টআপ টিমে dedicated Kafka/infra engineer নেই — MSK বেছে নেওয়া বুদ্ধিমান, কারণ broker patching/upgrade/basic monitoring AWS handle করে, টিম বিজনেস লজিকে ফোকাস করতে পারে। কিন্তু একটা বড় প্রতিষ্ঠানে (যেমন LinkedIn, যারা নিজেই Kafka তৈরি করেছে) self-managed cluster-এ পূর্ণ কন্ট্রোল আর optimization সুবিধা বেশি মূল্যবান, dedicated platform টিম আছে সেটা সামলানোর জন্য।
ড্রব্যাক
- MSK-তে lock-in ঝুঁকি (AWS-specific tooling/API-এর ওপর নির্ভরতা বাড়তে থাকে) — migration আউট করা পরে জটিল হতে পারে।
- Self-managed-এ একটা misconfiguration বা ভুল upgrade পুরো দায় নিজের টিমের ওপর পড়ে — কোনো managed support fallback নেই AWS-এর মতো।
ইন্টারভিউ প্রশ্ন — Module 13
প্র.১ — Kafka broker-এ JVM heap ৬GB-এর আশেপাশে রাখার সুপারিশ কেন, মেশিনে ৬৪GB RAM থাকলেও? উত্তর: বড় heap মানে বড় GC pause, যা broker-কে সাময়িকভাবে unresponsive করে দিতে পারে (ISR shrink, client timeout-এর মতো cascading সমস্যা তৈরি করে)। বাকি RAM ইচ্ছাকৃতভাবে OS page cache-এর জন্য রাখা হয় (Module 5), যেটা GC-মুক্ত এবং read performance-এর জন্য বেশি কার্যকর।
প্র.২ — UnderReplicatedPartitions metric 0-এর বেশি হলে এটা কী নির্দেশ করে, আর কী করা উচিত?
উত্তর: এটা নির্দেশ করে কিছু partition-এর ISR সংখ্যা replication.factor-এর কম — replication বা broker health সমস্যা হচ্ছে। এটা একটা তাৎক্ষণিক তদন্তযোগ্য alert হওয়া উচিত, কারণ এটা মানে durability guarantee দুর্বল হয়ে আছে সাময়িকভাবে (Module 7)।
প্র.৩ — Partition reassignment-এ throttling কেন ব্যবহার করবেন? উত্তর: Reassignment একটা bulk ডেটা কপি অপারেশন যা network bandwidth-এর বড় অংশ দখল করতে পারে, normal produce/consume traffic-এ latency spike তৈরি করতে পারে। Throttle দিয়ে migration bandwidth সীমাবদ্ধ রাখলে production impact কমানো যায়, বিনিময়ে migration ধীরে হয়।
প্র.৪ — SASL/PLAIN আর SASL/SCRAM-এর মধ্যে নিরাপত্তার পার্থক্য কী? উত্তর: PLAIN-এ raw username/password নেটওয়ার্কে যায় (TLS দিয়ে এনক্রিপ্ট করা আবশ্যক নিরাপদ থাকতে)। SCRAM salted challenge-response ব্যবহার করে — actual password নেটওয়ার্কে কখনো যায় না, TLS ছাড়াও তুলনামূলক বেশি নিরাপদ (যদিও TLS দুটোর সাথেই recommended)।
প্র.৫ — Active-Active multi-DC সেটআপে সবচেয়ে বড় challenge কী? উত্তর: Conflict resolution — যদি একই key দুই দিকের cluster-এ স্বাধীনভাবে আপডেট হতে পারে, কোনটা 'সঠিক' ফলাফল সেটা নির্ধারণ করা জটিল। সাধারণত geo-partitioned key ownership (একটা key শুধু একটা নির্দিষ্ট region-এ আপডেট হবে) দিয়ে এই সমস্যা এড়ানো হয়।
প্র.৬ — একটা ছোট স্টার্টআপ টিম যাদের dedicated infra engineer নেই, তাদের কি AWS MSK নাকি self-managed EC2 Kafka বেছে নেওয়া উচিত, আর কেন? উত্তর: MSK — অপারেশনাল বোঝা (patching, upgrade, basic monitoring) AWS-এর ওপর ছেড়ে দেওয়া যায়, টিম বিজনেস লজিকে ফোকাস করতে পারে। Self-managed-এ পূর্ণ কন্ট্রোল পাওয়া যায় কিন্তু dedicated expertise ছাড়া misconfiguration/অপারেশনাল ভুলের ঝুঁকি বেশি।
কর্নার কেস — Module 13
- Capacity planning শুধু average throughput দিয়ে করে peak/spike ট্রাফিক উপেক্ষা করা — একটা flash-sale বা viral event-এর সময় ৫-১০x traffic spike হলে under-provisioned cluster হঠাৎ ধসে পড়তে পারে, disk-full বা network saturation দিয়ে (Module 14)।
- TLS চালু করার সময় certificate expiry monitor না করা — একটা expired certificate পুরো cluster-এর সব client connection ব্যর্থ করে দিতে পারে হঠাৎ, একদম কোনো পূর্ব সতর্কতা ছাড়া যদি expiry alert সেট আপ না থাকে।
- Quota সেট করার সময় legitimate high-throughput client-দের কথা বিবেচনা না করা — একটা batch job (যেমন daily bulk export) যেটা মাঝেমধ্যে অনেক বেশি throughput দাবি করে, ভুল quota-তে সেটা throttled হয়ে expected সময়ের চেয়ে অনেক বেশি সময় নিতে পারে।
- Cross-AZ/DC replication bandwidth খরচ capacity planning-এ না ধরা — rack awareness (Module 7) বা multi-DC replication চালু থাকলে network egress খরচ প্রত্যাশার চেয়ে অনেক বেশি হতে পারে cloud bill-এ, বিশেষ করে বড় স্কেলে।
- MSK-তে একটা প্রয়োজনীয় broker-level config অনুপলব্ধ থাকা (কিছু low-level tuning MSK exposed না করে) — একটা টিম যদি self-managed-এর অভিজ্ঞতা নিয়ে MSK-তে মাইগ্রেট করে, কিছু পরিচিত টিউনিং অপশন খুঁজে না পেয়ে অবাক হতে পারে, migration-এর আগে feature-parity যাচাই করা জরুরি।
MODULE 14: Failure Playbook + Capstone
শেষ মডিউল — এখন পর্যন্ত শেখা সব থিওরি একসাথে জড়ো করে বাস্তব প্রোডাকশন ইনসিডেন্ট ডিবাগ করার একটা মানসিক প্লেবুক তৈরি করা হবে, তারপর DLQ ডিজাইন আর backpressure handling, আর সবশেষে একটা ক্যাপস্টোন — আপনার 100M+/day notification system-এর Kafka লেয়ার সম্পূর্ণভাবে ডিজাইন করা, একটা FAANG-স্টাইল মক ইন্টারভিউ ফরম্যাটে।
প্রোডাকশন ডিবাগিং: Consumer Lag Spike
লক্ষণ
Consumer lag হঠাৎ বাড়তে শুরু করেছে, স্থিতিশীল হচ্ছে না।
ডায়াগনস্টিক চেকলিস্ট
- Producer rate বেড়েছে কিনা — traffic spike (Module 13-এর capacity planning gap)।
- Consumer processing rate কমেছে কিনা — downstream dependency (DB, external API) স্লো হয়ে গেছে কিনা।
- Rebalance storm চলছে কিনা (Module 4) —
kafka-consumer-groups.sh --describeবারবার চালিয়ে consumer membership স্থিতিশীল কিনা দেখা। - একটা poison pill মেসেজে আটকে আছে কিনা — নির্দিষ্ট একটা partition-এর lag শুধু বাড়ছে, বাকিগুলো স্বাভাবিক — এটা একটা স্পষ্ট সংকেত।
- Under-provisioned consumer group — partition সংখ্যার তুলনায় consumer instance কম (Module 2)।
রিয়েল-লাইফ উদাহরণ
আপনার notification pipeline-এ lag হঠাৎ বাড়লে, প্রথমে kafka-consumer-groups.sh --describe চালিয়ে দেখুন lag সব partition-এ সমান নাকি একটা partition-এ concentrated। যদি একটাতে concentrated, সেই partition-এর সাম্প্রতিক মেসেজগুলো manually dump করে দেখুন (Module 5-এর kafka-dump-log.sh) — প্রায়ই একটা malformed/unexpected মেসেজ পাওয়া যায় যেটা consumer-এর deserializer/business logic-এ exception ছুঁড়ছে বারবার।
ড্রব্যাক / প্রতিরোধ
- Lag spike-এর root cause খুঁজতে সময় লাগলে, ততক্ষণে ব্যবহারকারীরা delayed notification পাচ্ছেন — একটা runbook আগে থেকে তৈরি রাখা (এই checklist-এর মতো) মেটাডেটা-ডিবাগিং সময় কমায় incident-এর সময়।
- Auto-scaling consumer instance (partition সংখ্যা অনুযায়ী সীমাবদ্ধ) capacity-related lag spike অনেকটা প্রতিরোধ করতে পারে, কিন্তু poison pill-জনিত lag-এ কোনো সাহায্য করে না (আরও instance যোগ করলেও একই মেসেজেই সবাই আটকাবে যদি key সেই partition-এ যায়)।
প্রোডাকশন ডিবাগিং: Rebalance Storm
লক্ষণ
Consumer group ক্রমাগত rebalance করছে, কখনো স্থিতিশীল হচ্ছে না — throughput প্রায় শূন্যে নেমে গেছে কারণ কেউ স্থিরভাবে কাজ করতে পারছে না (Module 4-এর eager rebalance stop-the-world সমস্যা)।
ডায়াগনস্টিক চেকলিস্ট
max.poll.interval.msvs actual processing time — processing সীমার কাছাকাছি বা বেশি সময় নিচ্ছে কিনা (Module 4)।- Pod/instance ঘন ঘন restart হচ্ছে কিনা — Kubernetes crash loop, OOM kill, বা aggressive autoscaling।
- Static membership (
group.instance.id) চালু আছে কিনা — না থাকলে প্রতিটা restart-ই নতুন rebalance ট্রিগার করছে। - Cooperative rebalance protocol ব্যবহার হচ্ছে কিনা — এখনো eager হলে প্রতিটা rebalance পুরো group থামিয়ে দিচ্ছে।
রিয়েল-লাইফ উদাহরণ
একটা Kubernetes deployment-এ HPA (Horizontal Pod Autoscaler) খুব sensitive threshold-এ সেট করা ছিল — CPU সামান্য বাড়া-কমাতেই pod scale up/down হচ্ছিল ঘন ঘন, প্রতিটা scale event consumer group rebalance ট্রিগার করছিল। HPA-এর cooldown period বাড়িয়ে আর static membership যোগ করে সমস্যা সমাধান হলো।
ড্রব্যাক / প্রতিরোধ
- Rebalance storm-এর মূল কারণ প্রায়ই infrastructure-level (autoscaling aggressiveness, resource limit) — শুধু Kafka কনফিগ দিয়ে এটা পুরোপুরি ঠিক করা যায় না, deployment/infra টিমের সাথে সমন্বয় প্রয়োজন হতে পারে।
- Cooperative sticky assignor migrate করার সময় সব consumer একই সময়ে নতুন protocol বুঝতে হবে (rolling upgrade সাবধানে) — নাহলে mixed-protocol state-এ আরও বিভ্রান্তি তৈরি হতে পারে।
প্রোডাকশন ডিবাগিং: ISR Shrink
লক্ষণ
UnderReplicatedPartitions metric 0-এর ওপরে (Module 13), acks=all producer-দের latency বেড়ে গেছে বা এরর আসছে।
ডায়াগনস্টিক চেকলিস্ট
- কোন broker(গুলো) lagging —
kafka-topics.sh --describeদিয়ে কোন replica ISR-এর বাইরে দেখা। - সেই broker-এর disk I/O/network saturation — একটা broker slow disk বা network bottleneck-এ থাকলে follower fetch দেরিতে হয়।
- GC pause (Module 13) — একটা দীর্ঘ GC pause broker-কে সাময়িকভাবে unresponsive করে ISR থেকে বাদ পড়ার কারণ হতে পারে।
replica.lag.time.max.msকনফিগ কতটা কড়া — খুব কম সেট থাকলে সামান্য সাময়িক ধীরগতিতেও ISR shrink হয়ে যায়।
রিয়েল-লাইফ উদাহরণ
একটা broker-এ একটা নতুন, ভারী batch job (ভুলবশত একই মেশিনে deploy হওয়া) disk I/O-এর একটা বড় অংশ দখল করে নিলো — সেই broker-এর replica fetch ধীর হয়ে গেলো, ISR থেকে বাদ পড়ে গেলো একাধিক partition-এ। সমাধান: dedicated Kafka broker মেশিনে অন্য কোনো ভারী workload না চালানো (Module 5-এর page cache আলোচনার সাথেও সম্পর্কিত)।
ড্রব্যাক / প্রতিরোধ
- ISR shrink-এর alert দেরিতে ধরা পড়লে,
min.insync.replicasপূরণ না হয়ে producer write ব্যর্থ হতে শুরু করতে পারে (Module 7) — proactive monitoring critical, reactive detection যথেষ্ট না। - একবার ISR shrink হলে, catch-up করতে (আবার ISR-এ ফিরে আসতে) সময় লাগে ডেটার পরিমাণের সমানুপাতিক — বড় lag হলে recovery সময়ও বড় হবে।
প্রোডাকশন ডিবাগিং: Disk Full
লক্ষণ
Broker-এ produce ব্যর্থ হচ্ছে, broker log-এ disk space এরর, বা broker পুরোপুরি ক্র্যাশ করছে।
ডায়াগনস্টিক চেকলিস্ট
- Retention policy সঠিকভাবে কাজ করছে কিনা —
retention.ms/retention.bytes(Module 6) প্রত্যাশিতভাবে পুরনো ডেটা মুছছে কিনা। - হঠাৎ traffic spike হয়েছে কিনা — capacity planning-এর হিসাবের চেয়ে বেশি ডেটা আসছে কিনা (Module 13)।
- একটা topic ভুলে unlimited retention-এ আছে কিনা —
retention.ms=-1টাইপ ভুল কনফিগ। - Log segment রোটেট হচ্ছে কিনা —
segment.ms/segment.bytes(Module 5) খুব বড় সেট থাকলে active segment কখনো rotate না হয়ে delete policy কার্যকর হতে দেরি হচ্ছে কিনা। - Replication slot lag (Module 11) — যদি এটা PostgreSQL সাইডে ডিস্ক ফুল, Kafka-র সমস্যা না — কিন্তু একই উপসর্গ (disk full) ভিন্ন সিস্টেমে।
রিয়েল-লাইফ উদাহরণ
একটা টিম একটা নতুন high-volume debug-logging topic তৈরি করেছিল টেস্টিং-এর জন্য, retention policy সেট করতে ভুলে গিয়েছিল (ডিফল্ট ৭ দিন থাকলেও এই ক্ষেত্রে ডেটা ভলিউম এত বেশি ছিল যে ৭ দিনের মধ্যেই ডিস্ক ভরে গেলো)। সমাধান: emergency retention.ms কমিয়ে দিয়ে দ্রুত পুরনো ডেটা clear করা, দীর্ঘমেয়াদে সব নতুন topic তৈরির জন্য একটা mandatory retention review প্রক্রিয়া যোগ করা।
ড্রব্যাক / প্রতিরোধ
- Disk usage-এর ওপর proactive alert (৭০-৮০% ভরাট হলে সতর্কতা, ৯০%+ হলে critical) থাকা আবশ্যক — disk পুরোপুরি ভরে যাওয়ার পরে react করা অনেক বেশি ক্ষতিকর (broker crash, সম্ভাব্য ডেটা করাপশন) প্রতিরোধমূলক ব্যবস্থার চেয়ে।
- Emergency retention কমানো একটা ধ্বংসাত্মক অপারেশন (ডেটা স্থায়ীভাবে মুছে যায়) — এটা করার আগে সচেতনভাবে বিবেচনা করা উচিত কোন ডেটা হারানো গ্রহণযোগ্য।
প্রোডাকশন ডিবাগিং: Hot Partition
লক্ষণ
একটা নির্দিষ্ট partition-এর throughput/lag বাকিগুলোর তুলনায় অনেক বেশি, সেই partition-এর leader broker-এ CPU/network load অস্বাভাবিক বেশি (Module 2-এ ধারণা)।
ডায়াগনস্টিক চেকলিস্ট
- Key distribution বিশ্লেষণ করা — কোন key(গুলো) সেই partition-এ যাচ্ছে বেশি, এবং কেন (একটা celebrity user, একটা viral product, একটা batch job যা একটা নির্দিষ্ট key দিয়ে বারবার produce করছে)।
- সেই broker-এর অন্য partition-ও প্রভাবিত হচ্ছে কিনা — যদি হয়, এটা broker-level সমস্যা, partition-level না।
- Producer batching pattern চেক করা — sticky partitioner (Module 3) ভুলভাবে একটা partition-এ concentrate করছে কিনা কোনো bug-এর কারণে।
রিয়েল-লাইফ উদাহরণ
একটা flash-sale-এর সময় একটা নির্দিষ্ট product_id অস্বাভাবিক বেশি অর্ডার ইভেন্ট জেনারেট করলো — সেই product_id key হিসেবে ব্যবহৃত হওয়ায় একটা নির্দিষ্ট partition-এ পুরো ট্রাফিক জমা হলো, সেই partition-এর consumer processing lag বাড়তে লাগলো যদিও বাকি সব partition স্বাভাবিক। সমাধান: এই ধরনের high-cardinality spike সম্ভাবনা থাকা key-এর জন্য একটা composite key strategy (product_id + random_suffix) ডিজাইন-টাইমেই বিবেচনা করা উচিত ছিল যদি strict per-key ordering প্রয়োজনীয় না হয়।
ড্রব্যাক / প্রতিরোধ
- Hot partition সমস্যা partition সংখ্যা বাড়িয়ে সমাধান হয় না যদি key distribution না বদলায় — এটা একটা সাধারণ ভুল প্রতিক্রিয়া যা কাজ করে না।
- Composite key ব্যবহার করলে সেই key-এর strict ordering guarantee হারিয়ে যায় — এটা একটা সচেতন ট্রেড-অফ, business requirement অনুযায়ী সিদ্ধান্ত নেওয়া উচিত।
প্রোডাকশন ডিবাগিং: Poison Pill
লক্ষণ
একটা নির্দিষ্ট partition-এর consumer lag ক্রমাগত বাড়ছে, কিন্তু সেই consumer instance ক্র্যাশ করছে না (বা বারবার ক্র্যাশ-রিস্টার্ট লুপে আছে) — একই offset-এ বারবার আটকে যাচ্ছে।
কীভাবে ঘটে
একটা মেসেজ যেটা deserialize করা যাচ্ছে না (corrupt data, unexpected schema — Module 10-এর breaking schema change একটা সাধারণ কারণ), অথবা business logic-এ এমন একটা exception ছোঁড়ে যেটা কখনো resolve হয় না (একটা null pointer, একটা ডেটাবেজ constraint violation) — যদি consumer-এর error handling শুধু retry করে (offset commit না করে), এটা অনির্দিষ্টকাল একই মেসেজে আটকে থাকবে, পরের সব মেসেজ পেছনে জমা হতে থাকবে।
ডায়াগনস্টিক
kafka-console-consumer.sh দিয়ে সেই নির্দিষ্ট offset-এর মেসেজ manually fetch করে দেখা (Module 5-এর kafka-dump-log.sh-ও কাজে লাগবে), consumer application-এর error log-এ exact exception খুঁজে বের করা।
সমাধান: Dead Letter Queue (DLQ)
একটা মেসেজ N বার retry করার পরও ব্যর্থ হলে, সেটাকে একটা আলাদা topic.DLQ টপিকে পাঠিয়ে দিয়ে মূল offset commit করে এগিয়ে যাওয়া — বাকি মেসেজ প্রসেসিং চালিয়ে যাওয়া সম্ভব হয়, সমস্যাযুক্ত মেসেজ পরে আলাদাভাবে তদন্ত/reprocess করা যায় DLQ থেকে।
রিয়েল-লাইফ উদাহরণ
আপনার notification consumer একটা মেসেজে phone_number field null পেয়ে NullPointerException ছুঁড়ছিল বারবার, retry লজিক infinite loop-এ আটকে গেলো, পুরো partition-এর lag ঘণ্টার মধ্যে হাজার হাজার মেসেজে পৌঁছালো। DLQ pattern প্রয়োগ করার পর, ৩ বার retry-এর পর সেই মেসেজ DLQ-তে যায়, বাকি সব মেসেজ স্বাভাবিকভাবে প্রসেস হতে থাকে — lag আর জমে না।
ড্রব্যাক
- DLQ-তে পাঠানো মেসেজ যদি নিয়মিত monitor/reprocess না করা হয়, সেগুলো silently হারিয়ে যাওয়ার মতোই — একটা DLQ processing/alerting প্রক্রিয়া থাকা জরুরি, শুধু সমস্যা 'লুকানোর' জায়গা হিসেবে ব্যবহার করা উচিত না।
Dead Letter Queue (DLQ) ডিজাইন
মূল নীতি
প্রতিটা মূল টপিকের জন্য একটা সংশ্লিষ্ট DLQ টপিক (যেমন notification.email → notification.email.DLQ)। একটা মেসেজ প্রসেস করতে ব্যর্থ হলে (কনফিগারযোগ্য সংখ্যক retry-এর পর), সেটা DLQ-তে পাঠানো হয় সহ কিছু metadata: মূল ব্যর্থতার কারণ (exception message), original topic/partition/offset, timestamp, retry count।
ডিজাইন সিদ্ধান্ত
- কে retry করবে — consumer নিজে (কয়েকবার in-process retry, তারপর DLQ), নাকি একটা আলাদা retry topic pattern (মূল টপিক → retry-1 topic (delay সহ) → retry-2 topic → DLQ) — পরেরটা বেশি জটিল কিন্তু delayed retry (exponential backoff-এর মতো) সম্ভব করে, যেটা transient failure (যেমন একটা downstream API সাময়িক ডাউন) এর জন্য বেশি কার্যকর।
- DLQ monitoring — DLQ-তে মেসেজ জমা হওয়া মানেই কিছু ভুল হচ্ছে — এটা একটা alert-worthy metric, ignore করা উচিত না।
- Replay মেকানিজম — DLQ-এর মেসেজ, root cause ফিক্স হওয়ার পর, মূল topic-এ (বা directly consumer-এ) আবার পাঠানোর একটা প্রক্রিয়া থাকা উচিত (ম্যানুয়াল script বা টুল)।
রিয়েল-লাইফ উদাহরণ
আপনার payment-processing consumer-এ একটা DLQ ডিজাইন: প্রথম ব্যর্থতায় in-process ৩ বার retry (কয়েক সেকেন্ড ব্যবধানে, transient network glitch কভার করতে), তারপরও ব্যর্থ হলে DLQ-তে। একটা আলাদা on-call alert DLQ-তে নতুন মেসেজ এলে ট্রিগার হয় — টিম manually তদন্ত করে, root cause (হয়তো একটা downstream payment gateway আউটেজ) ফিক্স হলে, DLQ থেকে মেসেজগুলো আবার মূল টপিকে replay করা হয়।
ড্রব্যাক
- Retry topic pattern (delay-সহ multi-stage retry) বাস্তবায়ন জটিল — একাধিক এক্সট্রা টপিক, coordination logic দরকার।
- DLQ ordering guarantee হারায় — একটা মূল টপিকের ordered স্ট্রিম থেকে DLQ-তে যাওয়া মেসেজ তাদের আপেক্ষিক ক্রম রাখতে পারে না অন্য successfully-processed মেসেজের সাথে সম্পর্কে (DLQ নিজের মধ্যে নতুন ক্রমে আসে)।
Backpressure Handling
সমস্যা
Producer-এর produce rate consumer-এর processing rate-কে ক্রমাগত ছাড়িয়ে গেলে, lag অনির্দিষ্টকাল বাড়তে থাকবে — এটা একটা সিস্টেম-লেভেল imbalance যা শুধু 'অপেক্ষা করলে ঠিক হয়ে যাবে' না, সক্রিয়ভাবে হ্যান্ডেল করা দরকার।
কৌশলসমূহ
- Consumer-side autoscaling — lag metric-ভিত্তিক HPA (Kubernetes) দিয়ে consumer instance সংখ্যা স্বয়ংক্রিয়ভাবে বাড়ানো (partition সংখ্যা পর্যন্ত, তার বেশি কোনো লাভ নেই — Module 2)।
- Producer-side rate limiting — যদি consumer scale করা সম্ভব না হয় (downstream external system-এর নিজস্ব rate limit আছে, যেমন একটা SMS gateway API), producer-সাইডেই rate limit চাপানো ভালো — Kafka-তে unbounded lag জমতে দেওয়ার চেয়ে।
- Priority topic/tiering — সব ট্রাফিক সমান গুরুত্বপূর্ণ না; critical মেসেজ (OTP) আর non-critical (marketing) আলাদা টপিকে আলাদা priority সহ প্রসেস করলে backpressure-এর সময়ও critical path প্রভাবিত হয় না।
- Load shedding — চরম ক্ষেত্রে, কিছু non-critical মেসেজ ইচ্ছাকৃতভাবে বাদ দেওয়া (drop) সিস্টেমকে সম্পূর্ণ ধসে পড়া থেকে বাঁচাতে পারে — একটা কড়া কিন্তু কখনো কখনো প্রয়োজনীয় সিদ্ধান্ত।
রিয়েল-লাইফ উদাহরণ
আপনার SMS notification pipeline downstream SMS gateway-এর rate limit-এ (যেমন ১০০/sec) বাঁধা — consumer instance যতই বাড়ান, downstream limit-এর বেশি throughput সম্ভব না। এখানে সঠিক backpressure কৌশল হলো producer/consumer-সাইডে rate limiting মেনে চলা (গেটওয়ে limit অনুযায়ী), আর OTP-টাইপ critical মেসেজের জন্য একটা আলাদা high-priority topic রাখা যাতে marketing SMS backlog-এর কারণে OTP delay না হয়।
ড্রব্যাক
- Load shedding মানে ইচ্ছাকৃতভাবে কিছু ডেটা/ফাংশনালিটি হারানো — কোন মেসেজ drop করা 'গ্রহণযোগ্য' সেই সিদ্ধান্ত business stakeholder-দের সাথে আগে থেকে ঠিক করা থাকা উচিত, ইনসিডেন্টের মাঝে তাৎক্ষণিকভাবে না।
- Priority tiering ডিজাইন জটিলতা বাড়ায় (একাধিক topic, রাউটিং লজিক) — শুরু থেকেই এটা ওভার-ইঞ্জিনিয়ারিং হতে পারে যদি backpressure বাস্তবে কখনো সমস্যা না হয়।
ক্যাপস্টোন: 100M+/day Notification System-এর Kafka লেয়ার ডিজাইন
সমস্যা বিবৃতি
প্রতিদিন ১০০ মিলিয়ন+ নোটিফিকেশন (email, SMS, push) পাঠাতে হবে বিভিন্ন চ্যানেলে, বিভিন্ন priority-তে (OTP তাৎক্ষণিক, marketing ব্যাচড), বিভিন্ন downstream provider-এর মধ্য দিয়ে (প্রতিটার নিজস্ব rate limit), duplicate delivery এড়িয়ে, আর একটা downstream provider আউটেজেও বাকি সব স্বাভাবিকভাবে চলতে হবে।
ডিজাইন উপাদান (এই পুরো journey-র সংশ্লেষণ)
- Topic design — চ্যানেল অনুযায়ী আলাদা topic (
notification.email,notification.sms,notification.push), প্রতিটার ভেতরে priority অনুযায়ী আলাদা sub-topic (.otp,.transactional,.marketing) — Module 1-2। - Partition strategy —
user_idkey দিয়ে partition (per-user ordering দরকার হলে), অথবা যদি ordering দরকার না হয়, higher-entropy key (composite) দিয়ে hot partition এড়ানো — Module 2, Module 14-এর hot partition আলোচনা। - Producer config — OTP/transactional-এ
acks=all+ idempotent producer (durability critical), marketing-এacks=1গ্রহণযোগ্য (throughput priority) — Module 3। - Consumer scaling — provider rate limit অনুযায়ী consumer instance সংখ্যা ক্যাপ করা (partition সংখ্যাও সেই অনুযায়ী প্ল্যান করা) — Module 4, Module 14-এর backpressure।
- Retry topics + DLQ — প্রতিটা চ্যানেলের নিজস্ব retry chain + DLQ, provider outage handle করতে — Module 14।
- Fan-out — একটা single
notification.requestedইভেন্ট থেকে channel-specific topic-এ route করা একটা routing service/Kafka Streams দিয়ে (Module 12)। - Delivery semantics — at-least-once + idempotent consumer (provider-সাইড deduplication key দিয়ে, Module 9) — পুরো Transaction ব্যবহার না করেই duplicate delivery এড়ানো।
- Replication/durability —
replication.factor=3, min.insync.replicas=2critical topic-এ (Module 7)। - Multi-DC — যদি global user base থাকে, geo-partitioned topic বা active-active MM2 সেটআপ (Module 13)।
কেন এই ডিজাইন
প্রতিটা সিদ্ধান্ত একটা নির্দিষ্ট trade-off-এর সচেতন পছন্দ — OTP-তে durability+speed উভয় priority (তাই acks=all কিন্তু ছোট, দ্রুত topic), marketing-এ throughput priority (acks=1, বড় batch)। এই ধরনের differentiated treatment-ই একটা বাস্তব large-scale সিস্টেম ডিজাইনের মূল দক্ষতা — সব কিছুতে একই কনফিগ ব্যবহার করা (blanket ভাবে সবচেয়ে কড়া বা সবচেয়ে ঢিলা সেটিং) একটা ওভার-সিম্প্লিফিকেশন ভুল।
FAANG-স্টাইল System Design Mock (Kafka-heavy)
মক ইন্টারভিউ ফরম্যাট
একটা টিপিক্যাল সিস্টেম ডিজাইন ইন্টারভিউ-এর কাঠামো অনুসরণ করে নিজেকে (বা একজন সহকর্মীকে) টেস্ট করুন:
- Requirements clarification (৫ মিনিট) — scale (100M/day = গড়ে ~১১৫৭/sec, কিন্তু peak-এ ৫-১০x), latency requirement (OTP <৫ সেকেন্ড, marketing মিনিট-স্কেল গ্রহণযোগ্য), consistency requirement (duplicate কতটা সহনীয়)।
- High-level architecture (১৫ মিনিট) — topic design, producer/consumer flow, downstream integration — বোর্ডে (বা কাগজে) আঁকুন।
- Deep dive (২০ মিনিট) — ইন্টারভিউয়ার সাধারণত একটা নির্দিষ্ট অংশে গভীরে যেতে বলবে: "partition strategy ব্যাখ্যা করুন", "একটা provider outage হলে কী হবে?", "duplicate কীভাবে এড়াবেন?" — এই journey-র Module 2, 7, 9, 14-এর জ্ঞান এখানে সরাসরি কাজে আসবে।
- Trade-offs আর সীমাবদ্ধতা (৫-১০ মিনিট) — কোনো ডিজাইনই নিখুঁত না; সচেতনভাবে কোন trade-off নিয়েছেন, আর ভবিষ্যতে scale বাড়লে কী বদলাবে তা আলোচনা করুন।
সাধারণ ফলো-আপ প্রশ্ন (এই journey থেকে সরাসরি)
- "একটা broker crash করলে কী হয় আপনার ডিজাইনে?" (Module 7-8)
- "একটা consumer group কীভাবে scale করবেন 10x traffic-এ?" (Module 2, 4, 13)
- "Exactly-once দরকার এখানে, নাকি at-least-once + idempotency যথেষ্ট?" (Module 9)
- "Schema বদলাতে হলে (নতুন notification channel যোগ) কীভাবে backward compatibility বজায় রাখবেন?" (Module 10)
রিয়েল-লাইফ উদাহরণ
একজন ইন্টারভিউয়ার যদি জিজ্ঞেস করেন "কেন Kafka, RabbitMQ না" — সরাসরি Module 1-এর message queue vs log আলোচনায় ফিরে গিয়ে উত্তর দিতে পারবেন: একাধিক independent consumer (email, SMS, push routing, analytics — সবাই একই notification.requested স্ট্রিম থেকে independently পড়তে পারে), আর replay ক্ষমতা (একটা নতুন notification channel যোগ হলে পুরনো ইতিহাস থেকে backfill করা সম্ভব) — এই দুটোই RabbitMQ-তে স্বাভাবিকভাবে পাওয়া যেত না।
চূড়ান্ত চ্যালেঞ্জ
এই পুরো ক্যাপস্টোন ডিজাইনটা একজন সহকর্মী বা বন্ধুর সামনে ৪৫ মিনিটে বোর্ডে এঁকে ব্যাখ্যা করুন, প্রশ্ন নিন — এটাই এই journey-র চূড়ান্ত পরীক্ষা।
ইন্টারভিউ প্রশ্ন — Module 14
প্র.১ — Consumer lag স্পাইক ডিবাগ করার সময় প্রথম কী চেক করবেন? উত্তর: lag সব partition-এ সমান নাকি একটা নির্দিষ্ট partition-এ concentrated তা দেখা — সমান হলে এটা capacity/traffic সমস্যা, একটাতে concentrated হলে সম্ভবত poison pill বা hot key সমস্যা।
প্র.২ — Dead Letter Queue-এর মূল উদ্দেশ্য কী, আর এটা ব্যবহার করার সময় কী ভুল করা উচিত না? উত্তর: একটা বারবার-ব্যর্থ-হওয়া মেসেজকে আলাদা করে বাকি প্রসেসিং চালিয়ে যাওয়া সম্ভব করা (পুরো pipeline আটকে না রাখা)। ভুল যা করা উচিত না: DLQ-কে monitor/reprocess না করা — তাহলে এটা কার্যত সাইলেন্ট ডেটা লসের মতোই।
প্র.৩ — Hot partition সমস্যায় partition সংখ্যা বাড়ানো কেন কাজ করে না সবসময়? উত্তর: যদি একটা নির্দিষ্ট key-ই সমস্যার কারণ (সব ট্রাফিক সেই key-তে concentrated), partition বাড়ালেও সেই key একই একটা partition-এ যেতে থাকবে (শুধু hash সূত্র বদলাবে, সমস্যা থেকেই যাবে) — মূল সমাধান key distribution বদলানো (composite key)।
প্র.৪ — Backpressure হ্যান্ডল করার সময় কেন শুধু consumer autoscale করাই যথেষ্ট নাও হতে পারে? উত্তর: যদি bottleneck downstream external system-এ থাকে (যেমন একটা SMS gateway-এর নিজস্ব rate limit), consumer instance বাড়ালেও downstream-এর ক্যাপাসিটি বাড়ে না — rate limiting বা priority tiering-এর মতো ভিন্ন কৌশল দরকার।
প্র.৫ — একটা 100M/day notification system ডিজাইন করতে বললে, OTP আর marketing notification-এর জন্য কেন ভিন্ন producer কনফিগ ব্যবহার করবেন? উত্তর: OTP-তে durability এবং low-latency দুটোই priority (acks=all, ছোট batch/কম linger.ms)। Marketing-এ throughput priority, সামান্য delay/occasional loss গ্রহণযোগ্য (acks=1 বা তার কাছাকাছি, বড় batch/বেশি linger.ms)। একই কনফিগ সব জায়গায় ব্যবহার করা একটা ডিজাইন ভুল।
প্র.৬ — ISR shrink আর consumer lag স্পাইক — এই দুই ইনসিডেন্টের root cause খোঁজার পদ্ধতিতে মূল পার্থক্য কী? উত্তর: ISR shrink-এর তদন্ত broker-level (disk I/O, network, GC pause) — এটা replication/durability সমস্যা। Consumer lag spike-এর তদন্ত মূলত consumer/application-level (processing rate, poison pill, rebalance storm) এবং producer-side traffic pattern-এর দিকে যায় — দুটো ভিন্ন স্তরের সমস্যা, ভিন্ন metric/লগ দেখতে হয়।
কর্নার কেস — Module 14
- একই সময়ে একাধিক ইনসিডেন্ট ঘটলে (যেমন ISR shrink আর consumer lag spike একসাথে) — root cause একটাই হতে পারে (যেমন একটা broker-এর disk সমস্যা একসাথে replication আর consumption দুটোই প্রভাবিত করছে), কিন্তু আলাদা আলাদা alert হিসেবে দেখা দিলে টিম দুটোকে ভুলবশত ভিন্ন সমস্যা ভেবে সময় নষ্ট করতে পারে — correlated alerting/root-cause-analysis টুলিং গুরুত্বপূর্ণ।
- DLQ-তে মেসেজ জমতে থাকা কিন্তু কেউ alert সেট করেনি — মাসের পর মাস silently মেসেজ হারিয়ে যেতে পারে (কার্যত), শুধু একটা audit বা customer complaint-এর মাধ্যমে ধরা পড়ে অনেক দেরিতে।
- Load shedding সিদ্ধান্ত ইনসিডেন্টের মাঝে তাড়াহুড়ো করে নেওয়া — কোন মেসেজ 'ড্রপযোগ্য' তা আগে থেকে ঠিক করা না থাকলে, ইনসিডেন্ট commander ভুল সিদ্ধান্ত নিতে পারেন চাপের মধ্যে (যেমন ভুলে OTP marketing-এর সাথে একই priority-তে ড্রপ করে ফেলা)।
- ক্যাপস্টোন ডিজাইনে single point of failure ভুলে যাওয়া — যেমন সব notification channel একটাই routing service-এর ওপর নির্ভরশীল হলে, সেই service ডাউন হলে পুরো সিস্টেম থেমে যায় — একটা ভালো ডিজাইন রিভিউ এই ধরনের bottleneck আগেই identify করবে।
- মক ইন্টারভিউতে শুধু 'happy path' ডিজাইন করে failure scenario আলোচনা না করা — বাস্তব ইন্টারভিউ এবং বাস্তব প্রোডাকশন উভয় জায়গাতেই failure mode (broker down, provider outage, poison pill) নিয়ে চিন্তা না করা একটা অসম্পূর্ণ ডিজাইনের সবচেয়ে সাধারণ লক্ষণ — এই journey-র Module 14-ই এই কারণে শেষে রাখা হয়েছে, যাতে failure-thinking শেষ takeaway হয়।