Skip to content

স্টোরেজ লেআউট ও লাইফসাইকেল

একটা ভিডিও প্রসেস হওয়ার পর হাজার হাজার অবজেক্ট হয়ে যায়। সেগুলোর কী কীভাবে সাজাবেন, কোনটা কতদিন রাখবেন, কীভাবে সত্যিই মুছবেন, আর ভাঙা বা হারানো ফাইল কীভাবে ধরবেন।

গল্পে বুঝি

মুন্সীগঞ্জে খোয়ারিজমির একটা আলুর হিমাগার। ভেতরে বারোটা চেম্বার, প্রতিটা চেম্বারে তাক, প্রতিটা তাকে নম্বর দেওয়া জায়গা। মৌসুমে কৃষক রাজি পঞ্চাশ বস্তা আলু নিয়ে আসেন। গেটে ফাতিমা মোটা খাতায় লেখেন: স্লিপ নম্বর ৪৮১৭, রাজি, পঞ্চাশ বস্তা, চেম্বার ৭, তাক ৩, জায়গা ১১ থেকে ৬০। প্রতিটা বস্তার গায়ে লাগানো হয় একটা ট্যাগ: ৪৮১৭-০১, ৪৮১৭-০২, এভাবে। বস্তার গায়ে কৃষকের নাম লেখা হয় না, কারণ এই এলাকায় তিনজন রাজি আছেন, আর একজন তো মাঝে মাঝে নিজের নামের বানান বদলে লেখেন। স্লিপ নম্বর কখনো বদলায় না, দুইবার দেওয়াও হয় না।

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

আরেকটা অভ্যাস আছে ফাতিমার। প্রতিটা বস্তা ঢোকার সময় ওজন মেপে খাতায় লেখেন, আর বের হওয়ার সময় আবার মাপেন। ওজন না মিললে বোঝা যায় বস্তা ছিঁড়েছে বা কেউ বদলে দিয়েছে। আর বন্যার বছরের পর থেকে খোয়ারিজমি খাতার একটা কপি রাখেন শহরের অফিসে, কারণ চেম্বার রক্ষা করা যায়, কিন্তু খাতা ভিজে গেলে কোন বস্তা কার, সেটা আর কেউ বলতে পারবে না।

মিলিয়ে নিই: হিমাগারটা হলো object storage বাকেট, চেম্বার-তাক-জায়গার নম্বর হলো object key। স্লিপ নম্বর হলো ডেটাবেসের asset id, আর কৃষকের নাম বস্তায় না লেখা মানে ইউজারের দেওয়া ফাইলনেম কী-তে না রাখা। এক কৃষকের সব বস্তা পাশাপাশি রাখা হলো asset অনুযায়ী prefix, যাতে এক কমান্ডে পুরোটা লিস্ট বা ডিলিট করা যায়। বীজ আলু আর খাওয়ার আলুর আলাদা চেম্বার হলো source mezzanine আর আউটপুটের আলাদা লাইফসাইকেল ও স্টোরেজ ক্লাস। খাতায় লিখে শেষ না করে তাক খালি হয়েছে কিনা দেখা হলো soft delete-এর পর সত্যিকারের delete আর তার যাচাই, আর পচা বস্তার গন্ধ হলো orphan অবজেক্ট যার বিল আসতেই থাকে। ঢোকা-বের হওয়ার ওজন হলো checksum, আর শহরের অফিসে খাতার কপি হলো ব্যাকআপ, যেটা replication থেকে আলাদা জিনিস। বাস্তবে প্রতিটা VOD প্ল্যাটফর্মে এই কয়েকটা সিদ্ধান্তই ঠিক করে দেয় বছর দুয়েক পর স্টোরেজের বিল কত হবে, আর একটা ভিডিও মুছতে বললে সেটা সত্যিই মোছা যাবে কিনা।

কেন লেআউট আগে ভাবতে হয়

Object storage-এ কোনো ফোল্ডার নেই, আছে শুধু একটা লম্বা key আর তার পেছনে বাইট। “ফোল্ডার” যা দেখেন, সেটা key-এর / দেখে টুল বানিয়ে দেখায়। এর একটা কঠিন ফল আছে: একটা অবজেক্টের নাম বদলানো মানে কপি করে পুরনোটা মোছা। দশ হাজার ভিডিও, প্রতিটায় কয়েক হাজার সেগমেন্ট, তাহলে লেআউট বদলানো মানে কোটি কোটি কপি অপারেশন, প্রতিটার জন্য রিকোয়েস্ট খরচ, আর মাঝখানে CDN-এর সব URL ভেঙে যাওয়া। তাই প্রথম দিনেই ঠিক করে ফেলুন।

