ყველაფერი, რასაც request-ისთვის მისაღებზე მეტი დრო სჭირდება, request-ში არ უნდა იყოს: წერილის გაგზავნა, სურათის ზომის შეცვლა, ნელი მესამე მხარის API-ის გამოძახება, ანგარიშის გენერირება. სტანდარტული პასუხია რიგი და worker, ხოლო სტანდარტულ რიგს Redis სჭირდება - რომელიც შეიძლება არ გქონდეს, არ გინდოდეს ფულის გადახდა და, რა თქმა უნდა, პირველ დღეს არ გჭირდებოდეს. ბაზას, რომელსაც უკვე უშვებ, შეუძლია რიგი პატარა სერვერის მოცულობებზე სრულიად კარგად შეინახოს, ხოლო ერთი დაგეგმილი ამოცანისთვის cron ჩანაწერი ორივეს სჯობს. ეს პოსტი კიბეა "გააკეთე პასუხის შემდეგ"-იდან "ცალკე worker პროცესამდე", სადაც ყოველი საფეხური ასვლის ღირსია, და იმის აღწერა, რა ჯდება, როცა ერთს არასწორად აირჩევ.
request-ის გარეთ სამუშაოს გაშვების ოთხი გზა#
| მიდგომა | გადაიტანს restart-ს | Retry-ები | კარგია რისთვის |
|---|---|---|---|
| პასუხის შემდეგ, იმავე პროცესში | არა | არა | Fire-and-forget ლოგირება, analytics ping-ები |
Scheduler პროცესის შიგნით (node-cron, APScheduler) | არა | თავად წერ | ერთი აპლიკაციის პროცესი, პერიოდული ამოცანები |
| რიგი შენს ბაზაში, worker იმავე პროცესში | დიახ | დიახ | უმეტესი პატარა აპლიკაცია |
| რიგი პლუს ცალკე worker პროცესი | დიახ | დიახ | CPU-ზე მძიმე სამუშაო, ყველაფერი, რაც საიტს არ უნდა შეანელებდეს |
პირველ გზას სამართლიანი მოსმენა ეკუთვნის, რადგან ზოგჯერ ის მართლაც კარგია:
app.post("/signup", async (req, res) => { const user = await createUser(req.body); res.status(201).json(user); // answer now sendWelcomeEmail(user).catch((error) => log.error(error)); // do this after});Request სწრაფია, მომხმარებელი შექმნილია და წერილი შემდეგ იგზავნება. რაც დათმე, ყველაფერია, რაც რიგს რიგად აქცევს: თუ პროცესი მომდევნო 200 მილიწამში გადაიტვირთება, წერილი არასოდეს გაიგზავნება და ამას არსად არაფერი აფიქსირებს, retry არ არის, როცა mail პროვაიდერი 503-ს აბრუნებს, და ვერ ხედავ, რა რჩება მოლოდინში. მისალმების წერილისთვის ეს მხრების აჩეჩვაა. ინვოისისთვის კი ეს bug-report-ია, რომელსაც სამ კვირაში მიიღებ მტკიცებულების გარეშე.
ხელის წესი: თუ გაგაბრაზებს იმის გაგება, რომ სამუშაო ჩუმად არ შესრულებულა, ის სადმე გამძლე ადგილას უნდა ჩაიწეროს request-ზე პასუხის გაცემამდე. რიგის სასარგებლოდ მთელი არგუმენტი სწორედ ესაა.
რას გაძლევს რიგი სინამდვილეში#
ოთხ რამეს, და ღირს მათი დასახელება, რადგან ისინი რიგის არჩევის კრიტერიუმებია.
- გამძლეობა. Job დისკზეა, სანამ request დაბრუნდება. Restart, deploy ან out-of-memory გაჩერება არაფერს კარგავს.
- Retry-ები backoff-ით. Mail პროვაიდერი ოთხი წუთით გათიშულია; job ცდის ხელახლა 1წმ, 2წმ, 4წმ, 8წმ-ზე და მიდის, ერთხელ ჩავარდნისა და გაქრობის ნაცვლად.
- Backpressure. ათი ათასი job-ის მოსვლა ათი ათას ერთდროულ ოპერაციად არ იქცევა. Worker იმდენს იღებს, რამდენსაც უმკლავდება.
- ხილვადობა. შეგიძლია უპასუხო კითხვაზე "რა ელოდება, რა ჩავარდა, რამდენი ხნისაა ყველაზე ძველი job" - ეს ერთადერთი ფონური სამუშაოს მონიტორინგია, რომელსაც აზრი აქვს.
შენიშნე, რა არ არის სიაში: სიჩქარე. რიგი response-ს სწრაფს ხდის სამუშაოს გადატანით და არა მისი უფრო სწრაფად შესრულებით. და ყოველი რიგი ნაგულისხმევად at-least-once-ია, რასაც შედეგები აქვს და ქვემოთაა განხილული.
რიგები Redis-ის გარეშე#
უმეტესობა Redis-ს იმიტომ იღებს, რომ ბიბლიოთეკა, რომელიც იპოვა, მას მოითხოვს. თუ რელაციურ ბაზას უკვე უშვებ, უკვე გაქვს ყველაფერი, რაც რიგს სჭირდება - გამძლეობა, ტრანზაქციები და PostgreSQL 9.5-დან SQL-ის ის ერთი ნაწილი, რომელიც მას ეფექტურს ხდის:
-- One worker claims one job. SKIP LOCKED means other workers-- step over this row instead of queueing behind the lock.UPDATE jobsSET status = 'running', started_at = now(), attempts = attempts + 1WHERE id = ( SELECT id FROM jobs WHERE status = 'queued' AND run_at <= now() ORDER BY priority DESC, run_at FOR UPDATE SKIP LOCKED LIMIT 1)RETURNING id, kind, payload;SKIP LOCKED-ის გარეშე ოცი worker, რომელიც ერთ ცხრილს ეკითხება, ერთი სტრიქონის lock-ის უკან ლაგდება და შენი რიგის გამტარუნარიანობა ერთია. მასთან ერთად თითოეული worker სხვა job-ს იღებს და პატერნი წუთში ათასობით job-მდე იზრდება არც ისე შთამბეჭდავ ტექნიკაზე. ეს პატარა აპლიკაციის მიერ წარმოებულს ბევრად სცდება.
უფრო დიდი მოგება ტრანზაქციული enqueue-ა. თუ job-ის სტრიქონი იმავე ტრანზაქციაში იწერება, რომელშიც ბიზნეს მონაცემები, შეუძლებელია გქონდეს მომხმარებელი მისალმების წერილის job-ის გარეშე, ან წერილი მომხმარებლისთვის, რომლის შექმნაც rollback-ით გაუქმდა. გარე broker-ს ამას ვერ მოგცემს, და ის "როგორ მოხდა ეს" ტიპის bug-ების მთელ კატეგორიას აქრობს.
ბიბლიოთეკები, რომლებიც ამას სწორად აკეთებენ, რომ SQL თავად არ დაწერო:
| ბიბლიოთეკა | ენა | შენიშვნები |
|---|---|---|
| pg-boss | Node | მხოლოდ Postgres, cron დაგეგმვა ჩაშენებულია |
| graphile-worker | Node | იყენებს LISTEN/NOTIFY-ს, ამიტომ დაყოვნება მილიწამებია და არა poll-ის ინტერვალი |
| procrastinate | Python | Postgres, Django-ს ინტეგრაცია, async მხარდაჭერა |
| Solid Queue | Ruby | ნაგულისხმევი ახალ Rails-ში |
Laravel database driver | PHP | უკვე framework-შია, ერთი კონფიგურაციის ხაზი |
კომპრომისები რეალურია და პატარა: polling მცირე მუდმივ დატვირთვას ამატებს (გამოიყენე LISTEN/NOTIFY ან ერთწამიანი poll და არა 50 მილიწამიანი), job-ების ცხრილი იზრდება, თუ დასრულებულ სტრიქონებს არასოდეს წაშლი, და მკვდარი სტრიქონები vacuum-ს საჭიროებს - Postgres vacuum და bloat განმარტავს, რატომ არის ხშირად ცვალებადი ცხრილი კლასიკური შემთხვევა. დასრულებული job-ები დღის შემდეგ წაშალე; ჩავარდნილები უფრო დიდხანს შეინახე.
ერთ მანქანაზე ნამდვილად დაბალი მოცულობებისთვის SQLite იმავე პატერნით მუშაობს. ჩართე WAL რეჟიმი (PRAGMA journal_mode=WAL) და დააყენე busy_timeout, რომ პარალელურმა ჩამწერებმა შეცდომის ნაცვლად დაელოდონ. ეს ერთი ფაილია და გასაშვები სერვისი არ სჭირდება, ჭერი კი მას ბევრად მაღლა აქვს, ვიდრე ის რამდენიმე job წუთში, რასაც უმეტესი სათამაშო პროექტი აწარმოებს.
ერთი რამ გასაგები უნდა იყოს: RE:NODE Redis-ს არ ყიდის. აქ database hosting არის PostgreSQL და MongoDB, ხოლო ბაზის სლოტები, რომლებიც app და web გეგმებშია ჩართული, პანელში იქმნება გენერირებული host-ით, მომხმარებლითა და პაროლით. ამიტომ ზემოთ აღწერილი Postgres-ზე დაფუძნებული გზა ის არის, რომელიც იმასთან მუშაობს, რისი ყიდვაც შეგიძლია, და ამ ზომაზე ის ნამდვილად სწორი ნაგულისხმევია. Redis და გჭირდება თუ არა ის უკვე იმავე არგუმენტს მეორე მხრიდან წარმოადგენს.
საკუთარი Redis-ის მოყვანა#
ზოგჯერ მაინც გინდა Redis-ის ეკოსისტემა - BullMQ-ის დაგვიანებული და განმეორებადი job-ები კარგია, Celery Python-ის სამყაროს ნაგულისხმევია, და არსებული კოდის ბაზა შეიძლება უბრალოდ ამას ვარაუდობდეს. გაქვს ორი პატიოსანი ვარიანტი: იქირავე Redis პროვაიდერისგან, რომელიც მას ყიდის, ან თავად გაუშვი მანქანაზე, რომელსაც აკონტროლებ. VDS ამას კომფორტულად გაართმევს თავს; RE:NODE-ზე ისინი ხელით მზადდება და 24 საათში გადმოგეცემა და არა ერთ წუთში, ამიტომ შეუკვეთე იმ შაბათ-კვირამდე, როცა გამოყენებას გეგმავ.
თუ საკუთარს უშვებ, სამი პარამეტრი წყვეტს, ის რიგია თუ ქეში, რომელიც რიგად ტყუის თავს:
maxmemory-policy noevictionappendonly yesappendfsync everysecრატომ იმსახურებს თითოეული ადგილს, პლუს წესი, რომელიც კონფიგურაციის ხაზი არ არის:
- `noeviction` სავალდებულოა BullMQ-სთვის და ძალიან კარგი იდეაა ყველაფრისთვის. ნაგულისხმევი eviction პოლიტიკები გასაღებებს შლიან, როცა მეხსიერება ივსება - რაც რიგში ჩუმად job-ების წაშლას ნიშნავს.
- `appendonly yes` გაძლევს მხოლოდ დამატებადი ლოგს. მხოლოდ snapshot-ებით ყველა job იკარგება ბოლო შენახვის შემდეგ, როცა პროცესი კვდება.
- მიაბი localhost-ს ან კერძო მისამართს და დააყენე პაროლი. ინტერნეტისთვის ღია Redis საათებში იპოვება და ტრივიალურად გამოიყენება ბრძანებების გასაშვებად.
მინიმალური BullMQ worker პარამეტრებით, რომლებსაც მნიშვნელობა აქვს:
import { Worker, Queue } from "bullmq";const connection = { host: "127.0.0.1", port: 6379 };export const emails = new Queue("emails", { connection });new Worker("emails", async (job) => { await sendEmail(job.data);}, { connection, concurrency: 2, // a hard ceiling, not a suggestion removeOnComplete: { count: 1000 }, // or Redis grows forever removeOnFail: { age: 7 * 24 * 3600 },});await emails.add("welcome", { userId: 42 }, { attempts: 5, backoff: { type: "exponential", delay: 1000 }, jobId: `welcome:42`, // dedupe: same id, one job});removeOnComplete ის ხაზია, რომელსაც ხალხი მას შემდეგ აღმოაჩენს, რაც მათი Redis ივსება. დასრულებული job-ები ნაგულისხმევად ინახება, რომ გამოიკვლიო, და პატარა instance-ზე ეს კვირაში მეხსიერების ლიმიტი ხდება.
Python-ისთვის Celery-ს ნამდვილი broker სჭირდება: Redis ან RabbitMQ. ძველ ვერსიებში არსებული ბაზაზე დაფუძნებული transport-ები Celery 5-ში მხარდაჭერილი არ არის, ამიტომ "Celery broker-ის გარეშე" არ არსებობს - თუ broker არ გაქვს, გამოიყენე procrastinate ან APScheduler ბაზაზე დაფუძნებული job store-ით. პატარა მანქანისთვის მორგებული Celery worker:
$ celery -A myapp worker --loglevel=info \ --concurrency=2 --prefetch-multiplier=1 \ --max-tasks-per-child=200 --time-limit=600 --soft-time-limit=540--prefetch-multiplier=1 worker-ს უშლის ხელს, აიღოს job-ების გროვა, რომლებიც არ დაუწყია, რაც restart-ების დროს მნიშვნელოვანია. --max-tasks-per-child პერიოდულად ახალ child პროცესს ქმნის და ნელი მეხსიერების გაჟონვის ყველაზე იაფი პასუხია.
დაგეგმვა: cron, beat და Schedules ჩანართი#
რიგები სამუშაოს იმ დროს ასრულებენ, როცა მოითხოვენ. ვიღაცამ მაინც უნდა მოითხოვოს გრაფიკით.
პროცესის შიგნით scheduler-ები - node-cron, APScheduler, setInterval - ყველაზე მარტივი რამაა, რაც მუშაობს, და მათ ერთი ჩავარდნის რეჟიმი აქვთ: ორი პროცესი ორ გაშვებას ნიშნავს. თუ ოდესმე მეორე instance-ს გაუშვებ, ღამის ანგარიში ორჯერ გენერირდება და ორჯერ იგზავნება. დაიცავი lock-ით და ნუ ენდობი საკუთარ თავს:
-- Returns true for exactly one caller; released when the session ends.SELECT pg_try_advisory_lock(hashtext('nightly-report'));Celery beat იგივე იდეაა ცალკე პროცესად. გაუშვი ზუსტად ერთი beat, ყოველთვის. ორი beat დუბლირებულ გრაფიკებს იძლევა, და რადგან beat მხოლოდ რიგში სვამს (worker-ები ასრულებენ), დუბლირება უხილავია, სანამ ვინმე ლოგს არ წაიკითხავს.
პანელის Schedules ჩანართი ის ვარიანტია, რომელსაც კონტეინერზე დაფუძნებულ ჰოსტზე ხალხი ამჩნევს, სადაც სისტემური crontab-ის ჩასასწორებლად არ არის. ის იღებს cron გამოსახულებას და უშვებს დალაგებულ ამოცანებს მათ შორის დაყოვნებებით: კონსოლის ბრძანება, backup ან power action. RE:NODE-ზე ეს ყველა გეგმაზეა ხელმისაწვდომი. ორ რამეს ის ძალიან კარგად აკეთებს:
- ღამის backup, რომელსაც ორი წუთის შემდეგ restart მოსდევს. დაგვიანებით დალაგებული ამოცანები ზუსტად ამისთვის არის შესაფერისი პრიმიტივი.
- შენი საკუთარი job-ის გაშვება. კონსოლის ბრძანება იწერება შენი აპლიკაციის standard input-ში, ამიტომ stdin-ის დამუშავების რამდენიმე ხაზი Schedules ჩანართს scheduler-ად აქცევს, რომელსაც შენი აპლიკაცია ემორჩილება:
process.stdin.on("data", (chunk) => { for (const line of chunk.toString().split("\n")) { if (line.trim() === "jobs:nightly") runNightly().catch((e) => console.error(e)); }});სანამ დაეყრდნობი, რომ "03:00" ნიშნავს 03:00-ს იქ, სადაც შენ ცხოვრობ, შეამოწმე, რომელ დროის სარტყელში ინტერპრეტირდება გრაფიკი, და გახსოვდეს, რომ cron გამოსახულებას ხუთი ველი აქვს და წამები არ არის - cron გამოსახულებები ახსნილი სინტაქსს მოიცავს, ხოლო დაგეგმილი ამოცანები, რომლებიც ღირს - რომელი მათგანი იმსახურებს ადგილს.
worker-ის ზომა ერთ მანქანაზე#
შეზღუდვა პატარა გეგმაზე job-ების გამტარუნარიანობა არ არის; ის მეხსიერება და CPU-ა, რომელსაც worker მომხმარებლებისთვის მომსახურე რამეს ართმევს.
| Runtime | მიახლოებითი resident მეხსიერება პროცესზე |
|---|---|
| Node worker, მოკრძალებული dependency ხე | 40-80 MB |
| Python worker Django-თი ან SQLAlchemy-ით | 80-150 MB |
| თითოეული Celery prefork child | დაახლოებით იგივე კიდევ |
| Redis რამდენიმე ათას პატარა job-ზე | ათობით MB |
--concurrency=4 Python worker-ზე ოთხი child პროცესია, ამიტომ 1 GB გეგმა, რომელიც web სერვერსა და ამ worker-ს უშვებს, უკვე მჭიდროა. დაიწყე concurrency 1-ით ან 2-ით და გაზარდე მხოლოდ მაშინ, როცა ყველაზე ძველი job-ის ასაკი გეუბნება, რომ საჭიროა.
CPU უფრო მკაცრია, რადგან RE:NODE-ზე CPU წილი მკაცრი შეზღუდვაა და არა სამიზნე - სერვერი, რომელიც 100%-ზე დგას, ნელია და არა გატეხილი, და ამის გამო არასოდეს ჩერდება, მაგრამ ყველაფერი ამ კონტეინერში ერთ ჭერს იყოფს. Worker, რომელიც სურათებს სრული სიჩქარით გარდაქმნის, შენს web request-ებს ანელებს, და პროცესის ვერცერთი პრიორიტეტი ჯამს ვერ შეცვლის. ორი გამოსავალი:
- შეზღუდე worker განზრახ. Concurrency 1 და პატარა პაუზა job-ებს შორის ხშირად საკმარისია, რომ საიტი სწრაფად დარჩეს, სანამ დაგროვება ოდნავ უფრო ნელა იცლება.
- გადაიტანე worker საკუთარ სერვერზე. მეორე app გეგმა საკუთარი კონტეინერია საკუთარი მეხსიერებითა და CPU წილით, ამიტომ მძიმე job-ები web პროცესს ვერ დაშიმშილებენ. ორივე სერვერი ერთსა და იმავე PostgreSQL გეგმას უკავშირდება, რომელიც საკუთარ host-სა და port-ზეა მისაწვდომი, ამიტომ რიგი რაიმე ეშმაკობის გარეშე ზიარია.
მეხსიერება ის ადგილია, სადაც worker ყველაზე მძიმედ ვარდება. ლიმიტის მიღწევა კონტეინერს ჩერებს და swap-ის ნაცვლად სუფთად იტვირთება - ამიტომ მიმდინარე job შუა ჩაწერაში კვდება. რაც მიგვიყვანს იმ ნაწილამდე, რომელიც წყვეტს, აქვს თუ არა ამას მნიშვნელობა.
job-ები, რომლებიც ორჯერ სრულდება, და job-ები, რომლებიც არასოდეს#
ყოველი რიგი, რომელიც გამოყენებად ღირს, at-least-once-ია. Worker, რომელიც job-ს იღებს და დადასტურებამდე კვდება, იმას ნიშნავს, რომ job ხელახლა მიეწოდება, რადგან ალტერნატივა - ჯერ დადასტურება - job-ებს კარგავს. დუბლიკატები არის ფასი იმისა, რომ სამუშაო არასოდეს დაიკარგოს, და გადახდის გზა იდემპოტენტურობაა.
- გახადე ოპერაცია გამეორებისთვის უსაფრთხო.
INSERT ... ON CONFLICT DO NOTHINGუნიკალურ გასაღებზე,sent_atსვეტი, რომელიც გაგზავნამდე მოწმდება, პროვაიდერის მხარეს idempotency გასაღები გადახდის გამოძახებაზე. - გამოიყენე დეტერმინისტული job id. BullMQ-ს
jobId, უნიკალური შეზღუდვა(kind, entity_id)-ზე შენს ცხრილში. ორჯერ დამატებული იგივე job ერთი job ხდება. - დაადასტურე გვიან და სერიოზულად. მონიშნე job შესრულებულად გვერდითი ეფექტის შემდეგ და არა მანამდე.
- შეზღუდე ცდები.
attempts: 5ექსპონენციალური backoff-ით, შემდეგ გადაიტანე dead-letter მდგომარეობაში, სადაც ადამიანს შეუძლია ნახოს. Job, რომელიც უსასრულოდ იმეორებს მუდმივ შეცდომას, bug-ის დამალვისა და CPU-ს დაწვის ერთდროული გზაა. - დააყენე დროის ლიმიტი. Celery-ს აქვს
--time-limitდა--soft-time-limit; BullMQ-ს საკუთარი per-job მკაცრი timeout არ აქვს, ამიტომ handler შეფუთეPromise.race-ში ტაიმერით. Job, რომელიც timeout-ის გარეშე socket-ზე გაჭედა, worker-ის სლოტს განუსაზღვრელი ხნით იკავებს, და ასე წყვეტს რიგი მოძრაობას მაშინ, როცა ყველა dashboard ამბობს, რომ worker ცოცხალია.
Restart-ებს საკუთარი დამუშავება სჭირდება. SIGTERM-ზე შეწყვიტე ახალი job-ების აღება, დაამთავრე მიმდინარე, თუ grace პერიოდში ეტევა, და გამოდი - await worker.close() BullMQ-ში, warm shutdown Celery-ში. Graceful shutdown და health check-ები სრულ პატერნს შეიცავს, იმის ჩათვლით, რატომ სჭირდება გრძელ job-ს checkpoint-ები და არა უფრო გრძელი timeout.
რას უყურო#
რიგის მონიტორინგს ერთი მეტრიკა აქვს, რომელსაც მნიშვნელობა აქვს, და რამდენიმე, რომლებიც ისე გამოიყურება, თითქოს აქვთ.
- ყველაზე ძველი მოლოდინში მყოფი job-ის ასაკი. ეს არის ის. თუ ყველაზე ძველი რიგში მდგარი job ოთხი საათისაა, რიგი გატეხილია, სხვა რიცხვები რასაც არ უნდა ამბობდნენ. ააწყე alert მასზე.
- რიგის სიღრმე მარტო ცუდი alert-ია: 5,000 სწრაფად დაცლადი job კარგადაა, 12 სამუდამოდ გაჭედილი - არა.
- ჩავარდნების მაჩვენებელი job-ის ტიპის მიხედვით. ერთი ტიპის ჩავარდნა bug-ია; ყველაფრის ჩავარდნა dependency-ია.
- Worker heartbeat. Worker, რომელიც ჩუმად გავიდა, რიგს ტოვებს, რომელიც ჩუმად ივსება, და ეს ფონური job-ების ყველაზე გავრცელებული გათიშვაა.
გამოიტანე ეს მაჩვენებლები რიცხვებად იმავე ადგილიდან, საიდანაც შენი აპლიკაცია health-ს აცხადებს, და alerting მოსაწყენი გქონდეს - იხილე მონიტორინგი, რომელიც რამეს გეუბნება. თუ რიგი შენს ბაზაში ცხოვრობს, ოთხივე ერთი SQL query-ა, რაც კიდევ ერთი არგუმენტია მის იქ დასაყენებლად. უბრალოდ თვალი ადევნე კავშირების რაოდენობას: worker-ის pool და web pool ერთსა და იმავე პატარა ბაზაზე ჯამდება, და connection pool-ები და ლიმიტები განმარტავს, რა ხდება, როცა ისინი იმაზე მეტს აჭარბებენ, რასაც სერვერი მიიღებს.
FAQ#
მჭირდება Redis background job-ებისთვის?
არა. PostgreSQL ცხრილი SELECT ... FOR UPDATE SKIP LOCKED-ით პატარა აპლიკაციის მიერ წარმოებულზე გაცილებით მეტ გამტარუნარიანობას ართმევს თავს და საშუალებას გაძლევს, job იმავე ტრანზაქციაში დაამატო, რომელშიც მისი გამომწვევი მონაცემები. Redis ღირს დამატებად, როცა კონკრეტული ბიბლიოთეკის შესაძლებლობები გინდა და არა ნაგულისხმევად.
შემიძლია worker იმავე პროცესში გავუშვა, რომელშიც ჩემი web აპლიკაცია?
დიახ, და პატარა სერვერზე ეს ხშირად სწორი არჩევანია - ერთი პროცესი, ერთი deploy, ნაკლები, რაც არასწორად წავა. გაიტანე გარეთ, როცა job-ები საკმარისად CPU-ზე მძიმეა, რომ request-ები შეანელოს, ან როცა ერთის გადატვირთვა მეორის გარეშე გინდა.
რატომ გაეშვა ჩემი job ორჯერ?
იმიტომ, რომ რიგები at-least-once-ია. Worker, რომელიც job-ის დაწყების შემდეგ, მაგრამ დადასტურებამდე ჩამოვარდება ან მოკლავენ, ხელახალ მიწოდებას იწვევს. გახადე სამუშაო იდემპოტენტური, იმის ნაცვლად რომ ეცადო, მიწოდება exactly-once გახადო, რასაც არცერთი რიგი რეალურად არ იძლევა.
როგორ გავუშვა დაგეგმილი ამოცანა ჰოსტზე crontab-ის გარეშე?
გამოიყენე პანელის Schedules ჩანართი cron გამოსახულებით, რომელსაც შეუძლია კონსოლის ბრძანების გაგზავნა, backup-ის აღება ან სერვერის restart. თუ ამოცანა შენს აპლიკაციას ეკუთვნის, ააკითხე აპლიკაციას ხაზი standard input-იდან და გაუშვი job, როცა ის მას დაინახავს.
რამდენი worker გავუშვა?
დაიწყე ერთით, concurrency-ით ერთი ან ორი. გაზარდე მხოლოდ მაშინ, როცა ყველაზე ძველი job-ის ასაკი იზრდება, და გაჩერდი, როცა მეხსიერება ან CPU წილია შემზღუდველი. worker-ები იმაზე მეტი, ვიდრე CPU წილი გაქვს, უბრალოდ ყოველ job-ს ანელებს.
რა ემართება მიმდინარე job-ს, თუ სერვერი გადაიტვირთება?
გვიანი დადასტურებისას ის რიგში ბრუნდება და ისევ სრულდება - ამიტომ გამეორებისთვის უსაფრთხო უნდა იყოს. გვიანი დადასტურების გარეშე ის იკარგება. შეამოწმე ეს განზრახ: მოკალი worker job-ის შუაში და დარწმუნდი, რომ შედეგი ამ ორიდან ერთია და არა ნახევრად დასრულებული ჩაწერა.




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