এই পর্বে যা শিখবেন
- একটি পুরো serverless web app-এর টুকরোগুলো চেনা, আর কে কার সাথে কীভাবে কথা বলে সেটি আঁকা
- API gateway-তে auth, throttling আর usage plan সাজানো, আর token bucket-এর হিসাব করা
- Thumbnail pipeline, fan-out আর fan-in নিরাপদভাবে বানানো
- Conditional write দিয়ে সত্যিকারের idempotent handler লেখা, আর partial batch failure সামলানো
- Function-per-route বনাম এক বড় function, database connection, observability, deployment আর local test নিয়ে সিদ্ধান্ত নেওয়া
- Container-এর সাথে মোট খরচ হিসাব করা, আর কখন serverless নয় তা যুক্তি দিয়ে বলা
১ · একটি পুরো serverless web app
পর্ব ৬৯-এ একটি একক function দেখেছেন, আর পর্ব ৭০-এ দেখেছেন queue আর event bus. এবার সব একসাথে জোড়া হবে। একটি কাল্পনিক উদাহরণ ধরি: “বাগানের আম”, রাজশাহীর কয়েকজন আমচাষির ছোট একটি অনলাইন দোকান। দোকানে ছবি আছে, অর্ডার আছে, আর মোবাইল-পেমেন্টের একটি কাল্পনিক gateway আছে। মে থেকে জুলাই ভিড়, বাকি নয় মাস প্রায় ফাঁকা। দলে একজনই developer, তিনি রাত জেগে server দেখতে চান না।
বিয়েবাড়ির ক্যাটারিং ভাবুন। সারা বছর পঞ্চাশজন বাবুর্চিকে বেতন দিয়ে রাখা হয় না। অতিথি অনুযায়ী লোক আসে, আর বিল হয় প্লেট ধরে। Serverless app এই রকম: scale to zero মানে অতিথি না থাকলে রান্নাঘর বন্ধ। তবে গেটের দারোয়ান, ভাঁড়ার আর হিসাবের খাতা ঠিকই লাগে। ওই অংশগুলোই API gateway, object storage আর database.
নিচের ছবিটি এই পর্বের মানচিত্র। একবার মন দিয়ে দেখে নিন। পরের প্রতিটি অংশ এই ছবির কোনো একটি তীর নিয়ে কথা বলবে।
Reference serverless web app
বাগানের আম: request-এর পথ ডানে, ছবির পথ বাঁয়ে
একটি কথা পরিষ্কার করে নেওয়া দরকার। Serverless আর FaaS এক জিনিস নয়, যদিও অনেকে একই অর্থে বলেন। FaaS হলো function চালানোর সেবা। Serverless বৃহত্তর শব্দ, যাতে object storage, queue বা ব্যবহার-অনুযায়ী বিলের database-ও পড়ে। সংজ্ঞা নিয়ে মতভেদ আছে, যেমন serverless container-কে কেউ এই দলে ধরেন, কেউ ধরেন না।
| টুকরো | AWS | Azure | Google Cloud |
|---|---|---|---|
| API gateway | API Gateway | API Management | API Gateway |
| Function | Lambda | Functions | Cloud Run functions |
| Managed NoSQL | DynamoDB | Cosmos DB | Firestore |
| Object storage | S3 | Blob Storage | Cloud Storage |
| Queue | SQS | Service Bus, Queue Storage | Pub/Sub |
| Workflow | Step Functions | Durable Functions, Logic Apps | Workflows |
“Serverless” শব্দটি প্রযুক্তি-লেখায় ২০১২ সালের দিকে Ken Fromm-এর একটি লেখায় দেখা যায় বলে সাধারণত উল্লেখ করা হয়। AWS Lambda ঘোষণা হয় ২০১৪ সালের শেষে, আর API Gateway আসে ২০১৫ সালে। এই দুটি জোড়া লাগতেই server ছাড়া পুরো API বানানোর ধারণাটি ছড়িয়ে পড়ে।
২ · API gateway: দরজার দারোয়ান
ছবির ডানদিকের পথটি ধরুন। ব্রাউজারের প্রতিটি API request আগে API gateway-তে আসে। এটি একটি managed reverse proxy. Function-এ কোড পৌঁছানোর আগেই অনেক কাজ এখানে সেরে ফেলা যায়, তাতে বিলও বাঁচে। নকশার তত্ত্ব আছে System Design-এর API gateway পর্বে। এখানে দেখব চালানোর দিকটি।
Auth
পরিচয় যাচাই। সাধারণ পথ হলো JWT authorizer: gateway token-এর সই আর মেয়াদ দেখে, ভুল হলে 401 দেয়। Function চলেই না।
401 · 403Throttling
গতির সীমা। সাধারণত token bucket দিয়ে। সীমা পেরোলে 429 ফেরে। পেছনের function আর database চাপ থেকে বাঁচে।
429Usage plan
গ্রাহক-ভিত্তিক সীমা। Usage plan বলে দেয় কোন API key সেকেন্ডে কত আর মাসে কত request করতে পারবে।
free · paidAPI key পরিচয়ের প্রমাণ নয়। এটি গ্রাহক চেনার আর হিসাব রাখার উপায়। ব্রাউজারের কোডে থাকা key যে কেউ দেখতে পান। AWS-এর নথিও usage plan-এর API key-কে authentication বা authorization হিসেবে ব্যবহার করতে নিষেধ করে। আসল পরিচয় যাচাই JWT, IAM বা অন্য authorizer দিয়ে করুন।
একটি হিসাব। ধরি rate সেকেন্ডে ১০০, burst ২০০। অর্থাৎ বালতিতে সর্বোচ্চ ২০০টি token থাকে, আর প্রতি সেকেন্ডে ১০০টি নতুন token ঢোকে। বালতি ভরা অবস্থায় এক সেকেন্ডে ৫০০টি request এলো। ওই সেকেন্ডে ব্যবহারযোগ্য token হলো ২০০ + ১০০ = ৩০০। তাই ৩০০টি যাবে, আর ২০০টি 429 পাবে। পরের সেকেন্ডে বালতি খালি থেকে শুরু, তাই তখন আর ১০০টির বেশি যাবে না। Burst মানে একবারের জমানো সঞ্চয়, স্থায়ী গতি নয়। আরও দেখুন rate limiting পর্বে।
Gateway-র আরেকটি গুণ, request-এর আকৃতি যাচাই। ভুল JSON ফেরত দিতে function চালাতে হয় না। তবে ব্যবসার নিয়ম, যেমন “এই অর্ডার কি এই ব্যবহারকারীর?”, function-এর ভেতরেই থাকবে। Gateway-র body আকার আর সময়ের সীমাও মনে রাখুন। লেখার সময়ে অনেক সেবায় সীমা কয়েক MB আর কয়েক সেকেন্ড থেকে কয়েক মিনিটের মধ্যে, provider-ভেদে আলাদা। এই কারণেই বড় ফাইল gateway দিয়ে পাঠানো হয় না।
৩ · Thumbnail pipeline, fan-out আর fan-in
চাষি একটি আমের ছবি তুললেন, ফোনের ক্যামেরায় ২ MB. দোকানের তালিকায় এত বড় ছবি দেখালে গ্রামের ধীর নেটে পাতা খুলবে না। তাই তিন মাপ দরকার: ১৫০, ৪৮০ আর ১০৮০ পিক্সেল। কাজটি কয়েক সেকেন্ডের, তাই ব্যবহারকারীকে অপেক্ষায় রাখা ঠিক নয়। নিচের stepper-এ একটি upload-এর পুরো যাত্রা দেখুন।
- URL চাওয়াAPI function
- সরাসরি PUTbucket
- Eventqueue
- Worker৩ মাপ
- READYCDN
{ "name": "mango-42.jpg", "type": "image/jpeg" }
200 OK
{ "url": "https://example-bucket.../uploads/mango-42.jpg?sig=FAKE",
"expires_in": 300 }
PUT uploads/mango-42.jpg Content-Type: image/jpeg Content-Length: 2000000 200 OK
{ "event": "ObjectCreated",
"key": "uploads/mango-42.jpg",
"size": 2000000,
"etag": "9a1f..." }
receive count = 1
done sizes = {}
resize 150 ... 480 ... 1080
thumbs/mango-42-150.jpg 18 KB thumbs/mango-42-480.jpg 95 KB thumbs/mango-42-1080.jpg 310 KB
photo: mango-42
status: READY
done sizes = {150, 480, 1080}
- ধাপ ১ — URL চাওয়া। ফোন ছবির নাম আর ধরন পাঠায়। Function একটি presigned URL বানায়, মেয়াদ ৩০০ সেকেন্ড। এটি শুধু এই একটি key-তে লেখার অনুমতি দেয়।
- ধাপ ২ — সরাসরি upload. ফোন ২০,০০,০০০ বাইট সোজা bucket-এ তোলে। Function এক বাইটও বহন করেনি, তাই সময় আর memory-র বিল বাঁচল, আর gateway-র আকারের সীমাও বাধা হলো না।
- ধাপ ৩ — Event. Object তৈরি হতেই storage একটি event notification পাঠায়। এটি সোজা function-এ না গিয়ে queue-তে জমে, যাতে একসাথে হাজার ছবি এলেও কাজ না হারায়। Delivery at-least-once, তাই duplicate হতে পারে।
- ধাপ ৪ — Worker. Queue থেকে worker বার্তা নেয়। Visibility timeout শুরু হয়: ৬০ সেকেন্ড বার্তাটি অন্যদের চোখের আড়ালে। এর মধ্যে মুছতে না পারলে বার্তা আবার দেখা দেবে।
- ধাপ ৫ — তিন মাপ লেখা। ফল যায়
thumbs/prefix-এ। ২ MB-র ছবির ছোট সংস্করণ ১৮ KB, প্রায় ১১১ ভাগের এক ভাগ। Event filter শুধুuploads/শোনে, নইলে নিজের ফলেই worker আবার জাগত। - ধাপ ৬ — শেষ। Database-এ অবস্থা READY, তিনটি মাপই লেখা। এরপরই worker বার্তা মোছে। মোছার আগে crash হলে কাজটি আবার চলবে, তাই প্রতিটি ধাপ idempotent হওয়া চাই। ছবি ব্যবহারকারীর কাছে যায় CDN দিয়ে।
নিজের লেজ কামড়ানো বিড়াল। একই bucket-এ ফল লিখলে আর filter না থাকলে recursive trigger হয়: ফল থেকে নতুন event, তা থেকে নতুন ফল। মিনিটে বিল লাফাতে পারে। আলাদা bucket বা prefix filter ব্যবহার করুন, আর function-এর reserved concurrency সীমিত রাখুন। কিছু provider এমন loop ধরার ব্যবস্থা দেয়, তবে তার ভরসায় নকশা করবেন না।
Fan-out আর fan-in
Fan-out মানে একটি ঘটনা থেকে অনেকগুলো সমান্তরাল কাজ। লঞ্চঘাটে একটি লঞ্চ ভিড়লে কুলিরা একসাথে মাল নামান, একজন একজন করে নয়। Fan-in মানে সবার ফল এক জায়গায় জড়ো করে পরের ধাপ শুরু করা। পথ তিনটি:
- Queue-তে অনেক বার্তা: Function প্রতি মাপের জন্য একটি বার্তা পাঠায়। সবচেয়ে সরল। Fan-in নিজে লিখতে হয়।
- Pub/sub: একটি event একাধিক subscriber-এর queue-তে যায়। একজন ছবি ছোট করে, আরেকজন ছবি যাচাই করে। একে অপরকে চেনে না। তত্ত্ব আছে pub/sub পর্বে।
- Workflow engine-এর parallel ধাপ: Fan-in সে নিজেই করে। সহজ, তবে প্রতি ধাপে বিল আসতে পারে। এ কথায় পরে ফিরব।
Fan-in-এর ফাঁদ। তিনটি কাজের জন্য remaining = 3 রেখে প্রতিটি শেষে ১ কমালেন। একটি বার্তা দুবার এলে কাউন্টার দুবার কমবে, আর কাজ অসম্পূর্ণ থাকতেই READY হয়ে যাবে। সংখ্যা না কমিয়ে set-এ নাম যোগ করুন: done = {150, 480}. একই নাম দুবার যোগ করলে set বদলায় না। তারপর set-এর আকার ৩ হলে READY. কাউন্টার idempotent নয়, set idempotent.
৪ · Idempotency: একই event দুবার এলে
এবার দোকানের সবচেয়ে সংবেদনশীল তীর। পেমেন্ট gateway webhook পাঠাল: অর্ডার ord-1042, ১,২০০ টাকা পরিশোধিত। Function জমা করল, কিন্তু উত্তর পৌঁছানোর আগেই নেট কেটে গেল। Gateway ভাবল পৌঁছায়নি, তাই আবার পাঠাল। যাচাই না থাকলে চাষির খাতায় ২,৪০০ টাকা উঠবে।
Cloud-এর queue, event আর webhook প্রায় সবই at-least-once. তাই নিয়মটি এক লাইনের: duplicate আটকানোর চেষ্টা নয়, duplicate-কে নিরীহ বানান। এই গুণটিই idempotency. হাতিয়ার হলো idempotency token, যাকে অনেক API-তে idempotency key বলে। এটি প্রতিটি কাজের একটি স্থায়ী পরিচয়, যেমন event id, যা retry-তে বদলায় না।
ব্যাংকের চেকের নম্বর ভাবুন। একই চেক দুবার জমা দিলে ক্যাশিয়ার খাতা খুলে দেখেন, এই নম্বর আগেই ভাঙানো। টাকা দ্বিতীয়বার যায় না। নম্বরটি চেকের সাথেই আসে, ক্যাশিয়ার বানান না।
কিন্তু “আগে দেখে নিই আছে কি না, তারপর লিখি” যথেষ্ট নয়। দুটি copy একই মুহূর্তে এলে দুজনেই দেখবে নেই, আর দুজনেই লিখবে। সমাধান conditional write: পড়া আর লেখা database-এর ভেতরে একটিই অবিভাজ্য কাজ। শর্তটি হলো: key না থাকলেই লেখা, নইলে পুরো কাজ ব্যর্থ। নিচের trace-এ দেখুন।
1def handler(event):2 ok = transact(put(event.id, if_absent),3 add(balance, event.taka))4 if not ok: count("duplicate")5 return "done"
| payments | |
| balance | |
| receive | |
| message |
- প্রথম copy এলো।
evt-71, ১,২০০ টাকা। টেবিল খালি, balance ০। - Transaction সফল।
evt-71নেই, তাই শর্ত পূরণ। Record আর balance একসাথে লেখা হলো, balance 1200. - উত্তর হারাল। Function শেষে timeout বা নেটের গোলমাল, ack পৌঁছায়নি। Visibility timeout শেষে বার্তা আবার দেখা দিল।
- দ্বিতীয় copy এলো। একই
evt-71, receive count এখন ২। অন্য একটি function instance-ও এটি ধরতে পারে। - শর্ত ব্যর্থ। Key আগেই আছে, তাই database পুরো transaction ফিরিয়ে দিল। Balance বদলায়নি, এখনো 1200.
- ব্যর্থতা নয়, সফলতা। Handler duplicate গুনে সফল ফেরত দেয়। Error ছুড়লে বার্তা আবার আসত। বার্তা মুছে গেল। ফল: টাকা ঠিক একবার জমা হয়েছে।
এবার সিমুলেটর। চারবারে পাঁচটি delivery আসে। শেষ দলে একই event-এর দুটি copy একই মুহূর্তে। তারপর দ্বিতীয় অংশে দেখবেন, দুটি লেখার মাঝে crash হলে কী হয়।
// Seller balance under duplicate delivery. Prices in taka.
// Each inner list arrives at the SAME moment (two function copies at once).
const taka = { "evt-71": 1200, "evt-72": 850, "evt-73": 400 };
const deliveries = [["evt-71"], ["evt-72"], ["evt-71"], ["evt-73", "evt-73"]];
function run(name, write) {
const table = new Set();
let balance = 0, blocked = 0;
for (const group of deliveries) {
const seen = group.map((id) => table.has(id)); // every copy reads first
group.forEach((id, i) => { // then every copy writes
if (write(table, id, seen[i])) balance += taka[id]; else blocked++;
});
}
console.log(name.padEnd(17), "balance", String(balance).padStart(5), " blocked", blocked);
}
const putIfAbsent = (t, id) => (t.has(id) ? false : (t.add(id), true));
run("no check", (t, id) => { t.add(id); return true; });
run("read then write", (t, id, seen) => { if (seen) return false; t.add(id); return true; });
run("conditional put", (t, id) => putIfAbsent(t, id));
console.log("correct balance", 1200 + 850 + 400);
// Crash between two steps, then the queue delivers the event again.
function crashThenRetry(sameTransaction) {
const table = new Set();
let balance = 0;
// first try: the key is saved, then the function dies before crediting
if (!sameTransaction) table.add("evt-71"); // a transaction saves nothing
// retry
if (putIfAbsent(table, "evt-71")) balance += taka["evt-71"];
return balance;
}
console.log("crash, two separate writes:", crashThenRetry(false));
console.log("crash, one transaction :", crashThenRetry(true));no check balance 4050 blocked 0
read then write balance 2850 blocked 1
conditional put balance 2450 blocked 2
correct balance 2450
crash, two separate writes: 0
crash, one transaction : 1200
read then write ধীরে আসা duplicate আটকেছে, কিন্তু একই মুহূর্তের দুটি copy আটকাতে পারেনি। তাই ৪০০ টাকা বেশি, ২,৮৫০। শুধু conditional put সঠিক ২,৪৫০ দিয়েছে।
শেষের দুই লাইন আরেকটি ফাঁদ দেখায়। Key আগে লিখে তারপর টাকা জমা করতে গিয়ে crash হলে retry-তে key পাওয়া যায়, তাই কাজটি বাদ পড়ে। হিসাব ০, টাকা হারাল। Duplicate আটকাতে গিয়ে আমরা কাজ হারানো ডেকে আনলাম। Key আর আসল লেখা একই transaction-এ থাকলে ১,২০০ ঠিকই জমা হয়।
বিভিন্ন database-এ conditional write
নিচে একটি উদাহরণ। এটি এখানে চালানো হয়নি, আউটপুট নমুনা।
# উদাহরণ: DynamoDB-তে key না থাকলেই লেখা
aws dynamodb put-item --table-name payments \
--item '{"pk":{"S":"evt-71"},"taka":{"N":"1200"}}' \
--condition-expression "attribute_not_exists(pk)"
# নমুনা আউটপুট, দ্বিতীয়বার চালালে (version-ভেদে আলাদা হতে পারে):
# An error occurred (ConditionalCheckFailedException) when calling
# the PutItem operation: The conditional request failed-- উদাহরণ: PostgreSQL-এ একই কাজ
INSERT INTO payments (event_id, taka)
VALUES ('evt-71', 1200)
ON CONFLICT (event_id) DO NOTHING;
-- প্রভাবিত row 0 হলে duplicate.Firestore-এ create আর Cosmos DB-তে একই id-র create-ও প্রায় একইভাবে ব্যর্থ হয়। Object storage-এও লেখার শর্ত আছে, যেমন If-None-Match: * header, তবে সমর্থন provider-ভেদে দেখে নিন।
চারটি বাস্তব সিদ্ধান্ত:
- Key কোথা থেকে? ব্যবসার পরিচয় থেকে, যেমন পেমেন্টের transaction id. Queue-র message id প্রায়ই ভালো নয়, কারণ উৎস আবার পাঠালে নতুন id হতে পারে। Client থেকে আসা API request-এ client নিজে একটি token বানিয়ে পাঠায়।
- Record কতদিন? Retry-র সর্বোচ্চ সময়ের চেয়ে বেশি। TTL দিয়ে পুরোনো key মুছুন, যেমন ৭ দিন।
- বাইরের কাজ? SMS বা অন্য API transaction-এ ঢোকে না। তাদের কাছেও idempotency key পাঠান। না থাকলে “অন্তত একবার” না “সর্বোচ্চ একবার”, সেটি বেছে নিতে হয়।
- উত্তরও মনে রাখুন। API-র ক্ষেত্রে দ্বিতীয় request-কে আগের একই ফল দিন। Key-এর পাশে response রেখে দেওয়া যায়।
আরও গভীরে: Pat Helland-এর “Idempotence Is Not a Medical Condition” (ACM Queue, ২০১২) আর “Life beyond Distributed Transactions” (CIDR, ২০০৭) এই চিন্তার ভিত্তি। HTTP-তে Idempotency-Key header নিয়ে IETF-এ একটি খসড়া কাজ হয়েছে, এবং কয়েকটি পেমেন্ট API-তে এমন header আগে থেকেই আছে। অবস্থা বদলাতে পারে, দেখে নিন। AWS-এর Powertools library-তে তৈরি idempotency সুবিধা আছে, তবে এই নিয়মগুলো না বুঝে ব্যবহার করবেন না।
৫ · Partial batch failure: শুধু ব্যর্থগুলোই আবার
Queue থেকে function সাধারণত একসাথে অনেক বার্তা নেয়, ধরুন ১০টি। এতে খরচ কমে। কিন্তু ৯টি সফল আর ১টি ব্যর্থ হলে কী হবে? ডিফল্ট আচরণে পুরো batch ব্যর্থ ধরা হয়, আর ১০টি বার্তাই আবার আসে। মানে ৯টি ঠিক কাজ আবার। সবচেয়ে খারাপ দিকটি হলো, একটি নষ্ট ছবি থাকলে পুরো দলটিই সীমায় পৌঁছে dead-letter queue-তে যেতে পারে।
এই একটি নষ্ট বার্তাই poison message. বাসের ছাদে একটি বেঁধে-না-রাখা বস্তা যেন পুরো বাসকে আটকে রেখেছে। সমাধান partial batch failure: function শুধু ব্যর্থ বার্তার পরিচয় ফেরত দেয়, বাকিগুলো মুছে যায়। Lambda-তে নিয়মটি হলো event source-এ ReportBatchItemFailures চালু করে এই আকারে উত্তর দেওয়া:
{ "batchItemFailures": [ { "itemIdentifier": "msg-07" } ] }অন্য provider-এ রূপ ভিন্ন, যেমন প্রতিটি বার্তা আলাদা করে ack বা complete করা। ধারণা একই। নিচের সিমুলেটরে ১০টি ছবির একটি batch আছে। img-07 নষ্ট, সে কখনো সফল হবে না। বাকিগুলো ২০% সময় ব্যর্থ হয়। সর্বোচ্চ তিনবার পাঠানো হবে।
// One batch of 10 images. img-07 is corrupt, others fail 20% of the time.
function mulberry32(a) {
return () => {
a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const ids = Array.from({ length: 10 }, (_, i) => "img-0" + i);
const CORRUPT = "img-07", MAX_RECEIVE = 3;
function simulate(partial) {
const rnd = mulberry32(71), done = new Set(), dlq = [], tries = {};
let pending = ids.slice(), calls = 0;
console.log(partial ? "\nretry only failed items" : "\nretry the whole batch");
for (let round = 1; pending.length; round++) {
const failed = [];
for (const id of pending) {
calls++; tries[id] = (tries[id] || 0) + 1;
if (id !== CORRUPT && rnd() > 0.2) done.add(id); else failed.push(id);
}
console.log(` round ${round}: sent ${pending.length}, failed [${failed.join(" ")}]`);
const again = partial ? failed : (failed.length ? pending : []);
pending = again.filter((id) => tries[id] < MAX_RECEIVE);
dlq.push(...again.filter((id) => tries[id] >= MAX_RECEIVE));
}
const usable = ids.filter((id) => done.has(id)).length;
const lost = dlq.filter((id) => done.has(id)).length;
console.log(` resize calls ${calls}, finished ${usable}/10, dead-letter ${dlq.length}`
+ ` (${lost} of them had already finished)`);
console.log(` fan-in: album ${usable === 10 ? "READY" : "PARTIAL"}`);
}
simulate(false);
simulate(true);
retry the whole batch
round 1: sent 10, failed [img-01 img-05 img-06 img-07]
round 2: sent 10, failed [img-07]
round 3: sent 10, failed [img-03 img-06 img-07 img-08]
resize calls 30, finished 9/10, dead-letter 10 (9 of them had already finished)
fan-in: album PARTIAL
retry only failed items
round 1: sent 10, failed [img-01 img-05 img-06 img-07]
round 2: sent 4, failed [img-07]
round 3: sent 1, failed [img-07]
resize calls 15, finished 9/10, dead-letter 1 (0 of them had already finished)
fan-in: album PARTIAL
দুই পথেই ৯টি ছবি তৈরি। পার্থক্য দামে। পুরো batch retry-তে ৩০টি resize, আর শুধু ব্যর্থগুলো retry করলে ১৫টি, অর্ধেক। আরও বড় কথা dead-letter queue. প্রথম পথে ১০টি বার্তাই সেখানে, যার ৯টি আসলে শেষ হয়ে গেছে। রাতে on-call engineer দেখবেন ১০টি “ব্যর্থ” বার্তা, অথচ সমস্যা একটিই। দ্বিতীয় পথে সেখানে শুধু img-07.
Partial retry-ও কিন্তু duplicate আটকায় না। Timeout-এ পুরো batch আবার আসতে পারে। তাই idempotency তবুও লাগবে। Fan-in বলছে album PARTIAL: একটি ছবি বাকি থাকলেও দোকান বাকি ৯টি দেখাতে পারে। পুরো কাজ থামানো উচিত কি না, সেটি ব্যবসার সিদ্ধান্ত।
Dead-letter queue কোনো ডাস্টবিন নয়, এটি মেরামতের খাতা। এর গভীরতায় alert রাখুন, ঠিক করার পর বার্তাগুলো আবার মূল queue-তে পাঠান (redrive), আর আগে নিশ্চিত হোন handler idempotent. একটি পরিচিত নিয়ম: queue-র visibility timeout function-এর timeout-এর কয়েক গুণ রাখুন। AWS-এর নথি অন্তত ৬ গুণের পরামর্শ দেয়, নইলে চলন্ত কাজের বার্তা আবার দেখা দিয়ে অকারণ duplicate বানায়।
৬ · এক route এক function, নাকি একটি বড় function? আর database
দোকানে ১২টি API route. প্রতিটির জন্য আলাদা function, নাকি একটি function-এর ভেতরেই ছোট web app? দ্বিতীয়টিকে অনানুষ্ঠানিকভাবে Lambdalith বলা হয়। এটি community-র শব্দ, কোনো মান নয়, আর বিতর্কটি এখনো চলছে। কোনো পক্ষকেই চিরন্তন সঠিক ধরবেন না।
| দিক | প্রতি route এক function | এক বড় function |
|---|---|---|
| অনুমতি | প্রতিটির নিজের ছোট role | এক role-এ সব অনুমতি জমে |
| Memory আর timeout | Route অনুযায়ী আলাদা | সবার জন্য এক মাপ |
| Cold start | ছোট প্যাকেজ, তবে প্রতিটি আলাদা ঠান্ডা হয় | বড় প্যাকেজ, তবে instance বেশি গরম থাকে |
| Local চালানো | কঠিন, অনেক টুকরো | সহজ, সাধারণ framework |
| Metric আর alert | প্রতি route আলাদা পাওয়া যায় | নিজে route ধরে মাপতে হয় |
| পরে সরানো | অনেক টুকরো গোছাতে হয় | Container-এ নেওয়া তুলনামূলক সহজ |
একটি মধ্যপথ প্রচলিত: ব্যবসার ক্ষেত্র ধরে ভাগ। অর্ডারের সব route এক function-এ, পেমেন্ট আলাদা, কারণ তার অনুমতি আর ঝুঁকি আলাদা। ভারী worker সবসময় আলাদা, কারণ তার memory আর timeout আলাদা। ছোট দলে “বাগানের আম”-এর জন্য তিনটি function যথেষ্ট: API, পেমেন্ট webhook, আর ছবির worker.
Database-এর কঠিন হিসাব
Function যতগুলো একসাথে চলে, database connection-ও ততগুলো হতে পারে, কারণ প্রতিটি instance নিজের connection রাখে। ধরুন ঈদের আগের সন্ধ্যায় ৫০০টি instance একসাথে চলছে, আর database-এর max_connections ২০০। এই connection storm-এ ৩০০টি instance সংযোগই পাবে না। প্রচলিত connection pool-ও সাহায্য করে না, কারণ প্রতিটি instance-এর pool আলাদা। পথ তিনটি:
- Database proxy: মাঝখানে বসে অনেক function-এর connection অল্প কিছু আসল connection-এ ভাগ করে। উদাহরণ RDS Proxy, বা নিজে চালানো PgBouncer. Connection pinning হলে লাভ কমে। বিস্তারিত পর্ব ২২-এ।
- HTTP-ভিত্তিক database: DynamoDB, Firestore বা Cosmos DB-তে স্থায়ী connection-এর সমস্যাটি প্রায় নেই। বিনিময়ে মডেল আগেই query ধরে সাজাতে হয়, দেখুন পর্ব ২৩।
- Serverless database: Aurora Serverless, Azure SQL-এর serverless স্তর বা Neon-এর মতো সেবা ক্ষমতা নিজে বাড়ায়-কমায়। শূন্যে নামা আর জাগতে সময় লাগা নিয়ে শর্ত সেবাভেদে আলাদা, লেখার সময়ের নথি দেখে নিন। “Serverless” নাম মানেই শূন্য বিল নয়।
Function-এ concurrency-র সীমা রাখা database রক্ষার সহজ উপায়। ১৫০-এ সীমা দিলে বাকিরা queue-তে অপেক্ষা করে বা 429 পায়, database পড়ে না। Serverless স্তর দ্রুত বাড়ে, কিন্তু সবচেয়ে ধীর টুকরোর গতিই পুরো ব্যবস্থার গতি।
৭ · চালানো: observability, deployment, test আর নিরাপত্তা
Observability: টুকরোগুলোর পথ জোড়া
Server-এ ঢুকে দেখার সুযোগ নেই, আর একটি কাজ ছড়িয়ে আছে gateway, দুটি function আর queue-তে। তাই প্রতিটি কাজে একটি correlation ID লাগে, যা structured log-এ থাকবে। HTTP-তে traceparent নিজে থেকে যেতে পারে, কিন্তু queue-র ওপারে সাধারণত নিজেকে পাঠাতে হয় message attribute-এ। Batch-এ একটি function অনেক বার্তা ধরলে এক parent মানায় না, তখন span link কাজে লাগে। বিস্তারিত পর্ব ৬৫-তে।
কয়েকটি metric অবশ্যই দেখুন: function-এর error আর throttle, p95 সময়, cold start-এর হার, queue-র সবচেয়ে পুরোনো বার্তার বয়স, আর dead-letter queue-র গভীরতা। শেষ দুটি সবচেয়ে জরুরি, কারণ async ব্যবস্থায় ব্যবহারকারী কোনো error দেখেন না, শুধু ছবিটি আসে না। Log-এর বিলও দেখুন, প্রতি request-এ লম্বা log লিখলে তা function-এর বিলকেও ছাড়াতে পারে।
Deployment
| হাতিয়ার | ধরন | খেয়াল রাখুন |
|---|---|---|
| AWS SAM | YAML, CloudFormation-এর ছোট রূপ | শুধু AWS, local চালানোর CLI আছে |
| Serverless Framework | YAML, একাধিক provider-এর ইতিহাস | নতুন version-এর license শর্ত দেখে নিন |
| AWS CDK | TypeScript, Python ইত্যাদিতে infrastructure | শেষে CloudFormation হয়, লজিক বেশি জটিল করবেন না |
| Terraform | HCL, যেকোনো provider | অন্য infrastructure-এর সাথে এক state-এ রাখা যায়, কোড প্যাকেজিং আলাদা ভাবতে হয় |
Azure-এ Bicep আর Google Cloud-এ Terraform বা gcloud-ও প্রচলিত। নিচে ছবির worker-এর একটি SAM উদাহরণ। এতে অংশ ৫-এর দুটি নিয়ম খেয়াল করুন: partial batch response, আর timeout ৬০ সেকেন্ডের ৬ গুণ visibility timeout.
# উদাহরণ, চালানো হয়নি। Deploy করলে খরচ হতে পারে।
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
ThumbWorker:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.13
Handler: app.handler
CodeUri: src/
MemorySize: 1024
Timeout: 60
ReservedConcurrentExecutions: 20
Events:
Uploads:
Type: SQS
Properties:
Queue:
Fn::GetAtt: [ThumbQueue, Arn]
BatchSize: 10
FunctionResponseTypes:
- ReportBatchItemFailures
ThumbQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 360
RedrivePolicy:
deadLetterTargetArn:
Fn::GetAtt: [ThumbDlq, Arn]
maxReceiveCount: 3
ThumbDlq:
Type: AWS::SQS::QueueLocal test: কোনটি কোথায়
Cloud-কে পুরোপুরি ল্যাপটপে আনা যায় না, তাই স্তর ভাগ করে test করুন।
- ব্যবসার লজিক: Handler থেকে আলাদা সাধারণ function রাখুন, আর সাধারণ unit test লিখুন। এতে cloud লাগে না, আর এখানেই বেশিরভাগ test থাকা উচিত।
- Handler: নমুনা event JSON দিয়ে চালান।
sam local invoke, Azure-এরfunc startবা Google-এর Functions Framework এই কাজে লাগে। Duplicate event আর batch-এ একটি নষ্ট বার্তা, এই দুটি test আবশ্যক। - সেবার সাথে সংযোগ: Emulator কাজে লাগে, তবে অনুমতি, সীমা আর সময়ের আচরণ হুবহু মেলে না। তাই আসল IAM আর আসল event-এর জন্য একটি স্বল্পস্থায়ী test stack-ই ভরসা। কাজ শেষে stack মুছে দিন, আর budget alert রাখুন।
নিরাপত্তার জমি
OS patch এখন provider-এর দায়, তাতে নিরাপত্তা শেষ হয় না, শুধু জায়গা বদলায় (shared responsibility). Server একটি থাকলে দরজা একটি, কিন্তু এখানে প্রতিটি trigger একটি দরজা।
| ঝুঁকি | প্রতিরক্ষা |
|---|---|
| বড় অনুমতি | প্রতি function নিজের role, least privilege, * নয়। দেখুন পর্ব ৩১ |
| Event-এর ভেতরের খারাপ ডেটা | Queue আর bucket-এর event-ও অবিশ্বস্ত ইনপুট। Schema যাচাই, webhook-এর সই যাচাই |
| Environment variable-এ secret | Secret manager থেকে আনা, সেখানে rotation. দেখুন পর্ব ৩৩ |
| পুরোনো library | Dependency scan, প্যাকেজ ছোট রাখা, পুরোনো runtime বদলানো |
| Denial of wallet | Throttling, usage plan, WAF, concurrency সীমা, budget alert |
| বাইরে খোলা URL | Function-এর সরাসরি URL বন্ধ, শুধু gateway দিয়ে প্রবেশ, bucket-এ block public access |
৮ · মোট খরচ: serverless বনাম সবসময় চালু container
পর্ব ১৯-এ শুধু compute-এর crossover দেখেছিলেন। এবার পুরো ব্যবস্থার হিসাব। সব দামই অনুমান, পড়ার সময় provider-এর pricing পাতা দেখে নিন। Serverless-এ প্রতি মিলিয়ন request-এ compute আর call $২.২০, gateway $১.০০, log $০.৩০; মোট $৩.৫০। Container-এ দুটি zone-এর জন্য সর্বনিম্ন ২টি task, প্রতিটি মাসে $২৮.৮০, আর load balancer $২০। একটি task সেকেন্ডে ১৫টি request নেয়। Fleet peak-এর মাপে স্থির। Database দুই দিকেই একই, তাই বাদ।
হাতে হিসাব। Container-এর সর্বনিম্ন বিল ২০ + ২ × ২৮.৮০ = $৭৭.৬০। Serverless সেখানে পৌঁছায় ৭৭.৬০ ÷ ৩.৫০ ≈ ২২.২ মিলিয়ন request-এ, গড়ে সেকেন্ডে প্রায় ৮.৬টি। Peak ২ গুণ হলে ১৭.১ req/s, দুটি task-এর ৩০-এর মধ্যে, তাই হিসাব ঠিক আছে।
// Monthly bill for one API: serverless vs always-on containers. ASSUMED prices.
const P = {
gwPerM: 1.0, fnPerM: 2.2, logPerM: 0.3, // $ per million requests
taskMonth: 28.8, taskRps: 15, minTasks: 2, // containers, two zones
lbMonth: 20, // load balancer, flat
};
const rps = (m) => m * 1e6 / (30 * 86400); // m = million requests a month
const serverless = (m) => m * (P.gwPerM + P.fnPerM + P.logPerM);
const tasks = (m, peak) =>
Math.max(P.minTasks, Math.ceil(rps(m) * peak / P.taskRps));
const containers = (m, peak) => P.lbMonth + tasks(m, peak) * P.taskMonth;
console.log("M req/mo avg rps serverless containers(peak x2) tasks");
for (const m of [0.1, 1, 5, 10, 22, 23, 30, 60, 100, 300]) {
console.log(
String(m).padStart(8), rps(m).toFixed(1).padStart(8),
("$" + serverless(m).toFixed(0)).padStart(11),
("$" + containers(m, 2).toFixed(0)).padStart(20),
String(tasks(m, 2)).padStart(6));
}
// From which load is serverless dearer for good? Fleet is sized for the peak.
console.log("\npeak/avg serverless stays dearer from");
for (const peak of [2, 3, 4, 5, 10]) {
let last = 0;
for (let m = 0.1; m <= 2000; m += 0.1) {
if (serverless(m) <= containers(m, peak)) last = m;
}
const text = last > 1999 ? "never (up to 2000M)" : (last + 0.1).toFixed(1) + "M";
console.log(String(peak).padStart(8), " ", text);
}M req/mo avg rps serverless containers(peak x2) tasks
0.1 0.0 $0 $78 2
1 0.4 $4 $78 2
5 1.9 $18 $78 2
10 3.9 $35 $78 2
22 8.5 $77 $78 2
23 8.9 $81 $78 2
30 11.6 $105 $78 2
60 23.1 $210 $135 4
100 38.6 $350 $193 6
300 115.7 $1050 $481 16
peak/avg serverless stays dearer from
2 22.2M
3 30.4M
4 88.1M
5 never (up to 2000M)
10 never (up to 2000M)
উপরের টেবিলে ২২ আর ২৩ মিলিয়নের মাঝে দুই বিল জায়গা বদলায়। হাতের হিসাবের সাথে মিলল। ৩০০ মিলিয়নে serverless $১,০৫০, আর container $৪৮১, দুই গুণের বেশি।
নিচের টেবিলটিই আসল শিক্ষা। Peak যত উঁচু, স্থির fleet তত বেশি অলস বসে থাকে। এক task-এ মাসে গড়ে ৩৮.৮৮ ÷ peak মিলিয়ন request যায়, তাই container-এর দাম দাঁড়ায় প্রতি মিলিয়নে প্রায় ০.৭৪ × peak ডলার। এটি $৩.৫০ ছাড়ায় peak প্রায় ৪.৭ গুণ হলে। তাই ৫ গুণে স্থির fleet আর কখনো সস্তা হয় না।
দুই বিল, ০ থেকে ৬০ মিলিয়ন request
Peak গড়ের ২ গুণ, অনুমিত দাম
এই হিসাবের বাইরে কী রইল? মানুষের সময়। ধরুন একজন engineer-এর এক ঘণ্টার দাম $১০, আর container fleet patch, scaling আর image-এর পেছনে মাসে ৫ ঘণ্টা বাড়তি লাগে। তাহলে container-এর দিকে $৫০ যোগ হয়, আর crossover (৭৭.৬০ + ৫০) ÷ ৩.৫০ ≈ ৩৬.৫ মিলিয়নে সরে যায়। অন্যদিকে autoscaling চালু থাকলে container-এর কার্যকর peak কমে, ফল উল্টো দিকে সরে। Adam Eivy-র “Be Wary of the Economics of ‘Serverless’ Cloud Computing” (IEEE Cloud Computing, ২০১৭) ঠিক এই সতর্কতাই দেয়: ব্যবহার-অনুযায়ী দাম সবসময় কম দাম নয়। বিলের খুঁটিনাটি পরের FinOps মডিউলে আসছে।
৯ · কখন serverless নয়, আর lock-in-এর বাস্তবতা
তিনটি পরিস্থিতিতে আগে থেমে ভাবুন:
- দীর্ঘ কাজ: Function-এর সময়সীমা আছে। লেখার সময়ে Lambda-তে সর্বোচ্চ ১৫ মিনিট, অন্য সেবায় ভিন্ন। ঘণ্টার পর ঘণ্টার কাজ টুকরো করা যায়, তবে তখন checkpoint আর সমন্বয়ের ঝামেলা আপনার।
- দিনরাত সমান উঁচু load: অলস সময় না থাকলে scale to zero-র কোনো লাভ নেই, আর প্রতি request-এর দাম চড়ে যায়। অংশ ৮-এর peak ২ গুণের সারি দেখুন।
- কম ও স্থির latency: Cold start শত মিলিসেকেন্ড থেকে কয়েক সেকেন্ড যোগ করতে পারে। Provisioned concurrency কমায়, তবে তখন অলস সময়েরও বিল আসে। Real-time গেম বা trading-এর মতো কাজে স্থায়ী process-ই ভালো।
এক রাতের কাজ দিয়ে সিদ্ধান্তটি ধাপে ধাপে দেখি। দোকান এখন কয়েকটি জেলার বাজারের দর নিয়ে রাতে একটি পূর্বাভাসের হিসাব চালাতে চায়। কাজটি একটানা ৫ ঘণ্টার, লাগে ৮ GB memory, আর একটি process-এর ভেতরে সব ডেটা থাকে।
1job: 5 h, 8 GB, nightly2fits function limit (15 min)?3can it split into chunks?4price: function vs container5spot + checkpoint?6decide; trigger stays serverless
| length | |
| function | |
| container | |
| always-on |
- কাজের মাপ। ৫ ঘণ্টা মানে ৩০০ মিনিট বা ১৮,০০০ সেকেন্ড, ৮ GB memory, প্রতি রাতে একবার। সবার আগে এই সংখ্যাগুলো লিখে নিন।
- সীমার পরীক্ষা। ধরি function-এর সীমা ১৫ মিনিট। ৩০০ মিনিট তার ২০ গুণ। এক টানে চলবে না।
- টুকরো করা যায়? ১০ মিনিটের টুকরো ধরলে ৩০০ ÷ ১০ = ৩০টি, মাঝে একটি workflow engine আর state রাখার জায়গা। কিন্তু এই কাজের ডেটা এক process-এর memory-তে থাকে, তাই কোড নতুন করে লিখতে হবে।
- দামের তুলনা। Function: ৮ × ১৮,০০০ = ১,৪৪,০০০ GB-second, প্রতিটি $০.০০০০২ ধরে রাতে $২.৮৮, মাসে $৮৬.৪০. ঘণ্টায় $০.৩২-এর container: ৫ × ৩০ × ০.৩২ = $৪৮. সেই একই মাপ সারা মাস চালু: ৭২০ × ০.৩২ = $২৩০.৪০.
- আরও কমানো। Spot ক্ষমতায় ধরি ৬০% ছাড়, মাসে $১৯.২০. কিন্তু মাঝপথে কেড়ে নিতে পারে, তাই প্রতি ১৫ মিনিটে checkpoint লাগবে, আর সকালের আগে শেষ করার সময় হাতে রাখতে হবে।
- সিদ্ধান্ত। Batch container: কাজ চলার সময়টুকুই বিল, রাত ফুরালে কিছু চলে না। যেমন Cloud Run jobs, AWS Batch, Azure Container Apps jobs বা Kubernetes Job. কিন্তু শুরু করার scheduler আর ব্যর্থতার alert function দিয়েই হবে। Serverless হয় পুরো হ্যাঁ বা পুরো না নয়।
একটি প্রকাশ্য উদাহরণ
২০২৩ সালের মার্চে Amazon Prime Video-র একটি দল নিজেদের প্রযুক্তি ব্লগে লিখেছিল, তাদের অডিও-ভিডিও মান যাচাইয়ের একটি টুল serverless টুকরো থেকে এক process-এ সরিয়ে আনা হয়েছে। তাদের লেখা অনুযায়ী মূল ব্যবস্থাটি বানানো ছিল workflow সেবা আর function দিয়ে, মাঝের ফ্রেম জমা থাকত object storage-এ। তাদের বলা কারণ দুটি: প্রতিটি stream-এর প্রতি সেকেন্ডে অনেক workflow state transition-এর খরচ, আর object storage-এ অসংখ্য call-এর খরচ। স্কেলের সীমাতেও আটকেছিল। সব এক process-এ এনে ফ্রেম memory-তেই পাঠানো হয়, container-এ চলে, আর তারা খরচ ৯০%-এর বেশি কমার কথা জানায়। মূল লেখাটি পরে সেই ঠিকানা থেকে সরানো হয়েছে বলে জানা যায়, তাই সংখ্যাগুলো তাদের দাবি হিসেবেই পড়ুন।
এর মানে “serverless খারাপ” নয়। এটি একটি কাজের গল্প, যেখানে বিশাল ডেটা টুকরোগুলোর মাঝে ঘুরছিল। ওই একই টুকরোগুলো ছোট পরিমাণে পরীক্ষার জন্য বেশ ভালো ছিল বলেও বোঝা যায়। নকশা কাজের আকারের সাথে বদলাতে পারে।
Vendor lock-in: কী আসলে আটকে যায়
Lock-in হ্যাঁ বা না নয়, এটি বের হওয়ার দাম। Function-এর ব্যবসার কোড সরানো তুলনামূলক সহজ। আসল আঠা অন্য জায়গায়: event-এর আকৃতি, IAM নিয়ম, workflow-এর ভাষা, NoSQL-এর ডেটা মডেল, আর data gravity.
| যা করা ভালো | যা সাধারণত লাভের চেয়ে দামি |
|---|---|
| ব্যবসার লজিক handler থেকে আলাদা (ports and adapters) | নিজের “সব cloud-এর এক API” বানানো |
| মানক ব্যবহার: PostgreSQL, S3-সমতুল্য API, OpenTelemetry, CloudEvents | প্রতিটি managed সেবার wrapper লেখা |
| Infrastructure কোডে রাখা, ডেটা রপ্তানির পথ জানা | সব provider-এ একসাথে চালানো, যখন প্রয়োজন নেই |
| Container image-এ function প্যাকেজ করা, যেখানে সম্ভব | শুধু ভবিষ্যৎ-ভয়ে শক্তিশালী সেবা বাদ দেওয়া |
সব আড়াল করতে গেলে পড়বেন lowest common denominator-এর ফাঁদে, আর আড়ালটিও ফাঁক দিয়ে বেরোবে (leaky abstraction). দীর্ঘ আলোচনা আসছে পর্ব ৭৪-এ। ক্যাটারিংয়ের ঠিকাদার বদলাতে খরচ আছে, তবু আপনি নিশ্চয়ই নিজের জন্য চুলা, বাসন আর পিঁয়াজের গুদাম বানিয়ে ঠিকাদার-ভয় কাটাবেন না।
পরীক্ষা ও ইন্টারভিউতে: ১. Idempotency আর exactly-once-এর পার্থক্য? প্রথমটি handler-এর গুণ, দ্বিতীয়টি delivery-র দাবি, যা সাধারণ network-এ নিশ্চিত করা যায় না। ২. Conditional write কেন, read-then-write কেন নয়? পড়া-লেখা অবিভাজ্য বলে দুটি একসাথের copy-ও আটকে যায়। ৩. বড় ফাইল upload কীভাবে? Presigned URL দিয়ে সরাসরি object storage-এ। ৪. Poison message কীভাবে সামলাবেন? Partial batch response আর dead-letter queue দিয়ে আলাদা করা হয়। ৫. Serverless-এ connection সমস্যা কেন? প্রতি instance-এর নিজের connection, সমাধান proxy, concurrency সীমা বা HTTP-ভিত্তিক database.
১০ · AI-এর যুগে serverless নকশা
AI কয়েক সেকেন্ডে পুরো template লিখে দেয়, আর তা deploy-ও হয়ে যায়। কিন্তু serverless-এর ভুল প্রায়ই স্বাভাবিক দিনে দেখা যায় না, ধরা পড়ে retry-র দিনে বা বিলের দিনে। AI-এর সাধারণ ভুলগুলো: function-এর role-এ dynamodb:* আর Resource: "*", public bucket, environment variable-এ secret, অস্তিত্বহীন property, বাতিল runtime, read-then-write “idempotency”, batch-এ partial response ভুলে যাওয়া, একই bucket-এ ফল লেখা, আর খরচের কোনো কথা না বলা। কখনো সে test stack মোছার ধ্বংসাত্মক কমান্ডও কোনো সতর্কতা ছাড়াই দেয়।
AI-কে কাজ না দিয়ে আগে নকশা যাচাই করতে বলুন, আর প্রতিটি ব্যর্থতার পথ জিজ্ঞেস করুন:
AI-এর লেখা serverless template যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১“Exactly-once delivery” কেন প্রায় অসম্ভব, অথচ কিছু সেবা “exactly-once” লেখে কীভাবে?
সমস্যাটি পুরোনো, আর তার নাম Two Generals Problem. দুই সেনাপতি বার্তাবাহক দিয়ে আক্রমণের সময় ঠিক করতে চান, কিন্তু বার্তাবাহক ধরা পড়তে পারে। পাঠানো বার্তার প্রাপ্তি-স্বীকারও তো একটি বার্তা, যেটিও হারাতে পারে। অবিশ্বস্ত পথে দুই পক্ষ কোনো সীমিত সংখ্যক বার্তায় নিশ্চিত হতে পারে না যে অন্য পক্ষ জানে।
Queue-র ক্ষেত্রে ব্যাপারটি এমন। Consumer কাজ শেষ করে ack পাঠাল, আর ack হারাল। Queue দুটি ঘটনা আলাদা করতে পারে না: কাজ হয়নি, নাকি কাজ হয়েছে কিন্তু খবর আসেনি। সে আবার পাঠালে duplicate হতে পারে (at-least-once), আর না পাঠালে কাজ হারাতে পারে (at-most-once). তৃতীয় পথ নেই।
তাহলে “exactly-once” লেখা সেবাগুলো কী করে? সাধারণত তারা একটি সীমিত গণ্ডির ভেতরে duplicate সরায়। যেমন SQS FIFO queue একটি সময়-জানালার মধ্যে deduplication করে। Apache Kafka-র transaction পড়া, প্রক্রিয়া আর লেখাকে একই Kafka-র ভেতরে একসাথে বাঁধে। কিন্তু আপনার function যদি SMS পাঠায়, সেই বাইরের কাজ ওই গণ্ডির বাইরে। আসল ফল তখনও আপনার handler-এর ওপর নির্ভর করে।
তাই প্রকৌশলীরা প্রায়ই effectively-once শব্দটি পছন্দ করেন: বার্তা একাধিকবার আসতে পারে, কিন্তু ফল একবারের সমান। সূত্রটি হলো at-least-once delivery + idempotent processing. একই চিন্তা Helland-এর লেখাতেও পাবেন।
মূল উপলব্ধি: “Exactly-once” কোনো delivery-র গুণ নয়, এটি পুরো ব্যবস্থার ফলের গুণ, আর সেই দায় নকশাকারের। কোনো সেবার বিজ্ঞাপনে এই শব্দ দেখলে জিজ্ঞেস করুন: কোন গণ্ডির ভেতরে, কত সময়ের জন্য?
প্র ০২Prime Video-র ঘটনা থেকে কি প্রমাণ হয় যে serverless ভুল, আর monolith-ই সঠিক?
না। ঘটনাটি প্রকাশের পর সামাজিক মাধ্যমে অনেকে এভাবেই বলেছিলেন, আর সেটি ছিল অতি-সরলীকরণ। দলের নিজের লেখা অনুযায়ী এটি একটি নির্দিষ্ট টুল, পুরো Prime Video নয়। আর যাঁরা দাবি করেন যে “microservice শেষ”, তাঁরা এই টুলের আকৃতিটি দেখেননি।
আকৃতিটি কী? প্রতিটি stream থেকে ফ্রেম আর অডিওর টুকরো আসে, অনেকগুলো detector সেগুলো দেখে। টুকরোগুলো একই ডেটা বারবার ছুঁয়ে যায়। অর্থাৎ কাজ ছোট, কিন্তু ডেটা বিশাল আর গতি চলমান। প্রতিটি হাতবদলে object storage-এ লেখা-পড়া আর workflow-এর একটি ধাপের দাম ছিল। অংশ ৮-এর খরচের সূত্রে বললে, এখানে “প্রতি মিলিয়ন” দাম তাদের জন্য বিশাল সংখ্যায় গুণ হচ্ছিল, আর peak গড়ের কাছাকাছি।
লক্ষ করুন দলটি কী করেনি। তারা cloud ছাড়েনি, managed সেবাও ছাড়েনি। তারা container-এ চালিয়েছে, যা নিজেও cloud-এর সেবা। শুধু হাতবদল কমিয়েছে। একই টুকরোগুলো process-এর ভেতরে memory দিয়ে কথা বলছে। এটি মূলত network সীমানা সরানোর সিদ্ধান্ত। Fowler-এর পরিচিত “distributed objects-এর প্রথম সূত্র” মনে পড়ে: প্রয়োজন না হলে ভাগ করবেন না।
আবার উল্টো দিকটিও সত্য। প্রথম নকশাটি নিশ্চয়ই দ্রুত বানানো গিয়েছিল। ছোট পরিমাণে পরীক্ষা করে তারপর বড় হলে নকশা বদলানো একটি স্বাভাবিক প্রকৌশল-পথ, যদি সংখ্যাগুলো মাপা হয়। “বাগানের আম”-এর মতো মৌসুমি দোকানে সেই একই হিসাব উল্টো ফল দেবে।
মূল উপলব্ধি: কোনো কেস স্টাডি পড়ে আগে তিনটি জিনিস দেখুন: load-এর আকৃতি, ডেটা কতটা ঘোরে, আর কোন সারির বিল বড়। কারও ঘটনা আপনার নিয়ম হতে পারে না, তবে তার হিসাবের পদ্ধতি নিতে পারেন।
প্র ০৩২০১৯ সালে Berkeley থেকে একই বিষয়ে দুটি উল্টো সুরের লেখা বেরোল। তারা কী বলেছিল, আর আজ কতটা খাটে?
Jonas ও সহলেখকদের “Cloud Programming Simplified: A Berkeley View on Serverless Computing” (২০১৯) বলেছিল, ক্লাউড প্রোগ্রামিংয়ের ভবিষ্যৎ অনেকটাই serverless-এর দিকে যাবে, যেমন ২০০৯-এর “Above the Clouds” cloud-কে নিয়ে ভবিষ্যদ্বাণী করেছিল। তাদের মতে বাধাগুলো গবেষণায় কেটে যাবে: টুকরোর মাঝে ডেটা পাঠানো, ক্ষণস্থায়ী storage, সমন্বয় আর নিরাপত্তা।
একই বছর Hellerstein ও সহলেখকদের “Serverless Computing: One Step Forward, Two Steps Back” (CIDR ২০১৯) আরও কড়া ছিল। তাঁদের যুক্তি, FaaS scale-এ এক ধাপ এগিয়েছে, কিন্তু দুই ধাপ পিছিয়েছে: এটি কোডকে ডেটা থেকে দূরে রাখে, আর function-গুলোর মধ্যে সরাসরি দ্রুত যোগাযোগ দেয় না। তাই ডেটা বারবার network পেরোয়, যাকে তাঁরা data shipping-এর ফাঁদ বলেন। Distributed কম্পিউটিং আর database-এর দীর্ঘ শিক্ষা উল্টো কথা বলে: কোড ডেটার কাছে নিন।
আজ কী দেখা যায়? দুই পক্ষই আংশিক ঠিক। সময়সীমা, memory আর provisioned concurrency-র মতো সীমা অনেক শিথিল হয়েছে, আর সাধারণ event-glue কাজে serverless সত্যিই প্রতিষ্ঠিত। আবার ডেটা-ঘোরা ভারী কাজে সমস্যাটি রয়েই গেছে, যা অংশ ৯-এর উদাহরণে দেখলেন। Durable execution, stateful function আর edge-এ চালানো function-এর মতো ধারণাগুলো এই ফাঁক পূরণের চেষ্টা, যদিও এদের অবস্থা দ্রুত বদলাচ্ছে।
একটি বড় পরিবর্তন: serverless শব্দটি এখন function ছাড়িয়ে গেছে। Serverless container আর ব্যবহার-অনুযায়ী বিলের database মিলে “সীমা কম, ঝক্কি কম” মাঝের জায়গা তৈরি করেছে।
মূল উপলব্ধি: কোনো প্রযুক্তির গুণ আর দোষ প্রায়ই একই নকশা-সিদ্ধান্ত থেকে আসে। FaaS-এর হালকা, stateless রূপই তাকে দ্রুত scale করায়, আবার সেটিই ডেটা-ভারী কাজে তাকে দুর্বল করে।
প্র ০৪Function database-এ লিখে তারপর event পাঠায়। মাঝখানে crash হলে কী হয়, আর এর ভদ্র সমাধান কী?
এটি dual write সমস্যা। অর্ডার database-এ লেখা হলো, আর “অর্ডার হয়েছে” event পাঠানোর আগেই function মারা গেল। Database বলছে অর্ডার আছে, অথচ গুদাম খবর পায়নি। ক্রম উল্টে দিলে সমস্যাও উল্টে যায়: event গেল, database লেখা ব্যর্থ, গুদাম অস্তিত্বহীন অর্ডারের মাল সাজাচ্ছে। দুটি আলাদা ব্যবস্থায় সাধারণত এক transaction হয় না।
সবচেয়ে পরিচিত সমাধান transactional outbox. Function একই transaction-এ দুটি জিনিস লেখে: অর্ডার, আর একটি outbox সারি, যাতে event-এর কথা আছে। Transaction এক, তাই দুটিই হবে বা কোনোটিই হবে না। আলাদা একটি প্রক্রিয়া outbox পড়ে event প্রকাশ করে, তারপর সারি মোছে বা চিহ্ন দেয়।
Crash হলে প্রকাশক আবার পাঠাতে পারে, তাই event duplicate হতে পারে, আবারও at-least-once. গ্রাহককে তাই idempotent হতেই হবে। অর্থাৎ outbox আর idempotent consumer জুটি হিসেবে আসে।
Serverless-এ এটি আরও সহজ। Change data capture ব্যবহার করে সারিগুলো নিজে পড়ার প্রক্রিয়া লিখতে হয় না। DynamoDB Streams, Cosmos DB change feed বা Postgres-এর logical replication লেখা পরিবর্তনগুলোকে event-এর স্রোতে আনে, আর একটি function তা প্রকাশ করে। পড়ার ক্রম আর ধরে রাখার মেয়াদ সেবাভেদে আলাদা।
একাধিক ধাপের কাজে আসে saga: প্রতিটি ধাপের নিজের transaction, ব্যর্থ হলে আগের ধাপ ফেরানোর ক্ষতিপূরণ-কাজ। Workflow engine এটি সাজানো সহজ করে। তত্ত্ব আছে microservices পর্বে।
মূল উপলব্ধি: যেখানেই একটি কাজ দুটি ব্যবস্থাকে ছোঁয়, সেখানে জিজ্ঞেস করুন “মাঝখানে বিদ্যুৎ গেলে কী হবে?” সমাধান প্রায়ই একটিই: একটি ব্যবস্থায় অবিভাজ্যভাবে লিখুন, তারপর নিরাপদে retry করে অন্যটিকে জানান।
প্র ০৫Lock-in এড়াতে আগেই একটি abstraction বানিয়ে রাখা কি বুদ্ধিমানের কাজ? সংখ্যা দিয়ে কীভাবে ভাববেন?
Gregor Hohpe ২০১৯ সালে martinfowler.com-এ “Don't get locked up into avoiding lock-in” লেখায় মূল কথাটি বলেন: lock-in-কে একটি ঝুঁকি হিসেবে দেখুন, আর ঝুঁকি মাপা যায়। Lock-in এড়ানোরও একটি দাম আছে, যা আজই দিতে হয়।
একটি কাল্পনিক হিসাব করি। “বাগানের আম”-এর একটি নিজস্ব abstraction স্তর বানাতে ২ মাস লাগবে, আর দেখভালে বছরে ১৫ দিন। ধরুন এতে মোট তিন বছরে ১০০ দিনের কাজ লাগে। Abstraction ছাড়া অন্য provider-এ যেতে লাগবে ধরি ৬০ দিন, আর থাকলে ১৫ দিন। তিন বছরে সরে যাওয়ার সম্ভাবনা ধরি ১০%. প্রত্যাশিত সাশ্রয় = ০.১ × (৬০ − ১৫) = ৪.৫ দিন। আপনি ১০০ দিন খরচ করছেন ৪.৫ দিন বাঁচাতে। সরে যাওয়ার সম্ভাবনা ১০০% হলেও সাশ্রয় ৪৫ দিন, তখনও লোকসান।
আরও দুটি লুকানো দাম আছে। প্রথমত, সব provider-এর সাধারণ অংশে আটকে গেলে managed সেবার সেরা গুণগুলো হারাবেন, এই ক্ষতি প্রতিদিনের। দ্বিতীয়ত, যে abstraction কখনো দ্বিতীয় provider-এ চলেনি, সেটি সত্যিই কাজ করবে কি না কেউ জানে না। পরীক্ষা না হওয়া backup-এর মতো।
তবে সব lock-in সমান নয়। Function-এর পোর্ট সহজ, কিন্তু কয়েক TB ডেটা আর হাজার IAM নিয়ম কঠিন। ব্যবসা বা নিয়ম সরাসরি বাধ্য করলে, যেমন data residency-র কারণে, হিসাব অন্যরকম। বাংলাদেশে তথ্য-সংক্রান্ত নিয়মও সময়ের সাথে বদলাতে পারে, তাই সংশ্লিষ্ট নিয়ম যাচাই করে সিদ্ধান্ত নিন।
সস্তা বিমা হলো ভালো প্রকৌশল-অভ্যাস: লজিক আলাদা, মানক protocol, ডেটা রপ্তানির পরীক্ষিত পথ।
মূল উপলব্ধি: Lock-in একটি বিনিময়। প্রশ্ন “আমি আটকে যাব কি না” নয়, প্রশ্ন “বের হওয়ার দাম আর সম্ভাবনা গুণ করে যা দাঁড়ায়, আজকের খরচ কি তার চেয়ে কম?”
অনুশীলন
সবগুলো খাতা-কলমে করা যায়। কোনো cloud account লাগবে না।
-
Crossover হিসাব: একটি API-তে অনুমিত দাম: serverless-এ প্রতি মিলিয়ন request-এ মোট $৫। Container-এ load balancer $৩০, আর প্রতি task মাসে $২৫, সর্বনিম্ন ২টি। প্রতি task সেকেন্ডে ১৫টি request নেয়, মাস ৩০ দিনের, আর fleet গড়ের ২ গুণ peak-এর মাপে স্থির। (ক) Crossover কত মিলিয়নে? সেখানে ২টি task যথেষ্ট তো? (খ) ৪০ মিলিয়ন request-এ দুই বিল কত?
(ক) Container-এর সর্বনিম্ন বিল ৩০ + ২ × ২৫ = $৮০। ৮০ ÷ ৫ = ১৬ মিলিয়ন. গড় = ১,৬০,০০,০০০ ÷ ২৫,৯২,০০০ ≈ ৬.১৭ req/s, peak ≈ ১২.৩ req/s, যা ২ × ১৫ = ৩০-এর কম। তাই ২টি task যথেষ্ট।
(খ) Serverless: ৪০ × ৫ = $২০০. গড় ≈ ১৫.৪ req/s, peak ≈ ৩০.৯ req/s, যা ৩০ ছাড়িয়েছে, তাই ৩টি task. Container: ৩০ + ৩ × ২৫ = $১০৫.
-
নকশা সমালোচনা: একজন junior এই নকশা দিলেন: ব্রাউজারের কোডে একটি API key আছে, সেটিই পরিচয়। ছবি gateway দিয়ে function-এ আসে। Function একই bucket-এর একই prefix-এ thumbnail লেখে। Webhook handler আগে পড়ে দেখে, তারপর লেখে। Batch-এ একটি ব্যর্থ হলে function error ছুড়ে দেয়। সব function একই role ব্যবহার করে,
*অনুমতি সহ। কমপক্ষে পাঁচটি সমস্যা আর তাদের সমাধান বলুন।- API key দিয়ে পরিচয়: Key ব্রাউজারে সবাই দেখেন। JWT authorizer ব্যবহার করুন, key শুধু গ্রাহক চেনার জন্য।
- ছবি function দিয়ে: আকারের সীমা আর বিল দুটিই সমস্যা। Presigned URL দিয়ে সরাসরি upload.
- একই prefix: Recursive trigger-এর ঝুঁকি। আলাদা bucket বা prefix filter, আর concurrency সীমা।
- Read-then-write: একসাথের দুটি copy আটকায় না। Conditional write, আর টাকা জমার সাথে এক transaction.
- পুরো batch ব্যর্থ: Partial batch response, আর dead-letter queue.
- এক role,
*: প্রতি function নিজের least-privilege role.
-
আউটপুট অনুমান: কোডটি না চালিয়ে বলুন কী ছাপবে। তারপর
ifলাইনটি মুছলে শেষ সংখ্যাটি কত হতো?const table = new Set(); let stock = 5; function reserve(id) { if (table.has(id)) return "dup"; table.add(id); stock -= 1; return "ok"; } const events = ["a", "b", "a", "c", "b", "a"]; console.log(events.map(reserve).join(" "), stock);ok ok dup ok dup dup 2তিনটি আলাদা id:
a,b,c. তাই ৫ − ৩ = ২।ifমুছলে ছয়টি event-ই গোনা হবে, ৫ − ৬ = −১, অর্থাৎ যে মাল নেই তা-ও বিক্রি। মনে রাখবেন, এই কোড এক process-এ নিরাপদ, কিন্তু একাধিক instance-এ আলাদাSetথাকবে। সেখানে শর্তটি database-এ দরকার। -
Retry গোনা: ১,০০০টি ছবি, batch-এ ১০টি করে। প্রথমবারে ৩০টি ছবি ব্যর্থ, আর তারা সবাই আলাদা batch-এ। দ্বিতীয়বারে তাদের ৫টি আবার ব্যর্থ, এরাও আলাদা batch-এ। তৃতীয়বারে সব সফল। পুরো batch retry করলে আর শুধু ব্যর্থগুলো retry করলে মোট কতবার resize চলবে? প্রথম পথে অন্য ছবি নতুন করে ব্যর্থ হয় না, ধরে নিন।
শুধু ব্যর্থগুলো: ১,০০০ + ৩০ + ৫ = ১,০৩৫.
পুরো batch: দ্বিতীয়বারে ৩০ × ১০ = ৩০০, তৃতীয়বারে ৫ × ১০ = ৫০। মোট ১,০০০ + ৩০০ + ৫০ = ১,৩৫০. প্রায় ৩০% বেশি কাজ, যদিও আসল ক্ষতি dead-letter queue-র গোলমালে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি ছবির serverless thumbnail pipeline সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- API function একটি presigned URL দেয়
- ব্যবহারকারী ছবি সরাসরি object storage-এ তোলেন
- Storage একটি event queue-তে পাঠায়
- Worker function তিনটি মাপ বানিয়ে অন্য prefix-এ লেখে
- Database-এ অবস্থা READY হয়, আর queue-র বার্তা মোছা হয়
একটি পেমেন্ট handler আগে database পড়ে দেখে event id আছে কি না, না থাকলে লেখে। Duplicate তবু কখন ঢুকে যায়?
- Duplicate অনেক পরে এলে
- Handler Python-এ লেখা হলে
- দুটি copy প্রায় একই মুহূর্তে এলে
- Batch-এর আকার ১ হলে
একটি API-তে token bucket-এর rate সেকেন্ডে ১০০, burst ২০০, আর বালতি ভরা। এক সেকেন্ডে ৫০০টি request এলে মোটামুটি কতগুলো 429 পাবে?
- ৩০০টি
- ২০০টি
- ০টি
- ৪০০টি
Function-এর batch-এ ১০টি বার্তার একটি নষ্ট। পুরো batch ব্যর্থ না করে কী করা ভালো?
- Error গিলে ফেলে সফল ফেরত দেওয়া
- Function-এর timeout বাড়ানো
- সীমাহীন retry চালু করা
- শুধু ব্যর্থ বার্তার পরিচয় ফেরত দেওয়া, সাথে dead-letter queue
৫০০টি function instance একসাথে চলছে, প্রতিটি relational database-এ একটি connection খোলে, আর max_connections ২০০। সবচেয়ে সরাসরি সমাধান কোনটি?
- Database proxy ব্যবহার, আর function-এর concurrency-তে সীমা
- প্রতিটি function-এর ভেতরে connection pool-এর আকার বাড়ানো
- Function-এর memory বাড়ানো
- Function-এর সামনে CDN বসানো
অনুমিত দামে serverless প্রতি মিলিয়ন request-এ $৩.৫০, আর container-এর সর্বনিম্ন মাসিক বিল $৭৭.৬০। প্রথম crossover প্রায় কত মিলিয়ন request-এ?
- ১০ মিলিয়ন
- ৫০ মিলিয়ন
- ২২ মিলিয়ন
- ৭৮ মিলিয়ন
প্রতি রাতে একটানা ৫ ঘণ্টা চলা কাজের জন্য, যার function-এর সময়সীমা ১৫ মিনিট, সাধারণত কোনটি সবচেয়ে উপযোগী?
- একটি বড় function, সর্বোচ্চ memory-তে
- Scheduler-এ চালু হওয়া batch container job
- সারাদিন চালু একটি বড় VM
- API gateway-র পেছনে দীর্ঘ HTTP request
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Serverless app = gateway + function + managed database + object storage + queue. বড় ফাইল function দিয়ে নয়, presigned URL দিয়ে।
- Duplicate আসবেই। উত্তর conditional write, আর key ও আসল কাজ একই transaction-এ।
- Batch-এ শুধু ব্যর্থ বার্তা ফেরত দিন, নষ্ট বার্তা dead-letter queue-তে পাঠান, আর সেখানে alert রাখুন।
- খরচের উত্তর traffic-এর আকৃতিতে: কম ও ঢেউখেলানো load-এ serverless, দিনরাত উঁচু load-এ container. মানুষের সময়ও হিসাবে ধরুন।
- দীর্ঘ কাজ, স্থির উঁচু load আর কড়া latency-তে থামুন। Lock-in একটি মাপা যায় এমন ঝুঁকি, ভয় নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৭২ · Cloud-এর বিল — on-demand, reserved, spot আর data transferপরবর্তী পর্বপরের মডিউলের শুরু: বিলের প্রতিটি সারি কোথা থেকে আসে।
- ক্লাউড কম্পিউটিং ও DevOps · কোর্স হোম৮১টি পর্বপুরো কোর্সের রোডম্যাপ।
- System Design · Message queueগভীরে যানQueue-র তত্ত্ব, ক্রম আর delivery-র নিশ্চয়তা।
- System Design · Resilience patternsপাশের ট্র্যাকRetry, timeout আর circuit breaker.
- DBMS ও SQL · Transaction আর ACIDপাশের ট্র্যাক“একসাথে হবে, নয়তো কিছুই না”-র ভিত্তি।