مقالات

طراحی سیستم عامل آماده تولید با ماندگاری و نقاط بازرسی


اعدامی که من مسئول آن بودم، یک بار چهارده مرحله به انجام یک کار رسید و سپس ناپدید شد، زیرا یک استقرار معمول گروهی را که روی آن کار می کرد، برنامه ریزی مجدد کرد. هیچ خطایی در گزارش‌ها وجود ندارد که ارزش نامگذاری را داشته باشد. فرآیند ارکستراسیون در اواسط اجرا ناپدید شد، حالت حافظه خود را با خود همراه کرد و کاربر را به کار خیره کرد که هرگز تمام نمی‌شد. مدل هیچ اشتباهی نکرده بود. وقتی زمین زیر او حرکت کرد، ما چیزی برای فرار نساختیم.

این شکاف بین «مدل کار می‌کند» و «سیستم به کار خود ادامه می‌دهد» مهار عامل است، و این جایی است که من بخش زیادی از دو سال گذشته را صرف ساختن زمان اجرا کرده‌ام که جریان‌های کاری عامل ما را به‌جای یک فرآیند واحد به مجموعه‌ای از گروه‌ها منتقل می‌کند.

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

همه چیز در اینجا در کتابخانه استاندارد پایتون پیاده سازی شده است. من از SQLite استفاده کردم، بنابراین بدون تنظیم کار می‌کند، اگرچه طراحی به Postgres منتقل می‌شود، که زمانی که حالت اجرا نیاز به زنده ماندن هر فرآیند جداگانه داشته باشد، در نهایت به آن می‌رسیم.

آنچه که آمادگی تولید در واقع مستلزم آن است

در اینجا لیست کوتاهی از کارهایی است که یک کمربند باید درست انجام دهد. بقیه مقاله اجرای آن است.

  1. هر اجرا را به عنوان حالت ماندگار در یک فروشگاه در نظر بگیرید، نه به عنوان اشیایی در حافظه که با فرآیند از بین می روند.
  2. نقطه بازرسی در هر انتقال مهم، بنابراین بازیابی از آخرین نقطه خوب ادامه می یابد.
  3. تماس‌های ابزار را ایمن کنید تا بیش از یک بار اجرا شوند، زیرا پس از یک خرابی اغلب نمی‌توانید تشخیص دهید که آخرین مورد اجرا شده است یا خیر.
  4. دو کارگر را همزمان از یک مسیر محافظت کنید.
  5. در هر مرحله به اندازه کافی ضبط کنید تا باگ هایی را که کسی ندیده است اشکال زدایی کند.

من یک چک لیست تقریباً یکسان برای یک زمانبندی کار می نویسم. تنها تفاوت در اینجا این است که کاری که هماهنگ می شود یک چرخه LLM است، نه بلوکی از داده ها.

مدل سازی در حال اجرا به عنوان یک حالت پایدار

از تلقی اجرا به عنوان فراخوانی تابع خودداری کنید. با یک شناسه، موقعیت و سابقه که می‌توانید آن را دوباره بارگیری کنید، آن را به عنوان یک رکورد در نظر بگیرید.

پایتون

@dataclass

کلاس مرحله:

نمایه: داخلی

نوع: str # “model” یا “Tool”

درخواست: دیکته

پاسخ: دیکته | هیچ = هیچ

وضعیت : StepStatus = StepStatus . در انتظار

@dataclass

کلاس اجرا:

run_id: خ

هدف: خیابان

مراحل: لیست = فیلد (default_factory=list)

مکان نما: int = 0 # شاخص مرحله اجرای بعدی

وضعیت: str = “در حال اجرا” # در حال اجرا | تکمیل شد | شکست خورد

نسخه: int = 0 # در هر ورودی دائمی افزایش می یابد