একটা ভিডিও প্রসেস হওয়ার পর কত অবজেক্ট হয়, একটা হিসাব দেখি (সংখ্যাগুলো উদাহরণ):

2-hour film, 4-second segments            -> 1,800 segments per rendition
6 video renditions + 2 audio tracks       -> 8 x 1,800 = 14,400 segment objects
+ init segments, playlists, manifests     -> ~30
+ poster, storyboard sprites, VTT, captions -> ~20
------------------------------------------------------------------
~14,450 objects for ONE film
x 2 if you also keep a cbcs and a cenc encrypted copy (chapter 6)

এই সংখ্যাটা মাথায় রাখুন। এর পরের প্রায় প্রতিটা সিদ্ধান্ত, লিস্ট করা, মোছা, রিকোয়েস্ট খরচ, cold tier-এ নেওয়া, এই “এক ভিডিও = হাজার হাজার অবজেক্ট” থেকেই আসে। স্টোরেজের মৌলিক ধারণা (object, block, file) আর MinIO চালানো নিয়ে স্টোরেজ ট্র্যাক আছে; এখানে শুধু VOD-এর দিকটা।

Object key ডিজাইন

একটা ভালো লেআউট দেখতে এমন:

vod-media-prod/
  t/7f3a2c/                                  <- tenant id (never the tenant's name)
    a/01JB8X2K9Q4M7N3P5R6S8T0V2W/            <- asset id (ULID from Postgres)
      src/
        9c1e...b4.mov                        <- sha256 of the uploaded bytes
      r/3/                                   <- revision 3 of the outputs
        v/1080p/init.mp4
        v/1080p/seg_00001.m4s
        v/720p/...
        a/bn/init.mp4
        a/bn/seg_00001.m4s
        master.m3u8
        manifest.mpd
      img/
        poster.jpg
        sprite_000.jpg
        storyboard.vtt
      cap/
        bn.vtt
        en.vtt

প্রতিটা অংশের পেছনে একটা কারণ আছে:

অংশকেন
t/ tenant idএকজন কাস্টমার চলে গেলে এক prefix ধরে সব লিস্ট ও ডিলিট; পারমিশন পলিসিও prefix ধরে লেখা যায়
a/ asset idডেটাবেসের id, যা কখনো বদলায় না; ইউজার টাইটেল বদলালেও key একই থাকে
src/ sha256একই ফাইল দুইবার আপলোড হলে চেনা যায়; key নিজেই প্রমাণ করে ভেতরে কী আছে
r/3/ revisionআবার এনকোড করলে নতুন revision; পুরনো URL CDN-এ ক্যাশ থাকলেও কখনো ভুল ফাইল দেয় না
v/, a/, cap/টাইপ অনুযায়ী আলাদা, যাতে “সব ক্যাপশন” বা “শুধু অডিও” লিস্ট করা সহজ

যে তিনটা ভুল সবচেয়ে বেশি হয়

১. ইউজারের ফাইলনেম key-তে রাখা। uploads/আমার বিয়ের ভিডিও (final) v2.MOV। এতে স্পেস, ব্র্যাকেট, Bangla অক্ষর, বড়-ছোট হাতের মিশ্রণ আছে, যেটা URL encoding-এ, শেল স্ক্রিপ্টে, আর কিছু টুলে আলাদা আলাদাভাবে ভাঙে। তার উপর দুইজন একই নামে আপলোড করলে একটা আরেকটাকে ওভাররাইট করে। আসল নাম ডেটাবেসের original_filename কলামে রাখুন, key-তে নয়।

২. ওভাররাইট করে “আপডেট” করা। ভিডিও আবার এনকোড করে একই key-তে লিখলে CDN-এ পুরনো সেগমেন্ট ক্যাশ হয়ে থাকে, আর প্লেয়ার নতুন playlist-এর সাথে পুরনো সেগমেন্ট মিশিয়ে ফেলে। ফল: মাঝপথে ছবি জমে যায়, বা অডিও-ভিডিও সিঙ্ক থেকে সরে যায়। নতুন revision নতুন prefix-এ লিখুন, তারপর ডেটাবেসে current_revision বদলান। এটা CDN ট্র্যাকের ভার্সনড URL-এর একই কৌশল, শুধু পুরো ভিডিওর লেভেলে।

৩. key দেখে মালিকানা ঠিক করা। key-তে tenant id আছে বলে সেটা দেখেই কাউকে অ্যাক্সেস দেওয়া যাবে না; যে কেউ URL বানিয়ে নিতে পারে। অ্যাক্সেস ঠিক হয় signed URL আর ডেটাবেসের পলিসি দিয়ে (চ্যাপ্টার ৮)।

একটা ছোট Go ফাংশন এই নিয়মগুলো এক জায়গায় রাখে, যাতে কোডের পাঁচ জায়গায় পাঁচ রকম key না বানায়:

package storage

import "fmt"

// Keys are built in one place so the layout can never drift between services.
type AssetKeys struct {
	Tenant   string
	Asset    string
	Revision int
}

func (k AssetKeys) prefix() string {
	return fmt.Sprintf("t/%s/a/%s", k.Tenant, k.Asset)
}

func (k AssetKeys) Source(sha256hex, ext string) string {
	return fmt.Sprintf("%s/src/%s.%s", k.prefix(), sha256hex, ext)
}

func (k AssetKeys) Segment(kind, rendition string, n int) string {
	return fmt.Sprintf("%s/r/%d/%s/%s/seg_%05d.m4s", k.prefix(), k.Revision, kind, rendition, n)
}

func (k AssetKeys) Master() string {
	return fmt.Sprintf("%s/r/%d/master.m3u8", k.prefix(), k.Revision)
}

// AssetPrefix is what deletion and listing use: everything for one asset.
func (k AssetKeys) AssetPrefix() string { return k.prefix() + "/" }

Content-addressed বনাম id-based

Content-addressed মানে key হলো কনটেন্টের hash। একই বাইট মানে একই key, তাই ডুপ্লিকেট আপনা থেকেই ধরা পড়ে, আর key দেখেই যাচাই করা যায় ভেতরের ফাইল ঠিক আছে কিনা। সমস্যা হলো মোছা: দুইটা asset যদি একই source শেয়ার করে, একটা মুছলে অন্যটার ফাইল মোছা যায় না, তাই reference count রাখতে হয়।

ব্যবহারিক নিয়ম: source ফাইলের জন্য hash ব্যবহার করুন কিন্তু asset-এর prefix-এর ভেতরে, যাতে ডিডুপ্লিকেশন না হলেও যাচাইয়ের সুবিধাটা থাকে, আর মোছা সোজা থাকে। আলাদা আলাদা কাস্টমারের মধ্যে ডিডুপ্লিকেশন প্রায় কখনো লাভজনক নয়, আর এতে একজনের ফাইল আছে কিনা সেটা আরেকজন বুঝে ফেলার একটা গোপনীয়তার ফাঁকও তৈরি হয়।

Source রাখবেন কিনা

আপলোড করা আসল ফাইলকে বলে mezzanine বা source। এটা সাধারণত সবচেয়ে বড় ফাইল (একটা ফোনের ৪K ভিডিও মিনিটে কয়েকশো MB পর্যন্ত হতে পারে), আর প্লেব্যাকে কখনো লাগে না। তাহলে রাখবেন কেন?

  • আবার এনকোড করার জন্য। নতুন কোডেক (ধরুন AV1) এলে, বা ladder বদলালে, বা একটা এনকোডার বাগ ধরা পড়লে, source ছাড়া উপায় নেই। আউটপুট থেকে আবার এনকোড করলে কোয়ালিটি আরেক ধাপ কমে।
  • আইনি বা কাস্টমারের চাহিদা। কিছু কাস্টমার চুক্তিতেই চায় আসল ফাইল রাখা হোক।

না রাখার কারণও আছে: খরচ, আর গোপনীয়তার ঝুঁকি (যত কম রাখবেন, তত কম ফাঁস হতে পারে)। এটা একটা পলিসির সিদ্ধান্ত, টেকনিক্যাল নয়, আর সেটা প্রাইসিং পেজে স্পষ্ট লিখে দেওয়া উচিত। কমন প্যাটার্নগুলো:

পলিসিকাদের জন্য
চিরকাল রাখা, cold tier-এস্টুডিও, আর্কাইভ, যারা পরে রিমাস্টার করবে
N দিন রেখে মুছে ফেলাইউজার-জেনারেটেড কনটেন্ট, কোর্স প্ল্যাটফর্ম
প্রসেস শেষ হলেই মুছে ফেলাখরচ যেখানে সবচেয়ে বড় চিন্তা, আর ভিডিওর আয়ু ছোট
কাস্টমার নিজে বেছে নেবেন (ভিন্ন দাম)B2B ভিডিও API

হাজার হাজার ছোট অবজেক্টের খরচ

ক্লাউড object storage-এ বিল আসে তিনভাবে: কত GB রাখছেন, কত রিকোয়েস্ট (PUT, GET, LIST) করছেন, আর কত বাইট বাইরে যাচ্ছে (egress)। VOD-এ দ্বিতীয়টা চমকে দেয়, কারণ প্রতিটা সেগমেন্ট আলাদা PUT। উপরের হিসাবে একটা ফিল্ম মানে প্রায় ১৪,৪৫০টা PUT, শুধু লেখার জন্য।

তিনটা জিনিস এই খরচ কমায়:

  1. লম্বা সেগমেন্ট। ৪ সেকেন্ডের বদলে ৬ সেকেন্ড মানে অবজেক্ট এক-তৃতীয়াংশ কম। তবে এর প্রভাব স্টার্টআপ আর ABR-এ পড়ে; ট্রেড-অফটা ভিডিও ট্র্যাকের প্যাকেজিং চ্যাপ্টারে আছে।
  2. Byte-range addressing। প্রতিটা রেন্ডিশন একটা বড় fMP4 ফাইল, আর playlist বলে দেয় কোন সেগমেন্ট কোন বাইট রেঞ্জে। ১,৮০০ অবজেক্টের বদলে একটা। CDN-কেও রেঞ্জ রিকোয়েস্ট ঠিকমতো ক্যাশ করতে হয়।
  3. Just-in-time packaging। স্টোরেজে শুধু কয়েকটা MP4 থাকে, সেগমেন্ট তৈরি হয় রিকোয়েস্টের সময় (চ্যাপ্টার ৬)।

Cold tier-এর লুকানো নিয়ম: অনেক প্রোভাইডারের সস্তা “infrequent access” বা আর্কাইভ ক্লাসে প্রতিটা অবজেক্টের একটা সর্বনিম্ন বিলযোগ্য সাইজ আর সর্বনিম্ন রাখার মেয়াদ থাকে, আর ফেরত আনতে আলাদা চার্জ। হাজার হাজার ছোট সেগমেন্ট সেখানে সরালে প্রতিটা ছোট ফাইল বড় ফাইলের দামে বিল হতে পারে, আর বিল কমার বদলে বাড়ে। cold tier-এ সাধারণত শুধু বড় source ফাইল পাঠান। সংখ্যাগুলো প্রোভাইডার ভেদে আলাদা আর বদলায়, তাই নিজের প্রোভাইডারের প্রাইসিং পেজ দেখে নিন।

রিকোয়েস্টের রেট লিমিটও আছে। বড় ক্লাউড প্রোভাইডার সাধারণত প্রতি prefix-এ প্রতি সেকেন্ডে কত রিকোয়েস্ট নেবে তার একটা সীমা রাখে, আর লোড বাড়লে prefix ভাগ করে নেয়। আমাদের লেআউটে প্রতিটা asset আলাদা prefix, তাই এক জনপ্রিয় ভিডিওর লোড অন্যগুলোর গায়ে পড়ে না। তবে প্লেব্যাকের লোড এমনিতেও CDN সামলায়; origin-এ সরাসরি কোটি GET যাওয়ার কথা না (origin shield দেখুন)।

পরিবেশ অনুযায়ী আলাদা বাকেট

vod-media-dev, vod-media-staging, vod-media-prod, প্রতিটার আলাদা credential। একই বাকেটে dev/ আর prod/ prefix রাখা লোভনীয়, কিন্তু তাহলে একটা ভুল স্ক্রিপ্ট বা ভুল env ভেরিয়েবল দিয়ে প্রোডাকশন মুছে ফেলা সম্ভব। আলাদা বাকেট আর আলাদা key থাকলে ডেভ মেশিনের credential দিয়ে প্রোডাকশনে লেখা শারীরিকভাবেই অসম্ভব। বাকেটের নাম config থেকে আসবে, আর না থাকলে অ্যাপ বুটেই থেমে যাবে; কোনো ডিফল্ট নয়।

আরেকটা আলাদা বাকেট দরকার আপলোড ইনবক্সের জন্য: ইউজার যেখানে সরাসরি presigned URL দিয়ে লেখে (চ্যাপ্টার ২)। ওটা অবিশ্বস্ত জায়গা, সেখানে অসম্পূর্ণ মাল্টিপার্ট আপলোড আর পরিত্যক্ত ফাইল জমে, তাই সেটার লাইফসাইকেল খুব কড়া রাখা হয়।

লাইফসাইকেল রুল

লাইফসাইকেল রুল হলো বাকেটকে বলে দেওয়া: এই prefix-এর অবজেক্ট এত দিন পর মুছে দাও বা অন্য ক্লাসে সরাও। কাজটা স্টোরেজ নিজে করে, আপনার কোনো cron লাগে না।

একটা asset-এর অবজেক্টগুলোর জীবন

S3-compatible API-তে রুলটা এমন দেখতে:

{
	"Rules": [
		{
			"ID": "expire-upload-inbox",
			"Status": "Enabled",
			"Filter": { "Prefix": "inbox/" },
			"Expiration": { "Days": 2 },
			"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 }
		}
	]
}
aws s3api put-bucket-lifecycle-configuration \
  --endpoint-url https://s3.baytalhikma.org \
  --bucket vod-upload-inbox \
  --lifecycle-configuration file://lifecycle.json

aws s3api get-bucket-lifecycle-configuration \
  --endpoint-url https://s3.baytalhikma.org \
  --bucket vod-upload-inbox

AbortIncompleteMultipartUpload রুলটা সবচেয়ে বেশি ভুলে যাওয়া হয়। একটা মাল্টিপার্ট আপলোড শুরু হয়ে মাঝপথে ইউজার ট্যাব বন্ধ করে দিলে, যে পার্টগুলো আপলোড হয়েছে সেগুলো বাকেটে থেকে যায়। এগুলো সাধারণ ls-এ দেখা যায় না, কিন্তু জায়গা নেয় আর বিল হয়। এই রুল ছাড়া মাসের পর মাস ধরে অদৃশ্য আবর্জনা জমতে থাকে।

সব S3-compatible স্টোর সব রুল বোঝে না। ২০২৬ সালের অক্টোবরে Garage-এর ডকস অনুযায়ী এর লাইফসাইকেলে শুধু Expiration আর AbortIncompleteMultipartUpload কাজ করে, আর prefix দিতে হয় Filter-এর ভেতরে (পুরনো top-level Prefix নয়)। স্টোরেজ ক্লাসে সরানোর (transition) রুল সেখানে নেই। নিজের স্টোরের S3 compatibility পেজ দেখে নিন, আর রুল লেখার পর get-bucket-lifecycle-configuration দিয়ে পড়ে নিশ্চিত হোন যে রুলটা সত্যিই বসেছে।

আউটপুট বাকেটে সাধারণত কোনো expiration রুল দেওয়া হয় না। প্লে হওয়া ভিডিও কতদিন থাকবে সেটা ঠিক করে ব্যবসা আর ইউজার, স্টোরেজ নয়। আউটপুট মোছা হয় শুধু কোড থেকে, ডেটাবেসের রেকর্ড ধরে।

Soft delete আর সত্যিকারের delete

ইউজার “ডিলিট” চাপলে দুই ধাপে কাজ হয়।

ধাপ ১, soft delete: ডেটাবেসে deleted_at বসানো হয়। প্লেব্যাক API সেই মুহূর্ত থেকে 404 দেয়, ক্যাটালগে আর দেখায় না। ফাইলগুলো এখনো আছে, তাই ভুল করে মুছলে একটা সময়সীমার মধ্যে ফেরত আনা যায়। এই সময়সীমাটা কত সেটা প্রোডাক্টে স্পষ্ট লিখুন।

ধাপ ২, purge: সময়সীমা পার হলে একটা ব্যাকগ্রাউন্ড জব আসল কাজটা করে:

  1. asset-এর prefix ধরে সব অবজেক্ট লিস্ট করা
  2. ব্যাচে মোছা (S3 API-এ এক DeleteObjects কলে সর্বোচ্চ ১,০০০টা key)
  3. CDN থেকে purge করা, যাতে এজে ক্যাশ থাকা কপিগুলোও চলে যায় (ট্যাগ দিয়ে purge এখানে সবচেয়ে কাজের)
  4. আবার লিস্ট করে যাচাই করা যে prefix খালি
  5. তারপর ডেটাবেসে purged_at বসানো
// PurgeAsset deletes every object under the asset prefix and verifies the
// prefix is empty before reporting success. Safe to retry.
func (s *Store) PurgeAsset(ctx context.Context, k AssetKeys) error {
	prefix := k.AssetPrefix()
	for {
		page, err := s.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
			Bucket: &s.bucket, Prefix: &prefix, MaxKeys: aws.Int32(1000),
		})
		if err != nil {
			return fmt.Errorf("list %s: %w", prefix, err)
		}
		if len(page.Contents) == 0 {
			return nil // verified empty
		}
		ids := make([]types.ObjectIdentifier, 0, len(page.Contents))
		for _, o := range page.Contents {
			ids = append(ids, types.ObjectIdentifier{Key: o.Key})
		}
		out, err := s.client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
			Bucket: &s.bucket, Delete: &types.Delete{Objects: ids, Quiet: aws.Bool(true)},
		})
		if err != nil {
			return fmt.Errorf("delete batch under %s: %w", prefix, err)
		}
		if len(out.Errors) > 0 {
			return fmt.Errorf("delete %s: %d keys failed, first: %s", prefix, len(out.Errors), aws.ToString(out.Errors[0].Message))
		}
	}
}

