ভিউ কাউন্ট হলো সেই প্রথম সংখ্যা যা একজন ভিজিটর বিশ্বাস করেন। এটি তাদের জানায় যে একটি ভিডিও তাদের ত্রিশ সেকেন্ড নাকি ত্রিশ মিনিটের সময় দেওয়ার মতো কি না। TopVideoHub-এ, এই সংখ্যাটি খুব দ্রুত পরিবর্তিত হয়। একটি ট্রেন্ডিং ক্লিপ মাত্র দশ মিনিটে ৪০,০০০ ভিউ পেয়ে যেতে পারে। যদি পেজের কাউন্টারটি স্থির হয়ে যায়, তবে মনে হয় যেন ঘরটি খালি। ব্যবহারকারীরা সাইট ছেড়ে চলে যায়।

সেই সংখ্যাটি ব্রাউজারে নিয়ে আসা শুনতে খুব সহজ মনে হতে পারে। কিন্তু আসলে তা নয়। বেশিরভাগ টিম প্রথমেই যে সমাধানটি বেছে নেয় তা হলো polling। এটি সেটআপ করা সহজ এবং স্টেজিং (staging) এনভায়রনমেন্টে এটি ঠিকঠাক কাজ করে। কিন্তু স্টেজিং সবসময় সঠিক চিত্র দেখায় না।

যখন Polling নিজের বিরুদ্ধে একটি DDoS হয়ে দাঁড়ায়

TopVideoHub টিম একটি সাধারণ JavaScript poller তৈরি করেছিল। এটি প্রতি পাঁচ সেকেন্ড অন্তর সর্বশেষ ভিউ কাউন্ট সংগ্রহ করত। তিনটি ব্রাউজার খোলা থাকা একটি টেস্ট এনভায়রনমেন্টে এটি দারুণ কাজ করছিল। কিন্তু প্রোডাকশনে (production), এটি প্ল্যাটফর্মটিকে ধসিয়ে দেয়।

প্রতি পাঁচ সেকেন্ড অন্তর রিফ্রেশ করা আট হাজার সমসাময়িক (concurrent) ভিউয়ার প্রতি সেকেন্ডে ১,৬০০টি রিকোয়েস্ট তৈরি করছিল। প্রতিটি রিকোয়েস্ট সরাসরি ডেটাবেসে আঘাত করছিল। Replication lag বেড়ে গিয়েছিল। Read replicas হিমশিম খাচ্ছিল। Cache layer গুলো বাইপাস হয়ে যাচ্ছিল। টিম ভিডিও সার্ভ করছিল না, বরং তারা নিজেরাই নিজেদের ওপর অতিরিক্ত লোড চাপিয়ে দিচ্ছিল।

Polling ততক্ষণ পর্যন্ত নির্দোষ যতক্ষণ না এটি সমস্যা তৈরি করে। কম ট্র্যাফিকের ড্যাশবোর্ড বা অ্যাডমিন প্যানেলের জন্য এটি ঠিক আছে। কিন্তু একটি ভাইরাল ভিডিও পেজের জন্য এটি একটি টাইম বোমা। টিমের সার্ভার থেকে ব্রাউজারে একটি পারসিস্টেন্ট পাইপ (persistent pipe) প্রয়োজন ছিল, তবে তাদের একটি ফুল-ডুপ্লেক্স প্রোটোকলের জটিলতা দরকার ছিল না।

কেন SSE ওয়ান-ওয়ে পাইপের জন্য উপযুক্ত

Server-Sent Events (SSE) ঠিক এই ধরনের সমস্যার জন্যই তৈরি করা হয়েছে: সার্ভারের কাছে ডেটা আছে, এবং ব্রাউজারকে শুধু তা শুনতে হবে।

WebSockets-এর মতো নয়, SSE সাধারণ HTTP-এর ওপর ভিত্তি করে চলে। এটি যতটা মনে হয় তার চেয়েও বেশি গুরুত্বপূর্ণ। আপনার নতুন কোনো প্রক্সি রুল, আপগ্রেড হেডার বা লোড-ব্যালেন্সারের জটিল কারসাজির প্রয়োজন নেই। আপনার সার্ভার যদি HTTP/1.1 বা HTTP/2 ব্যবহার করে, তবে SSE কাজ করবে। ডিবাগিং করা খুব সহজ কারণ স্ট্রিমটি কেবল টেক্সট। আপনি curl ব্যবহার করে এন্ডপয়েন্টে রিকোয়েস্ট পাঠিয়ে রিয়েল টাইমে সংখ্যাগুলো পরিবর্তন হতে দেখতে পারেন, যা একটি বাইনারি সকেট ফ্রেম কেন কাজ করছে না তা নিয়ে অনুমান করার চেয়ে অনেক ভালো।

ব্রাউজার নিজে থেকেই কঠিন কাজগুলো সামলে নেয়। যদি কানেকশন বিচ্ছিন্ন হয়ে যায়, তবে SSE Last-Event-ID হেডার ব্যবহার করে স্বয়ংক্রিয়ভাবে পুনরায় কানেক্ট হয়, যাতে সার্ভার বুঝতে পারে কোথা থেকে পুনরায় শুরু করতে হবে। JavaScript-এ এর API ব্যবহার করা খুবই সহজ: একটি EventSource তৈরি করুন, একটি onmessage হ্যান্ডলার যুক্ত করুন, এবং আপনার কাজ শেষ।

ক্যাশিং (Caching) হলো আসল আর্কিটেকচার

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

TopVideoHub এটি সমাধান করেছে APCu ব্যবহার করে, যা PHP-এর ইন-মেমরি অপকোড (opcode) এবং ইউজার ক্যাশ। এর প্রক্রিয়াটি সহজ। একটি ব্যাকগ্রাউন্ড প্রসেস—অথবা একটি টাইমার অনুযায়ী হালকা কোনো এন্ডপয়েন্ট—প্রতি দুই সেকেন্ড অন্তর বর্তমান ভিউ কাউন্ট APCu-তে লিখে রাখে। হাজার হাজার ব্রাউজার যে SSE এন্ডপয়েন্টটি খোলা রেখেছে, সেটি শুধুমাত্র APCu থেকে ডেটা পড়ে।

ফলাফল: দর্শক সংখ্যা যাই হোক না কেন, ডেটাবেসে প্রতি দুই সেকেন্ডে মাত্র একটি রিকোয়েস্ট যায়। ক্যাশ এখানে একটি shock absorber হিসেবে কাজ করে। APCu কোনো অদ্ভুত বা জটিল প্রযুক্তি নয়। এটি PHP-এর সাথেই আসে, শেয়ার্ড মেমরিতে থাকে এবং যেকোনো নেটওয়ার্ক রাউন্ড-ট্রিপের চেয়ে দ্রুত ডেটা পড়তে পারে। একটি নির্দিষ্ট সংখ্যা যা ঘন ঘন পরিবর্তিত হয় কিন্তু তাৎক্ষণিকভাবে নয়, তার জন্য এটিই সঠিক টুল।

আপনি যদি APCu ব্যবহার না করেন, তবে Redis বা Memcached-ও ব্যবহার করতে পারেন। মূল নীতিটি একই থাকে: ডেটাবেস থেকে 'hot read path' আলাদা করে ফেলুন।

PHP, LiteSpeed এবং Cloudflare-কে স্ট্রিমিং করতে রাজি করানো

PHP চায় কাজ শেষ করে বিদায় নিতে। ওয়েব সার্ভারগুলো আউটপুট বাফার (buffer) করতে এবং একটি গোছানো রেসপন্স দিতে চায়। কিন্তু SSE-এর প্রয়োজন এর ঠিক উল্টো: একটি কানেকশন যা খোলা থাকবে এবং ডেটা আসার সাথে সাথে বাইটগুলো ফ্লাশ (flush) করবে। সাবধানতা অবলম্বন না করলে, আপনার "স্ট্রিম"টি ৩০ সেকেন্ড পর একটি বড় ব্লকের মতো আসবে, যা পুরো উদ্দেশ্যকেই ব্যর্থ করে দেবে।

TopVideoHub যেভাবে পাইপটি বাধামুক্ত রেখেছিল তা নিচে দেওয়া হলো:

আউটপুট বাফারিং বন্ধ করুন। SSE স্ক্রিপ্টের শুরুতে PHP-এর যেকোনো বাফারিং লেয়ার ডিজেবল করে দিন। যদি কোনো বাফার সক্রিয় থাকে তবে ob_end_flush() কল করুন, এবং হেডার পাঠানোর পর ob_implicit_flush(true) ব্যবহার করে ইমপ্লিসিট ফ্লাশিং বন্ধ করে দিন।

প্রক্সিগুলোকে নির্দেশ দিন। X-Accel-Buffering: no হেডারটি পাঠান। Nginx এটি অনুসরণ করে। LiteSpeed-ও এটি অনুসরণ করে। এটি নির্দেশ দেয় যে রেসপন্সটি বাফার বা ক্যাশেবল ব্লকে কম্প্রেস করা উচিত নয়।

একটি স্বল্পস্থায়ী সময় নির্ধারণ করুন। প্রতিটি SSE কানেকশন একটি PHP ওয়ার্কারকে ব্যস্ত রাখে। TopVideoHub স্ট্রিমগুলোকে ৫৫ সেকেন্ডে সীমাবদ্ধ রাখে। যখন টাইমার শেষ হয়, সার্ভার একটি শেষ কমেন্ট পাঠায়, স্ট্রিমটি বন্ধ করে দেয় এবং ব্রাউজার স্বয়ংক্রিয়ভাবে পুনরায় কানেক্ট হয়। এই পুনঃসংযোগটি একটি নতুন ওয়ার্কারের মাধ্যমে সম্পন্ন হয়, যা কোনো একটি প্রসেসকে চিরকাল আটকে থাকতে বাধা দেয়।

সক্রিয় থাকতে পিং (Ping) করুন। প্রতি বিশ সেকেন্ড অন্তর একটি কমেন্ট লাইন পাঠান—যেমন : ping। SSE-তে কমেন্টগুলো ব্রাউজারের মেসেজ হ্যান্ডলার দ্বারা উপেক্ষা করা হয়, কিন্তু এগুলো TCP কানেকশনকে সচল (warm) রাখে। লোড ব্যালেন্সার এবং CDN প্রায়শই ৩০ বা ৬০ সেকেন্ড পর নীরব কানেকশনগুলো বিচ্ছিন্ন করে দেয়। একটি সাধারণ নিউলাইন (newline) আপনাকে এই সমস্যা থেকে রক্ষা করতে পারে।

ব্যবহারকারীর ট্যাবকে গুরুত্ব দিন। যখন কোনো ভিজিটর ট্যাবটি মিনিমাইজ বা হাইড করে ফেলে, তখন কানেকশনটি বন্ধ করে দিন। ব্রাউজারে visibilitychange ইভেন্টের জন্য অপেক্ষা করুন এবং eventSource.close() কল করুন। সার্ভারেরও উচিত ক্লায়েন্ট ডিসকানেক্ট হওয়া শনাক্ত করা এবং লুপটি বন্ধ করে দেওয়া। PHP-তে একটি লুপের ভেতরে connection_aborted() দিয়ে এটি চেক করা সম্ভব। যারা দশ মিনিট আগে চলে গেছে, তাদের জন্য 'ঘোস্ট কানেকশন' (ghost connections) রেখে প্রসেসর বা ওয়ার্কার (worker) নষ্ট করবেন না।

কঠোর সীমাবদ্ধতা: PHP Workers

PHP-তে SSE তার সীমাবদ্ধতা সম্পর্কে অত্যন্ত স্পষ্ট। প্রতিটি খোলা SSE কানেকশন একটি করে PHP worker ব্যবহার করে। আপনার পুলে যদি ১০০টি worker থাকে, তবে আপনি সর্বোচ্চ ১০০টি স্ট্রিম চালাতে পারবেন। এটাই শেষ কথা। আপনি যতক্ষণ Apache বা PHP-FPM প্রসেস মডেলের ভেতরে আছেন, ততক্ষণ কোনো async বিকল্প নেই। আপনি pm.max_children টিউন করতে পারেন, কিন্তু মেমরি এবং CPU-ই আসল সীমানা নির্ধারণ করে দেয়।

যদি আপনি নিয়মিত পেজ লোড, API কল এবং অ্যাসেট জেনারেশনের জন্যও worker ব্যবহার করেন, তবে এই সীমাবদ্ধতা খুব দ্রুত আপনার সমস্যা হয়ে দাঁড়াবে। আপনার worker saturation সতর্কতার সাথে পর্যবেক্ষণ করুন। যদি আপনার SSE endpoint কিউ (queue) হতে শুরু করে কারণ সব worker বিশ মিনিটের স্ট্রিম নিয়ে আটকে আছে, তবে আপনার পুরো সাইট ধীর হয়ে যাবে।

যখন সংখ্যাটি আর সামলানো সম্ভব হয় না, তখন পরিবর্তন আনুন। Go হলো পরবর্তী স্বাভাবিক ধাপ, যদিও Rust, Node.js বা Erlang একই ভূমিকা পালন করতে পারে। Go-এর goroutines হলো এর মূল চাবিকাঠি। একটি goroutine মাত্র কয়েক কিলোবাইট জায়গা নেয়। আপনি সাধারণ হার্ডওয়্যার ব্যবহার করেই কোনো সমস্যা ছাড়াই হাজার হাজার স্ট্রিম ধরে রাখতে পারেন। মূল লজিক একই থাকে—ক্যাশ থেকে পড়া এবং সকেটে লেখা—তবে রানটাইমটি ভারী প্রসেস থেকে হালকা থ্রেডে (lightweight threads) পরিবর্তিত হয়।

তবে শুরুতেই সেখানে যাবেন না। PHP আপনাকে আশ্চর্যজনকভাবে অনেক দূর নিয়ে যেতে পারে। প্রথমে প্রোডাক্টটি যাচাই করে নিন। যখন মেট্রিক্স পেজে ডেটাবেস ওভারলোডের বদলে 'worker exhaustion' দেখা দেবে, তখন বুঝবেন আপনি এই স্ট্যাকের চেয়ে বড় হয়ে গেছেন। এটি একটি ভালো সমস্যা।

সারসংক্ষেপ

লাইভ কাউন্টার মানে কেবল প্রযুক্তি নয়। এটি আপনার ব্যবহারকারীদের হাত থেকে ডেটাবেসকে রক্ষা করার বিষয়। SSE দিয়ে শুরু করুন কারণ এটি দেখতে যতটা জটিল মনে হয়, আসলে ততটা নয়। স্ট্রিম এবং ডেটাবেসের মাঝে আগ্রাসীভাবে (aggressively) ক্যাশিং ব্যবহার করুন যাতে কানেকশন সংখ্যা কোয়েরি সংখ্যায় পরিণত না হয়। আপনার worker লিমিটের ওপর তীক্ষ্ণ নজর রাখুন। এবং সহজভাবে শুরু করুন। যতক্ষণ প্রয়োজন না হচ্ছে, PHP-ই যথেষ্ট; আর যখন প্রয়োজন হবে, তখন আপনি নিজেই জানবেন কেন আপনাকে নতুন করে লিখতে হচ্ছে।

আপনার পরবর্তী প্রজেক্টের জন্য:

  • যখন ডেটা একমুখী হয় (সার্ভার থেকে ব্রাউজারে), তখন SSE ব্যবহার করুন।
  • ডেটাবেসের সামনে একটি ক্যাশ লেয়ার রাখুন। প্রতি কয়েক সেকেন্ডে একটি কোয়েরি হাজার হাজার কোয়েরির চেয়ে অনেক ভালো।
  • SSE কানেকশন এক মিনিটের নিচে সীমাবদ্ধ রাখুন এবং ব্রাউজারকে পুনরায় কানেক্ট হতে দিন।
  • ট্যাব হাইড করলে স্ট্রিম বন্ধ করে দিন। অলস (idle) কানেকশনের জন্য লাইভ ওয়ার্কার খরচ করবেন না।
  • PHP worker ব্যবহার পর্যবেক্ষণ করুন। যখন এটি সর্বোচ্চ সীমায় পৌঁছে যাবে, তখন স্ট্রিমিং লেয়ারটি Go-তে নিয়ে যান।