مکان نما چیزی است که باعث می شود رزومه کار کند. در بارگذاری مجدد، مهار آن را می‌خواند و می‌داند مرحله بعدی چیست، بدون اینکه چیزی برای پخش مجدد وجود داشته باشد. فیلد نسخه از بازنویسی دو فرآیند روی یکدیگر جلوگیری می کند و من در فروشگاه به آن بازخواهم گشت. حلقه کامل در یک سند JSON که توسط run_id کلید می‌خورد، سریال می‌شود. می‌توانید مراحل را در جدول خود عادی کنید، و بعد از چند هزار مرحله ممکن است مجبور شوید، اما یک سند در یک درخواست در طول مسیر بازیابی بازخوانی می‌شود، این مسیری است که بیشتر می‌خواهید سریع و ساده باشد.

تصمیم گیری در مورد نقاط کنترل

چک پوینت یک رکورد دائمی از وضعیت حلقه است. هر چند وقت یکبار یکی را می گیرید یک معامله خرج است، نه بیشتر.

ایست بازرسی جایی که هستی در سلامتی خرج کنید از دست دادن کار در تصادف وقتی مناسب است
یک بار بالاخره پایین ترین کل دویدن تعمیر مسیرهای کوتاه ارزان است
بعد از هر مرحله متوسط حداکثر یک قدم پیش فرض برای اکثر نمایندگان
در مورد هر عارضه جانبی بالاترین چیزی نیست که نتوانید آن را بازیابی کنید رون هایی که پول حمل می کنند یا پیام می فرستند

عامل تحقیقاتی فقط خواندنی پس از هر مرحله، نقطه بازرسی خوبی است. برای هر چیزی که در دنیای بیرون عمل می کند، من دو بار در مورد مرحله عمل می نویسم: یک بار برای اینکه بگویم آن را انجام خواهم داد، یک بار برای ثبت آنچه اتفاق افتاده است. تایپ اول بیهوده به نظر می رسد تا زمانی که کف در طول مکالمه از بین برود. وقتی مهار دوباره راه اندازی می شود و مرحله ای را در RUNNING تشخیص می دهد، این حالت ذخیره شده تنها دلیلی است که می داند ممکن است تماس قبلاً راه اندازی شده باشد.

یک لایه ارتجاعی که از همزمانی دوام می آورد

روشی که بار را به دوش می کشد، ایست بازرسی است. این یک نسخه مقایسه و جابجایی انجام می دهد، بنابراین رکورد فقط در صورتی فرود می آید که از زمانی که ما آن را خوانده ایم، کسی خط را لمس نکرده باشد.

پایتون

نقطه بازرسی def (خود، اجرا: اجرا) -> هیچ:

cur = self.joinfill(

“به روز رسانی اجرای داده های SET = ?، نسخه = نسخه + 1”

“WHERE run_id =؟ و نسخه =؟”،

(_serialize(run)، run. run_id، run. version)

)

خود پیوستن ()

اگر cur.تعداد ردیف ها == 0:

افزایش StaleWriteError(

f”run {run. run_id} نسخه قبلی منتقل شد {run . version}”

)

نسخه .+= 1 را اجرا کنید

و نسخه = ? بند کل ایده است. اگر دو کارگر نسخه 7 را بارگذاری کنند و هر دو بخواهند بنویسند، یکی ردیفی را که هنوز در 7 است به روز می کند، و دیگری rowcount == 0 به اضافه StaleWriteError دریافت می کند که به آن می گوید که دیگر مالک این اجرا نیست و باید دست از کار بکشد. این همان الگوی حصاری است که برنامه‌ریزان از آن برای جلوگیری از ادعای یک شغل، مقایسه و تعویض نسخه‌های دو کارگر استفاده می‌کنند. بنابراین فقط یک نویسنده برنده می شود. این یک موتور سیستم های توزیع شده قدیمی است که مشکل جدیدی را هدف قرار می دهد. محموله اکنون متفاوت است. مکانیسم یکسان است

یک چرخه اجرایی که به جای راه اندازی مجدد از سر گرفته می شود

حلقه یک حلقه را بارگیری می کند، مرحله فعلی و نقاط بازرسی را در دو طرف کار پیدا می کند. Die در هر خط و تماس بعدی می تواند همان run_id را بارگیری کند و ادامه دهد.

پایتون

step .status = StepStatus.RUN

store.checkpoint(run) # “درباره اجرای این مرحله”

سعی کنید:

if step .type == “model”:

step .response = model.call(run.goal, run.steps[:step.index])

