Microservices on Go مع مجموعة Go: مقدمة

في هذه المقالة سأصف استخدام مجموعة أدوات Go ، وهي مجموعة من الأدوات والمكتبات لإنشاء خدمات صغيرة على Go. هذه المقالة مقدمة لمجموعة أدوات Go. الجزء الأول في مدونتي ، كود المصدر للأمثلة متاح هنا .


يتم اختيار Go بشكل متزايد لتطوير الأنظمة الموزعة الحديثة. عندما تقوم بتطوير نظام موزع على السحابة ، قد تحتاج إلى دعم وظائف محددة متنوعة في خدماتك ، مثل: بروتوكولات النقل المختلفة ( إلخ ، HTTP ، gRPC ، إلخ ) وتنسيقات ترميز الرسائل لها ، وموثوقية RPC ، وتسجيل ، التتبع ، المقاييس والتنميط ، مقاطعة الطلبات ، تحديد عدد الطلبات ، الاندماج في البنية التحتية ، وحتى وصف البنية. Go هي لغة شائعة بسبب بساطتها ونهجها "لا سحر" ، وبالتالي فإن حزم Go ، على سبيل المثال ، مكتبة قياسية ، هي بالفعل أكثر ملاءمة لتطوير أنظمة موزعة من استخدام إطار كامل مع الكثير من "السحر تحت غطاء المحرك". أنا شخصياً [ تقريبًا. عبر. Shiju Varghese ] أنا لا أؤيد استخدام أطر العمل الكاملة ، أفضل استخدام المكتبات التي تمنح المزيد من الحرية للمطور. ملأت مجموعة Go الفجوة في النظام البيئي Go ، مما يجعل من الممكن استخدام مجموعة من المكتبات والحزم عند إنشاء الخدمات الدقيقة ، والتي بدورها تسمح باستخدام المبادئ الجيدة لتصميم الخدمات الفردية في الأنظمة الموزعة.


الصورة


مقدمة لمجموعة أدوات Go


Go kit عبارة عن مجموعة من حزم Go التي تجعل من السهل إنشاء خدمات دقيقة موثوقة ومدعومة. توفر Go kit مكتبات لتنفيذ مكونات متنوعة من بنية تطبيق شفافة وموثوقة ، باستخدام طبقات مثل: تسجيل الدخول ، والمقاييس ، والتتبع ، والحد من طلبات المقاطعة الضرورية لتشغيل الخدمات الصغيرة على المنتج. مجموعة أدوات Go جيدة لأنها تحتوي على أدوات جيدة التنفيذ للتفاعل مع مختلف البنى التحتية وتنسيقات ترميز الرسائل وطبقات النقل المختلفة.


بالإضافة إلى مجموعة المكتبات لخدمات العالم النامي ، فإنه يوفر ويشجع على استخدام المبادئ الجيدة لتصميم بنية خدماتك. تساعدك مجموعة أدوات Go على الالتزام بمبادئ SOLID ، والنهج الموجه نحو الموضوع (DDD) والعمارة السداسية التي اقترحها أليستير كوكبيرن أو أي نهج آخر من المبادئ المعمارية المعروفة باسم " هندسة البصل " من قبل جيفري باليرمو و " العمارة النظيفة " من قبل روبرت سي مارتن . على الرغم من أن مجموعة Go تم تصميمها كمجموعة من الحزم لتطوير الخدمات الدقيقة ، إلا أنها مناسبة أيضًا لتطوير متجانسة أنيقة.


مجموعة Go Architecture


المستويات الرئيسية الثلاثة في بنية التطبيقات التي تم تطويرها باستخدام مجموعة أدوات Go هي:


  • مستوى النقل
  • مستوى نقطة النهاية
  • مستوى الخدمة

مستوى النقل


