Slide 1

Slide 1 text

Go で始めるCloudflare Workers syumai Workers Tech Talks #2 (2023/12/15)

Slide 2

Slide 2 text

自己紹介 syumai ECMAScript 仕様輪読会 主催 株式会社ベースマキナで管理画面のSaaS を開発中 Go でGraphQL サーバー (gqlgen) や TypeScript でフロント エンドを書いています Software Design 12 月号からCloudflare Workers の連載をして ます Twitter: @__syumai Website: https://syum.ai

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

ベースマキナとは? DB やAPI の接続設定 & 呼び出し設定をするだけで、簡単にUI 生成が行える管理画面 SaaS API 呼び出しへの権限設定や、レビュー依頼 / 承認機能も簡単に使えます https://about.basemachina.com

Slide 5

Slide 5 text

本日話すこと syumai/workers の紹介 デモ syumai/workers の仕組み どんなアプリケーションが動くか パフォーマンス 注意点 今後の展望

Slide 6

Slide 6 text

syumai/workers の紹介

Slide 7

Slide 7 text

workers https://github.com/syumai/workers http.Handler を作って、 workers.Serve に渡すだけでCloudflare Workers 上でHTTP サ ーバーとして動作する http.ListenAndServe の代わりに workers.Serve 関数を呼ぶ形にするだけ TinyGo / Go を使ってWebAssembly を生成して実行する JavaScript 側のコードを触る必要が無い Cloudflare の機能のバインディングを提供 (R2, KV, D1 など)

Slide 8

Slide 8 text

作ったモチベーション WebAssembly とJavaScript の知識が無くてもGo でWorker を書けるようにしたかった 最終的に、gonew コマンドでtemplate からプロジェクト生成するだけで済む形に なった 実用的なWorker をGo で短時間で実装できるようなライブラリが欲しかった

Slide 9

Slide 9 text

デモ

Slide 10

Slide 10 text

デモ syumai/workers のQuick Start を実行してみます

Slide 11

Slide 11 text

Quick Start 必要なツール Node.js ( とnpm) wrangler npm install -g wrangler . tinygo 0.29.0 以上 gonew go install golang.org/x/tools/cmd/gonew@latest 最近Go 公式から試験的なものとして出た、Go のプロジェクトをテンプレートから 立ち上げるためのコマンド

Slide 12

Slide 12 text

プロジェクトの作成 $ gonew github.com/syumai/workers/_templates/cloudflare/worker-tinygo your.module/my-app $ cd my-app $ go mod tidy $ make dev # 開発サーバーの起動 $ curl http://localhost:8787/hello # outputs "Hello!" 今回は、 モジュール名を github.com/syumai/workers-tech-talk/hello-tinygo に します

Slide 13

Slide 13 text

ファイル構成 ├── Makefile ├── README.md ├── build # ここはいじる必要がない │ ├── app.wasm │ ├── polyfill_performance.js │ ├── shim.mjs │ ├── wasm_exec.js │ └── worker.mjs ├── go.mod ├── go.sum ├── main.go # アプリケーション本体 └── wrangler.toml

Slide 14

Slide 14 text

アプリケーションの中身 func main() { http.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) { msg := "Hello!" w.Write([]byte(msg)) }) http.HandleFunc("/echo", func(w http.ResponseWriter, req *http.Request) { io.Copy(w, req.Body) }) workers.Serve(nil) // use http.DefaultServeMux } 普通のHTTP Server 同様、HandleFunc を使っているだけ /hello ハンドラーの実装を変更すると、curl の結果が変わるのが確認できる

Slide 15

Slide 15 text

デプロイ make deploy で完了 アプリ名を変えたい場合は、 wrangler.toml を編集する

Slide 16

Slide 16 text

syumai/workers の仕組み

Slide 17

Slide 17 text

syumai/workers の仕組み Cloudflare Workers は、基本的にRequest オブジェクトを処理し、Response オブジェクトを 返すと言う構造になっているため、下記が実装できればOK JavaScript 側で受け取ったRequest オブジェクトをGo に渡す Go 側でResponse オブジェクトを組み立ててJavaScript 側に渡す これをGo の標準ライブラリのsyscall/js を使って実装した

Slide 18

Slide 18 text

syumai/workers の仕組み Worker のendpoint は(当然ながら)JS JS 側は、Go 側の初期化処理が終わるのを待つためのPromise を作成してglobalThis に設 定する Go 側の初期化処理で、Go にJS のRequest オブジェクトを渡すための handleRequest 関数をglobalThis に設定する Go 側の初期化処理が終わったら、Promise を解決 JS 側からhandleRequest 関数を呼んで、結果を返す

