Course-এর সূচি

3.4

Virtual Memory, Paging ও Swap

প্রতিটা process কীভাবে নিজের address space পায়, page table ও page fault, আর swap কেন আলাদা জিনিস।

50 মিনিট পড়া + Animation

টিপস: ডটেড-আন্ডারলাইন করা যেকোনো শব্দে ট্যাপ বা Hover করলে তার অর্থ দেখবে।

এই Lesson শেষে তুমি পারবে

  • বলতে, Virtual Memory আসলে কী (প্রতিটা Process-এর জন্য Address অনুবাদ), আর “Disk-কে Memory হিসেবে ব্যবহার” কথাটা কেন আসলে Swap-এর বর্ণনা, যা আলাদা আর ঐচ্ছিক জিনিস।
  • Virtual আর Physical Address, MMU, Page, Frame আর Page Table বোঝাতে, আর হাতে হিসাব করে একটা Address অনুবাদ করতে।
  • দেখাতে, Page Table কেন কয়েক স্তরের, আর একটা TLB Hit, TLB Miss আর Huge Page-এর দাম কত।
  • একটা Page Fault ধাপে ধাপে বোঝাতে, আর Minor Fault, Major Fault আর অবৈধ Access আলাদা করতে।
  • বলতে, Linux কীভাবে Memory ফিরিয়ে নেয়, Swap আর swappiness আসলে কী করে, Thrashing কেন হয়, আর একটা Server-এ Swap রাখা উচিত কি না সেই সিদ্ধান্ত নিতে।
  • Copy-on-Write আর fork বোঝাতে, সাথে এটাও যে Redis Snapshot-এ Memory কেন দ্বিগুণ হয়ে যেতে পারে।
  • mmap আর Page Cache বোঝাতে, Kafka আর Database কেন এগুলোর ওপর ভরসা করে, আর Process Crash আর বিদ্যুৎ চলে যাওয়ায় কী টেকে আর কী টেকে না তা বলতে।
  • Overcommit আর OOM Killer বোঝাতে, VSZ আর Resident Memory ঠিকভাবে পড়তে, আর Container-এর Memory Limit মাপতে।

#এই Lesson কোথায় বসে

Lesson ১.২-এ এক লাইনে বলেছিলাম, Swap শুরু হলে সব ধীর হয়ে যায়, আর তারপর OOM Killer Process-কে শেষ করে দেয়। Lesson ৩.১ বলেছে প্রতিটা Process-এর নিজের Address Space আছে, Thread-গুলো একটাই ভাগ করে, আর Copy-on-Write-এর জন্য fork সস্তা; সেই কথাটার ব্যাখ্যা এখানে পাবে। Lesson ৩.২-এ Context Switch-এর লুকানো খরচের তালিকায় TLB ছিল; কেন ছিল, সেটা এবার দেখবে।

একটা RAM-কে শত শত Process কীভাবে ভাগ করে, অথচ প্রত্যেকে ভাবে পুরো Machine তার একার, এই Lesson সেটাই বোঝায়। আর দেখায় Design-এ এর মানে কী: Restart-এর পর Database কেন ধীর, ছোট Heap নিয়েও Kafka কেন দ্রুত, Host-এ Memory খালি থাকতেও Container কেন মরে। এর নিচে Hypervisor অনুবাদের আরও একটা স্তর কীভাবে জোড়ে, সেটা Chapter 4-এর বিষয়।

#Virtual Memory আসলে কী

Virtual Memory এমন একটা কৌশল, যেখানে প্রতিটা Process পায় তার নিজের আলাদা Address Space, আর সে যে Address-ই ব্যবহার করুক, Hardware সেটাকে RAM-এর আসল জায়গায় অনুবাদ করে নেয়। Program কখনো RAM-এর আসল Address নিয়ে কাজ করে না। সে কাজ করে Virtual Address নিয়ে, আর প্রতিটা Address আসলে কোথায় পড়বে সেটা ঠিক করে CPU আর Kernel মিলে।

       Process A                       Process B
  Virtual address space           Virtual address space
           |                               |
           v                               v
      A's page tables                 B's page tables
           |                               |
           +---------------+---------------+
                           v
                    Physical memory

দুটো Program-ই 0x5000 Address ব্যবহার করতে পারে, কখনো ধাক্কা লাগবে না, কারণ তাদের Table ওই সংখ্যাকে পাঠায় আলাদা জায়গায়।

একই Address, দুটো Process, RAM-এর দুটো আলাদা জায়গা

প্রতিটা Process-এর নিজের একটা মানচিত্র আছে, Virtual Page থেকে Physical Frame পর্যন্ত। লাইনগুলো খেয়াল করো: এগুলোই Page Table।

পরিস্থিতি

দুটো Process-ই 0x5000 Address ব্যবহার করে, তবু কখনো ধাক্কা লাগে না।

Process APhysical RAMProcess B
0x4000 Code
0x5000 Data
0x6000 Heap
0x7000 Heap
Frame ১ (A)
Frame ২ (A)
Frame ৩ (B)
Frame ৪ (A)
Frame ৫ (B)
Frame ৬ (A)
0x4000 Code
0x5000 Data
ধাপ 1 / 5
1

একই Machine-এ দুটো Program চলছে, আর দুটোই নিজের Data রাখে 0x5000 Address-এ। অনুবাদ না থাকলে একে অন্যের Data মুছে দিত। Virtual Memory এই সমস্যাটাই সমাধান করে।

#কেন দরকার

সুবিধাকী দেয়বাস্তবে এর মানে
Process IsolationProcess শুধু নিজের Table-এর Memory-র নাম জানেএকটা Process-এর Crash বা ভুল Pointer অন্যটার Data নষ্ট করতে পারে না
Memory Protectionপ্রতিটা Page-এর Read, Write আর Execute অনুমতি আলাদাCode মুছে লেখা যায় না; Data চালানো যায় না
Allocation-এ দক্ষতাপাশাপাশি Virtual Memory ছড়ানো Frame-এ বসতে পারেRAM টুকরো টুকরো হলেও Program লম্বা Array পায়
Physical RAM নিয়ন্ত্রণকী RAM-এ থাকবে আর কী যাবে, ঠিক করে Kernelঅলস Memory ফিরিয়ে নেওয়া আর Container ধরে Limit দেওয়া যায়
Demand PagingPage প্রথম ছোঁয়ার সময়ই কেবল Frame মেলে১ GB Allocation-এ ব্যবহার না করা পর্যন্ত খরচ নেই; Program দ্রুত চালু হয়
Shared Memory আর Mappingএকাধিক Table এক Frame দেখাতে পারে; File Map করা যায়সব Process-এর জন্য libc-এর একটাই কপি; mmap; সস্তা fork

#Virtual Address, Physical Address আর MMU

Virtual Address হলো সেই সংখ্যা, যা Process ব্যবহার করে: এ পর্যন্ত যত Pointer Print করেছ, সব এটাই। Physical Address হলো RAM Hardware-এর আসল জায়গা। Process এটা কখনো দেখে না।

MMU (Memory Management Unit) হলো CPU-র সেই অংশ, যা প্রতিটা Memory Access-এ একটাকে অন্যটায় বদলায়: প্রতিটা Instruction আনা, প্রতিটা Load, প্রতিটা Store। সেকেন্ডে শত কোটিবার হয় বলে এটা Hardware-এই করতে হয়। Kernel-এর কাজ আলাদা: সে Page Table লেখে, MMU সেটা পড়ে; আর MMU যা পারে না, সেগুলো Kernel সামলায়।

৬৪-Bit Process আসলে পুরো ৬৪ Bit পায় না। চালু x86-64 CPU ব্যবহার করে ৪৮ Bit Virtual Address, মানে ২৫৬ TB; Linux তার নিচের অর্ধেক (১২৮ TB) দেয় User Space-কে, ওপরের অর্ধেক থাকে Kernel-এর জন্য। পাঁচ স্তরের Paging থাকলে এটা হয় ৫৭ Bit, ১২৮ PB। তাই অল্প RAM-এর Machine-এও VSZ বিশাল হতে পারে, আর এতে কোনো গরমিল নেই: Address Space তো শুধু কিছু সংখ্যার একটা পরিসর।

#Page, Frame আর Page Table

প্রতিটা Byte আলাদা করে অনুবাদ করতে গেলে Table-টা Memory-র চেয়েও বড় হতো। তাই Memory সামলানো হয় নির্দিষ্ট মাপের Block-এ:

  • Page হলো Virtual Memory-র একটা নির্দিষ্ট মাপের Block।
  • Frame হলো Physical Memory-র একই মাপের Block।
  • Page Table বলে দেয় কোন Virtual Page কোন Physical Frame-এ আছে।
Virtual memory            Page table             Physical RAM
+--------+                                       +---------+
| Page 1 |  ------------> frame 3 ------------>  | Frame 1 |
+--------+                                       +---------+
| Page 2 |  ------------> frame 1 ------------>  | Frame 2 |
+--------+                                       +---------+
| Page 3 |  ------------> frame 2 ------------>  | Frame 3 |
+--------+                                       +---------+

সাধারণ মাপ ৪ KB। তবে সব জায়গায় এটাই না: x86-64-এ ২ MB আর ১ GB Page-ও আছে, Arm Server-এ মূল মাপ ৪, ১৬ বা ৬৪ KB হতে পারে, আর Apple Silicon-এ ১৬ KB। getconf PAGESIZE বলে দেবে; যে Code নানা Machine-এ চলবে, সে ধরে না নিয়ে জিজ্ঞেস করে নেয়।

Page Table-এর প্রতিটা রেকর্ডকে বলে Page Table Entry (PTE), আর এতে শুধু Frame নম্বর থাকে না:

PTE-র অংশমানেকে ব্যবহার করে
Frame নম্বরPage কোথায় আছেMMU, অনুবাদের জন্য
Presentএই মুহূর্তে RAM-এ আছে কি?বন্ধ থাকলে Access-এ Page Fault
Writableলেখা যাবে কি?Protection, আর Copy-on-Write-এর কৌশল
UserUser Code ছুঁতে পারবে কি?Kernel-এর Memory-কে Process থেকে বাঁচায়
No-executeএখান থেকে Code চালানো নিষেধCode ঢুকিয়ে করা অনেক Attack থামায়
Accessedব্যবহার হলে Hardware চালু করেঅলস Page খুঁজতে Kernel এটা দেখে
Dirtyলেখা হলে Hardware চালু করেফেলে দেওয়ার আগে Save লাগবে কি না Kernel বোঝে

#হাতে হিসাব করে Address অনুবাদ

৪ KB Page-এ ৪,০৯৬ = ২^১২, তাই Address-এর নিচের ১২ Bit হলো Page-এর ভেতরের Offset, আর তার ওপরের সবটা Page নম্বর। Hexadecimal-এ ১২ Bit মানে ঠিক শেষের তিনটা অঙ্ক, তাই চোখে দেখেই ভাগ করা যায়।

ধরো Virtual Address 0x00403A7C, আর Page Table বলছে Page 0x403 আছে Frame 0x1F2-এ:

  1. ভাগ করো: Page নম্বর 0x00403, Offset 0xA7C।
  2. খোঁজো: Table-এর 0x403 নম্বর Entry পড়ো। এটা Array Index, খোঁজাখুঁজি না। Frame 0x1F2, Present।
  3. Offset রেখে দাও: Frame আর Page একই মাপের, তাই Offset বদলায় না।
  4. জোড়া দাও: 0x1F2-এর পরে A7C, মানে Physical Address 0x1F2A7C।

পরের Byte 0x00403A7D একই Page-এ, নতুন করে খুঁজতে হয় না। এজন্যই Memory ক্রমে পড়া এত সস্তা।

Address কাটা: Page নম্বর, Offset, আর Table ঘেঁটে যাওয়া

MMU কিছু খোঁজে না। সে Address-কে নির্দিষ্ট কয়েক ভাগে কাটে, আর সেগুলোকে Array Index হিসেবে ব্যবহার করে। এখানকার সব সংখ্যা হিসাব করে মেলানো।

পরিস্থিতি

৪ KB Page-এ একটা ৩২-Bit Address, হাতে হিসাব করে অনুবাদ।

0x00403A7C

Page নম্বর

0x00403

20 Bit

Offset

0xA7C

12 Bit

Page TableFrame
ধাপ 1 / 4
1

একটা Page ৪ KB, মানে ৪,০৯৬ = ২^১২ Byte। তাই নিচের ১২ Bit বলে Page-এর ভেতরে কোথায়, আর বাকি ২০ Bit বলে কোন Page। Hexadecimal-এ ১২ Bit মানে ঠিক শেষের তিনটা অঙ্ক।

নিজেকে যাচাই করোপ্রশ্ন

Page ৪ KB। একটা Program Virtual Address 0x0012F3A8 পড়ে, আর Page Table Page 0x12F-কে Frame 0x8B0-তে দেখায়। Physical Address কত?

#কয়েক স্তরের Page Table

একটা লম্বা Table অসম্ভব রকম বড় হয়। ৪৮-Bit Address আর ৪ KB Page-এ মোট ২^৩৬টা Page, প্রতি Entry ৮ Byte হলে প্রতিটা Process-এর জন্য ৫১২ GB Page Table।

কিন্তু Address Space-এর বেশিরভাগটাই ফাঁকা: বিশাল সমুদ্রে একটা Process-এর কয়েকটা দ্বীপ থাকে (Code, Heap, Library, Stack)। তাই Page Table হলো একটা Tree, আর ফাঁকা এলাকার ডালগুলো থাকেই না। চার স্তরের x86-64-এ:

 47        39 38        30 29        21 20        12 11         0
+------------+------------+------------+------------+------------+
| L4 index   | L3 index   | L2 index   | L1 index   | offset     |
| 9 bits     | 9 bits     | 9 bits     | 9 bits     | 12 bits    |
+------------+------------+------------+------------+------------+
  • প্রতিটা Table-এ ৫১২টা (২^৯) Entry, প্রতিটা ৮ Byte, তাই একটা Table ঠিক একটা ৪ KB Page। এই মিলটা ইচ্ছে করেই করা।
  • CPU-র একটা Register (x86-এ CR3) ধরে রাখে ওপরের Table-এর Physical Address। Process বদলানো মানে সেখানে অন্য মান বসানো।
  • ছোট Process-এর লাগে হাতে গোনা কয়েকটা Table, মানে কয়েকশো GB না, কয়েক দশ KB।
  • দামটা হলো সময়: কোনো সাহায্য ছাড়া একটা অনুবাদে লাগে চারটা Memory Read, আর Data-র জন্য আরও একটা।

তবে Page Table আসল Memory-ই খায়, আর সেটা জমতে থাকে। ৬৪ GB Heap পুরোটা ৪ KB Page-এ ছোঁয়া হলে একটা Process-এর শেষ স্তরের Entry-তেই লাগে প্রায় ১২৮ MB; আর শত শত Process যদি একটা বিশাল Shared এলাকা Map করে, কয়েক GB-ও লাগতে পারে। অনেক বড় shared_buffers আর অনেক Connection-ওয়ালা PostgreSQL এর চেনা উদাহরণ, আর সমাধান হিসেবে সাধারণত Huge Page আসে।

#TLB: অনুবাদকে প্রায় বিনা খরচের করা

প্রতিটা Load-এ পাঁচটা Memory Read লাগলে Computer চলতই না। TLB (Translation Lookaside Buffer) হলো CPU-র ভেতরে সম্প্রতি করা Page থেকে Frame-এর অনুবাদের ছোট আর দ্রুত একটা Cache।

  • TLB Hit: অনুবাদ চলে আসে প্রায় এক Clock Cycle-এ। প্রায় সব সময় এটাই ঘটে।
  • TLB Miss: Hardware-এর একটা Walker Tree-টা পড়ে। সাধারণত কয়েক দশ ন্যানোসেকেন্ড, আর Page Table-এর Read-গুলোও CPU Cache-এ Miss হলে আরও বেশি। এটা Fault না, কোনো Kernel Code চলে না।
  • Page Present না: Walker দেখে Present Bit বন্ধ, আর Page Fault তোলে। শুধু তখনই Kernel চলে।

