Overlay FS Lab
Overlay FS Lab
নিজের হাতে real `mount -t overlay` চালিয়ে lowerdir/upperdir/merged/workdir-এর priority resolution, copy-up, আর whiteout দেখা, তারপর সেই একই algorithm Python-এ from scratch লিখে যাচাই করা — শেষে একটা mini Dockerfile-স্টাইল image builder দিয়ে সরাসরি Docker layer-এর সাথে যোগসূত্র বসানো।
কেন এই প্রজেক্ট
virtualization-cloud/overlay-filesystems-image-layers লেসনে দেখেছেন
merged view কীভাবে গণনা হয়, read কখন copy করে না, আর প্রথম write কেন
পুরো ফাইল copy-up করে — কিন্তু এসব পড়ে “মেনে নেওয়া” আর নিজের হাতে
ls upperdir/ চালিয়ে সেই কপি হওয়া ফাইলটা চোখে দেখা সম্পূর্ণ ভিন্ন
জিনিস। এই প্রজেক্টের প্রথম অংশ আপনাকে একটা real Linux মেশিনে (বা VM-এ)
mount -t overlay চালাতে বাধ্য করবে, যাতে whiteout-এর character-device
marker-টা কোনো abstract বাক্য না থেকে আপনার টার্মিনালে ls -la-র
আউটপুটে সরাসরি দেখা একটা জিনিস হয়ে যায়।
কিন্তু এই সাইটের sandbox পরিবেশ (Darwin/macOS) real overlay mount করতে পারে না — Linux kernel module এখানে নেই। তাই দ্বিতীয় অংশে ঠিক একই priority-resolution আর whiteout-hiding নিয়ম Python-এ from scratch লিখে, এই sandbox-এই চালিয়ে, বাস্তবিক আউটপুট দিয়ে যাচাই করা হবে। শেষে একটা ছোট “image builder” বানিয়ে সরাসরি দেখানো হবে — Docker যখন একটা Dockerfile থেকে layer stack বানায়, সেটা আক্ষরিক অর্থে এই একই resolution algorithm-এর ওপর দাঁড়িয়ে।
দুটো ট্র্যাক আলাদা রাখা ইচ্ছাকৃত: ট্র্যাক A (real mount কমান্ড)
শুধু আপনার নিজের Linux পরিবেশে চালানো যাবে, এখানে শুধু প্রত্যাশিত
আউটপুট দেখানো হচ্ছে। ট্র্যাক B (Python simulation) এই sandbox-এই
সত্যিই চালানো হয়েছে, নিচের প্রতিটা আউটপুট copy করা প্রকৃত রান থেকে।
কোনটা কোন ট্র্যাকের, সেটা প্রতি ধাপে স্পষ্ট লেখা থাকবে — বাস্তব সিস্টেম
আর সিমুলেশন গুলিয়ে ফেলা এই বিষয়ে সবচেয়ে সাধারণ ভুল।
ধাপে ধাপে
১. Setup — overlapping lowerdir তৈরি [ট্র্যাক A, real Linux/VM]
তিনটা lowerdir বানান যাদের কিছু filename ইচ্ছাকৃতভাবে overlap করে, যাতে priority resolution পরীক্ষা করা যায়:
mkdir -p lab/{lower0-base,lower1-mid,lower2-top,upperdir,workdir,merged}
cd lab
echo "base motd" > lower0-base/etc-motd.txt
echo "base config" > lower0-base/app-config.txt
echo "base readme" > lower0-base/app-readme.txt
echo "middle config" > lower1-mid/app-config.txt # base-কে override করবে
echo "middle extra" > lower1-mid/app-extra.txt
echo "top config" > lower2-top/app-config.txt # সবচেয়ে বেশি অগ্রাধিকার
echo "top new" > lower2-top/app-new.txt
লক্ষ করুন app-config.txt তিনটা layer-েই আছে, ভিন্ন content নিয়ে —
resolution algorithm ঠিকভাবে কাজ করলে merged view-তে শুধু সবচেয়ে
বেশি-অগ্রাধিকারের ভার্সনটাই (top) দেখা যাবে।
Checkpoint: lab/ ডিরেক্টরিতে তিনটা lowerdir, একটা খালি upperdir,
একটা খালি workdir — সব প্রস্তুত, merged/ এখনো mount হয়নি।
২. Real overlay mount — priority resolution হাতে দেখা [ট্র্যাক A, real Linux/VM]
আপনার নিজের Linux মেশিনে (বা multipass/VirtualBox/Docker-in-Docker
দিয়ে বানানো একটা Linux VM-এ, root privilege লাগবে):
sudo mount -t overlay overlay \
-o lowerdir=lab/lower2-top:lab/lower1-mid:lab/lower0-base,\
upperdir=lab/upperdir,workdir=lab/workdir \
lab/merged
lowerdir= তালিকায় বাম দিকেরটাই সর্বোচ্চ অগ্রাধিকার পায় — তাই
lower2-top সবার আগে লেখা। এখন:
ls lab/merged/
cat lab/merged/app-config.txt
আপনার real Linux পরিবেশে প্রত্যাশিত আউটপুট:
$ ls lab/merged/
app-config.txt app-extra.txt app-new.txt app-readme.txt etc-motd.txt
$ cat lab/merged/app-config.txt
top config
পাঁচটা ফাইলই merged view-তে একসাথে দেখা যাচ্ছে (directory merge
recursively হয়), কিন্তু app-config.txt-এর content শুধু
lower2-top-এর — যদিও নামটা তিনটা layer-েই ছিল। এটাই লেসনের প্রথম
নিয়ম: সবচেয়ে উঁচু স্তর যেখানে নামটা পাওয়া যায়, সেটাই জেতে, বাকিগুলো
shadow হয়ে যায়, মোছে না।
Checkpoint (ট্র্যাক A): নিজের Linux পরিবেশে ঠিক এই কমান্ড চালিয়ে
app-config.txt-এর content top config দেখা — তারপর lower2-top/
থেকে ফাইলটা সরিয়ে (mv) আবার cat করে দেখুন resolution কীভাবে
পরের-অগ্রাধিকার layer-এ পড়ে যায়।
৩. Write ট্রিগার করে copy-up — টুকরো বদলে পুরো ফাইল কপি [ট্র্যাক A, real Linux/VM]
app-readme.txt শুধু lower0-base-এ আছে, upperdir-এ এখনো নেই। এখন
copy-up-এর আগে-পরে upperdir পরীক্ষা করুন:
echo "--- copy-up-এর আগে ---"
ls lab/upperdir/ # খালি বা app-config/extra/new নেই
echo "readme-তে এক লাইন যোগ করছি"
echo "extra line" >> lab/merged/app-readme.txt
echo "--- copy-up-এর পরে ---"
ls lab/upperdir/
diff lab/lower0-base/app-readme.txt lab/upperdir/app-readme.txt
প্রত্যাশিত আউটপুট:
--- copy-up-এর আগে ---
(কিছুই নেই, app-readme.txt upperdir-এ নেই)
--- copy-up-এর পরে ---
app-readme.txt
$ diff lab/lower0-base/app-readme.txt lab/upperdir/app-readme.txt
1a2
> extra line
গুরুত্বপূর্ণ অংশটা: শুধু এক লাইন যোগ করা হয়েছিল, কিন্তু
app-readme.txt-এর সম্পূর্ণ মূল content আগে lower0-base থেকে
upperdir-এ কপি হয়েছে (diff-এর 1a2 লাইনটাই প্রমাণ — পুরনো লাইন
অক্ষত আছে, শুধু নতুনটা যোগ হয়েছে), তারপর নতুন লাইনটা লেখা হয়েছে। এই
ফাইলটা যদি ৫০০MB হতো, একই এক-লাইন বদলে পুরো ৫০০MB copy-up হতো —
লেসনের copy-on-write খরচের দাবিটা এখানে হাতে-কলমে verify হলো।
Checkpoint (ট্র্যাক A): ls -la lab/lower0-base/app-readme.txt lab/upperdir/app-readme.txt দিয়ে দুটো ফাইলের size তুলনা করুন — দুটোই
প্রায় সমান (upperdir-েরটা শুধু কয়েক byte বড়, নতুন লাইনের জন্য), pure
diff না।
৪. Delete → whiteout — character device marker খুঁজে বের করা [ট্র্যাক A, real Linux/VM]
app-extra.txt শুধু lower1-mid-এ আছে। এখন merged view থেকে মুছুন:
rm lab/merged/app-extra.txt
ls -la lab/upperdir/
প্রত্যাশিত আউটপুট (মনোযোগ দিন file-type কলামে):
$ ls -la lab/upperdir/
total 8
drwxr-xr-x 2 user user 80 Sep 9 10:00 .
drwxr-xr-x 6 user user 160 Sep 9 09:58 ..
-rw-r--r-- 1 user user 23 Sep 9 09:59 app-readme.txt
c--------- 2 root root 0, 0 Sep 9 10:00 app-extra.txt
app-extra.txt-এর লাইনটা -rw-r--r---এর বদলে c--------- দিয়ে শুরু
— এই c মানেই character device, আর permission সব dash, আর owner
root:root, আর size-এর জায়গায় major/minor device number 0,0 —
এটাই লেসনে বর্ণিত whiteout marker, আক্ষরিক অর্থে। কোনো xattr না, কোনো
বিশেষ ফাঁকা ফাইল না — একটা real device-node inode, যা কোনো স্বাভাবিক
ফাইল কখনো হতে পারে না বলেই unambiguous।
মূল ফাইলটা lower layer-এ অক্ষত আছে তা যাচাই করুন:
cat lab/lower1-mid/app-extra.txt # এখনো "middle extra" দেখাবে
ls lab/merged/ # app-extra.txt আর নেই এখানে
Checkpoint (ট্র্যাক A): stat lab/upperdir/app-extra.txt চালিয়ে
File type: character special file আর Device type: 0,0 — দুটো লাইন
নিজের চোখে দেখা।
৫. resolve_merged_view() — from scratch লিখে চালানো [ট্র্যাক B, এই sandbox-এ সত্যিই চালানো]
উপরের চারটা ধাপের algorithm এবার Python-এ, কোনো real mount ছাড়া:
# resolve_sim.py
WHITEOUT = object()
def resolve_merged_view(lowerdirs, upperdir):
"""lowerdirs: dict-এর list, index 0 = সবচেয়ে কম অগ্রাধিকার (base),
শেষেরটা = lowerdir-গুলোর মধ্যে সবচেয়ে বেশি অগ্রাধিকার। upperdir
সবসময় সবার ওপরে। এটাই ধাপ ২-৪-এর ঠিক একই resolution + whiteout-hiding
নিয়ম, শুধু kernel না, প্লেইন Python dict দিয়ে।"""
layers = list(lowerdirs) + [upperdir]
result = {}
for layer in layers:
for name, content in layer.items():
if content is WHITEOUT:
result.pop(name, None)
else:
result[name] = content
return result
if __name__ == "__main__":
lower0_base = {
"etc/motd": "base motd",
"app/config.txt": "base config",
"app/readme.txt": "base readme",
}
lower1_middle = {
"app/config.txt": "middle config",
"app/extra.txt": "middle extra",
}
lower2_top = {
"app/config.txt": "top config",
"app/new.txt": "top new",
}
upperdir = {}
view = resolve_merged_view([lower0_base, lower1_middle, lower2_top], upperdir)
for name in sorted(view):
print(f"{name:20s} -> {view[name]!r}")
print()
print("--- upperdir-এ একটা whiteout বসিয়ে app/extra.txt মুছে ফেলার পর ---")
upperdir["app/extra.txt"] = WHITEOUT
view2 = resolve_merged_view([lower0_base, lower1_middle, lower2_top], upperdir)
for name in sorted(view2):
print(f"{name:20s} -> {view2[name]!r}")
print(f"'app/extra.txt' merged-এ আছে? {'app/extra.txt' in view2}")
print(f"lower1_middle-এ এখনো আছে? {'app/extra.txt' in lower1_middle}")
এই মেশিনে সত্যিই চালিয়ে পাওয়া output (deterministic, dict insertion
order-নির্ভর না — sorted() দিয়ে print করা):
app/config.txt -> 'top config'
app/extra.txt -> 'middle extra'
app/new.txt -> 'top new'
app/readme.txt -> 'base readme'
etc/motd -> 'base motd'
--- upperdir-এ একটা whiteout বসিয়ে app/extra.txt মুছে ফেলার পর ---
app/config.txt -> 'top config'
app/new.txt -> 'top new'
app/readme.txt -> 'base readme'
etc/motd -> 'base motd'
'app/extra.txt' merged-এ আছে? False
lower1_middle-এ এখনো আছে? True
প্রথম block-টা ঠিক ধাপ ২-এর real ls merged/ আউটপুটের সাথে মিলে যায় —
app/config.txt-এর জন্য top config জিতেছে, বাকি তিনটা ফাইল যার যার
স্তর থেকে দেখা যাচ্ছে। দ্বিতীয় block ধাপ ৪-এর whiteout প্রমাণ করে:
app/extra.txt merged view থেকে অদৃশ্য, কিন্তু lower1_middle
dict-এ (আসল lower layer) অক্ষত রয়ে গেছে — ঠিক real kernel-এর whiteout
সেমান্টিক্স।
Checkpoint: নিজের মেশিনে python3 resolve_sim.py চালিয়ে হুবহু এই
আউটপুট পাওয়া, তারপর কোড বদলে একটা চতুর্থ lowerdir যোগ করে দেখান
resolution ঠিক থাকে — একটা নতুন সবচেয়ে-বেশি-অগ্রাধিকার layer যোগ করলে
তার filename-গুলোই সবার ওপরে জেতে।
৬. Mini image builder — Dockerfile instruction থেকে layer stack [ট্র্যাক B, এই sandbox-এ সত্যিই চালানো]
এবার ধাপ ৫-এর resolution ব্যবহার করে একটা ছোট টুল বানান, যা
ADD/RUN-modify/RUN-delete টাইপের instruction-এর একটা sequence
থেকে layer stack বানায় — ঠিক Docker যেভাবে প্রতিটা filesystem-বদলানো
Dockerfile লাইনকে একটা নতুন layer বানায়:
# image_builder.py
WHITEOUT = object()
def resolve_merged_view(layers):
result = {}
for layer in layers:
for name, content in layer.items():
if content is WHITEOUT:
result.pop(name, None)
else:
result[name] = content
return result
def build_image(instructions):
"""instructions: [(op, name, value), ...] --
('ADD', filename, content) | ('RUN-modify', filename, new_content) |
('RUN-delete', filename, None). প্রতিটা instruction ঠিক একটা নতুন
layer বানায়, আগেরগুলোর ওপর স্ট্যাক হয় -- ঠিক Dockerfile-এর
RUN/COPY/ADD প্রতিটা এক নতুন layer বানানোর মতো।"""
layers = []
for op, name, value in instructions:
layer = {}
if op == "ADD":
layer[name] = value
elif op == "RUN-modify":
layer[name] = value
elif op == "RUN-delete":
layer[name] = WHITEOUT
else:
raise ValueError(f"unknown op {op}")
layers.append((f"{op} {name}", layer))
return layers
if __name__ == "__main__":
instructions = [
("ADD", "app/main.py", "print('v1')"),
("ADD", "app/config.json", '{"debug": false}'),
("RUN-modify", "app/config.json", '{"debug": true}'),
("ADD", "app/data/seed.csv", "id,name\n1,alice\n"),
("RUN-delete", "app/data/seed.csv", None),
("RUN-modify", "app/main.py", "print('v2')"),
]
built = build_image(instructions)
print("--- তৈরি হওয়া layer তালিকা (build-ক্রমে, নিচ থেকে ওপরে) ---")
for i, (label, layer) in enumerate(built):
content_desc = {
k: ("<WHITEOUT>" if v is WHITEOUT else v) for k, v in layer.items()
}
print(f"layer{i}: {label:30s} -> {content_desc}")
layers_only = [layer for _, layer in built]
final_view = resolve_merged_view(layers_only)
print()
print("--- চূড়ান্ত merged view (সব লেয়ার স্ট্যাক করে resolve করার পর) ---")
for name in sorted(final_view):
print(f"{name:20s} -> {final_view[name]!r}")
print(f"'app/data/seed.csv' merged-এ আছে? {'app/data/seed.csv' in final_view}")
এই মেশিনে সত্যিই চালিয়ে পাওয়া output:
--- তৈরি হওয়া layer তালিকা (build-ক্রমে, নিচ থেকে ওপরে) ---
layer0: ADD app/main.py -> {'app/main.py': "print('v1')"}
layer1: ADD app/config.json -> {'app/config.json': '{"debug": false}'}
layer2: RUN-modify app/config.json -> {'app/config.json': '{"debug": true}'}
layer3: ADD app/data/seed.csv -> {'app/data/seed.csv': 'id,name\n1,alice\n'}
layer4: RUN-delete app/data/seed.csv -> {'app/data/seed.csv': '<WHITEOUT>'}
layer5: RUN-modify app/main.py -> {'app/main.py': "print('v2')"}
--- চূড়ান্ত merged view (সব লেয়ার স্ট্যাক করে resolve করার পর) ---
app/config.json -> '{"debug": true}'
app/main.py -> "print('v2')"
'app/data/seed.csv' merged-এ আছে? False
ছয়টা layer তৈরি হয়েছে, প্রতিটা instruction থেকে ঠিক একটা — কিন্তু
চূড়ান্ত merged view-তে মাত্র দুটো ফাইল বেঁচে আছে। app/config.json-এর
জন্য layer2-এর RUN-modify layer1-এর মূল ভার্সন shadow করেছে
(ধাপ ২-এর একই নিয়ম)। app/data/seed.csv তৈরি হয়েছিল layer3-এ, কিন্তু
layer4-এর whiteout সেটাকে merged view থেকে মুছে দিয়েছে — যদিও ফাইলটা
এখনো layer3-এর dict-এ অক্ষত আছে, ঠিক যেমন একটা real Docker image-এ
পুরনো layer-গুলো ডিস্কে অক্ষত থেকে যায়, শুধু পরের layer-এর কাছে
shadow/whiteout হয়ে যায়। এটাই সরাসরি সেই যন্ত্র, যার ওপর
docker build-এর প্রতিটা layer দাঁড়িয়ে।
Checkpoint: image_builder.py চালিয়ে হুবহু এই আউটপুট পাওয়া, তারপর
একটা সপ্তম instruction (("RUN-delete", "app/main.py", None)) যোগ
করে দেখান final merged view-তে app/main.py-ও অদৃশ্য হয়ে যায় — একটা
image-এর প্রতিটা ফাইল যেকোনো পরের layer দিয়ে override বা delete হতে
পারে, এমনকি সেই ফাইল যদি সবচেয়ে প্রথম layer-েই আসে।
নিজেকে চ্যালেঞ্জ করুন
- আপনার নিজের Linux VM-এ একটা বড় (১GB+) ফাইল lowerdir-এ রেখে, তাতে
এক byte বদলে copy-up-এর real latency মাপুন (
timeকমান্ড দিয়ে) — ফাইলের আকার দ্বিগুণ করলে latency-ও মোটামুটি দ্বিগুণ হয় কি না যাচাই করুন resolve_merged_view()-এ opaque directory সাপোর্ট যোগ করুন — লেসনেরtrusted.overlay.opaque=yমেকানিজম, যেখানে একটা গোটা directory-র lower content একটা মাত্র flag দিয়ে আড়াল হয়ে যায়, প্রতিটা child আলাদা whiteout না করেইimage_builder.py-তে content-hash ভিত্তিক layer deduplication যোগ করুন — দুটো layer-এর content বাইট-বাইট এক হলে তাদের একই physical layer হিসেবে চিহ্নিত করুন (SHA-256, ঠিক OCI diffID-এর মতো), আর দেখান কতগুলো layer-reference আসলে ডিস্কে unique- একটা
docker save-স্টাইল ছোট tarball exporter বানান — আপনার simulated layer stack-কে একটা আসল.tarফাইলে serialize করুন, প্রতিটা layer একটা আলাদা এন্ট্রি, সাথে একটা manifest JSON যা layer-ক্রম বর্ণনা করে write()-এর জন্য partial copy-up অপ্টিমাইজেশন গবেষণা করুন — আধুনিক kernel-এ কিছু ফাইল সিস্টেম (যেমনdm-thinবা reflink-সমর্থিত btrfs/XFS-এ overlay) পুরো ফাইল copy না করে reflink/COW extent শেয়ার করতে পারে কি না, আর OverlayFS ডিফল্টে কেন সেটা করে না তা লিখুন- real
mount -t overlay-এ একই filename দুইটা ভিন্ন type-এর হলে কী হয় (একটা lowerdir-এ regular file, ওপরের lowerdir-এ একই নামে directory) — হাতে পরীক্ষা করে resolution rule-টা বের করুন, তারপরresolve_merged_view()-এ সেই rule যোগ করুন
যা এখানে শিখলেন, পরে যেখানে লাগবে
| এখানে | পরে |
|---|---|
| lowerdir/upperdir priority resolution | virtualization-cloud/container-runtime-oci-এ runc কীভাবে rootfs mount করে বোঝা |
| copy-up-এর পুরো-ফাইল খরচ | Production container-এ stateful ডেটা কেন volume/bind mount-এ রাখা হয়, তার ইঞ্জিনিয়ারিং কারণ |
| character-device whiteout marker | /var/lib/docker/overlay2/ ডিরেক্টরি নিজে খুঁজে debug করার সময় “এটা কী ফাইল” প্রশ্নের উত্তর |
| layer stack থেকে merged view resolve | Docker/OCI image layer caching, docker history, আর multi-stage build অপ্টিমাইজেশন পড়া |