هنگام ساخت سیستمهای توزیعشده با Go و gRPC، در دسترس بودن (Availability) حیاتی است. صرفاً شروع یک سرور و امیدوار به ادامه کار آن کافی نیست. در محیطهای ابری مدرن، ابزارهای ارکستراسیون مانند Kubernetes یا مشهای سرویس مانند Istio به شدت به مدیریت دقیق چرخه عمر وابستهاند. این امر نیازمند پیادهسازی پروتکلهای بررسی سلامت قوی و اطمینان از خاموشی تدریجی سرویسها برای جلوگیری از از دست رفتن داده یا قطع اتصالات است.
در این پست، ما بررسی خواهیم کرد که چگونه پروتکل استاندارد grpc-health را در سرورهای Go خود ادغام کنید و چگونه سیگنالها را برای یک خاموشی تمیز مدیریت نمایید. ما فراتر از پیادهسازیهای پایه حرکت کرده تا چالشهای همزمانی (Concurrency)، مانند شرایط رقابتی (Race Conditions) در حین خاموشی را برطرف کنیم.
پیادهسازی سرویس بررسی سلامت gRPC
اکوسیستم gRPC یک پروتکل بررسی سلامت استاندارد در grpc/health/v1 تعریف کرده است. به جای نوشتن نقاط پایانی (Endpoints) سفارشی برای بررسی آماده بودن سرویس خود، باید این رابط استاندارد را پیادهسازی کنید. این کار به لودبالانسرها و ارکستراتورها اجازه میدهد تا سرویس را بدون نیاز به دانستن منطق داخلی دامنه کسبوکار شما، بررسی کنند.
برای شروع، باید بسته سلامت را نصب کنید. فرض بر این است که از ماژولهای Go استفاده میکنید، بنابراین دستور زیر را اجرا کنید:
go get google.golang.org/grpc/health
go get google.golang.org/grpc/health/grpc_health_v1
پس از نصب، میتوانید سرویس سلامت را با سرور gRPC خود ثبت کنید. سرویس سلامت یک نقشه از نامهای سرویس به وضعیتهای سلامت آنها نگه میدارد. به طور پیشفرض، سرویس را تحت یک رشته خالی یا نام خاصی که در پروتوباف (Protobuf) خود تعریف کردهاید، ثبت میکنید.
در اینجا یک پیادهسازی عملی از یک بررسیکننده سلامت که وضعیت کلی برنامه شما را پایش میکند، آورده شده است:
package main
import (
"log"
"net"
"time"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/health"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
// سرویسهای کسبوکار خود را اینجا ثبت کنید
// pb.RegisterMyServiceServer(s, &myServer{})
// راهاندازی سرویس سلامت
healthSvc := health.NewServer()
// وضعیت پیشفرض را روی SERVING تنظیم کنید
// این به ارکستراتورها میگوید که سرور آماده است
healthSvc.SetServingStatus("", health.CheckResponse_SERVING)
// سرویس سلامت را با سرور gRPC ثبت کنید
health.RegisterHealthServer(s, healthSvc)
log.Println("Starting gRPC server on 50051...")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
این پیکربندی به هر کلاینتی اجازه میدهد تا grpc.health.v1.Health.Check را فراخوانی کند تا تعیین کند آیا سرور ترافیک را میپذیرد یا خیر. اگرچه پیادهسازی پیشفرض ساده است، در سناریوهای پیچیده، ممکن است بخواهید روش Check را برای بررسی اتصال به پایگاه داده یا در دسترس بودن سرویسهای پاییندستی قبل از گزارش وضعیت "SERVING" بازنویسی کنید.
مدیریت خاموشی تدریجی
توقف ناگهانی یک سرور gRPC میتواند منجر به درخواستهای نیمهپردازش شده، اتصالات شکسته و ناسازگاری دادهها شود. خاموشی تدریجی شامل توقف پذیرش RPCهای جدید، تخلیه اتصالات موجود و سپس خاموش کردن شنونده (Listener) است.
کتابخانه استاندارد Go دارای os/signal برای مدیریت سیگنالهای سیستمعامل مانند SIGINT (Ctrl+C) و SIGTERM (که توسط Kubernetes استفاده میشود) است. با این حال، جریان خاموشی استاندارد اغلب از یک شرایط رقابتی رنج میبرد: برنامه ممکن است تلاش کند سرویس سلامت را قبل از اینکه سرور gRPC کاملاً متوقف شود، خاموش کند که منجر به خطاها یا قفلشدگی (Deadlocks) میشود.
برای حل این مشکل، باید اطمینان حاصل کنیم که سرویس سلامت پس از اینکه سرور اصلی gRPC فرآیند خاموشی خود را به پایان رساند، متوقف میشود. در اینجا یک الگوی قوی برای مدیریت این موضوع آورده شده است:
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/health"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
healthSvc := health.NewServer()
healthSvc.SetServingStatus("", health.CheckResponse_SERVING)
health.RegisterHealthServer(s, healthSvc)
// شروع سرور در یک Goroutine
go func() {
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()
// ایجاد یک کانال برای گوش دادن به سیگنالهای وقفه
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
// تا دریافت سیگنال بلوکه شوید
sig := <-stop
log.Printf("Received signal: %v, initiating graceful shutdown...", sig)
// ایجاد یک Context با مهلت زمانی برای فرآیند خاموشی
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 1. توقف پذیرش RPCهای جدید
s.GracefulStop()
// 2. بهروزرسانی وضعیت سلامت به NOT_SERVING فوراً
// این اطمینان حاصل میکند که لودبالانسرها ترافیک را مسیریابی نمیکنند
healthSvc.SetServingStatus("", health.CheckResponse_NOT_SERVING)
// 3. توقف سرویس سلامت
// نکته: این باید پس از s.GracefulStop() یا در یک روتین جداگانه رخ دهد
// تا از شرایط رقابتی جلوگیری شود.
healthSvc.Shutdown()
log.Println("Shutdown complete.")
}
بهترین شیوهها برای محیط تولید
هنگام پیادهسازی این الگوها، نکات زیر را به خاطر داشته باشید:
- مهلتهای زمانی حیاتی هستند: همیشه یک حداکثر مدت زمان برای دوره مهلت خاموشی خود تعریف کنید. اگر سرویس شما هنگ کرد، نمیخواهید ارکستراتور به طور نامحدود منتظر بماند.
- دانهدانی بررسی سلامت: برای میکروسرویسهای حیاتی، در نظر بگیرید که پروبهای زنده بودن (Liveness) و آمادگی (Readiness) را به صورت جداگانه پیادهسازی کنید. بررسیهای زنده بودن کانتینرهای خراب را راهاندازی مجدد میکنند، در حالی که بررسیهای آمادگی از ترافیک ورودی به نمونههای آماده جلوگیری میکنند.
- ایمنی همزمانی: ترتیب عملیات در حین خاموشی مهم است. همیشه اطمینان حاصل کنید که سرویسهای داخلی به ترتیب وابستگی اطلاعرسانی یا متوقف میشوند تا از نشت منابع جلوگیری شود.
نتیجهگیری
پیادهسازی بررسیهای سلامت gRPC و خاموشی تدریجی فقط یک بهترین شیوه نیست؛ بلکه ضروری برای ساخت میکروسرویسهای مقاوم در Go است. با بهرهگیری از پروتکل سلامت استاندارد و مدیریت دقیق سیگنالها، اطمینان حاصل میکنید که سرویسهای شما به طور بینقص با ابزارهای زیرساخت مدرن یکپارچه میشوند. این رویکرد زمان خاموشی را به حداقل میرساند، از شکستهای جزئی درخواست جلوگیری میکند و دید روشنی درباره سلامت سیستم شما فراهم میآورد.