১১ অক্টো, ২০২৫·7 মিনিট পড়তে

ব্যাকগ্রাউন্ড কাজের জন্য Go: ওয়ার্কার-পুল বনাম goroutine-প্রতি-টাস্ক

Go ওয়ার্কার-পুল বনাম goroutine-প্রতি-টাস্ক: জানুন কিভাবে প্রতিটি মডেল ব্যাকগ্রাউন্ড প্রসেসিং ও দীর্ঘমেয়াদী ওয়ার্কফ্লোতে থ্রুপুট, মেমরি ব্যবহার ও ব্যাকপ্রেশারকে প্রভাবিত করে।

ব্যাকগ্রাউন্ড কাজের জন্য Go: ওয়ার্কার-পুল বনাম goroutine-প্রতি-টাস্ক

আমরা কোন সমস্যা সমাধান করছি?

অধিকাংশ Go সার্ভিস শুধু HTTP রিকুয়েস্টের উত্তর দেয় না। তারা ব্যাকগ্রাউন্ড কাজও চালায়: ইমেইল পাঠানো, ইমেজ রিসাইজ করা, ইনভয়েস জেনারেট করা, ডেটা সিঙ্ক করা, ইভেন্ট প্রসেস করা, বা সার্চ ইনডেক্স রিবিল্ড করা। কিছু জব দ্রুত ও স্বাধীন। অন্যগুলো দীর্ঘ ওয়ার্কফ্লো গড়ে তোলে যেখানে প্রতিটি ধাপ আগেরটার ওপর নির্ভর করে (কার্ড চার্জ করা, কনফার্মেশন প্রত্যাশা করা, তারপর কাস্টমারকে জানানো ও রিপোর্টিং আপডেট করা)।

মানুষ যখন "Go worker pools vs goroutine-per-task" তুলনা করে, তারা সাধারণত একটি প্রোডাকশন সমস্যা সমাধান করতে চায়: কীভাবে অনেক ব্যাকগ্রাউন্ড কাজ চালানো যায় যাতে সার্ভিস ধীর, ব্যয়বহুল, বা অস্থিতিশীল না হয়।

আপনি এটি কয়েক জায়গায় অনুভব করবেন:

  • Latency: ব্যাকগ্রাউন্ড কাজ CPU, মেমরি, ডাটাবেস কানেকশন এবং নেটওয়ার্ক ব্যান্ডউইথ চুরি করে ব্যবহারকারী-সামনাসামনি রিকুয়েস্ট থেকে।
  • Cost: অনিয়ন্ত্রিত কনকারেন্সি আপনাকে বড় মেশিন, বেশি ডাটাবেস ক্ষমতা, বা উচ্চ কিউ/এপিআই বিলের দিকে ঠেলে দেয়।
  • Stability: স্পাইক (ইম্পোর্ট, মার্কেটিং সেন্ড, রিট্রাই স্টর্ম) টাইমআউট, OOM ক্র্যাশ, বা কাসকেডিং ফেইলিওর ট্রিগার করতে পারে।

বাস্তব ট্রেডঅফ হচ্ছে সরলতা বনাম নিয়ন্ত্রণ। প্রতিটি টাস্কে একটি goroutine স্পন করা সহজ লিখতে এবং ভলিউম কম থাকলে সাধারণত ঠিক থাকে। একটি ওয়ার্কার পুল কাঠামো যোগ করে: সিদ্ধান্তগত কনকারেন্সি, পরিষ্কার লিমিট, এবং টাইমআউট, রিট্রাই, মেট্রিক্স রাখার স্বাভাবিক জায়গা। খরচ হচ্ছে অতিরিক্ত কোড এবং সিস্টেম ব্যস্ত হলে কী হবে সে নিয়ে সিদ্ধান্ত (টাস্কগুলো কি অপেক্ষা করবে, প্রত্যাখ্যাত হবে, না কি অন্য কোথাও সংরক্ষিত হবে?)।

এটি দৈনন্দিন ব্যাকগ্রাউন্ড প্রসেসিং বিষয়ে: থ্রুপুট, মেমরি, এবং ব্যাকপ্রেশার (কিভাবে আপনি ওভারলোড প্রতিরোধ করবেন)। এটি প্রতিটি কিউ প্রযুক্তি, বিতরণকৃত ওয়ার্কফ্লো ইঞ্জিন, বা একেবারে-একবার semantics কভার করার চেষ্টা করে না।

যদি আপনি AppMaster (appmaster.io) মত প্ল্যাটফর্ম ব্যবহার করে ব্যাকগ্রাউন্ড লজিক নিয়ে পুরো অ্যাপ বানান, একই প্রশ্ন দ্রুত সামনে আসবে। আপনার ব্যবসায়িক প্রসেস এবং ইন্টিগ্রেশনগুলোও ডাটাবেস, এক্সটার্নাল API এবং ইমেইল/SMS প্রোভাইডারের চারপাশে সীমা প্রয়োজন যাতে একটি ব্যস্ত ওয়ার্কফ্লো সবকিছু ধীর করে না।

স্পষ্ট ভাষায় দুইটি সাধারণ প্যাটার্ন

Goroutine-প্রতি-টাস্ক

এটি সবচেয়ে সরল পদ্ধতি: যখনই একটি জব আসে, সেটি হ্যান্ডেল করার জন্য একটি goroutine শুরু করুন। “কিউ” প্রায়শই যে বিষয়টি কাজ ট্রিগার করে তা—যেমন একটি channel রিসিভার বা HTTP হ্যান্ডলার থেকে সরাসরি কল—হয়ে থাকে।

