Kubernetes पर Next.js ऑटोस्केलिंग: HPA और KEDA

Kubernetes पर Next.js ऐप की ऑटोस्केलिंग सेट करें: यथार्थवादी CPU requests, HPA, KEDA, PodDisruptionBudget और OVHcloud पर मापी गई लागत।

डेटा सेंटर में रैक में लगे सर्वरों का नज़दीकी दृश्य, हरी बत्तियाँ जली हुई

संक्षिप्त उत्तर: किसी Next.js एप्लिकेशन को Kubernetes पर ठीक से स्केल करने के लिए HPA केवल आख़िरी टुकड़ा है। पहले ज़रूरी है: मापी गई CPU requests तय करना (हल्के सर्वर रेंडरिंग के लिए सामान्यतः प्रति पॉड 100 से 250 m), CPU limit के बिना एक मेमोरी limit, अलग-अलग readiness/startup probes, इनग्रेस के ड्रेन के अनुरूप terminationGracePeriodSeconds, और एक PodDisruptionBudget। उसके बाद ही CPU पर 60-70 % के लक्ष्य वाला HorizontalPodAutoscaler, और जब लोड एक कतार या नियोजित ट्रैफ़िक हो तो उसे KEDA से पूरा करें। इस पूर्व-कार्य के बिना HPA दोलन करता है, बिल दोगुना करता है और कुछ भी ठीक नहीं करता।

हम नियमित रूप से ऐसे क्लस्टर संभालते हैं जहाँ HPA महीनों से मौजूद है पर कभी काम नहीं आया। या तो वह <unknown>/70% दिखाता है क्योंकि पॉड्स में requests नहीं है, या वह ज़रा-सी इमेज कंपाइलेशन पर maxReplicas तक चढ़ जाता है और बीस मिनट बाद नीचे आ जाता है, या वह स्केल करता रहता है जबकि उपयोगकर्ताओं को 502 मिलते हैं क्योंकि पुराने पॉड्स को इनग्रेस के बैकएंड से हटाने से पहले ही मार दिया जाता है। यह लेख पूरी ट्यूनिंग का वर्णन करता है, उसी क्रम में जिसमें उसे करना चाहिए।

डेटा सेंटर में रैक में लगे सर्वरों का नज़दीकी दृश्य, हरी बत्तियाँ जली हुई

जब तक requests गलत हैं, HPA काम क्यों नहीं करता

Resource मोड में HorizontalPodAutoscaler निरपेक्ष CPU के हिसाब से नहीं सोचता। वह एक अनुपात निकालता है: देखी गई खपत को कंटेनर की घोषित requests से भाग देकर। HPA पर Kubernetes दस्तावेज़ साफ़ कहता है — resources.requests.cpu के बिना उपयोग मीट्रिक उपलब्ध नहीं होती और कंट्रोलर कोई निर्णय नहीं लेता।

व्यावहारिक परिणाम तीन स्थितियों में दिखते हैं, जो हर जगह मिलती हैं:

स्थितिHPA पर प्रभाव
requests.cpu नहीं हैtargetCPUUtilizationPercentage बेकार, HPA निष्क्रिय
requests.cpu बहुत ऊँची (« निश्चिंत रहने के लिए » 1 CPU)वास्तविक उपयोग 8 % पर, HPA कभी स्केल नहीं करता, और हर पॉड नोड पर एक कोर आरक्षित कर लेता है
requests.cpu बहुत नीची (10 m)उपयोग 400 % पर, HPA स्थायी रूप से maxReplicas पर

इसलिए पहला कदम एक माप है, अनुमान नहीं। किसी मौजूदा डिप्लॉयमेंट पर, metrics-server इंस्टॉल होने के साथ:

# consommation instantanée par pod
kubectl top pod -n prod -l app=web --containers

# sur une période, si vous avez Prometheus
# rate(container_cpu_usage_seconds_total{namespace="prod",container="web"}[5m])

