با بالغ شدن معماریهای میکروسرویس، انتخاب پروتکل ارتباطی به یک تصمیم معماری حیاتی تبدیل میشود. در حالی که REST و JSON همچنان فراگیر هستند، نیاز به سرویسهای با تأخیر کم و عملکرد بالا اغلب رویکردی کارآمدتر را ایجاب میکند. gRPC وارد میشود، یک چارچوب RPC جهانی منبعباز با عملکرد بالا که توسط گوگل نگهداری میشود. وقتی با کارایی و مدل همزمانی Go ترکیب شود، gRPC راهحلی مستحکم برای ساخت سیستمهای توزیعشدهای ارائه میدهد که میتوانند بهطور بینقصی مقیاسپذیر باشند.
این پست بررسی میکند که چگونه میتوان از gRPC در Go بهطور مؤثر استفاده کرد و از پیادهسازیهای پایه فراتر رفته و بهترین شیوهها برای تعریف سرویس، تولید کد و مدیریت سناریوهای پیچیده استریمینگ را مورد بحث قرار میدهد.
چرا gRPC به جای REST؟
قبل از غوطهور شدن در پیادهسازی، درک مزایای فنی که gRPC به یک بکاند Go میآورد ضروری است. تفاوت اصلی در فرمت سریالسازی است. gRPC از پروتکل بافرها (Protobuf) استفاده میکند، که مکانیسمی قابل گسترده، مستقل از زبان و مستقل از پلتفرم برای سریالسازی دادههای ساختاریافته است. در مقایسه با JSON:
- پیکربندیهای کوچکتر: پیامهای Protobuf باینری هستند و بهطور قابلتوجهی فشردهتر از همتایان JSON خود میباشند که باعث کاهش مصرف پهنای باند میشود.
- تایپسازی قوی: ماهیت تعریفشده با طرح (Schema) Protobuf، ایمنی نوع را بین سرویسها تضمین میکند و خطاهای زمان اجرا ناشی از JSON نادرست را کاهش میدهد.
- عملکرد: سریالسازی/دیسریالسازی باینری بسیار سریعتر از تجزیه JSON مبتنی بر متن است که منجر به کاهش بار پردازنده میشود.
علاوه بر این، gRPC بر پایه HTTP/2 ساخته شده است که ویژگیهایی مانند چندگانهسازی (Multiplexing)، فشردهسازی هدر و سرور پش (Server Push) را فراهم میکند که همگی به عملکرد برتر در محیطهای با همزمانی بالا، که معمولاً در برنامههای Go دیده میشود، کمک میکنند.
تعریف قرارداد سرویس شما
پایه هر پیادهسازی gRPC، فایل .proto است. این فایل به عنوان منبع حقیقت واحد برای قرارداد API شما عمل میکند. در پروژههای Go، این معمولاً مرسوم است که این فایلها را در یک دایرکتوری اختصاصی نگه دارید.
syntax = "proto3";
package user;
option go_package = "./proto";
service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc ListUsers (Empty) returns (stream UserResponse);
rpc CreateUser (stream UserRequest) returns (UserResponse);
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
message GetUserRequest {
string id = 1;
}
message UserResponse {
User user = 1;
}
message Empty {}
message UserRequest {
string name = 1;
string email = 2;
}
در این مثال، ما یک UserService با سه نوع RPC متمایم تعریف میکنیم: یونری (درخواست-پاسخ استاندارد)، استریمینگ سمت سرور (stream UserResponse) و استریمینگ سمت کلاینت (stream UserRequest). این انعطافپذیری به توسعهدهندگان اجازه میدهد تا کارآمدترین الگوی ارتباطی را برای موارد استفاده خاص، مانند آپلود مجموعههای داده بزرگ از طریق استریمینگ کلاینت، انتخاب کنند.
تولید کد Go و پیادهسازی
پس از تعریف فایل .proto، شما کد Go را با استفاده از کامپایلر protoc به همراه پلاگین Go تولید میکنید. این فرآیند، استابهای رابطی را که سرور Go شما باید پیادهسازی کند و کد کلاینتی که سایر سرویسها از آن استفاده میکنند، تولید میکند.
protoc --go_out=. --go-grpc_out=. proto/user.proto
در سمت سرور، شما رابط تولید شده را پیادهسازی میکنید. چندریختی مبتنی بر رابط در Go، این ادغام را تمیز و ایڈیوماتیک میکند.
type server struct {
pb.UnimplementedUserServiceServer
users map[string]*pb.User
}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
user, exists := s.users[req.Id]
if !exists {
return nil, status.Error(codes.NotFound, "user not found")
}
return &pb.UserResponse{User: user}, nil
}
به استفاده از context.Context توجه کنید که بومی Go است. این به شما امکان میدهد لغو، مهلتها و ارسال متادیتا را بهطور کارآمد مدیریت کنید که جنبهای حیاتی از ارتباطات میکروسرویس مقاوم است.
استریمینگ برای سناریوهای با عملکرد بالا
یکی از قدرتمندترین ویژگیهای gRPC، استریمینگ دوطرفه است. برخلاف REST که معمولاً پس از پاسخ، اتصال را بسته نگه میدارد، استریمهای gRPC اجازه میدهند دادهها بهطور پیوسته در هر دو جهت جریان یابند. این برای برنامههای بلادرنگ مانند سیستمهای چت یا جریانهای داده زنده ایدهآل است.
func (s *server) ListUsers(req *pb.Empty, stream pb.UserService_ListUsersServer) error {
for _, user := range s.users {
if err := stream.Send(&pb.UserResponse{User: user}); err != nil {
return err
}
}
return nil
}
هنگام پیادهسازی پردازشکنندههای استریم، مدیریت خطا حیاتی میشود. اگر استریم در میانه قطع شود، سرور باید بهطور شایسته با خطا برخورد کند و ممکن است روالهای پاکسازی را فعال کند. دستورات defer در Go در اینجا برای اطمینان از آزادسازی منابع، صرفنظر از موفقیت یا شکست، بهویژه مفید هستند.
نتیجهگیری
پذیرش gRPC در معماری میکروسرویس Go شما، مزایای قابلتوجهی از نظر عملکرد، ایمنی نوع و تجربه توسعهدهنده ارائه میدهد. با بهرهگیری از پروتکل بافرها و HTTP/2، توسعهدهندگان Go میتوانند سیستمهایی بسازند که نه تنها سریعتر، بلکه قابلنگهداریتر و مقیاسپذیرتر هستند. اگرچه منحنی یادگیری برای سینتکس Protobuf و مفاهیم استریمینگ وجود دارد، اما سودهای بلندمدت در کارایی سیستم و کاهش پیچیدگی عملیاتی، آن را به سرمایهگذاری ارزشمندی برای تیمهای مهندسی متوسط تا پیشرفته تبدیل میکند.
هنگام حرکت به جلو، در نظر بگیرید که gRPC را با ابزارهایی مانند Jaeger برای ردیابی توزیعشده و Envoy برای مدیریت Service Mesh ادغام کنید تا قابلیت مشاهده و قابلیت اطمینان میکروسرویسهای مبتنی بر gRPC خود را بیشتر بهبود بخشید.