کشاورزی هوشمند

داشبورد Real-Time کشاورزی: معماری سیستم پایش ۵۰۰ هکتار

مقدمه: یک شرکت کشاورزی بزرگ، یک نیاز واقعی

یک شرکت کشاورزی با اراضی پراکنده در سه استان کشور، مجموعاً ۵۰۰ هکتار زمین زراعی داشت. مدیران این شرکت می‌خواستند وضعیت سلامت محصول، رطوبت خاک تخمینی و هرگونه تنش غیرعادی را از راه دور و به صورت هفتگی پایش کنند. آن‌ها تجربه قبلی استفاده از نقشه‌های ماهواره‌ای داشتند اما فرآیند دریافت و تفسیر داده کاملاً دستی بود: یک کارشناس هر هفته تصاویر را دانلود و به صورت چشمی بررسی می‌کرد.

هدف پروژه ساخت یک سیستم خودکار بود که داده‌های ماهواره‌ای را هر هفته به طور خودکار دریافت، پردازش و در یک داشبورد تحت وب نمایش دهد. کارشناس زراعی شرکت به جای کار دستی هفتگی، باید فقط در موارد هشدار دریافتی اقدام می‌کرد. این تغییر پارادایم از «بررسی دوره‌ای» به «مدیریت بر اساس استثنا» بود.

معماری کلی سیستم

معماری سیستم بر اساس یک stack استاندارد و اثبات‌شده Django طراحی شد. اجزای اصلی به این شرح بودند: Django به عنوان فریمورک اصلی وب و API، Celery برای پردازش پس‌زمینه و اجرای وظایف زمان‌بر، Redis به عنوان Broker پیام بین Django و Celery و همچنین لایه Cache، Sentinel Hub به عنوان منبع داده ماهواره‌ای Sentinel-2 (از طریق API رسمی آن)، و PostgreSQL با افزونه PostGIS برای ذخیره هندسه‌ها و اطلاعات شاخص‌های گیاهی.

جریان کلی داده به این شکل بود: هر شنبه ساعت سه بامداد، یک Celery Beat Task به طور خودکار فعال می‌شد و لیست تمام مزارع فعال را از پایگاه داده دریافت می‌کرد. برای هر مزرعه، یک Task مجزا در صف Celery قرار می‌گرفت. هر Task به API سنتینل هاب متصل می‌شد، داده‌های NDVI، NDMI و SAVI را برای محدوده آن مزرعه دریافت و در پایگاه داده ذخیره می‌کرد. در صورت افت شدید شاخص‌ها نسبت به میانگین تاریخی، یک SMS هشدار برای مدیر مزرعه ارسال می‌شد.

Celery Task: دریافت شاخص‌های ماهواره‌ای

قلب سیستم، تابع Celery زیر است که وظیفه دریافت شاخص‌های هر مزرعه را دارد. پارامتر bind=True اجازه می‌دهد تابع به شیء Task خودش دسترسی داشته باشد که برای پیاده‌سازی retry ضروری است:

from celery import shared_task from .sentinel import SentinelClient from .models import Farm, IndexSnapshot @shared_task(bind=True, max_retries=3) def fetch_farm_indices(self, farm_id, date_from, date_to): try: farm = Farm.objects.get(id=farm_id) client = SentinelClient() indices = client.get_indices( polygon=farm.polygon, date_from=date_from, date_to=date_to, indices=['NDVI', 'NDMI', 'SAVI'] ) snapshot, created = IndexSnapshot.objects.update_or_create( farm=farm, date_from=date_from, date_to=date_to, defaults={'ndvi': indices['ndvi'], 'ndmi': indices['ndmi'], 'savi': indices['savi']} ) return snapshot.id except Exception as exc: raise self.retry(exc=exc, countdown=60)

استفاده از update_or_create به جای create ایمنی در برابر اجرای تکراری task را تضمین می‌کند (Idempotency). اگر به دلیلی task دو بار اجرا شود، داده تکراری ایجاد نمی‌شود بلکه رکورد موجود به‌روز می‌شود. countdown=60 یعنی در صورت شکست، ۶۰ ثانیه صبر کن و دوباره امتحان کن.

زمان‌بندی خودکار با Celery Beat

برای زمان‌بندی اجرای هفتگی خودکار، از Celery Beat با تعریف زیر در settings.py استفاده شد. زمان سه بامداد روز شنبه به این دلیل انتخاب شد که کمترین بار روی سرور بود و احتمال وجود تصاویر ماهواره‌ای به‌روزشده از هفته گذشته زیاد بود:

