সেশন ১০

Event-Driven Architecture - কাউকে না ডেকে সবাইকে জানানো

এই সেশনের লক্ষ্য: একটা ঘটনা অনেক service এর কাছে পৌঁছানোর পথ বোঝা, যাতে যে service ঘটনাটা ঘটায় তাকে বাকিদের চিনতে না হয়। Command আর event এর পার্থক্য, pub/sub আর topic, Kafka এর partition, offset আর consumer group, key দিয়ে ক্রম ঠিক রাখা, choreography, event এর format বদলানো, event sourcing, আর কখন event ব্যবহার করা উচিত নয়, এগুলো বোঝা।

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

সেশন ০৮ এ আমরা queue শিখেছি। একটা job queue তে যায়, আর একজন worker সেটা তুলে নিয়ে কাজ করে। কিন্তু একটা ঘটনার খবর যদি একসাথে পাঁচ জায়গায় পৌঁছাতে হয়? আর ছয় মাস পরে আরো তিনটা নতুন জায়গা যোগ হয়?

এই সেশনে আমরা দেখবো, একটা service কীভাবে শুধু “এটা ঘটেছে” বলে চুপ করে যায়, আর যার দরকার সে নিজেই খবরটা নিয়ে নেয়। এই ধরনের design কে বলে event-driven architecture। শেষে আপনি বুঝবেন, Kafka কেন এত জনপ্রিয়, আর কোথায় এই পথে না যাওয়াই ভালো।

  • ১ Order service নিজেই সবাইকে call করলে সমস্যা কোথায়? (tight coupling)
  • ২ "এটা করুন" আর "এটা ঘটেছে" এর মধ্যে পার্থক্য কী? (command, event)
  • ৩ একটা event অনেক service এর কাছে পৌঁছায় কীভাবে? (pub/sub, topic)
  • ৪ Kafka কেন queue নয়, একটা log? (partition, offset, consumer group, retention, replay)
  • ৫ একই order এর event গুলো ঠিক ক্রমে পৌঁছাবে তো? (ordering per key)
  • ৬ কেউ নির্দেশ না দিলে পুরো delivery flow চলে কীভাবে? (choreography)
  • ৭ Event এর format বদলালে কী হয়? (schema evolution, event-carried state)
  • ৮ শুধু event জমিয়ে রাখলে কেমন হয়? (event sourcing)
  • ৯ কখন event ব্যবহার করবেন না?

০১ Order service নিজেই সবাইকে call করলে সমস্যা কোথায়?

রমজানের এক বিকেল, ৫টা ৪০। ইফতারের আর বিশ মিনিট বাকি। “ঝটপট” নামের food delivery app এ প্রতি সেকেন্ডে প্রায় ৫০টা order ঢুকছে। রুবেল অফিসে বসে dashboard দেখছে, আর তার বুক ধুকপুক করছে। হঠাৎ checkout এর graph লাল হয়ে গেলো। SMS gateway ধীর হয়ে গেছে, আর সেই কারণে মানুষ order ই দিতে পারছেন না। ঠিক তখন Slack এ loyalty team এর মেসেজ এলো: “ভাই, order হলে আমাদের service কেও একটা call দিয়েন।”

রুবেল order service এর দায়িত্বে। দুই বছর আগে এই service এর কাজ ছিলো একটাই: order টা database এ লেখা। তারপর একে একে অনুরোধ এসেছে। Restaurant কে জানাতে হবে। Rider খুঁজতে হবে। Customer কে SMS দিতে হবে। Loyalty point দিতে হবে। Analytics এ হিসাব পাঠাতে হবে। প্রতিটা অনুরোধে রুবেল order service এ আরেকটা call যোগ করেছে:

CHECKOUT Order service একটার পর একটা call customer অপেক্ষা করছে ১. Restaurant service 80ms ২. Rider assignment 150ms ৩. SMS gateway 600ms · খারাপ দিনে 5s ৪. Loyalty points 50ms ৫. Analytics 70ms মোট: 40 (DB) + 80 + 150 + 600 + 50 + 70 ≈ 990ms যেকোনো একটা fail করলে পুরো order fail · পাঁচটা 99.9% service মিলে মাত্র 99.5%
আগের design: order service নিজেই পাঁচটা service কে পরপর call করে, আর customer পুরো সময়টা অপেক্ষা করে। সবচেয়ে ধীর SMS gateway পুরো checkout কে ধীর করে দেয়। যেকোনো একটা down হলে order টাই fail করে।

এই design এ তিনটা সমস্যা লুকিয়ে আছে:

  1. সবচেয়ে ধীর service সবাইকে ধীর করে: Order service পাঁচটা call একটার পর একটা করে। সব মিলিয়ে প্রায় 990ms লাগে, আর এর বেশিরভাগটাই SMS gateway এর। খারাপ দিনে SMS gateway 5 সেকেন্ড পর timeout দেয়, তখন customer ছয় সেকেন্ড ধরে spinner দেখেন।
  2. একজন down হলে order ই হয় না: প্রতিটা service আলাদাভাবে 99.9% সময় ঠিক থাকে। কিন্তু order সফল হতে পাঁচটাকেই একসাথে ঠিক থাকতে হয়। ০.৯৯৯ কে পাঁচবার গুণ করলে আসে প্রায় ০.৯৯৫, মানে ৯৯.৫%। দিনে দেড় লাখ order হলে প্রায় ৭৫০টা order শুধু এই কারণে fail করে। অথচ SMS না গেলেও খাবারটা পৌঁছাতে কোনো বাধা ছিলো না।
  3. প্রতিটা নতুন team এর জন্য order service বদলাতে হয়: Loyalty team এর জন্য নতুন code, নতুন deploy, নতুন ঝুঁকি। পরের মাসে fraud team আসবে। Order service এর মূল কাজ ছোট, কিন্তু তাকে বাকি সবার খবর রাখতে হয়।

