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 ব্যবহার করে, তবু কখনো ধাক্কা লাগে না।
একই Machine-এ দুটো Program চলছে, আর দুটোই নিজের Data রাখে 0x5000 Address-এ। অনুবাদ না থাকলে একে অন্যের Data মুছে দিত। Virtual Memory এই সমস্যাটাই সমাধান করে।
#কেন দরকার
| সুবিধা | কী দেয় | বাস্তবে এর মানে |
|---|---|---|
| Process Isolation | Process শুধু নিজের 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 Paging | Page প্রথম ছোঁয়ার সময়ই কেবল 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-এর কৌশল |
| User | User 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-এ:
- ভাগ করো: Page নম্বর
0x00403, Offset0xA7C। - খোঁজো: Table-এর
0x403নম্বর Entry পড়ো। এটা Array Index, খোঁজাখুঁজি না। Frame0x1F2, Present। - Offset রেখে দাও: Frame আর Page একই মাপের, তাই Offset বদলায় না।
- জোড়া দাও:
0x1F2-এর পরেA7C, মানে Physical Address0x1F2A7C।
পরের 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 ৪ 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 এই পথেই যায়।
Address-টা
Virtual Address
0x00403A7C
Page নম্বর
0x403
Offset
0xA7C
TLB (ছোট Cache-এর কয়েকটা Entry)
| Page | Frame | Flag |
|---|---|---|
| 0x403 | 0x1F2 | R W |
| 0x7A1 | 0x09C | R |
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-তে নেই:
Kernel যাচাই করে
MMU Fault হওয়া Address নিয়ে Kernel-এ ঢোকে। Kernel এই Process-এর বৈধ এলাকার তালিকা দেখে। কোনো এলাকায় Address-টা না পড়লে, বা Access অনুমতি না মানলে, সে
SIGSEGVপাঠায়, আর এখানেই শেষ।Page কোথা থেকে আসবে তা খোঁজে
নতুন Heap Page হলে Zero ভরা Memory, File-এর Page হলে Page Cache বা File, আগে সরানো Page হলে Swap Slot।
দরকার হলে Page আনে
একটা খালি Frame নেয়, প্রয়োজনে অন্য Page সরিয়ে, তারপর ভরে। Data Disk-এ থাকলে Thread ঘুমায়, অন্য Thread চলে।
Page Table আপডেট করে
Entry-তে এখন Frame নম্বর আছে, Present Bit চালু।
Instruction আবার চলে
CPU একই Instruction আবার গোড়া থেকে চালায়। Program জানতেই পারে না যে তাকে থামানো হয়েছিল।
#Minor, Major আর অবৈধ
| Minor Fault | Major Fault | অবৈধ Access | |
|---|---|---|---|
| কী | Disk ছাড়াই Page পাওয়া যায় | Data আনতে হবে Storage থেকে | বৈধ Mapping নেই, বা অনুমতি নেই |
| উদাহরণ | malloc-এর Memory-তে প্রথম ছোঁয়া; Page Cache-এ আগে থেকে থাকা File Page; Copy-on-Write-এর কপি | Cache-এ না আসা Map করা File; Swap-এ যাওয়া Page; ঠান্ডা অবস্থায় চালু হওয়ার সময়ের Code Page | Null 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বাmlockallPage-কে 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।
বড় 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-এ হুবহু একই কপি আগেই আছে।
চারটা 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 বানায়। সত্যিই কপি করলে খুব ধীর হতো। তাই:
- Kernel Page Table কপি করে, Frame না, আর দুই দিকেই প্রতিটা Writable Page-কে Read-only করে দেয়।
- Parent আর Child সব Frame ভাগ করে চলে, আর পড়তে কোনো খরচ নেই।
- কেউ লিখলে 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 করে।
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 InnoDB | Buffer Pool প্রায়ই RAM-এর বেশিরভাগ নেয় (৫০–৮০%) | কী সরবে তার পুরো নিয়ন্ত্রণ; Restart-এর পরে ঠান্ডা |
| নিজের Buffer, সাথে Page Cache | PostgreSQL | shared_buffers সাধারণত RAM-এর ২৫%-এর মতো; বাকিটা OS Cache করে | সহজ, কিছুটা দুবার Cache হয়; Planner-কে effective_cache_size জানায়; Restart-এর পরে আংশিক গরম |
| Map করা File | LMDB, Lucene (Elasticsearch, OpenSearch), Prometheus | সব ঠিক করে Kernel | Read-এ সহজ আর দ্রুত; জায়গা রাখতে 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 Memory | MAP_SHARED | সবাই একই Frame দেখে; File Mapping-এ বদল File-এও পৌঁছায় |
| File-এর আলাদা কপি | MAP_PRIVATE | লিখলে Copy-on-Write, File-এ যায় না; Program তার Data Segment এভাবেই লোড করে |
| বড় Allocation | MAP_ANONYMOUS | glibc-তে ডিফল্টে প্রায় ১২৮ 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-এর গতি।
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।
অনুরোধটা যুক্তিসঙ্গত মনে হয়, তাই Kernel হ্যাঁ বলে। শুধু পরিসরটা লিখে রাখে। মনে রেখো, এতে কোনো Frame খরচ হয় না।
#Memory-র সংখ্যা ঠিকভাবে পড়া
| মাপ | কোথায় দেখবে | কী গোনে | চেনা ভুল |
|---|---|---|---|
VSZ (Virtual Size) | ps, top-এর VIRT, VmSize | Map করা সবকিছু, ব্যবহার হোক বা না হোক | বড় মান দেখে সমস্যা ভাবা। Go, Java আর mmap-ব্যবহারকারী সবাই কোনো ক্ষতি ছাড়াই বিশাল মান দেখায় |
| Resident Memory | top-এর RES, /proc/PID/status-এর VmRSS | এখন RAM-এ থাকা Page, Shared-সহ | সব Process যোগ করা। Shared Page প্রতি Process-এ একবার করে গোনা হয়, তাই যোগফল RAM-কেও ছাড়াতে পারে |
Pss (Proportional) | /proc/PID/smaps_rollup | Resident, তবে Shared Page ভাগীদারদের মধ্যে ভাগ করে | প্রতি Process-এর সৎ হিসাব। পড়তে একটু ধীর |
VmSwap | /proc/PID/status | এই Process-এর যত Page Swap-এ | Resident ছোট দেখে এটা ভুলে যাওয়া |
available | free -h | Swap ছাড়া যতটা ব্যবহার করা যাবে | এর বদলে 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-নির্ভর যেকোনো Persistence | Copy-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 Limit | Limit = Heap + Non-heap + কিছু বাড়তি; Limit-এর আগেই Alert; SIGKILL ধরে নাও |
| Deployment আর Restart | ঠান্ডা Page ধরে নাও। গরম করে নাও, Load Balancer-এ Slow Start দাও |
| অনেক ছোট Process নাকি Thread | Thread Page Table ভাগ করে আর TLB গরম রাখে; Process ব্যর্থতা আলাদা রাখে, তবে Memory আর বদলের খরচ বেশি |
#Production-এর চেনা সমস্যা
| লক্ষণ | সম্ভাব্য কারণ | কী দেখবে | সমাধান |
|---|---|---|---|
Exit Code ১৩৭, OOMKilled | Container Limit পেরিয়েছে | kubectl describe pod, memory.events | Limit বাড়াও বা Leak ঠিক করো; Runtime-এর সীমা দাও |
| Process উধাও, Application Log-এ কিছু নেই | Host-এর OOM Killer | dmesg, journalctl -k | পাশের Process-এ Limit; oom_score_adj; RAM বাড়াও |
| সব ধীর, CPU-তে I/O Wait | Thrashing | vmstat 1 (si, so), PSI | Load কমাও, RAM বাড়াও |
| CPU না বাড়লেও p99 লাফায় | Major Fault বা Direct Reclaim | pidstat -r, sar -B | গরম Data-য় mlock, খালি জায়গা রাখো, আগেই ছুঁয়ে রাখো |
| THP চালু থাকলে হঠাৎ Latency | Compaction | /proc/vmstat-এর compact_stall | ওই কাজের জন্য THP madvise বা বন্ধ |
| প্রতি Save-এ নিয়মিত Latency লাফানো | fork-এর বিরতি | latest_fork_usec | Snapshot নাও Replica থেকে; ছোট Instance; THP বন্ধ |
| Restart-এর পরে কয়েক মিনিট ধীর | ঠান্ডা Page Cache আর Buffer | Disk Read বেশি, তারপর কমে | Warm Up; Slow Start |
RAM খালি থাকতেও fork: Cannot allocate memory | Overcommit নীতি, বা 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-এর Dirty | vm.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মেলে।
আগে নিজে উত্তর দাও, তারপর ব্যাখ্যাটা পড়ো। ঠিক হলেও পড়ো, কারণ ভুল Option-গুলো কেন ভুল, সেটাও ওখানে আছে।
নিজেকে যাচাই করোপ্রশ্ন
একজন Engineer বলল: আমাদের Server-এ Swap নেই, তাই আমরা Virtual Memory ব্যবহার করছি না। এই কথায় ভুল কোথায়?
নিজেকে যাচাই করোপ্রশ্ন
দুটো Process একটা করে Pointer Print করল, দুটোতেই 0x7ffd0000। একজন সেখানে লিখল। অন্যজন কী দেখবে?
নিজেকে যাচাই করোপ্রশ্ন
Page ৪ KB। Virtual Address-এর কত Bit Offset, আর ৩২-Bit Address Space-এ কতগুলো Page?
নিজেকে যাচাই করোপ্রশ্ন
Page ৪ KB। Virtual Address 0x00ABC123 আছে Page 0xABC-তে, যা Frame 0x042-তে Map করা। Physical Address কত?
নিজেকে যাচাই করোপ্রশ্ন
x86-64 একটা লম্বা Table-এর বদলে Page Table-এর Tree কেন ব্যবহার করে?
নিজেকে যাচাই করোপ্রশ্ন
চার স্তরের Paging-এ একটা Load Instruction TLB-তে Miss হলো, আর Page Present। CPU Cache বাদ দিলে মোট কয়টা Memory Read লাগবে, আর Kernel কি চলবে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Service-এর ৬৪ GB Heap, আর সে তার মধ্যে এলোমেলো Pointer ধরে লাফায়। perf দেখাচ্ছে অনেক TLB Miss, কোনো Fault নেই। সবচেয়ে বেশি কিসে লাভ হবে?
নিজেকে যাচাই করোপ্রশ্ন
Context Switch আর TLB নিয়ে কোন দুটো কথা ঠিক? দুটো বেছে নাও।
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
একটা Multi-threaded Proxy প্রতি Request-এ mmap আর munmap দিয়ে বড় Buffer নেয় আর ছাড়ে। ৩২ Core-এর পরে সে আর ভালো Scale করে না, আর অনেক Core-এ Interrupt সামলাতেই সময় যায়। সম্ভাব্য কারণ কী?
নিজেকে যাচাই করোপ্রশ্ন
একটা Program ৮ GB malloc করে, কিন্তু লেখে মাত্র ১ GB-তে। Virtual Size আর Resident Memory-র কী হবে?
নিজেকে যাচাই করোপ্রশ্ন
এগুলোর মধ্যে কোনটা Major Page Fault?
নিজেকে যাচাই করোপ্রশ্ন
NVMe Machine-এ Page Fault-এর দাম নিয়ে কোন কথাটা ঠিক?
নিজেকে যাচাই করোপ্রশ্ন
প্রতিটা Deployment-এর পরে কয়েক মিনিট একটা Service-এর p99 ৫ ms থেকে ৪০ ms হয়ে যায়, CPU কম, Error নেই। কোন দুটো ব্যাখ্যা আর সমাধান মানায়? দুটো বেছে নাও।
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
Memory-র চাপে Linux কোন Page সবচেয়ে সস্তায় ফিরিয়ে নিতে পারে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Server-এ Swap নেই, অলস Heap অনেক। সে বারবার Page Cache ফেলে দেয়, আর File Read ধীর। অল্প Swap দিলে কেন লাভ হতে পারে?
নিজেকে যাচাই করোপ্রশ্ন
একজন Engineer vm.swappiness=10 দিল, ভেবে যে RAM-এর ১০% খালি থাকলে তবেই Swap শুরু হবে। এই Setting আসলে কী করে?
নিজেকে যাচাই করোপ্রশ্ন
vmstat-এ si আর so প্রতি সেকেন্ডে হাজার হাজার, I/O Wait বেশি, Throughput ধসে পড়েছে। কোনটা আসল কারণ ধরে?
নিজেকে যাচাই করোপ্রশ্ন
Linux Server-এ সত্যিকারের Memory সমস্যার সবচেয়ে ভালো ইঙ্গিত কোন দুটো? দুটো বেছে নাও।
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
২০ GB-র একটা Process fork করল, আর Child সাথে সাথে exec ডাকল। কতটা Memory কপি হলো?
নিজেকে যাচাই করোপ্রশ্ন
৬৪ GB Machine-এ ৪০ GB-র একটা Redis ভারী লেখার মধ্যে Background Save চলাকালে OOM Killer-এর হাতে মরল। কৌশলটা কী?
নিজেকে যাচাই করোপ্রশ্ন
Transparent Huge Page Redis Snapshot-কে আরও দামি করে কেন?
নিজেকে যাচাই করোপ্রশ্ন
একটা Pre-fork Python Server-এর ৮টা Worker একটা বড় Read-only Data Set ভাগ করে। সময়ের সাথে তাদের Resident Memory বাড়ে, যদিও ওই Data-তে কেউ লেখে না। কেন?
নিজেকে যাচাই করোপ্রশ্ন
একটা Application write ডাকল, সফল পেল, তারপর kill -9-এ Process মরল। একটু পরে, Writeback হওয়ার আগেই, Machine-এর বিদ্যুৎ গেল। কোন কথাটা ঠিক?
নিজেকে যাচাই করোপ্রশ্ন
free -h দেখাচ্ছে total 62Gi, used 18Gi, free 312Mi, buff/cache 44Gi, available 43Gi, Swap 0। সহকর্মী এখনই RAM বাড়াতে চায়। তুমি কী বলবে?
নিজেকে যাচাই করোপ্রশ্ন
Kafka JVM Heap ছোট রেখে বেশিরভাগ RAM কেন Kernel-কে ছেড়ে দেয়, তার ব্যাখ্যা কোন দুটো? দুটো বেছে নাও।
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
একটা Kafka Broker-এর Latency ঠিকই ছিল, যতক্ষণ না কেউ এক সপ্তাহ আগের একটা Topic আবার পড়া শুরু করল। তারপর সব Client ধীর। কী হচ্ছে?
নিজেকে যাচাই করোপ্রশ্ন
অনেক বড় Relational Database তাদের Data File-এ mmap এড়িয়ে চলে কেন?
নিজেকে যাচাই করোপ্রশ্ন
ডিফল্ট Setting-এর Linux-এ, ১৬ GB Machine-এ ৬ GB আগেই ব্যবহার হচ্ছে, তবু ১২ GB-র malloc সফল হলো। পরে Process মারা গেল। কোন কথাটা ঠিক?
নিজেকে যাচাই করোপ্রশ্ন
৫১২ MB Limit-এর একটা Container Exit Code ১৩৭ দিয়ে মারা গেল, অথচ Host-এ ৪০ GB খালি। কোন দুটো কথা ঠিক? দুটো বেছে নাও।
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
২ GB Container-এ একটা Java Service চলে -Xmx2g দিয়ে, আর বারবার OOMKilled হয়, যদিও Heap-এর Graph সুস্থ। সমস্যা কী?