بلاگ پر واپس جائیں
28 اگست، 2026Sergei Solod12 منٹ پڑھنے کا وقت

Nginx کے پیچھے Bunny Storage کے ساتھ دو ماہ: میں نے تصاویر، ویڈیو اور آڈیو الگ میڈیا سرور پر کیوں منتقل کیے

دو ماہ تک میں نے Bunny CDN کے بجائے Bunny Storage کو اپنے Nginx cache کے پیچھے private origin کے طور پر استعمال کیا۔ Production logs میں cold media requests پر long-tail connection timeout اور MP4 کے لیے Nginx Slice اور غیر ہم آہنگ ETag کا الگ مسئلہ سامنے آیا؛ آخرکار میں ایک سادہ private media origin پر منتقل ہوگیا۔

Bunny StorageNginxویڈیو کیشنگCache missمیڈیا سرور

تقریباً دو ماہ تک میرا media delivery path یہ تھا:

Browser
  ↓
working server
  ↓
Nginx
  ↓
local proxy_cache
  ↓ cache MISS
Bunny Storage

تصاویر، ویڈیوز اور آڈیو Bunny Storage میں تھے۔ صارف Bunny سے براہ راست رابطہ نہیں کرتا تھا۔ میرا Nginx [https://storage.bunnycdn.com](https://storage.bunnycdn.com) سے object مانگتا، server-side AccessKey شامل کرتا، response کو local SSD پر cache کرتا اور public URL خود serve کرتا تھا۔

دائرہ واضح ہے: یہ Bunny CDN کا review نہیں ہے۔ اس تجربے میں میں نے Bunny CDN استعمال نہیں کیا۔ Storage Zone + Pull Zone/CDN مختلف delivery architecture ہے اور میں Bunny CDN کو اب بھی ایک مضبوط product سمجھتا ہوں۔ میں نے خاص طور پر Bunny Storage کو اپنے Nginx cache کے runtime origin کے طور پر آزمایا۔

ابتدائی architecture مناسب کیوں لگا

Workload میں بڑی تعداد میں immutable media objects تھے: AVIF، JPEG، PNG، MP4، WebM، audio اور دوسرے static files، چھوٹی تصاویر سے بڑے ویڈیوز تک۔ میں پورا dataset application server پر نہیں رکھنا چاہتا تھا، مگر public URLs، cache policy، redirects، missing files، Range اور fallback اپنے Nginx سے کنٹرول کرنا چاہتا تھا۔

                    ┌── HIT ── local SSD
                    │
Browser → Nginx cache
                    │
                    └── MISS ── Bunny Storage

HIT پر یہ بہت اچھا کام کرتا تھا۔ Nginx local disk سے object دیتا اور Bunny request میں شامل ہی نہیں ہوتا تھا۔ یہی اچھا behavior cold MISS کی اصل حالت کو کافی دیر تک چھپاتا رہا۔

مقبول files origin کی اصل latency چھپاتے تھے

بار بار مانگی جانے والی image cache میں رہنے کا زیادہ امکان رکھتی ہے۔ پرانی اور کم کھولی جانے والی file زیادہ آسانی سے evict ہوتی ہے۔ اسی لیے پرانے pages غیر ارادی طور پر origin benchmark بن گئے۔

جولائی کے ایک snapshot میں تقریباً 36 GB media cache، 355,000 سے زیادہ cache files، تقریباً 35 GB limit اور root filesystem تقریباً 93% بھرا ہوا تھا۔ قریب کے snapshots میں entries 400,000 کے قریب پہنچتی تھیں۔ اتنا بڑا cache بھی finite ہے۔

اہم سبق: تیز HIT، تیز origin ثابت نہیں کرتا؛ صرف local cache کی رفتار ثابت کرتا ہے۔

Production logs میں file body آنے سے پہلے failures تھے

upstream timed out
while connecting to upstream

اور:

upstream timed out ... while SSL handshaking to upstream

یہ بڑی file کے آہستہ download ہونے جیسا نہیں۔ Nginx ابھی upstream connection یا TLS handshake مکمل کرنے کی کوشش کر رہا تھا۔

30 جولائی کے diagnostic snapshot میں media error log کی آخری 5,000 lines میں 317 upstream-timeout matches تھے۔ پچھلے دن کے snapshot میں 578 تھے۔ یہ unique users نہیں، log matches ہیں، لیکن مسئلہ ایک request تک محدود نہیں تھا۔

خراب cases ایک storage IP تک محدود نہیں تھے

109.61.89.53
109.61.89.54
109.61.89.55
109.61.89.57
79.127.226.193

اس set کے مختلف addresses پر failure دکھائی دیے۔ لہٰذا میرے server کی نظر میں مسئلہ external origin path کے behavior میں تھا، کسی ایک ہمیشہ خراب IP میں نہیں۔

Logs سے یہ ثابت نہیں ہوتا کہ root cause storage backend، routing، peering، provider path، balancing یا کچھ اور تھا۔ میں یہ ثابت کرسکتا ہوں کہ کچھ cold MISS متوقع وقت میں Bunny Storage upstream connection قائم نہیں کر پائے۔

ایک measurement نے long tail واضح کردیا

109.61.89.53   total ≈ 29.8 ms
109.61.89.54   total ≈ 28.5 ms
109.61.89.57   total ≈ 42.7 ms
79.127.226.193 total ≈ 43.6 ms

ایک measured path بالکل مختلف تھا:

109.61.89.55
TCP    1.017782 s
TLS    1.048306 s
TOTAL  1.054474 s

اس کا مطلب یہ نہیں کہ Bunny Storage ہمیشہ ایک second slow تھا۔ زیادہ تر measurements چند درجن ms تھے۔ اصل مسئلہ variance تھا: وہی origin hostname کبھی مفید transfer شروع ہونے سے پہلے ایک second سے زیادہ connection delay دے سکتا تھا۔

Media pages میں average سے tail latency زیادہ اہم ہے

اگر 79 images 30–50 ms میں load ہوں اور ایک image ایک second لے، average پھر بھی اچھی لگ سکتی ہے۔ User average نہیں دیکھتا، ایک خالی image box دیکھتا ہے۔ اسی لیے p95، p99 اور slowest cold MISS میرے لیے زیادہ اہم ہوگئے۔

Video نے دوسرا، مکمل مختلف مسئلہ دکھایا

Browser byte Range مانگ سکتا ہے:

Range: bytes=0-1048575

اور seek کے بعد:

Range: bytes=50000000-51048575

206 Partial Content playback اور seek کو تمام پچھلے bytes download کیے بغیر ممکن بناتا ہے۔

پہلا Range cache ایک physical video کو کئی cache keys میں بانٹ رہا تھا

proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_cache_key "$scheme|$host|$request_uri|range=$http_range";
proxy_cache_valid 200 206 301 302 30d;

Browser Range arbitrary ہوتا ہے۔ bytes=0-1048575، bytes=0-999999 اور seek offsets ایک ہی MP4 کے لیے مختلف keys بناتے تھے۔ میں نے نہیں ناپا کہ fragmentation نے کتنے GB استعمال کیے، اس لیے کوئی عدد نہیں گھڑتا۔

پھر fixed 1 MB slices استعمال کیے

Nginx Slice module بڑے object کو fixed chunks میں normalize کرسکتا ہے:

0–1 MB
1–2 MB
2–3 MB
...
slice 1m;
proxy_set_header Range $slice_range;
proxy_cache_key "$scheme|$host|$uri|$slice_range";
proxy_cache_valid 200 206 30d;

Cache reuse کے لیے یہ بہتر تھا۔ پھر production میں یہ error آیا:

etag mismatch in slice response while reading response header from upstream

MP4 slices ETag consistency پر fail ہورہے تھے

30 جولائی کو ایک MP4 کے repeated slice subrequests میں etag mismatch in slice response آیا، جبکہ requests 109.61.89.53، 109.61.89.57، 79.127.226.193 اور 109.61.89.55 جیسے addresses پر جا رہے تھے۔ 31 جولائی کو دوسرے MP4 پر وہی error class دوبارہ آیا۔

میں نے پہلے اسے غیر رسمی طور پر مختلف “signatures” کہا تھا۔ درست اصطلاح ETag تھی، نہ AccessKey، نہ signed URL اور نہ cryptographic signature۔

Nginx مختلف ETag والے slices کو کیوں نہیں جوڑتا

slice 1 → bytes 0–1 MB → ETag A
slice 2 → bytes 1–2 MB → ETag A

consistent ہے۔ لیکن:

slice 1 → ETag A
slice 2 → ETag B

میں Nginx محفوظ طور پر یہ فرض نہیں کرسکتا کہ دونوں ایک ہی representation کے ہیں۔ Blind assembly مختلف versions کے bytes ملا سکتی ہے۔ Nginx کا byte-range caching article اسی validator check کو بیان کرتا ہے۔

Logs ثابت کرتے ہیں کہ Nginx نے ایک ہی MP4 کے slices میں incompatible ETag دیکھے۔ وہ exact internal cause ثابت نہیں کرتے کیونکہ میں ہر subrequest کا literal ETag record نہیں کر رہا تھا۔ Upstream address کی تبدیلی backend response inconsistency کو plausible بناتی ہے، مگر یہ inference ہے۔

اس کا مطلب یہ نہیں کہ Bunny Storage Range support نہیں کرتا

Range کام کررہا تھا اور میں 206 Partial Content cache کر رہا تھا۔ مخصوص مسئلہ Nginx Slice، متعدد storage responses اور safe assembly کے لیے ETag consistency کا combination تھا۔

Video normal origin timeout میں بھی جاسکتا تھا۔ اس لیے دو الگ failure modes تھے: origin connection/latency اور Slice/ETag consistency۔

آخرکار MP4 کے لیے Slice ہٹا دیا

proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_cache_key "$scheme|$host|$uri";
proxy_no_cache $http_range;
proxy_cache_valid 200 30d;

Partial response complete object کی طرح store نہیں ہوتی تھی۔ Assembly problem ختم ہوا، مگر cold Range دوبارہ external origin پر زیادہ dependent ہوگیا۔

مجھے احساس ہوا کہ Storage کے آگے اپنا CDN کا حصہ بنا رہا ہوں

Nginx میں cache lock، stale، background update، Range، 206، Slice، normalized chunks، keepalive، TLS reuse، retry، timeout tuning اور custom cache keys جمع ہوگئے۔ ہر چیز الگ سے معقول تھی، مگر origin سے حقیقی ضرورت بہت سادہ تھی: immutable file محفوظ کرو اور bytes دو۔

نیا origin جان بوجھ کر سادہ ہے

Media dataset کو dedicated server پر منتقل کیا جس کا کام تقریباً Nginx + SSD ہے۔ Pure static origin کے لیے تقریباً 1 vCPU، قریب 1 GB RAM اور چند سو GB SSD ایک مناسب starting point ہوسکتے ہیں، اگر network اور disk کافی ہوں۔

یہ universal sizing rule نہیں۔ Bandwidth، object size، concurrency، IOPS اور MISS rate اہم ہیں۔ موجودہ measurement والا origin زیادہ resources رکھتا ہے، اس لیے current peak کو exact 1-vCPU/1-GB benchmark نہیں کہتا۔

نئی architecture

Browser
   ↓
working server
   ↓
Nginx + local media cache
   │
   ├── HIT
   └── MISS
          ↓
      WireGuard
          ↓
     media origin
          ↓
        Nginx
          ↓
         SSD

Origin private ہے۔ Working server ایک fixed private IP سے بات کرتا ہے۔ Public HTTPS working server پر terminate ہوتا ہے، جبکہ origin traffic پہلے سے encrypted WireGuard tunnel کے اندر HTTP استعمال کرسکتا ہے۔

Cold path سے کیا نکل گیا

working server
↓
WireGuard
↓
fixed private IP
↓
Nginx
↓
SSD

اب اس direct connection میں public DNS، کئی storage addresses میں selection، public Internet path اور Storage API کا اضافی TLS handshake نہیں۔

Cold MISS اتنا سستا ہوگیا کہ HIT جیسا محسوس ہوتا ہے

Real-file پر چھوٹے Range tests عموماً:

CONNECT ≈ 9.5–12.5 ms
TTFB    ≈ 19–23 ms
TOTAL   ≈ 19–23 ms

تھے۔ 10/10، 20/20 sequential اور 20/20 parallel checks کامیاب ہوئے۔ Origin پر وہی چھوٹا request تقریباً 0.5–0.9 ms لیتا تھا۔

یہ چھوٹے Range/TTFB tests ہیں، full video download time نہیں۔ مکمل transfer size اور throughput پر منحصر ہے۔

پرانا bad case اور نیا cold path بہت مختلف ہیں

پرانے normal measurements اکثر 29–44 ms تھے۔ Bad observed path تقریباً 1.054 s تھا۔ نیا tiny cold request قریب 20 ms ہے۔

اس specific outlier کو 20 ms سے compare کریں تو latency تقریباً 52× مختلف بنتی ہے۔ اس کا مطلب “میرا server Bunny Storage سے 52× faster ہے” نہیں، صرف دو observed paths کا موازنہ ہے۔

MP4 اب زیادہ سادہ ہے

Cold fill میں Nginx private origin سے مکمل MP4 لے سکتا ہے، ایک cache entry رکھ سکتا ہے، پھر browser کے arbitrary byte Range کو local cache سے serve کرسکتا ہے۔ Fill کے وقت upstream Range ہٹایا جاسکتا ہے اور proxy_force_ranges local partial responses دیتا ہے۔

cold MISS
↓
private origin سے مکمل MP4
↓
ایک cached file
↓
client Range local 206

Full-object caching کی قیمت بھی ہے

اگر cold MP4 500 MB ہے اور پہلا user صرف دس seconds دیکھنا چاہتا ہے، cache fill پورے 500 MB منتقل کرسکتا ہے۔ میرے file sizes اور access pattern کے لیے یہ trade-off قابل قبول ہے۔ Mostly-cold multi-GB videos کے لیے stable slice cache، HLS/DASH، video CDN یا Bunny Stream بہتر ہوسکتا ہے۔

دوسرے بڑے media کے لیے fixed slice اب بھی مفید ہے

Nginx Slice خراب feature نہیں۔ Immutable object اور stable origin کے ساتھ مفید ہے۔ فرق یہ ہے کہ اب تمام chunks ایک controlled origin اور filesystem representation سے آتے ہیں۔

MISS سستا ہو تو چھوٹا cache بھی تیز محسوس ہوسکتا ہے

پرانا cache تقریباً 35–36 GB تھا۔ نیا cache قریب 16 GB پر مستحکم ہوا کیونکہ min_free کے ساتھ تقریباً 8 GB خالی رکھتا ہوں۔

پرانا:
HIT  = تیز
MISS = کبھی تکلیف دہ

نیا:
HIT  = تیز
MISS = کافی تیز

بڑا cache MISS کی تعداد گھٹاتا ہے۔ اچھا origin ہر MISS کی قیمت گھٹاتا ہے۔ اس workload میں دوسری چیز زیادہ اہم نکلی۔

Real production traffic نے origin کا کردار واضح کردیا

60-second snapshot میں working server صارفین کو تقریباً 96.84 Mbit/s دے رہا تھا اور media origin سے تقریباً 7.07 Mbit/s لے رہا تھا۔ اس window میں نئے 502، 503، 504 یا upstream timeout نہیں تھے۔

بعد کے 30-second snapshot میں public TX 61.36 Mbit/s اور origin سے WireGuard RX 1.40 Mbit/s تھا۔ Origin bytes/public TX ratio 2.28% تھا۔

میں اس کا الٹ exact cache-hit rate نہیں کہتا، کیونکہ public traffic صرف media نہیں اور byte ratio request ratio نہیں۔ مگر یہ دکھاتا ہے کہ warm-up کے بعد origin زیادہ تر cold fill کرتا ہے۔

اب میں کیا monitor کروں گا

$upstream_addr
$upstream_connect_time
$upstream_header_time
$upstream_response_time
$upstream_cache_status
$request_time
$status

HIT اور MISS الگ، خاص طور پر MISS p95/p99۔

Video میں first Range، middle Range، suffix Range، seek کے کئی non-sequential Range، cold/warm، اور 206، Content-Range، Content-Length، Accept-Ranges، ETag دیکھوں گا۔ Slice ہو تو کئی slices پر validator consistency لازماً test کروں گا۔

ایک cold URL اور 20 مختلف cold URL ایک test نہیں

proxy_cache_lock ایک cold cache key پر بہت requests میں مدد کرتا ہے۔ 20 الگ files کو ایک origin request نہیں بناتا۔ 50 مختلف cold images والی page، ایک URL بار بار چلانے والے benchmark سے زیادہ حقیقی origin test ہوسکتی ہے۔

Self-hosting control دیتا ہے، مفت reliability نہیں

Private media server critical path کو سمجھنا آسان بناتا ہے، مگر backup، disk health، free space، updates، firewall، monitoring، restore اور redundancy میری ذمہ داری ہیں۔ ایک origin uncached objects کے لیے single point of failure بھی ہے۔

کب Bunny پھر بھی منتخب کروں گا

Global delivery، managed redundancy، تیزی سے بڑھتا storage یا کم administration چاہیے ہو تو Bunny کو دوبارہ ضرور evaluate کروں گا اور Storage + Pull Zone/CDN architecture کو بھی براہ راست دیکھوں گا۔

یہ experiment ثابت نہیں کرتا کہ ایک VPS Bunny CDN سے بہتر ہے، Bunny Storage ہمیشہ slow ہے یا Bunny Range نہیں دیتا۔ یہ دکھاتا ہے کہ میرے workload میں Bunny Storage کو Nginx cache کا direct runtime origin بنانے سے ایسا cold-path اور sliced-video behavior ملا جسے میں مزید manage نہیں کرنا چاہتا تھا۔

اب میرا اصول

دو ماہ میں cache size، cache key، Range، 206، Slice، 1 MB chunks، keepalive، TLS reuse، retries، stale اور timeouts optimize کیے۔ سب سے مؤثر تبدیلی زیادہ سادہ تھی: origin بدلنا۔

اگر user کی مطلوبہ file cache میں نہ ہو تو کیا ہوتا ہے؟

اگر جواب stable origin، predictable TTFB، صحیح Range، consistent validators اور واضح failure modes ہے تو cache optimization ہے۔ اگر اچھی UX اس بات پر منحصر ہے کہ user کبھی MISS نہ دیکھے، cache گہری architecture problem چھپا رہا ہے۔

میرے workload کے لیے چھوٹا، private اور boring media origin بہتر ثابت ہوا۔ یہ CDN نہیں، جادو نہیں اور operational responsibility ختم نہیں کرتا۔ اس نے صرف cold path کو boring بنادیا۔ دو ماہ images، connection timeout، video Range اور etag mismatch in slice response debug کرنے کے بعد مجھے یہی چاہیے تھا۔