gRPC 使用 protobuf 構建微服務

原文連接,轉載註明來源便可。
本文代碼:GitHub
本文目錄:
image-20180504110112053php

微服務架構

單一的代碼庫

之前使用 Laravel 作 web 項目時,是根據 MVC 去劃分目錄結構的,即 Controller 層處理業務邏輯,Model 層處理數據庫的 CURD,View 層處理數據渲染與頁面交互。以及 MVP、MVVM 都是將整個項目的代碼是集中在一個代碼庫中,進行業務處理。這種單一聚合代碼的方式在前期實現業務的速度很快,但在後期會暴露不少問題:nginx

  • 開發與維護困難:隨着業務複雜度的增長,代碼的耦合度每每會變高,多個模塊相互耦合後不易橫向擴展
  • 效率和可靠性低:過大的代碼量將下降響應速度,應用潛在的安全問題也會累積

拆分的代碼庫

微服務是一種軟件架構,它將一個大且聚合的業務項目拆解爲多個小且獨立的業務模塊,模塊即服務,各服務間使用高效的協議(protobuf、JSON 等)相互調用便是 RPC。這種拆分代碼庫的方式有如下特色:git

  • 每一個服務應做爲小規模的、獨立的業務模塊在運行,相似 Unix 的 Do one thing and do it well
  • 每一個服務應在進行自動化測試和(分佈式)部署時,不影響其餘服務
  • 每一個服務內部進行細緻的錯誤檢查和處理,提升了健壯性

兩者對比

本質上,兩者只是聚合與拆分代碼的方式不一樣。github

image-20180427190322810

參考:微服務架構的優點與不足golang

構建微服務

UserInfoService 微服務

接下來建立一個處理用戶信息的微服務:UserInfoService,客戶端經過 name 向服務端查詢用戶的年齡、職位等詳細信息,需先安裝 gRPC 與 protoc 編譯器:web

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

目錄結構

├── proto
│   ├── user.proto        // 定義客戶端請求、服務端響應的數據格式
│   └── user.pb.go        // protoc 爲 gRPC 生成的讀寫數據的函數
├── server.go            // 實現微服務的服務端
└── client.go            // 調用微服務的客戶端

調用流程

image-20180503174554852

Protobuf 協議

每一個微服務有本身獨立的代碼庫,各自之間在通訊時須要高效的協議,要遵循必定的數據結構來解析和編碼要傳輸的數據,在微服務中常使用 protobuf 來定義。shell

Protobuf(protocal buffers)是谷歌推出的一種二進制數據編碼格式,相比 XML 和 JSON 的文本數據編碼格式更有優點:數據庫

讀寫更快、文件體積更小

它沒有 XML 的標籤名或 JSON 的字段名,更爲輕量,更多參考json

語言中立

只需定義一份 .proto 文件,便可使用各語言對應的 protobuf 編譯器對其編譯,生成的文件中有對 message 編碼、解碼的函數數組

對於 JSON
  • 在 PHP 中需使用 json_encode()json_decode() 去編解碼,在 Golang 中需使用 json 標準庫的 Marshal()Unmarshal() … 每次解析和編碼比較繁瑣
  • 優勢:可讀性好、開發成本低
  • 缺點:相比 protobuf 的讀寫速度更慢、存儲空間更多
對於 Protobuf
  • .proto 可生成 .php 或 *.pb.go … 在項目中可直接引用該文件中編譯器生成的編碼、解碼函數
  • 優勢:高效輕量、一處定義多處使用
  • 缺點:可讀性差、開發成本高

定義微服務的 user.proto 文件

syntax = "proto3";    // 指定語法格式,注意 proto3 再也不支持 proto2 的 required 和 optional
package proto;        // 指定生成的 user.pb.go 的包名,防止命名衝突


// service 定義開放調用的服務,即 UserInfoService 微服務
service UserInfoService {
    // rpc 定義服務內的 GetUserInfo 遠程調用
    rpc GetUserInfo (UserRequest) returns (UserResponse) {
    }
}