লুপটা প্রতিবার প্রথম পাতা থেকে লিস্ট করে, তাই মাঝপথে জব মরে গেলে আবার চালালে যেখানে থেমেছিল সেখান থেকেই চলে। এটাকে River-এর জব বানালে রিট্রাই বিনা পয়সায় পাওয়া যায়।

“মুছে ফেলা হয়েছে” বলার আগে চারটা জায়গা মনে রাখুন: স্টোরেজের অবজেক্ট, CDN-এর ক্যাশ, ব্যাকআপ, আর লগ বা অ্যানালিটিক্সে থাকা URL। কেউ যদি নিজের ডেটা মুছে দেওয়ার আইনি অধিকার প্রয়োগ করেন, প্রথম দুইটা purge জব সামলায়। ব্যাকআপ থেকে সাধারণত আলাদা করে একটা ফাইল মোছা যায় না; তাই নিয়ম হয় ব্যাকআপের নিজস্ব মেয়াদ শেষে সেটা চলে যাবে, আর রিস্টোর করলে আগে মোছা asset-গুলো আবার মুছে দেওয়া হবে। আপনার এলাকার আইনে কী লাগে, সেটা আইনজীবীর কাছ থেকে নিশ্চিত হোন।

Consistency: লেখার পরেই কি পড়া যায়?