CELERY_BEAT_SCHEDULE = { 'weekly-satellite-update': { 'task': 'farm.tasks.fetch_all_farms', 'schedule': crontab(hour=3, minute=0, day_of_week=6), # Saturday 3AM }, }

task اصلی fetch_all_farms فقط یک Orchestrator بود: تمام مزارع فعال را از پایگاه داده می‌خواند و برای هر کدام یک task مجزای fetch_farm_indices در صف قرار می‌داد. این جداسازی مسئولیت‌ها (Separation of Concerns) چند مزیت داشت: اول، شکست پردازش یک مزرعه روی بقیه تأثیر نمی‌گذاشت. دوم، مزارع به صورت موازی پردازش می‌شدند و زمان کل کاهش می‌یافت.

مدیریت خطا و استراتژی Retry

API سنتینل هاب مانند هر سرویس خارجی دیگری گاهی با خطاهای موقت روبه‌رو می‌شود: Rate Limit، timeout، یا اختلال سرور. بدون استراتژی retry مناسب، یک خطای موقت می‌توانست کل چرخه پایش هفتگی یک مزرعه را از بین ببرد.

استراتژی به‌کاررفته ترکیبی از max_retries=3 و countdown=60 بود. برای خطاهای Rate Limit، countdown به صورت Exponential Backoff افزایش یافت: اولین retry بعد از ۶۰ ثانیه، دومی بعد از ۱۸۰ ثانیه و سومی بعد از ۵۴۰ ثانیه. اگر پس از سه بار تلاش task همچنان شکست می‌خورد، در صف Dead Letter Queue جداگانه‌ای قرار می‌گرفت که هر روز صبح یک گزارش از آن‌ها به تیم فنی ارسال می‌شد.

نکته مهم: در Celery، وقتی self.retry() فراخوانی می‌شود، exception جدیدی raise می‌شود. این یعنی کد بعد از raise self.retry() اجرا نمی‌شود. برای log کردن خطا قبل از retry، باید آن را قبل از دستور retry انجام دهید.

بهینه‌سازی هزینه API

سنتینل هاب بر اساس تعداد پردازش‌های واحد (Processing Unit) هزینه می‌گیرد. در مدل اولیه سیستم، هر مزرعه به صورت مجزا و با درخواست‌های جداگانه پردازش می‌شد. این رویکرد در ماه اول هزینه‌ای بیش از حد انتظار ایجاد کرد.

سه تکنیک برای کاهش هزینه پیاده‌سازی شد: اول، Batching درخواست‌ها — مزارع نزدیک به هم که در یک scene واحد Sentinel-2 قرار می‌گرفتند در یک درخواست واحد پردازش می‌شدند. دوم، Cache کردن نتایج — اگر در همان روز همین داده برای مزرعه مجاور قبلاً دریافت شده بود، از Cache Redis استفاده می‌شد. سوم، کاهش دوره زمانی — به جای دریافت داده ۱۴ روزه، داده ۷ روزه دریافت شد که حجم پردازش را نصف کرد بدون آنکه به تناوب هفتگی گزارش‌دهی آسیبی وارد شود.

مقایسه هزینه ماهانه API قبل و بعد از بهینه‌سازی

مورد قبل از بهینه‌سازی بعد از بهینه‌سازی صرفه‌جویی
تعداد درخواست‌های API ماهانه ۳۲۰ درخواست ۸۸ درخواست ۷۲٪ کمتر
Processing Units مصرفی ۴۸۰۰ واحد ۱۴۴۰ واحد ۷۰٪ کمتر
هزینه ماهانه API (یورو) ~۱۲۰ یورو ~۳۸ یورو ۶۸٪ کمتر
زمان اجرای کل فرآیند هفتگی ۴ ساعت ۲۰ دقیقه ۱ ساعت ۱۵ دقیقه ۷۱٪ کمتر
نرخ شکست task ۸.۲٪ ۱.۴٪ ۸۳٪ بهتر

نظارت بر سیستم و آلارم‌های خودکار

یک سیستم بدون نظارت، یک سیستم نیمه‌کامل است. برای پایش سلامت خود سیستم، چند لایه نظارتی پیاده‌سازی شد. Sentry برای ردیابی خطاهای Python در زمان واقعی به کار رفت. هر exception مهم از جمله شکست‌های task که به Dead Letter Queue رفتند، در Sentry ثبت می‌شدند و اعلان فوری به تیم فنی ارسال می‌شد.

علاوه بر Sentry، یک داشبورد سلامت داخلی ساخته شد که وضعیت taskهای هفتگی را به تفکیک نشان می‌داد: چند مزرعه با موفقیت پردازش شدند، چند تا در صف هستند، چند تا شکست خوردند. این داشبورد از طریق یک endpoint ساده Django که داده‌های Redis را می‌خواند، تغذیه می‌شد و کارشناس پشتیبانی می‌توانست هر صبح شنبه وضعیت را چک کند.

آلارم‌های کشاورزی (برای مدیران مزارع، نه تیم فنی) بر اساس قوانین زیر ارسال می‌شدند: افت NDVI بیش از ۲۰٪ نسبت به میانگین سه هفته گذشته، NDMI زیر آستانه بحرانی ۰.۱- (نشانه تنش شدید آبی)، یا افزایش ناگهانی NDVI که می‌توانست نشانه رشد علف‌های هرز باشد.

نکات کلیدی
  • Celery به همراه Redis یک ترکیب اثبات‌شده برای پردازش پس‌زمینه در Django است؛ Idempotency را از ابتدا در طراحی Task در نظر بگیرید.
  • Batching درخواست‌های API برای مزارع نزدیک به هم می‌تواند هزینه را تا ۷۰٪ کاهش دهد؛ هزینه API سنتینل هاب را از همان ابتدا در طراحی لحاظ کنید.
  • Exponential Backoff در استراتژی retry از Rate Limit شدن و بلاک شدن IP توسط API‌های خارجی جلوگیری می‌کند.
  • Dead Letter Queue برای taskهای شکست‌خورده ضروری است؛ بدون آن نمی‌دانید کدام مزرعه‌ها داده ندارند.
  • سیستم پایش خود سیستم (Sentry + داشبورد سلامت داخلی) به اندازه خود سیستم اهمیت دارد.