Micro In Action:項目結構與啓動過程

項目結構

上篇文章中我們創建了一個簡單的項目, 並過將它運行起來。本篇將繼續這個旅程,先介紹項目結構及其中每個文件的用途。

項目結構如下:

.
├── main.go
├── generate.go
├── plugin.go
├── proto/hello
│   └── hello.proto
│   └── hello.pb.go
│   └── hello.pb.micro.go
├── handler
│   └── hello.go
├── subscriber
│   └── hello.go
├── Dockerfile
├── go.mod
├── go.sum
├── Makefile
└── README.md

每個文件的說明爲:

  • main.go ,項目主文件,後面會詳細說明
  • generate.go ,只包含一行 //go:generate make proto ,實現與 go generate 命令的集成。在運行 go generate 命令時自動調用 make proto
  • plugins.go,目前是空文件, 根據 Micro 的約定[2], 建議在這裏管理所需 plugin 的導入, 後續會用到。
  • proto/hello/hello.proto,gRPC 服務定義[3]文件, 定義了 rpc 服務Hello,服務中提供 3 種典型 gRPC 調用:單向 RPC,單向 Stream 和雙向 Stream
    **proto/hello/hello.pb.go,**根據上述 proto 文件, 由protoc 生成 gRPC 相關代碼
  • proto/hello/hello.pb.micro.go,由前文提到的 protoc-gen-micro 生成的, 進一步簡化開發者的工作。其中定義了HelloSerivce 接口, 以及 HelloHandler 接口。後者是我們需要去實現、完成業務邏輯的接口
  • handler/hello.go ,實現 gRPC 業務邏輯的地方。其中定義了 Hello 對象, 此對象實現了前面提到 HelloHandler 接口。
  • subscriber/hello.go,實現異步消息接收並處理的地方。其中展示了用兩種不同方式處理消息,一是以對象方法處理, 二是以一個函數來處理。
  • Dockerfile,定義如何構建 Docker 鏡像
  • go.mod / go.sum , Go Module 相關文件
  • Makefile,包含了幾個常用任務定義, 編譯、測試、生在 Docker 鏡像等
  • README.md,記錄了生成項目的基本信息,以及基本運行指南

:文件夾 proto有特殊含義。雖然在技術上沒有限制, 但在 Micro 的約定中,每個項目根目錄下的proto文件夾專門用來存放“接口”文件。這既包含本項目需要對外暴露的接口, 也包含本項目所依賴其它接口。舉例來說, 假如我們實現業務邏輯時需要依賴另外一個服務 foo。那麼我們會建立proto/foo 文件夾,並在其中放置 foo.proto, foo.pb.go, foo.pb.micro.go 三個文件,供業務代碼調用。

啓動過程解析

接下來看一看啓動代碼,main.go:

package main

import (
   "github.com/micro/go-micro/util/log"
   "github.com/micro/go-micro"
   "hello/handler"
   "hello/subscriber"

   hello "hello/proto/hello"
)

func main() {
   // New Service
   service := micro.NewService(
      micro.Name("com.foo.srv.hello"),
      micro.Version("latest"),
   )

   // Initialise service
   service.Init()

   // Register Handler
   hello.RegisterHelloHandler(service.Server(), new(handler.Hello))

   // Register Struct as Subscriber
   micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), new(subscriber.Hello))

   // Register Function as Subscriber
   micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), subscriber.Handler)

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

代碼大體分 4 個部分,分別是導入依賴、創建及初始化服務、註冊業務處理 Handler 和運行服務。

導入依賴

這部分只有一行代碼值得單獨說明:

hello "hello/proto/hello"

導入時定義了別名。這也是 Micro 的一個習慣約定:對所有接口導入包設置別名。這樣就可以避免依賴導入代碼的包名。實踐中, 如果不作特別設置,自動生成代碼的包名會比較長, 以 hello.pb.go 爲例, 它的包名是 com_foo_srv_hello。顯然設置一個別名是更好的選擇

