Django-ში Celery სამი ნაწილის მიერთებაა და ოთხი პარამეტრი, რომელიც ნაგულისხმევ მნიშვნელობაზე არ უნდა დატოვო. მიერთება ასეთია: celery.py settings.py-ის გვერდით, import-ი __init__.py-ში და CELERY_ პრეფიქსიანი პარამეტრები, რომლებიც Django-ს საკუთარი settings მოდულიდან იკითხება. პარამეტრები კი ესენია: acks_late, prefetch-ის მამრავლი, visibility timeout და მეხსიერების ზღვარი თითოეული შვილობილი პროცესისთვის. Valkey არის broker, და რადგან ის Redis-ის პროტოკოლზე მუშაობს, Celery-ს მასზე ჩვეულებრივი redis:// URL-ით მიუთითებ.
პატარა გეგმაზე პრობლემა კონფიგურაცია კი არა, არითმეტიკაა. Celery worker არის შენი Django პროექტის ასლი, რომელიც ერთხელ მშობელი პროცესისთვის მეორდება და ერთხელ - თითოეული შვილობილისთვის, და გვერდით დგას gunicorn, რომელიც იმავე პროექტის კიდევ ერთი გამეორებაა. ამ სტატიაში გავაკეთებთ broker-სა და beat-ის განრიგს, შემდეგ კი უმეტეს ადგილს დავუთმობთ კითხვას, რამდენი პროცესი ეტევა შენ მიერ ნაყიდ მეხსიერებაში და რა ვქნათ, როცა აღარ ეტევა. ვვარაუდობ, რომ პროექტი უკვე დაყენებულია Django-ს production-ზე გაშვების gunicorn-ით სტატიაში აღწერილი წესით, და აქ არ გავიმეორებ ბიბლიოთეკების შედარებას, რომელიც Valkey-ზე დავალებების რიგებში არის.
რას აშენებ#
სამი სახის პროცესი ერთ Valkey-ს იყენებს. ვებ პროცესი (gunicorn) დავალებას რიგში სვამს და პასუხს აბრუნებს. ერთი ან რამდენიმე worker დავალებებს რიგიდან იღებს და ასრულებს. ერთი beat პროცესი, ზუსტად ერთი, პერიოდულ დავალებებს საათის მიხედვით რიგში ამატებს. შეტყობინებებს შუაში Valkey ინახავს.
ამ სურათში ორი რამ განსაზღვრავს ყველაფერს, რასაც ქვემოთ წაიკითხავ. Beat დავალებებს არ ასრულებს, მხოლოდ აგზავნის, ამიტომ ძალიან პატარაა. ხოლო worker-ს საკუთარი ბაზასთან კავშირები სჭირდება, რადგან ის შენს მოდელებს import-ს უკეთებს და ბაზასთან ზუსტად ისევე მუშაობს, როგორც ვებ პროცესი. როგორ ჯამდება ეს კავშირები, კავშირების pool-ები და ლიმიტები სტატიაშია.
Celery-ის დაყენება Django-სთვის#
დააყენე Celery Redis transport-ის extra-თი, რომელიც ისეთ client ბიბლიოთეკას მოიყვანს, რომელიც Valkey-თანაც მუშაობს:
$ pip install "celery[redis]"ვერსია requirements.txt-ში დააფიქსირე, ისევე როგორც ყველაფერი დანარჩენი. Celery-ის კონფიგურაციის გასაღებების სახელები 4.x სერიაში შეიცვალა, ხოლო რამდენიმე ნაგულისხმევი მნიშვნელობა 5.x რელიზებს შორის გადაინაცვლა, ამიტომ მინორულ ვერსიებზე მცურავი დიაპაზონი ზუსტად ის გზაა, რომლითაც ღამის სამ საათზე restart-ი ქცევას ცვლის. lock ფაილებზე Python-ის requirements და virtualenv-ები სტატიაში წაიკითხავ.
Django-სთან ინტეგრაცია სამი ფაილია. ჯერ Celery აპლიკაცია, settings.py-ის მქონე იმავე package-ში:
import osfrom celery import Celeryos.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings.production")app = Celery("myproject")app.config_from_object("django.conf:settings", namespace="CELERY")app.autodiscover_tasks()შემდეგ დარწმუნდი, რომ ის Django-ს გაშვებისას import-დება, რათა @shared_task დეკორატორები ამ აპლიკაციას მიება:
from .celery import app as celery_app__all__ = ("celery_app",)namespace="CELERY" ნიშნავს, რომ settings.py-ში Celery-ის ყველა პარამეტრს აქვს CELERY_ პრეფიქსი და იწერება ზედა რეგისტრით: Celery-ის პარამეტრი task_acks_late ხდება CELERY_TASK_ACKS_LATE. autodiscover_tasks() INSTALLED_APPS-ის ყველა აპლიკაციიდან tasks.py მოდულს import-ს უკეთებს. ნებისმიერ სხვა ადგილას მდებარე დავალება არასოდეს რეგისტრირდება, და როცა მის შეტყობინებას worker მიიღებს, ლოგში Received unregistered task გამოჩნდება.
დავალებები აპლიკაციის შიგნით tasks.py-ში წერე, shared_task-ით, რათა აპლიკაციამ Celery-ის ეგზემპლარი არ გააკეთოს import-ად:
from celery import shared_taskfrom .models import Order@shared_task( autoretry_for=(ConnectionError, TimeoutError), retry_backoff=True, retry_backoff_max=600, retry_jitter=True, max_retries=5,)def send_receipt(order_id): order = Order.objects.get(pk=order_id) order.send_receipt_email()გადაეცი primary key-ი და არა მოდელის ობიექტი. მთელი ობიექტის მატარებელი დავალება მუშაობს იმ მდგომარეობით, რომელიც ობიექტს შეტყობინების ჩაწერისას ჰქონდა, და ეს შეიძლება ერთი საათის წინანდელი იყოს, თუ რიგი გადატვირთულია.
broker-ის პარამეტრები#
კავშირისა და ქცევის პარამეტრები Django-ს settings მოდულში შედის და გარემოდან იკითხება, რომ პაროლი repository-ში არ აღმოჩნდეს:
import osCELERY_BROKER_URL = os.environ["VALKEY_URL"] # redis://:password@host:port/0CELERY_RESULT_BACKEND = os.environ["VALKEY_URL"].replace("/0", "/1")CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = TrueCELERY_TASK_ACKS_LATE = TrueCELERY_TASK_REJECT_ON_WORKER_LOST = TrueCELERY_WORKER_PREFETCH_MULTIPLIER = 1CELERY_TASK_TIME_LIMIT = 300CELERY_TASK_SOFT_TIME_LIMIT = 240CELERY_RESULT_EXPIRES = 3600CELERY_TASK_IGNORE_RESULT = TrueCELERY_BROKER_TRANSPORT_OPTIONS = { "visibility_timeout": 3600, "global_keyprefix": "shop:",}CELERY_TIMEZONE = "UTC"თითოეული მათგანი რაღაცისთვის არის.
| პარამეტრი | მნიშვნელობა | რატომ |
|---|---|---|
CELERY_BROKER_URL | redis://:pw@host:port/0 | ცარიელი მომხმარებლის სახელი განზრახ არის: Valkey-ს ნაგულისხმევი მომხმარებელი მხოლოდ პაროლს იღებს |
CELERY_TASK_ACKS_LATE | True | დადასტურება დავალების დასრულების შემდეგ, და არა დაწყებისას |
CELERY_WORKER_PREFETCH_MULTIPLIER | 1 | შვილობილი პროცესი ერთ დაჯავშნილ შეტყობინებას ინახავს, და არა ოთხს |
CELERY_TASK_TIME_LIMIT | წამები | გაჭედილი დავალების იძულებითი შეწყვეტა |
CELERY_RESULT_EXPIRES | 3600 | შედეგები ვადაგასული ხდება და გასაღებებად არ გროვდება |
CELERY_TASK_IGNORE_RESULT | True | არაფერი ინახება, თუ დავალება თვითონ არ მოითხოვს |
URL მოდის შენი Valkey გეგმის კავშირის მონაცემებიდან: host, port და გენერირებული პაროლი, რომელთაგან არც ერთი ლოკალური ინსტალაციის ნაგულისხმევი არ არის. თუ პაროლში არის ისეთი სიმბოლოები, როგორიცაა @, / ან #, URL-ში percent-encoding გაუკეთე (@ ხდება %40), თორემ client სტრიქონს არასწორ ადგილას გაყოფს და გაუგებარ host-ის შეცდომას დაწერს. Python-ის urllib.parse.quote ამას აკეთებს.
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP restart-ისას მნიშვნელოვანია. მის გარეშე worker, რომელიც broker-ის ხელმისაწვდომობამდე რამდენიმე წამით ადრე ირთვება, ჩერდება, და Celery 5.x-ის ახალი რელიზები deprecation გაფრთხილებას ბეჭდავს, რომელიც ამ პარამეტრის დაყენებას გირჩევს. ჰოსტინგის პანელი deploy-ის შემდეგ ყველაფერს ერთდროულად ხელახლა უშვებს, ამიტომ ვინ ვის უსწრებს, შენს ხელში არ არის.
URL-ში დახრილი ხაზის შემდეგ მდგარი ბაზის ნომერი ერთი და იმავე Valkey-ს შიგნით ლოგიკური ბაზაა. შედეგების 1 ბაზაში ჩადებით ისინი რიგის გასაღებების სივრცეს გამოეყოფა, ამიტომ ერთის შემოწმება ან გასუფთავება მეორეს არ შეეხება. თუ დავალებების დაბრუნებულ მნიშვნელობებს არასოდეს კითხულობ, CELERY_TASK_IGNORE_RESULT = True და result backend-ის სრული არარსებობა ყველაზე იაფი ვარიანტია და ნარჩენი გასაღებების მთელ კლასს აქრობს.
Acks, prefetch და visibility timeout#
Redis transport-ს საკუთარი დადასტურების მექანიზმი არ აქვს, ამიტომ Celery თავისას აშენებს. worker-ის მიერ აღებული შეტყობინება გადადის დამალულ სტრუქტურაში, სახელად unacked. თუ worker visibility timeout-ის ფარგლებში დაადასტურებს, შეტყობინება იშლება; თუ არა, შეტყობინება რიგში ბრუნდება და მას სხვა worker გაუშვებს.
აქედან სამი წესი გამომდინარეობს.
- Visibility timeout შენს ყველაზე ხანგრძლივ დავალებაზე, მისი retry-ების countdown-ებითურთ, მეტი უნდა იყოს. ნაგულისხმევი ერთი საათია. ოთხმოცდაათი წუთის განმავლობაში მომუშავე დავალება ნაგულისხმევ მნიშვნელობაზე მეორედ მიეწოდება, სანამ პირველი ჯერ კიდევ მუშაობს. Redis-ის პროტოკოლიან broker-ზე "ჩემი დავალება ორჯერ გაეშვა" ყველაზე ხშირად სწორედ ამის გამო ხდება. გაითვალისწინე, რომ
ETAდაcountdownდავალებებს ვადის დადგომამდე worker ინახავს და არა Valkey, ამიტომ ხვალისთვის დაგეგმილი დავალება ერთსაათიანი timeout-ის დროს ყოველ საათში თავიდან მიეწოდება. მოკლე timeout და გრძელი countdown ერთმანეთს არ ეწყობა; შორეული მომავლის სამუშაო beat-ით დაგეგმე. - `acks_late`-ის დროს დავალება შეიძლება ერთზე მეტჯერ გაეშვას. თუ worker დავალების შუაში მოკვდა, შეტყობინება ბრუნდება და დავალება თავიდან იწყება. გააკეთე დავალებები idempotent: მოვლენის id-ზე unique constraint ან status სვეტი, რომელიც ჯერ მოწმდება.
CELERY_TASK_REJECT_ON_WORKER_LOSTამას ავრცელებს იმ შვილობილ პროცესზეც, რომელიც ბირთვმა მეხსიერების გამო მოკლა, და პატარა გეგმაზე სწორედ ეს შემთხვევაა მნიშვნელოვანი. - Prefetch 1 გამტარუნარიანობას სამართლიანობაში ცვლის. ნაგულისხმევი მამრავლი 4 ნიშნავს, რომ თითოეული შვილობილი პროცესი ოთხ შეტყობინებას წინასწარ იჯავშნის. თუ ერთი მათგანი ათწუთიანი ანგარიშია, მის უკან მდგარი სამი ელოდება, მაშინაც კი, თუ სხვა შვილობილები უქმად დგანან. სხვადასხვა ხანგრძლივობის დავალებებისთვის სწორია
1; ათასობით პატარა ერთნაირი დავალებისთვის ნაგულისხმევი უფრო სწრაფია.
acks_late-ს reject_on_worker_lost-ის გარეშე კიდევ ერთი მახე აქვს: თუ შვილობილ პროცესს out-of-memory killer მოკლავს, ზოგ ვერსიაში შეტყობინება retry-ის ნაცვლად წარუმატებლად დადასტურდება. ორივეს დაყენება აქცევს მეხსიერების გამო მოკვლას retry-ად.
რიგში ჩასმა ტრანზაქციის commit-ის შემდეგ გააკეთე და არა მანამდე. ბაზის ტრანზაქციაში გახვეულ request-ში delay()-ით ჩასმული დავალება worker-ამდე იმ ჩანაწერზე ადრე შეიძლება მივიდეს, რომელსაც ეხება, და DoesNotExist შეცდომით დასრულდეს:
from django.db import transactiondef create_order(request): order = Order.objects.create(...) transaction.on_commit(lambda: send_receipt.delay(order.pk))on_commit callback-ს გარეთა ტრანზაქციის commit-ის შემდეგ უშვებს და rollback-ის შემთხვევაში აბათილებს, რაც სწორედ ის არის, რაც გჭირდება დავალებისთვის, რომელიც მონაცემებით გამოწვეულ სამუშაოს აღწერს.
Beat: პერიოდული განრიგი#
Beat განრიგს კითხულობს და დავალებებს დადგომის დროს აგზავნის. ეს ცალკე პროცესია, და ერთ განრიგზე ზუსტად ერთი უნდა მუშაობდეს. ორი beat ნიშნავს, რომ ყველა პერიოდული დავალება ორჯერ გაიგზავნება. Celery-ის გაზრდის შესახებ ეს ყველაზე მნიშვნელოვანი ფაქტია: worker-ებს თავისუფლად ამატებ, მაგრამ მეორე beat-ს არასოდეს.
ყველაზე მარტივი განრიგი settings-ში ცხოვრობს:
from celery.schedules import crontabCELERY_BEAT_SCHEDULE = { "clear-expired-sessions": { "task": "accounts.tasks.clear_sessions", "schedule": crontab(hour=3, minute=30), }, "daily-digest": { "task": "notifications.tasks.send_digest", "schedule": crontab(hour=7, minute=0, day_of_week="mon-fri"), "options": {"expires": 3600}, }, "sync-stock": { "task": "shop.tasks.sync_stock", "schedule": 300.0, "args": (), },}უბრალო რიცხვი არის ინტერვალი წამებში, beat-ის გაშვების მომენტიდან გაზომილი. crontab კედლის საათის დროს ირთვება და CELERY_TIMEZONE-ს იყენებს; დააყენე ის პირდაპირ, რადგან ნაგულისხმევი UTC-ა და "07:00-ზე" დაგეგმილი digest სხვაგვარად შენთან 07:00-ზე არ მოვა. თუ cron-ის სინტაქსი გახსოვს ბუნდოვნად, cron გამოსახულებები განმარტებით არის საცნობარო, იმ დათქმით, რომ Celery-ის crontab() ხუთნაწილიანი სტრიქონის ნაცვლად სახელდებულ ველებს იღებს.
expires პარამეტრი გადასაღებად ღირს. თუ worker-ები ერთი საათით გაითიშა, ყოველ ხუთ წუთში გაშვებული განრიგი თორმეტ ერთნაირ sync_stock შეტყობინებას დატოვებს მოლოდინში, და worker-ების დაბრუნებისას ისინი ყველა ერთბაშად გაეშვება. expires-ით მოძველებულები წაუკითხავად გადაიყრება.
Beat ცალკე პროცესად გაუშვი:
$ celery -A myproject beat --loglevel=INFO -s /tmp/celerybeat-schedule-s განსაზღვრავს, სად ინახავს beat celerybeat-schedule-ს, პატარა ფაილს, რომელიც ჩაწერს, თითოეული ჩანაწერი ბოლოს როდის გაეშვა. ნაგულისხმევად ის სამუშაო დირექტორიაში იწერება. Beat-ს restart-ზე მისი დაკარგვა არ აწუხებს; ის მაშინ საათიდან გაარკვევს, რა არის დადგომილი, რაც crontab ჩანაწერებისთვის სწორია, ინტერვალიანებისთვის კი ნიშნავს "დათვლა თავიდან დაიწყე". დავალება, რომელიც დღეში ზუსტად ერთხელ უნდა გაეშვას, crontab ჩანაწერი უნდა იყოს და არა 86400-წამიანი ინტერვალი.
შეხვდები რჩევას, beat worker-ის შიგნით გაუშვა celery worker -B-ით. ის ერთ პროცესს ზოგავს და ერთი worker-ისთვის პატარა სერვერზე მისაღებია, მაგრამ სწორედ ამის გამო აღმოჩნდებიან ადამიანები ორი beat-ით იმ დღეს, როცა მეორე worker-ს დაამატებენ. გამოიყენე ის შეგნებულად: --concurrency პირდაპირ მიუთითე და worker-ების რაოდენობა ერთზე დააფიქსირე.
განრიგებისთვის, რომლებსაც თანამშრომლებმა deploy-ის გარეშე უნდა შეუცვალონ, django-celery-beat მათ ბაზაში ინახავს და admin გვერდებს ამატებს:
$ pip install django-celery-beat$ celery -A myproject beat --scheduler django_celery_beat.schedulers:DatabaseSchedulerდაამატე "django_celery_beat" INSTALLED_APPS-ში და გაუშვი python manage.py migrate. სანაცვლოდ მიიღებ ცხრილების კიდევ ერთ ნაკრებს და განრიგს, რომელიც ვერსიების კონტროლში აღარ ცხოვრობს. რამდენიმე უცვლელი დავალებისთვის ზემოთ ნაჩვენები dictionary-ი დატოვე.
მეხსიერება და concurrency პატარა გეგმაზე#
აქ წყდება, პატარა სერვერზე Celery გადარჩება თუ მოკვდება. Celery-ის ნაგულისხმევი pool, prefork, ერთ მშობელს და შემდეგ --concurrency რაოდენობის შვილობილ პროცესს უშვებს. ნაგულისხმევი concurrency არის პროცესისთვის ხილული CPU ბირთვების რაოდენობა, რაც გაზიარებულ host-ზე შეიძლება მთელი მანქანის ბირთვები იყოს და არა შენი წილი. დააყენე ის თვითონ.
თითოეული შვილობილი პროცესი შენს Django პროექტს import-ს უკეთებს, ამიტომ ღირს დაახლოებით იმდენი, რამდენიც ერთი gunicorn worker: მოკრძალებული პროექტისთვის ხშირად 100-200 MB, მძიმე დამოკიდებულებებით - მეტი. როგორ გაზომო, რამდენი RAM სჭირდება Python აპლიკაციას სტატიაში წერია. მშობელი მსგავს რაოდენობას ამატებს. ამიტომ --concurrency 2-ით გაშვებული worker სამი პროცესია, და გვერდით gunicorn-ით პროექტი ხუთ-ექვსჯერ იტვირთება.
| გეგმის RAM | განლაგება | შენიშვნები |
|---|---|---|
| 1 GB | მხოლოდ ვებ, worker სხვაგან | ერთი gunicorn worker მარტო დაახლოებით 200 MB-ია |
| 2 GB | gunicorn 1 + Celery -c 1 | მჭიდროა; დაჯერებამდე გაზომე |
| 4 GB | gunicorn 2 + Celery -c 2 | გონივრული პატარა production განლაგება |
| 6-8 GB | gunicorn 3 + Celery -c 3-4 | ამის შემდეგ დავალებებს ბაზა ზღუდავს |
ეს საწყისი წერტილებია, აგებული ზემოთ მოცემული ერთ პროცესზე გათვლილი მაჩვენებლებიდან და არა შენი კოდის გაზომვებიდან. განლაგებაზე გადაწყვეტამდე worker დატვირთვის ქვეშ გაუშვი და თითოეული პროცესის resident ზომა წაიკითხე ps -o pid,rss,cmd -C celery-ით. RE:NODE-ზე კონსოლი მეხსიერებას გეგმის ლიმიტის წინააღმდეგ გრაფიკად აჩვენებს, ხოლო ლიმიტი გამორთვის ღილაკს ჰგავს: მეხსიერების ლიმიტზე ბირთვი container-ს აჩერებს და ის swap-ში ჩაძირვის ნაცვლად სუფთად ირთვება. Celery worker-ისთვის ეს ყველა შვილობილი პროცესის ერთდროულ სიკვდილს ნიშნავს, და სწორედ ამიტომ არ არის ზემოთ ნახსენები acks_late და reject_on_worker_lost არასავალდებულო.
ოთხი ღონისძიება worker-ს ბიუჯეტში აჭერს.
აირჩიე pool დატვირთვის მიხედვით. Prefork სწორია CPU-ზე დამოკიდებული ან ბიბლიოთეკებით მძიმე დავალებებისთვის და ნამდვილ იზოლაციას იძლევა. დავალებებისთვის, რომლებიც ძირითადად ქსელურ გამოძახებას ელოდებიან, thread pool პროექტის ერთ ასლს ბევრ ერთდროულ დავალებაზე იყენებს:
$ celery -A myproject worker --pool=threads --concurrency=8 --loglevel=INFOThread-ები Django პროცესს იზიარებენ, ამიტომ გაჟონვის მქონე დავალება ყველას ახარჯვინებს, და შენს კოდზე thread-უსაფრთხოების ჩვეულებრივი დათქმები მოქმედებს. ელფოსტისა და webhook-ების დატვირთვისთვის დანაზოგი დიდია: რვა ერთდროული დავალება დაახლოებით ერთის მეხსიერებით. არსებობს ასევე --pool=solo, ერთდროულად ერთი დავალება მთავარ პროცესში, რაც ყველაზე პატარა შესაძლო კვალია და გონივრული არჩევანია 1 GB გეგმაზე, როცა სამუშაო იშვიათია.
გადაამუშავე შვილობილი პროცესები. Python პროცესები მეხსიერებას ოპერაციულ სისტემას იშვიათად უბრუნებენ, ამიტომ შვილობილი, რომელმაც ერთხელ დიდი ექსპორტი დაამუშავა, დიდი რჩება. ორი პარამეტრი ამას ზღუდავს:
CELERY_WORKER_MAX_TASKS_PER_CHILD = 200CELERY_WORKER_MAX_MEMORY_PER_CHILD = 300_000 # kilobytes, about 300 MBრომელიმე ზღვრის მიღწევის შემდეგ შვილობილი მიმდინარე დავალებას ასრულებს და ახალით იცვლება. მეხსიერების ლიმიტი კილობაიტებშია, რაც ადამიანებს ხშირად აბნევს. გადამუშავება გაჟონვის გამოსწორება არ არის, მაგრამ გაჟონვას მოშლიდან ჩანაწერად აქცევს ლოგში.
შეამცირე უქმი საუბარი. პატარა გეგმაზე სამი flag იმ ტრაფიკსა და პროცესებს ხსნის, რომლებიც ერთი worker-ის დროს არ გჭირდება:
$ celery -A myproject worker --concurrency=2 -Ofair \ --without-gossip --without-mingle --without-heartbeat-Ofair მშობელს აიძულებს სამუშაო მხოლოდ თავისუფალ შვილობილებს მისცეს, რაც გრძელი დავალებებისა და 1-ზე მეტი prefetch-ის დროს მნიშვნელოვანია. --without-gossip, --without-mingle და --without-heartbeat worker-ს აჩერებენ სხვა worker-ებთან კლასტერის მდგომარეობის გაცვლაზე, რაც მაშინ გამოგადგება, როცა სხვები არ არიან. heartbeat-ის გარეშე სხვა node-ებიდან გაშვებული celery status და inspect ბრძანებები ნაკლებ ინფორმაციას გვაძლევს, ამიტომ თუ რამდენიმე worker გაქვს და ამ ბრძანებებს ეყრდნობი, heartbeat ჩართული დატოვე.
გაყავი რიგები, შემდეგ შეზღუდე. ანგარიში, რომელსაც ხუთი წუთი სჭირდება, და პაროლის აღდგენა, რომელსაც ერთი წამი, ერთ შვილობილებს არ უნდა იზიარებდნენ. ერთმანეთისგან გამოყავი:
CELERY_TASK_DEFAULT_QUEUE = "default"CELERY_TASK_ROUTES = { "reports.tasks.*": {"queue": "slow"},}$ celery -A myproject worker -Q default --concurrency=2 -n fast@%h$ celery -A myproject worker -Q slow --concurrency=1 -n slow@%hნელი რიგი ერთ შვილობილს იღებს, ამიტომ მძიმე ექსპორტი შეიძლება მხოლოდ ერთი ბირთვის წილსა და ერთი პროცესის მეხსიერებას დაუწვდეს, ხოლო ყველაფერი გადაუდებელი განაგრძობს მოძრაობას. ფასი ის არის, რომ კიდევ ერთი მშობელი პროცესი ემატება. ძალიან პატარა გეგმაზე უფრო იაფია ერთი რიგი და ერთი worker დატოვო და შეეგუო, რომ გადაუდებელი დავალებები დაიცდიან.
CPU მეხსიერებისგან განცალკევებული ლიმიტია. RE:NODE-ზე CPU-ს მაჩვენებელი შენ მიერ ნაყიდ წილზე მკაცრი შეზღუდვაა, ამიტომ ნახევარ ბირთვზე --concurrency 4 worker ოთხ დავალებას ოთხი ბირთვის სიჩქარით არ ასრულებს; ასრულებს ნელა, მაგრამ ყველას ერთდროულად. CPU წილზე მეტი concurrency მხოლოდ იმ დავალებებს ეხმარება, რომლებიც დროს სხვა რამის ლოდინში ატარებენ.
სად მუშაობს პროცესები#
ვებ პროცესი, worker და beat სამი ხანგრძლივად მომუშავე ბრძანებაა. RE:NODE-ზე აპლიკაციის სერვერს ერთი გაშვების ბრძანება აქვს, ამიტომ პირდაპირი განლაგებაა ერთი repository-დან ცალკეული Python სერვერები, თითოეული საკუთარი გაშვების ბრძანებით, ყველა ერთსა და იმავე Valkey-სა და ბაზაზე გარემოს ცვლადებით მიმართული. ვებ სერვერზე gunicorn მუშაობს, worker სერვერზე celery -A myproject worker, ხოლო beat პროცესი ან worker-ის ბრძანებას ემატება, ან საკუთარ პატარა სერვერს იღებს. ეს ერთ გეგმაზე მეტი ჯდება, მაგრამ აქრობს კითხვას, რომელი პროცესი მოკვდება, როცა მეხსიერება ამოიწურება.
თუ ყველაფრის ერთ სერვერზე დატოვება გირჩევნია, გაშვების shell ბრძანება, რომელიც worker-ს ფონზე და gunicorn-ს წინა პლანზე უშვებს, მუშაობს, მაგრამ გასაგებად იცოდე, რაზე უარს ამბობ: თუ worker მოკვდება, gunicorn-ის გაგრძელებისას მას არავინ გადაატვირთავს და რიგი ცარიელდება ხილული წარუმატებლობის გარეშე. ყოველი deploy-ის შემდეგ კონსოლს დახედე, და რაიმე მნიშვნელოვანისთვის ცალკეული სერვერები არჩიე.
Valkey გეგმები 256 MB-დან იწყება, თვეში $2-ად, ხოლო Python გეგმები - $4-დან. Valkey სერვერთან დაკავშირება საკუთარ host-სა და port-ზე გენერირებული პაროლით ხდება, ამიტომ ვებ და worker სერვერები მასთან ერთნაირად უკავშირდებიან, სადაც არ უნდა მუშაობდნენ. არსებობს ასევე Python გეგმის, PostgreSQL გეგმისა და 256 MB Valkey გეგმის ნაკრები, ფასდაკლებით ერთად შესაძენი პანელის მაღაზიის Bundles ჩანართზე, რომელიც ამ მთელი სტატიის ფორმაა: ვებ აპლიკაცია, ბაზა და broker.
Git deploy-ები თითოეულ სერვერზე ცალ-ცალკე მოქმედებს. repository-ში push ყველა სერვერს ხელახლა უშვებს, რომელსაც deploy-on-push ჩართული აქვს, ამიტომ worker-ები ახალ კოდს ვებ პროცესთან ერთად იღებენ. მომუშავე Celery worker ძველ კოდს მეხსიერებაში restart-მდე ინახავს, ამიტომ ეს მნიშვნელოვანია. დავალების შუაში მოსული restart ჩვეულებრივი გაჩერებაა, და acks_late ნიშნავს, რომ შეწყვეტილი დავალება ხელახლა მიეწოდება. რატომ უნდა აძლევდეს გაჩერების სიგნალი დავალებას დასრულების საშუალებას, გრაციოზული გაჩერება და health check-ები სტატიაშია.
Valkey-ს პარამეტრები, რომლებიც Celery broker-ს სჭირდება#
სამი სერვერული პარამეტრი წყვეტს, შეტყობინებები გადარჩება თუ არა. დავალებების რიგები Valkey-ით მათ ყველა ბიბლიოთეკისთვის განიხილავს; Celery-სთვის მოკლე ვერსია ასეთია:
- Eviction. შეამოწმე
maxmemory-policyშენს ინსტანციაზე.noeviction-ის გარდა ნებისმიერმა მნიშვნელობამ შეიძლებაceleryრიგის სია ანunackedსტრუქტურა წაშალოს, როცა მეხსიერება სავსეა. პოლიტიკებს Valkey-ს მეხსიერება და eviction პოლიტიკები განმარტავს. - Persistence. RE:NODE-ზე Valkey გეგმა დისკზე AOF-ითა და snapshot-ებით ინახავს, ამიტომ რიგში მდგარი დავალებები restart-ს გადაურჩება. თითოეულის ფასს AOF და RDB განმარტავს.
- ზომა. შეტყობინება რამდენიმე ასი ბაიტიდან რამდენიმე კილობაიტამდეა. 256 MB ინსტანცია პატარა შეტყობინებების ძალიან დიდ დაგროვებას იტევს; ივსება ის ისე, რომ result backend-ს არავინ აცლის ვადას, ან დიდი არგუმენტებით.
რა შექმნა Celery-მ, valkey-cli-ით ნახე:
$ valkey-cli -h HOST -p PORT --askpass> KEYS *> LLEN celery> HLEN unacked> INFO memoryLLEN celery არის ნაგულისხმევ რიგში მოლოდინში მყოფი შეტყობინებების რაოდენობა, ხოლო HLEN unacked - აღებულის, მაგრამ ჯერ არდადასტურებულის. KEYS * პატარა ინსტანციაზე რიგზეა და დიდზე ცუდი ჩვევაა; იქ SCAN გამოიყენე. დაკავშირებაზე Valkey-ს დაწყება სტატიაში წაიკითხავ.
გაუშვი celery -A myproject inspect active, რომ ნახო, თითოეული worker ახლა რას ასრულებს, celery -A myproject inspect stats - pool-ისა და მეხსიერების მაჩვენებლებისთვის, და celery -A myproject purge, რომ მოლოდინში მყოფი ყველაფერი გადააგდო; ის დადასტურებას ითხოვს და გადაწყვეტილება შეგნებულად უნდა მიიღო.
პრობლემების გადაჭრა#
Worker ირთვება და მერე არაფერს აკეთებს. broker-ის URL არასწორია ისე, რომ დაუყოვნებლივ შეცდომას არ აჩენს, ან დავალებები სხვა რიგშია, ვიდრე worker უსმენს. შეამოწმე celery -A myproject inspect registered, შემდეგ LLEN რიგის სახელზე.
`Received unregistered task of type`. Worker-მა დავალების განმსაზღვრელი მოდული import-ად არ გააკეთა. შეამოწმე autodiscover_tasks(), არის თუ არა აპლიკაცია INSTALLED_APPS-ში და ფაილს ჰქვია თუ არა tasks.py.
`kombu.exceptions.OperationalError` ან `Connection refused`. host, port ან პაროლი არასწორია, ან ვებ პროცესს Valkey-მდე ქსელური გზა არ აქვს. ჯერ იგივე URL valkey-cli-ით გამოსცადე იმავე სერვერიდან.
დავალება ორჯერ გაეშვა. Visibility timeout დავალებაზე მოკლეა, ან შვილობილი დადასტურებამდე მოკვდა. გაზარდე timeout-ი, დააყენე acks_late reject_on_worker_lost-თან ერთად და დავალება idempotent გახადე.
პერიოდული დავალებები ერთსა და იმავე მომენტში ორჯერ გადის. ორი beat პროცესია. შეამოწმე მეორე სერვერი ან -B ერთზე მეტ worker-ზე.
პერიოდული დავალებები არასოდეს გადის. Beat არ მუშაობს, ან მუშაობს სხვა settings მოდულით, ვიდრე ის, რომელშიც CELERY_BEAT_SCHEDULE წერია. როცა მუშაობს, Beat თითოეულზე Sending due task-ს წერს ლოგში.
სერვერი ყოველ რამდენიმე საათში თავიდან ირთვება. მეხსიერების გამო მოკვლაა. ვებ worker-ების, მშობელისა და შვილობილების ჯამი გეგმას ps-ით შეადარე, შემდეგ ზემოთ აღწერილი ღონისძიებები გამოიყენე. Schedules ჩანართიდან ყოველკვირეული დაგეგმილი restart უხეში, მაგრამ ეფექტური დამცავი ბადეა; დროზე restart განრიგები, რომლებიც გვეხმარება სტატიაშია.
`DatabaseError: connection already closed` დავალების შიგნით. ხანგრძლივად მომუშავე worker-მა კავშირი შეინახა, რომელიც ბაზამ გაწყვიტა. გრძელი დავალებების დასაწყისში გამოიძახე django.db.close_old_connections(), ან CONN_MAX_AGE ზომიერი დატოვე იმ settings-ში, რომელსაც worker იყენებს.
დავალებები გროვდება, worker-ები კი უქმად დგანან. ხშირად prefetch-ის მამრავლი ერთ ძალიან გრძელ დავალებასთან ერთად არის მიზეზი. დააყენე 1 და გამოიყენე -Ofair.
FAQ#
მჭირდება თუ არა Celery Django პროექტისთვის?
ხშირად არა. განრიგით გაშვებული management command ღამის გასუფთავებასა და digest-ებს broker-ის გარეშე ფარავს, ბაზაზე დაფუძნებული რიგი კი - ზომიერ მოცულობას. Celery ღირს მაშინ, როცა დავალებები ხშირია, backoff-იანი retry-ები სჭირდება და restart-ს უნდა გადაურჩეს. პარამეტრებს ფონური დავალებები პატარა სერვერზე ადარებს.
შეუძლია Celery-ს Valkey-ის broker-ადაც და result backend-ადაც გამოყენება?
დიახ. გამოიყენე იგივე სერვერი სხვა ბაზის ნომრით, როგორც ზემოთ მოცემულ პარამეტრებშია, ან დააყენე CELERY_TASK_IGNORE_RESULT = True და backend გამოტოვე. Django-სპეციფიკური ალტერნატივაა django-celery-results, რომელიც შედეგებს შენს ბაზაში ინახავს CELERY_RESULT_BACKEND = "django-db"-ით.
რამდენი Celery worker უნდა გავუშვა?
საკმარისი შვილობილი, რომ პიკზე რიგის დაგროვება ნულთან ახლოს დარჩეს, და მეტი არა, ვიდრე მეხსიერება იძლევა. ქსელზე დამოკიდებული დავალებებისთვის ერთ პროცესზე 8-დან 20-მდე thread-იანი pool რვა prefork შვილობილზე იაფია. CPU-ზე დამოკიდებული დავალებებისთვის - ერთი შვილობილი თითო ბირთვზე, რომელიც ნამდვილად გაქვს. პატარა გეგმაზე სწორი პასუხი თითქმის ყოველთვის ნაგულისხმევზე ნაკლებია.
რატომ იზრდება Valkey-ში გასაღებების რაოდენობა გამუდმებით?
შენახული შედეგები ვადის გარეშე, ან unacked ჩანაწერები, რომლებიც არასოდეს იწმინდება, რადგან დავალებები არასოდეს დასტურდება. დააყენე CELERY_RESULT_EXPIRES, იგნორი გაუკეთე შედეგებს, რომლებსაც არ კითხულობ, და როცა worker არაერთხელ მოკვდა, HLEN unacked ნახე.
უსაფრთხოა თუ არა beat-ის ყველა სერვერზე გაშვება?
არა. ერთ განრიგზე ზუსტად ერთი beat პროცესი, თორემ ყოველი პერიოდული დავალება ყოველ beat-ზე ერთხელ გაიგზავნება. დადე ის ერთ სერვერზე, ან ჩაშენე -B-ით ერთ worker-ში, რომელსაც არასოდეს გაზრდი.
რა ემართება რიგში მდგარ დავალებებს, როცა Valkey თავიდან ირთვება?
persistence-ის ჩართვისას რიგი დისკიდან თავიდან იტვირთება, და worker-ები ხელახლა უკავშირდებიან, თუ CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP დაყენებულია. persistence-ის გარეშე ყველა მოლოდინში მყოფი დავალება იკარგება. unacked-ში მყოფი დავალებები რიგში visibility timeout-ის შემდეგ ბრუნდება.