ধরুন এনকোড জব শেষ সেগমেন্ট লিখল, তারপর master playlist লিখল, তারপর ডেটাবেসে “ready” বসাল। ইউজার সাথে সাথে প্লে চাপল। সব ফাইল কি পড়া যাবে?

AWS S3-এ হ্যাঁ। AWS-এর ডকস অনুযায়ী (২০২৬ সালের অক্টোবরে পড়া) সব রিজিয়নে অবজেক্টের PUT আর DELETE-এর জন্য strong read-after-write consistency আছে: সফল PUT-এর রেসপন্স পাওয়ার পর যেকোনো GET বা LIST নতুন ডেটাই দেখাবে। তবে বাকেটের কনফিগারেশন (যেমন versioning চালু করা) eventually consistent, আর একাধিক key জুড়ে কোনো atomic আপডেট নেই।

অন্য S3-compatible স্টোরে নিজে যাচাই করুন। প্রতিটা প্রজেক্ট নিজের consistency মডেল নিজের ডকসে লেখে, আর ক্লাস্টার কীভাবে সেটআপ করেছেন (কত নোড, কত replica) তার উপরও নির্ভর করে। ধরে নেবেন না।

যে স্টোরই হোক, একটা নিয়ম আপনাকে বাঁচাবে: manifest সবার শেষে লিখুন। সেগমেন্ট আগে, init সেগমেন্ট আগে, তারপর media playlist, তারপর master playlist, তারপর ডেটাবেস। manifest-টাই “commit point”; ওটা না থাকলে প্লেয়ার শুরুই করতে পারে না, তাই অর্ধেক লেখা ভিডিও কখনো প্লে হয় না। আর একাধিক key জুড়ে atomic আপডেট নেই বলে নতুন revision আলাদা prefix-এ লেখা হয় (উপরের r/3/), যাতে পুরনো revision শেষ মুহূর্ত পর্যন্ত অক্ষত থাকে।

