সরাসরি পর্বে যান
পর্ব ৩২ · ৬২-এর মধ্যে · মডিউল ৫

TCP congestion control — slow start, AIMD, CUBIC আর BBR

TCP Congestion Control — Congestion Collapse, cwnd, Slow Start, Fast Recovery, Tahoe and Reno, AIMD Fairness, the Mathis Formula, CUBIC, BBR, ECN, DCTCP and L4S

Taught in Bangla, this lesson explains TCP congestion control: congestion collapse, the congestion window and sending rate, slow start and ssthresh, fast recovery, Tahoe versus Reno, AIMD fairness, the Mathis throughput formula, and modern algorithms CUBIC and BBR, plus ECN, DCTCP and L4S, with simulators and worked cwnd numericals.

এই পর্বে যা শিখবেন

  • Congestion কী, তার তিনটি দাম, আর ১৯৮৬-এর ধসের পর Van Jacobson-এর সমাধান
  • cwnd, ssthresh, slow start, congestion avoidance, fast retransmit, fast recovery — Tahoe আর Reno-র পার্থক্যসহ
  • পরীক্ষার ক্লাসিক cwnd-বনাম-round গ্রাফের অঙ্ক হাতে আর simulator-এ পুরো সমাধান
  • AIMD কেন ন্যায্যতায় পৌঁছায় (phase plot-এর যুক্তি), Mathis সূত্রের প্রমাণ, RTT-র অন্যায্যতা আর সমান্তরাল সংযোগ
  • CUBIC-এর সূত্র, Vegas-এর delay-ধারণা, BBR-এর মডেল আর চার ধাপ, ECN-DCTCP-L4S, আর Linux-এ algorithm দেখা ও বদলানো

১ · Congestion কী — ঢাকার মোড়ের জ্যাম আর তার দাম

সন্ধ্যায় ফার্মগেটের মোড়ের কথা ভাবুন। রাস্তা প্রতি মিনিটে হয়তো ১০০টি গাড়ি পার করাতে পারে। ৮০টি এলে সবাই চলে যায়। ১২০টি এলে প্রতি মিনিটে ২০টি করে লাইন বাড়তেই থাকে — যতক্ষণ না মোড়টি আটকে যায়। নেটওয়ার্কেও তা-ই: কোনো link-এ তার ক্ষমতার চেয়ে বেশি ডেটা এলে router-এর buffer-এ queue জমে। এই অবস্থার নাম congestion, বাংলায় নেটওয়ার্কের জ্যাম।

Congestion-এর দাম তিনটি, আর তিনটিই পরপর আসে:

  1. Delay বাড়ে: পাঠানোর হার link-এর ক্ষমতার কাছে গেলে গড় queuing delay খাড়া হয়ে ওঠে — ρ/(১ − ρ)-এর বক্ররেখা, যা পর্ব ০৫-এ (Delay, loss আর throughput) হিসাব করেছেন।
  2. Packet হারায়: Buffer সসীম; ভরে গেলে router নতুন packet ফেলে দেয় (tail drop) — পর্ব ১৫-এর router-এর ভেতরে দেখেছেন।
  3. ক্ষমতা নষ্ট হয়: প্রেরক হারানো packet আবার পাঠায়। ফলে link-এর একটি অংশ যায় একই ডেটার দ্বিতীয় কপিতে। Timer আগেভাগে ফুরালে যে packet আসলে হারায়নি, তারও কপি যায়। আর যে packet শেষ hop-এ গিয়ে ফেলা হলো, তার জন্য আগের সব link-এর খাটুনি বৃথা। প্রাপকের অ্যাপের কাছে পৌঁছানো কাজের ডেটার হার — goodput — তখন পাঠানোর হারের চেয়ে অনেক কম।

ঢাকার জ্যামে একটি বাস আটকে থাকলে শুধু সেই বাস দেরি করে না — তার পেছনের মোড়ও আটকে যায়। আর যে যাত্রী অধৈর্য হয়ে নেমে আরেকটি বাসে উঠল, সে রাস্তায় একটি বাড়তি গাড়ি যোগ করল। নেটওয়ার্কে “আবার পাঠানো” ঠিক সেই অধৈর্য যাত্রী: জ্যাম যত বাড়ে, retransmission তত বাড়ে, আর retransmission জ্যাম আরও বাড়ায়।

Flow control বনাম congestion control

দুটিই প্রেরককে থামায়, কিন্তু কারণ আলাদা। পর্ব ৩১-এ (TCP flow control) দেখেছেন, flow control প্রাপককে বাঁচায় — প্রাপকের buffer যেন উপচে না পড়ে। Congestion control বাঁচায় পথের নেটওয়ার্ককে — মাঝের router-দের।

বিষয়Flow controlCongestion control
কাকে বাঁচায়প্রাপকের bufferপথের router আর link
সীমা কে জানায়প্রাপক নিজে, header-এর window ঘরে — rwndকেউ সরাসরি জানায় না; প্রেরক loss, delay বা ECN দেখে অনুমান করে — cwnd
দৈনন্দিন তুলনাদোকানদার বলছে “গুদামে আর ১০ বস্তার জায়গা”ট্রাকচালক রাস্তার জ্যাম দেখে নিজেই গতি কমাচ্ছে

২ · ১৯৮৬-এর ধস আর দুই পথ — end-to-end বনাম নেটওয়ার্কের সাহায্য

১৯৮৬ সালের অক্টোবর। তখনকার ইন্টারনেটে TCP-র কোনো congestion control ছিল না — প্রেরক প্রাপকের window যতটা অনুমতি দিত, ততটাই একবারে ছেড়ে দিত, আর timeout হলে আবার পুরোটা। Van Jacobson-এর পরের লেখা অনুযায়ী, Lawrence Berkeley Laboratory আর UC Berkeley-র মধ্যে — কয়েকশো মিটার দূরত্ব, মাঝে তিনটি hop — throughput প্রায় ৩২ kbps থেকে নেমে প্রায় ৪০ bps-এ দাঁড়িয়েছিল: মোটামুটি হাজার গুণ পতন। Link-গুলো পুরো ব্যস্ত, কিন্তু প্রায় সবই একই packet-এর বারবার পাঠানো কপি। John Nagle ১৯৮৪ সালেই RFC 896-এ এই বিপদের কথা লিখেছিলেন, নাম দিয়েছিলেন congestion collapse.

১৯৮৮: Jacobson আর Michael Karels-এর “Congestion Avoidance and Control” প্রবন্ধ (SIGCOMM ’88) slow start, congestion avoidance, ভালো RTT অনুমান আর exponential backoff প্রস্তাব করে; একই বছর এগুলো BSD-র “Tahoe” সংস্করণে আসে। ১৯৯০-এর “Reno” সংস্করণে আসে fast recovery. আজকের মানদণ্ড RFC 5681 (২০০৯)।

Jacobson-এর মূল ধারণা ছিল packet সংরক্ষণ: স্থিতিশীল অবস্থায় একটি packet নেটওয়ার্ক থেকে বের হলে (ACK ফিরলে) তবেই নতুন একটি ঢোকানো। ফলে ACK-এর ছন্দই পাঠানোর ছন্দ ঠিক করে — একে বলে ACK clocking. Bottleneck যত ধীর, ACK তত ফাঁকে ফাঁকে ফেরে, প্রেরকও তত ধীরে পাঠায়।

জ্যাম টের পাওয়ার দুই পথ

End-to-end

নেটওয়ার্ক কিছু বলে না। প্রেরক নিজে লক্ষণ দেখে অনুমান করে: packet হারাল (duplicate ACK বা timeout), নাকি RTT বাড়ছে। Reno, CUBIC, Vegas, BBR — সবাই মূলত এই দলে। সুবিধা: কোনো router বদলাতে হয় না।

TCP-র মূল পথ

Network-assisted

Router নিজে জানায়। আজকের প্রধান রূপ ECN (RFC 3168): ভিড়ের router packet না ফেলে IP header-এ CE দাগ দেয়; প্রাপক TCP header-এর ECE flag দিয়ে প্রেরককে খবর দেয়, প্রেরক গতি কমিয়ে CWR দিয়ে জানায় “কমিয়েছি”। ICMP Source Quench-এর মতো পুরোনো চেষ্টা বাতিল (পর্ব ২০)।

router-এর সহযোগিতা লাগে

ECN-এর দাগ দিতে router-এর লাগে AQM — RED বা CoDel-এর মতো নিয়ম, যা buffer ভরার আগেই সংকেত দেয়। এগুলোর কাজ পর্ব ১৫-এ, আর DSCP-র পাশের ECN-এর দুই bit পর্ব ২৬-এ দেখেছেন; এখানে শুধু TCP-র দিকটা।

৩ · Congestion window — গতির একটি সংখ্যা

TCP প্রেরক একটি চলক রাখে: cwnd (congestion window) — ACK পাওয়ার আগে নেটওয়ার্কে একসাথে সর্বোচ্চ কত বাইট ভাসমান রাখা যাবে, প্রেরকের নিজের অনুমানে। প্রাপকের দেওয়া সীমা rwnd-ও আছে, তাই আসল নিয়ম:

ACK-ছাড়া ভাসমান ডেটা ≤ min(rwnd, cwnd)

প্রতি RTT-তে প্রায় এক window পাঠানো যায়, তাই পাঠানোর হার ≈ cwnd ÷ RTT. উদাহরণ: cwnd = ২০ MSS, MSS = ১,৪৬০ byte, RTT ৪০ ms → ২০ × ১,৪৬০ × ৮ ÷ ০.০৪ = ৫.৮৪ Mbps. অর্থাৎ গতি বাড়াতে-কমাতে TCP শুধু cwnd বদলায়। বইয়ের অঙ্কে cwnd প্রায়ই MSS-এর এককে লেখা হয় (“cwnd = ২০”); kernel-এর ভেতরে Linux-ও segment গোনে।

Link পুরো ব্যবহার করতে হলে cwnd অন্তত পথের bandwidth-delay product-এর সমান হতে হবে (পর্ব ০৫)। ১০০ Mbps আর ৫০ ms-এ BDP = ৫ Mbit = ৬,২৫,০০০ byte — প্রায় ৪২৮টি ১,৪৬০ byte-এর segment. কিন্তু প্রেরক পথের bandwidth জানে না। তাই প্রশ্ন দাঁড়ায়: শুরুতে কত, আর কীভাবে বাড়াব-কমাব? নিচের তিনটি অংশ সেই উত্তর।

TCP-র তিনটি প্রশ্ন

(১) শুরুতে খালি পথের ক্ষমতা দ্রুত খুঁজব কীভাবে? — slow start. (২) কাছাকাছি পৌঁছে সাবধানে কীভাবে বাড়াব? — congestion avoidance. (৩) জ্যামের সংকেত পেলে কতটা কমাব? — fast recovery বা timeout-এর নিয়ম।

৪ · Slow start আর congestion avoidance — দ্রুত খোঁজা, তারপর সাবধানে

Slow start: সংযোগের শুরুতে cwnd ছোট — একটি initial window (IW). তারপর প্রতিটি নতুন ACK-এ cwnd বাড়ে ১ MSS করে। এক RTT-তে cwnd-সংখ্যক segment যায়, ফেরে ততগুলো ACK — তাই প্রতি RTT-তে cwnd দ্বিগুণ: ১, ২, ৪, ৮… নামে “slow”, কারণ আগের TCP শুরুতেই পুরো window ছেড়ে দিত; তার তুলনায় ১ থেকে শুরু করা ধীর। বৃদ্ধি আসলে exponential.

ssthresh: দ্বিগুণ হতে হতে cwnd যখন slow start threshold-এ পৌঁছায়, TCP সাবধানী মোডে যায় — congestion avoidance. এখানে প্রতিটি ACK-এ cwnd বাড়ে MSS × MSS ÷ cwnd, অর্থাৎ পুরো এক RTT-তে মোট মাত্র +১ MSS — রৈখিক বৃদ্ধি। শুরুতে ssthresh সাধারণত খুব বড় (কার্যত অসীম), তাই প্রথম loss পর্যন্ত slow start চলে; loss-এর পর ssthresh হয় তখনকার window-এর অর্ধেক।