TLB ছোট। একটা Core-এ সাধারণত প্রথম স্তরে কয়েক ডজন Entry, আর দ্বিতীয় স্তরে এক থেকে কয়েক হাজার। আসল হিসাব হলো TLB Reach: Entry × Page-এর মাপ।

Page-এর মাপপ্রায় ১,৫০০ Entry-তেযতটা Working Set আঁটে
৪ KB১,৫৩৬ × ৪ KBপ্রায় ৬ MB
২ MB১,৫৩৬ × ২ MBপ্রায় ৩ GB

তাই যে Program ৬৪ GB Heap জুড়ে এলোমেলো Pointer ধরে লাফায়, তার সারাক্ষণ TLB Miss হয়, আর অনেকটা সময় Page Table ঘাঁটতেই চলে যেতে পারে, যদিও কোনো Graph-এ এটাকে সমস্যা মনে হবে না। মাপতে চাইলে perf stat -e dTLB-load-misses। Hypervisor-এর নিচে এই Walk হয় দ্বিমাত্রিক, Guest আর Host-এর Table গুণ হয়ে যায়; Chapter 4-এ এটা আছে।

Design-এর জন্য দুটো খরচ মনে রেখো:

  • Process বদলালে TLB-র ক্ষতি। Page 0x403-এর মানে একেক Process-এ একেক রকম। পুরনো Hardware-এ প্রতিবার সব মুছতে হতো; PCID (x86) বা ASID (Arm) থাকলে Entry-তে Address Space-এর Tag থাকে, তাই সেগুলো থেকে যেতে পারে। একই Process-এর Thread-এর মধ্যে বদলালে Table একই থাকে, আর এজন্যই সেটা সস্তা (Lesson ৩.২)।
  • TLB Shootdown। প্রতিটা Core-এর নিজের TLB। Multi-threaded Process কোনো Memory Unmap করলে বা বদলালে x86-এ Kernel-কে অন্য Core-গুলোকে Interrupt করতে হয়, যাতে পুরনো Entry ফেলে দেয়। অনেক Core-এ ঘন ঘন munmap সত্যিই Scale আটকে দেয়।

একটা Memory Load: TLB Hit, TLB Miss, আর যখন Hardware হাল ছেড়ে দেয়

Hardware-এর পথে একই Load। Miss হলে কতটা বাড়তি কাজ হয়, আর শেষমেশ কখন Kernel আসে, সেটা খেয়াল করো।

পরিস্থিতি

স্বাভাবিক অবস্থা: সুস্থ Program-এর প্রায় সব Load এই পথেই যায়।

CPU CoreLoad চালায়
TLBCPU-র ভেতরে
RAM-এর DataFrame-টা
Page TableRAM-এ
KernelFault Handler
Page 0x403?

Address-টা

Virtual Address

0x00403A7C

Page নম্বর

0x403

Offset

0xA7C

TLB (ছোট Cache-এর কয়েকটা Entry)

PageFrameFlag
0x4030x1F2R W
0x7A10x09CR
ধাপ 1 / 3
1

Core চায় Virtual Address 0x00403A7C। CPU-র অনুবাদ করার অংশ, মানে MMU, Page নম্বরটা নিয়ে আগে TLB-কে জিজ্ঞেস করে।

#Huge Page

Huge Page Walk-টা আগেই থামিয়ে দেয়: ২ MB হলে Level ২-এ, ১ GB হলে Level ৩-এ। মূল লাভ TLB Reach, আর Tree ছোট হওয়ায় Memory-ও কিছুটা বাঁচে।

আলাদা করে রাখা Huge Page (HugeTLB)Transparent Huge Pages (THP)
কীভাবেআগে থেকে রাখা (vm.nr_hugepages), Program চেয়ে নেয়Kernel নিজে বানায়, khugepaged দিয়ে বা Fault-এর সময়
Code বদলাতে হয়?Application-কে চাইতে হয়, সাধারণত Config দিয়েনা
Swap হয়?না, সবসময় RAM-এহয়, সাধারণত আগে ভেঙে নিয়ে
ঝুঁকিব্যবহার না হলেও রাখা Memory অন্যরা পায় নাCompaction থেকে হঠাৎ Latency, Memory ফোলা, দামি Copy-on-Write
সাধারণ ব্যবহারবড় Database Buffer (PostgreSQL-এর huge_pages, Oracle), -XX:+UseLargePages দিয়ে JVMবড় Heap-এর সাধারণ কাজ, সাবধানে

যা ভেবে দেখবে:

  • ভেতরের অপচয়: এক Byte ছুঁলে পুরো ২ MB চলে যেতে পারে, তাই ছড়ানো Data Structure-এ Memory নষ্ট হয়।
  • Latency: ২ MB Page বানাতে Kernel-কে Memory গুছিয়ে নিতে (Compaction) হতে পারে, আর ততক্ষণ Fault করা Thread আটকে থাকে। /proc/vmstat-এ compact_stall দেখো।
  • Copy-on-Write: ছোট একটা লেখাতেই ২ MB কপি হয়, যা নিচের fork-এর সমস্যাকে আরও বাড়ায়।

পরামর্শ Product এমনকি Version ভেদেও বদলায়: Redis Startup-এ THP নিয়ে সতর্ক করে, MongoDB-র পরামর্শ Version ভেদে পাল্টেছে, আর অনেক JVM-এর কাজে এতে লাভ হয়। তোমার Product-এর Documentation পড়ো, তারপর মেপে দেখো। cat /sys/kernel/mm/transparent_hugepage/enabled তোমার Setting দেখায়; madvise (Program চাইলে তবেই THP) জনপ্রিয় মাঝামাঝি পথ।

#Page Fault

Page Fault হলো সেই Hardware Exception, যা ওঠে যখন একটা অনুবাদ শেষ করা যায় না। নাম শুনে ভয় লাগলেও এটা সাধারণত Error না: যে মুহূর্তে Page দরকার, ঠিক তখনই Kernel-এর হাতে নিয়ন্ত্রণ আসার উপায় এটা। Memory Allocate করলে শুধু একটা পরিসর লেখা হয়, Frame মেলে প্রথম ছোঁয়ায়। একেই বলে Demand Paging, আর এজন্যই বড় Program চালু করতে পুরো File RAM-এ পড়তে হয় না।

কোনো Process এমন Page ছুঁলে, যা এই মুহূর্তে Physical Memory-তে নেই:

  1. Kernel যাচাই করে

    MMU Fault হওয়া Address নিয়ে Kernel-এ ঢোকে। Kernel এই Process-এর বৈধ এলাকার তালিকা দেখে। কোনো এলাকায় Address-টা না পড়লে, বা Access অনুমতি না মানলে, সে SIGSEGV পাঠায়, আর এখানেই শেষ।

  2. Page কোথা থেকে আসবে তা খোঁজে

    নতুন Heap Page হলে Zero ভরা Memory, File-এর Page হলে Page Cache বা File, আগে সরানো Page হলে Swap Slot।

  3. দরকার হলে Page আনে

    একটা খালি Frame নেয়, প্রয়োজনে অন্য Page সরিয়ে, তারপর ভরে। Data Disk-এ থাকলে Thread ঘুমায়, অন্য Thread চলে।

  4. Page Table আপডেট করে

    Entry-তে এখন Frame নম্বর আছে, Present Bit চালু।

  5. Instruction আবার চলে

    CPU একই Instruction আবার গোড়া থেকে চালায়। Program জানতেই পারে না যে তাকে থামানো হয়েছিল।

#Minor, Major আর অবৈধ

