এই পর্বে যা শিখবেন
- একটি social news feed-এর requirement, পরিধির বাইরের তালিকা আর ইন্টারভিউয়ার যে প্রশ্নগুলো আশা করেন
- DAU থেকে পোস্ট, feed পড়া, fan-out-এর লেখা আর timeline cache-এর memory-র হিসাব — চালানো যায় এমন হিসাবযন্ত্রে
- Push, pull আর hybrid fan-out-এর খরচ একটি অসম follower graph-এ মাপা, আর সীমা (threshold) বাছাইয়ের প্রভাব
- Timeline cache, hydration, rebuild, post store-এর ভাগ, ছবি আর CDN, cursor-এর পাতা আর consistency-র প্রতিশ্রুতি
- সরল ranking-এর গঠন, like-গণনা, মোছা ও privacy-র প্রচার, অপব্যবহার ঠেকানো, বিকলের পরিকল্পনা আর AI-এর নকশা যাচাই
১ · সমস্যা আর পরিধি — কী বানাচ্ছি, কী বানাচ্ছি না
সকালে ফোন খুলে প্রথম যে পাতাটি দেখেন — বন্ধুর ছবি, প্রিয় লেখকের নতুন লেখা — সেটিই news feed, বা home timeline. প্রতিটি মানুষের তালিকা আলাদা, প্রতি মুহূর্তে বদলায়, আর কোটি মানুষ একসাথে চাইলেও দ্রুত খুলতে হয়। এই পর্বে একটি কাল্পনিক social অ্যাপের feed নকশা করব, পর্ব ৬ — একটি নকশা কীভাবে এগোয়-এর ধাপ মেনে: requirement → হিসাব → API → data model → বড় ছবি → গভীরে → ভাঙন। আগের মডিউলগুলোর ধারণা এখানে শুধু প্রয়োগ হবে, link-সহ।
পাড়ার ডাকঘরে প্রতিটি বাড়ির একটি খোপ। চিঠি এলেই পিয়ন সঠিক খোপে রাখেন — বাড়ির মানুষ শুধু নিজের খোপ দেখেন (push). উল্টো পথে খোপ নেই; প্রত্যেকে এসে বলেন, “আমার বিশজন আত্মীয়ের চিঠি খুঁজে দিন” — কেরানি প্রতিবার বিশটি বস্তা ঘাঁটেন (pull). আর সবার জন্য একটি সরকারি বিজ্ঞপ্তি এলে লাখো খোপে লাখো কপি না রেখে সেটি নোটিশ বোর্ডে টাঙানো হয়, সবাই খোপ দেখার সময় বোর্ডেও চোখ বোলান (hybrid). এই পর্বের নকশা এই তিন কৌশলের হিসাব।
Functional requirement — ব্যবহারকারী যা করতে পারবেন
- পোস্ট: লেখা আর কয়েকটি ছবি; মোছা; public বা শুধু follower-দের জন্য।
- Follow: একমুখী — অনুমতি লাগে না (বন্ধুত্বের মতো দুমুখী নয়); unfollow, block, mute.
- Home timeline: যাঁদের follow করেন তাঁদের পোস্ট, সময়ের বা সাজানো ক্রমে, scroll করলে পরের পাতা।
- সাড়া: like আর comment, আর প্রতিটি পোস্টে তাদের সংখ্যা।
Non-functional requirement — কতটা ভালোভাবে (সংখ্যাগুলো ধরা যাক)
- পড়া-ভারী আর দ্রুত: feed খোলার server-এর দিকের p99 ২০০ ms-এর নিচে — লক্ষ্য p99-এ, গড়ে নয় (পর্ব ৪ — Latency, throughput আর percentile).
- সতেজতা: follower-দের feed-এ নতুন পোস্ট p99 ১০ সেকেন্ডে; like-এর সংখ্যা কয়েক সেকেন্ড পিছিয়ে চলে।
- Durability আর availability: পোস্ট কখনো হারাবে না; feed-এর আগে-তৈরি তালিকা হারালে আবার বানানো যায়। কোনো অংশ বিকল হলেও feed খুলবে, হয়তো সরল রূপে।
- সঠিকতা: নিজের পোস্ট সঙ্গে সঙ্গে নিজের feed-এ; মোছা পোস্ট বা block করা মানুষের পোস্ট পরের পড়াতেই অদৃশ্য।
- মোবাইল-প্রধান: ধরা যাক বেশিরভাগ ব্যবহারকারী ফোনে, অনেকের network ধীর বা মাঝে মাঝে বিচ্ছিন্ন — ছোট উত্তর, কয়েক মাপের ছবি, নিরাপদ retry.
পরিধির বাইরে: সরাসরি message (পর্ব ৫৫ — Case study: chat আর messaging system), notification (পর্ব ৫৭ — Case study: notification system), খোঁজ (পর্ব ২৭ — Search), video (পর্ব ৫৯ — Case study: video streaming), বিজ্ঞাপন, story আর follow-এর বাইরের সুপারিশ। ইন্টারভিউতে এগুলো মুখে বলে বাদ দেওয়াও একটি দক্ষতা।
যে প্রশ্নগুলো আগে জিজ্ঞাসা করবেন
| প্রশ্ন | উত্তর বদলালে নকশার কী বদলায় |
|---|---|
| Follow একমুখী, না দুমুখী বন্ধুত্ব? সর্বোচ্চ কতজন? | দুমুখী আর ৫,০০০-এর সীমা থাকলে “বিখ্যাত” সমস্যাই প্রায় নেই; একমুখী হলে কারও কোটি follower হতে পারে |
| Feed শুধু সময়ের ক্রমে, না সাজানো (ranked)? | Ranked হলে প্রার্থী বাছাই, feature আর স্কোরের একটি পুরো পথ লাগে |
| কত ব্যবহারকারী, দিনে কতবার পোস্ট আর কতবার feed খোলা? | পড়া-লেখার অনুপাতই push বনাম pull-এর প্রথম ইঙ্গিত |
| কত পুরোনো পোস্ট পর্যন্ত scroll? Video আছে? | Timeline-এ কয়টি ID রাখবেন; media-র পথ আর CDN |
২ · হিসাব — পড়া, লেখা, fan-out আর memory
পর্ব ৩ — Back-of-the-envelope হিসাব-এর নিয়মে, প্রতিটি অনুমান আলাদা করে লিখে। নিচের সংখ্যাগুলো কোনো বাস্তব অ্যাপের পরিসংখ্যান নয় — শুধু ধরে নেওয়া মান, যাতে নকশার সিদ্ধান্তগুলো সংখ্যায় দাঁড়ায়:
- DAU ২ কোটি, মোট account ৬ কোটি; প্রতি সক্রিয় ব্যবহারকারী দিনে গড়ে ০.২৫টি পোস্ট আর ১০ বার feed খোলেন, প্রতিবার ২০টি পোস্ট।
- যিনি পোস্ট করেন তাঁর গড়ে ২০০ জন follower; ব্যস্ততম ঘণ্টায় চাপ গড়ের ৩ গুণ (peak factor).
- প্রতিটি timeline-এ ৫০০টি ৮-byte ID, আর প্রতি timeline-এ ১০০ byte বাড়তি হিসাব (key, store-এর নিজস্ব overhead — সংস্করণভেদে আলাদা)।
- একটি পোস্টের সারি ১ KB; ২০% পোস্টে ছবি, মূল ছবি ২০০ KB, আর feed-এ দেখানো ছোট রূপ ৫০ KB.
// News-feed estimation. Every input below is an assumption, not a real statistic.
const DAU = 20e6, registered = 60e6; // daily active users; all accounts
const postsPerDAU = 0.25; // posts per active user per day
const opensPerDAU = 10, pageSize = 20; // feed opens per day; posts shown per open
const avgFollowers = 200; // followers per posting author (push model)
const peak = 3; // peak hour = 3 x the daily average
const ids = 500, idBytes = 8, perTimelineOverhead = 100; // bytes
const DAY = 86400, fmt = n => Math.round(n).toLocaleString("en-US");
const postsDay = DAU * postsPerDAU, readsDay = DAU * opensPerDAU;
const postsS = postsDay / DAY, readsS = readsDay / DAY;
console.log("posts/day " + fmt(postsDay) + " avg/s " + fmt(postsS) + " peak/s " + fmt(postsS * peak));
console.log("feed opens/s avg " + fmt(readsS) + " peak " + fmt(readsS * peak) +
" read:write = " + (readsS / postsS).toFixed(0) + ":1");
const fan = postsS * avgFollowers;
console.log("push fan-out writes/s avg " + fmt(fan) + " peak " + fmt(fan * peak));
console.log("hydrated posts/s at peak " + fmt(readsS * peak * pageSize));
const perTL = ids * idBytes + perTimelineOverhead; // one timeline in cache
for (const [who, n] of [["DAU only", DAU], ["all accounts", registered]]) {
const gb = n * perTL / 1e9;
console.log(("timelines: " + who).padEnd(24) + gb.toFixed(0).padStart(5) + " GB, x2 replicas " +
(2 * gb).toFixed(0) + " GB");
}
const textKB = 1, imgShare = 0.2, imgKB = 200, years = 5; // post row; original image
const tb = x => (x / 1e9).toFixed(1) + " TB"; // input in KB
console.log("post rows over " + years + " y: " + tb(postsDay * textKB * 365 * years) +
" images: " + tb(postsDay * imgShare * imgKB * 365 * years));
const thumbKB = 50; // image size inside the feed
const gbit = readsS * pageSize * imgShare * thumbKB * 1e3 * 8 / 1e9;
console.log("feed image egress avg " + gbit.toFixed(1) + " Gbit/s peak " +
(gbit * peak).toFixed(1) + " Gbit/s (mostly from the CDN)");আউটপুট:
posts/day 5,000,000 avg/s 58 peak/s 174
feed opens/s avg 2,315 peak 6,944 read:write = 40:1
push fan-out writes/s avg 11,574 peak 34,722
hydrated posts/s at peak 138,889
timelines: DAU only 82 GB, x2 replicas 164 GB
timelines: all accounts 246 GB, x2 replicas 492 GB
post rows over 5 y: 9.1 TB images: 365.0 TB
feed image egress avg 3.7 Gbit/s peak 11.1 Gbit/s (mostly from the CDN)
পাঁচটি সিদ্ধান্ত সরাসরি এই সংখ্যাগুলো থেকে আসে:
- পড়া ৪০ গুণ: সেকেন্ডে গড়ে ৫৮টি পোস্ট, কিন্তু ২,৩১৫ বার feed খোলা (peak-এ ৬,৯৪৪)। বারবার পড়ায় লাগে এমন কাজ একবার লেখার সময়ে সেরে রাখাই প্রথম ভাবনা।
- তার দাম — fan-out: push-এ প্রতিটি পোস্ট গড়ে ২০০টি timeline-এ, তাই সেকেন্ডে গড়ে ১১,৫৭৪টি, peak-এ ৩৪,৭২২টি cache-লেখা — পোস্টের ২০০ গুণ। গড়টি নিরীহ; লেজের সমস্যা অংশ ৬-এ।
- Hydration: peak-এ সেকেন্ডে ১,৩৮,৮৮৯টি পোস্টের পূর্ণ রূপ লাগে — এটি database-এ পাঠানো চলে না; একটি post cache অবশ্যই লাগবে (পর্ব ২৮ — Caching কৌশল)।
- শুধু সক্রিয়দের timeline: একটি timeline প্রায় ৪.১ KB; শুধু DAU-র জন্য ৮২ GB, replica-সহ ১৬৪ GB. সব ৬ কোটি account-এর জন্য তিন গুণ, অথচ অনেকে মাসে একবারও খোলেন না।
- ছবি আলাদা পথে: ৫ বছরে পোস্টের সারি ৯.১ TB, কিন্তু মূল ছবি ৩৬৫ TB — object storage-এ; feed-এর ছবির traffic peak-এ প্রায় ১১.১ Gbit/s, যা CDN থেকে যাওয়াই উচিত (পর্ব ১৭ — CDN আর edge). হিসাবে ছবির ছোট রূপ, thumbnail আর replica ধরা হয়নি — বাস্তবে আরও বেশি।
৩ · API — পোস্ট, feed, follow আর like
REST-এর নিয়মগুলো পর্ব ৮ — REST API নকশা-এ; এখানে শুধু এই সমস্যার পাঁচটি সিদ্ধান্ত।
- ছবি সরাসরি storage-এ:
POST /v1/mediaএকটি স্বল্পমেয়াদি presigned URL দেয়; app ছবি সোজা object storage-এ তোলে (পর্ব ২৬ — Object storage), আমাদের server দিয়ে নয়। - পোস্টে idempotency: ধীর network-এ app আবার পাঠাবেই; idempotency key না থাকলে একই পোস্ট দুবার, সব follower-এর feed-এ।
- Like আর follow স্বভাবেই idempotent:
PUT /v1/posts/{id}/likeদুবারেও একটি like,DELETE-এ ফেরানো; “গণনা ১ বাড়ানো” ধাঁচের API-তে retry গণনা বাড়ায়। - Feed-এর পাতা cursor দিয়ে: “৪১ নম্বর থেকে ২০টি” (offset) নয় — ওপরে নতুন পোস্ট ঢুকলে offset সরে গিয়ে একই পোস্ট দুবার আসে। কারণ আর বিকল্প অংশ ৭-এ।
- নতুন পোস্টের খবর: feed নিজে থেকে ওপরে সরে না; app মাঝে মাঝে জিজ্ঞাসা করে বা খোলা সংযোগে খবর পায় (পর্ব ১০ — Real-time যোগাযোগ), আর ব্যবহারকারী “নতুন পোস্ট” বোতাম চাপলে ওপরের পাতা আসে।
POST /v1/posts HTTP/1.1
Authorization: Bearer <token>
Idempotency-Key: 8b1d4c2e-5f6a-4b7c-9d8e-1a2b3c4d5e6f
Content-Type: application/json
{ "text": "Rajshahi-te aaj aam-er haat", "media_ids": ["m_77a1"], "visibility": "public" }
HTTP/1.1 201 Created
Location: /v1/posts/1847562093310976001
{ "post_id": "1847562093310976001", "created_at": "2026-10-28T07:15:02Z" }GET /v1/feed?limit=20&cursor=eyJzIjoiZjlhMiIsIm8iOjQwfQ HTTP/1.1
Authorization: Bearer <token>
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [
{ "post_id": "1847562093310976001",
"author": { "id": "u_311", "name": "Rina", "avatar": "https://cdn.example.com/a/u_311_96.webp" },
"text": "Rajshahi-te aaj aam-er haat",
"media": [{ "url": "https://cdn.example.com/m/m_77a1_640.webp", "w": 640, "h": 480 }],
"likes": 128, "comments": 9, "liked_by_me": true }
],
"next_cursor": "eyJzIjoiZjlhMiIsIm8iOjYwfQ"
}খেয়াল করুন: post_id স্ট্রিং, কারণ ৬৪-bit সংখ্যা JavaScript-এর সাধারণ number-এ নিখুঁত থাকে না। ID-টি Snowflake-ধাঁচের — সময়ের ক্রমে সাজানো, তাই ID থেকেই পোস্টের বয়স বোঝা যায় (পর্ব ২৩)। liked_by_me প্রতিটি পাঠকের নিজের, তাই feed-এর উত্তর ব্যক্তিগত — CDN-এ যায় শুধু ছবি।
৪ · Data model — কোন ডেটা কোথায়, কোন key-তে ভাগ
প্রথম নিয়ম: সত্যের উৎস আর দ্রুত পড়ার কপি আলাদা। পোস্ট, follow আর like টেকসই store-এ; timeline cache হলো সেগুলো থেকে বানানো একটি read model — আসলে একটি materialized view, ইচ্ছাকৃত denormalization (পর্ব ৩৫ — Event sourcing আর CQRS; ডেটাবেস কোর্সের Denormalization). হারালে আবার বানানো যায় — এই একটি বাক্যই অংশ ৭ আর ৯-এর অনেক সিদ্ধান্তের ভিত্তি।
| ডেটা | Key আর ভাগ (partition) | Store (উদাহরণ) | কেন |
|---|---|---|---|
| পোস্ট | post_id-এর hash | sharded SQL বা wide-column store | Hydration সবসময় ID ধরে; hash-এ ভার সমান |
| লেখকের পোস্টের তালিকা (author timeline) | (author_id, মাস), ভেতরে post_id-এর উল্টো ক্রম | wide-column | Pull, hybrid আর rebuild-এ “অমুকের শেষ পোস্টগুলো”; মাস দিয়ে ভাগ অসীম বড় partition ঠেকায় |
| Follow — দুই দিকে | followers(followee_id → follower_id) আর following(follower_id → followee_id) | wide-column বা sharded SQL | Fan-out-এ “কারা আমাকে follow করেন”, rebuild-এ “আমি কাকে” — দুটিই এক partition-এ |
| Like | (post_id, user_id) unique | LSM-ভিত্তিক store | দুবার like অসম্ভব; লেখা-ভারী, তাই LSM (পর্ব ১৯ — Storage engine) |
| গণনা | post_id (গরম পোস্টে কয়েক ভাগে) | counter store + cache | প্রতিবার COUNT(*) নয় (অংশ ৮) |
| Home timeline | timeline:{user_id} → ≤ ৫০০টি post_id | Redis-ধাঁচের cache, replica-সহ | এক key-এ এক পাঠকের পুরো তালিকা |
| ছবি | object key, যেমন m/m_77a1_640.webp | object storage + CDN | বড়, বদলায় না, বহুবার পড়া হয় |
Partition key (পর্ব ২৩ — Partitioning আর sharding): পোস্টকে author_id দিয়ে ভাগ করলে বিখ্যাত লেখকের partition একটি hot spot, আর সেটি বাড়তেই থাকে। তাই পোস্টের সারি post_id-এর hash-এ, আর লেখকের তালিকা আলাদা index-এ, মাস ধরে ভাঙা। followers-এ বিখ্যাত কারও partition কোটি সারির হতে পারে — followee_id-এর সাথে একটি bucket নম্বর জুড়ে কয়েক ভাগে রাখলে fan-out worker-রা সমান্তরালে পড়ে। Query-গুলো সবই key ধরে, join নেই — তাই wide-column store স্বাভাবিক পছন্দ, sharded SQL-ও চলে (পর্ব ২০ — SQL বনাম NoSQL).
৫ · বড় ছবি — লেখার পথ আর পড়ার পথ
সিস্টেম দুই পথে ভাগ: লেখার পথ পোস্ট টেকসইভাবে রেখে পেছনে follower-দের timeline-এ ছড়ায়; পড়ার পথ timeline থেকে ID নিয়ে ছেঁকে, সাজিয়ে, পূর্ণ পোস্ট বানায়। মাঝে একটি message queue — পোস্টকারীকে fan-out-এর অপেক্ষা করতে হয় না।
News feed — লেখার পথ, পড়ার পথ আর ছবির পথ
Write path: post service → post store + outbox → queue → fan-out workers → timeline cache. Read path: timeline service → timeline cache → ranker → post cache (hydration) → client. Images: object storage → CDN.
লেখার পথ — চার ধাপে
(১) Gateway-তে token আর rate limit (পর্ব ১১ — Reverse proxy, API gateway আর BFF). (২) Post service একই local transaction-এ পোস্টের সারি আর outbox-এ একটি event লেখে, যাতে “পোস্ট আছে, event নেই” না হয় (পর্ব ২৫ — Distributed transaction), তারপর 201. (৩) Relay event-টি queue-তে তোলে (পর্ব ৩১ — Message queue; বড় আকারে পর্ব ৩৩ — Log-ভিত্তিক streaming). (৪) Fan-out worker সক্রিয় follower-দের timeline-এ ID বসায় — অংশ ৬-এর stepper.
পড়ার পথ — একটি অনুরোধ শুরু থেকে শেষ
নিচে পাঠক u42 feed খুললেন (সময়গুলো ধরে নেওয়া):
| কোথায় | |
| হাতে কয়টি ID | |
| Network call | |
| সময় (ধরা যাক) |
LRANGE timeline:u42 0 499
1847562093310976001
1847561987043233792
1847561870220394496
… আরও ৪৯৭টি · মোট প্রায় ৪ KB
post cache: MGET post:… ×22 → ২০ hit, ২ miss post store: ২টি ID ধরে → ১টি পাওয়া, ১টি মোছা (tombstone) বৈধ ২১টি → স্কোরের ক্রমে প্রথম ২০টি user cache: লেখকের নাম, ছবি → ২০ hit counter: like, comment → cache থেকে উত্তর: ২০টি পোস্ট + next_cursor · ছবি app আনে CDN থেকে
- দরজায় যাচাই। Gateway token-এর সই নিজেই যাচাই করে (পর্ব ১৫ — Stateless বনাম stateful), rate limit counter-এ একটি বৃদ্ধি, তারপর timeline service.
- আগে-তৈরি তালিকা। একটি call-এ
timeline:u42-এর ৫০০টি ID, প্রায় ৪ KB — ৩০৮ জন সাধারণ followee-র পোস্ট fan-out আগেই বসিয়ে রেখেছে। Database ছোঁয়া হলো না। - বিখ্যাতদের পোস্ট মেশানো। দুই বিখ্যাত followee-র author timeline থেকে শেষ ৪৮ ঘণ্টার ৪০টি ID. এই তালিকা লাখো পাঠকের একই, তাই cache-এ গরম — পর্ব ৩০ — Distributed cache-এর hot key-এর মতো L1-এ রাখা যায়। সাজানো তালিকাগুলো জোড়া লাগে (অংশ ৭-এর heap) — হাতে ৫৪০.
- ছাঁকনি। Mute আর block করা লেখক বাদ, আর আগে দেখানো পোস্ট বাদ — “দেখা হয়েছে কি না” প্রায়ই একটি ছোট Bloom filter-এ (পর্ব ৪৫ — Probabilistic data structure); এর মাঝেমধ্যের ভুল “দেখা হয়েছে” feed-এ মেনে নেওয়া চলে। থাকল ৪১২টি।
- স্কোর। Ranker ৪১২টির feature আনে — বয়স ID থেকে, সম্পর্ক feature store থেকে, গণনা counter cache থেকে — তিনটি batch call-এ; স্কোর করে সেরা ২০টি আর বাড়তি ২টি রাখে (অংশ ৮)। Ranker সাড়া না দিলে সময়ের ক্রমই নিরাপদ বিকল্প।
- Hydration আর উত্তর। শুধু ২২টির পূর্ণ রূপ: post cache-এ ২০টি, ২টি post store-এ — একটি মোছা (tombstone), বাদ; বাড়তি দুটি এজন্যই। প্রথম ২০টি, লেখকের তথ্য আর পরের পাতার cursor-সহ; ছবি আসে CDN থেকে। মোট প্রায় ৪৫ ms, network বাদে।
৩১০ জন followee থাকা সত্ত্বেও পড়া হলো মাত্র ৩টি তালিকা — push-এর পুরস্কার। আর মোছা পোস্টটি ধরা পড়ল পড়ার মুহূর্তে, timeline না ঘেঁটেই — শুধু ID রাখার পুরস্কার।
৬ · গভীরে ১ — fan-out on write, on read, না hybrid?
মোট কাজের তুলনা পর্ব ২ — Requirement আর trade-off-এর অংশ ৬-এ: মানুষ পোস্টের চেয়ে বেশিবার feed খুললে push সস্তা। আমাদের অনুপাত ৪০:১, তাই প্রথম উত্তর push. কিন্তু মোট সংখ্যা লুকায় একটি পোস্টের সবচেয়ে খারাপ খরচ আর পড়ার লেজ।
| কৌশল | একটি পোস্টে লেখা | একবার feed খুলতে পড়া | মূল ঝুঁকি |
|---|---|---|---|
| Push (on write) | F — লেখকের follower-সংখ্যা | ১টি তালিকা | বিখ্যাত লেখকের পোস্টে লাখো লেখা আর দেরি |
| Pull (on read) | ১ | K — followee-সংখ্যা | প্রতিবার K-টি call; সবচেয়ে ধীরটিই সময় ঠিক করে |
| Hybrid, সীমা T | F ≤ T হলে F, নইলে ১ | ১ + সীমার বেশি follower-ওয়ালা followee | T বাছাই; দুই পথের কোড |
Follower-সংখ্যা লম্বা লেজের (heavy-tailed): অল্প কয়েকজনের বিপুল, বাকিদের অল্প। নিচে ২০,০০০ জনের একটি কাল্পনিক graph — প্রত্যেকে ১০ থেকে ১৪৯ জনকে follow করেন, কাকে তা ঠিক হয় Pareto-ধাঁচের জনপ্রিয়তায়, আর তিনজন ইচ্ছা করে খুব জনপ্রিয়। প্রত্যেকে একটি পোস্ট দিলেন; প্রতিটি T-তে দেখানো হলো কতজন pull-এ, প্রতি পোস্টে লেখা, মোট লেখা, আর feed খুলতে কয়টি তালিকা মেলাতে হয়।
// Push vs pull vs hybrid on a seeded follower graph with a heavy tail and 3 huge accounts.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(56), N = 20000;
const w = Array.from({ length: N }, () => Math.pow(1 - rnd(), -1 / 1.8)); // Pareto popularity
w[0] = 900; w[1] = 500; w[2] = 300; // three celebrities
const cum = []; w.reduce((s, x, i) => (cum[i] = s + x), 0);
const pick = () => { const r = rnd() * cum[N - 1]; let lo = 0, hi = N - 1;
while (lo < hi) { const m = (lo + hi) >> 1; cum[m] < r ? lo = m + 1 : hi = m; } return lo; };
const follows = [], fol = new Array(N).fill(0); // follows[u] = whom u follows
for (let u = 0; u < N; u++) {
const s = new Set(), k = 10 + Math.floor(rnd() * 140); // 10..149 followees each
while (s.size < k) { const v = pick(); if (v !== u) s.add(v); }
follows.push([...s]); s.forEach(v => fol[v]++);
}
const pct = (a, p) => [...a].sort((x, y) => x - y)[Math.ceil(p / 100 * a.length) - 1];
const sorted = [...fol].sort((x, y) => y - x);
console.log("followers: median " + pct(fol, 50) + ", p99 " + pct(fol, 99) +
", top 5: " + sorted.slice(0, 5).join(", "));
console.log("T pulled writes/post p50 p99 max total writes read sources p50 p99");
for (const T of [Infinity, 5000, 1000, 200, 0]) { // Infinity = pure push, 0 = pure pull
const wpp = fol.map(f => (f <= T ? f : 0)); // one post by every user
const src = follows.map(l => (T === 0 ? l.length : 1 + l.filter(v => fol[v] > T).length));
const tot = wpp.reduce((a, b) => a + b, 0);
const pulled = fol.filter(f => f > T).length;
console.log(String(T === Infinity ? "push" : T === 0 ? "pull" : T).padEnd(6) + String(pulled).padStart(7) +
String(pct(wpp, 50)).padStart(18) + String(pct(wpp, 99)).padStart(5) + String(Math.max(...wpp)).padStart(7) +
tot.toLocaleString("en-US").padStart(15) + String(pct(src, 50)).padStart(19) + String(pct(src, 99)).padStart(5));
}আউটপুট:
followers: median 53, p99 450, top 5: 14610, 10899, 7792, 5702, 5472
T pulled writes/post p50 p99 max total writes read sources p50 p99
push 0 53 450 14610 1,585,932 1 1
5000 6 53 442 4199 1,536,010 3 7
1000 37 53 407 997 1,485,338 6 13
200 891 51 178 200 1,191,585 20 43
pull 20000 0 0 0 0 79 148
লম্বা লেজ: মাঝের মানুষের follower ৫৩, ৯৯%-এর ৪৫০ বা কম — কিন্তু সবচেয়ে জনপ্রিয়জনের ১৪,৬১০, মাঝের মানের প্রায় ২৭৬ গুণ; push-এ তাঁর একটি পোস্টই ১৪,৬১০টি লেখা।
সীমার প্রভাব: T = ১,০০০-এ মাত্র ৩৭ জন (০.১৯%) pull-এ গেলেন। মোট লেখা কমল প্রায় ৬% (১৫,৮৫,৯৩২ থেকে ১৪,৮৫,৩৩৮), কিন্তু একটি পোস্টের সবচেয়ে খারাপ খরচ ১৪,৬১০ থেকে ৯৯৭ — প্রায় ১৫ গুণ কম। দাম পড়ায়: মাঝের পাঠক ১টির বদলে ৬টি তালিকা মেলান, p99-এ ১৩টি। T = ২০০-তে মোট লেখা প্রায় ২৫% কম, কিন্তু p99 পাঠকের ৪৩টি তালিকা; পুরো pull-এ লেখা শূন্য, পড়া মাঝে ৭৯টি, p99-এ ১৪৮টি।
শিক্ষা: hybrid-এর লাভ মোট খরচে নয়, সবচেয়ে খারাপ ক্ষেত্রে; T নামালে খরচ লেখা থেকে পড়ায় সরে। মডেলটি বিখ্যাতদের তালিকা পড়াকে সাধারণের সমান ধরে — বাস্তবে সেগুলো ভাগের আর cache-এ গরম, তাই সস্তা।
পড়ার লেজ — কেন pull-এ p99 খারাপ
পর্ব ৪-এর fan-out-এর সূত্রে K-টি অংশের প্রতিটি ১% সময় ধীর হলে অন্তত একটি ধীর হওয়ার সম্ভাবনা 1 − 0.99K. Pull-এর মাঝের পাঠকের K = ৭৯-এ প্রায় ৫৪.৮%, K = ১৪৮-এ প্রায় ৭৭.৪%; hybrid-এ (T = ১,০০০) K = ৬-এ প্রায় ৫.৯%, ১৩-তে প্রায় ১২.২%. Pull-এর সমস্যা শুধু বেশি কাজ নয় — বেশি tail latency.
বিখ্যাতের পোস্ট — কত দেরি?
ধরা যাক fan-out-এর ক্ষমতা ঠিক অংশ ২-এর peak, সেকেন্ডে ৩৪,৭২২টি লেখা। তখন ১ কোটি follower-এর একজনের একটি পোস্ট একাই প্রায় ২৮৮ সেকেন্ড (প্রায় ৫ মিনিট) পুরো fan-out দখল করে, বাকিদের পোস্ট আটকে — এটিই celebrity problem. Hybrid-এ সেই পোস্ট যায় শুধু author timeline-এ।
নিচের stepper-এ দুটি পোস্ট: সাধারণ লেখক রিনার (৪ জন follower), আর বিখ্যাত S-এর (২,০০,০০০ জন)। সীমা T = ১০,০০০ ধরা হলো; প্রতিটি timeline সর্বোচ্চ ৫০০টি ID রাখে।
| Queue | |
| B-এর timeline | |
| E-এর timeline | |
| Timeline-এ লেখা |
ZADD timeline:B 1761635702114 9001 (score = পোস্টের সময়, ms · সদস্য = post ID) ZREMRANGEBYRANK timeline:B 0 -501 (সবচেয়ে পুরোনোগুলো বাদ, ৫০০ থাকে) … C আর D-তে একই … E: timeline:E নেই → লেখা বাদ
- পোস্ট টেকসই, event সারিতে। রিনার পোস্ট (ID 9001 — আসলে ১৯ অঙ্কের Snowflake, এখানে ছোট করে) post store-এ, একই transaction-এ outbox-এ event; relay সেটি queue-তে তুলল। রিনা আগেই
201পেয়েছেন। - Follower তালিকা। Worker W1 event তুলে follow graph থেকে রিনার follower আনল: B, C, D, E — ৪ জন, সীমার অনেক নিচে, তাই push.
- শুধু সক্রিয়দের লেখা। B, C আর D-এর timeline-এ ID বসল, ৫০০-তে ছাঁটা — প্রতি cache shard-এ একটি pipeline. E ৪০ দিন অনুপস্থিত, তাঁর timeline নেই, তাই বাদ; ফিরলে নতুন করে বানানো হবে (অংশ ৭)। লেখা শেষ, কিন্তু ack-এর আগেই W1 বন্ধ।
- দ্বিতীয়বার এলেও একবার। Queue at-least-once, তাই event আবার এল; W2 একই লেখা করল। Sorted set-এ সদস্য post ID, তাই একই ID দুবার মানে একটিই সদস্য — feed-এ পোস্ট একবারই (idempotent consumer; পর্ব ৩৪ — Delivery semantics).
- বিখ্যাতের পোস্ট — push নয়। S-এর 9002 গেল post store আর S-এর author timeline-এ। Follower ২,০০,০০০ — সীমার বেশি, তাই fan-out বাদ, event ack. দুই লাখ লেখা বাঁচল, সাধারণদের পোস্টের পেছনে জটও নেই।
- পড়ার মুহূর্তে মিলন। B feed খুললে নিজের timeline আর বিখ্যাত followee-দের author timeline মেলে (অংশ ৫-এর stepper-এর ধাপ ৩): 9002 সবার ওপরে, তারপর 9001. ব্যবহারকারী দুই পথের পার্থক্য টের পান না।
Fan-out-কে সস্তা আর নিরাপদ রাখার আরও কয়েকটি কৌশল
- জোট আর অগ্রাধিকার: follower-দের cache shard ধরে ভাগ করে প্রতি shard-এ একটি pipeline (batching); online follower আগে; বড় লেখকের fan-out আলাদা queue-তে, যাতে সাধারণ পোস্ট না আটকায় (পর্ব ১৮ — Overload সামলানো)।
- সীমার কিনারায় ওঠানামা: কারও follower ৯,৯৯৯ আর ১০,০০১-এর মাঝে দুললে পোস্ট একবার push, একবার pull. ওঠা আর নামার আলাদা সীমা রাখুন (যেমন ১০,০০০-এ উঠলে pull, ৮,০০০-এর নিচে নামলে push).
- Unfollow: পুরোনো ID পড়ার সময় “এখনো follow করেন কি?” দেখে ছাঁকা, পরে ধীরে মোছা।
৭ · গভীরে ২ — timeline-এর ভাণ্ডার, hydration, rebuild আর পাতা
কেন শুধু ID, কেন সীমিত দৈর্ঘ্য
পুরো পোস্টের (১ KB) বদলে ৮-byte ID-তে memory ১২৫ ভাগের এক ভাগ; আর সম্পাদনা, মোছা বা লেখকের ছবি বদলে লাখো timeline-এ হাত দিতে হয় না — পূর্ণ রূপ আসে পড়ার সময় post cache থেকে, cache-aside-এ (পর্ব ২৮; পর্ব ২৯ — Eviction আর invalidation). ৫০০-র পরে কেউ নামলে বাকিটা author timeline থেকে pull — বিরল।
List না sorted set? List-এ memory কম, কিন্তু দেরিতে আসা event ক্রম ভাঙে আর একই ID দুবার ঢোকে। Sorted set-এ সদস্য post ID, score পোস্টের সময় (ms) — ক্রম আর duplicate-রোধ আপনা থেকে, দামে বেশি memory (সংস্করণভেদে; নিজে মাপুন)। পুরো ৬৪-bit ID-কে score করবেন না: score একটি double, ২৫৩-এর বড় পূর্ণসংখ্যা নিখুঁত রাখে না (ডেটাবেস কোর্সের Redis).
অনুপস্থিতদের বাদ, ফিরলে rebuild
শুধু DAU-র timeline-এ ৮২ GB, সবার রাখলে ২৪৬ GB — বাড়তিটুকু প্রায় পড়াই হতো না। তাই ধরা যাক ৩০ দিন অনুপস্থিত হলে timeline মোছে, fan-out তাঁকে বাদ দেয়; ফিরলে followee-দের author timeline থেকে শেষ পোস্ট এনে সময়ের ক্রমে মিলিয়ে নতুন করে বানানো হয়। এটি k-way merge: সাজানো তালিকাগুলোর মাথা একটি heap-এ রেখে প্রতিবার সবচেয়ে নতুনটি তোলা (DSA কোর্সের Heap, heapsort আর priority queue). নিচে ৮ জন followee, প্রত্যেকের ৫০টি পোস্ট।
// Pull at read time: k-way merge of followees' newest-first post lists with a max-heap.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(7), K = 8, PER = 50, N = 10; // 8 followees, 50 recent posts each
const now = 1000000; // seconds; post time = its sort key
const lists = Array.from({ length: K }, (_, a) => {
const gap = 600 * (a + 1) ** 2; // some authors post far more often
let t = now - Math.floor(rnd() * gap);
return Array.from({ length: PER }, () => ({ a: "u" + a, t: (t -= 1 + Math.floor(rnd() * gap)) }));
});
let cmp = 0;
const heap = [], less = (i, j) => (cmp++, heap[i].p.t < heap[j].p.t);
const swap = (i, j) => ([heap[i], heap[j]] = [heap[j], heap[i]]);
function push(x) { heap.push(x); let i = heap.length - 1;
while (i > 0 && less((i - 1) >> 1, i)) { swap(i, (i - 1) >> 1); i = (i - 1) >> 1; } }
function pop() { const top = heap[0], last = heap.pop();
if (heap.length) { heap[0] = last; let i = 0;
for (;;) { let b = i; const l = 2 * i + 1, r = l + 1;
if (l < heap.length && less(b, l)) b = l; if (r < heap.length && less(b, r)) b = r;
if (b === i) break; swap(i, b); i = b; } }
return top; }
lists.forEach((l, s) => push({ p: l[0], s, k: 0 })); // the head of each list
const out = [];
while (out.length < N) {
const x = pop(); out.push(x.p);
if (x.k + 1 < PER) push({ p: lists[x.s][x.k + 1], s: x.s, k: x.k + 1 });
}
out.forEach((p, i) => console.log(String(i + 1).padStart(2) + ". " + p.a + " " + (now - p.t) + " s ago"));
const taken = {}; out.forEach(p => (taken[p.a] = (taken[p.a] || 0) + 1));
console.log("taken per author:", JSON.stringify(taken));
console.log("heap comparisons: " + cmp + " sort all " + K * PER + " then cut: about " +
Math.round(K * PER * Math.log2(K * PER)) + " comparisons");আউটপুট:
1. u0 45 s ago
2. u0 632 s ago
3. u0 1052 s ago
4. u1 1189 s ago
5. u0 1365 s ago
6. u1 1509 s ago
7. u0 1609 s ago
8. u0 1889 s ago
9. u0 2033 s ago
10. u0 2365 s ago
taken per author: {"u0":8,"u1":2}
heap comparisons: 77 sort all 400 then cut: about 3458 comparisons
সবচেয়ে নতুন ১০টির ৮টিই u0-এর, ২টি u1-এর — ঘন ঘন পোস্টকারীই জেতেন; merge ঠিক, সমস্যা সময়ের ক্রমের, আর তা ranking-এর শেষ ছাঁকনির কাজ (অংশ ৮)। খরচ: heap-এ ৭৭টি তুলনা, সব ৪০০টি সাজালে মোটামুটি ৩,৪৫৮টি — প্রায় ৪৫ গুণ কম। তবে আসল খরচ network: ৮টি তালিকা মানে ৮টি পড়া — অংশ ৬-এর “read sources”.
| timeline:E | |
| কাজ | |
| Network call | |
| E যা দেখছেন |
- Miss.
timeline:Eনেই। E-এর দুটি ফোন একসাথে চাইতে পারে, তাই ছোট একটি lock বা request coalescing — একজনই বানাবেন, বাকিরা অপেক্ষা করবেন; নইলে একই কাজ কয়েকবার (cache stampede). - কাকে follow করেন।
following-এর E-এর partition থেকে ১২০ জন; তিন বিখ্যাত এমনিতেই পড়ার সময় মেশেন, তাই বাদ — ১১৭ জন। - আনা আর মেলানো। ১১৭টি author timeline-এর শেষ ৭ দিনের ID, প্রায় ১০টি তালিকা এক batch-এ — ১২টি call, সমান্তরালে; তারপর heap-এ merge, প্রথম ৫০০টি। এই নকশার সবচেয়ে দামি পড়া, তাই বিরল রাখা জরুরি।
- লেখা আর সক্রিয় চিহ্ন। তালিকা cache-এ গেল, E সক্রিয় চিহ্নিত — পরের fan-out তাঁকে আর বাদ দেবে না। প্রথম পাতা অংশ ৫-এর পথে (ranking, hydration) চলে গেল।
- স্বাভাবিক জীবন। এখন সাধারণ followee-দের পোস্ট push হয়ে সরাসরি E-এর timeline-এ। Rebuild আর push-এর মাঝে আসা পোস্ট দুই পথেই ঢুকতে পারে — একই ID, তাই একবারই।
Rebuild-এর ঢল: একটি timeline cache node মরলে লাখো timeline একসাথে হারায়, আর কয়েক মিনিটে সবার rebuild — প্রত্যেকে ১১৭টি তালিকা পড়ে (পর্ব ৩০-এর node-বিকলের হিসাবের মতো)। প্রতিকার: replica; সীমিত-হারের আলাদা rebuild pool; আর সীমা ছাড়ালে সাময়িক সরল feed — graceful degradation.
Post store, post cache আর ছবি
Post store post_id-এর hash-এ ভাগ, প্রতি ভাগে leader আর replica (পর্ব ২২ — Replication); hydration-এর ১,৩৮,৮৮৯/s-এর প্রায় সবটা post cache থেকে, আর ভাইরাল পোস্ট app server-এর ছোট TTL-এর L1-এ। ছবি presigned URL-এ object storage-এ ওঠে; একটি পেছনের কাজ কয়েক মাপের রূপ বানায় (পর্ব ৩৭ — Background job); রূপগুলোর URL বদলায় না, তাই CDN-এ দীর্ঘ মেয়াদে cache হয় (নেটওয়ার্ক কোর্সের Web cache, proxy আর CDN).
Cursor দিয়ে পাতা — offset কেন ভাঙে
Offset-এর পাতায় প্রথম পাতার পর ওপরে ৩টি নতুন পোস্ট এলে পুরোনো ১৮–২০ নম্বর সরে ২১–২৩-এ, দ্বিতীয় পাতায় আবার আসে। সময়ের ক্রমে সমাধান keyset: cursor-এ “এই post ID-র চেয়ে পুরোনো” — Snowflake ID সময়ে সাজানো। Ranked feed-এ স্কোর বদলায়, তাই প্রথম পাতার সাজানো ID-তালিকা কয়েক মিনিটের একটি “feed session”-এ রাখা হয়, cursor-এ session আর অবস্থান — উপরের cursor-টি base64-এ ঠিক এটাই: {"s":"f9a2","o":40}. নতুন পোস্টের জন্য ওপরে “নতুন পোস্ট” বোতাম। Offset বনাম cursor-এর সাধারণ আলোচনা পর্ব ৮-এ।
Consistency — কার কাছে কী প্রতিশ্রুতি
পর্ব ৪০ — CAP, PACELC আর consistency model-এর ভাষায় follower-দের feed eventually consistent: queue আর replication lag-এ কয়েক সেকেন্ড দেরি স্বাভাবিক। তিনটি জায়গায় কড়া প্রতিশ্রুতি:
- নিজের পোস্ট নিজে দেখা (read-your-writes): না পেলে মানুষ আবার পোস্ট করেন। পড়ার সময় পাঠকের নিজের শেষ কয়েক মিনিটের পোস্ট post store-এর leader বা app-এর মনে রাখা তালিকা থেকে জুড়ে দিন — fan-out-এর ওপর ভরসা নয়।
- সময় পেছায় না (monotonic reads): একবার দেখা পোস্ট পরের refresh-এ পিছিয়ে থাকা replica থেকে পড়ে উধাও হবে না — একই ব্যবহারকারীকে একই replica-তে রাখা সাধারণ উপায়।
- মোছা আর block সঙ্গে সঙ্গে: timeline-কে বিশ্বাস না করে পড়ার মুহূর্তে বর্তমান অবস্থা দেখে ছাঁকা — অংশ ৫-এর stepper-এর ধাপ ৪ আর ৬।
৮ · গভীরে ৩ — ranking আর like-গণনা
সময়ের ক্রম থেকে সাজানো feed
সবচেয়ে সরল feed reverse-chronological — নতুনটি ওপরে; স্বচ্ছ, কিন্তু অংশ ৭-এর আউটপুটের মতো ঘন ঘন পোস্টকারী পাতা দখল করেন, আর বন্ধুর জরুরি খবর চাপা পড়ে। তাই বেশিরভাগ বড় feed কোনো না কোনোভাবে সাজানো, পর্ব ২৭-এর খোঁজের ranking-এর মতো ধাপে:
- Candidate generation: সস্তা নিয়মে কয়েকশো প্রার্থী — নিজের timeline cache, বিখ্যাতদের নতুন পোস্ট, হয়তো follow-এর বাইরের কিছু সুপারিশ।
- Feature আর স্কোর: প্রতিটি প্রার্থীর কয়েকটি সংকেত — বয়স, লেখকের সাথে পাঠকের সম্পর্ক বা affinity, like-comment-এর মতো engagement signal — একটি সূত্রে বা প্রশিক্ষিত মডেলে একটি সংখ্যা।
- Re-ranking আর ছাঁকনি: একই লেখকের পোস্ট পরপর না রাখা, দেখা পোস্ট বাদ, নীতিমালা আর privacy-র ছাঁকনি, তারপর পাতা।
নিচে একটি হাতে-লেখা সূত্র, কোনো বাস্তব পণ্যের নয়:
Log-এর কারণে ১০০০তম like-এর মূল্য প্রথম কয়েকটির চেয়ে অনেক কম; comment-এর ওজন like-এর ৩ গুণ, কারণ comment-এ বেশি মনোযোগ লাগে (ধরে নেওয়া)। শেষ গুণকটি time decay: প্রতি half-life-এ স্কোর অর্ধেক। ৩০টি কাল্পনিক প্রার্থীতে A (wa = ২, we = ১, half-life ৬ ঘণ্টা), তারপর দুটি বদল — B: engagement-এর ওজন ৩ গুণ; C: half-life ২৪ ঘণ্টা।
// Rank 30 seeded candidate posts: recency decay x (affinity + engagement). Not a real product's formula.
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rnd = mulberry32(2026);
const posts = Array.from({ length: 30 }, (_, i) => ({
id: "p" + String(i + 1).padStart(2, "0"),
age: +(rnd() * 48).toFixed(1), // hours since posted
aff: +(rnd() ** 2).toFixed(2), // 0..1: how much this reader interacts with the author
likes: Math.floor(Math.pow(1 - rnd(), -1.3) * 3) - 3, comments: 0 }));
posts.forEach(p => (p.comments = Math.floor(p.likes * rnd() * 0.2)));
function score(p, W) {
const eng = Math.log10(1 + p.likes + 3 * p.comments); // diminishing returns
return (W.aff * p.aff + W.eng * eng) * Math.pow(0.5, p.age / W.halfLife);
}
function best(W, n = 10) {
return [...posts].sort((a, b) => score(b, W) - score(a, W) || a.age - b.age).slice(0, n);
}
const show = (label, list) => console.log(label.padEnd(15) + list.map(p => p.id).join(" "));
const A = { aff: 2, eng: 1, halfLife: 6 };
show("chronological", [...posts].sort((a, b) => a.age - b.age).slice(0, 10));
show("ranked A", best(A));
show("B: eng x3", best({ ...A, eng: 3 }));
show("C: halfLife 24", best({ ...A, halfLife: 24 }));
console.log("\nid age h aff likes com score A");
for (const p of best(A, 5))
console.log(p.id.padEnd(6) + p.age.toFixed(1).padStart(5) + p.aff.toFixed(2).padStart(6) +
String(p.likes).padStart(7) + String(p.comments).padStart(5) + score(p, A).toFixed(3).padStart(9));
const viral = posts.reduce((m, p) => (p.likes > m.likes ? p : m));
console.log("most liked: " + viral.id + " (" + viral.likes + " likes, " + viral.age + " h old) rank A: " +
(best(A, 30).indexOf(viral) + 1) + ", rank B: " + (best({ ...A, eng: 3 }, 30).indexOf(viral) + 1) +
", rank C: " + (best({ ...A, halfLife: 24 }, 30).indexOf(viral) + 1));আউটপুট:
chronological p29 p04 p18 p23 p28 p10 p08 p06 p13 p05
ranked A p29 p18 p23 p08 p04 p28 p13 p06 p14 p20
B: eng x3 p29 p23 p04 p18 p08 p06 p28 p13 p14 p20
C: halfLife 24 p29 p08 p18 p23 p14 p13 p06 p28 p20 p04
id age h aff likes com score A
p29 0.9 0.83 37 2 2.977
p18 8.5 0.95 6 1 1.086
p23 8.6 0.01 151 16 0.859
p08 13.3 0.61 158 28 0.776
p04 2.0 0.00 7 0 0.717
most liked: p16 (246 likes, 47.7 h old) rank A: 21, rank B: 19, rank C: 14
সময় বনাম স্কোর: সময়ের ক্রমের দশে ছিল p10 (affinity ০.১৭, ১টি like) আর p05 (০.০১, ০টি like) — নতুন, কিন্তু প্রায় অচেনা লেখক আর সাড়া নেই। A তাদের সরিয়ে আনল p14 (প্রিয় লেখক, ০.৯৪, ১৯ ঘণ্টা আগের) আর p20. p29 দুই ক্রমেই প্রথম।
ওজনের সামান্য বদল: B-তে p23 (১৫১টি like, কিন্তু লেখক প্রায় অচেনা) তৃতীয় থেকে দ্বিতীয়তে, p18 (প্রিয় লেখক, ৬টি like) দ্বিতীয় থেকে চতুর্থে। C-তে পুরোনো ভালো পোস্ট p08 দ্বিতীয়তে, আর নতুন কিন্তু অচেনা p04 দশমে। সবচেয়ে বেশি like-এর p16 (২৪৬টি, ৪৭.৭ ঘণ্টা পুরোনো) A-তে ২১তম, C-তেও ১৪তম — ক্ষয়ই প্রধান। একটি সংখ্যায় পুরো অভিজ্ঞতা বদলায়; তাই ওজন ঠিক হয় পরীক্ষা (A/B test) আর মাপে।
Feature-এর সতেজতা আর মডেল — ধারণার স্তরে
প্রতিটি সংকেতের সতেজতার চাহিদা আলাদা (feature freshness): affinity সাধারণত রাতের batch-এ (পর্ব ৩৬ — Batch আর stream processing), like-গণনা stream থেকে মিনিটে, আর block বা মোছা কোনো feature নয় — পড়ার মুহূর্তের বাধ্যতামূলক ছাঁকনি। বড় সেবাগুলোর প্রকাশিত বিবরণে প্রায়ই সূত্রের বদলে প্রশিক্ষিত মডেল “পাঠক সাড়া দেবেন কি না”-র সম্ভাবনা অনুমান করে (learning to rank-এর ধারণা)। একটি নীরব বিপদ training-serving skew — মডেল শিখেছিল এক রকম feature-মানে, চালু অবস্থায় পায় আরেক রকম; প্রতিকারে প্রায়ই পরিবেশনের মুহূর্তের feature-ই log করে প্রশিক্ষণে নেওয়া হয়। মডেলের ভেতরটা এই কোর্সের বাইরে; system design-এর প্রশ্ন: feature কোথা থেকে, কত দ্রুত, আর ranker বিকল হলে কী।
Like-গণনা — সত্য একটি সারিতে, দেখানো সংখ্যা আরেক জায়গায়
প্রতিবার SELECT COUNT(*) মানে peak-এ সেকেন্ডে ১,৩৮,৮৮৯টি গণনা-query; আবার প্রতি like-এ একটি সারিতে +1 মানে ভাইরাল পোস্টে hot row — ধরা যাক সেকেন্ডে ৫,০০০টি like এক lock-এর লাইনে। তাই তিন স্তর:
- সত্য:
likes(post_id, user_id)-এ unique শর্ত — দুবার like অসম্ভব, retry নিরাপদ; “আমি like করেছি কি না” এখান থেকে। - দেখানোর গণনা: aggregator এক সেকেন্ডের like জমিয়ে একবারে “+৫,০০০” লেখে; গরম পোস্টে sharded counter — ১৬ ভাগে প্রতিটিতে সেকেন্ডে প্রায় ৩১৩, পড়ায় যোগফল (cache-এ)।
- মেলানো: পেছনের কাজ মাঝে মাঝে সত্যের টেবিল থেকে গুনে ফারাক ঠিক করে।
সব গণনা নিখুঁত হওয়ারও দরকার নেই: কতজন আলাদা মানুষ দেখেছেন — কয়েক KB-এর HyperLogLog, প্রায় ১% ভুলে; এই ঘণ্টায় কোন পোস্ট সবচেয়ে সাড়া পাচ্ছে — count-min sketch (দুটিই পর্ব ৪৫-এ)। পর্দায় “১২ হাজার” থাকলে কয়েকশোর ফারাক কেউ টের পান না।
৯ · ভাঙন, trade-off আর বিস্তার
কোনটি মরলে কী হয়
| যা বিকল | ব্যবহারকারী যা দেখেন | প্রতিকার |
|---|---|---|
| Timeline cache-এর একটি node | কিছু মানুষের feed ধীর বা খালি | Replica থেকে failover; সীমিত-হারের rebuild; সাময়িক সরল feed (অংশ ৭) |
| Fan-out worker বা queue জমে যাওয়া | নতুন পোস্ট দেরিতে পৌঁছায়, হারায় না | Queue টেকসই; worker বাড়ানো; বড় লেখকের আলাদা queue; ID-তে duplicate-রোধ |
| Post store-এর একটি shard | কয়েকটি পোস্ট নেই, বাকিরা আছে | Replica; hydration-এ না-পাওয়া পোস্ট বাদ দিয়ে পাতা পূরণ (বাড়তি প্রার্থী) |
| Ranker ধীর বা বন্ধ | Feed সময়ের ক্রমে | অল্প timeout, তারপর সময়ের ক্রম — পর্ব ৪৮ — Resilience pattern-এর circuit breaker |
| Counter service | গণনা একটু পুরোনো বা লুকানো | Cache-এর শেষ মান; গণনা ছাড়াই পোস্ট দেখানো |
১০ গুণ বড় হলে
DAU ২০ কোটি হলে peak-এ fan-out সেকেন্ডে প্রায় ৩.৫ লাখ লেখা, সক্রিয়দের timeline প্রায় ৮২০ GB (replica-সহ ১.৬ TB-এর বেশি)। হাতল: timeline ছোট করা; “সক্রিয়” ৩০ থেকে ৭ দিন; T নামানো; region ধরে আলাদা cache আর fan-out (পর্ব ৫১ — Failover আর disaster recovery). বড় খরচ timeline cache-এর RAM, fan-out আর CDN — দাম প্রদানকারী ও সময়ভেদে, তাই নিজে হিসাব করুন।
মোছা আর privacy-র প্রচার
পোস্টের কপি ছড়িয়ে আছে বহু জায়গায় — লাখো timeline-এ ID, post cache, CDN-এ ছবি, খোঁজের index, গণনা, backup. তাই মোছা দুই ধাপে। সঙ্গে সঙ্গে: post store-এ tombstone; hydration সেটি দেখেই বাদ দেয়, timeline-এ ID থাকলেও। পেছনে, নিশ্চিতভাবে: post cache মোছা, CDN-এ purge, index থেকে সরানো, timeline-এর ID ধীরে পরিষ্কার, আর retention policy মেনে backup-এর মেয়াদে মোছা। Account মোছায় crypto-shredding-এর মতো কৌশল আলোচিত; আইনের চোখে কতটা যথেষ্ট, তা এখতিয়ারভেদে আলাদা। Privacy বদল বা block-ও পড়ার মুহূর্তে যাচাই — “এই পাঠক কি এটি দেখার অধিকারী?” — object-level authorization (পর্ব ৫২ — নকশায় নিরাপত্তা; privacy by design).
অপব্যবহার ঠেকানো — রক্ষার দিক থেকে
- হারের সীমা: প্রতি account-এ পোস্ট, follow আর like-এর হার সীমিত, নতুন account-এ আরও কড়া (পর্ব ৪৯ — Rate limiting); follow-এর সীমা fan-out-এর খরচও বাঁধে।
- ছড়ানোর আগে যাচাই: সন্দেহজনক account-এর পোস্ট fan-out-এর আগে spam ও নীতিমালা-যাচাই পেরোয় — লাখো timeline-এ পৌঁছানোর আগেই থামে। Report থেকে পর্যালোচনা, নিশ্চিত হলে মোছার পথ।
- ভুয়া follower আর like: হঠাৎ অস্বাভাবিক লাফ চিহ্নিত করা, সন্দেহজনক সাড়া ranking-এ না গোনা। Unique শর্ত দুবার গোনা ঠেকায়, ভুয়া account নয় — সেজন্য আচরণ-বিশ্লেষণ।
কী মাপবেন, কিসে alert
পর্ব ৫০ — Observability ধরে: feed-এর p99 আর ত্রুটির হার; fan-out lag — পোস্ট থেকে follower-এর timeline পর্যন্ত সময়ের p99 (লক্ষ্য ১০ সেকেন্ড); queue-র সবচেয়ে পুরোনো event-এর বয়স; timeline cache-এর hit ratio আর মিনিটে rebuild (হঠাৎ বাড়লে একটি node মরেছে); hydration-এ না-পাওয়ার হার; খালি feed-এর হার; আর দেখানো ও সত্যের গণনার ফারাক। Alert দিন ব্যবহারকারীর অভিজ্ঞতায়, CPU-তে নয়।
“পড়া লেখার প্রায় ৪০ গুণ, তাই প্রত্যেক সক্রিয় ব্যবহারকারীর একটি আগে-তৈরি timeline — cache-এ ≤ ৫০০টি post ID, outbox আর queue-র পেছনে fan-out on write দিয়ে ভরা। Follower-সংখ্যা লম্বা লেজের, তাই সীমার বেশি follower-ওয়ালাদের পোস্ট পড়ার সময় মেশাই — একটি পোস্টের সবচেয়ে খারাপ খরচ বাঁধা, দামে পড়ায় কয়েকটি বাড়তি তালিকা। শুধু ID রাখি, তাই মোছা আর privacy পড়ার মুহূর্তের hydration আর ছাঁকনিতে কার্যকর; অনুপস্থিতদের timeline নেই, ফিরলে heap-এ merge করে বানাই। Ranking প্রার্থী → স্কোর → ছাঁকনি, বিকলে সময়ের ক্রম। Like-এর সত্য unique সারিতে, দেখানো গণনা জমানো ও ভাগ করা; পাতা cursor-এ; নিজের পোস্ট সঙ্গে সঙ্গে। মাপি fan-out lag আর feed p99; বড় ঝুঁকি cache node হারিয়ে rebuild-এর ঢল — replica আর সীমিত-হারের rebuild.”
আরও গভীরে: Martin Kleppmann-এর Designing Data-Intensive Applications (২০১৭)-এর প্রথম অধ্যায় Twitter-এর ২০১২ সালের একটি প্রকাশ্য বক্তৃতার তথ্য ধরে home timeline-এর pull → push → hybrid-এর গল্প বলে (ভাবনার প্রশ্ন ০১)। প্রতিটি লেখক-পাঠক জোড়ার জন্য push না pull, দুজনের হার দেখে বাছাইয়ের গবেষণা-রূপ Yahoo!-এর গবেষকদের “Feeding Frenzy” (Silberstein ও সহকর্মীরা, SIGMOD ২০১০)। Social graph-এর বিশাল ভাণ্ডারের একটি প্রকাশিত নকশা Facebook-এর TAO (Bronson ও সহকর্মীরা, USENIX ATC ২০১৩)। এগুলো প্রকাশের সময়ের বিবরণ; আজকের নকশা আলাদা হতে পারে।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউতে এই case-এ দুর্বল আর শক্তিশালী উত্তরের পার্থক্য সাধারণত এখানে:
- Push না pull? দুর্বল: “push দ্রুত।” শক্তিশালী: অনুপাত দিয়ে push, লেজের জন্য hybrid, সীমার দাম পড়ায় — সংখ্যাসহ।
- Timeline-এ কী? দুর্বল: পুরো পোস্ট। শক্তিশালী: শুধু ID, সীমিত দৈর্ঘ্য, আলাদা hydration, অনুপস্থিতদের বাদ আর rebuild.
- বিখ্যাত কেউ পোস্ট করলে? দুর্বল: “queue আছে।” শক্তিশালী: কোটি লেখায় কত সেকেন্ড, অন্যদের দেরি, পড়ার সময় মেশানো।
- Like-গণনা? দুর্বল: প্রতিবার COUNT. শক্তিশালী: unique সত্য, জমানো বৃদ্ধি, sharded counter, মেলানো।
- মোছা পোস্ট? দুর্বল: “সব timeline থেকে মুছব।” শক্তিশালী: পড়ার সময় tombstone-এ ছাঁকা, বাকি কপি পেছনে।
১০ · AI-এর যুগে — news feed-এর নকশা যাচাই
AI-কে feed-এর নকশা চাইলে কয়েক সেকেন্ডে পরিচ্ছন্ন ছবি আসে — Kafka, Redis, Cassandra, microservices, ML ranker. শব্দগুলো ঠিক; সমস্যা থাকে সংখ্যায়, লেজে আর বিকলে:
| AI-এর ভুল | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
| হিসাব ছাড়াই “Kafka, Redis আর microservices”, বা অঙ্কে ভুল | push না pull — সংখ্যা ছাড়া বলা যায় না; ৮৬,৪০০ দিয়ে ভাগ বাদ, bit আর byte গুলিয়ে ফেলা | অংশ ২-এর estimator নিজের অনুমানে চালান, এককসহ |
| শুধু গড় follower দিয়ে fan-out | লম্বা লেজ লুকায়; একজনের একটি পোস্টে লাখো লেখা | সবচেয়ে বড় follower-সংখ্যায় আলাদা হিসাব; অংশ ৬-এর simulation |
| Timeline-এ পুরো পোস্ট, সব account-এর জন্য | Memory বহু গুণ; মোছা আর সম্পাদনায় লাখো timeline | Memory আবার হিসাব; প্রশ্ন: মোছা পোস্ট কীভাবে লুকাবে? |
| বিকলের কথা নেই | Cache node মরলে rebuild-এর ঢল; ranker বন্ধ হলে feed বন্ধ | প্রতিটি বাক্সের জন্য “এটি মরলে কী?” — অংশ ৯-এর টেবিল |
| “Strong consistency রাখব” — কোন অর্থে, বলা নেই | ACID-এর consistency আর CAP-এর consistency আলাদা; feed-এর দরকার নির্দিষ্ট প্রতিশ্রুতি — নিজের পোস্ট, মোছা, block | প্রতিটি প্রতিশ্রুতি নাম ধরে লিখুন (পর্ব ৪০) |
| পণ্যের সীমা আর দাম বানানো | Store-এর memory-overhead, CDN-এর দাম সংস্করণ আর সময়ভেদে | অফিসিয়াল documentation; নিজে ভরে মাপুন |
| অতিরিক্ত নকশা | ছোট অ্যাপে প্রথম দিনেই ML ranker আর পাঁচটি সার্ভিস | প্রশ্ন: আজকের সংখ্যায় সবচেয়ে সরল কোনটি কাজ করে? |
AI-কে কাজটি এমনভাবে দিন যাতে প্রতিটি সিদ্ধান্তের পেছনে সংখ্যা, লেজ, বিকল আর consistency-র অর্থ দেখাতে হয়:
AI-এর feed নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট
গড় নয়, লেজ দেখুন; বাক্স নয়, বিকল দেখুন।
ভাবনার প্রশ্ন
ইন্টারভিউয়ার মূল নকশার পর যে প্রশ্নগুলোয় চাপ দিতে পারেন। নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Twitter-এর home timeline প্রথমে pull, পরে push, শেষে hybrid হয়েছিল বলে প্রকাশ্যে বর্ণিত — এই ইতিহাস থেকে নকশার কী শেখার আছে?
Martin Kleppmann-এর Designing Data-Intensive Applications (২০১৭)-এর প্রথম অধ্যায় গল্পটি বলে, ২০১২ সালের নভেম্বরে QCon San Francisco-তে Twitter-এর একটি প্রকাশ্য বক্তৃতার তথ্য ধরে। বইটির উদ্ধৃত সংখ্যা অনুযায়ী তখন গড়ে সেকেন্ডে প্রায় ৪.৬ হাজার tweet লেখা হতো (peak-এ ১২ হাজারের বেশি), কিন্তু home timeline পড়া হতো সেকেন্ডে প্রায় ৩ লাখ বার — পড়া লেখার প্রায় ৬৫ গুণ।
- প্রথম পথ — pull: একটি relational query: “আমি যাঁদের follow করি, তাঁদের সব tweet সময়ের ক্রমে দাও।” পড়ার চাপ বাড়তেই এটি টেকেনি।
- দ্বিতীয় পথ — push: প্রত্যেকের জন্য একটি timeline cache, নতুন tweet সেখানে ছড়ানো। বইটির বর্ণনায় গড় একটি tweet প্রায় ৭৫ জন follower-এর কাছে যেত, তাই সেকেন্ডে ৪.৬ হাজার tweet মানে প্রায় সাড়ে ৩ লাখ cache-লেখা — কিন্তু পড়া হয়ে গেল সস্তা।
- তৃতীয় পথ — hybrid: কিছু account-এর কয়েক কোটি follower; তাঁদের একটি tweet-এর fan-out-ই বিশাল। তাই তাঁদের tweet আলাদা করে পড়ার সময় মেশানো।
শিক্ষা তিনটি। এক, “load parameter” আগে চিনুন — এখানে আসল চাপের মাপ সেকেন্ডে tweet নয়, follower-সংখ্যার বণ্টন; বইটি ঠিক এই কথাটিই জোর দিয়ে বলে। দুই, নকশা স্থির নয় — একই পণ্য আকারের সাথে তিনবার মৌলিক কৌশল বদলেছে, আর প্রতিবার বদলের কারণ ছিল মাপা সংখ্যা। তিন, সবচেয়ে কঠিন অংশ ছিল গড় নয়, লেজ। মনে রাখবেন, এগুলো প্রকাশের সময়ের বর্ণনা; পরে প্ল্যাটফর্মটির নাম, মালিকানা আর feed-এর ধরন (যেমন সুপারিশ-ভিত্তিক timeline) বদলেছে, আর আজকের ভেতরের নকশা আলাদা হতে পারে।
মূল উপলব্ধি: ইন্টারভিউতে এই গল্পটি “আমি জানি অমুক কোম্পানি কী করে” হিসেবে নয়, যুক্তি হিসেবে বলুন — পড়া-লেখার অনুপাত push-কে, আর লম্বা লেজ hybrid-কে বাধ্য করে।
প্র ০২Hybrid-এর সীমা T কীভাবে ঠিক করবেন? শুধু follower-সংখ্যা দেখা কি যথেষ্ট?
না — follower-সংখ্যা একটি ভালো শুরু, কিন্তু আসল প্রশ্ন খরচের। একটি লেখক-পাঠক জোড়ার কথা ভাবুন। Push-এ খরচ হয় লেখক যতবার পোস্ট করেন ততবার (প্রতি পোস্টে পাঠকের timeline-এ একটি লেখা); pull-এ খরচ হয় পাঠক যতবার feed খোলেন ততবার (প্রতিবার লেখকের তালিকা একবার পড়া)। তাই মোটামুটি নিয়ম: পাঠক যদি লেখকের পোস্টের চেয়ে বেশিবার feed খোলেন, push; নইলে pull. Yahoo!-এর গবেষকদের “Feeding Frenzy” (Silberstein ও সহকর্মীরা, SIGMOD ২০১০) প্রতিটি জোড়ার জন্য আলাদা করে এই ধরনের সিদ্ধান্তের একটি রূপ প্রস্তাব করে।
এর থেকে কয়েকটি ব্যবহারিক সিদ্ধান্ত:
- সক্রিয় follower গুনুন, মোট নয়: ১ লাখ follower-এর ৯০% অনুপস্থিত হলে push-এর আসল খরচ ১০ হাজার — অংশ ৬-এর stepper-এর E-এর মতো বাকিরা এমনিতেই বাদ।
- পোস্টের হারও দেখুন: মাসে একবার পোস্ট করা বিখ্যাত কারও মোট খরচ কম, কিন্তু সেই এক মুহূর্তের ঢেউ আর দেরিটাই সমস্যা — তাই latency-র লক্ষ্য দিয়েও একটি ঊর্ধ্বসীমা: “একটি পোস্টের fan-out যেন N সেকেন্ডের বেশি না নেয়”।
- পড়ার দিকের দাম: T নামালে পাঠকের তালিকা বাড়ে (অংশ ৬-এর p99 কলাম)। পড়ার p99-এর বাজেট থেকে একটি নিম্নসীমা আসে।
- ওঠানামা আটকানো: ওঠার আর নামার আলাদা সীমা, আর সিদ্ধান্ত দিনে একবার — নইলে একজনের পোস্ট কখনো push, কখনো pull.
কিছু নকশায় মাঝামাঝি পথও আলোচিত: বিখ্যাতের পোস্ট তাঁর সবচেয়ে সক্রিয় অল্প কিছু follower-এর কাছে push, বাকিদের জন্য pull. সব ক্ষেত্রেই ভিত্তি মাপা হার, আর সীমা বদলের পর fan-out lag আর feed p99 দুটোই দেখুন।
মূল উপলব্ধি: T একটি সংখ্যা নয়, একটি খরচের ভারসাম্য — লেখার খরচ আর দেরি বনাম পড়ার খরচ আর লেজ। সেটি মাপা হার দিয়ে ঠিক হয়, আর আকার বদলালে আবার ঠিক করতে হয়।
প্র ০৩সময়ের ক্রম না সাজানো feed — কোনটি ব্যবহারকারীর জন্য ভালো, আর engagement-এর জন্য সাজানোর বিপদ কী?
দুটির পক্ষেই যুক্তি আছে। সময়ের ক্রম স্বচ্ছ — আপনি জানেন কেন কী দেখছেন, কেউ ক্রম নিয়ন্ত্রণ করছে না; কিন্তু বেশি পোস্টকারী জেতেন, জরুরি খবর হারায়। সাজানো feed প্রাসঙ্গিক জিনিস ওপরে আনে, কিন্তু অস্বচ্ছ, আর কী মাপা হচ্ছে তার ওপর পুরো অভিজ্ঞতা নির্ভর করে।
ইতিহাসে দুটি চিহ্ন। ২০১০ সালের f8 সম্মেলনে Facebook-এর প্রকৌশলীরা “EdgeRank” নামে একটি সরল ধারণা প্রকাশ্যে ব্যাখ্যা করেছিলেন বলে বহুল প্রচারিত: প্রতিটি ঘটনার স্কোর মোটামুটি সম্পর্ক (affinity) × ঘটনার ধরনের ওজন × সময়ের ক্ষয় — এই পর্বের সূত্রের খুব কাছাকাছি। পরে প্রতিষ্ঠানটি জানায় তারা বহু সংকেতের মডেলে সরে গেছে। আর ২০১৬ সালে Instagram সময়ের ক্রম থেকে সাজানো feed-এ যাওয়ার ঘোষণা দেয়; পরে বিভিন্ন প্ল্যাটফর্ম আবার সময়ের ক্রমের বিকল্পও ফিরিয়েছে।
Engagement-এর বিপদ: শুধু click, like আর সময় বাড়ানোর লক্ষ্যে সাজালে রাগ বা উত্তেজনা জাগানো জিনিস পুরস্কার পেতে পারে, কারণ সেগুলোতে সাড়া বেশি। আরেকটি আলোচিত আশঙ্কা filter bubble — শুধু আগের পছন্দের ধরনের জিনিস দেখানো; এর প্রভাব কতটা, তা নিয়ে গবেষণায় মতভেদ আছে। আইনও প্রবেশ করছে: ইউরোপীয় ইউনিয়নের Digital Services Act (২০২২) অনুযায়ী খুব বড় প্ল্যাটফর্মকে recommender-এর অন্তত একটি এমন বিকল্প দিতে হয় যা ব্যক্তিগত profiling-এর ওপর নির্ভর করে না — খুঁটিনাটি আর প্রয়োগ এখতিয়ার ও সময়ভেদে মিলিয়ে নিন।
নকশায় যা দাঁড়ায়: সময়ের ক্রম একটি বাস্তব বিকল্প আর ranker বিকলে ফেরার পথ; “কেন এই পোস্ট” ব্যাখ্যা; লক্ষ্য-মাপে শুধু click নয়, দীর্ঘমেয়াদি সন্তুষ্টি আর “এটি কম দেখাও”-এর মতো সংকেত; re-ranking-এ বৈচিত্র্য।
মূল উপলব্ধি: ranking-এর সূত্র একটি মূল্যবোধের ঘোষণা — কী মাপবেন, সেটাই বাড়বে। Engineer হিসেবে আপনার কাজ বিকল্প, স্বচ্ছতা আর মাপার সঠিক লক্ষ্য নকশায় রাখা।
প্র ০৪Like আর view-এর সংখ্যা কি সবসময় নিখুঁত হতে হবে? কোনটি নিখুঁত, কোনটি আনুমানিক রাখবেন?
প্রথমে প্রশ্ন করুন: সংখ্যাটি দিয়ে কে কী সিদ্ধান্ত নেন? তিন স্তরে ভাগ করা যায়।
- নিখুঁত, প্রতিটি মানুষের নিজের: “আমি কি like করেছি?” — ভুল হলে মানুষ দুবার চাপেন বা বিভ্রান্ত হন। এটি সত্যের টেবিলের unique সারি থেকে, read-your-writes-সহ।
- শেষ পর্যন্ত নিখুঁত, টাকার সাথে যুক্ত: লেখককে view বা সাড়ার ভিত্তিতে অর্থ দেওয়া হলে, বা বিজ্ঞাপনের হিসাবে — দেরি চলে, ভুল চলে না। Event log থেকে নিয়মিত মিলিয়ে দেখা (reconciliation), আর ভুয়া সাড়া বাদ দেওয়ার পর।
- আনুমানিক, শুধু দেখানোর: পোস্টের নিচের “১২ হাজার” — কয়েকশোর ফারাক কেউ টের পান না। Sharded counter, জমিয়ে লেখা, আর দেখার সংখ্যায় HyperLogLog.
একটি বিখ্যাত সতর্কবার্তা: ২০১৪ সালের ডিসেম্বরে YouTube জানায় একটি গানের video-র দেখার সংখ্যা ৩২-bit signed পূর্ণসংখ্যার সর্বোচ্চ মান ২,১৪,৭৪,৮৩,৬৪৭ ছাড়িয়ে যাওয়ার কাছাকাছি, তাই তারা গণনাটি ৬৪-bit-এ নিয়েছে — বহুল প্রচারিত ঘটনা। শিক্ষা: গণনার ঘর শুরু থেকেই ৬৪-bit রাখুন; “এত বড় কখনো হবে না” ভাবনাটিই ভুল। আরেকটি দিক মানুষের: ২০১৯ সালে Instagram কয়েকটি দেশে like-এর সংখ্যা লুকিয়ে পরীক্ষা চালায়, পরে ব্যবহারকারীদের নিজে লুকানোর বিকল্প দেয় বলে প্রকাশিত — অর্থাৎ সংখ্যাটি দেখানো হবে কি না, সেটিও পণ্যের সিদ্ধান্ত।
একাধিক region-এ একই পোস্টের like একসাথে বাড়লে প্রতিটি region-এর নিজের ঘর রেখে যোগফল নেওয়ার CRDT counter কাজে লাগে — পর্ব ৪৬ — Conflict resolution আর CRDT.
মূল উপলব্ধি: “গণনা” একটি নয়, কয়েকটি আলাদা প্রয়োজন। প্রত্যেকটির জন্য নিখুঁততা, দেরি আর খরচ আলাদা করে ঠিক করুন — সব সংখ্যাকে নিখুঁত রাখার চেষ্টাই সবচেয়ে দামি ভুল।
প্র ০৫বেশিরভাগ ব্যবহারকারী ফোনে, network ধীর বা মাঝে মাঝে বিচ্ছিন্ন, data-ও সীমিত — feed-এর নকশায় কী বদলাবেন?
Server-এর ৪৫ ms ব্যবহারকারীর কাছে কিছুই না, যদি network-এ ৩ সেকেন্ড যায়। ধরা যাক অনেক ব্যবহারকারী সীমিত mobile data-য়, ভিড়ের জায়গায় দুর্বল সংকেতে, বা ঘন ঘন সংযোগ হারানো অবস্থায় — এমন পরিবেশে নকশার অগ্রাধিকার বদলায়:
- ছোট উত্তর: পাতা ছোট (১০–২০টি পোস্ট), অপ্রয়োজনীয় ঘর বাদ, compression; দরকারে Protobuf-এর মতো সংক্ষিপ্ত ফরম্যাট (পর্ব ১২ — Data format আর serialization).
- লেখা আগে, ছবি পরে: feed-এর লেখা আগে আসে, ছবি আসে পরে, পর্দার কাছাকাছি এলে; ছবির কয়েকটি মাপ, আধুনিক ফরম্যাট, আর “data সাশ্রয়” মোডে ছোটটি।
- App-এ শেষ feed রাখা: অ্যাপ খুললেই আগের feed দেখানো, “শেষ হালনাগাদ” লেখা সহ, আর পেছনে নতুনটি আনা — stale-while-revalidate-এর ভাবনা।
- নিরাপদ retry: সংযোগ কাটলে app আবার পাঠাবেই — পোস্টে idempotency key, like আর follow-এ PUT/DELETE; পোস্ট লেখার সময় সংযোগ না থাকলে app-এর ভেতরের সারিতে রেখে পরে পাঠানো।
- কম round trip: একটি অনুরোধে feed, গণনা আর লেখকের তথ্য একসাথে (BFF, পর্ব ১১); সংযোগ বারবার নতুন না করা — HTTP/2 বা HTTP/3-এর সুবিধা (নেটওয়ার্ক কোর্সের HTTP/2 আর HTTP/3).
- ব্যাটারি: ঘন ঘন polling-এর বদলে নতুন পোস্টের খবর push notification-এ (পর্ব ৫৭) বা কম হারে।
সবচেয়ে জরুরি: মাপুন ব্যবহারকারীর ফোন থেকেই — app-এর ভেতর থেকে feed খুলতে আসল সময়ের p50 আর p99, network-এর ধরন অনুযায়ী ভাগ করে। Server-এর dashboard সবুজ থাকলেও ধীর network-এর ব্যবহারকারীর অভিজ্ঞতা খারাপ হতে পারে, আর গড়ে সেটি লুকিয়ে যায়।
মূল উপলব্ধি: latency-র বাজেট server-এ শেষ হয় না — ব্যবহারকারীর হাতে শেষ হয়। ধীর network-এ সবচেয়ে বড় লাভ আসে কম byte, কম round trip আর আগের ডেটা দেখিয়ে অপেক্ষা লুকানো থেকে।
অনুশীলন
আগে নিজে হিসাব করুন বা লিখুন, তারপর উত্তর মেলান। হিসাবগুলো অংশ ২ আর ৮-এর প্রোগ্রামে মান বদলে চালিয়েও যাচাই করতে পারেন।
-
হিসাব — বড় একটি অ্যাপ: ধরা যাক DAU ৫ কোটি, মোট account ৬ কোটি; প্রতি সক্রিয় ব্যবহারকারী দিনে ০.৪টি পোস্ট আর ১২ বার feed খোলেন (প্রতিবার ২০টি পোস্ট); পোস্টকারীর গড় follower ১৫০; peak গড়ের ৪ গুণ; প্রতিটি timeline-এ ৩০০টি ৮-byte ID আর ১০০ byte বাড়তি। (ক) পোস্ট/s আর feed খোলা/s, গড় ও peak? (খ) push-এ fan-out লেখা/s? (গ) শুধু DAU-র timeline-এর memory, replica-সহ? (ঘ) সবার timeline রাখলে কত বাড়ে, আর এবার অনুপস্থিতদের বাদ দেওয়ার লাভ কেন কম?
- (ক) পোস্ট: ৫ কোটি × ০.৪ = ২ কোটি/দিন; ÷ ৮৬,৪০০ ≈ ২৩১/s, peak ≈ ৯২৬/s. Feed: ৫ কোটি × ১২ = ৬০ কোটি/দিন ≈ ৬,৯৪৪/s, peak ≈ ২৭,৭৭৮/s; অনুপাত ৩০:১.
- (খ) ২৩১.৪৮ × ১৫০ ≈ ৩৪,৭২২/s গড়, peak-এ ১,৩৮,৮৮৯/s. Hydration-এ peak-এ ২৭,৭৭৮ × ২০ ≈ ৫,৫৫,৫৫৬টি পোস্ট/s — post cache আরও জরুরি।
- (গ) প্রতি timeline ৩০০ × ৮ + ১০০ = ২,৫০০ byte; ৫ কোটি × ২,৫০০ = ১২৫ GB, replica-সহ ২৫০ GB.
- (ঘ) ৬ কোটি × ২,৫০০ = ১৫০ GB (replica-সহ ৩০০) — এক কপিতে মাত্র ২৫ GB বেশি, কারণ এখানে account-এর ৮৩% প্রতিদিন সক্রিয়। অনুপস্থিতদের বাদ দেওয়ার লাভ DAU আর মোট account-এর অনুপাতে; অংশ ২-এ সেটি ১:৩ ছিল। Estimator-এ মানগুলো বসালে ঠিক এই সংখ্যাই আসে।
-
Requirement বদলে নতুন নকশা: পণ্য দল “পাবলিক পেজ” চালু করতে চায় — যেকোনো পেজ ১ কোটি পর্যন্ত follower পেতে পারে, আর পেজের পোস্ট সব follower-এর feed-এ ৫ সেকেন্ডের মধ্যে (p99) দেখা যেতে হবে। Fan-out-এর ক্ষমতা অংশ ২-এর peak, সেকেন্ডে ৩৪,৭২২টি লেখা। নকশা কীভাবে বদলাবেন?
- Push অসম্ভব: ১ কোটি ÷ ৩৪,৭২২ ≈ ২৮৮ সেকেন্ড; দশ গুণ ক্ষমতাতেও প্রায় ২৮.৮ সেকেন্ড — লক্ষ্য ভাঙে, আর অন্যদের পোস্ট আটকে থাকে।
- পেজ মানেই pull: পোস্ট শুধু পেজের author timeline-এ, সঙ্গে সঙ্গে; follower feed খুললে মেশে। দৃশ্যমান হতে লাগে শুধু author timeline-এর cache হালনাগাদের সময়, সেকেন্ডের কম।
- পড়ার দাম: প্রতিটি follow করা পেজ একটি বাড়তি তালিকা — পেজ follow-এর সীমা, বা বহু পেজের পাঠকের জন্য পেজগুলোর পোস্ট আগে একটি “পেজ-timeline”-এ মেলানো।
- Hot key: জনপ্রিয় পেজের author timeline-এ L1, কপি আর coalescing (পর্ব ৩০); কিন্তু L1-এর TTL-ও ৫ সেকেন্ডের বাজেটের অংশ — TTL ১–২ সেকেন্ড, বা pub/sub-এ invalidation.
- আলাদা চাহিদা: “feed-এ দেখা” আর “ফোনে বিজ্ঞপ্তি” এক নয় — দ্বিতীয়টি notification system-এর (পর্ব ৫৭)। আর পেজের পোস্ট থেকে দৃশ্যমান হওয়ার p99-কে নতুন SLI বানান।
-
একটি ত্রুটিপূর্ণ নকশার সমালোচনা: একজন সহকর্মীর প্রস্তাব (অংশ ২-এর সংখ্যায়, মোট account ৬ কোটি): (ক)
POST /posts-এর handler নিজেই সব follower-এর timeline-এ লিখে তবে উত্তর দেয়; (খ) timeline cache-এ সব account-এর জন্য ৫০০টি করে পুরো পোস্টের JSON (১ KB করে); (গ) feed-এর পাতা?offset=দিয়ে; (ঘ) প্রতিটি পোস্ট দেখানোর সময়SELECT COUNT(*)দিয়ে like-গণনা; (ঙ) পোস্ট মুছলে সব timeline খুঁজে মুছে তবে “মোছা হয়েছে” বলা। প্রতিটির সমস্যা সংখ্যাসহ বলুন আর বিকল্প দিন।- (ক) Synchronous fan-out: অংশ ৬-এর graph-এই একটি অনুরোধে ১৪,৬১০টি লেখা; কোটি follower-এ কয়েক মিনিট — timeout, retry, duplicate. বিকল্প: outbox + queue + worker, বিখ্যাতদের জন্য pull.
- (খ) পুরো পোস্ট, সবার জন্য: ৬ কোটি × ৫০০ × ১ KB = ৩০ TB RAM; ID-only আর শুধু DAU-তে ৮২ GB — প্রায় ৩৬৬ গুণ। প্রতিটি সম্পাদনায় লাখো কপি পুরোনো। বিকল্প: ID-only, hydration, অনুপস্থিতদের বাদ।
- (গ) Offset: ওপরে নতুন পোস্ট এলে পরের পাতায় একই পোস্ট ফেরে। বিকল্প: keyset বা feed session-এর cursor (অংশ ৭)।
- (ঘ) প্রতিবার COUNT: peak-এ সেকেন্ডে প্রায় ১,৩৮,৮৮৯টি গণনা-query. বিকল্প: জমানো, sharded counter, cache-এ (অংশ ৮)।
- (ঙ) সব timeline খুঁজে মোছা: কোন timeline-এ ID আছে তার index নেই — কোটি তালিকা ঘাঁটা, ততক্ষণ পোস্ট দৃশ্যমান, মাঝপথে বিকলে আধাআধি। বিকল্প: tombstone আর পড়ার সময় ছাঁকনি, বাকিটা পেছনে (অংশ ৯)।
-
আউটপুট অনুমান করুন: অংশ ৮-এর
feed_ranking.js-এA-এরhalfLife: 6-কেhalfLife: 2করলে “ranked A”-র লাইনটি সময়ের ক্রমের লাইনের কাছাকাছি যাবে, না দূরে? p05 আর p10 কি ফিরে আসবে? সবচেয়ে বেশি like-এর p16-এর A-তে স্থান উঠবে না নামবে? আগে অনুমান লিখুন, তারপর চালিয়ে মেলান।অনুমান: half-life ছোট মানে বয়সের শাস্তি কড়া — ক্রম সময়ের ক্রমের কাছে যাবে, দুর্বল কিন্তু নতুন পোস্ট ফিরতে পারে, পুরোনো p16 আরও নামবে।
আসল আউটপুট (প্রথম দুই লাইন আর শেষ লাইন):
chronological p29 p04 p18 p23 p28 p10 p08 p06 p13 p05 ranked A p29 p04 p18 p23 p08 p28 p13 p06 p10 p14 most liked: p16 (246 likes, 47.7 h old) rank A: 26, rank B: 24, rank C: 14প্রথম চারটি হুবহু সময়ের ক্রমে; দশটির নয়টিই সময়ের ক্রমের দশে — শুধু p05 (০টি like) বাদ, তার জায়গায় p14. p10 নবমে ফিরেছে: সেটি p14-এর চেয়ে ৫.৮ ঘণ্টা নতুন, half-life ২ ঘণ্টায় প্রায় ২২.৯ ≈ ৭.৫ গুণ সুবিধা — p14-এর বেশি affinity তা পুষাতে পারে না। p16 ২১ থেকে ২৬-এ নামল। B-ও বদলায়, কারণ সেটি A থেকে তৈরি (
{ ...A, eng: 3 }); C বদলায় না, তার half-life আলাদা করে ২৪। শিক্ষা: half-life খুব ছোট হলে ranking কার্যত সময়ের ক্রম।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমPush-ভিত্তিক একটি news feed-এ ছবিসহ একটি পোস্ট follower-এর পর্দায় পৌঁছানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📷 App presigned URL পেয়ে ছবিটি সরাসরি object storage-এ তোলে
- 📝 Post service পোস্টের সারি আর outbox-এর event একই transaction-এ লেখে, তারপর 201
- 📨 Relay event-টি queue-তে তোলে
- 👥 Fan-out worker লেখকের follower তালিকা আনে আর অনুপস্থিতদের বাদ দেয়
- 🗂️ সক্রিয় follower-দের timeline cache-এ post ID বসে, তালিকা ৫০০-তে ছাঁটা হয়
- 📱 Follower feed খুললে ID থেকে hydration হয়, ছবি আসে CDN থেকে
একটি hybrid news feed-এ সীমা T = ১০,০০০ follower. ২০ লাখ follower-এর একজন লেখক একটি পোস্ট দিলেন। পোস্টটি তাঁর follower-দের feed-এ কীভাবে পৌঁছায়?
- Fan-out worker ২০ লাখ timeline-এ ID লেখে
- পোস্ট শুধু লেখকের author timeline-এ যায়; follower feed খোলার মুহূর্তে সেটি টেনে এনে মেশানো হয়
- প্রত্যেক follower-কে notification পাঠানো হয়, feed-এ পোস্ট আসে না
- শুধু অনুপস্থিতদের বাদ দিয়ে বাকি সব timeline-এ লেখা হয়
News feed-এর timeline cache-এ পুরো পোস্টের বদলে শুধু post ID রাখার সবচেয়ে বড় লাভ কোনটি?
- Feed পড়তে আর কোনো বাড়তি lookup লাগে না
- ID থেকেই ranking-এর সব তথ্য পাওয়া যায়
- Memory অনেক কম লাগে, আর সম্পাদনা বা মোছায় timeline-গুলো বদলাতে হয় না
- Cache হারালেও আর rebuild লাগে না
একটি pull-ভিত্তিক feed খুলতে একজন পাঠকের ১০০ জন followee-র তালিকা সমান্তরালে পড়তে হয়। প্রতিটি পড়া স্বাধীনভাবে ১% সময় খুব ধীর হয়। একবার feed খোলায় অন্তত একটি ধীর পড়ার সম্ভাবনা মোটামুটি কত?
- প্রায় ৬৩%
- প্রায় ১%
- প্রায় ১০০%
- প্রায় ৫০%
একটি feed-এর পাতা ?offset=20&limit=20 দিয়ে আনা হয়। প্রথম পাতা পড়ার পর ওপরে ৩টি নতুন পোস্ট এলো। দ্বিতীয় পাতায় কী ঘটবে, আর সমাধান কী?
- কিছুই না — offset নতুন পোস্ট নিজে থেকে বাদ দেয়
- ৩টি পোস্ট বাদ পড়বে; সমাধান limit বাড়ানো
- Cache-এর TTL কমিয়ে দিলেই ঠিক হয়
- প্রথম পাতার শেষ ৩টি পোস্ট আবার দেখা যাবে; সমাধান cursor-ভিত্তিক পাতা
একটি ভাইরাল পোস্টে সেকেন্ডে প্রায় ৫,০০০টি like আসছে, আর প্রতিটি feed-এ like-এর সংখ্যা দেখাতে হয়। কোন নকশাটি সবচেয়ে উপযুক্ত?
- প্রতিবার দেখানোর সময় likes টেবিলে COUNT(*)
- (post_id, user_id) unique সারিতে সত্য; event জমিয়ে কয়েক ভাগের counter-এ লেখা, cache থেকে দেখানো
- একটি সারিতে প্রতিটি like-এ count = count + 1
- প্রতিটি app নিজে like গুনে server-কে মোট পাঠায়
একটি push-ভিত্তিক feed-এ fan-out-এর queue কয়েক সেকেন্ড পিছিয়ে আছে। একজন ব্যবহারকারী পোস্ট করে সঙ্গে সঙ্গে নিজের feed খুললেন। কোন ব্যবস্থাটি নিশ্চিত করে যে তিনি নিজের পোস্ট দেখবেন?
- Fan-out worker-এর সংখ্যা বাড়ানো
- Eventual consistency মেনে নেওয়া, কয়েক সেকেন্ড পরে আসবে
- পড়ার সময় পাঠকের নিজের শেষ কয়েক মিনিটের পোস্ট সরাসরি post store থেকে এনে মিশিয়ে দেওয়া
- Timeline cache-এর TTL কমিয়ে দেওয়া
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- News feed পড়া-ভারী — আগে অনুপাত মাপুন; ধরে নেওয়া সংখ্যায় ৪০:১, তাই আগে-তৈরি timeline আর fan-out on write.
- Follower-সংখ্যা লম্বা লেজের; push-এ একটি পোস্টের খরচ = follower-সংখ্যা। Hybrid: সীমার বেশি follower-ওয়ালাদের পোস্ট পড়ার সময় মেশানো — সবচেয়ে খারাপ খরচ বাঁধা, দামে পড়ায় কয়েকটি বাড়তি তালিকা।
- Timeline-এ শুধু ID, সীমিত দৈর্ঘ্য, শুধু সক্রিয়দের; পূর্ণ রূপ আসে hydration-এ; অনুপস্থিত ফিরলে heap-এ k-way merge করে rebuild, ঢল ঠেকাতে সীমিত হারে।
- Ranking = প্রার্থী → স্কোর → ছাঁকনি; ranker বিকল হলে সময়ের ক্রম। Like-এর সত্য unique সারিতে, দেখানো গণনা জমানো ও sharded.
- Cursor-এ পাতা; নিজের পোস্ট সঙ্গে সঙ্গে (read-your-writes); মোছা আর block পড়ার মুহূর্তে কার্যকর, বাকি কপি পেছনে পরিষ্কার; মাপুন fan-out lag আর feed p99.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৭ · Case study: notification system — push, SMS, email আর নির্ভরযোগ্য deliveryপরবর্তী পর্বএকটি ঘটনা থেকে কোটি বিজ্ঞপ্তি, নির্ভরযোগ্যভাবে।
- পর্ব ৫৫ · Case study: chat আর messaging system — delivery, ordering আর presenceএই মডিউলেআরেকটি fan-out — group-এ message পৌঁছানো আর ক্রম।
- পর্ব ২ · Requirement আর trade-offভিত্তিPush বনাম pull-এর মোট কাজের সূত্র।
- পর্ব ৩০ · Distributed cache — partitioning, replication, hot key আর বহু-স্তরের cacheভিত্তিTimeline cache-এর আকার, node বিকল আর hot key.
- পর্ব ২৩ · Partitioning আর sharding — range, hash, hot spot, rebalancing আর unique IDভিত্তিPost store-এর ভাগ আর Snowflake ID.
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- DSA · Heap, heapsort আর priority queueপাশের ট্র্যাকk-way merge-এর পেছনের heap.
- DBMS ও SQL · Denormalization — কখন নিয়ম ভাঙা যায়পাশের ট্র্যাকআগে-তৈরি timeline কেন একটি ইচ্ছাকৃত কপি।