প্রতিটি round = এক RTT; cwnd MSS-এর এককে
Round
শুরুতে cwnd
পাঠানো segment
প্রতি ACK-এ বৃদ্ধি
Round শেষে cwnd
ধাপ
  1. Round ১। cwnd = ১: একটি segment যায়। তার ACK এলে cwnd + ১ = ২.
  2. Round ২। দুটি segment, দুটি ACK — প্রতিটিতে +১, তাই cwnd = ৪. ACK-এর সংখ্যাই বৃদ্ধি ঠিক করে।
  3. Round ৩। চারটি segment, চারটি ACK → cwnd = ৮ = ssthresh. এখান থেকে নিয়ম বদলায়।
  4. Round ৪ — congestion avoidance. ৮টি segment, ৮টি ACK; প্রতিটিতে মাত্র ১/৮ MSS যোগ, পুরো round-এ +১ → ৯. (ভগ্নাংশ জমার কারণে আসল মান ৯-এর সামান্য কম; Linux একটি counter রেখে ঠিক cwnd-সংখ্যক ACK-এর পর পুরো ১ যোগ করে।)
  5. Round ৫। ৯টি segment, ৯টি ACK, প্রতিটিতে ১/৯ → ১০. এখন প্রতি RTT-তে মাত্র এক segment বেশি — সাবধানে সীমা খোঁজা।
  6. সারসংক্ষেপ। ৫ RTT-তে মোট ১ + ২ + ৪ + ৮ + ৯ = ২৪টি segment. cwnd ১০, MSS ১,৪৬০ আর RTT ১০০ ms হলে গতি ≈ ১৪,৬০০ × ৮ ÷ ০.১ = ১.১৬৮ Mbps.

শুরুর window — IW10

পুরোনো নিয়মে IW ছিল ১ segment, পরে RFC 3390 (২০০২) প্রায় ৪ KB পর্যন্ত (২–৪ segment) অনুমতি দেয়। Google-এর পরীক্ষার পর RFC 6928 (২০১৩, experimental) প্রস্তাব করে IW = min(১০·MSS, max(২·MSS, ১৪,৬০০ byte)) — সাধারণত ১০টি segment. Linux ২০১১ সালের দিকে (২.৬.৩৯ সংস্করণ) থেকে IW10 ব্যবহার করে; অন্য OS-এ ও CDN-এ মান আলাদা হতে পারে। ছোট ফাইলে এর প্রভাব বড়: ১,০০,০০০ byte-এর একটি ফাইল ≈ ৬৯টি segment. Loss না হলে আর rwnd বাধা না দিলে IW1-এ লাগে ৭ RTT (১, ৩, ৭, ১৫, ৩১, ৬৩, ১২৭ জমা), IW3-এ ৫ RTT, IW10-এ মাত্র ৩ RTT (১০, ৩০, ৭০)। ১০০ ms RTT-তে এটি ৪০০ ms-এর পার্থক্য — বেশিরভাগ ওয়েবপেজের ফাইল ছোট, তাই এই অংশই অনুভূত গতি ঠিক করে।

৫ · Loss-এর জবাব — fast retransmit, fast recovery আর timeout

TCP loss টের পায় দুইভাবে — বিস্তারিত পর্ব ৩০-এ (TCP-র নির্ভরযোগ্যতা)। এখানে প্রশ্ন শুধু: loss টের পেলে cwnd-এর কী হবে?

  • তিনটি duplicate ACK: প্রাপক একটি segment পায়নি, কিন্তু পরের segment-গুলো পাচ্ছে — তাই বারবার একই ACK নম্বর পাঠাচ্ছে। মানে, packet এখনো চলছে; জ্যাম হালকা. প্রেরক timer-এর অপেক্ষা না করে হারানোটি আবার পাঠায় — fast retransmit. তিনটির অপেক্ষা কারণ এক-দুটি duplicate ACK শুধু packet-এর ক্রম উল্টে যাওয়া (reordering) থেকেও আসতে পারে।
  • Timeout: RTO ফুরিয়ে গেল, কোনো খবরই নেই — হয়তো পুরো ঝাঁক হারিয়েছে। জ্যাম গুরুতর.
ঘটনাTCP Tahoe (১৯৮৮)TCP Reno (১৯৯০)
৩টি duplicate ACKssthresh = cwnd/2, cwnd = ১ MSS, slow startssthresh = cwnd/2, cwnd = ssthresh + ৩, fast recovery; নতুন ACK এলে cwnd = ssthresh, তারপর congestion avoidance
Timeoutssthresh = cwnd/2, cwnd = ১ MSS, slow startএকই — ssthresh = cwnd/2, cwnd = ১ MSS, slow start; RTO দ্বিগুণ (backoff)

RFC 5681-এর ভাষায় নতুন ssthresh = max(FlightSize/২, ২·MSS), যেখানে FlightSize = ACK-না-পাওয়া ডেটা। “+৩” কেন? তিনটি duplicate ACK মানে তিনটি segment নেটওয়ার্ক ছেড়ে প্রাপকের কাছে পৌঁছেছে — packet সংরক্ষণের হিসাবে তাদের জায়গা খালি। একই কারণে fast recovery-র সময় প্রতিটি বাড়তি duplicate ACK-এ cwnd +১ (inflation), যাতে ACK-এর ছন্দ না থামে।

