本文转载自微信公众号「凉凉的知识库」,作者凉凉的知识库 。转载本文请联系凉凉的知识库公众号。
合久必分,分久必合,技术圈也是如此。在大家纷纷从单体应用过渡到微服务的时候,谷歌携带着新时代的“单体”应用框架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,如何与现有的部署系统结合也是个需要考虑的问题