3 पॉइंट द्वारा GN⁺ 2025-05-06 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Go applications में Graceful Shutdown वह shutdown प्रक्रिया है जिसमें नए requests रोके जाते हैं, चल रहे काम के खत्म होने का इंतज़ार किया जाता है, और database connections, file locks, network listeners जैसे resources साफ किए जाते हैं
  • Shutdown handling की शुरुआत SIGTERM·SIGINT जैसे shutdown signals को os/signal या Go 1.16+ के signal.NotifyContext से पकड़कर default immediate shutdown behavior को बदलने से होती है
  • Kubernetes में default 30-second grace period के भीतर shutdown पूरा करना होता है, और preStop delay या readiness probe failure के जरिए external load balancer तक traffic रोकने की स्थिति propagate होने का समय देना चाहिए
  • http.Server.Shutdown नए connections रोकता है और active requests के पूरा होने का इंतज़ार करता है, लेकिन अगर handler context cancellation का पालन नहीं करता, तो partial writes, data loss, open transactions जैसी समस्याएँ हो सकती हैं
  • अहम resources को shutdown signal मिलते ही नहीं, बल्कि requests खत्म होने के बाद या timeout expiry के बाद साफ करना चाहिए; initialization के reverse order में shutdown करने से component dependencies बनाए रखना आसान होता है

Graceful Shutdown की न्यूनतम शर्तें

  • Graceful Shutdown को आम तौर पर तीन शर्तें पूरी करनी चाहिए
    • HTTP, pub/sub जैसे entry points पर नए requests या messages स्वीकार न करना
    • पहले से चल रहे requests के खत्म होने तक इंतज़ार करना, और बहुत ज्यादा समय लगने पर graceful error से जवाब देना
    • database connections, file locks, network listeners जैसे महत्वपूर्ण resources release करना और final cleanup करना
  • बाहरी services की ओर जाने वाले database या cache connections को नए requests रोकने वाले step में तुरंत disconnect नहीं किया जाता
  • focus HTTP servers और container applications पर है, लेकिन core principles दूसरे applications पर भी लागू हो सकते हैं

Shutdown signals handling

  • Unix-like systems में signals software interrupts होते हैं जो process को बताते हैं कि कोई खास स्थिति हुई है
  • process किसी specific signal के लिए handler register कर सकता है, और handler न होने पर default behavior follow होता है
    • default behavior terminate, stop, continue running, ignore आदि हो सकता है
    • SIGKILL जैसे कुछ signals पकड़े या ignore नहीं किए जा सकते और process को terminate करते हैं
  • Go runtime main function चलने से पहले ही SIGTERM, SIGQUIT, SIGILL, SIGTRAP आदि कई signal handlers automatically register करता है
  • Graceful Shutdown में मुख्य रूप से तीन shutdown signals महत्वपूर्ण होते हैं
    • SIGTERM: process को terminate करने का standard और polite तरीका, और Kubernetes द्वारा force termination से पहले application को भेजा जाने वाला signal
    • SIGINT: user जब terminal में Ctrl+C से process रोकने की कोशिश करता है, तब भेजा जाता है
    • SIGHUP: मूल रूप से terminal disconnect के लिए इस्तेमाल होता था, और अब अक्सर configuration reload signal के रूप में भी उपयोग होता है
  • अलग से handling न होने पर, SIGTERM, SIGINT, SIGHUP मिलने पर Go runtime application को terminate कर देता है