output: 'standalone' में चल रहे एक Next.js एप्लिकेशन के लिए, जो मुख्यतः पहले से रेंडर किए गए पेज और कुछ डायनामिक रूट्स सर्व करता है, हम सामान्यतः मध्यम ट्रैफ़िक पर निष्क्रिय अवस्था में प्रति पॉड 40 से 120 m CPU मापते हैं, और सर्वर रेंडरिंग या रीवैलिडेशन पर 400-600 m के छोटे शिखर। इसीलिए हम डिफ़ॉल्ट रूप से यह सेटिंग लागू करते हैं:

resources:
  requests:
    cpu: 150m
    memory: 256Mi
  limits:
    memory: 512Mi

दो विकल्प स्पष्टीकरण के योग्य हैं।

limits.cpu नहीं। CPU एक दबाव-सहनीय (compressible) संसाधन है: requests से ऊपर, पॉड नोड पर जो खाली है वह ले लेता है। limit लगाने से CFS थ्रॉटलिंग सक्रिय हो जाती है (cgroup v2 में cpu.max), और छोटे-छोटे शिखर बनाने वाले सिंगल-थ्रेड Node.js रनटाइम पर थ्रॉटलिंग सीधे p95 लेटेंसी में बदल जाती है। यह परिघटना वर्षों से दस्तावेज़ीकृत है और CFS burst का सुधार हर जगह सक्रिय नहीं है। हम CPU को बिना सीमा छोड़ते हैं और नोड की रक्षा अन्य वर्कलोड की requests से करते हैं।

वहीं एक limits.memory आवश्यक है। मेमोरी दबाव-असहनीय है: सीमा के बिना, किसी पॉड में एक लीक नोड को मेमोरी प्रेशर में डाल देती है और कहीं और evictions शुरू करा देती है। Node.js 22 के साथ, हीप को संरेखित करना याद रखें: 512 Mi तक सीमित कंटेनर में NODE_OPTIONS=--max-old-space-size=384 रखने से क्रूर OOMKill की जगह एक पठनीय JavaScript त्रुटि मिलती है।

QoS क्लास जाँचें

kubectl get pod -n prod -l app=web \
  -o custom-columns=NAME:.metadata.name,QOS:.status.qosClass

requests.memory के limits.memory के बराबर होने पर लेकिन limits.cpu न होने पर, आपको Burstable मिलता है। यह सही समझौता है: पॉड eviction का पहला उम्मीदवार नहीं होता, और पूरा कोर भी आरक्षित नहीं करता।

Probes: वह शर्त जिससे स्केलिंग कुछ न तोड़े

एक HPA जो तीन पॉड बनाता है जबकि इनग्रेस उन पॉड्स को ट्रैफ़िक भेज रहा है जो अभी तैयार नहीं हैं, कुछ भी सुधार नहीं करता। जिन तीन probes को अलग करना चाहिए:

startupProbe:
  httpGet: { path: /api/health, port: 3000 }
  periodSeconds: 2
  failureThreshold: 30      # 60 s pour démarrer, puis échec
readinessProbe:
  httpGet: { path: /api/health, port: 3000 }
  periodSeconds: 5
  failureThreshold: 2
livenessProbe:
  httpGet: { path: /api/health, port: 3000 }
  periodSeconds: 10
  failureThreshold: 3

startupProbe सबसे अधिक भुलाई जाती है। यह स्टार्टअप के दौरान liveness और readiness को स्थगित कर देती है, जिससे वह क्लासिक दुष्चक्र टलता है: लदा हुआ नोड, धीमा स्टार्टअप, एक livenessProbe जो पॉड को मार देती है, एक रीस्टार्ट, और उससे भी ज़्यादा लोड। standalone मोड में एक Next.js सर्वर सामान्यतः 1 से 3 सेकंड में शुरू होता है, लेकिन जो पॉड अपने वॉल्यूम या PostgreSQL कनेक्शन का इंतज़ार कर रहा है, वह 20 सेकंड से भी आगे जा सकता है।

हेल्थ रूट के मामले में, उसे मामूली और बाहरी निर्भरता से मुक्त रखें:

