০০ যে প্রশ্নগুলোর উত্তর খুঁজবো
Facebook, Instagram বা X (আগের Twitter) খুললে প্রথমেই যে লম্বা তালিকাটা দেখেন, সেটাই feed। দেখতে খুব সাধারণ: যাদের follow করেন, তাদের নতুন post গুলো একটার নিচে আরেকটা। কিন্তু কোটি মানুষের জন্য এই তালিকা এক সেকেন্ডের কম সময়ে বানানো system design এর সবচেয়ে প্রিয় interview প্রশ্নগুলোর একটা।
এই সেশনটা একটা পূর্ণ case study। সেশন ২৩ এর মতোই আমরা ধাপে ধাপে এগোবো: requirement, সংখ্যার হিসাব, API, data model, design, তারপর কঠিন জায়গাগুলোতে গভীরে নামা। আগের সেশনগুলোর cache, shard, CDN আর event এখানে একসাথে কাজ করবে।
- ১ Feed system টা ঠিক কী কী করবে? (functional ও non-functional requirement)
- ২ কত user, কত post, কত ছবি? (back-of-envelope estimation)
- ৩ API আর data model দেখতে কেমন?
- ৪ Home timeline কখন বানাবো: fan-out on read নাকি fan-out on write?
- ৫ ২ কোটি follower এর একজন post করলে কী হয়? (celebrity problem, hybrid approach)
- ৬ Timeline Redis এ কীভাবে রাখবো, আর পরের page কীভাবে আনবো? (timeline cache, cursor pagination)
- ৭ এত ছবি কোথায় রাখবো, আর মানুষের কাছে কীভাবে পৌঁছাবো? (object storage, CDN)
- ৮ এক post এ সেকেন্ডে হাজার like কীভাবে গুনবো, আর feed এর ক্রম কে ঠিক করে? (sharded counter, ranking)
- ৯ সব মিলিয়ে: পুরো design, bottleneck আর trade-off গুলো কী?
০১ Feed system টা ঠিক কী কী করবে?
রাত দশটা সাতচল্লিশ। মিরপুরে শেষ বলে ছক্কা হাঁকিয়ে বাংলাদেশকে জিতিয়েছেন তাওসিফ আলম। ড্রেসিংরুমে ফিরে ট্রফি হাতে দলের একটা ছবি তুললেন, আর “আড্ডা” app এ post করলেন। Caption ছোট: “এই জয় আপনাদের।” তাঁর follower ২ কোটি।
ঢাকার এক অফিসে তানভীর তখন আড্ডার feed team এর on-call engineer। খেলা দেখে সে-ও লাফিয়ে উঠেছিলো। এমন সময় রাজশাহী থেকে আম্মুর ফোন এলো: “তাওসিফের ছবিটা নাকি এসেছে? আমার feed এ তো দেখছি না!” তানভীর dashboard এর দিকে তাকালো। একটা graph খাড়া উপরে উঠে যাচ্ছে।
এই রাতটা কীভাবে ভালোভাবে পার করা যায়, সেটাই এই সেশনের গল্প। তবে তার আগে জানতে হবে, system টার আসলে কী কী করার কথা।
Functional requirement: system কী করবে
Functional requirement মানে system এর কাজের তালিকা, অর্থাৎ user কী কী করতে পারবেন। আড্ডার জন্য তালিকাটা এমন:
- Post করা: একজন user একটা ছবি আর ছোট caption দিয়ে post করতে পারেন।
- Follow করা: একজন আরেকজনকে follow করতে পারেন। Follow একমুখী, ফেরত follow করার দরকার নেই।
- Home timeline দেখা: যাদের follow করেন, তাদের post এক তালিকায় দেখা। App খুললেই এই তালিকা আসে। এর নাম home timeline।
- নিচে scroll করা: তালিকার শেষে গেলে আরো পুরোনো post আসে।
- Like দেওয়া: প্রতিটা post এ কতজন like দিলেন, সেটা দেখা যায়।
একটা নামের পার্থক্য এখনই মনে রাখুন। Home timeline হলো আপনি যাদের follow করেন, তাদের সবার post মিলিয়ে বানানো তালিকা। User timeline হলো একজন মানুষের নিজের post গুলো, যেমন তাওসিফের profile এ গেলে যা দেখেন। User timeline বানানো সহজ, এক জনের post এক জায়গা থেকে পড়লেই হয়। কঠিন হলো home timeline, কারণ সেটা শত শত মানুষের post মিলিয়ে বানাতে হয়।
Comment, story, message আর search এই সেশনে নেই। Search আমরা সেশন ২৪ এ দেখেছি।
Non-functional requirement: কতটা ভালোভাবে করবে
Non-functional requirement মানে system এর গুণ: কত দ্রুত, কত নির্ভরযোগ্য, কত বড়। আড্ডার জন্য:
| Requirement | লক্ষ্য | কেন |
|---|---|---|
| Feed খোলার latency | ১০০টা request এর ৯৯টাই ২০০ ms এর কম সময়ে | App খুলে ফাঁকা screen কেউ দেখতে চান না |
| Availability | ৯৯.৯৫% | Feed না খুললে মানুষ app ছেড়ে চলে যান |
| নতুন post পৌঁছানো | ৫ সেকেন্ডের মধ্যে | কয়েক সেকেন্ড দেরি চলে, কয়েক মিনিট নয় |
| Consistency | Eventual consistency চলবে | আম্মু ছবিটা দুই সেকেন্ড পরে দেখলে কোনো ক্ষতি নেই |
শেষ লাইনটা পুরো design এর দিক ঠিক করে দেয়। Eventual consistency মানে সব জায়গায় data একটু পরে হলেও শেষ পর্যন্ত মিলে যায় (সেশন ০৩)। ব্যাংকের balance এ এটা চলে না। কিন্তু feed এ চলে। এই ছাড়টুকু আছে বলেই আমরা পরে অনেক কাজ background এ সরিয়ে দিতে পারবো।
০২ কত user, কত post, কত ছবি?
Design শুরুর আগে সংখ্যাগুলো একবার মোটামুটি হিসাব করে নিতে হয়। খাতার কোণে করা এই দ্রুত হিসাবকে বলে back-of-envelope estimation (সেশন ২৩)। এখানে নিখুঁত সংখ্যা দরকার নেই। দরকার শুধু বোঝা, কোন জিনিসটা হাজারের ঘরে আর কোনটা কোটির ঘরে।
আড্ডার জন্য কিছু সংখ্যা ধরে নিই, তারপর ধাপে ধাপে গুণ করি:
# ধরে নেওয়া সংখ্যা (আড্ডা app) Registered user = 8 কোটি (80M) Daily active user (DAU) = 3 কোটি (30M) 1 দিন = 86,400 s (হিসাব সহজ করতে ~1 লাখ s ধরি) # 1. Post (write) প্রতি DAU দিনে 0.2 টা post → 30M x 0.2 = 6M post/day Average → 6M / 86,400 ≈ 70 post/s Peak (ম্যাচের রাত, x5) → ≈ 350 post/s # 2. Feed open (read) প্রতি DAU দিনে 10 বার feed → 30M x 10 = 300M feed load/day Average → 300M / 86,400 ≈ 3,500 load/s Peak (x5) → ≈ 17,500 load/s Read : Write → 3,500 : 70 ≈ 50 : 1 (read-heavy) # 3. Fan-out (timeline এ post_id বসানো) গড়ে 200 জন follower → 6M x 200 = 1.2B insert/day Average → 1.2B / 86,400 ≈ 14,000 insert/s # 4. Storage Post এর text + metadata ~1 KB → 6M x 1 KB = 6 GB/day ≈ 2.2 TB/year 70% post এ photo, 3 size মিলে ~2 MB → 4.2M x 2 MB = 8.4 TB/day ≈ 3 PB/year # 5. Egress (ছবি download) প্রতি feed load এ ~10 টা medium ছবি x 300 KB = 3 MB → 300M x 3 MB = 900 TB/day → 900 TB / 86,400 s ≈ 10 GB/s ≈ 80 Gbps
এই হিসাব থেকে চারটা সিদ্ধান্ত বেরিয়ে আসে:
- Read অনেক বেশি: প্রতি একটা post এর বিপরীতে প্রায় ৫০ বার feed খোলা হয়। তাই design এর মূল লক্ষ্য হবে feed পড়া সস্তা করা, দরকার হলে লেখার সময় একটু বেশি খাটুনি করে।
- Fan-out এর সংখ্যা বিশাল: দিনে ৬০ লাখ post, কিন্তু প্রতিটা গড়ে ২০০ জনের কাছে যায়। মোট দিনে ১২০ কোটি বার কোথাও না কোথাও লিখতে হয়। এই সংখ্যাটা মনে রাখুন, পরের section এ কাজে লাগবে।
- ছবি database এ রাখা যাবে না: বছরে ৩ PB ছবি। এর জন্য আলাদা ধরনের storage লাগবে (Q7)।
- ছবি পাঠানোও বড় কাজ: সেকেন্ডে প্রায় ৮০ Gbps ছবি বের হয়। এক data center থেকে এটা পাঠানো সম্ভব নয়, CDN লাগবে।
০৩ API আর data model দেখতে কেমন?
API
API হলো app আর server এর মধ্যে কথা বলার নিয়ম। আড্ডার মূল API গুলো এমন:
# 1. ছবি upload এর জন্য একটা URL চাওয়া
POST /v1/media/upload-url
→ 200 { "media_id": "m_77", "upload_url": "https://...", "expires_in": 900 }
# 2. Post তৈরি করা (ছবি আগেই upload হয়ে গেছে)
POST /v1/posts
body: { "caption": "এই জয় আপনাদের", "media_ids": ["m_77"] }
→ 201 { "post_id": 9001, "created_at": "2026-11-07T22:47:03+06:00" }
# 3. Home timeline পড়া (cursor দিয়ে পরের page)
GET /v1/feed?limit=20&cursor=8410
→ 200 { "posts": [ ...20 টা post... ], "next_cursor": "7702" }
# 4. Like / unlike (একই request দুইবার এলেও ফল একই)
PUT /v1/posts/9001/like
DELETE /v1/posts/9001/like
# 5. Follow
PUT /v1/users/taosif/follow
তিনটা জিনিস খেয়াল করুন:
- ছবি আলাদা পথে যায়: Post তৈরির request এ ছবি থাকে না, থাকে শুধু
media_id। ছবিটা আগেই অন্য পথে upload হয়ে গেছে। কেন এমন, সেটা Q7 এ দেখবো। - Feed এ
cursorআছে,page=3নেই: পরের page চাইতে app শেষ দেখা post এর একটা চিহ্ন পাঠায়। Q6 এ এর কারণ বুঝবো। - Like এর জন্য
PUT: একই like request দুইবার এলেও ফল একই থাকে, like দুইটা হয় না। মানে কাজটা idempotent (সেশন ০৮)। দুর্বল network এ app request আবার পাঠালেও কোনো সমস্যা হয় না।
Data model
users (user_id PK, name, avatar_url, follower_count, is_celebrity)
posts (post_id PK, author_id, caption, media_keys, created_at, like_count)
-- post_id সময়ের সাথে বাড়ে: নতুন post এর id সবসময় বড়
-- shard key: author_id (এক জনের সব post এক shard এ)
-- index: (author_id, created_at DESC)
follows (follower_id, followee_id, created_at)
-- PK: (follower_id, followee_id) → "আমি কাকে follow করি"
-- আরেকটা কপি: (followee_id, follower_id) → "আমাকে কে follow করে"
likes (post_id, user_id, created_at)
-- PK: (post_id, user_id) → একজন একটা post এ একবারই like দিতে পারে
# Redis (cache, database নয়)
timeline:{user_id} → sorted set: score = post_id, member = post_id (সর্বোচ্চ 800 টা)
কয়েকটা সিদ্ধান্তের পেছনের কারণ:
poststable shard হয়author_idদিয়ে: এক জনের সব post এক shard এ থাকে। তাই তাওসিফের profile খুললে একটা shard পড়লেই হয়। Shard key বাছাইয়ের নিয়ম সেশন ০৫ এ দেখেছিলেন।- নতুন post এর id সবসময় বড়: post_id এমনভাবে বানানো হয় যে সময়ের সাথে সংখ্যাটা বাড়তেই থাকে (X এর Snowflake id এভাবে কাজ করে)। ফলে id দেখেই বোঝা যায় কোন post নতুন। আলাদা করে সময় দিয়ে sort করার দরকার হয় না।
followsএর দুইটা কপি: “আম্মু কাকে কাকে follow করেন” আর “তাওসিফকে কে কে follow করে”, দুটো প্রশ্নই প্রতি মুহূর্তে আসে। দুই দিক থেকে index রাখলে দুটোই দ্রুত হয়। এই follow এর জাল রাখার জন্য অনেক কোম্পানি আলাদা একটা graph DB ব্যবহার করে, যেটা “কে কার সাথে যুক্ত” ধরনের প্রশ্নের জন্যই বানানো।- Timeline Redis এ, database এ নয়: Timeline হারালেও আবার বানানো যায়, কারণ আসল post আর follow এর data database এ নিরাপদে আছে। তাই এটা cache এ রাখাই যথেষ্ট।
০৪ Home timeline কখন বানাবো: fan-out on read নাকি fan-out on write?
এটাই এই সেশনের মূল প্রশ্ন। আম্মু ৩০০ জনকে follow করেন। তাঁর home timeline এ এই ৩০০ জনের post মিলিয়ে, নতুন থেকে পুরোনো ক্রমে সাজাতে হবে। প্রশ্ন হলো, এই মেলানোর কাজটা কখন হবে? আম্মু feed খোলার সময়, নাকি কেউ post করার সময়?
একটা জিনিস অনেক জায়গায় ছড়িয়ে দেওয়াকে বলে fan-out, হাতপাখা যেমন এক বিন্দু থেকে চারদিকে ছড়ায়। Feed এর ক্ষেত্রে একজনের post তাঁর সব follower এর কাছে পৌঁছানোই হলো fan-out।
পথ ১: fan-out on read (feed খোলার সময় বানানো)
সবচেয়ে সোজা পথ হলো, post করার সময় কিছুই না করা। Post শুধু database এ save হয়। আম্মু যখন feed খোলেন, তখনই system ৩০০ জনের post খুঁজে এনে সাজায়। এর নাম fan-out on read, কারণ ছড়ানোর কাজটা হয় পড়ার সময়। একে pull model ও বলে, কারণ পড়ার সময় সব জায়গা থেকে টেনে আনা হয়।
একজন user এর জন্য এটা চমৎকার। Post করা খুব সস্তা, আর feed সবসময় একদম fresh। সমস্যা হয় সংখ্যাটা বড় হলে। ম্যাচের রাতে সেকেন্ডে ১৭,৫০০ জন feed খোলেন। প্রত্যেকের জন্য ৩০০টা query মানে সেকেন্ডে প্রায় ৫২ লাখ query। একই কাজ বারবার হচ্ছে: রাফি আর আম্মু দুজনেই রুমানাকে follow করলে রুমানার post দুইজনের জন্য আলাদা আলাদা খোঁজা হয়।
পথ ২: fan-out on write (post করার সময় বানানো)
উল্টো পথটা হলো, ভারী কাজটা post করার সময়েই সেরে ফেলা। প্রত্যেক user এর জন্য একটা তৈরি করা তালিকা রাখা হয়, যেন একটা ব্যক্তিগত ডাকবাক্স। রুমানা post করলে একটা worker তাঁর ৩০০ জন follower এর প্রত্যেকের ডাকবাক্সে post এর id টা ফেলে দেয়। এর নাম fan-out on write, কারণ ছড়ানোর কাজটা হয় লেখার সময়। একে push model ও বলে।
এখন আম্মু feed খুললে কোনো খোঁজাখুঁজি নেই। শুধু নিজের ডাকবাক্স খুললেই হয়: একটা Redis read, ৫ ms এর মতো। Post করা এখন একটু ধীর, কারণ পেছনে ৩০০টা write হয়। কিন্তু রুমানাকে সেই অপেক্ষা করতে হয় না। Post save হলেই তিনি “posted” দেখেন। বাকি কাজ queue এর মাধ্যমে background এ চলে (সেশন ০৮)।
পাশাপাশি তুলনা
| Fan-out on read (pull) | Fan-out on write (push) | |
|---|---|---|
| Post করার খরচ | খুব কম, একটা write | বেশি, প্রতি follower এ একটা write |
| Feed খোলার খরচ | বেশি, শত শত query | খুব কম, একটা read |
| Feed খোলার latency | ১০০-১৭০ ms | ৫-১০ ms |
| নতুন post কখন দেখা যায় | সাথে সাথে | কয়েক সেকেন্ড পরে |
| কোথায় ভালো | কম পড়া হয় এমন feed | বেশি পড়া হয় এমন feed |
Q2 এর হিসাব মনে আছে? Read, write এর ৫০ গুণ। তাই আড্ডার জন্য fan-out on write ই স্বাভাবিক পছন্দ। লেখার সময় একবার খেটে নিলে পড়ার সময় ৫০ বার সুবিধা পাওয়া যায়।
কাজটা একবার না একবার করতেই হবে। প্রশ্ন শুধু, কখন করবেন। যে কাজ বেশিবার হয় (feed পড়া), সেটা সস্তা রাখুন। খরচটা সরিয়ে দিন কম ঘটা কাজের দিকে (post করা)।
০৫ ২ কোটি follower এর একজন post করলে কী হয়?
এবার ফিরে আসি রাত দশটা সাতচল্লিশে। আড্ডা তখন শুধু fan-out on write চালাতো। তাওসিফ ছবিটা post করলেন। Worker দের সামনে এখন একটা post এর জন্য ২ কোটি ZADD (Redis এর sorted set এ একটা item বসানোর command, Q6 এ বিস্তারিত)।
হিসাবটা সোজা। আড্ডার সব worker মিলে সেকেন্ডে ১ লাখ timeline write করতে পারে। স্বাভাবিক দিনে দরকার হয় সেকেন্ডে ১৪,০০০, তাই অনেক জায়গা ফাঁকা থাকে। কিন্তু ২ কোটি write শেষ করতে লাগে ২০০ সেকেন্ড, মানে তিন মিনিটের বেশি।
এই তিন মিনিটে তিনটা সমস্যা হয়:
- শেষের দিকের follower রা দেরিতে দেখেন: আম্মুর timeline এ ছবিটা হয়তো তিন মিনিট পরে পৌঁছায়। আর তিনি তখন ফোনে তানভীরকে জিজ্ঞেস করছেন।
- বাকি সবাই আটকে যায়: তাওসিফের ২ কোটি job এর পেছনে লাইনে দাঁড়িয়ে আছে রুমানার মতো সাধারণ মানুষের post ও। তাঁদের বন্ধুরাও তিন মিনিট কিছু পান না।
- অনেক কাজ বৃথা যায়: ২ কোটি follower এর বড় একটা অংশ আজ রাতে app খুলবেনই না। তাঁদের timeline এ লেখাটা শুধু Redis এর memory খরচ করলো।
আর তাওসিফ যদি পরের দশ মিনিটে আরো তিনটা ছবি দেন? লাইন আরো লম্বা হয়। এই সমস্যার নাম celebrity problem: যাঁর follower অস্বাভাবিক বেশি, তাঁর একটা post fan-out on write এর পুরো ব্যবস্থাকে আটকে দেয়।
সমাধান: hybrid approach
সমাধানটা হলো দুই পথ মিশিয়ে ফেলা। সব account কে একভাবে না দেখে দুই ভাগে ভাগ করা হয়:
- সাধারণ account (follower ১০ লাখের কম): আগের মতোই fan-out on write। রুমানার post তাঁর follower দের timeline এ push হয়।
- Celebrity account (follower ১০ লাখের বেশি): কোনো fan-out হয় না। তাওসিফের post শুধু তাঁর নিজের user timeline এ থাকে, আর একটা ছোট cache এ রাখা হয়।
আম্মু feed খুললে feed service দুই জায়গা থেকে post আনে, তারপর মিলিয়ে সাজায়:
আম্মু যতজন celebrity কে follow করেন, তাঁদের শেষ কয়েকটা post read এর সময় টেনে আনা হয়। একজন মানুষ সাধারণত হাতে গোনা কয়েকজন celebrity কে follow করেন, তাই এই pull খুব ছোট। আর তাওসিফের post টা কোটি মানুষ পড়লেও সবাই একই cache key পড়ে, যেটা সবসময় memory তে গরম থাকে।
এখানে আরেকটা জিনিস খেয়াল রাখতে হয়। কোটি মানুষ একই key পড়লে সেটা hot key হয়ে যায় (সেশন ০৫)। একটা Redis node এর উপর সব চাপ পড়ে। তাই celebrity post cache এর কয়েকটা কপি কয়েকটা node এ রাখা হয়। কিছু কোম্পানি app server এর নিজের memory তেও কয়েক সেকেন্ডের জন্য রেখে দেয়।
নিষ্ক্রিয় user দের বাদ দেওয়া
একই বুদ্ধি আরেক জায়গায়ও কাজে লাগে। যে user ৩০ দিন app খোলেননি, তাঁর timeline এ লিখে লাভ নেই। Fanout worker তাঁকে বাদ দেয়। তিনি ফিরে এলে প্রথমবার fan-out on read দিয়ে তাঁর timeline নতুন করে বানানো হয়। এতে ৮ কোটি registered user এর মধ্যে শুধু সক্রিয় ৩ কোটির জন্য memory খরচ হয়।
বেশিরভাগ মানুষের জন্য push, অল্প কয়েকজন বিশাল account এর জন্য pull। Push পড়াকে দ্রুত রাখে। Pull celebrity এর একটা post থেকে কোটি write এর ঢেউ ঠেকায়। কোন সীমায় ভাগ হবে (১০ লাখ, নাকি ৫ লাখ), সেটা system চালাতে চালাতে মেপে ঠিক করতে হয়।
০৬ Timeline Redis এ কীভাবে রাখবো, আর পরের page কীভাবে আনবো?
Sorted set: নিজে থেকেই সাজানো তালিকা
Timeline রাখার জন্য দরকার এমন একটা তালিকা, যেটা সবসময় নতুন থেকে পুরোনো ক্রমে সাজানো থাকে, আর মাঝখান থেকে দ্রুত পড়া যায়। Redis এ ঠিক এমন একটা জিনিস আছে, নাম sorted set। এখানে প্রতিটা item এর সাথে একটা নম্বর (score) থাকে। Redis সবসময় score অনুযায়ী item গুলো সাজিয়ে রাখে।
আমাদের ক্ষেত্রে item হলো post_id, আর score ও post_id। Post_id নিজেই সময়ের সাথে বাড়ে, তাই বড় score মানেই নতুন post।
# Fanout worker: রুমানার নতুন post (8977) আম্মুর timeline এ বসানো
# score = post_id, কারণ নতুন post এর id সবসময় বড়
ZADD timeline:ammu 8977 8977
# সবচেয়ে নতুন 800 টা রেখে বাকিগুলো ফেলে দেওয়া (memory বাঁচাতে)
ZREMRANGEBYRANK timeline:ammu 0 -801
# প্রথম page: সবচেয়ে নতুন 20 টা post_id
ZREVRANGE timeline:ammu 0 19
# পরের page: cursor=8410, মানে 8410 এর চেয়ে ছোট (পুরোনো) 20 টা id
ZREVRANGEBYSCORE timeline:ammu (8410 -inf LIMIT 0 20
# Memory হিসাব
3 কোটি active user x 800 id x 16 byte = 384 GB (raw)
Redis এর নিজের overhead মিলে ≈ ~800 GB → 64 GB এর ~13 টা node, replica সহ ~26 টা
চারটা সিদ্ধান্ত এখানে গুরুত্বপূর্ণ:
- শুধু id রাখা হয়, পুরো post নয়: তাওসিফের caption আর ছবির link লাখো timeline এ কপি করলে memory শেষ হয়ে যাবে। আর caption বদলালে লাখো জায়গায় বদলাতে হবে। তাই timeline এ থাকে শুধু ৮ byte এর একটা id।
- সর্বোচ্চ ৮০০টা id: প্রায় কেউই feed এ ৮০০ post এর বেশি নিচে scroll করেন না। তাই পুরোনোগুলো ছেঁটে ফেলা হয় (trim)। X ও তাদের timeline এ মোটামুটি এই সংখ্যাই রাখে।
- Memory এর হিসাব: ৩ কোটি সক্রিয় user, প্রত্যেকের ৮০০টা id। Redis এর নিজের খরচ মিলিয়ে প্রায় ৮০০ GB। ৬৪ GB এর ১৩টা machine, আর প্রতিটার একটা replica।
- Timeline না পেলে আবার বানানো: কোনো Redis node হঠাৎ down হলে বা কেউ অনেক দিন পরে ফিরলে timeline পাওয়া যায় না। তখন fan-out on read দিয়ে একবার বানিয়ে Redis এ রেখে দেওয়া হয়। এটা সেশন ০৪ এর cache-aside pattern: cache এ না পেলে মূল জায়গা থেকে এনে cache এ রাখা।
Hydration: id থেকে পুরো post
Redis থেকে আম্মু পান ২০টা সংখ্যা মাত্র। Screen এ দেখানোর জন্য দরকার caption, ছবি, লেখকের নাম আর like এর সংখ্যা। এই id গুলোকে পূর্ণ post এ রূপ দেওয়ার ধাপকে বলে hydration, শুকনো জিনিসে পানি দিয়ে আবার তাজা করার মতো।
Hydration এর সময় প্রতিটা id এর জন্য আলাদা request পাঠানো হয় না। Redis এর MGET একবারে ২০টা key পড়ে আনে। তাই পুরো ধাপটা কয়েক millisecond এ শেষ হয়।
Cursor pagination: পরের page আনা
আম্মু প্রথম ২০টা post দেখে নিচে scroll করলেন। এখন পরের ২০টা লাগবে। সবচেয়ে চেনা পথ হলো offset: “প্রথম ২০টা বাদ দিয়ে পরের ২০টা।” SQL এর LIMIT 20 OFFSET 20 এভাবেই কাজ করে।
Feed এ এই পথে একটা অদ্ভুত সমস্যা হয়। আম্মু যখন প্রথম page পড়ছিলেন, তখন উপরে নতুন post এসে গেছে। ফলে সবকিছু নিচের দিকে সরে গেছে:
এর সমাধান cursor pagination। “কয়টা বাদ দেবো” না বলে app বলে, “এই post এর চেয়ে পুরোনো ২০টা চাই।” শেষ দেখা post এর id টাই হলো cursor। উপরে যত নতুন post আসুক, “৮৪১০ এর চেয়ে পুরোনো” প্রশ্নের উত্তর বদলায় না। তাই কোনো post দুইবার আসে না, কোনোটা বাদও পড়ে না।
আরেকটা সুবিধা আছে। Offset ১০,০০০ হলে database কে আগে ১০,০০০টা row পড়ে ফেলে দিতে হয়, তারপর আসল ২০টা দেয়। Cursor থাকলে index দিয়ে সরাসরি ঐ জায়গায় চলে যাওয়া যায় (সেশন ০৭)। তাই একশো page নিচেও feed সমান দ্রুত থাকে।
০৭ এত ছবি কোথায় রাখবো, আর মানুষের কাছে কীভাবে পৌঁছাবো?
Q2 এর হিসাবে দেখেছি, বছরে ৩ PB ছবি জমে। এগুলো MySQL এর মতো database এ রাখা ভালো পছন্দ নয়। Database বানানো হয় ছোট ছোট row দ্রুত খোঁজার জন্য। কোটি কোটি বড় file এর জন্য দরকার আলাদা ধরনের storage।
Object storage
Object storage আমরা সেশন ২৩ এ Pastebin এর লেখা রাখার জন্য দেখেছি। মনে করিয়ে দিই: এটা বড় file রাখার storage, যেখানে প্রতিটা file একটা key দিয়ে পাওয়া যায় (যেমন photos/9001/medium.jpg)। জায়গা প্রায় অসীম, খরচ কম, আর প্রতিটা file এর কয়েকটা কপি আলাদা জায়গায় থাকে। ছবির জন্যও ঠিক একই জিনিস কাজে লাগে।
ছবি upload: app server কে পাশ কাটিয়ে
তাওসিফের ৪ MB এর ছবিটা app server এর ভেতর দিয়ে গেলে server এর network আর memory দুটোই অকারণে আটকে থাকে। তাই ছবি যায় সরাসরি object storage এ:
এখানে মূল কৌশল presigned URL। Media service object storage এর কাছ থেকে একটা বিশেষ link বানিয়ে নেয়। এই link দিয়ে শুধু একটা নির্দিষ্ট file, শুধু ১৫ মিনিটের জন্য upload করা যায়। তাওসিফের phone এই link এ ছবি পাঠায়। App server শুধু link টা দেয়, ছবির একটা byte ও তাকে ছুঁতে হয় না।
Upload শেষ হলে object storage একটা event পাঠায়। একটা worker ছবির তিনটা size বানায়: feed এর ছোট thumbnail, phone এর screen এর জন্য মাঝারি, আর আসল ছবি। কেউ ২ কোটি phone এ ৪ MB এর আসল ছবি পাঠাতে চায় না।
ছবি download: CDN
২ কোটি follower একসাথে একই ছবি চাইলে object storage এর একটা জায়গা থেকে পাঠানো ধীর আর খরচের। তাই মাঝখানে থাকে CDN (সেশন ০৬): মানুষের কাছাকাছি শহরে বসানো server, যেগুলো জনপ্রিয় file এর কপি রাখে। ঢাকার edge এ ছবিটা একবার এলে পরের লাখো মানুষ সেখান থেকেই ২০ ms এ পান। Object storage তখন শুধু origin, মানে CDN এর পেছনের আসল উৎস।
ছবির file নাম কখনো বদলায় না, আর একই নামে নতুন ছবিও বসে না। ছবি বদলালে নতুন key তৈরি হয়। তাই CDN কোনো ছবিকে অনেক দিন cache এ রাখতে পারে, cache invalidation এর ঝামেলা ছাড়াই।
০৮ এক post এ সেকেন্ডে হাজার like কীভাবে গুনবো, আর feed এর ক্রম কে ঠিক করে?
Like counter: একটা row এ লাইন
তাওসিফের ছবিতে প্রথম দশ মিনিটে সেকেন্ডে প্রায় ৫,০০০ like আসছে। সবচেয়ে সরল পথ হলো প্রতিটা like এ database এ লেখা: UPDATE posts SET like_count = like_count + 1 WHERE post_id = 9001।
সমস্যা হলো, ৫,০০০ request একই row বদলাতে চায়। Database একটা row একসাথে একজনকেই বদলাতে দেয়। বাকিরা lock এর জন্য লাইনে দাঁড়ায়, আর লাইনটা লম্বা হতেই থাকে। এটা আবারো hot key সমস্যা (সেশন ০৫), এবার write এর দিক থেকে।
সেশন ২৪ এ rate limiter এর counter আমরা Redis এ রেখেছিলাম। এখানেও Redis, শুধু একটা নতুন কৌশল যোগ হয়। সমাধানটা দুই ধাপে:
- Counter ভাগ করা (sharded counter): একটা counter এর বদলে Redis এ ১০টা ছোট counter রাখা হয়:
likes:9001:0থেকেlikes:9001:9। প্রতিটা like random একটা counter এINCRকরে (Redis এর command, যেটা একটা সংখ্যা এক বাড়ায়)। এখন প্রতিটা counter এ সেকেন্ডে মাত্র ৫০০ বার লেখা হয়। মোট like জানতে ১০টা যোগ করলেই হয়। - Database এ কয়েক সেকেন্ড পর পর লেখা: একটা flush job প্রতি ৫ সেকেন্ডে ১০টা counter যোগ করে database এ একবার লেখে। Database এর উপর চাপ সেকেন্ডে ৫,০০০ write থেকে নেমে আসে ৫ সেকেন্ডে একটায়।
তবে “কে like দিলেন” সেই তথ্য আলাদা করে likes table এ রাখতে হয়। এর primary key (post_id, user_id), তাই একজন একটা post এ দুইবার like দিতে পারেন না। আম্মু দুর্বল network এ দুইবার চাপলেও like একটাই হয়।
এর একটা দাম আছে: screen এর সংখ্যাটা কয়েক সেকেন্ড পিছিয়ে থাকে। আম্মু হয়তো ১২,৪০৩ দেখছেন, অথচ আসল সংখ্যা ১২,৪৫০। Like এর ক্ষেত্রে কেউ এটা টেরও পান না।
Ranking: শুধু সময়ের ক্রম যথেষ্ট নয়
এতক্ষণ feed সাজিয়েছি শুধু সময় দিয়ে। কিন্তু আম্মু ৩০০ জনকে follow করলে প্রতিদিন হয়তো হাজারখানেক post আসে। সময়ের ক্রমে সাজালে দশ মিনিট আগের এক দূরের আত্মীয়ের সাধারণ post উপরে থাকে, আর চল্লিশ মিনিট আগের তাওসিফের ছবি অনেক নিচে চলে যায়।
তাই বড় app গুলো feed কে ranking করে, মানে প্রতিটা post এর একটা score বের করে সেই অনুযায়ী সাজায়:
Score বের করতে কিছু signal দেখা হয়: post কতটা নতুন, লেখকের সাথে আম্মুর কতটা যোগাযোগ, post টা কত দ্রুত like পাচ্ছে, ছবি নাকি শুধু লেখা। বড় কোম্পানিগুলো এর জন্য machine learning model ব্যবহার করে। Design এর দিক থেকে শুধু মনে রাখুন: ranking timeline এর পরে বসে। Timeline থেকে কয়েকশো candidate আসে, ranking সেখান থেকে সেরা ২০টা বাছে।
Candidate generation আর sponsored post
Ranking এর আগের ধাপটার একটা নাম আছে: candidate generation। কয়েকটা সস্তা উৎস থেকে (আম্মুর timeline, trending post, কাছের মানুষের নতুন ছবি) কয়েকশো সম্ভাব্য post তোলা হয়। ভারী scoring চলে শুধু এই কয়েকশোর উপর, লাখো post এর উপর নয়। তাই feed দ্রুত আসে।
এরপর আসে বিজ্ঞাপন। কোম্পানি টাকা দিয়ে নিজের post feed এ তুলে আনতে পারে। কিন্তু feed এর অর্ধেক বিজ্ঞাপন হলে মানুষ app বন্ধ করে দেবে। তাই বিজ্ঞাপন বসে নির্দিষ্ট ঘরে, যেমন ৪, ১২ আর ২০ নম্বরে, আর প্রতিটার গায়ে “Sponsored” লেখা থাকে। বিজ্ঞাপন আসে আলাদা একটা ad service থেকে, আর ranking শেষ হওয়ার পর feed এ মেশানো হয়।
০৯ সব মিলিয়ে: পুরো design, bottleneck আর trade-off গুলো কী?
পুরো ছবি এক নজরে
Post database এ save হলে একটা post_created event Kafka তে যায়। Kafka হলো একটা event log, যেখানে event গুলো ক্রম অনুযায়ী জমা থাকে আর অনেকে আলাদাভাবে পড়তে পারে (সেশন ১০)। Fanout worker রা সেই event পড়ে। Like counting, notification বা analytics এর মতো অন্য team ও একই event পড়তে পারে, post service কে কিছু না জানিয়েই।
আবার রাত দশটা সাতচল্লিশ, এবার নতুন design এ
পরের সিরিজের ফাইনালে তাওসিফ আবার শেষ বলে জেতালেন, আর আবার ছবি দিলেন। এবার যা হলো:
- ১০:৪৭:০৩: ছবি সরাসরি object storage এ গেলো। Post save হলো, post_id ৯৫১২।
- ১০:৪৭:০৪: Fanout worker দেখলো, তাওসিফ celebrity। কারো timeline এ কিছু লিখলো না। Post টা শুধু celebrity post cache এ গেলো।
- ১০:৪৭:০৫: আম্মু feed খুললেন। Feed service তাঁর timeline আর celebrity cache মিলিয়ে ছবিটা সবার উপরে দিলো। মোট সময় প্রায় ১০ ms।
- ১০:৪৭:০৬: ঢাকার CDN edge প্রথমবার ছবিটা origin থেকে আনলো। পরের লাখো মানুষ edge থেকেই পেলেন।
- ১০:৫৭: দশ মিনিটে ১২ লাখ like। Redis এর ১০টা counter সেটা সামলালো, database পেলো ৫ সেকেন্ডে একটা করে UPDATE।
তানভীর সেই রাতে dashboard এ শুধু একটা ছোট ঢেউ দেখলো। আর আম্মু ফোন করলেন অন্য কারণে: “ছবিটা কী সুন্দর, দেখেছিস?”
Bottleneck গুলো কোথায় লুকিয়ে আছে
Design টা ভালো, কিন্তু নিখুঁত নয়। Interview তে নিজে থেকে এগুলো বললে ভালো ছাপ পড়ে:
- Redis এর memory: ৮০০ GB এর timeline সবচেয়ে দামি অংশ। User বাড়লে machine বাড়াতে হবে, বা ৮০০ এর সীমা কমাতে হবে।
- Fanout এর দেরি: বড় ঘটনার রাতে অনেকে একসাথে post করলে fanout queue লম্বা হয়। Queue এর দৈর্ঘ্য দেখে worker বাড়াতে হয় (সেশন ০৮)।
- মাঝারি account: ৯ লাখ follower এর কেউ celebrity সীমার নিচে, তাই তাঁর প্রতিটা post ৯ লাখ write তৈরি করে। সীমাটা ঠিক জায়গায় রাখা একটা চলমান কাজ।
- Graph DB: প্রতিটা fan-out এর আগে follower list পড়তে হয়। এটাও cache করতে হয়।
এই design আমাদের কী দিলো
- দ্রুত feed: feed খুললে একটাই Redis read হয়, তাই ২০টা post আসে ১০ ms এর কাছাকাছি সময়ে।
- Celebrity তে ভেঙে পড়ে না: বড় account এর post read এর সময় টেনে আনা হয়, তাই ২ কোটি write এর ঢেউ ওঠে না।
- ছবির ভার app server এ পড়ে না: upload যায় সরাসরি object storage এ, আর download আসে CDN থেকে।
- Like গোনা চলতে থাকে: counter গুলো ভাগ করে Redis এ রাখা হয়, database এ যায় কয়েক সেকেন্ড পর পর।
Downside গুলো
- Feed সবসময় একদম fresh নয়: fan-out শেষ হতে কয়েক সেকেন্ড লাগে। এটা eventual consistency (সেশন ০৩)।
- Memory খরচ বেশি: একই post_id হাজার হাজার timeline এ কপি হয়, Redis এ প্রায় ৮০০ GB লাগে।
- দুই রকম path চালাতে হয়: সাধারণ user এর জন্য push, celebrity এর জন্য pull। Code আর debugging দুটোই জটিল হয়।
- Like count কয়েক সেকেন্ড পিছিয়ে থাকে: screen এ ১২,৪০৩ দেখালেও আসল সংখ্যা হয়তো ১২,৪৫০।
Feed design এর মূল কথা একটাই: পড়া সস্তা রাখতে লেখার সময় খাটুনি করা, কিন্তু অন্ধভাবে নয়। সাধারণ মানুষের post push হয়, celebrity এর post pull হয়। Timeline এ থাকে শুধু id, পুরো post আসে hydration এ। ছবি চলে আলাদা পথে, object storage আর CDN দিয়ে। আর প্রতিটা জায়গায় আমরা একটু দেরি মেনে নিয়েছি, কারণ feed এ কয়েক সেকেন্ডের দেরিতে কারো ক্ষতি হয় না।
১০ মগজে প্রেশার দিন
আগে নিজে ভাবুন। তারপর উত্তর খুলে দেখুন: বেশিরভাগ মানুষ কীভাবে ভাবে, সেই চিন্তায় কী বাদ পড়ে, আর আসল কথাটা কোথায়।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“হ্যাঁ। Read, write এর ৫০ গুণ। Post করার সময় সবার timeline এ লিখে দিলে feed খোলা একটা Redis read হয়ে যায়। এর চেয়ে দ্রুত আর কিছু হয় না।”
সাধারণ user এর জন্য কথাটা ঠিক। কিন্তু দুই ধরনের account এ এই নিয়ম ভেঙে পড়ে।
যে জায়গাটা যোগ করা দরকার
প্রথমত, celebrity। ২ কোটি follower এর একটা post থেকে ২ কোটি write হয়। সেটা শেষ হতে কয়েক মিনিট লাগে, আর এর মধ্যে বাকি সবার post আটকে থাকে। দ্বিতীয়ত, নিষ্ক্রিয় user। যিনি মাসে একবারও app খোলেন না, তাঁর timeline এ প্রতিদিন লেখা শুধু memory নষ্ট করে।
তাই উত্তর হলো hybrid। বেশিরভাগ account এর জন্য push, বিশাল account এর জন্য pull, আর নিষ্ক্রিয় user কে fan-out থেকে বাদ।
Fan-out on write এর খরচ follower সংখ্যার সমানুপাতিক। Follower সংখ্যা যেখানে অস্বাভাবিক বড়, সেখানে push থামিয়ে pull করুন।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“Feed এ eventual consistency চলে। কয়েক সেকেন্ড পরে post টা চলে আসবে। এটাই তো আমাদের requirement।”
অন্যদের জন্য কথাটা ঠিক। কিন্তু যিনি নিজে post করলেন, তাঁর অভিজ্ঞতা আলাদা।
যে জায়গাটা যোগ করা দরকার
রাফি যদি রুমানার post দুই সেকেন্ড পরে দেখেন, তিনি টেরও পান না। কিন্তু রুমানা নিজে post করে feed এ না দেখলে ভাববেন post হয়নি। তিনি আবার post করবেন, আর duplicate তৈরি হবে।
তাই নিজের post এর ক্ষেত্রে আলাদা ব্যবস্থা থাকে। Post service save করার সাথে সাথেই রুমানার নিজের timeline এ id টা বসিয়ে দেয়, fanout এর অপেক্ষা না করে। অনেক app আরো সহজ পথ নেয়: phone নিজেই নতুন post টা feed এর উপরে দেখিয়ে দেয়। এই নিশ্চয়তার নাম read-your-writes, মানে নিজের লেখা নিজে সাথে সাথে পড়তে পারা। সেশন ১৫ এ consistency এর এই স্তরগুলো বিস্তারিত দেখবেন।
Eventual consistency সবার জন্য সমান নয়। অন্যরা একটু পরে দেখলে চলে। কিন্তু যিনি লিখলেন, তাঁকে নিজের লেখা সাথে সাথে দেখাতে হয়।
উত্তর দেখুন আগে নিজে ভাবুন
সবাই যেভাবে ভাবে
“Fan-out এর মতোই উল্টো কাজ করবো। একটা worker সব follower এর timeline থেকে ZREM দিয়ে post_id টা মুছে দেবে।”
এভাবে কাজ হয়, কিন্তু এটা ধীর আর ব্যয়বহুল। আর মোছা শেষ হওয়ার আগ পর্যন্ত post টা অনেকের feed এ দেখা যায়।
যে জায়গাটা যোগ করা দরকার
Timeline এ শুধু id থাকে, পুরো post থাকে না। Post টা দেখাতে হলে hydration এ post cache থেকে পড়তেই হয়। তাই post টাকে post cache আর database এ “deleted” চিহ্নিত করলেই যথেষ্ট। Hydration এর সময় deleted post বাদ পড়ে যায়। মাত্র এক জায়গায় একটা write, আর সাথে সাথে সবার feed থেকে post টা অদৃশ্য।
Timeline এ পড়ে থাকা পুরোনো id গুলো পরে ধীরে ধীরে trim এর সময় এমনিই চলে যায়। আর celebrity এর post তো কারো timeline এ ছিলোই না, তাই সেখানে মোছার কিছুই নেই।
Timeline এ id রাখা আর hydration এ পুরো post আনার একটা বড় লাভ এটাই। Post বদলানো বা মোছার জন্য লাখো timeline ছুঁতে হয় না। এক জায়গায় বদলালেই সবার feed এ বদলে যায়।
১১ যে টার্মগুলো শিখলাম
| টার্ম | মানে |
|---|---|
| Functional requirement | System কী কী কাজ করবে, অর্থাৎ user কী কী করতে পারবেন। |
| Non-functional requirement | System কতটা ভালোভাবে কাজ করবে: latency, availability, scale। |
| Home timeline | আপনি যাদের follow করেন, তাদের সবার post মিলিয়ে বানানো feed। |
| User timeline | একজন মানুষের নিজের post এর তালিকা, যা তাঁর profile এ দেখা যায়। |
| Fan-out | একজনের post তাঁর সব follower এর কাছে ছড়িয়ে দেওয়া। |
| Fan-out on read (pull) | Post করার সময় কিছু হয় না; feed খোলার সময় সবার post খুঁজে এনে সাজানো হয়। |
| Fan-out on write (push) | Post করার সময়েই প্রত্যেক follower এর timeline এ post_id বসিয়ে দেওয়া হয়। |
| Celebrity problem | অস্বাভাবিক বেশি follower এর একটা post থেকে কোটি write তৈরি হয়ে fan-out আটকে যাওয়া। |
| Hybrid approach | সাধারণ account এর জন্য push, বিশাল account এর জন্য pull, read এর সময় দুটো মিলিয়ে feed বানানো। |
| Sorted set | Redis এর এমন তালিকা, যেখানে প্রতিটা item একটা score অনুযায়ী সবসময় সাজানো থাকে। |
| Hydration | শুধু id থেকে caption, ছবি, নাম মিলিয়ে পূর্ণ post বানানোর ধাপ। |
| Cursor pagination | “শেষ দেখা post এর চেয়ে পুরোনোগুলো চাই” বলে পরের page আনা, যাতে নতুন post এলেও duplicate না হয়। |
| Offset pagination | “প্রথম N টা বাদ দিয়ে পরেরগুলো চাই” বলে page আনা; তালিকা বদলালে duplicate বা বাদ পড়ে। |
| Object storage | বড় file রাখার storage, যেখানে প্রতিটা file একটা key দিয়ে পাওয়া যায় (যেমন S3)। |
| Presigned URL | অল্প সময়ের জন্য একটা নির্দিষ্ট file upload বা download করার অনুমতিসহ বিশেষ link। |
| Sharded counter | একটা counter কে কয়েকটা ছোট counter এ ভাগ করা, যাতে একটার উপর সব write না পড়ে। |
| Ranking | প্রতিটা post এর score বের করে feed সাজানো, শুধু সময়ের ক্রমে নয়। |
| Candidate generation | ভারী ranking এর আগে সস্তা উপায়ে অল্প কিছু সম্ভাব্য post বাছাই করা। |
| Graph DB | “কে কার সাথে যুক্ত” ধরনের data রাখা আর খোঁজার জন্য বানানো database। |
১২ শর্ট সামারি
- Feed এ read, write এর প্রায় ৫০ গুণ। তাই fan-out on write দিয়ে post করার সময়েই প্রত্যেক follower এর Redis timeline এ post_id বসানো হয়, আর feed খোলা হয়ে যায় একটা read।
- Celebrity এর একটা post থেকে কোটি write তৈরি হয়, যা fan-out কে মিনিটের পর মিনিট আটকে রাখে। Hybrid approach এ বিশাল account এর post read এর সময় pull করা হয়, আর সাধারণ account এর post আগের মতোই push হয়।
- Timeline এ থাকে শুধু ৮০০টা id; পুরো post আসে hydration এ, আর পরের page আসে cursor pagination এ, যাতে নতুন post এলেও duplicate না হয়।
- ছবি সরাসরি object storage এ upload হয় আর CDN থেকে download হয়। Like গোনা হয় Redis এর কয়েকটা ভাগ করা counter এ, আর database এ যায় কয়েক সেকেন্ড পর পর।
১৩ মনে রাখুন
- যে কাজ বেশি হয়, সেটা সস্তা রাখুন। Feed বেশি পড়া হয়, তাই খাটুনিটা সরিয়ে দিন post করার সময়ে।
- সবার জন্য এক নিয়ম চলে না। সাধারণ মানুষের post push করুন, celebrity এর post pull করুন।
- Timeline এ id রাখুন, পুরো post নয়। Memory বাঁচে, আর post বদলালে বা মুছলে এক জায়গায় বদলালেই হয়।
- Feed এ offset নয়, cursor ব্যবহার করুন। নতুন post এলেও duplicate হয় না, আর গভীর page ও দ্রুত থাকে।
- ছবি কখনো app server এর ভেতর দিয়ে নেবেন না। Presigned URL দিয়ে সরাসরি object storage এ পাঠান, আর CDN দিয়ে ছড়িয়ে দিন।
- গরম counter কে ভাগ করুন। এক row এ হাজার write না দিয়ে কয়েকটা counter এ ভাগ করুন, আর database এ কয়েক সেকেন্ড পর পর লিখুন।
- কোথায় দেরি চলে, সেটা আগে ঠিক করুন। অন্যের post বা like এর সংখ্যায় কয়েক সেকেন্ড দেরি চলে, কিন্তু নিজের post নিজের কাছে সাথে সাথে দেখাতে হয়।