Protobufの破壊的変更を検知するProto Breaking Change Detectorを使ってみた

概要

Protobufの破壊的変更を検知するProto Breaking Change Detectorというツールを使ってみたら、良さそうだった。 github.com

経緯

uber/protool を使うと破壊的変更を検知できるようだったが、リポジトリがarchiveされている。

prototoolの代替として示されているBufの破壊的変更検出 は、buf.yamlディレクトリに置くというBufのルールに従わなければ利用できない。(たぶん。やり方を知っている人がいたら教えて欲しい。)

方法

動かしてみる

mainブランチと現在のワークツリーで、破壊的変更が発生していないかチェックする。

Proto Breaking Change Detector をインストールする。

pip install git+https://github.com/googleapis/proto-breaking-change-detector.git

2つのディレクトリ間でのprotobufの破壊的変更を検知できる。

# --original_api_definition_dirs: 比較元の--proto-pathに指定するディレクトリをカンマ区切りで
# --original_proto_files: 比較元のbeaking changeを検出したいファイルをカンマ区切りで
# --update_api_definition_dirs: 比較先の--proto-pathに指定するディレクトリをカンマ区切りで
# --update_proto_files: 比較先のbeaking changeを検出したいファイルをカンマ区切りで

PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python proto-breaking-change-detector \
    --original_api_definition_dirs=${ORIGINAL_PROTO_PATH} \
    --update_api_definition_dirs=${UPDATE_PROTO_PATH} \
    --original_proto_files=${ORIGINAL_FILE} \
    --update_proto_files=${UPDATE_FILE} \
    --human_readable_message

実行すると、detected_breaking_changes.jsonというファイルが作られて、そこに非破壊的変更("change_type": "MINOR")や、破壊的変更("change_type": "MAJOR")を含む変更の情報が入る。

mainブランチからの破壊的変更を検知してみる

mainブランチと今いるブランチの間で、破壊的変更が無いか比較してみるために、サンプルコードを作った。 github.com

cloneしてブランチを切る。

git clone git@github.com:shiba6v/pbcd_trial.git
cd pbcd_trial
git checkout -b feature/hoge

元のmainブランチのproto/fuga/fuga.proto は、次のような定義にした。

syntax = "proto3";

import "hoge.proto";

message Fuga {
    int64 id = 1;
    Hoge hoge = 2;
}

mainブランチをcloneしてきたままの状態で、proto-breaking-change-detectorを実行するスクリプトを実行する。

sh detect.sh

もちろんoriginのmainブランチから変更はないので、detected_breaking_changes.jsonの中身は空になっている。

[]

次に、破壊的変更と非破壊的変更を加えてみる。 具体的にはHoge hoge = 2;string hoge = 2;に変更して、int64 count = 3;を追加する。

syntax = "proto3";

import "hoge.proto";

message Fuga {
    int64 id = 1;
    string hoge = 2;
    int64 count = 3;
}

再度sh detect.shすると、

$ sh detect.sh
pbcd_trial/protobuf/src: warning: directory does not exist.
pbcd_trial/protobuf/src: warning: directory does not exist.
fuga.proto:3:1: warning: Import hoge.proto is unused.
fuga.proto L7: The type of an existing field `hoge` is changed from `message` to `string` in message `..Fuga`.

mainブランチから見ると破壊的変更("change_type": "MAJOR")が起こっていることが確認できる。

[
  {
    "category": "FIELD_ADDITION",
    "location": {
      "proto_file_name": "fuga.proto",
      "source_code_line": 8
    },
    "change_type": "MINOR",
    "extra_info": null,
    "subject": "count",
    "oldsubject": "",
    "context": "..Fuga",
    "type": "",
    "oldtype": ""
  },
  {
    "category": "FIELD_TYPE_CHANGE",
    "location": {
      "proto_file_name": "fuga.proto",
      "source_code_line": 7
    },
    "change_type": "MAJOR",
    "extra_info": [
      "message Fuga {",
      "hoge"
    ],
    "subject": "hoge",
    "oldsubject": "",
    "context": "..Fuga",
    "type": "string",
    "oldtype": "message"
  }
]

フィールドの追加は、非破壊的変更("change_type": "MINOR")と判定されており、これは正しい。

今後知りたいこと

  • ファイルが追加されるときや、messageがファイルを移動したときはどう検知されるか?
  • protobuf/src: warning: directory does not exist.のWarningを抑えたい。
    • protobuf/srcがデフォルトで--proto-pathに追加されている。

結論

github.com

を使おう。

ISUCON12予選に参加した