একটা service যখন অন্য অনেক service কে নাম ধরে চেনে আর তাদের উপর নির্ভর করে, তখন তাকে বলে tight coupling। একটার সমস্যা তখন সবার সমস্যা হয়ে যায়।

আসল সমস্যা

Order service এর আসল কাজ order নেওয়া। SMS গেলো কিনা, point যোগ হলো কিনা, সেগুলো তার মাথাব্যথা হওয়ার কথা না। কিন্তু এই design এ order service কে সবার কাজ শেষ হওয়া পর্যন্ত অপেক্ষা করতে হয়, আর নতুন কেউ এলে নিজের code বদলাতে হয়।

০২ “এটা করুন” আর “এটা ঘটেছে” এর মধ্যে পার্থক্য কী?

রুবেলের পুরোনো code এ order service প্রত্যেককে একটা করে নির্দেশ পাঠায়: “rider খুঁজুন”, “SMS পাঠান”, “point দিন”। এমন নির্দেশকে বলে command।

এবার একটু অন্যভাবে ভাবুন। Order service শুধু একটা ঘোষণা দিলো: “Order #5821 এইমাত্র হয়েছে।” কাকে কী করতে হবে, সে বললো না। যার দরকার, সে নিজেই শুনে নিজের কাজ করবে। এমন ঘোষণাকে বলে event।

COMMAND EVENT Order service send: AssignRider Rider service শুধু একজন "এই কাজটা করুন" (ভবিষ্যৎ) প্রাপক আগে থেকেই ঠিক করা কাজ না হলে sender জানতে চায় Order service publish: OrderPlaced Restaurant SMS Loyalty "এটা ঘটে গেছে" (অতীত) যে খুশি শুনতে পারে, কেউ না শুনলেও চলে Sender জানে না কে শুনলো
Command একজন নির্দিষ্ট প্রাপককে বলে "এটা করুন"। Event শুধু জানায় "এটা ঘটে গেছে"। কে শুনবে, সেটা event পাঠানো service জানেও না, জানার দরকারও নেই।
CommandEvent
নামের ধরনক্রিয়া, ভবিষ্যৎ: AssignRiderঅতীত কাল: OrderPlaced
মানে“এই কাজটা করুন”“এই ঘটনাটা ঘটে গেছে”
কতজন পায়ঠিক একজন, যাকে আগে থেকে ঠিক করাযে খুশি, শূন্য জনও হতে পারে
পাঠানো service কি প্রাপককে চেনে?হ্যাঁ, নাম ধরে চেনেনা, চেনার দরকার নেই
প্রাপক কি “না” বলতে পারে?হ্যাঁ, কাজটা fail করতে পারেনা, ঘটনাটা ঘটেই গেছে

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

নাম দেখেই চেনা যায়

Event এর নাম সবসময় অতীত কালে হয়: OrderPlaced, PaymentReceived, RiderAssigned। নামটা অতীত কালে লিখতে গিয়ে আটকে গেলে বুঝবেন, আপনি আসলে একটা command পাঠাতে চাইছেন।

০৩ একটা event অনেক service এর কাছে পৌঁছায় কীভাবে?

সেশন ০৮ এর queue এ একটা নিয়ম ছিলো: একটা job একজন worker ই পায়। Worker রা একই কাজের জন্য একে অপরের সাথে প্রতিযোগিতা করে। Video encode এর জন্য এটা ঠিক আছে, কারণ একটা video একবার encode করলেই যথেষ্ট।

কিন্তু রুবেলের OrderPlaced event টা restaurant, rider, SMS, loyalty আর analytics, সবারই দরকার। এখানে চাই উল্টো নিয়ম: প্রত্যেকে নিজের আলাদা কপি পাবে। এই নিয়মের নাম pub/sub (publish/subscribe)।

QUEUE PUB/SUB Producer QUEUE job #5821 Worker 1 পায়নি Worker 2 #5821 পেলো Worker 3 পায়নি একটা message, একজন worker Order service TOPIC orders Restaurant কপি পেলো SMS কপি পেলো Analytics কপি পেলো একটা event, প্রত্যেকে নিজের কপি
বাম দিকে সেশন ০৮ এর queue: তিনজন worker একই কাজের জন্য প্রতিযোগিতা করে, আর একটা job যায় শুধু একজনের কাছে। ডান দিকে pub/sub: topic এ একটা event এলে প্রত্যেক subscriber নিজের আলাদা কপি পায়।

Pub/sub এ তিনটা শব্দ বারবার আসবে:

Queue (সেশন ০৮)Pub/sub
একটা message কতজন পায়একজন workerপ্রত্যেক subscriber একটা করে কপি
কী কাজে লাগেভারী কাজ ভাগ করে দেওয়াএকটা ঘটনার খবর অনেককে দেওয়া
নতুন consumer এলেকাজ ভাগ হয়, প্রত্যেকে কম পায়সে নিজের পুরো কপি পায়
উদাহরণvideo encode, PDF বানানোOrderPlaced, PaymentReceived

রুবেল এবার order service কে বদলে দিলো। Order service এখন order টা নিজের database এ লেখে, orders topic এ একটা OrderPlaced event publish করে, আর customer কে উত্তর দিয়ে দেয়। বাকি সবাই নিজেরাই topic থেকে event পড়ে নেয়:

POST /orders ~50ms এ উত্তর Customer: order দিলো OrderPlaced publish ~10ms Order service DB তে লেখে, তারপর event publish TOPIC: orders Kafka Restaurant order দেখায় Rider rider খোঁজে SMS SMS পাঠায় Loyalty পয়েন্ট দেয় Analytics হিসাব রাখে Fraud (নতুন) শুধু subscribe Checkout: ~990ms থেকে ~50ms SMS down? Order তবুও হয়
নতুন design: order service শুধু নিজের DB তে লেখে আর একটা OrderPlaced event publish করে। বাকি ছয়টা service নিজেরাই topic থেকে event পড়ে নেয়। নতুন Fraud team এর জন্য order service এর code এ একটা লাইনও বদলাতে হয়নি।