Integrity: ফাইলটা কি সত্যিই ঠিক আছে?

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

আপলোডের সময় checksum। S3 API-তে লেখার সময় একটা checksum পাঠালে সার্ভার নিজে হিসাব করে মিলিয়ে নেয়, না মিললে লেখা প্রত্যাখ্যান করে। AWS-এর ডকস অনুযায়ী (২০২৬ সালের অক্টোবর) এর ডিফল্ট অ্যালগরিদম CRC64NVME, আর CRC32, CRC32C, SHA-1, SHA-256, MD5 সহ আরো কয়েকটা সাপোর্ট করে। অন্য S3-compatible স্টোর এই হেডারগুলোর সবগুলো বোঝে কিনা, ডকসে দেখে নিন।

নিজের ডেটাবেসে hash। ইনজেস্টের সময় source-এর SHA-256 হিসাব করে Postgres-এ রাখুন (key-তেও আছে)। পরে যেকোনো সময় ফাইল নামিয়ে মিলিয়ে দেখা যায়।

ETag-কে MD5 ধরে নেবেন না। একবারে আপলোড করা ফাইলের ETag প্রায়ই কনটেন্টের MD5 হয়, কিন্তু মাল্টিপার্ট আপলোডের ETag হলো পার্টগুলোর hash-এর hash, শেষে একটা - আর পার্টের সংখ্যা। বড় ভিডিও প্রায় সবসময় মাল্টিপার্টে আসে, তাই md5sum দিয়ে ETag মেলাতে গেলে কখনো মিলবে না, আর মনে হবে ফাইল ভাঙা।