ISUCON12予選に参加しました。 最終ベンチ結果は6815点で、最高ベンチ結果は10625点、failの可能性は高いです。 もし結果発表でfailしていたらブログを書く気に多分ならないと思う(あと記憶力が弱い)ので、今のうちに当日やったことを中心に振り返っていきます。

追記 (2022/7/25): failしていました :cry:

追記 (2022/11/26): プロファイラ的なツールを公開しました。 github.com

自分がやったこと

ファイルロックがボトルネックになっているっぽいことを見つけた

alpのログを見ると、ランキングやプレイヤー詳細のAPIボトルネックになっていることが分かりました。

alpだとエンドポイント単位でしか見られないんですが、pprofはポートを立ててWeb UIから見なければ詳しくは見られない上にちょっと見づらい・・・ということで、このときから作っていたツール1 を使って、関数ごとにかかった時間を計測しました。

alpで一番重かったランキングAPI (competitionRankingHandler) を分割して見たところ、一番時間がかかっているのはSQL文ではなく、ファイルロックをしている部分(competitionRankingHandler 4)でした。

ファイルロックをする関数(flockByTenantID)も、時間がかかっていることが分かります。

ファイルロックを消した (failの戦犯)

お題のアプリケーションはマルチテナントSaaSで、管理側はMySQLを使っており、テナントに入っている側はそれぞれ別のSQLiteを使っていました。

ロックをかけている部分のコードには、

// player_scoreを読んでいるときに更新が走ると不整合が起こるのでロックを取得する

とコメントがあるのですが、DBの読み込み時にかかっている排他ロックが、読み込みが非常に遅い原因になっていました。

読み込み時には共有ロックをかけると読み込みが同時に行えて性能が上がるだろうということで、ファイルロックをやめてSQLiteのトランザクションを使おうとしました。

トランザクションに書き換えると、error retrievePlayer: error Select player: id=395f19764, database is locked というエラーが出てしまうものの、スコアは5000点から10000点近くまで上昇しました。

追記 (2022/7/25): 正しくトランザクションが貼れていてもこのエラーは出るので無視してよかったらしい。そんな・・・

いろいろ試行錯誤した結果、このエラーが出たり出なかったりしてスコアが0になったり10000になったりしました。これが失格の原因になるのは困るということで、⚠️ロックを一切かけない⚠️ことにしました。このあたりで既に完全に沼に入っているので、冷静な判断ができていればrevertして5000点から再開していたと思います。失格になったらこの変更のせいなのでごめんなさい(泣)

リーダーボードで上位だった方のコミット履歴を見ると、SQLiteからMySQLにテーブル単位で移していました。 SQLiteのロックを外すことを考えるよりも先にその戦略を取れば、普段使っているMySQLトランザクションが使えるのでやりやすかったと思います。 SQLiteからMySQLへのリプレースは弊チームではリスクが高いと判断して取り組まなかったことを考えると、部分的に置き換えるというのはやはりISUCONにおいて重要戦略であると思いました。

追記 (2022/7/24): ファイルロックをsync.RWLockとsync.RLockにする案は思いついたんですが、tenant IDごとにロックを分けるためにmapを作る必要があって、そのmapを管理するためにロックが必要で・・・となって面倒くさそうでやめてしまいました。tenant IDは固定でmapはreadだけなので、本当はそんなに難しくなかっただろうなと思います・・・こっちでやればよかった:cry:

ファイルロックをRLockにもしてみたんですが、failしたのでロックが取れるまで待つ実装は自前でやらなければいけないのかなと勘違いしました。It will wait until it is able to obtain the shared file lock. とあるので、その方針でも自信をもってやればできただろうと思います。

https://pkg.go.dev/github.com/gofrs/flock#Flock.RLock

チームメンバーがやったこと

IDジェネレータをUUIDにした (AokabiC)

スロークエリログを見ると、id_generatorという謎テーブルへのINSERTが一番遅いことが分かりました。被らない連番IDを取るために、MySQLにわざわざレコードを追加して採番していたので、これをUUIDに変更してくれました。

  REPLACE INTO id_generator (stub) VALUES ('S')

AokabiC氏の目の付け所はいつも洗練されているのですごいなあと思います。

App, DBの2台構成にした (zatton)

ISUCON12予選では3台のサーバーが与えられますが、AppとDBを分離するのは、 - htopをするとAppとDBのどちらがボトルネックになっているかわかる - 負荷が軽くなる の2点のメリットがあるため、早い段階で分離しました。

弊チームは、/etcなどの設定ファイルは権限周りが面倒なのでGit管理せずにzatton氏がVimでゴリゴリ直接書いていく方針で、zatton氏がサクッとやってくれました。

visit_historyへの書き込みを減らす (AokabiC)

