- 投稿日:2020-02-26T22:51:06+09:00
GoでRace Conditionを発見する
goroutine を使う選択をしたときに、あなたはRace Conditionに注意しなければならない。
countを5000回加算するコード(直列処理)
main.gopackage main import ( "fmt" ) func main() { var count int for i := 0; i < 10000; i++ { count++ } fmt.Printf("count: %d\n", count) }結果
$ go run ./main.go count: 5000愚直に並行処理にする
この変更はRace Conditionを引き起こし問題になる
import ( "fmt" + "sync" ) func main() { var count int + var wg sync.WaitGroup for i := 0; i < 5000; i++ { - count++ + wg.Add(1) + go func() { + defer wg.Done() + count++ + }() } + wg.Wait() fmt.Printf("count: %d\n", count) }Bad: countを5000回加算するコード(並行処理)
これではいけない
package main import ( "fmt" "sync" ) func main() { var count int var wg sync.WaitGroup for i := 0; i < 5000; i++ { wg.Add(1) go func() { defer wg.Done() count++ }() } wg.Wait() fmt.Printf("count: %d\n", count) }結果
5000よりも少なくなる。 それぞれの goroutine が count を読んだタイミングが異なるため。
$ go run ./main.go count: 4354Race Conditionを発見する
-race
オプション
go run -race main.go
のように-race
オプションを付けるとRace Conditionを検出できる。このオプションは、go test
やgo build
でも有効。
-race
をつければRace Conditionを検出し、レポートを出力することができるが、メモリ使用量が増したり、実行時間が増えたりするので注意が必要。ベンチマークテストや負荷試験をするときに、CIで回すことが推奨される。次のようにどこで Race Condition が起きたかレポートしてくれる。
$ go run -race main.go ================== WARNING: DATA RACE Read at 0x00c0000ac008 by goroutine 8: main.main.func1() /Users/hogehoge/workspace/go-test/main.go:16 +0x6c Previous write at 0x00c0000ac008 by goroutine 7: main.main.func1() /Users/hogehoge/workspace/go-test/main.go:16 +0x82 Goroutine 8 (running) created at: main.main() /Users/hogehoge/workspace/go-test/main.go:14 +0xe7 Goroutine 7 (finished) created at: main.main() /Users/hogehoge/workspace/go-test/main.go:14 +0xe7 ================== count: 5000 Found 1 data race(s) exit status 66排他制御を導入してRace Conditionが起こらないように変更する
func main() { var count int + var mu sync.Mutex var wg sync.WaitGroup for i := 0; i < 5000; i++ { wg.Add(1) go func() { defer wg.Done() + mu.Lock() + defer mu.Unlock() count++ }() }Good: countを5000回加算するコード(並行処理)
main.gopackage main import ( "fmt" "sync" ) func main() { var count int var mu sync.Mutex var wg sync.WaitGroup for i := 0; i < 5000; i++ { wg.Add(1) go func() { defer wg.Done() mu.Lock() defer mu.Unlock() count++ }() } wg.Wait() fmt.Printf("count: %d\n", count) }結果
$ go run ./main5.go count: 5000参考資料
- 投稿日:2020-02-26T15:29:58+09:00
go get でプライベートリポジトリをダウンロードできない時
公開リポジトリはダウンロード出来るけど・・・
go get github.com/motemen/goreプライベートリポジトリはダウンロードできない!
pi@raspberrypi:~ $ go get github.com/CreatorsLab/test-repository # cd .; git clone -- https://github.com/CreatorsLab/test-repository /home/pi/go/src/github.com/CreatorsLab/test-repository Cloning into '/home/pi/go/src/github.com/CreatorsLab/test-repository'... remote: Invalid username or password. fatal: Authentication failed for 'https://c61axxxxxxxxxxxxxxxxxxxxxxxxx:x-oauth-basic@github.com/CreatorsLab/test-repository/' package github.com/CreatorsLab/test-repository: exit status 128結論 c61aいらないわ
以下URLには
https://github.com/k8sp/sextant/issues/29git config --global url."https://c61axxxxxxxxxxxxxxx:x-oauth-basic@github.com/".insteadOf "https://github.com/"こうしろって書いてあるけど、これだと動かなかった。
でも、git config --global url."https://xxxxxxxxxxxxxxx:x-oauth-basic@github.com/".insteadOf "https://github.com/"しろこうしたら動いた
- 投稿日:2020-02-26T15:19:44+09:00
プライベートリポジトリをgo getする方法。
結論
①トークンを発行してね。
参考サイト
https://qiita.com/kz800/items/497ec70bff3e555dacd0②トークンを.gitconfigに書き込んでね
$ nano ~/.gitconfig~/.gitconfig[url "https://xxxxxxxxxxxxxxx:x-oauth-basic@github.com/"] insteadOf = https://github.com/以下エラーが発生した場合トークンが間違ってるかも
# cd .; git clone -- https://github.com/CreatorsLab/test_repo /home/pi/go/src/github.com/CreatorsLab/test_repo Cloning into '/home/pi/go/src/github.com/CreatorsLab/test_repo'... remote: Invalid username or password. fatal: Authentication failed for 'https://c61adxxxxxxxxxxxxxxxxxxxxxxxx:x-oauth-basic@github.com/CreatorsLab/test_repo/' package github.com/CreatorsLab/test_repo: exit status 128参考サイト
- 投稿日:2020-02-26T08:34:55+09:00
Goのアプリケーションをテンプレートから自動生成するCLIツール
作成したもの
gogenerとは、テンプレートからGoのプロジェクトを自動生成するCLIツールです!
Chefのtemplateっぽく使えたらと思って作りました。動作
モチベーション
新しくWebのAPIを作成する時に、
一から書くと時間かかって面倒臭いので、
テンプレートだけ作っておいて、いつでもプロジェクトを使えるようにしようとましたどんなテンプレートがあるの?
テンプレートの取得には、gogener自身が持っているテンプレートと、外部に設置しているテンプレートがあります。
今のところ、
local - ryomak-app(ddd + clean architecture構成のWebAPI) reomte - ryomak/go-deep-util-example (GoでDeepLearningするexample) - ryomak/grpc-vue-go-example (I/Fがgrpcで作成したGoとvueのWebサイト) - ryomak/go-p2pchat (P2P型のgoのチャットCLIツール)があります。
ryomak-app
クリーンアーキテクチャで作成しています。WebAPIを作成するときは、このパッケージを再利用しています。
ryomak/go-deep-util-example
go-deepというディープラーニングするライブラリを利用して、画像分類するプログラムです。
手っ取り早くGoでディープラーニングやってみたい人にオススメです。
ryomak/grpc-vue-go-example
grpcを使ってみたくて、go(clean architecture)の構成でVue.jsのWebサイトを作りました。grpcのexampleで使えます。
GPSを取得して
参加したユーザの位置情報をレーダーのように表示します。
レーダーの実装で力尽きてしまい、見た目がまだまだなのと、部屋の管理がまだ上手くないので、こちら修正も別途していこうと思います。
ryomak/go-p2pchat
https://github.com/ryomak/go-p2pchat
これはP2P型のチャットCLIです。
一度一つのノードに接続すれば、ブロックチェーンのように、他の接続ノードへ伝搬します。
gogenerの使い方
$ go get github.com/ryomak/gogener/cmd/gogener $ gogener create -app (アプリ名) -mod (module名) ryomak-app
最後に
今回は初めてガッツリCLIを作ってみました。
Go言語はCLIが作りやすくていいですね。
それぞれのテンプレートの元になったプロジェクトの修正等あれば、プルリクお願いしますmm余談
テンプレートはGithubPagesで公開すれば、CLIツールで使えるようになります。是非作ってプルリク投げて欲しいです。
方法は、https://ryomak.github.io/gogener/
にまとめていますが、例で説明します。
プロジェクトを作成
基本的には生成するアプリと同じディレクトリ構成でテンプレートを配置します。配置するテンプレートはyamlファイルで記述しておきます。このファイルをgogenerに登録しておくと、このyamlに従って、プロジェクトを作成してくれます。
app_template.yamlname: "ryomak/go-p2pchat" bg-file-path: "bg.txt" templates: - "go.mod" - "util/userInput.go" - "util/util.go" - "Makefile" - "peer/util.go" - "peer/peer.go" - "README.md" - "chat.go" - "control/control.go" - "app_template.yaml"
テンプレートを生成
main.goを例に見てみます。
main.gopackage main import ( "fmt" ) func main() { fmt.Println("hello [[.AppName]]") }VueやAngularを含めたプロジェクトのテンプレートだと、
{{}}
で,かぶってしまうので、Delimsを[[]]
に変更しています。
デフォルトでテンプレートで使えるパラメータは、
- AppName
- ModName
- ToCamel
- ToLowerCamel が使えます。
gogenerにテンプレートを追加
gogenerにプルリクを投げます。修正は一行だけです。
gogener/interal/recipe/remote/templates.govar remoteMap = map[string]string{ "go-deep-util-example": "https://ryomak.github.io/templates-for-gogener/go-deep-util/app_template.yaml", "grpc-vue-go-example": "https://ryomak.github.io/templates-for-gogener/grpc-vue-example/app_template.yaml", + "exapp": "{github pages url}/app_template.yaml" }これで以上です。
最後に
今回、初めてしっかりしたCLIを作ってみました。
すぐにAPPを作成したい時は、使えると思います。個人的な学びとしては、CLI作成において、Github Actionで、CLIのリリースまで行いました。普段はCircleCIを用いていたのですが、GithubActionが便利で、乗り換えて行こうと思いました。
CLIのリリースにはgorelaserが簡単でした!下の記事をみたらすぐできます!
https://tellme.tokyo/post/2020/02/04/release-go-cli-tool/
- 投稿日:2020-02-26T08:01:56+09:00
ログのクラスタリング分析の処理をGoで実装し、Cloud RunとPub/Subを使ってサーバレスで処理できる仕組みを作る
先日、Firebase + GAEを使って実装したサービス「LogCrow」に新しい機能として、ログの登録を簡略化するために、ログファイルをアップロードして、ログの各行をクラスタリングし、類似ログ同士をまとめあげた上で、ログ登録できる機能を追加しました。
LogCrowのAnalyzeメニューにアクセスすると、ファイルをアップロードできるフォームがあります。ここに分析にかけたいログファイルをアップロードして実行すると分析処理がサーバサイドで走ります。
分析には多少時間がかかります。
分析が完了すると、分析結果を以下のような感じで確認できます。類似ログがまとめ上げられているので、その内容を眺め、ログの原因と対策の情報を記録しておきたいものを選択し、「Add log」を実行することで、このログメッセージをベースにした新規ログ登録画面に遷移します。
前置きが長くなりましたが、この機能の裏側でどのようなことが行われているのかを基盤のアーキテクチャの視点と分析処理の視点で紹介します。
この仕組みの裏側の流れ(基盤アーキテクチャ)
まずはこの処理を実行する基盤側の流れです。基本的に全てサーバレスで実装です。
裏側の仕組みとしてはこちらの記事で紹介したような感じで以下図の流れになります。
- フロントエンドのVue.jsからファイルをアップロード
- バックエンドのGAEのAPI側でファイルを受け取りCloud Storage上に一時的にファイルをアップロード
- Storageにファイルアップロードが完了したことをトリガーとしてCloud Functionsを実行
- Cloud Functionsの関数処理の中でCloud Pub/SubにメッセージをPush
- Pub/SubのSubscriberとして登録されているCloud Runを実行
- Cloud Runでは、Cloud Storageにアップロードされているファイルを読み込み分析処理を実行
- 実行結果はFirestoreに保存
- フロントエンドからは7.で登録された結果を確認
Cloud Runを採用した理由
当初はCloud Functionsで実行しようと考えていました。しかし、後述のログのクラスタリング分析処理をGo言語で実装しており、かつGoの1.12のバージョン以上でないと動かないライブラリに依存した実装にしていたため、Cloud FunctionsのGoランタイムの言語制約の1.11のバージョンに合致せず、コンテナベースで自由に動かせるCloud Runで実行することにしました。
Cloud Functions -> Pub/Sub -> Cloud Runの流れを採用した理由
Cloud Functionsの場合は様々なトリガーに対応しているのですが、Cloud Runは実行トリガーが以下のみ対応となります。
- HTTPSリクエストによる実行
- Pub/Subからのメッセージによる実行
- Cloud Schedulerによるスケジュール実行
- Cloud Tasksを使った実行
- Webhookによる呼び出しによる実行Storageへのアップロード完了を持って実行できるようにすることが最もリアルタイムに処理を後続にすすめることができると考えたので、Storageへの書き込み完了をトリガーとしてキックできるCloud Functions側の処理を間に挟むことにしました。Cloud Functionsからは上記のいずれかの方法で実行すれば良いので直接HTTPSリクエストをCloud Run宛に投げても実行できるのですが、認証情報の引き渡しのことや、連携失敗時の処理再実行のことなどを考慮し、Pub/Subを間に挟み、サービス間連携させるようにしました。
Cloud Runの呼び出しを認証済みの内部のサービスからのみ許可する仕組みにしたかったため、Cloud FunctionsからPub/Subを経由して呼び出すようにしました。この仕組みの裏側の流れ(分析アルゴリズム)
次にCloud Runで実行させている分析処理の中身です。
具体的にはこちらのリポジトリで公開しているようなクラスタリング処理を行っています。
- ログに含まれる時刻表記や単純な数字のみの単語などの不要なキーワードを除去
- ログ文をGoのproseというライブラリを使って形態素解析を行い品詞タグ付け
- 品詞タグのCD(数字、序数など)、SYM(記号)などの不要なキーワード以外のものを重要キーワードとしてピックアップ
- ピックアップされた単語をwegoというライブラリに含まれるWord2Vecでベクトル化
- 1行のログに含まれる各単語のベクトルを加算平均し、ログ1行のベクトルを算出
- goClusteringというライブラリを使い、Ward法を用いて各行のログをクラスタリング分類
こんな流れになります。
最後のクラスタリング分類の方法としては、K-meansとかも考えたのですが、K-meansだと予め決まった数のクラスタ数に分割するというトップダウンのアプローチになってしまい、今回の要件のように件数がわからない、場合によってはそれぞれ全く別のクラスタに分類されるケースもあり得るような場合に分割しづらかったため、ボトムアップのアプローチであるWard法を採用しています。イメージとしては上記図のような感じで、ログの各行が近いもの同士が並んでおり、どのレベルの距離以上でクラスタを区切るかを指定すればある程度の類似度以上の近しいものを同一クラスタとみなして分割することができます。上の図の場合、0.008以上の距離で分割すると、2つのクラスタに、0.004~0.007ぐらいの距離で分割すると3クラスタに分割されることになります。
まとめ
このような処理もサーバレスで実装できてしまいます。ある程度制約がある中でどのような構成が良いかを検討して組めばサーバレスでも十分に要件を満たせる実装は可能かと思います。
特にCloud Runはコンテナベースで非常に柔軟に処理実行組み込めるので非常に便利です。
LogCrowもぜひお試しいただければと思います。
- 投稿日:2020-02-26T05:22:28+09:00
Go 1.14 で defer が速くなるのは 8 個まで
TL;DR
2020/02/25 にリリースされた Go 1.14 では defer のインライン展開というランタイム高速化の改善が盛り込まれましたが、これは関数内の defer が 8 個までの場合に限り有効です。 8 個より多くの defer やループ内の defer を含む関数では従来と同じコードが生成され、パフォーマンスの改善はありません。
解説
2020/02/25 に開催された Go 1.14 Release Party in Japan の YouTube Live を視聴していて、 @tenntenn さんの defer トークで次のようなスライドを目にしました。
そこで思わず次のような質問をしてしまったのですが、
後で調べてみたところ Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case という文書の Implementation のところにずばり答えが書いてありました。
- We need to restrict the number of defers in a function to the size of the deferBits bitmask. To minimize code size, we currently make deferBits to be 8 bits, and don’t do open-coded defers if there are more than 8 defers in a function. If there are more than 8 defers in a function, we revert to the standard defer chain implementation.
要するに、ひとつの関数の中でインラインに展開される defer の数は 8 個までということです。その数を越える defer がある関数は、従来と同じコードが生成されます。
実際に次のようなプログラムでコードを生成して試してみました。
gen.gopackage main import ( "fmt" "os" "strconv" ) func main() { n := 0 if len(os.Args) > 1 { n, _ = strconv.Atoi(os.Args[1]) } fmt.Print("package main\nimport \"os\"\nfunc main() {\n") for i := 0; i < n; i++ { fmt.Printf("if len(os.Args) == %d { defer func() {}() }\n", i) } fmt.Print("}\n") }このプログラムは、コマンドライン引数に渡した数字だけの条件分岐と defer を含む main() を生成します。
コマンドライン$ go run gen.go 4 | gofmt package main import "os" func main() { if len(os.Args) == 0 { defer func() {}() } if len(os.Args) == 1 { defer func() {}() } if len(os.Args) == 2 { defer func() {}() } if len(os.Args) == 3 { defer func() {}() } }defer が 8 個のとき、 SSA では
Or8
を含む条件分岐のブロックが 8 セット生成されます。bashgo run gen.go 8 | go tool compile -d ssa/build/dump=main /dev/stdin
main_01__build.dump(抜粋)b2: <- b3 b4 v24 = Phi <mem> v22 v127 v224 = Phi <uint8> v21 v4 v25 = Load <[]string> v23 v24 v26 = SliceLen <int> v25 v28 = Eq64 <bool> v26 v27 If v28 -> b6 b7 b6: <- b2 v33 = Copy <mem> v24 v34 = Store <mem> {func()} v32 v29 v33 v36 = Copy <uint8> v224 v37 = Or8 <uint8> v36 v35 v38 = Store <mem> {uint8} v5 v37 v34 Plain -> b5 b7: <- b2 Plain -> b5defer が 9 個のとき、SSA では次のような
runtime.deferprocStack
の呼び出しが 9 セット生成されます。この呼び出しのエラーチェックとリターンも行っているため、だいぶ長くなっています。bashgo run gen.go 9 | go tool compile -d ssa/build/dump=main /dev/stdin
main_01__build.dump(抜粋)b2: <- b5 b4 v24 = Phi <mem> v20 v1 v25 = Load <[]string> v23 v24 v26 = SliceLen <int> v25 v28 = Eq64 <bool> v26 v27 If v28 -> b8 b9 b8: <- b2 v30 = Copy <mem> v24 v31 = VarDef <mem> {.autotmp_11} v30 v32 = LocalAddr <*struct { siz uint32; started bool; heap bool; openDefer bool; sp uintptr; pc uintptr; fn uintptr; _panic uintptr; link uintptr; framepc uintptr; varp uintptr; fd uintptr; args [0]uint8 }> {.autotmp_11} v2 v31 v33 = OffPtr <*uint32> [0] v32 v34 = Store <mem> {uint32} v33 v14 v31 v35 = OffPtr <**func()> [24] v32 v36 = Store <mem> {*func()} v35 v29 v34 v37 = Store <mem> {uintptr} v18 v32 v36 v38 = StaticCall <mem> {runtime.deferprocStack} [8] v37 Defer v38 -> b10 b11 (likely) b9: <- b2 Plain -> b7 b10: <- b8 Plain -> b7 b11: <- b8 v39 = Copy <mem> v38 v40 = StaticCall <mem> {runtime.deferreturn} v39 Ret v40ということで、 defer が 8 個と 9 個とで異なる SSA が生成されていました。また擬似コードにおける
deferBits
にはuint8
が使われていることがわかりました。感想
今回のイベントのトークはどれも興味深く、またためになる内容でよかったです。 Go の SSA というものを初めて読みましたが、全くわからんものではないということがわかったのが大きな収穫でした。 これなら自分もなにか作れそうな気がしてきました。
新型コロナウイルスの影響でイベント開催中止の決定が相次いでいるのは残念ですが、多くのコミュニティでオンライン開催に切り替えられ、急造ながらも質問や感想をその場で伝えられる双方向なイベントが実現できているのは大変素晴らしいことだと思います。イベントを主催される方々の熱意と努力に敬意を表します。また、今後の技術の洗練やノウハウの蓄積によるオンラインイベントの進化にも期待したいと思います。