// app/api/health/route.ts
export const dynamic = 'force-dynamic'

export function GET() {
  return new Response('ok', {
    status: 200,
    headers: { 'cache-control': 'no-store' },
  })
}

एक readinessProbe जो डेटाबेस से पूछताछ करती है, PostgreSQL की एक घटना को पूरे फ़्रंट की अनुपलब्धता में बदल देती है: सभी पॉड एक ही समय पर NotReady हो जाते हैं और इनग्रेस के पास कोई बैकएंड नहीं बचता। अगर आप डेटाबेस जाँचना चाहते हैं, तो उसे एक अलग रूट में करें जिसे मॉनिटरिंग उपयोग करे, kubelet नहीं। यही तर्क उन डेटाबेसों पर भी लागू होता है जिनका वर्णन हम अपनी तुलना Kubernetes पर PostgreSQL या मैनेज्ड डेटाबेस में करते हैं।

साफ़-सुथरा शटडाउन, अक्सर 502 का असली कारण

जब HPA रेप्लिका की संख्या घटाता है, या जब कोई rolling update पॉड्स बदलता है, तो Kubernetes SIGTERM भेजता है और पॉड का IP Endpoints से हटाता है — दोनों समांतर में, क्रम में नहीं। इनग्रेस (NGINX, Traefik, Kong) कुछ सेकंड तक पॉड की ओर रूटिंग जारी रख सकता है।

terminationGracePeriodSeconds: 40
lifecycle:
  preStop:
    exec:
      command: ["sleep", "10"]

10 सेकंड का preStop Endpoints के प्रसार को समय देता है। एप्लिकेशन की ओर, SIGTERM पर HTTP सर्वर बंद करना चाहिए, न कि प्रोसेस को किसी रिक्वेस्ट के बीच मरने देना:

// server.ts (custom server) ou wrapper autour de server.js standalone
const shutdown = () => {
  server.close(() => process.exit(0))
  setTimeout(() => process.exit(1), 25_000).unref()
}
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)

अगर आप Next.js के लिए Kubernetes पर GitLab CI/CD पर हमारे लेख में बताए गए पाइपलाइन से डिप्लॉय करते हैं, तो ये फ़ील्ड उसी Helm चार्ट में रहते हैं जिसमें HPA है: उन्हें साथ-साथ विकसित होना चाहिए।

नीली रोशनी वाले डेटासेंटर में सर्वर रैक की कतार, « NETWORK-2 » लेबल और हरी बत्तियाँ

वह HPA मैनिफ़ेस्ट जो हम डिप्लॉय करते हैं

autoscaling/v2 API Kubernetes 1.23 से स्थिर है और वह निर्णायक फ़ील्ड देता है: behavior।

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
  namespace: prod
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30
        - type: Pods
          value: 4
          periodSeconds: 30
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60

जो पैरामीटर मायने रखते हैं:

  • minReplicas: 2 और कभी 1 नहीं। एक ही रेप्लिका के साथ, कोई भी eviction, कोई भी नोड ड्रेन, कोई भी अपडेट सेवा बंद कर देता है। दो अलग-अलग नोड्स पर दो रेप्लिका, यह किसी प्रोडक्शन साइट के लिए न्यूनतम है।
  • averageUtilization: 65, डिफ़ॉल्ट 80 के बजाय। पूरी प्रतिक्रिया अवधि — मीट्रिक संग्रह (metrics-server की ओर डिफ़ॉल्ट 15 s), HPA का लूप (15 s), शेड्यूलिंग, पॉड का स्टार्टअप — दसियों सेकंड में गिनी जाती है। requests के 80 % पर, नए पॉड आने से पहले ही मार्जिन खप जाता है।
  • scaleUp.stabilizationWindowSeconds: 0: तेज़ी से ऊपर जाना। scaleDown 300 s पर: धीरे नीचे आना। यह असममिति ही ट्यूनिंग का सार है, यही वह दोलन (flapping) मिटाती है जो डिफ़ॉल्ट पर छोड़े गए HPA में दिखता है।

