本文轉載自微信公眾號「涼涼的知識庫」,作者涼涼的知識庫 。轉載本文請聯系涼涼的知識庫公眾號。
合久必分,分久必合,技術圈也是如此。在大家紛紛從單體應用過渡到微服務的時候,谷歌攜帶著新時代的“單體”應用框架Service Weaver來了!代碼倉庫位于:https://github.com/ServiceWeaver/weaver 才發布沒幾天已經超過了2.5k star,不得不感慨谷歌的號召力。
谷歌稱此框架為模塊化單體(modular monolith),谷歌為什么會在這個時候提出如此標新立異的框架?它究竟有什么獨特之處?讓我們來速速體驗下吧。
安裝
因為Service Weaver使用了泛型,且聲明的依賴版本為1.19。所以本地安裝的go版本需要大于1.19
$ go install github.com/ServiceWeaver/weaver/cmd/weaver@latest
如果你設置了正確的$GOPATH/bin路徑到你的PATH中那么你可以直接運行
$ weaver --help
USAGE
weaver generate // weaver code generator
weaver single <command> ... // for single process deployments
weaver multi <command> ... // for multiprocess deployments
...
教程
創建項目
$ mkdir hello/
$ cd hello/
$ go mod init github.com/liangwt/serviceweaver/hello
啟動服務
先來創建一個最簡單的HTTP服務。與Go內置的HTTP server的使用方式非常類似,唯一的區別是創建端口監聽的方式不同,Service Weaver需要使用github.com/ServiceWeaver/weaver包提供的函數來創建監聽
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/ServiceWeaver/weaver"
)
func main() {
// Get a network listener on address "localhost:12345".
root := weaver.Init(context.Background())
opts := weaver.ListenerOptions{LocalAddress: "localhost:12345"}
lis, err := root.Listener("hello", opts)
if err != nil {
log.Fatal(err)
}
fmt.Printf("hello listener available on %v\n", lis)
// Serve the /hello endpoint.
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!\n", r.URL.Query().Get("name"))
})
http.Serve(lis, nil)
}
// 內置Go http server使用方式
// func main() {
// lis, err := net.Listen("tcp", "localhost:12345")
// if err != nil {
// log.Fatal(err)
// }
// // Serve the /hello endpoint.
// http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintf(w, "Hello, %s!\n", r.URL.Query().Get("name"))
// })
// http.Serve(lis, nil)
// }
執行也和普通的代碼沒有區別
$ go mod tidy
$ go run .
hello listener available on 127.0.0.1:12345
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : f4bd112d-c90d-409c-a90f-5022fa5a7b3f │
╰───────────────────────────────────────────────────╯
當服務啟動之后我們就可以調用對應端口,直到這里依舊和普通的HTTP server沒有區別
$ curl 'localhost:12345/hello?name=Weaver'
Hello, Weaver!
組件(Components)
組件是Service Weaver中一個獨特的概念
什么是組件?一個應用會有多個組件,每一個組件就是一個Go的interface。
下面的反轉字符串type Reverser interface就是一個組件,type reverser struct是它的一個實現。需要注意一點reverser struct組合了weaver.Implements[Reverser]用以實現Service Weaver要求的其他接口
// Reverser component.
type Reverser interface {
Reverse(context.Context, string) (string, error)
}
// Implementation of the Reverser component.
type reverser struct{
weaver.Implements[Reverser]
}
func (r *reverser) Reverse(_ context.Context, s string) (string, error) {
runes := []rune(s)
n := len(runes)
for i := 0; i < n/2; i++ {
runes[i], runes[n-i-1] = runes[n-i-1], runes[i]
}
return string(runes), nil
}
組件該怎么用?我們可以用weaver.Get()獲取一個組件的實例。例如:
func main() {
// Get a network listener on address "localhost:12345".
...
fmt.Printf("hello listener available on %v\n", lis)
// Get a client to the Reverser component.
reverser, err := weaver.Get[Reverser](root)
if err != nil {
log.Fatal(err)
}
// Serve the /hello endpoint.
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
reversed, err := reverser.Reverse(r.Context(), r.URL.Query().Get("name"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
fmt.Fprintf(w, "Hello, %s!\n", reversed)
})
http.Serve(lis, nil)
}
這次在運行代碼前我們得先執行下面命令,文件中會多一個 weaver_gen.go 文件
重新運行代碼
$ go run .
hello listener available on 127.0.0.1:12345
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : 7581bbb5-21c1-4cd3-8192-3fa2ac2bfc70 │
╰───────────────────────────────────────────────────╯
調用對應的接口就會獲得反轉的字符串響應
$ curl 'localhost:12345/hello?name=Weaver'
Hello, revaeW!
對組件的看法
組件的概念,給我的第一感覺是非常像依賴注入中的IOC容器(不知道什么IOC的同學可以自行網上搜索充電)。
很多語言的很多框架都有依賴注入的功能:定義一個接口,實現這個接口,從IOC容器中獲取這個接口的實例。借助依賴注入來實現依賴解耦,簡化對象創建流程等
Service Weaver定義組件的概念也有其自身的目的,稍后體驗過多進程執行的之后,會對組件有一點更深的理解
多進程執行
先創建一個TOML文件weaver.toml,內容如下
[serviceweaver]
binary = "./hello"
編譯并使用weaver multi deploy來運行我們的應用
$ go build
$ weaver multi deploy weaver.toml
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : c42d8b31-e6e3-4e41-a56e-6e18956927d3 │
╰───────────────────────────────────────────────────╯
S0308 22:33:22.289059 stdout 451b9a05 ] hello listener available on 127.0.0.1:12345
S0308 22:33:22.289190 stdout 2c79c310 ] hello listener available on 127.0.0.1:12345
調用對應的接口依舊會獲得反轉的字符串響應
$ curl 'localhost:12345/hello?name=Weaver'
Hello, revaeW!
你可能會說這和go run .有啥區別?對于返回的響應來說確實沒有區別,但在運行機制上兩者已經有了巨大的差別。
當我們執行go run .時,我們的應用包含所有的組件運行在一個進程中,組件的調用就是Go中的正常的函數調用