عندما تكتب خدمات صغيرة للأنظمة الموزعة ، غالبًا ما يتعين على الخدمات فيها التواصل مع بعضها البعض باستخدام بروتوكولات النقل المختلفة ، مثل: HTTP أو gRPC ، أو استخدام أنظمة النشر / الفرعية ، مثل NATS. ترتبط طبقة النقل في مجموعة Go ببروتوكول نقل محدد (يشار إليه فيما بعد بالنقل). تدعم مجموعة أدوات النقل وسائل النقل المختلفة لخدمتك ، مثل: HTTP و gRPC و NATS و AMQP و Thirft ( تقريبًا. يمكنك أيضًا تطوير وسيلة النقل الخاصة بك لبروتوكولك ). لذلك ، غالبًا ما تركز الخدمات المكتوبة باستخدام مجموعة Go على تنفيذ منطق عمل محدد لا يعرف أي شيء عن النقل المستخدم ، ولديك مطلق الحرية في استخدام وسائل نقل مختلفة لنفس الخدمة. كمثال ، يمكن لخدمة واحدة مكتوبة في مجموعة Go توفير الوصول إليها في وقت واحد عبر HTTP و gRPC.


نقاط النهاية


نقطة النهاية أو نقطة النهاية هي لبنة البناء الأساسية للخدمات والعملاء. في مجموعة أدوات Go ، يكون نمط الاتصال الرئيسي هو RPC. يتم تقديم نقطة النهاية كطريقة RPC منفصلة. يتم تحويل كل طريقة خدمة في مجموعة Go إلى نقطة نهاية ، مما يتيح لك التواصل بين الخادم والعميل بأسلوب RCP. تكشف كل نقطة نهاية طريقة خدمة باستخدام طبقة النقل ، والتي بدورها تستخدم بروتوكولات نقل مختلفة ، مثل HTTP أو gRPC. يمكن كشف نقطة نهاية منفصلة خارج الخدمة في وقت واحد باستخدام العديد من وسائل النقل ( تقريبًا لكل HTTP و gRPC على منافذ مختلفة ).


الخدمات


يتم تنفيذ منطق الأعمال في طبقة الخدمة. تم تصميم الخدمات المكتوبة باستخدام مجموعة Go كواجهات. يحتوي منطق الأعمال في طبقة الخدمة على جوهر منطق الأعمال الأساسي ، والذي لا يحتاج إلى معرفة أي شيء عن نقاط النهاية المستخدمة أو بروتوكول نقل محدد ، مثل HTTP أو gRPC ، أو حول طلبات الترميز أو فك الترميز والردود على أنواع مختلفة من الرسائل. سيسمح لك ذلك بالالتزام بهندسة نظيفة في الخدمات المكتوبة باستخدام مجموعة Go. يتم تحويل كل طريقة خدمة إلى نقطة نهاية باستخدام محول وتعريضها للخارج باستخدام وسيلة نقل معينة. من خلال استخدام العمارة النظيفة ، يمكن تعيين طريقة واحدة باستخدام وسائل نقل متعددة في نفس الوقت.


أمثلة


والآن دعونا نلقي نظرة على الطبقات الموضحة أعلاه باستخدام مثال لتطبيق بسيط.


منطق الأعمال في الخدمة


تم تصميم منطق الأعمال في الخدمة باستخدام الواجهات. سنلقي نظرة على مثال لأمر في التجارة الإلكترونية:


// Service describes the Order service. type Service interface { Create(ctx context.Context, order Order) (string, error) GetByID(ctx context.Context, id string) (Order, error) ChangeStatus(ctx context.Context, id string, status string) error } 

تعمل واجهة خدمة الطلب مع كيان مجال الطلب:


 // Order represents an order type Order struct { ID string `json:"id,omitempty"` CustomerID string `json:"customer_id"` Status string `json:"status"` CreatedOn int64 `json:"created_on,omitempty"` RestaurantId string `json:"restaurant_id"` OrderItems []OrderItem `json:"order_items,omitempty"` } // OrderItem represents items in an order type OrderItem struct { ProductCode string `json:"product_code"` Name string `json:"name"` UnitPrice float32 `json:"unit_price"` Quantity int32 `json:"quantity"` } // Repository describes the persistence on order model type Repository interface { CreateOrder(ctx context.Context, order Order) error GetOrderByID(ctx context.Context, id string) (Order, error) ChangeOrderStatus(ctx context.Context, id string, status string) error } 

هنا نقوم بتنفيذ واجهة خدمة الطلب:


 package implementation import ( "context" "database/sql" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/gofrs/uuid" ordersvc "github.com/shijuvar/gokit-examples/services/order" ) // service implements the Order Service type service struct { repository ordersvc.Repository logger log.Logger } // NewService creates and returns a new Order service instance func NewService(rep ordersvc.Repository, logger log.Logger) ordersvc.Service { return &service{ repository: rep, logger: logger, } } // Create makes an order func (s *service) Create(ctx context.Context, order ordersvc.Order) (string, error) { logger := log.With(s.logger, "method", "Create") uuid, _ := uuid.NewV4() id := uuid.String() order.ID = id order.Status = "Pending" order.CreatedOn = time.Now().Unix() if err := s.repository.CreateOrder(ctx, order); err != nil { level.Error(logger).Log("err", err) return "", ordersvc.ErrCmdRepository } return id, nil } // GetByID returns an order given by id func (s *service) GetByID(ctx context.Context, id string) (ordersvc.Order, error) { logger := log.With(s.logger, "method", "GetByID") order, err := s.repository.GetOrderByID(ctx, id) if err != nil { level.Error(logger).Log("err", err) if err == sql.ErrNoRows { return order, ordersvc.ErrOrderNotFound } return order, ordersvc.ErrQueryRepository } return order, nil } // ChangeStatus changes the status of an order func (s *service) ChangeStatus(ctx context.Context, id string, status string) error { logger := log.With(s.logger, "method", "ChangeStatus") if err := s.repository.ChangeOrderStatus(ctx, id, status); err != nil { level.Error(logger).Log("err", err) return ordersvc.ErrCmdRepository } return nil } 

طلبات وإجابات نقاط نهاية RPC


يتم كشف طرق الخدمة كنقاط نهاية RPC. لذلك نحن بحاجة إلى تحديد أنواع الرسائل ( تقريبًا لكل DTO - كائن نقل البيانات ) التي سيتم استخدامها لإرسال واستقبال الرسائل من خلال نقاط نهاية RPC. دعنا الآن نحدد الهياكل لأنواع الطلبات والاستجابات لنقاط نهاية RPC في خدمة الطلب:


 // CreateRequest holds the request parameters for the Create method. type CreateRequest struct { Order order.Order } // CreateResponse holds the response values for the Create method. type CreateResponse struct { ID string `json:"id"` Err error `json:"error,omitempty"` } // GetByIDRequest holds the request parameters for the GetByID method. type GetByIDRequest struct { ID string } // GetByIDResponse holds the response values for the GetByID method. type GetByIDResponse struct { Order order.Order `json:"order"` Err error `json:"error,omitempty"` } // ChangeStatusRequest holds the request parameters for the ChangeStatus method. type ChangeStatusRequest struct { ID string `json:"id"` Status string `json:"status"` } // ChangeStatusResponse holds the response values for the ChangeStatus method. type ChangeStatusResponse struct { Err error `json:"error,omitempty"` } 

نقاط نهاية مجموعة أدوات النقل لطرق الخدمة مثل نقاط نهاية RPC


يتم فصل جوهر منطق أعمالنا عن باقي الكود ووضعه في طبقة الخدمة ، التي يتم كشفها باستخدام نقاط نهاية RPC ، والتي تستخدم تجريد مجموعة Go يسمى Endpoint .


هذا ما تبدو عليه نقطة النهاية من مجموعة Go:


 type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error) 