در غیر این صورت:

step .response = tools .call (

مرحله .درخواست،

idempotency_key=f”{run. run_id}:{step. index}”،

)

step .status = StepStatus.DONE

به جز ToolError:

step .status = StepStatus.FAILED

run .status = “شکست خورد”

ذخیره . checkpoint(run) # نتیجه (یا خطا) ثبت شده

غلافی را دنبال کنید که درست بعد از ایست بازرسی RUN اما قبل از نتیجه می میرد. در راه اندازی مجدد، دسته سیم حلقه را بارگیری می کند، مرحله ای را با برچسب RUNNING می بیند، و نمی تواند از روی رکورد تشخیص دهد که آیا فراخوانی ابزار خارج شده است یا خیر. یک گام مدل برای پخش مجدد ایمن است. مرحله ابزار فقط در صورتی ایمن است که ابزار برای آن ساخته شده باشد، که بخش بعدی است.

کلید idempotency run_id به اضافه ی step index است. از آنجایی که قطعی است، تلاش مجدد پس از خرابی، همان کلید اولین تلاش را ایجاد می کند، و این به لایه ابزار اجازه می دهد تا دوباره متوجه تلاش مجدد شود.

من نمی‌خواستم فقط ادعا کنم که کار می‌کند، بنابراین باندل را با یک تصادف به اجبار به مرحله ابزار اجرا کردم و سپس آن را دوباره راه‌اندازی کردم. خروجی واقعی:

متن

=== 1 را امتحان کنید (در مرحله 1 خراب می شود) ===

[crash] کپسول در مرحله 1 مرد

وضعیت پس از سقوط: [(0, ‘model’, ‘done’), (1, ‘tool’, ‘running’)] | نسخه: 4

=== آزمایش 2 (خلاصه) ===

حالت نهایی: [(0, ‘model’, ‘done’), (1, ‘tool’, ‘done’),

              (2, ‘model’, ‘done’), (3, ‘tool’, ‘done’)]

وضعیت اجرا: کامل |عوارض جانبی ابزار REAL اجرا شده: 2

اجرا دو عارضه جانبی از ابزار می‌خواست و دقیقاً دو اثر داشت، حتی اگر مرحله 1 در دو فرآیند جداگانه انجام شد. ریکاوری به جای شروع دوباره از حالت در حال اجرا مداوم از سر گرفته شد.

مهار نمی تواند قول دهد که ابزار دقیقا یک بار اجرا می شود. چیزی که سیستم های توزیع شده به شما می دهند حداقل یک بار تحویل به اضافه ضعف است، و این دو با هم همان نتیجه ای را به شما می دهند که تماس گیرنده دقیقاً یک بار می بیند. بنابراین وظیفه لایه ابزار این است که تکرار را بی ضرر کند.

پایتون

def call(self, request: dict, idempotency_key: str) ->dict:

row = self.joinfill(

“انتخاب نتیجه FROM tool_results WHERE key =?”، (idempotency_key،)

) .fetchone()

اگر ردیف None نباشد:

json.load(ord[0]) کلید # قبلاً اجرا شده است. استفاده مجدد از نتیجه آن

result = self._invoke(درخواست) # تماس واقعی با عوارض جانبی

self.conn.execute(

“INSERT INTO tool_results (کلید، نتیجه) VALUES (?، ?)”،

(idempotency_key، json. dumps (نتیجه))،

)

خود پیوستن ()

نتیجه برگشت

بررسی در جریان است. نسخه قوی‌تر زمانی که یک کلید دریافت می‌کند، همان کلید را به سرویس خارجی ارسال می‌کند. API های پرداخت مورد آشنای هستند که در آن کلید عدم توانایی به این معنی است که خود ارائه دهنده برای آن دو بار هزینه نمی گیرد. هنگامی که سرویس پایین‌دستی کلید شما را حذف می‌کند، حتی در صورت ناخوشایندی که اولین تماس از راه دور با موفقیت انجام می‌شود، تحت پوشش قرار می‌گیرید، اما فرآیند شما قبل از اینکه بتواند آن را بنویسد از بین رفته است. هنگامی که یک سرویس چیزی ارائه نمی دهد، من به کلید اصلی که عملیات از قبل دارد باز می گردم، معمولاً یک upsert از (run_id، step_index)، بنابراین پخش مجدد به جای تکرار بازنویسی می شود.