創建及初始化服務

// New Service
service := micro.NewService(
  micro.Name("com.foo.srv.hello"),
  micro.Version("latest"),
)

創建服務用到了 micro.NewService(opts …Option) Service 方法。此方法可接收多個 micro.Option 爲參數, 生成並返回 micro.Service 接口實例。

可見 micro.Option 是控制服務的關鍵。示例代碼用 Option 分別指定了服務的名稱和版本號。目前共有 25 個 Option 可供使用, 能夠控制服務的方方面面。有些 Option 可以指定多次,形成疊加效果(後面會提到)。

但是, 如此重要的選項竟沒有任何一份說明文檔,想要學習只能去查看源碼[4]。而很多 Option 的源碼中連註釋也沒有,這進一步提高了學習的難度。雖然本文並不打算成爲完備的 Micro 參考手冊,但這些 Option 對於理解和使用 Micro 非常重要,又沒有其它資料可參考, 所以我決定列出 v1.18.0 版本中全部 25 個 Option。逐一加以說明:

  1. micro.Name(n string) Option , 指定服務名稱。命名規則一般是“type.$name”。其中 namespace 代表項目的名稱空間, type 代表服務類型(例如 gRPC 和 web),一般會把 gRPC service 類型縮寫成 srv。服務實例運行後, 此名稱將自動註冊到 Registry, 成爲服務發現的依據。默認爲“go.micro.server”。注:因此此項必須要指定, 否則所有節點使用相同的默認名稱,會導致調用混亂
  2. micro.Version(v string) Option,指定服務版本。默認爲啓動時間格式化的字符串。恰當地選擇版本號再配合相應的 Selector, 可以實現優雅的輪轉升級、灰度發佈、A/B 測試等功能。
  3. micro.Address(addr string) Option,指定 gRPC 服務地址。默認爲隨機端口。由於客戶端是通過註冊中心來定位服務, 所以隨機端口並不影響使用。但實踐中經常是指定固定端口號的, 這會有利於運維管理和安全控制
  4. micro.RegisterTTL(t time.Duration) Option,指定服務註冊信息在註冊中心的有效期。默認爲一分種
  5. micro.RegisterInterval(t time.Duration) Option,指定服務主動向註冊中心報告健康狀態的時間間隔, 默認爲 30 秒。這兩個註冊中心相關的 Option 結合起來用,可以避免因服務意外宕機而未通知註冊中心,產生“無效註冊信息”
  6. micro.WrapHandler(w …server.HandlerWrapper) Option,包裝服務 Handler, 概念上類似於 Gin Middleware[5], 集中控制 Handler 行爲。可包裝多層,執行順序由外到內(後續會有實例)
  7. micro.WrapSubscriber(w …server.SubscriberWrapper) Option,與 WrapHandler 相似,不同之處在於它用來包裝異步消費處理中的“訂閱者”。
  8. micro.WrapCall(w …client.CallWrapper) Option,包裝客戶端發起的每一次方法調用。
  9. micro.WrapClient(w …client.Wrapper) Option,包裝客戶端,可包裝多層, 執行順序由內到外。
  10. micro.BeforeStart(fn func() error) Option,設置服務啓動前回調函數,可設置多個。
  11. micro.BeforeStop(fn func() error) Option,設置服務關閉前回調函數,可設置多個。
  12. micro.AfterStart(fn func() error) Option,設置服務啓動後回調函數,可設置多個。
  13. micro.AfterStop(fn func() error) Option,設置服務關閉後回調函數,可設置多個。
  14. micro.Action(a func(*cli.Context)) Option,處理命令行參數。支持子命令及控制標記。詳情請見 micro/cli[6]
  15. micro.Flags(flags …cli.Flag) Option,快捷支持命令行控制標記, 詳情請見 micro/cli[7]
  16. micro.Cmd(c cmd.Cmd) Option, 指定命令行處理對象。默認由 newCmd[8]生成,此對象包含了一系列默認的環境變量、命令行參數支持。可以看作是多個內置 cli.Flag 的集合。注:go-micro 框架對命令行處理的設計方案有利有弊。利是提供大量默認選項,可以節省開發者時間。弊是此設計對用戶程序的有強烈的侵入性:框架要求開發者必須以 micro/cli 統一要求的方式來處理命令行參數。如若不然, 程序會報錯無法運行。例如,我們運行 ./hello-srv --foo=bar 就會報出“Incorrect Usage. flag provided but not defined: -foo=bar”的錯誤。好在有這個 Option,可以彌補這種強侵入性帶來的弊端。假如一個現存項目想引入 Micro ,而它已經有自己的參數處理機制, 那麼就需要使用此 Option 覆蓋默認行爲(同時丟掉一些默認的參數處理能力)。關於命令行參數, 本文後面部分有進一步解釋。
  17. micro.Metadata(md map[string]string) Option,指定服務元數據。元數據時常被用來爲服務標記與分組, 實現特定的負載策略等
  18. icro.Transport(t transport.Transport) Option,指定傳輸協議, 默認爲 http 協議
  19. micro.Selector(s selector.Selector) Option ,指定節點選擇器, 實現不同負載策略。默認爲隨機 Selector
  20. micro.Registry(r registry.Registry) Option,指定用於服務發現的註冊機制, 默認爲基於 mDNS 的註冊機制
  21. micro.Server(s server.Server) Option, 指定自定義 Server, 用於默認 Server 不滿足業務要求的情況。默認爲 rpcServer
  22. micro.HandleSignal(b bool) Option, 是否允許服務自動響應 TERM, INT, QUIT 等信號。默認爲 true
  23. micro.Context(ctx context.Context) Option,指定服務初始 Context,默認爲 context.BackGround(),可用於控制服務生存期及其它
  24. micro.Client(c client.Client) Option,指定對外調用的客戶端。默認爲 rpcClient
  25. micro.Broker(b broker.Broker) Option, 指定用於 發佈/訂閱 消息通訊的 Broker。默認爲 http broker