كما قلنا أعلاه ، تمثل نقطة النهاية طريقة RPC منفصلة. يتم تحويل كل طريقة خدمة إلى endpoint.Endpoint . دعونا نجعل نقاط نهاية أدوات Go لأساليب خدمة الطلب:


 import ( "context" "github.com/go-kit/kit/endpoint" "github.com/shijuvar/gokit-examples/services/order" ) // Endpoints holds all Go kit endpoints for the Order service. type Endpoints struct { Create endpoint.Endpoint GetByID endpoint.Endpoint ChangeStatus endpoint.Endpoint } // MakeEndpoints initializes all Go kit endpoints for the Order service. func MakeEndpoints(s order.Service) Endpoints { return Endpoints{ Create: makeCreateEndpoint(s), GetByID: makeGetByIDEndpoint(s), ChangeStatus: makeChangeStatusEndpoint(s), } } func makeCreateEndpoint(s order.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(CreateRequest) id, err := s.Create(ctx, req.Order) return CreateResponse{ID: id, Err: err}, nil } } func makeGetByIDEndpoint(s order.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(GetByIDRequest) orderRes, err := s.GetByID(ctx, req.ID) return GetByIDResponse{Order: orderRes, Err: err}, nil } } func makeChangeStatusEndpoint(s order.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(ChangeStatusRequest) err := s.ChangeStatus(ctx, req.ID, req.Status) return ChangeStatusResponse{Err: err}, nil } } 

يقبل محول نقطة النهاية الواجهة كوسيطة للمدخلات ويحولها إلى تجريد endpoint.Enpoint مجموعة أدوات Go. يشير إلى جعل كل طريقة خدمة فردية نقطة نهاية. تقوم وظيفة المحول هذه بإجراء مقارنة ونوع التحويلات للطلبات ، واستدعاء طريقة الخدمة وإرجاع رسالة استجابة.


 func makeCreateEndpoint(s order.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(CreateRequest) id, err := s.Create(ctx, req.Order) return CreateResponse{ID: id, Err: err}, nil } } 

فضح الخدمة باستخدام HTTP


أنشأنا خدمتنا ووصفنا نقاط نهاية RPC لكشف طرق خدمتنا. نحتاج الآن إلى نشر خدمتنا في الخارج حتى تتمكن الخدمات الأخرى من استدعاء نقاط نهاية RCP. لفضح خدمتنا ، نحتاج إلى تحديد بروتوكول النقل لخدمتنا ، والذي بموجبه سيقبل الطلبات. مجموعة أدوات Go تدعم عمليات النقل المختلفة ، مثل HTTP و gRPC و NATS و AMQP و Thrift خارج الصندوق.


على سبيل المثال ، نستخدم نقل HTTP لخدمتنا. توفر حزمة go go github.com/go-kit/kit/transport/http القدرة على خدمة طلبات HTTP. NewServer وظيفة NewServer من حزمة transport/http إنشاء خادم http جديد يقوم بتطبيق http.Handler ويلف نقاط النهاية المقدمة.


فيما يلي الكود الذي يحول نقاط نهاية مجموعة أدوات Go إلى نقل HTTP يخدم طلبات HTTP:


 package http import ( "context" "encoding/json" "errors" "github.com/shijuvar/gokit-examples/services/order" "net/http" "github.com/go-kit/kit/log" kithttp "github.com/go-kit/kit/transport/http" "github.com/gorilla/mux" "github.com/shijuvar/gokit-examples/services/order/transport" ) var ( ErrBadRouting = errors.New("bad routing") ) // NewService wires Go kit endpoints to the HTTP transport. func NewService( svcEndpoints transport.Endpoints, logger log.Logger, ) http.Handler { // set-up router and initialize http endpoints r := mux.NewRouter() options := []kithttp.ServerOption{ kithttp.ServerErrorLogger(logger), kithttp.ServerErrorEncoder(encodeError), } // HTTP Post - /orders r.Methods("POST").Path("/orders").Handler(kithttp.NewServer( svcEndpoints.Create, decodeCreateRequest, encodeResponse, options..., )) // HTTP Post - /orders/{id} r.Methods("GET").Path("/orders/{id}").Handler(kithttp.NewServer( svcEndpoints.GetByID, decodeGetByIDRequest, encodeResponse, options..., )) // HTTP Post - /orders/status r.Methods("POST").Path("/orders/status").Handler(kithttp.NewServer( svcEndpoints.ChangeStatus, decodeChangeStausRequest, encodeResponse, options..., )) return r } func decodeCreateRequest(_ context.Context, r *http.Request) (request interface{}, err error) { var req transport.CreateRequest if e := json.NewDecoder(r.Body).Decode(&req.Order); e != nil { return nil, e } return req, nil } func decodeGetByIDRequest(_ context.Context, r *http.Request) (request interface{}, err error) { vars := mux.Vars(r) id, ok := vars["id"] if !ok { return nil, ErrBadRouting } return transport.GetByIDRequest{ID: id}, nil } func decodeChangeStausRequest(_ context.Context, r *http.Request) (request interface{}, err error) { var req transport.ChangeStatusRequest if e := json.NewDecoder(r.Body).Decode(&req); e != nil { return nil, e } return req, nil } func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { if e, ok := response.(errorer); ok && e.error() != nil { // Not a Go kit transport error, but a business-logic error. // Provide those as HTTP errors. encodeError(ctx, e.error(), w) return nil } w.Header().Set("Content-Type", "application/json; charset=utf-8") return json.NewEncoder(w).Encode(response) } 

