- कंटेनर पर CPU limit लगाने के बाद भी Go runtime डिफ़ॉल्ट रूप से इसके बारे में नहीं जानता, इसलिए वह पूरे host के cores के आधार पर threads बना सकता है और latency बढ़ा सकता है
- Go GC ज़्यादातर समय application के साथ concurrent चलता है, लेकिन Sweep Termination और Mark Termination में सभी goroutines को रोकने वाले stop-the-world(STW) हिस्सों की ज़रूरत होती है
- Linux CFS cores की संख्या को प्रति सेकंड CPU time में बाँटकर आवंटित करता है, और
--cpus=4 का मतलब है कि container को हर सेकंड 4 सेकंड के बराबर CPU time दिया जाता है
- 16-core host पर 4-core limit वाला container चलाने पर Go 16 OS threads पर goroutines रख सकता है, इसलिए CPU quota खत्म होने के बाद STW लंबा हो सकता है
GOMAXPROCS को container CPU limit के मुताबिक सेट करने पर example में GC cycle 2.5ms से कम से घटकर 1ms से कम हो गया, और STW लगभग 26μs तक घट गया
Container CPU limit और Go runtime का mismatch
- Container में Go application चलाते समय CPU limit host CPU को पूरा consume करने से रोकने का mechanism है
- समस्या यह है कि Go runtime डिफ़ॉल्ट रूप से container की CPU limit को पहचान नहीं पाता
- इस mismatch के कारण runtime मानता है कि वह असल quota से ज़्यादा CPU इस्तेमाल कर सकता है, जिससे high latency हो सकती है
Go GC में STW कहाँ होता है
- Go garbage collector ज़्यादातर समय application के साथ concurrently execute होता है
- लेकिन GC process में दो phases ऐसे हैं जहाँ सभी goroutines को रोकना पड़ता है
- Mark Phase से पहले write barrier लागू करने के लिए जो stopping step होता है, वह Sweep Termination है
- Mark Phase के बाद write barrier हटाने के लिए फिर से रोकने वाला step Mark Termination है
- STW sections आम तौर पर कुछ दर्जन microseconds के स्तर के होते हैं
- Example application एक simple web application है जो बहुत memory allocate करता है, और source code go-cfs-blog पर है
- Container 4 CPU limit के साथ चलाया गया है
docker run --cpus=4 -p 8080:8080 $(ko build -L main.go)
runtime/trace package से trace collect किया जा सकता है और go tool trace से analyze किया जा सकता है
- इस run में GC cycle 2.5ms से कम था, लेकिन उसमें से लगभग 10% STW section था
- Latency-sensitive applications में इतना अनुपात भी समस्या बन सकता है
Docker CPU limits और Linux CFS कैसे काम करता है
- Docker की
--cpus CPU limit एक hard limit है
--cpu-shares भी set किया जा सकता है, लेकिन यह तभी enforce होता है जब host CPU-constrained हो
- अगर host पर spare capacity है, तो container allocated CPU cores से ज़्यादा इस्तेमाल कर सकता है
- जब host constrained state में आता है, तो application पर limit लगती है
- Linux Completely Fair Scheduler(CFS) Linux 2.6.23 में introduce हुआ था और Linux 6.6 से पहले तक default scheduler था
- CFS एक proportional share scheduler है, जो process का weight उन CPU cores की संख्या के अनुपात में रखता है जिन्हें वह इस्तेमाल कर सकता है
- 4 CPU cores इस्तेमाल कर सकने वाले process का weight 4 होता है
- 2 CPU cores इस्तेमाल कर सकने वाले process का weight 2 होता है
- CFS CPU time को छोटे हिस्सों में बाँटकर allocate करता है
- 4-core system हर सेकंड 4 सेकंड के बराबर CPU time allocate कर सकता है
- Container को CPU cores की संख्या assign करना Linux scheduler से
n CPUs के बराबर time request करने जैसा है
--cpus=4 का मतलब है कि container को हर सेकंड 4 सेकंड के बराबर CPU time मिलता है
STW लंबा क्यों हो जाता है
- Go runtime start होते समय हर CPU core के लिए एक OS thread बनाता है
- 16-core machine पर CGroup CPU limit से स्वतंत्र रूप से वह 16 OS threads बना सकता है
- Runtime इन OS threads के ऊपर goroutines schedule करता है
- Container CPU limit 4 cores होने पर भी Go सभी 16 OS threads पर goroutines रख सकता है
- इस state में runtime उम्मीद करने लगता है कि वह हर सेकंड 16 सेकंड के बराबर CPU time इस्तेमाल कर सकता है
- लंबा STW time इसलिए होता है क्योंकि उन threads पर मौजूद goroutines को भी रोकना पड़ता है जो Linux scheduler द्वारा फिर से चलाए जाने का इंतज़ार कर रहे होते हैं
- Container के CPU quota पहले ही इस्तेमाल हो जाने के बाद वे threads schedule नहीं होते
GOMAXPROCS को CPU quota से मिलाना
- Go
GOMAXPROCS environment variable से runtime द्वारा इस्तेमाल किए जाने वाले CPU threads की संख्या limit कर सकता है
- CPU quota 4 वाले container में
GOMAXPROCS=4 भी साथ में specify करें
docker run --cpus=4 -e GOMAXPROCS=4 -p 8080:8080 $(ko build -L main.go)
- वही application और वही load होने पर
GOMAXPROCS को CPU quota से match करने से GC time छोटा हो गया
- Trace में GC cycle 1ms से कम हो गया, और STW section 26μs था
- यह
GOMAXPROCS limit न होने पर STW time की तुलना में लगभग 1/10 स्तर है
GOMAXPROCS को उन CPU cores की संख्या पर set करना चाहिए जिन्हें container इस्तेमाल कर सकता है
- fractional CPU allocate करने पर floor करें
- 1 CPU से कम allocate करने पर ceil करें
- Formula है
GOMAXPROCS=max(1, floor(CPUs))
- Uber की automaxprocs एक open source library है जो container cgroups से यह value automatically calculate करती है
- Go runtime में इसे default support करने के लिए एक GitHub Issue खुला हुआ है
Containerized Go services में क्या check करें
- सिर्फ CPU limit set करना काफ़ी नहीं है; Go runtime उस limit को reflect करे, इसके लिए GOMAXPROCS भी match करना होगा
- अगर खुद calculate करना मुश्किल हो, तो automaxprocs जैसी library से cgroups-based value automatically set की जा सकती है
- Latency-sensitive Go services को GC trace में STW time देखकर check करना चाहिए कि CPU quota और runtime settings mismatch तो नहीं हैं
1 टिप्पणियां
Hacker News टिप्पणियाँ
कई भाषाओं में दिखने वाली एक आम समस्या यह है कि application /proc/cpuinfo देखकर machine के core की संख्या detect करती है
लेकिन Docker container या दूसरी container technologies के अंदर यह file container host जैसी ही दिखती है, और container को असल में कितने भी core allocate किए गए हों, यह सभी cores list करती है
कुछ समय तक मुझे लगा कि शायद Docker काम को allocate किए गए सिर्फ “Docker CPU” list करने वाला fake /proc/cpuinfo बना सकता है, लेकिन दोबारा सोचने पर कई कारणों से यह ठीक से काम नहीं करेगा
limit इस बात पर होती है कि वह उन cores को कितनी देर तक इस्तेमाल कर सकता है
exceptions भी हैं और docs यहाँ हैं: https://kubernetes.io/docs/tasks/administer-cluster/cpu-mana...
nprocइस्तेमाल करता हूँ, और दूसरे containers में भीbundle install -j $(nproc)जैसा इस्तेमाल होते देखा हैयह CPU allocation का सम्मान करता है, इसलिए वह feature देता है जिसकी तलाश थी
arbitrary applications उपलब्ध होने पर nproc इस्तेमाल करती हैं या नहीं, यह मुझे नहीं पता
“current process के लिए उपलब्ध processing units की संख्या print करता है, जो online processors की संख्या से कम हो सकती है। अगर यह जानकारी नहीं मिल पाती, तो installed processors की संख्या print करता है”
https://www.gnu.org/software/coreutils/manual/html_node/npro...
https://www.flamingspork.com/blog/2020/11/25/why-you-should-...
Go start होते समय CPU mask की count देखता है, और उसके बाद फिर नहीं देखता
Kubernetes में process चलने के दौरान दिखने वाले CPU बदल सकते हैं, इसलिए यह problem बन जाता है
lxcfs एक FUSE filesystem है जो cgroup values infer करके /proc की नकल करता है, ताकि applications और libraries को यह चिंता न करनी पड़े कि वे container के अंदर चल रही हैं या नहीं
उदाहरण के लिए /proc/uptime को host के बजाय container का uptime reflect करना चाहिए, और /proc/cpuinfo को cpu.max और cpuset.cpus के combination में से lower limit को CPU count के रूप में reflect करना चाहिए
CPU count inference
sched_getaffinitysystem call से भी संभव है, और यह तरीका /proc/cpuinfo पर निर्भर नहीं करताइसलिए आप किस library का इस्तेमाल कर रहे हैं, इसके आधार पर मुश्किल में पड़ सकते हैं
यह explanation थोड़ा गलत है
Docker के perspective से CFS cgroup extensions में adjust किए जा सकने वाले कई knobs हैं:
cfs_quota_us,cfs_period_us(सामान्य default 1 second नहीं बल्कि 100ms है), और sharesshares set करने पर weight-based proportional scheduling लागू होती है, लेकिन इसका मतलब सिर्फ contention होने पर होता है
पहले दो values strict quota enforce करती हैं
Docker के
--cpuflag के बजाय--cpu-sharesइस्तेमाल करना बेहतर है, ताकि ज्यादातर बेकार quota enforcement से बचा जा सकेLinux docs के मुताबिक
cpu.sharessame hierarchy के हर group का weight है, औरcpu.cfs_period_usbandwidth निर्णय के लिए scheduler period है, जिसका default 100000us या 100ms हैcpu.cfs_quota_usहरcfs_period_usके दौरान current group के चल सकने का maximum time है, और यह value पूरे system के CPUs पर summed time है, इसलिए 2 CPUs को पूरी तरह इस्तेमाल कराने के लिए इसेcfs_period_usके double पर set करना होगा--cpuflag इस्तेमाल न करें और इसके बजाय…” वाली wording बिना ज्यादा context के बहुत strong हैइसे बिल्कुल भी “ज्यादातर बेकार” नहीं कहा जा सकता
shares और quota अलग-अलग use cases के लिए हैं, इसलिए अपना use case समझें और उसी के हिसाब से चुनें
--cpuइस्तेमाल करने पर application इसे detect कर सकती हैशायद इसकी वजह cpuset का इस्तेमाल होना लगता है
quota इस्तेमाल करने पर यह detect नहीं हो पाता, इसलिए जरूरत से ज्यादा threads बनने की संभावना अधिक है
मैं इस हिस्से को और clear करने की कोशिश करूँगा
मुझे लगता है symptoms इसी तरह दिखते हैं, लेकिन wording को और स्पष्ट करना होगा
application को सही तरह से काम करना चाहिए
CPU limits के बजाय CPU reservations इस्तेमाल करें तो ऐसी tuning की ज़रूरत नहीं पड़ती: https://home.robusta.dev/blog/stop-using-cpu-limits
CPU reservations भी असल में limits ही हैं, लेकिन वे एक implicit सीमा और guarantee के रूप में घोषित होती हैं
इसलिए Go runtime को उपलब्ध सभी CPU इस्तेमाल करने दें, और CPU contention होने पर Linux scheduler को घोषित reservations के अनुसार सीमित करने दें
वजह यह है कि आप ऐसी स्थिति के आदी नहीं होना चाहते जहाँ unguaranteed extra CPU इस्तेमाल किया जा सकता हो
जैसे-जैसे node पर दूसरे pods बढ़ते हैं, जो pod अभी तक ठीक चल रहा था वह अचानक slow हो सकता है
limits इस्तेमाल करने से आप वही behavior simulate कर सकते हैं और सही capacity planning के साथ तैयारी कर सकते हैं
यह इकलौता तरीका नहीं है, लेकिन सबसे सरल तरीका है
इस चर्चा के बारे में और जानने में दिलचस्पी है, लेकिन linked article लगता है सिर्फ इस बात को cover करता है कि लोग सोचते हैं सभी pods को CPU guarantee करने के लिए limit ज़रूरी है
यह article अपने-आप में गलत नहीं है और मोटे तौर पर content marketing जैसा है, लेकिन इसके दावे बहुत broad हैं और limits सेट करने की कई अच्छी वजहों को ignore करते हैं
इसी जगह के कुछ articles तो सीधे-सीधे गलत भी हैं: https://home.robusta.dev/blog/containers-dont-use-chroot
कुछ workloads छोटे से फायदे के लिए सारी burst capacity खा जाते हैं, और कई बार तय समय के भीतर खत्म होने वाले cronjob के बजाय HTTP server की burst capacity को priority देनी पड़ती है
ऐसे मामले भी हुए हैं जहाँ developers ने app की मांग बढ़ने के बाद भी requests update नहीं किए, और spare CPU time अचानक कम पड़ते ही outage हो गया
सिद्धांत रूप में यह minimum guaranteed resource है, लेकिन अगर उसी host पर busy containers साथ चल रहे हों तो tail latency और average latency असामान्य रूप से बढ़ सकती है
4-core EC2 instance पर CPU utilization 50% होने और 90% होने पर latency में काफी फर्क होता है
reservations में भी इसी तरह, भले ही हर container को अपनी reservation guarantee मिल जाए, उसी host पर दूसरे busy processes के कारण relative CPU utilization बहुत ज्यादा हो जाता है
OOMKiller मार सकता है
अगर CPU और memory limits दोनों नहीं हैं, तो Guaranteed QoS class नहीं मिल सकती, इसलिए किसी समय pod evict भी हो सकता है
containers और cgroup इस्तेमाल करते हुए CFS scheduler से कई बार चोट खाई है
नया scheduler क्या है, यह जानने की उत्सुकता है
यहाँ किसी ने इसे production cluster में इस्तेमाल किया है?
लगभग 20 साल से cores बर्बाद हो रहे हैं: https://people.ece.ubc.ca/sasha/papers/eurosys16-final29.pdf
समस्या यह है कि container ने resource limit लगाई है, लेकिन container के अंदर process यानी Go available parallelism की मात्रा calculate करते समय उस limit में इस्तेमाल हुई operating system feature को check नहीं करता
GOMAXPROCS के अलावा, हालिया Go releases में GOMEMLIMIT भी है
https://github.com/KimMachineGun/automemlimit इस्तेमाल करने पर https://github.com/uber-go/automaxprocs की तरह यह limit अपने-आप set की जा सकती है
पिछले साल अपनी पिछली नौकरी में platform engineer के रूप में on-premise Kubernetes cluster और CI/CD pipeline infrastructure manage करते समय मुझे यह मिला था
real CPU और allocated CPU के mismatch से खासकर CPU throttling जैसी समस्याएँ पैदा होती देखीं, लेकिन cluster की सभी Go deployments पर असर डालने वाला scalable solution ढूँढना मुश्किल था
सैकड़ों projects के सभी developers से autoprocs dependency जोड़ने को कहना कोई विकल्प नहीं था
विकल्प के तौर पर सभी CPU request/limit को integer में align करना और Kubernetes manifest में उस value को GOMAXPROCS environment variable में डालना भी झंझट भरा और practically feasible नहीं था
अंत में, बहुत ज्यादा multithreading इस्तेमाल करने वाली कुछ applications पर ही GOMAXPROCS variable लागू करके सुधार मिला, लेकिन microservices architecture की हर deployment पर लागू होने वाला solution अब भी नहीं मिला, जहाँ project के हिसाब से CPU requirements काफी बदलती रहती हैं
GOMAXPROCS को सीमित करने पर, जब process पर traffic अधिक हो और queueing simple हो, तो गंभीर latency issues पैदा हो सकते हैं
process average में कितना time इस्तेमाल करेगा, इस विचार से अलग, GOMAXPROCS को hardware द्वारा दिए गए value पर set करना वास्तव में सबसे अच्छा होता है
Docker या Go से परिचित न होने के नाते, मैं सोच रहा हूँ कि क्या यह behavior intentional है
क्या Go team इसे CGroups limit recognize करने लायक बना सकती है?
क्या दूसरे runtimes भी इसी तरह behave करते हैं?
या आपका मतलब containerd जैसे runtime से था?
Scala में था
pause को और छोटा बनाने वाली GC तकनीकें भी हैं
उदाहरण के लिए, pause के दौरान किए जाने वाले काम को साथ-साथ पूरा करने के बाद safe point पर फिर से दोहराने का तरीका
उम्मीद यह होती है कि concurrent work की वजह से safe point वाला काम “कुछ करने को नहीं है” जैसी आसान जाँच में बदल जाए
काम को दोगुना करने से GC throughput खराब हो सकता है
यह लेख containers की बात कर रहा है, लेकिन समस्या शायद हर बार पैदा होती है जब Go को उम्मीद से कम CPU time ही मिल पाता है
CPU इस्तेमाल करने वाली दूसरी processes वाले system पर Go चलाने पर भी क्या यही नहीं होगा?
यहाँ तक कि सिर्फ दो Go programs को साथ-साथ चलाने पर भी ऐसा नहीं होगा?