ফলাফলটা দেখুন। Checkout এখন 990ms এর বদলে প্রায় 50ms এ শেষ হয়। SMS gateway আধা ঘণ্টা down থাকলেও order আসতে থাকে। SMS service ফিরে এসে জমে থাকা event গুলো পড়ে, আর দেরিতে হলেও SMS পাঠিয়ে দেয়। সবচেয়ে মজার ঘটনা ঘটলো পরের মাসে। Fraud team এসে শুধু orders topic এ subscribe করলো। রুবেলকে order service এর একটা লাইনও বদলাতে হলো না।

মূল পরিবর্তন

আগে order service জানতো কে কে তার খবর চায়। এখন সে জানে না, জানার দরকারও নেই। সে শুধু ঘটনাটা ঘোষণা করে। কে শুনবে, সেটা শ্রোতারা নিজেরা ঠিক করে। এতে নতুন subscriber যোগ করতে publisher কে বদলাতে হয় না।

০৪ Kafka কেন queue নয়, একটা log?

রুবেলের team topic চালানোর জন্য Kafka বেছে নিলো। সেশন ০৮ এ এক লাইনে বলা হয়েছিলো, Kafka ঠিক queue নয়, একটা log। এবার কথাটার মানে খুলে দেখি।

Log মানে একটা খাতা

এখানে log মানে error log না। Log মানে এমন একটা খাতা, যেখানে লেখা শুধু শেষে যোগ হয়। আগের কোনো লাইন মোছা বা বদলানো হয় না। দোকানের হিসাবের খাতার মতো: নতুন বিক্রি হলে নিচে নতুন লাইন লেখা হয়।

Queue আর log এর সবচেয়ে বড় পার্থক্য হলো, পড়লে কী হয়। Queue তে worker ACK পাঠালে message মুছে যায়। Kafka তে কেউ পড়লেও event মোছে না। খাতাটা যেমন ছিলো তেমনই থাকে। শুধু যে পড়ছে, সে মনে রাখে কোন লাইন পর্যন্ত পড়া হয়েছে।

TOPIC: orders · PARTITION 0 7 দিন পার, মুছে যাবে প্রতিটা ঘর একটা event, ঘরের নম্বরটাই offset নতুন Producer শেষে যোগ 0 #5812 1 #5813 2 #5814 3 #5815 4 #5816 5 #5817 6 #5818 7 #5819 8 #5821 Loyalty (নতুন) offset 2 থেকে replay Analytics offset 5 · পিছিয়ে Restaurant offset 8 · up to date পড়লেও event মোছে না; প্রতিটা group নিজের offset মনে রাখে
Kafka এর একটা partition আসলে একটা খাতা: নতুন event শুধু শেষে যোগ হয়, আর প্রতিটা event এর একটা নম্বর থাকে (offset)। তিনটা consumer group একই খাতা পড়ছে, কিন্তু প্রত্যেকে নিজের জায়গা থেকে। নতুন Loyalty service চাইলে সবচেয়ে পুরোনো রাখা event থেকে আবার পড়তে (replay) পারে।

এই ছবির চারটা শব্দ চিনে নিন:

Replay কেন এত কাজের

ছবিতে নতুন Loyalty service টা দেখুন। সে আজই চালু হলো, কিন্তু সে চায় গত সাত দিনের order এর point ও দিতে। Queue হলে এটা অসম্ভব ছিলো, কারণ পুরোনো message গুলো অনেক আগেই মুছে গেছে। Kafka তে Loyalty শুধু বলে, “আমি সবচেয়ে পুরোনো offset থেকে পড়বো।” সাত দিনের সব order সে আবার পায়।

Replay আরেক জায়গাতেও খুব কাজে লাগে। ধরুন analytics service এর code এ একটা bug ছিলো, আর দুই দিন ধরে ভুল হিসাব হয়েছে। Bug ঠিক করে offset দুই দিন পিছিয়ে দিলেই সব হিসাব আবার ঠিকঠাক তৈরি হয়।

খরচ কেমন?

রুবেল একটা হিসাব করলো। দিনে দেড় লাখ order। প্রতি order এ গড়ে ৫টা event (placed, accepted, rider assigned, picked up, delivered)। প্রতিটা event প্রায় 1 KB।

16 GB একটা সাধারণ laptop এর disk এও ধরে যায়। সাত দিনের পুরো ইতিহাস রাখার খরচ তাই খুবই কম।

Consumer group: কাজ ভাগ, আবার সবাই পুরোটা পায়

ইফতারের আগে প্রতি সেকেন্ডে ৫০টা order আসে। Rider service এর একটা machine এত event সামলাতে পারে না। তাই rider team তিনটা machine চালায়। কিন্তু একই event তিনটা machine পেলে একই order এর জন্য তিনজন rider পাঠানো হবে।

এর সমাধান consumer group। একই কাজের জন্য চলা কয়েকটা consumer একটা group এর নামে চলে। Kafka তখন partition গুলো group এর সদস্যদের মধ্যে ভাগ করে দেয়:

GROUP: rider TOPIC: orders GROUP: sms Partition 0 ~17 event/s Partition 1 ~17 event/s Partition 2 ~17 event/s Rider C1 P0 পড়ে Rider C2 P1 পড়ে Rider C3 P2 পড়ে SMS C1 P0 আর P1 পড়ে SMS C2 P2 পড়ে একটা group এর ভেতরে: প্রতিটা partition পড়ে ঠিক একজন consumer আলাদা group হলে: প্রত্যেক group পুরো topic এর সব event পায়
একই topic দুইটা consumer group পড়ছে। Rider group এ তিনজন consumer, তাই প্রত্যেকে একটা করে partition পায়। SMS group এ দুইজন, তাই একজনকে দুইটা partition সামলাতে হয়। দুইটা group ই সব event পায়, কারণ group গুলো একে অপরের থেকে আলাদা।