os/signal और NotifyContext

  • signal.Notify Go runtime को निर्देश देता है कि specified signals को default behavior की जगह channel पर deliver करे
  • signal channel को buffer size 1 के साथ बनाना अधिक stable माना जाता है
    • Go internals channel send के लिए select और default का उपयोग करते हैं
    • buffer में जगह हो तो signal deliver होता है, और buffer full हो तो signal drop हो जाता है
    • unbuffered channel में अगर receive कर रही goroutine न हो तो signal miss हो सकता है
  • signal.Notify को एक ही signal के लिए कई बार call किया जा सकता है, और Go उस signal को सभी registered channels पर भेजता है
  • Ctrl+C कई बार दबाने पर भी आम तौर पर दूसरी input अपने आप SIGKILL में promote नहीं होती
    • अधिकांश bash या Linux shells automatic promotion नहीं करते
    • force termination के लिए kill -9 से सीधे SIGKILL भेजना पड़ता है
  • local development में दूसरे Ctrl+C से force termination करवाना हो तो पहला signal मिलते ही signal.Stop से आगे signal receiving रोक सकते हैं
  • Go 1.16 से signal.NotifyContext के जरिए signal handling को context cancellation से जोड़ा जा सकता है
    • ctx.Done() के बाद भी stop() call करना चाहिए, ताकि दूसरा Ctrl+C application को forcefully terminate कर सके

Shutdown timeout और Kubernetes behavior

  • shutdown signal मिलने के बाद application वास्तव में कितना shutdown time इस्तेमाल कर सकता है, यह पहले जानना चाहिए
  • Kubernetes का default grace period, अगर terminationGracePeriodSeconds अलग से specify न किया गया हो, 30 seconds है
  • यह समय बीतने पर Kubernetes SIGKILL भेजकर application को forcefully stop कर देता है
    • SIGKILL को पकड़ा या handle नहीं किया जा सकता
  • remaining request processing और resource release सहित पूरा shutdown logic इसी समय के भीतर खत्म होना चाहिए
  • default 30 seconds के आधार पर करीब 20% safety margin छोड़ें, तो पूरा shutdown 25 seconds के अंदर खत्म करना बेहतर है

नए requests रोकना और readiness handling

  • Go के net/http में http.Server.Shutdown से Graceful Shutdown किया जा सकता है
    • नए connections accept करना बंद करता है
    • active requests के complete होने तक इंतज़ार करता है
    • इसके बाद idle connections बंद करता है
  • पहले से चल रहे requests complete हो सकते हैं, और complete होने के बाद वह connection idle state में जाकर बंद हो जाता है
  • shutdown के दौरान नया connection try करने वाले clients को listener पहले ही बंद होने के कारण आम तौर पर connection refused error मिलता है
  • container environment या external load balancer वाली orchestration environment में नए requests accept करना तुरंत बंद न करना महत्वपूर्ण है
    • pod को termination target के रूप में mark किए जाने के बाद भी थोड़ी देर traffic मिल सकता है
    • Kubernetes internal component kube-proxy pod state के Terminating में बदलने को जल्दी पहचान लेता है
    • external load balancer Kubernetes से independent होकर अपना health check use करता है, इसलिए state propagation में समय लगता है
  • traffic blocking propagation का इंतज़ार करने के दो तरीके हैं
    • preStop hook में थोड़ी देर sleep करके external load balancer को pod termination state recognize करने का समय देना
      • preStop में लगा समय terminationGracePeriodSeconds में शामिल होता है
    • code level पर readiness probe fail करना और थोड़ी देर इंतज़ार करना
      • यह Kubernetes के अलावा उन environments में भी लागू हो सकता है जहाँ load balancer को ready state पता होनी चाहिए
  • readiness probe periodically check करता है कि container traffic receive करने के लिए ready है या नहीं
    • HTTP request, TCP connection, command execution जैसे तरीकों से health check किया जा सकता है
    • probe fail होने पर Kubernetes pod को service endpoint से हटा देता है ताकि वह traffic न ले
  • shutdown की तैयारी में isShuttingDown जैसे atomic.Bool का इस्तेमाल करके /healthz से HTTP 503 return करवाया जा सकता है
  • readiness state को fail में बदलने के बाद changes propagate होने के लिए कुछ seconds इंतज़ार करना चाहिए
    • example configuration periodSeconds: 5 है, और article example 5-second wait use करता है
    • exact wait time readiness probe configuration पर निर्भर करता है

चल रहे requests का handling

  • shutdown budget के अनुसार context.WithTimeout से timeout बनाकर server.Shutdown(ctx) को pass करें
  • server.Shutdown return होने की दो स्थितियाँ होती हैं
    • सभी active connections बंद हो गए और सभी handler processing खत्म हो गई
    • passed context handler completion से पहले expire हो गया और server ने waiting छोड़ दी
  • दोनों ही cases में Shutdown तब return करता है जब server request processing पूरी तरह रोक चुका होता है
  • handlers को तेज और context-aware तरीके से काम करना चाहिए
    • ऐसा न होने पर timeout expiry पर काम बीच में कट सकता है
    • partial writes, data loss, inconsistent state, open transactions, corrupted data जैसी समस्याएँ हो सकती हैं
  • handler तक shutdown signal पहुँचाने के दो common तरीके हैं
    • middleware के जरिए हर request context में cancellation logic inject करना
    • http.Server के BaseContext से सभी connections के लिए shared global context provide करना
  • HTTP server में customize किए जा सकने वाले contexts BaseContext और ConnContext हैं
    • Graceful Shutdown के लिए server-wide cancelable global context बनाने वाला BaseContext अधिक उपयुक्त है
  • Graceful Shutdown तभी प्रभावी होता है जब functions context cancellation का सम्मान करें
    • context.Background(), time.Sleep() जैसे cancellation ignore करने वाले uses से बचना चाहिए
    • time.Sleep(duration) को select से time.After(duration) और ctx.Done() का साथ में इंतज़ार करने वाले तरीके से replace किया जा सकता है
  • पुराने Go versions में time.After timer run होने तक memory leak कर सकता है
    • यह issue Go 1.23+ में fix हो गया है
    • version निश्चित न हो तो time.NewTimer और Stop, और जरूरत पड़ने पर <-t.C check का इस्तेमाल किया जा सकता है
    • संबंधित issue: time: stop requiring Timer/Ticker.Stop for prompt GC

Shutdown और Close का अंतर

  • यही principle HTTP servers के अलावा third-party services पर भी लागू होता है
  • database/sql का DB.Close database connections बंद करता है, नए queries शुरू होने से रोकता है, और चल रही queries के खत्म होने तक इंतज़ार करता है
  • मुख्य बात यह है कि नए requests या messages अब न लें, और existing work को defined grace period के भीतर खत्म होने का समय दें
  • server.Close() चल रहे connections का इंतज़ार किए बिना तुरंत terminate करता है
    • network use कर रहे handlers को read/write के समय error मिलता है
    • client को तुरंत ECONNRESET या socket hang up जैसे connection errors मिल सकते हैं
    • network से interact न करने वाले long-running handlers background में चलते रह सकते हैं
  • server.Shutdown() error return करने के बाद server.Close() इस्तेमाल किया जा सकता है, लेकिन यह shutdown strategy पर निर्भर करता है
  • shutdown signal को context के जरिए propagate करना अधिक reliable और graceful approach है

महत्वपूर्ण resources release करने का क्रम

  • एक common mistake यह है कि shutdown signal मिलते ही critical resources release कर दिए जाते हैं
  • इस समय handlers और in-flight requests अभी भी उन resources का उपयोग कर सकते हैं, इसलिए resource cleanup को shutdown timeout बीतने या सभी requests खत्म होने के बाद तक टालना चाहिए
  • कई cases में केवल process termination से भी operating system resources reclaim कर लेता है
    • Go द्वारा allocated memory process termination पर release हो जाती है
    • file descriptors operating system बंद कर देता है
    • process handles जैसे OS-level resources भी reclaim हो जाते हैं
  • explicit cleanup की जरूरत वाले cases भी होते हैं
    • database connections को properly close करना चाहिए, और open transactions को commit या rollback की जरूरत होती है
    • message queues और brokers को message flush, offset commit, client shutdown notification की जरूरत हो सकती है
    • external services connection drop तुरंत detect नहीं कर सकतीं, इसलिए manually connection close करने से TCP timeout का इंतज़ार करने की तुलना में cleanup जल्दी हो सकता है
  • components को initialization के reverse order में shutdown करना अच्छा rule है
    • Go का defer last registered function को पहले execute करता है, इसलिए यह pattern उससे अच्छी तरह match करता है
  • memory cache data को disk पर लिखना हो जैसे कुछ components के लिए अलग shutdown routine design करनी पड़ सकती है

पूरे example का flow

  • complete example signal.NotifyContext से SIGINT और SIGTERM receive करने वाला root context configure करता है
  • /healthz endpoint, isShuttingDown true होने पर HTTP 503 और Shutting down return करता है, अन्यथा OK return करता है
  • sample request handler 2 seconds बाद Hello, world! return करता है, या request context cancel होने पर HTTP request timeout से respond करता है
  • BaseContext में ongoingCtx connect किया जाता है ताकि in-flight requests SIGTERM के तुरंत बाद cancel न हों
  • shutdown signal मिलने पर flow इस क्रम में चलता है
    • additional default handling allow करने के लिए stop() call
    • isShuttingDown.Store(true) से readiness failure state बनाना
    • _readinessDrainDelay यानी 5 seconds तक readiness check propagation का इंतज़ार
    • _shutdownPeriod यानी 15-second timeout के साथ server.Shutdown call करना
    • stopOngoingGracefully() से in-progress context cancel करना
    • Shutdown fail होने पर _shutdownHardPeriod यानी 3 seconds तक forced cancellation wait time रखना

1 टिप्पणियां

 
GN⁺ 2025-05-06
Hacker News राय
  • Kubernetes में कुछ configurations में load balancer target IP अपडेट होने में उम्मीद से ज़्यादा समय लगने की वजह से मुझे नुकसान झेलना पड़ा था। मेरे मामले में graceful shutdown का 90% काम यह सुनिश्चित करना था कि pod समाप्त होने से पहले traffic सच में drain हो जाए
    global preStop hook में 15-second sleep डालने से HTTP 503 rate काफी घट गया, और load balancer deregistration शुरू होने के बाद application तक SIGTERM पहुंचने तक समय मिल गया, जिससे application-side handling बहुत सरल हो गई

    • सही। preStop sleep high-quality rolling deployments में SLO बनाए रखने का जादुई उपाय है
      मुझे लगता है Kubernetes दो चीज़ें बेहतर कर सकता है। pod को termination sequence शुरू करने से पहले Endpoints से हटाया जाना चाहिए, और termination grace की तरह termination delay option होना चाहिए। साथ ही PDB में eviction से पहले recreation allow करने का option होना चाहिए
  • अगर सामान्य Prometheus /metrics endpoint को हर N seconds में scrape किया जाता है, तो आखिरी scrape और actual process termination के बीच रिकॉर्ड हुए metrics propagate नहीं होते। इसलिए shutdown sequence के दौरान errors के बारे में गलत impression मिल सकता है
    सावधान न रहें तो service बंद होने से ठीक पहले के कुछ seconds के logs भी खो सकते हैं। उदाहरण के लिए, अगर Promtail या Vector जैसा sidecar log file को watch कर रहा हो, और service start होते समय उसी path को truncate करके फिर से लिखे, तो shutdown के दौरान logs गायब होने की race condition बनती है

    • observability stack कुछ बेतुका-सा लगता है। logs, metrics, tracing—हर एक का अपना database, sidecar, visualization stack है; language-specific integration libraries अलग-अलग हैं; और cloud costs भी भारी हैं
      इतना effort लगाने के बाद भी ज़्यादातर data पूरी तरह ignore हो जाता है, और business insight भी अक्सर server में ssh करके log file पर grep चलाने वाले गरीब संस्करण से बहुत बेहतर नहीं होती। इस ecosystem में लगाए गए effort से uptime, performance और usability वाकई meaningful तरीके से बेहतर हुए हैं या नहीं, मुझे पक्का नहीं पता
    • पिछले 8+ सालों से Go high-load applications संभालते हुए जिन समस्याओं से गुज़रा हूं, उन्हें platform library में ठीक इसी तरह handle कर रहा हूं। अलग-अलग companies में platform और rolling deployment को develop/improve करना मेरा hobby रहा है
      “log synchronization”, “ingress के liveness handler के साथ catch up करने तक wait” जैसी चीज़ों को cover करने वाला हूं
      https://github.com/utrack/caisson-go/blob/main/caiapp/caiapp...
      https://github.com/utrack/caisson-go/tree/main/closer
      docs अभी कम हैं और कुछ चीज़ें missing भी हैं, लेकिन vacation से लौटने पर पहला release करने की योजना है। आखिरकार यह सामान्य k8s/otel/grpc+http infrastructure संभालने वाली meta platform और reference platform library बनेगी
    • मुझे हमेशा समझ नहीं आया कि Prometheus और related tools pull model क्यों इस्तेमाल करते हैं। ज़्यादातर तो push model इस्तेमाल करते हैं
    • जिज्ञासा है कि क्या आपने इस समस्या का कोई सुविधाजनक समाधान देखा है। अगर scrape interval 15 seconds है, तो metrics को दो बार record करने के लिए 30 seconds wait नहीं कर सकते
      इसी behavior की वजह से हमारी service अभी भी statsd इस्तेमाल करती है, क्योंकि push-based model में यह समस्या नहीं होती
  • एक छोटी-सी आम trap अक्सर दिखती है: लोग सोचते हैं कि log.Fatal call करने पर भी defer execute होगा। असल में ऐसा नहीं होता
    log.Fatal("fatal") internally os.Exit call करता है, इसलिए तुरंत exit हो जाता है और defer नहीं चलता। वहीं panic("fatal") में fatal और in defer दोनों दिखते हैं

  • अगर distributed system के सही तरीके से काम करने के लिए यह assumption जरूरी है कि clients gracefully shutdown करेंगे, तो कभी न कभी वह बुरी तरह टूटेगा ही

    • मेरा इस पर इतना मजबूत विश्वास है कि design करते समय graceful shutdown को consider ही नहीं करता। components safe तरीके से, बल्कि अक्सर hard crash कर सकने चाहिए, और अगर system का कोई meaningful हिस्सा intended तरीके से चल रहा हो, तो पूरे system पर meaningful impact नहीं होना चाहिए
      system components के hard crash को झेल सकता है या नहीं, यह verify करने का इकलौता तरीका है कि hard crash को हमेशा होने वाली normal चीज़ बना दिया जाए। Chaos Monkey की जय
    • clients या workflow के लिए friendly होने वाला graceful shutdown और system के काम करने के लिए clients का उस पर निर्भर होना—इन दोनों में बड़ा फर्क है
    • पुराने physical server दौर में इसके लिए STONITH इस्तेमाल करते थे: https://smcleod.net/2015/07/delayed-serial-stonith/
    • recoverable situation में भी normal shutdown को catastrophic shutdown जैसा न दिखने देने की valid वजहें होती हैं
      application का sig int से नीचे आना और kill से मारा जाना—इनमें बड़ा अंतर है। उदाहरण के लिए blue-green migration में graceful shutdown behavior चाहिए होता है
    • सही। फिर भी, software को plug खींच लिए जाने को सहने के लिए design किया गया है, इसका मतलब यह नहीं कि shutdown करते समय सच में plug खींचना जरूरी है
      फिर सोचें तो शायद जरूरी हो। उस assumption के सच होने की guarantee करने का यही इकलौता तरीका हो सकता है। कुछ साल पहले Netflix के chaos monkey जैसा तरीका
  • मुझे लगा था इसमें यह तरीका बताया जाएगा कि नई service instance पुरानी instance से listening socket ले ले, ताकि incoming connections में से एक भी काटे बिना application restart हो सके
    systemd में इसे implement करना relatively simple है, और nginx भी 20 साल से ज्यादा समय से इसे support करता आया है। अफसोस, Kubernetes और Docker इसे support नहीं करते क्योंकि वे assume करते हैं कि इसे load balancer या reverse proxy handle करेगा

  • मेरे colleague हमेशा कहते थे कि अगर program ctrl c और कुछ shutdown commands को साफ-सुथरे तरीके से handle नहीं कर सकता, तो वह गलत तरीके से लिखा गया program है

    • Ctrl-C clipboard में copy करने के लिए reserved है। इसे program रोकने की action के रूप में इस्तेमाल करना बहुत counterintuitive है और users को नाराज़ करेगा
  • मुझे लगता है Elixir ऐसे हिस्सों को वाकई समझदारी से संभालता है। मेरा अनुभव बहुत ज़्यादा नहीं है, लेकिन छोटे VM processes को इस तरह डिज़ाइन किया गया है कि वे panic करें, terminate हों और फिर से बन जाएँ, इसलिए जानबूझकर graceful shutdown routine बनाने की ज़रूरत कम हो जाती है
    क्योंकि यह गुण पहले से ही application architecture में built-in है

    • जानना चाहूँगा कि यह लेखक द्वारा चर्चा की गई graceful shutdown की ज़रूरत को कैसे खत्म करता है
  • अपने project में graceful shutdown संभालने के लिए मैंने एक छोटी library बनाई: https://github.com/eberkund/graceful
    आम तौर पर कुछ services होती हैं जिन्हें start करना होता है, और हर एक का start और shutdown करने का तरीका अलग हो सकता है। कभी object को पहले instantiate करना पड़ता है, कभी कोई context होता है जिसे cancel करना चाहते हैं, और कभी कोई Stop method होता है जिसे call करना पड़ता है। इसे इन सबको एक unified API के ज़रिए एक जगह लाने के लिए डिज़ाइन किया गया है

    • मेरे पास भी बिल्कुल यही idea था। बस मेरा API थोड़ा कम elegant लगता है। शायद इसलिए कि इसमें caller कई signals और उन्हें handle करने का तरीका configure कर सकता है
      https://pkg.go.dev/git.sr.ht/~mariusor/wrapper#example-Regis...
    • मैंने भी कुछ मिलता-जुलता बनाया है: https://github.com/pseidemann/finish
  • जो pod shutdown हो रहा है, वह परिभाषा के अनुसार ready नहीं होता। service भी endpoint को terminating और not ready के रूप में mark करती है। यह Terminating state में जाने पर होता है, इसलिए खास तौर पर readiness check को fail कराने की ज़रूरत नहीं है
    SIGTERM और Pod.status या endpoint slice जैसे objects के update का exact order मुझे नहीं पता। SIGTERM के बाद भी connections आने की एक छोटी window हो सकती है, लेकिन जैसा लेख संकेत देता है, वैसा “readiness check fail होने तक” चलने वाला बड़ा interval नहीं होता। cluster manage करने के नजरिए से वह बेहद छोटी window खास मायने नहीं रखती। नए connections स्वीकार न करें, मौजूदा connections को gracefully बंद करें, और reasonable तेजी से exit कर जाएँ। हालांकि जिन apps से मेरा पाला पड़ता है, उनमें से आधे या तो SIGTERM handle करते हैं लेकिन shutdown में लंबा समय लेते हैं, या फिर SIGTERM handle ही नहीं कर पाते और फिर भी shutdown में लंबा समय लेते हैं

  • JustWatch के कुछ projects में हमने Google Wire अपनाया, और उसने game बदल दिया। यह हैरानी की बात है कि यह कम जाना जाता है, लेकिन Kubernetes में messy shutdown logic हटाने में मदद करता है
    Wire clean dependency injection enforce करता है, इसलिए अब सब कुछ किसी unknown order में नहीं, बल्कि तय order में shutdown होता है
    https://go.dev/blog/wire
    https://github.com/google/wire