सब कुछ जुड़ा है या नहीं, यह जाँचने के लिए:

kubectl get hpa web -n prod
# NAME  REFERENCE        TARGETS         MINPODS  MAXPODS  REPLICAS
# web   Deployment/web   cpu: 41%/65%    2        10       3

kubectl describe hpa web -n prod | tail -20

अगर TARGETS में <unknown>/65% दिखे, तो समस्या पहले के चरण में है: metrics-server अनुपस्थित, या requests.cpu गायब। पहले खंड पर लौटें।

PodDisruptionBudget, अविभाज्य

PDB के बिना, नोड अपडेट के दौरान एक kubectl drain सभी रेप्लिका को एक साथ खाली कर सकता है।

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web
  namespace: prod
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: web

minReplicas: 2 के साथ, minAvailable: 1 पर्याप्त है। 4 रेप्लिका से ऊपर, हम maxUnavailable: 25% पर चले जाते हैं। साथ ही kubernetes.io/hostname पर whenUnsatisfiable: ScheduleAnyway के साथ एक topologySpreadConstraints जोड़ें, जिससे सभी रेप्लिका एक ही नोड पर न पहुँच जाएँ।

जब CPU काफ़ी नहीं रहता: KEDA

सर्वर रेंडरिंग के लिए CPU एक अच्छा प्रॉक्सी है। तीन आम वर्कलोड के लिए यह बेकार है।

कतार वाले वर्कर। कोई वर्कर जो Redis या RabbitMQ की कतार से उपभोग करता है, अपना समय इंतज़ार में बिताता है: उसका CPU 5 % पर रहता है जबकि 8 000 संदेश जमा होते जाते हैं। KEDA (अगस्त 2023 से CNCF का graduated प्रोजेक्ट) बाहरी scalers उपलब्ध कराकर ठीक इसी स्थिति का समाधान करता है।

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: worker-emails
  namespace: prod
spec:
  scaleTargetRef:
    name: worker-emails
  minReplicaCount: 0
  maxReplicaCount: 20
  pollingInterval: 15
  cooldownPeriod: 120
  triggers:
    - type: redis
      metadata:
        addressFromEnv: REDIS_ADDRESS
        listName: queue:emails
        listLength: "50"

प्रतीक्षारत प्रत्येक 50 संदेशों के लिए एक पॉड, और minReplicaCount: 0 — KEDA शून्य तक नीचे आ सकता है, जो अकेला HPA नहीं कर पाता। व्यवहार में, KEDA पर्दे के पीछे एक HPA बनाता और चलाता है; आप केवल ScaledObject संभालते हैं।

पूर्वानुमेय ट्रैफ़िक। किसी SME साइट, जिसके दर्शक फ़्रांसीसी-यूरोपीय हैं, को रात 1 और सुबह 6 बजे के बीच लगभग कुछ नहीं मिलता। cron scaler बेकार रेप्लिका का भुगतान टाल देता है:

  triggers:
    - type: cron
      metadata:
        timezone: Europe/Paris
        start: "45 7 * * 1-5"
        end: "30 20 * * 1-5"
        desiredReplicas: "4"

व्यावसायिक मीट्रिक। प्रति सेकंड रिक्वेस्ट, HTTP कतार की लंबाई, p95 लेटेंसी: prometheus scaler किसी भी PromQL क्वेरी को ऑटोस्केलिंग से जोड़ देता है। Next.js के लिए यह अक्सर CPU से अधिक प्रासंगिक होता है, जहाँ एक I/O-भारी रिक्वेस्ट (API कॉल, डेटाबेस) साइकल खपाए बिना एक स्लॉट ब्लॉक कर देती है।

    - type: prometheus
      metadata:
        serverAddress: http://prometheus-server.monitoring:80
        query: sum(rate(http_requests_total{service="web"}[2m]))
        threshold: "60"