নিয়ম দুটো:

  1. একটা group এর ভেতরে প্রতিটা partition ঠিক একজন consumer পড়ে। তাই একটা event group এর মধ্যে একবারই process হয়। এটা queue এর মতো আচরণ।
  2. আলাদা group হলে প্রত্যেক group পুরো topic পায়। Rider group আর SMS group দুটোই সব event পায়। এটা pub/sub এর মতো আচরণ।

একটা মজার সীমাও আছে। Partition তিনটা হলে group এ চতুর্থ consumer যোগ করে লাভ নেই। সে কোনো partition পাবে না, অলস বসে থাকবে। তাই topic বানানোর সময়েই ভেবে নিতে হয়, ভবিষ্যতে সর্বোচ্চ কতজন consumer একসাথে লাগতে পারে।

Mental model

Topic হলো একটা বই, আর partition গুলো বইয়ের অধ্যায়। প্রতিটা consumer group আলাদা একটা পাঠক দল। প্রত্যেক দল পুরো বইটা পড়ে, কিন্তু দলের ভেতরে অধ্যায়গুলো ভাগ করে নেয়। আর প্রত্যেক দল নিজের bookmark (offset) রাখে, তাই কেউ কারো গতির উপর নির্ভর করে না।

Kafka এর delivery নিয়েও সেশন ০৮ এর কথাটা খাটে। Consumer event process করে তারপর offset সেভ করে। মাঝখানে crash করলে একই event আবার আসে। তাই Kafka এর consumer কেও idempotent হতে হয়: একই OrderPlaced দুইবার এলে loyalty point যেন দুইবার যোগ না হয়।

০৫ একই order এর event গুলো ঠিক ক্রমে পৌঁছাবে তো?

এক সন্ধ্যায় একটা অদ্ভুত অভিযোগ এলো। একজন customer ৭:৪০:০১ এ order দিয়ে আট সেকেন্ড পরেই cancel করেছেন। তবু একজন rider রেস্টুরেন্টে গিয়ে হাজির। খাবার নেই, customer নেই, rider এর কুড়ি মিনিট নষ্ট।

রুবেল খুঁজে দেখলো, event দুটো দুই আলাদা partition এ গিয়েছিলো:

KEY ছাড়া ১. OrderPlaced #5821 · 7:40:01 ২. OrderCancelled #5821 · 7:40:09 Partition 0 সামনে 300 event জমে আছে Partition 1 খালি, সাথে সাথে পড়া হয় Rider service আগে Cancelled পড়ে, পরে Placed ফল: বাতিল হওয়া order এর জন্য rider রওনা দেয় KEY: order_id ১. OrderPlaced key: 5821 ২. OrderCancelled key: 5821 Partition 2 hash(5821) % 3 = 2, দুটোই এখানে একই partition, তাই Placed সবসময় Cancelled এর আগে পড়া হয় ফল: rider service জানে order টা বাতিল, কেউ রওনা দেয় না
ওপরে: key না দিলে Kafka event গুলো ছড়িয়ে দেয়। একই order এর দুইটা event দুই partition এ যায়, আর পড়ার ক্রম উল্টে যায়। নিচে: order_id কে key বানালে একই order এর সব event একই partition এ যায়, আর partition এর ভেতরে ক্রম কখনো বদলায় না।

Kafka একটা কথাই নিশ্চিত করে: একটা partition এর ভেতরে event গুলো লেখার ক্রমেই পড়া হয়। কিন্তু দুই partition এর মধ্যে কোনো ক্রমের নিশ্চয়তা নেই। Partition 0 এ ৩০০টা event জমে থাকলে সেখানকার event দেরিতে পড়া হবে। Partition 1 খালি থাকলে সেখানকার event আগে পড়া হবে।

সমাধানটা সহজ। Event publish করার সময় একটা key দিতে হয়। Kafka key টাকে hash করে (একটা সংখ্যায় বদলে), partition এর সংখ্যা দিয়ে ভাগ করে, আর ভাগশেষ দেখে partition বেছে নেয়। সেশন ০৫ এ shard key দিয়ে ঠিক এভাবেই data ভাগ করেছিলেন। একই key সবসময় একই partition এ যায়। তাই order_id কে key বানালে order #5821 এর সব event একই partition এ, একই ক্রমে থাকে।

খেয়াল করুন, আমরা সব order এর মধ্যে ক্রম চাইনি। #5821 আর #5822 কোনটা আগে process হলো, তাতে কিছু যায় আসে না। আমাদের দরকার শুধু একই order এর ভেতরের ক্রম। এই ধারণার নাম ordering per key।

সাবধান

Topic এর partition সংখ্যা পরে বাড়ালে hash এর ভাগশেষ বদলে যায়। তখন একই order এর পুরোনো event এক partition এ, আর নতুন event অন্য partition এ চলে যেতে পারে। তাই partition সংখ্যা শুরুতেই একটু বেশি ধরে রাখুন। আর key এমন বাছুন, যাতে একটা key তে অস্বাভাবিক বেশি event না আসে (সেশন ০৫ এর hot key সমস্যা)।

০৬ কেউ নির্দেশ না দিলে পুরো delivery flow চলে কীভাবে?

একটা order এ শুধু OrderPlaced হয় না। Restaurant order টা accept করে। তারপর rider খোঁজা হয়। Rider পেলে customer কে জানানো হয়। এই ধাপগুলো একটার পর একটা হতে হবে।

পুরোনো design এ order service সবাইকে নির্দেশ দিতো, একজন conductor এর মতো। নতুন design এ কেউ কাউকে নির্দেশ দেয় না। প্রতিটা service শুধু নিজের পছন্দের event শোনে, নিজের কাজ করে, আর কাজ শেষে নিজের একটা নতুন event ছাড়ে:

OrderPlaced ১ Order service order DB তে লেখে OrderPlaced +0s Loyalty একই event শোনে ২ Restaurant service রান্নাঘরে টিকিট, তারপর accept OrderAccepted +40s ৩ Rider service কাছের ফাঁকা rider খোঁজে RiderAssigned +90s ৪ Notification service SMS: "আপনার rider আসছেন" কোনো boss নেই: প্রত্যেকে event শোনে, নিজের কাজ করে, নতুন event ছাড়ে
Choreography: পুরো delivery flow টা কেউ একা চালায় না। Restaurant service OrderPlaced শুনে নিজের কাজ করে আর OrderAccepted ছাড়ে। Rider service সেটা শুনে rider খোঁজে। প্রতিটা ধাপ আগের ধাপের event এর জবাবে নিজে থেকেই শুরু হয়।

এই পদ্ধতির নাম choreography। বিয়েবাড়ির নাচের কথা ভাবুন। গান বাজলে প্রত্যেকে জানে কোন তালে তার পা ফেলতে হবে। মঞ্চে দাঁড়িয়ে কেউ একে একে সবাইকে ডেকে বলে দিচ্ছে না, কখন কার পালা। প্রত্যেকে গানের সংকেত শুনে নিজের অংশটা করে।

এর উল্টো পদ্ধতি হলো orchestration, যেখানে একটা কেন্দ্রীয় service প্রত্যেককে ধাপে ধাপে command পাঠায়। দুটোর তুলনা:

ChoreographyOrchestration
কে চালায়কেউ না, প্রত্যেকে event শুনে নিজে চলেএকটা কেন্দ্রীয় service
নতুন ধাপ যোগ করানতুন service শুধু event শোনেকেন্দ্রীয় service এর code বদলাতে হয়
পুরো flow দেখাকঠিন, flow টা অনেক service এ ছড়ানোসহজ, একটা জায়গাতেই লেখা
কোথায় মানায়ছোট, সরল flow (৩-৪ ধাপ)লম্বা, অনেক শর্তওয়ালা flow

Choreography এর সবচেয়ে বড় downside হলো, পুরো ছবিটা কারো হাতে থাকে না। রুবেলের team এ একবার এমন হয়েছিলো: rider assign হচ্ছে না, কিন্তু কোন service এর কোন event আটকে আছে, তা খুঁজতে দুই ঘণ্টা লেগেছে। তাই প্রতিটা event এ order_id রাখুন, যাতে একটা order এর পুরো যাত্রা পরে খুঁজে বের করা যায়।

আরেকটা প্রশ্ন থেকে যায়: মাঝপথে কোনো ধাপ fail করলে কী হবে? যেমন, restaurant accept করলো, কিন্তু কোনো rider পাওয়া গেলো না। তখন আগের ধাপগুলো উল্টাতে হয়। এর জন্য saga নামের একটা আলাদা কৌশল আছে, যেটা আমরা সেশন ১৭ তে বিস্তারিত দেখবো।

০৭ Event এর format বদলালে কী হয়?

Schema evolution: পুরোনো পাঠককে না ভাঙিয়ে বদলানো

ছয় মাস পরে finance team চাইলো, OrderPlaced event এ payment method ও থাকুক (bKash, card, নাকি cash)। Event এর ভেতরে কোন কোন field থাকবে আর সেগুলো কোন ধরনের, এই কাঠামোকে বলে schema। আর সময়ের সাথে schema নিরাপদে বদলানোর কাজকে বলে schema evolution।

সমস্যাটা এখানে। Event এর publisher একজন, কিন্তু পাঠক ছয়জন। ছয়টা team একই দিনে deploy করবে, এটা আশা করা যায় না। তাই নতুন event পুরোনো পাঠকের কাছেও পড়ার মতো থাকতে হয়, আর পুরোনো event নতুন পাঠকের কাছেও। মনে রাখবেন, Kafka তে সাত দিনের পুরোনো event ও replay হতে পারে।

// v1: শুরুর event
{ "type": "OrderPlaced", "version": 1,
  "order_id": 5821, "total": 450 }

// v2: নতুন field যোগ করা (নিরাপদ)
// পুরোনো consumer অচেনা field টা এড়িয়ে যায়।
// নতুন consumer field না পেলে default ধরে: "cash"
{ "type": "OrderPlaced", "version": 2,
  "order_id": 5821, "total": 450,
  "payment_method": "bkash" }

// ভুল পথ: field এর নাম বদলানো
// পুরোনো consumer "order_id" খোঁজে, পায় না, crash করে।
{ "type": "OrderPlaced", "version": 3,
  "orderId": 5821, "amount": 450 }

নিরাপদ আর বিপজ্জনক বদলের তালিকা ছোট:

বড় team গুলো এজন্য schema registry ব্যবহার করে। এটা একটা আলাদা service, যেখানে প্রতিটা event এর schema জমা থাকে। কেউ বিপজ্জনক বদল publish করতে চাইলে registry সেটা আটকে দেয়।

Event-carried state: event এ কতটুকু তথ্য রাখবেন

প্রথম দিকে রুবেলের event এ শুধু order_id থাকতো। SMS service কে customer এর phone number জানতে order service কে আবার call করতে হতো। Restaurant service কে খাবারের তালিকা জানতে আবার call। এই ছোট event কে বলে thin event।

THIN EVENT FAT EVENT OrderPlaced শুধু order_id: 5821 Restaurant SMS Loyalty Order service প্রত্যেকে GET /orders/5821 50 event/s, 5 consumer: 250 ফিরতি call/s Order service down হলে সবাই আটকে যায় OrderPlaced order_id: 5821, total: 450 টাকা phone, address, items Restaurant SMS Loyalty ফিরতি call লাগে না Order service down হলেও কাজ চলে Downside: event বড় (~2 KB), data কপি হয় phone এর মতো ব্যক্তিগত data অনেক জায়গায় ছড়ায়
বাম দিকে thin event: শুধু order_id আসে, তাই প্রতিটা consumer বাকি তথ্যের জন্য order service কে আবার call করে। ডান দিকে fat event (event-carried state): দরকারি সব data event এর ভেতরেই থাকে, তাই কাউকে ফিরে call করতে হয় না।