Slide 19

Slide 19 text

Worker のendpoint (JS 側) のコード async function run() { // Go側の初期化完了を待つPromiseをglobalThisに設定 const readyPromise = new Promise((resolve) => { globalThis.ready = resolve; }); const instance = new WebAssembly.Instance(mod, go.importObject); go.run(instance); await readyPromise; } // Fetch Handlerを使うのは普通のWorkerと同じ export async function fetch(req, env, ctx) { await run(); // RequestをJS側からGo側に渡して処理する return handleRequest(req, { env, ctx }); } https://github.com/syumai/workers/blob/v0.18.0/cmd/workers-assets- gen/assets/common/shim.mjs

Slide 20

Slide 20 text

Go 側の初期化処理のコード ( 色々省略しています) init 関数の中で、JS 側からGo 側にRequest オブジェクトを渡して処理する handleRequest 関数をglobalThis に設定する func init() { handleRequestCallback := js.FuncOf(func(this js.Value, args []js.Value) any { reqObj := args[0] cb := js.FuncOf(func(_ js.Value, pArgs []js.Value) any { resolve := pArgs[0] go func() { res := handleRequest(reqObj) resolve.Invoke(res) }() return js.Undefined() }) return jsutil.NewPromise(cb) }) jsutil.Global.Set("handleRequest", handleRequestCallback) } https://github.com/syumai/workers/blob/v0.18.0/handler.go

Slide 21

Slide 21 text

Go 側の初期化処理のコード workers.Serve 関数が呼ばれた時点では既にmain 関数の処理に入っており、init 関数 の処理は終わっているので、JS 側で待ち受けていたPromise を解決する func Serve(handler http.Handler) { if handler == nil { handler = http.DefaultServeMux } httpHandler = handler jsutil.Global.Call("ready") select {} } https://github.com/syumai/workers/blob/v0.18.0/handler.go

Slide 22

Slide 22 text

変換処理 Go 側、JS 側でストリームの変換処理が必要 Go のhttp.ResponseWriter -> JS のReadableStream この辺りの話はZenn に書きました Cloudflare Workers で簡単にGo のHTTP サーバーを動かすためのライブラリを作っ た - https://zenn.dev/syumai/articles/ca9n4e91eqljc44k6ebg

Slide 23

Slide 23 text

どんなアプリケーションが動くか

Slide 24

Slide 24 text

gqlgen 製のGraphQL サーバー

Slide 25

Slide 25 text

gqlgen 製のGraphQL サーバー gqlgen 公式から拾ってきたSTAR WARS server STAR WARS 関連の情報をGraphQL 経由で引っ張るAPI 通常のGo を使っています (2023 年12 月現在、TinyGo では動作しませんでした) https://gqlgen-starwars-example.syumai.workers.dev/ https://github.com/syumai/workers-playground/tree/main/gqlgen-starwars-example こんな感じのクエリを投げると結果が返ってくる query Starships { starship(id: "3003") { id name length history } }

Slide 26

Slide 26 text

connect-go 製のサーバー 事前に定義したProtobuf をベースに、Buf でconnect-go のコードを生成、サーバーを実 装したもの 通常のGo を使っています (2023 年12 月現在、TinyGo では動作しませんでした) https://emoji.syum.ai message GetEmojiRequest { string short_name = 1; } message Emoji { string short_name = 1; string emoji = 2; } service EmojiService { rpc GetEmoji(GetEmojiRequest) returns (GetEmojiResponse) {} }

Slide 27

Slide 27 text

connect-go 製のサーバー $ curl -s -H 'Content-Type: application/json' \ https://emoji.syum.ai/emoji.v1.EmojiService/GetEmoji \ -d '{"short_name": "star"}' | gzip -d | jq . { "emoji": { "shortName": "star", "emoji": " " } } 実装はこちら: https://github.com/syumai/workers-playground/blob/main/connect-go- emoji-server

Slide 28

Slide 28 text

画像生成サーバー Go 製のsyumai のプロフィールページ TinyGo で動いています もともとGoogle App Engine で動いていたもの を、Cloudflare Workers に移管した グリーンピースの色をランダムに変化させて表 示したりする https://syum.ai/

Slide 29

Slide 29 text

画像生成サーバー image/png の利用イメージ import "image/png" func writePNG(w http.ResponseWriter, cMap syumaigen.ColorMap) { w.Header().Set("Content-Type", "image/png") img, _ := syumaigen.GenerateImage( syumaigen.Pattern, cMap, 10, ) var buf bytes.Buffer png.Encode(&buf, img) w.WriteHeader(http.StatusOK) io.Copy(w, &buf) } https://github.com/syumai/syum.ai/blob/main/server/image.go

Slide 30

Slide 30 text

その他 D1 を使ったブログサーバー https://github.com/syumai/workers/tree/main/_examples/d1-blog-server Pages Functions をGo のAPI にする例 https://github.com/syumai/workers/tree/main/_examples/pages-functions /functions/api/[[routes]].mjs を丸ごとGo にルーティングする形式

Slide 31

Slide 31 text

活用イメージ Go 製のちょっとしたアプリをデプロイする先として リクエストのあった時しか起動しないので、インスタンスを常時起動するタイプ のサービスを使うより安上がり D1 やKV も使える Scheduled Handler (Cron Trigger) も使える ( あんまり無いかも) Cloudflare Pages のBackend API として Pages Functions で、普通にGo のAPI サーバーが動かせる

Slide 32

Slide 32 text

パフォーマンス

Slide 33

Slide 33 text

パフォーマンス syum.ai (TinyGo 製) を対象に、hey でベンチマークを行った結果を示します 実行タイミングによって、結果にバラつきがある点ご了承ください

Slide 34

Slide 34 text

静的HTML 配信のベンチマーク (hey https://syum.ai で200req) Summary: Total: 0.2159 secs Slowest: 0.1564 secs Fastest: 0.0164 secs Average: 0.0427 secs Requests/sec: 926.2291 Response time histogram: 0.016 [1] | 0.030 [144] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.044 [5] |■ 0.058 [5] |■ 0.072 [2] |■ 0.086 [0] | 0.100 [0] | 0.114 [26] |■■■■■■■ 0.128 [10] |■■■ 0.142 [6] |■■ 0.156 [1] | Latency distribution: 10% in 0.0182 secs 25% in 0.0197 secs 50% in 0.0216 secs 75% in 0.0508 secs 90% in 0.1131 secs 95% in 0.1256 secs 99% in 0.1304 secs <- 99 percentileで約130ms

Slide 35

Slide 35 text

画像生成のベンチマーク (hey https://syum.ai/image で200req) Summary: Total: 0.3666 secs Slowest: 0.1868 secs Fastest: 0.0253 secs Average: 0.0608 secs Requests/sec: 545.4886 Response time histogram: 0.025 [1] | 0.041 [127] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.058 [20] |■■■■■■ 0.074 [0] | 0.090 [1] | 0.106 [0] | 0.122 [18] |■■■■■■ 0.138 [23] |■■■■■■■ 0.154 [1] | 0.171 [7] |■■ 0.187 [2] |■ Latency distribution: 10% in 0.0296 secs 25% in 0.0341 secs 50% in 0.0384 secs 75% in 0.1172 secs 90% in 0.1294 secs 95% in 0.1390 secs 99% in 0.1773 secs <- 99 percentileで約180ms

Slide 36

Slide 36 text

注意点

Slide 37

Slide 37 text

Worker のファイルサイズ制限 無料プランだと、圧縮後のサイズが1MB 以内でないといけない制約があるため、バイ ナリサイズが大きくなる通常のGo ではpublish 出来ない TinyGo じゃないと基本的に無理 有料プランなら、10MB までアップロード可能なので、通常のGo でもある程度のアプ リケーションが動かせる

Slide 38

Slide 38 text

TinyGo の制限 最近は問題がなくなりつつある 2023 年、ついにencoding/json も動くようになった ただし、複雑なアプリケーションでは、時々ビルドが通らないケースがある ビルドにやや時間がかかる 欠点を補って余りある優位性 ( バイナリサイズの小ささ) があるので、個人的には積極 的に使いたい

Slide 39

Slide 39 text

今後の展望

Slide 40

Slide 40 text

TCP sockets 対応 Cloudflare Workers 的には、 cloudflare:sockets の connect 関数でTCP sockets に 対応している これを使えばTCP 上で動作する任意のプロトコルが使えるようになるので、あらゆる DB が選択肢に入る Go 製のDB ドライバが使えるのは魅力的 コネクションプールの問題も、Hyperdrive が登場したことにより、ある程度解消が見 込めるのでセットで使いたい 実はPR はもらっている 年末年始休暇で何とかします!

Slide 41

Slide 41 text

ご清聴ありがとうございました!