একটি সাধারণ শেপ হচ্ছে: একটি জব নিন, তারপর go handle(job)। কখনও কখনও একটি channel এখনও ব্যবহৃত হয়, কিন্তু এটি সীমাবদ্ধকারী হিসেবে নয়, হ্যান্ডঅফ পয়েন্ট হিসেবে।

এটি ভাল কাজ করে যখন জবগুলো বেশিরভাগ I/O-এ অপেক্ষা করে (HTTP কল, DB কোয়েরি, আপলোড), জব ভলিউম নমনীয়, এবং স্পাইক ছোট বা পূর্বানুমেয়।

কিন্তু তদনন্তর কনকারেন্সি স্পষ্ট ক্যাপ ছাড়াও বাড়তে পারে। এটি মেমরি স্পাইক, অতিরিক্ত কানেকশন খোলা, বা ডাউনস্ট্রিম সার্ভিস ওভারলোড করতে পারে।

ওয়ার্কার পুল

ওয়ার্কার পুল কয়েকটি নির্দিষ্ট সংখ্যক ওয়ার্কার goroutine চালু করে এবং একটি কিউ (সাধারণত ইন-মেমরি বাফার্ড চ্যানেল) থেকে তাদের জব দেয়। প্রতিটি ওয়ার্কার লুপ করে: একটি জব নাও, প্রোসেস করো, পুনরাবৃত্তি করো।

মূল পার্থক্য হচ্ছে নিয়ন্ত্রণ। ওয়ার্কার সংখ্যা একটি কঠোর কনকারেন্সি সীমা। যদি জব আসে তত্পর্য যে ওয়ার্কার শেষ করতে পারে না, জবগুলো কিউয়েতে অপেক্ষা করবে (বা কিউ পূর্ণ হলে প্রত্যাখ্যাত হবে)।

ওয়ার্কার পুল ভাল ফিট যখন কাজ CPU-ভারি (ইমেজ প্রসেসিং, রিপোর্ট জেনারেশন), যখন পূর্বানুমেয় রিসোর্স ব্যবহার দরকার, বা যখন আপনাকে ডাটাবেস বা তৃতীয় পক্ষের API কে স্পাইক থেকে রক্ষা করতে হবে।

কিউ কোথায় থাকে

উভয় প্যাটার্ন ইন-মেমরি চ্যানেল ব্যবহার করতে পারে, যা দ্রুত কিন্তু রিস্টার্ট হলে মুছে যায়। “ম্যাট না হারানো” জব বা দীর্ঘ ওয়ার্কফ্লোর জন্য কিউ প্রায়শই প্রসেসের বাইরে চলে যায় (একটি ডাটাবেস টেবিল, Redis, বা মেসেজ ব্রোকার)। সেই সেটআপেও আপনি goroutine-প্রতি-টাস্ক এবং ওয়ার্কার পুলের মধ্যে বেছে নেবেন, কিন্তু তারা এখন বাহ্যিক কিউর কনজিউমার হিসেবে চলবে।

সরল উদাহরণ: সিস্টেম হঠাৎ 10,000 ইমেইল পাঠাতে চায়—goroutine-প্রতি-টাস্ক সবগুলো একসাথে পাঠাতে পারে চেষ্টা করবে। একটি পুল একসময় 50টা পাঠাবে এবং বাকিগুলো নিয়ন্ত্রিতভাবে অপেক্ষা করবে।

থ্রুপুট: কী পরিবর্তন হয় এবং কী নয়

প্রচলিত ধারনা আছে যে ওয়ার্কার পুল এবং goroutine-প্রতি-টাস্কের মধ্যে বড় থ্রুপুট পার্থক্য থাকবে। প্রায়ই কাঁচা থ্রুপুট অন্য কিছু দ্বারা সীমাবদ্ধ থাকে, কিভাবে আপনি goroutine শুরু করেন তা নয়।

থ্রুপুট সাধারণত সবচেয়ে ধীর শেয়ার্ড রিসোর্সে পৌঁছে: ডাটাবেস বা এক্সটার্নাল API সীমা, ডিস্ক বা নেটওয়ার্ক ব্যান্ডউইডথ, CPU-ভারি কাজ (JSON/PDF/ইমেজ রিসাইজিং), লক ও শেয়ার্ড স্টেট, বা ডাউনস্ট্রিম সার্ভিস যা লোডে ধীর হয়।

যদি একটি শেয়ার্ড রিসোর্স বটলনেক হয়, আরো goroutine শুরু করলেও কাজ দ্রুত শেষ হবে না। এটি সাধারণত একই গলার ঘাটে অপেক্ষা বাড়ায়।

Goroutine-প্রতি-টাস্ক তখন জিততে পারে যখন টাস্কগুলো ছোট, বেশিরভাগই I/O-অনুভূত এবং শেয়ার্ড সীমার জন্য প্রতিযোগিতা করে না। Go তে goroutine শুরু করা সস্তা, এবং অনেকগুলো goroutine ভালোভাবে শিডিউল করে। একটি “ফেচ, পার্স, এক রো লিখ” স্টাইল লুপে এটি CPU ব্যস্ত রাখে এবং নেটওয়ার্ক ল্যাটেন্সি লুকাতে সাহায্য করে।

ওয়ার্কার পুল তখন জিতবে যখন আপনি ব্যয়বহুল রিসোর্স সীমাবদ্ধ করতে চান। যদি প্রতিটি জব একটি DB কানেকশন ধরে রাখে, ফাইল খুলে, বড় বাফার আলোক করে, বা একটি API কোটা আঘাত করে, তাহলে ফিক্সড কনকারেন্সি সার্ভিসকে স্থিতিশীল রাখে এবং সর্বোচ্চ নিরাপদ থ্রুপুট পৌঁছাতে দেয়।