Minor FaultMajor Faultঅবৈধ Access
কীDisk ছাড়াই Page পাওয়া যায়Data আনতে হবে Storage থেকেবৈধ Mapping নেই, বা অনুমতি নেই
উদাহরণmalloc-এর Memory-তে প্রথম ছোঁয়া; Page Cache-এ আগে থেকে থাকা File Page; Copy-on-Write-এর কপিCache-এ না আসা Map করা File; Swap-এ যাওয়া Page; ঠান্ডা অবস্থায় চালু হওয়ার সময়ের Code PageNull Pointer, Free করার পরে ব্যবহার, Read-only Memory-তে লেখা
দামপ্রায় এক মাইক্রোসেকেন্ডNVMe-তে প্রায় ১০০ µs, Hard Disk-এ ৫–১০ msসাধারণত Process মরে
চিন্তার কিছু?সাধারণত না, তবে লাখ লাখ হলে CPU-র আসল খরচ আছেহ্যাঁ, যদি Latency-সংবেদনশীল পথে টানা হয়এটা Bug

সাধারণ RAM Access-এ লাগে প্রায় ১০০ ns, তাই NVMe-তে একটা Major Fault-এর দাম প্রায় হাজারটা Memory Access-এর সমান। একটা Request-এ এমন কয়েকটা হলে ১ ms থেকে ১০ ms হয়ে যেতে পারে, অথচ চোখে কিছুই পড়বে না: CPU বাড়ে না, Error নেই, গড়ও প্রায় নড়ে না। Tail Latency-র চেনা একটা উৎস হলো Major Fault।

Design-এ এর ফল:

  • ঠান্ডা শুরু (Cold Start)। Restart বা নতুন Deployment-এর পরে Code আর Data Memory-তে থাকে না, তাই প্রথম কিছু Request-এ Major Fault হয়। Traffic দেওয়ার আগে Instance গরম করে নেওয়া, আর Load Balancer-এ Slow Start দেওয়ার (Chapter 25) এটা একটা কারণ।
  • Latency-র জন্য জরুরি Memory। mlock বা mlockall Page-কে RAM-এ আটকে রাখে, আর Startup-এই ছুঁয়ে রাখলে (JVM-এর -XX:+AlwaysPreTouch) খরচটা Request-এর পথ থেকে সরে যায়।
  • মেপে দেখো। ps -o min_flt,maj_flt -p PID, pidstat -r 1, আর perf সবাই Fault-এর হার দেখায়।

Page Fault-এর তিনটা পরিণতি: চট করে ঠিক, Disk থেকে পড়া, বা মৃত্যু

CPU-র চোখে Fault একই রকম। পার্থক্য হলো, Address যাচাই করে Kernel কী পায়।

পরিস্থিতি

Allocate করলে RAM পাওয়া যায় না। ছুঁলে পাওয়া যায়। এটাই Demand Paging।

Threadতোমার CodeVirtual +১ GB, Resident +০
CPU / MMU
Kernel
DiskFile বা Swap
mmap: ১ GB দাও
ধাপ 1 / 6
1

বড় Block-এর malloc Kernel-এর কাছে Address Space চায়। Kernel শুধু লিখে রাখে “এই পরিসর বৈধ”। এখনো কোনো Frame খরচ হয়নি, Resident Memory-ও একই আছে।

#Swap আর Memory ফিরিয়ে নেওয়া (Reclaim)

খালি Memory কমে এলে Kernel Frame ফিরিয়ে নেয় (Reclaim), সাধারণত পেছনে kswapd দিয়ে। সে তাল না রাখতে পারলে যে Thread-এর Memory দরকার, তাকেই Reclaim করতে হয় (Direct Reclaim), আর তোমার Application Thread হঠাৎ Kernel-এ আটকে যায়। কী ছাড়বে, সেটা নির্ভর করে Page-এর ধরনের ওপর:

Page-এর ধরনKernel কী করেদাম
Clean File Page (Code, পড়া Data)ফেলে দেয়; File-এ কপি আছেইএখন বিনা খরচে; আবার লাগলে একটা Major Fault
Dirty File Pageআগে তার File-এ লেখে, তারপর ফেলেএকটা Disk Write
Anonymous Page (Heap, Stack)Swap-এ লেখে; Swap থাকলে তবেই সম্ভবএখন একটা Disk Write, পরে একটা Major Fault

কোন Page ছাড়বে তা বাছতে Linux মোটামুটি Least-Recently-Used তালিকা রাখে, PTE-র Accessed Bit দেখে (আর Kernel ৬.১ থেকে Multi-generational LRU-ও বেছে নেওয়া যায়)।

Swap হলো Anonymous Page রাখার জন্য Disk-এর জায়গা। Page বাইরে পাঠানো আর ফিরিয়ে আনাকেই নোটে Swapping বলা হয়েছে। এক সময় “Swapping” মানে ছিল পুরো Process সরানো; Linux সরায় এক একটা Page, তবে মানুষ দুটো শব্দই বলে। SSD আর Hard Disk RAM-এর চেয়ে বহুগুণ ধীর, তাই Swap-এ যাওয়া Page ছুঁলে কষ্ট আছে।

RAM ভর্তি: Kernel আগে কী ছাড়ে

Reclaim মানে একটা দামের তালিকা। Clean Page ফেলে দেওয়া বিনা খরচে, Dirty Page লিখতে I/O লাগে, আর Swap করতে Swap-এর জায়গা লাগে।

পরিস্থিতি

Reclaim করার সবচেয়ে সস্তা জিনিস। Disk-এ হুবহু একই কপি আগেই আছে।

Virtual PageRAM (৪টা Frame)Disk
Heap Page ১
Heap Page ২
libfoo.so
data.log
Heap ১
Heap ২
libfoo.so
data.log
libfoo.so File
data.log File
Swap: খালি
Anonymous (Heap)Clean File PageDirtySwap-এ
ধাপ 1 / 3
1

চারটা Frame-ই ভর্তি, আর নতুন Page-এর একটা Frame দরকার। Kernel-এর Reclaim Code দেখে কোনটা সম্প্রতি ব্যবহার হয়নি, মোটামুটি Least-Recently-Used তালিকা ধরে।

#swappiness: বেশিরভাগ মানুষ যা ভাবে, তা না

vm.swappiness-এর চালু ব্যাখ্যা হলো “খালি Memory এত শতাংশের নিচে নামলে Swap শুরু”। ভুল। Reclaim যখন হয়, তখন Anonymous Page আর File Page-এর মধ্যে কোনটা বেশি ছাড়বে, এটা তারই আপেক্ষিক পছন্দ। Kernel ৫.৮ থেকে পরিসর ০–২০০, ডিফল্ট ৬০, আর ১০০ মানে দুটোকে সমান ধরা। swappiness=0 Swap বন্ধ করে না; Process মারার আগে শেষ উপায় হিসেবে Kernel তবু Swap ব্যবহার করে। যেমন Kafka-র Documentation বলে vm.swappiness=1, যাতে একদম উপায় না থাকলে তবেই অলস Heap বের হয়।

#Thrashing

Thrashing হয় যখন সক্রিয় Working Set আর RAM-এ আঁটে না। যে Page আনা হয়, সে এমন Page-কে ঠেলে বের করে, যা একটু পরেই আবার লাগবে। Machine-এর সব সময় যায় Page সরাতে, কাজ প্রায় হয়ই না। লক্ষণ: vmstat-এ si আর so বেশি, I/O Wait বেশি, Throughput ধসে পড়া, SSH-এ সাড়া না পাওয়া। তারপর Health Check ব্যর্থ হয়, Load Balancer Traffic সরায় বাকি Node-এ, আর তারাও একই দশায় পড়তে পারে। বেশি Swap দিলে লাভ নেই, কারণ সমস্যা জায়গার না, গতির। Working Set কমাও, RAM বাড়াও, বা Load কমাও। Scheduling-এর ক্ষেত্রেও একই শব্দ ব্যবহার হয় (Lesson ৩.২), ব্যর্থতার চেহারাটাও হুবহু এক।

#Server-এ কি Swap রাখা উচিত?