cwnd = 16 MSS, ভাসমান #101–#116; #101 পথে হারাল
ঘটনা
cwnd (MSS)
ssthresh
প্রেরকের কাজ
ধাপ
  1. Loss. cwnd ১৬, segment #১০১–#১১৬ পথে। #১০১ একটি ভরা router-এ ফেলা হলো; বাকি ১৫টি পৌঁছাচ্ছে।
  2. প্রথম দুটি dupACK. প্রাপক #১০২, #১০৩ পেয়ে প্রতিবার বলে “ACK 101” — আমি এখনো #১০১ চাই। প্রেরক অপেক্ষা করে, কারণ এটি শুধু reordering হতে পারে।
  3. তৃতীয় dupACK: fast retransmit. প্রেরক #১০১ আবার পাঠায়। ssthresh = ১৬/২ = ৮, cwnd = ৮ + ৩ = ১১.
  4. Inflation. আরও ১২টি dupACK (মোট ১৫), প্রতিটিতে cwnd +১ → ২৩. ভাসমান এখনো ১৬, তাই ২৩ − ১৬ = ৭টি নতুন segment (#১১৭–#১২৩) যায় — ACK-এর ছন্দ বজায় থাকে।
  5. নতুন ACK: deflation. পুনরায় পাঠানো #১০১ পৌঁছাল; প্রাপকের কাছে #১১৬ পর্যন্ত সব আছে, তাই “ACK 117”. cwnd = ssthresh = ৮; ভাসমান ৭টি, তাই আর একটি (#১২৪) যায়। গতি অর্ধেক, কিন্তু চলা থামেনি।
  6. বিকল্প পথ — timeout. যদি পুরো ঝাঁক হারাত বা তিনটি dupACK না আসত, RTO ফুরাত। ssthresh = ৮, কিন্তু cwnd = ১; RTO দ্বিগুণ হয় যাতে বারবার timeout-এ নেটওয়ার্কে চাপ না বাড়ে।
  7. আবার slow start. ১ → ২ → ৪ → ৮: ৩ RTT লাগে ৮-এ ফিরতে, তার আগে RTO-র অপেক্ষা (Linux-এ ন্যূনতম প্রায় ২০০ ms, RFC 6298-এ ১ সেকেন্ড)। Tahoe তিনটি dupACK-এও এই পথেই যেত।

এক window-এ কয়েকটি segment হারালে মূল Reno একাধিকবার cwnd অর্ধেক করে বা timeout-এ পড়ে। NewReno (RFC 6582) “partial ACK” দেখে fast recovery-র ভেতরেই পরের ফাঁক পাঠায়, আর SACK ঠিক কোন কোন অংশ পৌঁছেছে জানিয়ে দেয় — আজকের Linux, Windows আর macOS সাধারণত SACK ব্যবহার করে। নিয়মের মূল কাঠামো (অর্ধেক করা, তারপর রৈখিক বৃদ্ধি) একই থাকে।

৬ · পরীক্ষার ক্লাসিক অঙ্ক — cwnd বনাম transmission round

বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় প্রায়ই একটি গ্রাফ দেওয়া থাকে: x-অক্ষে transmission round (প্রতিটি এক RTT), y-অক্ষে cwnd (segment-এ)। প্রশ্ন: কোথায় slow start, কোথায় congestion avoidance, কোন loss কোন ধরনের, আর ssthresh কত। নিচের গ্রাফটি একটি TCP Reno সংযোগের।

একটি TCP Reno সংযোগের cwnd, ২৪টি round

cwnd (segments) vs transmission round; dashed lines show ssthresh

cwnd round → 0 8 12 16 20 24 1 5 10 13 18 24 ssthresh 16 12 8 loss A loss B
ছবি ৩২.১ — নীল রেখা cwnd, কমলা ড্যাশ ssthresh, লাল বিন্দু loss. প্রতিটি round-এর মান: ১, ২, ৪, ৮, ১৬, ১৭ … ২৪ (round ১৩), ১২ … ১৬ (round ১৮), ১, ২, ৪, ৮, ৯, ১০.

পুরো সমাধান

  1. Slow start কোথায়? যেখানে cwnd দ্বিগুণ হয়: round ১–৫ (১ → ১৬) আর round ১৯–২২ (১ → ৮)। যে round-এ cwnd ssthresh ছোঁয় (৫ আর ২২), বইভেদে সেটিকে slow start-এর শেষ round বা congestion avoidance-এর প্রথম round ধরা হয় — উত্তরে নিজের নিয়মটি লিখে দিন।
  2. Congestion avoidance কোথায়? যেখানে প্রতি round-এ +১: round ৫–১৩, ১৪–১৮ আর ২২–২৪.
  3. শুরুর ssthresh? দ্বিগুণ হওয়া থামল ১৬-তে (৩২ হয়নি) — তাই ssthresh = ১৬.
  4. Loss A (round ১৩) কোন ধরনের? পরের round-এ cwnd ২৪ থেকে ১২ — অর্ধেক, ১ নয়। তাই তিনটি duplicate ACK, Reno-র fast recovery. নতুন ssthresh = ২৪/২ = ১২.
  5. Loss B (round ১৮) কোন ধরনের? cwnd ১৬ থেকে ১-এ — timeout. ssthresh = ১৬/২ = ৮; তাই round ২২-এ ৮ ছুঁয়ে আবার রৈখিক।
  6. Round ১৫ আর ২০-এ ssthresh? ১২ আর ৮.
  7. ৭০তম segment কোন round-এ যায়? জমা যোগফল: ১, ৩, ৭, ১৫, ৩১, ৪৮ (round ৬), ৬৬ (round ৭), ৮৫ (round ৮) — তাই round ৮.
  8. Round ২৪-এ তিনটি duplicate ACK হলে? cwnd ছিল ১০ → ssthresh = ৫, round ২৫-এ cwnd = ৫.
  9. এটি Tahoe হলে? Loss A-র পরেও cwnd ১-এ নামত — round ১৪-তে ১.

এক নিয়মে নজর রাখুন: অঙ্কের round-ভিত্তিক গ্রাফে fast recovery শেষে cwnd সাধারণত ssthresh ধরা হয় (এখানে ১২)। কিছু বইয়ের গ্রাফে ssthresh + ৩ (এখানে ১৫) দেখানো হয়, কারণ inflation-এর হিসাব ধরে। প্রশ্নে যে নিয়ম দেওয়া, সেটিই মানুন।

এবার একই অঙ্ক simulator-এ। Loss কোন round-এ কী ধরনের, তা আগে থেকে লেখা; প্রতিটি round-এ cwnd, এ পর্যন্ত মোট segment, পরের round-এর নিয়ম আর ssthresh ছাপা হয়, পাশে # দিয়ে ছোট একটি গ্রাফ। শেষে একই loss-এ Tahoe কী করত:

// TCP cwnd per transmission round (unit: MSS). Loss events are scripted.
const ROUNDS = 24, SSTHRESH0 = 16;
const loss = { 13: "3dup", 18: "timeout" };    // loss detected during this round
function run(flavour) {
  let cwnd = 1, ssthresh = SSTHRESH0, sent = 0;
  const rows = [];
  for (let r = 1; r <= ROUNDS; r++) {
    sent += cwnd;
    const ev = loss[r];
    if (ev) {                                    // both kinds: ssthresh = half the window
      ssthresh = Math.max(Math.floor(cwnd / 2), 2);
      rows.push([r, cwnd, sent, ev === "3dup" ? "3 dupACK" : "TIMEOUT", ssthresh]);
      cwnd = (ev === "3dup" && flavour === "Reno") ? ssthresh : 1;   // Reno: fast recovery
      continue;
    }
    rows.push([r, cwnd, sent, cwnd < ssthresh ? "x2 (slow start)" : "+1 (cong. avoid)", ssthresh]);
    cwnd = cwnd < ssthresh ? Math.min(2 * cwnd, ssthresh) : cwnd + 1;
  }
  return rows;
}
console.log(`TCP Reno, initial ssthresh = ${SSTHRESH0} MSS`);
console.log("round cwnd  sent-so-far  next step         ssthresh-after  plot");
for (const [r, w, s, rule, th] of run("Reno"))
  console.log(String(r).padStart(5), String(w).padStart(4), String(s).padStart(12),
    " " + rule.padEnd(17), String(th).padStart(14), " " + "#".repeat(w));
console.log("\nTCP Tahoe, same losses - cwnd per round:");
console.log(run("Tahoe").map(x => x[1]).join(" "));

আউটপুট:

TCP Reno, initial ssthresh = 16 MSS
round cwnd  sent-so-far  next step         ssthresh-after  plot
    1    1            1  x2 (slow start)               16  #
    2    2            3  x2 (slow start)               16  ##
    3    4            7  x2 (slow start)               16  ####
    4    8           15  x2 (slow start)               16  ########
    5   16           31  +1 (cong. avoid)              16  ################
    6   17           48  +1 (cong. avoid)              16  #################
    7   18           66  +1 (cong. avoid)              16  ##################
    8   19           85  +1 (cong. avoid)              16  ###################
    9   20          105  +1 (cong. avoid)              16  ####################
   10   21          126  +1 (cong. avoid)              16  #####################
   11   22          148  +1 (cong. avoid)              16  ######################
   12   23          171  +1 (cong. avoid)              16  #######################
   13   24          195  3 dupACK                      12  ########################
   14   12          207  +1 (cong. avoid)              12  ############
   15   13          220  +1 (cong. avoid)              12  #############
   16   14          234  +1 (cong. avoid)              12  ##############
   17   15          249  +1 (cong. avoid)              12  ###############
   18   16          265  TIMEOUT                        8  ################
   19    1          266  x2 (slow start)                8  #
   20    2          268  x2 (slow start)                8  ##
   21    4          272  x2 (slow start)                8  ####
   22    8          280  +1 (cong. avoid)               8  ########
   23    9          289  +1 (cong. avoid)               8  #########
   24   10          299  +1 (cong. avoid)               8  ##########

TCP Tahoe, same losses - cwnd per round:
1 2 4 8 16 17 18 19 20 21 22 23 24 1 2 4 8 12 1 2 4 6 7 8

গ্রাফের প্রতিটি মান মিলেছে। next step কলাম বলে এই round-এর পরে cwnd কোন নিয়মে বাড়বে: round ৫-এ cwnd ১৬ = ssthresh, তাই সেখান থেকে +1 — এ কারণেই slow start-এর শেষ round নিয়ে বইভেদে ভিন্ন লেখা। sent-so-far কলামে round ৭-এ ৬৬, round ৮-এ ৮৫ — ৭০তম segment round ৮-এ। Round ১৩-এর পর ssthresh ১২ আর cwnd ১২; round ১৮-এর timeout-এ ssthresh ৮, cwnd ১। ২৪ round-এ মোট ২৯৯টি segment.

Tahoe-র লাইন: round ১৪-তে ১, তারপর ২, ৪, ৮, আর round ১৮-তে ১২-তে থামে (min(১৬, ১২) — ssthresh ১২ পেরিয়ে দ্বিগুণ হয় না) — round ১৮-তে cwnd মাত্র ১২, তাই timeout-এর পর ssthresh ৬, শেষে ৬, ৭, ৮. একই loss-এ Tahoe কম পাঠায়। কোডে Math.min(2 * cwnd, ssthresh) বইয়ের সরল নিয়ম; আসল TCP প্রতিটি ACK ধরে বাড়ায়, তাই ssthresh পেরোনোর round-এ সামান্য ভগ্নাংশ বেশি হতে পারে। নিজে চেষ্টা করুন: loss-এ 24: "3dup" যোগ করে আর ROUNDS = 25 করে সমাধানের শেষ প্রশ্নের উত্তরের (round ২৫-এ cwnd ৫) সাথে মিলিয়ে দেখুন।

৭ · AIMD — করাতের দাঁত, ন্যায্যতা আর throughput-এর সূত্র

Loss বাদ দিলে স্থিতিশীল অবস্থায় Reno দুটি কাজই করে: প্রতি RTT-তে +১ (additive increase), loss-এ অর্ধেক (multiplicative decrease) — সংক্ষেপে AIMD. cwnd-এর গ্রাফ তাই করাতের দাঁতের মতো: W/২ থেকে W পর্যন্ত ঢালু ওঠা, তারপর হঠাৎ পতন।

কেন AIMD ন্যায্য — phase plot-এর যুক্তি

১৯৮৯ সালে Dah-Ming Chiu আর Raj Jain একটি সরল ছবিতে দেখান কেন এই নিয়মই ঠিক। ধরুন দুটি প্রবাহ A আর B একটি bottleneck ভাগ করে, যার ক্ষমতা C. x-অক্ষে A-র হার, y-অক্ষে B-র। দুটি রেখা গুরুত্বপূর্ণ:

  • দক্ষতার রেখা x + y = C: এর ওপরে জ্যাম (loss), নিচে link-এর ক্ষমতা অব্যবহৃত।
  • ন্যায্যতার রেখা x = y: দুজন সমান পায়। লক্ষ্য দুটি রেখার ছেদবিন্দু (C/২, C/২)।

এখন দুই নড়াচড়া দেখুন। Additive increase-এ দুজনেই +১, তাই বিন্দু ৪৫° কোণে সরে — ন্যায্যতার রেখার সমান্তরালে; পার্থক্য x − y একই থাকে। Multiplicative decrease-এ দুজনেই অর্ধেক, তাই বিন্দু মূলবিন্দুর দিকে সোজা রেখায় সরে — পার্থক্যও অর্ধেক হয়। প্রতিটি চক্রে পার্থক্য অর্ধেক: d, d/২, d/৪ … → ০. বিন্দু ঘুরতে ঘুরতে ছেদবিন্দুর কাছে দোল খায়।

অন্য নিয়ম কেন ব্যর্থ: AIAD-এ (যোগে বাড়া, বিয়োগে কমা) দুই নড়াচড়াই ৪৫°-এ, পার্থক্য কখনো কমে না। MIMD-এ দুটিই মূলবিন্দুর দিকের রেখায়, অনুপাত x/y কখনো বদলায় না। শুধু AIMD একসাথে দক্ষতা আর ন্যায্যতার দিকে যায়।

দুই প্রবাহের phase plot — A = ৪, B = ৫০ থেকে শুরু, C = ৬০

AIMD trajectory converges to the fair, efficient point

A-র হার → B-র হার দক্ষতা: A + B = C ন্যায্যতা: A = B শুরু (4, 50) লক্ষ্য (30, 30)
ছবি ৩২.২ — নীল পথ: ৪৫° কোণে ওঠা (additive increase), লাল রেখা পেরোলেই মূলবিন্দুর দিকে অর্ধেক (multiplicative decrease). প্রতিটি চক্রে ন্যায্যতার রেখা থেকে দূরত্ব অর্ধেক।

একই যুক্তি simulator-এ। দুটি প্রবাহ একটি bottleneck ভাগ করে, যার ক্ষমতা প্রতি RTT-তে ৬০ MSS; যোগফল ৬০ পেরোলে দুজনেই একই RTT-তে loss দেখে (synchronised loss — সরলীকরণ)। ন্যায্যতা মাপা হয় Jain's fairness index দিয়ে: (x + y)² ÷ (২(x² + y²)) — সমান হলে ১, একজন সব নিলে ০.৫.

// Two TCP flows share one bottleneck. Capacity = 60 MSS per RTT (sum of windows).
// Each RTT: both +1 (additive increase). If the sum exceeds capacity,
// both see a loss in the same RTT (synchronised) and halve (multiplicative decrease).
const CAP = 60;
const jain = (x, y) => (x + y) ** 2 / (2 * (x * x + y * y));   // 1 = perfectly fair
function simulate(decrease) {
  let a = 4, b = 50, losses = 0;
  const log = [];
  for (let rtt = 1; rtt <= 400; rtt++) {
    if (a + b > CAP) {
      losses++;
      if (losses <= 6) log.push(`  RTT ${String(rtt).padStart(3)}: loss at ${a.toFixed(1)} + ${b.toFixed(1)} = ` +
        `${(a + b).toFixed(1)}  gap at loss ${(b - a).toFixed(1)}  Jain ${jain(a, b).toFixed(3)}`);
      [a, b] = decrease(a, b);
    } else { a += 1; b += 1; }
  }
  log.push(`  after 400 RTTs: A = ${a.toFixed(1)}, B = ${b.toFixed(1)}, gap ${(b - a).toFixed(2)}, ` +
    `Jain ${jain(a, b).toFixed(3)}`);
  return log;
}
console.log(`start: A = 4, B = 50 (gap 46), Jain ${jain(4, 50).toFixed(3)}`);
console.log("AIMD (+1, x1/2):");
simulate((a, b) => [a / 2, b / 2]).forEach(l => console.log(l));
console.log("AIAD (+1, -4) for comparison:");
simulate((a, b) => [Math.max(a - 4, 1), Math.max(b - 4, 1)]).slice(-1).forEach(l => console.log(l));

আউটপুট:

start: A = 4, B = 50 (gap 46), Jain 0.579
AIMD (+1, x1/2):
  RTT   5: loss at 8.0 + 54.0 = 62.0  gap at loss 46.0  Jain 0.645
  RTT  21: loss at 19.0 + 42.0 = 61.0  gap at loss 23.0  Jain 0.876
  RTT  37: loss at 24.5 + 36.0 = 60.5  gap at loss 11.5  Jain 0.965
  RTT  53: loss at 27.3 + 33.0 = 60.3  gap at loss 5.8  Jain 0.991
  RTT  69: loss at 28.6 + 31.5 = 60.1  gap at loss 2.9  Jain 0.998
  RTT  85: loss at 29.3 + 30.8 = 60.1  gap at loss 1.4  Jain 0.999
  after 400 RTTs: A = 26.0, B = 26.0, gap 0.00, Jain 1.000
AIAD (+1, -4) for comparison:
  after 400 RTTs: A = 4.0, B = 50.0, gap 46.00, Jain 0.579

পার্থক্যের কলামটিই প্রমাণ: ৪৬ → ২৩ → ১১.৫ → ৫.৮ → ২.৯ → ১.৪ — প্রতিটি loss-এ ঠিক অর্ধেক, কারণ মাঝের additive increase পার্থক্য বদলায় না। Jain index ০.৫৭৯ থেকে ০.৯৯৯-এ উঠেছে। প্রতিটি চক্র ১৬ RTT: অর্ধেক করার পর যোগফল প্রায় ৩০, আর দুজন মিলে প্রতি RTT-তে +২ — ৩০ ফিরে পেতে ১৫–১৬ RTT. ৪০০ RTT পরে দুজনেই ২৬ (চক্রের মাঝপথ), পার্থক্য শূন্য। আর AIAD: ৪০০ RTT পরেও A = ৪, B = ৫০ — শুরুর অন্যায্যতা অটুট। নিজে চেষ্টা করুন: অর্ধেকের বদলে [a * 0.7, b * 0.7] (CUBIC-এর β) দিন — পার্থক্য প্রতিবার ৩০% কমবে, তাই মিলতে বেশি চক্র লাগবে।

Throughput-এর সূত্র — Mathis সূত্র

করাতের দাঁতে window W/২ থেকে W-তে ওঠে, প্রতি RTT-তে ১ — তাই একটি চক্রে W/২ RTT. গড় window ৩W/৪, তাই গড় throughput ≈ ০.৭৫ · W · MSS ÷ RTT — পরীক্ষার প্রিয় ফল। এবার loss-এর হার p যোগ করি:

  1. এক চক্রে পাঠানো segment = গড় window × RTT-র সংখ্যা = (৩W/৪) × (W/২) = ৩W²/৮.
  2. প্রতিটি চক্রে একটি loss, তাই p = ১ ÷ (৩W²/৮) → W = √(৮ ÷ ৩p).
  3. Throughput = (৩W/৪) · MSS ÷ RTT = (৩/৪)·√(৮/৩p) · MSS ÷ RTT = √(৩/২p) · MSS ÷ RTT.
Throughput ≈ 1.22 · MSS ÷ (RTT · √p)

এটি Mathis, Semke, Mahdavi আর Ott-এর ১৯৯৭ সালের মডেল — Mathis সূত্র. ১.২২ = √(৩/২) এসেছে প্রতিটি ACK একটি segment স্বীকার করে আর loss নিয়মিত ধরে নিয়ে; অনুমান বদলালে ধ্রুবকটিও বদলায় — যেমন delayed ACK-এ (প্রতি দুই segment-এ একটি ACK) √(৩/৪) ≈ ০.৮৭, আর এলোমেলো loss ধরলে প্রায় ১.৩১, আর timeout-এর প্রভাব ধরা আরও বিস্তারিত মডেল (Padhye ও অন্যান্য, ১৯৯৮) আছে। তাই এটিকে আনুমানিক ধরুন।

উদাহরণ: MSS = ১,৪৬০ byte = ১১,৬৮০ bit, RTT = ১০০ ms, p = ১০−৪ (প্রতি ১০,০০০-এ একটি loss). ১.২২৪৭ × ১১,৬৮০ ÷ (০.১ × ০.০১) ≈ ১৪.৩ Mbps. Link ১ Gbps হলেও এই সংযোগ এর বেশি পাবে না। p = ১০−২ হলে ১.৪৩ Mbps — loss ১০০ গুণ বাড়লে গতি ১০ গুণ কমে।

উচ্চ গতির সমস্যা: উল্টো হিসাব করুন — RTT ১০০ ms-এ ১০ Gbps চাইলে (MSS ১,৫০০ byte) লাগে p ≈ ২.২ × ১০−১০, অর্থাৎ প্রায় ৪৬০ কোটি segment-এ একটির বেশি loss নয়; window প্রায় ৮৩,৩৩৩ segment, আর একটি loss-এর পর অর্ধেক থেকে ফিরতে ৪১,৬৬৭ RTT ≈ ৪,১৬৭ সেকেন্ড — এক ঘণ্টার বেশি। এই হিসাব (RFC 3649-এ HighSpeed TCP-র প্রেরণা) থেকেই CUBIC-এর মতো নতুন algorithm-এর জন্ম (অংশ ৮)।

RTT-র অন্যায্যতা আর সমান্তরাল সংযোগ

সূত্রে RTT হর-এ। একই bottleneck আর একই p (১০−৪)-তে সূত্রের হিসাবে RTT ২০ ms-এর প্রবাহ প্রায় ৭১.৫ Mbps, আর ১০০ ms-এর প্রবাহ ১৪.৩ Mbps — মোটামুটি ৫ গুণ পার্থক্য। কারণ ছোট-RTT প্রবাহ প্রতি সেকেন্ডে বেশিবার +১ করে। ঢাকার কাছের সার্ভার থেকে ডাউনলোড আর দূর মহাদেশের সার্ভার থেকে ডাউনলোড একই লাইনে চললে, কাছেরটি বেশি ভাগ পায় — এটাই RTT unfairness.

TCP-র ন্যায্যতা আবার প্রতি সংযোগে, প্রতি ব্যবহারকারী বা প্রতি অ্যাপে নয়। ১০০ Mbps-এর একটি link-এ আগে থেকে ৯টি সংযোগ; নতুন অ্যাপ ১টি খুললে পায় প্রায় ১০০/১০ = ১০ Mbps, কিন্তু ১১টি খুললে ১১/২০ × ১০০ = ৫৫ Mbps. Download manager আর কিছু টুল এজন্যই একসাথে অনেক সংযোগ খোলে।

৮ · CUBIC, Vegas আর BBR — Reno-র পরের প্রজন্ম

CUBIC — সময়ের ঘড়িতে বাড়া

উচ্চ-BDP পথে Reno-র +১/RTT খুব ধীর (অংশ ৭-এর হিসাব)। CUBIC (Sangtae Ha, Injong Rhee আর Lisong Xu, ২০০৮) window-কে শেষ loss থেকে কেটে যাওয়া সময় t-এর ফাংশন বানায়, RTT-র সংখ্যার নয়:

W(t) = C·(t − K)³ + Wmax, K = ∛(Wmax·(1 − β) ÷ C)
  • Wmax = শেষ loss-এর মুহূর্তের window; loss-এ window নামে β·Wmax-এ, যেখানে β = ০.৭ (৩০% কমানো, Reno-র ৫০% নয়)। ধ্রুবক C = ০.৪.
  • t = ০-এ W = C·(−K)³ + Wmax = Wmax − Wmax(১ − β) = β·Wmax. ঠিক K সেকেন্ড পরে W আবার Wmax.
  • বক্ররেখার আকার: শুরুতে দ্রুত ওঠা (concave), Wmax-এর কাছে প্রায় সমতল — যেখানে আগে loss হয়েছিল সেখানে সাবধানে থাকা — তারপর Wmax পেরিয়ে আবার দ্রুত ওঠা (convex), নতুন ক্ষমতা খোঁজা।

Wmax = ১,০০০ MSS হলে K = ∛(১,০০০ × ০.৩ ÷ ০.৪) = ∛৭৫০ ≈ ৯.০৯ সেকেন্ড — RTT যা-ই হোক। Reno-র লাগে ৫০০ RTT. নিচের হিসাব দুটিকে পাশাপাশি রাখে:

// CUBIC after a loss (RFC 9438): W(t) = C*(t - K)^3 + Wmax,  K = cbrt(Wmax*(1 - beta)/C)
// Reno after the same loss: starts at Wmax/2, grows +1 MSS every RTT.
const C = 0.4, beta = 0.7, Wmax = 1000;            // window unit: MSS
const RTT = 0.1;                                    // seconds
const K = Math.cbrt(Wmax * (1 - beta) / C);
const cubic = t => C * (t - K) ** 3 + Wmax;
const reno = t => Wmax / 2 + t / RTT;
console.log(`Wmax = ${Wmax} MSS, RTT = ${RTT * 1000} ms, K = ${K.toFixed(2)} s`);
console.log("t(s)   CUBIC  Reno");
for (const t of [0, 1, 2, 4, 6, 8, K, 10, 12, 15, 20])
  console.log(t.toFixed(t === K ? 2 : 0).padStart(5), cubic(t).toFixed(0).padStart(6),
    reno(t).toFixed(0).padStart(5));
console.log("\ntime to climb back to Wmax:");
for (const rtt of [0.01, 0.1, 0.2])
  console.log(`  RTT ${String(rtt * 1000).padStart(3)} ms: CUBIC ${K.toFixed(1)} s, ` +
    `Reno ${(Wmax / 2 * rtt).toFixed(0)} s (${Wmax / 2} RTTs)`);
const MSS = 1460 * 8;                               // bits
console.log(`\nat Wmax: rate = Wmax*MSS/RTT = ${(Wmax * MSS / RTT / 1e6).toFixed(1)} Mbps`);

আউটপুট:

Wmax = 1000 MSS, RTT = 100 ms, K = 9.09 s
t(s)   CUBIC  Reno
    0    700   500
    1    789   510
    2    858   520
    4    947   540
    6    988   560
    8    999   580
 9.09   1000   591
   10   1000   600
   12   1010   620
   15   1083   650
   20   1520   700

time to climb back to Wmax:
  RTT  10 ms: CUBIC 9.1 s, Reno 5 s (500 RTTs)
  RTT 100 ms: CUBIC 9.1 s, Reno 50 s (500 RTTs)
  RTT 200 ms: CUBIC 9.1 s, Reno 100 s (500 RTTs)

at Wmax: rate = Wmax*MSS/RTT = 116.8 Mbps

Loss-এর পরেই CUBIC ৭০০ (β = ০.৭), Reno ৫০০. প্রথম ৪ সেকেন্ডে CUBIC ৭০০ → ৯৪৭ দ্রুত ওঠে, তারপর ৬–১০ সেকেন্ডে ৯৮৮, ৯৯৯, ১,০০০, ১,০০০ — পুরোনো loss-এর জায়গায় প্রায় থেমে থাকা। ১২ সেকেন্ডের পর আবার দ্রুত: ১৫-তে ১,০৮৩, ২০-এ ১,৫২০ (বাস্তবে তার আগেই আরেকটি loss হতো)। Reno তখনো মাত্র ৭০০-তে; Wmax-এ ফিরতে তার লাগে ৫০ সেকেন্ড, যে সময়ে ১১৬.৮ Mbps-এর সম্ভাব্য গতির বড় অংশ নষ্ট।

RTT-স্বাধীনতা: শেষ তিন লাইনে CUBIC সব RTT-তে ৯.১ সেকেন্ড, Reno RTT-র অনুপাতে ৫ থেকে ১০০ সেকেন্ড। তাই CUBIC-এ অংশ ৭-এর RTT unfairness অনেক কম। আর ১০ ms-এ Reno CUBIC-এর চেয়ে দ্রুত — এমন ছোট-RTT পথে আসল CUBIC একটি “Reno-friendly” অনুমানও চালায় আর দুটির বড়টি নেয়, যাতে Reno-র চেয়ে খারাপ না করে।

CUBIC Linux-এ ২.৬.১৯ সংস্করণ (২০০৬-এর দিকে) থেকে ডিফল্ট, তার আগে ছিল BIC; IETF মানদণ্ড RFC 8312 (২০১৮), হালনাগাদ RFC 9438 (২০২৩)। Windows-এর নতুন সংস্করণ আর Apple-এর OS-ও এখন সাধারণত CUBIC ব্যবহার করে। ইন্টারনেটের TCP traffic-এর বড় অংশ তাই CUBIC — Reno আজ মূলত পাঠ্যবইয়ের মডেল।

Delay-ভিত্তিক ধারণা — Vegas

Loss-ভিত্তিক algorithm জ্যাম টের পায় buffer উপচে পড়ার পরে। TCP Vegas (Brakmo আর Peterson, ১৯৯৪) আগেই টের পেতে চেয়েছিল: প্রত্যাশিত হার = cwnd ÷ BaseRTT (দেখা ন্যূনতম RTT), আসল হার = cwnd ÷ RTT. পার্থক্য × BaseRTT = queue-তে জমা আমার packet. উদাহরণ: cwnd ৩০, BaseRTT ১০০ ms, RTT ১২০ ms → (৩০০ − ২৫০) × ০.১ = ৫টি packet queue-তে। দুটি ছোট সীমা α আর β (কয়েকটি packet, যেমন ২ আর ৪) ধরে Vegas α-র কম হলে বাড়ায়, β-র বেশি হলে কমায় — এখানে ৫ > β, তাই কমায়। সমস্যা: loss-ভিত্তিক প্রবাহের পাশে Vegas আগে পিছিয়ে যায়, তাই ইন্টারনেটে ব্যাপকভাবে চলেনি। তবে “queue বাড়ছে = RTT বাড়ছে” ধারণাটি BBR, LEDBAT (ব্যাকগ্রাউন্ড আপডেট) আর অনেক আধুনিক algorithm-এ টিকে আছে।

BBR — মডেল দিয়ে চলা

Google-এর BBR (Neal Cardwell ও সহকর্মী, ২০১৬; Linux ৪.৯ থেকে) প্রশ্নটিই বদলে দেয়: loss-এর অপেক্ষা না করে পথের দুটি সংখ্যা মেপে নেওয়া। BtlBw — bottleneck-এর হার, সাম্প্রতিক প্রায় ১০ RTT-র delivery rate নমুনার সর্বোচ্চ; আর RTprop — queue-ছাড়া RTT, সাম্প্রতিক প্রায় ১০ সেকেন্ডের RTT নমুনার সর্বনিম্ন. আদর্শ চলার বিন্দু: হার = BtlBw, ভাসমান ডেটা = BtlBw × RTprop = BDP — link ভরা, queue খালি। BBR pacing করে pacing_gain × BtlBw হারে, আর cwnd ≈ ২ × BDP রাখে বাড়তি সীমা হিসেবে। চার ধাপ: Startup, Drain, ProbeBW, ProbeRTT.

BBR-এর দুই অনুমান আর তা থেকে হার ও cwnd
ধাপ
Delivery-rate নমুনা (packet/s)
BtlBw (সর্বোচ্চ ফিল্টার)
RTT নমুনা → RTprop
BDP = BtlBw × RTprop
Pacing gain → হার
  1. শুরু। Handshake থেকে প্রথম RTT নমুনা ৪০ ms. BtlBw অজানা, তাই Startup-এ pacing gain ২/ln ২ ≈ ২.৮৯ — প্রতি round-এ হার প্রায় দ্বিগুণ খোঁজা, slow start-এর মতো।
  2. Startup. Delivery rate ২৫০ → ৫০০ → ১,০০০ packet/s. BtlBw = সর্বোচ্চ = ১,০০০; BDP = ১,০০০ × ০.০৪ = ৪০ packet.
  3. পাইপ ভরা। পরের তিন round-এ হার ২৫%-ও বাড়েনি (১,০০০, ১,০০০, ১,০০০) — bottleneck পাওয়া গেছে। কিন্তু ২.৮৯ গুণে পাঠানোয় ভাসমান ≈ ২.৮৯ × ৪০ ≈ ১১৬, তার ৭৬টি queue-তে; RTT নমুনা ≈ ১১৬ ms. সর্বনিম্ন ফিল্টারের কারণে RTprop তবু ৪০ — queue-এর delay মডেলকে বিভ্রান্ত করে না।
  4. Drain. Gain ১/২.৮৯ ≈ ০.৩৫: প্রায় ৩৪৬ packet/s-এ পাঠানো, অথচ bottleneck বের করে ১,০০০ — queue প্রতি সেকেন্ডে ~৬৫৪ করে খালি হয়। ভাসমান ≤ BDP (৪০) হলে Drain শেষ।
  5. ProbeBW. বেশিরভাগ সময় এখানে: আট ধাপের চক্র — এক RTprop ১.২৫ গুণে (১,২৫০/s; বাড়তি ক্ষমতা আছে কি না পরীক্ষা, queue-তে ~১০ packet, RTT ~৫০), পরেরটি ০.৭৫ গুণে (সেই queue খালি), তারপর ছয়টি ১.০. Delivery ১,০০০-ই থাকল — নতুন ক্ষমতা নেই। cwnd = ২ × ৪০ = ৮০.
  6. ProbeRTT. প্রায় ১০ সেকেন্ড RTprop-এর নতুন সর্বনিম্ন না এলে cwnd মাত্র ৪ packet-এ নামে, অন্তত ২০০ ms (আর এক round) — queue খালি করে আসল ৪০ ms আবার মাপা। তারপর ProbeBW-তে ফেরা।
  7. পথ বদলাল। আরেকটি প্রবাহ এসে অর্ধেক নিল: নমুনা ৫০০. কিন্তু সর্বোচ্চ ফিল্টার প্রায় ১০ round পুরোনো ১,০০০ মনে রাখে — ততক্ষণ BBR বেশি পাঠায়, queue জমে। তারপর BtlBw = ৫০০, BDP = ২০. এই ধীর প্রতিক্রিয়াই BBR-এর ন্যায্যতা-বিতর্কের একটি উৎস।

বিতর্ক: BBR v1 loss-কে প্রায় উপেক্ষা করত। গবেষণায় দেখা গেছে, ছোট buffer-এর bottleneck-এ এটি অনেক retransmission ঘটাতে পারে আর CUBIC-এর চেয়ে অনেক বেশি ভাগ নিতে পারে; আবার বড় buffer-এ উল্টোটাও দেখা গেছে — ফল পরিস্থিতিভেদে আলাদা। BBR v2 আর v3 (২০১৯–২০২৩) loss আর ECN-কেও সংকেত হিসেবে নেয় আর ভাসমান ডেটার একটি ওপরের সীমা রাখে, যাতে CUBIC-এর পাশে আচরণ ভালো হয়; তবে এ নিয়ে মাপজোখ ও আলোচনা চলমান। Google জানিয়েছে, তাদের নিজেদের সার্ভিসে BBR throughput আর latency-তে উন্নতি এনেছে; QUIC-এর ভেতরেও BBR বা CUBIC বাছাই করা যায় (পর্ব ৩৩, QUIC).

৯ · ECN, DCTCP, L4S আর Linux-এ নিজে দেখা

Loss-ভিত্তিক TCP আর bufferbloat

Reno আর CUBIC loss না পাওয়া পর্যন্ত window বাড়ায় — অর্থাৎ bottleneck-এর buffer ভরে না যাওয়া পর্যন্ত। Buffer বড় হলে loss আসে দেরিতে, আর ততক্ষণ queue-তে শত শত ms জমে: এটাই bufferbloat (পর্ব ১৫)। বাসায় কেউ বড় ফাইল upload করলে ভিডিও কল আটকে যাওয়ার পেছনে প্রায়ই এই ঘটনা। প্রতিকার দুই দিক থেকে: router-এ AQM (CoDel, fq_codel) queue ছোট রাখে, আর প্রেরকের দিকে BBR-এর মতো algorithm queue না ভরেই চলতে চায়।

ECN — ফেলার বদলে দাগ

ECN চালু থাকলে (দুই প্রান্ত handshake-এ রাজি হলে) AQM packet না ফেলে CE দাগ দেয়। প্রাপক ACK-এ ECE flag দেয়, আর প্রেরক এক RTT-তে একবার loss-এর মতো প্রতিক্রিয়া করে (Reno-তে অর্ধেক), তারপর CWR পাঠায়। লাভ: retransmission নেই, timeout-এর ঝুঁকি নেই। Linux-এ ডিফল্টে tcp_ecn = 2 — অন্য পক্ষ চাইলে ECN মেনে নেয়, নিজে চায় না; কারণ পুরোনো কিছু middlebox ECN-যুক্ত packet ফেলে দিত। Apple-এর OS-এর মতো কিছু সিস্টেম নিজে থেকেও চায়।

DCTCP — data center-এর জন্য সূক্ষ্ম কমানো

Data center-এ RTT মাইক্রোসেকেন্ডে, switch-এর buffer ছোট, আর অনেক সার্ভার একসাথে একজনকে উত্তর দেয় (incast). DCTCP (Alizadeh ও সহকর্মী, ২০১০; RFC 8257) চালাকি করে: switch queue একটি ছোট সীমা K পেরোলেই দাগ দেয়, আর প্রেরক মাপে দাগের অনুপাত α (চলমান গড়: α ← (১ − g)·α + g·F, g = ১/১৬)। প্রতি RTT-তে cwnd ← cwnd × (১ − α/২). অল্প দাগ (α = ০.১) → মাত্র ৫% কমানো; সব দাগ (α = ১) → অর্ধেক, Reno-র মতো। ফল: queue খুব ছোট, throughput বেশি। কিন্তু এই আগ্রাসী দাগের নিয়ম সাধারণ ইন্টারনেটে Reno বা CUBIC-এর পাশে চালানো যায় না — তাই শুধু নিজের নিয়ন্ত্রিত নেটওয়ার্কে।

L4S — সংক্ষেপে

L4S (RFC 9330–9332, ২০২৩) DCTCP-র ধারণা ইন্টারনেটে আনার IETF-এর চেষ্টা: ECN-এর ECT(1) মান দিয়ে চিহ্নিত “scalable” প্রবাহ (যেমন TCP Prague) একটি আলাদা ছোট queue-তে যায়, আর পুরোনো প্রবাহ থাকে সাধারণ queue-তে — দুটির মধ্যে সমন্বয় করে DualQ Coupled AQM. লক্ষ্য প্রায় শূন্য queuing delay-তে পূর্ণ throughput. কিছু ISP আর যন্ত্র-নির্মাতা পরীক্ষামূলকভাবে চালু করছে বলে জানা যায়; কতটা ছড়াবে, তা এখনো দেখার বিষয়।

Linux-এ — কোন algorithm চলছে, আর বদলানো

Linux-এ congestion control আলাদা আলাদা module; প্রতিটি socket নিজের algorithm বেছে নিতে পারে। sysctl-এর তিনটি ফাইল (/proc/sys-এর নিচে) বলে দেয়: নতুন socket-এর ডিফল্ট, kernel-এ লোড করা algorithm, আর সাধারণ ব্যবহারকারী কোনগুলো বাছতে পারে। নিচের C প্রোগ্রাম ফাইল তিনটি পড়ে, তারপর একটি TCP socket-এ getsockopt(TCP_CONGESTION) দিয়ে নাম পড়ে আর setsockopt() দিয়ে পাঁচটি algorithm বসানোর চেষ্টা করে — কোনটি সফল, কোনটি কেন নয়:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>

static void show(const char *file) {              /* print one line of a sysctl file */
    char path[128], buf[256] = "(cannot read)\n";
    snprintf(path, sizeof path, "/proc/sys/net/ipv4/%s", file);
    FILE *f = fopen(path, "r");
    if (f) { if (!fgets(buf, sizeof buf, f)) strcpy(buf, "(empty)\n"); fclose(f); }
    printf("%-34s %s", file, buf);
}
int main(void) {
    show("tcp_congestion_control");               /* default for new sockets */
    show("tcp_available_congestion_control");     /* algorithms loaded in the kernel */
    show("tcp_allowed_congestion_control");       /* what a normal user may choose */
    int s = socket(AF_INET, SOCK_STREAM, 0);
    char name[16] = ""; socklen_t len = sizeof name - 1;
    getsockopt(s, IPPROTO_TCP, TCP_CONGESTION, name, &len);
    printf("\nnew TCP socket starts with: %s\n", name);
    const char *want[] = {"reno", "cubic", "bbr", "vegas", "dctcp"};
    for (int i = 0; i < 5; i++) {
        int r = setsockopt(s, IPPROTO_TCP, TCP_CONGESTION, want[i], strlen(want[i]));
        const char *why = r == 0 ? "OK" : strerror(errno);
        memset(name, 0, sizeof name); len = sizeof name - 1;
        getsockopt(s, IPPROTO_TCP, TCP_CONGESTION, name, &len);
        printf("set %-6s -> %-26s socket now uses: %s\n", want[i], why, name);
    }
    close(s);
    return 0;
}

আসল রানের আউটপুট:

tcp_congestion_control             bbr
tcp_available_congestion_control   reno cubic bbr
tcp_allowed_congestion_control     reno cubic bbr

new TCP socket starts with: bbr
set reno   -> OK                         socket now uses: reno
set cubic  -> OK                         socket now uses: cubic
set bbr    -> OK                         socket now uses: bbr
set vegas  -> No such file or directory  socket now uses: bbr
set dctcp  -> No such file or directory  socket now uses: bbr

এই কোড যে cloud যন্ত্রে চলেছে, তার মালিক ডিফল্ট হিসেবে bbr বেছেছেন; একটি সাধারণ Linux ডেস্কটপে সাধারণত দেখবেন cubic. Kernel-এ লোড আছে তিনটি — reno, cubic, bbr — আর তিনটিই সাধারণ ব্যবহারকারীর জন্য অনুমোদিত, তাই root ছাড়াই একটি socket-এ তিনটি বসানো গেল; প্রতিবার getsockopt নতুন নাম নিশ্চিত করেছে। vegas আর dctcp-এ ENOENT (“No such file or directory”) — এদের module লোড করা নেই, আর সাধারণ ব্যবহারকারীর অনুরোধে kernel module লোড করে না (তার জন্য CAP_NET_ADMIN লাগে)। ব্যর্থ চেষ্টার পর socket আগের algorithm-এই (bbr) থেকে গেছে। লক্ষ করুন, এটি প্রেরকের সিদ্ধান্ত — প্রাপক কোন algorithm চালায়, তাতে আপনার পাঠানোর গতি নির্ভর করে না।

নিজের Linux যন্ত্রে (root লাগে এমন কমান্ড শুধু নিজের যন্ত্রে বা ল্যাবে) একই কাজ কমান্ডে, আর একটি চলমান সংযোগের cwnd দেখা:

$ sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = cubic
$ sudo modprobe tcp_bbr                                  # module লোড
$ sudo sysctl -w net.ipv4.tcp_congestion_control=bbr     # নতুন সংযোগের ডিফল্ট
$ echo 'net.ipv4.tcp_congestion_control=bbr' | sudo tee /etc/sysctl.d/90-cc.conf   # reboot-এর পরেও
$ ss -tin state established '( dport = :443 )'
Recv-Q Send-Q  Local Address:Port    Peer Address:Port
0      0       192.168.0.105:51234   203.0.113.10:443
	 cubic wscale:7,7 rto:232 rtt:31.2/4.8 mss:1448 pmtu:1500 cwnd:10 ssthresh:7 send 3.7Mbps

ss -i-এর লাইনে সবই এই পর্বের ভাষা: algorithm cubic, cwnd ১০ segment, ssthresh ৭ (অর্থাৎ slow start আগেই শেষ হয়েছে — loss-এর কারণে, বা CUBIC-এর HyStart আগেভাগে থামানোয়), RTT ৩১.২ ms, আর send 3.7Mbps = ১০ × ১,৪৪৮ × ৮ ÷ ০.০৩১২ — ঠিক cwnd ÷ RTT. Windows-এ PowerShell-এ Get-NetTCPSetting | Select SettingName, CongestionProvider দেখায় কোন প্রোফাইলে কোন algorithm (নতুন সংস্করণে সাধারণত CUBIC).

আরও গভীরে: আধুনিক Linux fast recovery-তে পাঠ্যবইয়ের “+৩ আর inflation”-এর বদলে Proportional Rate Reduction (PRR, RFC 6937) ব্যবহার করে, যা recovery-র সময় পাঠানোর হার মসৃণভাবে নতুন ssthresh-এ নামায়; loss ধরার জন্য তিন dupACK-এর বদলে সময়-ভিত্তিক RACK-TLP (RFC 8985) চলে। CUBIC-এর সাথে থাকে HyStart, যা RTT বাড়তে দেখলে slow start আগেভাগে থামায়। প্রতিটি algorithm kernel-এ একটি tcp_congestion_ops কাঠামো (cong_avoid, ssthresh ইত্যাদি function) — আর নতুন kernel-এ eBPF দিয়েও algorithm লেখা যায়।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Flow control আর congestion control-এর পার্থক্য? — Flow control প্রাপকের buffer বাঁচায় (rwnd, প্রাপক জানায়); congestion control নেটওয়ার্ক বাঁচায় (cwnd, প্রেরক অনুমান করে); পাঠানো যায় min(rwnd, cwnd).
  • Slow start-কে “slow” বলা হয় কেন, অথচ বৃদ্ধি exponential? — পুরোনো TCP শুরুতেই পুরো window ছাড়ত; ১ (বা IW) থেকে শুরু করা তার তুলনায় ধীর, তারপর প্রতি RTT-তে দ্বিগুণ।
  • তিন dupACK আর timeout-এ Reno-র আচরণ আলাদা কেন? — dupACK মানে packet এখনো পৌঁছাচ্ছে (হালকা জ্যাম) → অর্ধেক; timeout মানে কিছুই পৌঁছাচ্ছে না (গুরুতর) → ১ MSS থেকে slow start.
  • AIMD কেন ন্যায্য? — Additive increase পার্থক্য বদলায় না, multiplicative decrease পার্থক্য অর্ধেক করে, তাই প্রতি চক্রে প্রবাহগুলো সমান ভাগের দিকে যায়।
  • CUBIC আর BBR-এর মূল পার্থক্য? — CUBIC loss-ভিত্তিক, loss থেকে সময়ের cubic ফাংশনে window বাড়ায়; BBR মডেল-ভিত্তিক, bottleneck-এর হার আর ন্যূনতম RTT মেপে সেই হারে pacing করে।

১০ · AI-এর যুগে congestion control

“আমার সার্ভার ধীর, TCP tune করে দাও” — এমন প্রশ্নে AI প্রায়ই একগাদা sysctl ধরিয়ে দেয়। কিছু ভুল খুব প্রচলিত:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
cwnd-গ্রাফে timeout-এর পর cwnd = ssthresh, বা ssthresh = cwnd − ১Timeout-এ cwnd = ১ MSS, ssthresh = cwnd/২অংশ ৬-এর simulator-এ একই loss দিয়ে মেলান
“BBR চালু করলেই সব দ্রুত হবে”শুধু প্রেরকের দিকে কাজ করে; পথ, buffer আর অন্য প্রবাহভেদে ফল আলাদা, অন্যের ক্ষতিও হতে পারেআগে-পরে throughput, RTT আর retransmission মেপে তুলনা
tcp_tw_recycle-এর মতো সরানো সেটিং, বা netstat-এ cwnd খোঁজাপুরোনো kernel-এর তথ্য; cwnd দেখায় ss -tiনিজের kernel-এর ডকুমেন্টেশন (ip-sysctl) আর man ss
Mathis সূত্রে √p ভুলে p দিয়ে ভাগ, বা bit-byte গুলিয়ে ফেলাউত্তর শত-হাজার গুণ ভুল হয়নিজে ক্যালকুলেটরে একক ধরে ধরে হিসাব
বানানো RFC নম্বর (“CUBIC RFC 5681”)RFC 5681 Reno-ধাঁচের মূল নিয়ম; CUBIC RFC 8312, হালনাগাদ 9438rfc-editor.org-এ নম্বর খুলে শিরোনাম মেলান
“Congestion হলে router প্রেরককে থামতে বলে”সাধারণ ইন্টারনেটে router কিছু বলে না — loss বা ECN দাগই সংকেতঅংশ ২-এর দুই পথ; capture-এ ECE/CWR flag খুঁজুন

AI-কে congestion control-এর প্রশ্ন দিলে নিজের পরিস্থিতি আর যাচাইয়ের শর্তসহ দিন:

আমার Linux সার্ভার থেকে বিদেশের ব্যবহারকারীদের (RTT প্রায় ২০০ ms) ডাউনলোড ধীর। ss -ti-তে cwnd, ssthresh, rtt আর retrans-এর মান দিচ্ছি: [এখানে আউটপুট]। ১. এই মানগুলো দেখে বলো সমস্যা congestion, flow control (rwnd), না অ্যাপ্লিকেশনের — প্রতিটি সিদ্ধান্তের কারণ দেখাও। ২. cwnd ÷ RTT দিয়ে বর্তমান গতি হিসাব করে দেখাও, একক লিখে লিখে। ৩. CUBIC থেকে BBR-এ গেলে কী লাভ আর ঝুঁকি, বিশেষত অন্য প্রবাহের ওপর প্রভাব — নিশ্চিত না হলে স্পষ্ট বলো। ৪. প্রতিটি sysctl-এর জন্য বলো কোন kernel সংস্করণে আছে আর কোন ডকুমেন্টেশনে যাচাই করব; সরানো বা পুরোনো সেটিং দেবে না। ৫. বানানো RFC নম্বর দেবে না; না জানলে “জানি না” বলো।

AI-এর উত্তর মানার আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১১৯৮৬-এর congestion collapse-এর সমাধান router-এ না করে প্রতিটি কম্পিউটারের TCP-তে করা হলো কেন? এই সিদ্ধান্তের দাম কী?

১৯৮৬ সালে ইন্টারনেট ছিল কয়েক হাজার host-এর একটি গবেষণা নেটওয়ার্ক, কিন্তু router-গুলো নানা প্রতিষ্ঠানের, নানা নির্মাতার। Router-এ সমাধান মানে প্রতিটি router-কে প্রতিটি প্রবাহের হিসাব রাখতে হতো আর পাশের router-দের সাথে নতুন protocol-এ কথা বলতে হতো — সব নির্মাতাকে রাজি করানো, সব যন্ত্র বদলানো। অন্যদিকে TCP চলত host-এর OS-এ, আর BSD Unix-এর একটি সংস্করণ বদলালেই বিশ্ববিদ্যালয়গুলোর বড় অংশে পৌঁছানো যেত। Van Jacobson আর Michael Karels ঠিক তা-ই করলেন: ১৯৮৮-এর BSD Tahoe-তে slow start, congestion avoidance আর ভালো timer — কোনো router না ছুঁয়ে।

এটি end-to-end নীতির (পর্ব ০৩) একটি ক্লাসিক প্রয়োগ: নেটওয়ার্কের মাঝখান সরল থাকুক, বুদ্ধি থাকুক প্রান্তে। যুক্তিটি ছিল এমন — router-এর একমাত্র সংকেত packet ফেলা, যা সে ভিড়ে এমনিতেই করে; প্রেরক loss-কে জ্যামের চিহ্ন ধরে নিলেই হয়। তারযুক্ত নেটওয়ার্কে বিট-ত্রুটি বিরল ছিল, তাই অনুমানটি কাজ করেছিল।

দাম অনেক দিক থেকে এসেছে। প্রথমত, এটি সহযোগিতার ওপর দাঁড়ানো ব্যবস্থা: যে প্রেরক নিয়ম মানে না (UDP-তে বেপরোয়া পাঠানো, বা নিজের সুবিধায় বদলানো TCP), সে বেশি ভাগ পায় — নেটওয়ার্ক তাকে থামায় না। দ্বিতীয়ত, ন্যায্যতা প্রতি সংযোগে, তাই অনেক সংযোগ খোলা অ্যাপ বেশি পায়। তৃতীয়ত, loss-ই সংকেত হওয়ায় buffer ভরে না যাওয়া পর্যন্ত প্রেরক থামে না — bufferbloat এর সরাসরি ফল। আর বেতার link-এ loss জ্যাম ছাড়াও হয়, তখন TCP অযথা গতি কমায়। পরের দশকগুলোর RED, ECN, AQM, fq_codel — সবই নেটওয়ার্ককে আবার একটু সাহায্যের ভূমিকায় ফেরানোর চেষ্টা, তবে মূল দায় এখনো প্রান্তে।

মূল উপলব্ধি: যে সমাধান তখনকার বাস্তবতায় চালু করা যায়, সেটিই জেতে। End-to-end congestion control দ্রুত ছড়িয়ে ইন্টারনেট বাঁচিয়েছিল, কিন্তু তার সীমাবদ্ধতা — সহযোগিতার ওপর নির্ভরতা আর loss-কে একমাত্র সংকেত ধরা — আজও গবেষণার বিষয়।

প্র ০২AIMD-তে কমানো “গুণ” করে আর বাড়ানো “যোগ” করে — উল্টোটা হলে কী হতো? CUBIC কেন অর্ধেকের বদলে ৩০% কমায়?

Chiu আর Jain-এর ১৯৮৯ সালের বিশ্লেষণ চারটি সম্ভাবনা তুলনা করেছিল: AIAD, AIMD, MIAD, MIMD. Phase plot-এ (অংশ ৭) যোগ করা মানে ৪৫° কোণে নড়া, যা দুই প্রবাহের পার্থক্য বদলায় না; গুণ করা মানে মূলবিন্দুর দিকে নড়া, যা অনুপাত বদলায় না কিন্তু পার্থক্য বদলায়। ন্যায্যতায় পৌঁছাতে পার্থক্য কমাতে হবে, আর তা করে শুধু গুণ করে কমানো. গুণ করে বাড়ালে (MI) পার্থক্য উল্টো বাড়ে — বড় প্রবাহ আরও বড় হয়। তাই একমাত্র AIMD-ই দক্ষতা আর ন্যায্যতা দুটোর দিকে একসাথে যায়।

স্থিতিশীলতারও একটি যুক্তি আছে। জ্যামে queue-র delay দ্রুত বাড়ে (ρ/(১ − ρ)-এর বক্ররেখা), তাই সংকেত পেলে দ্রুত সরে আসা দরকার — অর্ধেক করলে কয়েক RTT-তেই চাপ কমে। আবার বাড়ানো সাবধানে, যাতে একসাথে অনেক প্রবাহ বাড়লেও ঝাঁপিয়ে জ্যাম না হয়। Jacobson এটিকে নিয়ন্ত্রণ-তত্ত্বের ভাষায় দেখেছিলেন: ব্যবস্থাটি যেন নিজে থেকে ভারসাম্যে ফেরে।

কিন্তু “অর্ধেক” একটি বেছে নেওয়া সংখ্যা, প্রকৃতির নিয়ম নয়। বড় window-তে অর্ধেক করা মানে বিশাল ক্ষমতা হারানো, ফিরতে বহু RTT. CUBIC তাই β = ০.৭ নিয়েছে — কম কমানো, দ্রুত ফেরা। দাম: অংশ ৭-এর simulator-এ দেখেছেন, ০.৭ গুণে পার্থক্য প্রতি চক্রে মাত্র ৩০% কমে, ফলে ন্যায্যতায় পৌঁছাতে বেশি চক্র লাগে। CUBIC এর ক্ষতিপূরণ করে “fast convergence” দিয়ে: নতুন Wmax আগেরটির চেয়ে কম হলে (অর্থাৎ নতুন প্রতিযোগী এসেছে) Wmax আরও কমিয়ে ধরে, যাতে পুরোনো প্রবাহ জায়গা ছাড়ে। DCTCP আরও এক ধাপ যায় — জ্যামের পরিমাণের অনুপাতে কমায়।

মূল উপলব্ধি: AIMD-এর ন্যায্যতা আসে একটি জ্যামিতিক সত্য থেকে — শুধু গুণ করে কমানোই পার্থক্য কমায়। কতটা কমানো হবে (০.৫, ০.৭, নাকি জ্যামের অনুপাতে), তা দ্রুত ন্যায্যতা আর বেশি ব্যবহারের মধ্যে একটি সচেতন বিনিময়।

প্র ০৩BBR আর CUBIC একই bottleneck ভাগ করলে কী হয়? “TCP-friendly” হওয়া কি নতুন algorithm-এর জন্য ঠিক মানদণ্ড?

দুটির দর্শন আলাদা। CUBIC loss না পাওয়া পর্যন্ত বাড়ে, তাই bottleneck-এর buffer ভরে ফেলে। BBR v1 মাপা BDP-র প্রায় দ্বিগুণের বেশি ভাসমান রাখে না আর loss-কে প্রায় পাত্তা দেয় না। একসাথে চললে কী হয়, তা নির্ভর করে buffer-এর আকারের ওপর। ২০১৭–২০১৯ সালের বিভিন্ন গবেষণা (যেমন Hock ও সহকর্মীদের মাপজোখ, এবং Ware ও সহকর্মীদের মডেল) দেখিয়েছে: ছোট buffer-এ BBR v1 অনেক বেশি ভাগ নিতে পারে, কারণ CUBIC বারবার loss দেখে কমায় অথচ BBR কমায় না; আবার বড় buffer-এ CUBIC queue ভরে RTT বাড়ায়, যা BBR-এর cwnd সীমাকে ছোট করে দিতে পারে। আরও দেখা গেছে, CUBIC প্রবাহ যতগুলোই হোক, একটি BBR v1 প্রবাহের ভাগ প্রায় একই থাকে — যা ন্যায্যতার প্রচলিত ধারণার সাথে মেলে না। বিস্তারিত সংখ্যা পরীক্ষাভেদে আলাদা।

Google এর জবাবে BBR v2 আর পরে v3 আনে: loss-এর হার একটি সীমা (মোটামুটি ২%) পেরোলে বা ECN দাগ এলে ভাসমান ডেটার ওপরের সীমা কমানো, আর ProbeBW-কে আরও সাবধানী করা। প্রতিবেদন অনুযায়ী এতে CUBIC-এর পাশে আচরণ ভালো হয়েছে, তবে মাপজোখ চলছে।

মানদণ্ডের প্রশ্নটিও আলোচিত। প্রচলিত দাবি ছিল “নতুন algorithm Reno-র চেয়ে বেশি নেবে না” (TCP-friendliness). কিন্তু Reno নিজেই উচ্চ-BDP পথে খারাপ, আর ছোট-RTT প্রবাহকে বেশি দেয় — তাকে আদর্শ ধরা কতটা যুক্তিসংগত? ২০১৯ সালে Ware ও সহকর্মীরা প্রস্তাব করেন “ক্ষতি-ভিত্তিক” মানদণ্ড: নতুন algorithm পুরোনোটির throughput বা delay-এর যতটা ক্ষতি করে, তা পুরোনো algorithm নিজে নিজের প্রতি যতটা করে, তার বেশি না হলেই গ্রহণযোগ্য। IETF-এর আলোচনাতেও এই ধরনের চিন্তা এসেছে।

মূল উপলব্ধি: ইন্টারনেটে congestion control একা চলে না — প্রতিটি নতুন algorithm পুরোনোদের পাশে চলে। “আমার সংযোগ দ্রুত হলো” যথেষ্ট নয়; অন্যদের কতটা ক্ষতি হলো, সেটিও মাপতে হয়।

প্র ০৪Router-এর buffer কত বড় হওয়া উচিত? “বড় buffer = কম loss = ভালো” ধারণাটি কোথায় ভুল?

পুরোনো নিয়ম ছিল buffer = RTT × C (link-এর গতি), অর্থাৎ একটি BDP. যুক্তিটি এই পর্বের সাথে সরাসরি যুক্ত: একটি Reno প্রবাহ loss-এ window অর্ধেক করে; buffer ঠিক BDP হলে অর্ধেক হওয়ার মুহূর্তেও queue-তে যা জমা ছিল তা link-কে ব্যস্ত রাখে, তাই link কখনো খালি হয় না। ২০০৪ সালে Appenzeller, Keslassy আর McKeown দেখান, অনেক প্রবাহ একসাথে থাকলে (backbone-এ) তাদের করাতের দাঁত একসাথে পড়ে না, তাই BDP ÷ √n-এর মতো অনেক ছোট buffer যথেষ্ট। পর্ব ১৫-এ এই হিসাব দেখেছেন।

কিন্তু বাসার router, কেবল মডেম আর মোবাইল নেটওয়ার্কে উল্টো সমস্যা দেখা গেল। Memory সস্তা হওয়ায় নির্মাতারা “কোনো packet যেন না হারায়” ভেবে বিশাল buffer দিলেন। Loss-ভিত্তিক TCP (Reno, CUBIC) buffer না ভরা পর্যন্ত বাড়ে — তাই buffer যত বড়, স্থায়ী queue তত বড়। একটি ধীর upload link-এ কয়েক MB-এর buffer মানে সেকেন্ডের হিসাবে delay. Jim Gettys ২০১০-এর দিকে এর নাম দেন bufferbloat. ফল অদ্ভুত: loss কম, throughput ঠিক, অথচ ভিডিও কল, গেম আর ওয়েবপেজ খোলা ভয়ানক ধীর।

সমাধান তিন স্তরে। Router-এ AQM — CoDel (Nichols আর Jacobson, ২০১২) packet কতক্ষণ queue-তে ছিল মেপে লক্ষ্যের (৫ ms) বেশি হলে ফেলে বা দাগ দেয়; fq_codel প্রতিটি প্রবাহকে আলাদা queue দেয়, ফলে ছোট প্রবাহ বড় download-এর পেছনে আটকায় না। প্রেরকে BBR-এর মতো queue না ভরে চলা algorithm. আর ECN আর L4S, যা packet না ফেলে আগাম সংকেত দেয়। Linux-এর অনেক ডিস্ট্রিবিউশন আর OpenWrt-এর মতো router firmware এখন fq_codel বা CAKE ডিফল্ট বা সহজলভ্য রাখে।

মূল উপলব্ধি: Buffer-এর কাজ ঝাপটা শোষণ করা, স্থায়ী queue রাখা নয়। Loss-ভিত্তিক congestion control আর বড় buffer একসাথে হলে delay-ই দাম — তাই আধুনিক নেটওয়ার্কে “কত বড় buffer” প্রশ্নের চেয়ে “queue কে, কীভাবে ছোট রাখবে” প্রশ্নটি বেশি গুরুত্বপূর্ণ।

প্র ০৫Wi-Fi, মোবাইল ডেটা বা স্যাটেলাইটে loss মানেই জ্যাম নয় — loss-ভিত্তিক TCP এমন পথে কী সমস্যায় পড়ে, আর কী সমাধান এসেছে?

Reno-র মৌলিক অনুমান: packet হারাল মানে কোনো buffer উপচে পড়েছে। তারের নেটওয়ার্কে প্রায় সত্য। কিন্তু বেতারে দেয়াল, দূরত্ব, অন্য নেটওয়ার্কের হস্তক্ষেপ বা চলন্ত বাসে টাওয়ার বদলের (handover) সময়ও packet হারায়। TCP প্রতিটি loss-এ window অর্ধেক করে, ফলে জ্যাম না থাকলেও গতি পড়ে যায়। Mathis সূত্রে দেখুন: p মাত্র ১% হলেই ১০০ ms RTT-তে throughput দেড় Mbps-এর কাছাকাছি নামে, link যত দ্রুতই হোক।

প্রথম সমাধান link স্তরেই: Wi-Fi প্রতিটি frame-এর ACK নেয় আর হারালে নিজে আবার পাঠায় (পর্ব ১৪), আর 4G-5G-তে HARQ ও RLC-এর মতো স্তরও হারানো অংশ নিজেরাই আবার পাঠায়। ফলে TCP অনেক loss দেখেই না — কিন্তু তার বদলে দেখে হঠাৎ বেড়ে যাওয়া আর ওঠানামা করা RTT. মোবাইল নেটওয়ার্কের বেস স্টেশনে প্রতিটি ব্যবহারকারীর জন্য বড় buffer থাকায় সেখানেও bufferbloat-এর অভিযোগ গবেষণায় এসেছে।

দ্বিতীয় দিক algorithm-এ। Westwood-এর মতো রূপ ACK-এর হার থেকে bandwidth অনুমান করে loss-এর পর অর্ধেকের বদলে সেই অনুমানে নামত। BBR loss-কে প্রধান সংকেত না ধরায় এলোমেলো loss-এ কম ভোগে — যা Google-এর প্রতিবেদনে উন্নয়নশীল দেশের ব্যবহারকারীদের জন্য বিশেষ লাভ হিসেবে উল্লেখ হয়েছিল। স্যাটেলাইটে (geostationary-তে RTT ৬০০ ms-এর মতো) slow start আর AIMD দুটোই খুব ধীর, তাই সেখানে প্রায়ই performance-enhancing proxy সংযোগটি দুই ভাগ করে মাঝখানে নিজস্ব protocol চালায়; নিচু কক্ষপথের নতুন স্যাটেলাইট নেটওয়ার্কে RTT অনেক কম হলেও ঘন ঘন পথ বদল নতুন সমস্যা আনে। বাংলাদেশের মতো দেশে যেখানে বড় অংশের ইন্টারনেট মোবাইল নেটওয়ার্কে, সেখানে এই প্রশ্নগুলো বাস্তব — তবে নির্দিষ্ট পরিসংখ্যান অপারেটর আর সময়ভেদে আলাদা।

মূল উপলব্ধি: একটি সংকেতের অর্থ পরিবেশের ওপর নির্ভর করে। “Loss = জ্যাম” তারে কাজ করেছিল; বেতারে delay, delivery rate আর ECN-এর মতো আরও সংকেত মিলিয়ে দেখতে হয় — আধুনিক algorithm-এর বিবর্তন মূলত এই দিকেই।

অনুশীলন

খাতা-কলম নিন — round ধরে ধরে cwnd আর ssthresh লিখুন।

  1. হাতে trace (পরীক্ষার ধরন): একটি TCP Reno সংযোগে শুরুতে cwnd = ১ MSS, ssthresh = ২০ MSS. Round ১০-এ তিনটি duplicate ACK আর round ১৬-তে timeout ধরা পড়ে; আর কোনো loss নেই। round-ভিত্তিক সরল নিয়ম ধরুন (slow start-এ cwnd = min(২·cwnd, ssthresh), fast recovery শেষে cwnd = ssthresh). (ক) round ১–২১-এর cwnd লিখুন। (খ) Slow start আর congestion avoidance কোন কোন round-এ? (গ) Round ১২ আর ১৮-তে ssthresh কত? (ঘ) ৫০তম segment কোন round-এ যায়? (ঙ) এটি Tahoe হলে round ১১-তে cwnd কত?

    (ক) ১, ২, ৪, ৮, ১৬ (round ৫), ২০ (round ৬ — ৩২ নয়, ssthresh-এ আটকায়), ২১, ২২, ২৩, ২৪ (round ১০, তিন dupACK) → ssthresh = ১২, round ১১-তে ১২, তারপর ১৩, ১৪, ১৫, ১৬, ১৭ (round ১৬, timeout) → ssthresh = ৮, round ১৭-তে ১, তারপর ২, ৪, ৮ (round ২০), ৯ (round ২১)।

    (খ) Slow start: round ১–৬ আর ১৭–২০ (দ্বিগুণ হয়ে ssthresh ছোঁয়া পর্যন্ত)। Congestion avoidance: round ৬–১০, ১১–১৬, ২০–২১. (গ) Round ১২-তে ১২, round ১৮-তে ৮. (ঘ) জমা: ১, ৩, ৭, ১৫, ৩১, ৫১ — round ৫ শেষে ৩১, round ৬-এ ৩২ থেকে ৫১, তাই ৫০তম segment round ৬-এ। (ঙ) Tahoe-তে তিন dupACK-এও cwnd = ১, তাই round ১১-তে ১ (ssthresh ১২)। অংশ ৬-এর simulator-এ SSTHRESH0 = 20, loss = { 10: "3dup", 16: "timeout" }, ROUNDS = 21 দিয়ে যাচাই করুন — Tahoe লাইন আসবে 1 2 4 8 16 20 21 22 23 24 1 2 4 8 12 13 1 2 4 6 7.

  2. আউটপুট অনুমান করুন: অংশ ৬-এর cwnd_rounds.js-এ শুধু SSTHRESH0 = 8 আর loss = { 10: "timeout" } করা হলো। Round ৪, ১০, ১১, ১৪ আর ২৪-এর cwnd কত, round ১০-এর পর ssthresh কত, আর Tahoe-র লাইন কি Reno-র থেকে আলাদা হবে? চালানোর আগে হিসাব করুন।

    ১, ২, ৪, ৮ (round ৪, ssthresh ছুঁল), তারপর ৯, ১০ … round ১০-এ ১৪ — timeout. ssthresh = ১৪/২ = ৭, round ১১-তে ১, তারপর ২, ৪, আর round ১৪-তে min(৮, ৭) = ৭. এরপর +১ করে round ২৪-এ ১৭. প্রথম লাইনে লেখা আসবে initial ssthresh = 8 MSS, আর মোট segment ২২৩. কোনো তিন-dupACK ঘটনা নেই, তাই Tahoe-র লাইন হুবহু একই: 1 2 4 8 9 10 11 12 13 14 1 2 4 7 8 9 10 11 12 13 14 15 16 17 — Tahoe আর Reno-র পার্থক্য শুধু তিন dupACK-এ।

  3. Throughput-এর অঙ্ক: একটি Reno প্রবাহ স্থিতিশীল করাতের দাঁতে চলছে; loss হয় যখন window W = ৪০ MSS. MSS = ১,৪৬০ byte, RTT = ৬০ ms. (ক) সর্বোচ্চ আর গড় throughput? (খ) এক চক্র কত সময়, আর এক চক্রে কতগুলো segment? তা থেকে loss rate p? (গ) Mathis সূত্রে এই p দিয়ে throughput হিসাব করে (ক)-এর সাথে মেলান। (ঘ) একই প্রবাহ CUBIC হলে (Wmax = ৪০০ MSS ধরুন, C = ০.৪, β = ০.৭) loss-এর ঠিক পরে window কত, আর কত সেকেন্ডে Wmax-এ ফেরে?

    (ক) সর্বোচ্চ = ৪০ × ১১,৬৮০ ÷ ০.০৬ = ৭.৭৯ Mbps; গড় = ০.৭৫ × ৭.৭৯ = ৫.৮৪ Mbps.

    (খ) W/২ = ২০ RTT = ১.২ সেকেন্ড. Segment = ৩W²/৮ = ৩ × ১,৬০০ ÷ ৮ = ৬০০. প্রতি চক্রে একটি loss → p = ১/৬০০ ≈ ০.০০১৬৭.

    (গ) √p = ১/√৬০০ ≈ ০.০৪০৮; ১.২২৪৭ × ১১,৬৮০ ÷ (০.০৬ × ০.০৪০৮) ≈ ৫.৮৪ Mbps — (ক)-এর গড়ের সাথে হুবহু মিলেছে, কারণ সূত্রটি ঠিক এই মডেল থেকেই এসেছে।

    (ঘ) Loss-এর পরে ০.৭ × ৪০০ = ২৮০ MSS. K = ∛(৪০০ × ০.৩ ÷ ০.৪) = ∛৩০০ ≈ ৬.৬৯ সেকেন্ড — RTT-র ওপর নির্ভর করে না।

  4. কমান্ড আর কোড লিখুন (Linux): (ক) নিজের যন্ত্রের ডিফল্ট আর লোড করা congestion control algorithm দেখার কমান্ড। (খ) ডিফল্ট BBR করা, reboot-এর পরেও টিকিয়ে রাখা। (গ) চলমান সংযোগের cwnd আর ssthresh দেখা। (ঘ) C-তে শুধু একটি socket-এ (সংযোগের আগে) CUBIC বসানোর এক লাইন, আর ব্যর্থ হলে ENOENT আর EPERM কী বোঝায়।
    sysctl net.ipv4.tcp_congestion_control net.ipv4.tcp_available_congestion_control   # (ক)
    sudo modprobe tcp_bbr && sudo sysctl -w net.ipv4.tcp_congestion_control=bbr        # (খ)
    echo 'net.ipv4.tcp_congestion_control=bbr' | sudo tee /etc/sysctl.d/90-cc.conf      # (খ) স্থায়ী
    ss -tin state established                                                         # (গ)
    setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "cubic", strlen("cubic"));             /* (ঘ) */

    (ঘ)-তে ENOENT মানে ওই নামের algorithm kernel-এ লোড নেই (আর সাধারণ ব্যবহারকারীর হয়ে kernel module লোড করে না); EPERM মানে algorithm আছে কিন্তু tcp_allowed_congestion_control-এ নেই, আর প্রক্রিয়ার CAP_NET_ADMIN নেই। অংশ ৯-এর প্রোগ্রামে vegas আর dctcp-তে প্রথমটিই দেখেছেন। সার্ভারে বদলানোর আগে আগে-পরে মাপুন, আর শুধু নিজের যন্ত্রে বা ল্যাবে করুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমTCP Reno-তে একটি segment হারানোর পরের ঘটনা — সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. প্রাপক পরের segment-গুলো পেয়ে একই ACK নম্বর বারবার পাঠায়
  2. প্রেরক তৃতীয় duplicate ACK পায়
  3. ssthresh = cwnd/২ বসিয়ে হারানো segment আবার পাঠায়
  4. প্রতিটি বাড়তি duplicate ACK-এ cwnd এক MSS বাড়িয়ে নতুন segment পাঠায়
  5. নতুন ACK এলে cwnd = ssthresh করে fast recovery শেষ করে
  6. Congestion avoidance-এ প্রতি RTT-তে cwnd এক MSS করে বাড়ায়