Latency (বিশেষ করে p99) এখানে প্রায়ই পার্থক্য দেখায়। কম লোডে goroutine-প্রতি-টাস্ক চমৎকার দেখাতে পারে, তারপর যখন টাস্ক জমে যায় তখন তা খুব খারাপ হয়ে যায়। পুল কিউয়িং ডিলে যুক্ত করে (টাস্কগুলোকে ফ্রি ওয়ার্কারের অপেক্ষা করতে হয়), কিন্তু আচরণ অনেক বেশি স্থিতিশীল থাকে কারণ আপনি একই সীমা নিয়ে থান্ডারিং হার্ড-এ লড়াই এড়ান।

সরল মানসিক মডেল:

  • কাজ যদি সস্তা এবং স্বাধীন হয়, বেশি কনকারেন্সি থ্রুপুট বাড়াতে পারে।
  • কাজ যদি শেয়ার্ড সীমা দ্বারা গেট করা হয়, বেশি কনকারেন্সি মূলত অপেক্ষা বাড়ায়।
  • আপনি যদি p99 কে গুরুত্ব দেন, কিউ সময়কে প্রক্রেসিং সময় থেকে আলাদা করে মাপুন।

মেমরি ও রিসোর্স ব্যবহার

ওয়ার্কার-পুল বনাম goroutine-প্রতি-টাস্ক বিবাদটি অনেকটাই আসলে মেমরি নিয়ে। CPU প্রায়ই স্কেল করা যায়; মেমরি ফেইলুর বেশি হঠাৎ হয়ে সার্ভিসটিকে ডাউন করতে পারে।

একটি goroutine সস্তা, কিন্তু নাহি বিনামূল্যের। প্রতিটি শুরু হয় একটি ছোট স্ট্যাক দিয়ে যা গভীর কল বা বড় লোকাল ভ্যারিয়েবল ধরে রাখলে বাড়ে। এছাড়া আছে শিডিউলার ও রানটাইম বুককিপিং। দশ হাজার goroutine ঠিক থাকতে পারে। এক লক্ষ হতে পারে চমকপ্রদ যদি প্রতিটি কিছু বড় ডেটা ধরে রাখে।

বড় গোপন খরচ প্রায়শই goroutine নিজেই নয়, বরং যা তা লাইভ রাখে। যদি টাস্কগুলো শেষ হওয়ার চাইতে দ্রুত আসে, goroutine-প্রতি-টাস্ক অগণিত ব্যাকলগ তৈরি করে। “কিউ” ইম্প্লিসিট হতে পারে (goroutine লক বা I/O-তে অপেক্ষা করছে) বা এক্সপ্লিসিট (বাফার্ড চ্যানেল, স্লাইস, ইন-মেমরি ব্যাচ)। যাই হোক, ব্যাকলগ বাড়লে মেমরি বেড়ে যায়।

ওয়ার্কার পুল সাহায্য করে কারণ এটি একটি ক্যাপ জোর করে। ফিক্সড ওয়ার্কার ও একটি বাধ্যতামূলক কিউ সহ, আপনি একটি বাস্তব মেমরি সীমা পান এবং একটি পরিষ্কার ব্যর্থতা মোড: কিউ পূর্ণ হলে ব্লক, লোড শেড, বা upstream-এ push back।

দ্রুত ব্যাক-অফ-দ্য-এনভেলপ গণনা:

  • পিক goroutines = ওয়ার্কার + ইন-ফ্লাইট জব + "ওয়েটিং" জবগুলি যা আপনি তৈরি করেছেন
  • প্রতিজন জবের মেমরি = পে-লোড (বাইট) + মেটাডাটা + কিছু রেফারেন্স (রিকুয়েস্ট, ডিকোড করা JSON, DB রো)
  • পিক ব্যাকলগ মেমরি ~= ওয়েটিং জব * প্রতিজন জবের মেমরি

উদাহরণ: যদি প্রতিটি জব 200 KB পে-লোড ধরে রাখে এবং আপনি 5,000 জব কিউতে জমতে দেন, তা প্রায় 1 GB হবে শুধুমাত্র পে-লোডের জন্য। গোরুটিন জাদুকরির মতো ফ্রি হলেও ব্যাকলগের নিজস্ব খরচ থাকে।

ব্যাকপ্রেশার: সিস্টেম গলে পড়া রোধ করা

আপনার স্ট্যাকে হুক করুন
ইমেইল, SMS, Telegram, Stripe ইত্যাদি কানেক্ট করুন আপনার কোর সার্ভিস পুনর্লিখন না করে।
ইন্টিগ্রেশন যোগ করুন

ব্যাকপ্রেশার সহজ: যখন কাজ আপনি শেষ করতে পারার চাইতে দ্রুত আসে, সিস্টেম কন্ট্রোলডভাবে পুশব্যাক করে যাতে ধীরে ধীরে জমা না হয়। ব্যাকপ্রেশার না থাকলে, ধীর হওয়ার চেয়ে বেশি ঘটনা ঘটে: টাইমআউট, মেমরি বৃদ্ধি, এবং পুনরাবৃত্ত ব্যর্থতা।

আপনি সাধারণত ব্যাকপ্রেশার নেই দেখে থাকেন যখন একটি স্পাইক (ইম্পোর্ট, ইমেইল, এক্সপোর্ট) এমন প্যাটার্ন ট্রিগার করে: মেমরি বাড়ে এবং কমে না, কিউ সময় বাড়ে কিন্তু CPU ব্যস্ত থাকে, unrelated রিকুয়েস্টগুলোর latency স্পাইক করে, রিট্রাই জমে যায়, বা "too many open files" ও কানেকশন পুল এক্সহস্টেশন মতো এরর আসে।

প্রায়োগিক টুল হচ্ছে একটি বাধ্যতামূলক চ্যানেল: কতগুলো জব অপেক্ষা করতে পারবে তা ক্যাপ করুন। কিউ পূর্ণ হলে প্রোডিউসার ব্লক করে, যা উৎসে জব ক্রিয়েশন ধীর করে।

ব্লক করা সবসময় সঠিক নয়। অপশনাল কাজে একটি স্পষ্ট পলিসি বেছে নিন যাতে ওভারলোড পূর্বানুমেয় হয়:

  • ড্রপ কম-মূল্যবান টাস্ক (উদাহরণ: ডুপ্লিকেট নোটিফিকেশন)
  • ব্যাচ অনেক ছোট টাস্ককে এক সাথে একটি write বা এক API কল করে সমন্বয় করা
  • বিলম্ব জবগুলো জিটারের সাথে দেরি করা যাতে রিট্রাই স্পাইক এড়ানো যায়
  • ডিফার এক স্থায়ী কিউতে পাঠিয়ে দ্রুত রেসপন্স ফেরত দেওয়া
  • লোড শেড পরিষ্কার এরর দিয়ে যখন সিস্টেম ইতিমধ্যেই ওভারলোডেড

রেট লিমিটিং এবং টাইমআউটও ব্যাকপ্রেশার টুল। রেট লিমিটার কিভাবে দ্রুত আপনি একটি নির্ভরশীল সার্ভিস (ইমেইল প্রোভাইডার, ডাটাবেস, থার্ড-পার্টি API) হিট করবেন তা সীমিত করে। টাইমআউট কতক্ষণ একটি ওয়ার্কার আটকে থাকতে পারে সেটি ক্যাপ করে। একসাথে, তারা ধীর ডিপেন্ডেন্সিকে পুরো আউটেজে পরিণত হওয়া থেকে বাধা দেয়।

উদাহরণ: মাসের শেষে স্টেটমেন্ট জেনারেশন। যদি 10,000 রিকুয়েস্ট একসাথে আসে, অনিয়ন্ত্রিত goroutine 10,000 PDF রেন্ডার ও আপলোড ট্রিগার করতে পারে। একটি বাধ্যতামূলক কিউ ও ফিক্সড ওয়ার্কার সহ আপনি নিরাপদ গতি বজায় রেখে রেন্ডার ও রিট্রাই করবেন।

কিভাবে ধাপে ধাপে ওয়ার্কার পুল বানাবেন

নিরাপদ ব্যাকগ্রাউন্ড জব তৈরি করুন
স্পষ্ট সীমা দিয়ে ব্যাকগ্রাউন্ড ওয়ার্কফ্লো মডেল করুন, তারপর প্রোডাকশন-রেডি Go কোড জেনারেট করুন।
AppMaster চেষ্টা করুন

ওয়ার্কার পুল কনকারেন্সি ক্যাপ করে নির্দিষ্ট সংখ্যক ওয়ার্কার চালু করে এবং তাদের জব দিয়ে একটি কিউ খাওয়ায়।

1) নিরাপদ কনকারেন্সি সীমা নির্ধারণ করুন

আপনার জবগুলো কোথায় সময় ব্যয় করে তা থেকে শুরু করুন:

  • CPU-ভারি কাজের জন্য, ওয়ার্কার সংখ্যা CPU কোরের কাছাকাছি রাখুন।
  • I/O-ভারি কাজ (DB, HTTP, স্টোরেজ) হলে আপনি বেশি রাখতে পারেন, কিন্তু তখন থ্রোটলিং বা টাইমআউট দেখা দিলে থামুন।
  • মিক্সড কাজে, মাপুন এবং টিউন করুন। সাধারণ স্টার্টিং রেঞ্জ প্রায় 2x থেকে 10x CPU কোর হতে পারে, পরে টিউন করুন।
  • শেয়ার্ড সীমাগুলো সম্মান করুন। যদি DB পুল 20 কানেকশন থাকে, 200 ওয়ার্কার কেবল ওই 20 এর জন্য লড়াই করবে।

2) কিউ বেছে নিন এবং এর সাইজ নির্ধারণ করুন

বাফার্ড চ্যানেল সাধারণ কারণ এটি বিল্ট-ইন এবং বোঝা সহজ। বাফার আপনার শক অ্যাবসরবার।

ছোট বাফার ওভারলোড দ্রুত সার্ফেস করে (সেন্ডাররা তাড়াতাড়ি ব্লক হয়)। বড় বাফার স্পাইক সোল করতে পারে কিন্তু সমস্যা লুকিয়ে রাখতে পারে এবং মেমরি ও ল্যাটেন্সি বাড়ায়। উদ্দেশ্যপ্রণোদিতভাবে বাফার সাইজ নির্ধারণ করুন এবং কিউ পূর্ণ হলে কী হবে তা ঠিক করুন।

3) প্রতিটি টাস্ককে cancelable করুন

প্রতিটি জব-এ context.Context পাঠান এবং নিশ্চিত করুন জব কোড এটি ব্যবহার করে (DB, HTTP)। এটি ডিপ্লয়, শাটডাউন এবং টাইমআউটে পরিষ্কারভাবে থামানোর উপায়।

func StartPool(ctx context.Context, workers, queueSize int, handle func(context.Context, Job) error) chan<- Job {
    jobs := make(chan Job, queueSize)
    for i := 0; i < workers; i++ {
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return
                case j := <-jobs:
                    _ = handle(ctx, j)
                }
            }
        }()
    }
    return jobs
}

4) আপনি যেসব মেট্রিক ব্যবহার করবেন সেগুলো যোগ করুন