সিদ্ধান্তভালো দিকখারাপ দিককোথায় মানায়
Swap নেইব্যর্থতা দ্রুত আর পরিষ্কার: OOM Kill, তারপর Restart। Latency আন্দাজ করা যায়অলস Heap Page চিরকাল RAM-এ থাকে, Page Cache-এ টান পড়ে; হঠাৎ চাপ সামলানোর জায়গা কমLoad Balancer-এর পেছনের Stateless, Replicated Service; বেশিরভাগ Container
অল্প Swap, কম swappinessঅলস Page বেরিয়ে যেতে পারে, কাজের Data-র জায়গা বাড়ে; ধীরে বাড়তে থাকা Leak ধরার সময় মেলেনজর না রাখলে পরিষ্কার ব্যর্থতার বদলে ধীরে খারাপ হওয়াসাধারণ Server; অনেক Database Host
RAM-এ Compressed Swap (zram)Disk-এর চেয়ে অনেক দ্রুত; Disk ক্ষয় হয় নাCPU আর কিছুটা RAM খরচছোট VM, Desktop, Memory টানাটানির Node
বড় Swapকাগজে বড় চাপও সামলায়Node “চালু” দেখিয়ে দীর্ঘ সময় Thrash করতে পারেLatency-সংবেদনশীল Server-এ কদাচিৎ ভালো

অনেক Cloud Image আসে Swap ছাড়া, আর Kubernetes আগে এটা বন্ধ রাখতে বাধ্য করত; নতুন Version-এ সীমিতভাবে ব্যবহার করা যায়, তোমারটায় মিলিয়ে নিয়ো। Elasticsearch-এর মতো Cluster System বলে Swap বন্ধ রাখতে বা Memory Lock করতে, কারণ Swap-এ আটকে থাকা Node-কে তার সঙ্গীরা মৃত ঘোষণা করে দিতে পারে। ধীর Node-এর চেয়ে মৃত Node সামলানো সহজ।

Alert দাও হারের ওপর, পরিমাণের ওপর না। ভর্তি Swap আর শূন্য si কোনো ক্ষতি করে না: ওই Page-গুলো সত্যিই অলস। অল্প কিন্তু টানা si আসল সমস্যা। /proc/pressure/memory (PSI) সরাসরি জানায় Task কত শতাংশ সময় Memory-র জন্য আটকে ছিল, আর একটা সংকেত বেছে নিতে হলে এটাই সেরা।

#Copy-on-Write আর fork

Lesson ৩.১-এ fork দেখেছ, যা Parent-এর Memory-র কপি নিয়ে একটা Child বানায়। সত্যিই কপি করলে খুব ধীর হতো। তাই:

  1. Kernel Page Table কপি করে, Frame না, আর দুই দিকেই প্রতিটা Writable Page-কে Read-only করে দেয়।
  2. Parent আর Child সব Frame ভাগ করে চলে, আর পড়তে কোনো খরচ নেই।
  3. কেউ লিখলে MMU Protection Fault তোলে। Kernel চিনে ফেলে এটা Copy-on-Write (CoW) Page, শুধু ওই Page-টার আলাদা কপি বানায়, যে লিখছে তাকে সেখানে দেখায়, আর Instruction আবার চালায়।

তাই fork আর তারপর exec, মানে প্রতিটা Shell যেভাবে Program চালায়, প্রায় কিছুই কপি করে না। বাকি থাকে শুধু Page Table-এর খরচ, যা Process যত বড় তত বাড়ে। বড় Process Helper চালাতে posix_spawn বা vfork নিলে ভালো।

System Design-এর উদাহরণ: Redis Persistence। BGSAVE fork করে। Child তার জমাট বাঁধা Memory-র দৃশ্য থেকে Snapshot লেখে, আর Parent সার্ভ করতে থাকে। চমৎকার নকশা, কিন্তু দুটো খরচ মানুষকে ধরে ফেলে:

  • fork Server-কে থামায়। Redis Command চালায় একটা Thread-এ, আর Page Table কপির সময় সেই Thread fork-এ আটকে থাকে। ২৪ GB-তে কপি করার Table প্রায় ৪৮ MB; সাধারণ Hardware-এ মোটামুটি প্রতি GB-তে ১০ ms-এর মতো, কিছু VM-এ অনেক ধীর। INFO stats-এ latest_fork_usec দেখা যায়।
  • Memory দ্বিগুণ হতে পারে। Save চলাকালে আপডেট হওয়া প্রতিটা Page কপি হয়। ৪০ GB Data-র ৩০% বদলালে লাগে প্রায় ৫২ GB। সবচেয়ে খারাপ অবস্থায় ৮০ GB। Data-র ঠিক সমান মাপের Machine-এ Save-এর সময় OOM Killer আসবে, সাধারণত সবচেয়ে খারাপ মুহূর্তে। THP প্রতি লেখায় ২ MB কপি করে অবস্থা আরও খারাপ করে।

এজন্যই Redis-কে কখনো RAM-এর ১০০% ধরে মাপা হয় না, আর এজন্যই Redis চায় vm.overcommit_memory=1: ডিফল্ট Heuristic-এ বড় fork প্রত্যাখ্যাত হতে পারে।

Pre-fork Server যতটা ভেবেছিলে তার চেয়ে কম কেন ভাগ করে, Copy-on-Write সেটাও বোঝায়। Python আর Ruby Object-এর ভেতরেই Reference Count বা Garbage Collection-এর দাগ রাখে, তাই একটা Object শুধু পড়লেও তার Page-এ লেখা হয়ে ভাগ ভেঙে যেতে পারে। এই কারণেই Instagram Python-এর Garbage Collector-এর একটা অংশ বন্ধ করে দিয়েছিল, আর gc.freeze() এসেছে এই সমস্যা থেকেই।

Copy-on-Write: কেউ না লেখা পর্যন্ত fork সব ভাগ করে চলে

Page Table কপি হয়, Frame হয় না। এজন্যই fork সস্তা; আবার Redis Snapshot-এর হঠাৎ Memory বাড়ার উৎসও এটাই।

পরিস্থিতি

মূল কৌশলটা, এক এক Page করে।

ParentPhysical RAMChild
Page ১
Page ২
Page ৩
Frame ১
Frame ২
Frame ৩
খালি
ব্যবহৃত Frame: 3
ধাপ 1 / 5
1

fork-এর আগে: Parent-এর তিনটা Page, তিনটা Frame-এ, সাধারণভাবে লেখা যায়।

#mmap আর Page Cache

#Page Cache

Page Cache হলো RAM-এর সেই অংশ, যেখানে Kernel File-এর Data রাখে। প্রতিটা সাধারণ read আর write এর ভেতর দিয়ে যায়, আর অলস প্রায় সব RAM এটাই নিয়ে নেয়, কারণ খালি RAM তোমার কোনো কাজে আসে না। Program চাইলেই সে Memory ফিরিয়ে দেয়।

  • Read: Hit হলে লাগে প্রায় এক মাইক্রোসেকেন্ড, ১০০ µs বা কয়েক মিলিসেকেন্ডের বদলে। ক্রমে পড়লে Read-ahead হয়, তাই অপেক্ষা খুব কমই করতে হয়।
  • Write: Data Cache-এ কপি হয়ে Dirty হয়, আর Disk-এ কিছু পৌঁছানোর আগেই write ফিরে আসে। Flusher Thread পরে লেখে, ডিফল্টে প্রায় ৩০ সেকেন্ড পুরনো হলে বা vm.dirty_background_ratio ছাড়ালে। vm.dirty_ratio ছাড়ালে Writer-কে অপেক্ষা করতে হয়, Write Latency লাফানোর চেনা কারণ।
  • টেকসই হওয়া: স্থায়ী Storage-এ পাঠাতে পারে শুধু fsync (বা fdatasync)।
  • মালিকানা: Cache Kernel-এর। write যা নিয়েছে, Process Crash-এ তা হারায় না; বিদ্যুৎ বিভ্রাট বা Kernel Panic-এ সব Dirty Page হারায়। Process Restart-এও Cache টিকে থাকে, তাই Page Cache-নির্ভর Database Restart-এর পরেও গরম থাকে, আর শুধু নিজের Buffer Pool-নির্ভর হলে ঠান্ডা।

#Kafka আর Database কেন এতে এত গুরুত্ব দেয়

Kafka JVM Heap ছোট রাখে, প্রায়ই এক অঙ্কের GB, আর বাকি RAM ছেড়ে দেয় Page Cache-কে। Message যোগ হয় সাধারণ Write দিয়ে, আর তাল মিলিয়ে চলা Consumer পড়ে এখনো Cache-এ থাকা Data, তাই Disk লাগে না। sendfile Cache করা Page সোজা Socket-এ দেয়, Application-এর ভেতর দিয়ে কপি ছাড়াই (তবে User Space-এ TLS হলে, Kernel TLS না থাকলে এই শর্টকাট চলে যায়)। Broker Restart-এও Cache টিকে থাকে, আর সে Garbage Collection-এর বাইরে। প্রতি Message-এ fsync না করে টেকসই থাকে Replication দিয়ে। ব্যর্থতার দিক: অনেক পিছিয়ে থাকা একজন Consumer Disk থেকে পুরনো Data টানে, আর নতুন Page ঠেলে বের করে দিয়ে সবাইকে ধীর করে। Messaging নিজে Chapter 30–32-এ।

Database-গুলো আলাদা আলাদা, আর Interviewer জিজ্ঞেস করতে পারেন কেন:

পদ্ধতিউদাহরণMemory কীভাবে ব্যবহার করেফল
নিজের Buffer Pool, Kernel-কে পাশ কাটিয়েO_DIRECT সহ MySQL InnoDBBuffer Pool প্রায়ই RAM-এর বেশিরভাগ নেয় (৫০–৮০%)কী সরবে তার পুরো নিয়ন্ত্রণ; Restart-এর পরে ঠান্ডা
নিজের Buffer, সাথে Page CachePostgreSQLshared_buffers সাধারণত RAM-এর ২৫%-এর মতো; বাকিটা OS Cache করেসহজ, কিছুটা দুবার Cache হয়; Planner-কে effective_cache_size জানায়; Restart-এর পরে আংশিক গরম
Map করা FileLMDB, Lucene (Elasticsearch, OpenSearch), Prometheusসব ঠিক করে KernelRead-এ সহজ আর দ্রুত; জায়গা রাখতে Heap মোট RAM-এর অনেক নিচে রাখতে হয়

এজন্যই “কত RAM লাগবে?” প্রশ্নটা আসলে “Working Set কত বড়?”। ২ TB File আর ২০০ GB গরম Data-র Database-এ লাগে প্রায় ২০০ GB Cache। Working Set Cache ছাড়িয়ে গেলে Performance ধীরে নামে না; Hit যখন Disk Read হয়ে যায়, তখন খাড়া পাহাড় থেকে পড়ার মতো নামে। Caching নিয়ে সাধারণ আলোচনা Chapter 23-এ।

#mmap

mmap Kernel-কে বলে, একটা File বা সাধারণ Memory-কে তোমার Address Space-এর একটা এলাকা বানিয়ে দাও।

ব্যবহারFlagকী করে
File-কে Memory হিসেবে পড়াMAP_SHARED বা MAP_PRIVATE, Read-onlyদরকার মতো Page Cache থেকে Page Fault হয়ে আসে
Process-এর মধ্যে Shared MemoryMAP_SHAREDসবাই একই Frame দেখে; File Mapping-এ বদল File-এও পৌঁছায়
File-এর আলাদা কপিMAP_PRIVATEলিখলে Copy-on-Write, File-এ যায় না; Program তার Data Segment এভাবেই লোড করে
বড় AllocationMAP_ANONYMOUSglibc-তে ডিফল্টে প্রায় ১২৮ KB থেকে malloc বড় Block এভাবেই পায়

এতে প্রতি Read-এ কপি আর System Call বাঁচে, আর RAM-এর চেয়ে বড় File-ও Address করা যায়। তবে Storage Engine-এর জন্য খরচগুলো বড়:

  • লুকানো আটকে যাওয়া। যেকোনো Pointer Dereference-এ Major Fault হতে পারে, আর কোনটায় হবে বোঝার উপায় নেই।
  • Error হয়ে যায় Signal। Disk Error আসে SIGBUS হিসেবে, Return Code হিসেবে না।
  • কী সরবে আর কখন লেখা হবে, তার নিয়ন্ত্রণ নেই। Database-কে নিশ্চিত করতে হয় Data Page-এর আগে Log Disk-এ পৌঁছেছে, অথচ Kernel Map করা Page যেকোনো সময় লিখতে পারে।
  • Shootdown। Map করা Page সরাতে Core-গুলোতে TLB Shootdown লাগতে পারে।

MongoDB তার mmap Engine, MMAPv1, ৪.২ Version-এ সরিয়ে দিয়েছে। ২০২২ সালের একটা পরিচিত Paper, “Are You Sure You Want to Use MMAP in Your Database Management System?”, এর বিপক্ষে যুক্তি দেয়। তবে LMDB আর Lucene-এর মতো Read-প্রধান System-এ এটা এখনো খুব ভালো চলে।

Page Cache: কেন Read দ্রুত, Write আগেই ফেরে, আর Kafka চলে

Linux ফাঁকা RAM-এ File-এর Data রেখে দেয়। আলাদা করে না বললে প্রতিটা Read আর Write এর ভেতর দিয়েই যায়। সাধারণ Read থেকে বিদ্যুৎ চলে যাওয়া পর্যন্ত সব দেখো।

পরিস্থিতি

প্রথম Read Disk-এর দাম দেয়। তারপর সবাই পায় RAM-এর গতি।

Application
Page CacheKernel, RAM-এ
Disk
read(fd, ৮ KB)
ধাপ 1 / 6
1

read System Call যায় Kernel-এ, যে আগে Page Cache-এ File-এর Page খোঁজে।

#Overcommit আর OOM Killer

Linux ডিফল্টে Overcommit করে: যতটা আছে, তার চেয়ে বেশি Virtual Memory দিয়ে দেয়, কারণ বেশিরভাগ Program যতটা নেয় তার চেয়ে অনেক কম ছোঁয়। vm.overcommit_memory নীতি ঠিক করে:

ModeআচরণAllocation কখন ব্যর্থ
০ (ডিফল্ট)Heuristic: স্পষ্ট বাড়াবাড়ি হলে তবেই না বলেকদাচিৎ; ঝামেলা আসে পরে, Page ছোঁয়ার সময়
১সবসময় হ্যাঁপ্রায় কখনো না; Redis-এর মতো বড় fork-এর জন্য ব্যবহার হয়
২CommitLimit ধরে কড়া হিসাব (Swap আর RAM-এর vm.overcommit_ratio%, vm.overcommit_kbytes না দিলে)সাথে সাথে: malloc Null দেয়, যা Code সামলাতে পারে

Page ছোঁয়ার সময় যদি আর কিছু Reclaim করার না থাকে, Page Fault মেটানো যায় না, আর malloc-এর ব্যর্থ হওয়ার সময় অনেক আগেই পার। তখন OOM Killer একটা Process বাছে, মূলত Memory ব্যবহার দেখে (/proc/PID/oom_score), oom_score_adj দিয়ে কমবেশি করে, যা -১০০০ (কখনো না) থেকে ১০০০ (সবার আগে), আর পাঠায় SIGKILL। Graceful Shutdown বলে কিছু নেই। dmesg বা journalctl -k-এ দেখবে Out of memory: Killed process।

  • যে মরে, সে দোষী না-ও হতে পারে। Leak করা একটা Sidecar তোমার Database-কে মারিয়ে দিতে পারে, তাই জরুরি Service-কে কম oom_score_adj দাও, আর বাকি সবকিছুতে Limit দাও।
  • Linux-এ malloc সফল হওয়া কিছুই প্রমাণ করে না। Null ফেরত সাবধানে সামলানো Code-ও পরে মরতে পারে।
  • Kernel-এর OOM Killer দেরিতে আসতে পারে, Machine কয়েক মিনিট হামাগুড়ি দেওয়ার পরে। systemd-oomd আর earlyoom চাপের সংকেত দেখে আগেই কাজ করে।

Overcommit আর OOM Killer: Kernel যে কথা রাখতে পারে না

Linux তার Memory-র চেয়ে বেশি Address Space দিয়ে দেয়, এই বাজি ধরে যে বেশিরভাগটা কেউ ছোঁবেই না। বাজি হারলে কী হয় দেখো, Host-এ আর Container-এ।

পরিস্থিতি

১৬ GB RAM-এর একটা Machine, Swap নেই। ডিফল্ট Heuristic Mode।

তোমার ProcessVirtual ১২ GB
KernelMemory Manager
অন্য Process
malloc ১২ GB
ধাপ 1 / 7
1

অনুরোধটা যুক্তিসঙ্গত মনে হয়, তাই Kernel হ্যাঁ বলে। শুধু পরিসরটা লিখে রাখে। মনে রেখো, এতে কোনো Frame খরচ হয় না।

#Memory-র সংখ্যা ঠিকভাবে পড়া

মাপকোথায় দেখবেকী গোনেচেনা ভুল
VSZ (Virtual Size)ps, top-এর VIRT, VmSizeMap করা সবকিছু, ব্যবহার হোক বা না হোকবড় মান দেখে সমস্যা ভাবা। Go, Java আর mmap-ব্যবহারকারী সবাই কোনো ক্ষতি ছাড়াই বিশাল মান দেখায়
Resident Memorytop-এর RES, /proc/PID/status-এর VmRSSএখন RAM-এ থাকা Page, Shared-সহসব Process যোগ করা। Shared Page প্রতি Process-এ একবার করে গোনা হয়, তাই যোগফল RAM-কেও ছাড়াতে পারে
Pss (Proportional)/proc/PID/smaps_rollupResident, তবে Shared Page ভাগীদারদের মধ্যে ভাগ করেপ্রতি Process-এর সৎ হিসাব। পড়তে একটু ধীর
VmSwap/proc/PID/statusএই Process-এর যত Page Swap-এResident ছোট দেখে এটা ভুলে যাওয়া
availablefree -hSwap ছাড়া যতটা ব্যবহার করা যাবেএর বদলে free পড়া

উদাহরণ: ৮টা Pre-fork Worker-এর প্রত্যেকে ২ GB Resident দেখায়। এর মানে ১৬ GB ব্যবহার না; ১.৫ GB Shared হলে আসল মোট প্রায় ১.৫ + ৮ × ০.৫ = ৫.৫ GB। Pss প্রত্যেককে দেখায় প্রায় ০.৬৯ GB।

আর free()-এর পরেও Resident বেশি থাকা মানেই Leak না: Allocator প্রায়ই ছেড়ে দেওয়া Memory আবার ব্যবহারের জন্য রেখে দেয়। Leak হলো সেই Resident Memory, যা একই Load-এ বাড়তেই থাকে।

#Container-এর Memory Limit

Container কোনো VM না: সে Host-এর Kernel-ই ব্যবহার করে, সাথে একটা cgroup Limit (cgroup v2-তে memory.max)। Group যে Frame ব্যবহার করে, Page Cache-সহ, Kernel তা Group-এর হিসাবে লেখে।

  • Limit পেরোলে: আগে Group-এর ভেতরে Reclaim, তারপর Group-এর ভেতরে OOM Kill। Exit Code ১৩৭ (১২৮ + ৯), Kubernetes-এ OOMKilled। Host-এ Memory খালি থাকলেও কাজে আসে না। CPU Limit শুধু ধীর করে, কিন্তু Memory ওভাবে আটকানো যায় না, তাই Memory Limit মেরে ফেলে। memory.high একটা নরম সীমা, যা চাপে Group-কে ধীর করে।
  • Page Cache গোনা হয়, তবে ফিরিয়ে নেওয়া যায়। বড় File পড়া Container-এর Usage Limit-এর কাছে দেখালেও সে পুরো সুস্থ হতে পারে। দেখো Working Set (Usage বাদ Inactive File Cache), kubelet-ও তাই দেখে। docker stats-ও Inactive Cache বাদ দেয়।
  • free আর /proc/meminfo দেখায় Host-এর Memory, Container-এর ভেতর থেকেও। তার বদলে পড়ো /sys/fs/cgroup/memory.current আর memory.max।
  • Runtime-কে Limit জানতে হবে। আধুনিক JVM Container চেনে, আর ডিফল্টে Max Heap রাখে Limit-এর ২৫% (-XX:MaxRAMPercentage)। কিন্তু Heap মানেই পুরো Process না: Metaspace, Thread Stack, Direct Buffer আর JIT Code সব এর বাইরে। ১ GB Limit-এ ১ GB Heap দিলে মরবেই। Go-তে আছে GOMEMLIMIT, Node-এ --max-old-space-size।
  • Kubernetes-এর Request আর Limit। Request লাগে Scheduling-এ, Scheduler-এর হিসাবের খাতার বরাদ্দ। Limit Kernel প্রয়োগ করে। Memory-র Request আর Limit সমান রাখলে Node-এ Overcommit হয় না, তাই Stateful কাজে এটাই চালু পরামর্শ।

#NUMA নিয়ে ছোট্ট কথা

একাধিক Socket-এর Server-এ প্রতিটা Socket-এর নিজের RAM থাকে। অন্য Socket-এর Memory-তে পৌঁছাতে সময় বেশি লাগে, মোটামুটি ১.৩ থেকে ২ গুণ। এটাই NUMA (Non-Uniform Memory Access)। Linux ডিফল্টে Page বসায় সেই Node-এ, যে Thread প্রথম ছোঁয়; তাই Startup-এর একটা Thread বড় Buffer সাজালে পুরোটা এক Node-এই বসে যেতে পারে। চেনা ব্যর্থতা: একটা Database এক Node-এর RAM ছাড়িয়ে যায়, সেই Node ফুরিয়ে যায়, আর অন্য Node খালি থাকতেও Kernel Swap বা Reclaim করে। দেখতে numactl --hardware আর numastat; ঠিক করতে Memory Interleave (numactl --interleave=all) করো বা Process-কে Node-এ বেঁধে দাও। ছোট Cloud VM-এ সাধারণত একটাই Node দেখায়, বড় Instance-এ নাও হতে পারে।

#System Design-এ এর মানে

সিদ্ধান্তMemory-র ব্যবস্থা কী বলে
Cache বা Database Host-এর মাপWorking Set ধরে মাপো, আর সেটা RAM-এ রাখো। Memory থেকে পড়ে যাওয়া মানে খাড়া পাহাড়, ঢাল না
Redis বা fork-নির্ভর যেকোনো PersistenceCopy-on-Write-এর জন্য জায়গা রাখো, ভারী লেখায় ২ গুণ পর্যন্ত; THP বন্ধ রাখো; fork-এর সময় দেখো
Kafka আর Log ধরনের Storeছোট Heap, বড় Page Cache, ক্রমিক I/O, আর পিছিয়ে পড়া Consumer-এর দিকে নজর
টেকসই থাকার দাবিসফল write মানে টেকসই না। Commit মানে fsync বা Replica
Stateless Service-এর ব্যর্থতার ধরনখুঁড়িয়ে চলা Node-এর চেয়ে Swap ছাড়া OOM Kill আর Restart ভালো
Container LimitLimit = Heap + Non-heap + কিছু বাড়তি; Limit-এর আগেই Alert; SIGKILL ধরে নাও
Deployment আর Restartঠান্ডা Page ধরে নাও। গরম করে নাও, Load Balancer-এ Slow Start দাও
অনেক ছোট Process নাকি ThreadThread Page Table ভাগ করে আর TLB গরম রাখে; Process ব্যর্থতা আলাদা রাখে, তবে Memory আর বদলের খরচ বেশি

#Production-এর চেনা সমস্যা