نقوم بإنشاء http.Handler باستخدام وظيفة NewServer من حزمة transport/http ، والتي تزودنا NewServer نهاية وطلب وظائف فك التشفير (إرجاع قيمة type DecodeRequestFunc func ) وترميز الاستجابة (على سبيل المثال type EncodeReponseFunc func ).


فيما يلي أمثلة على DecodeRequestFunc و EncodeResponseFunc :


 // For decoding request type DecodeRequestFunc func(context.Context, *http.Request) (request interface{}, err error) 

 // For encoding response type EncodeResponseFunc func(context.Context, http.ResponseWriter, interface{}) error 

بدء خادم HTTP


أخيرًا ، يمكننا تشغيل خادم HTTP الخاص بنا لمعالجة الطلبات. تطبق وظيفة NewService الموضحة أعلاه واجهة http.Handler والتي تتيح لنا تشغيلها http.Handler HTTP:


 func main() { var ( httpAddr = flag.String("http.addr", ":8080", "HTTP listen address") ) flag.Parse() var logger log.Logger { logger = log.NewLogfmtLogger(os.Stderr) logger = log.NewSyncLogger(logger) logger = level.NewFilter(logger, level.AllowDebug()) logger = log.With(logger, "svc", "order", "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller, ) } level.Info(logger).Log("msg", "service started") defer level.Info(logger).Log("msg", "service ended") var db *sql.DB { var err error // Connect to the "ordersdb" database db, err = sql.Open("postgres", "postgresql://shijuvar@localhost:26257/ordersdb?sslmode=disable") if err != nil { level.Error(logger).Log("exit", err) os.Exit(-1) } } // Create Order Service var svc order.Service { repository, err := cockroachdb.New(db, logger) if err != nil { level.Error(logger).Log("exit", err) os.Exit(-1) } svc = ordersvc.NewService(repository, logger) } var h http.Handler { endpoints := transport.MakeEndpoints(svc) h = httptransport.NewService(endpoints, logger) } errs := make(chan error) go func() { c := make(chan os.Signal) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) errs <- fmt.Errorf("%s", <-c) }() go func() { level.Info(logger).Log("transport", "HTTP", "addr", *httpAddr) server := &http.Server{ Addr: *httpAddr, Handler: h, } errs <- server.ListenAndServe() }() level.Error(logger).Log("exit", <-errs) } 

الآن تم إطلاق خدمتنا وتستخدم بروتوكول HTTP على مستوى النقل. يمكن بدء نفس الخدمة باستخدام وسيلة نقل أخرى. على سبيل المثال ، يمكن كشف الخدمة باستخدام gRPC أو Apache Thrift.


بالنسبة للمقالة التمهيدية ، استخدمنا بالفعل بدائل Go kit بشكل كافٍ ، ولكنه يوفر أيضًا المزيد من الوظائف لإنشاء أنظمة ذات أنماط شفافة وموثوقة واكتشاف الخدمة وموازنة الحمل وما إلى ذلك. سنناقش هذه الأشياء وأشياء أخرى في مجموعة Go في المقالات التالية.


كود المصدر


يمكن الاطلاع على شفرة المصدر الكاملة للأمثلة على GitHub هنا.


الوسيطة في مجموعة Go


مجموعة أدوات Go تستعد لاستخدام المبادئ الجيدة لتصميم النظام ، مثل الطبقات. يمكن عزل مكونات الخدمة ونقاط النهاية باستخدام Middlewares ( نمط وسيط المسار تقريبًا ). توفر الوسيطة في مجموعة Go آلية قوية يمكنك من خلالها التفاف الخدمات ونقاط النهاية وإضافة وظائف (مكونات معزولة) ، مثل تسجيل الدخول أو مقاطعة الطلب أو الحد من عدد الطلبات أو موازنة التحميل أو التتبع الموزع.


فيما يلي صورة من موقع Go kit على الويب ، والتي تم تصويرها على أنها "هندسة بصل" نموذجية باستخدام Middlewares في مجموعة Go:
الصورة


حذار من متلازمة Microservices الربيع التمهيد


مثل مجموعة أدوات Go ، يعد Spring Boot مجموعة أدوات للخدمة الصغيرة في عالم Java. ولكن ، على عكس مجموعة Go ، يعتبر Spring Boot إطارًا ناضجًا للغاية. أيضًا ، يستخدم العديد من مطوري Java Spring Boot لإنشاء خدمات عالمية باستخدام مكدس Java مع ملاحظات إيجابية من الاستخدام ، ويعتقد البعض أن الخدمات الدقيقة تتعلق فقط باستخدام Spring Boot. أرى العديد من فرق التطوير التي تسيء تفسير استخدام الخدمات الدقيقة ، وأنه لا يمكن تطويرها إلا باستخدام Spring Boot و OSS Netflix ولا يرون الخدمات الدقيقة كنمط عند تطوير الأنظمة الموزعة.


لذا ضع في اعتبارك أنه من خلال مجموعة من الأدوات ، مثل مجموعة Go أو نوع ما من إطار العمل ، يمكنك توجيه تطويرك نحو أجهزة صغرى ، كنمط تصميم. على الرغم من أن الخدمات الصغيرة تحل العديد من مشكلات القياس لكل من الأوامر والأنظمة ، إلا أنها تخلق أيضًا العديد من المشاكل لأن البيانات في أنظمة الخدمات الدقيقة متناثرة عبر قواعد بيانات مختلفة ، والتي تخلق أحيانًا العديد من المشاكل عند إنشاء استعلامات المعاملات أو البيانات. كل هذا يتوقف على مشكلة مجال الموضوع وسياق نظامك. الشيء الرائع هو أن مجموعة Go ، المصممة كأداة لإنشاء خدمات دقيقة ، كانت مناسبة أيضًا لإنشاء متجانسة أنيقة تم إنشاؤها باستخدام تصميم معماري جيد لأنظمتك.


كما تتوفر بعض ميزات مجموعة أدوات Go ، مثل مقاطعة الطلبات وتقييدها ، على الأنظمة الأساسية لشبكات الخدمة ، مثل Istio. لذا ، إذا كنت تستخدم شيئًا مثل Istio لإطلاق وحدات الميكروسيك الخاصة بك ، فقد لا تحتاج إلى بعض الأشياء من مجموعة Go ، ولكن ليس لدى الجميع ما يكفي من عرض القناة لاستخدام شبكة الخدمة لإنشاء اتصال بين الخدمات ، لأن هذا يضيف المزيد مستوى واحد وتعقيد إضافي.


ملاحظة


قد لا يشارك مؤلف الترجمة رأي مؤلف النص الأصلي ، وقد تمت ترجمة هذه المقالة لأغراض تعليمية فقط لمجتمع اللغة الروسية Go.


UPD
هذه هي أيضًا المقالة الأولى في قسم الترجمة وسأكون ممتنًا لأية تعليقات على الترجمة.

Source: https://habr.com/ru/post/ar430300/


All Articles