გადატვირთვა მოთხოვნებს ერთი მიზეზით კარგავს: შენი პროცესი მათ მომსახურების შუაში მოკვდა. გამოსწორება ორი პატარა კოდის ნაწილია. ერთი იჭერს SIGTERM-ს, წყვეტს ახალი კავშირების მიღებას, ნებას აძლევს მიმდინარეებს დასრულდნენ, ხურავს ბაზის pool-ს და ნულოვანი სტატუსით გადის. მეორე არის endpoint, რომელიც ამბობს, მზადაა თუ არა პროცესი ტრაფიკისთვის - და, რაც მთავარია, არ ურევს "ბაზას ერთი წამით გაუჭირდა" და "დაუყოვნებლივ გადამტვირთე". ორივე ერთ შუადღეს მოითხოვს. ერთად ისინი განსხვავებაა deploy-ს შორის, რომელსაც ვერავინ ამჩნევს, და deploy-ს შორის, რომელიც შენს error tracker-ში ჩანს.
რა აჩერებს შენს პროცესს სინამდვილეში#
ყველაფერი, რაც პროგრამას აჩერებს, მას ორიდან ერთ რამეს უგზავნის და ეს განსხვავება მთელი თემაა.
| სიგნალი | დაჭერადია | ვინ აგზავნის |
|---|---|---|
SIGTERM (15) | დიახ | docker stop, პანელის Stop ღილაკი, systemctl stop, kill დროშის გარეშე |
SIGINT (2) | დიახ | Ctrl-C ტერმინალში |
SIGHUP (1) | დიახ | ტერმინალი დაიხურა; ხშირად "კონფიგურაციის გადატვირთვად" გამოიყენება |
SIGKILL (9) | არა | kill -9, stop timeout-ის ამოწურვა, kernel-ის out-of-memory killer |
SIGTERM თხოვნაა. SIGKILL თხოვნა არ არის - kernel პროცესს აშორებს და შენი კოდიდან არაფერი სრულდება. ყოველი დახურვის მექანიზმი, რომელსაც ღირებულება აქვს, ამ ორს შორის შეჯიბრია: რაღაც SIGTERM-ს აგზავნის, ფიქსირებულ წამებს ელოდება, შემდეგ SIGKILL-ს აგზავნის. Docker-ის ნაგულისხმევი მოცდის პერიოდია 10 წამი. Kubernetes 30-ს იყენებს. რაც არ უნდა იდგეს შენი აპლიკაციის წინ, გაარკვიე ეს რიცხვი, რადგან შენი დახურვა მასში უნდა დასრულდეს, თორემ ის მხოლოდ დეკორაციაა.
ორი გაჩერება თავაზიან ვერსიას სრულიად აცდენს. Kill ღილაკი SIGKILL-ს განზრახ აგზავნის - სწორედ ამისთვის არის და ესაა სწორი ქმედება, როცა პროცესი გაიჭედა. და out-of-memory გაჩერება არაფერს გიტოვებს: RE:NODE-ზე მეხსიერების ლიმიტის მიღწევა container-ს აჩერებს და სუფთად გადატვირთავს, მანქანის swap-ში ჩავარდნის ნაცვლად, რაც node-ზე დანარჩენებისთვის უკეთესია და ფატალურია იმისთვის, რასაც შენი პროცესი მეხსიერებაში ინახავდა. თუ მიმდინარე სამუშაოს დაკარგვას მნიშვნელობა აქვს, პასუხი უფრო ჭკვიანი shutdown handler კი არა, მეხსიერების არ გამოლევაა - Node-ის მეხსიერების ლიმიტები ახსნილი აღწერს ჩვეულებრივ მიზეზებს.
PID 1-ის პრობლემა: სიგნალი, რომელიც არასოდეს მოდის#
ნებისმიერი handler-ის წერამდე შეამოწმე, საერთოდ აღწევს თუ არა სიგნალი შენს კოდს. ეს არის ყველაზე გავრცელებული მიზეზი, რის გამოც სწორი shutdown ფუნქცია არასოდეს სრულდება.
თუ შენი გაშვების ბრძანებაა npm start, SIGTERM-ს npm იღებს, ხოლო შენი აპლიკაცია მისი შვილობილი პროცესია. სიგნალებთან npm-ის ქცევა ვერსიებსა და პლატფორმებზე იცვლებოდა და ჩავარდნა ჩუმია: stop timeout-ით სრულდება, ყველაფერი იგდება და შენი handler არასოდეს გამოიძახება. იგივე ეხება shell wrapper-ს:
#!/bin/sh# Wrong: the shell stays as PID 1 and the signal stops herenode dist/server.js# Right: the shell replaces itself with node, which becomes PID 1exec node dist/server.jsწესები, რომლებიც აქედან გამომდინარეობს:
- გაშვების ბრძანება თავად runtime იყოს:
node dist/server.js,python -m gunicorn .... არაnpm start, არაyarn dev, არა სკრიპტი, რომელსაცexecდავიწყებია. - თუ wrapper აუცილებელია, ბოლო ხაზზე გამოიყენე
exec. - პროცესს, რომელიც container-ში ნამდვილად PID 1-ია, ეკისრება ობოლი შვილობილი პროცესების დამუშავების მოვალეობაც. Node და Python ამას არ აკეთებენ, რისთვისაც
--initდაtiniარსებობს. ეს მნიშვნელოვანია, თუ შენი აპლიკაცია subprocess-ებს უშვებს; სხვა შემთხვევაში იგნორირება გააკეთე.
გადაამოწმე და ნუ დაუშვებ. ჩაწერე handler-ში ერთი ხაზი ლოგში, გააჩერე სერვერი პანელიდან და იპოვე ეს ხაზი კონსოლში. RE:NODE-ზე კონსოლის გამოტანა გაუფილტრავია, ამიტომ shutdown-ის ლოგის ხაზი მართლა ჩანს და არ იჭრება - კონსოლის კითხვა აღწერს, კიდევ რის დაბეჭდვა ღირს იქ.
HTTP სერვერის დრენაჟი Node-ში#
ფორმა ყოველთვის ერთია: შეწყვიტე ახალი სამუშაოს აღება, დაასრულე ძველი, გაათავისუფლე რესურსები, გადი. Node-ში დახვეწილი ნაწილი keep-alive კავშირებია, რომლებიც პასუხის შემდეგ ღიად რჩება და server.close()-ს არასოდეს აძლევს callback-ის გამოძახების საშუალებას.
const server = app.listen(process.env.PORT || 3000);let shuttingDown = false;async function shutdown(signal) { if (shuttingDown) return; shuttingDown = true; console.log(`${signal} received, draining`); // 1. Fail readiness first, so anything checking stops sending traffic. app.locals.ready = false; // 2. Give the checker one interval to notice before we close the door. await new Promise((resolve) => setTimeout(resolve, 5000)); // 3. Stop accepting connections; the callback fires when the last one ends. server.close(async () => { await pool.end(); // database await worker?.close(); // job queue console.log("drained, exiting"); process.exit(0); }); // 4. Idle keep-alive sockets would hold us open forever. Node 18.2+. server.closeIdleConnections(); // 5. A hard deadline, shorter than whatever will send SIGKILL. setTimeout(() => { console.error("drain timed out, forcing exit"); process.exit(1); }, 20_000).unref();}process.on("SIGTERM", () => shutdown("SIGTERM"));process.on("SIGINT", () => shutdown("SIGINT"));ნაბიჯები, რომლებსაც ხალხი გამოტოვებს, არის მე-2 და მე-4.
მე-2 ნაბიჯი არსებობს იმიტომ, რომ health check-ები იკითხება და არ იგზავნება. თუ load balancer ყოველ ხუთ წამში ამოწმებს და შენ მაშინვე იხურები, ის მოთხოვნებს შენკენ გზავნას გააგრძელებს იმ მომენტის შემდეგაც, რაც მოსმენა შეწყვიტე, ხუთ წამამდე, და თითოეული მათგანი 502-ია. ჯერ readiness-ის ჩავარდნა, შემდეგ ერთი polling ინტერვალის მოცდა ამათ მოთხოვნებად აქცევს, რომლებიც არასოდეს გაგზავნილა. როცა გარედან არაფერი ამოწმებს - ერთი პროცესი ერთი proxy-ს უკან - შეგიძლია ეს შეამციროს, მაგრამ ნულამდე არასოდეს.
მე-4 ნაბიჯი keep-alive-ის მახეა. ბრაუზერი უმოქმედო HTTP კავშირს ხელახალი გამოყენებისთვის ღიად ინახავს; Node-ის საკუთარი server.keepAliveTimeout ნაგულისხმევად ხუთი წამია, მაგრამ კლიენტს, რომელიც მოთხოვნებს აგზავნის, სოკეტის განუსაზღვრელი ხნით დაჭერა შეუძლია. server.closeIdleConnections() ხურავს იმათ, რომლებიც ახლა არაფერს აკეთებს, და ტოვებს მოთხოვნის შუაში მყოფებს. Node ასევე იწყებს არსებულ კავშირებზე პასუხების Connection: close-ით მონიშვნას, როგორც კი სერვერი იხურება, რაც კლიენტებს ეუბნება, რომ სხვაგან ხელახლა დაუკავშირდნენ.
მე-5 ნაბიჯის მკაცრი ვადა პლატფორმის მოცდის პერიოდზე მოკლე უნდა იყოს, თორემ SIGKILL უბრალოდ უფრო გვიან გადაგიწევია. .unref() ტაიმერს უშლის ხელს, რომ event loop ღიად დაიჭიროს, როცა ყველაფერი დანარჩენი დასრულდა.
WebSocket კავშირები თავისით არასოდეს მთავრდება, ამიტომ server.close() მათ სამუდამოდ დაელოდება. დახურე ისინი პირდაპირ close კოდით (1001, "going away"), რომ კლიენტებმა იცოდნენ ხელახლა დაკავშირება, და კლიენტის მხარეს ხელახალი კავშირი შემთხვევითი დაყოვნებით გაანაწილე - თორემ ყველა მომხმარებელი ერთსა და იმავე მეათედ წამში დაუკავშირდება. WebSocket-ები reverse proxy-ს უკან უფრო დეტალურად აღწერს reconnect storm-ს.
Python: gunicorn, uvicorn და სხვები#
Gunicorn მთელ სქემას უკვე ახორციელებს; საქმე უმეტესად იმის ცოდნაა, რომელი სიგნალი რას აკეთებს.
| სიგნალი | ეფექტი |
|---|---|
TERM | Graceful shutdown: worker-ები მიმდინარე მოთხოვნებს ასრულებენ, --graceful-timeout-მდე |
QUIT | სწრაფი დახურვა: worker-ები ახლავე ჩერდებიან |
HUP | კონფიგურაციის ხელახლა წაკითხვა და worker-ების სათითაოდ გადატვირთვა |
USR2 | ახალი master-ის გაშვება ძველის გვერდით |
--graceful-timeout ნაგულისხმევად 30 წამია და --timeout (რამდენ ხანს შეიძლება worker მდუმარე იყოს, სანამ master მას მოკლავს) ასევე ნაგულისხმევად 30. თუ შენი პლატფორმის მოცდის პერიოდი 10 წამია, gunicorn-ის 30 უაზროა - დაწიე, რომ ჯდებოდეს:
$ gunicorn app.wsgi:application \ --bind 0.0.0.0:8000 --workers 3 \ --timeout 30 --graceful-timeout 8 --max-requests 1000 --max-requests-jitter 100--max-requests worker-ს ამ რაოდენობა მოთხოვნის შემდეგ ახალი ვერსიით ცვლის, რაც ნელი მეხსიერების გაჟონვის უხეში, მაგრამ ეფექტური პასუხია. Jitter ხელს უშლის ყველა worker-ის ერთდროულ გადატვირთვას.
ASGI-სთვის uvicorn SIGTERM-ს თავად ამუშავებს და უჭერს მხარს --timeout-graceful-shutdown-ს. FastAPI-ში გასუფთავება lifespan handler-ში უნდა იყოს:
from contextlib import asynccontextmanager@asynccontextmanagerasync def lifespan(app): pool = await create_pool() app.state.pool = pool yield # the application runs here await pool.close() # runs on shutdown, before the process exitsyield-ის შემდეგ ყველაფერი shutdown-ის დროს სრულდება. თუ ჯერ კიდევ იყენებ @app.on_event("shutdown")-ს, ის მუშაობს, მაგრამ ახალი კოდისთვის lifespan არის სწორი ადგილი.
Worker-ის გაჩერება job-ის შუაში#
HTTP მოთხოვნა მილიწამებს გრძელდება. Background job შეიძლება წუთობით გაგრძელდეს, ხოლო იგივე 10-წამიანი მოცდის პერიოდი მასაც ეხება, ამიტომ "დაასრულე მიმდინარე job" ხშირად ვარიანტი არ არის. პასუხი უფრო გრძელი timeout კი არა, შეწყვეტის უსაფრთხოდ ქცევაა.
- დაადასტურე გვიან. Celery-ის
task_acks_late=Trueდა ანალოგები ნიშნავს, რომ job შესრულებულად მხოლოდ დასრულების შემდეგ მოინიშნება. Worker შუაში მოკალი და job რიგში ბრუნდება და არ ქრება. - გახადე job-ები idempotent. თუ გვიანი დადასტურება job-ს ხელახლა გადმოგზავნის, ის ზოგჯერ ორჯერ შესრულდება. ბარათიდან ორჯერ ჩამოწერა ჰიპოთეტური არ არის.
- ჯერ შეწყვიტე მოხმარება.
SIGTERM-ზე worker-ს უთხარი, ახალი job-ები აღარ აიღოს, შემდეგ დაე მიმდინარემ ვადის ფარგლებში დაასრულოს. Celery ამას warm shutdown-ს უწოდებს და მეორეSIGTERMმას cold-ად აქცევს. BullMQ-ისawait worker.close()აქტიურ job-ებს ელოდება;true-ს გადაცემა აიძულებს. - ნაკლები prefetch. Worker, რომელმაც ორმოცდაათი job აიღო და არ დაუწყია, გადატვირთვისას ორმოცდაათ job-ს მძევლად ინახავს. Celery-ში
worker_prefetch_multiplier = 1, სხვაგან მცირე concurrency. - Checkpoint გაუკეთე გრძელ სამუშაოს. Job-მა, რომელიც 50 000 სტრიქონს ამუშავებს, პროგრესი უნდა ჩაიწეროს, რომ გადატვირთვამ თავიდან დაწყების ნაცვლად გაგრძელება შეძლოს.
Background job-ები პატარა სერვერზე თავად რიგის ვარიანტებს აღწერს, იმის ჩათვლით, რა უნდა გააკეთო, როცა Redis არ გაქვს.
Liveness, readiness და რატომ იწვევს მათი აღრევა outage-ებს#
ორი კითხვა, ორი განსხვავებული პასუხი, და მათი აღრევა არის გზა, რომლითაც ბაზის ხუთწამიანი შეფერხება ოცწუთიან outage-ად იქცევა.
- Liveness: არის თუ არა ეს პროცესი შეუქცევადად გატეხილი? ჩავარდნა ნიშნავს "გადამტვირთე". ის თითქმის არაფერს უნდა ამოწმებდეს - რომ event loop ტრიალებს და HTTP სერვერი პასუხობს. არავითარი ბაზა, cache ან მესამე მხარის API.
- Readiness: უნდა მიიღოს თუ არა ამ პროცესმა ტრაფიკი ახლა? ჩავარდნა ნიშნავს "ჯერ გვერდი ამიარე". ამას შეუძლია დამოკიდებულებების შემოწმება, რადგან ბაზასთან კავშირის გარეშე დაწყება მოთხოვნების არგაგზავნის რეალური მიზეზია.
ბაზა liveness შემოწმებაში ჩადე და გააკეთე გამაძლიერებელი. ბაზას შეფერხება აქვს, ყველა instance ვერ გადის liveness-ს, ყველა instance გადაიტვირთება, ყველა ერთდროულად ხელახლა უკავშირდება და ბაზა, რომელიც წამით ნელი იყო, ახლა მართლა გათიშულია. ამასობაში გადატვირთვამ გაანადგურა cache-ები და კავშირების pool-ები, რომლებიც მას აღდგენაში დაეხმარებოდა.
მესამე სახეობა ღირს, რომ იცოდე, თუ შენს აპლიკაციას ჩატვირთვას დრო სჭირდება: startup შემოწმება, რომელიც liveness-ის დაწყებამდე გრძელ მოცდის პერიოდს იძლევა. JVM-ს ან დიდ Python აპლიკაციას, რომელსაც გასაშვებად 60 წამი სჭირდება, სხვაგვარად 10-წამიანი timeout-ის liveness შემოწმება მოკლავს, სამუდამოდ, ციკლში, რომელიც ზუსტად crash-ს ჰგავს. თუ სერვისი დაუსრულებლად გადაიტვირთება და ლოგები ყოველ ჯერზე სუფთა გაშვებას აჩვენებს, ეს ჩვეულებრივ მიზეზია - იგივე ფორმის პრობლემა, რაც თამაშის სერვერი, რომელიც ყოველ ჯერზე გადაიტვირთება.
Health endpoint, რომელიც ღირს#
// Liveness: no dependencies. If this cannot answer, the process is stuck.app.get("/healthz", (req, res) => res.status(200).json({ status: "ok" }));// Readiness: cheap dependency checks, short timeouts, cached briefly.let cached = { at: 0, body: null, code: 503 };app.get("/readyz", async (req, res) => { if (Date.now() - cached.at < 2000) return res.status(cached.code).json(cached.body); const checks = {}; try { await Promise.race([ pool.query("SELECT 1"), new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 1500)), ]); checks.database = "ok"; } catch (error) { checks.database = `failed: ${error.message}`; } const ok = app.locals.ready && Object.values(checks).every((v) => v === "ok"); cached = { at: Date.now(), code: ok ? 200 : 503, body: { status: ok ? "ok" : "degraded", checks, version: process.env.GIT_SHA }, }; if (!ok) res.set("Retry-After", "5"); res.status(cached.code).json(cached.body);});დეტალები, რომლებიც მას დეკორატიულის ნაცვლად სასარგებლოს ხდის:
- `SELECT 1` და არა რეალური query. შენ ამოწმებ, რომ კავშირი მუშაობს და არა იმას, რომ სქემა სწორია.
- Timeout ყოველ შემოწმებაზე. Health endpoint, რომელიც გაჭედავს, ჩავარდნილზე უარესია, რადგან შედეგს მომწმებლის საკუთარი timeout წყვეტს და ის ჩვეულებრივ გაცილებით გრძელია.
- შედეგი ერთი-ორი წამით cache-ში შეინახე. მონიტორი, proxy და deploy სკრიპტი, რომლებიც ყოველ ხუთ წამში პოლინგს აკეთებენ, გასაკვირად დიდი დატვირთვაა, თუ თითოეული პოლი კავშირს ხსნის.
- `503` და არასოდეს `500`.
503 Service UnavailableRetry-After-ით არის სწორი "ახლა არა, სცადე თავიდან";500ნიშნავს, რომ რაღაცამ გამონაკლისი ისროლა, და მონიტორები მას სხვანაირად ეპყრობიან. - დააბრუნე ვერსია. Commit SHA-ის ჩაწერა პასუხში endpoint-ს deploy შემოწმებად აქცევს: ხედავ, რომელი build პასუხობს რეალურად.
- არავითარი საიდუმლო პასუხში. ეს endpoint-ები ავთენტიფიკაციის გარეშე რჩება. კავშირის სტრიქონებს, hostname-ებს და stack trace-ებს იქ ადგილი არ აქვს.
Health check-ები ასევე უზარმაზარ ლოგის ხმაურს წარმოქმნის - ერთი ხაზი ყოველ რამდენიმე წამში, სამუდამოდ, სასურველ ხაზებს ახრჩობს. RE:NODE-ზე კონსოლი web-სერვერის ხმაურს რიცხვის უკან კეცავს, რომლის გამორთვაც შეგიძლია, და ეს ზუსტად ეს პრობლემაა. თუ საკუთარ ლოგინგს წერ, health ბილიკები access ლოგიდან გამორიცხე და არ წაიკითხო მათ გვერდით ავლით. ლოგები, რომლებიც ღირს შენახვა ამ არგუმენტის დანარჩენ ნაწილს შეიცავს.
ვინ მოიხმარს ამათ? პატარა მოწყობაზე ხალხის ვარაუდზე ნაკლები. ღია კოდის nginx-ს მხოლოდ პასიური health checking აქვს - max_fails და fail_timeout upstream-ს ჩავარდნილი მოთხოვნების შემდეგ ცუდად ნიშნავს - და აქტიური polling კომერციული ფუნქციაა. ამიტომ ერთ მანქანაზე სასარგებლო მომხმარებლები არის შენი deploy სკრიპტი (ტრაფიკის გადართვამდე დაელოდე /readyz-ს), შენი uptime მონიტორი და შენ. ესეც ღირს: იხილე მონიტორინგი, რომელიც რაღაცას გეუბნება, რაზე უნდა გაფრთხილდე, და ეს ნამდვილად არ არის ყოველი ჩავარდნილი შემოწმება.
გატესტე, სანამ დაგჭირდება#
არაფრის მუშაობა არ შეიძლება ვარაუდით. დამტკიცებას ოცი წუთი სჭირდება:
- გაუშვი აპლიკაცია და მიაწოდე მას მუდმივი დატვირთვა -
autocannon -c 20 -d 30 http://localhost:3000/ანhey, ნებისმიერი, რომელიც non-2xx პასუხებს აჩვენებს. - შუაში გაგზავნე რეალური სიგნალი:
kill -TERM $(pgrep -f "node dist/server.js"), ან დააჭირე Stop-ს პანელში. - უყურე კონსოლს. უნდა დაინახო შენი handler-ის ლოგის ხაზი, შემდეგ drain ხაზი, შემდეგ პროცესის გასვლა.
- წაიკითხე დატვირთვის ხელსაწყოს შეჯამება. ნულოვანი ჩავარდნილი მოთხოვნა არის გამსვლელი ქულა. რამდენიმე ნიშნავს, რომ drain-ის რიგი არასწორია; ასობით - რომ სიგნალი არასოდეს მოვიდა.
- გაზომე დრო. თუ drain მოცდის პერიოდზე დიდხანს გრძელდება, შეამცირე მკაცრი ვადა, სანამ არ ჩაეტევა.
შემდეგ განზრახ გატეხე: გააჩერე ბაზა და შეამოწმე, რომ /readyz 503-ს აბრუნებს, ხოლო /healthz კვლავ 200-ს. თუ ორივე ვარდება, შენი liveness შემოწმება ზედმეტს აკეთებს და შენ ზემოთ აღწერილი გამაძლიერებელი ააშენე.
ყოველკვირეული დაგეგმილი გადატვირთვა ამ ყველაფრისთვის კარგი იძულებაა. თუ shutdown-ის გზა სწორია, გადატვირთვა უხილავია; თუ არა, ამას შენ შენს გრაფიკზე გაიგებ და არა incident-ის დროს. RE:NODE-ზე Schedules ჩანართი cron გამოსახულებას დალაგებულ ამოცანებზე უშვებს - power action, backup, კონსოლის ბრძანება - ამიტომ ორშაბათს 04:00-ზე გადატვირთვა ერთ წუთში დგება. Restart განრიგები, რომლებიც ეხმარება აღწერს, როდის ღირს ისინი და როდის არის გაჟონვის იგნორირების საშუალება.
ერთი გაფრთხილება restart loop-ებზე. RE:NODE-ზე crash watcher ყოველ ორ წუთში ამოწმებს uptime-ს, რომელიც უკან წავიდა; საათში სამი გადატვირთვა სერვერის გვერდზე გაფრთხილებას სვამს და ticket-ს ხსნის, ექვსი კი სერვერს აჩერებს. შენ მოთხოვნილი გადატვირთვები არ ითვლება. ეს უსაფრთხოების ბადეა პროცესისთვის, რომელსაც ვერ ეშვება, მაგრამ ასევე ნიშნავს, რომ ცუდად გამართულ health check-ს, რომელიც ჯანმრთელ აპლიკაციას კლავს, საბოლოოდ მისი მთლიანად გაჩერება შეუძლია.
FAQ#
რა განსხვავებაა SIGTERM-სა და SIGKILL-ს შორის?
SIGTERM პროცესს გაჩერებას სთხოვს და შეიძლება დაიჭირო, ამიტომ შენი გასუფთავების კოდი სრულდება. SIGKILL-ის დაჭერა, დაბლოკვა ან დამუშავება შეუძლებელია - kernel პროცესს მაშინვე აშორებს. ყველაფერი, რაც container-ს აჩერებს, ჯერ SIGTERM-ს აგზავნის, timeout-ის შემდეგ - SIGKILL-ს.
რამდენ ხანს უნდა გაგრძელდეს graceful shutdown?
იმ მოცდის პერიოდზე ნაკლები, რომელიც მაინც მოგკლავს - ჩვეულებრივ 10-დან 30 წამამდე. დააყენე მკაცრი ვადა მასზე რამდენიმე წამით ადრე და ამოწურვისას აიძულე გასვლა, რომ გაჭედილმა მოთხოვნამ გადატვირთვა killing-ად არ აქციოს.
რატომ არ სრულდება ჩემი shutdown handler?
თითქმის ყოველთვის იმიტომ, რომ სიგნალი wrapper-მა მიიღო. თუ გაშვების ბრძანებაა npm start ან shell სკრიპტი exec-ის გარეშე, შენი აპლიკაცია შვილობილი პროცესია და SIGTERM-ს არასოდეს ხედავს. გაუშვი runtime პირდაპირ.
უნდა ამოწმებდეს ჩემი health check ბაზას?
Readiness-ში - დიახ, მოკლე timeout-ით. Liveness-ში - არა. Liveness შემოწმება, რომელიც ბაზაზეა დამოკიდებული, ბაზის მოკლე პრობლემას ყველა instance-ის ერთდროულ გადატვირთვად აქცევს, რაც პრობლემას ამძიმებს.
რომელი სტატუს კოდი დააბრუნოს არაჯანსაღმა endpoint-მა?
503 Service Unavailable, Retry-After header-ით. 500 დატოვე ნამდვილი შეცდომებისთვის, ხოლო 200 დააბრუნე მხოლოდ მაშინ, როცა პროცესი მართლა მზადაა მომსახურებისთვის.
ეხმარება health check-ები ერთ სერვერზე?
დიახ, თუმცა სხვანაირად. აქ გვერდის ასავლელი არაფერია, ამიტომ readiness უმეტესად შენს deploy სკრიპტს ეუბნება, როდის ამოვიდა ახალი პროცესი, და მონიტორს აძლევს ზუსტ რამეს, რაზეც გაფრთხილება უნდა გააკეთოს. დრენაჟის ნაწილი უფრო მნიშვნელოვანია: სწორედ ის ხდის გადატვირთვებს უფასოს.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.