大家好,我是萧楚河,公众号:golang面试经典讲解,感谢关注,一起学习一起成长。
今年6月,一群谷歌员工(由谷歌软件工程师Michael Whittaker领导)发表了一篇名为“Towards Modern Development of Cloud Applications”的论文。
正如Whittaker等人所指出的,从架构上讲,微服务本身设置就有问题,它是一个没有边界的结构它们将逻辑边界(如何编写代码)与物理边界(如何部署代码)混为一谈。这就是问题的开始。
因此,谷歌的工程师们提出了一种堪称“微服务2.0”的方法。将应用程序构建为逻辑整体,但将其交给自动化运行时,后者可以根据应用程序所需的内容和可用的内容来决定在哪里运行工作负载。
基于新提出的结构,他们能够将系统的延迟降低15倍,成本降低9倍。
“从有组织的模块化代码开始,我们就可以将部署架构作为实现细节,”Google开发人员倡导者Kelsey Hightower在10月份对这项工作表示了下一步计划。
鉴于以上实践,诞生了Service Weaver。
ServiceWeaver是一个用于编写、部署和管理分布式应用程序的编程框架。您可以在机器上本地运行、测试和调试Service Weaver应用程序,然后使用单个命令将该应用程序部署到云中。
Service Weaver应用程序由许多组件组成。组件被呈现为常规Go接口,组件之间通过调用这些接口定义的方法来相互交互。这使得编写Service Weaver应用程序变得容易。你不需要编写任何联网或序列化代码,你只要写Go。Service Weaver还提供用于日志记录、度量、跟踪、路由、测试等的库。
对于不熟悉的人来说,模块化单体应用是一种体系结构,其中整个应用程序都写成一个单独的应用程序,在一个单一的代码库中。模块化方面意味着单体应用被分成单独的组件,并且在不同组件之间有干净明确的接口。
这里是一个例子:
在这个单体应用中,有三个组件:订单、支付和运输。每个组件实现单体应用的一个特定部分,关键在于每个组件的大部分都是私有的,并且组件之间的任何通信都是通过明确定义的接口进行的。
这使得每个组件的内部可以进行更改和更新,而不会影响任何其他组件,假设接口未更改或破坏。
当多个团队在单体应用上工作时,这确实有助于在团队之间设定明确的边界,并使每个组件独立于其他任何组件发展,同时在组件之间显示明确的依赖关系。
每当你的单体应用被部署时,它都被部署为一个单一的应用程序,每个单体应用的实例都运行一个单一的进程。例如,如果你要部署到AWS,每个单体应用的实例都将作为一个进程运行在EC2实例上。
Service Weaver和传统的模块化单体应用之间的区别在于部署方式。使用Service Weaver构建的应用程序在部署时,不是将所有组件运行在同一台机器上的一个大进程。
相反,每个组件都作为一个独立的微服务进行部署。这非常巧妙,因为您既可以获得将所有代码放在单个代码库中并进行方便的本地开发的好处,又可以获得运行分布式架构的好处,其中您可以根据需要缩放每个组件,例如内存,CPU和实例数量等。
ServiceWeaver的核心抽象是组件。组件就像一个参与者,ServiceWeaver应用程序是作为一组组件来实现的。具体地说,组件用一个常规Go接口表示,组件通过调用这些接口定义的方法来相互交互。
在本节中,我们将定义一个简单的hello组件,它只打印一个字符串并返回。首先,运行go mod init hello来创建一个go模块。
mkdir hello/
cd hello/
go mod init hello
然后,创建一个名为main.go的文件,其中包含以下内容:
package main
import (
“context”
“fmt”
“log”
"github.com/ServiceWeaver/weaver"
)
func main() {
if err := weaver.Run(context.Background(), serve); err != nil {
log.Fatal(err)
}
}
// app is the main component of the application. weaver.Run creates
// it and passes it to serve.
type app struct{
weaver.Implements[weaver.Main]
}
// serve is called by weaver.Run and contains the body of the application.
func serve(context.Context, *app) error {
fmt.Println(“Hello”)
return nil
}
在构建和运行应用程序之前,我们需要运行ServiceWeaver的代码生成器,称为weavergenerate。weavergenerate编写一个weaver_gen.go文件,其中包含ServiceWeaver运行时所需的代码。我们将详细说明weaver generate到底做了什么,以及为什么我们稍后需要运行它。最后,运行应用程序!
$ go mod tidy
$ weaver generate .
$ go run .
Hello
在ServiceWeaver应用程序中,任何组件都可以调用任何其他组件。为了演示这一点,我们引入了第二个Reverser组件。创建一个文件Reverser.go。转到以下内容:
package main
import (
"context"
"github.com/ServiceWeaver/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
}
Reverser组件由Reverse接口表示,该接口具有反转字符串的Reverse方法。reverser结构是我们对reverser组件的实现(如weaver.Implements[Reverser]所示它包含的字段)。
接下来,在main.go中编辑应用程序组件以使用reverser组件:
package main
import (
"context"
"fmt"
"log"
"github.com/ServiceWeaver/weaver"
)
func main() {
if err := weaver.Run(context.Background(), serve); err != nil {
log.Fatal(err)
}
}
type app struct{
weaver.Implements[weaver.Main]
reverser weaver.Ref[Reverser]
hello weaver.Listener
}
func serve(ctx context.Context, app *app) error {
// Call the Reverse method.
// The hello listener will listen on a random port chosen by the operating
// system. This behavior can be changed in the config file.
fmt.Printf("hello listener available on %v\n", app.hello)
// Serve the /hello endpoint.
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
reversed, err := app.reverser.Get().Reverse(ctx, name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Hello, %s!\n", reversed)
})
return http.Serve(app.hello, nil)
}
应用程序结构有一个类型为weaver.Ref[Reverser]的新字段,提供对反向器组件的访问权限。
我们已经了解了如何通过go run在单个流程中运行Service Weaver应用程序。现在,我们将在多个进程中运行我们的应用程序,在作为RPC执行的组件之间进行方法调用。首先,创建一个名为weaver.toml的TOML配置文件,其中包含以下内容:
[serviceweaver]
binary = "./hello"
[multi]
listeners.hello = {address = "localhost:12345"}
此配置文件指定ServiceWeaver应用程序的二进制文件,以及hello侦听器的固定地址。接下来,使用weaver multi-deploy构建并运行应用程序:
go build # build the ./hello binary
weaver multi deploy weaver.toml # deploy the application
我发现图表有助于理解的一个例子,这是Google解释了这些不同部分如何组合在一起的图表:
我们还没有谈到 Service Weaver 框架的多功能性。传统微服务的一个缺点是,你经常会遇到接口过于频繁的问题。毕竟,没有人能预见架构的演变方向。
然后,你不得不忍受增加的延迟和更高的网络调用失败率,或者花时间将这两个微服务合并起来。
使用 Service Weaver,这个问题得到了解决。如果你查看上面的图表,你会发现有四个模块被定义了。当部署为微服务时,你会注意到 A 和 B 住在一起,C 和 D 是他们自己的微服务。
使用 Service Weaver,你可以自由地定义组件在哪里部署。你可以选择让多个组件一起运行在单个微服务中,或者将所有组件都部署为单独的微服务。如果你的应用程序演变到两个组件变得过于频繁,并且作为单独的微服务运行,你可以轻松地将它们合并,而不需要改变代码,并在 Service Weaver 中快速更改配置。
这里基于我的理解对Service Weaver的初步介绍,鉴于很多人可能希望自己实践一下,可以通过如下地址进入官方网站进行学习:https://serviceweaver.dev