在Golang和gRPC上构建微服务架构,第1部分

微服务架构简介


第1部分(共10部分)


改编的Ewan Valentine文章。


这是一个由十部分组成的系列,我将尝试每月撰写一次有关在Golang上构建微服务的文章。 我将使用protobuf和gRPC作为主要的传输协议。


我使用的堆栈:golang,mongodb,grpc,docker,Google Cloud,Kubernetes,NATS,CircleCI,Terraform和go-micro。


我为什么需要这个? 由于花了我很长时间才弄清楚并解决累积的问题。 我还想与您分享我在Go和其他新技术上创建,测试和部署微服务的知识。


在这一部分中,我想展示构建微服务的基本概念和技术。 让我们编写一个简单的实现。 该项目将具有以下实体:


  • 货运
  • 库存
  • 试用
  • 用户
  • 角色
  • 认证方式


要走得更远,您需要安装Golang和必要的库,并创建一个git存储库。


理论


什么是微服务架构?


微服务将单独的功能隔离到服务中,就此服务执行的功能而言,它们是自给自足的。 为了与其他服务兼容,它具有一个众所周知的预定义接口。
微服务使用通过某些中间消息代理传输的消息相互通信。



由于微服务架构,该应用程序无法整体扩展,但可以部分扩展。 例如,如果授权服务比其他服务更频繁地“抽搐”,我们可以增加实例数量。 该概念总体上与云计算和容器化的概念一致。


为什么选择高朗


几乎所有语言都支持微服务,毕竟,微服务是一个概念,而不是特定的结构或工具。 但是,某些语言比其他语言更适合,并且对微服务也有更好的支持。 Golang是一种受支持的语言。


认识protobuf / gRPC


如前所述,微服务被划分为单独的代码库,与微服务相关的重要问题之一是通信。 如果您拥有整体,则可以直接从程序的另一个位置直接调用代码。


为了解决通信问题,我们可以使用传统的REST方法,并通过HTTP以JSON或XML格式传输数据。 但是这种方法有其缺点,例如,在发送消息之前,您必须先对数据进行编码,然后再将其解码回接收端。 这是开销,并且增加了代码的复杂性。


有解决办法! 这是gRPC协议-一种轻量级的,基于二进制的协议,消除了HTTP标头的传输,这将节省一些字节。 未来的HTTP2还暗示使用二进制数据,这再次代表了gRPC。 HTTP2允许双向通信,它很棒!


GRPC还允许您以友好的格式定义服务的接口-这是protobuf


练习


创建文件/project/consigment.proto。
Protobuf官方文档


调配协议
//consigment.proto syntax = "proto3"; package go.micro.srv.consignment; service ShippingService { rpc CreateConsignment(Consignment) returns (Response) {} } message Consignment { string id = 1; string description = 2; int32 weight = 3; repeated Container containers = 4; string vessel_id = 5; } message Container { string id = 1; string customer_id = 2; string origin = 3; string user_id = 4; } message Response { bool created = 1; Consignment consignment = 2; } 

这是一个简单的示例,其中包含要提供给其他服务的服务:service ShippingService,然后我们将定义消息。 Protobuf是一种静态类型的协议,我们可以创建自定义类型(类似于golang中的结构)。 在这里,容器嵌套在批处理中。


安装库,编译器并编译我们的协议:


 $ go get -u google.golang.org/grpc $ go get -u github.com/golang/protobuf/protoc-gen-go $ sudo apt install protobuf-compiler $ mkdir consignment && cd consignment $ protoc -I=. --go_out=plugins=grpc:. consignment.proto 

输出应为文件:


consignment.pb.go
 // Code generated by protoc-gen-go. DO NOT EDIT. // source: consignment.proto package consignment import ( fmt "fmt" proto "github.com/golang/protobuf/proto" context "golang.org/x/net/context" grpc "google.golang.org/grpc" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type Consignment struct { Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` Weight int32 `protobuf:"varint,3,opt,name=weight,proto3" json:"weight,omitempty"` Containers []*Container `protobuf:"bytes,4,rep,name=containers,proto3" json:"containers,omitempty"` VesselId string `protobuf:"bytes,5,opt,name=vessel_id,json=vesselId,proto3" json:"vessel_id,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Consignment) Reset() { *m = Consignment{} } func (m *Consignment) String() string { return proto.CompactTextString(m) } func (*Consignment) ProtoMessage() {} func (*Consignment) Descriptor() ([]byte, []int) { return fileDescriptor_3804bf87090b51a9, []int{0} } func (m *Consignment) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Consignment.Unmarshal(m, b) } func (m *Consignment) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Consignment.Marshal(b, m, deterministic) } func (m *Consignment) XXX_Merge(src proto.Message) { xxx_messageInfo_Consignment.Merge(m, src) } func (m *Consignment) XXX_Size() int { return xxx_messageInfo_Consignment.Size(m) } func (m *Consignment) XXX_DiscardUnknown() { xxx_messageInfo_Consignment.DiscardUnknown(m) } var xxx_messageInfo_Consignment proto.InternalMessageInfo func (m *Consignment) GetId() int32 { if m != nil { return m.Id } return 0 } func (m *Consignment) GetDescription() string { if m != nil { return m.Description } return "" } func (m *Consignment) GetWeight() int32 { if m != nil { return m.Weight } return 0 } func (m *Consignment) GetContainers() []*Container { if m != nil { return m.Containers } return nil } func (m *Consignment) GetVesselId() string { if m != nil { return m.VesselId } return "" } type Container struct { Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` CustomerId string `protobuf:"bytes,2,opt,name=customer_id,json=customerId,proto3" json:"customer_id,omitempty"` Origin string `protobuf:"bytes,3,opt,name=origin,proto3" json:"origin,omitempty"` UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Container) Reset() { *m = Container{} } func (m *Container) String() string { return proto.CompactTextString(m) } func (*Container) ProtoMessage() {} func (*Container) Descriptor() ([]byte, []int) { return fileDescriptor_3804bf87090b51a9, []int{1} } func (m *Container) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Container.Unmarshal(m, b) } func (m *Container) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Container.Marshal(b, m, deterministic) } func (m *Container) XXX_Merge(src proto.Message) { xxx_messageInfo_Container.Merge(m, src) } func (m *Container) XXX_Size() int { return xxx_messageInfo_Container.Size(m) } func (m *Container) XXX_DiscardUnknown() { xxx_messageInfo_Container.DiscardUnknown(m) } var xxx_messageInfo_Container proto.InternalMessageInfo func (m *Container) GetId() int32 { if m != nil { return m.Id } return 0 } func (m *Container) GetCustomerId() string { if m != nil { return m.CustomerId } return "" } func (m *Container) GetOrigin() string { if m != nil { return m.Origin } return "" } func (m *Container) GetUserId() string { if m != nil { return m.UserId } return "" } type Response struct { Created bool `protobuf:"varint,1,opt,name=created,proto3" json:"created,omitempty"` Consignment *Consignment `protobuf:"bytes,2,opt,name=consignment,proto3" json:"consignment,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { return fileDescriptor_3804bf87090b51a9, []int{2} } func (m *Response) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Response.Unmarshal(m, b) } func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Response.Marshal(b, m, deterministic) } func (m *Response) XXX_Merge(src proto.Message) { xxx_messageInfo_Response.Merge(m, src) } func (m *Response) XXX_Size() int { return xxx_messageInfo_Response.Size(m) } func (m *Response) XXX_DiscardUnknown() { xxx_messageInfo_Response.DiscardUnknown(m) } var xxx_messageInfo_Response proto.InternalMessageInfo func (m *Response) GetCreated() bool { if m != nil { return m.Created } return false } func (m *Response) GetConsignment() *Consignment { if m != nil { return m.Consignment } return nil } func init() { proto.RegisterType((*Consignment)(nil), "Consignment") proto.RegisterType((*Container)(nil), "Container") proto.RegisterType((*Response)(nil), "Response") } func init() { proto.RegisterFile("consignment.proto", fileDescriptor_3804bf87090b51a9) } var fileDescriptor_3804bf87090b51a9 = []byte{ // 281 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xbf, 0x4e, 0x33, 0x31, 0x10, 0xc4, 0xbf, 0xcb, 0xff, 0x5b, 0x7f, 0x02, 0xc5, 0x05, 0x58, 0x50, 0x70, 0xba, 0x2a, 0xa2, 0x70, 0x11, 0x9e, 0x00, 0xa5, 0x4a, 0xeb, 0xd0, 0xa3, 0x60, 0xaf, 0x2e, 0x2b, 0x11, 0xfb, 0x64, 0x3b, 0xe1, 0x75, 0x78, 0x54, 0x74, 0xbe, 0x1c, 0x18, 0x51, 0xce, 0xac, 0x67, 0xf7, 0xa7, 0x31, 0x2c, 0xb5, 0xb3, 0x81, 0x1a, 0x7b, 0x44, 0x1b, 0x65, 0xeb, 0x5d, 0x74, 0xf5, 0x67, 0x01, 0x6c, 0xf3, 0xe3, 0xf2, 0x2b, 0x18, 0x91, 0x11, 0x45, 0x55, 0xac, 0xa6, 0x6a, 0x44, 0x86, 0x57, 0xc0, 0x0c, 0x06, 0xed, 0xa9, 0x8d, 0xe4, 0xac, 0x18, 0x55, 0xc5, 0xaa, 0x54, 0xb9, 0xc5, 0x6f, 0x60, 0xf6, 0x81, 0xd4, 0x1c, 0xa2, 0x18, 0xa7, 0xd4, 0x45, 0xf1, 0x47, 0x00, 0xed, 0x6c, 0xdc, 0x93, 0x45, 0x1f, 0xc4, 0xa4, 0x1a, 0xaf, 0xd8, 0x1a, 0xe4, 0x66, 0xb0, 0x54, 0x36, 0xe5, 0xf7, 0x50, 0x9e, 0x31, 0x04, 0x7c, 0x7f, 0x25, 0x23, 0xa6, 0xe9, 0xc6, 0xa2, 0x37, 0xb6, 0xa6, 0x3e, 0x42, 0xf9, 0x9d, 0xfa, 0xc3, 0xf7, 0x00, 0x4c, 0x9f, 0x42, 0x74, 0x47, 0xf4, 0x5d, 0xb6, 0xe7, 0x83, 0xc1, 0xda, 0x9a, 0x0e, 0xcf, 0x79, 0x6a, 0xc8, 0x26, 0xbc, 0x52, 0x5d, 0x14, 0xbf, 0x85, 0xf9, 0x29, 0xf4, 0xa1, 0x49, 0x3f, 0xe8, 0xe4, 0xd6, 0xd4, 0x2f, 0xb0, 0x50, 0x18, 0x5a, 0x67, 0x03, 0x72, 0x01, 0x73, 0xed, 0x71, 0x1f, 0xb1, 0x3f, 0xb9, 0x50, 0x83, 0xe4, 0x12, 0x58, 0x56, 0x66, 0xba, 0xcb, 0xd6, 0xff, 0x65, 0x56, 0xa5, 0xca, 0x1f, 0xac, 0x9f, 0xe1, 0x7a, 0x77, 0xa0, 0xb6, 0x25, 0xdb, 0xec, 0xd0, 0x9f, 0x49, 0x23, 0x97, 0xb0, 0xdc, 0xa4, 0x6d, 0x79, 0xff, 0xbf, 0x56, 0xdc, 0x95, 0x72, 0x40, 0xa9, 0xff, 0xbd, 0xcd, 0xd2, 0x8f, 0x3d, 0x7d, 0x05, 0x00, 0x00, 0xff, 0xff, 0x84, 0x5c, 0xa4, 0x06, 0xc6, 0x01, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // ShippingServiceClient is the client API for ShippingService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type ShippingServiceClient interface { CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error) } type shippingServiceClient struct { cc *grpc.ClientConn } func NewShippingServiceClient(cc *grpc.ClientConn) ShippingServiceClient { return &shippingServiceClient{cc} } func (c *shippingServiceClient) CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error) { out := new(Response) err := c.cc.Invoke(ctx, "/ShippingService/CreateConsignment", in, out, opts...) if err != nil { return nil, err } return out, nil } // ShippingServiceServer is the server API for ShippingService service. type ShippingServiceServer interface { CreateConsignment(context.Context, *Consignment) (*Response, error) } func RegisterShippingServiceServer(s *grpc.Server, srv ShippingServiceServer) { s.RegisterService(&_ShippingService_serviceDesc, srv) } func _ShippingService_CreateConsignment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Consignment) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ShippingServiceServer).CreateConsignment(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/ShippingService/CreateConsignment", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ShippingServiceServer).CreateConsignment(ctx, req.(*Consignment)) } return interceptor(ctx, in, info, handler) } var _ShippingService_serviceDesc = grpc.ServiceDesc{ ServiceName: "ShippingService", HandlerType: (*ShippingServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "CreateConsignment", Handler: _ShippingService_CreateConsignment_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "consignment.proto", } 

