تصور کنید یک عامل هوش مصنوعی هر بار که سرور شما ریاستارت میشود، مبلغی را از حساب مشتریانتان میدزدد. در ۶ سپتامبر ۲۰۲۶، یک تحلیل فنی عمیق در dev.to فاش کرد که یک نقص سیستمی در نحوه مدیریت «پیشگرفتن» (Preemption) در محاسبات موقت (Ephemeral Compute)، منجر به اجرای تکراری ابزارها و تخریب اسنپشاتهای وضعیت (State Snapshots) میشود.
بسیاری از توسعهدهندگان مرگ یک Worker را مانند یک توقف تمیز و ساده میبینند. اما در واقعیت، در محاسبات پیشگرفتنی (Preemptible) یا لایههای رایگان (Free-tier)، Worker اغلب درست بعد از اینکه یک ابزار اثر جانبی (Side Effect) را ثبت کرده اما قبل از اینکه عامل بتواند یک نقطه بازرسی (Checkpoint) بنویسد، ناپدید میشود. این وضعیت یک «حالت زامبی» ایجاد میکند؛ وضعیتی که در آن سیستم باور دارد آن مرحله هرگز اجرا نشده است و همین امر باعث تحریک یک تلاش مجدد (Retry) میشود که نتیجهاش شارژ مضاعف حساب مشتری یا ارسال ایمیلهای تکراری است.
برای درک بهتر، یک عامل پرداخت را تصور کنید که برای کسر مبلغ ۵۰ دلار، یک API بانکی را فراخوانی میکند. بانک تراکنش را تأیید و ثبت میکند، اما سرور یک میلیثانیه بعد از آن میمیرد. یک Worker جدید بالا میآید، میبیند که آخرین نقطه بازرسی موفق قبل از پرداخت بوده است و به سادگی دستور پرداخت را دوباره اجرا میکند. اگر سیستم «حصار» (Fencing) نداشته باشد، مشتری برای یک مرحله واحد، دوبار شارژ میشود. این اتفاق به این دلیل میافتد که پیادهسازیهای رایج، قابلیت بازگشت به آخرین نقطه بازرسی را حفظ میکنند اما توکن حصار را بیصدا حذف میکنند؛ این متغیری است که هرگز نباید در محیط عملیاتی نادیده گرفته شود. این چالش با مدیریت تراکنشها در محیطهای غیرمتمرکز شباهت زیادی دارد، همانطور که در بررسی راهکارهای جلوگیری از اتلاف سرمایه در عاملهای سولانا به اهمیت شبیهسازی تراکنشها برای جلوگیری از هزینههای تکراری اشاره کردیم.
همانطور که در تحلیلهای قبلی ما دربارهی امنیت مدلهای بازمتن اشاره کردیم، اعتماد کورکورانه به وضعیت حافظه در محیطهای توزیعشده خطرناک است.
کالبدشکافی باگ زامبی
طبق گزارش dev.to، ریشه این مشکل در نقض ترتیب رویدادهاست. توالی شکست به این شکل رخ میدهد:
- Worker 1 یک اجاره (Lease) با شماره Epoch 1 دریافت میکند.
- Worker 1 ابزاری را فراخوانی میکند (مثلاً
charge(key=run/step, epoch=1)). - درگاه ابزار (Tool Gateway)، عملیات را در ذخیرهساز ثبت میکند.
- Worker 1 قبل از اینکه بتواند نقطه بازرسی را در لاگ دائمی بنویسد، پیشگرفته (Preempted) و حذف میشود.
- Worker 2 یک اجاره جدید با شماره Epoch 2 دریافت میکند.
- Worker 2 اسنپشات قدیمی را میخواند و نبودِ ثبتِ نهایی را به عنوان دلیلی بر این میبیند که ابزار هرگز اجرا نشده است.
- Worker 2 دوباره ابزار را فعال میکند.
این شکست با یک الگوی ضد (Anti-pattern) رایج تشدید میشود: تولید یک کلید یکتاییساز (Idempotency Key) جدید هر زمان که شماره Epoch افزایش مییابد. اگر کلید به صورت (run_id, step_id, epoch) باشد، بانک درخواست دوم را یک عملیات تجاری کاملاً جدید میبیند، نه تکرار درخواست اول. نویسنده مقاله یک سوال حیاتی میپرسد: «آیا شما یک خط لوله استرداد وجه (Refund Pipeline) را روی کلیدی شرط میبندید که هر بار با ناپدید شدن یک سرور، تغییر میکند؟»
دو Worker که یک run_id مشترک دارند، هرگز نباید اثرات جانبی متضاد را تحت یک Epoch اجاره اعمال کنند، حتی پس از مرگ Worker اول. اگر یک تأییدیه (Ack) دیررس از Epoch پیشگرفته شده هنوز برسد، سیستم باید آن را رد کند یا جبران نماید، و هرگز نباید آن را به عنوان یک موفقیت تازه ثبت کند. ما باید مدلسازی مرگ را از یک «توقف تمیز» به یک «نویسنده همزمان» (Concurrent Writer) تغییر دهیم.
زمینه و مفروضات معماری
برای تحلیل این موضوع، ما باید به جای یک کالبدشکافی پس از حادثه با اعداد ساختگی برای تأخیر یا تعداد مشتریان، یک معماری گسسته را تعریف کنیم. استدلال این مقاله بر اساس این مفروضات خاص است:
- شناسههای پایدار: هر اجرای منطقی عامل دارای یک
run_idپایدار و یکlease_epochاست که به صورت یکنواخت افزایش مییابد. - یکتاییسازی (Idempotency): اثرات جانبی ابزارها تنها زمانی منحصربهفرد میشوند که فراخواننده یک کلید یکتاییساز پایدار ارائه دهد.
- دوام (Durability): نقاط بازرسی دائمی هستند، اما حافظه Worker و بدنه درخواستهای HTTP در حال ارسال، دائمی نیستند.
- زمانبندی پیشگرفتن: پیشگرفتن میتواند دقیقاً بعد از ثبت ابزار و قبل از نوشتن نقطه بازرسی رخ دهد.
- پاسخهای دیررس: پاسخ دیررس از یک Worker قدیمی میتواند پس از فعال شدن اجاره جدید برسد.
در این مدل، معیار پذیرش، ساعتهای کاری نیست، بلکه تعداد «درهمتنیدگیهای پیشگرفتن» (Preemption Interleavings) تزریق شده است. هدف این است که اطمینان حاصل شود برای هر (run_id, step_id) حداکثر یک اثر جانبی ثبت شده وجود داشته باشد، مگر اینکه یک رکورد جبرانی صراحتاً ثبت شده باشد. اگر ابزارهای شما در حال حاضر بر اساس یک کلید سمت سرور که در طول Epochها پایدار میماند حصارگذاری شدهاند، مسیر رد کردن محلی همچنان برای مدیریت زامبیها مفید است. بررسی حیاتی این است که آیا شما واقعاً آن کلید پایدار را ارسال میکنید یا به طور تصادفی هشِ Epoch را میفرستید.
جریان داده در یک سیستم حصارگذاری شده
محاسبات موقت، دامنه شکست را بیشتر از مسیر موفقیت برنامهریز تغییر میدهد. یک Worker رایگان یا پیشگرفتنی، مکانی قانونی برای اجرای استنتاج و مسیریابی ابزار است، تا زمانی که سرور در میانه یک مرحله ناپدید شود. علاوه بر این، پرش مدل (Model Hop) یک صف نامحدود دیگر است زیرا توکنها دیر میرسند و این باعث میشود یک تلاش مجدد شبیه به پیشرفت واقعی به نظر برسد.
برای جلوگیری از اعمال مضاعف، جریان داده باید هر «پرش» را صریح کند:
۱. قصد کاربر $ \rightarrow $ لاگ قصد دائمی $ \rightarrow $ اعطای اجاره $ \rightarrow $ Worker
۲. Worker $ \rightarrow $ پرش مدل (اختیاری، در صف؛ توکنها ممکن است دیر برسند)
۳. Worker $ \rightarrow $ درگاه ابزار (کلید پایدار + هدر Epoch) $ \rightarrow $ ذخیره اثر جانبی
۴. Worker $ \rightarrow $ ذخیره نقطه بازرسی (Epoch + توالی)
۵. پیشگرفتن $ \rightarrow $ انقضای اجاره $ \rightarrow $ اعطای جدید با Epoch+1
۶. تأییدیه دیررس $ \rightarrow $ حصار درگاه $ \rightarrow $ رد کردن، تکرار همان کلید، یا جبران
در این معماری، لاگ قصد (Intent Log) منبع حقیقت است و Worker صرفاً یک حافظه موقت (Cache) اجارهای است که در مقابل آن قرار دارد. اگر هنوز در نمودارهای خود، Worker را مرکز جهان میکشید، معماری شما اساساً معیوب است.
چهار قلمرو شکست
برای حل این مشکل، معماری باید به چهار قلمرو مستقل تقسیم شود تا از پنهان شدن شکستها در یک پردازش واحد جلوگیری شود. ترکیب اینها در یک زمان اجرای واحد (Runtime)، همان دلیلی است که شارژ مضاعف در یک دموی موفق نفوذ میکند:
- مدیریت اجاره و لاگ قصد: باید در برابر کرش مقاوم، حصارگذاری شده و در جایگاهی جدا از Worker باشد.
- Worker موقت: لایهی ناپایداری که ممکن است بمیرد، منجمد شود یا پس از یک جدایش شبکهای کوتاه، دو بار بوت شود. گزینههای سرور رایگان در این قلمرو قرار دارند و دقیقاً به همین دلیل است که در اینجا به اجاره (Lease) نیاز است.
- درگاه ابزار (Tool Gateway): حصار حیاتی. این درگاه باید هم کلید پایدار و هم Epoch را ببیند؛ در غیر این صورت نمیتواند زامبیها را رد کند.
- پرش مدل (Model Hop): محدود به صف و بودجه، نه خوشبینی Worker. دسترسی رایگان به مدلها در اینجا به عنوان یک صف با زمان انتظار (Timeout) تعریف میشود، نه یک فراخوانی تابع محلی.
این حالت شکست را در نظر بگیرید: چه اتفاقی میافتد وقتی قلمرو دوم (Worker) میمیرد در حالی که قلمرو سوم (درگاه) قبلاً عملیات را ثبت کرده و قلمرو چهارم (پرش مدل) هنوز یک پاسخ استریم در حال ارسال دارد؟
پروتکل پنجمرحلهای برای قابلیت اطمینان
به نقل از تحلیل dev.to، تیمها باید از «شعارات قابلیت اطمینان» فاصله گرفته و یک پروتکل گسسته را پیادهسازی کنند. هر مرحله یک اثر ملموس تولید میکند که میتوانید در کنار برنامهریز نگه دارید:
- گام ۱: اولویت با ماشین وضعیت. قبل از حلقه برنامهریز، ماشین وضعیت را بنویسید. من از خواندن برنامهریزی که حافظه را تغییر میدهد و سپس سازگاری را در یک کامنت توضیح میدهد، امتناع میکنم. مفاهیمی چون اعطا، اعمال، نقطه بازرسی، پیشگرفتن و تأییدیه دیررس را به عنوان انتقالاتی با رد کردنهای صریح کدگذاری کنید. اگر یک انتقال نمیتواند Epoch مورد نیاز خود را نام ببرد، نباید در Worker باشد. از خود بپرسید: آیا حلقه فعلی شما همچنان کامپایل میشد اگر نوشتن نقطه بازرسی روی یک Epoch قدیمی ممنوع بود؟
- گام ۲: جداسازی کلید از حصار.
(run_id, step_id)را در کلید یکتاییساز ابزار قرار دهید و آن را بعد از پیشگرفتن بدون تغییر نگه دارید.lease_epochرا در هدر قرار دهید تا درگاه قبل از ارسال، آن را بررسی کند. ادغام Epoch در کلید، یک «بازگشت» را به یک «عملیات تجاری جدید» تبدیل میکند که منجر به صورتحساب مضاعف میشود. بررسی کنید که آیا SDK شما به طور تصادفی این فیلدها را با هم ترکیب میکند یا خیر. - گام ۳: تزریق پیشگرفتن. تستهای واحدی که Worker را قبل از بازگشت ابزار میکشند، ناکافی هستند. شما به یک محیط شبیهساز رویداد گسسته نیاز دارید که درهمتنیدگیهایی را شبیهسازی کند که در آن ذخیره اثر جانبی ثبت شده، اسنپشات ثبت نشده و یک تأییدیه دیررس پس از اعطای اجاره جدید میرسد. این باید بر اساس ترتیب رویدادها باشد، نه تصادفِ استفاده از
sleep(). - گام ۴: اندازهگیری مخرج. قابلیت اطمینان یک «حس» نیست. تعداد اعمال مضاعف و نقاط بازرسی قدیمی را به ازای هر درهمتنیدگی پیشگرفتن تزریق شده بشمارید. هدف، صفر اعمال مضاعف و صفر پذیرش نقاط بازرسی قدیمی در ۲۰۰ مورد تزریق است. اگر نمیتوانید مخرج کسر را بیان کنید، شما در حال اندازهگیری متغیر ثابت (Invariant) نیستید.
- گام ۵: تعریف افعال بازیابی. برنامهریز نباید بازیابی را زیر یک دستور کلی
resume()پنهان کند. شارژها، ایمیلها و نوشتن در حافظه، فعل بازیابی یکسانی ندارند. سیستم باید بر اساس نوع مرحله انتخاب کند:- تکرار (Replay): زمانی که ابزار در سمت ارائهدهنده یکتاییساز است، از همان کلید پایدار استفاده کنید. این رویکرد در تحلیل ما درباره پروتکلهای بازپخش در برابر بنچمارکها به عنوان راهکاری برای تضمین پایداری مدلهای عملیاتی مورد بحث قرار گرفت.
- جبران (Compensate): وقتی ارائهدهنده تحت کلیدی ثبت کرده که قابل استفاده مجدد نیست، جبران را به عنوان یک مرحله مجزا ثبت کنید.
- رد کردن (Reject): وقتی Epoch قدیمی است و تصمیم مرحله قبلاً گرفته شده است.
ماتریس سبک-سنگین (Tradeoff Matrix)
انتخاب مسیر بازیابی اشتباه منجر به حالتهای شکست متفاوتی میشود:
| انتخاب | آنچه به دست میآورید | آنچه میپردازید | زمان شکست |
|---|---|---|---|
| کلید پایدار، Epoch در هدر | بازگشت تبدیل به تکرار میشود | درگاه باید Epoch-aware باشد | ارائهدهنده هدرها را نادیده بگیرد |
| Epoch داخل کلید یکتاییساز | مهر زدن یکتایی ساده است | پیشگرفتن باعث شارژ جدید میشود | هر بار مرگ Worker |
| نقطه بازرسی قبل از فراخوانی | نبودِ ثبتهای یتیم | از دست رفتن کار هنگام مرگ، تأخیر بیشتر | پرشهای طولانی مدل |
| فراخوانی قبل از نقطه بازرسی | پیشرفت در صورت موفقیت | ثبتهای یتیم هنگام پیشگرفتن | نبودِ حصار (Fence) |
| همیشه جبران هنگام بازگشت | روایت مالی شفاف | فشار بیشتر روی ارائهدهنده، دامنه شکست جدید | پیشگرفته شدن خودِ جبران |
نویسنده اشاره میکند که ثبت نقطه بازرسی قبل از فراخوانی برای پرشهای طولانی مدل غیرقابل قبول است زیرا در حالی که توکنها قطرهقطره میرسند، اجاره را متوقف میکند. بادوامترین ترکیب، یک کلید پایدار همراه با حصار Epoch و یک مرحله جبران صریح است، زمانی که ارائهدهنده نمیتواند عملیات را تکرار کند.
تست همگرایی نهایی
یک مورد لبه (Edge Case) باقیمانده، «نقطه بازرسی دیررس» است. این توالی را تصور کنید: Worker 2 یک کلید پایدار را تکرار میکند، ارائهدهنده رسید اصلی را برمیگرداند و سپس نقطه بازرسی تأخیری Worker 1 برای Epoch 1 میرسد و اسنپشات Worker 2 را با یک شماره توالی (seq) کوچکتر بازنویسی میکند.
این باگی است که مردم اغلب به اشتباه «سازگاری نهایی» (Eventual Consistency) مینامند، در حالی که در واقع حصار را از دست دادهاند. یک طراحی همگرا باید پاسخ دهد: آیا لاگ شما آن نوشتن را رد میکند، اسنپشات جدیدتر را تکرار میکند یا با منجمد کردن اجرا، جبران میکند؟ اگر نمیتوانید یکی را انتخاب کنید، طراحی شما هنوز همگرا نشده است.
پیادهسازی و محدودیتها
برای تأیید این موضوع، نویسنده یک شبیهساز (fence_lease_sim.py) را پیشنهاد میکند که با ادعاهای معماری به عنوان دروازه برخورد میکند. با اجرای ۲۰۰ تکرار از پیشگرفتنهای تزریق شده، سیستم میتواند این ویژگی را ثابت کند: شارژ خالص روی ۵۰ دلار میماند. اگر این ادعا شکست بخورد، معماری اشتباه است، حتی قبل از اینکه بحث کیفیت مدل شروع شود. چرا باید پرامپت را دیباگ کنید وقتی پروتکل اجاره در حال نشت پول است؟
چه کسانی نباید از این روش استفاده کنند:
- Workerهایی که اجارههای انحصاری و طولانیمدت روی ماشینهای دائمی دارند.
- ارائهدهندگانی که در حال حاضر در سمت سرور بر اساس
(run_id, step_id)کلیدگذاری میکنند. - اسکریپتهای تک-پردازشی که هیچ اثر جانبی خارج از حافظه ندارند.
در نهایت، این حصار نباید در قالب یک دستور در پرامپت گنجانده شود. برای سختتر کردن سیستم، بررسی Epoch باید به خودِ درگاه ابزار منتقل شود، زیرا هدرِ یک Worker مودب، اگر Worker ناپدید شده باشد، حصار نیست. من همچنین یک رکورد Outbox قبل از فراخوانی HTTP مینویسم تا مرگ بین ارسال و نقطه بازرسی، همچنان یک قصد دائمی برای تکرار داشته باشد. در نهایت، جبران را به یک مرحله حصارگذاری شده مجزا تقسیم کنید، زیرا یک «جبرانِ پیشگرفته شده»، یک نویسنده همزمان جدید است، نه یک پاورقی.
گام بعدی شما
- بررسی کنید آیا SDK شما به طور خودکار Epoch را با کلید یکتاییساز ترکیب میکند یا خیر؛ اگر بله، آن را فوراً جدا کنید.
- یک شبیهساز برای تزریق پیشگرفتن (Preemption) در محیط تست خود پیادهسازی کنید تا نرخ شارژ مضاعف را بسنجید.
- منطق بازیابی عامل خود را از
resume()کلی به افعال تخصصی (تکرار/جبران) تغییر دهید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو