Serverless-এর ভেতরের কাজ — FaaS, Cold Start, আর Firecracker microVM
Serverless Internals: FaaS Execution Model, Cold Starts, and Firecracker MicroVMs
এই module-এর প্রথম অর্ধেক (hardware-virtualization পর্যন্ত) দেখিয়েছে কীভাবে VT-x/EPT একটা guest-কে সম্পূর্ণ, শক্তভাবে বিচ্ছিন্ন একটা কম্পিউটারের বিভ্রম দেয়, দ্বিতীয় অর্ধেক (container-runtime-oci পর্যন্ত) দেখিয়েছে কীভাবে namespace/cgroup আর OCI runtime সেই একই বিচ্ছিন্নতা অনেক সস্তায়, অনেক দ্রুত, একটাই kernel শেয়ার করে দেয়। এই লেসন এই দুইটা arc-কে একটা বাস্তব সিস্টেমে মুখোমুখি দাঁড় করায় — serverless/FaaS platform-কে প্রতিটা customer-এর untrusted function-এর জন্য, প্রতিটা invocation-এ, সাব-সেকেন্ড লেটেন্সিতে, একটা নতুন, নিরাপদভাবে বিচ্ছিন্ন execution environment বানাতে হয়। container যথেষ্ট নিরাপদ না (আগের multi-tenant আলোচনার সরাসরি প্রয়োগ), full VM যথেষ্ট দ্রুত না — আর AWS-এর Firecracker ঠিক এই দুইয়ের সংযোগস্থলে দাঁড়িয়ে থাকা, module-এর দুইটা arc-এরই সরাসরি ফসল একটা বাস্তব সমাধান।
আগে এটা বুঝি
hardware-virtualization লেসন পর্যন্ত এই module-এর প্রথম অর্ধেক একটা একক গল্প বলেছে — VMX root/non-root mode, VM-exit/VM-entry-র hardware-নিয়ন্ত্রিত যন্ত্রপাতি, EPT/NPT-এর দুই-মাত্রিক page walk — সবই একটা guest-কে সম্পূর্ণ, শক্তভাবে বিচ্ছিন্ন একটা কম্পিউটারের বিভ্রম দেওয়ার জন্য। container-runtime-oci লেসন পর্যন্ত দ্বিতীয় অর্ধেক সম্পূর্ণ বিপরীত দিক থেকে একই সমস্যায় এসেছে — namespace, cgroup, OCI Runtime Spec, runc/containerd/containerd-shim — সবই দেখিয়েছে কীভাবে একটাই host kernel শেয়ার করেও একটা process-গোষ্ঠীকে যথেষ্ট বিচ্ছিন্ন রাখা যায়, অনেক সস্তায়, অনেক দ্রুত। containers-vs-vms লেসন এই দুইটা arc-কে সরাসরি একে অপরের বিপরীতে দাঁড় করিয়েছিল — একটা honest তুলনা, attack surface বনাম density, isolation বনাম গতি।
আজকের লেসন একটা বাস্তব সিস্টেমে এই ঠিক এই টানাপোড়েনটাকেই চরম পরীক্ষায় ফেলে। কল্পনা করুন একটা cloud platform — AWS Lambda, Google Cloud Functions, Azure Functions-এর মতো — যেখানে হাজার হাজার সম্পূর্ণ ভিন্ন, একে অপরের প্রতি সম্ভাব্য শত্রুভাবাপন্ন গ্রাহকের কোড একই physical hardware-এ, পাশাপাশি চলে। প্রতিটা invocation-এর জন্য প্ল্যাটফর্মকে একটা নতুন, নিরাপদভাবে বিচ্ছিন্ন execution environment বানাতে হয় — কিন্তু ব্যবহারকারী একটা HTTP request পাঠানোর পর কয়েক সেকেন্ড অপেক্ষা করতে রাজি না, তাকে সাব-সেকেন্ড স্কেলে উত্তর দিতে হবে।
container যথেষ্ট নিরাপদ না (এত বড় multi-tenant, mutually-untrusted পরিসরে), full VM যথেষ্ট দ্রুত না — তাহলে বাস্তবে এই সমস্যাটা সমাধান হয় কীভাবে?
এই একটা প্রশ্নের উত্তরই আজকের লেসনের সম্পূর্ণ কাঠামো — প্রথমে FaaS (Function-as-a-Service) execution মডেলটা ঠিক কী, তারপর cold start নামের যান্ত্রিক সমস্যাটা কোথা থেকে আসে, তারপর multi-tenant isolation-এর টানাপোড়েনটা module-এর দুইটা arc-এর ভাষায় আবার স্পষ্ট করে বলা, আর সবশেষে AWS-এর Firecracker — একটা concrete, বাস্তবে-চলা সমাধান, যা এই module-এর প্রায় প্রতিটা আগের লেসনের ধারণা একত্র করে দাঁড় করানো।
মূল ধারণা
FaaS — ঠিক কী আপলোড হয়, কী প্ল্যাটফর্মের দায়িত্ব
FaaS (Function-as-a-Service) execution মডেলের কেন্দ্রীয় দাবিটা একটা বাক্যে বলা যায়:
ডেভেলপার শুধু একটা ফাংশন আপলোড করে — না একটা সার্ভার, না (ধারণাগতভাবে) এমনকি একটা সম্পূর্ণ চলমান container। প্ল্যাটফর্ম দায়িত্ব নেয় একটা ইভেন্ট এলে সেই ফাংশনের জন্য একটা execution environment on-demand তৈরি করা, ফাংশন চালানো, আর কাজ শেষে সেটা teardown করা।
এই বাক্যের প্রতিটা শব্দ ইচ্ছাকৃত। “শুধু একটা ফাংশন” — মানে ডেভেলপার একটা handler(event, context)-জাতীয় একটা কোড ইউনিট লেখে, একটা নির্দিষ্ট runtime (Python, Node.js, Java, Go…) টার্গেট করে; সে কোনো web server চালু করে না, কোনো port listen করে না, কোনো OS প্রসেস নিজে spawn করে না। “(ধারণাগতভাবে) সম্পূর্ণ চলমান container না” — বাস্তবে অনেক platform (AWS Lambda-সহ) ফাংশন-কোড একটা OCI-সামঞ্জস্যপূর্ণ container image হিসেবেই প্যাকেজ করতে দেয় (container-runtime-oci লেসনের Image Spec-এর সরাসরি প্রয়োগ), কিন্তু সেই image কখনোই ডেভেলপারের নিয়ন্ত্রণে চলমান একটা process না — এটা শুধু একটা প্যাকেজিং ফরম্যাট, বিলিং এখনও per-invocation, per-server-uptime না।
Billing — per-invocation, per-always-on-server না
IaaS (VM ভাড়া নেওয়া) বা এমনকি একটা container orchestrator-এ (containers-vs-vms লেসনের “same-team microservices” ব্যবহারের ক্ষেত্র), বিলিং সাধারণত সময়ের সাথে — একটা VM/node চালু থাকলে, সেটা ব্যবহার হোক বা না হোক, বিল আসতে থাকে। FaaS-এ বিলিং সম্পূর্ণ ভিন্ন মাত্রার — প্রতিটা invocation-এর জন্য, আর সেই invocation ঠিক কতক্ষণ চলল (সাধারণত মিলিসেকেন্ড-রেজোলিউশনে) তার ওপর ভিত্তি করে। কোনো request না এলে, বিল শূন্য — কোনো “always-on” সার্ভার নেই যার জন্য টাকা যাচ্ছে (Adzic ও Chatley-র ২০১৭ সালের গবেষণাপত্র এই economic মডেলটাই প্রথম বিস্তারিত ডকুমেন্ট করে — references দেখুন)।
| IaaS (VM ভাড়া) | CaaS/container orchestration | FaaS | |
|---|---|---|---|
| কী ডিপ্লয় হয় | একটা পূর্ণ OS image | একটা container image (long-running process) | শুধু একটা ফাংশন (image হিসেবে প্যাকেজড হতে পারে) |
| কে provisioning-এর সিদ্ধান্ত নেয় | ডেভেলপার (কয়টা VM, কখন) | ডেভেলপার/orchestrator (replica count) | প্ল্যাটফর্ম, সম্পূর্ণ স্বয়ংক্রিয়, প্রতি ইভেন্টে |
| বিলিং একক | সার্ভার-সময় (uptime) | সার্ভার/pod-সময় (uptime) | invocation + execution-সময় |
| Idle থাকলে বিল? | হ্যাঁ, পুরো uptime-এর জন্য | হ্যাঁ, replica চালু থাকলে | না — কোনো invocation নেই মানে কোনো বিল নেই |
ইভেন্ট-চালিত trigger তিন ধরনের প্রধান শ্রেণীতে পড়ে — একটা HTTP request (API Gateway-র মধ্য দিয়ে), একটা queue message (SQS/Pub-Sub-জাতীয় কিছু নতুন আইটেম পেলে), অথবা একটা timer (cron-জাতীয় নির্ধারিত সময়ে)। প্রতিটার প্রতিক্রিয়ায় প্ল্যাটফর্ম একই কাজ করে — একটা execution environment খোঁজে (বা নতুন বানায়), ফাংশন চালায়, ফলাফল ফেরত দেয়, environment-টা কিছুক্ষণ রেখে দেয় (কেন, সেটাই পরের সেকশনের বিষয়) বা টিয়ারডাউন করে।
ভেতরে কী ঘটছে
Cold start — যান্ত্রিকভাবে, ধাপে ধাপে
“Cold start ধীর” — এই বাক্যটা প্রায় সবাই শুনেছে, কিন্তু কেন ধীর, ঠিক কোন কোন যান্ত্রিক ধাপ তার পেছনে আছে, সেটা প্রায়ই অস্পষ্ট থেকে যায়। একটা request যখন এমন একটা ফাংশনের জন্য আসে যার কোনো বর্তমানে-warm execution environment নেই, তখন user-এর কোনো কোড চলার আগেই প্ল্যাটফর্মকে এই ধাপগুলো সম্পূর্ণ করতে হয়:
- ইভেন্ট গ্রহণ + রাউটিং সিদ্ধান্ত~৫ ms — প্ল্যাটফর্ম যাচাই করে এই ফাংশনের জন্য কোনো warm execution environment বর্তমানে উপলব্ধ কিনা
- কোনো warm environment নেই → নতুন sandbox provisioning~১৫০ ms (illustrative) — এখানেই containers-vs-vms লেসনের VM-boot-time বনাম container-start-time ট্রেড-অফ সরাসরি প্রযোজ্য; কোন isolation technology বেছে নেওয়া হয়েছে তার ওপর এই সংখ্যা সবচেয়ে বেশি নির্ভরশীল
- ফাংশনের কোড লোড করা~২৫ ms — প্যাকেজ (zip বা container image layer) ইতিমধ্যে host-এ cache করা না থাকলে ডাউনলোড/unpack করা লাগে
- Language runtime initialize করা~১৫০ ms (illustrative; ভাষাভেদে বিশাল পার্থক্য) — JVM classloading, বা Python interpreter + import statement চালানো (নিচের experiment সেকশনে এই একটা ধাপই বাস্তবে মাপা হয়েছে)
- ফাংশনের init-টাইম কোড (module-level, handler-এর বাইরে)~৫০ ms — DB connection pool তৈরি, config লোড, ইত্যাদি — handler-এর প্রতিটা invocation-এ পুনরাবৃত্তি হয় না, শুধু cold start-এই ঘটে
- user-এর handler(event, context) অবশেষে চলেএখানেই আসল কাজ শুরু — এর আগের প্রতিটা ধাপ user-এর কোনো লজিকই স্পর্শ করেনি
উপরের প্রথম পাঁচটা ধাপ যোগ করলে (৫ + ১৫০ + ২৫ + ১৫০ + ৫০) দাঁড়ায় ~৩৮০ ms — এই সংখ্যাটাই এই লেসনের build সেকশনের সিমুলেশনে সরাসরি ব্যবহৃত হয়েছে, নিছক আলংকারিক না।
Cold start-এর এই পাঁচ-ধাপি ভাঙন Wang et al.-এর ২০১৮ সালের empirical গবেষণার সাথে সামঞ্জস্যপূর্ণ (references দেখুন) — তারা AWS Lambda, Google Cloud Functions, আর Azure Functions জুড়ে ঠিক এই ধরনের স্তরভিত্তিক লেটেন্সি পরিমাপ করেছিলেন, আর দেখিয়েছিলেন provisioning + runtime-init মিলিয়েই cold-start overhead-এর সিংহভাগ — actual handler execution-এর তুলনায় বহুগুণ বেশি সময় গ্রাস করে ছোট, দ্রুত ফাংশনের ক্ষেত্রে।
উদাহরণ
Runtime-ভেদে সংখ্যা বদলায় — কেন Python আর JVM এক না
আগের সেকশনের “language runtime initialize করা” ধাপটা কোনো একটা ধ্রুবক সংখ্যা না — এটা runtime-ভেদে ব্যাপক তারতম্য দেখায়, আর এই তারতম্যটাই বাস্তবে অনেক প্রকৌশল সিদ্ধান্ত নির্ধারণ করে (কোন runtime বেছে নেওয়া, কতটা dependency import করা)।
যেখানে p_cold হলো একটা invocation cold হওয়ার সম্ভাবনা (আগের request কতক্ষণ আগে এসেছিল, আর keep-warm window কতটা, তার ফাংশন — build সেকশনে এটাই সিমুলেট করা হয়েছে)। এই সূত্রটাই দেখায় কেন COLD আর WARM-এর মধ্যেকার ফারাক (এখানে ~৩৮০ ms বনাম ~৪ ms) এত গুরুত্বপূর্ণ — p_cold সামান্য বাড়লেও গড় লেটেন্সি ব্যাপকভাবে বেড়ে যেতে পারে।
| Runtime | Cold-start-এ runtime-init-এর order-of-magnitude | কেন |
|---|---|---|
| Python (কয়েকটা stdlib import) | ~৫০-১৫০ ms | interpreter start দ্রুত, কিন্তু প্রতিটা import module-level কোড চালায় |
| Node.js (V8) | ~৫০-১০০ ms | V8 engine দ্রুত initialize হয়, কিন্তু বড় node_modules tree হলে বাড়ে |
| Java (JVM) | ~৩০০-৮০০ ms বা বেশি | JVM startup + classloading + JIT warm-up — historically সবচেয়ে ভারী cold-start প্রোফাইল |
| Go (compiled binary) | ~২০-৫০ ms | কোনো interpreter/VM নেই — সরাসরি native বাইনারি execve() হয়, প্রায় “runtime init” ধাপটাই অনুপস্থিত |
ছবিতে
COLD invocation (কোনো warm environment নেই):
┌─────┐ ┌─────────────────┐ ┌──────────┐ ┌───────────────┐ ┌────────┐ ┌─────────┐
│route│→│sandbox provision │→│code load │→│runtime init │→│init code│→│ handler │
│ ~5ms│ │ ~150ms │ │ ~25ms │ │ ~150ms │ │ ~50ms │ │ (user) │
└─────┘ └─────────────────┘ └──────────┘ └───────────────┘ └────────┘ └─────────┘
╰──────────────────────── added latency: ~380ms ─────────────────────────╯
WARM invocation (environment ইতিমধ্যে live):
┌─────┐ ┌─────────┐
│route│→│ handler │
│ ~5ms│ │ (user) │
└─────┘ └─────────┘
╰── added latency: ~4ms ──╯দ্রুততর বুট, দুর্বলতর isolation ধীরতর বুট, শক্তিশালীতর isolation
◄──────────────────────────────────────────────────────────────────────────────►
bare OS process container Firecracker microVM full VM
(fork/exec) (namespace+cgroup) (KVM/VT-x, minimal device) (QEMU, full BIOS)
~1-3 ms ~50-100 ms <125 ms ~1-40+ s
isolation: কম isolation: মাঝারি isolation: VM-স্তরের isolation: VM-স্তরের
(container-runtime- (host kernel শেয়ার, (নিজস্ব guest kernel, (নিজস্ব guest kernel,
oci লেসনের বিষয়) ~৪৫০ syscall attack EPT-backed, কিন্তু পূর্ণ device model,
surface — containers- device model প্রায় containers-vs-vms
vs-vms লেসনের বিষয়) অনুপস্থিত) লেসনের বিষয়)
▲
আজকের লেসনের কেন্দ্রীয়
বিন্দু — module-এর দুই
arc-এর সংযোগস্থলনিজে চালিয়ে দেখুন
Cold-start-এর একটা বাস্তব উপাদান মাপা — Python interpreter start + import overhead
এই লেসনের hood সেকশনের LayerTrace-এ “language runtime initialize করা” ধাপটার একটা বাস্তব, পরিমাপযোগ্য উপাদান আছে — একটা নতুন Python প্রসেস কতক্ষণে শুধু interpreter চালু করে exit করে, বনাম কতক্ষণে সেই একই কাজ করে plus একগুচ্ছ stdlib module import করে (একটা typical FaaS handler যা করে — json, re, hashlib, urllib, http.client-এর মতো কিছু)। এটা একটা সম্পূর্ণ Lambda cold start না (কোনো sandbox provisioning এখানে নেই, শুধু process-level interpreter cost) — কিন্তু এই sandbox যা সততার সাথে সরাসরি মাপতে পারে, ঠিক ততটুকুই।
#!/usr/bin/env python3
# runtime_init_timing.py -- measure, for real, how much wall time a fresh
# Python PROCESS takes to (a) just start the interpreter and exit, vs
# (b) start the interpreter AND import a realistic handful of stdlib
# modules a typical FaaS handler would need. This is a real, measurable
# proxy for the "initialize the language runtime" step of a cold start.
import subprocess
import time
N = 50
def timed_runs(code, n=N):
times = []
for _ in range(n):
t0 = time.perf_counter()
subprocess.run(["python3", "-c", code], check=True)
times.append((time.perf_counter() - t0) * 1000)
times.sort()
avg = sum(times) / len(times)
p50 = times[len(times) // 2]
return avg, p50, min(times), max(times)
bare_avg, bare_p50, bare_min, bare_max = timed_runs("pass")
heavy_avg, heavy_p50, heavy_min, heavy_max = timed_runs(
"import json, re, hashlib, urllib.request, http.client, "
"email.mime.text, xml.etree.ElementTree"
)
print(f"runs per condition : {N}")
print()
print("bare interpreter start (python3 -c 'pass'):")
print(f" avg : {bare_avg:.2f} ms")
print(f" p50 : {bare_p50:.2f} ms")
print(f" min : {bare_min:.2f} ms")
print(f" max : {bare_max:.2f} ms")
print()
print("interpreter start + 7 stdlib imports (json/re/hashlib/urllib/http/email/xml):")
print(f" avg : {heavy_avg:.2f} ms")
print(f" p50 : {heavy_p50:.2f} ms")
print(f" min : {heavy_min:.2f} ms")
print(f" max : {heavy_max:.2f} ms")
print()
delta = heavy_avg - bare_avg
print(f"import overhead (heavy avg - bare avg) : {delta:.2f} ms")
print(f"import overhead as a fraction of heavy total : {delta/heavy_avg*100:.1f}%")এই sandbox-এ সত্যিই চালিয়ে পাওয়া output (macOS, Python 3.9.6):
runs per condition : 50
bare interpreter start (python3 -c 'pass'):
avg : 57.60 ms
p50 : 57.46 ms
min : 48.91 ms
max : 70.28 ms
interpreter start + 7 stdlib imports (json/re/hashlib/urllib/http/email/xml):
avg : 121.38 ms
p50 : 115.85 ms
min : 105.94 ms
max : 364.17 ms
import overhead (heavy avg - bare avg) : 63.78 ms
import overhead as a fraction of heavy total : 52.5%লক্ষ্য করুন — শুধু ৭টা stdlib import (কোনো third-party dependency না, কোনো heavy framework না) যোগ করাই average সময় প্রায় দ্বিগুণ করে দিচ্ছে (৫৭.৬০ ms থেকে ১২১.৩৮ ms), আর import overhead একাই মোট সময়ের ৫২.৫%। এটাই ঠিক আগের example সেকশনের “runtime initialize করা ~১৫০ ms” অনুমানের বাস্তব ভিত্তি — একটা real-world Lambda handler-এ (যেখানে প্রায়ই third-party dependency, ML লাইব্রেরি, ORM-এর মতো ভারী import থাকে) এই সংখ্যা আরও অনেক বড় হতে পারে। max কলামের ৩৬৪.১৭ ms outlier-টাও বাস্তবসম্মত — OS scheduler jitter, disk cache miss-এর মতো কারণে cold-start লেটেন্সির tail সবসময় গড়ের চেয়ে বহুগুণ খারাপ হতে পারে, ঠিক যেমন production FaaS প্ল্যাটফর্মে p50 আর p99 cold-start লেটেন্সির মধ্যে বিশাল ফারাক দেখা যায় (Wang et al. 2018-এর কেন্দ্রীয় পর্যবেক্ষণগুলোর একটা)।
hood সেকশনের LayerTrace-এ 'language runtime initialize করা' ধাপটা কোনো তাত্ত্বিক সংখ্যা না — এই একটা component (interpreter start + stdlib import) সরাসরি, বাস্তবে মাপা যায়, আর কয়েকটা import-ই লক্ষণীয় লেটেন্সি যোগ করে।
নিজে বানান
Cold Start vs Keep-Warm — একটা বাস্তব-সংখ্যার ট্রেড-অফ সিমুলেশন
- একটা time-varying request stream generator লিখুন — বেশিরভাগ সময় বিরল অনুরোধ, মাঝে মাঝে ঘন ঘন অনুরোধের একটা "burst" (একটা বাস্তব traffic প্যাটার্নের মতো)
- একটা simulate() ফাংশন লিখুন যা প্রতিটা request-কে cold না warm হিসেবে classify করে, keep_warm_s প্যারামিটারের ভিত্তিতে — request আগের environment-এর warm-window-এর মধ্যে এলে warm, নাহলে cold
- প্রতিটা request-এর added latency track করুন (cold হলে COLD_START_MS, warm হলে WARM_DISPATCH_MS)
- idle-reserved environment-hours হিসাব করুন — environment প্রতিটা "warm session"-এ কতক্ষণ ধরে বিদ্যমান ছিল (busy বা idle, দুইটাই), এটাই idle-resource billing-এর প্রক্সি
- বিভিন্ন keep_warm_s মান (০ সেকেন্ড থেকে ১৫ মিনিট পর্যন্ত) পাশাপাশি চালিয়ে cold-start rate, average added latency, আর reserved environment-hours-এর real ট্রেড-অফ দেখান
এই সিমুলেশনটা এই লেসনের hood সেকশনের LayerTrace থেকে পাওয়া COLD_START_MS = 380 (৫+১৫০+২৫+১৫০+৫০) আর WARM_DISPATCH_MS = 4 সংখ্যা দুইটাই সরাসরি ব্যবহার করে — এলোমেলো অনুমান না, আগের সেকশনগুলোর সাথে সামঞ্জস্যপূর্ণ। traffic pattern-টা ইচ্ছাকৃতভাবে বাস্তবসম্মত করে বানানো হয়েছে (একটা repeating cycle-এ ১ মিনিট “burst” + ৯ মিনিট “quiet”) — সম্পূর্ণ uniform random traffic-এ keep-warm window-এর প্রভাব এত স্পষ্টভাবে দেখা যেত না।
#!/usr/bin/env python3
"""
keepwarm_sim.py -- a toy, but honestly-computed, simulation of the
cold-start vs keep-warm tradeoff a real FaaS platform faces.
"""
import random
COLD_START_MS = 380.0 # from this lesson's cold-start LayerTrace total
WARM_DISPATCH_MS = 4.0 # negligible: route + hand off to an already-live sandbox
EXEC_S = 0.05 # assumed handler execution time, illustrative
def generate_request_stream(duration_s, seed):
"""
Time-varying Poisson-ish process: repeating 10-minute cycles, each
made of a 1-minute 'burst' (mean inter-arrival 4s) followed by a
9-minute 'quiet' stretch (mean inter-arrival 180s).
"""
rng = random.Random(seed)
times = []
t = 0.0
cycle_s = 600.0
burst_s = 60.0
while t < duration_s:
phase = t % cycle_s
mean_gap = 4.0 if phase < burst_s else 180.0
gap = rng.expovariate(1.0 / mean_gap)
t += gap
if t < duration_s:
times.append(t)
return times
def simulate(request_times, keep_warm_s):
cold = warm = 0
total_added_ms = 0.0
session_end = None
session_start = None
session_last_activity_end = None
reserved_s = 0.0
def close_session():
nonlocal reserved_s
if session_start is not None:
reserved_s += (session_last_activity_end + keep_warm_s) - session_start
for t in request_times:
if session_end is not None and t <= session_end:
warm += 1
added = WARM_DISPATCH_MS
else:
close_session()
cold += 1
added = COLD_START_MS
session_start = t
total_added_ms += added
end_t = t + EXEC_S + added / 1000.0
session_last_activity_end = end_t
session_end = end_t + keep_warm_s
close_session()
n = len(request_times)
return {
"n": n, "cold": cold, "warm": warm,
"cold_rate_pct": 100.0 * cold / n,
"avg_added_ms": total_added_ms / n,
"reserved_hours": reserved_s / 3600.0,
}
if __name__ == "__main__":
DURATION_S = 2 * 3600
SEED = 7
requests = generate_request_stream(DURATION_S, SEED)
print(f"simulated duration : {DURATION_S/3600:.1f} hours")
print(f"total requests generated : {len(requests)}")
print(f"COLD_START_MS : {COLD_START_MS}")
print(f"WARM_DISPATCH_MS : {WARM_DISPATCH_MS}")
print()
keep_warm_windows_s = [0, 30, 60, 300, 600, 900]
header = (f"{'keep-warm window':>18} | {'cold':>6} | {'warm':>6} | "
f"{'cold-rate':>10} | {'avg added lat':>14} | {'reserved (env-hours)':>21}")
print(header)
print("-" * len(header))
rows = []
for kw in keep_warm_windows_s:
r = simulate(requests, kw)
rows.append((kw, r))
label = f"{kw}s" if kw < 60 else f"{kw//60}min"
print(f"{label:>18} | {r['cold']:>6} | {r['warm']:>6} | "
f"{r['cold_rate_pct']:>9.1f}% | {r['avg_added_ms']:>12.1f}ms | "
f"{r['reserved_hours']:>19.2f}h")
print()
base = rows[0][1]
best = rows[-1][1]
print("reading the tradeoff (keep_warm=0s vs keep_warm=900s/15min):")
print(f" cold-start rate falls from {base['cold_rate_pct']:.1f}% to {best['cold_rate_pct']:.1f}%")
print(f" average added latency falls from {base['avg_added_ms']:.1f}ms to {best['avg_added_ms']:.1f}ms")
print(f" but idle-reserved environment-hours rise from {base['reserved_hours']:.2f}h "
f"to {best['reserved_hours']:.2f}h over this {DURATION_S/3600:.1f}h window "
f"({best['reserved_hours']/max(base['reserved_hours'],1e-9):.1f}x more billable idle time)")সত্যিই চালিয়ে পাওয়া output (deterministic — seed=7 দিয়ে, হুবহু এটাই আসবে):
simulated duration : 2.0 hours
total requests generated : 118
COLD_START_MS : 380.0
WARM_DISPATCH_MS : 4.0
keep-warm window | cold | warm | cold-rate | avg added lat | reserved (env-hours)
------------------------------------------------------------------------------------------
0s | 108 | 10 | 91.5% | 348.1ms | 0.01h
30s | 27 | 91 | 22.9% | 90.0ms | 0.35h
1min | 24 | 94 | 20.3% | 80.5ms | 0.56h
5min | 10 | 108 | 8.5% | 35.9ms | 1.57h
10min | 3 | 115 | 2.5% | 13.6ms | 1.98h
15min | 1 | 117 | 0.8% | 7.2ms | 2.18h
reading the tradeoff (keep_warm=0s vs keep_warm=900s/15min):
cold-start rate falls from 91.5% to 0.8%
average added latency falls from 348.1ms to 7.2ms
but idle-reserved environment-hours rise from 0.01h to 2.18h over this 2.0h window (174.8x more billable idle time)সংখ্যাগুলো নিজেই এই লেসনের কেন্দ্রীয় ট্রেড-অফটা প্রমাণ করে দেয়। keep_warm_s = 0-এ (প্রতিটা invocation-এর পরেই environment সাথে সাথে টিয়ারডাউন) cold-start rate ৯১.৫% — প্রায় প্রতিটা request-ই cold, কারণ এই burst/quiet traffic প্যাটার্নে দুইটা পরপর request-এর মধ্যে গড় ব্যবধান window-এর চেয়ে অনেক বড়। window ১৫ মিনিটে বাড়ালে cold-start rate ০.৮%-এ নেমে আসে (প্রায় প্রতিটা request-ই warm) — কিন্তু environment-hours ২১৮ গুণ (০.০১h → ২.১৮h) বেড়ে যায়, কারণ প্রতিটা invocation-এর পরে environment ১৫ মিনিট ধরে idle-এ “রিজার্ভড” থেকে যাচ্ছে, ব্যবহার হোক বা না হোক। এই idle-reserved সময়টাই বাস্তব cloud provider-দের কাছে সরাসরি খরচ — মেমরি/CPU রিজার্ভেশন কেউ ব্যবহার না করলেও প্ল্যাটফর্মের নিজের হার্ডওয়্যারে জায়গা দখল করে থাকে। এই কারণেই AWS Lambda-র মতো প্ল্যাটফর্ম “Provisioned Concurrency”-র মতো ফিচার আলাদাভাবে মূল্য নির্ধারণ করে — keep-warm একটা বিনামূল্যের সুবিধা না, এটা একটা সচেতন, পরিমাপযোগ্য latency-বনাম-cost সিদ্ধান্ত।
বাস্তব সিস্টেমে
Firecracker — module-এর দুইটা arc-এর সরাসরি সংযোগস্থল
আগের সেকশনগুলো cold-start-এর যান্ত্রিকতা আর keep-warm-এর অর্থনীতি দেখিয়েছে — কিন্তু এখনও মূল প্রশ্নের উত্তর দেয়নি: প্রতিটা নতুন execution environment-এর জন্য isolation technology হিসেবে ঠিক কী ব্যবহার করা হয়? containers-vs-vms লেসনের honest তুলনাটা এখানে সরাসরি প্রযোজ্য।
Container-level isolation যথেষ্ট না — একটা FaaS প্ল্যাটফর্মে একই physical host-এ শত শত সম্পূর্ণ ভিন্ন গ্রাহকের, একে অপরের প্রতি সম্ভাব্য শত্রুভাবাপন্ন ফাংশন পাশাপাশি চলে। containers-vs-vms লেসনের সংখ্যাটা মনে করুন — container escape মানে হোস্ট kernel-এর ~৪৫০টা syscall-এর যেকোনো একটার bug exploit করা, আর একটা bug-ই সেই host-এ চলা প্রতিটা গ্রাহকের ফাংশন-কে প্রকাশ করে দেয়। NIST SP 800-190-এর সরকারি সুপারিশও (আগের লেসনে উদ্ধৃত) ঠিক এই পরিস্থিতির জন্যই namespace/cgroup-এর ওপর একা নির্ভর না করার কথা বলে।
Full traditional VM যথেষ্ট দ্রুত না — hardware-virtualization লেসনের guest kernel বুট (firmware/BIOS → kernel init → device probe → init system) সেকেন্ড-স্কেলের অপারেশন। একটা FaaS invocation-এর জন্য এত দীর্ঘ অপেক্ষা ব্যবহারিকভাবে অগ্রহণযোগ্য — বিশেষ করে যখন আজকের build সেকশনের সিমুলেশনই দেখিয়েছে cold start নিজেই (মাত্র ৩৮০ ms ধরে নিয়েও) গড় লেটেন্সিতে বিশাল প্রভাব ফেলে।
সমাধান — একই hardware primitive, ন্যূনতম device model
AWS-এর Firecracker (Agache et al., NSDI 2020 — references দেখুন), বিশেষভাবে Lambda আর Fargate-এর জন্য বানানো, ঠিক এই সংযোগস্থলে দাঁড়িয়ে। এর কেন্দ্রীয় ডিজাইন-দাবিটা দুই ভাগে বোঝা দরকার:
-
Isolation — অপরিবর্তিত, পূর্ণ VM-স্তরের। Firecracker
hardware-virtualizationলেসনের ঠিক একই hardware primitive ব্যবহার করে — KVM-এর মাধ্যমে VT-x/AMD-V, প্রতিটা microVM-এর নিজস্ব guest kernel, EPT/NPT-backed মেমরি বিচ্ছিন্নতা, VM-exit-নিয়ন্ত্রিত trap boundary। এই দিক থেকে একটা Firecracker microVM একটা “ছোট container” না — এটা এখনওcontainers-vs-vmsলেসনের সংজ্ঞা অনুযায়ী একটা সম্পূর্ণ VM: নিজস্ব বুট-হওয়া kernel আছে, host kernel সরাসরি শেয়ার হয় না। একজন আক্রমণকারীকে “বেরিয়ে আসতে” এখনও সেই একই সংকীর্ণ, ভারীভাবে-নিরীক্ষিত hypervisor interface ভাঙতে হয় — পুরো host syscall table না। -
গতি — device model প্রায় সম্পূর্ণ বাদ দিয়ে। এখানেই আসল প্রকৌশল-কৌশল। একটা সাধারণ QEMU/KVM VM বুট ধীর হওয়ার একটা বড় কারণ firmware/BIOS handoff আর দশ-দশটা emulated legacy device (floppy controller, VGA, ACPI টেবিল, একাধিক PCI bus, USB controller…) probe/enumerate করা — এই ডিভাইসগুলো general-purpose guest OS compatibility-র জন্য দরকার, কিন্তু একটা FaaS ফাংশনের কোনোটারই প্রয়োজন নেই। Firecracker ইচ্ছাকৃতভাবে এই পুরো legacy device landscape বাদ দেয় — কোনো BIOS/firmware handoff নেই (direct kernel boot), আর মাত্র হাতে-গোনা কয়েকটা device emulate করে:
virtio-net(নেটওয়ার্ক),virtio-block(ডিস্ক), একটা minimal serial console, আর একটা ন্যূনতম keyboard-controller-জাতীয় শাটডাউন-পথ — প্রায় আর কিছুই না।
Agache et al.-এর পেপারে প্রকাশিত সংখ্যা অনুযায়ী — একটা Firecracker microVM ১২৫ মিলিসেকেন্ডের নিচে বুট করতে পারে, প্রতি microVM-এ ৫ মেগাবাইটের কম মেমরি overhead-এ, আর একটা একক host-এ হাজার হাজার microVM একসাথে চালানো সম্ভব (উচ্চ density, container-এর কাছাকাছি — containers-vs-vms লেসনের density আলোচনার সরাসরি প্রতিধ্বনি)। প্রতিটা microVM ঘিরে একটা অতিরিক্ত process — “jailer” — seccomp filter, cgroup limit, আর chroot দিয়ে defense-in-depth-এর আরেকটা স্তর যোগ করে (security/sandboxing-isolation লেসনের ধারণাগুলোর সরাসরি প্রয়োগ)।
সংক্ষেপে, এই পুরো module যেখানে পৌঁছাতে চেয়েছিল, Firecracker ঠিক সেখানেই দাঁড়িয়ে — hardware-virtualization থেকে containers-vs-vms পর্যন্ত লেসনগুলোর VM isolation (নিজস্ব guest kernel, hardware-enforced trap boundary), আর containers-vs-vms-এর পরের namespace/cgroup/OCI arc-এর গতি ও density-র অনুভূতি — দুইটাই একসাথে, একই সিস্টেমে।
যে ভুলগুলো সবাই করে
“'Serverless' মানে সত্যিই কোনো সার্ভার নেই।”
ভুল, নামটা একটা মার্কেটিং-বান্ধব অতিসরলীকরণ। প্রতিটা ফাংশন invocation আসলে কোনো না কোনো physical server-এ, কোনো না কোনো execution environment-এ (container বা microVM) চলে — সার্ভার সম্পূর্ণভাবেই বিদ্যমান। “Serverless” মানে শুধু এই — ডেভেলপারকে কোনো সার্ভার provision, patch, scale, বা manage করতে হয় না; সেই দায়িত্বটা সম্পূর্ণ প্ল্যাটফর্মের কাঁধে চলে যায়। এই লেসনের পুরো hood/build/realworld সেকশনই আসলে দেখিয়েছে সেই “অদৃশ্য” সার্ভার-ম্যানেজমেন্টের ভেতরে ঠিক কতটা যন্ত্রপাতি লুকানো — cold start, keep-warm নীতি, microVM provisioning, সবই।
“Firecracker একটা container runtime — অনেকটা runc-এর মতোই কিছু একটা।”
ভুল, এবং এই ভুলটা module-এর দুইটা arc গুলিয়ে ফেলে। container-runtime-oci লেসনের runc একটা config.json পড়ে namespace/cgroup/mount তৈরি করে, তারপর execve() দিয়ে host kernel-এর ওপরেই entrypoint চালায় — কোনো নতুন kernel বুট হয় না। Firecracker সম্পূর্ণ ভিন্ন স্তরে কাজ করে — এটা একটা VMM (Virtual Machine Monitor), KVM ব্যবহার করে একটা সম্পূর্ণ, স্বাধীন guest Linux kernel বুট করে, প্রতিটা microVM-এর নিজস্ব kernel memory, নিজস্ব EPT-backed address space। Firecracker-এর প্রসেস মডেল runc-এর “one-shot, execve-তে রূপান্তরিত” প্যাটার্নের সাথে মেলে না — এটা প্রতিটা microVM-এর জীবনকাল ধরে একটা VMM প্রসেস হিসেবে চলমান থাকে, guest-এর জন্য virtio device serve করতে থাকে।
“Cold start সবসময় কয়েক সেকেন্ড ধরে চলে, তাই serverless ইন্টারেক্টিভ/লো-লেটেন্সি workload-এর জন্য মৌলিকভাবে অনুপযুক্ত।”
অতিসরলীকরণ। “কয়েক সেকেন্ড” cold-start লেটেন্সি ঐতিহাসিকভাবে ভারী runtime (JVM, বড় dependency tree) আর পুরনো, কম-অপ্টিমাইজড provisioning pipeline-এর একটা বৈশিষ্ট্য ছিল — সার্বজনীন সত্য না। এই লেসনের নিজস্ব সংখ্যা দেখায় Firecracker microVM provisioning-ই ১২৫ ms-এর নিচে, আর runtime-init optimization (হালকা dependency, compiled ভাষা, snapshot-based restore-এর মতো আধুনিক কৌশল) মিলিয়ে অনেক production cold start আজ দশ-থেকে-কয়েকশো মিলিসেকেন্ড স্কেলে থাকে — সেকেন্ড না। এর ওপর keep-warm নীতি (আজকের build সেকশন) cold-start-এর frequency-ই ব্যাপকভাবে কমিয়ে দেয় বাস্তব traffic pattern-এ।
“Keep-warm window যত বড় করা যায় ততই ভালো — এর কোনো downside নেই, তাই সবসময় সর্বোচ্চ সম্ভব window ব্যবহার করা উচিত।”
আজকের build সেকশনের সিমুলেশন সরাসরি এর বিপরীত প্রমাণ করে। keep-warm window ০ সেকেন্ড থেকে ১৫ মিনিটে বাড়ালে cold-start rate ৯১.৫% থেকে ০.৮%-এ নামে ঠিকই, কিন্তু idle-reserved environment-hours একই সময়ে ~১৭৫ গুণ বেড়ে যায় — একটা সরাসরি, পরিমাপযোগ্য খরচ, কারণ প্ল্যাটফর্মকে সেই idle memory/CPU রিজার্ভেশন বহন করতে হয় ব্যবহার না হলেও। এই কারণেই AWS Lambda “Provisioned Concurrency”-র মতো ফিচার আলাদা মূল্যে বিক্রি করে — longer keep-warm একটা সচেতন latency-বনাম-cost সিদ্ধান্ত, বিনামূল্যের সুবিধা না।
বুঝেছেন কি না দেখুন
1FaaS execution মডেলে ডেভেলপার ঠিক কী আপলোড করে, আর প্ল্যাটফর্ম ঠিক কী দায়িত্ব নেয়? বিলিং ইউনিটটা IaaS/CaaS-এর থেকে কীভাবে ভিন্ন?স্মরণ
handler-জাতীয় কোড ইউনিট, একটা নির্দিষ্ট runtime টার্গেট করে) — কোনো সার্ভার না, ধারণাগতভাবে কোনো চলমান container-ও না, যদিও প্যাকেজিং container image হিসেবে হতে পারে। প্ল্যাটফর্ম দায়িত্ব নেয় একটা ইভেন্ট (HTTP request, queue message, timer) এলে একটা execution environment on-demand তৈরি করা, ফাংশন চালানো, আর পরে টিয়ারডাউন করা। বিলিং per-invocation/per-execution-time — IaaS/CaaS-এর মতো সার্ভার/pod uptime-ভিত্তিক না; কোনো request না এলে বিল শূন্য।2একটা multi-tenant FaaS প্ল্যাটফর্মে container-level isolation-কে কেন যথেষ্ট নিরাপদ বিবেচনা করা হয় না? containers-vs-vms লেসনের যুক্তিটা এখানে ঠিক কীভাবে প্রযোজ্য?যুক্তি
3যদি VM-level isolation নিরাপত্তার দিক থেকে স্পষ্টভাবে ভালো, তাহলে FaaS প্ল্যাটফর্মগুলো কেন সরাসরি traditional, পূর্ণাঙ্গ VM ব্যবহার করে না প্রতিটা invocation-এর জন্য?যুক্তি
hardware-virtualization লেসনের পুরো chain — firmware/BIOS handoff, guest kernel init, device probe, init system চালু হওয়া — যা সেকেন্ড-স্কেলের অপারেশন। FaaS-এর প্রতিটা invocation-এ এত দীর্ঘ latency ব্যবহারিকভাবে অগ্রহণযোগ্য, বিশেষ করে যখন আজকের সিমুলেশনই দেখিয়েছে মাত্র কয়েকশো মিলিসেকেন্ডের cold start-ও গড় লেটেন্সিতে বিশাল প্রভাব ফেলতে পারে। তাই দরকার একটা প্রযুক্তি যা VM-level isolation রাখে কিন্তু বুট-লেটেন্সি নাটকীয়ভাবে কমায় — এটাই Firecracker-এর মতো microVM-এর জন্মকারণ।4আজকের build সেকশনের সিমুলেশনের সংখ্যা ব্যবহার করে বলুন — একটা ফাংশন যদি সত্যিই কালেভদ্রে (দিনে একবার) invoke হয়, keep-warm window বাড়ানো তার জন্য অর্থনৈতিকভাবে বুদ্ধিমানের কাজ হবে কি? কেন বা কেন না?প্রয়োগ
5Firecracker-এর ন্যূনতম device model cold-start-এর hood সেকশনে বর্ণিত কোন নির্দিষ্ট ধাপটাকে সবচেয়ে বেশি দ্রুত করে? আর কোন ধাপটাকে Firecracker সরাসরি স্পর্শই করে না?প্রয়োগ
here-চিহ্নিত ধাপ) — firmware/BIOS handoff আর দশ-দশটা legacy device probe/enumerate করার খরচ প্রায় সম্পূর্ণ বাদ দিয়ে। কিন্তু Firecracker “language runtime initialize করা” ধাপটাকে স্পর্শই করে না — JVM classloading, বা Python-এর import overhead (আজকের experiment সেকশনে বাস্তবে মাপা ~৬৪ ms) সম্পূর্ণভাবে guest-এর ভেতরের সফটওয়্যার স্তরের খরচ, isolation technology কী তার ওপর নির্ভর করে না। এই নির্ভুল পার্থক্যটাই ব্যাখ্যা করে কেন শুধু Firecracker যথেষ্ট না — production cold-start optimization-এ runtime-নির্বাচন আর dependency-হালকা রাখাও সমান গুরুত্বপূর্ণ, দুইটা সম্পূর্ণ ভিন্ন স্তরের সমস্যা।6আপনাকে একটা নতুন multi-tenant FaaS প্ল্যাটফর্ম ডিজাইন করতে বলা হয়েছে যেখানে গ্রাহকরা একে অপরের প্রতি সম্ভাব্য শত্রুভাবাপন্ন কোড আপলোড করবেন, p99 cold-start লেটেন্সি ১০০ ms-এর নিচে রাখতে হবে, আর একই হার্ডওয়্যারে যতটা সম্ভব বেশি ফাংশন density চাই। isolation technology, keep-warm নীতি, আর runtime-নির্বাচন — এই তিনটা সিদ্ধান্ত কীভাবে নেবেন, আর কোন ট্রেড-অফের ভিত্তিতে?ডিজাইন
Isolation: পূর্ণাঙ্গ container শুধুমাত্র প্রত্যাখ্যাত হবে — mutually-hostile multi-tenant traffic-এ পুরো host kernel syscall surface একটা ভাগ-করা ঝুঁকি। পূর্ণ traditional VM-ও প্রত্যাখ্যাত — সেকেন্ড-স্কেল বুট ১০০ ms লক্ষ্যের সাথে সাংঘর্ষিক। সঠিক পছন্দ Firecracker-জাতীয় microVM — KVM/VT-x-এর পূর্ণ VM isolation, অথচ ~১২৫ ms-এর নিচে বুট, container-এর কাছাকাছি density।
Keep-warm নীতি: একটা uniform, ফিক্সড window এড়িয়ে ফাংশনের নিজস্ব invocation-frequency অনুযায়ী অভিযোজিত window বেছে নেওয়া বুদ্ধিমানের কাজ — ঘন ঘন invoke হওয়া ফাংশনের জন্য দীর্ঘ window (আজকের সিমুলেশন যেমন দেখিয়েছে, burst-এর মধ্যে window কাজে লাগে), কালেভদ্রে invoke হওয়া ফাংশনের জন্য ছোট বা শূন্য window (৪ নম্বর প্রশ্নের যুক্তি অনুযায়ী, নাহলে বিশুদ্ধ অপচয়)। গুরুত্বপূর্ণ, high-value গ্রাহকদের জন্য “provisioned concurrency”-জাতীয় একটা paid opt-in আলাদাভাবে রাখা যেতে পারে, যেখানে গ্রাহক নিজেই idle-খরচ বহন করার সিদ্ধান্ত নেয়।
Runtime-নির্বাচন: যেহেতু p99 লক্ষ্য কড়া, প্ল্যাটফর্ম নিজে compiled-ভাষা (Go/Rust)-কে উৎসাহিত করতে পারে (দ্রুততর runtime init, ৪ নম্বর example সেকশনের সারণি অনুযায়ী), অথবা heavier runtime (JVM)-এর জন্য snapshot-based restore-এর মতো অতিরিক্ত কৌশল যোগ করতে পারে — কারণ microVM provisioning যতই দ্রুত হোক, runtime-init খরচ সেটা স্পর্শই করে না (৫ নম্বর প্রশ্নের সরাসরি প্রয়োগ), তাই p99 লক্ষ্য অর্জনে দুইটা স্তরকেই আলাদাভাবে অপ্টিমাইজ করতে হবে।
এরপর কী
Firecracker-এর microVM পদ্ধতি এখনও প্রতিটা cold start-এ একটা (খুবই ছোট) VM বুট করে — full VM-এর তুলনায় নাটকীয়ভাবে দ্রুত, কিন্তু তবুও সম্পূর্ণ instant না। আজকের লেসন module-এর VM-arc আর container-arc-কে একটা বাস্তব সিস্টেমে মিলিয়ে দেখিয়েছে; module-এর শেষ লেসন, virtualization-cloud/edge-computing-cloudflare-workers, ঠিক এই একই isolation-বনাম-গতি টানাপোড়েনের একটা আরও radical সমাধান নিয়ে আসবে — এমন একটা পদ্ধতি যা VM/container-এর সীমানাটাই সম্পূর্ণভাবে ছেড়ে দেয়, একটা সম্পূর্ণ ভিন্ন isolation primitive-এর পক্ষে।
আরও পড়ুন
- Firecracker: Lightweight Virtualization for Serverless Applications — Alexandru Agache, Marc Brooker, Alexandra Iordache, Anthony Liguori, Rolf Neugebauer, Phil Piwonka, Diana-Maria Popa — Amazon Web Services (USENIX NSDI, 2020) · এই লেসনের কেন্দ্রীয় রেফারেন্স — microVM boot-latency, memory overhead, আর minimal device-model ডিজাইনের প্রামাণ্য উৎস; realworld সেকশনের প্রায় প্রতিটা সংখ্যা এখান থেকে উদ্ধৃত
- Peeking Behind the Curtains of Serverless Platforms — Liang Wang, Mengyuan Li, Yinqian Zhang, Thomas Ristenpart, Michael Swift (USENIX ATC 2018) · AWS Lambda, Azure Functions, Google Cloud Functions জুড়ে cold-start লেটেন্সির স্তরভিত্তিক measurement — hood সেকশনের LayerTrace-এর order-of-magnitude সময়গুলোর পেছনের empirical উৎস
- Serverless Computing: Economic and Architectural Impact — Gojko Adzic, Robert Chatley (ESEC/FSE 2017) · per-invocation billing মডেল ও FaaS-এর architectural প্রভাব নিয়ে early, প্রভাবশালী industrial-case-study গবেষণা — concept সেকশনের billing আলোচনার ভিত্তি
- Lambda execution environment — AWS ডকুমেন্টেশন · execution environment reuse (warm invocation), init phase, আর provisioned concurrency-র প্রামাণ্য, production-স্তরের বিবরণ