項目結構
在上篇文章中我們創建了一個簡單的項目, 並過將它運行起來。本篇將繼續這個旅程,先介紹項目結構及其中每個文件的用途。
項目結構如下:
.
├── 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。逐一加以說明:
- micro.Name(n string) Option , 指定服務名稱。命名規則一般是“type.$name”。其中 namespace 代表項目的名稱空間, type 代表服務類型(例如 gRPC 和 web),一般會把 gRPC service 類型縮寫成 srv。服務實例運行後, 此名稱將自動註冊到 Registry, 成爲服務發現的依據。默認爲“go.micro.server”。注:因此此項必須要指定, 否則所有節點使用相同的默認名稱,會導致調用混亂
- micro.Version(v string) Option,指定服務版本。默認爲啓動時間格式化的字符串。恰當地選擇版本號再配合相應的 Selector, 可以實現優雅的輪轉升級、灰度發佈、A/B 測試等功能。
- micro.Address(addr string) Option,指定 gRPC 服務地址。默認爲隨機端口。由於客戶端是通過註冊中心來定位服務, 所以隨機端口並不影響使用。但實踐中經常是指定固定端口號的, 這會有利於運維管理和安全控制
- micro.RegisterTTL(t time.Duration) Option,指定服務註冊信息在註冊中心的有效期。默認爲一分種
- micro.RegisterInterval(t time.Duration) Option,指定服務主動向註冊中心報告健康狀態的時間間隔, 默認爲 30 秒。這兩個註冊中心相關的 Option 結合起來用,可以避免因服務意外宕機而未通知註冊中心,產生“無效註冊信息”
- micro.WrapHandler(w …server.HandlerWrapper) Option,包裝服務 Handler, 概念上類似於 Gin Middleware[5], 集中控制 Handler 行爲。可包裝多層,執行順序由外到內(後續會有實例)
- micro.WrapSubscriber(w …server.SubscriberWrapper) Option,與 WrapHandler 相似,不同之處在於它用來包裝異步消費處理中的“訂閱者”。
- micro.WrapCall(w …client.CallWrapper) Option,包裝客戶端發起的每一次方法調用。
- micro.WrapClient(w …client.Wrapper) Option,包裝客戶端,可包裝多層, 執行順序由內到外。
- micro.BeforeStart(fn func() error) Option,設置服務啓動前回調函數,可設置多個。
- micro.BeforeStop(fn func() error) Option,設置服務關閉前回調函數,可設置多個。
- micro.AfterStart(fn func() error) Option,設置服務啓動後回調函數,可設置多個。
- micro.AfterStop(fn func() error) Option,設置服務關閉後回調函數,可設置多個。
- micro.Action(a func(*cli.Context)) Option,處理命令行參數。支持子命令及控制標記。詳情請見 micro/cli[6]
- micro.Flags(flags …cli.Flag) Option,快捷支持命令行控制標記, 詳情請見 micro/cli[7]
- 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 覆蓋默認行爲(同時丟掉一些默認的參數處理能力)。關於命令行參數, 本文後面部分有進一步解釋。
- micro.Metadata(md map[string]string) Option,指定服務元數據。元數據時常被用來爲服務標記與分組, 實現特定的負載策略等
- icro.Transport(t transport.Transport) Option,指定傳輸協議, 默認爲 http 協議
- micro.Selector(s selector.Selector) Option ,指定節點選擇器, 實現不同負載策略。默認爲隨機 Selector
- micro.Registry(r registry.Registry) Option,指定用於服務發現的註冊機制, 默認爲基於 mDNS 的註冊機制
- micro.Server(s server.Server) Option, 指定自定義 Server, 用於默認 Server 不滿足業務要求的情況。默認爲 rpcServer
- micro.HandleSignal(b bool) Option, 是否允許服務自動響應 TERM, INT, QUIT 等信號。默認爲 true
- micro.Context(ctx context.Context) Option,指定服務初始 Context,默認爲 context.BackGround(),可用於控制服務生存期及其它
- micro.Client(c client.Client) Option,指定對外調用的客戶端。默認爲 rpcClient
- 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 個典型的註冊操作:
- 註冊 gRPC handler。創建handler.Hello對象, 並註冊到 Server 上。由於handler.Hello實現了HelloHandler 接口, 所以它纔可以作爲 hello.RegisterHelloHandler 的方法參數被傳入,否則會報錯。一個服務中可以註冊多個 Handler 以完成不同業務功能。
- 註冊消息處理對象。第一個參數爲消息 Topic, 第二個參數是 Server, 第三個參數是消息處理對象。
- 註冊消息處理函數。與對象註冊相似, 只是第三個參數是對應的消息處理函數
關於消息處理的更多細節, 我們將在後續文章中專門說明。
運行服務
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 系列的第二篇文章, 我們作了幾件事:
- 介紹了上篇文章所創建的項目結構, 說明每一個文件的用途。
- 對照源碼逐行分析一個 Micro 服務的啓動過程。
- 考慮到 Micro 文檔的缺失, 本文完整介紹了創建 Micro 服務所支持的全部 Option
- 最後用 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