摘要:本節將學習來統一管理和部署微服務,引入第三個微服務并進行存儲數據。到目前為止,要想啟動微服務的容器,均在其中的同時設置其環境變量,服務多了以后管理起來十分麻煩。
譯文鏈接:wuYin/blog
原文鏈接:ewanvalentine.io,翻譯已獲作者 Ewan Valentine 授權。
本文完整代碼:GitHub
在上節中,我們使用 go-micro 重新實現了微服務并進行了 Docker 化,但是每個微服務都要多帶帶維護自己的 Makefile 未免過于繁瑣。本節將學習 docker-compose 來統一管理和部署微服務,引入第三個微服務 user-service 并進行存儲數據。
MongoDB 與 Postgres 微服務的數據存儲到目前為止,consignment-cli 要托運的貨物數據直接存儲在 consignment-service 管理的內存中,當服務重啟時這些數據將會丟失。為了便于管理和搜索貨物信息,需將其存儲到數據庫中。
可以為每個獨立運行的微服務提供獨立的數據庫,不過因為管理繁瑣少有人這么做。如何為不同的微服務選擇合適的數據庫,可參考:How to choose a database for your microservices
選擇關系型數據庫與 NoSQL如果對存儲數據的可靠性、一致性要求不那么高,那 NoSQL 將是很好的選擇,因為它能存儲的數據格式十分靈活,比如常常將數據存為 JSON 進行處理,在本節中選用性能和生態俱佳的MongoDB
如果要存儲的數據本身就比較完整,數據之間關系也有較強關聯性的話,可以選用關系型數據庫。事先捋一下要存儲數據的結構,根據業務看一下是讀更多還是寫更多?高頻查詢的復不復雜?… 鑒于本文的較小的數據量與操作,作者選用了 Postgres,讀者可自行更換為 MySQL 等。
更多參考:如何選擇NoSQL數據庫、梳理關系型數據庫和NoSQL的使用情景
docker-compose 引入原因上節把微服務 Docker 化后,使其運行在輕量級、只包含服務必需依賴的容器中。到目前為止,要想啟動微服務的容器,均在其 Makefile 中 docker run 的同時設置其環境變量,服務多了以后管理起來十分麻煩。
基本使用docker-compose 工具能直接用一個 docker-compose.yaml 來編排管理多個容器,同時設置各容器的 metadata 和 run-time 環境(環境變量),文件的 service 配置項來像先前 docker run 命令一樣來啟動容器。舉個例子:
docker 命令管理容器
$ docker run -p 50052:50051 -e MICRO_SERVER_ADDRESS=:50051 -e MICRO_REGISTRY=mdns vessel-service
等效于 docker-compose 來管理
version: "3.1" vessel-service: build: ./vessel-service ports: - 50052:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns"
想加減和配置微服務,直接修改 docker-compose.yaml,是十分方便的。
更多參考:使用 docker-compose 編排容器
編排當前項目的容器針對當前項目,使用 docker-compose 管理 3 個容器,在項目根目錄下新建文件:
# docker-compose.yaml # 同樣遵循嚴格的縮進 version: "3.1" # services 定義容器列表 services: consignment-cli: build: ./consignment-cli environment: MICRO_REGISTRY: "mdns" consignment-service: build: ./consignment-service ports: - 50051:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns" DB_HOST: "datastore:27017" vessel-service: build: ./vessel-service ports: - 50052:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns"
首先,我們指定了要使用的 docker-compose 的版本是 3.1,然后使用 services 來列出了三個待管理的容器。
每個微服務都定義了自己容器的名字, build 指定目錄下的 Dockerfile 將會用來編譯鏡像,也可以直接使用 image 選項直接指向已編譯好的鏡像(后邊會用到);其他選項則指定了容器的端口映射規則、環境變量等。
可使用 docker-compose build 來編譯生成三個對應的鏡像;使用 docker-compose run 來運行指定的容器, docker-compose up -d 可在后臺運行;使用 docker stop $(docker ps -aq ) 來停止所有正在運行的容器。
運行效果使用 docker-compose 的運行效果如下:
Protobuf 與數據庫操作 復用及其局限性到目前為止,我們的兩個 protobuf 協議文件,定義了微服務客戶端與服務端數據請求、響應的數據結構。由于 protobuf 的規范性,也可將其生成的 struct 作為數據庫表 Model 進行數據操作。這種復用有其局限性,比如 protobuf 中數據類型必須與數據庫表字段嚴格一致,二者是高耦合的。很多人并不贊將 protobuf 數據結構作為數據庫中的表結構:Do you use Protobufs in place of structs ?
中間層邏輯轉換一般來說,在表結構變化后與 protobuf 不一致,需要在二者之間做一層邏輯轉換,處理差異字段:
func (service *Service) (ctx context.Context, req *proto.User, res *proto.Response) error { entity := &models.User{ Name: req.Name. Email: req.Email, Password: req.Password, } err := service.repo.Create(entity) // 無中間轉換層 // err := service.repo.Create(req) ... }
這樣隔離數據庫實體 models 和 proto.* 結構體,似乎很方便。但當 .proto 中定義 message 各種嵌套時,models 也要對應嵌套,比較麻煩。
上邊隔不隔離由讀者自行決定,就我個人而言,中間用 models 做轉換是不太有必要的,protobuf 已足夠規范,直接使用即可。
consignment-service 重構回頭看第一個微服務 consignment-service,會發現服務端實現、接口實現等都往 main.go 里邊塞,功能跑通了,現在要拆分代碼,使項目結構更加清晰,更易維護。
MVC 代碼結構對于熟悉 MVC 開發模式的同學來說,可能會把代碼按功能拆分到不同目錄中,比如:
main.go models/ user.go handlers/ auth.go user.go services/ auth.go微服務代碼結構
不過這種組織方式并不是 Golang 的 style,因為微服務是切割出來獨立的,要做到簡潔明了。對于大型 Golang 項目,應該如下組織:
main.go users/ services/ auth.go handlers/ auth.go user.go users/ user.go containers/ services/ manage.go models/ container.go
這種組織方式叫類別(domain)驅動,而不是 MVC 的功能驅動。
consignment-service 的重構由于微服務的簡潔性,我們會把該服務相關的代碼全放到一個文件夾下,同時為每個文件起一個合適的名字。
在 consignmet-service/ 下創建三個文件:handler.go、datastore.go 和 repository.go
consignmet-service/ ├── Dockerfile ├── Makefile ├── datastore.go # 創建與 MongoDB 的主會話 ├── handler.go # 實現微服務的服務端,處理業務邏輯 ├── main.go # 注冊并啟動服務 ├── proto └── repository.go # 實現數據庫的基本 CURD 操作負責連接 MongoDB 的 datastore.go
package main import "gopkg.in/mgo.v2" // 創建與 MongoDB 交互的主回話 func CreateSession(host string) (*mgo.Session, error) { s, err := mgo.Dial(host) if err != nil { return nil, err } s.SetMode(mgo.Monotonic, true) return s, nil }
連接 MongoDB 的代碼夠精簡,傳參是數據庫地址,返回數據庫會話以及可能發生的錯誤,在微服務啟動的時候就會去連接數據庫。
負責與 MongoDB 交互的 repository.go現在讓我們來將 main.go 與數據庫交互的代碼拆解出來,可以參考注釋加以理解:
package main import (...) const ( DB_NAME = "shippy" CON_COLLECTION = "consignments" ) type Repository interface { Create(*pb.Consignment) error GetAll() ([]*pb.Consignment, error) Close() } type ConsignmentRepository struct { session *mgo.Session } // 接口實現 func (repo *ConsignmentRepository) Create(c *pb.Consignment) error { return repo.collection().Insert(c) } // 獲取全部數據 func (repo *ConsignmentRepository) GetAll() ([]*pb.Consignment, error) { var cons []*pb.Consignment // Find() 一般用來執行查詢,如果想執行 select * 則直接傳入 nil 即可 // 通過 .All() 將查詢結果綁定到 cons 變量上 // 對應的 .One() 則只取第一行記錄 err := repo.collection().Find(nil).All(&cons) return cons, err } // 關閉連接 func (repo *ConsignmentRepository) Close() { // Close() 會在每次查詢結束的時候關閉會話 // Mgo 會在啟動的時候生成一個 "主" 會話 // 你可以使用 Copy() 直接從主會話復制出新會話來執行,即每個查詢都會有自己的數據庫會話 // 同時每個會話都有自己連接到數據庫的 socket 及錯誤處理,這么做既安全又高效 // 如果只使用一個連接到數據庫的主 socket 來執行查詢,那很多請求處理都會阻塞 // Mgo 因此能在不使用鎖的情況下完美處理并發請求 // 不過弊端就是,每次查詢結束之后,必須確保數據庫會話要手動 Close // 否則將建立過多無用的連接,白白浪費數據庫資源 repo.session.Close() } // 返回所有貨物信息 func (repo *ConsignmentRepository) collection() *mgo.Collection { return repo.session.DB(DB_NAME).C(CON_COLLECTION) }拆分后的 main.go
package main import (...) const ( DEFAULT_HOST = "localhost:27017" ) func main() { // 獲取容器設置的數據庫地址環境變量的值 dbHost := os.Getenv("DB_HOST") if dbHost == ""{ dbHost = DEFAULT_HOST } session, err := CreateSession(dbHost) // 創建于 MongoDB 的主會話,需在退出 main() 時候手動釋放連接 defer session.Close() if err != nil { log.Fatalf("create session error: %v ", err) } server := micro.NewService( // 必須和 consignment.proto 中的 package 一致 micro.Name("go.micro.srv.consignment"), micro.Version("latest"), ) // 解析命令行參數 server.Init() // 作為 vessel-service 的客戶端 vClient := vesselPb.NewVesselServiceClient("go.micro.srv.vessel", server.Client()) // 將 server 作為微服務的服務端 pb.RegisterShippingServiceHandler(server.Server(), &handler{session, vClient}) if err := server.Run(); err != nil { log.Fatalf("failed to serve: %v", err) } }實現服務端的 handler.go
將 main.go 中實現微服務服務端 interface 的代碼多帶帶拆解到 handler.go,實現業務邏輯的處理。
package main import (...) // 微服務服務端 struct handler 必須實現 protobuf 中定義的 rpc 方法 // 實現方法的傳參等可參考生成的 consignment.pb.go type handler struct { session *mgo.Session vesselClient vesselPb.VesselServiceClient } // 從主會話中 Clone() 出新會話處理查詢 func (h *handler)GetRepo()Repository { return &ConsignmentRepository{h.session.Clone()} } func (h *handler)CreateConsignment(ctx context.Context, req *pb.Consignment, resp *pb.Response) error { defer h.GetRepo().Close() // 檢查是否有適合的貨輪 vReq := &vesselPb.Specification{ Capacity: int32(len(req.Containers)), MaxWeight: req.Weight, } vResp, err := h.vesselClient.FindAvailable(context.Background(), vReq) if err != nil { return err } // 貨物被承運 log.Printf("found vessel: %s ", vResp.Vessel.Name) req.VesselId = vResp.Vessel.Id //consignment, err := h.repo.Create(req) err = h.GetRepo().Create(req) if err != nil { return err } resp.Created = true resp.Consignment = req return nil } func (h *handler)GetConsignments(ctx context.Context, req *pb.GetRequest, resp *pb.Response) error { defer h.GetRepo().Close() consignments, err := h.GetRepo().GetAll() if err != nil { return err } resp.Consignments = consignments return nil }
至此,main.go 拆分完畢,代碼文件分工明確,十分清爽。
mgo 庫的 Copy() 與 Clone()在 handler.go 的 GetRepo() 中我們使用 Clone() 來創建新的數據庫連接。
可看到在 main.go 中創建主會話后我們就再也沒用到它,反而使用 session.Clonse() 來創建新的會話進行查詢處理,可以看 repository.go 中 Close() 的注釋,如果每次查詢都用主會話,那所有請求都是同一個底層 socket 執行查詢,后邊的請求將會阻塞,不能發揮 Go 天生支持并發的優勢。
為了避免請求的阻塞,mgo 庫提供了 Copy() 和 Clone() 函數來創建新會話,二者在功能上相差無幾,但在細微之處卻有重要的區別。Clone 出來的新會話重用了主會話的 socket,避免了創建 socket 在三次握手時間、資源上的開銷,尤其適合那些快速寫入的請求。如果進行了復雜查詢、大數據量操作時依舊會阻塞 socket 導致后邊的請求阻塞。Copy 為會話創建新的 socket,開銷大。
應當根據應用場景不同來選擇二者,本文的查詢既不復雜數據量也不大,就直接復用主會話的 socket 即可。不過用完都要 Close(),謹記。
vessel-service 重構拆解完 consignment-service/main.go 的代碼,現在用同樣的方式重構 vessel-service
新增貨輪我們在此添加一個方法:添加新的貨輪,更改 protobuf 文件如下:
syntax = "proto3"; package go.micro.srv.vessel; service VesselService { // 檢查是否有能運送貨物的輪船 rpc FindAvailable (Specification) returns (Response) {} // 創建貨輪 rpc Create(Vessel) returns (Response){} } // ... // 貨輪裝得下的話 // 返回的多條貨輪信息 message Response { Vessel vessel = 1; repeated Vessel vessels = 2; bool created = 3; }
我們創建了一個 Create() 方法來創建新的貨輪,參數是 Vessel 返回 Response,注意 Response 中添加了 created 字段,標識是否創建成功。使用 make build 生成新的 vessel.pb.go 文件。
拆分數據庫操作與業務邏輯處理之后在對應的 repository.go 和 handler.go 中實現 Create()
// vesell-service/repository.go // 完成與數據庫交互的創建動作 func (repo *VesselRepository) Create(v *pb.Vessel) error { return repo.collection().Insert(v) }
// vesell-service/handler.go func (h *handler) GetRepo() Repository { return &VesselRepository{h.session.Clone()} } // 實現微服務的服務端 func (h *handler) Create(ctx context.Context, req *pb.Vessel, resp *pb.Response) error { defer h.GetRepo().Close() if err := h.GetRepo().Create(req); err != nil { return err } resp.Vessel = req resp.Created = true return nil }引入 MongoDB
兩個微服務均已重構完畢,是時候在容器中引入 MongoDB 了。在 docker-compose.yaml 添加 datastore 選項:
services: ... datastore: image: mongo ports: - 27017:27017
同時更新兩個微服務的環境變量,增加 DB_HOST: "datastore:27017",在這里我們使用 datastore 做主機名而不是 localhost,是因為 docker 有內置強大的 DNS 機制。參考:docker內置dnsserver工作機制
修改完畢后的 docker-compose.yaml:
# docker-compose.yaml version: "3.1" services: consigment-cli: build: ./consignment-cli environment: MICRO_REGISTRY: "mdns" consignment-service: build: ./consignment-service ports: - 50051:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns" DB_HOST: "datastore:27017" vessel-service: build: ./vessel-service ports: - 50052:50051 environment: MICRO_ADRESS: ":50051" MICRO_REGISTRY: "mdns" DB_HOST: "datastore:27017" datastore: image: mongo ports: - 27017:27017
修改完代碼需重新 make build,構建鏡像時需 docker-compose build --no-cache 來全部重新編譯。
user-service 引入 Postgres現在來創建第三個微服務,在 docker-compose.yaml 中引入 Postgres:
... user-service: build: ./user-service ports: - 50053:50051 environment: MICRO_ADDRESS: ":50051" MICRO_REGISTRY: "mdns" ... database: image: postgres ports: - 5432:5432
在項目根目錄下創建 user-service 目錄,并且像前兩個服務那樣依次創建下列文件:
handler.go, main.go, repository.go, database.go, Dockerfile, Makefile,定義 protobuf 文件
創建 proto/user/user.proto 且內容如下:
// user-service/user/user.proto syntax = "proto3"; package go.micro.srv.user; service UserService { rpc Create (User) returns (Response) {} rpc Get (User) returns (Response) {} rpc GetAll (Request) returns (Response) {} rpc Auth (User) returns (Token) {} rpc ValidateToken (Token) returns (Token) {} } // 用戶信息 message User { string id = 1; string name = 2; string company = 3; string email = 4; string password = 5; } message Request { } message Response { User user = 1; repeated User users = 2; repeated Error errors = 3; } message Token { string token = 1; bool valid = 2; Error errors = 3; } message Error { int32 code = 1; string description = 2; }
確保你的 user-service 有像類似前兩個微服務的 Makefile,使用 make build 來生成 gRPC 代碼。
實現業務邏輯處理的 handler.go在 handler.go 實現的服務端代碼中,認證模塊將在下一節使用 JWT 做認證。
// user-service/handler.go package main import ( "context" pb "shippy/user-service/proto/user" ) type handler struct { repo Repository } func (h *handler) Create(ctx context.Context, req *pb.User, resp *pb.Response) error { if err := h.repo.Create(req); err != nil { return nil } resp.User = req return nil } func (h *handler) Get(ctx context.Context, req *pb.User, resp *pb.Response) error { u, err := h.repo.Get(req.Id); if err != nil { return err } resp.User = u return nil } func (h *handler) GetAll(ctx context.Context, req *pb.Request, resp *pb.Response) error { users, err := h.repo.GetAll() if err != nil { return err } resp.Users = users return nil } func (h *handler) Auth(ctx context.Context, req *pb.User, resp *pb.Token) error { _, err := h.repo.GetByEmailAndPassword(req) if err != nil { return err } resp.Token = "`x_2nam" return nil } func (h *handler) ValidateToken(ctx context.Context, req *pb.Token, resp *pb.Token) error { return nil }實現數據庫交互的 repository.go
package main import ( "github.com/jinzhu/gorm" pb "shippy/user-service/proto/user" ) type Repository interface { Get(id string) (*pb.User, error) GetAll() ([]*pb.User, error) Create(*pb.User) error GetByEmailAndPassword(*pb.User) (*pb.User, error) } type UserRepository struct { db *gorm.DB } func (repo *UserRepository) Get(id string) (*pb.User, error) { var u *pb.User u.Id = id if err := repo.db.First(&u).Error; err != nil { return nil, err } return u, nil } func (repo *UserRepository) GetAll() ([]*pb.User, error) { var users []*pb.User if err := repo.db.Find(&users).Error; err != nil { return nil, err } return users, nil } func (repo *UserRepository) Create(u *pb.User) error { if err := repo.db.Create(&u).Error; err != nil { return err } return nil } func (repo *UserRepository) GetByEmailAndPassword(u *pb.User) (*pb.User, error) { if err := repo.db.Find(&u).Error; err != nil { return nil, err } return u, nil }使用 UUID
我們將 ORM 創建的 UUID 字符串修改為一個整數,用來作為表的主鍵或 ID 是比較安全的。MongoDB 使用了類似的技術,但是 Postgres 需要我們使用第三方庫手動來生成。在 user-service/proto/user 目錄下創建 extension.go 文件:
package go_micro_srv_user import ( "github.com/jinzhu/gorm" uuid "github.com/satori/go.uuid" "github.com/labstack/gommon/log" ) func (user *User) BeforeCreate(scope *gorm.Scope) error { uuid, err := uuid.NewV4() if err != nil { log.Fatalf("created uuid error: %v ", err) } return scope.SetColumn("Id", uuid.String()) }
函數 BeforeCreate() 指定了 GORM 庫使用 uuid 作為 ID 列值。參考:doc.gorm.io/callbacks
GORMGorm 是一個簡單易用輕量級的 ORM 框架,支持 ?Postgres, MySQL, Sqlite 等數據庫。
到目前三個微服務涉及到的數據量小、操作也少,用原生 SQL 完全可以 hold 住,所以是不是要 ORM 取決于你自己。
user-cli類比 consignment-service 的測試,現在創建 user-cli 命令行應用來測試 user-service
在項目根目錄下創建 user-cli 目錄,并創建 cli.go 文件:
package main import ( "log" "os" pb "shippy/user-service/proto/user" microclient "github.com/micro/go-micro/client" "github.com/micro/go-micro/cmd" "golang.org/x/net/context" "github.com/micro/cli" "github.com/micro/go-micro" ) func main() { cmd.Init() // 創建 user-service 微服務的客戶端 client := pb.NewUserServiceClient("go.micro.srv.user", microclient.DefaultClient) // 設置命令行參數 service := micro.NewService( micro.Flags( cli.StringFlag{ Name: "name", Usage: "You full name", }, cli.StringFlag{ Name: "email", Usage: "Your email", }, cli.StringFlag{ Name: "password", Usage: "Your password", }, cli.StringFlag{ Name: "company", Usage: "Your company", }, ), ) service.Init( micro.Action(func(c *cli.Context) { name := c.String("name") email := c.String("email") password := c.String("password") company := c.String("company") r, err := client.Create(context.TODO(), &pb.User{ Name: name, Email: email, Password: password, Company: company, }) if err != nil { log.Fatalf("Could not create: %v", err) } log.Printf("Created: %v", r.User.Id) getAll, err := client.GetAll(context.Background(), &pb.Request{}) if err != nil { log.Fatalf("Could not list users: %v", err) } for _, v := range getAll.Users { log.Println(v) } os.Exit(0) }), ) // 啟動客戶端 if err := service.Run(); err != nil { log.Println(err) } }測試 運行成功
在此之前,需要手動拉取 Postgres 鏡像并運行:
$ docker pull postgres $ docker run --name postgres -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres用戶數據創建并存儲成功: 總結
到目前為止,我們創建了三個微服務:consignment-service、vessel-service 和 user-service,它們均使用 go-micro 實現并進行了 Docker 化,使用 docker-compose 進行統一管理。此外,我們還使用 GORM 庫與 Postgres 數據庫進行交互,并將命令行的數據存儲進去。
上邊的 user-cli 僅是測試使用,明文保存密碼一點也不安全。在本節完成基本功能的基礎上,下節將引入 JWT 做驗證。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/27336.html
摘要:定義微服務作為客戶端調用的函數實現中的接口,使作為的服務端檢查是否有適合的貨輪貨物被承運解析命令行參數作為的客戶端增加貨物并運行更新中的貨物,塞入三個集裝箱,重量和容量都變大。 譯文鏈接:wuYin/blog原文鏈接:ewanvalentine.io,翻譯已獲作者 Ewan Valentine 授權。 本節未細致介紹 Docker,更多可參考:《第一本Docker書 修訂版》 前言 在...
摘要:編程書籍的整理和收集最近一直在學習深度學習和機器學習的東西,發現深入地去學習就需要不斷的去提高自己算法和高數的能力然后也找了很多的書和文章,隨著不斷的學習,也整理了下自己的學習筆記準備分享出來給大家后續的文章和總結會繼續分享,先分享一部分的 編程書籍的整理和收集 最近一直在學習deep learning深度學習和機器學習的東西,發現深入地去學習就需要不斷的去提高自己算法和高數的能力然后...
閱讀 1571·2021-09-24 10:38
閱讀 1498·2021-09-22 15:15
閱讀 3059·2021-09-09 09:33
閱讀 905·2019-08-30 11:08
閱讀 638·2019-08-30 10:52
閱讀 1253·2019-08-30 10:52
閱讀 2344·2019-08-28 18:01
閱讀 520·2019-08-28 17:55