একটি TCP Reno সংযোগে congestion avoidance চলছে, cwnd = ২০ MSS. তিনটি duplicate ACK এলো, তারপর fast recovery শেষ হলো। নতুন ssthresh আর fast recovery শেষে cwnd কত?

  • ssthresh ১০, cwnd ১০
  • ssthresh ১০, cwnd ১
  • ssthresh ২০, cwnd ১০
  • ssthresh ১০, cwnd ১৩

TCP Reno তিনটি duplicate ACK-এ cwnd অর্ধেক করে, কিন্তু timeout-এ ১ MSS-এ নামায়। এর মূল কারণ কী?

  • Timeout-এ segment আবার পাঠানো হয় না
  • Duplicate ACK router পাঠায়, তাই বিশ্বাসযোগ্য
  • Duplicate ACK দেখায় packet এখনো পৌঁছাচ্ছে, timeout দেখায় হয়তো কিছুই পৌঁছাচ্ছে না
  • Timeout-এ ssthresh বদলায় না

MSS = ১,২৫০ byte, RTT = ৫০ ms আর packet loss rate p = ০.০০০১ হলে Mathis সূত্র (throughput ≈ 1.22·MSS ÷ (RTT·√p)) অনুযায়ী একটি Reno প্রবাহের throughput প্রায় কত?

  • ২০ Mbps
  • ২৪.৪ Mbps
  • ২.৪৪ Gbps
  • ২.৪৪ Mbps

