詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

作業ログと履歴をシンプルに共有できる furoshiki ってツールを書いた

おはようございます。この記事ははてなエンジニアアドベントカレンダー2017の25日目の記事です。昨日は id:alpicola さんによる 社内で機械学習ハッカソンを開催しました でした。


サービスのデプロイをはじめとして、チーム内の開発者が共通して担当すべき業務というのはさまざまに存在し、基本的に定型化されているものですが、開発者が手元で実行するなど自動化までは行えていないような場合、以下のような点が問題になります。

  • 作業履歴が共有されない
  • 同様に作業中に意図しない不具合が生じた場合、エラーログが実行した環境にしか残らない

それぞれ、デプロイのタイミングを Mackerel や Slack に投稿して共有する、Gist にエラー時のログを貼るなど、チームに合わせた方法が存在していることと思います。また作業環境を同一にするため、チームにデプロイサーバを用意して作業はそこで行う、という方法も考えられますね。

はてなでもいろいろな方法でこれらへの対処をおこなっているわけですが、シンプルかつ統一された方法でこの共有を行いたいと思い、手元の実行ログを Git 経由で共有するコマンドとして furoshiki というものを作りました。薄いラッパーってことで風呂敷です。いちど作り直したので名前が furo2 になってます……。

ちょっとした調査やスクリプトの実行ログも、このコマンドを経由すれば特に設定の必要もなく共有できるところがポイントになってます。

初期設定 & コマンドの実行

インストール は Homebrew か pip で行えます(いつもは Go で書いてますが、proxychains 下で動かしにくいのと、困ったときにデバッグ&ハックがやりやすいようにと思って Python を使ってみました)。

% brew install --HEAD motemen/furoshiki2/furoshiki2
% pip3 install git+https://github.com/motemen/furoshiki2

さて、最初に一度だけ行うべき設定として、ログリポジトリの指定を行います。組織内で共有する、ログ用のリポジトリを一つ作ったら、環境変数 FURO_LOGS_REPOSITORY を push 可能な Git のリモートに設定してやります:

# .zshrc
FURO_LOGS_REPOSITORY=git@github.com:motemen/furo-logs-example.git

これで完了。あとは適当な Git リポジトリのワークツリー内で furo2 exec コマンドを実行すれば:

% furo2 exec COMMAND ...

実行したコマンドと内容がログ用のリポジトリにコミット & push されます。例えば上記設定で、motemen/test-repository 内で furo2 exec whoami を実行したコマンドのログが https://github.com/motemen/furo-logs-example/commit/db45e1992a787814564ba9d58b4df18e4f9691f3 になります。

ログの閲覧

ログの閲覧も furo2 history コマンドでターミナル上から行えます。ここでは分かりませんが色もつく。

% furo2 history # 履歴一覧を表示
6d8eeba [2017-12-25 12:34:07 +0900] (motemen) git status -sb
db45e19 [2017-12-25 12:05:40 +0900] (motemen) whoami
% furo2 history show # 最新の実行ログを表示
command:     ["git", "status", "-sb"]
user:        motemen
repoPath:    github.com/motemen/test-repository
projectPath: github.com/motemen/test-repository
gitRevision: b7e19f710a1727dd49b0ae9673267fa5f3bc5ced
furoVersion: 2.0.0-alpha
exitCode:    0
---
## master...origin/master [behind 2]
 M .git-pr-release

ログを残したり表示したりするのに Git を使ってるので、furo2 history show HEAD~1 などとすれば一つ前のログも見られるような感じになってます。

たぶんこんなふうにしたら手元で動きを確認できると思います。

% git clone https://github.com/motemen/test-repository.git
% cd test-repository
% # push 権限がないので exec は失敗するけど、履歴の閲覧はできる
% export FURO_LOGS_REPOSITORY=https://github.com/motemen/furo-logs-example.git
% # 手元にログがないので最初に pull します
% furo2 history pull
% furo2 history show

仕組み

