سوال: چگونه از نابود شدن اکتور سیستم در زمانی که مشکلی وجود دارد جلوگیری میکنیم؟
جواب: با نظارت
"آهان! ببین چه گندی زدی! حالا درستش کن"
شوخی کردم. ولی احتمالا همچین چیزی تا به حال به گوشتان خورده. این چیزی است که نظارت در اکتور مدل تقریبا شبیه آن است: یک اکتور والد خطاهای فرزندانش را مشاهده و نظارت میکند و تصمیم میگیرد که به چه صورت خرابی را رفع کند و چه اتفاقی بیفتد.
نظارت مفهومی است که اجازه میدهد در اکتور سیستم به سرعت مشکل را مشخص و خرابی را برطرف کنید.
نظارت از بالا به پایین در سلسله مراتب اکتور ها تضمین میکند که وقتی قسمتی از سیستم دچار یک خطای ناخواسته شود ( مثلا network timeout) آن خطا و مشکل صرفا در همان قسمت از سیستم بماند و همانجا فقط تاثیر بگذارد. تمام اکتور های دیگر به کار خودشان ادامه میدهند بصورتی که انگار هیچ اتفاقی نیفتاده است. به این وضعیت Failure Isolation یا جداسازی شکست میگوییم.
نخست یک یادآوری جزئی این که هر اکتوری والد دارد و بعضی از اکتور ها فرزند دارند.
از آنجایی که والد ها بر فرزندان خود نظارت می کنند در نتیجه تمام اکتورها ناظر دارند و بعضی از اکتور ها ناظر هستند.
در اکتور سیستم ، اکتور ها به صورت سلسله مراتبی مرتب شده اند. یک نمای کلی از این ساختار به شکل زیر است:
اکتور های نگهبان ریشه ی اصلی تمام اکتور ها در سیستم هستند. به تصویر زیر توجه کنید:
اکتور "/" ریشه اصلی در کل اکتور سیستم است و به آن ریشه نگهبان هم گفته میشود. این اکتور وظیفه نظارت بر دو اکتور ریشه ای دیگر به نام های /user و /system را به عهده دارد.
تمام اکتور ها به والد نیاز دارند غیر از این اکتور. به این اکتور bubble-walker هم گفته میشود چون خارج از حباب اکتور سیستم است.
کار اصلی این اکتور این است که تضمین کند سیستم به صورت درست و مرتب خاموش شود و بر اکتور هایی که قابلیت های چارچوب akka را پیاده سازی کرده اند مانند لاگ و … نظارت میکند.
این شاخه نقطه ای است که کار ما شروع میشود و به عنوان یک توسعه دهنده تمام وقت ما در این قسمت صرف میشود. از نگاه کاربر، user/ ریشه ی اصلی اکتور سیستم است و معمولا به آن اکتور ریشه گفته میشود.
به عنوان یک کاربر لازم نیست نگران اکتور های نگهبان باشید. فقط باید مطمئن باشید که به طریق درستی از نظارت ذیل user/ استفاده کرده ایم تا خطا ها به سطح نگهبانان نرسد و کل سیستم را خراب نکند.
هرچه هست همین جا است! تمام اکتورهایی که در برنامه ی خود تعریف می کنیم اینجا قرار میگیرد:
اکتورهایی که مستقیما از user/ منشعب می شوند "top level actors" نامیده می شوند.
اکتور ها همیشه به عنوان فرزندان دیگر اکتور ها ساخته میشوند.
وقتی یک اکتور را مستقیما از کانتکست اکتور سیستم ایجاد میکنید ، آن اکتور یک تاپ لول اکتور میشود:
حالا اگر بخواهیم برای اکتور a2 فرزندی ایجاد کنیم با استفاده از context آن به صورت زیر عمل میکنیم:
هر اکتوری یک آدرس منحصر به فرد دارد. برای ارسال پیام از یک اکتور به اکتور دیگر لازم است که آدرس آن (ActorPath) را داشته باشید. برای یادآوری تصویر زیر ساختار آدرس یک اکتور را نمایش میدهد:
قسمت path از آدرس یک اکتور توضیحی است از جایی که اکتور در سلسله مراتب اکتور ها قرار گرفته است. هر سطح از این سلسله مراتب با یک / مشخص شده است.
برای مثال اگر روی localhost باشیم آدرس کامل اکتور b2 به صورت زیر است:
akka.tcp://MyActorSystem@localhost:9001/user/a1/b2
سوالی که ممکن است مطرح شود این است که "آیا اکتور ها فقط میتوانند در یک نقطه خاص از این سلسله قرار داشته باشند؟" مثلا اگر یک اکتورکلاس FooActor داشته باشیم آیا فقط میتوان آن را به عنوان فرزند BarActor در سلسله مراتب ایجاد کنیم؟
پاسخ این است که هر اکتور ممکن است هرجایی از سلسله مراتب قرار داده شود.
حالا که نحوه مدیریت اکتور ها در ساختار سلسله مراتبی را درک کرده ایم باید این را بدانیم که اکتور ها فقط فرزند مستقیم خود را نظارت میکنند و نوه ها و نتیجه ها و … را نظارت نمی کند.
موقعی که مشکلی پیدا شود! هر موقع که برای فرزند یک unhandled exception پیش بیاید و دچار خرابی شود به والدش خبر میدهد و میپرسد که چه کاری انجام دهد.
در واقع فرزند به والد خود یک پیام می فرستد که از نوع خرابی است (Failure class) و حالا با والد است که تصمیم بگیرد چه کاری انجام دهد.
دو عامل تعیین کننده برای حل مشکل وجود دارد:
هنگامی که یک اکتور والد پیام خرابی فرزندش را دریافت میکند ، میتواند بر اساس یکی از موارد(Directives) عمل نماید. استراتژی های نظارتی بر اساس خرابی های مختلف با دیرکتیو مربوط انطباق پیدا میکند و این به سیستم اجازه میدهد در حالت های مختلف به طرق مختلفی با مشکل مواجه شود.
انواع دیرکتیوهای نظارتی به صورت زیر است:
نکته قابل توجه در این مورد این است که هر اقدامی در والد اتخاذ شود ، آن تصمیم و اقدام به فرزندان منتشر می شود. مثلا اگر والدی متوقف شود تمام فرزندانش نیز متوقف خواهند شد و اگر راه اندازی مجدد شود تمام فرزندانش نیز دوباره راه اندازی میشوند.
دو استراتژی توکار وجود دارد
تفاوت اساسی بین این دو مورد در میزان شیوع این تاثیر توسط دستورالعمل مورد استفاده آن است.
در یکی برای یکی ، دستورالعملی که برای والد تعیین میشود فقط روی فرزندی که خراب است اعمال میشود و روی فرزند های دیگر آن والد تاثیری ندارد. این استراتژی پیش فرض است در صورتی که استراتژی دیگری تعیین نشود.
در همه برای یکی ، دستورالعملی که برای والد مشخص میشود برای فرزند خراب و همه ی فرزندان والد اعمال میشود.
یک مورد مهم دیگر در انتخاب استراتژی های نظارت این است که چند بار یک فرزند می تواند و مجاز است که در یک بازه زمانی مشخص، خراب شود قبل از اینکه به طور کامل متوقف شود. مثلا تنها 10 خطا در 60 ثانیه مجاز باشد تا متوقف نشود.
به مثال زیر توجه کنید:
تمام مطلب در استراتژی های نظارت این است که بتوان خرابی را داخل سیستم با استفاده از قابلیت خود التیام بخشی مهار کرد.
بدین صورت که عملیات هایی که بالقوه خطرناک هستند را از والد به فرزند منتقل میکنیم. فرزندی که تنها وظیفه اش انجام همام عملیات بالقوه خطرناک است.
فرض کنید سیستمی داریم که قرار است امتیازات جام جهانی فوتبال و آمار بازیکنان را نگهداری و پردازش کند و به صورت تناوبی باید با فراخوانی داده ها از api که فیفا در اختیار آن قرار داده بروز رسانی شود. این فراخوانی api همیشه با خطر همراه است. به این معنی که اگر این درخواست دچار خطا شود ، اکتوری که این درخواست را ایجاد کرده خراب میشود.
ما آمار و اعداد را در اکتور والد نگه داری میکنیم و کار فراخوانی api را به یک اکتور فرزند می سپاریم. در این حالت اگر فرزند دچار خرابی شود روی والد تاثیری ندارد و تمام داده های مهم و حساس در والد بصورت امن باقی میماند. با این کار وسعت خرابی را محدود می کنیم و از انتشار آن در کل سیستم جلوگیری میکنیم.
بخاطر داشته باشید که می توانیم چندین کلون از این ساختار داشته باشیم تا به صورت موازی مثلا برای هر بازی فوتبال داده ها را جمع کرده و دنبال کند و لازم نیست کد جدیدی برای این کار اضافه کنیم.
این یک سوال رایج است که اگر تعدادی پیام در صندوق اکتور ناظر منتظر پردازش باشند چقدر طول میکشد تا پیام خطا و خرابی فرزند به والد برسد؟ آیا فرزند نباید منتظر بماند تا پیام های صندوق والد پردازش شود تا برسد به پیام اکتور فرزند و پاسخ دهد؟
پاسخ: در واقع خیر. وقتی اکتوری پیام خطا برای ناظر خود ارسال میکند ، این پیام از نوع ویژه ای از پیام های سیستمی به ناظر میرسد و در اول صف پیام های صندوق ناظر قرار میگیرد و قبل از اینکه ناظر به حالت عادی برگردد پردازش میشود.
بدون در نظر گرفتن اینکه خرابی در اکتور پردازنده ی پیام اتفاق افتاده یا در والد آن که منجر به توقف آن شده ، پیام می تواند ذخیره شود تا دوباره بعد از راه اندازی مجدد اکتور، پردازش شود. چند راه برای این موضوع وجود دارد. یکی از رایج ترین روش های موجود استفاده از متد preRestart است. اکتور میتواند پیام را به خودش ارسال کند. در این حالت پیام به جای محل ذخیره موقت در حافظه، به صندوق ماندگار منتقل میشود (persistent mailbox).
اگر درست عمل کنید، اکتور سیستم شما خود ترمیم کننده ای خوب میشود و به طور باور نکردنی انعطاف پذیر و میشود و هر نوع خطا و خرابی را تحمل میکند و شما یک نفس راحت خواهید کشید.
حالا باید یک درک کلی و خوب از این داشته باشید که نظارت در اکتور سیستم چطور عمل میکند و چطور یک سیستم منعطف اما برگشت پذیر را میتوان با این روش ایجاد کرد.