CUBIC-এ C = ০.৪, β = ০.৭. শেষ loss-এর সময় window Wmax = ১০০ MSS ছিল। Loss-এর ঠিক পরে window কত, আর প্রায় কত সেকেন্ড পর window আবার ১০০-তে পৌঁছায় (K = ∛(Wmax·(১ − β) ÷ C))?

  • ৫০ MSS, ৫ সেকেন্ড
  • ৭০ MSS, ৭৫ সেকেন্ড
  • ৩০ MSS, ৪.২ সেকেন্ড
  • ৭০ MSS, প্রায় ৪.২ সেকেন্ড

একটি পথে BBR অনুমান করেছে BtlBw = ১০০ Mbps আর RTprop = ২০ ms. ProbeBW ধাপে BBR-এর cwnd প্রায় কত হবে (cwnd ≈ ২ × BDP)?

  • ২,৫০,০০০ byte
  • ৫,০০,০০০ byte
  • ৪০,০০,০০০ byte
  • ৫০,০০,০০,০০০ byte

একটি data center-এ DCTCP চলছে। শেষ RTT-গুলোতে ECN দাগের অনুপাতের চলমান গড় α = ০.২, আর cwnd = ৫০ MSS. DCTCP-র নিয়ম cwnd ← cwnd × (১ − α/২) অনুযায়ী নতুন cwnd কত?

  • ২৫ MSS
  • ৪০ MSS
  • ৪৫ MSS
  • ৪৯ MSS

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Congestion-এর দাম: queuing delay, loss আর retransmission-এ নষ্ট ক্ষমতা; চরম রূপ congestion collapse (১৯৮৬)। সমাধান end-to-end — প্রেরক নিজে loss, delay বা ECN দেখে গতি ঠিক করে।
  2. cwnd: হার ≈ cwnd ÷ RTT; ভাসমান ≤ min(rwnd, cwnd). Slow start-এ প্রতি ACK-এ +১ (প্রতি RTT-তে দ্বিগুণ), ssthresh-এর পর প্রতি RTT-তে +১; IW সাধারণত ১০ segment.
  3. Loss-এ: তিন dupACK → ssthresh = cwnd/২, Reno-তে cwnd = ssthresh (+৩ inflation, তারপর deflation), Tahoe-তে ১; timeout → দুটিতেই cwnd = ১, slow start.
  4. AIMD আর সূত্র: গুণ করে কমানোয় পার্থক্য অর্ধেক → ন্যায্যতা; গড় throughput ০.৭৫·W·MSS/RTT ≈ ১.২২·MSS/(RTT·√p) — ছোট RTT বেশি পায়, বেশি সংযোগ বেশি পায়।
  5. আধুনিক: CUBIC W(t) = C(t − K)³ + Wmax, β = ০.৭, RTT-স্বাধীন বৃদ্ধি; BBR BtlBw আর RTprop মেপে pacing, cwnd ≈ ২·BDP, চার ধাপ; ECN-AQM, DCTCP (α-অনুপাতে কমানো) আর L4S queue ছোট রাখে।

পরের পর্বে: TCP-র handshake, নির্ভরযোগ্যতা আর congestion control kernel-এর ভেতরে বাঁধা — বদলাতে লাগে OS-এর হালনাগাদ। QUIC এগুলো UDP-র ওপর অ্যাপের ভেতরে নিয়ে আসে, এনক্রিপশনসহ, আর congestion control-ও সেখানে বদলানো যায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান