Object Storage-এর অন্দরমহল — Flat Namespace, Consistency, আর Erasure Coding
Object Storage Internals: Flat Namespace, Consistency, and Erasure Coding
Object storage-এ 'folder' একটা illusion — bucket-এর ভেতরে সবকিছু আসলে flat key, listing মানে prefix query; S3 বহু বছর eventually consistent ছিল, ২০২০-এ strong হলো; আর durability আসে replication বা তার চেয়ে সস্তা এক পথে — erasure coding দিয়ে, যেখানে k-টা data chunk + m-টা parity chunk-এর যেকোনো k-টা বেঁচে থাকলেই পুরো object ফিরে আসে।
আগে এটা বুঝি
virtualization-cloud/reverse-proxy-nginx-envoy লেসন যেখানে থেমেছিল, সেখান থেকেই আজ শুরু। এতদিন এই module একটা প্রশ্নের চারপাশে ঘুরেছে: compute-কে কীভাবে অর্কেস্ট্রেট করা যায় — VM থেকে container, namespace/cgroup থেকে Pod, load balancer থেকে reverse proxy আর service mesh পর্যন্ত। কিন্তু একটা অ্যাপ্লিকেশনের অর্ধেক কাজ কখনো compute না — সেটা data। প্রতিটা photo, video, backup, log file, ML dataset — কোথাও-না-কোথাও, বিশাল স্কেলে, স্থায়ীভাবে সংরক্ষিত থাকতে হয়। আজকের লেসন থেকে module-এর শেষ কয়েকটা লেসন compute থেকে জুম আউট করে অন্য cloud primitive-গুলোর দিকে যাচ্ছে — প্রথমে storage, তারপর network।
আর storage-এর আধুনিক archetype একটাই নাম: object storage, যার সবচেয়ে পরিচিত বাস্তবায়ন Amazon S3 (2006-এ চালু, cloud-এর প্রথম সত্যিকারের “primitive” service-গুলোর একটা)। এর UI/CLI-তে আপনি “folder” দেখেন — photos/2024/vacation.jpg আপলোড করলে মনে হয় যেন photos নামের একটা folder-এর ভেতরে 2024 নামের আরেকটা folder-এর ভেতরে ফাইলটা বসল। এই illusion-টা এতই বিশ্বাসযোগ্য যে অভিজ্ঞ ইঞ্জিনিয়াররাও মাঝে মাঝে ভুলে যান — এর নিচে আসলে কোনো ডিরেক্টরি-ট্রি নেই। আজকের প্রথম কাজ এই illusion-টা ভেঙে দেখানো, আর তার performance-পরিণতি বের করা।
তারপর আমরা সরাসরি ফিরে যাব distributed-systems/consistency-models লেসনে — কারণ S3-র নিজের ইতিহাসে consistency ladder-এর সবচেয়ে বিখ্যাত, সবচেয়ে ব্যয়বহুল climb ঘটেছে: বছরের পর বছর eventually consistent থাকার পর, ২০২০-এ AWS ঘোষণা করল strong read-after-write consistency। এই একটা ঘোষণা কেন এত বড় খবর ছিল, তা বুঝতে হলে ওই লেসনের ভাষাটাই লাগবে।
শেষে দুইটা concrete মেকানিজম: multipart upload (বড় object কীভাবে অংশে-অংশে ওঠে, আর কেন অসম্পূর্ণ upload কখনো “half an object” হিসেবে দেখা যায় না), আর durability — কীভাবে object storage data হারানো থেকে রক্ষা করে, plain replication দিয়ে না হলে তার চেয়ে সস্তা এক কৌশলে, erasure coding দিয়ে, যেখানে আমরা নিজেরাই একটা ছোট সংস্করণ লিখে চালাব।
মূল ধারণা
Bucket + key — যেখানে “folder” একটা মিথ্যা সান্ত্বনা
Object storage-এর ডেটা মডেল বিস্ময়করভাবে সরল: প্রতিটা object চেনা যায় ঠিক দুইটা জিনিস দিয়ে — একটা bucket (top-level namespace, সাধারণত একটা globally-unique নাম) আর তার ভেতরে একটা key (একটা string, যা সেই bucket-এর মধ্যে object-টাকে uniquely identify করে)। ব্যস, এটাই পুরো ঠিকানা-ব্যবস্থা: bucket + key = object। কোনো তৃতীয় dimension নেই।
যখন আপনি photos/2024/vacation.jpg নামের একটা key-তে একটা object আপলোড করেন, আপনার মনে হয় আপনি photos folder-এর ভেতরে 2024 নামের sub-folder-এ একটা file রাখলেন। বাস্তবে যা ঘটেছে তা আরও বিরক্তিকরভাবে সরল: আপনি একটা single string তৈরি করেছেন — "photos/2024/vacation.jpg" — যেটার ভেতরে দুইটা / অক্ষর আছে। ব্যস। Object storage-এর ভেতরের ইঞ্জিন এই string-টাকে কখনোই parse করে “প্রথমে photos নামের একটা directory খুঁজি, তার ভেতরে 2024 নামের subdirectory খুঁজি, তারপর vacation.jpg খুঁজি” — এইভাবে হাঁটে না। এটা string-টাকে একটা অখণ্ড key হিসেবে দেখে, ঠিক যেভাবে একটা key-value store-এ "user:42:email" একটা single key — কেউ user-কে “folder” ভাবে না।
এই একটা তথ্য থেকেই একটা গুরুত্বপূর্ণ পরিণতি বেরোয়: যখন আপনি S3 console-এ photos/2024/ “folder”-এ ক্লিক করেন, বা aws s3 ls s3://bucket/photos/2024/ চালান — এটা আসলে directory-র entry পড়া না। এটা একটা prefix query: “এই bucket-এর সব key-র মধ্যে, যেগুলো photos/2024/ string দিয়ে শুরু হয়, সেগুলো আমাকে দাও।” S3-র ভেতরের key-index একটা sorted structure (lexicographic order-এ), তাই এই query দক্ষভাবেই চালানো যায় — key-গুলো যেহেতু sort করা, একই prefix-ওয়ালা সব key sorted order-এ পাশাপাশি বসে, তাই একটা range scan (কোথা থেকে শুরু, কোথায় শেষ, দুইটা binary-search-ঘরানার lookup) দিয়েই পুরো matching set বের করা যায় — পুরো bucket linearly স্ক্যান করতে হয় না।
কিন্তু এটা এখনও একটা query, আর এখানেই ফাইলসিস্টেমের সাথে আসল পার্থক্য। একটা সত্যিকারের ফাইলসিস্টেমের readdir() কল একটা directory inode পড়ে — তার cost নির্ভর করে সেই directory-র নিজের entry-সংখ্যার ওপর, বাকি পুরো ফাইলসিস্টেমে যত ফাইলই থাকুক না কেন। কিন্তু object storage-এ prefix listing-এর cost নির্ভর করে matched key-সংখ্যার ওপর — photos/2024/ prefix-এ যদি ৫০ লাখ object থাকে, সেই listing ৫০ লাখ entry-ই ফেরত দিতে হবে (বা pagination-এ ভাগ করে), অন্য কোনো “folder”-এ কী আছে তার ওপর cost নির্ভর না করলেও। এটাই সেই সূক্ষ্ম কিন্তু বাস্তব পার্থক্য: object storage-এর “folder listing” একটা O(matches) prefix query, ফাইলসিস্টেমের directory listing একটা O(children) local read — চেহারায় একই কাজ মনে হলেও তাদের performance-চরিত্র সম্পূর্ণ আলাদা জগতের।
Consistency-র ইতিহাস — একটা callback, distributed-systems/consistency-models থেকে
distributed-systems/consistency-models লেসনের ভাষায় ফিরে যাই — ঠিক সেই ladder: linearizable > sequential > causal > eventual। S3-র consistency-ইতিহাস এই ladder-এর একটা বাস্তব, বহু-বছরব্যাপী, শিল্প-কাঁপানো case study।
২০২০-এর ডিসেম্বরের আগে পর্যন্ত, S3 ছিল eventually consistent — ঠিক সেই সংজ্ঞায় যা consistency-models লেসনে পড়েছেন: promise ছিল শুধু, লেখা “থামলে” (quiescence) সব replica শেষে একই মান-এ মিলে যাবে; পথের কোনো মুহূর্তের, কোনো read-এর, কোনো ক্রমের গ্যারান্টি ছিল না। ব্যবহারিক অর্থে এর মানে ছিল: একটা key-তে PUT (write) সফলভাবে সম্পূর্ণ হওয়ার সাথে সাথেই সেই একই key-তে GET (read) চালালে — কখনো কখনো — পুরনো মান পাওয়া যেত, বা key-টা নতুন তৈরি হলে একদম “not found” পাওয়া যেত। এটা ঠিক consistency-models লেসনের intro-history-র বাস্তব রূপ: write সম্পূর্ণ, তবু read পুরনো মান দেখল।
এটা বছরের পর বছর একটা পরিচিত, সবাই-জানা operational gotcha ছিল। একটা সাধারণ প্যাটার্ন যা সরাসরি এই সমস্যায় পড়ত: কোনো pipeline একটা S3 object লিখেই সাথে সাথে সেটা পড়তে যায় (যেমন একটা ETL job output লিখে পরের step-এ সেটা read করে) — আর মাঝে মাঝে সেই read পুরনো/অনুপস্থিত data পেত, বিশেষভাবে নতুন key তৈরির (create) ক্ষেত্রে (updates-এর ক্ষেত্রে সমস্যাটা তুলনামূলক কম দেখা যেত, কিন্তু সম্পূর্ণ অনুপস্থিত ছিল না)। ইঞ্জিনিয়াররা এর চারপাশে ঘুরে কাজ করতেন — retry-with-backoff, বা S3-এর বাইরে একটা আলাদা metadata-layer (যেমন DynamoDB) রেখে “এই key নিশ্চিতভাবে লেখা হয়ে গেছে” ট্র্যাক করা।
এটা কেন এত বড় খবর ছিল — সেটা বুঝতে consistency-models লেসনের trade-off-ভাষাটাই দরকার। সেই লেসন স্পষ্ট করেছিল: ladder-এ ওপরে ওঠা মানেই coordination-এর দাম — প্রতিটা op-এ বা প্রতি key-তে কোনো না কোনো ধরনের agreement লাগে, majority-র সাথে কথা বলতে হয়, latency বাড়ে। S3 চলে বিশ্বের সবচেয়ে বড় স্কেলে — trillions of objects, লাখ লাখ request প্রতি সেকেন্ডে, শত শত availability zone জুড়ে। এই স্কেলে strong consistency দেওয়া মানে এই coordination-খরচ প্রতিটা operation-এ বহন করা, অথচ latency বা throughput-এ কোনো user-facing regression ছাড়া। এটাই ছিল বছরের-পর-বছর সমাধান-না-হওয়া engineering সমস্যা — শুধু “কীভাবে strong করা যায়” প্রশ্ন না, “কীভাবে strong করা যায় বিনামূল্যে, বিদ্যমান স্কেলে, কোনো user-facing খরচ ছাড়া” প্রশ্ন।
ভেতরে কী ঘটছে
কীভাবে সম্ভব — সাধারণ কৌশল, exact বাস্তবায়ন না
AWS কখনো S3-র ভেতরের সঠিক implementation প্রকাশ করেনি — এটা proprietary, undisclosed। কিন্তু distributed-systems/consistency-models লেসনের ভোকাবুলারি দিয়েই আমরা বলতে পারি কোন শ্রেণীর কৌশল সাধারণভাবে এই ধরনের guarantee সম্ভব করে, exact কোড না জেনেও:
কৌশল ১ — Quorum-ধাঁচে read/write overlap। সেই লেসনের experiment-এ দেখেছিলাম: W+R>N হলে (write আর read-এর replica-set একে অপরকে ছুঁতে বাধ্য) প্রতিটা read-এ অন্তত একটা “সর্বশেষ write দেখা” replica থাকবেই — staleness window বন্ধ হয়ে যায়। যদি প্রতিটা object-এর metadata (কোন key কোথায়, কোন version) এমন একটা quorum-ওভারল্যাপ কাঠামোয় লেখা আর পড়া হয়, প্রতিটা read স্বয়ংক্রিয়ভাবে সাম্প্রতিকতম write দেখতে বাধ্য হয়।
কৌশল ২ — Canonical routing। consistency-models লেসনের “sticky routing”-ধারণার একটা generalization: যদি একটা নির্দিষ্ট key-র জন্য সব write এবং সব read একটামাত্র নির্দিষ্ট, নির্ধারিত (canonical) path/node/index দিয়ে যেতে বাধ্য হয় — অন্য কোনো stale replica থেকে read serve করার সুযোগই না থাকে — তাহলে সেই key-র জন্য read-after-write স্বয়ংক্রিয়ভাবে সত্য হয়ে যায়, কারণ কোনো “অন্য পথ দিয়ে পুরনো কপি পড়া” সম্ভবই না। এর দাম: সেই canonical path-টা একটা concentration point, যাকে নিজে বিশাল স্কেলে সামলাতে হয়।
কৌশল ৩ — একটা কেন্দ্রীয়, strongly-consistent metadata-layer। অনেক বড় storage system (S3 সহ, বিভিন্ন প্রকাশিত architecture-আলোচনায় ইঙ্গিত পাওয়া যায়) actual object bytes আর তাদের metadata (কোন key কোন physical location-এ, কোন version সবচেয়ে নতুন) আলাদা করে রাখে। যদি metadata-layer নিজে strongly consistent হয় (ছোট, দ্রুত, consensus-ভিত্তিক), আর প্রতিটা read প্রথমে সেই metadata-layer জিজ্ঞেস করে “সর্বশেষ version কোথায়”, তাহলে actual bulk data বিশাল, eventually-propagating replication-এ থাকলেও, client-এর দৃষ্টিতে পুরো system strong দেখায় — ভারী অংশ (bytes) সস্তায় eventual থাকে, হালকা অংশ (pointer) দামি strong-এ থাকে।
তিনটা কৌশলই একই মূল সত্যের ওপর দাঁড়িয়ে, যা consistency-models লেসন থেকেই এসেছে: strong guarantee পেতে হলে কোথাও-না-কোথাও coordination লাগবেই — প্রশ্ন শুধু সেই coordination-টাকে কতটা ছোট, কতটা সস্তা, আর কতটা off-the-critical-path রাখা যায়। S3-র অর্জন কোনো নতুন consistency-তত্ত্ব আবিষ্কার না — এটা প্রকৌশলে সেই coordination-খরচকে বিশাল স্কেলে অদৃশ্য করে ফেলার কৃতিত্ব।
উদাহরণ
Multipart upload — অংশে অংশে ওঠা, সব-নয়তো-কিছুই সেলাই
একটা 50 GB video file সরাসরি এক HTTP request-এ আপলোড করার কথা ভাবুন — নেটওয়ার্ক এক সেকেন্ডের জন্যও কেটে গেলে, পুরো ৫০ GB আবার প্রথম থেকে পাঠাতে হবে। এই সমস্যার সমাধান multipart upload: বড় object-কে client-সাইডে কয়েকটা অংশে (part) ভেঙে, প্রতিটা part স্বাধীনভাবে আপলোড করা — একেকটা part একেক সময়ে, এমনকি সমান্তরালে (parallel) কয়েকটা connection দিয়েও।
মেকানিজমটা তিন ধাপে:
- Initiate — client bucket-কে বলে “আমি
photos/2024/big-video.mp4নামে একটা multipart upload শুরু করছি” — server একটাUploadIdফেরত দেয়, এই session-টাকে চিহ্নিত করার জন্য। - Upload parts — client প্রতিটা part আলাদাভাবে পাঠায় (part number সহ,
UploadIdসহ) — সার্ভার প্রতিটা part receive করার পর তার একটা checksum (ETag) ফেরত দেয়, client-কে নিশ্চিত করে সেই নির্দিষ্ট part ঠিকভাবে পৌঁছেছে। কোনো part fail করলে, শুধু সেই part আবার পাঠালেই চলে — পুরো object না। - Complete — সব part সফলভাবে ওঠার পর, client একটা
CompleteMultipartUploadকল পাঠায়, প্রতিটা part-এর number + checksum-এর তালিকা সহ। এই মুহূর্তেই সার্ভার সব part-কে ক্রমানুসারে stitch/সেলাই করে একটাই object বানায় — আর ঠিক এই মুহূর্ত থেকেই object-টা bucket-এ visible হয়।
- Initiate: UploadId পাওয়া গেলএখনো bucket-এ কোনো object নেই — শুধু একটা session শুরু হলো
- Part 1..4 সফলভাবে uploaded, প্রতিটার checksum ফেরত এসেছেএখনো bucket-এ কোনো object নেই, listing-এ কিছু দেখা যায় না
- Part 5 আপলোডের মাঝপথে network কেটে গেলশুধু Part 5 আবার পাঠাতে হবে — Part 1-4 আগের মতোই সংরক্ষিত আছে server-এ
- Part 5 retry সফল — এবার সব ৫টা part-এর checksum হাতেএখনো bucket-এ কোনো object নেই
- CompleteMultipartUpload কল — ৫টা part stitch হলোঠিক এই মুহূর্তে object প্রথমবার visible হলো bucket listing-এ
এই ডিজাইন যে দুইটা সমস্যা সমাধান করে তা এখন স্পষ্ট: resumability (একটা network glitch পুরো আপলোড বাতিল করে না, শুধু একটা part আবার পাঠাতে হয়) আর parallelism (একাধিক part একসাথে, একাধিক connection দিয়ে আপলোড করলে total wall-clock time কমে, একটা single-threaded stream-এর তুলনায়)।
ছবিতে
Durability-র দুই পথ — replication বনাম erasure coding
Object storage শুধু “কোথায় রাখব” প্রশ্নের উত্তর দেয় না — “কীভাবে হারাব না” প্রশ্নেরও উত্তর দেয়। কোনো একটা disk crash করলে, বা একটা পুরো server rack বিদ্যুৎ হারালে, object-টা যেন হারিয়ে না যায় — এই গ্যারান্টির নাম durability (S3-এর বিখ্যাত দাবি: “eleven nines” — 99.999999999% — বছরে ১০,০০০ object রাখলে গড়ে একটা হারানোর সম্ভাবনা প্রায় ১০,০০০ বছরে একবার)।
সবচেয়ে সরল পথ, যা databases/replication লেসনে দেখেছেন সেই ধারণারই সম্প্রসারণ: N-way replication — একই object-এর N-টা সম্পূর্ণ কপি N-টা আলাদা disk/node/rack-এ রাখা। যেকোনো (N-1)-টা কপি হারালেও, একটা বেঁচে থাকলেই object survive করে। কিন্তু দামটা সরাসরি linear: N=3-way replication মানে ৩ গুণ storage — 1 GB-র object 3 GB storage দখল করে।
৩-way replication (৩টা সম্পূর্ণ কপি, ৩টা node/rack-এ)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ copy #1 │ │ copy #2 │ │ copy #3 │
│ (পুরো object)│ │ (পুরো object)│ │ (পুরো object)│
└─────────────┘ └─────────────┘ └─────────────┘
storage = 3x | tolerates: যেকোনো ২টা node হারালেও চলে (৩ কপির ১টা টিকলেই)
Erasure coding, k=4 data chunks + m=2 parity chunks (মোট ৬টা location)
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────────┐ ┌────────┐
│ d1 │ │ d2 │ │ d3 │ │ d4 │ │ parity1│ │ parity2│
└────┘ └────┘ └────┘ └────┘ └────────┘ └────────┘
\_______________________________/\____________/
৪টা মূল data chunk ২টা parity chunk
storage = 6/4 = 1.5x | tolerates: যেকোনো ২টা location হারালেও চলে
(৬টার যেকোনো ৪টা বেঁচে থাকলেই পুরো object পুনর্গঠনযোগ্য)লক্ষ্য করুন — দুইটা scheme-ই একই fault-tolerance দিচ্ছে (যেকোনো ২টা node হারালেও object বাঁচে), কিন্তু replication-এর storage-খরচ 3x, erasure coding-এর মাত্র 1.5x। এই পার্থক্যটাই erasure coding-কে বিশাল স্কেলের object storage-এ (S3, Azure Blob, GCS, Backblaze B2) এত আকর্ষণীয় করে তোলে — একই durability, অর্ধেক storage-বিল।
মূল ধারণা: k-টা data chunk-কে গাণিতিকভাবে (Reed-Solomon-ধরনের কোডে, যা polynomial/finite-field গণিতের ওপর দাঁড়িয়ে) combine করে m-টা parity/redundancy chunk তৈরি করা হয়, যেগুলো এমনভাবে গঠিত যে মোট (k+m)-টা chunk-এর যেকোনো k-টা থেকেই বাকি সব পুনর্গঠন করা সম্ভব — কোন k-টা বেঁচে আছে সেটা কোনো ব্যাপার না, শুধু সংখ্যাটা k হলেই চলে। এটাই replication-এর “যেকোনো একটা কপি থাকলেই চলে” ধারণাটার একটা গাণিতিকভাবে অনেক বেশি efficient সাধারণীকরণ।
নিজে চালিয়ে দেখুন
flat_namespace_prefix.py — 'folder listing' আসলে prefix query, directory read না
এই স্ক্রিপ্ট সত্যিই চালানো হয়েছে। এটা একটা synthetic bucket তৈরি করে (কয়েক হাজার key, বাস্তব “folder”-প্যাটার্নে ছড়ানো — photos/<year>/<month>/img_NNNN.jpg, videos/2024/clip_NNNNN.mp4, backups/db/NNNN.tar.gz), তারপর একই prefix-query দুইভাবে চালায়: naive linear scan (প্রতিটা key check করা) বনাম sorted-key range scan (bisect দিয়ে, S3-র নিজের sorted key-index-এর একটা সরলীকৃত মডেল)।
# flat_namespace_prefix.py -- object storage has NO real directory tree:
# every "folder listing" is actually a PREFIX QUERY over a flat, sorted
# key space. This script builds a synthetic bucket of keys, then compares
# two ways to answer "list everything under photos/2024/":
# (1) naive linear scan across ALL keys (what you'd do with no index)
# (2) sorted-key range scan via bisect (conceptually how S3's internal
# key index works: keys are stored in lexicographic sort order, so
# a prefix query is just "find the [start, end) range of that sort
# order" -- cost is O(log n) to find the boundary + O(matches) to
# read them, NOT O(total keys in the bucket) and NOT O(directory
# children) the way a real filesystem's readdir() would be.
import bisect
def make_bucket_keys():
"""Synthetic bucket: a realistic mix of 'folders' and object counts."""
keys = []
for year in (2022, 2023, 2024):
for month in range(1, 13):
count = 40 if year == 2024 else 25
for i in range(count):
keys.append(f"photos/{year}/{month:02d}/img_{i:04d}.jpg")
for i in range(5000):
keys.append(f"videos/2024/clip_{i:05d}.mp4")
for i in range(300):
keys.append(f"backups/db/{i:04d}.tar.gz")
return keys
def linear_scan_count(keys, prefix):
"""Naive approach: check every single key in the bucket."""
comparisons = 0
matches = 0
for k in keys:
comparisons += 1
if k.startswith(prefix):
matches += 1
return matches, comparisons
def sorted_range_count(sorted_keys, prefix):
"""Sorted-index approach: binary search for where the prefix's range
starts and ends in lexicographic order, then the match count is just
that range's length -- no need to touch keys outside the range."""
lo = bisect.bisect_left(sorted_keys, prefix)
hi = bisect.bisect_left(sorted_keys, prefix + "")
matches = hi - lo
import math
n = len(sorted_keys)
comparisons = 2 * max(1, math.ceil(math.log2(n))) if n > 1 else 1
return matches, comparisons, lo, hi
if __name__ == "__main__":
keys = make_bucket_keys()
total = len(keys)
print(f"total objects in bucket : {total}")
sorted_keys = sorted(keys) # this is the ACTUAL storage order in a
# real object store's key index -- keys
# are not grouped by "folder" at all,
# they just happen to sort that way
# because the key STRING contains '/'
for prefix in ["photos/2024/03/", "photos/2024/", "videos/2024/", "backups/"]:
lin_matches, lin_cmp = linear_scan_count(keys, prefix)
idx_matches, idx_cmp, lo, hi = sorted_range_count(sorted_keys, prefix)
assert lin_matches == idx_matches, "match-count mismatch!"
print(f"\nprefix = {prefix!r}")
print(f" matched objects : {lin_matches}")
print(f" linear scan comparisons : {lin_cmp} (= total bucket size)")
print(f" sorted-range comparisons : {idx_cmp} (~ 2*log2(n), independent of match count)")
print(f" speedup (comparisons) : {lin_cmp / idx_cmp:.1f}x")
print("\n=== key takeaway ===")
print("linear-scan cost stays constant at", total, "regardless of prefix,")
print(f"while sorted-range cost stays near {idx_cmp} regardless of prefix too --")
print("but BOTH still must then iterate the matched objects to return them,")
print("so listing a 'folder' with many objects is proportionally slower")
print("than listing one with few -- unlike a real filesystem directory,")
print("whose readdir() cost depends only on ITS OWN entry count, never")
print("on how many files exist elsewhere in the tree.")প্রকৃত চালিয়ে পাওয়া output (deterministic — synthetic bucket-এর গঠন fixed, হুবহু এটাই আসবে):
total objects in bucket : 6380
prefix = 'photos/2024/03/'
matched objects : 40
linear scan comparisons : 6380 (= total bucket size)
sorted-range comparisons : 26 (~ 2*log2(n), independent of match count)
speedup (comparisons) : 245.4x
prefix = 'photos/2024/'
matched objects : 480
linear scan comparisons : 6380 (= total bucket size)
sorted-range comparisons : 26 (~ 2*log2(n), independent of match count)
speedup (comparisons) : 245.4x
prefix = 'videos/2024/'
matched objects : 5000
linear scan comparisons : 6380 (= total bucket size)
sorted-range comparisons : 26 (~ 2*log2(n), independent of match count)
speedup (comparisons) : 245.4x
prefix = 'backups/'
matched objects : 300
linear scan comparisons : 6380 (= total bucket size)
sorted-range comparisons : 26 (~ 2*log2(n), independent of match count)
speedup (comparisons) : 245.4x
=== key takeaway ===
linear-scan cost stays constant at 6380 regardless of prefix,
while sorted-range cost stays near 26 regardless of prefix too --
but BOTH still must then iterate the matched objects to return them,
so listing a 'folder' with many objects is proportionally slower
than listing one with few -- unlike a real filesystem directory,
whose readdir() cost depends only on ITS OWN entry count, never
on how many files exist elsewhere in the tree.পড়ার সময়: প্রতিটা prefix-এই sorted-range approach মাত্র ২৬টা comparison-এ boundary খুঁজে পায়, match যত ৪০-ই হোক বা ৫০০০-ই হোক — bucket-এর মোট আকার-এর ওপর নির্ভরতা log-scale-এ কমে যায়। এটাই S3-র “folder” খুলতে গেলে instant মনে হওয়ার পেছনের গণিত। কিন্তু videos/2024/ prefix-এ ৫০০০-টা match — সেই ৫০০০টা key ফেরত পাঠাতেই সময় লাগবে, backups/ prefix-এর মাত্র ৩০০-এর তুলনায় — এই অংশটাই matched-set-নির্ভর, আর এখানেই object storage-এর “folder listing” সত্যিকারের filesystem-directory-listing থেকে চরিত্রগতভাবে আলাদা। নিজে বাড়ান: make_bucket_keys()-এ আরও একটা “folder” যোগ করুন যাতে ১০ লাখ key থাকে, আর দেখুন sorted-range comparison-সংখ্যা কতটা কম বাড়ে (log2 growth) বনাম linear scan-এর সরাসরি সমানুপাতিক বৃদ্ধি।
Object storage-এ prefix-based 'folder listing'-এর cost bucket-এর মোট object-সংখ্যার ওপর না, matched key-সংখ্যার ওপর নির্ভর করে — sorted key-index থাকলে naive linear scan-এর তুলনায় শত গুণ কম comparison লাগে, কিন্তু তবু match-সংখ্যা বেশি হলে সেই matched set ফেরত দেওয়ার খরচ বেড়েই যায়, ঠিক যেমনটা একটা true filesystem directory-তে হতো না।
নিজে বানান
erasure_xor.py — একটা toy XOR-parity erasure code লিখুন আর হারানো chunk পুনর্গঠন করুন
- একটা split_into_chunks() ফাংশন লিখুন যা কাঁচা bytes-কে k-টা সমান আকারের chunk-এ ভাগ করে
- একটা xor_bytes() হেল্পার লিখুন, আর তা দিয়ে encode() ফাংশনে সব data chunk XOR করে একটা parity chunk বানান (k=2, m=1 — এটা সত্যিকারের Reed-Solomon না, শুধু pedagogical সরলীকরণ)
- একটা chunk "হারানো" simulate করুন (তালিকা থেকে বাদ দিন), তারপর reconstruct_missing() লিখুন যা বেঁচে-থাকা chunk-গুলো আর parity XOR করে হারানো chunk ফিরিয়ে আনে
- পুনর্গঠিত object মূল data-র সাথে বাইট-বাই-বাইট মিলিয়ে verify করুন, আর replication-বনাম-erasure-coding-এর storage-overhead তুলনা-টেবিল ছাপুন
আগের experiment দেখাল listing কেন cheap থাকে flat namespace-এও — এই build সেই একই module-এর আরেকটা কেন্দ্রীয় প্রশ্নে যায়: durability কীভাবে replication-এর চেয়ে সস্তায় পাওয়া যায়। এখানে আমরা একটা toy XOR-parity scheme লিখব — k=2 data chunk + m=1 parity chunk — যা real Reed-Solomon-এর (যেটা GF(2⁸)-এর polynomial গণিতের ওপর দাঁড়ায়, m একের বেশি হতে পারে) একটা pedagogically সরলীকৃত সংস্করণ, শুধু একটামাত্র chunk হারালে চলে এমন ক্ষেত্রে।
# erasure_xor.py -- a pedagogically simplified erasure scheme.
# k=2 data chunks + m=1 XOR parity chunk. This is NOT real Reed-Solomon
# (which uses polynomial/matrix math over GF(2^8) and supports m>1); XOR
# parity only tolerates losing exactly one chunk out of the k+m total.
# Demonstrates: split -> encode -> "lose" one chunk -> reconstruct.
def split_into_chunks(data: bytes, k: int) -> list:
"""Split data into k equal-length chunks, zero-padding the last one."""
chunk_len = -(-len(data) // k) # ceil division
padded = data + b"\x00" * (chunk_len * k - len(data))
return [padded[i * chunk_len:(i + 1) * chunk_len] for i in range(k)]
def xor_bytes(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
def encode(data: bytes, k: int = 2):
"""Return k data chunks + 1 parity chunk (k=2,m=1 XOR scheme)."""
chunks = split_into_chunks(data, k)
parity = chunks[0]
for c in chunks[1:]:
parity = xor_bytes(parity, c)
return chunks, parity
def reconstruct_missing(surviving: dict, k: int, parity: bytes) -> bytes:
"""surviving: {index: chunk_bytes} for the k-1 data chunks that survived
(exactly one is missing). XOR every surviving chunk with parity to
recover the missing one -- this works because parity = c0 ^ c1 ^ ... ^
c(k-1), and XOR-ing a value with itself cancels it out."""
result = parity
for i, c in surviving.items():
result = xor_bytes(result, c)
return result
if __name__ == "__main__":
original = b"OBJECT-STORE-DURABILITY-DEMO!!!!" # 32 bytes, k=2 -> 16B/chunk
print(f"original data : {original!r}")
print(f"original length : {len(original)} bytes")
K = 2
chunks, parity = encode(original, K)
for i, c in enumerate(chunks):
print(f"data chunk {i} : {c!r}")
print(f"parity chunk (XOR of both) : {parity!r}")
# simulate losing data chunk 0 ("a disk/node holding it died")
lost_index = 0
surviving = {i: c for i, c in enumerate(chunks) if i != lost_index}
print(f"\nsimulating loss of chunk {lost_index} -- surviving indices: "
f"{sorted(surviving)}, parity chunk present")
recovered = reconstruct_missing(surviving, K, parity)
print(f"reconstructed chunk {lost_index} : {recovered!r}")
print(f"matches original chunk {lost_index}? : {recovered == chunks[lost_index]}")
chunk_len = len(chunks[0])
full = bytearray(chunk_len * K)
for i, c in surviving.items():
full[i * chunk_len:(i + 1) * chunk_len] = c
full[lost_index * chunk_len:(lost_index + 1) * chunk_len] = recovered
print(f"reassembled object : {bytes(full)!r}")
print(f"reassembled == original? : {bytes(full) == original}")
print("\n=== storage-overhead comparison for a target durability level ===")
print(f"{'scheme':<30}{'total chunks':>13}{'k (needed)':>12}{'tolerates losing':>18}{'overhead':>10}")
schemes = [
("3-way replication", 3, 1),
("2-way replication", 2, 1),
("erasure k=2,m=1 (this demo)", 3, 2),
("erasure k=4,m=2", 6, 4),
("erasure k=8,m=3", 11, 8),
]
for name, total, k in schemes:
tolerance = total - k
overhead = total / k
print(f"{name:<30}{total:>13}{k:>12}{tolerance:>18}{overhead:>9.3f}x")প্রকৃত চালিয়ে পাওয়া output:
original data : b'OBJECT-STORE-DURABILITY-DEMO!!!!'
original length : 32 bytes
data chunk 0 : b'OBJECT-STORE-DUR'
data chunk 1 : b'ABILITY-DEMO!!!!'
parity chunk (XOR of both) : b'\x0e\x00\x03\t\n\x00t~\x10\n\x1f\n\x0cets'
simulating loss of chunk 0 -- surviving indices: [1], parity chunk present
reconstructed chunk 0 : b'OBJECT-STORE-DUR'
matches original chunk 0? : True
reassembled object : b'OBJECT-STORE-DURABILITY-DEMO!!!!'
reassembled == original? : True
=== storage-overhead comparison for a target durability level ===
scheme total chunks k (needed) tolerates losing overhead
3-way replication 3 1 2 3.000x
2-way replication 2 1 1 2.000x
erasure k=2,m=1 (this demo) 3 2 1 1.500x
erasure k=4,m=2 6 4 2 1.500x
erasure k=8,m=3 11 8 3 1.375xপড়ার সময়: parity chunk 0 হারানোর পরও reconstructed chunk 0 হুবহু মূল chunk-এর সাথে মিলে গেছে — কারণ parity = chunk0 XOR chunk1, তাই chunk0 = parity XOR chunk1 (XOR-এর নিজের-সাথে-নিজে-cancel-হওয়া বৈশিষ্ট্যের সরাসরি প্রয়োগ, ঠিক এক-বাক্যে যা docstring-এ লেখা)। আর overhead-টেবিলে লক্ষ্য করুন — k=4,m=2 (মোট ৬টা chunk, যেকোনো ২টা হারালেও চলে) ঠিক ৩-way replication-এর সমান fault-tolerance দেয় (দুইটা হারালেও বাঁচে), অথচ overhead মাত্র 1.5x, replication-এর 3x-এর অর্ধেক। আরও বড় k-তে (k=8,m=3) overhead আরও নামে (1.375x), কিন্তু দাম আছে যা টেবিলে দেখা যায় না — বেশি k মানে reconstruction-এ বেশি chunk লাগে, encode/decode-এ বেশি CPU গণিত (real Reed-Solomon-এ, XOR না), আর একটা lost chunk মেরামত করতে বেশি node-থেকে read করতে হয়। নিজে বাড়ান: (১) original-এর length বদলে দেখুন padding কীভাবে কাজ করে অ-জোড় length-এ; (২) দুইটা chunk একসাথে হারিয়ে দেখুন — এই toy XOR scheme-এ reconstruction fail করবে, কারণ m=1 মানে মাত্র ১টা chunk-লস সহ্য করে — বাস্তব Reed-Solomon-এ m বাড়ালে আরও বেশি simultaneous loss সহ্য করা যায়।
বাস্তব সিস্টেমে
বাস্তব জগতে
S3-র নিজের durability-claim। AWS প্রতিটা S3 Standard object-এর জন্য 99.999999999% (“eleven nines”) annual durability দাবি করে — এই সংখ্যাটা multi-AZ redundancy আর (S3-র নিজস্ব, undisclosed প্যারামিটারের) erasure-coding-জাতীয় কৌশলের সমন্বয়ে অর্জিত, শুধু plain replication দিয়ে এই স্কেলে এই durability economically সম্ভব হতো না।
Backblaze-এর প্রকাশিত architecture। Backblaze তাদের “Vault” architecture নিয়ে প্রকাশ্যে লিখেছে (আজকের references দেখুন) — ২০-টা storage pod জুড়ে ১৭-এর-মধ্যে-২০ (১৭ data + ৩ parity)-ধরনের Reed-Solomon scheme ব্যবহার করে, overhead মাত্র প্রায় 1.176x (20/17), অথচ যেকোনো ৩-টা pod একসাথে হারালেও data বাঁচে। এটাই এই লেসনের k+m গণিতের একটা concrete, প্রকাশিত production সংখ্যা।
Read-heavy pipeline-এর multipart-upload-নির্ভরতা। কোনো video-processing বা ML-training pipeline যেখানে বহু-GB dataset বা model-checkpoint S3-তে লেখা হয়, সেখানে multipart upload standard practice — SDK-গুলো (boto3, AWS CLI) স্বয়ংক্রিয়ভাবে একটা threshold-এর (সাধারণত কয়েক MB) বেশি হলেই multipart-এ switch করে দেয়, ইঞ্জিনিয়ারকে ম্যানুয়ালি সিদ্ধান্ত নিতে হয় না।
Strong-consistency-এর পরে বদলে যাওয়া architecture-প্যাটার্ন। ২০২০-এর আগে অনেক team S3-এর eventual consistency এড়াতে DynamoDB-তে আলাদা “এই key নিশ্চিতভাবে লেখা হয়েছে” metadata-table রাখত (উপরের hood section-এর কৌশল ৩-এর একটা user-level সংস্করণ)। ঘোষণার পরে এই workaround-layer বহু pipeline থেকে সরিয়ে ফেলা হয়েছে — একটা platform-level guarantee বদলে দিয়েছে অসংখ্য application-এর architecture, সরাসরি প্রমাণ যে consistency model কোনো abstract তত্ত্ব না, বাস্তব কোডের গঠন নির্ধারণ করে।
যে ভুলগুলো সবাই করে
“S3-তে 'folder' মানে বাস্তবেই একটা directory যেমন আপনার laptop-এর ফাইলসিস্টেমে থাকে”
সম্পূর্ণ illusion। Object storage-এ শুধু bucket + key আছে — key একটা flat string, তার ভেতরের / অক্ষর শুধু string-এর অংশ, কোনো real nesting না। “Folder” console/CLI-এর client-সাইড রেন্ডারিং — key-গুলোকে / ধরে ভেঙে দেখানো একটা tree-view। কোনো “empty folder object” আলাদাভাবে exist করে না, আর “folder” খোলা মানে actual একটা prefix query চালানো, directory inode read করা না।
“S3-র eventual consistency ছিল শুধু AWS-এর অলসতা বা cost-cutting — strong consistency যোগ করা সবসময়ই সহজ ছিল, তারা করেনি এতদিন”
উল্টো — distributed-systems/consistency-models লেসনের trade-off অনুযায়ী, ladder-এ ওপরে ওঠা মানেই coordination-এর দাম, আর S3-র স্কেলে (trillions of objects, বিশ্বজুড়ে) সেই দাম বহন করা প্রকৌশলগতভাবে অত্যন্ত কঠিন — বিশেষভাবে কোনো user-facing latency/throughput regression ছাড়া। এজন্যই ২০২০-এর ঘোষণা industry-তে এত বড় খবর ছিল: এটা কোনো সহজ flag-flip না, বছরের-পর-বছর engineering-এর ফসল।
“Erasure coding সবসময় replication-এর চেয়ে ভালো — কোনো downside নেই, তাই সবক্ষেত্রে ব্যবহার করা উচিত”
Storage-overhead কম হলেও অন্য খরচ আছে যা টেবিলে দেখা যায় না। Erasure coding-এ reconstruction করতে একাধিক chunk বিভিন্ন node থেকে read করে গণিত করতে হয় (real Reed-Solomon-এ ভারী polynomial/matrix গণিত, plain replication-এ শুধু একটা copy পড়লেই চলে) — CPU খরচ আর reconstruction-latency বেশি। ছোট, frequently-accessed object-এর জন্য replication-ই বেশি ব্যবহারিক (hot data); বড়, infrequently-accessed archival object-এর জন্য erasure coding-এর storage-সাশ্রয় জেতে (cold data)। এটাই cloud provider-দের storage-class (Standard বনাম Infrequent-Access/Glacier-জাতীয়) ভাগ করার একটা কারণ।
“একটা multipart upload অসম্পূর্ণ/abandoned থাকলে সেটা harmless — invisible মানে ওটা এমনিতেই মুছে গেছে বা কোনো খরচ নেই”
Visible না হওয়া আর মুছে যাওয়া এক কথা না। আপলোড-করা part-গুলো storage-এ থেকেই যায় এবং storage-খরচ চলতেই থাকে, যতক্ষণ না explicit AbortMultipartUpload কল হয় বা bucket-এর lifecycle policy সেগুলো স্বয়ংক্রিয়ভাবে expire করে। প্রোডাকশনে “silently-abandoned incomplete multipart uploads পুঞ্জীভূত হয়ে বিল বাড়াচ্ছে” একটা পরিচিত অপারেশনাল সমস্যা — এজন্যই lifecycle rule-এ incomplete-multipart-expiration একটা standard best practice।
বুঝেছেন কি না দেখুন
1এক বাক্যে — object storage-এ একটা object-কে ঠিক কী দিয়ে identify করা হয়, আর “folder” কেন সেই মডেলের অংশ না?স্মরণ
/ অক্ষর শুধু string-এর একটা চরিত্র, কোনো real nested directory-structure তৈরি করে না; folder-view client-সাইড রেন্ডারিং মাত্র।2কেন object storage-এ একটা prefix-এ ৫০ লাখ object থাকলে সেই “folder” list করা একটা সত্যিকারের ফাইলসিস্টেমের কোনো directory-র ৫০ লাখ ফাইল list করার চেয়ে ভিন্ন performance-চরিত্র দেখায়, শুধু “উভয়েই ধীর” না বলে সুনির্দিষ্টভাবে ব্যাখ্যা করুন?যুক্তি
readdir() কল একটা directory inode পড়ে — cost শুধু সেই directory-র নিজের entry-সংখ্যার ফাংশন, বাকি ফাইলসিস্টেমে যাই থাকুক। Object storage-এ prefix listing একটা query — sorted key-index-এ boundary খুঁজতে log-scale cost লাগে (bucket-এর মোট আকারের ওপর নির্ভর করে, তবে log-এ), কিন্তু matched key-গুলো ফেরত পাঠানোর cost সরাসরি match-সংখ্যার সমানুপাতিক। ফলাফল একই রকম দেখতে হলেও দুইটার cost-model ভিন্ন উৎস থেকে আসে — একটা “local read”, আরেকটা “distributed range query + result-transfer”।3S3-র ২০২০-এর strong-consistency ঘোষণা এত বড় খবর ছিল কেন — distributed-systems/consistency-models লেসনের trade-off-ভাষায় ব্যাখ্যা করুন।যুক্তি
4একটা ৮ GB object multipart upload হচ্ছে, ৪টা part-এ ভাগ করে। ৩টা part সফলভাবে uploaded, checksum-ও পাওয়া গেছে; চতুর্থ part আপলোডের সময় client crash করল আর আর ফেরত এলো না। এই মুহূর্তে bucket listing চালালে কী দেখা যাবে, আর কী দেখা যাবে না — আর কোনো cost বহন হতেই থাকবে কি?প্রয়োগ
CompleteMultipartUpload কল না হওয়া পর্যন্ত কিছুই visible হয় না, ৩টা part সফল হওয়া সত্ত্বেও। কিন্তু আপলোড-করা ৩টা part storage-এ থেকেই যাবে এবং storage-খরচ বহন হতেই থাকবে, যতক্ষণ না explicit AbortMultipartUpload কল হয় বা lifecycle policy সেগুলো expire করে দেয় — invisible হওয়া মানে বিনামূল্যে না।5আপনার কাছে একটা target durability আছে যেটা “যেকোনো ৩-টা node একসাথে হারালেও data বাঁচবে” চায়। প্লেইন replication দিয়ে এটা করতে কমপক্ষে কতগুলো কপি লাগবে, আর erasure coding-এ k=8, m=3 নিলে overhead কত? দুইটার সংখ্যা তুলনা করুন।প্রয়োগ
6আপনি একটা photo-sharing platform ডিজাইন করছেন যেখানে দুই ধরনের data আছে: (ক) সদ্য-আপলোড-হওয়া photo যা আপলোডের সাথে সাথেই user-এর নিজের feed-এ দেখাতে হবে (read-after-write নির্ভরশীল), আর (খ) ৫ বছরের পুরনো photo যা কালেভদ্রে খোলা হয় কিন্তু হারানো চলবে না। প্রতিটার জন্য আপনি কোন consistency-প্রত্যাশা আর কোন durability-কৌশল বেছে নেবেন, আর কেন একই কৌশল দুইটাতেই optimal না?ডিজাইন
এরপর কী
আজকের লেসন module-এর নতুন অধ্যায়ের প্রথম ইট বসাল — cloud provider-রা কীভাবে প্রতিটা অ্যাপ্লিকেশনের data অর্ধেক টিকিয়ে রাখে বিশাল স্কেলে: flat bucket+key namespace, prefix-query listing, বছরের-পর-বছরের consistency-journey, multipart upload-এর resumable মেকানিজম, আর replication-এর চেয়ে সস্তায় durability দেওয়া erasure coding।
পরের লেসন virtualization-cloud/cloud-networking-vpc-এ আমরা storage থেকে সরে যাব network অর্ধেকের দিকে — cloud provider কীভাবে হাজার হাজার ভিন্ন কাস্টমারের shared physical infrastructure-এর ভেতরে আপনাকে দেয় আপনার নিজের, private, isolated network — যেখানে আপনার VM-গুলো একে অপরকে দেখে, বাইরের কেউ দেখে না, অথচ নিচে সবাই একই physical switch/router শেয়ার করছে।
আরও পড়ুন
- Amazon S3 Update – Strong Read-After-Write Consistency — Jeff Barr — AWS News Blog, December 2020 · মূল ঘোষণা — কেন এই একটা paragraph পুরো industry-র জন্য এত বড় খবর ছিল তার প্রাথমিক উৎস
- Amazon S3 — Uploading and copying objects using multipart upload — AWS Documentation · Part-checksum, completion-step, আর অসম্পূর্ণ upload-এর visibility-নিয়মের আনুষ্ঠানিক রেফারেন্স
- Polynomial Codes Over Certain Finite Fields — Irving S. Reed, Gustave Solomon — Journal of the Society for Industrial and Applied Mathematics, 8(2), 1960 · Reed-Solomon erasure coding-এর মূল গাণিতিক পেপার — আজকের toy XOR-scheme যে সাধারণ পরিবারের একটা সরলীকৃত সদস্য
- Backblaze Vaults: Zettabyte-Scale Cloud Storage Architecture — Backblaze Engineering Blog · Production object storage-এ erasure coding (k+m distribution) কীভাবে বাস্তবে ডিজাইন হয় তার concrete case-study