當我們執行weaver multi deploy weaver.toml時,我們的應用中的組件會變的像微服務一樣,運行中多個進程中,此時組件的調用就通過RPC的方式了

更進一步,既然組件都可以運行中在不同的進程中,自然也可以運行在不同的機器中
Service Weaver目前僅提供了GKE(Google Kubernetes Engine)的支持。快速將應用部署到GKE的不同容器中
$ weaver gke deploy weaver.toml

對多進程的看法
此時再回顧下對組件的看法,weaver.Get:當單進程部署時,它返回一個接口的本地實例,當多進程部署時,它返回一個RPC的client
Service Weaver稱自己為模塊化單體(modular monolith)。通過組件這個概念讓你寫代碼和部署代碼的動作分離開,你可以按照單體的方式寫代碼,然后在其他進程或者機器按照微服務的方式運行組件的進程
總結
因為也是剛剛接觸到這個框架,很多細節還不太理解,目前業界更是沒有實踐落地。這里說些我自己的看法,也歡迎大家批評指正
單體向微服務的演進不是由于某個單一的原因造成,團隊的分工,龐大的代碼,復雜的依賴等諸多原因造就了現在流行微服務架構。Service Weaver也并沒有要取代傳統微服務,在不同團隊間使用傳統微服務,在同一個團隊內部或者同一功能的服務使用模塊化單體是一種新的選擇
?? 關于模塊化單體(modular monolith)中的單體
單體的開發確實會降低我們的心智負擔,我們不用關心不同模塊之間的RPC協議,模塊之間的服務發現,在本地測試的時候也不需要專門構建其他依賴微服務的模塊
Monorepo 倉庫能到達一樣的效果么?部分能,我們可以把一套微服務放到一個Monorepo中,并使用一個BFF服務對外提供統一的訪問入口,能實現類似于單體開發的體驗。但我們依舊需要關心RPC協議、服務發現等一系列的問題
?? 關于模塊化單體(modular monolith)中的模塊
有了Service Weaver我們就可以不用關心服務拆分了么?不是的,我們依舊面臨著服務拆分,傳統的微服務拆分到不同的代碼倉庫,Service Weaver拆分到了不同的邏輯(組件)。所以傳統的服務拆分的方法論和經驗,在這里依舊適用
?? 模塊化單體(modular monolith)之外的東西
Service Weaver也提供了Logging、Metrics、Tracing、Profiling的基本能力,這部分沒啥特殊的,絕大部分的RPC框架都集成了類似的能力
Service Weaver提供了Google Kubernetes Engine (GKE) 部署的能力,如果你的公司恰好使用標準的k8s服務,或許不難擴展。如果你的公司使用了非標準的k8s,如何與現有的部署系統結合也是個需要考慮的問題