چگونه برنامه های Go را به زیبایی خاتمه دهیم – راهنمای خاموش شدن های زیبا
آیا تا به حال با ناامیدی سیم برق را از کامپیوتر خود بیرون کشیده اید؟ در حالی که این ممکن است راه حلی سریع برای مشکلات خاص به نظر برسد، اما می تواند منجر به از دست دادن داده ها و بی ثباتی سیستم شود.
در دنیای نرم افزار، مفهوم مشابهی وجود دارد: خاموش شدن سخت. این خاتمه ناگهانی می تواند مانند همتای فیزیکی خود مشکلاتی ایجاد کند. خوشبختانه، راه بهتری وجود دارد: خاموشی دلپذیر.
برای برنامههایی که در محیطهای هماهنگ (مانند Kubernetes) مستقر میشوند، مدیریت برازنده سیگنالهای خاتمه بسیار مهم است.
با ادغام خاموش شدن برازنده، اعلان قبلی را به سرویس ارائه می دهید. این امکان را به آن میدهد تا درخواستهای جاری را تکمیل کند، به طور بالقوه اطلاعات وضعیت را روی دیسک ذخیره کند و در نهایت از خراب شدن دادهها در حین خاموش شدن جلوگیری کند.
در این راهنما، ما به دنیای خاموش شدنهای دلپذیر، بهویژه بر اجرای آنها در برنامههای Go که در Kubernetes اجرا میشوند، میپردازیم.
سیگنال ها در سیستم های یونیکس
یکی از ابزارهای کلیدی برای دستیابی به خاموشی های دلپذیر در سیستم های مبتنی بر یونیکس، مفهوم سیگنال ها است. اینها، در اصطلاح اساسی، یک راه ساده برای برقراری ارتباط یک چیز خاص به یک فرآیند، از یک فرآیند دیگر هستند.
با درک نحوه عملکرد سیگنالها، میتوانید از آنها برای پیادهسازی رویههای خاتمه کنترلشده در برنامههای خود استفاده کنید، و از فرآیند خاموش کردن هموار و ایمن برای دادهها اطمینان حاصل کنید.
سیگنال های زیادی وجود دارد، و می توانید آنها را در اینجا پیدا کنید. اما نگرانی ما در این مقاله فقط سیگنال های خاموش شدن است:
SIGTERM - برای درخواست خاتمه آن به فرآیند ارسال می شود. بیشتر مورد استفاده قرار می گیرد و بعداً روی آن تمرکز خواهیم کرد.
SIGKILL - "فوراً ترک کنید"، نمی توان مداخله کرد.
SIGINT – سیگنال وقفه (مانند Ctrl+C)
SIGQUIT – سیگنال خروج (مانند Ctrl+D)
این سیگنال ها را می توان از کاربر (Ctrl+C / Ctrl+D)، از برنامه/فرآیند دیگر یا از خود سیستم (هسته / سیستم عامل) ارسال کرد. به عنوان مثال، یک خطای تقسیم بندی SIGSEGV با نام مستعار توسط سیستم عامل ارسال می شود.
خدمات خوکچه هندی ما
برای کاوش در دنیای خاموش شدن های دلپذیر در یک محیط عملی، بیایید یک سرویس ساده ایجاد کنیم که بتوانیم آن را آزمایش کنیم. این سرویس "خوکچه هندی" یک نقطه پایانی واحد خواهد داشت که با فراخوانی دستور INCR Redis، برخی از کارهای دنیای واقعی را شبیهسازی میکند (با کمی تأخیر اضافه میکنیم). ما همچنین یک پیکربندی اولیه Kubernetes را برای آزمایش نحوه مدیریت پلتفرم سیگنالهای خاتمه ارائه میکنیم.
هدف نهایی: اطمینان حاصل کنیم که سرویس ما با ظرافت خاموش شدن را بدون از دست دادن هیچ درخواست/داده ای کنترل می کند. با مقایسه تعداد درخواستهای ارسال شده به موازات با مقدار شمارنده نهایی در Redis، میتوانیم تأیید کنیم که آیا اجرای خاموش کردن برازنده ما موفقیت آمیز است یا خیر.
ما وارد جزئیات راهاندازی خوشه Kubernetes و Redis نمیشویم، اما میتوانید تنظیمات کامل را در این مخزن Github بیابید.
فرآیند تأیید به شرح زیر است:
برنامه Redis و Go را در Kubernetes مستقر کنید.
برای ارسال 1000 درخواست (25 در ثانیه در 40 ثانیه) از گیاه گیاهی استفاده کنید.
در حالی که vegeta در حال اجرا است، با بهروزرسانی برچسب تصویر، یک بهروزرسانی Rolling Kubernetes را راهاندازی کنید.
به Redis متصل شوید تا "counter" را تأیید کنید، باید 1000 باشد.
بیایید با Go HTTP Server پایه خود شروع کنیم.
hard-shutdown/main.go:
package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main () { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv( "REDIS_ADDR" ), }) server := http.Server{ Addr: ":8080" , } http.HandleFunc( "/incr" , func (w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest (redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5 ) redisdb.Incr( "counter" ) }
هنگامی که روش تأیید خود را با استفاده از این کد اجرا می کنیم، خواهیم دید که برخی از درخواست ها با شکست مواجه می شوند و شمارنده کمتر از 1000 است (تعداد ممکن است در هر اجرا متفاوت باشد).
که به وضوح به این معنی است که ما برخی از داده ها را در طول به روز رسانی چرخان از دست دادیم. 😢
نحوه مدیریت سیگنال ها در Go
Go یک بسته سیگنال ارائه می دهد که به شما امکان می دهد سیگنال های یونیکس را مدیریت کنید. توجه به این نکته ضروری است که به طور پیش فرض سیگنال های SIGINT و SIGTERM باعث خروج برنامه Go می شوند. و برای اینکه برنامه Go ما به این صورت ناگهانی خارج نشود، باید سیگنال های دریافتی را مدیریت کنیم.
دو گزینه برای انجام این کار وجود دارد.
اولین مورد استفاده از کانال:
c := make ( chan os.Signal, 1 ) signal.Notify(c, syscall.SIGTERM)
دوم استفاده از زمینه است (رویکرد ترجیحی امروزه):
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()
NotifyContext یک کپی از زمینه والد را برمیگرداند که علامتگذاری شده است (کانال Done آن بسته است) زمانی که یکی از سیگنالهای فهرست شده میرسد، زمانی که تابع stop() برگشتی فراخوانی میشود، یا زمانی که کانال Done زمینه اصلی بسته میشود – هر کدام که اول اتفاق بیفتد. .
مشکلات کمی در اجرای فعلی سرور HTTP ما وجود دارد:
ما processRequest
کندی داریم و از آنجایی که سیگنال خاتمه را کنترل نمی کنیم، برنامه به طور خودکار خارج می شود. این به این معنی است که تمام گوروتین های در حال اجرا نیز خاتمه می یابند.
برنامه هیچ اتصالی را نمی بندد.
بیایید آن را بازنویسی کنیم.
graceful-shutdown/main.go:
package main // imports var wg sync.WaitGroup func main () { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc( "/incr" , func (w http.ResponseWriter, r *http.Request) { wg.Add( 1 ) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf( "could not shutdown: %v\n" , err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit( 0 ) } func processRequest (redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5 ) redisdb.Incr( "counter" ) }
در اینجا خلاصه به روز رسانی ها آمده است:
signal.NotifyContext برای گوش دادن به سیگنال خاتمه SIGTERM اضافه شد.
یک sync.WaitGroup را برای ردیابی درخواستهای حین پرواز (processRequest goroutines) معرفی کرد.
سرور را در یک گوروتین پیچیده و از سرور استفاده کرد. خاموش کردن با زمینه برای جلوگیری از پذیرش اتصالات جدید.
از wg.Wait() برای اطمینان از اتمام تمام درخواستهای حین پرواز (processRequest goroutines) قبل از ادامه استفاده شد.
Resource Cleanup: () redisdb.Close اضافه شد تا اتصال Redis را قبل از خروج به درستی ببندد.
Clean Exit: از os.Exit(0) برای نشان دادن پایان موفقیت آمیز استفاده می شود.
حال اگر روند تایید خود را تکرار کنیم، خواهیم دید که تمامی 1000 درخواست به درستی پردازش شده اند. 🎉
چارچوب های وب / کتابخانه HTTP
چارچوبهایی مانند اکو، جین، فیبر و موارد دیگر برای هر درخواست دریافتی، یک گوروتین ایجاد میکنند. این به آن یک زمینه می دهد و سپس تابع / کنترل کننده شما را بسته به مسیری که تصمیم گرفتید فراخوانی می کند. در مورد ما، این تابع ناشناس خواهد بود که برای مسیر "/incr" به HandleFunc داده می شود.
وقتی سیگنال SIGTERM را رهگیری میکنید و از چارچوب خود میخواهید که بهخوبی خاموش شود، دو چیز مهم اتفاق میافتد (برای سادهسازی بیش از حد):
فریمورک شما درخواست های دریافتی را نمی پذیرد
منتظر می ماند تا درخواست های دریافتی موجود تمام شود (به طور ضمنی منتظر پایان گوروتین ها است).
توجه: Kubernetes همچنین هدایت ترافیک ورودی از Loadbalancer به pod شما را پس از اینکه آن را به عنوان Terminating برچسب گذاری کرد متوقف می کند.
اختیاری: زمان خاموش شدن
خاتمه یک فرآیند می تواند پیچیده باشد، به خصوص اگر مراحل زیادی مانند بستن اتصالات وجود داشته باشد. برای اطمینان از اینکه کارها به خوبی پیش می روند، می توانید یک بازه زمانی تعیین کنید. این وقفه به عنوان یک شبکه ایمنی عمل میکند و اگر بیشتر از حد انتظار طول بکشد، بهراحتی از فرآیند خارج میشود.
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10 *time.Second) defer cancel() go func () { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf( "could not shutdown: %v\n" , err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln( "timeout exceeded, forcing shutdown" ) } os.Exit( 0 ) }
چرخه عمر خاتمه Kubernetes
از آنجایی که ما از Kubernetes برای استقرار سرویس خود استفاده کردیم، بیایید عمیقتر به نحوه پایان دادن به پادها بپردازیم. هنگامی که Kubernetes تصمیم به پایان دادن به پاد گرفت، رویدادهای زیر رخ خواهند داد:
Pod روی حالت "خاتمه" تنظیم شده و از فهرست نقاط پایانی همه سرویس ها حذف می شود.
PreStop Hook در صورت تعریف اجرا می شود.
سیگنال SIGTERM به غلاف ارسال می شود. اما هی، اکنون برنامه ما می داند که چه کاری انجام دهد!
Kubernetes منتظر یک دوره مهلت ( terminationGracePeriodSeconds ) است که به طور پیش فرض 30 ثانیه است.
سیگنال SIGKILL به غلاف ارسال می شود و غلاف حذف می شود.
همانطور که می بینید، اگر یک فرآیند خاتمه طولانی مدت دارید، ممکن است لازم باشد تنظیمات پایان GracePeriodSeconds را افزایش دهید . این اجازه می دهد تا برنامه شما زمان کافی برای خاموش شدن برازنده داشته باشد.
نتیجه گیری
خاموشیهای زیبا از یکپارچگی دادهها محافظت میکنند، تجربه کاربری یکپارچه را حفظ میکنند و مدیریت منابع را بهینه میکنند. Go با کتابخانه استاندارد غنی خود و تاکید بر همزمانی، به توسعه دهندگان این امکان را می دهد تا بدون زحمت شیوه های خاموش کردن برازنده را ادغام کنند - یک ضرورت برای برنامه های کاربردی مستقر در محیط های کانتینری یا هماهنگ شده مانند Kubernetes.
می توانید کد Go و مانیفست های Kubernetes را در این مخزن Github پیدا کنید.
منابع
مقالات بیشتری را از packagemain.tech کاوش کنید
ارسال نظر