這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文連結,轉載註明來源即可。
本文代碼:GitHub
本文目錄:
微服務架構
單一的程式碼程式庫
以前使用 Laravel 做 Web 專案時,是根據 MVC 去劃分目錄結構的,即 Controller 層處理商務邏輯,Model 層處理資料庫的 CURD,View 層處理資料渲染與頁面互動。以及 MVP、MVVM 都是將整個項目的代碼是集中在一個程式碼程式庫中,進行業務處理。這種單一彙總代碼的方式在前期實現業務的速度很快,但在後期會暴露很多問題:
- 開發與維護困難:隨著業務複雜度的增加,代碼的耦合度往往會變高,多個模組相互耦合後不易橫向擴充
- 效率和可靠性低:過大的代碼量將降低響應速度,應用潛在的安全問題也會累積
拆分的程式碼程式庫
微服務是一種軟體架構,它將一個大且彙總的商務專案拆解為多個小且獨立的業務模組,模組即服務,各服務間使用高效的協議(protobuf、JSON 等)相互調用即是 RPC。這種拆分程式碼程式庫的方式有以下特點:
- 每個服務應作為小規模的、獨立的業務模組在運行,類似 Unix 的 Do one thing and do it well
- 每個服務應在進行自動化測試和(分布式)部署時,不影響其他服務
- 每個服務內部進行細緻的錯誤檢查和處理,提高了健壯性
二者對比
本質上,二者只是彙總與拆分代碼的方式不同。
參考:微服務架構的優勢與不足
構建微服務
UserInfoService 微服務
接下來建立一個處理使用者資訊的微服務:UserInfoService,用戶端通過 name 向服務端查詢使用者的年齡、職位等詳細資料,需先安裝 gRPC 與 protoc 編譯器:
go get -u google.golang.org/grpcgo get -u github.com/golang/protobuf/protoc-gen-go
目錄結構
├── proto│ ├── user.proto // 定義用戶端請求、服務端響應的資料格式│ └── user.pb.go // protoc 為 gRPC 產生的讀寫資料的函數├── server.go // 實現微服務的服務端└── client.go // 調用微服務的用戶端
調用流程
Protobuf 協議
每個微服務有自己獨立的程式碼程式庫,各自之間在通訊時需要高效的協議,要遵循一定的資料結構來解析和編碼要傳輸的資料,在微服務中常使用 protobuf 來定義。
Protobuf(protocal buffers)是Google推出的一種位元據編碼格式,相比 XML 和 JSON 的文本資料編碼格式更有優勢:
讀寫更快、檔案體積更小
它沒有 XML 的標籤名或 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 和 optionalpackage 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 protoimport ( 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"`}// 為欄位自動產生的 Getterfunc (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) {...}
服務端實現微服務
實現流程
代碼參考
package mainimport (...)// 定義服務端實現約定的介面type UserInfoService struct{}var u = UserInfoService{}// 實現 interfacefunc (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)}
運行監聽:
用戶端調用
實現流程
代碼參考
package mainimport (...)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)}
運行調用成功:
總結
在上邊 UserInfoService 微服務的實現過程中,會發現每個微服務都需要自己管理服務端監聽連接埠,用戶端串連後調用,當有很多個微服務時連接埠的管理會比較麻煩,相比 gRPC,go-micro 實現了服務發現(Service Discovery)來方便的管理微服務,下節將隨服務的 Docker 化一起學習。
更多參考:Nginx 的微服務系列教程