برخی از عوارض جانبی ناتوانی را به چالش می کشند، و بهتر است این را بگوییم تا اینکه وانمود کنیم که مدل رایگان است. ایمیل یکی از مواردی است که همه با آن روبرو هستند. پاسخ عملی یک جدول dedupe است که فرستنده قبل از ارسال آن را بررسی می کند که پنجره ارسال تکراری را بدون بسته شدن کامل کوچک می کند. برای بسیاری از سیستم ها، این پنجره محدود هدف منصفانه است.

چیزی که بعد از شروع آن تماشا می کنم

کار با این چند چیز را نشان داد که طراحی نشان داد. run_id، فهرست مرحله و نسخه را در هر انتقال ثبت کنید. این تاریخچه نسخه نزدیک‌ترین چیزی است که می‌توانید به یک ضبط کننده پرواز داشته باشید، و من فقط با خواندن مجموعه‌ای از برآمدگی‌های نسخه برای یک run_id، برخی از حوادث واقعا عجیب را کشف کرده‌ام.

شماره‌ای که در نهایت بیشتر به آن علاقه داشتم، تأخیر یا هزینه توکن نبود. این تعداد گام‌هایی بود که به‌عنوان RUN در طول ریکاوری برگشته‌اند، زیرا این یک محاسبه مستقیم است از اینکه چند بار در میانه عارضه جانبی می‌میرید و برای پوشاندن آن به بی‌توانی متکی هستید. وقتی این عدد بالا می رود، معمولا وابستگی بالادستی تحت بار ناپایدار می شود و این متریک تمایل دارد قبل از نرخ خطای پایین دستی حرکت کند و آن را به یک سیگنال اولیه مفید تبدیل می کند.

TTL را روی مراحل در حال اجرا تنظیم کنید و بعد از آن هشدار دهید. گامی که در RUNNING بسیار فراتر از زمان اجرای معقول گیر کرده است، تقریباً همیشه کارگری است که اجرا را گرفته و بدون رها کردن آن می‌میرد، و این هشدار همچنین موارد لبه اجاره‌ای را می‌گیرد که نمی‌توانید به راحتی آن‌ها را در یک آزمایش بازتولید کنید.

بعدش اینو از کجا بیاریم

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

مدل پایداری نیز حفاظت بازیابی خود را به دست می آورد. از آنجایی که هر اجرا یک رکورد قابل پرس و جو با تاریخچه کامل گام است، می توانید اجراهای قدیمی را با یک مدل جدید دوباره پخش کنید و آنها را مقایسه کنید، که در حال حاضر بخش بزرگی از سیستم امتیازدهی است. اگر قابلیت اطمینان تنها چیزی نیست که از حالتی که قبلاً ذخیره کرده اید می خواهید، این مسیر ارزش دنبال کردن را دارد.

از بین همه موارد بالا، ارزان ترین ایده همان ایده ای است که من در ابتدا به شما می دهم. قبل از اجرا، ایست بازرسی را بنویسید که «در حال اجرا» است. این یک سفر رفت و برگشت اضافی است، در بررسی اضافی به نظر می رسد، و این خطی است که یک حالت تصادف مبهم را به حالتی تبدیل می کند که می توانید از آن بازیابی کنید.

Nandish Nanyappa یک مهندس نرم‌افزار کارکنان در Attentive است، جایی که او جریان‌های کاری عامل هوش مصنوعی را به طور قابل اعتماد در یک ناوگان پاد نگه می‌دارد. او پس از بیش از یک دهه در ذخیره سازی و توزیع سیستم ها به هوش مصنوعی آمد و متوجه شد که بیشتر مشکلات سخت قافیه هستند. او در مورد کارهای زیرساختی نامطلوب می نویسد که عوامل هوش مصنوعی را قابل اعتماد می کند.


Source link

نوشته های مشابه

دیدگاهتان را بنویسید

دکمه بازگشت به بالا