हम जो नियम लागू करते हैं: प्रति Deployment केवल एक ऑटोस्केलर। एक ही लक्ष्य पर निशाना लगाने वाले HPA और ScaledObject आपस में लड़ते हैं और निर्माण/विलोपन के चक्र पैदा करते हैं। अगर आप KEDA पर जाते हैं, तो हाथ से लिखा HPA हटा दें।

डेटा सेंटर में नीली रोशनी वाला रैक सर्वर, हरी और नारंगी बत्तियों के साथ

इसकी असली लागत क्या है

क्षैतिज ऑटोस्केलिंग बिल तभी घटाती है जब नोड्स साथ चलें। वरना आप लगातार शिखर की क्षमता का भुगतान करते रहते हैं। दो उपाय:

  • नोड ऑटोस्केलिंग। OVHcloud Managed Kubernetes और Scaleway Kapsule, दोनों परिवर्तनीय आकार के पूल देते हैं, प्रति पूल एक न्यूनतम और एक अधिकतम के साथ। नोड जुड़ने में लगने वाला समय मिनटों में गिना जाता है, सेकंडों में नहीं: पॉड्स का कुछ मार्जिन गर्म रखें।
  • ओवर-प्रोविज़निंग के « pause » पॉड। ऋणात्मक प्राथमिकता वाले पॉड्स का एक Deployment, जो जगह आरक्षित रखते हैं और असली पॉड आते ही बाहर कर दिए जाते हैं। लागत: एक नोड का अंश, और लोड बढ़ने पर कई मिनट का लाभ।

हमारे द्वारा संचालित एक कॉन्फ़िगरेशन का अनुमानित परिमाण: 4 vCPU / 8 GB के तीन नोड एक स्थिर पूल में, साथ में 0 से 3 समान नोड्स का एक लचीला पूल। 150 m की requests के साथ, 4 vCPU का एक नोड सैद्धांतिक रूप से बीस के आसपास web पॉड रख सकता है, व्यवहार में सिस्टम, CSI एजेंट और मॉनिटरिंग हटाने के बाद बारह के आसपास। OVHcloud और Scaleway की सार्वजनिक दरें बदलती रहती हैं: उनके मैनेज्ड ऑफ़र पेजों से उसी दिन की सूची लें, और स्टोरेज तथा आउटबाउंड बैंडविड्थ अलग से गिनें। हम यह दृष्टिकोण अपने इन्फ़्रास्ट्रक्चर सेवाएँ पेज पर विस्तार से बताते हैं।

बिलिंग का सबसे आम जाल वही रहता है: « ज़रूरत पड़ने पर » लगाया गया 50 का maxReplicas, एक रीवैलिडेशन लूप जो बेकाबू हो जाता है, और एक लचीले पूल पर 40 रेप्लिका वाली एक रात। maxReplicas उतना रखें जितना आपका बजट सहन करे, न कि जितना क्लस्टर झेल सकता है, और kube_horizontalpodautoscaler_status_current_replicas के अधिकतम के पास पहुँचने पर एक अलर्ट लगाएँ।

अक्सर पूछे जाने वाले प्रश्न

क्या Next.js पॉड पर CPU लिमिट लगानी चाहिए?

अधिकांश मामलों में नहीं। छोटे-छोटे शिखर बनाने वाले Node.js रनटाइम पर CFS थ्रॉटलिंग p95 लेटेंसी ख़राब करती है, और पॉड्स के बीच निष्पक्षता सुनिश्चित करने के लिए requests पर्याप्त है। अपवाद: कई ग्राहकों के बीच साझा एक क्लस्टर, जहाँ कोई ResourceQuota या आंतरिक नीति limit अनिवार्य करती है। उस स्थिति में उसे requests के कम से कम 4 गुना पर रखें और container_cpu_cfs_throttled_seconds_total पर नज़र रखें।

मेरा HPA <unknown> क्यों दिखाता है?

तीन कारण, आवृत्ति के क्रम में: metrics-server इंस्टॉल नहीं है या जवाब नहीं देता (kubectl top node विफल होता है), कंटेनर में resources.requests.cpu नहीं है, या scaleTargetRef किसी ऐसे संसाधन की ओर इशारा करता है जो मौजूद नहीं है। kubectl describe hpa सटीक कारण ScalingActive और AbleToScale conditions में बताता है।

