這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。## 介紹Golang 中的微服務系列總計十部分,預計每周更新。本系列的解決方案採用了 protobuf 和 gRPC 作為底層傳輸協議。為什麼採用這兩個技術呢?我花了相當長的時間,才想出並決定採用這個方案。這個方案對開發人員來說,非常清晰而簡明。我也很樂意把自己在搭建、測試和部署端到端的微服務過程中的心得,分享給想接觸這塊的朋友們。在這個教程中,我們將先接觸幾個基礎的概念和術語,然後開始搭建第一個簡單的微服務模型。本系列中,我們將會建立以下服務:- 委託- 存貨清單- 使用者- 認證- 角色- 容器整個技術棧從底至頂主要可劃分為:golang、mongodb、grpc、docker、Google Cloud、Kubernetes、NATS、CircleCI、Terraform 和 go-micro。接下來,你可以根據我的 [git 倉庫](https://github.com/EwanValentine/shippy)(每篇文章都有自己的分支)中的指導逐步操作,不過必須注意把根據你的開發環境調整 GOPATH 。同時你需要注意,我是在 Macbook 上開發的,你或許會需要替換 Makefiles 中的 `$(GOPATH)` 為 `$GOPATH` 。作業系統不一致帶來的問題可能不止這一個,但這裡就不一一列舉了。## 先決條件- 掌握 golang 語言和其開發環境- 安裝 gRPC / protobuf [查看連結](https://grpc.io/docs/quickstart/go.html)- 安裝 golang [查看連結](https://golang.org/doc/install)- 按照下列指令,安裝 go 的第三方庫```go get -u google.golang.org/grpc go get -u github.com/golang/protobuf/protoc-gen-go ```## 我們要搭建的是?我們將搭建的是一個非常通用的微服務 —— 船運集裝箱的管理平台。當然,我也可以用微服務搭建一個部落格作為例子,但這實在是太簡單了,我更希望能夠展示**分離複雜性**的功能。最終我選擇了這個管理平台為例,作為一個挑戰!那麼,接下來我們先瞭解幾個知識點:## 什麼是微服務?在傳統的單體應用中,所有系統的特性都被寫入單個應用程式中。有時候我們用類型來區分這些特性,例如控制器、單元模組、工廠等等;其它情況下,例如在更大型的應用中,用互相間的關係或者各自的特徵來區分應用特性,所以你可能會有授權程式包、好友關係處理包以及文章管理組件,這些包可能都有各自的工廠、服務、資料庫、資料模型等。不過,最終它們都被塞入了一個程式碼程式庫中。微服務就是把第二種解決方案做得更徹底:將原先的關係分離出來,每個程式包都儲存到獨立的、可啟動並執行程式碼程式庫中。## 為什麼選用微服務?**複雜性** —— 依照特性把程式分割成多個微服務,有助於把大塊代碼分割成更小的模組。正如一句 Unix 中的老格言所說:把一件事做好(doing one thing well)。在單體應用的系統中,各模組傾向於緊密結合,模組間的關係很模糊。這個會導致系統的升級更為危險和複雜、存在更多的潛在 bug、整合的難度更高。**擴充性** —— 在單體應用的系統中,總有特定模組的代碼會比其餘模組用得更為頻繁,而你只能擴大整個庫的規模來解決。例如你的鑒權模組被高頻率地調用,對系統造成了高負荷的壓力。於是你擴大了庫規模,而原因僅僅是一個小小鑒權模組。如果換成了微服務,那麼你可以獨立地擴大任何一個服務模組,這意味著我們可以更有效地進行橫向擴充。這種分離性對多核、多地區的雲端運算帶來了極大的協助。**Nginx 有個極好的微服務系列,講述了各種概念,[請點選連結訪問](https://www.nginx.com/blog/introduction-to-microservices/)。**## 為什麼選擇 Golang?幾乎所有的語言都支援微服務。微服務不是一個具體的架構或工具,而是一個概念。這就意味著,在選擇構建微服務的語言中,總有一些更為合適、或者說支援性更好。Golang 就是其中的佼佼者。Golang 是一個輕量級、運行速度快、對高並發支援極好的語言,很有力地支援了多核、多裝置啟動並執行情境。Golang 在網路服務上,也具有強大的標準庫。目前,已有一個強大的微服務架構 —— **go-micro**,我們在這個系列中會用到它。## protobuf/gRPC 簡介微服務被分割成多個獨立的程式碼程式庫,這就帶來了一個重要的問題 —— 通訊。在單體應用的系統中,你可以在程式碼程式庫的任何地方調用想要的代碼,所以不存在通訊的問題。而微服務分布在不同的程式碼程式庫中,不具備直接調用的能力。所以,你需要找到一個途徑,使得不同服務之間可以儘可能低延遲地進行資料互動。這裡,我們可以採用傳統的 REST 架構,例如通過 http 傳輸 JSON 或者 XML 。但這種方案會帶來了一個問題:服務 A 把未經處理資料編碼成 JSON/XML 格式,發送一長串字元給服務 B,B 通過解碼還原成未經處理資料。不過,當未經處理資料量很大時,這可能對通訊造成嚴重影響。當我們和網路瀏覽器的通訊時,只要約定了服務間的通訊方式、固定了編碼和解碼方法,那麼格式可以任意。[gRPC](https://grpc.io/) 就應運而生。gRPC 是一個由 Google 開發、基於 RPC 通訊、輕量級的二進位傳輸協議。這個定義有點複雜,下面請由我細細道來。gRPC 核心資料格式採用的是二進位,而在上面 RESTful 的例子中,我們用的是 JSON 格式,也就是通過 http 發送一串字串。字串包括了它的編碼格式、長度和其它佔用位元組的資訊,所以總體資料量很大。基於用戶端的字串資料,伺服器可以通知傳統的瀏覽器,解析得到預期的資料。但在兩個微服務的通訊間,我們不需要字串中的所有資料,所以我們採用難理解但更加輕量的位元據進行互動。gRPC 採用的是支援位元據的 HTTP 2.0 規範,這個規範還能支援雙向的通訊流,相當炫酷!HTTP 2 是 gRPC 工作的基礎。如果你想進一步瞭解 HTTP 2,可以點擊這個[Google 連結](https://developers.google.com/web/fundamentals/performance/http2/)。接下來的問題是,位元據該如何處理呢?不用擔心,gRPC 有一個內部的數字類比語言,叫 protobuf。Protobuf 支援自訂介面格式,對開發人員很友好。瞭解 gRPC 和 protobuf,我們準備實戰,開始建立第一個服務的定義。首先,在程式碼程式庫的根目錄下建立如下檔案 `consignment-service/proto/consignment/consignment.proto`。目前,為了讓這個教程閱讀起來更容易,我採用的是單一倉,就是把所有的服務都存放在一個程式碼程式庫中。對使用單一倉很多爭論和反對意見,這邊我暫不深入探討。當然,你在開發中最好把不同的服務和組件存放在分離的代碼倉中,這種方式普遍受歡迎,但比較複雜。在剛才建立的 `consignment.proto` 檔案中,添加如下內容:```protobuf// consignment-service/proto/consignment/consignment.protosyntax = "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;}```這是個非常基礎的定義的例子,不過還是有幾點需要我們掌握。首先,你定義了服務內容,它應該包括你希望暴露給其他服務的方法。接著,你需要定義訊息類型,這些資料結構體都非常簡潔。正如上面的 `Container` 結構體,Protobuf 是一種可以自訂的靜態類型。每個訊息體都是他們自訂的類型。現在已經使用到了兩個庫:訊息通過 protobuf 處理;服務通過 gRPC 的 protobuf 外掛程式處理,把訊息編譯成代碼,從而進行互動,正如 proto 檔案中的 `service` 部分。protobuf 定義的結構,可以通過用戶端介面,自動產生相應語言的位元據和功能。說到這,我們就來一起給我們的服務建立一個 Makefile,路徑如下 `$ touch consignment-service/Makefile`。```makefilebuild: protoc -I. --go_out=plugins=grpc:$(GOPATH)/src/github.com/ewanvalentine/shipper/consignment-service \ proto/consignment/consignment.proto```這個 Makefile 會調用 protoc 庫,將你的 protobuf 編譯成對應的代碼。同時,我們也指定了 gRPC 外掛程式、編譯目錄和輸出目錄。產生 Makefile 檔案後,進入服務所在的檔案夾,運行 `$ make build` 指令,然後你就能在 `proto/consignment/` 下看到一個名為 `consignment.pb.go` 的新 Go 檔案。這裡使用了 gRPC/protobuf 庫,把自訂的 protobuf 結構自動轉換成你想要的代碼。接下來,我們就可以正式搭建服務了。進入項目的根目錄,建立一個檔案 main.go `$ touch consignment-service/main.go`。```go// consignment-service/main.gopackage mainimport ( "log" "net" // 匯入產生的 protobuf 代碼 pb "github.com/ewanvalentine/shipper/consignment-service/proto/consignment" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/reflection")const ( port = ":50051")type IRepository interface { Create(*pb.Consignment) (*pb.Consignment, error)}// Repository - 一個類比資料儲存的虛擬倉庫,以後我們會替換成真實的資料倉儲type Repository struct { consignments []*pb.Consignment}func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) { updated := append(repo.consignments, consignment) repo.consignments = updated return consignment, nil}// 服務需要實現所有在 protobuf 裡定義的方法。// 你可以參考 protobuf 產生的 go 檔案中的介面資訊。type service struct { repo IRepository}// CreateConsignment - 目前只建立了這個方法,包括 `ctx` (環境資訊)和 `req`(委託請求)兩個參數,會通過 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 } // 返回和 protobuf 中定義匹配的 `Response` 訊息 return &pb.Response{Created: true, Consignment: consignment}, nil}func main() { repo := &Repository{} // 啟動 gRPC 伺服器。 lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() // 註冊服務到 gRPC 伺服器,會把已定義的 protobuf 與自動產生的程式碼介面進行綁定。 pb.RegisterShippingServiceServer(s, &service{repo}) // 在 gRPC 伺服器上註冊 reflection 服務。 reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) }}```請仔細閱讀代碼中的注釋,有助於你對這個服務的理解。簡單來說,這些代碼實現的功能是:在 50051 連接埠建立一個的 gRPC 伺服器,通過 protobuf 產生的訊息格式,實現 gRPC 介面互動的邏輯。就這樣,你完成了一個完整功能的 gRPC 服務!你可以輸入指令 `$ go run main.go` 來運行這個程式,不過,目前,從介面上你還看不到任何東西。那如何能直觀看到這個 gRPC 伺服器正常工作了呢?我們來一起建立個與它對接的用戶端吧!下面,我們來寫一個命令列互動的程式,用來讀取一個包含委託資訊的 JSON 檔案,和我們已建立的 gRPC 伺服器互動。進入根目錄,輸入命令列建立一個新的子檔案夾 `$ mkdir consignment-cli`。在檔案夾中,建立一個新檔案 `cli.go`,代碼如下:```go// consignment-cli/cli.gopackage mainimport ( "encoding/json" "io/ioutil" "log" "os" pb "github.com/ewanvalentine/shipper/consignment-service/proto/consignment" "golang.org/x/net/context" "google.golang.org/grpc")const ( address = "localhost:50051" defaultFilename = "consignment.json")func parseFile(file string) (*pb.Consignment, error) { var consignment *pb.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("Did not connect: %v", err) } defer conn.Close() client := pb.NewShippingServiceClient(conn) // 和伺服器通訊,並列印出返回資訊 file := defaultFilename if len(os.Args) > 1 { file = os.Args[1] } consignment, err := parseFile(file) if err != nil { log.Fatalf("Could not parse file: %v", err) } r, err := client.CreateConsignment(context.Background(), consignment) if err != nil { log.Fatalf("Could not greet: %v", err) } log.Printf("Created: %t", r.Created)}```同時,再建立一個委託資訊檔 `consignment-cli/consignment.json````json{ "description": "This is a test consignment", "weight": 550, "containers": [ { "customer_id": "cust001", "user_id": "user001", "origin": "Manchester, United Kingdom" } ], "vessel_id": "vessel001"}```完成以上步驟後,在 `consignment-service` 下運行 `$ go run main.go`,然後開啟一個新的終端介面,運行 `$ go run cli.go`,這時你就能看到一條訊息 `Created: true`。不過,如何我們才能確認,這個委託真正地產生了?讓我們繼續更新我們的服務,添加一個 `GetConsignments` 方法,能夠看到所有已建立的委託。首先需要更新我們的 proto 定義(我在修改部分添加了備忘)```protobuf// consignment-service/proto/consignment/consignment.protosyntax = "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 GetRequest {}message Response { bool created = 1; Consignment consignment = 2; // 增加一個數組,用來返回委託列表 repeated Consignment consignments = 3;}```我們成功地在服務上建立了一個叫 `GetConsignments` 和 `GetRequest` 的新方法,後者目前不含任何內容。我們也在回複的訊息中,添加了 `consignments` 參數。你可能會注意到,該參數的類型前有個關鍵詞:`repeated`。顧名思義,這代表該參數是以數組的方式儲存的。現在,讓我們再次運行 `$ make build` 指令,並啟動你的服務,你會看到一個類似 `*service does not implement go_micro_srv_consignment.ShippingServiceServer (missing GetConsignments method)` 的錯誤資訊。protobuf 庫產生的介面在通訊兩端必須完全符合,這是實現 gRPC 的基礎。所以,我們需要確認 proto 的定義是否一致。讓我們更新下 `consignment-service/main.go` 檔案:```gopackage mainimport ( "log" "net" // 匯入產生的 protobuf 代碼 pb "github.com/ewanvalentine/shipper/consignment-service/proto/consignment" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/reflection")const ( port = ":50051")type IRepository interface { Create(*pb.Consignment) (*pb.Consignment, error) GetAll() []*pb.Consignment}// Repository - 一個類比資料儲存的虛擬倉庫,以後我們會替換成真實的資料倉儲type Repository struct { consignments []*pb.Consignment}func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) { updated := append(repo.consignments, consignment) repo.consignments = updated return consignment, nil}func (repo *Repository) GetAll() []*pb.Consignment { return repo.consignments}// 服務需要實現所有在 protobuf 裡定義的方法。// 你可以參考 protobuf 產生的 go 檔案中的介面資訊。type service struct { repo IRepository}// CreateConsignment - 目前只建立了這個方法,包括 `ctx` (環境資訊)和 `req`(委託請求)兩個參數,會通過 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 } // 返回和 protobuf 中定義匹配的 `Response` 訊息 return &pb.Response{Created: true, Consignment: consignment}, nil}func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest) (*pb.Response, error) { consignments := s.repo.GetAll() return &pb.Response{Consignments: consignments}, nil}func main() { repo := &Repository{} // 啟動 gRPC 伺服器。 lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() //註冊服務到 gRPC 伺服器,會把已定義的 protobuf 與自動產生的程式碼介面進行綁定。 pb.RegisterShippingServiceServer(s, &service{repo}) // 在 gRPC 伺服器上註冊 reflection 服務。 reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) }}```現在,我們引用了新的 `GetConsignments` 方法、更新了庫和介面,也就滿足了兩邊的 proto 定義一致。再次運行 `go run main.go` 後,服務能正常工作了。再回到我們的用戶端工具,我們通過調用 `GetConsignments` 這個方法,列出所有的委託:```gofunc main() { ... // ·...`表示和之前代碼一致,這裡不再重複 getAll, err := client.GetConsignments(context.Background(), &pb.GetRequest{}) if err != nil { log.Fatalf("Could not list consignments: %v", err) } for _, v := range getAll.Consignments { log.Println(v) }}```在原先 main 函數中,找到列印 `Created: success` 日誌的位置,在這之後添加上述代碼,然後運行 `$ go run cli.go`。程式就會建立一個委託,緊接著調用 `GetConsignments`。當你運行次數越多,委託列表就會越來越長。*注意:為了看起來簡潔,我有時會用 `...` 來表示和之前的代碼完全一致。之後幾行新增的代碼,需要手動添加到原代碼中*到這裡,我們通過 protobuf 和 gRPC,完整地建立了一個微服務,以及一個與之互動的用戶端。本系列的下一章節將圍繞著整合 [go-micro](https://github.com/micro/go-micro) 展開。go-micro 是一個基於微服務的、建立 gRPC 的強大架構。我們也會在下章建立第二個微服務 —— Container Service。光說“容器”二字也許會令你困惑,這裡具體指的是 Docker 中的“容器”概念。我們會在下一章探索微服務在 Docker 容器中的運行情況。如果對此文有任何 bug、錯誤或者反饋,請直接郵箱[聯絡我](ewan.valentine89@gmail.com)。本教程所包含的程式碼程式庫[連結](https://github.com/ewanvalentine/shippy),用 `git` 工具 checkout 分支`tutorial-1` 第一章。第二章也將在近期更新。編寫本文花了我很長的時間以及大量的精力。如果你覺得這個系列有協助,請考慮順手打賞我(完全取決於你的意願)。十分感謝鳴謝:Microservices Newsletter (22nd November 2017)
via: https://ewanvalentine.io/microservices-in-golang-part-1/
作者:Ewan Valentine 譯者:Junedayday 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
4627 次點擊 ∙ 6 贊