摘要:定義微服務作為客戶端調用的函數實現中的接口,使作為的服務端檢查是否有適合的貨輪貨物被承運解析命令行參數作為的客戶端增加貨物并運行更新中的貨物,塞入三個集裝箱,重量和容量都變大。
譯文鏈接:wuYin/blog
原文鏈接:ewanvalentine.io,翻譯已獲作者 Ewan Valentine 授權。
本節未細致介紹 Docker,更多可參考:《第一本Docker書 修訂版》
前言在上一篇中,我們使用 gRPC 初步實現了我們的微服務,本節將 Docker 化該微服務并引入 go-micro 框架代替 gRPC 簡化服務的實現。
Docker 背景占據著云計算的優勢,微服務架構越來越流行,同時它的云端分布式的運行環境也對我們的開發、測試和部署提出了很高的要求,容器(container)便是一項解決方案。
在傳統軟件開發中,應用直接部署在環境和依賴都準備好的系統上,或在一臺物理服務器上部署在由 Chef 或 Puppet 管理的虛擬集群里。這種部署方案不利于橫向擴展,比如要部署多臺物理服務器,需要都安裝相同的依賴,再部署,很是麻煩。
vagrant 這類管理多個虛擬機的工具,雖然使項目的部署更為遍歷,但每個虛擬機都運行有一個完整的操作系統,十分耗費宿主主機的資源,并不適合微服務的開發和部署。
容器 特性容器 是精簡版的操作系統,但并不運行一個 kernel 或系統底層相關的驅動,它只包含一些 run-time 必需的庫,多個容器共享宿主主機的 kernel,多個容器之間相互隔離,互補影響。可參考:Redhat topic
優勢容器的運行環境只包含代碼所需要的依賴,而不是使用完整的操作系統包含一大堆不需要的組件。此外,容器本身的體積相比虛擬機是比較小的,比如對比 ubuntu 16.04 優勢不言而喻:
虛擬機大小
容器鏡像大小
Docker 與容器一般人會認為容器技術就是 Docker,實則不然,Docker 只是容器技術的一種實現,因為其操作簡便且學習門檻低,所以如此流行。
Docker 化微服務 Dockerfile創建微服務部署的 Dockerfile
# 若運行環境是 Linux 則需把 alpine 換成 debian # 使用最新版 alpine 作為基礎鏡像 FROM alpine:latest # 在容器的根目錄下創建 app 目錄 RUN mkdir /app # 將工作目錄切換到 /app 下 WORKDIR /app # 將微服務的服務端運行文件拷貝到 /app 下 ADD consignment-service /app/consignment-service # 運行服務端 CMD ["./consignment-service"]
alpine 是一個超輕量級 Linux 發行版本,專為 Docker 中 Web 應用而生。它能保證絕大多數 web 應用可以正常運行,即使它只包含必要的 run-time 文件和依賴,鏡像大小只有 4 MB,相比上邊 Ubuntu16.4 節約了 99.7% 的空間:
由于 docker 鏡像的超輕量級,在上邊部署和運行微服務耗費的資源是很小的。
編譯項目為了在 alpine 上運行我們的微服務,向 Makefile 追加命令:
build: ... # 告知 Go 編譯器生成二進制文件的目標環境:amd64 CPU 的 Linux 系統 GOOS=linux GOARCH=amd64 go build # 根據當前目錄下的 Dockerfile 生成名為 consignment-service 的鏡像 docker build -t consignment-service .
需手動指定 GOOS 和 GOARCH 的值,否則在 macOS 上編譯出的文件是無法在 alpine 容器中運行的。
其中 docker build 將程序的執行文件 consignment-service 及其所需的 run-time 環境打包成了一個鏡像,以后在 docker 中直接 run 鏡像即可啟動該微服務。
你可以把你的鏡像分享到 DockerHub,二者的關系類比 npm 與 nodejs、composer 與 PHP,去 DockerHub 瞧一瞧,會發現很多優秀的開源軟件都已 Docker 化,參考演講:Willy Wonka of Containers
關于 Docker 構建鏡像的細節,請參考書籍《第一本 Docker 書》第四章
運行 Docker 化后的微服務繼續在 Makefile 中追加命令:
build: ... run: # 在 Docker alpine 容器的 50001 端口上運行 consignment-service 服務 # 可添加 -d 參數將微服務放到后臺運行 docker run -p 50051:50051 consignment-service
由于 Docker 有自己獨立的網絡層,所以需要指定將容器的端口映射到本機的那個端口,使用 -p 參數即可指定,比如 -p 8080:50051 是將容器 50051端口映射到本機 8080 端口,注意順序是反的。更多參考:Docker 文檔
現在運行 make build && make run 即可在 docker 中運行我們的微服務,此時在本機執行微服務的客戶端代碼,將成功調用 docker 中的微服務:
Go-micro 為什么不繼續使用 gRPC ? 管理麻煩在客戶端代碼(consignment-cli/cli.go)中,我們手動指定了服務端的地址和端口,在本地修改不是很麻煩。但在生產環境中,各服務可能不在同一臺主機上(分布式獨立運行),其中任一服務重新部署后 IP 或運行的端口發生變化,其他服務將無法再調用它。如果你有很多個服務,彼此指定 IP 和端口來相互調用,那管理起來很麻煩
服務發現為解決服務間的調用問題,服務發現(service discovery)出現了,它作為一個注冊中心會記錄每個微服務的 IP 和端口,各微服務上線時會在它那注冊,下線時會注銷,其他服務可通過名字或 ID 找到該服務類比門面模式。
為不重復造輪子,我們直接使用實現了服務注冊的 go-micro 框架。
安裝go get -u github.com/micro/protobuf/proto go get -u github.com/micro/protobuf/protoc-gen-go
使用 go-micro 自己的編譯器插件,在 Makefile 中修改 protoc 命令:
build: # 不再使用 grpc 插件 protoc -I. --go_out=plugins=micro:$(GOPATH)/src/shippy/consignment-service proto/consignment/consignment.proto服務端使用 go-micro
你會發現重新生成的 consignment.pb.go 大有不同。修改服務端代碼 main.go 使用 go-micro
package main import ( pb "shippy/consignment-service/proto/consignment" "context" "log" "github.com/micro/go-micro" ) // // 倉庫接口 // 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 中的 ShippingServiceHandler 接口 // 使 service 作為 gRPC 的服務端 // // 托運新的貨物 // func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment) (*pb.Response, error) { func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment, resp *pb.Response) error { // 接收承運的貨物 consignment, err := s.repo.Create(req) if err != nil { return err } resp = &pb.Response{Created: true, Consignment: consignment} return nil } // 獲取目前所有托運的貨物 // func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest) (*pb.Response, error) { func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest, resp *pb.Response) error { allConsignments := s.repo.GetAll() resp = &pb.Response{Consignments: allConsignments} return nil } func main() { server := micro.NewService( // 必須和 consignment.proto 中的 package 一致 micro.Name("go.micro.srv.consignment"), micro.Version("latest"), ) // 解析命令行參數 server.Init() repo := Repository{} pb.RegisterShippingServiceHandler(server.Server(), &service{repo}) if err := server.Run(); err != nil { log.Fatalf("failed to serve: %v", err) } }
go-micro 的實現相比 gRPC 有 3 個主要的變化:
創建 RPC 服務器的流程micro.NewService(...Option) 簡化了微服務的注冊流程, micro.Run() 也簡化了 gRPCServer.Serve(),不再需要手動創建 TCP 連接并監聽。
微服務的 interface注意看代碼中第 47、59 行,會發現 go-micro 將響應參數 Response 提到了入參,只返回 error,整合了 gRPC 的 [四種運行模式]()
運行地址的管理服務的監聽端口沒有在代碼中寫死,go-mirco 會自動使用系統或命令行中變量 MICRO_SERVER_ADDRESS 的地址
對應更新一下 Makefile
run: docker run -p 50051:50051 -e MICRO_SERVER_ADDRESS=:50051 -e MICRO_REGISTRY=mdns consignment-service
-e 選項用于設置鏡像中的環境變量,其中 MICRO_REGISTRY=mdns 會使 go-micro 在本地使用 mdns 多播作為服務發現的中間層。在生產環境一般會使用 Consul 或 Etcd 代替 mdns 做服務發現,在本地開發先一切從簡。
現在執行 make build && make run,你的 consignment-service 就有服務發現的功能了。
客戶端使用 go-micro我們需要更新一下客戶端的代碼,使用 go-micro 來調用微服務:
func main() { cmd.Init() // 創建微服務的客戶端,簡化了手動 Dial 連接服務端的步驟 client := pb.NewShippingServiceClient("go.micro.srv.consignment", microclient.DefaultClient) ... }
現在運行 go run cli.go 會報錯:
因為服務端運行在 Docker 中,而 Docker 有自己獨立的 mdns,與宿主主機 Mac 的 mdns 不一致。把客戶端也 Docker 化,這樣服務端與客戶端就在同一個網絡層下,順利使用 mdns 做服務發現。
Docker 化客戶端創建客戶端的 Dokerfile
FROM alpine:latest RUN mkdir -p /app WORKDIR /app # 將當前目錄下的貨物信息文件 consignment.json 拷貝到 /app 目錄下 ADD consignment.json /app/consignment.json ADD consignment-cli /app/consignment-cli CMD ["./consignment-cli"]
創建文件 consignment-cli/Makefile
build: GOOS=linux GOARCH=amd64 go build docker build -t consignment-cli . run: docker run -e MICRO_REGISTRY=mdns consignment-cli調用微服務
執行 make build && make run,即可看到客戶端成功調用 RPC:
注明:譯者的代碼暫時未把 Golang 集成到 Dockerfile 中,讀者有興趣可參考原文。
VesselService上邊的 consignment-service 負責記錄貨物的托運信息,現在創建第二個微服務 vessel-service 來選擇合適的貨輪來運送貨物,關系如下:
consignment.json 文件中的三個集裝箱組成的貨物,目前可以通過 consignment-service 管理貨物的信息,現在用 vessel-service 去檢查貨輪是否能裝得下這批貨物。
創建 protobuf 文件// vessel-service/proto/vessel/vessel.proto syntax = "proto3"; package go.micro.srv.vessel; service VesselService { // 檢查是否有能運送貨物的輪船 rpc FindAvailable (Specification) returns (Response) { } } // 每條貨輪的熟悉 message Vessel { string id = 1; // 編號 int32 capacity = 2; // 最大容量(船體容量即是集裝箱的個數) int32 max_weight = 3; // 最大載重 string name = 4; // 名字 bool available = 5; // 是否可用 string ower_id = 6; // 歸屬 } // 等待運送的貨物 message Specification { int32 capacity = 1; // 容量(內部集裝箱的個數) int32 max_weight = 2; // 重量 } // 貨輪裝得下的話 // 返回的多條貨輪信息 message Response { Vessel vessel = 1; repeated Vessel vessels = 2; }創建 Makefile 與 Dockerfile
現在創建 vessel-service/Makefile 來編譯項目:
build: protoc -I. --go_out=plugins=micro:$(GOPATH)/src/shippy/vessel-service proto/vessel/vessel.proto # dep 工具暫不可用,直接手動編譯 GOOS=linux GOARCH=amd64 go build docker build -t vessel-service . run: docker run -p 50052:50051 -e MICRO_SERVER_ADDRESS=:50051 -e MICRO_REGISTRY=mdns vessel-service
注意第二個微服務運行在宿主主機(macOS)的 50052 端口,50051 已被第一個占用。
現在創建 Dockerfile 來容器化 vessel-service:
# 暫未將 Golang 集成到 docker 中 FROM alpine:latest RUN mkdir /app WORKDIR /app ADD vessel-service /app/vessel-service CMD ["./vessel-service"]實現貨船微服務的邏輯
package main import ( pb "shippy/vessel-service/proto/vessel" "github.com/pkg/errors" "context" "github.com/micro/go-micro" "log" ) type Repository interface { FindAvailable(*pb.Specification) (*pb.Vessel, error) } type VesselRepository struct { vessels []*pb.Vessel } // 接口實現 func (repo *VesselRepository) FindAvailable(spec *pb.Specification) (*pb.Vessel, error) { // 選擇最近一條容量、載重都符合的貨輪 for _, v := range repo.vessels { if v.Capacity >= spec.Capacity && v.MaxWeight >= spec.MaxWeight { return v, nil } } return nil, errors.New("No vessel can"t be use") } // 定義貨船服務 type service struct { repo Repository } // 實現服務端 func (s *service) FindAvailable(ctx context.Context, spec *pb.Specification, resp *pb.Response) error { // 調用內部方法查找 v, err := s.repo.FindAvailable(spec) if err != nil { return err } resp.Vessel = v return nil } func main() { // 停留在港口的貨船,先寫死 vessels := []*pb.Vessel{ {Id: "vessel001", Name: "Boaty McBoatface", MaxWeight: 200000, Capacity: 500}, } repo := &VesselRepository{vessels} server := micro.NewService( micro.Name("go.micro.srv.vessel"), micro.Version("latest"), ) server.Init() // 將實現服務端的 API 注冊到服務端 pb.RegisterVesselServiceHandler(server.Server(), &service{repo}) if err := server.Run(); err != nil { log.Fatalf("failed to serve: %v", err) } }貨運服務與貨船服務交互
現在需要修改 consignent-service/main.go,使其作為客戶端去調用 vessel-service,查看有沒有合適的輪船來運輸這批貨物。
// consignent-service/main.go package main import (...) // 定義微服務 type service struct { repo Repository // consignment-service 作為客戶端調用 vessel-service 的函數 vesselClient vesselPb.VesselServiceClient } // 實現 consignment.pb.go 中的 ShippingServiceHandler 接口,使 service 作為 gRPC 的服務端 func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment, resp *pb.Response) error { // 檢查是否有適合的貨輪 vReq := &vesselPb.Specification{ Capacity: int32(len(req.Containers)), MaxWeight: req.Weight, } vResp, err := s.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 := s.repo.Create(req) if err != nil { return err } resp.Created = true resp.Consignment = consignment return nil } // ... func main() { // ... // 解析命令行參數 server.Init() repo := Repository{} // 作為 vessel-service 的客戶端 vClient := vesselPb.NewVesselServiceClient("go.micro.srv.vessel", server.Client()) pb.RegisterShippingServiceHandler(server.Server(), &service{repo, vClient}) if err := server.Run(); err != nil { log.Fatalf("failed to serve: %v", err) } }增加貨物并運行
更新 consignment-cli/consignment.json 中的貨物,塞入三個集裝箱,重量和容量都變大。
{ "description": "This is a test consignment", "weight": 55000, "containers": [ { "customer_id": "cust001", "user_id": "user001", "origin": "Manchester, United Kingdom" }, { "customer_id": "cust002", "user_id": "user001", "origin": "Derby, United Kingdom" }, { "customer_id": "cust005", "user_id": "user001", "origin": "Sheffield, United Kingdom" } ] }
至此,我們完整的將 consignment-cli,consignment-service,vessel-service 三者流程跑通了。
客戶端用戶請求托運貨物,貨運服務向貨船服務檢查容量、重量是否超標,再運送:
總結本節中將更為易用的 go-micro 替代了 gRPC 同時進行了微服務的 Docker 化。最后創建了 vessel-service 貨輪微服務來運送貨物,并成功與貨輪微服務進行了通信。
不過貨物數據都是存放在文件 consignment.json 中的,第三節我們將這些數據存儲到 MongoDB 數據庫中,并在代碼中使用 ORM 對數據進行操作,同時使用 docker-compose 來統一 Docker 化后的兩個微服務。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/27342.html
摘要:本節將學習來統一管理和部署微服務,引入第三個微服務并進行存儲數據。到目前為止,要想啟動微服務的容器,均在其中的同時設置其環境變量,服務多了以后管理起來十分麻煩。 譯文鏈接:wuYin/blog原文鏈接:ewanvalentine.io,翻譯已獲作者 Ewan Valentine 授權。 本文完整代碼:GitHub 在上節中,我們使用 go-micro 重新實現了微服務并進行了 Doc...
摘要:平日學習接觸過的網站積累,以每月的形式發布。年以前看這個網址概況在線地址前端開發群月報提交原則技術文章新的為主。 平日學習接觸過的網站積累,以每月的形式發布。2017年以前看這個網址:http://www.kancloud.cn/jsfron... 概況 在線地址:http://www.kancloud.cn/jsfront/month/82796 JS前端開發群月報 提交原則: 技...
摘要:平日學習接觸過的網站積累,以每月的形式發布。年以前看這個網址概況在線地址前端開發群月報提交原則技術文章新的為主。 平日學習接觸過的網站積累,以每月的形式發布。2017年以前看這個網址:http://www.kancloud.cn/jsfron... 概況 在線地址:http://www.kancloud.cn/jsfront/month/82796 JS前端開發群月報 提交原則: 技...
摘要:平日學習接觸過的網站積累,以每月的形式發布。年以前看這個網址概況在線地址前端開發群月報提交原則技術文章新的為主。 平日學習接觸過的網站積累,以每月的形式發布。2017年以前看這個網址:http://www.kancloud.cn/jsfron... 概況 在線地址:http://www.kancloud.cn/jsfront/month/82796 JS前端開發群月報 提交原則: 技...
閱讀 958·2022-06-21 15:13
閱讀 1848·2021-10-20 13:48
閱讀 1029·2021-09-22 15:47
閱讀 1365·2019-08-30 15:55
閱讀 3112·2019-08-30 15:53
閱讀 519·2019-08-29 12:33
閱讀 712·2019-08-28 18:15
閱讀 3458·2019-08-26 13:58