go-micro v2運開實踐-框架篇(3)註冊第一個微服務

huangyanming發表於2022-04-26

修改使用者服務程式碼

前面我們已經安裝好了微服務的一些基礎設施,現在我們需要開始編寫微服務程式碼,構建容器,啟動服務並將其註冊到註冊中心中。

更正引用錯誤

開啟micro生成的使用者服務程式碼模板的入口檔案main.go,我們發現因為我們修改了go.mod檔案所以導致一些引用失效,所以我們需要將這些檔案的引用更正

修改main.go

package main

import (
    "github.com/869413421/micro-service/user/handler"
    "github.com/869413421/micro-service/user/subscriber"
    "github.com/micro/go-micro/v2"
    log "github.com/micro/go-micro/v2/logger"

    proto "github.com/869413421/micro-service/user/proto/user"
)

func main() {
    // New Service
    service := micro.NewService(
        micro.Name("micro.service.user"),
        micro.Version("latest"),
    )

    // Initialise service
    service.Init()

    // Register Handler
    proto.RegisterUserHandler(service.Server(), new(handler.User))

    // Register Struct as Subscriber
    micro.RegisterSubscriber("micro.service.user", service.Server(), new(subscriber.User))

    // Run service
    if err := service.Run(); err != nil {
        log.Fatal(err)
    }
}

package handler

import (
    "context"
    "github.com/869413421/micro-service/user/proto/user"

    log "github.com/micro/go-micro/v2/logger"

    proto "github.com/869413421/micro-service/user/proto/user"
)

type User struct{}

// Call is a single request handler called via client.Call or the generated client code
func (e *User) Call(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
    log.Info("Received User.Call request")
    rsp.Msg = "Hello " + req.Name
    return nil
}

// Stream is a server side stream handler called via client.Stream or the generated client code
func (e *User) Stream(ctx context.Context, req *proto.StreamingRequest, stream proto.User_StreamStream) error {
    log.Infof("Received User.Stream request with count: %d", req.Count)

    for i := 0; i < int(req.Count); i++ {
        log.Infof("Responding: %d", i)
        if err := stream.Send(&user.StreamingResponse{
            Count: int64(i),
        }); err != nil {
            return err
        }
    }

    return nil
}

// PingPong is a bidirectional stream handler called via client.Stream or the generated client code
func (e *User) PingPong(ctx context.Context, stream proto.User_PingPongStream) error {
    for {
        req, err := stream.Recv()
        if err != nil {
            return err
        }
        log.Infof("Got ping %v", req.Stroke)
        if err := stream.Send(&user.Pong{Stroke: req.Stroke}); err != nil {
            return err
        }
    }
}

修改handler/user.go

package handler

import (
    "context"
    "github.com/869413421/micro-service/user/proto/user"

    log "github.com/micro/go-micro/v2/logger"

    proto "github.com/869413421/micro-service/user/proto/user"
)

type User struct{}

// Call is a single request handler called via client.Call or the generated client code
func (e *User) Call(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
    log.Info("Received User.Call request")
    rsp.Msg = "Hello " + req.Name
    return nil
}

// Stream is a server side stream handler called via client.Stream or the generated client code
func (e *User) Stream(ctx context.Context, req *proto.StreamingRequest, stream proto.User_StreamStream) error {
    log.Infof("Received User.Stream request with count: %d", req.Count)

    for i := 0; i < int(req.Count); i++ {
        log.Infof("Responding: %d", i)
        if err := stream.Send(&user.StreamingResponse{
            Count: int64(i),
        }); err != nil {
            return err
        }
    }

    return nil
}

// PingPong is a bidirectional stream handler called via client.Stream or the generated client code
func (e *User) PingPong(ctx context.Context, stream proto.User_PingPongStream) error {
    for {
        req, err := stream.Recv()
        if err != nil {
            return err
        }
        log.Infof("Got ping %v", req.Stroke)
        if err := stream.Send(&user.Pong{Stroke: req.Stroke}); err != nil {
            return err
        }
    }
}

修改subscriber/user.go

package subscriber

import (
    "context"
    log "github.com/micro/go-micro/v2/logger"

    proto "github.com/869413421/micro-service/user/proto/user"
)

type User struct{}

func (e *User) Handle(ctx context.Context, msg *proto.Message) error {
    log.Info("Handler Received message: ", msg.Say)
    return nil
}

func Handler(ctx context.Context, msg *proto.Message) error {
    log.Info("Function Received message: ", msg.Say)
    return nil
}

至此,我們正常調整了程式碼。上面的程式碼只是作示例作為使用,後續會重構重新書寫我們的業務,暫時不需要過於糾結。

測試程式碼是否能正常編譯

執行go run main.go

可以看到我們的服務成功透過編譯正常執行。