visit_historyはランキング取得のタイミングで履歴を書き込むのですが、このテーブルは、最新のものしか取得されていません。そこで、visit_history_summerizeというテーブルを作って、最新のものだけを書き込むようにしました。インデックスも貼りました。

自分がやろうとしたこと

player_score を剥がす

ファイルロックがボトルネックから外れた後は、player_scoreテーブルを使う関数がボトルネックになってきたのでこれをRedisに移そうとしました。しかし、整合性チェックでfailしてデバッグする時間もなく断念。RedisではなくMySQLにInsertしている方が整合性に関して安心できるという点で良い方針だったのかもしれません。

チームメンバーがやろうとしたこと

3台構成にする (zatton)

3台構成にする際は、Appを2台(nginx+AppとApp)にするかDBを2台(リードレプリカや垂直分割)にするか、その他の構成にするかが悩みどころです。 今回は、2台構成にしたところAppのCPU負荷が100%に張り付いていたので、Appを2台にすることを考えました。しかし、Appサーバーのファイルシステム上にはSQLiteがあり、Appを2台にするならNFSなどで同期しなければいけないことが分かります。ということで、これは一旦放置することにしました。

player_scoreへの書き込みを減らす (AokabiC)

player_scoreもvisit_historyと同様に書き込みが減らせそうということで、取り組んでいましたが時間が足りず断念。

感想

既存のISUCON攻略法を封じてくるような問題だったので、面白かったです。 具体的には、DBがSQLiteMySQLに分離されていたので、MySQLのスロークエリログには出ないボトルネックがありました。

余談

当日は、SQLiteMySQLがトレンド入りして盛り上がりました。

ISUCON界隈の外からはMySQLトレンド入りの原因が謎だったっぽい。

ここは違うよ、というのがあれば、TwitterのDMで教えてもらえると助かります。


  1. 本当はこういう感じのラインプロファイラがGoにも欲しいんですけどね・・・ 自作ツールは需要があったら公開したいですが、品質が低すぎるので準備中です。

セマンティックセグメンテーションでやっていく年賀状作成 2022

この記事は、CAMPHOR- Advent Calendar 2021 の3日目の記事です。

概要

年賀状やメッセージカード作成に役立つWebツール、SSフォトカードメーカー を作りました。 このツールを使うと、写真からこういった画像を作成できます[^1]。 f:id:shiba6v:20211202205444j:plain

全面写真の年賀状と「上に文字が乗らない問題」

年末といえば、年賀状作成ですね。最近はSNSで送る人もいるみたいです。 テンプレートに小さい写真を入れていくのではなく、全面が写真の年賀状もおしゃれですよね。

続きを読む

論文のPDFから図を自動で抽出する

この記事は、CAMPHOR- Advent Calendar 2020 の2日目の記事です。

論文のPDFから図を自動で抽出する

こんにちは、シバニャンです。最近やっていることと言えば、修士論文の執筆です。修士論文を書くためには、普段から大量の論文を読んでおく必要があります。僕の研究分野はコンピュータビジョンという画像を中心に扱う分野で、年数回、国際会議があるたびに1000本程の論文が発表されます。その中から自分の研究に関係があったり、直接関係はなくても役立ちそうなアイディアがある論文を見つける必要があります。

論文ギャラリーを作りたい

f:id:shiba6v:20201201102848p:plain

続きを読む

LaTeXで下線の引き方

近況報告

こんにちは. 最近は,DC1の申請書を書いていました. 業績が全然無いので,運良く通ったらいいな〜くらいの気持ちで書き始めたんですが, 先生や先輩から文章の書き方について学ぶ良い機会になりました.

(今まで自分が書いてきた文章は,日本語ではなくて別の何かだったという気持ち・・・)

科研費LaTeX最高!

DC1はWordを使って書くことが推奨されていますが,Wordはやっぱり使いたくないので,代替として用意されている科研費LaTeXを使いました. http://osksn2.hep.sci.osaka-u.ac.jp/~taku/kakenhiLaTeX/

続きを読む

配列の形状(Shape)のコメントを付けるJupyter Notebook拡張を作った

加筆(2019/12/12)

注意 JupyterNotebook拡張は使いづらかったので,IPython拡張としてリニューアルしました. こちらを使ってください.

github.com

このページを読む代わりに,下の記事を読むことをお勧めします. (検索順位はこのページの方が高いので微妙な気持ちです・・・) shiba6v.hatenablog.com

続きを読む

日本語のJupyter NotebookをPDFとしてダウンロードする裏技

Jupyter NotebookをPDF化する際にLaTeXを入れて頑張る方法がありますが,もっと楽をする方法があります.

ブラウザの機能を使って印刷すれば良いんです.

f:id:shiba6v:20190627235218p:plain
notebook

続きを読む