Upgrade to Pro — share decks privately, control downloads, hide ads and more …

高速で統一的な自動生成ツールをprotocプラグインとして実装した話

QualiArts
April 23, 2022

 高速で統一的な自動生成ツールをprotocプラグインとして実装した話

Go言語には総称型が実装されていないためコードを自動生成して賄うことが多いです。

ここで自動生成のソースをGo言語自体とした場合、よくある手法としてreflectパッケージによる生成が行われますが、ソースが多いと実効速度がネックとなってしまいます。 また、StructTagを活用したオプション設定は便利ですが、文字列による設定なのでタイプミスも発生します。

そこでProtocolBuffersをソースとする自動生成ツールをprotocプラグインとして実装することで、オプションを型安全にしつつ実行速度を大幅に向上させることに成功しました。 今回はこのprotocプラグインを紹介します。

QualiArts

April 23, 2022
Tweet

More Decks by QualiArts

Other Decks in Programming

Transcript

  1. 自己紹介 鈴木 光(すずき ひかる) • 2019年CyberAgent新卒入社 -> QualiArts • マッチングアプリ開発

    -> 既存ゲーム運用 -> 既存ゲーム運用 -> 社内システム 開発 -> 新規ゲーム開発 • 公式HPやエンジニアブログ、社内ツールの開発や運用も兼務 • 趣味は筋トレとゲーム、特技はソシャゲのデイリーをこなすこと • Twitter:https://twitter.com/hikyaru_suzuki @hikyaru_suzuki • GitHub:https://github.com/hikyaru-suzuki ◦ 今日のサンプルコード:https://github.com/hikyaru-suzuki/go-con-2022-spring-sample もし質問などがあれば、DMやメンションなどでお気軽にどうぞ!!! 2
  2. 開発の流れ(旧) 1. enum定義 -> $ make enum-gen(enumのGoコードやproto生成) 2. setting定義 ->

    $ make setting-gen(settingマスタのデータを生成) 3. entityを定義 -> $ make db-gen(entityからDB関連コード生成) 4. packerを定義 -> $ make packer-gen(マスタ配信関連コード生成) 5. apiを定義 -> $ make protoc(API関連コードを生成) 6. 生成物を元に実装していく 8
  3. 課題 • 手順や定義が色々あってわかりにくい • 実行が遅い ◦ API以外はGoによる定義からリフレクションを用いて生成していたため実 行に数分かかっていた ◦ 特にdb-genは生成物も多いため遅かった

    • structタグの限界 ◦ バリデーションやjson化した時の文字列、生成するDDLに含める情報な どをstructタグに突っ込んでいたが、structタグはただの文字列なので typoや記入漏れが発生していた 9
  4. protoc plugin • Protocol Buffersは拡張ができるよう設計されている • --go_outや--go-grpc_outもprotoc pluiginの一種 ◦ 実際こいつらを使うためにprotoc-gen-goやprotoc-gen-go-grpcなどのバイナリをインス

    トールしているはず • protocはオプションとして--hoge_outを渡すとprotoc-gen-hogeコマンド が呼び出されるようになっている ◦ このとき入力されたprotoファイルやオプションなどの情報がpluginの実装に渡される 14
  5. 開発の流れ(旧) 1. enum定義 -> $ make enum-gen(enumのGoコードやproto生成) 2. setting定義 ->

    $ make setting-gen(settingマスタのデータを生成) 3. entityを定義 -> $ make db-gen(entityからDB関連コード生成) 4. packerを定義 -> $ make packer-gen(マスタ配信関連コード生成) 5. apiを定義 -> $ make protoc(API関連コードを生成) 6. 生成物を元に実装していく 16
  6. 19

  7. Q. なんで速いの? A. I/O処理の極小化、並列処理、goimportsの最 適化を行ったから • 本当に最後だけファイルI/Oが発生するような設計に ◦ Goテンプレートはembedで埋め込み ◦

    ファイルフォーマットさえI/Oレスで • テンプレート処理、整形、ファイル出力などあらゆる処理を並列化 • import文を見直してgoimports向けに最適化 26
  8. goimports(1/4) こういうこと、してませんか? 28 {{ template "autogen_comment" }} package repository import

    ( "context" ) type {{ .GoName }}Repository interface { SelectAll(ctx context.Context) ([]*entity.{{ .GoName }}, error) {{ range $col := .KeyColumns }} SelectBy{{ $col.PascalName }}( ctx context.Context, {{ $col.CamelName }} {{ $col.Type }}, ) ([]*entity.{{ .GoName }}, error) {{ end }} } おすすめ Extend的なことが できる contextパッケージだけでいいの?
  9. goimports(2/4) • 生成自体はエラーにならない • gofmtもエラーにならない • goimportsもエラーにならない • Lintは流石にエラーとなる 29

    // DO NOT EDITコメント package repository import ( "context" ) type HogeRepository interface { SelectAll(ctx context.Context) ([]*entity.Hoge, error) SelectByHogeType( ctx context.Context, hogeType enum.HogeType, ) ([]*entity.Hoge, error) } 生成されるもの
  10. goimports(3/4) 30 // DO NOT EDITコメント package repository import (

    "context" “github.com/hikyaru-suzuki/hoge-server/pkg/enum” “github.com/hikyaru-suzuki/hoge-server/pkg/entity” ) type HogeRepository interface { SelectAll(ctx context.Context) ([]*entity.Hoge, error) SelectByHogeType( ctx context.Context, hogeType enum.HogeType, ) ([]*entity.Hoge, error) } • goimportsはパッケージが宣言されて いない場合、GOPATH配下などを自動 で探索してimportへ追加する ◦ なかったらエラーになる ◦ これがめっちゃ重い • 探索速度はGOPATH配下にあるパッ ケージ量に依存する ◦ https://cs.opensource.google/go/x/tools/+/m aster:internal/imports/fix.go;l=1386 ◦ https://cs.opensource.google/go/x/tools/+/m aster:internal/gopathwalk/walk.go;l=53 • ツールを作っていた際には最大数分か かるものもあった パッケージが存在 すると追記される
  11. // DO NOT EDITコメント package repository import ( "context" “github.com/hikyaru-suzuki/hoge-server/pkg/enum”

    “github.com/hikyaru-suzuki/hoge-server/pkg/entity” ) type HogeRepository interface { SelectAll(ctx context.Context) ([]*entity.Hoge, error) SelectByHogeType( ctx context.Context, hogeType enum.HogeType, ) ([]*entity.Hoge, error) } goimports(4/4) 31 • 解決策は利用される可能性がある パッケージを全てimportへ追加して おくこと • パッケージ探索はディレクトリス キャンが走るので重いが、未使用 パッケージの削除はAST解析だけでわ かるので速い 使われるであろう パッケージは 予め追加しておく
  12. 自動生成の冪等性(1/3) • 自動生成にありがちな課題 ◦ ディレクトリ全削除 -> 自動生成としているので、 そのディレクトリ配下に手動で作成した実装を差し 込めない ◦

    そもそも全削除 -> 全作成が重い ◦ ディレクトリ全削除をしないと、ゴミコードが残る • これを解決するため、「gen-file-remover」 というスクリプトを実装し、protoc plugin と連携するようにした ◦ 全作成はするが差分だけ削除されるようになる gen-file-removerは 100行にも満たないスクリプト 34
  13. 自動生成の冪等性(2/3) 1. protoc plugin側で自動生成物のファイルパスを 「/tmp/generated_files.txt」に対して書き込むよう実装する 2. protoc pluginによる自動生成が終了したら、 gen-file-removerコマンド(バイナリ)を呼び出す 3.

    gen-file-removerがプロジェクトの特定ディレクトリをスキャンし、 自動生成物(<名前>-gen.go)を探索する 4. 発見した自動生成ファイルが「/tmp/generated_files.txt」になければ それを削除する 35
  14. 自動生成の冪等性(3/3) ディレクトリスキャンの実装 • filepath.Walk ◦ 実は結構遅い ◦ ファイルツリーを同期的に探索 • filepath.Walkdir

    ◦ Go 1.16で追加 ◦ 不要な情報を取得しないため速い ◦ https://github.com/golang/go/issue s/42027 36
  15. 自動生成物の拡張 /** 自動生成ファイル */ type HogeRepository interface { FindAll() []*Hoge

    } /** 手動作成ファイル */ type HogeRepositoryExt interface { HogeRepository FindByName(name string) []*Hoge } 手動実装による拡張例(1) 自動生成物を埋め込み、 新しいメソッドを追加した 拡張実装をDIして使う Ext = Extension(拡張) 38
  16. 自動生成物の拡張 /** 自動生成ファイル */ type HogeRepository interface { FindAll() []*Hoge

    FindByName(name string) []*Hoge } type hogeRepository struct {} func (h *hogeRepository) FindAll() []*Hoge { /** すごい実装 */ } func (h *hogeRepository) FindByName(name string) []*Hoge { /** すごい実装 */ } func NewHogeRepository() HogeRepository { return &hogeRepository{} } /** 手動作成ファイル */ type hogeRepositoryExt struct { hogeRepository } func (h *hogeRepositoryExt) FindAll() []*Hoge { /** さらにすごい実装 */ } func NewHogeRepositoryExt() HogeRepository { return &hogeRepositoryExt{} } 手動実装による拡張例(2) Interfaceとしては 自動生成物の型だが、 特定のメソッドだけを オーバーライドした 実装をDIして使う 39
  17. 変数名を工夫 • 「名前」を定義するときはスネー クケースにすると良い ◦ 🤔:Name -> character_id, CharacterID ?

    ◦ 👍:SnakeName = character_id, GoName = CharacterID • 自動生成の際にパスカルケース、 キャメルケース、スネークケース など色々な形に変形する作業を 行っていると、元の形が何かわか らず変換ミスが多発するため • 元の形を統一しておけば前述の キャッシュが効きやすい 41
  18. PG* • https://github.com/lyft/protoc-gen-star • > !!! THIS PROJECT IS A

    WORK-IN-PROGRESS | THE API SHOULD BE CONSIDERED UNSTABLE !!! ◦ とはいえ使っている有名なプロジェクトもある ▪ https://github.com/envoyproxy/protoc-gen-validate • 学習コストが高そう 43
  19. まとめ • 全ての自動生成定義がprotoに集約し、コマンド1発で生成可能に ◦ APIスキーマ、DBスキーマ、関連コード etc… • protoc pluginの独自実装によって自動生成が数分から10数秒に ◦

    I/Oを極限まで減らすことがポイント ▪ この発想はprotoc pluginの実装を参考に ◦ 並列処理を積極的に使うのも大事 ◦ import文をgoimports向けに最適化 • もう自動生成待ちという言い訳でサボることはできません 😎 自動生成ライフの参考になれば嬉しいです!!! 45