// message 對應生成代碼的 struct
// 定義客戶端請求的數據格式
message UserRequest {
    // [修飾符] 類型 字段名 = 標識符;
    string name = 1;
}


// 定義服務端響應的數據格式
message UserResponse {
    int32 id = 1;
    string name = 2;
    int32 age = 3;
    repeated string title = 4;    // repeated 修飾符表示字段是可變數組,即 slice 類型
}

編譯 user.proto 文件

# protoc 編譯器的 grpc 插件會處理 service 字段定義的 UserInfoService
# 使 service 能編碼、解碼 message
$ protoc -I . --go_out=plugins=grpc:. ./user.proto

生成 user.pb.go

package proto

import (
    context "golang.org/x/net/context"
    grpc "google.golang.org/grpc"
)

// 請求結構
type UserRequest struct {
    Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}

// 爲字段自動生成的 Getter
func (m *UserRequest) GetName() string {
    if m != nil {
        return m.Name
    }
    return ""
}

// 響應結構
type UserResponse struct {
    Id    int32    `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
    Name  string   `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
    Age   int32    `protobuf:"varint,3,opt,name=age" json:"age,omitempty"`
    Title []string `protobuf:"bytes,4,rep,name=title" json:"title,omitempty"`
}
// ...

// 客戶端需實現的接口
type UserInfoServiceClient interface {
    GetUserInfo(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error)
}


// 服務端需實現的接口
type UserInfoServiceServer interface {
    GetUserInfo(context.Context, *UserRequest) (*UserResponse, error)
}

// 將微服務註冊到 grpc 
func RegisterUserInfoServiceServer(s *grpc.Server, srv UserInfoServiceServer) {
    s.RegisterService(&_UserInfoService_serviceDesc, srv)
}
// 處理請求
func _UserInfoService_GetUserInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {...}

服務端實現微服務

實現流程

image-20180504095727827

代碼參考

package main
import (...)

// 定義服務端實現約定的接口
type UserInfoService struct{}
var u = UserInfoService{}

// 實現 interface
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
    name := req.Name

    // 模擬在數據庫中查找用戶信息
    // ...
    if name == "wuYin" {
        resp = &pb.UserResponse{
            Id:    233,
            Name:  name,
            Age:   20,
            Title: []string{"Gopher", "PHPer"}, // repeated 字段是 slice 類型
        }
    }
    err = nil
    return
}

func main() {
    port := ":2333"
    l, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("listen error: %v\n", err)
    }
    fmt.Printf("listen %s\n", port)
    s := grpc.NewServer()

    // 將 UserInfoService 註冊到 gRPC
    // 注意第二個參數 UserInfoServiceServer 是接口類型的變量
    // 須要取地址傳參
    pb.RegisterUserInfoServiceServer(s, &u)
    s.Serve(l)
}

運行監聽:

image-20180504094201953

客戶端調用

實現流程

image-20180504100357221

代碼參考

package main
import (...)

func main() {
    conn, err := grpc.Dial(":2333", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("dial error: %v\n", err)
    }
    defer conn.Close()

    // 實例化 UserInfoService 微服務的客戶端
    client := pb.NewUserInfoServiceClient(conn)

    // 調用服務
    req := new(pb.UserRequest)
    req.Name = "wuYin"
    resp, err := client.GetUserInfo(context.Background(), req)
    if err != nil {
        log.Fatalf("resp error: %v\n", err)
    }

    fmt.Printf("Recevied: %v\n", resp)
}

運行調用成功: image-20180504094246792

總結

在上邊 UserInfoService 微服務的實現過程當中,會發現每一個微服務都須要本身管理服務端監聽端口,客戶端鏈接後調用,當有不少個微服務時端口的管理會比較麻煩,相比 gRPC,go-micro 實現了服務發現(Service Discovery)來方便的管理微服務,下節將隨服務的 Docker 化一塊兒學習。

更多參考:Nginx 的微服務系列教程

相關文章
相關標籤/搜索