如果是,则出了点问题。 请注意,参数-I是编译器正在寻找文件的路径,--go_out是将在其中创建新文件的路径。 总是有帮助


 $ protoc -h 

这是gRPC / protobuf库自动生成的代码,因此您可以将protobuf定义与自己的代码关联。


我们将编写main.go


main.go
 package seaport import ( "log" "net" //    pbf "seaport/consignment" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) const ( port = ":50051" ) //IRepository -   type IRepository interface { Create(*pbf.Consignment) (*pbf.Consignment, error) } // Repository -    , //        type Repository struct { consignments []*pbf.Consignment } //Create -    func (repo *Repository) Create(consignment *pbf.Consignment) (*pbf.Consignment, error) { updated := append(repo.consignments, consignment) repo.consignments = updated return consignment, nil } //         //       .     //         . . type service struct { repo IRepository } // CreateConsignment -        , //    create,      //     gRPC. func (s *service) CreateConsignment(ctx context.Context, req *pbf.Consignment) (*pbf.Response, error) { //      consignment, err := s.repo.Create(req) if err != nil { return nil, err } //   `Response`, //        return &pbf.Response{Created: true, Consignment: consignment}, nil } func main() { repo := &Repository{} //   gRPC    tcp lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() //      gRPC,    //        //  `Response`,       pbf.RegisterShippingServiceServer(s, &service{repo}) //      gRPC. reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } 

请仔细阅读代码中的注释。 显然,这里我们正在创建实现逻辑,在该逻辑中,我们的gRPC方法使用生成的格式进行交互,并在端口50051上创建了一个新的gRPC服务器。
您可以使用$ go run main.go来运行它 ,但是您将看不到任何东西,也将无法使用它……因此,让我们创建一个客户端以实际运行它。


让我们创建一个命令行界面,该界面需要一个JSON文件并与我们的gRPC服务进行交互。


在根目录中,创建$ mkdir consignment-cli的新子目录。 在此目录中,创建具有以下内容的cli.go文件:


cli.go
 package main import ( "encoding/json" "io/ioutil" "log" "os" pbf "seaport/consignment" "golang.org/x/net/context" "google.golang.org/grpc" ) const ( address = "localhost:50051" defaultFilename = "consignment.json" ) //    func parseFile(file string) (*pbf.Consignment, error) { var consignment *pbf.Consignment data, err := ioutil.ReadFile(file) if err != nil { return nil, err } json.Unmarshal(data, &consignment) return consignment, err } func main() { //     conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("  : %v", err) } defer conn.Close() client := pbf.NewShippingServiceClient(conn) //    consignment.json, //          file := defaultFilename if len(os.Args) > 1 { file = os.Args[1] } consignment, err := parseFile(file) if err != nil { log.Fatalf("   : %v", err) } r, err := client.CreateConsignment(context.Background(), consignment) if err != nil { log.Fatalf("  : %v", err) } log.Printf(": %t", r.Created) } 

现在创建一个批处理(consignment-cli / consignment.json):


 { "description": "  ", "weight": 100, "containers": [ { "customer_id": "_001", "user_id": "_001", "origin": " " } ], "vessel_id": "_001" } 

现在,如果您从海港软件包中运行$ go运行main.go ,然后在单独的终端面板中运行$ go运行cli.go。 您应该看到消息“ Created:true”。
但是我们如何验证确切创建了什么呢? 让我们使用GetConsignments方法更新我们的服务,以便我们可以查看所有创建的批处理。


调配协议
 //consigment.proto syntax = "proto3"; service ShippingService{ rpc CreateConsignment(Consignment) returns (Response) {} //    rpc GetConsignments(GetRequest) returns (Response) {} } message Consignment { int32 id = 1; string description = 2; int32 weight = 3; repeated Container containers = 4; string vessel_id = 5; } message Container { int32 id =1; string customer_id =2; string origin = 3; string user_id = 4; } //    message GetRequest {} message Response { bool created = 1; Consignment consignment = 2; //     //     repeated Consignment consignments = 3; } 

因此,在这里我们在服务上创建了一个名为GetConsignments的新方法,并且还创建了一个新的GetRequest ,该方法尚未包含任何内容。 我们还在回复邮件中添加了已发送批次的字段。 您会注意到这里的类型具有重复的关键字,直到该类型为止。 您可能已经猜到了,这只是意味着将此字段视为这些类型的数组。


不要急于运行该程序,我们的gRPC方法的实现是基于protobuf库创建的接口的匹配,我们需要确保我们的实现与我们的proto定义相匹配。


 //seaport/main.go //IRepository -   type IRepository interface { Create(*pbf.Consignment) (*pbf.Consignment, error) GetAll() []*pbf.Consignment } //GetAll -       func (repo *Repository) GetAll() []*pbf.Consignment { return repo.consignments } //GetConsignments -         func (s *service) GetConsignments(ctx context.Context, req *pbf.GetRequest) (*pbf.Response, error) { consignments := s.repo.GetAll() return &pbf.Response{Consignments: consignments}, nil } 

在这里,我们包括了新的GetConsignments方法,更新了分别在consignments.proto定义中创建的存储库和接口。 如果再次运行$ go,请再次运行main.go ,该程序将再次运行。


让我们更新cli工具,使其包含调用此方法的功能,并且可以列出我们的参与者:


cli.go
 package main import ( "encoding/json" "io/ioutil" "log" "os" pbf "seaport/consignment" "golang.org/x/net/context" "google.golang.org/grpc" ) const ( address = "localhost:50051" defaultFilename = "consignment.json" ) //    func parseFile(file string) (*pbf.Consignment, error) { var consignment *pbf.Consignment data, err := ioutil.ReadFile(file) if err != nil { return nil, err } json.Unmarshal(data, &consignment) return consignment, err } func main() { //     conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("  : %v", err) } defer conn.Close() client := pbf.NewShippingServiceClient(conn) //    consignment.json, //          file := defaultFilename if len(os.Args) > 1 { file = os.Args[1] } consignment, err := parseFile(file) if err != nil { log.Fatalf("   : %v", err) } r, err := client.CreateConsignment(context.Background(), consignment) if err != nil { log.Fatalf("  : %v", err) } log.Printf(": %t", r.Created) getAll, err := client.GetConsignments(context.Background(), &pbf.GetRequest{}) if err != nil { log.Fatalf("    : %v", err) } for _, cns := range getAll.Consignments { fmt.Printf("Id: %v\n", cns.GetId()) fmt.Printf("Description: %v\n", cns.GetDescription()) fmt.Printf("Weight: %d\n", cns.GetWeight()) fmt.Printf("VesselId: %v\n", cns.GetVesselId()) for _, cnt := range cns.GetContainers() { fmt.Printf("\tId: %v\n", cnt.GetId()) fmt.Printf("\tUserId: %v\n", cnt.GetUserId()) fmt.Printf("\tCustomerId: %v\n", cnt.GetCustomerId()) fmt.Printf("\tOrigin: %v\n", cnt.GetOrigin()) } } } 

将上面的代码添加到cli.go中, 然后再次运行$ go运行cli.go。 客户端将运行CreateConsignment,然后调用GetConsignments。 而且您应该在答案列表中看到该聚会的组成。


因此,我们拥有第一个使用protobuf和gRPC与之交互的微服务和客户端。


本系列的下一部分将包括go-micro集成,这是创建基于gRPC的微服务的强大基础。 我们还将创建第二项服务。 在本系列文章的下一部分,请考虑我们在Docker容器中服务的工作。

Source: https://habr.com/ru/post/zh-CN425653/


All Articles