Replication বনাম ব্যাকআপ

দুইটা আলাদা জিনিস, আর একটা আরেকটার বদলি নয়:

  • Replication একটা অবজেক্টের কপি আরেক জায়গায় রাখে, প্রায় সাথে সাথে। একটা ডেটা সেন্টার পুড়ে গেলে বাঁচায়। কিন্তু একটা বাগ সব ভিডিও মুছে দিলে, replication সেই মুছে ফেলাটাও সাথে সাথে কপি করে দেয়।
  • ব্যাকআপ একটা নির্দিষ্ট সময়ের ছবি, যেটা পরের ভুল থেকে আলাদা থাকে। versioning বা object lock (WORM) চালু থাকলে স্টোরের ভেতরেই কিছুটা পাওয়া যায়।

VOD-এ প্রশ্নটা হলো: কোনটা হারালে আর বানানো যায় না? আউটপুট source থেকে আবার বানানো যায় (সময় আর এনকোডিং খরচ লাগে)। source আর ডেটাবেস আর বানানো যায় না। তাই সবচেয়ে কড়া ব্যাকআপ লাগে Postgres-এর (কোন key কার, কোন revision চালু) আর source ফাইলের। খোয়ারিজমির শহরের অফিসের খাতাটাই আসল সম্পদ। বিস্তারিত ডিজাস্টার রিকভারি ট্র্যাকে।

নিজে হোস্ট করলে কোন স্টোর

ডিফল্ট স্ট্যাক self-hosted, তাই এই অংশটা গুরুত্বপূর্ণ, আর এখানকার পরিস্থিতি দ্রুত বদলায়।

  • MinIO: দীর্ঘদিন self-hosted S3-এর ডিফল্ট ছিল। ২০২৫ সালের শেষ দিকে community edition maintenance mode-এ যায়, প্রি-বিল্ট বাইনারি আর ইমেজ দেওয়া বন্ধ হয়, আর ২০২৬ সালের অক্টোবরে GitHub-এ minio/minio রিপোজিটরি archived দেখাচ্ছে। চালু সেটআপ হয়তো চলবে, কিন্তু নতুন সিকিউরিটি ফিক্স আসবে কিনা সেটা অনিশ্চিত। নতুন প্রজেক্ট শুরু করার আগে বর্তমান অবস্থা নিজে দেখে নিন। পুরনো সেটআপের জন্য MinIO চ্যাপ্টার দেখুন।
  • Garage: ছোট, একাধিক জায়গায় ছড়িয়ে চালানোর জন্য বানানো, রিসোর্স কম লাগে। ২০২৬ সালের অক্টোবরে এর ডকস অনুযায়ী presigned URL আর মাল্টিপার্ট আপলোড আছে, কিন্তু object versioning, object lock আর বাকেট replication API নেই, আর লাইফসাইকেলে শুধু expiration ও অসম্পূর্ণ মাল্টিপার্ট মোছা। তার মানে ব্যাকআপ আপনাকে বাইরের টুল দিয়ে করতে হবে (যেমন rclone দিয়ে আরেকটা ক্লাস্টারে নিয়মিত সিঙ্ক, যেটা replication নয়, কারণ সেখানে মোছাগুলো কপি না করার অপশন রাখা যায়)।
  • SeaweedFS, Ceph RGW: বড় স্কেলে আর বেশি ফিচারের জন্য, কিন্তু চালানো জটিল। Ceph-এর জন্য আলাদা অপারেশন দক্ষতা লাগে।

যেটাই বাছেন, অ্যাপের কোড শুধু S3 API-তে কথা বলুক, কোনো স্টোর-স্পেসিফিক ফিচারে নয়। তাহলে স্টোর বদলানো মানে endpoint আর credential বদলানো, কোড নয়।

ডিবাগিং: এই চ্যাপ্টারের সমস্যাগুলো