হিসাবটা দেখুন। প্রতি সেকেন্ডে ৫০টা event, আর পাঁচটা consumer প্রত্যেকে একবার করে ফিরতি call করে। ফলে order service এর উপর প্রতি সেকেন্ডে ২৫০টা বাড়তি request আসে। তার চেয়েও খারাপ কথা, order service down হলে সবাই আবার আটকে যায়। যে coupling থেকে মুক্তি পেতে event এনেছিলাম, সেটা পেছনের দরজা দিয়ে ফিরে এসেছে।

সমাধান হলো দরকারি তথ্যটা event এর ভেতরেই পাঠানো: phone, address, খাবারের তালিকা, মোট দাম। একে বলে event-carried state transfer। Consumer কে আর কাউকে জিজ্ঞেস করতে হয় না। দরকারি data সে নিজের database এ রেখে দেয়।

এরও দাম আছে। Event বড় হয় (1 KB থেকে 2 KB)। একই data অনেক জায়গায় কপি থাকে। আর customer এর phone number এর মতো ব্যক্তিগত তথ্য অনেক service এ ছড়িয়ে পড়ে, যেটা নিরাপত্তার দিক থেকে ঝুঁকি। তাই প্রতিটা field এর জন্য প্রশ্ন করুন: কোনো consumer কি সত্যিই এটা চায়?

মধ্যপথ

সব data event এ ঢুকিয়ে দেবেন না, আবার শুধু ID ও পাঠাবেন না। যে তথ্য বেশিরভাগ consumer এর লাগে, শুধু সেটুকু event এ রাখুন। বাকিটা যার দরকার, সে আলাদা করে চেয়ে নেবে।

০৮ শুধু event জমিয়ে রাখলে কেমন হয়?

এখন পর্যন্ত রুবেলের database এ প্রতিটা order এর বর্তমান অবস্থাটা লেখা থাকে: “order #5821, মোট 460 টাকা”। Event গুলো শুধু খবর পৌঁছানোর কাজ করে।

একটা ভিন্ন পথ আছে। বর্তমান অবস্থা না লিখে শুধু ঘটনাগুলো লিখে রাখা। আপনার পাড়ার মুদি দোকানের বাকির খাতার কথা ভাবুন। দোকানদার খাতায় “রহিম সাহেবের মোট বাকি ৮৫০ টাকা” লিখে রাখেন না। লেখেন: “৩ তারিখ চাল ৫০০”, “৭ তারিখ তেল ৪৫০”, “১০ তারিখ জমা ১০০”। মোট কত, সেটা লাইনগুলো যোগ করে বের করেন।

EVENT LOG · ORDER #5821 ১. OrderPlaced (বিরিয়ানি) +450 ২. ItemAdded (ফুচকা) +120 ৩. ItemRemoved (কোক) -60 ৪. CouponApplied -50 replay বর্তমান state order #5821 মোট: 460 টাকা পুরোনো লাইন কখনো বদলায় না; ভুল হলে নতুন একটা লাইন যোগ হয়
মুদি দোকানের বাকির খাতার মতো: database এ "মোট 460 টাকা" লেখা থাকে না, থাকে চারটা ঘটনার লাইন। লাইনগুলো শুরু থেকে যোগ করলে (replay) বর্তমান অবস্থা পাওয়া যায়। কেন 460 হলো, সেটাও খাতা দেখেই বলা যায়।

এই পদ্ধতির নাম event sourcing। এখানে event গুলোই আসল data। বর্তমান অবস্থা আলাদা করে জমা থাকে না, প্রয়োজনে event গুলো replay করে বের করা হয়।

এর লাভ হলো, কখনো কোনো তথ্য হারায় না। Customer যদি বলেন “আমার কাছ থেকে বেশি টাকা কেটেছে”, খাতা খুলে দেখা যায় কোন মুহূর্তে কী হয়েছিলো। ব্যাংকের লেনদেনের হিসাব এভাবেই রাখা হয়।

কিন্তু খরচও কম না। একটা পুরোনো account এ দশ হাজার event থাকলে প্রতিবার সব যোগ করা ধীর। তাই মাঝে মাঝে একটা snapshot (ঐ মুহূর্তের মোট হিসাব) রেখে দিতে হয়। পুরোনো event কখনো মোছা যায় না, তাই তাদের schema ও চিরকাল সামলাতে হয়। আর “আজকের সব বাতিল order দেখাও” এর মতো সাধারণ query ও সরাসরি চালানো যায় না।

একটা সতর্কতা

Event-driven architecture আর event sourcing এক জিনিস নয়। প্রথমটা হলো service গুলোর মধ্যে খবর পাঠানোর পদ্ধতি। দ্বিতীয়টা হলো একটা service এর ভেতরে data জমা রাখার পদ্ধতি। রুবেলের পুরো app event-driven, কিন্তু শুধু payment এর হিসাবটা event sourcing এ রাখা যুক্তিসঙ্গত। বাকি সব জায়গায় সাধারণ table ই যথেষ্ট।

০৯ কখন event ব্যবহার করবেন না?

Event এর সুবিধা দেখে রুবেলের team এর এক junior সব কিছুতে event বসাতে চাইলো, এমনকি restaurant খোলা আছে কিনা সেই প্রশ্নেও। রুবেল তাকে থামালো। Event এর সবচেয়ে বড় বৈশিষ্ট্য হলো, publisher উত্তরের জন্য অপেক্ষা করে না। যেখানে উত্তর ছাড়া এগোনো যায় না, সেখানে event ভুল জিনিস।