編寫多階段構建dockerfile

在微服務中我們正常編寫好程式碼後,需要部署容器來執行服務,我們可以透過兩種方式。

  • 編寫好dockerfile,編譯好映象,在docker-compose直接拉取部署
  • 編寫好dockerfile,透過docker-compose幫我們編譯映象執行

這裡我們選擇第二種方式

修改dockerfile

# user-service/Dockerfile

# 使用golang官方映象,並命名為builder
FROM golang:1.13-alpine as builder

# 啟用go Modules
ENV GO111MODULE on

# 安裝git
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    apk update && \
    apk add --no-cache git


# 設定工作目錄
WORKDIR /app/micro-user-service

# 將目錄中程式碼複製到映象中
COPY . .

# 下載依賴,
RUN  go env -w GOPROXY=https://mirrors.aliyun.com/goproxy,direct && go mod tidy

# 構建二進位制檔案,新增一些額外引數方便在alpin中執行它
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' -o micro-user-service ./main.go

# 第二階段構造
FROM alpine:latest

# 更新依賴軟體
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    apk update && \
    apk add --no-cache bash ca-certificates &&\
    apk add curl 

# 和上個階段一樣設定工作目錄
RUN mkdir -p /app/logs
WORKDIR /app

# 這一步不再從宿主機複製二進位制檔案,而是從上一個階段構建的 builder 容器中拉取
COPY --from=builder /app/micro-user-service/micro-user-service .

# 啟動使用者服務
CMD ["./micro-user-service"]

為什麼適用多階段構建?

我們知道在docker映象中每增加一個指令,映象都會產生一個新的層,等層級越多,一個映象就越臃腫,執行效率更低,佔用資源更多。一個高效的dockerfile應該在實際執行中清除掉不需要的資源。像我們在程式執行中其實只依賴一個編譯好的可執行檔案,所以我們並不依賴go的環境,當我們在第一階段將透過go映象編譯好之後,這些資源便可以拋棄掉,從而達到一個映象瘦身的效果。

編譯執行服務

上面我們已經書寫好dockerfile,這是我們透過docker-compose來對dockerfile編譯並且部署,使其註冊到服務中心去。

修改docker-compose.yaml

  ...

  micro-user-service:
    depends_on: # 啟動依賴,需要等etcd叢集啟動後才啟動當前容器
      - etcd1
      - etcd2
      - etcd3
    build: ./user # dockerfile所在目錄
    environment:
      MICRO_SERVER_ADDRESS: ":9091" # 服務埠
      MICRO_REGISTRY: "etcd" # 註冊中心型別
      MICRO_REGISTRY_ADDRESS: "etcd1:2379,etcd2:2379,etcd3:2379" # 註冊中心叢集地址
    ports:
      - 9092:9091
    networks:
      - micro-network

 ...

執行docker-compose up -d micro-user-service

得益於go-micro良好的程式碼機制,我們無需修改任何程式碼就可以透過設定環境變數直接指定註冊中心驅動以及地址。當服務執行時會預設讀取環境變數在程式碼中執行,將服務註冊到服務中。但是這些環境變數設定並無相關文件說明,需要閱讀原始碼或者搜尋得到零星的說明。文件缺失,是我使用go-micro開發時的痛苦根源。

需要注意的是,我們在編譯映象的時候經常會因為網路原因導致編譯耗時非常之久,如果在本地開發的時候頻繁修改程式碼後每次都需要編譯執行會使我們的效率相當之低,這裡我說下我的解決方法。

第一次編譯透過之後,每次修改程式碼後不對映象進行重新構建。我們直接編譯專案的可執行檔案,然後將整個程式碼目錄其掛載在容器之中,然後重啟容器。就可以馬上看到程式碼修改的效果了,等到正式上線去掉掛載之後再重新構建映象。

在docker-compose對應的服務中加上一行 volumes

  ...

  micro-user-service:
    depends_on: # 啟動依賴,需要等etcd叢集啟動後才啟動當前容器
      - etcd1
      - etcd2
      - etcd3
    build: ./user # dockerfile所在目錄
    environment:
      MICRO_SERVER_ADDRESS: ":9091" # 服務埠
      MICRO_REGISTRY: "etcd" # 註冊中心型別
      MICRO_REGISTRY_ADDRESS: "etcd1:2379,etcd2:2379,etcd3:2379" # 註冊中心叢集地址
    ports:
      - 9092:9091
    volumes:
      - ./user:/app
    networks:
      - micro-network

 ...

檢查服務是否註冊成功

開啟dockerdesktop檢查容器是否正常執行

開啟micro-webservices

可以看到micro.service.user已經顯示在列表,點選詳情相關的rpc方法也已經存在,至此我們第一個服務已經註冊成功。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章