যদি আপনি মাত্র কয়েকটি সংখ্যা ট্র্যাক করেন, তবে এগুলো রাখুন:

  • কিউ গভীরতা (কতটা পিছিয়ে আছেন)
  • ওয়ার্কারের ব্যস্ত সময় (পুল কতটুকু স্যাচুরেটেড)
  • টাস্ক সময় (p50, p95, p99)
  • এরর রেট (এবং রিট্রাই কাউন্ট যদি রিট্রাই করেন)

এগুলো দিয়ে আপনি ওয়ার্কার কাউন্ট ও কিউ সাইজ প্রমাণ ভিত্তিতে টিউন করতে পারবেন, অনুমানের উপর নয়।

সাধারণ ভুল এবং ফাঁদ

অধিকাংশ টিম "ভুল" প্যাটার্ন বেছে নিয়ে আহত হয় না; তারা ছোট ডিফল্ট সেটিংসের কারণে আঘাত পায় যা ট্রাফিক স্পাইক হলে আউটেজে পরিণত হয়।

যখন goroutine বাড়ে

ক্লাসিক ফাঁদ হল স্পাইক হলে প্রতিটি জবের জন্য একটি goroutine স্পন করা। কয়েকশো ঠিক আছে। কয়েক লক্ষ হলে শিডিউলার, হিপ, লগ এবং সোকেট ফ্লাড হতে পারে। প্রতিটি goroutine ছোট হলে ওয়াও না ছাড়াও, মোট খরচ যোগ হয়, এবং রিকভারি সময় লাগে কারণ কাজ ইতিমধ্যে ইন-ফ্লাইট।

আরেকটি ভুল হল একটি বিশাল বাফার্ড চ্যানেলকে "ব্যাকপ্রেশার" ভাবা। বড় বাফার কেবল একটি লুকানো কিউ। এটি সময় কিনতে পারে, কিন্তু সমস্যা লুকিয়ে রাখে যতক্ষণ না মেমরি দেয়াল লঙ্ঘিত হয়। যদি কিউ দরকার হয়, সেটি ইচ্ছাকৃতভাবে সাইজ করুন এবং কিউ পূর্ণ হলে কী হবে তা ঠিক করুন (ব্লক, ড্রপ, পরে রিট্রাই, বা স্টোরেজে পিসিস্ট)।

লুকানো বটলনেক

অনেক ব্যাকগ্রাউন্ড জব CPU-নিয়ন্ত্রিত নয়; সেগুলো ডাউনস্ট্রিম দ্বারা সীমাবদ্ধ। আপনি যদি ঐ সীমাগুলো উপেক্ষা করেন, দ্রুত প্রোডিউসার ধীর কনজিউমারকে ওভারহেল্ম করে দেবে।

সাধারণ ফাঁদ:

  • কোনো ক্যানসেলেশন বা টাইমআউট নেই, ফলে ওয়ার্কাররা API বা DB কোয়েরিতে চিরকাল ব্লক থাকতে পারে
  • বাস্তব সীমা না দেখে ওয়ার্কার গণনা করা (DB কানেকশন, ডিস্ক I/O, থার্ড-পার্টি রেট কেপ)
  • রিট্রাই যা লোড বাড়ায় (1,000 ব্যর্থ জবের অবিলম্বে রিট্রাই)
  • একটি শেয়ার্ড লক বা একক ট্রানজেকশন যা সবকিছু সিরিয়ালাইজ করে, ফলে "আরও ওয়ার্কার" কেবল overhead বাড়ায়
  • ভিজিবিলিটি অনুপস্থিত: কিউ গভীরতা, জব বয়স, রিট্রাই কাউন্ট, এবং ওয়ার্কার ইউটিলাইজেশনের মেট্রিক নেই

উদাহরণ: একটি নাইটলি এক্সপোর্ট 20,000 "সেন্ড নোটিফিকেশন" টাস্ক ট্রিগার করে। প্রতিটি টাস্ক ডাটাবেস ও ইমেইল প্রোভাইডারকে হিট করলে কানেকশন পুল বা কোটা ছাড়িয়ে যাওয়া সহজ। 50 ওয়ার্কার পুল সহ প্রতি টাস্ক টাইমআউটসহ ও ছোট কিউ এই সীমা স্পষ্ট করে। প্রতিটি টাস্কের জন্য আলাদা goroutine ও একটি বিশাল বাফার সেই পর্যন্ত ঠিক দেখালে সব ঠিক আছে মনে হতে পারে, পরে হঠাৎ সংকট দেখা দেয়।

উদাহরণ: স্পাইক হওয়া এক্সপোর্ট ও নোটিফিকেশন

ডেটাকে অটোমেশন বানান
PostgreSQL-ফারস্ট মডেলে আপনার ডেটা ডিজাইন করুন এবং জব लॉজিক ওর কাছেই রাখুন।
ব্যাকএন্ড তৈরি করুন

একটি সাপোর্ট টীম ধরা যাক যে অডিটের জন্য ডেটা চাইছে। একজন ক্লিক করে "Export" বাটনে, তারপর কয়েকজন সহকর্মী একই করে, এবং এক মিনিটের মধ্যে হঠাৎ 5,000 এক্সপোর্ট জব তৈরি হয়ে যায়। প্রতিটি এক্সপোর্ট DB থেকে পড়ে, CSV ফরম্যাট করে, ফাইল সংরক্ষণ করে, এবং প্রস্তুত হলে নোটিফিকেশন (ইমেইল বা Telegram) পাঠায়।

Goroutine-প্রতি-টাস্ক পন্থায় সিস্টেম প্রথমে দারুণ লাগে। সব 5,000 জব প্রায় একেবারে শুরু হয়, এবং মনে হয় কিউ দ্রুত শেষ হচ্ছে। তারপর খরচ দেখা যায়: হাজার হাজার কনকারেন্ট DB কোয়েরি কানেকশনের জন্য লড়ে, মেমরি বাড়ে কারণ একসঙ্গে বহু জব বাফার ধরে আছে, এবং টাইমআউট সাধারণ হয়ে যায়। সহজে শেষ হওয়ার কাজগুলো রিট্রাই ও ধীর কোয়েরির পেছনে আটকে পড়ে।

ওয়ার্কার পুল সহ শুরু ধীর কিন্তু সার্বিকভাবে শান্তি থাকে। 50 ওয়ার্কার থাকলে একসাথে মাত্র 50 এক্সপোর্ট ভারী কাজ করে। ডাটাবেস ব্যবহার পূর্বানুমেয় থাকে, বাফার রিইউজ হয়, এবং ল্যাটেন্সি স্থিতিশীল থাকে। মোট সম্পন্ন সময়ও অনুমানযোগ্য: প্রায় (জব / ওয়ার্কার) * গড় জব সময়, কিছু ওভারহেড সহ।

মূল পার্থক্য এই নয় যে পুল জাদুকরীভাবে দ্রুত—এটি সিস্টেমকে স্পাইক চলাকালীন নিজের ক্ষতি করা থেকে বাধা দেয়। নিয়ন্ত্রিত 50-একসাথে রান প্রায়ই 5,000 টিকে নিজেদের মধ্যে লড়াই করায় অনেক দ্রুত শেষ করে।

আপনি কোথায় ব্যাকপ্রেশার প্রয়োগ করবেন তা নির্ভর করে আপনি কী রক্ষা করতে চান:

  • API স্তরে, সিস্টেম ব্যস্ত হলে নতুন এক্সপোর্ট অনুরোধ প্রত্যাখ্যান বা বিলম্ব করুন।
  • কিউতে, অনুরোধ গ্রহণ করুন কিন্তু জবগুলোকে এনকিউ করুন এবং নিরাপদ গতিতে ড্রেইন করুন।
  • ওয়ার্কার পুলে, ব্যয়বহুল অংশগুলোর কনকারেন্সি ক্যাপ করুন (DB রিড, ফাইল জেনারেশন, নোটিফিকেশন পাঠানো)।
  • প্রতিটি রিসোর্সের জন্য আলাদা সীমা দিন (যেমন, এক্সপোর্টের জন্য 40 ওয়ার্কার কিন্তু নোটিফিকেশনের জন্য মাত্র 10)।
  • এক্সটার্নাল কলগুলিতে ইমেইল/SMS/Telegram-কে রেট-লিমিট করুন যাতে আপনাকে ব্লক না করে।

সরবরাহের আগে দ্রুত চেকলিস্ট

দীর্ঘ ওয়ার্কফ্লো অর্কেস্ট্রেট করুন
একাধিক ধাপে ওয়ার্কফ্লো—যেমন এক্সপোর্ট ও নোটিফিকেশন—ড্রাগ-অ্যান্ড-ড্রপ ব্যবসায়িক প্রক্রিয়া হিসেবে ম্যাপ করুন।
BP এডিটর চেষ্টা করুন

প্রোডাকশনে ব্যাকগ্রাউন্ড জব চালানোর আগে সীমা, দৃশ্যমানতা, এবং ব্যর্থতা হ্যান্ডলিং নিয়ে একটি পাস করুন। বেশিরভাগ ইনসিডেন্ট "স্লো কোড" থেকে হয় না; তারা ঘটে যখন লোড স্পাইক বা ডিপেন্ডেন্সি ফ্লাকি হলে গার্ডরেইল নেই।

  • প্রতিটি ডিপেন্ডেন্সির জন্য হার্ড ম্যাক্স কনকারেন্সি সেট করুন। একটি গ্লোবাল সংখ্যা নিয়ে বসে থাকবেন না; DB রাইট, আউটবাউন্ড HTTP কল, এবং CPU-ভারি কাজ আলাদাভাবে ক্যাপ করুন।
  • কিউকে বাধ্যতামূলক ও পর্যবেক্ষণযোগ্য করুন। অপেক্ষমান জবগুলোর একটি বাস্তব সীমা বসান এবং কয়েকটি মেট্রিক প্রকাশ করুন: কিউ গভীরতা, সবচেয়ে পুরানো জবের বয়স, এবং প্রোসেসিং রেট।
  • রিট্রাই যোগ করুন জিটারসহ এবং ডেড-লেটার পথ। সিলেক্টিভভাবে রিট্রাই করুন, রিট্রাইগুলো ছড়িয়ে দিন, এবং N ব্যর্থতার পর জবটিকে ডেড-লেটার কিউ বা "failed" টেবিলে পাঠান পর্যাপ্ত বর্ণনার সাথে যেন পরে রিভিউ ও রেপ্লে করা যায়।
  • শাটডাউন আচরণ যাচাই করুন: ড্রেইন, ক্যানসেল, নিরাপদে রিসিউম। ডিপ্লয় বা ক্র্যাশ হলে কী হবে তা ঠিক করুন। জবগুলো idempotent রাখুন যাতে পুনরায় প্রসেস করা নিরাপদ হয়, এবং দীর্ঘ ওয়ার্কফ্লোর জন্য প্রগতি সংরক্ষণ করুন।
  • টাইমআউট ও সার্কিট ব্রেক দিয়ে সিস্টেম রক্ষা করুন। প্রত্যেক এক্সটার্নাল কলের টাইমআউট থাকা উচিত। যদি একটি ডিপেন্ডেন্সি ডাউন থাকে, দ্রুত ব্যর্থ করুন (বা ইনটেক পজ করুন) যাতে কাজ স্তূপ না হয়।

