譯文連接:wuYin/blog
原文連接:ewanvalentine.io,翻譯已獲做者受權。html
本節對 gRPC 的使用淺嘗輒止,更多可參考:gRPC 中 Client 與 Server 數據交互的 4 種模式linux
《Golang 微服務教程》分爲 10 篇,總結微服務開發、測試到部署的完整過程。nginx
本節先介紹微服務的基礎概念、術語,再建立咱們的第一個微服務 consignment-service 的簡潔版。在接下來的第 2~10 節文章中,咱們會陸續建立如下微服務:git
用到的完整技術棧以下:github
Golang, gRPC, go-micro // 開發語言及其 RPC 框架 Google Cloud, MongoDB // 雲平臺與數據存儲 Docker, Kubernetes, Terrafrom // 容器化與集羣架構 NATS, CircleCI // 消息系統與持續集成
做者代碼:EwanValentine/shippy,譯者的中文註釋代碼: wuYin/shippygolang
每一個章節對應倉庫的一個分支,好比本文part1 的代碼在 feature/part1web
筆者的開發環境爲 macOS,本文中使用了 make 工具來高效編譯,Windows 用戶需 手動安裝shell
$ go env GOARCH="amd64" # macOS 環境 GOOS="darwin" # 在第二節使用 Docker 構建 alpine 鏡像時需修改成 linux GOPATH="/Users/wuyin/Go" GOROOT="/usr/local/go"
掌握 Golang 的基礎語法:推薦閱讀謝大的《Go Web 編程》編程
安裝 gRPC / protobufjson
go get -u google.golang.org/grpc # 安裝 gRPC 框架 go get -u github.com/golang/protobuf/protoc-gen-go # 安裝 Go 版本的 protobuf 編譯器
咱們要搭建一個港口的貨物管理平臺。本項目以微服務的架構開發,總體簡單且概念通用。閒話很少說讓咱們開始微服務之旅吧。
在傳統的軟件開發中,整個應用的代碼都組織在一個單一的代碼庫,通常會有如下拆分代碼的形式:
無論怎麼拆分,最終兩者的代碼都會集中在一個庫中進行開發和管理,可參考:谷歌的單一代碼庫管理
微服務是上述第二種拆分方式的拓展,按功能將代碼拆分紅幾個包,都是可獨立運行的單一代碼庫。區別以下:
將整個應用的代碼按功能對應拆分爲小且獨立的微服務代碼庫,這不由讓人聯想到 Unix 哲學:Do One Thing and Do It Well,在傳統單一代碼庫的應用中,模塊之間是緊耦合且邊界模糊的,隨着產品不斷迭代,代碼的開發和維護將變得更爲複雜,潛在的 bug 和漏洞也會愈來愈多。
在項目開發中,可能有一部分代碼會在多個模塊中頻繁的被用到,這種複用性很高的模塊經常會抽離出來做爲公共代碼庫使用,好比驗證模塊,當它要擴展功能(添加短信驗證碼登陸等)時,單一代碼庫的規模只增不減, 整個應用還需從新部署。在微服務架構中,驗證模塊可做爲單個服務獨立出來,能獨立運行、測試和部署。
遵循微服務拆分代碼的理念,能大大下降模塊間的耦合性,橫向擴展也會容易許多,正適合當下雲計算的高性能、高可用和分佈式的開發環境。
Nginx 有一系列文章來探討微服務的許多概念,可 點此閱讀
微服務是一種架構理念而不是具體的框架項目,許多編程語言均可以實現,但有的語言對微服務開發具有天生的優點,Golang 即是其中之一
Golang 自己十分輕量級,運行效率極高,同時對併發編程有着原生的支持,從而能更好的利用多核處理器。內置 net
標準庫對網絡開發的支持也十分完善。可參考謝大的短文:Go 語言的優點
此外,Golang 社區有一個很棒的開源微服務框架 go-mirco,咱們在下一節會用到。
在傳統應用的單一代碼庫中,各模塊間可直接相互調用函數。但在微服務架構中,因爲每一個服務對應的代碼庫是獨立運行的,沒法直接調用,彼此間的通訊就是個大問題,解決方案有 2 個:
微服務之間可以使用基於 HTTP 的 JSON 或 XML 協議進行通訊:服務 A 與服務 B 進行通訊前,A 必須把要傳遞的數據 encode 成 JSON / XML 格式,再以字符串的形式傳遞給 B,B 接收到數據須要 decode 後才能在代碼中使用:
下邊的 JSON 數據就使用 description
、weight
等元數據來描述數據自己的意義,在 Browser / Server 架構中用得不少,以方便瀏覽器解析:
{ "description": "This is a test consignment", "weight": 550, "containers": [ { "customer_id": "cust001", "user_id": "user001", "origin": "Manchester, United Kingdom" } ], "vessel_id": "vessel001" }
但在兩個微服務之間通訊時,若彼此約定好傳輸數據的格式,可直接使用二進制數據流進行通訊,再也不須要笨重冗餘的元數據。
gRPC 是谷歌開源的輕量級 RPC 通訊框架,其中的通訊協議基於二進制數據流,使得 gRPC 具備優異的性能。
gRPC 支持 HTTP 2.0 協議,使用二進制幀進行數據傳輸,還能夠爲通訊雙方創建持續的雙向數據流。可參考:Google HTTP/2 簡介
兩個微服務之間經過基於 HTTP 2.0 二進制數據幀通訊,那麼如何約定二進制數據的格式呢?答案是使用 gRPC 內置的 protobuf 協議,其 DSL 語法 可清晰定義服務間通訊的數據結構。可參考:gRPC Go: Beyond the basics
通過上邊必要的概念解釋,如今讓咱們開始開發咱們的第一個微服務:consignment-service
假設本項目名爲 shippy,你須要:
$GOPATH
的 src 目錄下新建 shippy 項目目錄consignment-service/proto/consignment/consignment.proto
爲便於教學,我會把本項目的全部微服務的代碼統一放在 shippy 目錄下,這種項目結構被稱爲 "mono-repo",讀者也能夠按照 "multi-repo" 將各個微服務拆爲獨立的項目。更多參考 REPO 風格之爭:MONO VS MULTI
如今你的項目結構應該以下:
$GOPATH/src └── shippy └── consignment-service └── proto └── consignment └── consignment.proto
// shipper/consignment-service/proto/consignment/consignment.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;// 新託運的貨物 }
語法參考: Protobuf doc
爲避免重複的在終端執行編譯、運行命令,本項目使用 make 工具,新建 consignment-service/Makefile
build: # 必定要注意 Makefile 中的縮進,不然 make build 可能報錯 Nothing to be done for build # protoc 命令前邊是一個 Tab,不是四個或八個空格 protoc -I. --go_out=plugins=grpc:$(GOPATH)/src/shippy/consignment-service proto/consignment/consignment.proto
執行 make build
,會在 proto/consignment
目錄下生成 consignment.pb.go
service:定義了微服務 ShippingService 要暴露爲外界調用的函數:CreateConsignment
,由 protobuf 編譯器的 grpc 插件處理後生成 interface
type ShippingServiceClient interface { // 託運一批貨物 CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error) }
message:定義了通訊的數據格式,由 protobuf 編譯器處理後生成 struct
type Consignment struct { Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` Description string `protobuf:"bytes,2,opt,name=description" json:"description,omitempty"` Weight int32 `protobuf:"varint,3,opt,name=weight" json:"weight,omitempty"` Containers []*Container `protobuf:"bytes,4,rep,name=containers" json:"containers,omitempty"` // ... }
服務端需實現 ShippingServiceClient
接口,建立consignment-service/main.go
package main import ( // 導如 protoc 自動生成的包 pb "shippy/consignment-service/proto/consignment" "context" "net" "log" "google.golang.org/grpc" ) const ( PORT = ":50051" ) // // 倉庫接口 // type IRepository interface { Create(consignment *pb.Consignment) (*pb.Consignment, error) // 存放新貨物 } // // 咱們存放多批貨物的倉庫,實現了 IRepository 接口 // type Repository struct { consignments []*pb.Consignment } func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) { repo.consignments = append(repo.consignments, consignment) return consignment, nil } func (repo *Repository) GetAll() []*pb.Consignment { return repo.consignments } // // 定義微服務 // type service struct { repo Repository } // // service 實現 consignment.pb.go 中的 ShippingServiceServer 接口 // 使 service 做爲 gRPC 的服務端 // // 託運新的貨物 func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) { // 接收承運的貨物 consignment, err := s.repo.Create(req) if err != nil { return nil, err } resp := &pb.Response{Created: true, Consignment: consignment} return resp, nil } func main() { listener, err := net.Listen("tcp", PORT) if err != nil { log.Fatalf("failed to listen: %v", err) } log.Printf("listen on: %s\n", PORT) server := grpc.NewServer() repo := Repository{} // 向 rRPC 服務器註冊微服務 // 此時會把咱們本身實現的微服務 service 與協議中的 ShippingServiceServer 綁定 pb.RegisterShippingServiceServer(server, &service{repo}) if err := server.Serve(listener); err != nil { log.Fatalf("failed to serve: %v", err) } }
上邊的代碼實現了 consignment-service 微服務所須要的方法,並創建了一個 gRPC 服務器監聽 50051 端口。若是你此時運行 go run main.go
,將成功啓動服務端:
咱們將要託運的貨物信息放到 consignment-cli/consignment.json
:
{ "description": "This is a test consignment", "weight": 550, "containers": [ { "customer_id": "cust001", "user_id": "user001", "origin": "Manchester, United Kingdom" } ], "vessel_id": "vessel001" }
客戶端會讀取這個 JSON 文件並將該貨物託運。在項目目錄下新建文件:consingment-cli/cli.go
package main import ( pb "shippy/consignment-service/proto/consignment" "io/ioutil" "encoding/json" "errors" "google.golang.org/grpc" "log" "os" "context" ) const ( ADDRESS = "localhost:50051" DEFAULT_INFO_FILE = "consignment.json" ) // 讀取 consignment.json 中記錄的貨物信息 func parseFile(fileName string) (*pb.Consignment, error) { data, err := ioutil.ReadFile(fileName) if err != nil { return nil, err } var consignment *pb.Consignment err = json.Unmarshal(data, &consignment) if err != nil { return nil, errors.New("consignment.json file content error") } return consignment, nil } func main() { // 鏈接到 gRPC 服務器 conn, err := grpc.Dial(ADDRESS, grpc.WithInsecure()) if err != nil { log.Fatalf("connect error: %v", err) } defer conn.Close() // 初始化 gRPC 客戶端 client := pb.NewShippingServiceClient(conn) // 在命令行中指定新的貨物信息 json 文件 infoFile := DEFAULT_INFO_FILE if len(os.Args) > 1 { infoFile = os.Args[1] } // 解析貨物信息 consignment, err := parseFile(infoFile) if err != nil { log.Fatalf("parse info file error: %v", err) } // 調用 RPC // 將貨物存儲到咱們本身的倉庫裏 resp, err := client.CreateConsignment(context.Background(), consignment) if err != nil { log.Fatalf("create consignment error: %v", err) } // 新貨物是否託運成功 log.Printf("created: %t", resp.Created) }
運行 go run main.go
後再運行 go run cli.go
:
咱們能夠新增一個 RPC 查看全部被託運的貨物,加入一個GetConsignments
方法,這樣,咱們就能看到全部存在的consignment
了:
// shipper/consignment-service/proto/consignment/consignment.proto syntax = "proto3"; package go.micro.srv.consignment; // 貨輪微服務 service ShippingService { // 託運一批貨物 rpc CreateConsignment (Consignment) returns (Response) { } // 查看託運貨物的信息 rpc GetConsignments (GetRequest) 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; // 新託運的貨物 repeated Consignment consignments = 3; // 目前全部託運的貨物 } // 查看貨物信息的請求 // 客戶端想要從服務端請求數據,必須有請求格式,哪怕爲空 message GetRequest { }
如今運行make build
來得到最新編譯後的微服務界面。若是此時你運行go run main.go
,你會得到一個相似這樣的錯誤信息:
熟悉Go的你確定知道,你忘記實現一個interface
所須要的方法了。讓咱們更新consignment-service/main.go
:
package main import ( pb "shippy/consignment-service/proto/consignment" "context" "net" "log" "google.golang.org/grpc" ) const ( PORT = ":50051" ) // // 倉庫接口 // type IRepository interface { Create(consignment *pb.Consignment) (*pb.Consignment, error) // 存放新貨物 GetAll() []*pb.Consignment // 獲取倉庫中全部的貨物 } // // 咱們存放多批貨物的倉庫,實現了 IRepository 接口 // type Repository struct { consignments []*pb.Consignment } func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) { repo.consignments = append(repo.consignments, consignment) return consignment, nil } func (repo *Repository) GetAll() []*pb.Consignment { return repo.consignments } // // 定義微服務 // type service struct { repo Repository } // // 實現 consignment.pb.go 中的 ShippingServiceServer 接口 // 使 service 做爲 gRPC 的服務端 // // 託運新的貨物 func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) { // 接收承運的貨物 consignment, err := s.repo.Create(req) if err != nil { return nil, err } resp := &pb.Response{Created: true, Consignment: consignment} return resp, nil } // 獲取目前全部託運的貨物 func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest) (*pb.Response, error) { allConsignments := s.repo.GetAll() resp := &pb.Response{Consignments: allConsignments} return resp, nil } func main() { listener, err := net.Listen("tcp", PORT) if err != nil { log.Fatalf("failed to listen: %v", err) } log.Printf("listen on: %s\n", PORT) server := grpc.NewServer() repo := Repository{} pb.RegisterShippingServiceServer(server, &service{repo}) if err := server.Serve(listener); err != nil { log.Fatalf("failed to serve: %v", err) } }
若是如今使用go run main.go
,一切應該正常:
最後讓咱們更新consignment-cli/cli.go
來得到consignment
信息:
func main() { ... // 列出目前全部託運的貨物 resp, err = client.GetConsignments(context.Background(), &pb.GetRequest{}) if err != nil { log.Fatalf("failed to list consignments: %v", err) } for _, c := range resp.Consignments { log.Printf("%+v", c) } }
此時再運行go run cli.go
,你應該能看到所建立的全部consignment
,屢次運行將看到多個貨物被託運:
至此,咱們使用protobuf和grpc建立了一個微服務以及一個客戶端。
在下一篇文章中,咱們將介紹使用go-micro
框架,以及建立咱們的第二個微服務。同時在下一篇文章中,咱們將介紹如何容Docker來容器化咱們的微服務。