লক্ষণকীভাবে নিশ্চিত হবেনসমাধান
ভিডিও নেই, কিন্তু স্টোরেজের বিল কমছে নাaws s3api list-multipart-uploads --bucket vod-upload-inbox দিয়ে অসম্পূর্ণ আপলোড দেখুন; asset prefix ধরে aws s3 ls --recursive --summarize চালিয়ে মোছা asset-এর ফাইল আছে কিনা দেখুনAbortIncompleteMultipartUpload রুল যোগ করুন; purge জব চালু আছে কিনা আর সেটা যাচাই করে কিনা দেখুন
আবার এনকোড করার পর প্লেয়ার মাঝপথে আটকায়, বা অডিও সরে যায়curl -sI দিয়ে একটা সেগমেন্টের Age আর Last-Modified দেখুন; একই key-তে নতুন ফাইল লেখা হয়েছে কিনানতুন revision নতুন prefix-এ লিখুন; একই key ওভাররাইট বন্ধ করুন
“ready” দেখানো ভিডিও প্রথম কয়েক সেকেন্ড 404 দেয়জব লগে লেখার ক্রম দেখুন; manifest কি সেগমেন্টের আগে লেখা হয়েছে?manifest আর ডেটাবেস আপডেট সবার শেষে; লেখার পর HEAD করে নিশ্চিত হন
ফাইল মিলিয়ে দেখলে hash মেলে নাaws s3api head-object দিয়ে ETag দেখুন; শেষে -N থাকলে মাল্টিপার্টETag নয়, নিজের রাখা SHA-256 বা x-amz-checksum-* দিয়ে মেলান
মোছার পরও ভিডিও কিছু দেশে চলছেcurl -sI দিয়ে CDN রেসপন্সে HIT আর Age দেখুনpurge জবে CDN purge যোগ করুন; ট্যাগ দিয়ে asset-এর সব URL একবারে purge
লাইফসাইকেল রুল দিয়েছি, কিছু মুছছে নাget-bucket-lifecycle-configuration দিয়ে রুলটা পড়ুন; স্টোরের ডকসে রুল টাইপ সাপোর্ট দেখুনtop-level Prefix-এর বদলে Filter; স্টোর যে রুল বোঝে না সেটা কোডের জব দিয়ে করুন
ডেভের একটা স্ক্রিপ্ট প্রোডাকশনের ফাইল মুছে দিলকোন credential দিয়ে কল হয়েছে, স্টোরের access log দেখুনপরিবেশ অনুযায়ী আলাদা বাকেট আর আলাদা credential; বাকেট নাম config ছাড়া চালু হবে না
ইউজারের ফাইলনেমওয়ালা কিছু ভিডিও প্রসেস হয় নাফেইল হওয়া key-তে স্পেস, Bangla অক্ষর বা + আছে কিনা দেখুনkey-তে শুধু id আর hash; আসল নাম ডেটাবেসে

মূল শিক্ষা

  1. লেআউট প্রথম দিনেই ঠিক করুন। object storage-এ নাম বদলানো মানে কপি করে মোছা, আর এক ভিডিওতেই হাজার হাজার অবজেক্ট।
  2. key-তে শুধু id আর hash, কখনো ইউজারের দেওয়া নাম নয়। tenant আর asset prefix থাকলে এক কমান্ডে লিস্ট আর ডিলিট।
  3. কখনো ওভাররাইট নয়, নতুন revision। ক্যাশ করা পুরনো সেগমেন্ট আর নতুন playlist মিশে গেলে প্লেব্যাক ভাঙে।
  4. source রাখা একটা পলিসি, আর সেটা কাস্টমারকে জানিয়ে দিন। cold tier-এ শুধু বড় ফাইল, ছোট সেগমেন্ট নয়।
  5. অসম্পূর্ণ মাল্টিপার্ট আপলোড অদৃশ্য আবর্জনা। লাইফসাইকেল রুল দিয়ে মুছুন, আর রুলটা পড়ে নিশ্চিত হোন।
  6. মোছা মানে স্টোরেজ, CDN, আর যাচাই। prefix খালি কিনা না দেখে “deleted” লিখবেন না।
  7. manifest সবার শেষে। ওটাই commit point; অর্ধেক লেখা ভিডিও তাহলে কখনো প্লে হয় না।
  8. ETag checksum নয়। নিজের SHA-256 রাখুন।
  9. replication ব্যাকআপ নয়। সবচেয়ে মূল্যবান হলো ডেটাবেস আর source; আউটপুট আবার বানানো যায়।
  10. self-hosted স্টোরের অবস্থা দ্রুত বদলায়। অ্যাপ শুধু S3 API-তে কথা বলুক, যাতে স্টোর বদলানো মানে শুধু endpoint বদলানো।