はじめに
Google が、この 3月にオープンソースとして公開した Service Weaver というフレームワークを公開しました。
簡単に紹介すると、
"モノリスなアーキテクチャーのようにコードを書くだけで、マイクロサービスとしてデプロイ可能なアプリを書くためのフレームワーク"
です。
「は?何を言っているの?」でしょうし、字面をなんとなく理解できたとしても、「そんな魔法のような話がある訳ないでしょ?」だと思います。
今回は、「あぁ、こういうことか!便利だな!」と感じられるように、実際に手を動かして、実際の動きを見てみたいと思います。
前提
OS への依存は、特には無いとは思いますが、Ubuntu 22.04 LTS を利用しています。
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.4 LTS"
また、Go 言語は go1.22.1
を利用しています。
$ go version
go version go1.22.1 linux/amd64
公式ドキュメントの Installation の記載からは、1.21 以降が必要そうな記載でした。
手順
基本は、公式ドキュメントのチュートリアルを習う形で試しています。
ちなみに、公式ドキュメントでは、下記のようなステップで解説されていますので、それに従って試していきます。
- (単一) コンポーネント
- とりあえず、最低限の使い方
- 複数コンポーネント
- 単一プロセスのまま、複数のコンポーネントにアプリを分割
- リスナー
- サービスとしてリクエストを受けるための仕組み
- 複数プロセス
- 実際に複数のプロセスに分割
インストール
まずは、一般的な Go 言語のツールと同様に、Servive Weaver の実行コマンドである weaver
をインストールします。
go install github.com/ServiceWeaver/weaver/cmd/weaver@latest
これで、$GOBIN
配下、デフォルでは $HOME/go/bin
だと思いますが、そこに weaver
コマンドがインストールされているはずです。
確認として、バージョンを表示してみます。
$ weaver version
weaver v0.23.0 linux/amd64
1. (単一) コンポーネント
最初のステップとして、ここでは単一のコンポーネントとして実装して、Service Weaver の「とりあえずの使い方」を見ていきます。
公式ドキュメントも、"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
}
公式ドキュメントの解説によると、ざっくり↓こんな感じのようです。
-
weaver.Run(...)
- Service Weaver を実行する部分
- ここでは、
serve
を最初に実行すべき関数として指定
-
type app struct {...}
- Service Weaver の Main コンポーネントを実装する構造体として定義
-
func serve(...)
-
weaver.Run(...)
の部分で、最初に実行すべきと指定された関数 - ここではシンプルに
"Hello"
だけ出力して終了
-
このアプリを Service Weaver のお作法に従って、実行していきます。
まずは、通常の Go 言語のアプリと同様に、依存関係を解決します。
go mod tidy
その上で、Service Weaver が、まさに魔法の部分として、必要なコードを生成します。
weaver generate .
こうすると何やら weaver_gen.go
というファイルが生成されていますが、DO NOT EDIT.
とあるので、弄らないようにしておきます。
(自動生成されたコードであれば、当たり前かも知れませんが)
// Code generated by "weaver generate". DO NOT EDIT.
//go:build !ignoreWeaverGen
package main
import (
"context"
"github.com/ServiceWeaver/weaver"
"github.com/ServiceWeaver/weaver/runtime/codegen"
"go.opentelemetry.io/otel/trace"
"reflect"
)
// ...
いよいよ実行してみます。
$ $ go run .
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : 42e61327-ed93-4e24-966a-ca84bb8d31ed │
╰───────────────────────────────────────────────────╯
Hello
何やら魔法がかかった部分っぽい表示の後に、目的の "Hello"
が表示されています。
一応、動きはしましたが、これだけでは、単に 1つのプロセスとして、アプリを実行しただけで、シンプルなモノリスの実装です。
なので、公式ドキュメントの通り、複数のコンポーネント (同じマシン上で動作する複数のプロセスであったり、別のマシン上で動作するプロセスだったり) に分けて、それぞれが分担して処理を実行するアプリに拡張していく必要があります。
2. 複数コンポーネント
よりマイクロサービスなアーキテクチャーにしていくために、まずは、単一プロセスのままですが、複数の論理的なコンポーネントに分けて、処理を分担できるように拡張します。
先ほどの main.go
と同じディレクトリに、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
}
公式ドキュメントの解説はシンプルですが、まず、Main 側 (main.go
) から呼ばれる Reverser というコンポーネントとして、reverser
構造体を定義しています。この構造体の定義には、weaver.Implements[Reverser]
とありますので、Reverser
というインタフェースが実装されているようです。実際に type Reverser interface {...}
の部分を読むと、Reverse
という関数が紐付けられています。
よって、マイクロサービス的なイメージでは、「Reverser というサービスが、Reverse
という関数 (API) を実装している」という感じです。
では、この Reverser を Main 側から呼ぶように、コードを修正していきます。
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]
}
func serve(ctx context.Context, app *app) error {
// Call the Reverse method.
var r Reverser = app.reverser.Get()
reversed, err := r.Reverse(ctx, "!dlroW ,olleH")
if err != nil {
return err
}
fmt.Println(reversed)
return nil
}
変更になったところは、まず、type app struct {...}
の定義内で、「このコンポーネントは、Reverser
を reverser
という名前で参照するよ」と追加になっています。
そして、実際、これを使って serve
関数内で、Reverser
を呼び出して、文字列を逆順に並べ替えさせて、その結果を出力しています。
では、Hello の時と同様に、実行していきましょう。
# 依存関係は変更無しのため、何も起きないはず
go mod tidy
# 魔法のコードを生成
weaver generate .
$ go run .
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : c1d3c8fb-7f8e-4185-932d-9a7aa74d4a7f │
╰───────────────────────────────────────────────────╯
Hello, World!
単一プロセスのままですが、Main と Reverser という論理的なコンポーネントに分割することができました。
3. リスナー
マイクロサービスアーキテクチャーにおいては、それぞれのサービスが互いに API を呼び出し合って連携するので、サービスとしてリクエストを受けるための仕組みが必要になります。Service Weaver では、リスナー (Lisnter
) という仕組みで、それを実装するようです。
main.go
をリスナーを使った実装に変更します。
package main
import (
"context"
"fmt"
"log"
"net/http"
"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 {
// 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)
}
まず、app
構造体に hello
というフィールドが、weaver.Listener
型で定義されています。これは net.Listen
と似たネットワークリスナーとのことです。
よって、一般的な Go 言語の Web アプリのように http.HandleFunc()
を使って、/hello?name=<name>
というエンドポイントで、ハンドラを登録できています。
実際に、このアプリを動かしていく前に、Service Weaver では、デフォルトではランダムなポートで待ち受けるため、後で使いやすいように、ポート番号を指定しておきます。そのためには、設定ファイルとして、weaver.toml
を作成し、かつ環境変数で SERVICEWEAVER_CONFIG=weaver
のように指定する必要があるようです。
[single]
listeners.hello = {address = "localhost:12345"}
では、実行していきます。
# 依存関係は変更無しのため、何も起きないはず
go mod tidy
# 魔法のコードを生成
weaver generate .
アプリ起動時は、環境変数を使って、設定ファイルを指定して実行する必要があります。
SERVICEWEAVER_CONFIG=weaver.toml go run .
$ SERVICEWEAVER_CONFIG=weaver.toml go run .
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : 2434ac2d-e1ab-491b-b5f9-e02d23aff6bf │
╰───────────────────────────────────────────────────╯
hello listener available on 127.0.0.1:12345
http://localhost:12345/hello?name=<name>
で待ち受けを始めたので、別のターミナルから、その API を叩いてみます。
curl -X GET http://localhost:12345/hello?name=<name>
叩いている内容を補足する意味で、公式ドキュメントの curl コマンドより記述やオプションを追加しています。
$ curl -X GET http://localhost:12345/hello?name=revaeW
Hello, Weaver!
また、ここまでのように、単一プロセスとして動作している場合には、下記のコマンドでアプリの実行状態を確認できます。
weaver single status
$ weaver single status
╭──────────────────────────────────────────────────────╮
│ DEPLOYMENTS │
├───────┬──────────────────────────────────────┬───────┤
│ APP │ DEPLOYMENT │ AGE │
├───────┼──────────────────────────────────────┼───────┤
│ hello │ 2434ac2d-e1ab-491b-b5f9-e02d23aff6bf │ 2m39s │
╰───────┴──────────────────────────────────────┴───────╯
╭────────────────────────────────────────────────────╮
│ COMPONENTS │
├───────┬────────────┬────────────────┬──────────────┤
│ APP │ DEPLOYMENT │ COMPONENT │ REPLICA PIDS │
├───────┼────────────┼────────────────┼──────────────┤
│ hello │ 2434ac2d │ weaver.Main │ 31399 │
│ hello │ 2434ac2d │ hello.Reverser │ 31399 │
╰───────┴────────────┴────────────────┴──────────────╯
╭─────────────────────────────────────────────────╮
│ LISTENERS │
├───────┬────────────┬──────────┬─────────────────┤
│ APP │ DEPLOYMENT │ LISTENER │ ADDRESS │
├───────┼────────────┼──────────┼─────────────────┤
│ hello │ 2434ac2d │ hello │ 127.0.0.1:12345 │
╰───────┴────────────┴──────────┴─────────────────╯
さらに、下記のコマンドを使うと、上記の内容をブラウザのダッシュボードとしても確認できます。
# ブラウザのダッシュボードとして表示
weaver single dashboard
4. 複数プロセス
ここまで、単一プロセスとしてアプリをデプロイしてきましたが、いよいよ複数のプロセスとしてアプリを実行していきます。
複数プロセスになることで、各関数同士の呼び出しは、自動で RPC 経由になるとのことです。これは魔法ですね。
まず、複数プロセスでのアプリ起動では、これまで使っていた go run .
コマンドではなく、weaver multi deploy <config-file>
コマンドでデプロイします。そのため、先ほど作成した設定ファイルに、アプリの所在を追記していきます。
[serviceweaver]
binary = "./hello"
[multi]
listeners.hello = {address = "localhost:12345"}
実行すべきアプリのバイナリが、./hello
であることが追加されています。
では、実際に実行していきますが、まずは、先ほどまではバイナリをビルドしていなかったので、まずは ./hello
としてビルドします。
# 依存関係は変更無しのため、何も起きないはず
go mod tidy
# 魔法のコードを生成
weaver generate .
# アプリのビルド -> ./hello が生成されるはず
go build .
前述の通り、アプリ起動は、weaver multi deploy <config-file>
コマンドを使います。
weaver multi deploy weaver.toml
$ weaver multi deploy weaver.toml
╭───────────────────────────────────────────────────╮
│ app : hello │
│ deployment : b9ee2568-46ea-4d32-aebd-452c69ea433d │
╰───────────────────────────────────────────────────╯
S0101 09:00:00.000000 stdout ccb4c971 │ hello listener available on 127.0.0.1:12345
S0101 09:00:00.000000 stdout c12f2c34 │ hello listener available on 127.0.0.1:12345
単一プロセスの場合と異なり、hello に関する出力が 2行に増えています。これは Service Weaver が、全てのコンポーネントを 2つずつ作成するためだそうです。詳しくは、公式ドキュメントの Components
のセクションを参照とのこと。
一応、単一プロセスの時と同じように、動作を確認してみます。
$ curl -X GET http://localhost:12345/hello?name=revaeW
Hello, Weaver!
また、先ほどは単一プロセスだったので、weaver single ...
で実行状態を確認しましたが、複数プロセスの場合も、同様に確認できます。
weaver multi status
$ weaver multi status
╭──────────────────────────────────────────────────────╮
│ DEPLOYMENTS │
├───────┬──────────────────────────────────────┬───────┤
│ APP │ DEPLOYMENT │ AGE │
├───────┼──────────────────────────────────────┼───────┤
│ hello │ f1268dc5-fbcb-4184-8e23-46ddb27a54bc │ 5m26s │
╰───────┴──────────────────────────────────────┴───────╯
╭────────────────────────────────────────────────────╮
│ COMPONENTS │
├───────┬────────────┬────────────────┬──────────────┤
│ APP │ DEPLOYMENT │ COMPONENT │ REPLICA PIDS │
├───────┼────────────┼────────────────┼──────────────┤
│ hello │ f1268dc5 │ weaver.Main │ 31752, 31759 │
│ hello │ f1268dc5 │ hello.Reverser │ 31766, 31774 │
╰───────┴────────────┴────────────────┴──────────────╯
╭─────────────────────────────────────────────────╮
│ LISTENERS │
├───────┬────────────┬──────────┬─────────────────┤
│ APP │ DEPLOYMENT │ LISTENER │ ADDRESS │
├───────┼────────────┼──────────┼─────────────────┤
│ hello │ f1268dc5 │ hello │ 127.0.0.1:12345 │
╰───────┴────────────┴──────────┴─────────────────╯
これを見ると、確かに COMPONENTS
の REPLICA PIDS
が 2つずつになっていますね。
この先は...
公式ドキュメントでは、さらに GKE や Kubernetes 上に、アプリをデプロイしていく手順が続いています。
しかし、記事としてはちょっと長くなってきたので、一旦、ここで区切りとします。
今後、Kubernetes 上へのデプロイ等を試してみたいと思います。