因此,通過在創建時指定恰當的 Option,便可以高度定製服務的行爲。例如要想修改註冊信息有效期:

...
// New Service
service := micro.NewService(
   micro.Name("foo.bar"),
   micro.Version("v1.0"),
   // change default TTL value
   micro.RegisterTTL(5 * time.Minute),
   ...
)
...

:上述大部分 Option 可以通過多種方式指定。在源碼中硬編碼只是幾種其中之一。事實上, Micro 建議用戶優先通過環境變量來指定某些 Option, 因爲這樣可以提供更大的靈活性。以micro.RegisterTTL 爲例 , 我們可以在運行時通過環境變量 **$**MICRO_REGISTER_TTL或者命令行參數 --register_ttl value 來指定(單位是秒)。運行 ./hello-srv -h 可以看到這些內置參數的簡要說明。如果想了解全部細節,目前沒有完整文檔,需要自行查看 newCmd[9] 源碼。本系列後續文章對此話題會作進一步解讀。

創建之後就可以初始化服務了:

// Initialize service
service.Init()

service.Init 方法可以接收與 micro.NewService 相同的參數。所以上述 25 個 Option 也可以用在 service.Init方法中。他們效果相同只是時機有差異。由於此時服務已經創建, 我們可以使用服務實例的某些信息。例如,可自動讀取隨機端口:

// Initialize service
service.Init(
   // print log after start
   micro.AfterStart(func() error {
      log.Infof("service listening on %s!",
         service.Options().Server.Options().Address,
      )
      return nil
   }),
)

註冊業務處理 Handler

// Register Handler
hello.RegisterHelloHandler(service.Server(), new(handler.Hello))

// Register Struct as Subscriber
micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), new(subscriber.Hello))

// Register Function as Subscriber
micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), subscriber.Handler)