বাস্তবধর্মী পরবর্তী ধাপ

সেই প্যাটার্ন বেছে নিন যা আপনার সিস্টেম দৈনন্দিন দিনে কেমন দেখায় তার উপর ভিত্তি করে, নয় তো পারফেক্ট দিনে। যদি কাজ স্পাইকি আসে (আপলোড, এক্সপোর্ট, ইমেইল ব্লাস্ট), একটি ফিক্সড ওয়ার্কার পুল ও বাধ্যতামূলক কিউ সাধারণত নিরাপদ ডিফল্ট। যদি কাজ স্থির এবং প্রতিটি টাস্ক ছোট হয়, goroutine-প্রতি-টাস্ক ঠিক থাকতে পারে, তবে কোথাও সীমা আরোপ করুন।

জিতুন যে পছন্দটি এমন যা ব্যর্থতাকে 'বোরিং' করে দেয়। পুল সীমা স্পষ্ট করে; goroutine-প্রতি-টাস্ক সীমা ভুলে যাওয়া সহজ করে দেয় যতক্ষণ না প্রথম বড় স্পাইক আসে।

সরলভাবে শুরু করুন, পরে বাধা ও দৃশ্যমানতা যোগ করুন

কিছু সরল দিয়ে শুরু করুন, কিন্তু দুটি নিয়ন্ত্রণ শীঘ্রই যোগ করুন: কনকারেন্সির ছাদ এবং কিউইং ও ব্যর্থতা দেখার উপায়।

একটি বাস্তব উপায়ে রোলআউট পরিকল্পনা:

  • আপনার ওয়ার্কলোডের ধরন নির্ধারণ করুন: স্পাইকি, স্থির, না মিক্সড (এবং "পিক" কেমন দেখায়)।
  • ইন-ফ্লাইট কাজের উপর একটি হার্ড ক্যাপ বসান (পুল সাইজ, সেমাফর, বা বাধ্যতামূলক চ্যানেল)।
  • ক্যাপ লাগলে কী হবে ঠিক করুন: ব্লক, ড্রপ, বা স্পষ্ট ত্রুটি রিটার্ন করুন।
  • বেসিক মেট্রিক যোগ করুন: কিউ গভীরতা, কিউ-তে কাটানো সময়, প্রক্রেসিং সময়, রিট্রাই, এবং ডেড লেটারস।
  • আপনার প্রত্যাশিত পিকের 5x স্পাইক দিয়ে লোড টেস্ট করুন এবং মেমরি ও ল্যাটেন্সি দেখুন।

কখন একটি পুল যথেষ্ট নয়

যদি ওয়ার্কফ্লো মিনিট থেকে দিনে পর্যন্ত চলতে পারে, একটি সরল পুল সংগ্রাম করতে পারে কারণ কাজ শুধুই "একবার করো" নয়। আপনাকে স্টেট, রিট্রাই, এবং রিসিউমেবলিটি দরকার হবে। সাধারণত এর মানে হচ্ছে প্রগতি পিসিস্ট করা, idempotent ধাপ ব্যবহার করা, এবং ব্যাকঅফ প্রয়োগ করা। এটাও হতে পারে একটি বড় জবকে ছোট ধাপে ভাগ করা যাতে ক্র্যাশের পরে নিরাপদে রিসিউম করা যায়।

আপনি যদি দ্রুত একটি ফুল ব্যাকএন্ড শিপ করতে চান ওয়ার্কফ্লো নিয়ে, AppMaster (appmaster.io) একটি ব্যবহারিক অপশন হতে পারে: আপনি ভিজ্যুয়ালি ডেটা ও ব্যবসায়িক লজিক মডেল করেন, এবং এটি ব্যাকএন্ডের জন্য বাস্তব Go কোড জেনারেট করে যাতে আপনি কনকারেন্সি সীমা, কিউইং এবং ব্যাকপ্রেশারের অনুশাসন একইভাবে বজায় রাখতে পারেন হ্যান্ড-ওয়্যারিং না করেই।

প্রশ্নোত্তর

কখন একটি ওয়ার্কার পুল ব্যবহার করা উচিত, আর কখন প্রতিটি টাস্কের জন্য goroutine শুরু করা উচিত?

ডিফল্টভাবে ওয়ার্কার পুল ব্যবহার করুন যখন জবগুলো বৃষ্টি-রকম ঢুকে পড়তে পারে বা ডেটাবেস কানেকশন, CPU, বা তৃতীয় পক্ষের API কোটা মতো শেয়ার্ড সীমা স্পর্শ করে। যখন ভলিউম কম, টাস্কগুলো সংক্ষিপ্ত, এবং আপনার কোথাও স্পষ্ট সীমা আছে (যেমন সেমাফর বা রেট লিমিটার), তখন goroutine-প্রতি-টাস্ক ব্যবহার করা ঠিক আছে।

goroutine-প্রতি-টাস্ক এবং ওয়ার্কার পুলের মধ্যে বাস্তব ট্রেডঅফ কী?

প্রতি টাস্ক একটি goroutine শুরু করা দ্রুত লিখবার জন্য এবং কম লোডে ভালো থ্রুপুট দিতে পারে, কিন্তু স্পাইক হলে এটি অনিয়ন্ত্রিত ব্যাকলগ তৈরি করতে পারে। ওয়ার্কার পুল কঠোর কনকারেন্সি ক্যাপ দেয় এবং টাইমআউট, রিট্রাই এবং মেট্রিক্স প্রয়োগ করার সঠিক জায়গা দেয়, ফলে প্রোডাকশনে ব্যহেভিয়ার আরও পূর্বানুমেয় হয়।