লক্ষণসম্ভাব্য কারণকী দেখবেসমাধান
Exit Code ১৩৭, OOMKilledContainer Limit পেরিয়েছেkubectl describe pod, memory.eventsLimit বাড়াও বা Leak ঠিক করো; Runtime-এর সীমা দাও
Process উধাও, Application Log-এ কিছু নেইHost-এর OOM Killerdmesg, journalctl -kপাশের Process-এ Limit; oom_score_adj; RAM বাড়াও
সব ধীর, CPU-তে I/O WaitThrashingvmstat 1 (si, so), PSILoad কমাও, RAM বাড়াও
CPU না বাড়লেও p99 লাফায়Major Fault বা Direct Reclaimpidstat -r, sar -Bগরম Data-য় mlock, খালি জায়গা রাখো, আগেই ছুঁয়ে রাখো
THP চালু থাকলে হঠাৎ LatencyCompaction/proc/vmstat-এর compact_stallওই কাজের জন্য THP madvise বা বন্ধ
প্রতি Save-এ নিয়মিত Latency লাফানোfork-এর বিরতিlatest_fork_usecSnapshot নাও Replica থেকে; ছোট Instance; THP বন্ধ
Restart-এর পরে কয়েক মিনিট ধীরঠান্ডা Page Cache আর BufferDisk Read বেশি, তারপর কমেWarm Up; Slow Start
RAM খালি থাকতেও fork: Cannot allocate memoryOvercommit নীতি, বা Process সংখ্যার সীমাvm.overcommit_memory, ulimit -uসেই ক্ষেত্রে vm.overcommit_memory=1, বা posix_spawn
Batch Job বা Backup-এ Live Traffic-এর ক্ষতিগরম Page Cache থেকে বেরিয়ে গেছেcachestat, Hit Ratio কমছেposix_fadvise(DONTNEED), O_DIRECT, আলাদা Host
ঝাঁকে ঝাঁকে Write Latency লাফায়Dirty Page অনেক বেশি/proc/meminfo-এর Dirtyvm.dirty_* সীমা কমাও; দ্রুত Disk

#Interview-এর কোণ

  • “Virtual Memory কী?” শুরু করো অনুবাদ আর Isolation দিয়ে। Swap-কে বলো শুধু ঐচ্ছিক একটা সংযোজন। এই একটা উত্তরেই Candidate আলাদা হয়ে যায়।
  • “Kafka এত দ্রুত কেন?” ক্রমিক I/O, Page Cache আর sendfile, কোনো জাদু না।
  • “Redis-এর বাড়তি Memory কেন লাগে?” fork আর Copy-on-Write।
  • “আমার Container মারা গেল, অথচ Node-এ Memory খালি।” cgroup Limit, Host-এর Memory না।
  • “write-এর পরে Data কি নিরাপদ?” Process Crash থেকে হ্যাঁ, fsync না হওয়া পর্যন্ত বিদ্যুৎ বিভ্রাট থেকে না।
  • “Process বদলের চেয়ে Thread বদল সস্তা কেন?” Page Table একই, তাই TLB কাজের থাকে।

#নিজে করে দেখো

সবই যেকোনো Linux Machine-এ চলবে, Windows Subsystem for Linux-সহ। শুধু দেখার Command Production-এও নিরাপদ; drop_caches-এর লাইনটা Production-এর জন্য না।

# Page size and your memory picture
getconf PAGESIZE
free -h

# A process's virtual layout (code, heap, libraries, stack)
cat /proc/self/maps

# Virtual size vs resident memory for your shell
grep -E 'VmSize|VmRSS|VmSwap' /proc/$$/status

# Top memory users, with minor and major fault counts
ps -eo pid,comm,vsz,rss,min_flt,maj_flt --sort=-rss | head

# Swap activity and memory pressure: si and so should stay near 0
vmstat 1 5
cat /proc/pressure/memory

# Settings from this lesson
sysctl vm.swappiness vm.overcommit_memory vm.dirty_ratio
cat /sys/kernel/mm/transparent_hugepage/enabled

# NUMA layout, if numactl is installed
numactl --hardware

Demand Paging নিজের চোখে দেখো। এই Python Script ১ GB Map করে, আর ছোঁয়ার আগে ও পরে Resident Memory দেখায়:

import mmap, re, resource

def resident():
    text = open('/proc/self/status').read()
    return re.search(r'VmRSS:\s+(\d+)', text).group(1) + ' kB'

def minor_faults():
    return resource.getrusage(resource.RUSAGE_SELF).ru_minflt

size = 1024 * 1024 * 1024
print('start          ', resident(), minor_faults())
m = mmap.mmap(-1, size)                  # anonymous mapping: 1 GB of address space
print('after mapping  ', resident(), minor_faults())
for offset in range(0, size, 4096):      # touch one byte in each 4 KB page
    m[offset] = 1
print('after touching ', resident(), minor_faults())

Map করলে প্রায় কিছুই বদলায় না। ছুঁলে Resident Memory ১ GB বাড়ে, সাথে প্রায় ২,৬২,০০০টা Minor Fault (THP যদি ২ MB Page দেয় তবে অনেক কম, আর সেটা চালু কি না বোঝার এটা ভালো উপায়)।

Page Cache নিজের চোখে দেখো। দ্বিতীয়বার পড়া অনেক দ্রুত:

dd if=/dev/zero of=$HOME/big.bin bs=1M count=1024  # create a 1 GB file
sync
echo 3 | sudo tee /proc/sys/vm/drop_caches          # empty the cache (not on production)
time cat $HOME/big.bin > /dev/null                  # slow: from disk
time cat $HOME/big.bin > /dev/null                  # fast: page cache
free -h                                             # buff/cache grew
rm $HOME/big.bin

Container Limit-এ মৃত্যু দেখো। ১০০ MB Limit-এর নিচে Python ২০০ MB-র একটা String বানাতে যায়:

docker run --rm -m 100m --memory-swap 100m python:3-alpine \
  python -c "x = b'a' * (200 * 1024 * 1024); print('survived')"
echo "exit code: $?"        # 137, with no Python traceback

#সারসংক্ষেপ

মূল কথা

  • Virtual Memory মানে প্রতিটা Process-এর জন্য Address অনুবাদ, “Disk-কে RAM বানানো” না। Swap ঐচ্ছিক, আর অনেক Server একদম ছাড়াই চলে।
  • Address ভাগ হয় Page নম্বর আর Offset-এ। Page Table Page-কে Frame-এ নিয়ে যায়, Offset একই থাকে। MMU এটা করে প্রতিটা Access-এ।
  • Page Table কয়েক স্তরের, কারণ লম্বা একটা Table হতো প্রতি Process-এ ৫১২ GB। সাহায্য ছাড়া একটা খোঁজে ৪টা Read, আর Data।
  • TLB অনুবাদ Cache করে। তার Reach হলো Entry × Page-এর মাপ, এজন্যই Huge Page বড় Heap-এ কাজে লাগে। THP আবার হঠাৎ Latency-ও আনতে পারে।
  • Page Fault সাধারণত স্বাভাবিক। Minor প্রায় এক মাইক্রোসেকেন্ড, Major NVMe-তে প্রায় ১০০ µs বা Hard Disk-এ কয়েক মিলিসেকেন্ড, আর অবৈধ হলে SIGSEGV।
  • Reclaim Clean Page বিনা খরচে ফেলে, Dirty Page লেখে, Anonymous Page পাঠায় Swap-এ। Working Set না আঁটলে হয় Thrashing। swappiness একটা পছন্দ, সীমারেখা না।
  • Copy-on-Write fork-কে সস্তা করে, কিন্তু ভারী লেখার মধ্যে Redis Snapshot-এ দ্বিগুণ পর্যন্ত Memory লাগতে পারে।
  • Page Cache অলস সব RAM ব্যবহার করে। Disk-এর আগেই write সফল হয়; Process Crash-এ Data থাকে, বিদ্যুৎ গেলে থাকে না। Kafka আর অনেক Database এর ওপর নির্ভর করে। mmap সেই Frame-ই ভাগ করে।
  • Linux Overcommit করে। বাজি হারলে OOM Killer SIGKILL পাঠায়; Container-এ সেটা হয় Limit-এই, Exit Code ১৩৭, Host-এ যতই খালি থাকুক।
  • free না, available পড়ো। VSZ বেশিরভাগই অর্থহীন। Resident Memory সব Process যোগ করলে মেলে না; Pss মেলে।