Event ব্যবহার করবেন না, যখন...

  • Caller এর এখনই উত্তর দরকার: Payment approve হলো কিনা, বা restaurant খোলা কিনা, সেটা না জেনে order confirm করা যায় না। এখানে সরাসরি call করুন।
  • শোনার মতো মাত্র একজন আছে: একজন consumer এর জন্য topic বানানো মানে বাড়তি ঝামেলা। সরাসরি call বা সেশন ০৮ এর queue ই যথেষ্ট।
  • User নিজের লেখা সাথে সাথে দেখতে চায়: Profile এর নাম বদলে page refresh করলে পুরোনো নাম দেখা গেলে user ভয় পায়। Event এর পথে এই দেরিটা থাকেই।
  • Team ছোট, system ছোট: তিনজনের team আর দিনে ৫০০ order হলে Kafka চালানোর খরচ লাভের চেয়ে বেশি। একটা সাধারণ function call ই সবচেয়ে সহজ।
  • কয়েকটা কাজ একসাথে সফল বা একসাথে বাতিল হতে হবে: Event এর পথে এটা অনেক কঠিন। এর জন্য saga নামের আলাদা কৌশল লাগে (সেশন ১৭)।

আরেকটা কথা মনে রাখুন। Event এর পথে সব কাজ একটু দেরিতে হয়। Order হওয়ার পর loyalty point যোগ হতে হয়তো দুই সেকেন্ড লাগে। এই সময়ে customer app খুললে পুরোনো point দেখবেন। একে বলে eventual consistency: সব জায়গা শেষমেশ একই অবস্থায় পৌঁছায়, তবে সাথে সাথে নয়। সেশন ০৩ এ replication lag এর সময় এটা প্রথম দেখেছিলেন। Event ব্যবহার করলে এই দেরিটা মেনে নিতে হয়, আর UI তে “point যোগ হচ্ছে” এর মতো বার্তা দেখাতে হয়।

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

Event-driven architecture একটা ঘটনার খবর অনেক service এর কাছে পৌঁছে দেয়, আর publisher কে সেই service গুলো চিনতে হয় না। এতে checkout দ্রুত হয়, একজনের সমস্যা সবার সমস্যা হয় না, আর নতুন team কাউকে না জিজ্ঞেস করেই যোগ দিতে পারে।

বিনিময়ে আপনি পান eventual consistency, duplicate event, ক্রমের মাথাব্যথা, schema সামলানোর দায়িত্ব, আর এমন একটা flow যেটা এক জায়গায় বসে দেখা যায় না। Caller এর এখনই উত্তর দরকার হলে event নয়, সরাসরি call করুন।

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

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

প্রশ্ন ১: Kafka আর সেশন ০৮ এর queue, দুটোই তো message রাখে। পার্থক্যটা কোথায়?
উত্তর দেখুন আগে নিজে ভাবুন

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

“Kafka অনেক দ্রুত, প্রতি সেকেন্ডে লাখ লাখ message সামলাতে পারে। Queue ধীর, ছোট কাজের জন্য।”

গতির কথাটা সত্যি, কিন্তু আসল পার্থক্য গতিতে নয়।

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

আসল পার্থক্য হলো, পড়ার পর message এর কী হয়। Queue তে ACK এলে message মুছে যায়, আর একটা message একজন worker ই পায়। Kafka তে পড়ার পরও event retention এর সময় পর্যন্ত থেকে যায়। প্রতিটা consumer group নিজের offset রাখে, তাই একই event অনেক group পড়তে পারে। পুরোনো offset এ ফিরে গিয়ে replay ও করা যায়।

তাই “একটা কাজ একজন করবে” হলে queue মানানসই। “একটা ঘটনা অনেকে জানবে, আর দরকার হলে পরে আবার পড়বে” হলে Kafka এর মতো log মানানসই।

সঠিক ভাষায়

Queue হলো কাজের লাইন: কাজ শেষ, message শেষ। Log হলো ঘটনার খাতা: পড়া হলেও লাইন থেকে যায়, আর প্রত্যেক পাঠক নিজের bookmark রাখে।

প্রশ্ন ২: Interview তে বলা হলো, 'একজন customer এর সব order event ঠিক ক্রমে process করতে হবে।' Topic এ ১টা partition রাখলেই তো সব ক্রম ঠিক থাকে?
উত্তর দেখুন আগে নিজে ভাবুন

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

“হ্যাঁ। একটা partition মানে একটাই খাতা, তাই সব event লেখার ক্রমেই পড়া হবে। সমস্যা মিটে গেলো।”

ক্রম ঠিক থাকবে, কথাটা সত্যি। কিন্তু এতে একটা বড় দাম দিতে হয়।

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

একটা partition একটা group এর ভেতরে মাত্র একজন consumer পড়তে পারে। তাই পুরো system এর গতি আটকে যায় একটা machine এর গতিতে। ইফতারের আগে প্রতি সেকেন্ডে ৫০টা order এলে, আর একটা machine সেকেন্ডে ২০টা সামলাতে পারলে, প্রতি সেকেন্ডে ৩০টা করে জমতে থাকবে।

আমাদের আসলে সব event এর মধ্যে ক্রম দরকার নেই। দরকার একই customer এর event গুলোর মধ্যে ক্রম। তাই customer_id কে key বানিয়ে অনেক partition রাখুন। একই customer এর event একই partition এ যাবে, আর ভিন্ন customer এর event অনেক machine এ সমান্তরালে process হবে।

মূল pattern

পুরো system এর ক্রম খুব কম ক্ষেত্রেই দরকার হয়। কোন জিনিসের ভেতরে ক্রম দরকার, সেটা খুঁজে বের করুন। সেটাকেই key বানান।

প্রশ্ন ৩: 'Event-driven করলে service গুলো পুরোপুরি স্বাধীন হয়ে যায়।' এই দাবির বিপক্ষে যুক্তি দিন।
উত্তর দেখুন আগে নিজে ভাবুন

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

“ঠিকই তো। Publisher subscriber কে চেনে না, subscriber publisher কে call করে না। দুজনের মধ্যে কোনো সম্পর্কই নেই।”

সরাসরি call এর বাঁধনটা সত্যিই কেটে যায়। কিন্তু আরেকটা বাঁধন থেকে যায়।

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

Service গুলো এখনো event এর format এর উপর নির্ভর করে। Order service OrderPlaced থেকে order_id field এর নাম বদলে দিলে পাঁচটা consumer একসাথে ভেঙে পড়বে, অথচ order service এর team হয়তো জানেই না কে কে এই field পড়ে। সরাসরি call এ অন্তত error সাথে সাথে চোখে পড়তো। এখানে ভুলটা চুপচাপ ঘটে।

তাই event এর schema কে একটা চুক্তি হিসেবে দেখতে হয়। শুধু নতুন field যোগ করুন, পুরোনো field মুছবেন না। Schema registry দিয়ে বিপজ্জনক বদল আটকান।

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

Event coupling মুছে দেয় না, শুধু জায়গা বদলায়। Service এর নাম থেকে coupling সরে যায় event এর format এ। সেই format কে public API এর মতো যত্ন করে রাখুন।

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

টার্মমানে
Tight couplingএকটা service অন্য অনেক service কে নাম ধরে চেনে আর তাদের উপর নির্ভর করে, তাই একজনের সমস্যা সবার সমস্যা হয়।
Commandএকজন নির্দিষ্ট প্রাপকের কাছে পাঠানো নির্দেশ: “এই কাজটা করুন”। প্রাপক কাজটা করতে ব্যর্থ হতে পারে।
Eventঅতীতের একটা ঘটনার ঘোষণা: “এটা ঘটে গেছে”। যে খুশি শুনতে পারে, publisher কাউকে চেনে না।
Event-driven architectureএমন design, যেখানে service গুলো একে অপরকে call না করে event এর মাধ্যমে খবর আদান-প্রদান করে।
Pub/subPublish/subscribe: publisher একটা topic এ event পাঠায়, আর প্রত্যেক subscriber নিজের আলাদা কপি পায়।
Topicএকটা নাম দেওয়া চ্যানেল, যেখানে একই ধরনের event জমা হয়, যেমন orders।
PartitionTopic এর ভেতরের এক একটা খাতা। এর ভেতরে event গুলো লেখার ক্রমেই থাকে।
OffsetPartition এর ভেতরে প্রতিটা event এর ক্রমিক নম্বর। Consumer মনে রাখে সে কোন offset পর্যন্ত পড়েছে।
Consumer groupএকই কাজের কয়েকটা consumer এর দল। দলের ভেতরে partition ভাগ হয়, আর প্রতিটা দল পুরো topic পায়।
RetentionKafka একটা event কতদিন রেখে দেবে (default ৭ দিন), কেউ পড়ুক বা না পড়ুক।
Replayপুরোনো offset এ ফিরে গিয়ে event গুলো আবার পড়া।
Ordering per keyএকই key এর সব event একই partition এ রেখে শুধু সেই key এর ভেতরে ক্রম ঠিক রাখা।
Choreographyকোনো কেন্দ্রীয় নির্দেশদাতা ছাড়া, প্রতিটা service event শুনে নিজের কাজ করে আর নতুন event ছাড়ে।
Orchestrationএকটা কেন্দ্রীয় service প্রত্যেককে ধাপে ধাপে command পাঠিয়ে পুরো flow চালায়।
Schema / Schema evolutionEvent এর ভেতরের field গুলোর কাঠামো, আর পুরোনো পাঠককে না ভাঙিয়ে সেই কাঠামো বদলানোর নিয়ম।
Schema registryEvent এর schema জমা রাখার service, যেটা বিপজ্জনক বদল আটকে দেয়।
Event-carried state transferদরকারি data event এর ভেতরেই পাঠানো, যাতে consumer কে ফিরতি call করতে না হয়।
Event sourcingবর্তমান অবস্থা না লিখে শুধু ঘটনাগুলো জমা রাখা, আর replay করে বর্তমান অবস্থা বের করা।
SnapshotEvent sourcing এ কোনো এক মুহূর্তের মোট হিসাব, যাতে প্রতিবার শুরু থেকে replay করতে না হয়।

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

  1. Command একজন নির্দিষ্ট প্রাপককে কাজ করতে বলে, আর event শুধু জানায় যে কিছু একটা ঘটে গেছে। Pub/sub এ publisher একটা topic এ event পাঠায়, আর প্রত্যেক subscriber নিজের কপি পায়, তাই নতুন subscriber যোগ করতে publisher কে বদলাতে হয় না।
  2. Kafka একটা log: event শুধু partition এর শেষে যোগ হয়, পড়লেও মোছে না, আর retention শেষ হলে মোছে। প্রতিটা consumer group নিজের offset রাখে, group এর ভেতরে partition ভাগ হয়, আর পুরোনো offset এ ফিরে replay করা যায়।
  3. Kafka শুধু একটা partition এর ভেতরে ক্রম নিশ্চিত করে, তাই order_id এর মতো key দিয়ে একই জিনিসের সব event একই partition এ পাঠাতে হয়। Choreography তে কোনো কেন্দ্রীয় নির্দেশদাতা থাকে না, আর schema বদলাতে হয় শুধু default সহ নতুন field যোগ করে।
  4. Event এর পথে সব কিছু eventual consistency তে চলে, আর flow টা এক জায়গায় দেখা যায় না। Caller এর এখনই উত্তর দরকার হলে, বা শ্রোতা মাত্র একজন হলে, event ব্যবহার করবেন না।

১৩ মনে রাখুন