HPA या VPA?

VerticalPodAutoscaler किसी पॉड की requests समायोजित करता है, HPA पॉड्स की संख्या। एक ही मीट्रिक पर दोनों टकराते हैं। एक स्टेटलेस वेब फ़्रंट के लिए HPA ही सही उपकरण है। VPA केवल recommender मोड में उपयोगी है, सही requests मापने के लिए, बिना कुछ स्वतः लागू किए — हम इसे किसी नए डिप्लॉयमेंट के पहले दो सप्ताहों में इसी तरह उपयोग करते हैं।

और जब पॉड्स बढ़ते जाएँ तो Next.js का ISR कैश?

यह असली ध्यान देने वाली बात है। standalone मोड में, इंक्रीमेंटल रीवैलिडेशन कैश पॉड की फ़ाइल सिस्टम पर संग्रहित होता है: दस रेप्लिका, दस कैश, एक ही पेज के दस पुनर्निर्माण। एक कस्टम CacheHandler (Redis, S3) के ज़रिए साझा कैश चाहिए, या फिर डुप्लिकेशन स्वीकार करें। इसके बिना, ऑटोस्केलिंग डेटाबेस पर लोड बाँटने के बजाय उसे गुणा कर देती है।

ऑटोस्केलिंग काम करती है, यह कैसे परखें?

एक लोड जनरेटर से, शुक्रवार को प्रोडक्शन में नहीं। क्लस्टर के किसी पॉड से चलाए गए k6, vegeta या hey, एक प्रतिनिधि डायनामिक रूट पर, 10 मिनट तक, और समांतर में एक kubectl get hpa -w। आप तीन चीज़ें प्रमाणित करते हैं: HPA स्केल करता है, नए पॉड अपेक्षित समय में Ready होते हैं, और scale-down क्लाइंट की ओर त्रुटियाँ नहीं पैदा करता। यही प्रोटोकॉल रिस्टोर जाँचने के काम आता है, जैसा हमारी गाइड Velero के साथ Kubernetes बैकअप में है।

सारांश

  • HPA तभी काम करता है जब resources.requests.cpu मापी गई हो, अनुमानित नहीं: एक सामान्य standalone Next.js पॉड के लिए 100 से 250 m।
  • कोई limits.cpu नहीं (CFS थ्रॉटलिंग और p95 लेटेंसी), लेकिन --max-old-space-size के अनुरूप एक limits.memory।
  • अलग startupProbe, डेटाबेस पर निर्भरता रहित हेल्थ रूट, scale-down पर 502 मिटाने के लिए preStop: sleep 10 और terminationGracePeriodSeconds: 40।
  • 65 % उपयोग पर autoscaling/v2 HPA, minReplicas: 2, behavior के ज़रिए तत्काल वृद्धि और 300 s में कमी।
  • PodDisruptionBudget और topologySpreadConstraints हमेशा, वरना एक नोड ड्रेन पूरा लाभ रद्द कर देता है।
  • कतारों (शून्य तक स्केल), नियोजित ट्रैफ़िक (cron) और Prometheus मीट्रिक के लिए KEDA — पर प्रति Deployment केवल एक ऑटोस्केलर।
  • नोड्स की ऑटोस्केलिंग की योजना बनाएँ और maxReplicas बजट के हिसाब से तय करें, क्लस्टर की क्षमता के हिसाब से नहीं।
  • Next.js की ओर, जैसे ही रेप्लिका की संख्या बदलती है, एक साझा ISR कैश अनिवार्य हो जाता है।

ट्रैफ़िक के शिखर की तैयारी कर रहे हैं या हर डिप्लॉयमेंट पर 502 देख रहे हैं? संपर्क पेज के ज़रिए हमें अपने क्लस्टर के बारे में बताएँ: इन्फ़्रास्ट्रक्चर को छूने से पहले हम requests, probes और HPA का ऑडिट करते हैं।