ওয়ার্কার পুল কি goroutine-প্রতি-টাস্কের তুলনায় থ্রুপুট কমিয়ে দেয়?

সাধারণত তেমন বড় পার্থক্য থাকে না। অধিকাংশ সিস্টেমে থ্রুপুট শেয়ার্ড বটলনেক—যেমন ডাটাবেস, তৃতীয় পক্ষের API, ডিস্ক I/O বা CPU-ভারি ধাপ—দিয়ে সীমাবদ্ধ। আরো goroutine সেই সীমাকে কাটতে পারে না; বরং অপেক্ষা ও কনটেনশন বাড়ায়।

এই প্যাটার্নগুলো latency-কে (বিশেষ করে p99) কিভাবে প্রভাবিত করে?

কম লোডে goroutine-প্রতি-টাস্ক প্রায়ই latency ভাল দেয়, কিন্তু হাই লোডে এটির পারফরম্যান্স খুব খারাপ হতে পারে কারণ সবকিছু একসাথে প্রতিযোগিতা করে। পুল কিউয়িং ডিলে যুক্ত করে, কিন্তু এটি p99-কে অনেক সময় স্থিতিশীল রাখে কারণ এটি একই ডিপেন্ডেন্সির ওপর থান্ডারিং হার্ড বন্ধ করে দেয়।

goroutine-প্রতি-টাস্ক কেন মেমরি স্পাইক সৃষ্টি করতে পারে?

গোরুটিন নিজেই সাধারণত সবচেয়ে বড় খরচ নয়; ব্যাকলগই বড় সমস্যা। টাস্কগুলো জমা হলে এবং প্রতিটি টাস্ক বড় পে-লোড বা অবজেক্ট ধরে রাখলে মেমরি দ্রুত বাড়ে। ওয়ার্কার পুল ও সীমাবদ্ধ কিউ এইটিকে একটি সংজ্ঞায়িত মেমরি ছাদের মধ্যে নিয়ে আসে এবং ওভারলোডের ক্ষেত্রে প্রত্যাশিত আচরণ দেয়।

ব্যাকপ্রেশার কী, এবং আমি কিভাবে Go-তে এটি যোগ করব?

ব্যাকপ্রেশার মানে হচ্ছে কাজ আপনি শেষ করতে পারার চেয়ে দ্রুত এলে সিস্টেম কনট্রোলডভাবে চাপ কমায়, যেন কাজ থুপি না জমে। Go-তে একটি সীমাবদ্ধ (bounded) কিউ সহজ উপায়: যখন কিউ পূর্ণ, প্রোডিউসার ব্লক হয় বা একটি ত্রুটি ফেরত দেয়, যা runaway মেমরি ও কানেকশন এক্সহস্টশন রোধ করে।

কীভাবে সঠিক সংখ্যক ওয়ার্কার নির্বাচন করব?

বাস্তব সীমা থেকে শুরু করুন। CPU-ভারি কাজের জন্য, কর্মীদের সংখ্যা CPU কোরের কাছাকাছি রাখুন। I/O-ভারি কাজের জন্য আপনাকে বেশি যেতে পারে, কিন্তু ডাটাবেস বা নেটওয়ার্ক শুরু করলে থ্রোটলিং বা টাইমআউট দেখা দিলে থামুন। ডেটাবেস কানেকশন পুল সাইজকে সম্মান করুন।

জব কিউ/বাফার কত বড় হওয়া উচিত?

যতটুকু স্বাভাবিক বর্শট ঢেকে দেয়, সেই মতো একটি সাইজ নিন কিন্তু সমস্যা কয়েকমিনিট ধরে লুকিয়ে রাখবে না। ছোট বাফার ওভারলোড দৃশ্যায়িত করে; বড় বাফার মেমরি ব্যবহার বাড়ায় এবং ব্যর্থতা দেরিতে দেখায়। সিদ্ধান্ত নিন কিউ পূর্ণ হলে কী হবে: ব্লক, রিজেক্ট, ড্রপ, না কি অন্য কোথাও পিসিস্ট।

কিভাবে ওয়ার্কারদের থেকে চিরস্থায়ীভাবে আটকে থাকা রোধ করব?

প্রতিটি জবের জন্য context.Context ব্যবহার করুন এবং নিশ্চিত করুন DB ও HTTP কলগুলি এটাকে সম্মান করে। এক্সটার্নাল কলগুলিতে টাইমআউট দিন, এবং শাটডাউন আচরণ স্পষ্ট রাখুন যাতে ওয়ার্কাররা পরিষ্কারভাবে বন্ধ হতে পারে এবং হ্যাং থাকা গোরুটিন বা অর্ধেক করা কাজ না থাকে।

ব্যাকগ্রাউন্ড জবগুলির জন্য কোন মেট্রিক মনিটর করা উচিত?

কিউ ডেপথ, কিউতে অপেক্ষার সময়, টাস্ক ডিউরেশন (p50/p95/p99), এবং এরর/রিট্রাই কাউন্ট ট্র্যাক করুন। এই মেট্রিকগুলো বলে দেবে আপনাকে কি বেশি ওয়ার্কার দরকার, কিউ ছোট করা দরকার, টাইমআউট কড়া করা দরকার, না কি নির্দিষ্ট ডিপেন্ডেন্সির বিরুদ্ধে রেট লিমিট লাগানো দরকার।

শুরু করা সহজ
কিছু আশ্চর্যজনকতৈরি করুন

বিনামূল্যের পরিকল্পনা সহ অ্যাপমাস্টারের সাথে পরীক্ষা করুন।
আপনি যখন প্রস্তুত হবেন তখন আপনি সঠিক সদস্যতা বেছে নিতে পারেন৷

এবার শুরু করা যাক