只有在完成 Handler 註冊後, 我們的業務代碼才能真正對外提供服務。這裏展示了 3 個典型的註冊操作:

  1. 註冊 gRPC handler。創建handler.Hello對象, 並註冊到 Server 上。由於handler.Hello實現了HelloHandler 接口, 所以它纔可以作爲 hello.RegisterHelloHandler 的方法參數被傳入,否則會報錯。一個服務中可以註冊多個 Handler 以完成不同業務功能。
  2. 註冊消息處理對象。第一個參數爲消息 Topic, 第二個參數是 Server, 第三個參數是消息處理對象。
  3. 註冊消息處理函數。與對象註冊相似, 只是第三個參數是對應的消息處理函數

關於消息處理的更多細節, 我們將在後續文章中專門說明。

運行服務

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

至此, 服務便真正運行起來了

查看運行時狀態

上一篇文章提到, micro 這個命令行工具可以用來在運行時查看和操作服務。下面我們來試一下。

在服務啓動之後, 運行 micro web命令:

$ micro web
2020/01/15 18:13:25 : [web] HTTP API Listening on [::]:8082
2020/01/15 18:13:25 : [web] Transport [http] Listening on [::]:59005
2020/01/15 18:13:25 : [web] Broker [http] Connected to [::]:59006
2020/01/15 18:13:25 : [web] Registry [mdns] Registering node: go.micro.web-950a8b2b-003d-47c1-a512-53aedebc9d12

可見此命令已在本機 8082 端口上服務。注:8082 端口是默認值,可以通過環境變量或命令行參數修改。具體可以運行 micro web -h查看說明

從瀏覽器訪問 http://127.0.0.1:8082/registry?service=com.foo.srv.hello 將能以網頁形式查看服務狀態。截圖如下:

在這裏插入圖片描述

從上圖中, 我們可以看到該服務的各種關鍵信息:

  • 服務名稱。
  • 服務節點列表。如果此服務有多個節點同時運行, 此處會看到多行
  • 每個節點中顯示了版本號, 名稱,編一 ID,地址,元數據等
  • Endpoints。服務的接口定義, 方法名,參數結構與數據類型等等

可見通過 micro web 可以很方便的瞭解各種運行時狀態。你可能會問, 我們的服務與 micro web 之間並沒有互相調用, 它是怎麼知道這些信息的呢?答案在於前文提到的服務發現。 Micro 內置支持服務發現, 在未作特別設置的情況下, 默認的服務發現是基於 mDNS 的, 因此只要在同一個局域內, 就可以自動發現彼此。

當然 micro web 的功能不只於此,我們只是展現與本篇主題相關的內容。後續文章會展開介紹。

總結

本文是 Micro in Action 系列的第二篇文章, 我們作了幾件事:

  1. 介紹了上篇文章所創建的項目結構, 說明每一個文件的用途。
  2. 對照源碼逐行分析一個 Micro 服務的啓動過程。
  3. 考慮到 Micro 文檔的缺失, 本文完整介紹了創建 Micro 服務所支持的全部 Option
  4. 最後用 micro web 查看了服務的運行時狀態

參考資料

[1]
Micro: https://micro.mu/
[2]
約定: https://micro.mu/docs/plugins.html#usage
[3]
服務定義: https://grpc.io/docs/guides/concepts/
[4]
源碼: https://github.com/micro/go-micro/blob/v1.18.0/options.go
[5]
Gin Middleware: https://github.com/gin-gonic/gin#using-middleware
[6]
micro/cli: https://github.com/micro/cli
[7]
micro/cli: https://github.com/micro/cli
[8]
newCmd: https://github.com/micro/go-micro/blob/v1.18.0/config/cmd/cmd.go#L263
[9]
newCmd: https://github.com/micro/go-micro/blob/v1.18.0/config/cmd/cmd.go#L263
[10]
https://mp.weixin.qq.com/s/WG3wv-FWPLoJIWYEVNdNsA

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章