बैकग्राउंड जॉब्स के लिए Go: worker pools बनाम goroutine‑per‑task
Go worker pools बनाम goroutine‑per‑task: जानें कि बैकग्राउंड प्रोसेसिंग और लंबे वर्कफ़्लो पर हर मॉडल का असर थ्रूपुट, मेमोरी उपयोग, और बैकप्रेशर पर कैसे पड़ता है।

हम किस समस्या का हल कर रहे हैं?
अधिकतर Go सर्विसेज सिर्फ HTTP रिक्वेस्ट का जवाब ही नहीं देतीं। वे बैकग्राउंड काम भी चलाती हैं: ईमेल भेजना, इमेज रिसाइज़ करना, इनवॉइस जनरेट करना, डेटा सिंक करना, इवेंट प्रोसेस करना, या सर्च इंडेक्स रीबिल्ड करना। कुछ जॉब्स जल्दी और स्वतंत्र होते हैं। कुछ लंबे वर्कफ़्लो बनाते हैं जहाँ हर कदम पिछली स्टेप पर निर्भर होता है (कार्ड चार्ज करना, कन्फर्मेशन का इंतज़ार, फिर कस्टमर को नोटिफ़ाई करना और रिपोर्टिंग अपडेट करना)।
जब लोग “Go worker pools vs goroutine-per-task” की तुलना करते हैं, तो वे आमतौर पर एक प्रोडक्शन समस्या का हल ढूँढ रहे होते हैं: बहुत सारा बैकग्राउंड काम कैसे चलाएँ बिना सर्विस को धीमा, महंगा, या अस्थिर बनाए।
आपको इसका प्रभाव कुछ जगहों पर दिखता है:
- लेटेंसी: बैकग्राउंड काम CPU, मेमोरी, DB कनेक्शंस और नेटवर्क बैंडविड्थ यूज़र‑फ़ेसिंग रिक्वेस्ट से छीन लेता है।
- कॉस्ट: अनकंट्रोल्ड concurrency आपको बड़े मशीन, अधिक DB क्षमता, या उच्च क्यू/एपीआई बिल्स की तरफ धकेलता है।
- स्टेबिलिटी: बर्स्ट्स (इम्पोर्ट्स, मार्केटिंग सेंड, रिट्राई स्टॉर्म) टाइमआउट्स, OOM क्रैश या कासकैडिंग फेलियर्स ट्रिगर कर सकते हैं।
असल ट्रेडऑफ है सरलता बनाम नियंत्रण। हर टास्क के लिए goroutine स्पॉन करना लिखने में आसान है और जब वॉल्यूम कम या स्वाभाविक रूप से सीमित हो तो यह अक्सर ठीक रहता है। एक worker pool स्ट्रक्चर जोड़ता है: फिक्स्ड concurrency, साफ‑सुथरी लिमिट्स, और टाइमआउट्स, रिट्राई और मेट्रिक्स रखने की एक नेचुरल जगह। कीमत है थोड़ा ज्यादा कोड और यह निर्णय कि सिस्टम व्यस्त होने पर क्या होगा (क्या टास्क्स वेट करेंगे, रिजेक्ट होंगे, या कहीं और स्टोर होंगे?)।
यह रोज़मर्रा की बैकग्राउंड प्रोसेसिंग के बारे में है: थ्रूपुट, मेमोरी, और बैकप्रेशर (ओवरलोड को कैसे रोका जाए)। यह हर क्यू टेक्नोलॉजी, डिस्ट्रिब्यूटेड वर्कफ़्लो इंजन, या एक्सैक्टली‑वन सेमेन्टिक्स को कवर करने की कोशिश नहीं करता।
अगर आप AppMaster जैसी प्लेटफ़ॉर्म पर बैकग्राउंड लॉजिक वाले पूरे ऐप्स बना रहे हैं, तो यही सवाल जल्दी सामने आते हैं। आपके बिज़नेस प्रोसेसेस और इंटीग्रेशन को अभी भी DB, बाहरी APIs, और ईमेल/SMS प्रदाताओं के चारों ओर सीमाएँ चाहिए ताकि एक व्यस्त वर्कफ़्लो बाकी सब कुछ धीमा न कर दे।
दो सामान्य पैटर्न सरल शब्दों में
Goroutine‑per‑task
यह सबसे सरल तरीका है: जब भी कोई जॉब आता है, उसे हैंडल करने के लिए एक goroutine स्टार्ट कर दें। “क्यू” अक्सर वही होता है जो काम ट्रिगर करता है, जैसे चैनल रिसीवर या HTTP हैंडलर से डायरेक्ट कॉल।
आम रूप यह है: जॉब प्राप्त करें, फिर go handle(job)। कभी‑कभी चैनल भी शामिल होता है, पर वह लिमिटर के रूप में नहीं, सिर्फ हैंडऑफ़ पॉइंट के रूप में होता है।
यह तब अच्छे से काम करता है जब जॉब्स ज्यादातर I/O पर wait करते हैं (HTTP कॉल, DB क्वेरीज, अपलोड्स), जॉब वॉल्यूम मामूली है, और बर्स्ट्स छोटे या प्रेडिक्टेबल हैं।
नुकसान यह है कि concurrency का कोई स्पष्ट कैप नहीं होता। इससे मेमोरी स्पाइक, बहुत अधिक कनेक्शंस खुलना, या डाउनस्ट्रीम सर्विस का ओवरलोड हो सकता है।
Worker pool
एक worker pool फिक्स्ड नंबर के worker goroutines स्टार्ट करता है और उन्हें एक क्यू से जॉब्स खिलाता है, आमतौर पर इन‑मेमोरी buffered channel। प्रत्येक worker लूप करता है: जॉब लें, प्रोसेस करें, दोहराएँ।
मुख्य अंतर नियंत्रण है। वर्कर्स की संख्या एक हार्ड concurrency लिमिट है। अगर जॉब्स वर्कर्स से जल्दी आ रहे हैं, तो जॉब्स कतार में रुकते हैं (या अगर कतार भरी हो तो रिजेक्ट हो सकते हैं)।
Worker pools उन केसों के लिए अच्छे हैं जब काम CPU‑भारी हो (इमेज प्रोसेसिंग, रिपोर्ट जेनेरेशन), जब आपको संसाधन‑उपयोग पूर्वानुमानित चाहिए, या जब आपको DB या थर्ड‑पार्टी API को बर्स्ट से बचाना हो।
कतार कहाँ रहती है
दोनों पैटर्न इन‑मेमोरी चैनल इस्तेमाल कर सकते हैं, जो तेज़ है पर रिस्टार्ट पर गायब हो जाता है। "नहीं खोना चाहिए" वाले जॉब्स या लंबे वर्कफ़्लो के लिए कतार अक्सर प्रोसेस के बाहर चली जाती है (DB टेबल, Redis, या मैसेज ब्रोकเกอร์)। उस सेटअप में भी आप goroutine‑per‑task और worker pools के बीच चुनते हैं, पर अब वे बाहरी क्यू के कंज्यूमर के रूप में चलते हैं।
सरल उदाहरण: सिस्टम अचानक 10,000 ईमेल भेजने की ज़रूरत पड़ता है, तो goroutine‑per‑task सब एक साथ भेजने की कोशिश कर सकता है। एक pool 50‑50 भेजेगा और बाकी को नियंत्रित तरीके से इंतज़ार कराएगा।
थ्रूपुट: क्या बदलता है और क्या नहीं
अक्सर उम्मीद की जाती है कि worker pools और goroutine‑per‑task में बड़ा थ्रूपुट अंतर होगा। अधिकांश समय कच्चा थ्रूपुट किसी अन्य चीज से सीमित होता है, न कि goroutine शुरुआत से।
थ्रूपुट आम तौर पर उस सबसे धीमे साझा संसाधन पर छत पहुँचना शुरू कर देता है: DB या बाहरी API लिमिट्स, डिस्क या नेटवर्क बैंडविड्थ, CPU‑भारी काम (JSON/PDF/इमेज रिसाइज़), लॉक और साझा स्टेट, या डाउनस्ट्रीम सर्विसेज जो लोड में धीमी पड़ती हैं।
अगर साझा संसाधन बॉटलनेक है, तो और goroutine लॉन्च करने से काम तेज़ नहीं होगा। यह मुख्यतः उसी choke point पर अधिक इंतज़ार पैदा करेगा।
Goroutine‑per‑task तब जीत सकता है जब टास्क छोटे हों, ज्यादातर I/O‑बाउंड हों, और साझा सीमाओं पर प्रतियोगिता न हो। Goroutine स्टार्टअप सस्ता है, और Go बड़ी संख्या में उन्हें अच्छी तरह शेड्यूल करता है। "fetch, parse, write one row" जैसे लूप में यह CPU को व्यस्त रख सकता है और नेटवर्क लेटेंसी को छिपा सकता है।
Worker pools तब जीतते हैं जब आपको महंगे संसाधनों को बाउंड करना हो। अगर हर जॉब एक DB कनेक्शन पकड़ता है, फाइलें खोलता है, बड़े बफर अलोकेट करता है, या API कोटा को हिट करता है, तो फिक्स्ड concurrency सर्विस को स्थिर रखता है जबकि सुरक्षित थ्रूपुट तक पहुँचना जारी रहता है।
लेटेंसी (खासकर p99) वह जगह है जहाँ अंतर अक्सर दिखता है। Goroutine‑per‑task कम लोड पर अच्छा दिख सकता है, फिर बहुत खराब हो सकता है जब बहुत सारे टास्क जमा हो जाएँ। Pools कतार‑डिले जोड़ते हैं, पर व्यवहार अधिक स्थिर रहता है क्योंकि आप एक ही सीमा पर लड़ने वाले थंडरिंग हर्ड को टालते हैं।
एक सरल मानसिक मॉडल:
- अगर काम सस्ता और स्वतंत्र है, तो अधिक concurrency थ्रूपुट बढ़ा सकती है।
- अगर काम किसी साझा सीमा से गेटेड है, तो अधिक concurrency ज्यादातर इंतज़ार बढ़ाएगी।
- अगर आप p99 के बारे में परवाह करते हैं, तो कतार समय को प्रोसेसिंग समय से अलग मापें।
मेमोरी और संसाधन उपयोग
वर्कर‑पूल बनाम goroutine‑per‑task बहस बहुत हद तक मेमोरी के बारे में होती है। CPU को अक्सर ऊपर‑नीचे स्केल किया जा सकता है। मेमोरी फेलियर्स अधिक अचानक होते हैं और पूरी सर्विस को डाउन कर सकते हैं।
एक goroutine सस्ता है, पर मुफ़्त नहीं। हर एक की छोटी‑सी स्टैक होती है जो गहरी कॉल्स या बड़े लोकल वेरिएबल्स होने पर बढ़ती है। इसके अलावा scheduler और runtime bookkeeping होता है। दस हज़ार goroutines ठीक हो सकते हैं। एक लाख आश्चर्यजनक हो सकते हैं अगर हर एक बड़े जॉब डेटा को रेफर कर रहा हो।
बड़ा छिपा हुआ खर्च अक्सर goroutine खुद नहीं, बल्कि वह चीज़ें हैं जिन्हें वह ज़िंदा रखता है। अगर टास्क्स खत्म होने से ज्यादा तेज़ी से आते हैं, तो goroutine‑per‑task अनबाउंडेड बैकलॉग बनाता है। "क्यू" इम्प्लिसिट (goroutines लॉक या I/O पर वेट कर रहे हैं) या एक्सप्लिसिट (buffered channel, slice, इन‑मेमोरी बैच) हो सकती है। किसी भी तरह, बैकलॉग के साथ मेमोरी बढ़ती है।
Worker pools मदद करते हैं क्योंकि वे कैप ज़ोर देते हैं। फिक्स्ड वर्कर्स और bounded queue के साथ आप एक वास्तविक मेमोरी लिमिट पाते हैं और एक स्पष्ट फेलियर मोड: एक बार queue भरने पर आप ब्लॉक करते हैं, लोड शेड करते हैं, या ऊपर‑स्ट्रीम को वापस धकेलते हैं।
एक त्वरित बैक‑ऑफ़‑द‑एन्वैलप चेक:
- पीक goroutines = workers + in‑flight jobs + आपने जो “वेटिंग” जॉब बनाए हैं
- प्रति जॉब मेमोरी = payload (बाइट्स) + मेटाडेटा + कुछ भी रेफर किया गया (requests, decoded JSON, DB rows)
- पीक बैकलॉग मेमोरी ~= waiting jobs * memory per job
उदाहरण: अगर हर जॉब 200 KB payload रखता है और आप 5,000 जॉब्स को जमा होने देते हैं, तो यह सिर्फ payload के लिए ~1 GB है। भले ही goroutines जादुई रूप से मुफ्त हों, बैकलॉग मुफ्त नहीं है।
बैकप्रेशर: सिस्टम को पिघलने से बचाना
बैकप्रेशर सरल है: जब काम finish करने से ज्यादा तेज़ी से आता है, सिस्टम नियंत्रित तरीके से पीछे धकेलता है बजाय इसके कि धीरे‑धीरे काम जमा होने दे। इसके बिना, आप सिर्फ धीमा नहीं होते—आपको टाइमआउट्स, मेमोरी ग्रोथ, और ऐसे फेलियर्स मिलते हैं जिन्हें reproduce करना मुश्किल होता है।
आप आमतौर पर गायब बैकप्रेशर तब नोटिस करते हैं जब एक बर्स्ट (इम्पोर्ट्स, ईमेल्स, एक्सपोर्ट्स) मेमोरी वृद्धि और न गिरने, कतार समय बढ़ने जबकि CPU व्यस्त रहता है, अप्रासंगिक रिक्वेस्ट्स के लिए लेटेंसी स्पाइक्स, रिट्राई का जमा होना, या “too many open files” और कनेक्शन पूल खत्म होने जैसे एरर दिखाता है।
एक व्यावहारिक टूल bounded channel है: कितने जॉब्स वेट कर सकते हैं इसे कैप करें। जब चैनल भर जाए तो प्रोड्यूसर ब्लॉक होते हैं, जो जॉब निर्माण को स्रोत पर धीमा कर देता है।
ब्लॉकिंग हमेशा सही विकल्प नहीं होता। वैकल्पिक काम के लिए एक स्पष्ट नीति चुनें ताकि ओवरलोड प्रेडिक्टेबल रहे:
- ड्रोप निम्न‑मूल्य वाले टास्क (उदा., डुप्लिकेट नोटिफ़िकेशन)
- बैच कई छोटे टास्क्स को एक ही राइट या एक ही API कॉल में मिलाएँ
- डिले काम को जिटर के साथ, ताकि रिट्राई स्पाइक्स न हों
- डिफर काम को एक पर्सिस्टेंट कतार को सौंपकर जल्दी वापसी करें
- शेड लोड एक स्पष्ट एरर के साथ जब पहले से ओवरलोड हो
रेट‑लिमिटिंग और टाइमआउट भी बैकप्रेशर टूल हैं। रेट‑लिमिट किसी निर्भरता को आप कितनी तेज़ मार रहे हैं उसे कैप करता है (ईमेल प्रोवाइडर, DB, थर्ड‑पार्टी API)। टाइमआउट यह कैप करता है कि worker कितनी देर फंसा रह सकता है। साथ में, वे एक धीमी निर्भरता को पूरे आउटेज में बदलने से रोकते हैं।
उदाहरण: महीने‑के‑अंत स्टेटमेंट जेनेरेशन। अगर 10,000 रिक्वेस्ट एक साथ आएँ, अनलिमिटेड goroutines 10,000 PDF रेंडर और अपलोड ट्रिगर कर सकते हैं। बाउंडेड कतार और फिक्स्ड वर्कर्स के साथ आप सुरक्षित गति से रेंडर और रिट्राई करते हैं।
वर्कर पूल कैसे बनायें — स्टेप बाय स्टेप
एक worker pool फिक्स्ड वर्कर्स चलाकर concurrency को कैप करता है और उन्हें जॉब्स एक कतार से देता है।
1) एक सुरक्षित concurrency लिमिट चुनें
सबसे पहले देखें कि आपके जॉब्स का समय कहाँ लग रहा है।
- CPU‑भारी काम के लिए, workers को अपने CPU कोर काउंट के पास रखें।
- I/O‑भारी काम (DB, HTTP, स्टोरेज) के लिए आप अधिक रख सकते हैं, पर तब रोकें जब निर्भरताएँ टाइमआउट या थ्रॉटल करने लगें।
- मिश्रित काम के लिए मापें और समायोजित करें। सामान्य शुरुआती रेंज अक्सर CPU कोर का 2x से 10x होती है, फिर ट्यून करें।
- साझा सीमाओं का सम्मान करें। अगर DB पूल 20 कनेक्शंस है, तो 200 workers केवल उन 20 के लिए लड़ेंगे।
2) कतार चुनें और उसका साइज़ सेट करें
एक buffered channel आम है क्योंकि यह बिल्ट‑इन है और समझने में आसान है। बफ़र आपके बर्स्ट के लिए शॉक‑एब्जॉर्बर है।
छोटे बफ़र ओवरलोड को जल्दी दिखाते हैं (सेंडर्स जल्दी ब्लॉक होते हैं)। बड़े बफ़र spikes को स्मूद करते हैं पर समस्याओं को छिपा सकते हैं और मेमोरी तथा लेटेंसी बढ़ा सकते हैं। बफ़र का साइज़ जानबूझकर चुनें और तय करें कि भरने पर क्या होगा।
3) हर टास्क को cancelable बनाएं
प्रत्येक जॉब में एक context.Context पास करें और सुनिश्चित करें कि जॉब कोड इसे इस्तेमाल करता है (DB, HTTP)। यह deploys, shutdowns, और टाइमआउट्स पर साफ़‑सफाई से रुकने का तरीका है。
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) जिन मेट्रिक्स की आपको वाकई ज़रूरत है उन्हें जोड़ें
अगर आप सिर्फ कुछ नंबर ट्रैक करते हैं, तो इन्हें रखें:
- Queue depth (आप कितने पीछे हैं)
- Worker busy time (पूल कितना saturated है)
- Task duration (p50, p95, p99)
- Error rate (और यदि आप retry करते हैं तो retry counts)
ये काफ़ी हैं ताकि आप सबूत के आधार पर worker count और queue size ट्यून कर सकें, अनुमान नहीं लगाकर।
आम गलतियाँ और जाल
ज़्यादातर टीमें “गलत” पैटर्न चुनकर नहीं चोट खातीं। वे उन छोटे defaults से चोट खातीं जो ट्रैफ़िक spike पर incidents बन जाते हैं।
जब goroutines बढ़ती हैं
क्लासिक जाल है हर जॉब के लिए एक goroutine spawn करना जब bursts हों। कुछ सैंकड़ों ठीक हैं। कुछ लाख scheduler, heap, logs, और नेटवर्क सॉकेट्स को भर सकते हैं। भले ही हर goroutine छोटा हो, कुल लागत जमा हो जाती है, और recovery में समय लगता है क्योंकि काम पहले से इन‑फ्लाइट है।
एक और गलती है एक बड़े buffered channel को “बैकप्रेशर” समझना। बड़ा बफ़र सिर्फ एक छिपा हुआ कतार है। यह समय खरीद सकता है, पर यह समस्याओं को तब तक छिपाता है जब तक आप मेमोरी दीवार से नहीं टकराते। अगर आपको कतार चाहिए, तो इसे जानबूझकर साइज करें और तय करें कि भरने पर क्या होगा (ब्लॉक, ड्रॉप, बाद में रिट्राई, या स्टोरेज में पर्सिस्ट)।
छिपी हुई बाधाएँ
कई बैकग्राउंड जॉब्स CPU‑बाउंड नहीं होते। वे किसी डाउनस्ट्रीम चीज़ से सीमित होते हैं। यदि आप उन सीमाओं को अनदेखा करते हैं, तो तेज़ प्रोड्यूसर धीमे कंज्यूमर को ओवरवhelm कर देगा।
सामान्य गलतियाँ:
- कोई cancellation या timeout नहीं, इसलिए workers API रिक्वेस्ट या DB क्वेरी पर हमेशा के लिए ब्लॉक हो सकते हैं
- worker counts बिना असली सीमाओं (DB कनेक्शंस, डिस्क I/O, थर्ड‑पार्टी रेट कैप्स) की जांच के चुने गए
- रिट्राइज़ जो लोड बढ़ाते हैं (1,000 फ़ेल हुए जॉब्स पर तुरंत रिट्राई)
- एक साझा लॉक या एकल ट्रांज़ैक्शन जो सबकुछ सीरियलाइज़ कर देता है, तो “ज़्यादा workers” केवल ओवरहेड जोड़ते हैं
- दृश्यता की कमी: कतार‑गहराई, जॉब उम्र, retry काउंट, और worker utilization के लिए मेट्रिक्स नहीं
उदाहरण: नाइटली एक्सपोर्ट 20,000 “send notification” टास्क ट्रिगर करता है। अगर हर टास्क DB और ईमेल प्रोवाइडर को हिट करता है, तो कनेक्शन पूल या कोटा जल्दी पार हो सकते हैं। 50 workers के पूल के साथ per‑task timeouts और छोटी कतार यह सीमा स्पष्ट कर देते हैं। एक‑per‑task goroutine और विशाल बफ़र सिस्टम को ठीक दिखा सकते हैं जब तक अचानक नहीं दिखेगा।
उदाहरण: बर्स्टी एक्सपोर्ट्स और नोटिफ़िकेशन्स
सोचें कि एक support टीम को ऑडिट के लिए डेटा चाहिए। एक व्यक्ति "Export" बटन क्लिक करता है, फिर कुछ टीम‑मेट्स भी करते हैं, और अचानक एक मिनट में 5,000 एक्सपोर्ट जॉब्स बन जाते हैं। हर एक्सपोर्ट DB से पढ़ता है, CSV फॉर्मेट करता है, फाइल स्टोर करता है, और तैयार होने पर नोटिफ़िकेशन (ईमेल या Telegram) भेजता है।
Goroutine‑per‑task के साथ सिस्टम एक पल के लिए बढ़िया लगता है। सब 5,000 जॉब्स लगभग तत्काल शुरू हो जाते हैं, और ऐसा लगता है जैसे कतार तेज़ी से खाली हो रही हो। फिर लागतें दिखती हैं: हजारों समवर्ती DB क्वेरीज कनेक्शंस के लिए मुकाबला करती हैं, मेमोरी बढ़ती है क्योंकि जॉब्स एक साथ बफ़र पकड़ते हैं, और टाइमआउट्स आम हो जाते हैं। जो जॉब्स जल्दी खत्म हो सकते थे, वे रिट्राई और धीमी क्वेरीज के पीछे फंस जाते हैं।
Worker pool के साथ शुरुआत धीमी होती है पर कुल रन शांत रहता है। 50 workers के साथ, एक बार में सिर्फ 50 एक्सपोर्ट्स भारी काम करते हैं। DB उपयोग अनुमानित सीमा में रहता है, बफ़र्स अधिक बार reuse होते हैं, और लेटेंसी स्थिर रहती है। कुल कम्पलीशन टाइम का अनुमान भी आसान है: लगभग (jobs / workers) * average job duration, कुछ ओवरहेड के साथ।
मुख्य अंतर यह नहीं कि pools जादुई रूप से तेज़ हैं। अंतर यह है कि वे बर्स्ट्स के दौरान सिस्टम को खुद को नुकसान पहुँचाने से रोकते हैं। नियंत्रित 50‑at‑a‑time रन अक्सर उन 5,000 जॉब्स के बीच लड़ने से पहले पूरा हो जाता है।
आप बैकप्रेशर कहाँ लागू करते हैं यह इस पर निर्भर करता है कि आप किसे बचाना चाहते हैं:
- API लेयर पर, जब सिस्टम व्यस्त हो तो नए एक्सपोर्ट रिक्वेस्ट्स रिजेक्ट या डिले करें।
- कतार पर, रिक्वेस्ट स्वीकार करें पर जॉब्स enqueue करें और सुरक्षित दर से ड्रेन करें।
- वर्कर पूल में, महंगे हिस्सों (DB रीड, फ़ाइल जनरेशन, नोटिफ़िकेशन) के लिए concurrency कैप करें।
- हर संसाधन के लिए अलग‑अलग लिमिट रखें (उदा., एक्सपोर्ट्स के लिए 40 workers पर, नोटिफ़िकेशन्स के लिए सिर्फ 10)।
- बाहरी कॉल्स पर ईमेल/SMS/Telegram को रेट‑लिमिट करें ताकि आपको ब्लॉक न किया जाए।
शिप करने से पहले तेज़ चेकलिस्ट
प्रोडक्शन में बैकग्राउंड जॉब्स चलाने से पहले सीमाओं, दृश्यता, और फेलियर हैंडलिंग की एक पास करें। ज़्यादातर incidents "धीमे कोड" की वजह से नहीं होते। वे उस समय होते हैं जब लोड spike या किसी निर्भरता के flaky होने परगार्डरेल्स गायब होते हैं।
- प्रति निर्भरता हार्ड मैक्स concurrency सेट करें। एक ग्लोबल नंबर न चुनें और उम्मीद करें कि वह सबकुछ फिट होगा। DB writes, outbound HTTP कॉल्स, और CPU‑भारी काम अलग‑अलग कैप करें।
- कतार bounded और observable रखें। पेंडिंग जॉब्स पर वास्तविक सीमा लगाएँ और कुछ मेट्रिक्स एक्सपोज़ करें: queue depth, oldest job की उम्र, और processing rate।
- रिट्राई के साथ जिटर और एक dead‑letter पाथ जोड़ें। चयनात्मक रूप से retry करें, retries फैलाएँ, और N फ़ेलियर्स के बाद जॉब को dead‑letter queue या "failed" टेबल में डालें ताकि रिव्यू और रिप्ले संभव हो।
- शटडाउन व्यवहार वेरिफ़ाई करें: ड्रेन, कैंसल, सुरक्षित रूप से resume करें। deploy या क्रैश पर क्या होगा तय करें। जॉब्स idempotent बनायें ताकि reprocessing सुरक्षित हो, और लंबे वर्कफ़्लो के लिए प्रोग्रेस स्टोर करें।
- टाइमआउट और सर्किट ब्रेकर्स से सिस्टम को प्रोटेक्ट करें। हर बाहरी कॉल में टाइमआउट चाहिए। अगर कोई निर्भरता डाउन है तो फेल फास्ट (या intake को pause) करें बजाय इसके कि काम जमता जाए।
व्यावहारिक अगले कदम
उस पैटर्न को चुनें जो आपके सिस्टम के सामान्य दिन के अनुरूप हो, न कि एक परफेक्ट दिन के। अगर काम बर्स्टी आता है (अपलोड्स, एक्सपोर्ट्स, ईमेल ब्लास्ट), तो फिक्स्ड worker pool और bounded queue आम तौर पर सुरक्षित डिफ़ॉल्ट है। अगर काम steady है और हर टास्क छोटा है, तो goroutine‑per‑task ठीक हो सकता है, बशर्ते आप कहीं न कहीं सीमा लागू रखें।
जीतने वाला विकल्प आम तौर पर वही होता है जो फेलियर को नीरस बनाता है। Pools सीमाएँ स्पष्ट करते हैं। Goroutine‑per‑task सीमाएँ भूल जाने में आसान बनाता है जब तक पहली असली स्पाइक न आ जाए।
सरल से शुरू करें, फिर bounds और दृश्यता जोड़ें
साधारण से शुरू करें, पर दो नियंत्रण जल्दी जोड़ें: concurrency पर एक कैप और कतारिंग व फेलियर्स देखने का तरीका।
एक व्यावहारिक rollout योजना:
- अपना वर्कलोड आकार परिभाषित करें: बर्स्टी, steady, या मिक्स्ड (और "पीक" क्या दिखता है)
- इन‑फ्लाइट काम पर हार्ड कैप लगाएँ (pool size, semaphore, या bounded channel)
- तय करें कि कैप hit होने पर क्या होगा: ब्लॉक, ड्रॉप, या स्पष्ट एरर लौटाएँ
- बुनियादी मेट्रिक्स जोड़ें: queue depth, time‑in‑queue, processing time, retries, और dead letters
- अपने अपेक्षित पीक का 5x बर्स्ट लोड‑टेस्ट करें और मेमोरी व लेटेंसी देखें
जब एक pool पर्याप्त नहीं होता
अगर वर्कफ़्लो मिनट्स से दिनों तक चल सकते हैं, तो सादा pool संघर्ष कर सकता है क्योंकि काम सिर्फ "एक बार करो" नहीं होता। आपको स्टेट, रिट्राई, और resumability चाहिए। इसका मतलब आम तौर पर प्रोग्रेस पर्सिस्ट करना, idempotent स्टेप्स, और बैकऑफ लागू करना होता है। साथ ही एक बड़े जॉब को छोटे स्टेप्स में बाँटना ताकि क्रैश के बाद सुरक्षित रूप से resume किया जा सके।
अगर आप वर्कफ़्लो के साथ एक पूरा बैकएंड जल्दी शिप करना चाहते हैं तो AppMaster (appmaster.io) एक व्यावहारिक विकल्प हो सकता है: आप डेटा और बिज़नेस लॉजिक को विज़ुअली मॉडल करते हैं, और यह बैकएंड के लिए वास्तविक Go कोड जनरेट करता है ताकि आप concurrency लिमिट्स, कतारिंग, और बैकप्रेशर के आसपास वही अनुशासन रख सकें बिना हर चीज़ को हाथ से जोड़ने के।
सामान्य प्रश्न
डिफ़ॉल्ट रूप से worker pool तब चुनें जब जॉब्स बर्स्ट में आ सकते हों या DB कनेक्शन, CPU, या बाहरी API कोटा जैसी साझा सीमाएँ छू रहे हों। Goroutine‑per‑task तब इस्तेमाल करें जब वॉल्यूम मामूली हो, टास्क छोटे हों, और कहीं न कहीं स्पष्ट सीमा मौजूद हो (जैसे semaphore या rate limiter)।
हर टास्क के लिए goroutine शुरू करना लिखने में तेज़ और कम जटिल है और कम लोड पर अच्छा थ्रूपुट दे सकता है, लेकिन spikes के दौरान अनबाउंडेड बैकलॉग बना सकता है। एक worker pool सख्त concurrency कैप देता है और टाइमआउट, रिट्राई, और मेट्रिक्स लागू करने की एक साफ जगह देता है, जिससे प्रोडक्शन व्यवहार आमतौर पर अधिक अनुमाननीय होता है।
अधिकतर मामलों में नहीं बहुत। ज़्यादातर सिस्टम में थ्रूपुट किसी साझा बाधा (DB, बाहरी API, डिस्क/नेटवर्क, या CPU‑भारी स्टेप) से सीमित होता है। ज़्यादा goroutine उस सीमा को पार नहीं कर पाएँगी; वे ज़्यादातर इंतज़ार और contention बढ़ाती हैं।
कम लोड पर goroutine‑per‑task बेहतर लेटेंसी दिखा सकता है, लेकिन हाई लोड पर सब कुछ बुरी तरह धीमा हो सकता है क्योंकि सबकुछ एक साथ प्रतिस्पर्धा करता है। एक pool कतार‑डिले जोड़ता है, पर यह p99 को अधिक स्थिर रखता है क्योंकि यह थ्रेडिंग हर्ड को रोकता है और निर्भरता पर नियंत्रण बनाए रखता है।
समस्या अक्सर बैकलॉग की वजह से होती है: अगर टास्क्स जमा हो जाएँ और हर टास्क बड़े payload या ऑब्जेक्ट्स पकड़ता रहे, तो मेमोरी तेजी से बढ़ सकती है। goroutine खुद छोटा है, पर बैकलॉग का स्मृति‑कौस्ट बड़ा हो सकता है। एक worker pool और bounded queue इसको परिभाषित मेमोरी‑सीमा और अनुमाननीय ओवरलोड व्यवहार में बदल देता है।
बैकप्रेशर का मतलब है जब सिस्टम व्यस्त है तब आप नए काम को धीमा या रोक दें, ताकि काम चुपचाप जमा न हो। एक bounded queue सरल तरीका है: जब भर जाए तो producers ब्लॉक हों या आप एरर लौटाएँ, इससे मेमोरी और कनेक्शन एक्सहॉस्ट होने से बचाव होता है।
वास्तविक सीमा से शुरू करें। CPU‑भारी काम के लिए workers को CPU कोर संख्या के पास रखें। I/O‑भारी काम के लिए आप अधिक रख सकते हैं, पर तब भी तब तक न बढ़ाएँ जब तक आपकी DB या नेटवर्क थ्रॉटल न करने लगे; और कनेक्शन पूल साइज़ का ध्यान रखें।
ऐसा साइज चुनें जो सामान्य बर्स्ट को संभाले पर समस्या को मिनटों तक छिपा कर न रखे। छोटे बफ़र ओवरलोड जल्दी दिखाते हैं; बड़े बफ़र मेमोरी बढ़ाते और विफलताओं को देर से उजागर करते हैं। तय करें कि जब queue भर जाए तो क्या होगा: ब्लॉक, रिजेक्ट, ड्रॉप, या कहीं और persist करें।
प्रत्येक जॉब में context.Context पास करें और DB/HTTP कॉल में उसका पालन कराएँ। बाहरी कॉल्स के टाइमआउट सेट करें और shutdown व्यवहार स्पष्ट रखें ताकि workers cleanly रुक सकें और हंग goroutine या अधूरे काम न छूटें।
क्यू‑डेप्थ, कतार में बिताया गया समय, टास्क ड्यूरेशन (p50/p95/p99), और एरर/रिट्राई काउंट मॉनिटर करें। ये मेट्रिक्स बताते हैं कि आपको workers बढ़ाने, queue छोटा करने, टाइमआउट कड़ा करने, या किसी डिपेंडेंसी पर rate limiting लागू करने की ज़रूरत है।


