2 पॉइंट द्वारा GN⁺ 2023-11-09 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • कंटेनर पर 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 टिप्पणियां

 
GN⁺ 2023-11-09
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 बना सकता है, लेकिन दोबारा सोचने पर कई कारणों से यह ठीक से काम नहीं करेगा

    • quota-based limit इस्तेमाल करने पर container host के सभी CPU cores इस्तेमाल कर सकता है
      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 ऐसा नहीं करता
      Go start होते समय CPU mask की count देखता है, और उसके बाद फिर नहीं देखता
      Kubernetes में process चलने के दौरान दिखने वाले CPU बदल सकते हैं, इसलिए यह problem बन जाता है
    • fake /proc/cpuinfo पहले से मौजूद है: https://github.com/lxc/lxcfs
      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_getaffinity system call से भी संभव है, और यह तरीका /proc/cpuinfo पर निर्भर नहीं करता
      इसलिए आप किस library का इस्तेमाल कर रहे हैं, इसके आधार पर मुश्किल में पड़ सकते हैं
    • यह देखकर निष्कर्ष निकलता है कि containers एक ढीला abstraction हैं और VMware ने मौका गंवा दिया
  • यह explanation थोड़ा गलत है
    Docker के perspective से CFS cgroup extensions में adjust किए जा सकने वाले कई knobs हैं: cfs_quota_us, cfs_period_us(सामान्य default 1 second नहीं बल्कि 100ms है), और shares
    shares set करने पर weight-based proportional scheduling लागू होती है, लेकिन इसका मतलब सिर्फ contention होने पर होता है
    पहले दो values strict quota enforce करती हैं
    Docker के --cpu flag के बजाय --cpu-shares इस्तेमाल करना बेहतर है, ताकि ज्यादातर बेकार quota enforcement से बचा जा सके
    Linux docs के मुताबिक cpu.shares same hierarchy के हर group का weight है, और cpu.cfs_period_us bandwidth निर्णय के लिए 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 करना होगा

    • “Docker का --cpu flag इस्तेमाल न करें और इसके बजाय…” वाली wording बिना ज्यादा context के बहुत strong है
      इसे बिल्कुल भी “ज्यादातर बेकार” नहीं कहा जा सकता
      shares और quota अलग-अलग use cases के लिए हैं, इसलिए अपना use case समझें और उसी के हिसाब से चुनें
    • एक ध्यान देने वाली बात यह है कि --cpu इस्तेमाल करने पर application इसे detect कर सकती है
      शायद इसकी वजह cpuset का इस्तेमाल होना लगता है
      quota इस्तेमाल करने पर यह detect नहीं हो पाता, इसलिए जरूरत से ज्यादा threads बनने की संभावना अधिक है
    • मैं blog author हूँ, feedback के लिए धन्यवाद
      मैं इस हिस्से को और clear करने की कोशिश करूँगा
      मुझे लगता है symptoms इसी तरह दिखते हैं, लेकिन wording को और स्पष्ट करना होगा
    • Kubernetes इस्तेमाल करने वाले लोग ऐसी settings को सीधे adjust या change नहीं करते
      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 के अनुसार सीमित करने दें

    • limits सेट करने की वजह यह नहीं है कि कोई pod दूसरे pods को प्रभावित कर देगा
      वजह यह है कि आप ऐसी स्थिति के आदी नहीं होना चाहते जहाँ unguaranteed extra CPU इस्तेमाल किया जा सकता हो
      जैसे-जैसे node पर दूसरे pods बढ़ते हैं, जो pod अभी तक ठीक चल रहा था वह अचानक slow हो सकता है
      limits इस्तेमाल करने से आप वही behavior simulate कर सकते हैं और सही capacity planning के साथ तैयारी कर सकते हैं
      यह इकलौता तरीका नहीं है, लेकिन सबसे सरल तरीका है
    • 128-core configuration पर कुछ चीज़ें चला रहा हूँ, और CPU limits को request से काफी ज्यादा रखता हूँ, लेकिन फिर भी सेट करता हूँ ताकि कोई चीज़ बेकाबू न हो जाए
      इस चर्चा के बारे में और जानने में दिलचस्पी है, लेकिन linked article लगता है सिर्फ इस बात को cover करता है कि लोग सोचते हैं सभी pods को CPU guarantee करने के लिए limit ज़रूरी है
    • Kubernetes community में लगता है यह चर्चा हर दो हफ्ते में होती है
      यह 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 हो गया
    • reservations limits नहीं हैं, बल्कि minimum guaranteed CPU usage constraint हैं
      सिद्धांत रूप में यह 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 बहुत ज्यादा हो जाता है
    • दिलचस्प है, लेकिन क्या यह memory पर लागू नहीं होता?
      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

    • यहाँ समस्या scheduler नहीं है
      समस्या यह है कि container ने resource limit लगाई है, लेकिन container के अंदर process यानी Go available parallelism की मात्रा calculate करते समय उस limit में इस्तेमाल हुई operating system feature को check नहीं करता
    • https://kernelnewbies.org/Linux_6.6#New_task_scheduler:_EEVD...
  • 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 करना वास्तव में सबसे अच्छा होता है
    • आप एक mutating webhook define कर सकते हैं जो सभी pod containers में GOMAXPROCS inject करे
  • Docker या Go से परिचित न होने के नाते, मैं सोच रहा हूँ कि क्या यह behavior intentional है
    क्या Go team इसे CGroups limit recognize करने लायक बना सकती है?
    क्या दूसरे runtimes भी इसी तरह behave करते हैं?

    • मुझे काफी यकीन है कि .NET को भी यह समस्या handle करनी पड़ी थी, और याद है कि Java में भी यह समस्या थी या शायद अब भी है
      या आपका मतलब containerd जैसे runtime से था?
    • JVM में भी यही समस्या झेली थी
      Scala में था
  • pause को और छोटा बनाने वाली GC तकनीकें भी हैं
    उदाहरण के लिए, pause के दौरान किए जाने वाले काम को साथ-साथ पूरा करने के बाद safe point पर फिर से दोहराने का तरीका
    उम्मीद यह होती है कि concurrent work की वजह से safe point वाला काम “कुछ करने को नहीं है” जैसी आसान जाँच में बदल जाए
    काम को दोगुना करने से GC throughput खराब हो सकता है

  • यह लेख containers की बात कर रहा है, लेकिन समस्या शायद हर बार पैदा होती है जब Go को उम्मीद से कम CPU time ही मिल पाता है
    CPU इस्तेमाल करने वाली दूसरी processes वाले system पर Go चलाने पर भी क्या यही नहीं होगा?
    यहाँ तक कि सिर्फ दो Go programs को साथ-साथ चलाने पर भी ऐसा नहीं होगा?