Golang 微服務教程(二)

譯文連接:wuYin/blog
原文連接:ewanvalentine.io,翻譯已獲做者 Ewan Valentine 受權。node

本節未細緻介紹 Docker,更多可參考:《第一本Docker書 修訂版》linux

前言

在上一篇中,咱們使用 gRPC 初步實現了咱們的微服務,本節將 Docker 化該微服務並引入 go-micro 框架代替 gRPC 簡化服務的實現。git

Docker

背景

佔據着雲計算的優點,微服務架構愈來愈流行,同時它的雲端分佈式的運行環境也對咱們的開發、測試和部署提出了很高的要求,容器(container)即是一項解決方案。github

在傳統軟件開發中,應用直接部署在環境和依賴都準備好的系統上,或在一臺物理服務器上部署在由 Chef 或 Puppet 管理的虛擬集羣裏。這種部署方案不利於橫向擴展,好比要部署多臺物理服務器,須要都安裝相同的依賴,再部署,非常麻煩。golang

vagrant 這類管理多個虛擬機的工具,雖然使項目的部署更爲遍歷,但每一個虛擬機都運行有一個完整的操做系統,十分耗費宿主主機的資源,並不適合微服務的開發和部署。web

容器

特性

容器 是精簡版的操做系統,但並不運行一個 kernel 或系統底層相關的驅動,它只包含一些 run-time 必需的庫,多個容器共享宿主主機的 kernel,多個容器之間相互隔離,互補影響。可參考:Redhat topicdocker

優點

容器的運行環境只包含代碼所須要的依賴,而不是使用完整的操做系統包含一大堆不須要的組件。此外,容器自己的體積相比虛擬機是比較小的,好比對比 ubuntu 16.04 優點不言而喻:shell

  • 虛擬機大小

    image-20180512154621284

  • 容器鏡像大小

    image-20180512154904850

Docker 與容器

通常人會認爲容器技術就是 Docker,實則否則,Docker 只是容器技術的一種實現,由於其操做簡便且學習門檻低,因此如此流行。數據庫

Docker 化微服務

Dockerfile

建立微服務部署的 Dockerfilenpm

# 若運行環境是 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% 的空間:

image-20180512162131600

因爲 docker 鏡像的超輕量級,在上邊部署和運行微服務耗費的資源是很小的。

編譯項目

爲了在 alpine 上運行咱們的微服務,向 Makefile 追加命令:

build:
    ...
    # 告知 Go 編譯器生成二進制文件的目標環境:amd64 CPU 的 Linux 系統
    GOOS=linux GOARCH=amd64 go build    
    # 根據當前目錄下的 Dockerfile 生成名爲 consignment-service 的鏡像
    docker build -t consignment-service .

需手動指定 GOOSGOARCH 的值,不然在 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 中的微服務:

dockerd

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

注意看代碼中第 4七、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 多播做爲服務發現的中間層。在生產環境通常會使用 ConsulEtcd 代替 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 會報錯:

image-20180513095911084

由於服務端運行在 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:2.2

註明:譯者的代碼暫時未把 Golang 集成到 Dockerfile 中,讀者有興趣可參考原文。

VesselService

上邊的 consignment-service 負責記錄貨物的託運信息,如今建立第二個微服務 vessel-service 來選擇合適的貨輪來運送貨物,關係以下:

image-20180522174448548

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\n", 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 三者流程跑通了。

客戶端用戶請求託運貨物,貨運服務向貨船服務檢查容量、重量是否超標,再運送:

2.3

總結

本節中將更爲易用的 go-micro 替代了 gRPC 同時進行了微服務的 Docker 化。最後建立了 vessel-service 貨輪微服務來運送貨物,併成功與貨輪微服務進行了通訊。

不過貨物數據都是存放在文件 consignment.json 中的,第三節咱們將這些數據存儲到 MongoDB 數據庫中,並在代碼中使用 ORM 對數據進行操做,同時使用 docker-compose 來統一 Docker 化後的兩個微服務。

相關文章
相關標籤/搜索