大雑把な流れはこんな感じ:

  1. どこかのリポジトリ(たとえば https://github.com/motemen/test-repository)のワーキングツリー内で furo2 exec COMMAND [ARGS...] する
  2. と、~/.furo2/logs/github.com/motemen/test-repository/YYYYmmdd_HHMMSS にログが書かれる
  3. これがログリポジトリの github.com/motemen/test-repository ブランチに push される

furoshiki では作業ディレクトリが何らかの Git リポジトリのワークツリーになっていることを期待しますが、これをログリポジトリのブランチ名として利用します。

たとえば https://github.com/motemen/test-repository.git リポジトリにおける作業ログであれば、ログリポジトリには対応するブランチとして github.com/motemen/test-repository ブランチが作られ、そのコミットログが作業ログにそのまま対応する、というような形です。furo2 history は、カスタマイズされた git log を呼び出しているだけ。

ログリポジトリのクローンは ~/.furo2/logs/<repo_path> に作られ、ここで作業ログのコミットや push、pull が行われています。なので履歴が何かしらおかしくなったらここを削除すればオッケー(furo2 history fix でも行えます)。

作業ログの記録自体は、script コマンドを使ってファイルに書き出しているだけです。それに実行者やコマンドなどのメタ情報を加えたいちファイルをコミットするような感じ。

以上

作業ログ共有のシンプルなインタフェースとしての furoshiki の紹介でした。

とくに多くのリポジトリを触る必要があるチームにあっては、(プロセスを整えていく必要があるのは当然として、)先人がどのような作業を行ってきたかを参照できるのは大変重宝します。

f:id:motemen:20171225130715p:plain

GitHub の webhook を使えば Slack 連携も容易で、おすすめです。


以上、はてなエンジニアアドベントカレンダーでした。これまでのエントリの一覧は Qiita でも確認できます ので、冬休みのおともにお楽しみください。それではよいお年を!

ISUCON7本戦で3位でした(ソン・モテメン・マサヨシ)

いろいろあって、本戦からだいぶ時間が経ってしまいました。チーム全体としての流れはSongmuさんが書いてくれているので、自分の視点でどうだったかな〜というのを時系列を無視して思い出しつつ書きます。

チームは、会社(はてな)の同僚で組んだ Songmu, masayoshi, motemen の3人。全員の名前からとって「ソン(ムー)・モテメン・マサヨシ」です。おしゃれ。リポジトリはここ

本戦の問題は Cookie Clicker をモデルにしたもの。競技の開始まですこし時間の余裕があったので、その間にオリジナルをプレイして、アプリケーションの仕様を学習した。話題になったときに遊んでなかったので、このとき初めてやったんだけど、たしかに中毒性がありそう……。

Cookie Clicker のほうは放置しつつ方針の相談。まあルームごとにサーバを割り当てて、オンメモリでやってく感じだよね、というのですぐに落ち着いた。ソースコードを読んだところ、ゲームのコア部分がえらく複雑そうで、ここはあんまり触りたくないなあという気持ちになってしまった(これがよくなかった)。

ともあれ MySQL の仕事をなくしていこう、ということで、効果はとくに期待せず、まずは肩慣らしにアイテム情報をハードコード。たしかその間に Songmu さんがルーム名のハッシュ値から振り分け先のサーバを決定するロジックを書いてたと思う。このとき Songmu さんがなんか単語をド忘れしてたらしく、「サーバをアレにする」「アレですよね」みたいな会話をしたんだけど、ちゃんと意図が伝わってたので面白かった。

想定どおり振り分けでスコアはあがるんだけど、ベンチマーカの振り分けが偏ってしまうとスコアに大きく影響があるので、競技中はもっぱら1台構成でスコア測定を安定させ、最後に4台構成でバーンとスコアを上げるという作戦をとった。あとサーバの振り分けも最終的にはラウンドロビンになった。

さて MySQL やめてオンメモリにするか、と考えていたころに masayoshi がアプリケーションがボトルネックですねー、と教えてくれたので、ついに出番だ! ということで go-torch によるプロファイリング を仕込んだ。予選のときも pprof と go-torch だけは使い方を覚えておこう、と準備していたものの出番がなかったのだ。結局アプリケーションのボトルは解消できず、MySQL は最後まで残った。

プロファイリングしてみると calcStatus と、その中の math/big 周辺の処理の重いことが見てとれたんだけど、ここでけっこう停滞してしまった。calcStatus をなんとかしないことにはスコアが上がらないことはわかっているんだけど、時間のかかる取り組みになることも見えていて、躊躇する気持ちが勝ってしまっていたのだと思う。例えばキャッシュを入れたとして、ここでロジックを壊してしまったときに検証するのが大変そうだなーという思いが先にきてしまうという……。

結局有効だったのは、クライアントからのアクセスごとにいちいち getStatus するのじゃなく、ルームにつきひとつだけ getStatus 計算用の goroutine を用意して、それを各クライアントに配るような形にしたことだった。これで math/big 周りの呼び出し回数を少し減らすことができ、スコア増につながった。最初チャンネルの close など雑に書いていたらリークしまくってたけど、おおむねすんなり実装できたのでよかった。

ちなみにこの方法は methane さんにコメントをもらっていて(Songmu さんの記事に対して)、

なるほどたしかに。もうちょっと周辺のコードを見ておけばよかった……。そういうコード理解の低さが今回の伸び悩みの原因だったなと思う。

閑話休題。あらためてプロファイリングすると、今度は GC の占める割合も大きくなってきていた。この時点で残り時間も心もとなかったので、パラメータ調整だけで容易にスコアの伸びが狙える GOGC 調整に手をつけはじめた。これは最初 GC 切ってみたらめちゃくちゃスコアが下がって(だったか、リソースを異様に食うようになったのだったかで)大笑いしてしまった……。GC 頻度を減らすことで、これでもそれなりにスコアが伸ばせた。

再起動後、ベンチガチャをしていたらいい点数が出たので各自ブラウザを閉じてフィニッシュ。個人的には最高順位の3位だったけど、賞金獲得はならず。終わってみるとやはり calcStatus がキモで、そこに真正面から取り組む気持ちを持てたかどうかが勝敗の分かれ道だったなー、と思った。しかしキャッシュ入れて fail したチームも多かったみたいで、難しい選択には違いなかったのだと思う。

今回の ISUCON は予選からすごく楽しませてもらったし、そのぶん運営は大変だったと思います。こんなイベントが年一で体験できるのは運営の皆さんのおかげで、感謝しかない。お疲れさまでした。そして来年も100万円ほしいでしゅ!

チャンネルを使って、決まった数のリソースをgoroutine間で共有するパターン

生成が重いリソース(や重い処理の実行権)を goroutine 間で共有し使いまわすようなパターンです。よく知られていて名前がついていそうだけど、ぐぐっても分からなかったので書いておく。

コネクションプールに近い感じで、最初にリソースを生成したあと、それらを大事に取り回します。リソースが空いてなかったら goroutine は待つことにする。sync.Pool は「プールにあったら使うけど、なかったら新しく作る」くらいの感じなので、ちょっとスタンスが違う。

チャンネルによる実装は簡単で、以下のエントリにも書いたセマフォを応用すればよい。

ざっくりと書いてみた例がこちら: https://play.golang.org/p/QWAXsA_89Y

チャンネルによるセマフォの実装は、「バッファありチャンネルに何か(struct{})を挿入できた goroutine が実行の権利を持つ」というものでしたが、今回は「バッファありチャンネルからリソースを取得できた goroutine がリソースの権利を持つ」というふうになっています。

最初にリソースのプール(チャンネル)を作り:

pool := make(chan *worker, 5)
for i := 0; i < 5; i++ {
    pool <- &worker{i}
}

各 gorountine では、リソースをプールから取得。リソースを使い終わったらプールに戻します。プールに戻すまではリソースを占有できていることが保証されています。

    case w := <-pool:
        w.work()
        pool <- w
    }

リソースを取得しようとするとき、空いているものがなければブロックするので、セマフォの上位版ということになりますね。